// plan_progress_test.go - 模块 17.2 进度追踪测试 // // 测试覆盖: // - StepProgress: Duration,IsTerminal // - PlanProgress: 注册步骤,StartStep,FinishStep,SkipDependents // - PlanProgressSnapshot: 聚合计数,ReadySteps,ProgressPercent,IsComplete,IsSuccess // - OnProgress 回调触发 // - Observer 事件上报 // - 并发安全 // - PlanModeManager.AttachProgress / Progress // - PlanProgressEvent 满足 Event 接口 package engine import ( "context" "errors" "strconv" "strings" "sync" "testing" "time" ) // itoa 是测试辅助函数,将整数转为字符串. // 原 production 代码中的 itoa 已删除(改用 strconv.Itoa),测试保留此别名. func itoa(n int) string { return strconv.Itoa(n) } // ───────────────────────────────────────────── // 辅助函数 // ───────────────────────────────────────────── // makeSteps 创建 n 个简单的测试步骤(s1, s2, ... sN,无依赖). func makeSteps(n int) []PlanStep { steps := make([]PlanStep, n) for i := 0; i < n; i++ { steps[i] = PlanStep{ ID: itoa(i + 1), Description: "step " + itoa(i+1), Complexity: ComplexityLow, Deps: []string{}, } } return steps } // captureObserver 捕获事件的测试 Observer. type captureObserver struct { mu sync.Mutex events []string errors []error } func (c *captureObserver) Event(name string, _ map[string]any) { c.mu.Lock() defer c.mu.Unlock() c.events = append(c.events, name) } func (c *captureObserver) Error(err error, _ map[string]any) { c.mu.Lock() defer c.mu.Unlock() c.errors = append(c.errors, err) } func (c *captureObserver) hasEvent(name string) bool { c.mu.Lock() defer c.mu.Unlock() for _, e := range c.events { if e == name { return true } } return false } func (c *captureObserver) countEvent(name string) int { c.mu.Lock() defer c.mu.Unlock() count := 0 for _, e := range c.events { if e == name { count++ } } return count } // ───────────────────────────────────────────── // StepProgress 测试 // ───────────────────────────────────────────── func TestStepProgress_Duration_NotStarted(t *testing.T) { sp := StepProgress{Step: PlanStep{ID: "s1"}, Status: StepStatusPending} if sp.Duration() != 0 { t.Errorf("expected 0 duration for unstarted step, got %v", sp.Duration()) } } func TestStepProgress_Duration_Running(t *testing.T) { sp := StepProgress{ Step: PlanStep{ID: "s1"}, Status: StepStatusRunning, StartedAt: time.Now().Add(-100 * time.Millisecond), } if sp.Duration() < 50*time.Millisecond { t.Errorf("expected duration >= 50ms for running step, got %v", sp.Duration()) } } func TestStepProgress_Duration_Finished(t *testing.T) { start := time.Now().Add(-200 * time.Millisecond) end := time.Now().Add(-50 * time.Millisecond) sp := StepProgress{ Step: PlanStep{ID: "s1"}, Status: StepStatusDone, StartedAt: start, FinishedAt: end, } d := sp.Duration() if d < 100*time.Millisecond || d > 300*time.Millisecond { t.Errorf("unexpected duration for finished step: %v", d) } } func TestStepProgress_IsTerminal(t *testing.T) { cases := []struct { status StepStatus terminal bool }{ {StepStatusPending, false}, {StepStatusRunning, false}, {StepStatusDone, true}, {StepStatusFailed, true}, {StepStatusSkipped, true}, } for _, tc := range cases { sp := StepProgress{Status: tc.status} if sp.IsTerminal() != tc.terminal { t.Errorf("status %q: IsTerminal() = %v, want %v", tc.status, sp.IsTerminal(), tc.terminal) } } } // ───────────────────────────────────────────── // PlanProgress 基础测试 // ───────────────────────────────────────────── func TestNewPlanProgress_EmptySteps(t *testing.T) { p := NewPlanProgress("s1", nil, nil) snap := p.Snapshot() if snap.TotalCount != 0 { t.Errorf("expected 0 steps, got %d", snap.TotalCount) } if snap.IsComplete() { // 0 步骤不算 complete(TotalCount==0 → IsComplete=false) t.Error("empty plan should not be complete") } } func TestNewPlanProgress_WithSteps(t *testing.T) { steps := makeSteps(3) p := NewPlanProgress("sess", steps, nil) snap := p.Snapshot() if snap.TotalCount != 3 { t.Errorf("expected 3 steps, got %d", snap.TotalCount) } if snap.PendingCount != 3 { t.Errorf("expected 3 pending, got %d", snap.PendingCount) } if snap.IsComplete() { t.Error("fresh plan should not be complete") } } func TestPlanProgress_RegisterStep(t *testing.T) { p := NewPlanProgress("sess", nil, nil) step := PlanStep{ID: "new", Description: "new step", Deps: []string{}} if err := p.RegisterStep(step); err != nil { t.Fatalf("RegisterStep: %v", err) } snap := p.Snapshot() if snap.TotalCount != 1 { t.Errorf("expected 1 step after register, got %d", snap.TotalCount) } } func TestPlanProgress_RegisterStep_Duplicate(t *testing.T) { steps := makeSteps(1) p := NewPlanProgress("sess", steps, nil) if err := p.RegisterStep(steps[0]); err == nil { t.Error("expected error on duplicate step registration") } } // ───────────────────────────────────────────── // StartStep / FinishStep 状态机测试 // ───────────────────────────────────────────── func TestPlanProgress_StartStep(t *testing.T) { p := NewPlanProgress("s", makeSteps(2), nil) if err := p.StartStep("1", "agent-1"); err != nil { t.Fatalf("StartStep: %v", err) } snap := p.Snapshot() if snap.RunningCount != 1 { t.Errorf("expected 1 running, got %d", snap.RunningCount) } if snap.PendingCount != 1 { t.Errorf("expected 1 pending, got %d", snap.PendingCount) } // 步骤的 AgentID 已记录 if snap.Steps[0].AgentID != "agent-1" { t.Errorf("unexpected AgentID: %q", snap.Steps[0].AgentID) } // StartedAt 已记录 if snap.Steps[0].StartedAt.IsZero() { t.Error("StartedAt should be set") } } func TestPlanProgress_StartStep_Unknown(t *testing.T) { p := NewPlanProgress("s", makeSteps(1), nil) if err := p.StartStep("nonexistent", ""); err == nil { t.Error("expected error for unknown step") } } func TestPlanProgress_StartStep_AlreadyRunning(t *testing.T) { p := NewPlanProgress("s", makeSteps(1), nil) _ = p.StartStep("1", "") if err := p.StartStep("1", ""); err == nil { t.Error("expected error when starting already-running step") } } func TestPlanProgress_FinishStep_Done(t *testing.T) { p := NewPlanProgress("s", makeSteps(1), nil) _ = p.StartStep("1", "") if err := p.FinishStep("1", StepStatusDone, ""); err != nil { t.Fatalf("FinishStep: %v", err) } snap := p.Snapshot() if snap.DoneCount != 1 { t.Errorf("expected 1 done, got %d", snap.DoneCount) } if !snap.IsComplete() { t.Error("single-step plan should be complete after finishing") } if !snap.IsSuccess() { t.Error("should be success with no failures") } if snap.Steps[0].FinishedAt.IsZero() { t.Error("FinishedAt should be set") } } func TestPlanProgress_FinishStep_Failed(t *testing.T) { p := NewPlanProgress("s", makeSteps(1), nil) _ = p.StartStep("1", "") if err := p.FinishStep("1", StepStatusFailed, "timeout error"); err != nil { t.Fatalf("FinishStep: %v", err) } snap := p.Snapshot() if snap.FailedCount != 1 { t.Errorf("expected 1 failed, got %d", snap.FailedCount) } if !snap.IsComplete() { t.Error("should be complete after terminal state") } if snap.IsSuccess() { t.Error("should not be success with failures") } if snap.HasFailed() != true { t.Error("HasFailed should return true") } if snap.Steps[0].ErrorMessage != "timeout error" { t.Errorf("unexpected error message: %q", snap.Steps[0].ErrorMessage) } } func TestPlanProgress_FinishStep_InvalidStatus(t *testing.T) { p := NewPlanProgress("s", makeSteps(1), nil) _ = p.StartStep("1", "") if err := p.FinishStep("1", StepStatusRunning, ""); err == nil { t.Error("expected error for non-terminal finish status") } } func TestPlanProgress_FinishStep_AlreadyTerminal(t *testing.T) { p := NewPlanProgress("s", makeSteps(1), nil) _ = p.StartStep("1", "") _ = p.FinishStep("1", StepStatusDone, "") if err := p.FinishStep("1", StepStatusDone, ""); err == nil { t.Error("expected error when finishing already-terminal step") } } func TestPlanProgress_FinishStep_SkipFromPending(t *testing.T) { // skip 可以跳过从未 start 的步骤 p := NewPlanProgress("s", makeSteps(1), nil) if err := p.FinishStep("1", StepStatusSkipped, "dep failed"); err != nil { t.Fatalf("FinishStep skip from pending: %v", err) } snap := p.Snapshot() if snap.SkippedCount != 1 { t.Errorf("expected 1 skipped, got %d", snap.SkippedCount) } } // ───────────────────────────────────────────── // SkipDependents 测试 // ───────────────────────────────────────────── func TestPlanProgress_SkipDependents_Linear(t *testing.T) { // s1 → s2 → s3(链式依赖) steps := []PlanStep{ {ID: "s1", Deps: []string{}}, {ID: "s2", Deps: []string{"s1"}}, {ID: "s3", Deps: []string{"s2"}}, } p := NewPlanProgress("sess", steps, nil) _ = p.StartStep("s1", "") _ = p.FinishStep("s1", StepStatusFailed, "boom") skipped := p.SkipDependents("s1") if len(skipped) != 2 { t.Errorf("expected 2 skipped, got %d: %v", len(skipped), skipped) } snap := p.Snapshot() if snap.SkippedCount != 2 { t.Errorf("expected 2 skipped in snapshot, got %d", snap.SkippedCount) } if !snap.IsComplete() { t.Error("plan should be complete (1 failed + 2 skipped)") } } func TestPlanProgress_SkipDependents_Diamond(t *testing.T) { // 菱形依赖:s1 → s2, s1 → s3, s2+s3 → s4 steps := []PlanStep{ {ID: "s1", Deps: []string{}}, {ID: "s2", Deps: []string{"s1"}}, {ID: "s3", Deps: []string{"s1"}}, {ID: "s4", Deps: []string{"s2", "s3"}}, } p := NewPlanProgress("sess", steps, nil) _ = p.StartStep("s1", "") _ = p.FinishStep("s1", StepStatusFailed, "network error") skipped := p.SkipDependents("s1") if len(skipped) != 3 { t.Errorf("expected 3 skipped (s2,s3,s4), got %d: %v", len(skipped), skipped) } } func TestPlanProgress_SkipDependents_NoDependents(t *testing.T) { // 独立步骤,无依赖 steps := makeSteps(3) p := NewPlanProgress("sess", steps, nil) _ = p.StartStep("1", "") _ = p.FinishStep("1", StepStatusFailed, "err") skipped := p.SkipDependents("1") if len(skipped) != 0 { t.Errorf("expected 0 skipped for independent steps, got %d", len(skipped)) } } func TestPlanProgress_SkipDependents_AlreadyTerminal(t *testing.T) { // 已经 terminal 的步骤不应该被 skip steps := []PlanStep{ {ID: "s1", Deps: []string{}}, {ID: "s2", Deps: []string{"s1"}}, } p := NewPlanProgress("sess", steps, nil) _ = p.StartStep("s2", "") // s2 已 running _ = p.FinishStep("s1", StepStatusFailed, "") skipped := p.SkipDependents("s1") // s2 虽然依赖 s1,但已经是 running 状态,不应再 skip if len(skipped) != 0 { t.Errorf("expected 0 skipped for already-running step, got %d: %v", len(skipped), skipped) } } // ───────────────────────────────────────────── // PlanProgressSnapshot 测试 // ───────────────────────────────────────────── func TestSnapshot_ProgressPercent(t *testing.T) { steps := makeSteps(4) p := NewPlanProgress("s", steps, nil) // 0% 初始 snap := p.Snapshot() if snap.ProgressPercent() != 0 { t.Errorf("expected 0%%, got %.1f%%", snap.ProgressPercent()) } // 完成 2/4 = 50% _ = p.StartStep("1", "") _ = p.FinishStep("1", StepStatusDone, "") _ = p.StartStep("2", "") _ = p.FinishStep("2", StepStatusFailed, "err") snap = p.Snapshot() if snap.ProgressPercent() != 50 { t.Errorf("expected 50%%, got %.1f%%", snap.ProgressPercent()) } } func TestSnapshot_Duration(t *testing.T) { steps := makeSteps(1) p := NewPlanProgress("s", steps, nil) // 未开始时 Duration 为 0 if p.Snapshot().Duration() != 0 { t.Error("duration should be 0 before start") } _ = p.StartStep("1", "") // 开始后 duration > 0 if p.Snapshot().Duration() <= 0 { t.Error("duration should be > 0 after start") } _ = p.FinishStep("1", StepStatusDone, "") // 完成后 duration 固定 d1 := p.Snapshot().Duration() time.Sleep(5 * time.Millisecond) d2 := p.Snapshot().Duration() if d1 != d2 { t.Errorf("duration should be fixed after finish: %v vs %v", d1, d2) } } func TestSnapshot_ReadySteps_NoDeps(t *testing.T) { // 无依赖步骤 - 全部 ready steps := makeSteps(3) p := NewPlanProgress("s", steps, nil) snap := p.Snapshot() ready := snap.ReadySteps() if len(ready) != 3 { t.Errorf("expected 3 ready steps (no deps), got %d", len(ready)) } } func TestSnapshot_ReadySteps_WithDeps(t *testing.T) { // s2 依赖 s1 - 初始只有 s1 ready steps := []PlanStep{ {ID: "s1", Deps: []string{}}, {ID: "s2", Deps: []string{"s1"}}, {ID: "s3", Deps: []string{}}, } p := NewPlanProgress("s", steps, nil) snap := p.Snapshot() ready := snap.ReadySteps() if len(ready) != 2 { t.Errorf("expected 2 ready steps (s1, s3), got %d", len(ready)) } readyIDs := map[string]bool{} for _, r := range ready { readyIDs[r.Step.ID] = true } if !readyIDs["s1"] || !readyIDs["s3"] { t.Errorf("unexpected ready steps: %v", ready) } // 完成 s1 后,s2 变 ready _ = p.StartStep("s1", "") _ = p.FinishStep("s1", StepStatusDone, "") snap = p.Snapshot() ready = snap.ReadySteps() if len(ready) != 2 { // s2 和 s3 t.Errorf("after s1 done, expected 2 ready (s2, s3), got %d", len(ready)) } } func TestSnapshot_ReadySteps_RunningNotReady(t *testing.T) { // 已 running 的步骤不应出现在 ready 列表 steps := makeSteps(2) p := NewPlanProgress("s", steps, nil) _ = p.StartStep("1", "") snap := p.Snapshot() ready := snap.ReadySteps() for _, r := range ready { if r.Step.ID == "1" { t.Error("running step should not appear in ReadySteps") } } } func TestSnapshot_IsComplete_AllDone(t *testing.T) { steps := makeSteps(2) p := NewPlanProgress("s", steps, nil) _ = p.StartStep("1", "") _ = p.FinishStep("1", StepStatusDone, "") _ = p.StartStep("2", "") _ = p.FinishStep("2", StepStatusDone, "") if !p.Snapshot().IsComplete() { t.Error("should be complete when all done") } } func TestSnapshot_IsComplete_MixedTerminal(t *testing.T) { steps := makeSteps(3) p := NewPlanProgress("s", steps, nil) _ = p.StartStep("1", "") _ = p.FinishStep("1", StepStatusDone, "") _ = p.FinishStep("2", StepStatusSkipped, "dep failed") _ = p.StartStep("3", "") _ = p.FinishStep("3", StepStatusFailed, "err") if !p.Snapshot().IsComplete() { t.Error("should be complete with mixed terminal states") } } // ───────────────────────────────────────────── // OnProgress 回调测试 // ───────────────────────────────────────────── func TestPlanProgress_OnProgress_CalledOnStart(t *testing.T) { p := NewPlanProgress("s", makeSteps(2), nil) var calls int p.SetOnProgress(func(_ PlanProgressSnapshot) { calls++ }) _ = p.StartStep("1", "") if calls != 1 { t.Errorf("expected 1 callback on StartStep, got %d", calls) } } func TestPlanProgress_OnProgress_CalledOnFinish(t *testing.T) { p := NewPlanProgress("s", makeSteps(1), nil) var snapshots []PlanProgressSnapshot p.SetOnProgress(func(snap PlanProgressSnapshot) { snapshots = append(snapshots, snap) }) _ = p.StartStep("1", "") _ = p.FinishStep("1", StepStatusDone, "") if len(snapshots) != 2 { t.Errorf("expected 2 callbacks (start+finish), got %d", len(snapshots)) } // 第二个快照应显示 complete if !snapshots[1].IsComplete() { t.Error("second snapshot should show complete") } } func TestPlanProgress_OnProgress_SnapshotIsImmutable(t *testing.T) { // 回调收到的快照(值类型)在后续状态变化后不受影响 // 精妙之处(CLEVER): PlanProgressSnapshot 是值类型,回调收到的是副本-- // 测试通过"锁定第一个快照,然后验证 Steps slice 不被后续赋值破坏"来确认此不变量. p := NewPlanProgress("s", makeSteps(2), nil) var snapshots []PlanProgressSnapshot p.SetOnProgress(func(snap PlanProgressSnapshot) { snapshots = append(snapshots, snap) }) _ = p.StartStep("1", "") _ = p.StartStep("2", "") // 第一个快照:RunningCount 应为 1(只有 step1 running) // 第二个快照:RunningCount 应为 2(step1 和 step2 都 running) // 关键:第一个快照在第二次回调后不应被修改 if len(snapshots) < 2 { t.Fatalf("expected at least 2 snapshots, got %d", len(snapshots)) } first := snapshots[0] if first.RunningCount != 1 { t.Errorf("first snapshot should have RunningCount=1, got %d", first.RunningCount) } second := snapshots[1] if second.RunningCount != 2 { t.Errorf("second snapshot should have RunningCount=2, got %d", second.RunningCount) } // 第一个快照不受第二次回调影响(值语义保证) if first.RunningCount != 1 { t.Error("first snapshot mutated after second callback — should be immutable") } } func TestPlanProgress_OnProgress_CalledOnSkipDependents(t *testing.T) { steps := []PlanStep{ {ID: "s1", Deps: []string{}}, {ID: "s2", Deps: []string{"s1"}}, } p := NewPlanProgress("s", steps, nil) var calls int p.SetOnProgress(func(_ PlanProgressSnapshot) { calls++ }) _ = p.StartStep("s1", "") _ = p.FinishStep("s1", StepStatusFailed, "") calls = 0 // 重置计数,只统计 SkipDependents p.SkipDependents("s1") if calls != 1 { t.Errorf("expected 1 callback from SkipDependents, got %d", calls) } } func TestPlanProgress_OnProgress_RegisterStep(t *testing.T) { // RegisterStep 不触发 onProgress(只有状态变更才触发) p := NewPlanProgress("s", nil, nil) var calls int p.SetOnProgress(func(_ PlanProgressSnapshot) { calls++ }) _ = p.RegisterStep(PlanStep{ID: "new", Deps: []string{}}) if calls != 0 { t.Errorf("RegisterStep should not trigger onProgress, got %d calls", calls) } } // ───────────────────────────────────────────── // Observer 事件测试 // ───────────────────────────────────────────── func TestPlanProgress_Observer_StartStep(t *testing.T) { obs := &captureObserver{} p := NewPlanProgress("s", makeSteps(1), obs) _ = p.StartStep("1", "") if !obs.hasEvent("plan_step_started") { t.Error("expected plan_step_started event") } } func TestPlanProgress_Observer_FinishStep(t *testing.T) { obs := &captureObserver{} p := NewPlanProgress("s", makeSteps(1), obs) _ = p.StartStep("1", "") _ = p.FinishStep("1", StepStatusDone, "") if !obs.hasEvent("plan_step_finished") { t.Error("expected plan_step_finished event") } } func TestPlanProgress_Observer_RegisterStep(t *testing.T) { obs := &captureObserver{} p := NewPlanProgress("s", nil, obs) _ = p.RegisterStep(PlanStep{ID: "x", Deps: []string{}}) if !obs.hasEvent("plan_step_registered") { t.Error("expected plan_step_registered event") } } func TestPlanProgress_Observer_SkipDependents(t *testing.T) { obs := &captureObserver{} steps := []PlanStep{ {ID: "s1", Deps: []string{}}, {ID: "s2", Deps: []string{"s1"}}, } p := NewPlanProgress("s", steps, obs) _ = p.StartStep("s1", "") _ = p.FinishStep("s1", StepStatusFailed, "") p.SkipDependents("s1") if !obs.hasEvent("plan_step_skipped") { t.Error("expected plan_step_skipped event") } } func TestPlanProgress_Observer_NilSafe(t *testing.T) { // nil observer 不应 panic p := NewPlanProgress("s", makeSteps(1), nil) _ = p.StartStep("1", "") _ = p.FinishStep("1", StepStatusDone, "") // 不 panic 即通过 } // ───────────────────────────────────────────── // 并发安全测试 // ───────────────────────────────────────────── func TestPlanProgress_Concurrent_StartFinish(t *testing.T) { // 100 个步骤,并发执行 n := 100 stepList := make([]PlanStep, n) for i := 0; i < n; i++ { stepList[i] = PlanStep{ID: "step-" + itoa(i), Deps: []string{}} } p := NewPlanProgress("s", stepList, nil) var wg sync.WaitGroup for i := 0; i < n; i++ { wg.Add(1) go func(idx int) { defer wg.Done() id := "step-" + itoa(idx) _ = p.StartStep(id, "") _ = p.FinishStep(id, StepStatusDone, "") }(i) } wg.Wait() snap := p.Snapshot() if snap.DoneCount != n { t.Errorf("expected %d done, got %d", n, snap.DoneCount) } if !snap.IsComplete() { t.Error("should be complete after all concurrent steps") } } func TestPlanProgress_Concurrent_SnapshotRead(t *testing.T) { // 并发读写不 panic(data race 检测) steps := makeSteps(10) p := NewPlanProgress("s", steps, nil) var wg sync.WaitGroup // 写 goroutine wg.Add(1) go func() { defer wg.Done() for i := 0; i < 10; i++ { _ = p.StartStep(itoa(i+1), "") _ = p.FinishStep(itoa(i+1), StepStatusDone, "") } }() // 读 goroutine wg.Add(1) go func() { defer wg.Done() for i := 0; i < 50; i++ { _ = p.Snapshot() } }() wg.Wait() } // ───────────────────────────────────────────── // PlanModeManager.AttachProgress 集成测试 // ───────────────────────────────────────────── func TestPlanModeManager_AttachProgress(t *testing.T) { m := newTestManager() if m.Progress() != nil { t.Error("initial progress should be nil") } prog := NewPlanProgress("s", makeSteps(2), nil) m.AttachProgress(prog) if m.Progress() != prog { t.Error("Progress() should return attached tracker") } } func TestPlanModeManager_AttachProgress_Nil(t *testing.T) { m := newTestManager() prog := NewPlanProgress("s", makeSteps(1), nil) m.AttachProgress(prog) m.AttachProgress(nil) if m.Progress() != nil { t.Error("after attaching nil, Progress() should return nil") } } func TestPlanModeManager_AttachProgress_Workflow(t *testing.T) { // 完整工作流:Enter → 审批 → AttachProgress → 执行步骤 store := NewMemoryPlanStore("") _ = store.WritePlan("sess", "# Plan\nStep 1: write tests") var capturedEvent PlanApprovalEvent policy := FuncApprovalPolicy{ Fn: func(_ context.Context, event PlanApprovalEvent) (bool, string, error) { capturedEvent = event return true, "", nil }, } m := newTestManager() m.store = store m.approval = policy m.SetSessionID("sess") // Enter if err := m.Enter(); err != nil { t.Fatalf("Enter: %v", err) } // Exit(审批) _, err := m.Exit(context.Background(), nil) if err != nil { t.Fatalf("Exit: %v", err) } _ = capturedEvent // 消费以避免 unused 警告 // AttachProgress(消费方在审批通过后绑定) steps := []PlanStep{ {ID: "s1", Description: "write tests", Complexity: ComplexityLow, Deps: []string{}}, } prog := NewPlanProgress("sess", steps, nil) m.AttachProgress(prog) // 执行步骤 if err := prog.StartStep("s1", "exec-agent"); err != nil { t.Fatalf("StartStep: %v", err) } if err := prog.FinishStep("s1", StepStatusDone, ""); err != nil { t.Fatalf("FinishStep: %v", err) } snap := prog.Snapshot() if !snap.IsComplete() { t.Error("workflow: plan should be complete") } if !snap.IsSuccess() { t.Error("workflow: plan should be success") } } // ───────────────────────────────────────────── // PlanProgressEvent 接口测试 // ───────────────────────────────────────────── func TestPlanProgressEvent_ImplementsEventInterface(t *testing.T) { // PlanProgressEvent 必须满足 Event 接口 var _ Event = &PlanProgressEvent{} } func TestPlanProgressEvent_EventType(t *testing.T) { e := &PlanProgressEvent{Snapshot: PlanProgressSnapshot{SessionID: "s1"}} if e.EventType() != "plan_progress" { t.Errorf("unexpected event type: %q", e.EventType()) } } func TestPlanProgressEvent_SnapshotField(t *testing.T) { snap := PlanProgressSnapshot{ SessionID: "test-sess", TotalCount: 3, DoneCount: 1, } e := &PlanProgressEvent{Snapshot: snap} if e.Snapshot.SessionID != "test-sess" { t.Errorf("unexpected session ID: %q", e.Snapshot.SessionID) } } // ───────────────────────────────────────────── // 边界条件 & 错误路径测试 // ───────────────────────────────────────────── func TestPlanProgress_FinishStep_Unknown(t *testing.T) { p := NewPlanProgress("s", makeSteps(1), nil) if err := p.FinishStep("nonexistent", StepStatusDone, ""); err == nil { t.Error("expected error for unknown step") } } func TestPlanProgress_Observer_MultipleEvents(t *testing.T) { obs := &captureObserver{} steps := makeSteps(3) p := NewPlanProgress("s", steps, obs) for i := 1; i <= 3; i++ { _ = p.StartStep(itoa(i), "") _ = p.FinishStep(itoa(i), StepStatusDone, "") } // 3 start + 3 finish if obs.countEvent("plan_step_started") != 3 { t.Errorf("expected 3 plan_step_started events, got %d", obs.countEvent("plan_step_started")) } if obs.countEvent("plan_step_finished") != 3 { t.Errorf("expected 3 plan_step_finished events, got %d", obs.countEvent("plan_step_finished")) } } func TestPlanProgress_FinishedAt_SetWhenAllDone(t *testing.T) { steps := makeSteps(2) p := NewPlanProgress("s", steps, nil) _ = p.StartStep("1", "") _ = p.FinishStep("1", StepStatusDone, "") // 还有步骤未完成,finishedAt 应为零 if !p.Snapshot().FinishedAt.IsZero() { t.Error("FinishedAt should be zero while steps remain") } _ = p.StartStep("2", "") _ = p.FinishStep("2", StepStatusDone, "") // 全部完成后 finishedAt 应非零 if p.Snapshot().FinishedAt.IsZero() { t.Error("FinishedAt should be set when all steps done") } } func TestPlanProgress_StepStatus_Constants(t *testing.T) { // 常量值不应变化(破坏性变更保护) cases := map[StepStatus]string{ StepStatusPending: "pending", StepStatusRunning: "running", StepStatusDone: "done", StepStatusFailed: "failed", StepStatusSkipped: "skipped", } for k, v := range cases { if string(k) != v { t.Errorf("StepStatus constant changed: %q != %q", k, v) } } } func TestPlanProgress_StartedAt_TracksFirst(t *testing.T) { // startedAt 只记录第一个步骤 start 的时间 steps := makeSteps(2) p := NewPlanProgress("s", steps, nil) _ = p.StartStep("1", "") t1 := p.Snapshot().StartedAt time.Sleep(2 * time.Millisecond) _ = p.StartStep("2", "") t2 := p.Snapshot().StartedAt if t1 != t2 { t.Error("StartedAt should not change after first step starts") } } // ───────────────────────────────────────────── // 重规划场景测试(RegisterStep 动态追加) // ───────────────────────────────────────────── func TestPlanProgress_Replan_AppendStep(t *testing.T) { // 初始 2 步,s1 失败后追加补救步骤 s3 steps := makeSteps(2) p := NewPlanProgress("s", steps, nil) _ = p.StartStep("1", "") _ = p.FinishStep("1", StepStatusFailed, "network error") p.SkipDependents("1") // s2 无依赖关系,不受影响 // 补救步骤 rescue := PlanStep{ID: "rescue-1", Description: "retry with fallback", Deps: []string{}} if err := p.RegisterStep(rescue); err != nil { t.Fatalf("RegisterStep rescue: %v", err) } _ = p.StartStep("rescue-1", "") _ = p.FinishStep("rescue-1", StepStatusDone, "") _ = p.StartStep("2", "") _ = p.FinishStep("2", StepStatusDone, "") snap := p.Snapshot() if snap.TotalCount != 3 { t.Errorf("expected 3 total (2 original + 1 rescue), got %d", snap.TotalCount) } if snap.DoneCount != 2 { t.Errorf("expected 2 done (rescue + s2), got %d", snap.DoneCount) } if snap.FailedCount != 1 { t.Errorf("expected 1 failed (s1), got %d", snap.FailedCount) } } // ───────────────────────────────────────────── // 辅助函数:确保 errors 包在测试里有意义 // ───────────────────────────────────────────── func TestPlanProgress_ErrorsAreDescriptive(t *testing.T) { p := NewPlanProgress("s", makeSteps(1), nil) err := p.StartStep("nonexistent", "") if err == nil || !strings.Contains(err.Error(), "unknown step") { t.Errorf("error should mention 'unknown step', got: %v", err) } _ = p.StartStep("1", "") err = p.StartStep("1", "") if err == nil || !strings.Contains(err.Error(), "cannot start") { t.Errorf("error should mention 'cannot start', got: %v", err) } err = p.FinishStep("1", StepStatusPending, "") // invalid finish status if err == nil { t.Error("expected error for invalid finish status") } // 确保 errors 包被使用 _ = errors.New("placeholder") }