审计(DevDocs/audit/11-trace-observability.md 发现 #1):scheduler 用
span.SetProp("persona", sub.PersonaID) 设了这个 prop,api-client v0.47 也已把它写进
对外承诺,但 trace/graph.go 的 safePropKeys 白名单不含 "persona",而 safeSpanProps
只遍历白名单 ⇒ 该键在到达客户端前被静默丢弃(实测:整个 props 键都消失)。
修法:把 "persona" 加进 safePropKeys,并在同处写清它为什么可以前送 ——
① 它必然是注册表校验过的角色 id(scheduler.checkPersona 未知 id 一律 ErrUnknownPersona
拒绝整批派发);② 它本来就随 GET /personas 与 message_persisted.personaId 对外可见;
③ 值仍过 redactValue → RedactText(凭据形状清洗 + 80 rune 截断)。
测试:新增 trace/graph_persona_test.go(真 Tracer → 真 SetProp → 真 Snapshot,两条断言:
有 persona / 不指定角色时键不出现 = 钉住 v0.47 的"纯增量")+ 网关 flow_update_ws_test.go
追加 TestFlowGraphUpdateRelaysPersonaProp(真 bus Publish → 真 WS 帧 → 断言帧内节点
props.persona=="yuki")。
验证:go build/vet 绿;trace + gateway 包测试绿;全树 go test ./... = 58 包 ok / 0 FAIL;
linux/amd64 + darwin/arm64 交叉编译绿;gofmt -l 干净;三文件 471/57/370 行、最长函数 64 行。
变异验证:把 persona 从白名单摘掉 → 两条断言分别 FAIL(trace 层 props 为空;
WS 帧里 span-sub 节点的 props 键整个消失),还原后复绿。
遗留(登记):前端尚未消费该键(flow 节点未显示"以谁的身份替班")。
58 lines
2.3 KiB
Go
58 lines
2.3 KiB
Go
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)
|
||
}
|
||
}
|