package taskrun_test import ( "testing" "chat-agent-system/internal/stream" "chat-agent-system/internal/taskrun" ) func TestFoldPhaseEvents_MirrorsFrontendSemantics(t *testing.T) { events := []taskrun.StoredEvent{ {Seq: 1, Event: stream.StreamEvent{Type: stream.EventPhase, Phase: &stream.PhasePayload{PhaseID: "squad_assembly", Message: "组建小队"}}}, {Seq: 2, Event: stream.StreamEvent{Type: stream.EventPhase, Phase: &stream.PhasePayload{PhaseID: "step_executing", Message: "撰写中", StepIndex: 1, TotalSteps: 2}}}, {Seq: 3, Event: stream.StreamEvent{Type: stream.EventPhase, Phase: &stream.PhasePayload{PhaseID: "step_completed", Message: "完成"}}}, {Seq: 4, Event: stream.StreamEvent{Type: stream.EventStatus, Status: &stream.StatusPayload{Message: "ignore"}}}, } steps, label := taskrun.FoldPhaseEvents(events) if len(steps) != 2 { t.Fatalf("steps=%d want 2: %+v", len(steps), steps) } if steps[0].Status != "completed" || steps[1].Status != "completed" { t.Fatalf("expected both completed, got %+v", steps) } if label != "完成" { t.Fatalf("label=%q", label) } if steps[1].Detail != "撰写中" { t.Fatalf("detail=%q", steps[1].Detail) } // 收口后的 label 必须归一化为静态措辞:进度中叙述被短标签替换,步数结构后缀保留。 if steps[0].Label != "组建小队已完成" { t.Fatalf("steps[0].label=%q want 组建小队已完成", steps[0].Label) } if steps[1].Label != "专员执行(第 1/2 步)已完成" { t.Fatalf("steps[1].label=%q want 专员执行(第 1/2 步)已完成", steps[1].Label) } } func TestBuildPhaseSnapshot_SettlesRunningOnTerminal(t *testing.T) { events := []taskrun.StoredEvent{ {Seq: 1, Event: stream.StreamEvent{Type: stream.EventPhase, Phase: &stream.PhasePayload{PhaseID: "quality_gate", Message: "门禁中", Progress: 90}}}, } snap := taskrun.BuildPhaseSnapshot(events, 12_000, taskrun.StatusCompleted, "用户初始需求收集表") if snap == nil { t.Fatal("nil snapshot") } if snap.DurationMs != 12_000 || snap.Title != "用户初始需求收集表" { t.Fatalf("meta %+v", snap) } if len(snap.Steps) != 1 || snap.Steps[0].Status != "completed" { t.Fatalf("steps %+v", snap.Steps) } // 终态收口后 label 不得冻结为"门禁中(90%)"。 if snap.Steps[0].Label != "质量门禁已完成" { t.Fatalf("label=%q want 质量门禁已完成", snap.Steps[0].Label) } } func TestBuildPhaseSnapshot_FailedMarksRunningFailed(t *testing.T) { events := []taskrun.StoredEvent{ {Seq: 1, Event: stream.StreamEvent{Type: stream.EventPhase, Phase: &stream.PhasePayload{PhaseID: "step_executing", Message: "执行中"}}}, } snap := taskrun.BuildPhaseSnapshot(events, 0, taskrun.StatusFailed, "") if snap.Steps[0].Status != "failed" { t.Fatalf("status=%s", snap.Steps[0].Status) } // 失败步骤有意保留原步骤文案(错误细节由错误气泡承载),不做归一化。 if snap.Steps[0].Label != "执行中" { t.Fatalf("label=%q want 执行中", snap.Steps[0].Label) } } // TestBuildPhaseSnapshot_AdvisorBriefingFrozenLabel 复现消息 #4424: // advisor_briefing 以 running + "正在…(98%)" 落盘后被终态收口, // label 必须归一化,任务不得展示"已完成却像还在跑"的冻结文案。 func TestBuildPhaseSnapshot_AdvisorBriefingFrozenLabel(t *testing.T) { events := []taskrun.StoredEvent{ {Seq: 1, Event: stream.StreamEvent{Type: stream.EventPhase, Phase: &stream.PhasePayload{PhaseID: "squad_assembly", Message: "正在组建并启动【用户初始需求收集表】智能体小队(后端: pi-agent)...", Progress: 5}}}, {Seq: 2, Event: stream.StreamEvent{Type: stream.EventPhase, Phase: &stream.PhasePayload{PhaseID: "step_executing", Message: "审核主管正在进行:审核并定稿交付..."}}}, {Seq: 3, Event: stream.StreamEvent{Type: stream.EventPhase, Phase: &stream.PhasePayload{PhaseID: "quality_gate", Message: "质量门禁审核通过(评分: 100.0 / 100)", Progress: 95}}}, {Seq: 4, Event: stream.StreamEvent{Type: stream.EventPhase, Phase: &stream.PhasePayload{PhaseID: "advisor_briefing", Message: "贴身业务参谋正在提炼关键决策摘要与风险雷达...", Progress: 98}}}, } snap := taskrun.BuildPhaseSnapshot(events, 244_864, taskrun.StatusPendingAuthorConfirmation, "用户初始需求收集表") if snap == nil { t.Fatal("nil snapshot") } if len(snap.Steps) != 4 { t.Fatalf("steps=%d want 4", len(snap.Steps)) } want := []string{"组建小队已完成", "专员执行已完成", "质量门禁已完成", "参谋简报已完成"} for i, st := range snap.Steps { if st.Status != "completed" { t.Fatalf("steps[%d].status=%s want completed", i, st.Status) } if st.Label != want[i] { t.Fatalf("steps[%d].label=%q want %q", i, st.Label, want[i]) } } // Detail 保留原始叙述(当前 UI 不展示,供审计)。 if snap.Steps[3].Detail != "贴身业务参谋正在提炼关键决策摘要与风险雷达..." { t.Fatalf("steps[3].detail=%q", snap.Steps[3].Detail) } } // TestNormalizeSettledStep_Idempotent 保证回填脚本可安全重复执行。 func TestNormalizeSettledStep_Idempotent(t *testing.T) { step := taskrun.PhaseStepSnapshot{Step: "advisor_briefing#2", Status: "completed", Label: "贴身业务参谋正在提炼关键决策摘要与风险雷达...(98%)"} once := taskrun.NormalizeSettledStep(step) if once.Label != "参谋简报已完成" { t.Fatalf("label=%q", once.Label) } twice := taskrun.NormalizeSettledStep(once) if twice != once { t.Fatalf("not idempotent: %+v vs %+v", once, twice) } // running / pending 不改动。 run := taskrun.PhaseStepSnapshot{Step: "quality_gate", Status: "running", Label: "门禁中(90%)"} if got := taskrun.NormalizeSettledStep(run); got != run { t.Fatalf("running step mutated: %+v", got) } }