Skip to content

fix(cli): quiesce the fire-and-forget serve handler across tests - #11417

Merged
wenshao merged 12 commits into
mainfrom
autofix/issue-11414
Sep 11, 2026
Merged

wenshao merged 12 commits into
mainfrom
autofix/issue-11414

Conversation

@qwen-code-dev-bot

@qwen-code-dev-bot qwen-code-dev-bot commented Sep 9, 2026 •

Copy link
Copy Markdown
Collaborator

What this PR does

Makes one more serve unit test wait for its fire-and-forget handler to settle before the test returns. The test applies authenticated open before the yargs path starts the daemon starts the handler with --open-with-auth and used to return as soon as the daemon mock had been called, while the handler still had its authenticated browser-open step ahead of it; it now also waits until that step has called the mocked browser launcher. This is the same quiescence pattern the neighbouring Local Control tests already use. Test-only; no production code changes.

Note: an earlier revision of this PR also added the equivalent wait to the Local Control pairing test (forwards --token and --allow-origin …). #11362 landed that identical change on main first, so it is no longer part of this diff. The browser-open wait described here is what this PR still contributes; it is not a no-op now that #11362 has landed.

Why it's needed

Without the wait, the handler's browser-open call can land in the next test, prints the authenticated manual URL on the yargs headless path, whose final assertion is that the browser was never opened — so that test would fail for a reason that has nothing to do with its own behavior.

On the file as it stands this cannot happen yet: the whole chain from the daemon mock to the browser open completes within a single microtask drain, before vi.waitFor's timer-based poll can observe the daemon call, so CPU load alone does not trigger it. It becomes a real cross-test leak as soon as that window gains an asynchronous step of roughly one poll interval (≥ ~50 ms) — for example a production await before the browser open, or a mock whose runtimeReady is genuinely asynchronous. This PR closes that latent hazard in the same way #11362 closed the equivalent one in the pairing test.

Unlike the pairing-phase leak, this one cannot cause the unattributed whole-run failure (an unhandled process.exit(1)): every failure of the browser launch is caught inside the handler, so its worst case is a named failure of the next test.

Reviewer Test Plan

How to verify

The hazard only fires with an asynchronous stall in that window, so verify with a forced race window applied identically with and without this PR, then run npx vitest run src/commands/serve.test.ts in packages/cli:

  1. In applies authenticated open before the yargs path starts the daemon, make the mocked runtimeReady resolve after 100 ms instead of Promise.resolve().
  2. In prints the authenticated manual URL on the yargs headless path, make its runtimeReady resolve after 400 ms.
  3. Without this PR the run exits 1: the second test fails with AssertionError: expected "spy" to not be called at all, but actually been called 1 times. With this PR all 70 tests pass.

Also confirmed under the same window: waiting on an earlier side effect instead (mockApplyOpenWithAuth or mockShouldLaunchBrowser) still fails, so the chosen wait target is the one that matters. With the file unmodified, both arms pass 70/70 idle, with every CPU core saturated, and under --sequence.shuffle (seeds 1–8), so the new wait does not hang when test order changes.

Evidence (Before & After)

N/A (test-only change, no user-visible behavior). Maintainer verification with forced-race A/B, mutants and reachability probes: #11417 (comment) (Linux) and #11417 (comment) (macOS).

Tested on

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

Environment (optional)

N/A — unit tests only (Linux x64 with Node 22.22.2 and macOS arm64 with Node 24.18.1, vitest 3.2.7).

Risk & Scope

  • Main risk or tradeoff: minimal — on this path the awaited browser-launch call always happens, including when test order is shuffled, and vi.waitFor bounds the wait at 1 s even if a future change breaks that. With this change every fire-and-forget test in the file that still has work after its anchor is quiesced.
  • Not validated / out of scope: this hazard has not been observed in CI and cannot fire on the current file without a change in that window; Windows was not run locally; moving the quiescence into the shared startServeHandlerWithArgs helper, which would prevent the next hand-missed wait, is left as a follow-up.
  • Breaking changes / migration notes: none.

Linked Issues

Related to #11346 (same cross-test leak class; the pairing-test wait for it landed in #11362). #11414 is not addressed by this PR: that run failed on an unrelated packages/web-shell test, which #11406 resolved.

中文说明

本 PR 做什么

让又一个 serve 单元测试在返回前等待它的 fire-and-forget handler 稳定下来。测试 applies authenticated open before the yargs path starts the daemon 以 --open-with-auth 启动 handler,原先在 daemon mock 被调用后就立即返回,而 handler 后面还有带认证的浏览器打开步骤没执行;现在它还会等到这一步调用了被 mock 的浏览器启动函数。这与相邻 Local Control 测试已经采用的静默模式相同。仅测试改动,不涉及生产代码。

说明:本 PR 的早期版本还给 Local Control 配对测试(forwards --token and --allow-origin …)加了同样的等待。#11362 先把完全相同的改动合入了 main,所以它已不在本 diff 中。这里描述的浏览器打开等待才是本 PR 仍然贡献的内容;#11362 合入后,本 PR 并不是空改动。

为什么需要

没有这处等待时,handler 的浏览器打开调用可能落到下一个测试 prints the authenticated manual URL on the yargs headless path 里,而该测试最后一个断言正是"浏览器从未被打开"——于是它会因为与自身行为无关的原因失败。

以当前文件的状况,这还不会发生:从 daemon mock 到浏览器打开的整条链在同一次微任务清空内就跑完了,早于 vi.waitFor 基于定时器的轮询观察到 daemon 调用,因此单靠 CPU 负载不会触发。一旦这个窗口里多出一个大约一个轮询周期(≥ 约 50 ms)的异步步骤,它就会变成真实的跨测试泄漏——例如生产代码在打开浏览器前多一个 await,或者 mock 的 runtimeReady 变成真正异步。本 PR 以 #11362 关闭配对测试中同类隐患的相同方式,关闭这个潜在隐患。

与配对阶段的泄漏不同,这一处不会造成无归属的整轮失败(未处理的 process.exit(1)):浏览器启动的任何失败都在 handler 内部被捕获,所以它最坏的结果是下一个测试出现一次有名字的失败。

审查者测试计划

如何验证

该隐患只有在那个窗口里出现异步停顿时才会触发,因此请用强制竞态窗口验证:在有无本 PR 的两种情况下等同地施加,然后在 packages/cli 下运行 npx vitest run src/commands/serve.test.ts:

  1. 在 applies authenticated open before the yargs path starts the daemon 中,把 mock 的 runtimeReady 从 Promise.resolve() 改为 100 ms 后解析。
  2. 在 prints the authenticated manual URL on the yargs headless path 中,让它的 runtimeReady 400 ms 后解析。
  3. 没有本 PR 时运行以退出码 1 结束:第二个测试失败,报 AssertionError: expected "spy" to not be called at all, but actually been called 1 times。有本 PR 时 70 个测试全部通过。

在同一窗口下还确认了:改为等待更早的副作用(mockApplyOpenWithAuth 或 mockShouldLaunchBrowser)仍然失败,说明所选的等待目标才是关键。文件不做改动时,两臂在空闲、所有 CPU 核心占满、以及 --sequence.shuffle(种子 1–8)下都是 70/70 通过,因此新等待在测试顺序变化时不会挂死。

前后对比证据

N/A(仅测试改动,无用户可见行为)。维护者验证(含强制竞态 A/B、变异体与可达性探针):#11417 (comment) (Linux)与 #11417 (comment) (macOS)。

已测试平台

操作系统 状态
🍏 macOS ✅ 已测试
🪟 Windows ⚠️ 未测试
🐧 Linux ✅ 已测试

环境(可选)

N/A——仅单元测试(Linux x64 + Node 22.22.2,以及 macOS arm64 + Node 24.18.1,vitest 3.2.7)。

风险与范围

  • 主要风险或取舍:很小——在这条路径上,被等待的浏览器启动调用必然发生,打乱测试顺序时也是如此;即使将来有改动破坏了这一点,vi.waitFor 也会把等待限制在 1 秒内。本改动之后,文件中所有在锚点之后仍有后续工作的 fire-and-forget 测试都已静默。
  • 未验证 / 超出范围:该隐患尚未在 CI 中观测到,且在当前文件上不改动那个窗口就不会触发;Windows 未在本地运行;把静默逻辑收进共享的 startServeHandlerWithArgs helper、从而防止下一次漏加等待,留作后续工作。
  • 破坏性变更 / 迁移说明:无。

关联 Issue

与 #11346 相关(同一类跨测试泄漏;针对它的配对测试等待已通过 #11362 合入)。本 PR 不处理 #11414:那次运行失败的是 packages/web-shell 中一个无关的测试,已由 #11406 解决。

)

The 'forwards --token and --allow-origin' serve test returned as soon as
runQwenServe had been called while its handler kept running in the
background. Under load the handler's Local Control pairing phase was
still in flight when the next test installed its one-shot throwing QR
mock: the leaked handler consumed it, failed into the serve catch, and
hit the mocked process.exit as an unhandled rejection, while the victim
test's own handler got the default mock and parked in blockForever until
its timeout. On Linux an unhandled rejection fails the whole vitest run
without attributing a failure line to any test — the signature behind
the per-commit main CI failure #11414 (the same mechanism diagnosed on
 #11346 and #11363, whose fixes never landed on main).

Wait out the pairing phase before the test returns, the same quiescence
the neighbouring Local Control tests already perform. Reproduced with a
forced race window (a delayed enable): unfixed, the run dies with an
unhandled 'process.exit(1) called' rejection; fixed, the file is green.

Fixes #11414
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

Autofix E2E Report — Issue #11414

What failed

Main-branch CI run 34289483217 on 422929b failed the Test (ubuntu-latest, Node 22.x) job in step Run tests and generate reports. The failure was filed as a per-commit issue, which the failure-signature tooling (main-ci-failure-signature.mjs) uses only when the failed run's logs contain zero FAIL <test> lines — i.e. the vitest run died without attributing a failure to any test file.

Root cause

packages/cli/src/commands/serve.test.ts — the test forwards --token and --allow-origin through to runQwenServe with --local-control returns as soon as runQwenServe has been called, while the handler it launched (void handler(argv)) keeps running in the background. The handler's Local Control pairing phase (await import('qrcode-terminal') → qrcode.generate(...)) is then still in flight when the next test, closes the daemon when pairing output fails, installs its one-shot throwing QR mock and its throwing process.exit spy. When the leaked handler resumes it consumes that one-shot mock, fails into the serve catch (serve.ts:982), and calls the mocked process.exit(1), which throws — on a promise nobody awaits. On Linux, dangerouslyIgnoreUnhandledErrors is false (packages/cli/vitest.config.ts), so the unhandled rejection fails the entire vitest run with no FAIL line for any test — exactly the per-commit signature this issue tracks. The victim test's own handler meanwhile gets the default mock and parks in blockForever.

The same mechanism was diagnosed twice before for the same step and the same per-commit signature — issues #11346 and #11363 — but main never received the fix, so the hazard was still live at 422929b and at current HEAD. Both earlier fixes are still open as PRs #11362 and #11376 and carry the identical three-line quiescence; whichever PR lands first resolves the hazard and turns the others into no-ops, and this PR keeps the fix tracked against #11414. Two neighbouring --local-control tests in the same file already perform this quiescence; this one was missed.

Reproduction (forced race window)

The wild race is load-dependent (the leak must survive the inter-test boundary), so I forced the window open deterministically in a temporary probe: delay the first test's enable() by 100 ms and the victim test's enable() by 1000 ms. Both probe edits were reverted afterwards; they are not part of the fix.

  • Unfixed code + forced window: npx vitest run src/commands/serve.test.ts exits 1 with Unhandled Rejection: process.exit(1) called (stack: handler src/commands/serve.ts:982 via processTicksAndRejections) and the victim test parking until its 15 s timeout — the diagnosed mechanism, reproduced end to end.
  • Fixed code + same forced window: all 70 tests pass, no unhandled errors.

Fix

One change in packages/cli/src/commands/serve.test.ts: after startServeHandlerWithArgs(...) in the forwards --token and --allow-origin test, wait out the fire-and-forget handler's pairing phase before the test returns — await vi.waitFor(() => expect(mockQr.generate).toHaveBeenCalled()); — the same quiescence the neighbouring Local Control tests already perform. With the test unable to return before its own handler consumes the QR call, no work leaks into the next test and there is nothing left to consume its one-shot mock. 3 lines, test-only, no production code touched.

Verification

  • Forced-race mutation probe, unfixed: vitest run src/commands/serve.test.ts → exit 1, Unhandled Rejection: process.exit(1) called (reproduces the defect).
  • Forced-race mutation probe, fixed (same delays): → 70/70 passed, no unhandled errors (the fix is what turns the red run green; probe delays then reverted).
  • cd packages/cli && npx vitest run src/commands/serve.test.ts (final tree) → 70/70 passed.
  • npm run build → passed.
  • npm run typecheck → passed.
  • npm run lint → passed.
  • Regression check of the blamed commit's own suites: npx vitest run src/commands/review/lib/worktree.test.ts src/commands/review/test-efficacy.test.ts → 223/223 passed; npx vitest run src/commands/review/test-efficacy.integration.test.ts → 37/37 passed.
  • Exploratory full-suite run (packages/cli, CI-like env with QWEN_CI_COVERAGE=1): all 1006 test files passed, but the run then died in the v8 coverage merge with ENOENT: coverage/.tmp/coverage-440.json (vitest "Unhandled Error", exit 1) — observed once in three runs and not root-caused here. It shares the same no-FAIL-line signature and is not addressed by this PR; flagging it as a separate suspected flake for maintainers. (Full-suite runs in this sandbox also show unrelated environment artifacts — e.g. Footer snapshot diffs from ambient SANDBOX env vars — which do not occur in CI.)

Integration tests were not run: the change is a unit-test-only quiescence wait and exercises no bundled-CLI or integration-harness behaviour. CI remains the final verification gate.

中文说明

故障现象

主分支 CI 运行 34289483217(提交 422929b)在 Run tests and generate reports 步骤中失败了 Test (ubuntu-latest, Node 22.x) 任务。该故障被记录为按提交追踪的 issue——失败签名工具(main-ci-failure-signature.mjs)只有在失败运行的日志中没有任何 FAIL <test> 行时才使用这种形式,即 vitest 运行在未能将失败归属到任何测试文件的情况下崩溃。

根因

packages/cli/src/commands/serve.test.ts 中,测试 forwards --token and --allow-origin through to runQwenServe with --local-control 在 runQwenServe 刚被调用时就返回了,而它启动的 handler(void handler(argv))仍在后台继续运行。当下一个测试 closes the daemon when pairing output fails 安装一次性抛出异常的 QR mock 和抛出异常的 process.exit spy 时,该 handler 的 Local Control 配对阶段(await import('qrcode-terminal') → qrcode.generate(...))仍在进行中。泄漏的 handler 恢复执行后消费了那个一次性 mock,进入 serve 的 catch 分支(serve.ts:982),调用了被 mock 的 process.exit(1) 并抛出异常——而这发生在一个无人 await 的 promise 上。在 Linux 上 dangerouslyIgnoreUnhandledErrors 为 false(见 packages/cli/vitest.config.ts),因此未处理的 rejection 会使整个 vitest 运行失败,且任何测试文件都没有 FAIL 行——这正是本 issue 所追踪的按提交签名。与此同时,受害测试自己的 handler 拿到的是默认 mock,并停滞在 blockForever 中。

同一机制此前已被诊断过两次,同样是该步骤、同样是按提交签名——issue #11346 和 #11363——但 main 从未合入该修复,因此该隐患在 422929b 和当前 HEAD 上仍然存在。此前的两个修复仍以 PR #11362 和 #11376 的形式处于 open 状态,且携带完全相同的三行静默改动;任意一个先合入即可消除该隐患,其余的随即成为空改动,本 PR 则让该修复同时被 #11414 追踪。同一文件中相邻的两个 --local-control 测试已经做了同样的静默等待,唯独这个被遗漏了。

复现(强制竞态窗口)

野外竞态依赖负载(泄漏必须跨越测试边界存活),因此我用一个临时探针确定性地撑开了窗口:把第一个测试的 enable() 延迟 100 ms,受害测试的 enable() 延迟 1000 ms。两处探针改动事后均已还原,不属于修复内容。

  • 未修复代码 + 强制窗口: npx vitest run src/commands/serve.test.ts 以退出码 1 失败,报 Unhandled Rejection: process.exit(1) called(调用栈:handler src/commands/serve.ts:982,经由 processTicksAndRejections),受害测试停滞直至 15 秒超时——完整复现了所诊断的机制。
  • 修复后代码 + 相同强制窗口: 70 个测试全部通过,无未处理错误。

修复内容

仅改动 packages/cli/src/commands/serve.test.ts:在 forwards --token and --allow-origin 测试的 startServeHandlerWithArgs(...) 之后,等待后台 handler 的配对阶段结束再让测试返回——await vi.waitFor(() => expect(mockQr.generate).toHaveBeenCalled());——与相邻 Local Control 测试已有的静默等待完全一致。测试在其自身 handler 消费 QR 调用之前无法返回,因此不会有工作泄漏到下一个测试中,也就没有东西去消费它的一次性 mock。共 3 行,仅测试代码,未触碰生产代码。

验证

  • 强制竞态变异探针,未修复:vitest run src/commands/serve.test.ts → 退出码 1,Unhandled Rejection: process.exit(1) called(复现缺陷)。
  • 强制竞态变异探针,修复后(相同延迟):→ 70/70 通过,无未处理错误(修复是让红变绿的唯一变量;随后还原探针延迟)。
  • cd packages/cli && npx vitest run src/commands/serve.test.ts(最终代码树)→ 70/70 通过。
  • npm run build → 通过。
  • npm run typecheck → 通过。
  • npm run lint → 通过。
  • 对被指提交自身测试套件的回归检查:npx vitest run src/commands/review/lib/worktree.test.ts src/commands/review/test-efficacy.test.ts → 223/223 通过;npx vitest run src/commands/review/test-efficacy.integration.test.ts → 37/37 通过。
  • 探索性全量运行(packages/cli,带 QWEN_CI_COVERAGE=1 的类 CI 环境):全部 1006 个测试文件通过,但运行随后在 v8 覆盖率合并阶段死于 ENOENT: coverage/.tmp/coverage-440.json(vitest "Unhandled Error",退出码 1)——三次运行中观察到一次,本 PR 未对其定位根因。它与本故障共享"无 FAIL 行"的签名,不在本 PR 处理范围内,在此标记为另一个疑似 flake 供维护者参考。(本沙箱中的全量运行还会出现与环境相关的无关失败——例如环境变量 SANDBOX 导致的 Footer 快照差异——这些在 CI 中不会发生。)

未运行集成测试:本次改动仅为单元测试的静默等待,不涉及任何打包 CLI 或集成测试框架的行为。CI 仍是最终验证关卡。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.23.1

@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Sep 9, 2026
@wenshao

wenshao commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Local verification report — PR #11417

I built a real two-arm environment for this PR, ran it, and then went back to the CI evidence behind the linked issue.

Short version: the three added lines are correct and measurably buy something — I reproduced the leak they close, twice, including once with no source mutation at all. But the issue they are attributed to has a different, deterministic root cause that is already fixed on main. My recommendation is to merge the change with a corrected description, not to merge it as the fix for #11414.

Environment

Host Linux 6.12.63 x86_64, 16 cores; Node v22.22.2, npm 10.9.7, vitest 3.2.7 (the lockfile version)
BASE arm origin/main @ 3a75f37ef5
HEAD arm 3a75f37ef5 + a2420452fd cherry-picked → f3282fcf38
Isolation two separate git worktrees, each with its own node_modules and its own built dist/ for acp-bridge, web-shell, web-templates and all nine channel packages

packages/cli/src/commands/serve.test.ts and packages/cli/src/commands/serve.ts are byte-identical between this PR's merge base d670d47efe and current main, so the two arms differ only by the PR's three lines.


1. What the change buys — measured

1.1 Forced-race A/B

I applied the same mutation to both arms (Local Control enable() resolves after 100 ms in forwards --token and --allow-origin …, after 1000 ms in closes the daemon when pairing output fails) and ran npx vitest run src/commands/serve.test.ts from packages/cli:

arm exit result
BASE 1 closes the daemon when pairing output fails parks in blockForever and fails on the 15 s test timeout; plus Unhandled Rejection: Error: process.exit(1) called, stack through handler src/commands/serve.ts:982
HEAD 0 70 passed, no unhandled errors

Same outcome under CI's --retry=2: BASE fails all three attempts, HEAD passes.

forced-race A/B

1.2 It also reproduces with no source mutation at all

Unmodified tree in both arms, under CPU contention:

profile BASE HEAD
30 runs/arm, taskset -c 0,1 + 6 busy-loop competitors 29 passed, 1 failed (iteration 9) 30 passed, 0 failed
40 runs/arm, one contended core each + 2 competitors 40 passed, 0 failed 40 passed, 0 failed

One natural hit in 70 BASE runs, zero in 70 HEAD runs. Rare — but the failing run is the forced-race signature exactly: victim-test timeout and Unhandled Rejection: process.exit(1) called. So the hazard is live in main today, not merely theoretical, and this PR closes it.

natural race and PR CI

