# 修复单 2 · 采用旁白超时测试挂死事件循环,拖垮同文件后三条用例(2026-09-04) 基线:`origin/staging` `13dded9f`(`TASK-rectification-adopt-narration-fix-20260904.md` 的实现)。本单只改 `adopt-narration-agent.ts` 的超时实现与对应进度记录,不动决策逻辑、不动提示词。 ## 0. 验收结论(对照修复单 1) | 项 | 结论 | 证据 | | --- | --- | --- | | 5.1 早退分支用本轮决策;三条调用方传 `decision`;facts / 模板用合成 receipt | 通过 | `persistNextInterviewAfterChoice` 函数体无 `decideFromDossier(`;`dossierWithCurrentInference`;点选用例改为点选前 dossier(revision 5、23/16/7),断言模型 1 次、facts 05:00 / 21/18/9 / `ready_to_adopt`;非法输出时模板范围来自本轮决策 | | 5.2 提示词「不要出现」;诊断枚举 `adopt_narration=`;8s 超时 | 通过(实现)/ **未通过(测试,见 §1)** | 提示词与校验器同口径;`deliverAdoptNarration` 返回 `adopt_narration`,`console.info` 不含模型原文;四种结果各有断言 | | 5.3 `stopFactsFromDropped` 婚恋标签 | 推迟(允许) | 进度记录已写明 | | BUG-521 / BUG-522 | 通过 | 无案例 ID、无用户资料;复发链接 BUG-440 | | CHANGELOG、`docs/testing` 清单 | 通过 | 点选入口列为优先手测项 | | tsc / lint | 通过 | `tsc` 0 错;lint 0 error 74 warning(既有) | | 定向套件 | **未通过** | `rectification-* / agentic-rectification-* / birth-time-*`:871 条,pass 858,fail 0,**cancelled 4**,进程退出码 1 | ## 1. 事故实证(P1 · 超时测试挂死) `adopt-narration-agent.ts`: ```ts function composedAbortSignal(signal, timeoutMs) { const timeout = AbortSignal.timeout(timeoutMs); return signal ? AbortSignal.any([signal, timeout]) : timeout; } ``` Node 的 `AbortSignal.timeout()` 内部计时器是 **unref** 的(所有版本,含 CI 用的 Node 22):它不会让事件循环保持活跃。测试「adopt narration times out to the template without throwing」把 `generateText` 挂成永不 resolve 的 Promise,此时进程里唯一待办就是这个 unref 计时器 → 事件循环直接排空,测试运行器判 `cancelledByParent: Promise resolution is still pending but the event loop has already resolved`。同一文件后面三条用例(`applyCollectFocusDenial …`、`distinguish declined …`、`family collect declined vs extra distinguish declined …`——后两条正是 BUG-520 的回归锁)也被一并取消。 本地复现(Node 20.19,`npx tsx --test tests/rectification-adopt-narration-20260904.test.ts`): ``` ok 1 … ok 6 not ok 7 - adopt narration times out to the template without throwing (cancelledByParent) not ok 8 / 9 / 10 (cancelledByParent) # tests 10 # pass 6 # fail 0 # cancelled 4 exit=1 ``` 独立验证(去掉所有业务代码): ```js const s = AbortSignal.timeout(30); await Promise.race([new Promise(() => {}), new Promise((_, rej) => s.addEventListener("abort", () => rej(s.reason)))]); // 进程直接退出,退出码 13,abort 从未触发 ``` 后果:`backend-quality-gate` 的 `npm test --prefix frontend` 会以退出码 1 失败,`13dded9f` 不会部署(本单落笔时 staging `/api/health` 仍是 `0aaa0d70`)。 进度记录写的「adopt-narration 10/10、871 pass / 0 fail」与实测不符:Node 摘要行 `# fail 0` 后面还有 `# cancelled 4`,且退出码为 1。 生产侧:`next start` 有监听 socket 撑住事件循环,超时在真实请求里会生效,本单**不是**线上功能故障;但计时器不随模型返回而清理,每次调用都会留一个 8 秒的悬挂计时器。 ## 2. 根因 超时用了 `AbortSignal.timeout()`,没意识到它的计时器是 unref 的;测试又依赖它在空事件循环里触发。验收只看了 `# fail`,没看 `# cancelled` 与退出码。 ## 3. 决策记录 原任务书与修复单 1 的决策不变。本单不新增产品决策。 ## 4. 硬红线 修复单 1 §4 全部沿用。追加: 1. 超时必须用 **ref** 的计时器(`setTimeout` + `AbortController`),并在模型返回、校验完成或外部 signal 触发后 `clearTimeout`;不得靠 `AbortSignal.timeout()`。 2. 不得为了让测试过而删掉超时用例或把它改成 `todo` / `skip`。 3. 进度记录必须贴 Node 摘要的完整六行(tests / pass / fail / cancelled / skipped / todo)与进程退出码。 ## 5. 任务分解 ### 5.1 P1 · 超时改为可清理的 ref 计时器(BUG-523) 1. `composedAbortSignal` 改为返回 `{ signal, dispose }`:内部 `new AbortController()`,`setTimeout(() => controller.abort(new DOMException("adopt narration timed out", "TimeoutError")), timeoutMs)`(不 `unref`),若有外部 `signal` 则监听其 `abort` 转发到 controller;`dispose()` 清计时器并移除监听。 2. `deliverAdoptNarration` 的 `Promise.race` 用 `try / finally` 调用 `dispose()`;超时仍归 `template:model_error`(枚举不变)。 3. `whenAborted` 的监听在 `dispose` 时一并移除,避免每次调用留下悬挂监听器。 验收: - `npx tsx --test tests/rectification-adopt-narration-20260904.test.ts` → `# tests 10 # pass 10 # cancelled 0`,退出码 0。 - 定向套件 `rectification-* / agentic-rectification-* / birth-time-*` → `# fail 0 # cancelled 0`,退出码 0。 - `npm test`(全量):与无 Docker 基线一致,即除 `docker ENOENT` 的 25 条外无其它失败,`# cancelled 0`。 - 新增一条用例:fake `generateText` 立即返回合法文案时,`deliverAdoptNarration` resolve 后不再有活跃计时器(可用 `setTimeout` 计数或断言 `dispose` 被调用),防止每次调用漏一个 8 秒计时器。 - `docs/BUG_HISTORY.md` 新增 BUG-523,关联 BUG-522;防复发写明「超时不用 `AbortSignal.timeout`」与「验收看 cancelled 与退出码」。 - `PROGRESS-rectification-adopt-narration-fix-20260904.md` 补一节更正:原「10/10、871 pass / 0 fail」实测为 pass 858 / cancelled 4 / exit 1。 ## 6. 让步顺序 只有 5.1 一条,必做;没有它 `13dded9f` 进不了 staging。 ## 7. 开工前置命令 ```bash git fetch origin --prune git worktree add -b codex/rectification-adopt-narration-fix2-20260904 .worktrees/rectification-adopt-narration-fix2-20260904 origin/staging cd .worktrees/rectification-adopt-narration-fix2-20260904/frontend npx tsx --test tests/rectification-adopt-narration-20260904.test.ts; echo "exit=$?" # 开工前应复现 cancelled 4 / exit 1 ``` ## 8. BUG 编号起点 截至本单:BUG-522。本单从 **BUG-523** 起;开工时再核对。