Skip to content

fix(cli): stop review test-efficacy tests depending on ambient tmpdir vitest - #8537

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
wenshao:fix/cli-test-efficacy-env-flaky
Aug 4, 2026
Merged

wenshao merged 1 commit into
QwenLM:mainfrom
wenshao:fix/cli-test-efficacy-env-flaky

Conversation

@wenshao

@wenshao wenshao commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Stabilizes two environment-dependent tests in packages/cli/src/commands/review/test-efficacy.test.ts that fail on hosts where vitest resolves up-tree from os.tmpdir(). Both tests keep every assertion verbatim; only the way the failure condition is produced becomes host-deterministic. The findVitestBin helper gains an optional resolver parameter whose default is exactly the previous inline behavior, and one test plants a local shadow package instead of relying on a bare tmpdir having no vitest anywhere above it.

Why it's needed

CI's ubuntu Test job fails with AssertionError: expected [Function] to throw an error in two cases: findVitestBin > names the search root when vitest cannot be resolved and runControlMutant > leaves the probe file byte-identical when it cannot run (PR run 30909672420; the same signature appears on the latest main run 30893612230). Both tests assumed nothing up-tree of os.tmpdir() provides a vitest/package.json — true on macOS dev machines, false on the project's self-hosted runner where a node_modules above TMPDIR supplies one. In that environment the first test never throws and the second actually executes the probe suite instead of throwing.

Reviewer Test Plan

How to verify

  1. Reproduce the CI condition locally: mkdir -p /tmp/qwen-fake-tmp/node_modules && ln -s <repo>/node_modules/vitest /tmp/qwen-fake-tmp/node_modules/vitest, then run cd packages/cli && TMPDIR=/tmp/qwen-fake-tmp npx vitest run src/commands/review/test-efficacy.test.ts. With the old code both tests fail exactly as on CI; with this PR the full file passes (128/128).
  2. Confirm the production call path is untouched: findVitestBin is called without the new parameter at its only production call site, so the default resolver (anchored createRequire) is identical to before.
  3. Confirm no assertion changed: both tests still assert the same toThrow(...) messages and the same byte-identical probe file restore.
  4. cd packages/cli && npm run typecheck plus ESLint/Prettier on the two touched files — all clean locally.

Evidence (Before & After)

Before: both tests fail on the self-hosted ubuntu runner (and locally under the fake TMPDIR harness above). After: full file passes 128/128 in the repro environment and in 4 consecutive runs in the normal environment. Full evidence is in the e2e report comment below.

Tested on

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

Environment (optional)

macOS local runs plus a fake TMPDIR harness that simulates the Linux self-hosted runner's ambient vitest; the real ubuntu runner validates via this PR's own CI run.

Risk & Scope

  • Main risk or tradeoff: findVitestBin gains an optional parameter used only by tests; its default is the previous inline createRequire anchor, so production behavior is unchanged.
  • Not validated / out of scope: other tests in the same file that plant their own packages were already shadow-deterministic and are untouched; the runner's ambient node_modules itself is left as-is.
  • Breaking changes / migration notes: none.

Linked Issues

None.

中文说明

本 PR 做了什么

稳定 packages/cli/src/commands/review/test-efficacy.test.ts 中两个依赖环境的测试——它们在能从 os.tmpdir() 向上解析到 vitest 的主机上会失败。两个测试的断言全部逐字保留,只把失败条件的构造方式改为在任何宿主上都确定。findVitestBin 增加一个可选解析器参数,其默认值与之前的内联行为完全一致;另一个测试改为种植本地 shadow 包,不再依赖裸 tmpdir 上方恰好没有 vitest。

为什么需要

CI 的 ubuntu Test 任务以 AssertionError: expected [Function] to throw an error 失败于两个用例:findVitestBin > names the search root when vitest cannot be resolved 和 runControlMutant > leaves the probe file byte-identical when it cannot run(PR run 30909672420;最新 main run 30893612230 同签名)。两个测试都假设 os.tmpdir() 向上没有任何 vitest/package.json——在 macOS 开发机上成立,在项目自托管 runner 上不成立(TMPDIR 之上的 node_modules 提供了 vitest)。该环境下第一个测试从不抛错,第二个则真实执行了探针套件而非抛错。

