Files
Agent-System/internal/taskrun/phase_snapshot_test.go
T
weichenandCursor 561709178a fix(taskrun): 收口 completed 步骤时归一化冻结的进度中 label
步骤从 running 收口为 completed 时,此前只翻转 Status,Label 永久冻结为
上游下发的"正在…(NN%)"进度中叙述,导致卡片即便步骤已完成仍读起来像还在跑
(缺陷见消息 #4424)。

新增导出函数 NormalizeSettledStep:completed 步骤改写为短标签 + "已完成",
保留"(第 x/y 步)"结构后缀以区分多步任务,原始叙述留在 Detail 供审计;
failed/pending/running 有意不改(失败细节由错误气泡承载)。
接入 applyPhasePayload 三处收口分支、existing-step 刷新分支,
以及 BuildPhaseSnapshot 终态收口 + 存量兜底全量归一化。

导出供回填脚本复用,避免逻辑漂移。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-21 10:32:38 +00:00

122 lines
5.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}