140 lines
6.7 KiB
Go
140 lines
6.7 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
rvboxv1 "github.com/rvbox/rvbox/gen/go/rvbox/v1"
|
|
"github.com/rvbox/rvbox/internal/agentproto"
|
|
"github.com/rvbox/rvbox/internal/client/spool"
|
|
"github.com/rvbox/rvbox/internal/domain"
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
)
|
|
|
|
func TestRunOnceReconcilesReplaysAndAdvertisesCapacity_HP_RUNTIME_01(t *testing.T) {
|
|
ctx := context.Background()
|
|
store, err := spool.Open(ctx, spool.Options{DataDir: filepath.Join(t.TempDir(), "spool"), BusyTimeout: time.Second})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = store.Close() })
|
|
issue, err := domain.ParseUUIDv7("019c46f1-1d02-7000-8000-0000000000b1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
hash := [32]byte{1}
|
|
if _, err := store.AcceptCommand(ctx, spool.Command{IssueUUID: issue, ImmutableSHA256: hash, Revision: 1, Phase: uint32(rvboxv1.CommandLifecycle_COMMAND_ACCEPTED), ExecutionSpec: []byte("spec")}, time.Now().UTC()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
eventPayload, err := proto.Marshal(&rvboxv1.CommandEvent{Payload: &rvboxv1.CommandEvent_Lifecycle{Lifecycle: &rvboxv1.LifecycleChange{Lifecycle: rvboxv1.CommandLifecycle_COMMAND_RUNNING, CommandRevision: 1}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.AppendEvent(ctx, issue, spool.EventInput{Kind: 4, Compression: 1, RawBytes: uint64(len(eventPayload)), Payload: eventPayload, CreatedAt: time.Now().UTC()}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.AssignSendWindow(ctx, issue, 1, 1<<20); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
welcome, _ := proto.Marshal(&rvboxv1.AgentEnvelope{SessionId: "session", SessionGeneration: 1, Payload: &rvboxv1.AgentEnvelope_ServerWelcome{ServerWelcome: &rvboxv1.ServerWelcome{SelectedProtocol: &rvboxv1.ProtocolVersion{Major: 1, Minor: 0}, ServerTime: timestamppb.Now()}}})
|
|
reconcileRequest, _ := proto.Marshal(&rvboxv1.AgentEnvelope{SessionId: "session", SessionGeneration: 1, Payload: &rvboxv1.AgentEnvelope_ReconcileRequest{ReconcileRequest: &rvboxv1.ReconcileRequest{}}})
|
|
reconcileResult, _ := proto.Marshal(&rvboxv1.AgentEnvelope{SessionId: "session", SessionGeneration: 1, Payload: &rvboxv1.AgentEnvelope_ReconcileResult{ReconcileResult: &rvboxv1.ReconcileResult{}}})
|
|
transport := &runnerTransport{reads: [][]byte{welcome, reconcileRequest, reconcileResult}, terminal: errors.New("transport closed")}
|
|
hello := &rvboxv1.ClientHello{ClientId: "runner-client", SupportedProtocol: &rvboxv1.ProtocolRange{Major: 1, MinMinor: 0, MaxMinor: 0}, DaemonVersion: "test", Platform: rvboxv1.Platform_PLATFORM_WINDOWS, Architecture: "amd64", DaemonCwd: `C:\`, SupportedShells: []rvboxv1.ShellType{rvboxv1.ShellType_SHELL_POWERSHELL}, ClientInstanceId: store.ClientInstanceID().String(), MaxRunningCommands: 1, MaxQueuedCommands: 1, SentAt: timestamppb.Now()}
|
|
err = RunOnce(ctx, RunnerOptions{Store: store, Dial: func(context.Context) (Transport, error) { return transport, nil }, Hello: hello, Limits: agentproto.DefaultLimits(), Backoff: BackoffOptions{Initial: time.Millisecond, Maximum: time.Millisecond, StableReset: time.Second}, Jitter: func(value time.Duration) time.Duration { return 0 }, Now: func() time.Time { return time.Now().UTC() }})
|
|
if !errors.Is(err, transport.terminal) {
|
|
t.Fatalf("RunOnce error = %v, want transport close", err)
|
|
}
|
|
transport.mu.Lock()
|
|
writes := append([][]byte(nil), transport.writes...)
|
|
transport.mu.Unlock()
|
|
if len(writes) != 4 {
|
|
t.Fatalf("wire write count = %d, want hello/snapshot/event/capacity", len(writes))
|
|
}
|
|
last, err := agentproto.DecodeEnvelope(writes[len(writes)-1], agentproto.DefaultLimits(), rvboxv1.Platform_PLATFORM_WINDOWS)
|
|
if err != nil || last.GetClientCapacity() == nil || last.GetClientCapacity().GetMaxRunningCommands() != 1 {
|
|
t.Fatalf("capacity advertisement = %#v, %v", last, err)
|
|
}
|
|
eventEnvelope, err := agentproto.DecodeEnvelope(writes[2], agentproto.DefaultLimits(), rvboxv1.Platform_PLATFORM_WINDOWS)
|
|
if err != nil || eventEnvelope.GetCommandEvent() == nil || eventEnvelope.GetCommandEvent().GetEventSeq() != 1 || eventEnvelope.GetCommandEvent().GetIssueUuid() != issue.String() {
|
|
t.Fatalf("replayed event = %#v, %v", eventEnvelope, err)
|
|
}
|
|
}
|
|
|
|
func TestRunnerOptionsRejectMissingJitter_HP_RUNTIME_02(t *testing.T) {
|
|
if err := (RunnerOptions{}).validate(); err == nil {
|
|
t.Fatal("empty runner options unexpectedly validated")
|
|
}
|
|
}
|
|
|
|
func TestFlushEventsAssignsAndSendsOnlyUnacknowledgedRows_HP_RUNTIME_03(t *testing.T) {
|
|
ctx := context.Background()
|
|
store, err := spool.Open(ctx, spool.Options{DataDir: filepath.Join(t.TempDir(), "spool"), BusyTimeout: time.Second})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer store.Close()
|
|
issue, err := domain.ParseUUIDv7("019c46f1-1d02-7000-8000-0000000000b2")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
hash := [32]byte{2}
|
|
if _, err := store.AcceptCommand(ctx, spool.Command{IssueUUID: issue, ImmutableSHA256: hash, Revision: 1, Phase: uint32(rvboxv1.CommandLifecycle_COMMAND_ACCEPTED)}, time.Now().UTC()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
payload, err := proto.Marshal(&rvboxv1.CommandEvent{Payload: &rvboxv1.CommandEvent_Lifecycle{Lifecycle: &rvboxv1.LifecycleChange{Lifecycle: rvboxv1.CommandLifecycle_COMMAND_RUNNING, CommandRevision: 1}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.AppendEvent(ctx, issue, spool.EventInput{Kind: 4, Compression: 1, RawBytes: uint64(len(payload)), Payload: payload, CreatedAt: time.Now().UTC()}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
transport := &runnerTransport{}
|
|
sent := map[domain.UUID]uint64{}
|
|
session := Session{ID: "session", Generation: 1}
|
|
if err := flushEvents(ctx, transport, store, session, issue, agentproto.DefaultLimits(), sent); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if sent[issue] != 1 || len(transport.writes) != 1 {
|
|
t.Fatalf("sent cursor/writes = %d/%d", sent[issue], len(transport.writes))
|
|
}
|
|
if err := flushEvents(ctx, transport, store, session, issue, agentproto.DefaultLimits(), sent); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(transport.writes) != 1 {
|
|
t.Fatalf("already-sent event was duplicated: %d writes", len(transport.writes))
|
|
}
|
|
}
|
|
|
|
type runnerTransport struct {
|
|
mu sync.Mutex
|
|
reads [][]byte
|
|
writes [][]byte
|
|
terminal error
|
|
}
|
|
|
|
func (transport *runnerTransport) Write(_ context.Context, payload []byte) error {
|
|
transport.mu.Lock()
|
|
defer transport.mu.Unlock()
|
|
transport.writes = append(transport.writes, append([]byte(nil), payload...))
|
|
return nil
|
|
}
|
|
|
|
func (transport *runnerTransport) Read(context.Context) ([]byte, error) {
|
|
transport.mu.Lock()
|
|
defer transport.mu.Unlock()
|
|
if len(transport.reads) == 0 {
|
|
return nil, transport.terminal
|
|
}
|
|
payload := transport.reads[0]
|
|
transport.reads = transport.reads[1:]
|
|
return append([]byte(nil), payload...), nil
|
|
}
|
|
|
|
func (transport *runnerTransport) Close() error { return nil }
|