test(flake): 清掉最后三条墙钟/条件竞态(mcp 订阅门控 + vision 注入时钟 + voice 兜底期限)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- #6 capability/mcp TestRegistry_ToolListChangedRefreshes:真因**不是慢,而是条件竞态** —— 本机协商到协议 2026-07-28(SEP-2575)时服务器**只**把 notifications/tools/list_changed 投给已登记 subscriptions/listen 的会话,而客户端订阅是 fire-and-forget(Connect 返回 ≠ 服务器已登记),服务器又在变更后 10ms 发一次、丢了不补 ⇒ 订阅登记晚于那个定时器,该次变更**永远**不生效(旧 waitFor(5s) 只能超时)。修法:显式门控 —— 先等服务器 notifications/subscriptions/acknowledged(服务器先登记再回确认)→ 再触发变更 → 再断言刷新由 tools/list_changed 驱动(既有断言一条未少、另加一条更强的驱动断言)。协议回落旧版时门控自动跳过。

- #7 multimodal/vision TestImageCache_RefetchesAfterTTLWhenContentChanges:真因是把**墙钟当判据**(ttl=20ms 下「TTL 内」成立与否只由调度抖动决定,实测空载≈0、并行负载下 >20ms 会同时打红两条断言)。修法:注入可手动推进的 testClock(生产本就有 cache.now 字段、默认 time.Now,零生产改动),断言一条未少。

- 附带:修掉 G5 批次遗留的第三条墙钟兜底 —— gateway/voice_turn_test.go 的 waitFrame 3s → 15s。**放宽的是兜底期限、不是断言**:它断言的是「帧确实到达」,帧没到照样 Fatal;实测单跑 -count=20 全绿、只在整树并行下偶红(语音回合要过编排器 → 门控假 runner → 假 TTS → 定向回连接)。

- 主会话独立验收:隔离 worktree(F2 三个测试文件)build/vet 绿、两条目标用例各 -count=20 绿;加本条兜底修复后整树连跑 2 次均 59 ok / 0 FAIL(修前那次整树抓到 voice 那条红 → 归因见上)。

- 子代理自报(已核实并清理):它在加压时用 30 个 yes 做负载、第一次 kill 没杀干净 ⇒ 整机 12 核被占满约 15 分钟(期间所有 go 命令近乎停滞,也解释了本轮测试为何异常慢);已全部终止(Get-Process yes = 0),此后证据均在干净环境重跑。它同时**如实说明**:在纯加压下没能直接撞出这两条原始 flake(窗口极窄),确定性论证建立在「可控对抗条件复现 + 根因」上。
This commit is contained in:
AskaEth committed 2026-10-04 21:11:17 +08:00
1 parent 3ba31b58ff
commit 6066165394
4 files changed
+121 -12

No files matched your search

+33 -3
View File
@@ -249,18 +249,44 @@ func TestRegistry_Unregister_RemovesTools(t *testing.T) {
}
}
const (
// methodSubscriptionsAck 是 SEP-2575 的订阅确认通知方法名(SDK 未导出该常量)。
methodSubscriptionsAck = "notifications/subscriptions/acknowledged"
// methodToolListChanged 是工具清单变更通知方法名(SDK 未导出该常量)。
methodToolListChanged = "notifications/tools/list_changed"
// sep2575ProtocolVersion 是引入 subscriptions/listen 订阅流的协议版本:自它起
// 服务器只向**已登记订阅**的会话投递 list_changed 通知(更早的协议版本对全部
// 会话广播,不存在下面这个窗口)。
sep2575ProtocolVersion = "2026-07-28"
// notificationWaitBudget 是等上述事件的兜底时限:只用于避免测试挂死,
// 事件本身才是断言对象。
notificationWaitBudget = 5 * time.Second
)
// TestRegistry_ToolListChangedRefreshes 服务器运行中加工具 → 通知 → registry 增量刷新,
// 同时 schema 缓存按服务器整表失效(design/09 §1.1)。
//
// 确定性要点:现代协议(≥ sep2575ProtocolVersion)下服务器**只**把 tools/list_changed
// 投给已登记 subscriptions/listen 订阅的会话,而客户端发订阅是 fire-and-forget
// (Connect 返回 ≠ 服务器已登记),通知本身又是「变更后 10ms 发一次、丢了不补」——
// 订阅登记之前触发的变更会被永久静默丢弃。旧写法(改工具 → 轮询 5s)等于拿这个竞态
// 与机器负载对赌,输的那次等多久都等不到 mul。现在先等服务器回订阅确认(服务器先登记
// 订阅、再回确认)再触发变更,把「通知会不会来」变回确定的事。
func TestRegistry_ToolListChangedRefreshes(t *testing.T) {
t.Parallel()
srv := newFakeServer()
reg := registry.New()
tr := startFakeServer(t, srv)
r := NewRegistry(reg, MCPConfig{Name: "calc"}, WithTransport(tr))
sn := &sniffTransport{inner: startFakeServer(t, srv)}
r := NewRegistry(reg, MCPConfig{Name: "calc"}, WithTransport(sn))
if err := r.Register(context.Background()); err != nil {
t.Fatalf("Register: %v", err)
}
if v := r.mcp.ServerInfo().ProtocolVersion; v >= sep2575ProtocolVersion {
waitFor(t, notificationWaitBudget, "服务器回订阅确认 "+methodSubscriptionsAck,
func() bool { return sn.received(methodSubscriptionsAck) })
}
_, _ = reg.Get("echo") // 让缓存先有命中记录,便于确认失效计数
srv.AddTool(&mcpsdk.Tool{
Name: "mul",
@@ -268,10 +294,14 @@ func TestRegistry_ToolListChangedRefreshes(t *testing.T) {
InputSchema: addToolSpec().InputSchema,
}, addHandler)
waitFor(t, 5*time.Second, "registry 出现新工具 mul", func() bool {
waitFor(t, notificationWaitBudget, "registry 出现新工具 mul", func() bool {
_, err := reg.Get("mul")
return err == nil
})
// 新工具必须由变更通知驱动刷进来,而不是别的路径顺手带进来的。
if !sn.received(methodToolListChanged) {
t.Fatalf("registry 出现了 mul,但客户端没收到 %s", methodToolListChanged)
}
if _, err := reg.Get("echo"); err != nil {
t.Fatalf("刷新后 echo 丢失:%v", err)
}