Reviewer Test Plan

如何验证

  1. 本地复现 CI 条件:mkdir -p /tmp/qwen-fake-tmp/node_modules && ln -s <repo>/node_modules/vitest /tmp/qwen-fake-tmp/node_modules/vitest,然后 cd packages/cli && TMPDIR=/tmp/qwen-fake-tmp npx vitest run src/commands/review/test-efficacy.test.ts。旧代码下两个测试与 CI 完全一致地失败;本 PR 下整文件通过(128/128)。
  2. 确认生产调用路径未动:findVitestBin 在唯一生产调用点不传新参数,默认解析器(锚定 createRequire)与之前完全相同。
  3. 确认断言未变:两个测试仍断言相同的 toThrow(...) 消息与相同的探针文件字节恢复。
  4. cd packages/cli && npm run typecheck 加两个改动文件的 ESLint/Prettier——本地全部通过。

Evidence (Before & After)

改动前:两个测试在自托管 ubuntu runner 上失败(本地用上述 fake TMPDIR 装置同样复现)。改动后:整文件在复现环境与常规环境 4 轮连续通过(128/128)。完整证据见下方 e2e report 评论。

Tested on

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

Environment (optional)

macOS 本地运行,外加模拟 Linux 自托管 runner 环境 vitest 的 fake TMPDIR 装置;真实 ubuntu runner 由本 PR 自身的 CI 运行验证。

Risk & Scope

  • 主要风险或权衡:findVitestBin 新增仅测试使用的可选参数;默认值即原先内联的 createRequire 锚定,生产行为不变。
  • 未验证 / 不在范围内:同文件中其他自种包的测试本就具备 shadow 确定性,未改动;runner 上的环境 node_modules 维持原样。
  • 破坏性变更 / 迁移说明:无。

Linked Issues

无。

… vitest

Two tests failed on hosts where vitest resolves up-tree from os.tmpdir()
(observed on self-hosted CI where a node_modules above TMPDIR provides
one): findVitestBin's "cannot be resolved" case never threw, and
runControlMutant's "cannot run" case executed the probe for real instead
of throwing.

Make the failure conditions host-deterministic while keeping every
assertion: findVitestBin accepts an injected resolver (default
unchanged) so the MODULE_NOT_FOUND case is forced directly, and the
runControlMutant test plants a shadow vitest whose exports hide
package.json, which wins resolution from any ancestor install and makes
the run fail deterministically.
@wenshao

wenshao commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

E2E Report — deflake review test-efficacy tests

Flaky mechanism

packages/cli/src/commands/review/test-efficacy.test.ts had two environment-dependent tests, both failing on CI with AssertionError: expected [Function] to throw an error (run 30909672420, ubuntu-latest job; same signature as latest main run 30893612230):

  • findVitestBin > names the search root when vitest cannot be resolved — expects findVitestBin(<bare tmpdir>) to throw MODULE_NOT_FOUND.
  • runControlMutant > leaves the probe file byte-identical when it cannot run — expects the vitest run to never start, so the function throws out of the suite helper.

Both assumed vitest/package.json is not resolvable up-tree from os.tmpdir(). On the project's self-hosted runner, a node_modules above TMPDIR provides vitest, so resolution succeeds: test 1 never throws, and test 2 runs the probe for real (no throw). On macOS dev machines (/var/folders/... has nothing above it) both pass — the classic environment-dependent flake.

Which fix was applied and why

Allowed-fix class 4 (isolate/serialize interference): remove the dependence on ambient filesystem state, preserving every assertion verbatim.

  1. findVitestBin now accepts an optional injected resolver (default is the unchanged createRequire(join(worktree, 'noop.js')).resolve). The test injects a resolver that throws MODULE_NOT_FOUND directly. A vi.doMock('node:module') alternative was prototyped and empirically rejected — builtin imports inside user modules are externalized by this repo's vite-node setup and bypass the mock registry (verified with a throwaway probe test).
  2. The runControlMutant test plants a shadow node_modules/vitest inside the probe dir whose exports map hides package.json. The innermost node_modules wins Node resolution on every host, so findVitestBin surfaces ERR_PACKAGE_PATH_NOT_EXPORTED, runControlMutant throws through the same finally restore path, and the probe file stays byte-identical — on machines with and without an ambient vitest alike.

