iop/packages/go/agenttask/state_machine_test.go

339 lines
10 KiB
Go

package agenttask
import (
"context"
"errors"
"reflect"
"testing"
"time"
)
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 TestEventDeliveryCheckpointCloneValidation(t *testing.T) {
now := time.Date(2026, 7, 30, 17, 0, 0, 0, time.UTC)
unit := testUnit("work-delivery", WriteSetDisjoint)
work := WorkRecord{
Unit: unit,
State: WorkStateReady,
Attempt: 1,
AttemptID: attemptID(unit.ID, 1),
DispatchOrdinal: 7,
Locators: make(map[LocatorKind]LocatorRecord),
FailureBudgets: make(map[FailureStage]FailureBudgetRecord),
UpdatedAt: now,
}
project := ProjectRecord{
ProjectID: "project-delivery",
WorkspaceID: "workspace-delivery",
Status: ProjectStatusRunning,
Works: map[WorkUnitID]WorkRecord{
unit.ID: work,
},
UpdatedAt: now,
}
event := normalizeEventIdentity(Event{
Type: EventDependencyReady,
ProjectID: project.ProjectID,
WorkspaceID: project.WorkspaceID,
WorkUnitID: unit.ID,
AttemptID: work.AttemptID,
Ordinal: work.DispatchOrdinal,
State: work.State,
Detail: "dependency committed",
Timestamp: now,
})
state := cloneState(ManagerState{
SchemaVersion: currentSchemaVersion,
Projects: map[ProjectID]ProjectRecord{
project.ProjectID: project,
},
PendingEvents: map[string]EventDelivery{
event.EventID: {
Event: event,
EvidenceRevision: "revision-7",
Project: &project,
Work: &work,
},
},
})
if err := validateManagerState(state); err != nil {
t.Fatalf("valid pending delivery rejected: %v", err)
}
cloned := cloneState(state)
clonedDelivery := cloned.PendingEvents[event.EventID]
clonedDelivery.Work.Unit.Metadata = map[string]string{"mutated": "true"}
clonedProjectWork := clonedDelivery.Project.Works[unit.ID]
clonedProjectWork.Unit.Metadata = map[string]string{"project-mutated": "true"}
clonedDelivery.Project.Works[unit.ID] = clonedProjectWork
cloned.PendingEvents[event.EventID] = clonedDelivery
original := state.PendingEvents[event.EventID]
if original.Work.Unit.Metadata != nil ||
original.Project.Works[unit.ID].Unit.Metadata != nil {
t.Fatal("pending delivery clone aliases original evidence")
}
tests := []struct {
name string
mutate func(*ManagerState)
}{
{
name: "map key drift",
mutate: func(candidate *ManagerState) {
delivery := candidate.PendingEvents[event.EventID]
delete(candidate.PendingEvents, event.EventID)
candidate.PendingEvents["other-event"] = delivery
},
},
{
name: "project identity drift",
mutate: func(candidate *ManagerState) {
delivery := candidate.PendingEvents[event.EventID]
delivery.Project.ProjectID = "other-project"
candidate.PendingEvents[event.EventID] = delivery
},
},
{
name: "work snapshot drift",
mutate: func(candidate *ManagerState) {
delivery := candidate.PendingEvents[event.EventID]
delivery.Work.State = WorkStateBlocked
candidate.PendingEvents[event.EventID] = delivery
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
candidate := cloneState(state)
test.mutate(&candidate)
if err := validateManagerState(candidate); err == nil {
t.Fatal("drifted pending delivery passed validation")
}
})
}
}
func TestEventDeliveryEnqueueReplayKeepsTimestampAndRejectsConflict(t *testing.T) {
store := newMemoryStore()
now := time.Date(2026, 7, 30, 18, 0, 0, 0, time.UTC)
clock := &advancingClock{now: now}
unit := testUnit("work-event", WriteSetUnknown)
work := WorkRecord{
Unit: unit,
State: WorkStateReady,
Attempt: 1,
AttemptID: attemptID(unit.ID, 1),
DispatchOrdinal: 1,
Locators: make(map[LocatorKind]LocatorRecord),
FailureBudgets: make(map[FailureStage]FailureBudgetRecord),
}
project := ProjectRecord{
ProjectID: "project-event",
WorkspaceID: "workspace-event",
Status: ProjectStatusRunning,
Works: map[WorkUnitID]WorkRecord{unit.ID: work},
}
store.edit(func(state *ManagerState) {
state.Projects[project.ProjectID] = project
})
manager := &Manager{
config: ManagerConfig{StateWriteAttempts: 4},
clock: clock,
store: store,
events: &recordingEvents{},
}
event := Event{
Type: EventDependencyReady,
ProjectID: project.ProjectID,
WorkspaceID: project.WorkspaceID,
WorkUnitID: unit.ID,
AttemptID: work.AttemptID,
Ordinal: work.DispatchOrdinal,
State: work.State,
Detail: "ready",
}
first, err := manager.enqueueEvent(context.Background(), event)
if err != nil {
t.Fatalf("first enqueue: %v", err)
}
clock.Advance(time.Hour)
replayed, err := manager.enqueueEvent(context.Background(), event)
if err != nil {
t.Fatalf("replayed enqueue: %v", err)
}
if first.Event.EventID != replayed.Event.EventID ||
!first.Event.Timestamp.Equal(replayed.Event.Timestamp) ||
first.EvidenceRevision != replayed.EvidenceRevision ||
!reflect.DeepEqual(first, replayed) {
t.Fatalf("pending replay changed delivery:\nfirst=%+v\nreplay=%+v", first, replayed)
}
conflicting := event
conflicting.EventID = first.Event.EventID
conflicting.Detail = "different logical content"
if _, err := manager.enqueueEvent(context.Background(), conflicting); err == nil {
t.Fatal("conflicting pending EventID reuse was accepted")
}
}
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")
}
}