Files
AskaEth 4bf1f7cac6
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
fix(trace): 让 graph_update 的分支根节点 props.persona 真的前送(白名单漏登记)
审计(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 节点未显示"以谁的身份替班")。
2026-10-04 10:37:22 +08:00

58 lines
2.3 KiB
Go
Raw Permalink 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 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)
}
}