No assertion was weakened, skipped, or deleted; the production call path is byte-for-byte the default.

Repeated-run evidence

A/B reproduction harness on macOS: TMPDIR=/tmp/qwen-fake-tmp with node_modules/vitest symlinked to the repo's real vitest — reproduces the CI condition locally.

  • Old code + repro env: both tests FAIL (matches CI signature exactly).
  • New code + repro env: full file 128/128 pass.
  • New code + normal env: full file 128/128 pass, 4 consecutive runs (3 normal + 1 repro).
  • npm run typecheck (packages/cli), ESLint, Prettier: all clean.

中文说明

失败机制

packages/cli/src/commands/review/test-efficacy.test.ts 有两个依赖环境的测试,在 CI 上以 AssertionError: expected [Function] to throw an error 失败(run 30909672420,ubuntu-latest;与最新 main run 30893612230 同签名):两个用例都假设从 os.tmpdir() 向上解析不到 vitest/package.json。项目自托管 runner 上 TMPDIR 之上的 node_modules 提供了 vitest,解析成功 → 用例 1 不抛错,用例 2 真实执行了探针(也不抛错)。macOS 开发机上 /var/folders/... 上方无 vitest,所以通过——典型的环境依赖型 flaky。

修复方式

断言逐字保留,消除对环境文件系统状态的依赖:

  1. findVitestBin 增加可选注入解析器(默认行为不变),测试直接注入抛 MODULE_NOT_FOUND 的解析器。vi.doMock('node:module') 备选方案经探针实验证明在此仓库 vite-node 配置下无效(builtin 导入被 externalize,绕过 mock 注册表)。
  2. runControlMutant 测试在探针目录内种植一个 exports 不含 package.json 的 shadow vitest 包;最内层 node_modules 在任何宿主上都赢得解析,findVitestBin 抛出 ERR_PACKAGE_PATH_NOT_EXPORTED,runControlMutant 经同一 finally 恢复路径抛错,探针文件保持字节一致。

未弱化、跳过或删除任何断言;生产调用路径与默认值完全一致。

重复运行证据

macOS 本地 A/B 复现:以 TMPDIR=/tmp/qwen-fake-tmp 并在其中 symlink 仓库真实 vitest,复现 CI 条件。旧代码 + 复现环境:两个测试都失败(与 CI 签名完全一致);新代码 + 复现环境:整文件 128/128 通过;新代码 + 常规环境:整文件 128/128 通过,连续 4 轮。packages/cli typecheck、ESLint、Prettier 全部通过。

@wenshao

wenshao commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

Code Review — #8537

Verdict: LGTM, approve with two minor suggestions. I reproduced the CI condition and verified every claim in the PR body independently.

What it does

Two tests in test-efficacy.test.ts produced their failure condition by relying on os.tmpdir() having no vitest/package.json anywhere up-tree. That is true on macOS dev machines and false on the self-hosted ubuntu runner, where a node_modules above TMPDIR resolves one. The PR replaces the ambient dependency with two host-deterministic constructions: a MODULE_NOT_FOUND-throwing resolver injected via a new optional findVitestBin parameter, and a planted shadow vitest whose exports hides package.json.

Verification I ran

Built the fake-TMPDIR harness from the test plan (node_modules/vitest symlink above TMPDIR), then ran the file in a clean worktree of the PR head:

Case Result
Pre-PR code, fake TMPDIR ✗ 2 failed / 126 passed — AssertionError: expected [Function] to throw an error on exactly the two named tests. Matches the CI signature verbatim.
PR code, fake TMPDIR ✓ 128/128
PR code, clean TMPDIR ✓ 128/128
tsc --noEmit + eslint on both touched files clean (no diagnostics mentioning test-efficacy)

