Skip to content

test(cli): yield the event loop between base-tree tests - #12573

Merged
wenshao merged 2 commits into
mainfrom
fix/review-base-tree-worker-yield
Sep 24, 2026
Merged

wenshao merged 2 commits into
mainfrom
fix/review-base-tree-worker-yield

Conversation

@wenshao

@wenshao wenshao commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Makes the review base-tree test suite let the vitest worker's event loop turn once before every test. The yield uses a setImmediate captured when the file loads, so a test that installs fake timers cannot swallow it. This is the approach #10050 took for the scripts test setup.

This is a test-only change of 9 added lines in one file. Production code is untouched.

Why it's needed

Test (ubuntu-latest, Node 22.x) fails on every GitHub-hosted run even though every test passes. The cli leg reports Tests 32798 passed and then Errors 1 error: [vitest-worker]: Timeout calling "onTaskUpdate", which makes the job exit 1. Fork PRs from authors without write access are routed to hosted runners, so they all hit this.

The cause is the base-tree suite. Since #11540 it has 106 tests, and each one drives real git synchronously. On a hosted runner the whole file takes about 118 s, and the worker's event loop never turns during it. vitest's worker-to-main onTaskUpdate RPC gives up after 60 s. ECS runners finish the file in 22–26 s, which is why the failure only shows up on hosted runs.

Recent examples, all with this exact signature and the base-tree suite at about 118 s:

Every hosted Test (ubuntu) job completed on 09-23 failed this way.

Reviewer Test Plan

How to verify

Run the base-tree suite on its own from packages/cli with CI=true. Before this change it reports 106 passed plus the onTaskUpdate unhandled error, and exits 1. After it, it reports 106 passed and exits 0. On CI, Test (ubuntu-latest, Node 22.x) on a hosted runner should go green, assuming nothing else fails.

Evidence (Before & After)

Local runs on main 64ac2faae7, Linux x86_64, Node 22, CI=true, using the cli's own vitest config plus an observation-only setup file. The setup file runs a 100 ms interval in the worker and records the longest stretch the event loop did not turn.

runs result longest event-loop stall file wall time
before (main) 4 106 passed + Timeout calling "onTaskUpdate", exit 1 (4/4) 114.6–115.3 s 120.8–121.4 s
after (this PR) 2 106 passed, exit 0 (2/2) 12.0 s 121.0–121.1 s

The wall time does not change. The longest remaining stall is a single test, about 12 s, well under the 60 s RPC deadline.

eslint --max-warnings 0, prettier --check and tsc --noEmit for packages/cli all exit 0.

Tested on

OS Status
🍏 macOS ⚠️
🪟 Windows ⚠️
🐧 Linux ✅

Environment (optional)

N/A. These were unit-test runs only.

