package trace import ( "testing" "time" ) // TestFlowStatusForPhase 锁住 phase → 节点状态的映射(design/04 §1.1 // 细粒度 phase、design/19 §1 统一枚举):三个已知 phase 原样通过, // 未知 phase 返回空串让调用方回落到 span 终态推导。 func TestFlowStatusForPhase(t *testing.T) { t.Parallel() cases := map[string]string{ FlowStatusRunning: FlowStatusRunning, FlowStatusDone: FlowStatusDone, FlowStatusError: FlowStatusError, "started": "", "": "", } for phase, want := range cases { if got := FlowStatusForPhase(phase); got != want { t.Fatalf("FlowStatusForPhase(%q) = %q, want %q", phase, got, want) } } } // TestAddSpanToolPhaseDrivesStatus 锁住裁决 03.4.4:tool 节点的终态由 // span 携带的细粒度 phase 驱动(与 WS 的 tool_execution.end 帧同一 // 取值),缺 phase 的 tool span 回落到 span 终态推导,两路绝不矛盾。 func TestAddSpanToolPhaseDrivesStatus(t *testing.T) { t.Parallel() now := time.Now() tool := func(id, phase string, status SpanStatus) SpanRecord { rec := SpanRecord{ID: id, Name: "tool.echo", Kind: SpanKindTool, Status: status, StartedAt: now, EndedAt: now} if phase != "" { rec.Props = map[string]any{"phase": phase} } return rec } cases := []struct { name string rec SpanRecord want string }{ {"phase done", tool("t1", FlowStatusDone, SpanStatusOK), FlowStatusDone}, {"phase error overrides ok status", tool("t2", FlowStatusError, SpanStatusOK), FlowStatusError}, {"no phase falls back to span status", tool("t3", "", SpanStatusOK), FlowStatusDone}, {"no phase falls back to span error", tool("t4", "", SpanStatusError), FlowStatusError}, {"unknown phase falls back to span status", tool("t5", "started", SpanStatusError), FlowStatusError}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { g := NewRunGraph("web") g.Start(1, now) g.AddSpan(tc.rec) if len(g.nodes) != 1 { t.Fatalf("nodes = %d, want 1", len(g.nodes)) } if got := g.nodes[tc.rec.ID].Status; got != tc.want { t.Fatalf("tool node status = %q, want %q", got, tc.want) } }) } } // TestAddSpanNonToolIgnoresPhase 确保 phase 只驱动 tool 节点:其他 // kind 的 span 即使带 phase prop(未来不该有),也不改变其 // 终态推导路径。 func TestAddSpanNonToolIgnoresPhase(t *testing.T) { t.Parallel() now := time.Now() g := NewRunGraph("web") g.Start(1, now) g.AddSpan(SpanRecord{ID: "l1", Name: "llm.call", Kind: SpanKindLLM, Status: SpanStatusOK, StartedAt: now, EndedAt: now, Props: map[string]any{"phase": FlowStatusError}}) if got := g.nodes["l1"].Status; got != FlowStatusDone { t.Fatalf("llm node status = %q, want %q (phase must not drive non-tool nodes)", got, FlowStatusDone) } }