package trace import ( "context" "testing" ) // branchRootFor 用真实链路构建一张图:真 Tracer.StartSpan 建出子代理分支根 // (挂在 spawn 工具 span 之下,与 orchestration/scheduler.go 的父子关系一致), // 由调用方在 span 上设 prop,然后 Finish + Snapshot 后折叠进 RunGraph。 // 返回图与分支根节点 id。 func branchRootFor(t *testing.T, props map[string]any) (*FlowGraph, string) { t.Helper() tr := New(nil) toolCtx, toolSpan := tr.StartSpan(context.Background(), "tool.subagent_spawn", SpanKindTool) _, subSpan := tr.StartSpan(toolCtx, "subagent.sub-1", SpanKindSubagent) for k, v := range props { if err := subSpan.SetProp(k, v); err != nil { t.Fatalf("set prop %q: %v", k, err) } } subSpan.Finish(SpanStatusOK) toolSpan.Finish(SpanStatusOK) // 先折叠父工具 span,再折叠分支根,模拟 loop.FlowPublisher 的到达顺序。 return graphFor([]SpanRecord{toolSpan.Snapshot(), subSpan.Snapshot()}, "q", "a"), subSpan.ID } // TestSnapshotCarriesPersonaProp 钉住 Docs/api-client.md §3.5(v0.47)的第一半: // subagent_spawn 指定目标角色时,该分支根节点的 props 必须真的带 persona 键, // 值 = 目标角色 id(design/22 §8-2)。断言落在真 Span.SetProp → 真 Snapshot() // → 真 RunGraph.Snapshot() 的产物上,而不是白名单常量本身,所以构建器一旦 // 不再前送该键(例如从 safePropKeys 里漏掉),这里就会失败。 func TestSnapshotCarriesPersonaProp(t *testing.T) { g, rootID := branchRootFor(t, map[string]any{"persona": "yuki"}) root := nodeByID(g, rootID) if root.ID != rootID { t.Fatalf("分支根 %q 没进快照: %+v", rootID, g.Nodes) } if root.Kind != nodeKindSubagent { t.Fatalf("分支根 kind = %q, want %q", root.Kind, nodeKindSubagent) } if got := root.Props["persona"]; got != "yuki" { t.Fatalf("分支根 props.persona = %v, want yuki (props=%+v)", got, root.Props) } } // TestSnapshotOmitsPersonaPropWhenNoRole 钉住 v0.47 的另一半:不指定角色派发时 // 该键不出现(纯增量,老客户端逐字无感)。 func TestSnapshotOmitsPersonaPropWhenNoRole(t *testing.T) { g, rootID := branchRootFor(t, nil) root := nodeByID(g, rootID) if _, ok := root.Props["persona"]; ok { t.Fatalf("未指定角色却前送了 persona: %+v", root.Props) } }