독립 호스트에서 안전한 작업 실행과 복구를 제공하기 위해 런타임 설정, 정책, 상태 저장소, 워크스페이스 격리 및 AgentTask 오케스트레이션을 확장한다.
171 lines
5.3 KiB
Go
171 lines
5.3 KiB
Go
package agenttask
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
func TestStateMachineLegalAndIllegalTransitions(t *testing.T) {
|
|
record := WorkRecord{State: WorkStateObserved}
|
|
for _, next := range []WorkState{
|
|
WorkStateReady, WorkStatePreparing, WorkStateDispatching,
|
|
WorkStateSubmitted, WorkStateReviewing, WorkStatePendingIntegration,
|
|
WorkStateIntegrating, WorkStateCompleted,
|
|
} {
|
|
if err := transitionWork(&record, next); err != nil {
|
|
t.Fatalf("transition to %s: %v", next, err)
|
|
}
|
|
}
|
|
if err := transitionWork(&record, WorkStateReady); err == nil {
|
|
t.Fatal("completed -> ready unexpectedly allowed")
|
|
}
|
|
}
|
|
|
|
func TestStateMachineDuplicateCommandIdempotency(t *testing.T) {
|
|
harness := newHarness(t, map[ProjectID]ProjectWorkflowSnapshot{}, 1)
|
|
request := StartRequest{
|
|
CommandID: "command", ProjectID: "project", WorkspaceID: "workspace",
|
|
MilestoneID: "milestone", WorkflowRevision: "workflow-r1",
|
|
ConfigRevision: "config-r1", GrantRevision: "grant-r1",
|
|
}
|
|
if err := harness.manager.StartProject(context.Background(), request); err != nil {
|
|
t.Fatalf("first StartProject: %v", err)
|
|
}
|
|
if err := harness.manager.StartProject(context.Background(), request); err != nil {
|
|
t.Fatalf("idempotent StartProject: %v", err)
|
|
}
|
|
request.ConfigRevision = "config-r2"
|
|
if err := harness.manager.StartProject(context.Background(), request); err == nil {
|
|
t.Fatal("same command with different immutable input accepted")
|
|
}
|
|
}
|
|
|
|
func TestStateMachineCorruptIdentityBlocker(t *testing.T) {
|
|
unit := testUnit("same", WriteSetUnknown)
|
|
snapshot := testSnapshot("project", "workspace", unit, unit)
|
|
harness := newHarness(t, map[ProjectID]ProjectWorkflowSnapshot{"project": snapshot}, 1)
|
|
harness.start("project", "workspace", nil)
|
|
if err := harness.manager.Reconcile(context.Background()); err != nil {
|
|
t.Fatalf("Reconcile: %v", err)
|
|
}
|
|
project := harness.store.snapshot().Projects["project"]
|
|
if project.Status != ProjectStatusBlocked || project.Blocker == nil ||
|
|
project.Blocker.Code != BlockerInvalidIdentity {
|
|
t.Fatalf("project = %#v, want corrupt identity blocker", project)
|
|
}
|
|
if harness.invoker.callCount() != 0 {
|
|
t.Fatalf("corrupt workflow invoked provider %d times", harness.invoker.callCount())
|
|
}
|
|
}
|
|
|
|
func TestNewManagerRequiresStrictExecutionPorts(t *testing.T) {
|
|
_, err := NewManager(
|
|
ManagerConfig{OwnerID: "manager"},
|
|
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
|
|
)
|
|
if err == nil {
|
|
t.Fatal("NewManager accepted missing strict ports")
|
|
}
|
|
var identityErr *IdentityError
|
|
if _, err = NewManager(
|
|
ManagerConfig{}, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
|
|
); !errors.As(err, &identityErr) {
|
|
t.Fatalf("empty owner error = %v, want IdentityError", err)
|
|
}
|
|
}
|
|
|
|
func TestDurableIdentityEncodingIsInjective(t *testing.T) {
|
|
id1 := durableIdentity("dispatch-v1", "p1/w", "2", "1")
|
|
id2 := durableIdentity("dispatch-v1", "p1", "w/2", "1")
|
|
if id1 == id2 {
|
|
t.Fatalf("durable identity collision: %q == %q", id1, id2)
|
|
}
|
|
|
|
dk1 := dispatchKey("p1/w", "2", 1)
|
|
dk2 := dispatchKey("p1", "w/2", 1)
|
|
if dk1 == dk2 {
|
|
t.Fatalf("dispatchKey collision: %q == %q", dk1, dk2)
|
|
}
|
|
|
|
rk1 := reviewKey("p1", "w", 1, "art#1")
|
|
rk2 := reviewKey("p1", "w#1", 1, "art")
|
|
if rk1 == rk2 {
|
|
t.Fatalf("reviewKey collision: %q == %q", rk1, rk2)
|
|
}
|
|
}
|
|
|
|
func TestEventIdentityDistinguishesCommandsAndReplays(t *testing.T) {
|
|
e1 := Event{
|
|
Type: EventManualStart,
|
|
ProjectID: "p1",
|
|
WorkspaceID: "w1",
|
|
CommandID: "cmd-1",
|
|
WorkflowRevision: "rev-1",
|
|
}
|
|
e2 := Event{
|
|
Type: EventManualStart,
|
|
ProjectID: "p1",
|
|
WorkspaceID: "w1",
|
|
CommandID: "cmd-2",
|
|
WorkflowRevision: "rev-1",
|
|
}
|
|
m := &Manager{clock: systemClock{}, events: &recordingEvents{}}
|
|
m.emit(context.Background(), e1)
|
|
m.emit(context.Background(), e2)
|
|
|
|
var id1, id2, id1Replay string
|
|
m.events = &testSink{onEmit: func(e Event) { id1 = e.EventID }}
|
|
m.emit(context.Background(), e1)
|
|
m.events = &testSink{onEmit: func(e Event) { id2 = e.EventID }}
|
|
m.emit(context.Background(), e2)
|
|
m.events = &testSink{onEmit: func(e Event) { id1Replay = e.EventID }}
|
|
m.emit(context.Background(), e1)
|
|
|
|
if id1 == id2 {
|
|
t.Fatalf("different commands produced identical EventID: %q", id1)
|
|
}
|
|
if id1 != id1Replay {
|
|
t.Fatalf("exact replay produced different EventID: %q vs %q", id1, id1Replay)
|
|
}
|
|
}
|
|
|
|
type testSink struct {
|
|
onEmit func(Event)
|
|
}
|
|
|
|
func (s *testSink) Emit(_ context.Context, e Event) error {
|
|
if s.onEmit != nil {
|
|
s.onEmit(e)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestInterruptedResumeEmitsStableEvent(t *testing.T) {
|
|
snapshot := testSnapshot("project", "workspace", testUnit("work", WriteSetUnknown))
|
|
harness := newHarness(t, map[ProjectID]ProjectWorkflowSnapshot{"project": snapshot}, 1)
|
|
harness.start("project", "workspace", nil)
|
|
harness.store.edit(func(state *ManagerState) {
|
|
project := state.Projects["project"]
|
|
project.Status = ProjectStatusRunning
|
|
state.Projects["project"] = project
|
|
})
|
|
|
|
if err := harness.manager.Reconcile(context.Background()); err != nil {
|
|
t.Fatalf("Reconcile: %v", err)
|
|
}
|
|
|
|
eventsEmitted := harness.events.snapshot()
|
|
var hasAutoResume bool
|
|
for _, e := range eventsEmitted {
|
|
if e.Type == EventAutoResume {
|
|
hasAutoResume = true
|
|
if e.CommandID == "" || e.WorkflowRevision == "" {
|
|
t.Fatalf("EventAutoResume missing logical discriminator: %#v", e)
|
|
}
|
|
}
|
|
}
|
|
if !hasAutoResume {
|
|
t.Fatalf("auto resume did not emit EventAutoResume event")
|
|
}
|
|
}
|