问题
packages/shared/src/research/service.test.ts 里的 waitForTerminal() 用固定次数轮询等待 run 进入终态:
for (let i = 0; i < 100; i += 1) {
const run = await service.getRun(runId);
if (run && terminal.has(run.status)) return run.status;
await new Promise((resolve) => setTimeout(resolve, 5));
}
throw new Error(`run ${runId} did not reach a terminal status`);
每次循环都包含一次 service.getRun()(回读磁盘上的 run 存储)加一次 setTimeout(5),因此 100 次循环实际只买到约 500–700ms 墙钟时间。而 ResearchService 的流水线本身在慢机器 / 满载 CI Runner 上跑完就要 700ms 以上,于是健康的 run 被判定为「did not reach a terminal status」,把本来通过的行为报成失败。
这是等待预算的问题,不是被测行为的问题:run 最终确实会收敛,只是轮询窗口比流水线耗时还短。
复现
在干净 main(7c9b550,2026-09-22)上直接跑:
bun test packages/shared/src/research/service.test.ts --isolate
本机(Windows 11 / Bun 1.3.x)连跑 3 次:
| 次数 |
pass |
fail |
失败项 |
| 1 |
4 |
3 |
runs a report end-to-end and persists it、lists runs newest-first、plans from the strategy and persists strategyId onto the report |
| 2 |
5 |
2 |
runs a report end-to-end and persists it、plans from the strategy and persists strategyId onto the report |
| 3 |
5 |
2 |
同上 |
报错一致:error: run research-<uuid> did not reach a terminal status,耗时 709–769ms(> 100×5ms 预算)。
同一问题也在 GitHub Actions 上出现过:PR #138 的 Full unit tests (advisory) 作业(run 35678224696 / job 106589165526)以 1605 pass / 2 fail 失败,两个失败项正是
ResearchService > lists runs newest-first(538.59ms)
ResearchService > plans from the strategy and persists strategyId onto the report(620.87ms)
根因
轮询预算用「迭代次数」而不是「墙钟时间」表达。迭代的真实墙钟成本(≈7ms/次,含一次磁盘回读)远大于 setTimeout(5) 的 5ms,所以 100 次迭代覆盖的时间随机器负载漂移;一旦流水线耗时超过这个漂移窗口,测试就假失败。
预期 / 实际
- 预期:run 只要在合理时间内收敛,
waitForTerminal() 就返回终态;测试只因「行为不对」而失败。
- 实际:run 尚未收敛时预算已耗尽,抛
did not reach a terminal status;失败与被测行为无关,且在慢环境上稳定复现。
影响
Full unit tests (advisory) 是 PR 门禁里的可观测信号。这类假失败会淹没真实回归,也让贡献者无法从 advisory 红灯判断自己的改动是否安全。
建议修复
把固定 100 次迭代改成墙钟 deadline(例如 TERMINAL_WAIT_MS = 5000)驱动轮询,并补一条「模拟 800ms 后才收敛的流水线」的回归用例——该用例在旧的固定预算下必然失败,可钉住这个行为。
范围
仅 packages/shared/src/research/service.test.ts(测试辅助函数 + 新增用例),不触碰生产代码。
问题
packages/shared/src/research/service.test.ts里的waitForTerminal()用固定次数轮询等待 run 进入终态:每次循环都包含一次
service.getRun()(回读磁盘上的 run 存储)加一次setTimeout(5),因此 100 次循环实际只买到约 500–700ms 墙钟时间。而 ResearchService 的流水线本身在慢机器 / 满载 CI Runner 上跑完就要 700ms 以上,于是健康的 run 被判定为「did not reach a terminal status」,把本来通过的行为报成失败。这是等待预算的问题,不是被测行为的问题:run 最终确实会收敛,只是轮询窗口比流水线耗时还短。
复现
在干净
main(7c9b550,2026-09-22)上直接跑:本机(Windows 11 / Bun 1.3.x)连跑 3 次:
runs a report end-to-end and persists it、lists runs newest-first、plans from the strategy and persists strategyId onto the reportruns a report end-to-end and persists it、plans from the strategy and persists strategyId onto the report报错一致:
error: run research-<uuid> did not reach a terminal status,耗时 709–769ms(> 100×5ms 预算)。同一问题也在 GitHub Actions 上出现过:PR #138 的
Full unit tests (advisory)作业(run35678224696/ job106589165526)以 1605 pass / 2 fail 失败,两个失败项正是ResearchService > lists runs newest-first(538.59ms)ResearchService > plans from the strategy and persists strategyId onto the report(620.87ms)根因
轮询预算用「迭代次数」而不是「墙钟时间」表达。迭代的真实墙钟成本(≈7ms/次,含一次磁盘回读)远大于
setTimeout(5)的 5ms,所以 100 次迭代覆盖的时间随机器负载漂移;一旦流水线耗时超过这个漂移窗口,测试就假失败。预期 / 实际
waitForTerminal()就返回终态;测试只因「行为不对」而失败。did not reach a terminal status;失败与被测行为无关,且在慢环境上稳定复现。影响
Full unit tests (advisory)是 PR 门禁里的可观测信号。这类假失败会淹没真实回归,也让贡献者无法从 advisory 红灯判断自己的改动是否安全。建议修复
把固定 100 次迭代改成墙钟 deadline(例如
TERMINAL_WAIT_MS = 5000)驱动轮询,并补一条「模拟 800ms 后才收敛的流水线」的回归用例——该用例在旧的固定预算下必然失败,可钉住这个行为。范围
仅
packages/shared/src/research/service.test.ts(测试辅助函数 + 新增用例),不触碰生产代码。