Mutation check — the reworked test still has teeth. I broke the production invariant it guards, moving the restore in runControlMutant out of finally onto the success path only:

const __v = perFile.some((r) => r.verdict === 'gated');
writeFileSync(abs, original, 'utf8');
return __v;
} finally { /* no longer survives the throw path */ }

→ × leaves the probe file byte-identical when it cannot run. The shadow-package rewrite did not weaken the assertion's ability to catch a restore that doesn't survive the throw. This was my main concern with the change and it is settled.

Production path is untouched. findVitestBin has exactly one production call site (test-efficacy.ts:1477), called with one argument, so the default resolver runs. createRequire(...).resolve extracted unbound is safe — Node's resolve closes over the module rather than using this; confirmed it still throws MODULE_NOT_FOUND correctly when detached. The default is also still exercised for real by the two sibling findVitestBin tests (declares no bin, hides its package.json), so the anchor itself remains covered.

Convention fit. The optional-parameter seam matches what this file already does — runControlMutant(…, now = Date.now), runOneMutant(…, dependencyRoot = probeTree). Consistent, not a new pattern.

Side benefit worth noting: the reworked test 2 now throws before any spawn, dropping from ~604ms (it was actually executing a real vitest run under the ambient-vitest condition) to ~10ms.


Suggestions

1. Pin test 2's assertion to the error the comment promises (test-efficacy.test.ts:349)

The comment states the point precisely — "findVitestBin surfaces the ERR_PACKAGE_PATH_NOT_EXPORTED" — but the assertion is a bare toThrow(). Since the entire change is about which throw the shadow package produces, a bare match doesn't hold that. Anything that throws earlier in a later refactor (probe-tree validation, a spawn failure, even a typo in the planted JSON) keeps this green while silently no longer testing the described path.

-      expect(() => runControlMutant(dir, 'a.test.ts')).toThrow();
+      expect(() => runControlMutant(dir, 'a.test.ts')).toThrow(
+        /not defined by "exports"/,
+      );

I applied this and it passes — the throw genuinely is that one. It also matches the sibling keeps the real error when vitest is present but hides its package.json test, which already asserts that exact regex.

2. mkdtempSync in test 1 is now dead weight (test-efficacy.test.ts:185)

With the resolver injected, worktree is only interpolated into the expected message — the directory is never touched. I confirmed the test passes with a path that does not exist at all. The real mkdtempSync now just leaks an unremoved no-vitest-* dir per run (verified against a scratch TMPDIR). Leaking is a pre-existing pattern in this file, but here it's newly pointless:

-    const worktree = mkdtempSync(join(tmpdir(), 'no-vitest-'));
+    // Never touched on disk — the injected resolver decides the outcome and
+    // the path only has to appear in the message.
+    const worktree = join(tmpdir(), 'no-vitest-unused');

(Alternatively keep mkdtempSync and wrap in try/finally { rmSync } like the runControlMutant tests do — but the dir buys nothing here.)

Note, not a defect

After this change nothing verifies the link "a genuine require.resolve failure carries code === 'MODULE_NOT_FOUND', therefore this branch fires." The branch body is covered via injection; its real-world trigger is now assumed. That is unavoidable — you cannot force a genuine not-found on a host with ambient vitest, and MODULE_NOT_FOUND is a stable Node contract. Worth one sentence in the findVitestBin JSDoc (which still describes only the createRequire anchor) so the next reader knows the second parameter is a test-only seam and why it exists.

Risk / security / performance

No production behavior change, no new dependency, no I/O outside per-test temp dirs, and the planted shadow package lives inside a mkdtemp tree removed in finally. Net performance is a small improvement. Nothing to flag.

@wenshao

wenshao commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

@doudouOUC doudouOUC 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. The default findVitestBin behavior remains unchanged, and the tests now exercise the intended error paths without relying on ambient TMPDIR node_modules layout.

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Aug 4, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@wenshao
wenshao added this pull request to the merge queue Aug 4, 2026
Merged via the queue into QwenLM:main with commit eacc85e Aug 4, 2026
59 of 60 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants