步骤从 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>
122 lines
5.7 KiB
Go
122 lines
5.7 KiB
Go
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)
|
||
}
|
||
}
|