iop/packages/go/agenttask/state_machine_test.go
toki 3c48879c44 feat(agent-runtime): 독립 Agent CLI 런타임 기반을 구현한다
독립 호스트에서 안전한 작업 실행과 복구를 제공하기 위해 런타임 설정, 정책, 상태 저장소, 워크스페이스 격리 및 AgentTask 오케스트레이션을 확장한다.
2026-07-29 13:12:21 +09:00

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")
}
}