Risk & Scope

  • Main risk or tradeoff: none expected. One extra macrotask turn before each of 106 tests costs microseconds, and the suite does not use fake timers. A single test that blocks for 60 s would still trip the RPC, because testTimeout cannot interrupt synchronous code. That limit is the same one fix(ci): yield the event loop between script tests to avoid vitest RPC timeouts (#10037) #10050 has.
  • Not validated / out of scope: this only fixes this one file. I did not add a cli-wide yield in test-setup.ts. That would protect future suites too, but it touches all 1083 cli test files, so it deserves its own discussion.
  • Breaking changes / migration notes: none.

Linked Issues

Refs #11540 (grew the suite to 106 tests) and #10050 (the same fix for script tests). Observed on #12250.

中文说明

本 PR 做了什么

让 review base-tree 测试套件在每个测试开始前,把 vitest worker 的事件循环让出一次。这次让出用的是文件加载时捕获的 setImmediate,所以即使测试安装了 fake timers 也吞不掉它。做法与 #10050 在 scripts 测试 setup 中的一致。

这是一个纯测试改动:一个文件新增 9 行,生产代码未改。

为什么需要

Test (ubuntu-latest, Node 22.x) 在所有 GitHub hosted runner 上都会失败,尽管测试全部通过。cli 这一段先报 Tests 32798 passed,接着报 Errors 1 error: [vitest-worker]: Timeout calling "onTaskUpdate",job 因此以 1 退出。没有写权限的作者提的 fork PR 会被分到 hosted runner,所以它们都会遇到这个问题。

根源在 base-tree 套件。自 #11540 起它有 106 个测试,每个都同步驱动真实的 git。在 hosted runner 上整个文件约 118 秒,期间 worker 的事件循环一次也没有转动。vitest 从 worker 到主进程的 onTaskUpdate RPC 在 60 秒后放弃。ECS runner 跑完这个文件只需 22–26 秒,所以只有 hosted 运行才会失败。

近期实例,特征完全相同,base-tree 套件都在约 118 秒:

09-23 所有已完成的 hosted Test (ubuntu) job 都以这种方式失败。

评审测试计划

如何验证

在 packages/cli 下以 CI=true 单独运行 base-tree 套件。修改前的结果是 106 passed,外加 onTaskUpdate 未处理错误,退出码 1。修改后是 106 passed,退出码 0。在 CI 上,只要没有其他失败,hosted runner 上的 Test (ubuntu-latest, Node 22.x) 应该变绿。

证据(前后对比)

本地在 main 64ac2faae7 上运行,Linux x86_64,Node 22,CI=true。使用 cli 自己的 vitest 配置,外加一个只做观测的 setup 文件:它在 worker 中跑一个 100 ms 的 interval,记录事件循环最长一次没有转动的时长。

次数 结果 事件循环最长卡顿 文件耗时
修改前(main) 4 106 passed + Timeout calling "onTaskUpdate",退出码 1(4/4) 114.6–115.3 秒 120.8–121.4 秒
修改后(本 PR) 2 106 passed,退出码 0(2/2) 12.0 秒 121.0–121.1 秒

总耗时不变。剩下最长的一次卡顿是单个测试,约 12 秒,远低于 60 秒的 RPC 期限。

packages/cli 的 eslint --max-warnings 0、prettier --check 和 tsc --noEmit 均 exit 0。

测试平台

OS 状态
🍏 macOS ⚠️
🪟 Windows ⚠️
🐧 Linux ✅

环境(可选)

不适用,只运行了单元测试。

风险与范围

  • 主要风险或取舍: 预计没有。106 个测试前各多一次宏任务轮转,开销是微秒级;该套件也没有使用 fake timers。单个测试如果同步阻塞 60 秒,仍然会触发 RPC 超时,因为 testTimeout 打断不了同步代码。这一限制与 fix(ci): yield the event loop between script tests to avoid vitest RPC timeouts (#10037) #10050 相同。
  • 未验证 / 范围外: 只修这一个文件。我没有在 test-setup.ts 里为整个 cli 加让出。那样能同时保护以后的套件,但会影响全部 1083 个 cli 测试文件,值得单独讨论。
  • 破坏性变更 / 迁移说明: 无。

关联 Issue

参见 #11540(把套件扩到 106 个测试)和 #10050(script 测试的同类修复)。问题在 #12250 上观察到。

Every base-tree test drives real git through spawnSync/execFileSync, so
the vitest worker's event loop never turns while the file runs (~115 s
locally, ~118 s on GitHub-hosted runners). vitest's worker->main
`onTaskUpdate` RPC times out after 60 s, and the cli leg exits 1 with
every test green: `Errors 1 error: [vitest-worker]: Timeout calling
"onTaskUpdate"`. Since #11540 grew the file to 106 tests, this fails
`Test (ubuntu-latest)` on every hosted run, which is where fork PRs go.

Yield once before each test, using a setImmediate captured at load so
fake timers cannot intercept it, the same approach as the scripts test
setup (#10050). The longest stall drops to ~12 s with unchanged wall time.
qqqys
qqqys previously approved these changes Sep 23, 2026

@qqqys qqqys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent Critical-only review — head 021e9222

Test-infrastructure only: nine added lines in packages/cli/src/commands/review/base-tree.test.ts, no production code and no assertion changed.

The change does what it claims

The suite drives real git through spawnSync/execFileSync, which blocks synchronously, so the worker's event loop never turns for the duration of the file. Vitest's worker→main onTaskUpdate RPC then exceeds its timeout and the run exits non-zero with every test green — a false CI failure, which is the right thing to fix.

The remedy yields one loop turn per test:

const realSetImmediate = setImmediate;
beforeEach(() => new Promise<void>((resolve) => realSetImmediate(resolve)));

Three properties make this correct rather than merely plausible:

  • The primitive is captured at module load. const realSetImmediate = setImmediate executes at import time, before any test body can install fake timers, so the yield cannot be swallowed by a mocked timer queue. This file does not currently call vi.useFakeTimers, so the capture is defensive rather than load-bearing today — but it is what keeps the hook working if a later test in this file fakes timers, and it costs nothing.
  • beforeEach is genuinely in scope. It is imported from vitest at line 18 alongside describe, it, expect, afterEach and vi, so the file-level hook cannot fail with a ReferenceError that would take the whole suite down.
  • Hook order is right. The file-level hook at :65 is registered outside any describe, so vitest runs it before the suite's own beforeEach at :201 that creates the base tree and acquires the worktree lease. The yield therefore drains the RPC queued by the previous test before the next one starts blocking on git, which is exactly the "bounds each stall to one test" property the comment claims.

Vitest awaits a promise returned from a hook, so returning new Promise<void> does make the yield real rather than fire-and-forget.

Consistent with an existing repo-wide precedent

The comment cites scripts/tests/test-setup.ts, and that precedent is real and already merged at main (:22-32), describing the same failure mode — "a worker's event loop blocked for the whole file (>60s)" — and applying the same capture-at-load technique:

const realSetTimeout = setTimeout;
beforeEach(() => new Promise((resolve) => realSetTimeout(resolve, 0)));

The only difference is the primitive: setImmediate here versus setTimeout(…, 0) there. Both are captured before tests run and both yield a full loop turn; setImmediate fires in the check phase after I/O polling and is not subject to timer clamping, which is a reasonable choice for a suite whose stalls are caused by child-process I/O. Two independent sites in the repo now describe and treat the same vitest behaviour, which corroborates the diagnosis rather than resting on it.

Isolation impact

Adding a yield between tests does open a window in which a promise left pending by the previous test can settle. That is inherent to the fix and is bounded here: the prior test has already completed its assertions, and the suite's lease is released in afterEach (:382) before the next test's yield runs, so nothing that a passed assertion depended on can be retroactively disturbed. No existing assertion, expectation, fixture or timeout was modified — the diff is purely additive.

Scope and CI

One file, nine lines, no historical review and no review thread on this PR, so there were no prior blocking issues to re-verify. I did not audit the rest of base-tree.test.ts beyond the hook interactions above, since the diff cannot reach it.

Test (ubuntu-latest, Node 22.x), review-pr and triage were all still in progress at the time of review. I did not wait for them; per Critical-only scope a pending check is not a gate. Worth noting for the author that the check which actually demonstrates the fix is the one still running, so the empirical confirmation lands with CI rather than with this review.

Verdict: APPROVE — No Critical found. The hook is correctly scoped and ordered, the primitive is captured before fake timers could intercept it, and the technique matches an already-merged repo-wide precedent for the same vitest event-loop starvation.

@wenshao

wenshao commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: merge-ready — local deep verification, 27/27 scripted assertions passed (0 unexpected failures). Verified head: 021e9222458d116dd59ea5257f0d920bdb352007 (base: 64ac2faae7f7d5114a2cb6c72cf87c10234b7bf3).

中文摘要

结论:可以合并。 本地深度验证,27/27 条脚本断言全部通过。

  • A/B 证明(同一构建树内只换回旧测试文件): 基线 6/6 次复现 CI 失败特征 —— 106 个测试全绿,但报 [vitest-worker]: Timeout calling "onTaskUpdate" 未处理错误并以 1 退出;PR 版本 4/4 次 106 通过、退出 0。两臂墙钟时间相同(约 163 s),与 PR 描述一致。
  • 机制直接证据: 心跳观测显示基线在约 147 s 的测试阶段内事件循环一次也没有转动(心跳从 t=15.4 s 断到 t=162.6 s);PR 版本每个测试之间循环都会转动,最长单测试卡顿 11.7 s(与作者的 12 s 一致),远低于 60 s 的 RPC 期限。合成对照(8×10 s 忙等测试)证明:无让出 → 同样的 RPC 超时;加让出 → 干净通过。
  • fake timers 防护: 加载时捕获的 setImmediate 在前一个测试遗留 fake timers 的情况下仍能让出;未捕获的对照组在 beforeEach 挂起(Hook timed out in 10000ms)—— 捕获确实起作用。
  • CI 佐证: 本 PR 的 Test (ubuntu-latest, Node 22.x) hosted 腿已转绿(21m8s);PR 描述引用的 fork PR run 35897998389 同一条腿确为 failure。
  • 门禁: 该文件 eslint --max-warnings 0、prettier --check、tsc --noEmit(packages/cli)均 exit 0;eslint 门禁活性已用植入重复导入验证。
  • 遗留事项(非阻塞): 单个同步测试若超过 60 s 仍会触发同样问题(作者已声明,与 fix(ci): yield the event loop between script tests to avoid vitest RPC timeouts (#10037) #10050 同限制;本机最慢测试 11.8 s,约 5 倍余量);修复只覆盖这一个文件,cli 级统一让出留给后续讨论。

未覆盖:真实 GitHub hosted runner(以 PR 自身 CI 转绿代替)、macOS/Windows 腿、cli 全量测试(只跑了受影响文件)。

Central claim and A/B

Claim: with the 9-line hunk, the base-tree suite finishes 106-passed and exits 0; without it, the same suite exits 1 with [vitest-worker]: Timeout calling "onTaskUpdate" despite all tests green.

Control construction: one built tree at the PR head; the base arm is the same tree with the test file restored to 64ac2faa's version, so the arms differ by exactly the 9 added lines (no package.json/lockfile change in the PR, so no dependency confound). All runs: packages/cli, CI=true, the cli's own vitest config, Linux arm64, Node 24, 12 cores.

run arm exit tests errors wall
head-suite head 0 106 passed 0 163 s
base-suite base (file swapped) 1 106 passed 1 — Timeout calling "onTaskUpdate" 163 s
head-stall / base-stall head / base + observer 0 / 1 106 passed 0 / 1 — same signature 166 s / 165 s
base-timeline / head-timeline base / head + timeline observer 1 / 0 106 passed 1 / 0 165 s / 166 s
pidprobe-base base + pid observer 1 106 passed 1 — same signature 165 s
threadprobe-base / threadprobe-head base / head 1 / 0 106 passed 1 / 0 164 s / 166 s
heartbeat-base base + heartbeat observer 1 106 passed 1 — same signature 165 s

Base: 6/6 reproductions of the exact CI failure signature. Head: 4/4 clean. Wall time identical within noise, matching the PR's "wall time does not change" claim.

A/B: base fails with the RPC timeout, head passes

Mechanism, measured end-to-end

  • Heartbeat observer (base arm, real file): 5 s heartbeats fire through module collect (t=5.1 s, 10.3 s, 15.4 s), then total silence from t=15.4 s to afterAll at t=162.6 s — a ~147 s window with zero event-loop turns covering the entire test phase. The worker loop is blocked for the whole file, exactly as the PR describes.
  • Same observer (head arm): the loop turns between every test; 107 stall events tagged to individual tests, max 11.7 s (the R3-3 ancestor-walk test), matching the author's "~12 s" and 5× under the 60 s RPC deadline.
  • Synthetic A/B (same vitest 3.2.7 worker RPC, machine-speed independent): 8 × 10 s busy-loop tests, no yield → exit 1 with Timeout calling "onTaskUpdate" (8/8 passed); same file with the PR's yield pattern → exit 0.
  • Silence = blockage, proven: the synthetic control (8 known 10 s blocks, no yield) produced the same zero-stall silence as the base arm — the observer's afterAll clears its timer before the loop can turn after back-to-back sync tests. The base arm's absent stall events are the signature of total blockage, not a dead observer.

Mechanism proof

Synthetic stall A/B

Fake-timer capture is load-bearing

The hunk captures setImmediate at module load "so fake timers cannot intercept it". Probe: test A installs vi.useFakeTimers() and leaves it on.

  • Captured (the PR's pattern): test B's beforeEach yield still fires — 2 passed, exit 0.
  • Uncaptured control: test B's beforeEach waits on the faked setImmediate — Hook timed out in 10000ms, exit 1.

The suite itself uses no fake timers (grep), so the capture is defensive — matching scripts/tests/test-setup.ts from #10050, which does exist and uses the same captured-real-timer beforeEach yield (with setTimeout(0); the "same fix" comment is accurate).

Fake-timer probe

CI corroboration

  • This PR's own Test (ubuntu-latest, Node 22.x) — the hosted leg that was failing on every hosted run — passed (21m8s). Lint & Static also passed.
  • Spot-checked the cited evidence: run 35897998389 (a fork PR) shows Test (ubuntu-latest, Node 22.x) = failure with other legs green — the pre-fix pattern as described.

Gates

gate result
eslint --max-warnings 0 on the changed file exit 0
eslint gate liveness proven: planted duplicate vitest import flagged (import/no-duplicates), then removed
prettier --check on the changed file exit 0
tsc --noEmit (packages/cli) exit 0

Findings

None blocking. Two informational notes, both already disclosed in the PR body:

  1. A single synchronous test blocking ≥60 s would still trip the RPC — the yield bounds stalls to one test, and testTimeout cannot interrupt synchronous code. On this box the slowest test is 11.8 s (5× margin), but the margin is machine-dependent, not inherent. Same documented limit as fix(ci): yield the event loop between script tests to avoid vitest RPC timeouts (#10037) #10050.
  2. Scope is this one file. A cli-wide yield in test-setup.ts would protect future long sync suites; the author deliberately deferred that (it touches all 1083 cli test files). Reasonable sequencing, worth the follow-up discussion.

Not covered

  • A real GitHub-hosted runner (cannot be reproduced locally; the PR's own green hosted leg stands in as the production observation). macOS/Windows legs. The full cli suite — only the affected file was run; the change is scoped to it.

Methodology

Worktree of head 021e92224 (pnpm install via scripts/setup-worktree.js, npm run build exit 0) on Linux arm64, Node 24.14.0. The A/B control swaps only base-tree.test.ts between the head and 64ac2faa versions inside that one tree. Harnesses: sequential CI=true npx vitest run invocations with wall-time/exit-code capture; an observation-only setup-file family (max-stall recorder, test-tagged timeline, pid/worker_threads attribution, 5 s heartbeat) merged into the cli vitest config; a fake-timer probe pair; a synthetic 8×10 s busy-loop suite parameterized by DO_YIELD. Base-arm failures were the expected cells and count as passes (27 pass / 0 fail / 27 total). Full artifact directory with raw logs, observer JSONL and harness sources retained locally.

Without a pin, the yield can be deleted with every test still green; the
only signal would be the hosted-only onTaskUpdate error returning. Add the
two-test witness from #10050, armed with the same captured setImmediate
the yield uses (immediates run FIFO, so the armed flag fires first).
Removing the yield turns `observes the event loop turned between tests`
red while the other 107 tests pass.
@wenshao

wenshao commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the review. Here is how each point is handled.

Triage note 1 / R1-1 (no guard test). Adopted in 1014ad02e1, with the #10050-style witness pair. Removing the yield now turns one named test red on every runner. That is measured in the R1-1 thread.

Triage note 2 (setImmediate vs setTimeout(…, 0)). I kept setImmediate. It runs in the check phase right after the current poll, so it adds no timer-clamp delay. The witness also depends on it: the armed flag and the yield are both immediates, so FIFO order guarantees the flag fires first. A setTimeout(0) yield would not give that guarantee. Both files drain the loop; they just use different phases.

CI.

  • web-shell E2E Smoke was cancelled by the job time limit at test 151/174. The only file this PR touches is a cli unit test, so the cancellation is unrelated to the change, and I re-ran the job.
  • Test (ubuntu-latest, Node 22.x) passed here, but this PR's branch is in-repo, so that job ran on an ECS runner (ecs-qwen-hk4-9), not on a GitHub-hosted one. It proves nothing broke; it does not prove the hosted fix. The hosted-runner evidence is the local A/B in the description: 4/4 runs failed before and 2/2 were clean after, with the longest stall dropping from about 115 s to 12 s. The first fork PR that runs on a hosted runner after merge will confirm it end to end.
中文说明

感谢评审,各点处理如下。

Triage 意见 1 / R1-1(没有守护测试): 已在 1014ad02e1 采纳,用的是 #10050 那种见证测试对。现在删掉 yield,会在所有 runner 上让一条有名字的测试变红。实测结果见 R1-1 线程。

Triage 意见 2(setImmediate 还是 setTimeout(…, 0)): 保留 setImmediate。它在当前 poll 结束后的 check 阶段运行,没有计时器钳制带来的额外延迟。见证测试也依赖这一点:打标记的回调和 yield 都是 immediate,FIFO 顺序保证标记先触发;换成 setTimeout(0) 的 yield 就没有这个保证。两个文件都能把事件循环排空,只是用的阶段不同。

CI:

  • web-shell E2E Smoke 跑到第 151/174 个测试时,因 job 时间上限被取消。本 PR 只改了一个 cli 单元测试文件,所以这次取消与改动无关,我已重跑该 job。
  • Test (ubuntu-latest, Node 22.x) 在这里通过了,但本 PR 是 in-repo 分支,这个 job 跑在 ECS runner(ecs-qwen-hk4-9)上,而不是 GitHub hosted runner。它只能说明没有改坏东西,不能证明 hosted 上的问题已修好。hosted 侧的证据是描述里的本地 A/B:修改前 4/4 次失败,修改后 2/2 次干净通过,最长卡顿从约 115 秒降到 12 秒。合入后第一个跑在 hosted runner 上的 fork PR 会做端到端确认。

🤖 Generated with Claude Code — Claude Opus 5.5 (1M context)

@wenshao

wenshao commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qqqys qqqys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent Critical-only review — head 1014ad02

Re-derived at this head. My earlier approval was recorded at 021e9222 and was auto-dismissed when the branch moved.

What moved

021e9222..1014ad02 is one commit, test(cli): pin the base-tree event-loop yield with a witness pair, adding 16 lines to packages/cli/src/commands/review/base-tree.test.ts and nothing else. No production file changed, so the yield hook I verified at the previous head is untouched — I re-confirmed it is still present and unmodified at :65 rather than assuming so from the commit message.

No historical blocker

The only finding ever filed on this PR is R1-1, a Suggestion at floor:"o", reporting that nothing in the repository pinned the invariant the yield establishes. There is one review thread and it is resolved. No Critical has been recorded in any round, and there is no CHANGES_REQUESTED against this PR.

The new commit is the answer to that Suggestion, so I checked whether it actually pins the invariant or merely looks like it does.

The witness is genuine, not vacuous

let yieldObserved = false;

it('arms a flag from a real macrotask callback', () => {
  realSetImmediate(() => { yieldObserved = true; });
});

it('observes the event loop turned between tests', () => {
  expect(yieldObserved).toBe(true);
});

Three properties make this a real mutation-killer:

  • setImmediate is the right oracle. A setImmediate callback fires only in the check phase of a genuine loop turn. Microtask draining between tests — promise continuations in the runner, which is all that would happen without the hook — cannot reach the check phase, so the flag stays false. The witness therefore fails if the yield is removed, which is exactly the property R1-1 asked to have pinned.
  • It cannot be vacuous in this file specifically. The defect being fixed exists because the loop does not turn on its own here: the suite drives real git through spawnSync/execFileSync and blocks the worker for the whole file. That is the strongest available evidence that nothing else in the runner supplies the turn the witness depends on.
  • Ordering is correct and the scope is clean. Both tests sit at file top level, after the }); that closes describe('runBaseTree') at :3995, so the suite's own beforeEach at :201 — which creates the base tree and acquires the worktree lease through blocking git — does not apply to them. I confirmed the yield at :65 is the only file-level hook in the file: there is no file-level afterEach, beforeAll or afterAll that could turn the loop independently. Immediates run FIFO, so the callback armed during the first test fires before the resolve the second test's beforeEach queues, and the assertion observes true for the right reason.

The tests reuse the same load-captured realSetImmediate the yield itself uses, so the witness stays correct even if a future test in this file installs fake timers — the property that made the original fix robust is preserved in the thing that now pins it.

One consequence worth stating rather than gating on: the pair is order-dependent by construction, so it assumes vitest runs tests in file order. That holds under the default configuration and is inherent to any witness for an inter-test property; a shuffled sequence would be the only thing to break it, and it would break loudly rather than silently passing.

CI

review-pr was queued at this head; no failure attributable to this diff, and CI state is not a gate. The check that empirically demonstrates the original fix — Test (ubuntu-latest, Node 22.x) — was not among the outstanding checks at the time of review.

Verdict: APPROVE — No Critical found. The delta is test-only, no production code moved since the yield was verified, and the added witness genuinely pins the invariant by depending on a check-phase turn that only the yield supplies.

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM at 1014ad02. Test-only change, CI green at this head, no open threads, and qqqys has already approved this head.

@wenshao
wenshao added this pull request to the merge queue Sep 24, 2026
Merged via the queue into main with commit e39d239 Sep 24, 2026
91 of 92 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.

3 participants