1.3 Baseline and static gates

  • Without contention both arms are 70/70.
  • eslint --max-warnings 0 on the changed file → clean. prettier --check → clean.
  • Differential tsc -p packages/cli --noEmit on both arms → identical error sets (2023 output lines each: 1442 in src/ui/opentui/* from an @opentui typing my local dependency farm lacks, one @qwen-code/channel-dws TS2307, two TS6305 for an unbuilt audio-capture — all environment artifacts). None names serve.test.ts.

1.4 The added wait cannot hang

On this path startLocalControl always reaches qrcode.generate — the mocked handle has webShellMounted: true and getLocalControl().enable() resolves with a url — so the awaited call is unconditional, and vi.waitFor bounds it anyway. The two neighbouring Local Control tests already carry the identical line.


2. Where the PR's reasoning does not hold

2.1 Run 34289483217 did attribute its failure

The archived job log (job 102272630674) is downloadable now, and it contains six FAIL lines — all App.test.tsx > App session callbacks > does not rerender App for other split sessions, from ReferenceError: mockUseDaemonActivePromptBridge is not defined at App.test.tsx:28940. That undefined symbol was still present at 422929b3a7. The string process.exit(1) called appears zero times in the whole log.

Running the repo's own extractor over that log names both tests:

$ node .github/scripts/ci/main-failure-signature.mjs analyze \
    --workflow "Qwen Code CI" --jobs failed-jobs.tsv ci-job.log | jq -r '.tests[].id'
App.test.tsx > App session callbacks > does not rerender App for other split sessions (outer pending: false)
App.test.tsx > App session callbacks > does not rerender App for other split sessions (outer pending: true)

The per-commit fallback came from a log-download race, not from an unattributable failure. The issue-filer job (102277172988) logged this 36 s after the run ended:

Failed jobs: 1
##[warning]Could not download the log of job 102272630674
Failing tests identified: 0

That web-shell ReferenceError was fixed by #11406, merged as 3a75f37ef5 — which is current main HEAD. So #11414 is already fixed on main, by a different change, and Fixes #11414 in this body would auto-close it against the wrong commit.

what actually failed

2.2 None of the four cited runs is this leak

issue run what actually failed
#11346 34197708856 all 1017 cli test files passed; the run died on Error: [vitest-worker]: Timeout calling "onTaskUpdate" at load ≈ 160
#11363 34207166214 src/serve/capabilities-docs-contract.test.ts > … keeps the daemon index capability counts in sync (deterministic; that test passes on today's tree)
#11404 34257817936 the same App.test.tsx ReferenceError
#11414 34289483217 the same App.test.tsx ReferenceError

process.exit(1) called appears zero times in all four job logs.

All four issue-filer runs logged the same ##[warning]Could not download the log of job … followed by Failing tests identified: 0 (jobs 102277172988, 102182712398, and the two at filer runs 34201070808 / 34209871251). For three of the four the archived log does name the failing test; #11346 is the one case where the run genuinely printed no FAIL line, because nothing failed as a test.

2.3 This leak cannot produce a "no FAIL line" run

When it fires, the victim's handler never settles (it reaches blockForever), so the test fails on the 15 s test timeout and vitest always prints a FAIL line naming it — that happened in every reproduction I ran, with and without --retry=2. So "the logs contained no FAIL lines ⇒ it was this leak" does not hold in either direction, and the PR's causal chain needs replacing with the measured argument in §1.


3. Non-blocking observations

N1 — this PR's own CI Test job fails on the real cause. Run 34296692250, job 102294835954: the same two App.test.tsx failures. The branch forked at d670d47efe, which predates #11406. A rebase onto current main should turn it green.

N2 — a second handler of the same class is still un-quiesced. applies authenticated open before the yargs path starts the daemon returns while its handler is still heading into maybeOpenWebShellBrowser, and the very next test asserts expect(mockOpenBrowserSecurely).not.toHaveBeenCalled(). Delaying only that handler's runtimeReady by 50 ms (and the next test's by 200 ms) makes both arms fail identically with expected "spy" to not be called at all, but actually been called 1 times. A per-site wait would work, but the structural fix is probably to have startServeHandlerWithArgs retain the handler promise and settle it in afterEach, so no future test has to remember.

second un-quiesced handler

N3 — the guard's budget is vi.waitFor's 1 s default. Widening the injected pairing delay on the HEAD arm: 500 ms and 900 ms pass; at 1100 ms the wait itself fails (expected "spy" to be called at least once) and the original leak comes back. Not a regression — the two neighbouring tests carry the same 1 s budget, and what is being awaited is microtask work on mocked modules — but an explicit { timeout: 5000 } would remove the cliff.

N4 — three open PRs now carry a byte-identical hunk. #11362, #11376 and this one. #11376's capability-doc-count hunk is no longer in its diff (that contract test passes on today's tree), so all three are equivalent: land one, close the other two as duplicates.


Recommendation

Merge the code — the leak is real and this closes it. Before merging, please:

  1. rebase onto current main (fixes the red Test job, N1);
  2. change Fixes #11414 to a plain reference, and replace the "no FAIL line" argument with the measured one — the leak reproduces without any mutation, it is simply not what Main CI failed: Qwen Code CI on 422929b3a7df #11414 / Main CI failed: Qwen Code CI on 0d1e0fbfa6f3 #11346 / Main CI failed: Qwen Code CI on cffc40495a34 #11363 / Main CI failed: Qwen Code CI on 70cf3633950b #11404 recorded;
  3. close Main CI failed: Qwen Code CI on 422929b3a7df #11414 pointing at 3a75f37ef5, and close fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362 / fix(cli): quiesce serve test leak and sync capability doc count (#11363) #11376 as duplicates of whichever lands. Main CI failed: Qwen Code CI on 0d1e0fbfa6f3 #11346 is a different animal — its run died on the vitest-worker RPC timeout under load, so neither this hunk nor fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362's is its fix.

Separately worth a look by whoever owns main-ci-failure-issue.yml: the job-log fetch loses the race with log archival — it did so on all four of these runs — and silently degrades a fully attributable failure into a per-commit issue. Four issues and at least three autofix PRs came out of that one warning line. A short retry/backoff around the gh api …/logs call would have prevented most of them.

How to reproduce

# two arms
git worktree add --detach base origin/main
git worktree add --detach head origin/main && (cd head && git cherry-pick a2420452fd)

# forced race, applied identically to both arms:
#   test 'forwards --token and --allow-origin …'      enable() -> resolves after  100 ms
#   test 'closes the daemon when pairing output fails' enable() -> resolves after 1000 ms
(cd <arm>/packages/cli && npx vitest run src/commands/serve.test.ts)
# BASE -> exit 1, 15 s timeout + Unhandled Rejection: process.exit(1) called
# HEAD -> exit 0, 70 passed

# natural race, no mutation
taskset -c 0,1 npx vitest run src/commands/serve.test.ts   # with 6 spinners pinned to cores 0,1

# the CI evidence
gh api repos/QwenLM/qwen-code/actions/jobs/102272630674/logs | grep -c 'process.exit(1) called'   # 0
gh api repos/QwenLM/qwen-code/actions/jobs/102272630674/logs > ci-job.log
printf 'Test (ubuntu-latest, Node 22.x)\tRun tests and generate reports\n' > failed-jobs.tsv
node .github/scripts/ci/main-failure-signature.mjs analyze \
  --workflow "Qwen Code CI" --jobs failed-jobs.tsv ci-job.log | jq -r '.tests[].id'
中文说明

PR #11417 本地验证报告

我为这个 PR 搭了一套真实的双臂环境跑了一遍,然后回头核对了关联 issue 背后的 CI 证据。

结论先说:这三行改动是对的,也确实买到了东西——我两次复现了它所修补的泄漏,其中一次完全没有改动源码。但它所归属的那个 issue,真实根因是另一回事,而且已经在 main 上修好了。 我的建议是:改正描述后合入,而不是把它当作 #11414 的修复合入。

环境

主机 Linux 6.12.63 x86_64,16 核;Node v22.22.2、npm 10.9.7、vitest 3.2.7(锁文件版本)
BASE 臂 origin/main @ 3a75f37ef5
HEAD 臂 3a75f37ef5 + cherry-pick a2420452fd → f3282fcf38
隔离 两个独立 git worktree,各自的 node_modules,各自构建 acp-bridge、web-shell、web-templates 与全部九个 channel 包的 dist/

packages/cli/src/commands/serve.test.ts 与 packages/cli/src/commands/serve.ts 在本 PR 的 merge base d670d47efe 与当前 main 之间逐字节一致,因此两臂只差 PR 的这三行。


1. 这个改动买到了什么——实测

1.1 强制竞态 A/B

我把同一份变异同时打到两臂(forwards --token and --allow-origin … 的 Local Control enable() 延迟 100 ms 解析,closes the daemon when pairing output fails 延迟 1000 ms),在 packages/cli 下跑 npx vitest run src/commands/serve.test.ts:

臂 退出码 结果
BASE 1 closes the daemon when pairing output fails 停在 blockForever,15 s 测试超时失败;另有 Unhandled Rejection: Error: process.exit(1) called,栈经过 handler src/commands/serve.ts:982
HEAD 0 70 通过,无未处理错误

加上 CI 的 --retry=2 结论相同:BASE 三次尝试全失败,HEAD 通过。

1.2 不做任何源码变异也能复现

两臂均为未改动的代码树,在 CPU 争用下:

争用配置 BASE HEAD
每臂 30 次,taskset -c 0,1 + 同核 6 个忙等竞争者 29 通过,1 失败(第 9 次) 30 通过,0 失败
每臂 40 次,各占一个被争用的核 + 2 个竞争者 40 通过,0 失败 40 通过,0 失败

BASE 70 次中自然命中 1 次,HEAD 70 次中 0 次。概率低——但失败那次的签名与强制竞态完全一致:受害测试超时加上 Unhandled Rejection: process.exit(1) called。所以这个隐患在今天的 main 上是活的,不只是理论上的,本 PR 确实把它堵住了。

1.3 基线与静态门

  • 无争用时两臂均 70/70。
  • 对改动文件 eslint --max-warnings 0 通过;prettier --check 通过。
  • 两臂做差分 tsc -p packages/cli --noEmit:错误集合完全一致(各 2023 行;其中 1442 行在 src/ui/opentui/*,来自我本地依赖农场缺少的 @opentui 类型,另有 1 条 @qwen-code/channel-dws 的 TS2307 与 2 条未构建 audio-capture 的 TS6305——都是环境产物)。没有一条涉及 serve.test.ts。

1.4 新增的等待不会挂住

这条路径上 startLocalControl 必然走到 qrcode.generate——mock 的 handle 是 webShellMounted: true,getLocalControl().enable() 解析出带 url 的状态——所以被等待的调用是无条件发生的,何况 vi.waitFor 本身有上限。相邻的两个 Local Control 测试早已带着同一行。


2. PR 的推理在哪里不成立

2.1 run 34289483217 其实是有归属的

那次失败作业(job 102272630674)的归档日志现在可以下载,里面有 6 条 FAIL 行,全部是 App.test.tsx > App session callbacks > does not rerender App for other split sessions,根因是 App.test.tsx:28940 的 ReferenceError: mockUseDaemonActivePromptBridge is not defined。该未定义符号在 422929b3a7 上确实还在。整份日志里 process.exit(1) called 出现 0 次。

用仓库自己的提取器跑这份日志,两个测试都能识别出来:

$ node .github/scripts/ci/main-failure-signature.mjs analyze \
    --workflow "Qwen Code CI" --jobs failed-jobs.tsv ci-job.log | jq -r '.tests[].id'
App.test.tsx > App session callbacks > does not rerender App for other split sessions (outer pending: false)
App.test.tsx > App session callbacks > does not rerender App for other split sessions (outer pending: true)

按提交追踪的兜底并不是因为失败无法归属,而是因为日志下载抢跑。开 issue 的那个作业(102277172988)在运行结束 36 秒后记录了:

Failed jobs: 1
##[warning]Could not download the log of job 102272630674
Failing tests identified: 0

那个 web-shell 的 ReferenceError 已由 #11406 修复,合入为 3a75f37ef5——也就是当前 main 的 HEAD。所以 #11414 已经在 main 上被另一个改动修好了,本 PR 正文里的 Fixes #11414 会把它按错误的提交自动关闭。

2.2 被引用的四次运行没有一次是这个泄漏

issue run 实际失败的是什么
#11346 34197708856 cli 的 1017 个测试文件全部通过;运行死于负载约 160 时的 Error: [vitest-worker]: Timeout calling "onTaskUpdate"
#11363 34207166214 src/serve/capabilities-docs-contract.test.ts > … keeps the daemon index capability counts in sync(确定性失败;该测试在今天的代码树上已通过)
#11404 34257817936 同一个 App.test.tsx ReferenceError
#11414 34289483217 同一个 App.test.tsx ReferenceError

四份作业日志里 process.exit(1) called 均出现 0 次。

四次开 issue 的作业都记录了同样的 ##[warning]Could not download the log of job … 以及随后的 Failing tests identified: 0(作业 102277172988、102182712398,以及 filer run 34201070808 / 34209871251 上的两个)。其中三次的归档日志确实指名了失败测试;只有 #11346 那次运行本来就没有 FAIL 行——因为没有任何测试失败。

2.3 这个泄漏不可能产生"没有 FAIL 行"的运行

它一旦触发,受害测试的 handler 永远不会 settle(它走到 blockForever),因此该测试会以 15 s 测试超时失败,vitest 必然打印一条指名它的 FAIL 行——我做的每一次复现都是如此,加不加 --retry=2 都一样。所以"日志里没有 FAIL 行 ⟹ 就是这个泄漏"这条推理两个方向都不成立,PR 的因果链需要换成 §1 里那套实测论据。


3. 非阻塞观察

N1 —— 本 PR 自己的 CI Test 作业正是挂在真实根因上。 run 34296692250、job 102294835954:同样那两个 App.test.tsx 失败。分支从 d670d47efe 分出,早于 #11406。rebase 到当前 main 应该就绿了。

N2 —— 同一类的第二个 handler 仍未静默。 applies authenticated open before the yargs path starts the daemon 在其 handler 正要进入 maybeOpenWebShellBrowser 时就返回了,而紧接着的下一个测试断言 expect(mockOpenBrowserSecurely).not.toHaveBeenCalled()。只把该 handler 的 runtimeReady 延迟 50 ms(下一个测试延迟 200 ms),两臂都会以 expected "spy" to not be called at all, but actually been called 1 times 同样失败。逐点加等待可行,但结构性的做法大概是让 startServeHandlerWithArgs 保留 handler 的 promise 并在 afterEach 里收尾,这样以后的测试就不必"记得"了。

N3 —— 这道守卫的预算就是 vi.waitFor 的 1 s 默认值。 在 HEAD 臂上加大注入的配对延迟:500 ms、900 ms 通过;到 1100 ms 时等待本身失败(expected "spy" to be called at least once),并且原来的泄漏又回来了。这不算回归——相邻两个测试同样是 1 s 预算,而且被等待的是 mock 模块上的微任务——但显式写 { timeout: 5000 } 可以把这个悬崖去掉。

N4 —— 现在有三个 open PR 带着逐字节相同的 hunk。 #11362、#11376 和本 PR。#11376 里那处 capability 文档计数的 hunk 已不在其 diff 中(该契约测试在今天的代码树上通过),所以三者等价:合一个,另外两个按重复关掉。


建议

代码可以合——泄漏是真的,这个改动确实堵住了它。合入前请:

  1. rebase 到当前 main(修掉红掉的 Test 作业,见 N1);
  2. 把 Fixes #11414 改成普通引用,并把"没有 FAIL 行"那套论据换成实测论据——泄漏不需要任何变异就能复现,只是它并不是 Main CI failed: Qwen Code CI on 422929b3a7df #11414 / Main CI failed: Qwen Code CI on 0d1e0fbfa6f3 #11346 / Main CI failed: Qwen Code CI on cffc40495a34 #11363 / Main CI failed: Qwen Code CI on 70cf3633950b #11404 所记录的那件事;
  3. 关闭 Main CI failed: Qwen Code CI on 422929b3a7df #11414 并指向 3a75f37ef5,同时把 fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362 / fix(cli): quiesce serve test leak and sync capability doc count (#11363) #11376 作为重复关掉。Main CI failed: Qwen Code CI on 0d1e0fbfa6f3 #11346 是另一码事——那次运行死于负载下的 vitest-worker RPC 超时,本 hunk 和 fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362 的 hunk 都不是它的修复。

另外,main-ci-failure-issue.yml 的维护者值得看一眼:抓取作业日志会与日志归档抢跑——这四次运行全部中招——从而把一次完全可归属的失败悄悄降级成按提交追踪的 issue。就这一行 warning,衍生出了 4 个 issue 和至少 3 个 autofix PR。在 gh api …/logs 调用外面加一层短重试/退避,大部分都能避免。


Verified locally with Claude Code (Opus 5, 1M context). All numbers above come from runs on the machine described in the Environment table; screenshots are unedited renderings of the captured terminal output.

@qwen-code-dev-bot

qwen-code-dev-bot commented Sep 9, 2026 •

Copy link
Copy Markdown
Collaborator Author

✅ AutoFix round 2 finished — view run. See this round's report below.

中文说明

✅ AutoFix 第 2 轮已完成 —— 查看运行。本轮报告见下方。

…11417)

The 'applies authenticated open before the yargs path starts the daemon'
test returned as soon as runQwenServe had been called while its
fire-and-forget handler continued into maybeOpenWebShellBrowser. A slow
runtimeReady lets that handler's openBrowserSecurely call land after the
next test's clearAllMocks, failing its not.toHaveBeenCalled assertion.

Wait out the browser-open phase, the same quiescence the neighbouring
Local Control tests already perform.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 Addressed the latest review feedback (round 1/10). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/10 轮)。改动内容与我反驳保留之处如下:

Autofix round summary — PR #11417

What changed

One commit, e7e2ecdf20 — test(cli): quiesce the authenticated-open serve handler across tests (#11417) (+3 lines in packages/cli/src/commands/serve.test.ts). The applies authenticated open before the yargs path starts the daemon test returned as soon as runQwenServe had been called while its fire-and-forget handler continued into maybeOpenWebShellBrowser; the test now waits for the handler's openBrowserSecurely call before returning, the same quiescence pattern the three neighbouring Local Control tests already use. Handlers park in blockForever() by design and never settle, so a per-site wait on the terminal observable is the applicable shape here — an afterEach handler-drain would hang every test.

Feedback dispositions

[rv:5149104112] / [rc:3964084627] — Critical R1-1: Fixes #11414 certifies a fix this diff never touches — escalated to maintainer (thread left open)

Both reviewers agree the three added lines are sound test hygiene and stay. What mis-certifies lives outside the working tree, so no commit of mine can resolve it:

So the re-scope needs a maintainer: edit the PR body (plain reference to #11414, measured-leak rationale per @wenshao's §1), and close #11414 pointing at 3a75f37ef5.

The rebase half of the remedy was already done by merge fbabbff9 and is now verified on this head, including the mutation the review asked for:

  • git merge-base --is-ancestor 3a75f37ef5 HEAD → yes; the branch's App.test.tsx contains zero mockUseDaemonActivePromptBridge references (10× the renamed mockUseDaemonSessionActivityBridge).
  • Green side: App session callbacks > does not rerender App for other split sessions ×2 pass on HEAD.
  • Mutation (red side): with App.test.tsx temporarily restored from a2420452, both tests fail with ReferenceError: mockUseDaemonActivePromptBridge is not defined; restored to HEAD afterwards, working tree clean.

The remaining thread question (re-scoped wording + which of #11362 / #11376 / #11417 lands and which two close as duplicates) is answered in a reply on the thread and stays open for you.

[ic:5594752664] — @wenshao's local verification report

  • Recommendation 1 (rebase onto main, N1) — already satisfied by merge fbabbff9; verified above. This also clears the PR's red Test job cause (the two App.test.tsx ReferenceErrors), which is fixed by 3a75f37ef5 now in the branch.
  • Recommendation 2 (re-scope Fixes #11414) — maintainer action, see R1-1 above. Not actionable from the working tree.
  • Recommendation 3 (close Main CI failed: Qwen Code CI on 422929b3a7df #11414 → 3a75f37ef5; close fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362 / fix(cli): quiesce serve test leak and sync capability doc count (#11363) #11376 as duplicates; N4) — maintainer decision; not mine to make. All three PRs now carry a byte-identical hunk, and this round's commit deliberately does not reference Main CI failed: Qwen Code CI on 422929b3a7df #11414 as a closer.
  • N2 (second un-quiesced handler) — implemented (the commit above). Reproduced first with the prescribed probe: delaying the offending handler's runtimeReady by 50 ms (and the next test's by 200 ms) fails prints the authenticated manual URL on the yargs headless path with expected "spy" to not be called at all, but actually been called 1 times. With the wait added and the probe still injected: green. Probe removed before committing.
  • N3 (explicit { timeout: 5000 } on the wait) — declined. The 1 s vi.waitFor default matches both neighbouring Local Control waits; the observed cliff needs an artificial ≥1.1 s injected pairing delay, which this microtask-only mocked path cannot produce. An explicit timeout would defend an impossible-in-practice scenario and diverge from the file's established pattern.
  • Closing observation (main-ci-failure-issue.yml loses the log-download race on all four runs) — verified-real, but the fix (retry/backoff around the gh api …/logs fetch) lives in .github/, which autofix rounds may not touch. Recorded in deferred-findings.json for the follow-up queue.

Verification

  • npm run build — passed (also required first: the runner's pre-built state was stale and @qwen-code/sdk/daemon did not resolve until rebuilt)
  • npm run typecheck — passed
  • npm run lint — passed
  • npx prettier --check packages/cli/src/commands/serve.test.ts — passed
  • cd packages/cli && npx vitest run src/commands/serve.test.ts — 70 passed (final committed state); baseline before the change also 70 passed
  • cd packages/web-shell && npx vitest run client/App.test.tsx -t "does not rerender App for other split sessions" — 2 passed on HEAD
  • Mutation probe (R1-1): App.test.tsx from a2420452 → both tests fail, ReferenceError: mockUseDaemonActivePromptBridge is not defined; file restored to HEAD → 2 passed
  • Mutation probe (N2, the new guard is load-bearing): runtimeReady delayed 50 ms in applies authenticated open… + 200 ms in the next test, guard absent → 1 failed (expected "spy" to not be called at all, but actually been called 1 times); guard present with the same injection → 70 passed; injection then removed and the committed state re-run → 70 passed

Not run: integration tests / npm run bundle (the change is exercised by the unit suite directly); npm run generate:settings-schema (no settings source touched).

中文说明

Autofix 本轮总结 — PR #11417

改动内容

一个提交,e7e2ecdf20 —— test(cli): quiesce the authenticated-open serve handler across tests (#11417)(packages/cli/src/commands/serve.test.ts 增加 3 行)。applies authenticated open before the yargs path starts the daemon 这个测试此前在 runQwenServe 被调用后就返回了,而其 fire-and-forget 的 handler 仍在继续进入 maybeOpenWebShellBrowser;现在该测试在返回前会等待该 handler 的 openBrowserSecurely 调用完成,与相邻三个 Local Control 测试已有的静默等待写法一致。handler 按设计停在 blockForever() 且永远不会 settle,因此"在终点可观测量上逐点等待"是这里唯一适用的形态 —— 在 afterEach 里排空 handler 会让每个测试都挂住。

反馈处置

[rv:5149104112] / [rc:3964084627] —— Critical R1-1:Fixes #11414 为一个本 diff 从未触及的失败作证 —— 已升级给维护者(讨论串保持未关闭)

两位评审者都认同:新增的三行是合理的测试卫生改动,予以保留。误作证的部分不在工作树内,因此我的任何提交都无法解决它:

因此重新界定声明需要维护者操作:编辑 PR 正文(改为对 #11414 的普通引用,根因论述换成 @wenshao §1 的实测论据),并关闭 #11414 且指向 3a75f37ef5。

修复方案中的 rebase 一半已由合并 fbabbff9 完成,并已在当前 head 上验证,包括评审要求的变异确认:

  • git merge-base --is-ancestor 3a75f37ef5 HEAD → 是;分支上的 App.test.tsx 中 mockUseDaemonActivePromptBridge 出现 0 次(已改名为 mockUseDaemonSessionActivityBridge,共 10 处)。
  • 绿色一侧:App session callbacks > does not rerender App for other split sessions 两个测试在 HEAD 上通过。
  • 变异(红色一侧):临时把 App.test.tsx 恢复为 a2420452 的版本后,两个测试均以 ReferenceError: mockUseDaemonActivePromptBridge is not defined 失败;随后恢复为 HEAD,工作树干净。

讨论串上遗留的问题(改写措辞,以及 #11362 / #11376 / #11417 三者合哪个、关哪两个)已在该串回复中说明,保持开放等待您定夺。

[ic:5594752664] —— @wenshao 的本地验证报告

  • 建议 1(rebase 到 main,N1) —— 已由合并 fbabbff9 满足;验证见上。这同时清掉了本 PR 红色 Test 作业的原因(那两个 App.test.tsx 的 ReferenceError),即已被带入分支的 3a75f37ef5 所修复的问题。
  • 建议 2(重新界定 Fixes #11414) —— 维护者操作,见上方 R1-1。工作树内无法执行。
  • 建议 3(关闭 Main CI failed: Qwen Code CI on 422929b3a7df #11414 并指向 3a75f37ef5;把 fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362 / fix(cli): quiesce serve test leak and sync capability doc count (#11363) #11376 按重复关闭;N4) —— 属于维护者决策,不由我定夺。三个 PR 目前携带逐字节相同的 hunk,本轮提交刻意没有把 Main CI failed: Qwen Code CI on 422929b3a7df #11414 作为关闭对象引用。
  • N2(第二个未静默的 handler) —— 已实现(即上述提交)。先按规定探针复现:把肇事 handler 的 runtimeReady 延迟 50 ms(下一个测试延迟 200 ms)后,prints the authenticated manual URL on the yargs headless path 以 expected "spy" to not be called at all, but actually been called 1 times 失败。加入等待且保留探针:转绿。提交前已移除探针。
  • N3(给等待显式加 { timeout: 5000 }) —— 拒绝。vi.waitFor 的 1 s 默认值与相邻两个 Local Control 等待一致;观察到的悬崖需要人工注入 ≥1.1 s 的配对延迟,而这条纯微任务的 mock 路径不可能产生该延迟。显式超时是在防一个实际不可能发生的场景,且会偏离本文件的既有写法。
  • 末尾观察(main-ci-failure-issue.yml 在四次运行中都输掉了日志下载竞态) —— 已核实为真,但修复(给 gh api …/logs 抓取加 retry/backoff)位于 .github/,autofix 轮次不得触碰。已记入 deferred-findings.json,进入后续跟进队列。

验证

  • npm run build —— 通过(这也是前置必需:runner 预构建的产物已过期,重建前 @qwen-code/sdk/daemon 无法解析)
  • npm run typecheck —— 通过
  • npm run lint —— 通过
  • npx prettier --check packages/cli/src/commands/serve.test.ts —— 通过
  • cd packages/cli && npx vitest run src/commands/serve.test.ts —— 70 通过(最终提交状态);改动前基线同为 70 通过
  • cd packages/web-shell && npx vitest run client/App.test.tsx -t "does not rerender App for other split sessions" —— HEAD 上 2 通过
  • 变异探针(R1-1):恢复为 a2420452 的 App.test.tsx → 两个测试失败,ReferenceError: mockUseDaemonActivePromptBridge is not defined;恢复为 HEAD → 2 通过
  • 变异探针(N2,证明新守卫承重):把 applies authenticated open… 的 runtimeReady 延迟 50 ms、下一个测试延迟 200 ms,无守卫 → 1 个失败(expected "spy" to not be called at all, but actually been called 1 times);同样注入下有守卫 → 70 通过;随后移除注入并复跑提交状态 → 70 通过

未运行:集成测试 / npm run bundle(本改动由单元测试直接覆盖);npm run generate:settings-schema(未触碰 settings 源)。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 10 this bot stops and leaves the PR for a human. · 有空请复审;第 10 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.23.1

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix round — PR #11417: no action

Outcome

No code change this round; nothing committed. The only feedback newer than the last evaluation (2026-09-09T04:04:31Z) is an automated review-infrastructure timeout notice, which carries no actionable code finding.

Feedback triage

1. Issue-level comment [ic:5597437904] — qwen-review-fallback (declined, not actionable)

The comment is the review workflow's own fallback notice: "Qwen Code review timed out after 5400 seconds (of the 90-minute budget)." It contains no review findings, no defect claims, and no requested code change. Its suggested remedy — commenting @qwen-code /review --timeout=360 to re-run the review with a longer budget — is a maintainer action on the PR thread. Autofix has no GitHub credentials and must not post comments, and modifying the review workflow or any CI machinery (.github/) is explicitly out of scope for this loop. There is nothing in the repository to change in response.

2. Failed check review-pr: FAILURE (same event, not a project check failure)

The review-pr job of the "🧐 Qwen Pull Request Review" workflow (run 34314094292) is the check-level record of the same timeout described above — the review bot exceeded its time budget. It is not a build, lint, typecheck, or test failure of the PR's code. The diff under review is six added lines in a single test file (packages/cli/src/commands/serve.test.ts); a diff of that size cannot plausibly cause a 90-minute review timeout, and no code change in this PR can influence the review runner's budget.

3. Prior Critical rc:3964084627 (already resolved before this window)

The earlier Critical — that the PR certified a fix for a CI failure whose real defect lived on main — predates the last evaluation timestamp and is not listed as open feedback this round. It was resolved in round 1: the branch merged main at fbabbff9bf, which brought in the actual CI fix (a5bc6c5497, "fix(ci): isolate serve route E2E from fork pressure"), and the round-1 commit e7e2ecdf20 kept the test-quiescing waits as the PR's remaining value.

Current PR state

  • HEAD: e7e2ecdf20 — test(cli): quiesce the authenticated-open serve handler across tests (#11417)
  • Diff vs origin/main: +6 lines in packages/cli/src/commands/serve.test.ts only (two vi.waitFor quiescing waits with explanatory comments).
  • All substantive CI checks on this HEAD are green: Test (ubuntu-latest, Node 22.x), Lint & Static, Integration Tests (no-AK, No Sandbox), Desktop Shell (ubuntu-22.04 and windows-2022), web-shell E2E Smoke, Classify PR, and both tui-parity gates. The only red check is review-pr, the infrastructure timeout above.
  • No Deferred non-Critical feedback or Growth audit required sections are present; diff growth this window is source 0 / test 3, within budget.

Verification

No commands were run beyond read-only inspection (git status, git log, git diff origin/main...HEAD, and reading the prepared feedback and check-status JSON in the workdir), because no code was changed and no commit was made. CI evidence cited above is from the workflow-supplied checks.json for HEAD e7e2ecdf20.

中文说明

Autofix 本轮处理 —— PR #11417:无需改动

结论

本轮没有代码改动,也未提交任何内容。距上次评估(2026-09-09T04:04:31Z)之后唯一新增的反馈是一条自动化的评审基础设施超时通知,其中不包含任何可操作的代码问题。

反馈分诊

1. PR 级评论 [ic:5597437904] —— qwen-review-fallback(不予处理,无可操作项)

该评论是评审工作流自身的兜底通知:"Qwen Code review timed out after 5400 seconds (of the 90-minute budget)."(Qwen Code 评审在 5400 秒后超时,即 90 分钟预算耗尽。)它不包含任何评审发现、缺陷声明或代码修改要求。其建议的补救方式——在 PR 中评论 @qwen-code /review --timeout=360 以更长预算重跑评审——是维护者在 PR 讨论区执行的操作。Autofix 没有 GitHub 凭据,不得发表评论;修改评审工作流或任何 CI 设施(.github/)也明确超出本循环的范围。仓库中没有任何可据此修改的内容。

2. 失败检查 review-pr: FAILURE(同一事件,并非项目检查失败)

"🧐 Qwen Pull Request Review" 工作流(运行 34314094292)的 review-pr 任务,是上述同一超时事件在检查层面的记录——评审机器人超出了其时间预算。它不是本 PR 代码的构建、lint、类型检查或测试失败。被评审的 diff 仅在一个测试文件(packages/cli/src/commands/serve.test.ts)中新增六行;这种规模的 diff 不可能导致 90 分钟的评审超时,且本 PR 中没有任何代码改动能够影响评审运行器的预算。

3. 此前的 Critical rc:3964084627(在本时间窗口之前已解决)

更早的那条 Critical——即 PR 声称修复了一个真正缺陷位于 main 上的 CI 失败——早于上次评估时间戳,本轮未被列为待处理反馈。它已在第 1 轮解决:分支在 fbabbff9bf 合并了 main,引入了真正的 CI 修复(a5bc6c5497,"fix(ci): isolate serve route E2E from fork pressure"),而第 1 轮的提交 e7e2ecdf20 将测试静默等待保留为本 PR 的剩余价值。

当前 PR 状态

  • HEAD:e7e2ecdf20 —— test(cli): quiesce the authenticated-open serve handler across tests (#11417)
  • 相对 origin/main 的 diff:仅在 packages/cli/src/commands/serve.test.ts 中新增 6 行(两个带说明注释的 vi.waitFor 静默等待)。
  • 该 HEAD 上所有实质性 CI 检查均为绿色:Test (ubuntu-latest, Node 22.x)、Lint & Static、Integration Tests (no-AK, No Sandbox)、Desktop Shell(ubuntu-22.04 与 windows-2022)、web-shell E2E Smoke、Classify PR,以及两个 tui-parity 门禁。唯一红色的检查是 review-pr,即上述基础设施超时。
  • 本轮反馈中不存在 Deferred non-Critical feedback 或 Growth audit required 部分;本窗口 diff 净增长为源码 0 行 / 测试 3 行,在预算之内。

验证情况

除只读检查(git status、git log、git diff origin/main...HEAD,以及读取工作目录中预先准备的反馈与检查状态 JSON)外未运行任何命令,因为本轮没有代码改动、也没有提交。上述 CI 证据来自工作流为 HEAD e7e2ecdf20 提供的 checks.json。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.23.1

@wenshao

wenshao commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix round — PR #11417: no action

Outcome

No code change this round; nothing committed. The only feedback newer than the last evaluation (2026-09-09T06:50:45Z) is an automated "sandboxed verification is running" status notice, and the one still-red check is the already-triaged review-runner timeout. Neither carries an actionable code finding.

Feedback triage

1. Issue-level comment [ic:5599438751] — verify-running status notice (not actionable)

Posted by the triage bot after @wenshao's @qwen-code /triage (ic:5599429078). It announces that sandboxed verification is running (workflow run 34333605149) and that a report will be posted when the run completes. It is a transient status notice: no defect claim, no requested change. The verification itself executes in the workflow's own infrastructure; nothing in the repository changes in response. If the eventual report contains findings, they will arrive as new feedback in a later round.

2. Still-red check review-pr: FAILURE (same infrastructure timeout, already triaged)

This is the check-level record of the review runner exhausting its 90-minute budget: the review-pr job of the "🧐 Qwen Pull Request Review" workflow (run 34314094292) ran 05:20:01Z → 06:50:45Z, and the fallback-comment job posted the timeout notice (ic:5597437904) at 06:50:33Z. The previous round already declined this exact item: it is not a build, lint, typecheck, or test failure of this PR's code — the diff is six added lines in a single test file, and no working-tree change can influence the review runner's budget. The suggested remedy (@qwen-code /review --timeout=360) is a maintainer action on the PR thread, and modifying review/CI machinery (.github/) is out of scope for this loop. No new evidence has appeared since that disposition.

3. Prior Critical R1-1 (rc:3964084627) — code side verified closed; certification re-scope remains with the maintainer

R1-1 predates the evaluation window and stays in its round-1 disposition (escalated, thread deliberately left open). Re-verified on this HEAD: packages/web-shell/client/App.test.tsx contains zero mockUseDaemonActivePromptBridge references — the branch carries main's real fix 3a75f37ef5 via merge fbabbff9bf, and the CI Test job on this HEAD is green. The remaining piece — editing the PR body to drop the Fixes #11414 trailer, closing #11414 against 3a75f37ef5, and choosing which of the byte-identical PRs #11362 / #11376 / #11417 lands — is a maintainer decision that no commit from this bot can make (no GitHub write access; additive-commit policy forbids rewriting the original commit message).

Current PR state

  • HEAD: e7e2ecdf20 — test(cli): quiesce the authenticated-open serve handler across tests (#11417); working tree clean; origin/main has newer commits but --conflict false was passed, so no merge was performed.
  • Diff vs origin/main: +6 lines in packages/cli/src/commands/serve.test.ts only (two vi.waitFor quiescing waits).
  • All substantive checks on this HEAD are green per the workflow-supplied checks.json: Test (ubuntu-latest, Node 22.x), Lint & Static, Integration Tests (no-AK, No Sandbox), Desktop Shell (ubuntu-22.04 and windows-2022), web-shell E2E Smoke, Classify PR, and both tui-parity gates. The only red check is the review-pr timeout above.
  • Diff growth this window: source 0 / test 3, within budget; no Deferred non-Critical feedback or Growth audit required section is present.

Verification

Read-only inspection only, because no code was changed and no commit was made:

  • git status — clean, branch up to date with origin/autofix/issue-11414
  • git diff origin/main...HEAD — 6-line test-only diff as described above
  • git merge-base --is-ancestor origin/main HEAD — main has newer commits; no conflict flagged (--conflict false), so no merge
  • grep mockUseDaemonActivePromptBridge packages/web-shell/client/App.test.tsx — 0 matches (R1-1 code side confirmed closed on this HEAD)
  • Workflow-supplied checks.json, rv.json, rc.json, ic.json — check outcomes and prior round dispositions cited above
中文说明

Autofix 本轮处理 —— PR #11417:无需改动

结论

本轮没有代码改动,也未提交任何内容。距上次评估(2026-09-09T06:50:45Z)之后唯一新增的反馈是一条自动化的"沙箱验证正在运行"状态通知,而唯一仍红的检查是此前已分诊过的评审运行器超时。两者都不包含可操作的代码问题。

反馈分诊

1. PR 级评论 [ic:5599438751] —— 验证运行中状态通知(无可操作项)

该评论由 triage 机器人在 @wenshao 触发 @qwen-code /triage(ic:5599429078)之后发布,内容是宣布沙箱验证正在运行(工作流运行 34333605149),运行结束后会发布报告。它是一条瞬时状态通知:没有缺陷声明,也没有修改要求。验证本身在工作流自身的基础设施中执行,仓库无需因此改动。如果最终报告包含问题发现,它们会作为新反馈在后续轮次到达。

2. 仍红的检查 review-pr: FAILURE(同一基础设施超时,已分诊)

这是评审运行器耗尽 90 分钟预算在检查层面的记录:"🧐 Qwen Pull Request Review" 工作流(运行 34314094292)的 review-pr 任务从 05:20:01Z 运行到 06:50:45Z,fallback-comment 任务于 06:50:33Z 发布了超时通知(ic:5597437904)。上一轮已对该同一事项作出不予处理的结论:它不是本 PR 代码的构建、lint、类型检查或测试失败 —— diff 仅在一个测试文件中新增六行,任何工作树改动都无法影响评审运行器的预算。其建议的补救方式(@qwen-code /review --timeout=360)是维护者在 PR 讨论区执行的操作,而修改评审/CI 设施(.github/)超出本循环的范围。自该结论作出以来没有出现新证据。

3. 此前的 Critical R1-1(rc:3964084627)—— 代码侧已验证关闭;声明的重新界定仍待维护者处理

R1-1 早于本次评估时间窗口,维持第 1 轮的处置(已升级给维护者,讨论串刻意保持开放)。已在当前 HEAD 上复核:packages/web-shell/client/App.test.tsx 中 mockUseDaemonActivePromptBridge 出现 0 次 —— 分支通过合并 fbabbff9bf 携带了 main 上真正的修复 3a75f37ef5,且该 HEAD 上的 CI Test 作业为绿色。剩余部分 —— 编辑 PR 正文以去掉 Fixes #11414 trailer、关闭 #11414 并指向 3a75f37ef5、以及在携带逐字节相同 diff 的 #11362 / #11376 / #11417 中选定合入哪一个 —— 属于维护者决策,本 bot 的任何提交都无法完成(没有 GitHub 写权限;仅可追加提交的策略禁止改写原始提交信息)。

当前 PR 状态

  • HEAD:e7e2ecdf20 —— test(cli): quiesce the authenticated-open serve handler across tests (#11417);工作树干净;origin/main 有更新的提交,但本次调用传入 --conflict false,因此未执行合并。
  • 相对 origin/main 的 diff:仅在 packages/cli/src/commands/serve.test.ts 中新增 6 行(两个 vi.waitFor 静默等待)。
  • 根据工作流提供的 checks.json,该 HEAD 上所有实质性检查均为绿色:Test (ubuntu-latest, Node 22.x)、Lint & Static、Integration Tests (no-AK, No Sandbox)、Desktop Shell(ubuntu-22.04 与 windows-2022)、web-shell E2E Smoke、Classify PR,以及两个 tui-parity 门禁。唯一红色的检查是上述 review-pr 超时。
  • 本窗口 diff 净增长:源码 0 行 / 测试 3 行,在预算之内;反馈中没有 Deferred non-Critical feedback 或 Growth audit required 部分。

验证情况

仅执行了只读检查,因为本轮没有代码改动、也没有提交:

  • git status —— 干净,分支与 origin/autofix/issue-11414 同步
  • git diff origin/main...HEAD —— 如上所述的 6 行纯测试 diff
  • git merge-base --is-ancestor origin/main HEAD —— main 有更新的提交;未标记冲突(--conflict false),因此不合并
  • grep mockUseDaemonActivePromptBridge packages/web-shell/client/App.test.tsx —— 0 处匹配(确认 R1-1 的代码侧在当前 HEAD 已关闭)
  • 工作流提供的 checks.json、rv.json、rc.json、ic.json —— 上文引用的检查结果与既往轮次处置

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.23.1

@wenshao

wenshao commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Local runtime verification

I built a real A/B environment for this instead of reading the diff, and drove the two hazards it fixes until they fired. Verdict: the code change is right — merge it, but drop Fixes #11414 first. The 6 added lines close two genuine cross-test leaks, they are complete for their class (5 of 5 fire-and-forget starters are now quiesced), neither wait can hang, and they cost nothing. What does not hold up is the issue link: I pulled the job log for run 34289483217 — the one the PR says was unavailable — and serve.test.ts passed in it. #11414 was a ReferenceError in packages/web-shell, already fixed on main by #11406 forty minutes before this PR was opened.

Harness

  • One worktree at PR head e7e2ecdf20, two file variants. BEFORE = git show a5bc6c5497:packages/cli/src/commands/serve.test.ts (the PR's merge-base with main); AFTER = PR head. The diff between arms is exactly the PR's 6 added lines and nothing else — and that BEFORE file is byte-identical to origin/main's copy today, so the A/B measures the actual merge decision, not a stale base.
  • Arms are emitted by a generator that aborts unless every anchor it edits is found the exact expected number of times, so a silently-missed patch cannot be reported as a passing arm.
  • npx vitest run src/commands/serve.test.ts (70 tests), vitest 3.2.7, Node v22.22.2, Linux, 16 vCPU. Workspace dist/ prerequisites and src/generated/git-commit.ts linked in from the main checkout so vitest's global setup passes.
  • Stated up front: everything ran as uid 0 on Linux only; the wild race is load-dependent, so the reproductions below use explicit async delays, and I say exactly which.

1. Both leaks are real — and the second one is not in the PR body

mechanism

The mechanism reproduces verbatim, down to the stack frame: handler src/commands/serve.ts:982:15 — the catch-all process.exit(1) — reached on a promise nobody awaits, plus the victim parking until the 15 s test timeout. packages/cli/vitest.config.ts:248 (dangerouslyIgnoreUnhandledErrors: process.platform !== 'linux') is why that only kills the run on Linux, exactly as described.

Using the PR's own recipe (leaker's enable() resolves after 100 ms, victim's after 1000 ms):

pair BEFORE AFTER
QR / mockQr.generate one-shot exit 1 — 1 failed, 1 unhandled error: process.exit(1) called exit 0 — 70 passed
browser-open / mockOpenBrowserSecurely exit 1 — expected "spy" to not be called at all, but actually been called 1 times → openBrowserSecurely("http://127.0.0.1:4170/#token=generated-token") exit 0 — 70 passed

The second row is the head commit e7e2ecdf20 ("quiesce the authenticated-open serve handler"), which the PR body never mentions — it still says "Adds one quiescence wait". Same class of bug, equally real, worth having in the description.

2. The fair version of the experiment, and a sharp threshold

Hand-picking a leaker/victim pair proves the mechanism but not the exposure, so I re-ran it as a uniform load model: every one of the 7 runtimeReady: Promise.resolve() sites in the file becomes delayedPromise(D) — every mocked daemon takes D ms to become ready, no test singled out.

a/b matrix

D (ms) 30 40 50 55 60 70 150 400 900 1200
BEFORE pass pass 1 failed 2 failed 2 failed +1 unhandled 2 failed 2 failed +1 unhandled — — —
AFTER — — pass pass pass — pass pass pass 6 failed

The flip is at exactly 50 ms, which is vi.waitFor's poll interval (vitest/dist/chunks/vi.bdSIJ99Y.js:3709: interval = 50, timeout = 1e3). startServeHandlerWithArgs waits on mockRunQwenServe, whose first synchronous check always fails, so the handler gets a free ~50 ms head start; the leak only becomes reachable when the handler's own post-runQwenServe chain outlives that window. The failures on BEFORE are exactly — and only — the two tests this PR quiesces, at every delay from 50 ms to 150 ms.

At D = 1200 the AFTER arm fails 6 tests, all bounded vi.waitFor 1000 ms timeouts, and two of them are the waits that pre-date this PR. So the 1 s budget is a property of the established pattern, not something this PR introduces; the fix has ~900 ms of headroom against a stall in that window.

3. Why it is rare and unattributable in CI — measured, not assumed

40 busy-loop processes on 16 vCPU, 1-minute load average 45–52 (the failing CI job ran at load 33.6), no artificial delay, 8 consecutive runs per arm: 0/8 failures on both arms, 0 unhandled errors. CPU pressure alone cannot do it. With every dependency stubbed by mockResolvedValue(), the handler's remaining chain is pure microtasks and always finishes inside the 50 ms head start. In CI the gap has to come from a real stall in that specific window — a GC pause, worker starvation, a cold await import('qrcode-terminal'). That is consistent with a hazard that fires occasionally and is hard to attribute, and it is a good reason to close it even though I could not catch it in the wild.

Cost of the fix on the unmodified file, 5 consecutive runs per arm: BEFORE 7.7–8.1 s, AFTER 7.7–8.0 s, 70/70 every time. No measurable cost.

4. Complete for its class, and neither wait can hang

completeness

22 tests use the fire-and-forget starter. After runQwenServe resolves, the handler only touches shared mocks when the args reach startLocalControl (--local-control) or maybeOpenWebShellBrowser (--open/--open-with-auth); the other 17 (--no-web) fall straight into blockForever() and touch nothing. Five starters leave mock-touching work, and after this PR all five are quiesced — three already were, these two were the gap. The dynamic sweep agrees: nothing else in the file ever breaks on either arm. grep -rn "void handler(" --include=*.test.ts packages/ returns 2 hits in 1 file, so the pattern exists nowhere else in the repo.

Two properties the new waits depend on, checked against vitest 3.2.7 rather than assumed:

  • mockQr.generate is called unconditionally by startLocalControl once status.url is set (serve.ts:104), and the mock always supplies one — the QR wait cannot hang.
  • mockOpenBrowserSecurely needs shouldLaunchBrowser() === true. A later test flips it to false with mockReturnValue, which vi.clearAllMocks() does not undo — but vi.restoreAllMocks() in afterEach does reset a vi.fn(impl) back to impl (verified with a standalone probe: the next test sees true). So that wait holds independently of test order.
  • Worst case if a future change breaks either precondition is a bounded, clearly-named 1 s failure, never a hang — observed at D = 1200.

Blast radius is exactly one test, as the body says: an unconsumed mockImplementationOnce survives vi.clearAllMocks() on its own, but is cleared by the vi.restoreAllMocks() this file also runs in afterEach (both probed directly). A leaked handler can only corrupt the immediately following test, not a later one.

CI on this PR is green where it matters — Test (ubuntu-latest, Node 22.x) pass, Lint & Static pass — so the build/typecheck/lint claim holds.

5. Fixes #11414 is wrong — the log names a different failure

issue 11414

I fetched the 2.8 MB job log with gh api repos/QwenLM/qwen-code/actions/jobs/102272630674/logs:

  • ✓ src/commands/serve.test.ts (70 tests) 3094ms — this file passed in the very run Main CI failed: Qwen Code CI on 422929b3a7df #11414 was filed for, and so did all of packages/cli: 1022 files, 29272 tests, 0 failures.
  • The single Failed Tests block in the entire log is in packages/web-shell: App.test.tsx > App session callbacks > does not rerender App for other split sessions, ReferenceError: mockUseDaemonActivePromptBridge is not defined at App.test.tsx:28940. 2 failed / 6710 passed.
  • grep -c "Unhandled Rejection" = 0. grep -c "process.exit(1) called" = 0. There is no unattributed failure anywhere in the log — the run named the file, the test and the error.
  • At 422929b3a7, packages/web-shell/client/App.test.tsx uses mockUseDaemonActivePromptBridge three times and declares it zero times: a deterministic missing declaration, not a race. git log -S says it was removed by 3a75f37ef5 — fix(web-shell): Replace undefined mock in split rerender tests (#11404) #11406, "Replace undefined mock in split rerender tests (Main CI failed: Qwen Code CI on 70cf3633950b #11404)", merged 2026-09-09T00:10:49Z, i.e. 40 minutes before this PR was opened. It is already gone from main.

So the premise in the body ("Main CI keeps dying … with no failing test attributed") does not describe run 34289483217. The issue body's "failed before any test result was reported" is the filing bot's generic per-commit template, not a reading of this log — which is probably how the wrong root cause got attached.

What I would change before merging

  1. Drop Fixes #11414 (reference it as context if you like). As written, merging auto-closes an issue this PR does not fix and attributes fix(web-shell): Replace undefined mock in split rerender tests (#11404) #11406's fix to it. Main CI failed: Qwen Code CI on 422929b3a7df #11414 should be closed pointing at fix(web-shell): Replace undefined mock in split rerender tests (#11404) #11406 instead.
  2. Update the body: it says "Adds one quiescence wait … No production code changes"; the head adds two waits, and the browser-open one is the more easily-triggered of the pair (it fires first, at D = 50 ms). Worth a sentence, and a retitle around the real subject.
  3. Close fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362 and fix(cli): quiesce serve test leak and sync capability doc count (#11363) #11376 as subsumed — both carry only the QR hunk, so this PR is a strict superset of each.
  4. Follow-up, not a blocker: this is the third PR for the same hazard, each patching one call site by hand, so the next unwaited starter reopens it. Moving the quiescence into the helper — have startServeHandlerWithArgs take the observable it must settle on, or keep the returned promise and add a generic afterEach that asserts no shared mock is called after the test body returns — would make a fourth occurrence structurally impossible. That afterEach probe is what I used for the audit above and it works.
  5. Nit: a test-only change under a fix(cli): title; the head commit itself uses test(cli):.

Not validated

macOS and Windows (the Test jobs for both are skipping on this PR, and dangerouslyIgnoreUnhandledErrors means the unhandled-rejection half of this cannot fail there anyway); the wild race under genuine CI conditions (it did not fire in 16 loaded runs — every reproduction here uses an explicit async delay); and the coverage/.tmp ENOENT flake the body sets out of scope, which I did not touch.

中文说明

本地运行时验证

我没有只读 diff,而是搭了真实的 A/B 环境,把这个 PR 修的两处竞态都逼到复现为止。结论:代码改动是对的 —— 可以合,但合之前请去掉 Fixes #11414。 这 6 行确实关掉了两个真实的跨测试泄漏,对这一类问题是完备的(5 个 fire-and-forget starter 现在全部静默),两处等待都不会挂死,而且没有任何开销。站不住的是 issue 关联:我把 PR 里说"修复环境无法获取"的那次运行 34289483217 的 job 日志拉下来了 —— serve.test.ts 在那次运行里是通过的。#11414 的真实原因是 packages/web-shell 里的一个 ReferenceError,而且在本 PR 开出前 40 分钟就已经被 #11406 修掉并合入 main 了。

环境

  • 一个 PR head e7e2ecdf20 的 worktree,两份文件变体。BEFORE = git show a5bc6c5497:packages/cli/src/commands/serve.test.ts(本 PR 与 main 的 merge-base);AFTER = PR head。两臂之间的差异就是本 PR 新增的那 6 行,别无其他 —— 而且这份 BEFORE 文件与今天 origin/main 上的副本逐字节相同,所以这个 A/B 衡量的就是真实的合并决策,而不是一个过时的基线。
  • 各臂由一个生成器产出,它要求每个被改的锚点出现次数与预期完全一致,否则直接报错退出 —— 因此"补丁静默没打上却报成通过"这种情况不可能发生。
  • npx vitest run src/commands/serve.test.ts(70 个测试),vitest 3.2.7、Node v22.22.2、Linux、16 vCPU。workspace 的 dist/ 前置产物与 src/generated/git-commit.ts 从主检出链入,以便通过 vitest 的 global setup。
  • 先说清楚:全部以 uid 0 在 Linux 上运行;野外竞态依赖负载,因此下面的复现都使用了显式的异步延迟,我会逐处说明。

1. 两处泄漏都是真的 —— 而第二处并不在 PR 描述里

机制逐字复现,连栈帧都对得上:handler src/commands/serve.ts:982:15,即那个兜底的 process.exit(1),在一个无人 await 的 promise 上被触发;受害测试则一直停滞到 15 秒超时。packages/cli/vitest.config.ts:248(dangerouslyIgnoreUnhandledErrors: process.platform !== 'linux')正是"只在 Linux 上会让整个运行失败"的原因,与描述一致。

按 PR 自己给的方法(泄漏方 enable() 延迟 100 ms 解析,受害方延迟 1000 ms):

配对 BEFORE AFTER
QR / mockQr.generate 一次性 mock exit 1 —— 1 failed,1 个未处理错误:process.exit(1) called exit 0 —— 70 passed
浏览器打开 / mockOpenBrowserSecurely exit 1 —— expected "spy" to not be called at all, but actually been called 1 times → openBrowserSecurely("http://127.0.0.1:4170/#token=generated-token") exit 0 —— 70 passed

第二行对应 head commit e7e2ecdf20("quiesce the authenticated-open serve handler"),而 PR 描述从未提到它 —— 描述里仍然写着"增加一处静默等待"。同一类缺陷,同样真实,值得写进描述。

2. 更公平的实验版本,以及一个非常锐利的阈值

手工挑一对"泄漏方/受害方"能证明机制,但证明不了暴露面。所以我把它改成统一的负载模型:文件里 7 处 runtimeReady: Promise.resolve() 全部替换为 delayedPromise(D) —— 每个被 mock 的 daemon 都要 D 毫秒才 ready,不针对任何一个测试。

D (ms) 30 40 50 55 60 70 150 400 900 1200
BEFORE 通过 通过 1 failed 2 failed 2 failed +1 未处理 2 failed 2 failed +1 未处理 — — —
AFTER — — 通过 通过 通过 — 通过 通过 通过 6 failed

翻转点正好在 50 ms,也就是 vi.waitFor 的轮询间隔(vitest/dist/chunks/vi.bdSIJ99Y.js:3709:interval = 50, timeout = 1e3)。startServeHandlerWithArgs 等的是 mockRunQwenServe,它的第一次同步检查必然失败,于是 handler 白得约 50 ms 的领先;只有当 handler 自己在 runQwenServe 之后的那条链超出这个窗口,泄漏才变得可达。BEFORE 上失败的测试,恰好且仅仅是本 PR 静默的那两个,从 50 ms 到 150 ms 都是如此。

在 D = 1200 时 AFTER 臂有 6 个测试失败,全部是有界的 vi.waitFor 1000 ms 超时,其中两个是本 PR 之前就存在的等待。所以这 1 秒预算是既有模式的固有属性,不是本 PR 引入的;该修复对这个窗口内的停顿有约 900 ms 余量。

3. 为什么它在 CI 里罕见且难以归因 —— 实测而非推断

16 vCPU 上开 40 个忙等进程,1 分钟负载 45–52(失败的那次 CI job 负载为 33.6),不加任何人工延迟,每臂连跑 8 次:两臂都 0/8 失败,0 个未处理错误。单靠 CPU 压力做不到。当所有依赖都被 mockResolvedValue() 打桩后,handler 剩下的链全是微任务,总是在 50 ms 领先窗口内跑完。在 CI 里,这个间隙必须来自那个特定窗口内的真实停顿 —— GC 暂停、worker 被饿死、一次冷启动的 await import('qrcode-terminal')。这与"偶发且难以归因"的表现一致,也正是即使我在野外抓不到它、也值得把这个口子关掉的理由。

修复在未改动文件上的开销,每臂连跑 5 次:BEFORE 7.7–8.1 秒,AFTER 7.7–8.0 秒,每次都 70/70。无可测量开销。

4. 对这一类问题是完备的,且两处等待都不会挂死

22 个测试使用这个 fire-and-forget starter。runQwenServe 返回之后,只有当参数走到 startLocalControl(--local-control)或 maybeOpenWebShellBrowser(--open/--open-with-auth)时,handler 才会再碰共享 mock;其余 17 个(--no-web)直接落进 blockForever(),什么都不碰。共有 5 个 starter 留下了会碰 mock 的后续工作,本 PR 之后这 5 个全部静默 —— 其中 3 个原本就有等待,这 2 个是缺口。动态扫描也印证了这一点:文件里其他任何测试在两臂上都从未失败。grep -rn "void handler(" --include=*.test.ts packages/ 只有 1 个文件 2 处命中,说明这个模式在仓库里别无他处。

两处新等待所依赖的前提条件,我是对着 vitest 3.2.7 实测的,而不是假设:

  • status.url 存在时,startLocalControl 必然调用 mockQr.generate(serve.ts:104),而 mock 总会给出一个 URL —— QR 那处等待不会挂死。
  • mockOpenBrowserSecurely 需要 shouldLaunchBrowser() === true。后面有个测试用 mockReturnValue 把它翻成 false,而 vi.clearAllMocks() 不会撤销这个实现 —— 但 afterEach 里的 vi.restoreAllMocks() 会把 vi.fn(impl) 重置回 impl(我用独立探针验证过:下一个测试看到的是 true)。所以这处等待与测试顺序无关地成立。
  • 万一将来有改动破坏了上述前提,最坏结果也只是一次有界、报错清晰的 1 秒失败,绝不会挂死 —— 这在 D = 1200 时观测到了。

影响半径确实就是一个测试,与描述一致:未被消费的 mockImplementationOnce 单靠 vi.clearAllMocks() 是会存活的,但会被这个文件同样在 afterEach 里执行的 vi.restoreAllMocks() 清掉(两者我都直接探测过)。泄漏的 handler 只能污染紧随其后的那一个测试,污染不到更后面的。

本 PR 的 CI 在关键项上是绿的 —— Test (ubuntu-latest, Node 22.x) 通过、Lint & Static 通过 —— 所以 build/typecheck/lint 的说法成立。

5. Fixes #11414 是错的 —— 日志指向的是另一个失败

我用 gh api repos/QwenLM/qwen-code/actions/jobs/102272630674/logs 取到了那份 2.8 MB 的 job 日志:

  • ✓ src/commands/serve.test.ts (70 tests) 3094ms —— 在 Main CI failed: Qwen Code CI on 422929b3a7df #11414 所对应的那次运行里,这个文件是通过的,整个 packages/cli 也是:1022 个文件、29272 个测试、0 失败。
  • 整份日志里唯一的 Failed Tests 区块在 packages/web-shell:App.test.tsx > App session callbacks > does not rerender App for other split sessions,ReferenceError: mockUseDaemonActivePromptBridge is not defined,位置 App.test.tsx:28940。2 failed / 6710 passed。
  • grep -c "Unhandled Rejection" = 0;grep -c "process.exit(1) called" = 0。日志里不存在任何"无归属失败"—— 那次运行明确指出了文件、测试与错误。
  • 在 422929b3a7 上,packages/web-shell/client/App.test.tsx 使用 mockUseDaemonActivePromptBridge 三次、声明零次:这是一个确定性的缺失声明,不是竞态。git log -S 显示它被 3a75f37ef5 移除 —— 即 fix(web-shell): Replace undefined mock in split rerender tests (#11404) #11406,"Replace undefined mock in split rerender tests (Main CI failed: Qwen Code CI on 70cf3633950b #11404)",合并于 2026-09-09T00:10:49Z,比本 PR 开出早 40 分钟。它在 main 上已经不存在了。

所以描述里的前提("主分支 CI 反复失败……且没有任何测试被标记为失败")并不描述运行 34289483217。issue 正文那句"在任何测试结果被报告前就失败"是提单机器人的通用逐提交模板,而不是对这份日志的解读 —— 错误的根因大概就是这样被挂上去的。

合并前我建议改的

  1. 去掉 Fixes #11414(想保留可以只作为背景引用)。按现在的写法,合并会自动关闭一个本 PR 并未修复的 issue,并把 fix(web-shell): Replace undefined mock in split rerender tests (#11404) #11406 的功劳记到它头上。Main CI failed: Qwen Code CI on 422929b3a7df #11414 应该指向 fix(web-shell): Replace undefined mock in split rerender tests (#11404) #11406 来关闭。
  2. 更新描述:现在写的是"增加一处静默等待……不涉及生产代码改动",而 head 实际加了两处;其中浏览器打开那处更容易被触发(它先炸,在 D = 50 ms)。值得补一句,并把标题改到真实主题上。
  3. 把 fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362 与 fix(cli): quiesce serve test leak and sync capability doc count (#11363) #11376 作为被涵盖项关闭 —— 两者都只带 QR 那一处,本 PR 是它们各自的严格超集。
  4. 后续项,不阻塞合并:这已经是同一隐患的第三个 PR,每次都手工修一个调用点,因此下一个漏加等待的 starter 会把口子重新打开。把静默逻辑收进 helper —— 让 startServeHandlerWithArgs 接收它必须等到的可观测量,或者保留返回的 promise 并加一个通用 afterEach 断言"测试体返回后不再有共享 mock 被调用" —— 就能让第四次在结构上不可能发生。上面那个审计用的就是这个 afterEach 探针,可用。
  5. 小问题:仅测试的改动用了 fix(cli): 标题;head commit 本身用的是 test(cli):。

未验证部分

macOS 与 Windows(本 PR 上这两个 Test job 都是 skipping,而且 dangerouslyIgnoreUnhandledErrors 意味着这里"未处理 rejection"那一半在这两个平台上本来就不会失败);真实 CI 条件下的野外竞态(16 次高负载运行中都没有触发 —— 这里所有复现都用了显式异步延迟);以及描述中列为超出范围的 coverage/.tmp ENOENT flake,我没有触碰。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🔀 Base updated: red check(s) [review-pr] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [review-pr] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

@yiliang114

Copy link
Copy Markdown
Collaborator

The linked issue #11414 is now closed as resolved by #11406. Run 34289483217 did not have an unattributed serve teardown failure: its only failed tests were the two Web Shell split-session cases with ReferenceError: mockUseDaemonActivePromptBridge is not defined. This test-quiescence change may still be relevant to #11346, but it should not be presented as the fix for #11414.

@wenshao

wenshao commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao
wenshao enabled auto-merge September 9, 2026 12:30
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🕐 Review received — an automatic review of the current head is still running, so this round is held until it lands (a push now would cancel it and discard its work, #8888). Your feedback stays queued for the next eligible round.

中文说明

🕐 已收到评审 —— 当前 head 上仍有一轮自动 review 在运行,本轮暂缓(现在推送会取消该 review 并丢弃其工作,#8888)。反馈保持排队,等待下一次可运行的轮次处理。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix round — no code change

Outcome: no change, no commit. The diff under review is exactly the 6 added lines in packages/cli/src/commands/serve.test.ts, and this round's feedback — including @wenshao's full A/B runtime validation — converges on "the code change is right". Every remaining actionable item is GitHub-side metadata that this agent cannot perform (no GitHub credentials; PR title/body, review threads, and issue state are workflow- or maintainer-owned), plus one explicit follow-up that is deferred rather than implemented here.

Feedback dispositions

  • [ic:5599890530] @wenshao (runtime validation report) — code verdict accepted; dispositions per item:
    1. Drop Fixes #11414 — agreed with the finding, but not agent-actionable: the PR body is workflow-owned metadata and address-review mode has no channel to edit it. Maintainer action required before merge: as written, merging auto-closes Main CI failed: Qwen Code CI on 422929b3a7df #11414, which the job log shows was a packages/web-shell ReferenceError already fixed by fix(web-shell): Replace undefined mock in split rerender tests (#11404) #11406 — serve.test.ts passed in run 34289483217.
    2. Update the body / retitle (two quiescence waits, not one) — same as above: PR-metadata edit, maintainer/workflow-side.
    3. Close fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362 and fix(cli): quiesce serve test leak and sync capability doc count (#11363) #11376 as subsumed — issue-state operation, maintainer action.
    4. Move quiescence into the helper (structural follow-up) — recorded in deferred-findings.json. The comment itself marks it "Follow-up, not a blocker", and folding a structural refactor into this 6-line test fix would be scope drift.
    5. test(cli): vs fix(cli): title nit — acknowledged; it concerns commit/PR titles, and this round creates no commit.
  • [ic:5601273879] @yiliang114 — same root item (the Fixes #11414 link must go); the noted Main CI failed: Qwen Code CI on 0d1e0fbfa6f3 #11346 relevance is useful context for whoever re-titles the PR. Nothing code-actionable.
  • [ic:5600957526], [ic:5602180141] (review-timeout notices) and [ic:5601855039] (verification-in-progress) — bot status notices; no action.
  • Inline [rc:3964084627] (automated Critical R1-1, Fixes #11414 certifies-falsely) — predates this round's evaluation window, but it is the same root issue as item 1 above: the finding is correct, its resolution is the PR-body edit, and that edit is a maintainer/workflow action. Replied on the thread with this disposition; left unresolved so the open question stays visible.

Failed check: review-pr — diagnosis from evidence

The only failing check is review-pr in the "🧐 Qwen Pull Request Review" workflow (run 34344946307, failed 2026-09-09T12:55:44Z). The PR carries two bot comments — [ic:5600957526] and [ic:5602180141] — stating "Qwen Code review timed out after 5400 seconds" for that run: the review job exhausted its own time budget. A 6-line test-only diff cannot cause or cure a review-job timeout, and CI/verification machinery is outside what a round may modify, so there is no in-scope fix. All code-gating checks on this head are green: Test (ubuntu-latest, Node 22.x), Lint & Static, Integration Tests (no-AK, No Sandbox), web-shell E2E Smoke, both tui-parity gates, and Desktop Shell (ubuntu-22.04, windows-2022) — SUCCESS.

Verification

  • npm run build — passed (required to materialize workspace dist/ outputs for the vitest globalSetup guard).
  • cd packages/cli && npx vitest run src/commands/serve.test.ts — 70/70 passed (1 file, ~11 s), consistent with the maintainer's A/B result on the same head.
  • Tree state: git status --porcelain=v1 clean; git diff origin/main...HEAD still exactly the 6 reviewed lines; no commit created.
中文说明

Autofix 本轮 —— 无代码改动

结论:无改动、无提交。 当前评审的 diff 就是 packages/cli/src/commands/serve.test.ts 中新增的 6 行,而本轮反馈 —— 包括 @wenshao 完整的 A/B 运行时验证 —— 一致收敛于"代码改动是对的"。剩余可执行项全部是本 agent 无法操作的 GitHub 侧元数据(无 GitHub 凭据;PR 标题/正文、评审线程与 issue 状态归 workflow 或维护者所有),另有一项明确标注的后续项,已记录为跟进而非在本 PR 实现。

反馈处置

  • [ic:5599890530] @wenshao(运行时验证报告) —— 接受其对代码的结论;逐条处置如下:
    1. 去掉 Fixes #11414 —— 认同该发现,但 agent 无法执行:PR 正文是 workflow 管理的元数据,address-review 模式没有修改它的通道。合并前需要维护者操作: 按现状合并会自动关闭 Main CI failed: Qwen Code CI on 422929b3a7df #11414,而 job 日志显示 Main CI failed: Qwen Code CI on 422929b3a7df #11414 是 packages/web-shell 的一个 ReferenceError,已被 fix(web-shell): Replace undefined mock in split rerender tests (#11404) #11406 修复 —— serve.test.ts 在运行 34289483217 中是通过的。
    2. 更新正文 / 改标题(两处静默等待,而非一处)—— 同上:属于 PR 元数据编辑,需维护者/workflow 侧操作。
    3. 将 fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362 与 fix(cli): quiesce serve test leak and sync capability doc count (#11363) #11376 作为被涵盖项关闭 —— issue 状态操作,需维护者执行。
    4. 把静默逻辑收进 helper(结构性后续项) —— 已记录到 deferred-findings.json。原评论明确标注"后续项,不阻塞合并",而把结构性重构塞进这个 6 行测试修复里属于范围漂移。
    5. test(cli): 与 fix(cli): 标题小问题 —— 已知悉;它针对的是提交/PR 标题,本轮不产生提交。
  • [ic:5601273879] @yiliang114 —— 同一根因(Fixes #11414 关联必须去掉);其中提到的与 Main CI failed: Qwen Code CI on 0d1e0fbfa6f3 #11346 的关联可作为重拟标题时的参考。无可执行的代码改动。
  • [ic:5600957526]、[ic:5602180141](评审超时通知)与 [ic:5601855039](验证进行中)—— 机器人状态通知,无需处理。
  • 行内评论 [rc:3964084627](自动评审 Critical R1-1,Fixes #11414 误证) —— 早于本轮评估窗口,但与上面第 1 条是同一根因:该发现属实,其修复就是改 PR 正文,而该编辑属于维护者/workflow 操作。已在该线程回复说明处置;线程保持未解决状态,以便这个待办问题保持可见。

失败检查:review-pr —— 基于证据的诊断

唯一失败的检查是 "🧐 Qwen Pull Request Review" 工作流中的 review-pr(运行 34344946307,失败于 2026-09-09T12:55:44Z)。PR 上有两条机器人评论 —— [ic:5600957526] 与 [ic:5602180141] —— 说明该运行"Qwen Code 评审在 5400 秒后超时":是评审作业自身耗尽了时间预算。一个仅 6 行的测试改动既不可能导致、也不可能治愈评审作业超时,而 CI/验证设施又在本轮允许修改的范围之外,因此没有在范围内的修复可做。该 head 上所有代码门禁检查均为绿色:Test (ubuntu-latest, Node 22.x)、Lint & Static、Integration Tests (no-AK, No Sandbox)、web-shell E2E Smoke、两个 tui-parity 门禁、以及 Desktop Shell(ubuntu-22.04、windows-2022)均为 SUCCESS。

验证

  • npm run build —— 通过(用于产出 vitest globalSetup 守卫所需的 workspace dist/ 产物)。
  • cd packages/cli && npx vitest run src/commands/serve.test.ts —— 70/70 通过(1 个文件,约 11 秒),与维护者在同一 head 上的 A/B 结果一致。
  • 工作区状态:git status --porcelain=v1 干净;git diff origin/main...HEAD 仍恰好是被评审的那 6 行;未创建提交。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.23.2

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🔀 Base updated: red check(s) [review-pr] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [review-pr] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix round: no action required

All feedback newer than the last evaluation (2026-09-09T12:55:44Z) was triaged: one issue-level comment and one failed check. Neither is actionable in this PR's code, so the working tree is unchanged and no commit was created.

Feedback triage

  • [ic:5605176413] (@qwen-code-ci-bot) — "Qwen Code review timed out after 5400 seconds" and the associated review-pr: FAILURE check are the same event: the automated PR-review workflow exhausted its 90-minute budget. This is a timeout of the review tooling itself, not a defect reported against this PR:
    • The failing step is the review agent exceeding its time budget. It never reached a verdict, so there is no review content to address.
    • The PR's net diff against the merge base is 3 added lines in packages/cli/src/commands/serve.test.ts (a vi.waitFor quiescence for the authenticated-open serve handler). The earlier pairing-phase fix this PR originally carried has already landed on main via fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362 with identical wording, which is why the remaining diff is this small. A 3-line test-only diff cannot plausibly drive the review agent into a timeout, and no code change in this PR would make that job finish faster.
    • The review-pr check is the AI review workflow run; it is not runnable from this environment, so there is no local check to re-run or fix.
    • The bot's suggested remedy — a maintainer commenting @qwen-code /review --timeout=360 — requires GitHub credentials that this fix workflow intentionally does not have, and changing the review workflow's timeout would mean editing .github/ CI machinery, which is outside this PR's footprint and out of bounds for an autonomous fix round. Re-running or re-configuring the review is owned by maintainers, not by code changes on this branch.
  • No inline review comments, no review bodies, and no still-red build/test/lint checks are pending.

Verification

No code was changed, so no build, typecheck, lint, or test commands were run this round. Tree state was confirmed by inspection only:

  • git diff origin/main...HEAD — 1 file, 3 insertions (packages/cli/src/commands/serve.test.ts), matching the PR's second commit.
  • git status --short — clean working tree.
  • git show d5bcf28293:packages/cli/src/commands/serve.test.ts — confirmed the merge base already contains the pairing-phase quiescence (landed via fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362), so the first commit's fix is preserved on main and the remaining PR diff is exactly the authenticated-open quiescence.
中文说明

Autofix 本轮:无需改动

已 triage 上次评估(2026-09-09T12:55:44Z)之后的全部反馈:一条 issue 级评论和一个失败的检查。两者都不需要修改本 PR 的代码,因此工作区保持不变,也未创建新的提交。

反馈 triage

  • ic:5605176413——"Qwen Code review timed out after 5400 seconds" 以及对应的 review-pr: FAILURE 检查是同一事件:自动化 PR 审查工作流耗尽了 90 分钟的预算。这是审查工具自身的超时,而不是针对本 PR 报告的缺陷:
    • 失败的步骤是审查 agent 超出时间预算。它从未给出审查结论,因此没有任何审查内容需要处理。
    • 本 PR 相对 merge base 的净 diff 是在 packages/cli/src/commands/serve.test.ts 中新增 3 行(针对 authenticated-open serve handler 的 vi.waitFor 静止等待)。本 PR 最初携带的 pairing 阶段修复已通过 fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362 以完全相同的措辞合入 main,这就是剩余 diff 如此之小的原因。一个仅 3 行的测试 diff 不可能导致审查 agent 超时,本 PR 中也没有任何代码改动能让该任务更快完成。
    • review-pr 检查是 AI 审查工作流的运行实例,无法在当前环境中运行,因此没有可在本地重跑或修复的检查。
    • bot 建议的补救措施——由 maintainer 评论 @qwen-code /review --timeout=360——需要本修复工作流刻意不具备的 GitHub 凭据;而修改审查工作流的超时时间意味着编辑 .github/ 的 CI 设施,这超出本 PR 的范围,也不属于自治修复轮的边界。重跑或重新配置审查由 maintainer 负责,而不是通过本分支上的代码改动解决。
  • 没有待处理的行内审查评论、审查正文,也没有仍然失败的 build/test/lint 检查。

验证

本轮未修改任何代码,因此未运行 build、typecheck、lint 或测试命令。仅通过检查确认仓库状态:

  • git diff origin/main...HEAD —— 1 个文件,新增 3 行(packages/cli/src/commands/serve.test.ts),与 PR 的第二个提交一致。
  • git status --short —— 工作区干净。
  • git show d5bcf28293:packages/cli/src/commands/serve.test.ts —— 确认 merge base 已包含 pairing 阶段的静止等待(经 fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362 合入),因此第一个提交的修复已保留在 main 上,PR 剩余的 diff 恰好就是 authenticated-open 的静止等待。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.23.2

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🔀 Base updated: red check(s) [review-pr] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [review-pr] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix round: no action

What was triaged

Feedback newer than the last evaluation (2026-09-09T20:49:45Z) contains no actionable code findings:

  • Inline review comments: none.
  • Review bodies: none.
  • Issue-level comments: two automated notices from @qwen-code-ci-bot reporting that the Qwen Pull Request Review workflow timed out after 5400 seconds (the 90-minute budget), on runs 34450828377 and 34465471604. These are infrastructure notices, not review feedback on the diff.
  • Failed checks: review-pr (FAILURE) — this is the same "🧐 Qwen Pull Request Review" workflow run (34465471604) that the second bot comment reports as timed out. Confirmed against the check-run payload: every substantive CI gate (build, lint, unit/integration tests, TUI parity, freshness guards) is green; only the review job failed, by timeout.

Why no code change

The diff is three lines in one test file (packages/cli/src/commands/serve.test.ts), and the only failing check is the review workflow exhausting its own time budget — a CI-infrastructure condition, not a defect in this PR. The bot's suggested remedy (commenting @qwen-code /review --timeout=360 to retry with a longer budget) is a maintainer-triggered action requiring GitHub credentials; this round has none, and modifying CI configuration is outside the permitted scope of an autofix round.

There is therefore nothing to fix, decline, or defer in code. The branch is left exactly as it was; no commit was created.

Verification (sanity check of current branch state)

  • npm run build — passed.
  • npx vitest run src/commands/serve.test.ts (in packages/cli) — 70/70 tests passed, including the applies authenticated open before the yargs path starts the daemon test this PR hardens.
中文说明

Autofix 本轮:无需改动

分类结果

上次评估(2026-09-09T20:49:45Z)之后的新反馈中没有任何可执行的代码意见:

  • 行内评审评论: 无。
  • 评审正文: 无。
  • Issue 级评论: 两条来自 @qwen-code-ci-bot 的自动通知,报告 Qwen Pull Request Review 工作流在 5400 秒(90 分钟预算)后超时,分别对应运行 34450828377 和 34465471604。这些是基础设施通知,不是针对 diff 的评审意见。
  • 失败的检查: review-pr(FAILURE)——这正是第二条 bot 通知中报告超时的同一个"🧐 Qwen Pull Request Review"工作流运行(34465471604)。已对照 check-run 数据确认:所有实质性 CI 门禁(构建、lint、单元/集成测试、TUI 一致性、新鲜度检查)均为绿色;只有评审任务因超时失败。

为什么不改动代码

本 PR 的 diff 只有一个测试文件中的三行(packages/cli/src/commands/serve.test.ts),唯一失败的检查是评审工作流自身耗尽其时间预算——这是 CI 基础设施状况,不是本 PR 的缺陷。bot 建议的补救措施(评论 @qwen-code /review --timeout=360 以更长预算重试)是需要 GitHub 凭据的维护者操作;本轮没有凭据,且修改 CI 配置超出 autofix 轮次的允许范围。

因此,代码中没有需要修复、拒绝或暂缓的事项。分支保持原样,未创建任何提交。

验证(对当前分支状态的健全性检查)

  • npm run build —— 通过。
  • npx vitest run src/commands/serve.test.ts(在 packages/cli 中运行)—— 70/70 测试全部通过,包括本 PR 加固的 applies authenticated open before the yargs path starts the daemon 测试。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.23.2

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🔀 Base updated: red check(s) [review-pr] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [review-pr] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

wenshao pushed a commit to wenshao/qwen-code that referenced this pull request Sep 10, 2026
@wenshao

wenshao commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Local re-verification at the current head — PR #11417 (round 3)

My two earlier reports on this thread tested heads a2420452fd and e7e2ecdf20. Since then #11362 has landed on main and this PR's effective diff has changed, so I rebuilt the environment against head 6ca85e0b62 on today's main and re-ran everything the change touches.

For the merge decision:

  • The code is fine to merge. The 3 lines that would merge today are correct and cannot hang. Of the three wait targets I tried, only the one the PR uses closes the race: two mutants that wait on an earlier side effect still leak.
  • It hardens a latent hazard, not a live one. On the file as it is on main, an ordering probe shows the leaked call cannot reach the next test. 27 unmodified runs (12 idle, 15 with every core saturated) never triggered it. It only fires when an async stall of roughly one vi.waitFor poll (≥ ~50 ms) is injected into that exact window.
  • The PR description no longer matches the diff. The change it describes is already on main via fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362. Its own Reviewer Test Plan now gives the same result on both arms. Fixes #11414 points at an issue that was closed for a different root cause. The body needs fixing before merge; the code needs no change.

What changed since my last report

what would merge today

Environment

  • macOS 26.6.2 arm64 (Apple M1 Max, 10 cores), Node v24.18.1, npm 11.16.0, vitest 3.2.7. npm run build exit 0.
  • BASE = origin/main @ 07b1cd033e. PR = 07b1cd033e + the PR patch. The tested serve.test.ts blob is f151086f7b, the same blob as in git merge-tree --write-tree origin/main 6ca85e0b62. So the PR arm is exactly what a squash merge would produce. main has since moved to c46cb85cf2, but serve.ts and serve.test.ts are unchanged there, so both arms still match.
  • A/B method: one worktree, arms swapped by file. Each harness edit is applied identically to both arms by a generator that aborts unless every anchor it edits matches exactly once. Runs use npx vitest run <file> --coverage.enabled=false from packages/cli.
  • The unmodified PR arm passes 70/70. eslint --max-warnings 0 and prettier --check on the changed file are both clean.

1. The PR's own Reviewer Test Plan no longer tells the arms apart

I applied the body's recipe to both arms: the leaker's Local Control enable() resolves after 100 ms, the victim's after 1000 ms. I added a positive control to prove the recipe was applied correctly.

arm exit result
BASE (main) 0 70 passed
PR 0 70 passed
control: main minus #11362's 3 lines 1 closes the daemon when pairing output fails times out at 15000 ms, plus unhandled Error: process.exit(1) called

The hazard the body describes is exactly the one #11362 already closed. Following the body's instructions today cannot validate this PR.

2. The hunk that would actually merge: forced-race A/B and mutants

applies authenticated open … (test A) returns as soon as runQwenServe has been called, while its handler still has await handle.runtimeReady → openBrowserSecurely(...) ahead of it (serve.ts:151, :183). The next test, prints the authenticated manual URL on the yargs headless path (test B), ends with expect(mockOpenBrowserSecurely).not.toHaveBeenCalled() (serve.test.ts:457 on main). So the leaked call lands directly on test B's last assertion.

To force the race, test A's runtimeReady resolves after 100 ms and test B's after 400 ms, on every arm:

arm exit result
BASE (main) 1 test B: AssertionError: expected "spy" to not be called at all, but actually been called 1 times
PR 0 70 passed
mutant M1: PR, but wait on mockApplyOpenWithAuth 1 same failure as BASE
mutant M2: PR, but wait on mockShouldLaunchBrowser 1 same failure as BASE

A probe log confirms the timeline. On BASE, test A returns with 0 calls and the call lands inside test B. On PR, the call lands inside test A (≈104 ms in), and A returns with 1 call. Both mutants wait on a side effect that happens before await handle.runtimeReady, so both still leak. Only the final side effect closes the window, and that is the one the PR waits on.

A/B matrix

3. Can it fire without an injected delay? Not on the current file

  • Ordering probe (BASE, no timing changes). Inside test A's runQwenServe mock I logged the openBrowserSecurely call count at that moment (0). I also queued a setImmediate and a setTimeout(0) there; both see 1 when they run.
    • So the whole runQwenServe → openBrowserSecurely chain finishes within a single microtask drain.
    • vi.waitFor polls on a timer (interval = 50, node_modules/vitest/dist/chunks/vi.bdSIJ99Y.js:3709), so it can never see "runQwenServe called" before openBrowserSecurely has also been called.
    • GC pauses or CPU starvation delay the chain and the poll timer alike; they cannot reorder them. For this hunk, that refines §3 of my previous report.
  • Threshold on BASE (test A's runtimeReady = D ms, test B at 400 ms):
    • D = 0: pass. D = 30: pass. In both, the call still lands inside test A.
    • D = 70: fail, with the call landing inside test B.
    • The leak needs an async stall long enough to outlive test A's remaining waitFor poll, i.e. up to 50 ms.
  • Unmodified file under stress (BASE):
    • idle: 12/12 passed.
    • 10 yes hogs on 10 cores: 15/15 passed, 0 unhandled errors.
    • --sequence.shuffle, seeds 1–8: BASE 8/8 and PR 8/8.
    • The shuffle runs also show the new wait does not hang when test order changes. mockShouldLaunchBrowser is flipped to false by a later test, and vi.restoreAllMocks() resets it.

So this is defensive hardening that matches the neighbouring pattern. It becomes load-bearing the moment that window gains a real async step: a production await before the browser open, or a mock whose runtimeReady is genuinely async.

reachability

4. CI at this head

Every non-skipped check on 6ca85e0b62 passes: Test (ubuntu-latest, Node 22.x), Lint & Static, Integration Tests (no-AK), web-shell E2E Smoke, review-pr, and the OpenTUI gates. The macOS and Windows Test jobs are skipped on this PR. This round's local runs cover macOS.

Merge reference

  1. Fix the PR body before merging. The code needs no change.
    • Rewrite "What this PR does" around the browser-open wait in applies authenticated open before the yargs path starts the daemon.
    • Drop both Fixes #11414 lines and the paragraph that attributes run 34289483217 to this hazard.
    • Replace the Reviewer Test Plan with the §2 recipe (test A runtimeReady ≥ 70 ms, test B 400 ms → BASE fails on test B's not.toHaveBeenCalled(), PR passes).
    • Nit: a test-only change fits test(cli): better than fix(cli):.
  2. Merge gate. reviewDecision is CHANGES_REQUESTED and mergeStateStatus is BLOCKED. The visible blocker is qwen-code-ci-bot's review at a2420452fd (R1-1, which is about the body); only that reviewer's own approval or a dismissal clears it. main's ruleset requires 1 approval (a maintainer approval is on record, and stale approvals are not dismissed on push) and does not require thread resolution. .github/CODEOWNERS has no rule covering packages/cli/. So once the body is fixed, dismiss that stale bot review (or admin-merge). I could not evaluate the ruleset's require_extra_approval_for_unattributed_changes flag from outside; it may ask for one more approval, since 10 of the 11 commits on this branch are authored by qwen-code-dev-bot.
  3. Close fix(cli): quiesce serve test leak and sync capability doc count (#11363) #11376. Its diff is now empty.
  4. Follow-up, unchanged from my previous report, not a blocker. This is the third hand-placed wait for the same pattern. Moving the quiescence into startServeHandlerWithArgs, or adding an afterEach that fails when a shared mock is called after the test body returns, would stop a fourth occurrence structurally.

Not validated

  • Linux and Windows in this round. My earlier reports ran the Linux A/B at older heads, and CI's Test (ubuntu-latest) is green at this head.
  • A real CI occurrence of this hazard. Per §3, it cannot occur on the current file without a code change in that window.
  • The coverage/.tmp ENOENT flake that the body puts out of scope.
中文说明

当前 head 的本地复验 —— PR #11417(第 3 轮)

我在本线程之前的两份报告测的是 head a2420452fd 和 e7e2ecdf20。此后 #11362 已合入 main,本 PR 的实际 diff 也变了,所以我基于今天的 main 针对 head 6ca85e0b62 重建了环境,把这个改动涉及的内容全部重跑了一遍。

供合并决策参考:

  • 代码可以合。 今天会被合入的这 3 行是正确的,不会挂死。我试的三个等待目标里,只有 PR 用的这个能关掉竞态:两个改成等更早副作用的变异体仍然泄漏。
  • 它加固的是潜在隐患,而不是正在发生的问题。 以 main 上的文件现状,时序探针表明泄漏调用到不了下一个测试。27 次未改动运行(12 次空闲、15 次全部核心占满)一次都没触发。只有往那个特定窗口里注入约一个 vi.waitFor 轮询周期(≥ 约 50 ms)的异步停顿,它才会触发。
  • PR 描述与 diff 已经对不上。 它描述的改动已经通过 fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362 在 main 上了。它自己的 Reviewer Test Plan 现在在两臂上结果相同。Fixes #11414 指向的 issue 已因另一个根因关闭。合并前需要修正描述;代码无需改动。

自上次报告以来的变化

今天会合入什么

环境

  • macOS 26.6.2 arm64(Apple M1 Max,10 核),Node v24.18.1,npm 11.16.0,vitest 3.2.7。npm run build 退出码 0。
  • BASE = origin/main @ 07b1cd033e。PR = 07b1cd033e + PR 补丁。实测的 serve.test.ts blob 是 f151086f7b,与 git merge-tree --write-tree origin/main 6ca85e0b62 结果中的 blob 相同。所以 PR 臂就是 squash 合并会产出的内容。main 此后已前进到 c46cb85cf2,但 serve.ts 和 serve.test.ts 均未改动,两臂依然对应。
  • A/B 方法: 同一个 worktree,按文件切换两臂。每处 harness 改动都由生成器等同地施加到两臂,生成器要求每个被改锚点恰好匹配一次,否则直接报错。运行命令是在 packages/cli 下 npx vitest run <file> --coverage.enabled=false。
  • 未改动的 PR 臂 70/70 通过。对改动文件跑 eslint --max-warnings 0 和 prettier --check 均干净。

1. PR 自己的 Reviewer Test Plan 已无法区分两臂

我把描述里的方法施加到两臂:泄漏方的 Local Control enable() 延迟 100 ms 解析,受害方延迟 1000 ms。另加一个阳性对照,证明方法确实施加正确。

臂 退出码 结果
BASE(main) 0 70 passed
PR 0 70 passed
对照:main 去掉 #11362 的 3 行 1 closes the daemon when pairing output fails 在 15000 ms 超时,另有未处理的 Error: process.exit(1) called

描述里写的隐患,正是 #11362 已经关掉的那个。今天照描述去做,无法验证本 PR。

2. 真正会被合入的那处 hunk:强制竞态 A/B 与变异体

applies authenticated open …(测试 A)在 runQwenServe 被调用后就立刻返回,而它的 handler 后面还有 await handle.runtimeReady → openBrowserSecurely(...) 没执行(serve.ts:151、:183)。紧接着的测试 prints the authenticated manual URL on the yargs headless path(测试 B)以 expect(mockOpenBrowserSecurely).not.toHaveBeenCalled() 结尾(main 上 serve.test.ts:457)。所以泄漏的调用正好砸在测试 B 的最后一个断言上。

为强制触发竞态,所有臂都让测试 A 的 runtimeReady 延迟 100 ms 解析、测试 B 的延迟 400 ms:

臂 退出码 结果
BASE(main) 1 测试 B:AssertionError: expected "spy" to not be called at all, but actually been called 1 times
PR 0 70 passed
变异体 M1:PR,但改为等待 mockApplyOpenWithAuth 1 与 BASE 相同的失败
变异体 M2:PR,但改为等待 mockShouldLaunchBrowser 1 与 BASE 相同的失败

探针日志印证了时间线。BASE 上,测试 A 返回时调用次数为 0,调用落在测试 B 内。PR 上,调用落在测试 A 内(约第 104 ms),A 返回时调用次数为 1。两个变异体等的都是发生在 await handle.runtimeReady 之前的副作用,所以都仍然泄漏。只有最后那个副作用能关掉窗口,而 PR 等的正是它。

A/B 矩阵

3. 不注入延迟能触发吗?在当前文件上不能

  • 时序探针(BASE,不改任何时序)。 我在测试 A 的 runQwenServe mock 里记录了那一刻 openBrowserSecurely 的调用次数(0)。同时在那里排入一个 setImmediate 和一个 setTimeout(0);两者执行时看到的都是 1。
    • 所以 runQwenServe → openBrowserSecurely 整条链在同一次微任务清空内就跑完了。
    • vi.waitFor 是靠定时器轮询的(interval = 50,node_modules/vitest/dist/chunks/vi.bdSIJ99Y.js:3709),所以它永远不可能在 openBrowserSecurely 还没被调用时就看到"runQwenServe 已调用"。
    • GC 暂停或 CPU 饥饿会同等地推迟这条链和轮询定时器,无法改变二者的先后。对这处 hunk 而言,这修正细化了我上一份报告的第 3 节。
  • BASE 上的阈值(测试 A 的 runtimeReady = D ms,测试 B 为 400 ms):
    • D = 0:通过。D = 30:通过。两者调用都仍落在测试 A 内。
    • D = 70:失败,调用落在测试 B 内。
    • 泄漏需要一个足够长的异步停顿,长到熬过测试 A 剩余的 waitFor 轮询,即最多 50 ms。
  • 未改动文件的压力运行(BASE):
    • 空闲:12/12 通过。
    • 10 核上开 10 个 yes 占满:15/15 通过,0 个未处理错误。
    • --sequence.shuffle,种子 1–8:BASE 8/8,PR 8/8。
    • 打乱顺序的运行也说明新等待在测试顺序变化时不会挂死。mockShouldLaunchBrowser 会被后面的测试翻成 false,而 vi.restoreAllMocks() 会把它重置回来。

所以这是与相邻测试一致的防御性加固。一旦那个窗口里出现真正的异步步骤,它就会变成承重件:比如生产代码在打开浏览器前多一个 await,或者 mock 的 runtimeReady 变成真正异步。

可达性

4. 当前 head 的 CI

6ca85e0b62 上所有未跳过的检查都通过:Test (ubuntu-latest, Node 22.x)、Lint & Static、Integration Tests (no-AK)、web-shell E2E Smoke、review-pr,以及 OpenTUI 相关门禁。本 PR 上 macOS 与 Windows 的 Test job 被跳过。本轮本地运行覆盖了 macOS。

合并参考

  1. 合并前修正 PR 描述。 代码无需改动。
    • 把"本 PR 做什么"改写为围绕 applies authenticated open before the yargs path starts the daemon 里的浏览器打开等待。
    • 删掉两处 Fixes #11414,以及把运行 34289483217 归因于此隐患的那段。
    • 把 Reviewer Test Plan 换成第 2 节的方法(测试 A runtimeReady ≥ 70 ms、测试 B 400 ms → BASE 在测试 B 的 not.toHaveBeenCalled() 上失败,PR 通过)。
    • 小问题:仅测试的改动用 test(cli): 比 fix(cli): 更合适。
  2. 合并门禁。 reviewDecision 为 CHANGES_REQUESTED,mergeStateStatus 为 BLOCKED。可见的拦截来自 qwen-code-ci-bot 在 a2420452fd 上的 review(R1-1,内容是关于描述的);只有该 reviewer 自己批准或被 dismiss 才能清除。main 的规则集要求 1 个批准(已有维护者批准,且 push 不会作废旧批准),且不要求解决评审线程。.github/CODEOWNERS 没有覆盖 packages/cli/ 的规则。所以描述修正后,dismiss 那条过时的 bot review(或管理员合并)即可。规则集里的 require_extra_approval_for_unattributed_changes 我无法从外部判定;由于这个分支 11 个提交里有 10 个由 qwen-code-dev-bot 提交,它可能还会要求再多一个批准。
  3. 关闭 fix(cli): quiesce serve test leak and sync capability doc count (#11363) #11376。 它的 diff 已为空。
  4. 后续项,与上一份报告相同,不阻塞合并。 这已是同一模式下第三处手工补的等待。把静默逻辑收进 startServeHandlerWithArgs,或加一个 afterEach,在测试体返回后若有共享 mock 被调用就判失败,就能从结构上杜绝第四次。

未验证部分

  • 本轮未测 Linux 与 Windows。我之前的报告在较早的 head 上跑过 Linux A/B,且本 head 上 CI 的 Test (ubuntu-latest) 是绿的。
  • 该隐患在真实 CI 中的发生。按第 3 节,当前文件不在那个窗口里改代码就不会发生。
  • 描述中列为超出范围的 coverage/.tmp ENOENT flake。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

🤖 AutoFix round report — no code change; the one actionable item (rewriting the PR body) is a GitHub-side edit this loop cannot perform, so it is escalated here with a ready-to-paste replacement.

Feedback triaged

Dispositions

  1. "The code is fine to merge" — agreed; no code change made. The forced-race A/B plus the two mutants show the 3-line wait targets the only side effect that closes the cross-test leak window, and on the current file the leak cannot fire without an injected ≥ ~50 ms async stall — so this is defensive hardening matching the neighbouring pattern, not a live-hazard fix. Re-verified first-hand this round: the focused suite passes 70/70 (see Verification). Any additional code would be bloat.
  2. "Fix the PR body before merging" — escalated: needs a human. The autofix agent runs with no GitHub credentials, and the address-review workflow has no PR-body/title edit step (the body is written exactly once, by gh pr create --body-file, at PR creation; the round flow contains no gh pr edit). Modifying the workflow to add one would itself be a maintainer decision about CI machinery. A ready-to-paste replacement title and body, built to the maintainer's four instructions (rewrite the summary around the browser-open wait; drop both Fixes #11414 lines and the run-34289483217 paragraph; replace the Reviewer Test Plan with the forced-race recipe; retitle as test(cli):), are provided below.
  3. Merge-gate mechanics — maintainer actions, noted for the record. (a) The visible merge blocker is qwen-code-ci-bot's CHANGES_REQUESTED review at a2420452fd (R1-1, which is about the body); only that reviewer's own approval or a dismissal clears it. (b) Close fix(cli): quiesce serve test leak and sync capability doc count (#11363) #11376 — its diff is now +0 −0. (c) The ruleset's require_extra_approval_for_unattributed_changes flag could not be evaluated from outside; 10 of the 11 branch commits are bot-authored, so one more approval may be required.
  4. Structural follow-up — verified real, deferred to the follow-up queue. Confirmed in the file: serve.test.ts now carries multiple hand-placed quiescence waits for fire-and-forget serve-handler phases (QR/pairing at ~L551, ~L596, ~L635 — the last from fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362 — and browser-open at ~L571 plus this PR's ~L434). Centralizing the quiescence into startServeHandlerWithArgs, or adding an afterEach that fails when a shared mock is called after the test body returns, would stop a fourth occurrence structurally — but that restructures the shared harness and touches tests far outside this PR's 3-line footprint. Recorded in deferred-findings.json so it lands in the per-PR "Deferred review findings" issue rather than being lost at merge.
  5. Title nit — folded into the escalation. A test-only change fits test(cli): better than fix(cli):; the suggested replacement title is below. Retitling is the same GitHub-side edit the loop cannot perform.

Suggested replacement PR title

test(cli): wait out the serve handler's browser-open phase in the authenticated-open test

Suggested replacement PR body (ready to paste)

## What this PR does

Adds a quiescence wait to the "applies authenticated open before the yargs path starts the daemon" serve-command test. That test's fire-and-forget handler still has the runtime-ready await and the secure browser-open call ahead of it when the test body returns, and the next test ends by asserting the browser-open mock was never called — so a delayed browser-open call from the first test would land directly on the second test's final assertion. The new wait blocks the first test on the handler's final side effect (the browser-open call itself) before it returns, closing the cross-test leak window. Mutant testing showed that waiting on either earlier side effect (the auth-application mock or the browser-launch-decision mock) still leaks; only the final effect closes the window.

## Why it's needed

Defensive hardening against cross-test mock leakage, matching the quiescence pattern the neighbouring tests already use for the QR/pairing phase. On the current file the whole chain from the serve invocation to the browser open completes within a single microtask drain, so the leak cannot fire today without a code change in that window — but the moment the window gains a real async step (a production await before the browser open, or a genuinely asynchronous runtime-ready mock), the leaked call would flake the next test's never-called assertion.

## Reviewer Test Plan

### How to verify

Force the race in packages/cli/src/commands/serve.test.ts on both arms (main, and main plus this PR): make the first test's runtime-ready promise resolve after ≥ ~70 ms (e.g. 100 ms) and the next test's after 400 ms, then run npx vitest run src/commands/serve.test.ts --coverage.enabled=false from packages/cli. Expected: on BASE the next test fails with "expected spy to not be called at all, but actually been called 1 times", because the leaked browser-open call lands inside it; on the PR arm all 70 tests pass, because the call lands inside the first test before it returns. Unmodified, both arms pass 70/70, and --sequence.shuffle with seeds 1–8 passes on both arms, confirming the new wait does not hang when test order changes.

### Evidence (Before & After)

N/A — test-only change; the forced-race recipe above is the evidence.

### Tested on

|     OS     |               Status               |
| :--------: | :--------------------------------: |
|  🍏 macOS  |        ✅ tested (70/70 pass)       |
| 🪟 Windows |           ⚠️ not tested            |
|  🐧 Linux  | ✅ CI Test (ubuntu-latest, 22.x)    |

### Environment (optional)

N/A — unit tests only (vitest).

## Risk & Scope

- Main risk or tradeoff: none identified; test-only, and the wait targets an unconditional side effect on this path, so it cannot hang (vitest waitFor would time out loudly if the path regressed).
- Not validated / out of scope: local Windows run; the coverage/.tmp ENOENT flake is unrelated and unchanged.
- Breaking changes / migration notes: none.

## Linked Issues

Refs #11362 (sibling QR/pairing quiescence wait, already merged). Originally opened for #11414; that issue was closed for a different root cause, so this PR intentionally does not claim to fix it.

<details>
<summary>中文说明</summary>

## 本 PR 做了什么

在 "applies authenticated open before the yargs path starts the daemon" 这个 serve 命令测试中新增一处静默等待。该测试的 fire-and-forget handler 在测试体返回时,仍有 runtime-ready 的 await 和安全打开浏览器的调用未执行,而下一个测试以断言浏览器打开 mock 从未被调用结尾——因此第一个测试里延迟的浏览器打开调用会直接落在第二个测试的最后一个断言上。新等待让第一个测试在返回前阻塞在 handler 的最后一个副作用(浏览器打开调用本身)上,关闭跨测试泄漏窗口。变异体测试表明,等待两个更早副作用(鉴权应用 mock 或浏览器启动决策 mock)中的任何一个仍然会泄漏;只有最后一个副作用能关闭窗口。

## 为什么需要

这是针对跨测试 mock 泄漏的防御性加固,与相邻测试用于 QR/配对阶段的静默等待模式一致。以当前文件而言,从 serve 调用到打开浏览器的整条链在一次微任务清空内即可完成,所以今天不在该窗口内改动代码就不会触发泄漏——但一旦该窗口出现真正的异步步骤(生产代码在打开浏览器前多一个 await,或 runtime-ready mock 变为真正异步),泄漏的调用就会使下一个测试的"从未调用"断言抖动。

## 评审者测试计划

### 如何验证

在两臂(main,以及 main 加本 PR)的 packages/cli/src/commands/serve.test.ts 中强制触发竞态:让第一个测试的 runtime-ready promise 在 ≥ 约 70 ms(例如 100 ms)后解析、下一个测试的在 400 ms 后解析,然后在 packages/cli 下运行 npx vitest run src/commands/serve.test.ts --coverage.enabled=false。预期:BASE 上后一个测试失败,报 "expected spy to not be called at all, but actually been called 1 times",因为泄漏的浏览器打开调用落入了该测试;PR 臂上 70 个测试全部通过,因为该调用在第一个测试返回前已落入其中。不做改动时两臂均为 70/70 通过,且 --sequence.shuffle 种子 1–8 在两臂都通过,说明新等待在测试顺序变化时不会挂死。

### 证据(前后对比)

N/A —— 仅测试改动;上述强制竞态方法即证据。

### 测试平台

|     操作系统     |               状态               |
| :--------: | :--------------------------------: |
|  🍏 macOS  |        ✅ 已测试(70/70 通过)       |
| 🪟 Windows |           ⚠️ 未测试            |
|  🐧 Linux  | ✅ CI Test(ubuntu-latest,22.x)    |

### 环境(可选)

N/A —— 仅单元测试(vitest)。

## 风险与范围

- 主要风险或取舍:未发现;仅测试改动,且等待目标在该路径上是无条件副作用,因此不会挂死(若该路径回归,vitest waitFor 会明显超时)。
- 未验证 / 超出范围:本地 Windows 运行;coverage/.tmp ENOENT flake 与本 PR 无关且未改动。
- 破坏性变更 / 迁移说明:无。

## 关联 Issue

Refs #11362(同源的 QR/配对静默等待,已合入)。本 PR 最初因 #11414 而开;该 issue 已因另一根因关闭,因此本 PR 有意不声称修复它。

</details>

Verification

  • npm run build — passed (exit 0; required once by the vitest globalSetup guard before package-local tests can run).
  • cd packages/cli && npx vitest run src/commands/serve.test.ts --coverage.enabled=false — 70/70 passed (Test Files 1 passed).
  • No lint/typecheck run and no commit made: no file changed this round (working tree clean; HEAD 6ca85e0b62 unchanged). The round's only outputs are this report and deferred-findings.json.
  • External evidence relied on for the "code is correct" disposition: the maintainer's round-3 forced-race A/B and mutant runs at this exact head (BASE fails test B's not.toHaveBeenCalled() under the injected 100 ms/400 ms race; PR passes; both earlier-side-effect mutants still leak), plus green CI at 6ca85e0b62.
中文说明

🤖 AutoFix 本轮报告 —— 无代码改动;唯一可执行项(重写 PR 描述)是本循环无权执行的 GitHub 侧编辑,因此在此升级移交,并附上可直接粘贴的替换文本。

已分诊的反馈

各项处置

  1. "代码可以合" —— 同意;未做代码改动。 强制竞态 A/B 加两个变异体表明,这 3 行等待所等的副作用是唯一能关闭跨测试泄漏窗口的;以当前文件现状,不注入 ≥ 约 50 ms 的异步停顿泄漏就不会触发——因此这是与相邻模式一致的防御性加固,而非在修正在发生的问题。本轮已亲自复验:聚焦测试套件 70/70 通过(见"验证"节)。任何额外代码都是冗余。
  2. "合并前修正 PR 描述" —— 升级移交:需要人工。 autofix agent 没有 GitHub 凭据,且 address-review 工作流没有修改 PR 描述/标题的步骤(描述只在 PR 创建时由 gh pr create --body-file 写入一次;轮次流程中没有任何 gh pr edit)。而为工作流增加该步骤本身属于维护者对 CI 机制的决策。下方提供了按维护者四条指示(围绕浏览器打开等待重写"本 PR 做什么";删掉两处 Fixes #11414 及归因 run 34289483217 的段落;用强制竞态方法替换评审者测试计划;标题改为 test(cli):)准备好的可直接粘贴的替换标题与正文。
  3. 合并门禁事项 —— 维护者操作,记录在案。 (a) 当前可见的合并拦截来自 qwen-code-ci-bot 在 a2420452fd 上的 CHANGES_REQUESTED 评审(R1-1,内容关于描述);只有该评审者自己批准或被 dismiss 才能清除。(b) 关闭 fix(cli): quiesce serve test leak and sync capability doc count (#11363) #11376 —— 其 diff 现为 +0 −0。(c) 规则集的 require_extra_approval_for_unattributed_changes 标志无法从外部判定;本分支 11 个提交中 10 个为 bot 提交,可能还需一个批准。
  4. 结构性后续项 —— 已核实为真,转入后续队列。 已在文件中确认:serve.test.ts 现有多个为 fire-and-forget serve-handler 各阶段手工放置的静默等待(QR/配对的约在 L551、L596、L635——最后一处来自 fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362——以及浏览器打开的约在 L571 和本 PR 的 L434)。把静默逻辑收进 startServeHandlerWithArgs,或添加一个"测试体返回后共享 mock 被调用即判失败"的 afterEach,能从结构上杜绝第四次出现——但那会重构共享 harness,波及的测试远超本 PR 的 3 行范围。已记录到 deferred-findings.json,使其进入该 PR 的"Deferred review findings" issue,不会在合并时丢失。
  5. 标题小建议 —— 并入上述升级项。 仅测试的改动用 test(cli): 比 fix(cli): 更合适;建议的替换标题见下。改标题同样是本循环无权执行的 GitHub 侧编辑。

建议的替换 PR 标题

test(cli): wait out the serve handler's browser-open phase in the authenticated-open test

建议的替换 PR 正文(可直接粘贴)

见上方英文区块中的 markdown 代码块——该草案本身已按仓库模板双语齐备(内含完整中文折叠块),可直接整体粘贴。

验证

  • npm run build —— 通过(退出码 0;vitest globalSetup 守卫要求先构建一次,包内单测才能运行)。
  • cd packages/cli && npx vitest run src/commands/serve.test.ts --coverage.enabled=false —— 70/70 通过(Test Files 1 passed)。
  • 本轮未运行 lint/typecheck、未提交:没有任何文件改动(工作区干净;HEAD 保持 6ca85e0b62)。本轮产物仅有本报告与 deferred-findings.json。
  • "代码正确"这一处置所依赖的外部证据:维护者在该精确 head 上的第 3 轮强制竞态 A/B 与变异体运行(注入 100 ms/400 ms 竞态时 BASE 在测试 B 的 not.toHaveBeenCalled() 上失败;PR 通过;两个等更早副作用的变异体仍泄漏),以及 6ca85e0b62 上全绿的 CI。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.23.3

@wenshao

wenshao commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

1 similar comment
@wenshao

wenshao commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix round — no action needed

No actionable feedback arrived since the last evaluation (2026-09-10T20:02:46Z).

Feedback triage:

  • Reviews: none.
  • Inline comments: none.
  • Issue-level comments: one entry ([ic:5627847337]) is the CI bot's status note that sandboxed verification is running. It is a progress notification, not review feedback, so there is nothing to address in code. The verification report will arrive as a new comment and can be triaged in a later round if it contains findings.
  • Failed checks: none.
  • Still-red checks: none.
  • Base conflict: none (--conflict false); no merge was performed.

Sanity check performed: the PR's net diff vs main is a 3-line test-only change in packages/cli/src/commands/serve.test.ts that waits out the fire-and-forget serve handler's browser-open phase. I ran the focused test file to confirm the branch is healthy:

  • npm run build — passed (needed to build workspace dist/ outputs before package-local tests could run).
  • npx vitest run src/commands/serve.test.ts (in packages/cli) — 1 file passed, 70/70 tests passed.

No commits were made and the working tree remains clean.

中文说明

Autofix 本轮 —— 无需处理

自上次评估(2026-09-10T20:02:46Z)以来没有收到可处理的反馈。

反馈分类:

  • 评审(Reviews): 无。
  • 行内评论(Inline comments): 无。
  • PR 级评论(Issue-level comments): 有一条([ic:5627847337]),是 CI 机器人发布的“沙箱验证正在运行”的状态通知。它只是进度提示,不是评审反馈,因此代码层面无需处理。验证报告会以新评论的形式发布,如其中包含发现,可在后续轮次中处理。
  • 失败的检查: 无。
  • 持续失败的检查: 无。
  • 与 base 分支的冲突: 无(--conflict false);未执行合并。

已执行的健全性检查: 该 PR 相对 main 的净 diff 是 packages/cli/src/commands/serve.test.ts 中仅 3 行的测试改动,用于等待 fire-and-forget serve handler 的浏览器打开阶段完成。我运行了该聚焦测试文件以确认分支健康:

  • npm run build —— 通过(需要先构建各 workspace 包的 dist/ 产物,包内测试才能运行)。
  • npx vitest run src/commands/serve.test.ts(在 packages/cli 下)—— 1 个文件通过,70/70 个测试全部通过。

本轮未创建任何提交,工作区保持干净。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.23.3

wenshao pushed a commit to wenshao/qwen-code that referenced this pull request Sep 11, 2026
@wenshao

wenshao commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification — real local A/B on this branch

I built a real environment for this PR (worktree at 6ca85e0b62, full npm ci including the postinstall package build, Node v22.22.2, vitest 3, Linux/16 cores) and A/B'd the change by adding and removing exactly its three lines in place.

Verdict: the code change is correct, well-targeted and safe to merge. The PR body, however, describes a different change than the one on the branch, and Fixes #11414 is not carried by this diff. Please update the description before merging.


What the branch actually contributes

The three-dot diff against main is 3 lines in applies authenticated open before the yargs path starts the daemon (packages/cli/src/commands/serve.test.ts:429), waiting on mockOpenBrowserSecurely.

The branch has two functional commits:

commit change net effect on main
a2420452fd mockQr.generate wait on forwards --token and --allow-origin none — main already has it via merged #11362 (10895031e2, 2026-09-09)
e7e2ecdf20 mockOpenBrowserSecurely wait on --open-with-auth this is what ships

shipped diff vs described change


Findings

1. (Important, docs) The body describes a change that is no longer in the diff

The body says "the forwards --token and --allow-origin Local Control test now waits for its fire-and-forget handler's pairing phase". The diff waits for the --open-with-auth test's browser-open phase. Different test, different mock, different phase.

That matters for review, because the Reviewer Test Plan is a recipe for the merged #11362 change, not for this one. I ran the body's recipe verbatim (Local Control enable() of the first test delayed ~100 ms, the next test's ~1000 ms):

So a reviewer who follows the stated plan verifies main, not this PR.

#11414 signature attribution

2. (Important) Fixes #11414 overstates what this diff does

The #11414 signature is an unattributed whole-run failure driven by an unhandled process.exit(1). The leak this diff closes cannot produce that signature: its only escape is openBrowserSecurely, and every failure mode of that call is caught by maybeOpenWebShellBrowser's own try/catch. I verified it directly — I made the leaked call consume a one-shot mockOpenBrowserSecurely.mockRejectedValueOnce(new Error('leak-boom')) installed by the next test, and the run reported:

qwen serve: failed to open browser: leak-boom. Please open this URL manually: http://127.0.0.1:4170/#token=generated-token
Tests  1 failed | 69 passed (70)      <- named victim test, no "Errors" line

No unhandled rejection, no process.exit. The worst case this leak can cause is a named test failure. The hazard #11414 actually named was already closed on main by #11362, and #11414 itself was closed manually on 2026-09-09, so the Fixes trailer is inert as well as inaccurate.

3. (Nit) On today's mocks the wait is a no-op — it is insurance, not the thing that turns CI green

startServeHandlerWithArgs anchors on mockRunQwenServe having been called. vi.waitFor's first check runs synchronously and always fails here (the handler awaits import('../serve/run-qwen-serve.js') before calling it); every later check is a 50 ms setInterval macrotask. By the time the anchor is observed, the whole downstream chain — runQwenServe → runtimeReady: Promise.resolve() → openBrowserSecurely — has already drained, because it is microtasks only.

Measured on the unpatched tree: openBrowserSecurely had already been called in 25/25 runs (15 idle + 10 under CPU oversubscription, load average ~38 on 16 cores) at the moment test A reaches its assertions. Suite cost is unchanged (tests 2.70s vs 2.72s).

clean baseline and leak-window measurement

4. The wait does do what it claims, and its scope is right

Forcing a genuine macrotask window between the anchor and the observable (test A's runtimeReady delayed 300 ms) makes the leak real, and the three lines close it — against the file's existing assertion, not a synthetic one:

  • without the PR: the leaked openBrowserSecurely lands inside prints the authenticated manual URL on the yargs headless path and breaks its expect(mockOpenBrowserSecurely).not.toHaveBeenCalled() → 1 failed | 69 passed;
  • with the PR: 70/70.

forced-race A/B

Two risk probes, both clean:

  • Can it hang? No. With the awaited call made unreachable (test A forced down the headless path), vi.waitFor gives up at its 1000 ms default with a named assertion failure. Bounded and attributed; worst case +1 s.
  • Is a sibling left leaky? No. The one structurally similar site — keeps Local Control pairing separate from the temporary primary token, also --open-with-auth, also anchored only on mockQr.generate — survives the same forced race, because startLocalControl awaits runtimeReady before the QR call the test anchors on, so nothing observable outlives that anchor. No further quiescence is needed in this file.

risk probes

5. (Housekeeping) #11376 is now empty

The other sibling named in the body, #11376, is an empty diff against main (compare main...autofix/issue-11363 → 0 files changed). It can be closed.


Local gates

gate result
npx vitest run src/commands/serve.test.ts (with PR) 70/70, 8/8 consecutive runs
npx vitest run src/commands/serve.test.ts (without PR) 70/70
npm run typecheck exit 0
npm run lint exit 0
npm run build exit 0
npx vitest run src/commands (whole directory, with PR) 170 files, 7098 passed | 19 skipped
PR CI green

Recommendation

Merge, after fixing the description. The change itself is a correct, bounded, well-scoped piece of test hardening with no measurable cost. But the record should match the code:

  1. rewrite "What this PR does" / "Why it's needed" to describe the --open-with-auth → openBrowserSecurely wait;
  2. replace the Reviewer Test Plan repro with one that exercises this diff (delay test A's runtimeReady, not Local Control's enable());
  3. drop Fixes #11414 to a plain reference — Main CI failed: Qwen Code CI on 422929b3a7df #11414's mechanism is already closed by fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362, and leaving the trailer attributes that fix to the wrong commit, which will mislead whoever investigates the next recurrence of this CI signature.

All terminal captures, both reports and the harness notes live on wenshao/qwen-code@assets-pr11417.

中文版(点击展开)

维护者验证 —— 在本分支上做了真实的本地 A/B

我为这个 PR 搭了真实环境(worktree 检出到 6ca85e0b62,完整 npm ci(含 postinstall 的包构建),Node v22.22.2,vitest 3,Linux / 16 核),并通过在原位增删这三行来做 A/B 对比。

结论:代码改动本身正确、定位准确、可以安全合并。但 PR 描述写的是另一个改动,而且 Fixes #11414 并不由这份 diff 兑现。 请在合并前先更新描述。


本分支实际贡献了什么

相对 main 的三点 diff 是 applies authenticated open before the yargs path starts the daemon(packages/cli/src/commands/serve.test.ts:429)中的 3 行,等待的是 mockOpenBrowserSecurely。

分支上有两个实质提交:

提交 改动 相对 main 的净效果
a2420452fd 在 forwards --token and --allow-origin 上等待 mockQr.generate 无 —— main 已经通过已合并的 #11362(10895031e2,2026-09-09)拥有它
e7e2ecdf20 在 --open-with-auth 上等待 mockOpenBrowserSecurely 这才是真正随本 PR 发布的改动

发现

1.(重要,文档问题)描述写的是已经不在 diff 里的改动

描述说「forwards --token and --allow-origin 这个 Local Control 测试现在会等待其 handler 的配对阶段」。而 diff 等待的是 --open-with-auth 测试的打开浏览器阶段。测试不同、mock 不同、阶段也不同。

这对评审有实际影响,因为 「审查者测试计划」是已合并的 #11362 的复现配方,不是本 PR 的。我逐字执行了描述里的配方(第一个测试的 Local Control enable() 延迟约 100 ms,下一个测试延迟约 1000 ms):

也就是说,按照描述里的步骤去验证,验证的是 main,不是这个 PR。

2.(重要)Fixes #11414 夸大了这份 diff 的作用

#11414 的签名是由未处理的 process.exit(1) 导致的、无归属的整轮运行失败。本 diff 所封堵的泄漏不可能产生该签名:它唯一的出口是 openBrowserSecurely,而这个调用的所有失败路径都被 maybeOpenWebShellBrowser 自己的 try/catch 接住了。我直接验证过 —— 让泄漏的调用去消费下一个测试安装的一次性 mockOpenBrowserSecurely.mockRejectedValueOnce(new Error('leak-boom')),运行结果是:

qwen serve: failed to open browser: leak-boom. Please open this URL manually: http://127.0.0.1:4170/#token=generated-token
Tests  1 failed | 69 passed (70)      <- 受害测试被点名,没有 "Errors" 行

没有未处理 rejection,也没有 process.exit。这个泄漏最坏只会造成一个被点名的测试失败。#11414 真正指向的隐患已经由 #11362 在 main 上关闭,而且 #11414 本身已于 2026-09-09 被手动关闭,所以这条 Fixes 既不准确也已失效。

3.(小问题)在当前的 mock 下,这个等待是空操作 —— 它是保险,不是让 CI 转绿的那一环

startServeHandlerWithArgs 的锚点是「mockRunQwenServe 已被调用」。vi.waitFor 的第一次检查是同步的、在这里必然失败(handler 在调用它之前 await 了 import('../serve/run-qwen-serve.js')),之后的每次检查都是 50 ms 的 setInterval 宏任务。等到锚点被观察到时,下游链路 —— runQwenServe → runtimeReady: Promise.resolve() → openBrowserSecurely —— 早已跑完,因为它全是微任务。

在未打补丁的树上实测:在测试 A 走到断言的那一刻,openBrowserSecurely 25/25 次运行都已经被调用过(15 次空载 + 10 次 CPU 超额订阅、16 核上负载约 38)。套件耗时没有变化(tests 2.70s vs 2.72s)。

4. 这个等待确实做到了它声称的事,范围也划得对

在锚点与可观测点之间强行制造一个真正的宏任务窗口(把测试 A 的 runtimeReady 延迟 300 ms),泄漏就真实发生,而这三行把它封住了 —— 打的是文件里已有的断言,不是我造的断言:

  • 不带本 PR:泄漏的 openBrowserSecurely 落进 prints the authenticated manual URL on the yargs headless path,打破它的 expect(mockOpenBrowserSecurely).not.toHaveBeenCalled() → 1 failed | 69 passed;
  • 带本 PR:70/70。

两项风险探针都干净:

  • 会不会挂住? 不会。把被等待的调用变成不可达(强制测试 A 走 headless 路径)后,vi.waitFor 在其 1000 ms 默认超时处放弃,并给出一个被点名的断言失败。有界且有归属,最坏多花 1 秒。
  • 是否漏掉了同类的兄弟测试? 没有。唯一结构相似的地方 —— keeps Local Control pairing separate from the temporary primary token,同样是 --open-with-auth、同样只锚在 mockQr.generate 上 —— 在同样的强制竞态下依然通过,因为 startLocalControl 在测试所锚定的 QR 调用之前就 await 了 runtimeReady,因此没有任何可观测行为能活过那个锚点。本文件不需要再补静默等待。

5.(清理)#11376 现在是空的

描述中提到的另一个兄弟 PR #11376,相对 main 已是空 diff(compare main...autofix/issue-11363 → 0 个文件改动),可以关闭。


本地门禁

门禁 结果
npx vitest run src/commands/serve.test.ts(带 PR) 70/70,连续 8/8 次
npx vitest run src/commands/serve.test.ts(不带 PR) 70/70
npm run typecheck exit 0
npm run lint exit 0
npm run build exit 0
npx vitest run src/commands(整个目录,带 PR) 170 个文件,7098 passed | 19 skipped
PR CI 全绿

建议

修好描述后合并。 改动本身是一处正确、有界、范围准确的测试加固,没有可测量的成本。但记录应当与代码一致:

  1. 重写「本 PR 做什么 / 为什么需要」,改为描述 --open-with-auth → openBrowserSecurely 的等待;
  2. 把「审查者测试计划」的复现步骤换成能真正触发本 diff 的那一个(延迟测试 A 的 runtimeReady,而不是 Local Control 的 enable());
  3. 把 Fixes #11414 降级为普通引用 —— Main CI failed: Qwen Code CI on 422929b3a7df #11414 的机制已由 fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362 关闭,保留这条 trailer 会把那次修复归到错误的提交上,误导下一个排查同类 CI 签名的人。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

AutoFix round — no code change (PR-body update requested, needs a maintainer)

Verdict

No code change this round. The only actionable feedback — @wenshao's maintainer verification (ic:5628332933) — explicitly found the code "correct, well-targeted and safe to merge" and asked for PR-description edits, which this loop cannot perform: the address-review mode has no GitHub credentials and no PR-body output mechanism (the workflow owns all GitHub writes, and pr-body.md is only consumed when a PR is first created). A paste-ready replacement body implementing all three recommendations is included below for a maintainer to apply.

Feedback dispositions

  • ic:5628332933 (@wenshao, maintainer verification):
    • Finding 1 (body describes the already-merged fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362 change, not this diff) + Finding 2 (Fixes #11414 overstates what this diff does) + Recommendations 1–3 → maintainer action requested — verified as accurate (see "Verified facts" below), but the fix is an edit to GitHub PR metadata, which needs a human with write access. Replacement body provided below.
    • Finding 3 (nit: on today's mocks the wait is a no-op) → declined (keep the wait) — the maintainer's own forced-race A/B shows the wait is load-bearing insurance: delaying the first test's runtimeReady by 300 ms makes the leak real, and the three lines close it (without: 1 failed | 69 passed via the existing expect(mockOpenBrowserSecurely).not.toHaveBeenCalled(); with: 70/70). Insurance against scheduling delays is the point of the change.
    • Finding 4 (the wait does what it claims; scope is right; the sibling test needs nothing) → confirmation, independently verified against the source; no action.
    • Finding 5 (fix(cli): quiesce serve test leak and sync capability doc count (#11363) #11376 is an empty diff against main and can be closed) → maintainer housekeeping — closing a PR requires GitHub credentials this mode does not have.
  • ic:5628599712 (ci-bot "verification is running") → status notice, not actionable feedback.

Verified facts (this round)

  • The three-dot diff against origin/main is exactly the 3-line mockOpenBrowserSecurely wait in applies authenticated open before the yargs path starts the daemon (packages/cli/src/commands/serve.test.ts:432-434) — the branch's only net contribution.
  • fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362 (10895031e2) is on origin/main and already carries the mockQr.generate quiescence, so commit a2420452fd is a no-op relative to main — confirming the body describes a change that is no longer in the diff.
  • The sibling keeps Local Control pairing separate from the temporary primary token anchors on mockQr.generate, which startLocalControl reaches only after awaiting runtimeReady — nothing observable outlives that anchor, so no further quiescence is needed in this file.
  • npx vitest run src/commands/serve.test.ts on this branch: 70/70 passed.

Paste-ready replacement PR body

Applies recommendations 1–3 verbatim: describes the --open-with-auth → openBrowserSecurely wait, replaces the Reviewer Test Plan with the runtimeReady-delay repro that exercises this diff, and downgrades Fixes #11414 to a plain reference. (The draft already contains its own Chinese translation per the PR template.)

## What this PR does

The serve command's test for authenticated open (`--open-with-auth`) now waits for its fire-and-forget handler's browser-open phase to finish before the test returns, so a trailing browser-open call can no longer leak into the next test, which asserts the browser was never opened.

## Why it's needed

The serve handler is fire-and-forget: the test's anchor resolves as soon as the daemon entry point is invoked, but the handler continues downstream — awaiting the runtime-ready promise and then opening the browser — on work the test never awaited. Under a real scheduling delay (e.g. an oversubscribed CI host) that browser-open can execute after its test has returned and fail the following test's assertion. #11362 quiesced the Local Control pairing phase of this same handler; this PR closes the remaining browser-open phase of the authenticated-open path. (See #11414 for the CI signature that motivated the original investigation — its mechanism was already closed on main by #11362.)

## Reviewer Test Plan

### How to verify

Force a real macrotask window between the test's anchor and the browser-open: in the first `--open-with-auth` test, make the mocked daemon result's runtime-ready promise resolve ~300 ms late, e.g. `runtimeReady: new Promise((resolve) => setTimeout(resolve, 300))`. Then run `cd packages/cli && npx vitest run src/commands/serve.test.ts`:

- With this PR's wait removed: the delayed browser-open lands in the next test and breaks its assertion that the browser was never opened — 1 failed | 69 passed.
- With this PR as-is: 70/70 passed.

On unmodified mocks the wait is a no-op — the downstream chain is microtasks only and has always drained by the time the anchor is observed (measured 25/25 runs, including under CPU oversubscription) — so this is scheduling insurance, not a fix for a currently-red signature. Suite cost is unchanged. If the awaited browser-open ever became unreachable, the wait gives up at its 1000 ms default with a named, attributed assertion failure — bounded, never a hang.

### Evidence (Before & After)

N/A — test-only hardening; no user-visible behavior change.

### Tested on

|     OS     |      Status      |
| :--------: | :--------------: |
|  🍏 macOS  |  ⚠️ not tested   |
| 🪟 Windows |  ⚠️ not tested   |
|  🐧 Linux  | ✅ tested (70/70) |

### Environment (optional)

N/A — unit tests only.

## Risk & Scope

- Main risk or tradeoff: worst case is a bounded, attributed 1 s wait-timeout failure if the awaited call ever became unreachable; never a hang.
- Not validated / out of scope: no other test in this file needs the same wait — the one structurally similar sibling awaits the runtime-ready promise before its anchor point, so nothing observable outlives its anchor.
- Breaking changes / migration notes: none.

## Linked Issues

Related to #11414 (mechanism already closed on main by #11362) and #11362.

<details>
<summary>中文说明</summary>

## 本 PR 做了什么

serve 命令的认证打开(`--open-with-auth`)测试现在会等待其 fire-and-forget handler 的打开浏览器阶段结束后才返回,因此滞后的打开浏览器调用不会再泄漏到下一个(断言浏览器从未被打开的)测试中。

## 为什么需要

serve handler 是 fire-and-forget 的:测试的锚点在 daemon 入口被调用时即完成,但 handler 仍会继续向下游执行 —— await runtime-ready promise,然后打开浏览器 —— 这些工作测试从未等待。在真实的调度延迟下(例如超负荷的 CI 主机),这次打开浏览器可能在其所属测试返回后才执行,从而使紧随其后那个测试的断言失败。#11362 已经静默了同一 handler 的 Local Control 配对阶段;本 PR 关闭认证打开路径剩余的打开浏览器阶段。(最初促成此项排查的 CI 失败签名见 #11414 —— 其机制已由 #11362 在 main 上关闭。)

## 审查者测试计划

### 如何验证

在第一个 `--open-with-auth` 测试中,让 mock 的 daemon 结果里的 runtime-ready promise 延迟约 300 ms 解决,在锚点与打开浏览器之间制造真实的宏任务窗口,例如 `runtimeReady: new Promise((resolve) => setTimeout(resolve, 300))`。然后运行 `cd packages/cli && npx vitest run src/commands/serve.test.ts`:

- 去掉本 PR 的等待:被延迟的打开浏览器落进下一个测试,打破其「浏览器从未被打开」的断言 —— 1 failed | 69 passed。
- 保持本 PR 原样:70/70 通过。

在未改动的 mock 下该等待是空操作 —— 下游链路全是微任务,在锚点被观察到时总是已执行完毕(实测 25/25 次运行,包括 CPU 超额订阅场景)—— 因此这是针对调度延迟的保险,而不是对当前红色签名的修复。套件耗时不变。若被等待的打开浏览器调用变得不可达,等待会在 1000 ms 默认超时处放弃并给出有归属的具名断言失败 —— 有界,绝不悬挂。

### 证据(前后对比)

N/A —— 仅测试加固;无用户可见行为变化。

### 测试平台

|     操作系统     |       状态       |
| :--------------: | :--------------: |
|     🍏 macOS     |   ⚠️ 未测试      |
|   🪟 Windows     |   ⚠️ 未测试      |
|     🐧 Linux     | ✅ 已测试(70/70)|

### 环境(可选)

N/A —— 仅单元测试。

## 风险与范围

- 主要风险或取舍:最坏情况是被等待的调用变得不可达时,等待在 1 秒超时处给出一个有归属的有界失败;绝不悬挂。
- 未验证 / 范围之外:本文件没有其他测试需要同样的等待 —— 唯一结构相似的兄弟测试在其锚点之前就 await 了 runtime-ready promise,因此没有任何可观测行为能活过其锚点。
- 破坏性变更 / 迁移说明:无。

## 关联 Issue

关联 #11414(其机制已由 #11362 在 main 上关闭)与 #11362。

</details>

Verification

  • git diff origin/main...HEAD — inspected: 1 file, +3 lines, matching the maintainer's description of the shipped change.
  • git log origin/main -- packages/cli/src/commands/serve.test.ts — confirmed 10895031e2 (fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362) is on main.
  • npm run build — passed (prerequisite for package-local vitest).
  • cd packages/cli && npx vitest run src/commands/serve.test.ts — 70/70 passed on this branch.
  • npm run typecheck / npm run lint — not re-run: no code changed this round, so their results are unchanged from the green state the maintainer measured.
中文说明

AutoFix 本轮 —— 无代码改动(请求更新 PR 描述,需要维护者操作)

结论

本轮无代码改动。唯一可行动的反馈 —— @wenshao 的维护者验证(ic:5628332933)—— 明确认定代码「正确、定位准确、可以安全合并」,要求修改的是 PR 描述,而本循环无法执行该操作:address-review 模式没有 GitHub 凭据,也没有 PR 正文输出机制(所有 GitHub 写操作均由 workflow 负责,且 pr-body.md 仅在首次创建 PR 时被消费)。下方提供了一份可直接粘贴的替换描述,逐条落实了维护者的三项建议,供维护者手动更新。

反馈处置

  • ic:5628332933(@wenshao,维护者验证):
    • 发现 1(描述写的是已合并的 fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362 的改动,而非本 diff)+ 发现 2(Fixes #11414 夸大了本 diff 的作用)+ 建议 1–3 → 请求维护者操作 —— 内容属实(见下方「本轮核实的事实」),但修复手段是编辑 GitHub PR 元数据,需要有写权限的人工操作。替换描述见下文。
    • 发现 3(小问题:在当前 mock 下该等待是空操作)→ 拒绝改动(保留等待) —— 维护者自己的强制竞态 A/B 已证明该等待是起作用的保险:把第一个测试的 runtimeReady 延迟 300 ms 后泄漏真实发生,而这三行将其封堵(不带:1 failed | 69 passed,打在已有的 expect(mockOpenBrowserSecurely).not.toHaveBeenCalled() 上;带:70/70)。针对调度延迟的保险正是本改动的目的。
    • 发现 4(等待确实生效;范围正确;兄弟测试无需补充)→ 确认性内容,已对照源码独立核实;无需操作。
    • 发现 5(fix(cli): quiesce serve test leak and sync capability doc count (#11363) #11376 相对 main 已是空 diff,可以关闭)→ 维护者例行清理 —— 关闭 PR 需要本模式不具备的 GitHub 凭据。
  • ic:5628599712(ci-bot「验证正在运行」)→ 状态通知,非可行动反馈。

本轮核实的事实

  • 相对 origin/main 的三点 diff 恰好是 applies authenticated open before the yargs path starts the daemon 中的 3 行 mockOpenBrowserSecurely 等待(packages/cli/src/commands/serve.test.ts:432-434)—— 这是本分支唯一的净贡献。
  • fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362(10895031e2)已在 origin/main 上并携带 mockQr.generate 静默等待,因此提交 a2420452fd 相对 main 无净效果 —— 证实描述写的是已不在 diff 中的改动。
  • 兄弟测试 keeps Local Control pairing separate from the temporary primary token 锚定在 mockQr.generate 上,而 startLocalControl 只有在 await 完 runtimeReady 之后才会走到该调用 —— 没有任何可观测行为能活过该锚点,因此本文件无需再补静默等待。
  • 在本分支上运行 npx vitest run src/commands/serve.test.ts:70/70 通过。

可直接粘贴的替换 PR 描述

(英文正文中的代码块即完整替换文本,逐条落实建议 1–3:改为描述 --open-with-auth → openBrowserSecurely 的等待;把审查者测试计划换成能真正触发本 diff 的 runtimeReady 延迟复现;将 Fixes #11414 降级为普通引用。该草稿已按 PR 模板自带中文翻译,此处不再重复粘贴。)

验证

  • git diff origin/main...HEAD —— 已检查:1 个文件、+3 行,与维护者描述的实际改动一致。
  • git log origin/main -- packages/cli/src/commands/serve.test.ts —— 确认 10895031e2(fix(cli): quiesce a fire-and-forget serve handler across tests (#11346) #11362)已在 main 上。
  • npm run build —— 通过(package 级 vitest 的前置条件)。
  • cd packages/cli && npx vitest run src/commands/serve.test.ts —— 本分支 70/70 通过。
  • npm run typecheck / npm run lint —— 未重跑:本轮无代码改动,其结果与维护者实测的全绿状态一致,不会变化。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.23.3

@wenshao

wenshao commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

@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.

Critical-only review at head 6ca85e0b. Not approving: the one blocking finding on this PR still stands at this head, and it is about the claim the PR makes rather than the code it ships. The code itself I found clean.

R1-1 still stands — the body certifies a fix the diff does not contain

Read against the live PR body and the live issue at this head, not the thread flag:

  • The body still says Fixes #11414 — in both language halves — and its "Why it's needed" section still names run 34289483217 / commit 422929b3a7 as the failure this change repairs, and still offers that run's signature (an unattributed failure in Test (ubuntu-latest, Node 22.x), step Run tests and generate reports) as the mechanism being fixed.
  • Issue #11414 is a different failure and is already closed. It is CLOSED / completed since 2026-09-09T11:42:46Z, and its body records exactly one observable: the Test (ubuntu-latest, Node 22.x) job of run 34289483217 failing at commit 422929b3a7. @yiliang114 ruled on the thread at 2026-09-09T11:42:49Z that the issue was resolved by #11406, that the run had no unattributed serve teardown failure — its only failed tests were the two web-shell split-session cases raising ReferenceError: mockUseDaemonActivePromptBridge is not defined — and that this change "should not be presented as the fix for #11414".
  • The whole diff at this head is three lines in packages/cli/src/commands/serve.test.ts: one await vi.waitFor(...) plus its two-line comment. Nothing in packages/web-shell, no production code.
  • The author-side loop has confirmed the finding is correct and cannot remedy it: the autofix agent holds no GitHub credentials and the PR body is workflow-owned metadata, so it escalated the body edit as a maintainer action and left the thread open on purpose. reviewDecision is CHANGES_REQUESTED on the strength of exactly this finding.

This is not resolvable from the code, and it is the only thing between this PR and a merge.

Current head — Critical-only scan, no finding

The change adds, inside applies authenticated open before the yargs path starts the daemon:

await startServeHandlerWithArgs('--open-with-auth');
// Wait out the fire-and-forget handler's browser-open phase so its
// openBrowserSecurely call cannot land in the next test.
await vi.waitFor(() => expect(mockOpenBrowserSecurely).toHaveBeenCalled());
  • It is assertion-preserving. No expectation is removed, weakened or reordered; the three existing assertions still run after the wait. This is the shape the repository's deflake policy asks for.
  • It cannot hang. vi.waitFor bounds itself, so a handler that never reaches the browser-open phase fails the test in about a second rather than parking it until the suite deadline — which is the failure mode the issue signature describes.
  • It waits on the right side effect. The wait target is the last observable of the fire-and-forget handler's own work, so the handler is quiesced before the next test installs its one-shot throwing mock; waiting on an earlier side effect would leave the race open. The maintainer's forced-race A/B at this exact head reports that of the three candidate wait targets only this one closes the race, and that the two mutants which wait earlier still leak.
  • Scope is complete for the file. No other call site in the suite is left starting an unawaited handler that this diff's own class of leak would cover, and the change touches no production path, so no runtime behaviour can regress.

CI at this head

Every check passes — unit tests, lint and static analysis, integration tests, the web-shell smoke gate, the TUI gates and review-pr. Nothing pending, and no failure attributable to this PR.

Next step

A maintainer needs to edit the PR body: drop Fixes #11414 from both language halves and keep the issue as context, pointing the tracking claim at the occurrence this change actually addresses (#11346 is the one the thread names as still relevant). The autofix loop has a ready-to-paste replacement in its latest round report. Once the body no longer certifies a fix for a failure it does not touch, the code side needs nothing further — it is already verified at this head by a maintainer's A/B run and by a green suite.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

AutoFix round — no code change (PR-body edit remains the only blocker; needs a maintainer)

Verdict

No code change this round. The one blocking finding — [rv:5174992550] R1-1, the PR body still certifying Fixes #11414 — is verified accurate, but its remedy is an edit to GitHub PR metadata, which this loop cannot perform: the autofix agent holds no GitHub credentials and the workflow owns every GitHub write (pr-body.md is consumed only at PR creation; the round flow has no gh pr edit step). The reviewer confirms the same: "This is not resolvable from the code, and it is the only thing between this PR and a merge." A paste-ready replacement body, updated for the reviewer's latest instruction (point the tracking claim at #11346), is included below.

Feedback dispositions

  • [rv:5174992550] @qqqys — R1-1 still stands (body certifies a fix the diff does not contain) → escalated — maintainer action required. Verified first-hand against the exact code: the three-dot diff at head 6ca85e0b62 is exactly the 3-line mockOpenBrowserSecurely quiescence wait in packages/cli/src/commands/serve.test.ts; nothing in packages/web-shell, no production code. The reviewer's code-side assessment (assertion-preserving, bounded, waits on the correct side effect, scope complete for the file) matches what the diff shows. Only the body edit is outstanding — replacement below.
  • [ic:5629517767] @qwen-code-ci-bot — "Sandboxed verification is running" → status notice, not actionable feedback.

Verified facts (this round)

  • git diff origin/main...HEAD — 1 file, +3/−0 lines: one await vi.waitFor(() => expect(mockOpenBrowserSecurely).toHaveBeenCalled()) plus its two-line comment in applies authenticated open before the yargs path starts the daemon. Matches the reviewer's description exactly.
  • Head is 6ca85e0b62 with a clean working tree — byte-identical to the head the maintainer's forced-race A/B ran on and the head the previous round measured at 70/70 on the focused suite, so those results still stand.
  • CI at this head (from the workflow's checks snapshot): 44 checks, 0 failing; the only in-progress entry is this autofix run itself. Unit tests, lint, integration tests, web-shell smoke, TUI gates, and review-pr all pass — confirming the reviewer's "CI at this head" section.
  • No new inline review comments since the last evaluation; the existing R1-1 thread (rc:3964084627) intentionally stays open, already carrying the loop's escalation reply.
  • The structural follow-up recorded in a prior round (centralizing the serve-test quiescence pattern, deferred to the per-PR follow-up issue) still stands; nothing this round changes it.

Paste-ready replacement PR body (updated)

Implements the reviewer's next step verbatim: drops Fixes #11414 from both language halves, keeps every issue as annotated context, and names #11346 as the occurrence the thread marks as still relevant. It also corrects one stale attribution the previous draft carried: #11414 was closed as resolved by #11406 (its recorded failure was the Web Shell mockUseDaemonActivePromptBridge ReferenceError), not by #11362 — #11362 is the merged sibling that quiesced the pairing phase of this same handler.

## What this PR does

The serve command's test for authenticated open (`--open-with-auth`) now waits for its fire-and-forget handler's browser-open phase to finish before the test returns, so a trailing browser-open call can no longer leak into the next test, which asserts the browser was never opened.

## Why it's needed

The serve handler is fire-and-forget: the test's anchor resolves as soon as the daemon entry point is invoked, but the handler continues downstream — awaiting the runtime-ready promise and then opening the browser — on work the test never awaited. Under a real scheduling delay (e.g. an oversubscribed CI host) that browser-open can execute after its test has returned and fail the following test's assertion. #11362 quiesced the Local Control pairing phase of this same handler; this PR closes the remaining browser-open phase of the authenticated-open path. (See #11414 for the CI signature that motivated the original investigation — its recorded failure was a Web Shell `ReferenceError`, already resolved by #11406.)

## Reviewer Test Plan

### How to verify

Force a real macrotask window between the test's anchor and the browser-open: in the first `--open-with-auth` test, make the mocked daemon result's runtime-ready promise resolve ~300 ms late, e.g. `runtimeReady: new Promise((resolve) => setTimeout(resolve, 300))`. Then run `cd packages/cli && npx vitest run src/commands/serve.test.ts`:

- With this PR's wait removed: the delayed browser-open lands in the next test and breaks its assertion that the browser was never opened — 1 failed | 69 passed.
- With this PR as-is: 70/70 passed.

On unmodified mocks the wait is a no-op — the downstream chain is microtasks only and has always drained by the time the anchor is observed (measured 25/25 runs, including under CPU oversubscription) — so this is scheduling insurance, not a fix for a currently-red signature. Suite cost is unchanged. If the awaited browser-open ever became unreachable, the wait gives up at its 1000 ms default with a named, attributed assertion failure — bounded, never a hang.

### Evidence (Before & After)

N/A — test-only hardening; no user-visible behavior change.

### Tested on

|     OS     |      Status      |
| :--------: | :--------------: |
|  🍏 macOS  |  ⚠️ not tested   |
| 🪟 Windows |  ⚠️ not tested   |
|  🐧 Linux  | ✅ tested (70/70) |

### Environment (optional)

N/A — unit tests only.

## Risk & Scope

- Main risk or tradeoff: worst case is a bounded, attributed 1 s wait-timeout failure if the awaited call ever became unreachable; never a hang.
- Not validated / out of scope: no other test in this file needs the same wait — the one structurally similar sibling awaits the runtime-ready promise before its anchor point, so nothing observable outlives its anchor.
- Breaking changes / migration notes: none.

## Linked Issues

Related to #11346 (the occurrence the thread names as still relevant), #11414 (closed as resolved by #11406 — a different failure signature), and #11362 (merged sibling quiescence for this handler).

<details>
<summary>中文说明</summary>

## 本 PR 做了什么

serve 命令的认证打开(`--open-with-auth`)测试现在会等待其 fire-and-forget handler 的打开浏览器阶段结束后才返回,因此滞后的打开浏览器调用不会再泄漏到下一个(断言浏览器从未被打开的)测试中。

## 为什么需要

serve handler 是 fire-and-forget 的:测试的锚点在 daemon 入口被调用时即完成,但 handler 仍会继续向下游执行 —— await runtime-ready promise,然后打开浏览器 —— 这些工作测试从未等待。在真实的调度延迟下(例如超负荷的 CI 主机),这次打开浏览器可能在其所属测试返回后才执行,从而使紧随其后那个测试的断言失败。#11362 已经静默了同一 handler 的 Local Control 配对阶段;本 PR 关闭认证打开路径剩余的打开浏览器阶段。(最初促成此项排查的 CI 失败签名见 #11414 —— 其记录的失败是 Web Shell 的 `ReferenceError`,已由 #11406 解决。)

## 审查者测试计划

### 如何验证

在第一个 `--open-with-auth` 测试中,让 mock 的 daemon 结果里的 runtime-ready promise 延迟约 300 ms 解决,在锚点与打开浏览器之间制造真实的宏任务窗口,例如 `runtimeReady: new Promise((resolve) => setTimeout(resolve, 300))`。然后运行 `cd packages/cli && npx vitest run src/commands/serve.test.ts`:

- 去掉本 PR 的等待:被延迟的打开浏览器落进下一个测试,打破其「浏览器从未被打开」的断言 —— 1 failed | 69 passed。
- 保持本 PR 原样:70/70 通过。

在未改动的 mock 下该等待是空操作 —— 下游链路全是微任务,在锚点被观察到时总是已执行完毕(实测 25/25 次运行,包括 CPU 超额订阅场景)—— 因此这是针对调度延迟的保险,而不是对当前红色签名的修复。套件耗时不变。若被等待的打开浏览器调用变得不可达,等待会在 1000 ms 默认超时处放弃并给出有归属的具名断言失败 —— 有界,绝不悬挂。

### 证据(前后对比)

N/A —— 仅测试加固;无用户可见行为变化。

### 测试平台

|     操作系统     |       状态       |
| :--------------: | :--------------: |
|     🍏 macOS     |   ⚠️ 未测试      |
|   🪟 Windows     |   ⚠️ 未测试      |
|     🐧 Linux     | ✅ 已测试(70/70)|

### 环境(可选)

N/A —— 仅单元测试。

## 风险与范围

- 主要风险或取舍:最坏情况是被等待的调用变得不可达时,等待在 1 秒超时处给出一个有归属的有界失败;绝不悬挂。
- 未验证 / 范围之外:本文件没有其他测试需要同样的等待 —— 唯一结构相似的兄弟测试在其锚点之前就 await 了 runtime-ready promise,因此没有任何可观测行为能活过其锚点。
- 破坏性变更 / 迁移说明:无。

## 关联 Issue

关联 #11346(讨论串中指认的仍然相关的 occurrence)、#11414(已由 #11406 关闭 —— 失败签名不同)与 #11362(已合并的、针对同一 handler 的兄弟静默改动)。

</details>

Once a maintainer applies that body edit (retitling along the lines of test(cli): quiesce the authenticated-open browser-open phase in serve tests would also match the actual diff), the code side needs nothing further — it is verified at this exact head by the maintainer's forced-race A/B and by a fully green CI.

Verification

  • git diff origin/main...HEAD — inspected: 1 file, +3/−0 lines, exactly the quiescence wait the review describes.
  • git status --short / git log — clean tree at head 6ca85e0b62, identical to the head the maintainer A/B-verified and the prior round tested 70/70.
  • Workflow checks snapshot (checks.json) — 44 checks, 0 failing, none pending except this autofix run itself.
  • Review/comment snapshots (rv.json, rc.json, ic.json) — triaged: one blocking review finding (PR-body only), no new inline comments, one ci-bot status notice.
  • Build/typecheck/lint/vitest — not re-run this round: no code changed, and the tree is byte-identical to the previously verified head, so their green results are unchanged.
中文说明

AutoFix 本轮 —— 无代码改动(PR 描述编辑仍是唯一阻塞项,需要维护者操作)

结论

本轮无代码改动。唯一的阻塞发现 —— [rv:5174992550] R1-1,即 PR 描述仍声称 Fixes #11414 —— 经核实属实,但其修复手段是编辑 GitHub PR 元数据,本循环无法执行:autofix agent 没有 GitHub 凭据,所有 GitHub 写操作均由 workflow 负责(pr-body.md 仅在创建 PR 时被消费,轮次流程中没有 gh pr edit 步骤)。评审者也确认了同一点:「这无法从代码侧解决,且它是本 PR 与合并之间唯一的障碍。」下方提供了可直接粘贴的替换描述,并已按评审者的最新指示更新(追踪指向 #11346)。

反馈处置

  • [rv:5174992550] @qqqys —— R1-1 仍然成立(描述认证了 diff 并不包含的修复) → 升级 —— 需要维护者操作。 已对照确切代码第一手核实:head 6ca85e0b62 的三点 diff 恰好是 packages/cli/src/commands/serve.test.ts 中 3 行 mockOpenBrowserSecurely 静默等待;不涉及 packages/web-shell,无生产代码。评审者对代码侧的评估(保留断言、有界、等待正确的副作用、文件范围完整)与 diff 内容一致。仅剩描述编辑 —— 替换文本见下文。
  • [ic:5629517767] @qwen-code-ci-bot ——「沙箱验证正在运行」 → 状态通知,非可行动反馈。

本轮核实的事实

  • git diff origin/main...HEAD —— 1 个文件,+3/−0 行:在 applies authenticated open before the yargs path starts the daemon 中加入一行 await vi.waitFor(() => expect(mockOpenBrowserSecurely).toHaveBeenCalled()) 及其两行注释。与评审者描述完全一致。
  • head 为 6ca85e0b62,工作区干净 —— 与维护者做强制竞态 A/B 时、以及上一轮实测聚焦套件 70/70 时的 head 逐字节一致,因此这些结果仍然成立。
  • 该 head 的 CI(来自 workflow 的检查快照):44 项检查,0 失败;唯一进行中的条目是本次 autofix 运行本身。单元测试、lint、集成测试、web-shell 冒烟、TUI 门禁与 review-pr 全部通过 —— 证实评审者的「CI at this head」一节。
  • 自上次评估以来没有新的行内评审评论;既有的 R1-1 讨论串(rc:3964084627)有意保持开启,已携带本循环的升级回复。
  • 上一轮记录的结构化后续项(将 serve 测试的静默等待模式集中化,已推迟到本 PR 的后续追踪 issue)仍然成立;本轮无任何改动影响它。

可直接粘贴的替换 PR 描述(已更新)

逐字落实评审者的下一步:在两个语言版本中去掉 Fixes #11414,所有 issue 均以带注释的上下文形式保留,并将 #11346 列为讨论串中指认的仍然相关的 occurrence。同时修正了上一稿携带的一处过时归因:#11414 是由 #11406 关闭的(其记录的失败是 Web Shell 的 mockUseDaemonActivePromptBridge ReferenceError),而非 #11362 —— #11362 是已合并的、静默同一 handler 配对阶段的兄弟改动。

(上方英文部分的代码块即完整替换文本,已按 PR 模板自带中文翻译,此处不再重复粘贴。)

维护者应用该描述编辑后(标题同步改为类似 test(cli): quiesce the authenticated-open browser-open phase in serve tests 也更贴合实际 diff),代码侧无需任何进一步改动 —— 它已在当前确切 head 上由维护者的强制竞态 A/B 与全绿 CI 双重验证。

验证

  • git diff origin/main...HEAD —— 已检查:1 个文件、+3/−0 行,正是评审所描述的静默等待。
  • git status --short / git log —— head 6ca85e0b62 工作区干净,与维护者 A/B 验证及上一轮实测 70/70 的 head 完全一致。
  • workflow 检查快照(checks.json)—— 44 项检查,0 失败,除本次 autofix 运行本身外无进行中项。
  • 评审/评论快照(rv.json、rc.json、ic.json)—— 已完成分诊:一个阻塞性评审发现(仅涉及 PR 描述)、无新增行内评论、一条 ci-bot 状态通知。
  • build/typecheck/lint/vitest —— 本轮未重跑:无代码改动,且工作树与此前已验证的 head 逐字节一致,其全绿结果不会变化。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.23.3

@github-actions github-actions Bot removed the review/self-reported The linked issue was opened by the PR author (self-reported) label Sep 11, 2026
@qqqys

qqqys commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Withdrawing the withholding in my two prior reviews — the one blocking ground is resolved at 9fc78a24c105, and I now approve

Both of my earlier reviews on this PR (5159715181 at 6f262bf0, 5174992550 at 6ca85e0b) said "Not approving: the one blocking finding on this PR still stands at this head". That finding was R1-1, and it was never about the code — it was about the body certifying a fix the diff does not contain. As of the state read immediately before posting this comment, R1-1 is resolved, so the withholding no longer has a ground and this comment carries the approval.

1. R1-1 closed — re-verified at post time, not at drafting time

Checked against the live body and the live head in the same call that gates this write:

R1-1 asked for State at 9fc78a24c105
Drop the Fixes #11414 trailer from both language halves Gone from both — no Fixes trailer anywhere in the body
Keep #11414 as context, disclaiming it Both halves now carry it: "#11414 is not addressed by this PR: that run failed on an unrelated packages/web-shell test, which #11406 resolved." / "本 PR 不处理 #11414…"
Point the tracking claim at the occurrence this change actually addresses Related to #11346 (same cross-test leak class; the pairing-test wait for it landed in #11362)
Drop the superseded run/commit citation 34289483217 and 422929b3a7 no longer appear in the body
Align "What this PR does" with the hunk actually at head It now describes the browser-open wait in applies authenticated open before the yargs path starts the daemon, and names #11362 as having landed the Local Control pairing-test half on main first

That closes both halves of R1-1 — the false root-cause certification and the description/hunk mismatch.

2. The diff I measured is the diff at head

The head moved 6ca85e0b622a → 9fc78a24c105 via a Merge branch 'main', so the PR's own content is unchanged: 1 file, packages/cli/src/commands/serve.test.ts, blob f151086f7b92, +3/−0, zero production files — byte-identical to what my 04:34Z review scanned.

Because a main merge can falsify a +import line invisibly (the patch is a diff against the merge base, so a symbol main contributed through the merge is on neither side of it), I ran the compile-level check on the head blob rather than on the patch: exactly one declaration site of mockOpenBrowserSecurely (:15, vi.hoisted), 7 import lines, zero duplicate import paths. No TS2300-class collision landed through the merge.

3. Executed A/B at the new head — when the wait is load-bearing, and what it costs

Run in a git archive scratch tree at 9fc78a24c105 (no build, no worktree), Node v24.18.1 / vitest 3.2.7, --retry=0. Evidence is a file witness, not stdout: packages/cli/vitest.config.ts:237 sets silent: true, so a console.log witness is muted and a muted zero is byte-identical to a true negative. Probes record mockOpenBrowserSecurely.mock.calls.length at four points; beforeEach calls vi.clearAllMocks(), so a call recorded in a later probe belongs to a later test.

Arm Change vs head 417_at_start_return 417_end 440_pre_assert Result
A ×3 none (wait present) calls=1 calls=1 calls=0 70/70 pass, :417 ≈ 54 ms
B ×3 the 3 added lines removed calls=1 calls=1 calls=0 70/70 pass — indistinguishable from A
D runtimeReady deferred 300 ms, wait absent calls=0 calls=0 calls=0 :417 returns in 56 ms with the browser-open call still outstanding
E runtimeReady deferred 300 ms, wait present calls=0 calls=1 calls=0 70/70 pass, :417 = 306.5 ms
C browser launch disabled, wait present calls=0 (never reached) calls=0 1 failed / 69 passed in 1064 ms

What that settles:

  • The wait is load-bearing exactly when the browser-open phase outlasts the harness's own poll — which is the production shape. maybeOpenWebShellBrowser does await handle.runtimeReady (commands/serve.ts:151) before it can reach openBrowserSecurely (:183), and in production runtimeReady settles on real I/O. The shipped fixture supplies runtimeReady: Promise.resolve(), so the whole path is microtask-only and arm B cannot reproduce the leak: A ≡ B in 3/3 trials, and setTimeout(…, 0) is still inside the poll window (D ≡ E at 0 ms). Defer it by 300 ms and the un-waited test returns with the call outstanding (arm D), while the waited test absorbs exactly the delay and records the call inside the test that caused it (arm E). That is why the original failure is intermittent rather than constant: it needs a runner slow enough to push runtime-ready past the poll.
  • The wait's cost is proportional, not fixed. Arm E's :417 took 306.5 ms against a 300 ms deferral — it waits as long as the phase takes and no longer. Neighbouring tests are unaffected (:440 55.2 ms in both arms).
  • "Cannot hang" is now measured, not asserted. My earlier review claimed vi.waitFor bounds itself. Arm C forces the browser-open phase never to happen with the wait left in place: the suite fails one test in 1064 ms (1 failed / 69 passed) rather than parking until the suite deadline. So a future production change that stops launching the browser fails this test loudly in about a second.
  • The suite is green at the post-merge head: 70/70 in serve.test.ts, 3/3 trials on the pristine blob (f151086f7b92, restored and re-hashed after every arm).

4. CI at head

commits/9fc78a24c105/check-runs census 23/23 complete. 11 product lanes: 8 success, 3 skipped, 0 non-green — including Test (ubuntu-latest, Node 22.x), Lint & Static, Integration Tests (no-AK, No Sandbox), web-shell E2E Smoke, TUI parity snapshots, OpenTUI no-flicker gate and both Desktop Shell lanes. The three skipped are Test (macos-latest, …) and Test (windows-latest, …), which do not run for a non-main base. The only lane repo-wide that is not green is review-pr, still in progress — the review bot's own lane, never a CI verdict.

5. What this comment does not claim

  • No production surface was exercised, because there is none — the diff is one test file. A tmux/interactive-TUI report could not measure anything about it, so the instrument here is the suite the change lives in plus the arms above.
  • I did not reproduce the maintainer's forced-race A/B, and arm B is the reason: the necessity of the wait under CI load rests on that A/B (three candidate wait targets, only this one closing the race) plus arm D's ordering inversion, not on anything I ran at head.
  • The NO CRITICAL I recorded at 6ca85e0b covers the three added lines and their isolation mechanism. It is not a claim about the other 69 cases in the file.

Verdict: approving at 9fc78a24c105. The code side needed nothing further at 6ca85e0b and is byte-identical now; the body side was the only outstanding item and it is closed.

@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.

Approving at 9fc78a24c105. My two prior reviews withheld on one ground — the body certifying Fixes #11414, a fix this diff does not contain — and that ground is closed at this head: no Fixes trailer in either language half, both halves now disclaim #11414 and name #11406 as what resolved it, and the tracking claim points at #11346. The diff is byte-identical to the one I scanned at 6ca85e0b (1 file, packages/cli/src/commands/serve.test.ts, blob f151086f7b92, +3/−0, zero production files). The executed A/B, the bounded-failure measurement and the 23/23 CI census are in the comment posted alongside this review.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🕐 Review received — an automatic review of the current head is still running, so this round is held until it lands (a push now would cancel it and discard its work, #8888). Your feedback stays queued for the next eligible round.

中文说明

🕐 已收到评审 —— 当前 head 上仍有一轮自动 review 在运行,本轮暂缓(现在推送会取消该 review 并丢弃其工作,#8888)。反馈保持排队,等待下一次可运行的轮次处理。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix review round — no action required

Feedback triage

Exactly one feedback item arrived since the last evaluation (2026-09-11T04:34:24Z), and it requests no change:

  • [ic:5632697420] @qqqys (issue-level comment) — approval, nothing to address. The reviewer withdrew the withholding from their two prior reviews: the sole blocking finding R1-1 was about the PR body certifying a fix the diff did not contain, and they re-verified at the live head 9fc78a24c105 that the body is now corrected (no Fixes #11414 trailer in either language half, Main CI failed: Qwen Code CI on 422929b3a7df #11414 properly disclaimed as unrelated, tracking pointed at Main CI failed: Qwen Code CI on 0d1e0fbfa6f3 #11346, superseded run/commit citations dropped). They also re-ran an instrumented A/B at the current head confirming the added vi.waitFor on mockOpenBrowserSecurely is load-bearing exactly when the browser-open phase outlasts the harness poll (the production shape), bounded in cost, and fails loudly in ~1 s rather than hanging if the browser-open phase ever stops happening. Their verdict: approving at 9fc78a24c105. The comment contains no request, question, or outstanding finding.

There are no inline comments, no review-body findings, no failed checks, and no still-red checks in this round's feedback.

Action taken

None. The PR head is unchanged at 9fc78a24c105c0141a89f3e009ab3a3be56c7e8e and the working tree is clean. With the only feedback being an approval of the current head, any edit would be unrequested churn.

Verification

No code was changed, so no pre-commit verification was required. As independent confirmation of the state the approval certifies, the suite the change lives in was run once at this head:

  • npm run build — passed (workspace dist/ outputs needed by the CLI test guard)
  • cd packages/cli && npx vitest run src/commands/serve.test.ts — 70/70 passed (21.77s), matching the reviewer's measurement at the same head
中文说明

Autofix 评审轮次 —— 无需处理

反馈分类

自上次评估(2026-09-11T04:34:24Z)以来只有一条反馈,且它不要求任何改动:

  • [ic:5632697420] @qqqys(issue 级评论)—— 批准,无需处理。 评审者撤回了其前两轮评审中的保留意见:唯一的阻断项 R1-1 针对的是 PR 描述 认证了一个 diff 中并不存在的修复,评审者已在当前 head 9fc78a24c105 上重新核实描述已改正(两种语言部分均不再含 Fixes #11414 尾注;Main CI failed: Qwen Code CI on 422929b3a7df #11414 已被正确地声明为与本 PR 无关;跟踪指向 Main CI failed: Qwen Code CI on 0d1e0fbfa6f3 #11346;过时的 run/commit 引用已删除)。评审者还在当前 head 上重新运行了带探针的 A/B 实验,确认新增的针对 mockOpenBrowserSecurely 的 vi.waitFor 等待恰好是承重墙 —— 当浏览器打开阶段超出测试框架自身轮询窗口时(即生产环境形态)它就是必需的;其成本是有界的;并且如果浏览器打开阶段将来不再发生,测试会在约 1 秒内明确失败而不是挂起。评审结论:在 9fc78a24c105 批准。 该评论不包含任何请求、问题或未解决的发现。

本轮反馈中没有行内评论、没有评审正文发现、没有失败的检查,也没有持续红色的检查。

采取的操作

无。PR head 保持在 9fc78a24c105c0141a89f3e009ab3a3be56c7e8e 未变,工作树干净。既然唯一的反馈是对当前 head 的批准,任何编辑都将是未被请求的扰动。

验证

没有改动代码,因此不需要提交前验证。作为对批准所认证状态的独立确认,在本 head 上运行了一次该改动所在的测试套件:

  • npm run build —— 通过(CLI 测试守卫所需的各工作区 dist/ 产物)
  • cd packages/cli && npx vitest run src/commands/serve.test.ts —— 70/70 通过(21.77 秒),与评审者在同一 head 上的测量一致

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.23.3

@wenshao
wenshao dismissed a stale review September 11, 2026 22:50

fixed

@wenshao
wenshao added this pull request to the merge queue Sep 11, 2026
Merged via the queue into main with commit 2b5078b Sep 11, 2026
45 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.

4 participants