339 lines
10 KiB
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")
|
|
}
|
|
}
|