Skip to content

fix(test): drain the process-level run registry between route checks - #133

Merged
fengzhi09 merged 2 commits into
mainfrom
fix/chat-first-turn-guard-flaky
Sep 30, 2026
Merged

fengzhi09 merged 2 commits into
mainfrom
fix/chat-first-turn-guard-flaky

Conversation

@fengzhi09

Copy link
Copy Markdown
Collaborator

背景

#132 修的 chat-run-mirror flaky 留下一条姊妹隐患:本 PR 处理同一类问题的第二处,并把排空逻辑抽成共用 helper。

诊断:两个问题只中一个

问题一存在且可复现 ✅

state-bus.js:717-718 的 runsByCid / runsBySid 是模块级 Map,activeRunCount() 读全局,无重置钩子。

注入 1 个真失败(把首条用例的 409 断言改 418)→ 3/3 红,后两条是 1 !== 0 与 3 !== 2——都是首条遗弃回合的残留 claim 被误算。

与姊妹文件的差别只在:失败点之后的 waitFor(activeRunCount()===0) 走不到,所以不烧 2 秒,级联表现为快速误报而非 14 秒。本质同一个坑。

问题二不存在,未硬改 ❌

派单预判姊妹文件也有「抛硬币的行数断言」,实测证伪:

  • 抛硬币那行确实产生(探针 A 打印 8 次:["› first","● ok","§§ processed_duration=3ms"])
  • 但本文件 4 处 chat 断言全走 chatLines()(剥所有 §§ 行)且用 assert.deepEqual 逐行比对,全文件无裸行数断言
  • 且每条用例前都有 waitFor 轮询,跨过毫秒边界,8/8 次都写了这一行——连抛硬币都不是

所以没有加 durable() 过滤器。

这是故障隔离,不是自发性 flaky

场景 结果
修复前连跑 10 次 10/10 绿
nice -n 19 + 14 个 CPU burner 下 10 次 10/10 绿
注入 1 个真失败(修复后) 1 红 2 绿 ✅ 级联被截断
注入 1 个真失败(修复前) 3/3 红

文件本来就不自发性抖动。一次失败曾让三条一起红。

Helper 设计

新增 packages/webui/test/helpers/turn-drain.mjs:

createTurnDrain({ label, activeRunCount, unblock?, releaseParkedPrompts, timeoutMs })
  → { track, cleanup, pending }

它不 import 任何东西——不碰传输层、不碰 state-bus、不解析 server 路径,只接注入的回调。

于是**「这个文件可能留下什么」就是构造点里那几个参数**:省略即声明,释放顺序错了在调用点看得见。

cleanup() 顺序固定且承重:

unblock → releaseParkedPrompts → await 已跟踪回合(5s 上界)→ 断言 activeRunCount() === 0

最后那步断言把静默泄漏变成指名道姓的失败。

两个调用点:guard 文件不传 unblock(无 session/new 闸门);run-mirror 传 unblock: releaseNewSessionGate。闸门本身留在各自文件里。

变异验证

变体 结果
修复版无变异 1 红 2 绿 ✅ 级联截断
M1 排空整体失效 3 红,两条各烧 5000ms
M3 去掉 releaseParkedPrompts 3 红,两条各烧 5000ms
M2 去掉 await 不变红 —— 如实记录

M2 单独说明:构造过存储污染探针(泄漏用例后接一条探针,等 200ms 读 sessions.loadSessions()),修复版与 M2 变体下存储都是 []。本文件两条用例之间没留出让 finalize 晚到越过 rmSync 的空档。所以 await 是排序加固,防的是没能确定性复现的时序竞争。

没有夸大,也没有因此删掉它。

门禁

门禁 退出码 结果
pnpm webapp:typecheck / typecheck 0 —
pnpm test:webapp ×2 0 / 0 两次均 1842/1842
pnpm test:webui ×2 0 / 0 两次均 2181 tests / 0 fail / 2 skipped
pnpm check:source 0 4901 files
check-docs-alignment.mjs 0 all checks passed
test-tmp-leak verify-registry 0 169 entries
test-isolation-lint 0 clean

产品代码 0 行。

文档

不改 docs/,也不动 AGENTS.md。

曾建议在 AGENTS.md 的 "Run the gates the way CI runs them" 追加一句,现决定不加,理由:那规则只对「真实 handleSend + 假 ACP 传输」的 route check 成立;send-run-guard.test.js 直接调 beginRun/endRun、不 park prompt。写成全仓库通用规则会让不适用它的文件以为要加钩子。

因果链已完整写在 helper 的模块头注释里——下一个姊妹文件 import 时就会读到,比全仓库泛化的一行更精准,也不产生第二处会漂移的事实来源。若日后这类 check 变多,正确做法是扩 AGENTS.md 里已有的 tmp-helper 那条,而不是新起平行规则。

与 #132 的关系

两者都基于同一诊断。建议 #132 先合(它是本 PR 的前置事实来源),本 PR 可独立合入——它同时移植了 #132 的修复并改用同一 helper。

🤖 Generated with DeepSeek Harness

s39-dev and others added 2 commits September 30, 2026 20:43
The guard suite shared the failure mode that made run-mirror look
like a slow machine: beginRun/endRun keeps its tables in module scope,
activeRunCount() reads them globally, and nothing drains them when a
test abandons a turn. Inject one real failure and the following tests
report 1 !== 0 and 3 !== 2 — counts that belong to the abandoned
turn, not to them. Here the cascade is fast rather than slow, because
the waitFor after the failing point is never reached.

It is a fault-isolation defect, not a spontaneous flake: the file ran
10/10 clean before this and 10/10 clean under nice plus 14 CPU
burners. One failure used to cost three.

The drain helper imports nothing — no transport, no state-bus, no
server paths. The call site lists what the file might leave behind by
naming the callbacks, and a missing one is a visible omission rather
than a silent leak. Cleanup unblocks, releases, awaits what it
tracked, then asserts activeRunCount() === 0 so a leak fails by name.

The coin-flip row count is left alone here on purpose: every chat
assertion in this file goes through chatLines() with deepEqual, there
is no bare length assertion to bypass, and the waitFor before each case
means the row is always written — 8 out of 8 probes.
#132 and this branch touched the same file: the drain helper was
extracted here while #132 was still open, so the fix is present in
both. Kept whichever side protects the run-mirror assertions more
completely and dropped the duplicate.
@fengzhi09
fengzhi09 merged commit 3236536 into main Sep 30, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant