Skip to content

chore: restore regression coverage after WebUI retirement - #11101

Merged
yiliang114 merged 13 commits into
mainfrom
fix/follow-up-9812-review-suggestions
Sep 8, 2026
Merged

chore: restore regression coverage after WebUI retirement#11101
yiliang114 merged 13 commits into
mainfrom
fix/follow-up-9812-review-suggestions

Conversation

@yiliang114

@yiliang114 yiliang114 commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Restores focused regression coverage and supporting behavior left after the @qwen-code/webui retirement. It pins the platform-sensitivity substring boundary, rejects local file: links in rendered Markdown, preserves transcript export when the helper is invoked through a symlink, strengthens the exported-document theme assertion, and routes the new helper test through CI. It also pins the root Playwright dependency to the currently matched browser revision and corrects follow-up-suggestion guidance for daemon clients.

Why it's needed

These are delta-specific gaps found while auditing the WebUI retirement in #9812. In particular, a symlinked exporter invocation could exit successfully without writing output, several important boundaries lacked direct regression coverage, and headless daemon clients were not told how to avoid suggestion-generation cost they cannot use. The four items originally tracked in #11076 were completed separately by #11107; this PR neither reimplements nor closes #11076.

Reviewer Test Plan

How to verify

  • Run node --test .github/scripts/ci/classify-platform-sensitivity.test.mjs and confirm the shell-substring boundary passes.
  • Run node --test integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs and confirm direct, symlinked, and imported main-module cases pass.
  • Run npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/export-html-from-chatrecord-jsonl.test.js and confirm legacy JSONL rejection plus the ChatRecord happy path pass.
  • From packages/web-shell, run npx vitest run client/components/messages/Markdown.test.ts and confirm file: URLs remain rejected.
  • Run npm run typecheck && npm run lint.
  • Run npm ls playwright @playwright/test and confirm both resolve to 1.61.1 in the current lockfile. Long-term manifest-level parity enforcement is tracked in chore: follow up deferred #11101 review suggestions #11336.

Evidence (Before & After)

N/A — documentation, tests, a helper bug fix, and a dependency pin; no UI change.

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️ not tested locally
🐧 Linux ✅ verified in CI

Environment (optional)

Local Node 22 checkout; focused Node and Vitest suites were run directly in the worktree. The exported-document browser gate was also verified in the sandboxed PR run linked in the discussion.

Risk & Scope

  • Main risk or tradeoff: pinning root playwright to 1.61.1 trades caret flexibility for the browser-revision parity required by the current lockfile; future manifest-level parity enforcement is tracked in chore: follow up deferred #11101 review suggestions #11336.
  • Not validated / out of scope: Windows was not tested locally; CI covers the merge-only sync with current main.
  • Breaking changes / migration notes: none.

Linked Issues

Context: #9812, #11076, and #11107. Deferred non-blocking review suggestions are tracked in #11336.

中文说明

本 PR 做了什么

补回 @qwen-code/webui 退役后仍缺失的聚焦回归覆盖及其必要行为:固定 platform-sensitivity 的子串边界,拒绝渲染 Markdown 中的本地 file: 链接,保证 transcript exporter 通过软链接调用时仍会执行,加强导出文档的主题断言,并让新增 helper 测试进入 CI。同时把根目录 Playwright 依赖固定到当前匹配的浏览器版本,并修正 daemon 客户端的 follow-up suggestion 使用说明。

为什么需要

这些是审计 #9812 的 WebUI 退役工作时发现、且确实属于当前 delta 的缺口。此前通过软链接调用 exporter 时,进程可能成功退出却没有写出文件;若干关键边界缺少直接回归覆盖;headless daemon 客户端也没有被告知如何关闭无法消费却会产生费用的 suggestion generation。#11076 原先跟踪的四项内容已由 #11107 单独完成;本 PR 不重复实现,也不关闭 #11076

Reviewer Test Plan

如何验证

  • 运行 node --test .github/scripts/ci/classify-platform-sensitivity.test.mjs,确认 shell 子串边界测试通过。
  • 运行 node --test integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs,确认直接调用、软链接调用和被 import 三种 main-module 场景通过。
  • 运行 npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/export-html-from-chatrecord-jsonl.test.js,确认 legacy JSONL 拒绝路径和 ChatRecord 正常路径通过。
  • packages/web-shell 中运行 npx vitest run client/components/messages/Markdown.test.ts,确认 file: URL 仍被拒绝。
  • 运行 npm run typecheck && npm run lint
  • 运行 npm ls playwright @playwright/test,确认当前 lockfile 中两者都解析为 1.61.1。长期的 manifest 级版本一致性约束由 chore: follow up deferred #11101 review suggestions #11336 跟踪。

证据(Before & After)

N/A —— 文档、测试、helper bug 修复和依赖固定;无 UI 变化。

测试环境

系统 状态
🍏 macOS
🪟 Windows ⚠️ 本地未测试
🐧 Linux ✅ CI 已验证

环境(可选)

本地 Node 22 checkout;聚焦的 Node 与 Vitest 测试直接在 worktree 中运行。导出文档的浏览器 gate 也已在 PR 讨论中链接的沙箱验证任务里通过。

风险与范围

  • 主要风险或取舍:把根目录 playwright 固定为 1.61.1,以放弃 caret 灵活性换取当前 lockfile 所需的浏览器版本一致性;后续的 manifest 级一致性约束由 chore: follow up deferred #11101 review suggestions #11336 跟踪。
  • 未验证 / 范围外:Windows 未在本地验证;与最新 main 的纯合并同步由 CI 覆盖。
  • 破坏性变更 / 迁移说明:无。

关联问题

背景参考:#9812#11076#11107。延后的非阻塞 review 建议由 #11336 跟踪。

…tions

Address the remaining review suggestions from the @qwen-code/webui
retirement (#9812) and the deferred items tracked in #11076:

- Fix the 01-architecture.md adapter diagram path (missing session/).
- Correct the followup-suggestions doc: generation fires on end_turn, and
  headless/SDK daemon clients should opt out to avoid the per-turn LLM cost.
- Pin playwright to 1.61.1 to restore version parity with @playwright/test.
- Restore file: scheme rejection coverage in Markdown.test.ts.
- Pin the historical sessionStorage key prefix with a literal assertion.
- Cover the legacy exported JSONL rejection and ChatRecord happy path.
- Restore the shell-substring classifier trap with a live path.
- Point useDaemonFollowupSuggestion docs at ChatEditor, not the deleted InputForm.
- Replace the theme-toggle timing-race assertion and exercise the
  unhandledrejection fail-closed path in the document browser gate.

Closes #11076
@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Sep 5, 2026
@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Re-run at 95a35e06, replacing my pass at c5a45ce. Short version: the code holds up, every assertion in it now pins something real, and CI is green on the lane that matters. One word in the PR body is the only thing still firing the gate.

Gate: Stage 1-pre has no substantive complaint left — but it still trips, on a parser technicality.

#11076 is closed as completed by #11107 (merged), so the gate compares this diff against main. I re-checked all twelve files rather than trusting my earlier pass, and none of the delta is there: the exporter still carries invokedDirectly + fileURLToPath, package.json still says ^1.57.0, Markdown.test.ts has no file: case, the classifier has no shellCommandProcessor fixture, the followup doc has no end_turn wording, ci.yml doesn't register the new helper test, and review-context.json has no concurrent-runner rule. So this is purely additive work — nothing here re-does #11107.

Last round's asks are met: rebased onto main (6ba04fc2, mergeable: true), retitled to chore:, description rescoped. I'm not re-submitting the old stage=1-pre request-changes review — it already stands on the PR and its asks are answered.

The one line worth fixing: Linked Issues now reads "this PR neither reimplements nor closes #11076". GitHub's closing-reference parser is not intent-aware, so the closes #11076 buried in that negation is still a live closing reference — closingIssuesReferences returns #11076 at this very commit. Harmless to the repo (the issue is already closed), but it keeps this gate firing on every re-run and shows a misleading link in the sidebar. Reword to drop the keyword — "nor does it re-do the work in #11076" — and the reference disappears.

Problem: observed, not theoretical. Node realpaths import.meta.url for the ESM main module but leaves process.argv[1] as it was invoked, so the old path.resolve(argv1) === fileURLToPath(metaUrl) comparison fails under a symlink and the exporter exits 0 having written nothing. Silent success is the worst shape for a helper whose entire job is producing a file.

Direction: aligned. Restoring coverage that evaporated when a package was deleted is maintenance, not scope creep.

Size: 220 lines / 12 files — roughly 41 production/config (25 of those the exporter helper), 73 test, 106 lockfile. No core-module path is touched: the two files under packages/ are both tests, outside packages/core/src/** and the auth|providers|models|config|tools|services globs. Stage 0 not triggered, and well under the 500/1000 thresholds.

Approach: the scope is right now. The unhandledrejection block that couldn't fail is gone, and so is the Shellfish.tsx hunk that overlapped #11099 — the classifier change only adds a camelCase fixture alongside it. Three drive-bys still ride along (the playwright pin, the followup-suggestions doc rewrite, the review-context domain mapping). I asked about splitting last round; you kept them and disclosed the tradeoff in Risk & Scope, and at this size I'm not going to keep asking. Carried into Stage 2 as notes, not blockers.

Risk: no Stage 1e match — nothing here touches the paths that correlate with post-merge reverts.

Moving on to code review. 🔍

中文说明

95a35e06 重跑,取代我在 c5a45ce 的那一轮。简短结论:代码站得住,里面每条断言现在都真的钉住了行为,关键 lane 的 CI 也是绿的。PR 描述里一个词,是门禁目前唯一还在触发的东西。

门禁:Stage 1-pre 已经没有实质性意见了——但仍会因为一个解析器的技术细节而触发。

#11076 已由 #11107(已合并)以 completed 关闭,所以门禁会把本 diff 与 main 对比。我没有沿用上一轮的结论,而是重新逐个核对了十二个文件,delta 全都不在 main 上:exporter 仍是 invokedDirectly + fileURLToPathpackage.json 仍写着 ^1.57.0Markdown.test.ts 没有 file: 用例,classifier 没有 shellCommandProcessor fixture,followup 文档没有 end_turn 措辞,ci.yml 没注册新的 helper 测试,review-context.json 也没有 concurrent-runner 规则。所以这些完全是新增工作——没有重复 #11107

上一轮的要求都已满足:已 rebase 到 main6ba04fc2mergeable: true),标题改为 chore:,描述已重新界定范围。我不会重复提交之前那条 stage=1-pre 的 request-changes review——它已经挂在 PR 上,且其要求都已被回应。

唯一值得改的一行: Linked Issues 现在写的是「this PR neither reimplements nor closes #11076」。GitHub 的 closing-reference 解析器不判断语义,所以这句否定里嵌着的 closes #11076 仍然是一个有效的关闭引用——在当前这个 commit 上,closingIssuesReferences 依然返回 #11076。对仓库无害(该 issue 已经关闭),但它会让这道门禁在每次重跑时继续触发,并在侧栏显示一个有误导性的关联。把关键词去掉改成「nor does it re-do the work in #11076」,这个引用就会消失。

问题: 是已观测到的,不是理论性的。Node 会对 ESM 主模块的 import.meta.url 做 realpath,但 process.argv[1] 保持调用时的写法,所以旧的 path.resolve(argv1) === fileURLToPath(metaUrl) 比较在软链接下失败,exporter 会退出码 0 却什么都没写。对一个职责就是产出文件的 helper 来说,「静默成功」是最糟的失败形态。

方向: 对齐。补回一个包被删除后蒸发掉的覆盖率,属于维护,不是范围蔓延。

规模: 220 行 / 12 个文件——大约 41 行生产/配置(其中 25 行是 exporter helper),73 行测试,106 行 lockfile。没有触及核心模块路径:packages/ 下的两个文件都是测试,不在 packages/core/src/** 以及 auth|providers|models|config|tools|services 这些 glob 内。Stage 0 未触发,也远低于 500/1000 阈值。

方案: 现在范围是合理的。那个不可能失败的 unhandledrejection 代码块已经删掉了,与 #11099 重叠的 Shellfish.tsx hunk 也没有了——classifier 的改动只是在其旁边新增了一个 camelCase fixture。仍有三个顺带改动搭在一起(playwright 固定版本、followup-suggestions 文档重写、review-context 领域映射)。上一轮我问过是否拆分;你保留了它们并在 Risk & Scope 里说明了取舍,在这个体量下我不会继续纠缠。这些会作为备注带进 Stage 2,不是阻塞项。

风险: Stage 1e 无命中——这里没有触及与合并后 revert 相关的路径。

进入代码审查 🔍

Qwen Code · qwen3.8-max-2026-09-02

Reviewed at 95a35e0615068325336269c977e353bec1de4069 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

Rendered against a mock daemon (no real backend): the PR base vs this PR head 95a35e0. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

terminal-turn-error-copy-narrow-dark before/after

Full-resolution recordings (.webm) are attached to the workflow run.

Qwen Code · web-shell visuals

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Code review

Read against the base tree at the reviewed commit; nothing in this PR was built or executed here — the test evidence below is the PR's own CI, read through the API.

No Critical findings. My independent proposal for "restore the coverage that evaporated with the WebUI retirement" was: pin each lost boundary with a focused test, fix whatever made the module untestable, register the new lane in CI, and keep the committed manifest mirror in sync. That's what this does, so I spent the pass trying to break the assertions rather than looking for a different shape.

Every assertion checks out against the implementation it claims to pin. I verified each one rather than taking the diff's word:

  • file: rejection — isSafeHref / isSafeImageSrc (Markdown.tsx:164-190) gate on the allowlist /^(https?:|mailto:)/i, so file: fails closed on both paths. The two new cases pin existing behaviour; nothing in the renderer changed.
  • Theme toggle — document-index.html:2 ships <html lang="en" class="dark">, document-main.tsx:211-212 toggles dark and light against the same condition, nothing else in export-html touches documentElement, and readInitialTheme (:105) defaults to 'dark'. So the class attribute really is exactly one token and toBe('light') is the correct strengthening. Two wins in one hunk: it pins mutual exclusivity, which toContain('light') could not, and expect.poll fixes a genuine race — Playwright's click() waits for actionability, not for React's effect, so the old single read could sample before the toggle landed. expect.poll is vitest-native (root vitest is ^3.2.4), which is the right answer to the earlier Critical about toHaveClass being a @playwright/test matcher in a file that only imports vitest's expect.
  • Classifier fixture — shellCommandProcessor.ts is genuinely PLATFORM_INSENSITIVE, and I checked both regexes rather than assuming: SUBSYSTEM_SEGMENT needs a bare shell segment optionally followed by .ext (here the next char is C), and SUBSYSTEM_STEM_HEAD needs shell followed by - or _ (camelCase is neither). That's exactly the boundary worth pinning — if someone later teaches the stem-head rule about case transitions, this test is what tells them they widened the expensive lanes.
  • Followup-suggestions doc — #maybeEmitFollowupSuggestion (Session.ts:5089) opens with if (result.stopReason !== 'end_turn') return;, and the enumerated non-qualifying reasons match the ACP StopReason union. The earlier "unconditional" contradiction and the invented interrupted value are both gone.
  • ci.yml comment 25-file26-file — I counted the HELPER_TESTS list at this commit: exactly 26 entries.
  • Lane routing — the new file is the first node:test .mjs under integration-tests/, and it is correctly not collectable by vitest: the root vitest.config.ts delegates to projects, and integration-tests/vitest.config.ts includes only **/*.test.ts. It runs solely in the node --test helper lane. The new review-context.json rule is additive rather than overriding, because manifest-repository-context.ts:597 flat-maps all matching rules through cappedSortedUnique — so JSON ordering is not load-bearing here, and the committed mirror test was updated in the matching position.
  • Playwright pin — 1.61.1 exact lets root playwright dedupe onto the revision @playwright/test already resolved, which is what removes the nested @playwright/test/node_modules/{playwright,playwright-core,fsevents} copies and leaves one browser revision serving the exported-document gate. The pnpm-lock.yaml specifier was regenerated to match (the one-line diff from the Install (windows-latest) failure you caught).
  • The exporter is safe to import under test: its static import closure is node: builtins only, loadExportApi()'s dynamic import('@qwen-code/qwen-code/export') is reachable only from main(), and there is no top-level await — so registering it in the helper lane needs no build, and the isMain guard correctly returns false when node --test runs the file.

Two things worth a look, neither blocking

The symlink case is proven one level below the behaviour it claims. packages/chrome-extension/scripts/main-entry.test.js already exists for precisely this concern — same root-cause comment about Node realpathing the ESM entry but not process.argv[1] — and its runViaSymlink / expectSameRun helpers spawn the script through a real symlink and assert identical status, stdout and stderr, which proves the script actually ran. The new test asserts the predicate returns true instead. That is the right unit and it does fail against the old code, but it stops short of "a symlinked invocation writes the HTML". Same file also has no win32 guard on its fs.symlinkSync, where the established suite wraps itself in describe.skipIf(process.platform === 'win32'). Latent only, because HELPER_TESTS executes on ubuntu_runner (ci.yml:1233) and never on Windows today — but the repo's own precedent guards it, and it would bite a contributor running node --test on Windows without developer mode.

The same bug still sits in the shared helper. scripts/release-script-utils.js:61 does path.resolve(process.argv[1]) === fileURLToPath(importMetaUrl) — the identical non-realpath comparison — and gates main() in five release scripts (generate-changelog, generate-release-notes, verify-installation-release, build-hosted-installation-assets, upload-aliyun-oss-assets). The practical trigger is narrow, since npm run passes a literal path, so I'm not calling this an incident. But this PR fixes one copy and leaves the shared one exposed, and the repo now has seven implementations of this guard — two of which (packages/node-repl/src/index.ts:64, scripts/check-tui-dep-direction.mjs:499) already do it the realpath way. Converging on the shared helper, fixed, looks like the better follow-up than an eighth copy; #11336 seems the right home.

Smaller, take or leave: the new test's import closure is node: builtins only, so it would also qualify for HELPER_TESTS_DEP_FREE and could run before npm ci for earlier feedback. And root playwright is now 1.61.1 while packages/mobile-mcp still resolves 1.58.2 (nested rather than hoisted now) — root parity is what the browser gate needs and it is achieved, but the pin is not tree-wide, which is #11336's F1.

Testing

Evidence carried here is the PR's own CI at the reviewed commit, read via the check-runs and job-log APIs. I did not build or run anything from this branch.

The load-bearing lane is green, and I confirmed the new tests actually executed rather than inferring it from a green check — Lint & Static (ubuntu-latest, Node 22.x) is where HELPER_TESTS runs, and its log reports # tests 560 / # pass 560 / # fail 0 with all three new cases present:

ok 287 - recognizes a direct main-module invocation
ok 288 - recognizes a main-module invocation through a symlinked path
ok 289 - does not treat an imported module as the main module

Install (windows-latest), Install (macos-latest) and Install (ubuntu-latest) are all green, which is the real coverage for the playwright pin and the regenerated pnpm specifier. The same log also shows ✓ scripts/tests/export-html-from-chatrecord-jsonl.test.js (10 tests) and ✓ script main-entry guards under symlinked paths, so the input-gate suite and the pre-existing symlink harness both still pass alongside the additions.

The skipped Test (macos/windows-latest, Node 22.x) and Integration Tests (CLI, No Sandbox) are the fast-lane profile from Classify PR, not a gap this PR caused. The remaining skipped/cancelled entries are bot-orchestration jobs (review-pr, fallback-comment, precheck-pr, resolve-pr and friends) that don't apply to a same-repo branch. Three checks were still in flight when I read them.

Check Conclusion
Lint & Static (ubuntu-latest, Node 22.x) success
Test (ubuntu-latest, Node 22.x) success
Test (macos-latest, Node 22.x) skipped (fast lane)
Test (windows-latest, Node 22.x) skipped (fast lane)
Integration Tests (no-AK, No Sandbox) success
Integration Tests (CLI, No Sandbox) skipped
Install (ubuntu-latest) success
Install (macos-latest) success
Install (windows-latest) success
Capture web-shell visuals (ubuntu-latest, Node 22.x) success
Desktop Shell (ubuntu-22.04) success
Desktop Shell (windows-2022) success
OpenTUI no-flicker gate success
TUI parity snapshots (ink vs opentui) success
Real daemon E2E / Java 11 success
SDK Java (5 lanes) success
Classify PR success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) in_progress
delete-asset-branch in_progress
assign queued

Sandboxed verification would settle one thing CI cannot: @qwen-code /verify — the green lane proves isMainModule returns true for a symlinked path (ok 288), but nothing yet proves a symlinked invocation of the exporter actually writes the HTML file, which is the behaviour the PR says it restores. An A/B run against the base build would pin that end-to-end.

Not verified here: Windows behaviour of the realpath comparison. It is low-risk — both sides derive from the same OS path through the same function, and the exporter is a repo-internal CI helper that Windows lanes invoke with a literal path — but no lane at this head exercises it, and the new symlink test has no win32 skip.

中文说明

代码审查

在被审 commit 的基线树上静态阅读;本 PR 的任何代码都没有在这里被构建或执行——下面的测试证据来自 PR 自己的 CI,通过 API 读取。

没有 Critical 发现。对「补回随 WebUI 退役而蒸发的覆盖率」这个目标,我自己的方案是:为每个丢失的边界补一个聚焦测试、修掉让模块无法被测的那个问题、把新 lane 注册进 CI、并让提交的 manifest 镜像保持同步。这个 PR 做的正是这件事,所以我把这一轮花在尝试推翻它的断言上,而不是去找另一种形态。

每条断言都对照它声称要钉住的实现核对过了,我没有采信 diff 的自述:

  • file: 拒绝——isSafeHref / isSafeImageSrcMarkdown.tsx:164-190)用白名单 /^(https?:|mailto:)/i 把关,两条路径上 file: 都是 fail closed。新增的两个用例钉住的是既有行为,渲染器没有改动。
  • 主题切换——document-index.html:2 出厂就是 <html lang="en" class="dark">document-main.tsx:211-212 用同一个条件互斥地切换 darklightexport-html 里没有其他地方碰 documentElementreadInitialTheme:105)默认返回 'dark'。所以 class 属性确实只有一个 token,toBe('light') 是正确的加强。这一个 hunk 有两处收益:钉住了互斥性(toContain('light') 做不到),并且 expect.poll 修掉了一个真实的竞态——Playwright 的 click() 只等待可操作性,不等 React 的 effect,所以旧的单次读取可能在切换生效前就采样了。expect.poll 是 vitest 原生 API(根 vitest 为 ^3.2.4),这也正确回应了之前那条 Critical:toHaveClass@playwright/test 的 matcher,而该文件只引入了 vitest 的 expect
  • classifier fixture——shellCommandProcessor.ts 确实是 PLATFORM_INSENSITIVE,我核对了两个正则而不是想当然:SUBSYSTEM_SEGMENT 要求一个恰好是 shell 的段、后面可接 .ext(这里下一个字符是 C),SUBSYSTEM_STEM_HEAD 要求 shell 后面跟 -_(camelCase 两者都不是)。这正是值得钉住的边界——如果以后有人让 stem-head 规则识别大小写切换,这个测试会告诉他他把昂贵的 lane 放宽了。
  • followup-suggestions 文档——#maybeEmitFollowupSuggestionSession.ts:5089)第一行就是 if (result.stopReason !== 'end_turn') return;,枚举的不满足条件也与 ACP 的 StopReason 联合类型一致。之前的「unconditional」自相矛盾和杜撰的 interrupted 都已消失。
  • ci.yml 注释 25-file26-file——我数了当前 commit 的 HELPER_TESTS:正好 26 项。
  • lane 路由——新文件是 integration-tests/ 下第一个 node:test.mjs,并且确实不会被 vitest 收集:根 vitest.config.ts 交给 projects,而 integration-tests/vitest.config.ts 只 include **/*.test.ts。它只在 node --test 的 helper lane 里跑。新增的 review-context.json 规则是叠加而非覆盖,因为 manifest-repository-context.ts:597 会把所有命中的规则 flat-map 后过 cappedSortedUnique——所以这里 JSON 顺序不是关键,提交的镜像测试也在对应位置做了同步。
  • Playwright 固定版本——精确的 1.61.1 让根 playwright 能与 @playwright/test 已解析的版本去重,这正是移除嵌套的 @playwright/test/node_modules/{playwright,playwright-core,fsevents} 的原因,从而让导出文档的浏览器 gate 只面对一个 browser revision。pnpm-lock.yaml 的 specifier 也已重新生成以保持一致(就是你发现的 Install (windows-latest) 失败带来的那一行 diff)。
  • exporter 在测试下被 import 是安全的:它的静态 import 闭包只有 node: 内置模块,loadExportApi() 里的动态 import('@qwen-code/qwen-code/export') 只能从 main() 到达,且没有 top-level await——所以注册进 helper lane 不需要构建,而 node --test 运行该文件时 isMain 守卫会正确返回 false。

两处值得看一下,都不阻塞

软链接场景的证明层次低于它声称的行为。 packages/chrome-extension/scripts/main-entry.test.js 正是为同一问题而存在的——注释里写着同样的根因(Node 对 ESM 入口做 realpath,但对 process.argv[1] 不做)——它的 runViaSymlink / expectSameRun 会真的通过软链接 spawn 脚本,并断言 status、stdout、stderr 完全一致,这才证明脚本确实跑起来了。新测试断言的是谓词返回 true。这个单元选得对,对旧代码也确实会失败,但没有走到「软链接调用会写出 HTML」这一步。同一文件里 fs.symlinkSync 也没有 win32 守卫,而既有套件用 describe.skipIf(process.platform === 'win32') 包了起来。目前只是潜在问题,因为 HELPER_TESTS 跑在 ubuntu_runner 上(ci.yml:1233),今天不会在 Windows 执行——但仓库自己的先例是加了守卫的,而且在没开开发者模式的 Windows 上本地跑 node --test 的贡献者会被它绊到。

同一个 bug 还在共享 helper 里。 scripts/release-script-utils.js:61 写的是 path.resolve(process.argv[1]) === fileURLToPath(importMetaUrl)——完全相同的非 realpath 比较——并且为五个 release 脚本(generate-changeloggenerate-release-notesverify-installation-releasebuild-hosted-installation-assetsupload-aliyun-oss-assets)把关 main()。实际触发面很窄,因为 npm run 传的是字面路径,所以我不把它称作事故。但本 PR 修了其中一份副本,共享的那份仍然暴露着,而这个守卫在仓库里现在有七份实现——其中两份(packages/node-repl/src/index.ts:64scripts/check-tui-dep-direction.mjs:499)已经用的是 realpath 写法。与其出现第八份副本,更好的后续是收敛到修好的共享 helper;#11336 看起来是合适的归属。

更小的、可选:新测试的 import 闭包只有 node: 内置模块,所以它也符合 HELPER_TESTS_DEP_FREE 的条件,可以在 npm ci 之前跑以获得更早反馈。另外根 playwright 现在是 1.61.1,而 packages/mobile-mcp 仍解析到 1.58.2(现在变成嵌套而非提升)——浏览器 gate 需要的是根级别的一致性,这一点已经达成,但这个固定不是全树范围的,也就是 #11336 的 F1。

测试

这里承载的证据是 PR 自己在被审 commit 上的 CI,通过 check-runs 与 job-log API 读取。我没有从这个分支构建或运行任何东西。

关键的 lane 是绿的,而且我确认了新测试确实执行了,而不是从一个绿勾去推断——HELPER_TESTS 跑在 Lint & Static (ubuntu-latest, Node 22.x),它的日志给出 # tests 560 / # pass 560 / # fail 0,三个新用例都在:

ok 287 - recognizes a direct main-module invocation
ok 288 - recognizes a main-module invocation through a symlinked path
ok 289 - does not treat an imported module as the main module

Install (windows-latest)Install (macos-latest)Install (ubuntu-latest) 全绿,这才是 playwright 固定版本与重新生成的 pnpm specifier 的实际覆盖。同一份日志还显示 ✓ scripts/tests/export-html-from-chatrecord-jsonl.test.js (10 tests)✓ script main-entry guards under symlinked paths,说明输入 gate 套件和既有的软链接套件在新增内容旁边依然通过。

被 skip 的 Test (macos/windows-latest, Node 22.x)Integration Tests (CLI, No Sandbox) 来自 Classify PR 的 fast-lane profile,不是本 PR 造成的缺口。其余 skipped/cancelled 条目是 bot 编排任务(review-prfallback-commentprecheck-prresolve-pr 等),对同仓分支不适用。我读取时还有三个 check 在飞行中。

(上方表格由 finalize 任务在 CI 收敛后原地更新。)

沙箱验证能解决一件 CI 解决不了的事:@qwen-code /verify——绿 lane 证明了 isMainModule 对软链接路径返回 trueok 288),但还没有任何东西证明通过软链接调用 exporter 会真的写出 HTML 文件,而这正是 PR 声称恢复的行为。对基线构建做一次 A/B 就能端到端钉住它。

此处未验证:realpath 比较在 Windows 上的行为。风险低——两侧都由同一个 OS 路径经同一个函数推导,且 exporter 是仓库内部的 CI helper,Windows lane 用字面路径调用它——但当前 head 上没有 lane 会走到它,而新的软链接测试也没有 win32 skip。

Qwen Code · qwen3.8-max-2026-09-02

Reviewed at 95a35e0615068325336269c977e353bec1de4069 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — everything I could check statically checks out and CI substantiates the part that matters; the nits are real but none of them is a reason to hold this back.

Stepping back against the proposal I wrote before reading the diff: this is what I would have done, and in two places it is better than what I would have done. Keeping the committed manifest mirror in the same commit as the rule it mirrors is the discipline that stops those two drifting, and registering the new file in HELPER_TESTS rather than leaving it as an orphan that nothing runs is the difference between coverage that exists and coverage that counts. The whole PR is 220 lines, and roughly half of it is lockfile.

The thing I was watching for, given why this PR exists, is a green test that pins nothing — that was the headline finding last round, and it is the failure mode this PR was opened to fix. That block is gone rather than reworded, which is the right resolution. What replaced it in the same file is stronger than what it superseded: expect.poll(...).toBe('light') fixes a race and pins mutual exclusivity in one hunk, and I confirmed against document-index.html and document-main.tsx that <html>'s class really is a single token, so the tightened assertion cannot flake. The classifier fixture is the other one I'd single out — it pins a camelCase head that neither subsystem regex catches, which is a boundary someone will otherwise "improve" straight into summoning both expensive lanes.

The two notes in Stage 2 are the honest reservations, and I want to be clear about their weight. The symlink test proves the predicate rather than the end-to-end write; that is one level below the claim, and the repo already owns a harness that proves the stronger thing. And scripts/release-script-utils.js still carries the identical non-realpath comparison across five release scripts, so this PR fixes one of seven copies. Neither is a defect introduced here, both are cheap to converge later, and under the round-6 rule you've already applied to F1–F4 they belong in #11336 rather than widening this diff. I'd merge with them open.

Six months from now this reads fine: the comments say why (the realpath asymmetry, the mutual-exclusivity invariant, the dep-free lane constraint) instead of narrating what, and every assertion is anchored to a named implementation. That's the part that keeps coverage from rotting again the next time a package is retired.

Verdict is approve. CI has one PR workflow still in flight at this commit, so I'm not posting the approval in this run — it will land automatically, pinned to the commit below, once everything on it goes green.

One mechanical thing a human needs to do. reviewDecision is still CHANGES_REQUESTED, from three of my own reviews at older commits (2a11002, 503a5b4, a3ddb060). Pushes dismiss stale approvals under this repo's branch protection but not outstanding request-changes reviews, so a green approval alone will not make this mergeable — the earlier reviews need dismissing. I'm not dismissing my own gating reviews unilaterally on a re-run; a maintainer should eyeball them and clear the ones whose asks are now met. For the record, the stage=1-pre one at 503a5b4 is fully answered (rebased onto 6ba04fc2, retitled, rescoped), and the Critical behind the a3ddb060 one is fixed by the expect.poll rewrite.

The one-line body fix from Stage 1 is still open and still worth doing: "neither reimplements nor closes #11076" keeps a live closing reference to an already-closed issue, because GitHub's parser doesn't read intent. Drop the keyword and this gate stops re-firing on every re-run.

中文说明

Confidence: 4/5 —— 能静态核对的部分全部对得上,CI 也证实了最关键的那部分;这些瑕疵是真实存在的,但没有一个足以成为压住这个 PR 的理由。

对照我在读 diff 之前写下的方案回看:这就是我会做的,而且有两处比我本来会做的更好。把提交的 manifest 镜像和它所镜像的规则放在同一个 commit 里,是防止两者漂移的纪律;把新文件注册进 HELPER_TESTS 而不是留成一个没人跑的孤儿,是「覆盖率存在」和「覆盖率有效」之间的区别。整个 PR 只有 220 行,其中大约一半是 lockfile。

考虑到这个 PR 的由来,我最提防的就是一个钉不住任何东西的绿测试——那是上一轮的头号发现,也正是本 PR 要修的失败形态。那个代码块是被删掉而不是被改写,这是正确的处理方式。同一文件里替代它的内容比它取代的更强:expect.poll(...).toBe('light') 在一个 hunk 里既修了竞态又钉住了互斥性,而我对照 document-index.htmldocument-main.tsx 确认了 <html> 的 class 确实只有一个 token,所以收紧后的断言不会 flake。另一个我会单独点出的是 classifier fixture——它钉住了两个 subsystem 正则都抓不到的 camelCase 词头,否则将来有人会把这个边界「优化」成同时召唤两条昂贵 lane。

Stage 2 里的两条备注是我真实的保留意见,我想把它们的分量说清楚。软链接测试证明的是谓词,而不是端到端的写文件;这比它声称的低一层,而仓库里已经有一个能证明更强命题的 harness。另外 scripts/release-script-utils.js 在五个 release 脚本上仍然带着完全相同的非 realpath 比较,所以本 PR 修的是七份副本中的一份。两者都不是这里引入的缺陷,后续收敛成本都很低,而且按你已经用在 F1–F4 上的 round-6 规则,它们应该归到 #11336,而不是继续撑大这个 diff。带着它们开放,我也会合并。

六个月后再看这份代码是没问题的:注释写的是为什么(realpath 的不对称、互斥不变量、dep-free lane 的约束),而不是复述做了什么,每条断言都锚定在一个有名字的实现上。这正是下一次再有包被退役时,覆盖率不会再次腐坏的关键。

结论是 approve。当前 commit 上还有一个 PR workflow 在飞行中,所以这一轮我不发布 approval——它会自动落地,并钉在下面的 commit 上,等该 commit 上的一切都变绿。

有一件机械性的事需要人来做。 reviewDecision 仍是 CHANGES_REQUESTED,来自我自己在更早 commit 上的三次 review(2a11002503a5b4a3ddb060)。本仓库的分支保护会在 push 时 dismiss 过期的 approval,但不会 dismiss 仍然挂着的 request-changes review,所以仅仅一个绿的 approval 并不能让它可合并——早先那几次 review 需要被 dismiss。我不会在一次重跑里单方面 dismiss 自己的门禁 review;应该由 maintainer 过一眼,清掉那些要求已被满足的。作为记录:503a5b4 上那条 stage=1-pre 已完全被回应(rebase 到 6ba04fc2、改标题、重定范围),而 a3ddb060 那条背后的 Critical 已由 expect.poll 重写修掉。

Stage 1 里那一行的修改仍然开放,也仍然值得做:「neither reimplements nor closes #11076」会让一个已关闭的 issue 保持有效的关闭引用,因为 GitHub 的解析器不判断语义。去掉这个关键词,这道门禁就不会在每次重跑时继续触发。

Qwen Code · qwen3.8-max-2026-09-02

Reviewed at 95a35e0615068325336269c977e353bec1de4069 · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot 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.

Partially reviewed — gaps disclosed.

Not explored to full depth (tool budget reached): "agent 1a": did not execute the two web-shell vitest files ( Markdown.test.ts , clientLifecycle.test.ts ); both were resolved by reading the implementations they assert ag…; "agent 6a": live mutation probe of Finding 1 (delete document-index.html:57 , run the fails closed… integration test under Chromium to observe it stay green) — resolved …; "agent 6a": running packages/web-shell vitest for Markdown.test.ts and clientLifecycle.test.ts — both verified against their implementations by reading instead.; "agent 2": enumerating all 21 stopReason: 'end_turn' producers in packages/cli/src/acp-integration/session/Session.ts to test whether a guard/loop-protection stop also….

Not reviewed: reverse audit — stopped before round 1 by the review time budget.

Not linted (tool limitation, not a blocker): .github/workflows/ci.yml — actionlint embedded-shell source mapping is not yet supported.

中文说明

仅完成部分审查,审查缺口已披露。

未探索到全部深度(达到工具调用预算):"agent 1a"did not execute the two web-shell vitest files ( Markdown.test.ts , clientLifecycle.test.ts ); both were resolved by reading the implementations they assert ag…"agent 6a"live mutation probe of Finding 1 (delete document-index.html:57 , run the fails closed… integration test under Chromium to observe it stay green) — resolved …"agent 6a"running packages/web-shell vitest for Markdown.test.ts and clientLifecycle.test.ts — both verified against their implementations by reading instead."agent 2"enumerating all 21 stopReason: 'end_turn' producers in packages/cli/src/acp-integration/session/Session.ts to test whether a guard/loop-protection stop also…

未审查:反向审计——评审时间预算不足,未能开始第 1 轮。

未检查(工具限制,非阻断):.github/workflows/ci.yml——actionlint 对 workflow 内嵌 shell 的源映射尚未支持。

— qwen3.8-max via Qwen Code /review (v0.23.0)

Comment thread integration-tests/chat-transcript-document.test.ts Outdated
Comment thread integration-tests/chat-transcript-document.test.ts Outdated
Comment thread integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.js Outdated
Comment thread docs/users/features/followup-suggestions.md Outdated
Comment thread docs/users/features/followup-suggestions.md Outdated
Comment thread integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs Outdated
Comment thread integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs Outdated
Comment thread docs/users/features/followup-suggestions.md Outdated
Comment thread integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs Outdated
The browser gate imports expect from vitest and drives playwright's
Locator directly, so toHaveClass does not exist on the assertion and
typecheck:integration failed with TS2339 on this line. Poll the class
attribute through the same expect.poll idiom the neighbouring
assertions use, which keeps the retry the assertion was switched to
and needs no @playwright/test dependency.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-conflict/jmtooas8sln

@qwen-code-ci-bot qwen-code-ci-bot 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.

Reviewed. Suggestions are inline.

8 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R1-2 inert synthetic-unhandledrejection block — integration-tests/chat-transcript-document.test.ts:670 — already reported (comment 3940965876)
  • R1-3 isMain path-spelling guard — integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.js:169 — already reported (comment 3940965877)
  • R1-5 stop-reason enumeration in the added parenthetical — docs/users/features/followup-suggestions.md:5 — already reported (comment 3940965878)
  • R1-6 "unconditional" wording — docs/users/features/followup-suggestions.md:39 — already reported (comment 3940965880)
  • R1-8 legacy-JSONL rejection path not reached — integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs:39 — already reported (comment 3940965882)
  • R1-9 hand-rolled ChatRecord factory — integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs:15 — already reported (comment 3940965884)
  • R1-10 untested negative side of the stop-reason gate — docs/users/features/followup-suggestions.md:5 — already reported (comment 3940965886)
  • R1-11 node:test file under a vitest-mapped path — integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs:13 — already reported (comment 3940965889)

Not explored to full depth (tool budget reached): "agent 1a": none — I did not execute the integration test (no node_modules in this review worktree and the renderer dist/ is unbuilt), so the claim "this block passes w….

中文说明

已审查。 建议见行内评论。

本轮确认的 8 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未探索到全部深度(达到工具调用预算):"agent 1a"none — I did not execute the integration test (no node_modules in this review worktree and the renderer dist/ is unbuilt), so the claim "this block passes w…

— qwen3.8-max via Qwen Code /review (v0.23.0)

Comment thread integration-tests/chat-transcript-document.test.ts Outdated
yiliang114 and others added 3 commits September 6, 2026 06:54
`document-main.tsx` toggles `dark` and `light` on <html> mutually
exclusively and nothing else writes `documentElement` classes, so the
attribute is always exactly one of them. Matching `/light/` as an
unanchored substring would also accept `dark light`, which is the
theme-toggle breakage this poll exists to catch.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtow5pgslz
Node realpath-resolves `import.meta.url` for the ESM main module but
leaves `process.argv[1]` as invoked, so comparing the raw spellings made
a symlinked invocation skip `main()` and exit 0 having written nothing —
a silent no-op the harness reads as success. Compare realpaths instead
and export the predicate so both directions are pinned.

Also extract the input gate into `assertRenderableJsonl` so the legacy
exported-JSONL rejection can be asserted directly: the previous tests
only pinned the two predicates feeding it, so deleting the guard and
losing the remediation hint kept every test green.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtow5pgslz
main landed #11107 ("close the four deferred #9812 review follow-ups")
while this branch was open, and it overlaps most of this branch's work.
Conflict resolutions:

- .github/scripts/ci/classify-platform-sensitivity.test.mjs: both sides
  appended a different substring-trap fixture to the same list; kept both
  (main's Shellfish.tsx, this branch's shellCommandProcessor.ts). The
  classifier itself is unchanged on both sides.

- packages/web-shell/client/daemon/useDaemonFollowupSuggestion.ts: main's
  version is a superset of this branch's (same InputForm -> ChatEditor doc
  rename, plus the Prettier wrap and an extra note), so took main's.

- integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.js:
  main extracted the same input gate as selectChatRecords and added
  renderHtmlFromObjects, so dropped this branch's duplicate
  assertRenderableJsonl and kept main's structure. Kept this branch's
  realpath-based isMainModule guard, because main's inline path.resolve
  compare still misses a symlinked invocation, and folded main's
  typeof-string argv1 check into it.

- export-html-from-chatrecord-jsonl.test.mjs: the gate is now covered by
  main's vitest suite (scripts/tests/export-html-from-chatrecord-jsonl.test.js),
  so this helper test keeps only the main-module cases, including the
  symlink one that nothing else covers.

Verified locally: node --test on both helper test files (3/3 and 12/12
passing), node scripts/check-lockfile.js passing on the merged
package.json/package-lock.json (playwright pinned to 1.61.1), no conflict
markers, git diff --check clean. The web-shell vitest and Playwright
additions were not run here (no node_modules in this worktree); they were
checked against the implementations they pin, which main did not touch.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-conflict/jmtp15qxqm5

@qwen-code-ci-bot qwen-code-ci-bot 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.

Partially reviewed — gaps disclosed. Suggestions are inline.

4 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R1-2 inert synthetic-unhandledrejection block — integration-tests/chat-transcript-document.test.ts:671 — already reported (comment 3940965876)
  • R1-5 stop-reason enumeration in the added parenthetical — docs/users/features/followup-suggestions.md:5 — already reported (comment 3940965878)
  • R1-6 "unconditional" wording — docs/users/features/followup-suggestions.md:39 — already reported (comment 3940965880)
  • R1-10 untested negative side of the stop-reason gate — docs/users/features/followup-suggestions.md:5 — already reported (comment 3940965886)

Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": did not open react-markdown's defaultUrlTransform (applied upstream at Markdown.tsx:752 ) to confirm what entity/control-character normalization the URL has …; "agent 1b": I did not run the web_shell_e2e_smoke browser gate ( chat-transcript-document.test.ts ) locally — no Chromium installed here — so the strengthened .toBe('lig…; "agent 2": did not execute integration-tests/chat-transcript-document.test.ts (requires a Chromium install) — its new fail-closed block and theme assertion were reviewed….

Not reviewed: reverse audit — stopped before round 3 by the review time budget.

Not linted (tool limitation, not a blocker): .github/workflows/ci.yml — actionlint embedded-shell source mapping is not yet supported.

Deferred under the convergence posture (round 3, not a blocker) — recorded, not requested in this round:

  • .github/scripts/ci/classify-platform-sensitivity.test.mjs:127 — [probe] fixture pins a host-coupled file as PLATFORM_INSENSITIVE
  • .github/workflows/ci.yml:82 — [probe] new suite's only CI path is an unpinned hand-edited list

Convergence: round 3 posted 2 inline comment(s), 2 of them reported for the first time; the previous round posted 1 (1 new). The rate of new findings is not falling. Batching the remaining fixes and verifying them before the next push, or dropping this PR's reviews to --severity-floor critical, keeps the loop from re-deriving the same set. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

本轮确认的 4 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)"did not open react-markdown's defaultUrlTransform (applied upstream at Markdown.tsx:752 ) to confirm what entity/control-character normalization the URL has …"agent 1b"I did not run the web_shell_e2e_smoke browser gate ( chat-transcript-document.test.ts ) locally — no Chromium installed here — so the strengthened .toBe('lig…"agent 2"did not execute integration-tests/chat-transcript-document.test.ts (requires a Chromium install) — its new fail-closed block and theme assertion were reviewed…

未审查:反向审计——评审时间预算不足,未能开始第 3 轮。

未检查(工具限制,非阻断):.github/workflows/ci.yml——actionlint 对 workflow 内嵌 shell 的源映射尚未支持。

收敛姿态下延后(第 3 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 3 轮发布了 2 条行内评论,其中 2 条是首次提出;上一轮发布了 1 条(其中 1 条首次提出)。新发现的产出速度没有下降。把剩余修复攒成一批、验证后再推送,或将本 PR 的评审降到 --severity-floor critical,可以避免循环反复推导同一组发现。(仅为观察——本轮评审未因此扣留任何内容。)

— qwen3.8-max via Qwen Code /review (v0.23.0)

Comment thread packages/web-shell/client/daemon/session/clientLifecycle.test.ts Outdated
yiliang114 and others added 4 commits September 6, 2026 11:51
The parenthetical this PR added named `interrupted` as a daemon stop
reason. The ACP `StopReason` union is
`end_turn | max_tokens | max_turn_requests | refusal | cancelled`, so
there is no `interrupted`; an interrupted turn reports `cancelled`. Name
the four suppressing values the union actually defines so an integrator
writing `stopReason === '...'` gets a branch that can fire.

"unconditional" was wider than the code and contradicted the condition
list on the same page: `Session.ts#maybeEmitFollowupSuggestion` returns
early on the stop reason, the todo stop guard,
`ui.enableFollowupSuggestions`, PLAN approval mode, a missing chat, and a
non-model last history entry. Point at those conditions instead; the cost
advice still holds because the `enableFollowupSuggestions === false`
return sits before `generatePromptSuggestion` is reached.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtp6vibomd
`persists under the literal historical key prefix`, added on this branch,
became a byte-equivalent twin of `writes under the historical WebUI key`,
which reached main with #11107 and arrived here through the merge at this
PR's head: same `persistStableClientId('client-a', 'session-a')`, same
literal `qwen-code-webui-client-id:session:session-a`, same expectation.
One fact was reported under two names, so a prefix rename reddened two
tests and an auditor could not tell which copy was load-bearing.

The surviving test's comment already carries the rename rationale, so
dropping the copy loses no coverage.

Verified with `cd packages/web-shell && npx vitest run
client/daemon/session/clientLifecycle.test.ts`: 19 passed before, 18
passed after. Mutating SESSION_CLIENT_ID_STORAGE_PREFIX to
`qwen-code-webshell-client-id:session:` after the dedupe still reddens
3 tests (15 passed), including the surviving `writes under the historical
WebUI key`; reverting the mutation returns to 18 passed.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtp6vibomd
The block this PR added after the CDN-failure loop could not reach the
`window.addEventListener('unhandledrejection', showLoadError)` handler its
comment claimed to exercise. `page.route('**/*', route =>
route.abort('blockedbyclient'))` fails the `<script id="transcript-renderer">`
load, the template's capture-phase `error` listener (document-index.html:45-56,
matching `event.target.id === 'transcript-renderer'`) calls `showLoadError()`
first, and `setContent(..., { waitUntil: 'load' })` only resolves after that
subresource has failed - so `data-render-complete='error'` and the alert text
were already in place before `page.evaluate` dispatched anything, and
showLoadError's one-shot guard (`if (document.body.dataset.renderComplete)
return;`, document-index.html:30) made the synthetic rejection a no-op. The
block was a strict duplicate of the loop's own `rendererBody === null`
iteration: identical all-abort route, identical two assertions.

Exercising the handler for real needs a renderer body that passes the
document's pinned SRI integrity and then rejects during boot; no body this gate
can serve passes SRI, so the arm cannot be made honest here. Deleting it
returns the file to origin/main's state for this test - the handler and its
missing coverage are both pre-existing on main (packages/web-templates is not
in this PR's diff), so no coverage this PR was responsible for is lost.

Verified: `npm run typecheck:integration` reports 0 errors in this file (6
remain, all in packages/** against generated/dist paths absent from an unbuilt
worktree: channels/feishu, cli/src/generated/git-commit.js,
web-templates/src/generated/*); `npx prettier --check` and `npx eslint` clean
on the file. The browser gate itself was not executed locally - it needs the
full build chain plus Chromium.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtpfg5ewmp
…ts lane

This PR added
`integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs`,
the first `node:test` file under `integration-tests/`, and wired it into CI
through the `HELPER_TESTS` env list in `.github/workflows/ci.yml`. The tracked
review-context manifest still mapped the whole `integration-tests/**` path to
`recommendedTests: ['integration-tests']`, whose vitest lane collects
`**/*.test.ts` only (`integration-tests/vitest.config.ts:21`), so a future PR
touching only the exporter and its `.mjs` test is told to run a lane that
cannot collect the file - the same silent drop `HELPER_TESTS` exists to
prevent.

Add a narrower rule for `integration-tests/concurrent-runner/**` recommending
`helper-tests`. Matching rules merge rather than first-match, so the lane list
for that directory becomes the union and every other `integration-tests/` path
is untouched. The pinned `expectedManifest` in
`manifest-repository-context.committed.test.ts` moves with it, which is what
that pin is for.

Verified through the real provider (tsx importing
`packages/cli/src/commands/review/lib/manifest-repository-context.ts`) because
the packages/cli vitest globalSetup gate refuses to run on this unbuilt
worktree:

  concurrent-runner .mjs           -> ["helper-tests","integration-tests"]
  concurrent-runner .js            -> ["helper-tests","integration-tests"]
  chat-transcript-document.test.ts -> ["integration-tests"] (unchanged)
  all 10 rules co-matching         -> non-null, 80 relatedPaths, and every
                                      `paths` probe matched some rule

Also checked the committed manifest deep-equals the test's pinned literal (10
rules on both sides), and that dropping the new rule breaks that equality, so
the pin is not tautological. `npx prettier --check` and `npx eslint` clean on
both files.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtpfg5ewmp

@qwen-code-ci-bot qwen-code-ci-bot 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.

@yiliang114 — the triage gate stopped here, on a duplicate-scope check, before code review.

The linked issue is already done. This PR says Closes #11076, but #11076 was closed as completed by #11107 (fix(web-shell): close the four deferred #9812 review follow-ups, merged 2026-09-05). I checked all four tracked items against main, and every one has landed:

#11076 item where it lives on main
literal qwen-code-webui-client-id:session: compat pin packages/web-shell/client/daemon/session/clientLifecycle.test.ts
legacy exported JSONL rejection + ChatRecord happy path scripts/tests/export-html-from-chatrecord-jsonl.test.js
Shellfish.tsx substring-trap fixture on a live path .github/scripts/ci/classify-platform-sensitivity.test.mjs (line 126)
InputFormChatEditor / ChatPane in public docs packages/web-shell/client/daemon/useDaemonFollowupSuggestion.ts

So merging this PR would close nothing — the reference is stale.

There is still a real delta, and it is not #11076's scope. None of the following exists on main:

  • export-html-from-chatrecord-jsonl.jsinvokedDirectly replaced by an exported isMainModule() that compares realpaths, plus a new node:test file pinning direct / symlinked / imported invocation, plus its HELPER_TESTS registration in ci.yml
  • Markdown.test.tsfile: scheme rejection for both isSafeHref and isSafeImageSrc
  • chat-transcript-document.test.ts — the theme-toggle assertion replaced with a poll for exactly light
  • classify-platform-sensitivity.test.mjs — one further shellCommandProcessor.ts fixture
  • docs/users/features/followup-suggestions.md — the stop-reason gate wording and the headless/SDK opt-out note
  • package.json / package-lock.jsonplaywright moved from ^1.57.0 to an exact 1.61.1
  • .qwen/review-context.json and its mirror manifest-repository-context.committed.test.ts — a new integration-tests/concurrent-runner/**helper-tests domain mapping

None of that is redundant work, and I am not asking you to drop it. The problem is that the PR no longer matches its own stated goal: it claims to close an issue that is closed, and what it actually carries is a different set of changes.

What I'd like before this goes further:

  1. Rebase onto current main. The merge base (03c0909) is 25 commits behind. It does already contain #11107 — the diff removes the invokedDirectly / fileURLToPath guard that #11107 added, and the classifier fixture list already carries the Shellfish.tsx path — which is why the overlap correctly disappeared from your later commits.
  2. Reduce the PR to that delta and re-scope it: drop Closes #11076, and retitle and redescribe it as what it now is — largely follow-ups to the /review findings on this PR, plus a few independent items.
  3. Consider splitting the drive-bys. The playwright pin, the followup-suggestions doc rewrite, and the review-context.json domain mapping each stand on their own and would be easier to review and to revert separately.

One note so the two are not confused: a separate CHANGES_REQUESTED from the /review skill also stands on this PR, at the older commit 2a11002 and on different grounds. This comment is the triage duplicate-scope gate.

中文说明

@yiliang114 —— triage 门禁在此停下,原因是重复范围检查,尚未进入代码审查。

关联 issue 已经完成了。 本 PR 写的是 Closes #11076,但 #11076 已由 #11107fix(web-shell): close the four deferred #9812 review follow-ups,2026-09-05 合并)以 completed 关闭。我把四项待办逐条对照 main 核实过,全部已经落地:

  • 字面量 qwen-code-webui-client-id:session: 兼容性断言 → packages/web-shell/client/daemon/session/clientLifecycle.test.ts
  • legacy exported JSONL 拒绝路径 + ChatRecord 正常路径 → scripts/tests/export-html-from-chatrecord-jsonl.test.js
  • 使用仍存在的 Web Shell 路径补回 Shellfish.tsx substring-trap fixture → .github/scripts/ci/classify-platform-sensitivity.test.mjs(第 126 行)
  • 公共文档中 InputForm 改为 ChatEditor / ChatPanepackages/web-shell/client/daemon/useDaemonFollowupSuggestion.ts

因此合并本 PR 不会关闭任何东西——这个关联已经失效。

但仍存在真实的增量,而且不属于 #11076 的范围。 以下内容在 main 上都不存在:

  • export-html-from-chatrecord-jsonl.js——把 invokedDirectly 换成导出的 isMainModule()(比较 realpath),并新增一个 node:test 文件钉住直接调用/软链调用/被 import 三种情形,外加 ci.yml 里的 HELPER_TESTS 注册
  • Markdown.test.ts——为 isSafeHrefisSafeImageSrc 补上 file: 协议拒绝
  • chat-transcript-document.test.ts——主题切换断言改为轮询精确等于 light
  • classify-platform-sensitivity.test.mjs——再补一条 shellCommandProcessor.ts fixture
  • docs/users/features/followup-suggestions.md——stop-reason 触发条件的措辞,以及 headless/SDK 客户端应关闭的提示
  • package.json / package-lock.json——playwright^1.57.0 改为精确的 1.61.1
  • .qwen/review-context.json 及其镜像测试 manifest-repository-context.committed.test.ts——新增 integration-tests/concurrent-runner/**helper-tests 的领域映射

这些都不是重复劳动,我没有要求你丢弃它们。问题在于这个 PR 已经与它自己声明的目标不符:它声称要关闭一个已经关闭的 issue,而实际携带的是另一组改动。

在继续之前希望你做的:

  1. rebase 到当前 main。merge base(03c0909)已落后 25 个提交。它确实已经包含 #11107——本 diff 删除的正是 #11107 加入的 invokedDirectly / fileURLToPath 判定,classifier fixture 列表里也已经有 Shellfish.tsx 路径——这也是为什么后续提交中重叠部分已经正确消失。
  2. 把 PR 收敛到上述增量并重新定位范围:去掉 Closes #11076,把标题和描述改写成它现在的真实内容——主要是针对本 PR /review 发现的跟进,外加几项独立改动。
  3. 建议拆分顺手带上的改动。playwright 版本固定、followup-suggestions 文档改写、review-context.json 领域映射,这三项都能独立成立,分开更容易 review、也更容易回滚。

另外说明一下以免混淆:本 PR 上还有另一条来自 /review skill 的 CHANGES_REQUESTED,它针对的是较早的提交 2a11002,理由也不同。本条是 triage 的重复范围门禁。

Qwen Code · qwen3.8-max-2026-09-02

@qwen-code-ci-bot qwen-code-ci-bot 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.

Partially reviewed — gaps disclosed.

1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • D4-6 unpinned hand-edited HELPER_TESTS list — already reported (review 5123901497, round-3 convergence-posture deferral at .github/workflows/ci.yml:82)

Not explored to full depth (tool budget reached): "agent 3c": I did not execute the vitest suites I reasoned about ( packages/web-shell/client/components/messages/Markdown.test.ts , integration-tests/chat-transcript-docum…; "agent reverse-audit (round 2)": did not execute packages/cli 's manifest-repository-context.committed.test.ts (it resolves the package entry into dist/ , which this worktree has not built)…; "agent reverse-audit (round 2)": did not run the chat-transcript-document.test.ts browser gate myself (needs a chromium download); the expect.poll(...).toBe('light') hunk is assessed from t…; "agent 2": verifying the doc's stop-reason claim ( end_turn -only generation, cancelled / refusal / max_tokens / max_turn_requests excluded) against the daemon's followu…; "agent 6b": did not execute packages/web-shell/client/components/messages/Markdown.test.ts under vitest — verified the two new file: assertions against Markdown.tsx:16…, and 3 more.

Not reviewed: reverse audit — stopped before round 3 by the review time budget.

Not linted (tool limitation, not a blocker): .github/workflows/ci.yml — actionlint embedded-shell source mapping is not yet supported.

Test Plan (not a blocker): client/daemon/session/clientLifecycle.test.tsno such file or directory.

Deferred under the convergence posture (round 4, not a blocker) — recorded, not requested in this round:

  • .github/workflows/ci.yml:82 — [test] the 25th HELPER_TESTS entry falsifies the list's own descriptions — ci.yml:832 still says "24-file", ci.yml:78 still scopes it to .github/scripts
  • integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs:16 — [probe] the comment's reason for a second test home ("cannot exercise without spawning") is measured false — the graft passes 11/11 and still kills the muta…
  • integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs:16 — [review] Closes #11076, but #11107 delivered all four items inside the merge base and this PR's net diff to each is empty
  • integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs:17 — [probe] its directory puts this test outside the qwen-triage flake gate — the only one of 25 HELPER_TESTS entries that never runs there
  • package.json:161 — [probe] the pin falsifies the two-Playwright-trees premise qwen-triage.yml:3950-3958 states as present fact
中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未探索到全部深度(达到工具调用预算):"agent 3c"I did not execute the vitest suites I reasoned about ( packages/web-shell/client/components/messages/Markdown.test.ts , integration-tests/chat-transcript-docum…"agent reverse-audit (round 2)"did not execute packages/cli 's manifest-repository-context.committed.test.ts (it resolves the package entry into dist/ , which this worktree has not built)…"agent reverse-audit (round 2)"did not run the chat-transcript-document.test.ts browser gate myself (needs a chromium download); the expect.poll(...).toBe('light') hunk is assessed from t…"agent 2"verifying the doc's stop-reason claim ( end_turn -only generation, cancelled / refusal / max_tokens / max_turn_requests excluded) against the daemon's followu…"agent 6b"did not execute packages/web-shell/client/components/messages/Markdown.test.ts under vitest — verified the two new file: assertions against Markdown.tsx:16…,另有 3 条。

未审查:反向审计——评审时间预算不足,未能开始第 3 轮。

未检查(工具限制,非阻断):.github/workflows/ci.yml——actionlint 对 workflow 内嵌 shell 的源映射尚未支持。

Test Plan(非阻断):client/daemon/session/clientLifecycle.test.tsno such file or directory

收敛姿态下延后(第 4 轮,非阻断)——已记录,本轮不要求修改:共 5 条(原文未翻译,列表见上方英文部分)。

— qwen3.8-max via Qwen Code /review (v0.23.0)

Brings this PR up to date with main (00fe690) so it is mergeable again.
Two files conflicted; both were mechanical and resolved as a union, so
neither side's intent is lost.

.github/workflows/ci.yml -- both sides appended to env.HELPER_TESTS. Kept
main's whole block (its rewritten comment, the new HELPER_TESTS_DEP_FREE
fast-lane list, and its update-ecs-runner-qwen-workflow.test.mjs entry) and
appended this PR's entry
integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs
at the end, so the merged list is exactly main's 25 entries plus this PR's
one: 26 entries, no duplicates, every path present on disk. The new suite is
deliberately NOT added to HELPER_TESTS_DEP_FREE -- main scopes that list to
.github/scripts node-builtin-only suites, and the full-profile helper step
still runs the complete HELPER_TESTS list after the dependency install. Also
bumped the adjacent "25-file HELPER_TESTS battery" comment to 26-file so the
count next to the list stays true; that is the same one-word bump main made
for 24 -> 25 when it added its own entry.

integration-tests/chat-transcript-document.test.ts -- adjacent but disjoint
assertions, not competing rewrites of one. Kept this PR's stricter
`await expect.poll(...).toBe('light')` for the <html> class, and kept main's
post-#11091 mermaid assertions (`div[class*="mermaidInline"]` count is 0,
plus the fence source visible in body text), which replace the old
`mermaidInline svg` poll that this PR never touched and that would now fail
against main's stubbed-out mermaid.

.size-baseline is left unchanged on purpose. It did not conflict (only main
moved it, 131378 -> 134426), and the merged ci.yml is 135313 bytes: 887 over
that record, inside the gate's 4096-byte GROWTH_ALLOWANCE, so no bump is owed
by this PR. 808 of the 887 is pre-existing main-side drift, and the ratchet's
design wants that reclaimed on main rather than silently absorbed here.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtpq5ybkn4
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Merged origin/main (00fe690482) into this branch to clear the conflict — merge commit c5a45ce45c, pushed non-force as 503a5b4f70..c5a45ce45c. mergeable is now MERGEABLE (was CONFLICTING/DIRTY). Two files conflicted; both turned out to be mechanical, so both are resolved as a union rather than a pick-a-side.

.github/workflows/ci.yml — both sides appended to env.HELPER_TESTS. Main rewrote the comment above it, added the new HELPER_TESTS_DEP_FREE fast-lane list, and inserted .github/scripts/update-ecs-runner-qwen-workflow.test.mjs; this PR appended integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs. Kept main's whole block and appended this PR's entry at the end, so the merged list is exactly main's 25 entries plus this PR's one — 26 entries, no duplicates, every path present on disk.

The new suite is deliberately not added to HELPER_TESTS_DEP_FREE. Main scopes that list to .github/scripts node-builtin-only suites, and this PR predates the variable; the full-profile helper step still runs the complete HELPER_TESTS list after the dependency install, so coverage is unchanged. Worth a look if you'd rather have it in the fast lane too — that's a policy call, not a merge one.

I also bumped the adjacent 25-file HELPER_TESTS battery comment to 26-file, which is the same one-word bump main made for 24 → 25 when it added its own entry. Drop it if you'd rather keep the merge commit strictly to conflict resolution.

integration-tests/chat-transcript-document.test.ts — adjacent but disjoint assertions, not two rewrites of the same one. Kept this PR's stricter await expect.poll(...).toBe('light') for the <html> class, and kept main's post-#11091 mermaid assertions (div[class*="mermaidInline"] count is 0, plus the fence source visible in body text). Main's block replaces the old mermaidInline svg .toBeGreaterThan(0) poll, which this PR never touched — it was context in this PR's diff — and which would now fail against main's stubbed-out mermaid.

Two things checked that could have been semantic but weren't:

  • The playwright pin. Main is still ^1.57.0, so 1.61.1 is not a downgrade. package.json and package-lock.json auto-merged, and every playwright-related lock key in the merged tree matches this PR's side exactly (node_modules/playwright 1.61.1 with a nested playwright-core 1.61.1); main never touched playwright.
  • .github/workflows/.size-baseline is unchanged. It didn't conflict — only main moved it, 131378134426. The merged ci.yml is 135313 bytes, 887 over that record, which is inside the gate's 4096-byte GROWTH_ALLOWANCE, so no bump is owed here. 808 of the 887 is pre-existing main-side drift (main's own ci.yml is already 135234 against a 134426 record), and the ratchet's design wants that reclaimed on main rather than silently absorbed by an unrelated PR.

Verified locally — this environment has no node_modules and can't install any, so no vitest, tsc, or Playwright run happened here. What was checked, all dependency-free:

  • yaml.safe_load on the merged ci.yml: parses, 9 jobs, both HELPER_TESTS* env keys present, WORKFLOW_SIZE_BASE_SHA intact.
  • HELPER_TESTS set equality: merged list == main's list + [this PR's entry], 26 unique entries, all 26 paths (and all 15 HELPER_TESTS_DEP_FREE paths) exist on disk.
  • .github/scripts/check-workflow-size.sh run twice (pre- and post-commit) with WORKFLOW_SIZE_BASE_SHA set to the PR base: exit 0 both times. The qwen-autofix.yml 90%-of-limit warning it emits is pre-existing main-side state, unrelated to this PR.
  • Line-by-line comparison of this PR's diff before vs after the merge: same 11 files, and 10 of them byte-identical — including chat-transcript-document.test.ts at +5/-1, so this PR's stricter assertion survived the union untouched. Only ci.yml differs, and only by the union'd HELPER_TESTS line plus the count comment; the total moved 154/62 → 155/63, i.e. exactly that one comment line.
  • Bracket balance across the resolved test file (() 388/388, {} 129/129, [] 97/97), and the stale mermaidInline svg poll is gone with no duplicated .katex assertion.

Left to CI: everything that needs dependencies — Test, Lint, Integration, Desktop Shell and the web-shell E2E lanes. In particular the two assertions that now sit side by side in chat-transcript-document.test.ts are only proven together by an actual Playwright run, which is the honest limit of a conflict-only merge.

mergeStateStatus reads BLOCKED solely on the existing CHANGES_REQUESTED review, not on mergeability. No review was re-requested and no /triage was triggered from here.

@qwen-code-ci-bot qwen-code-ci-bot 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] Blocking finding(s) follow.

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — "Integration Tests (CLI, No Sandbox)" was skipped in CI and integration-tests/chat-transcript-document.test.ts's browser gate did not run locally either (needs a Chromium install plus the built export-transcript-document.js asset); its typecheck half was covered locally and npm run typecheck:integration exits 0.

Not explored to full depth (tool budget reached): "agent 6b": did not execute the changed browser gate in integration-tests/chat-transcript-document.test.ts (needs a full workspace build plus a chromium download); the .….

Not reviewed: reverse audit — stopped before round 1 by the review time budget.

Not reviewed: verification — the review posts findings, but no verifier was launched with a prompt this skill builds — they were ruled on, if at all, without the verdict bar its brief carries.

⚠️ 12 finding(s) still carried the — [unverified] tag when the loop ended — the verifier never ruled on them, and they are not confirmed.

Not linted (tool limitation, not a blocker): .github/workflows/ci.yml — actionlint embedded-shell source mapping is not yet supported.

Test Plan (not a blocker): client/daemon/session/clientLifecycle.test.tsno such file or directory.

[Critical] An existing blocker on this PR still stands and this review is re-asserting it rather than filing a new claim: the triage gate's CHANGES_REQUESTED (review 5124792569) stopped this PR on a duplicate-scope check. Its ask 1 has landed — the branch merged current main at c5a45ce, so the merge base is no longer 25 commits behind. Its ask 2 has not: the description still carries Closes #11076, and #11076 is CLOSED (closedAt 2026-09-05T23:41:17Z) with all four of its tracked items satisfied at the reviewed commit by files this diff does not touch — the literal compat pin in packages/web-shell/client/daemon/session/clientLifecycle.test.ts, the legacy-JSONL rejection plus happy path in scripts/tests/export-html-from-chatrecord-jsonl.test.js, the Shellfish.tsx classifier fixture at .github/scripts/ci/classify-platform-sensitivity.test.mjs:126 (present in this diff only as unchanged context), and zero remaining InputForm references. Merging therefore fires a close keyword at an already-closed issue and closes nothing, while the description's "this PR restores the rest" and "fixes two stale documentation references" send a reviewer hunting for work the diff does not contain. This is the same ask the triage gate already made; it is not a new finding of this round.

中文说明

仅完成部分审查,审查缺口已披露。

未审查(原文为英文):build-and-test — "Integration Tests (CLI, No Sandbox)" was skipped in CI and integration-tests/chat-transcript-document.test.ts's browser gate did not run locally either (needs a Chromium install plus the built export-transcript-document.js asset); its typecheck half was covered locally and npm run typecheck:integration exits 0.

未探索到全部深度(达到工具调用预算):"agent 6b"did not execute the changed browser gate in integration-tests/chat-transcript-document.test.ts (needs a full workspace build plus a chromium download); the .…

未审查:反向审计——评审时间预算不足,未能开始第 1 轮。

未审查:验证——本次评审发布了发现,但没有验证 agent 是用本 skill 构建的 prompt 启动的——这些发现即便被裁定过,也缺失了 brief 承载的裁定标准。

⚠️ 循环结束时仍有 12 条发现带着 — [unverified] 标记——验证者从未对它们作出裁决,它们不算已确认。

未检查(工具限制,非阻断):.github/workflows/ci.yml——actionlint 对 workflow 内嵌 shell 的源映射尚未支持。

Test Plan(非阻断):client/daemon/session/clientLifecycle.test.tsno such file or directory

[Critical] An existing blocker on this PR still stands and this review is re-asserting it rather than filing a new claim: the triage gate's CHANGES_REQUESTED (review 5124792569) stopped this PR on a duplicate-scope check. Its ask 1 has landed — the branch merged current main at c5a45ce, so the merge base is no longer 25 commits behind. Its ask 2 has not: the description still carries Closes #11076, and #11076 is CLOSED (closedAt 2026-09-05T23:41:17Z) with all four of its tracked items satisfied at the reviewed commit by files this diff does not touch — the literal compat pin in packages/web-shell/client/daemon/session/clientLifecycle.test.ts, the legacy-JSONL rejection plus happy path in scripts/tests/export-html-from-chatrecord-jsonl.test.js, the Shellfish.tsx classifier fixture at .github/scripts/ci/classify-platform-sensitivity.test.mjs:126 (present in this diff only as unchanged context), and zero remaining InputForm references. Merging therefore fires a close keyword at an already-closed issue and closes nothing, while the description's "this PR restores the rest" and "fixes two stale documentation references" send a reviewer hunting for work the diff does not contain. This is the same ask the triage gate already made; it is not a new finding of this round.

— qwen3.8-max via Qwen Code /review (v0.23.0)

@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 106 passed · 0 failed · 106 total

Flakiness gate: ✅ 3 changed test file(s) x 5 identical rounds, no divergence

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:106 通过 · 0 失败 · 106 总计

抖动门:✅ 3 changed test file(s) x 5 identical rounds, no divergence

Verification report

PR #11101 — Deep Verification Report

Verdict: merge-ready — 106 scripted assertions executed, 0 unexpected failures.
Verified head OID: c5a45ce45cb36a144826ce96c2921c933f0f3a32 (git rev-parse HEAD^2, matches the snapshot's headRefOid).
Base arm for every A/B: 1a86cd6c594062f3a7eb403fbbcb256c92c70f04 (HEAD^1, the merge-ref base tip).
Three non-blocking observations are recorded under Findings; none is a merge condition.

中文摘要

结论:merge-ready —— 共执行 106 条脚本化断言,0 条非预期失败

A/B 结论(中心主张):本 PR 唯一的生产代码改动是导出脚本的 main-module 判定(改为比较 realpath)。用两个只差这一个 hunk 的构建做 A/B,以「是否真的写出 HTML 文件」为 oracle:base 在 2 种调用方式中只成功 1 种,head 成功 2 种。base 经软链调用时 main() 被跳过、退出码 0、stdout/stderr 全空、不写任何文件 —— 正是提交信息描述的「静默空操作」;head 经软链调用写出了与直接调用规范化后完全一致的文档(6968 字节,promptCount:2)。详见下表 Central claim A/B01-ab-symlinked-invocation-base-vs-head.png

依赖固定的 A/B:Playwright 版本一致性无法用 A/B 衡量(两侧代码相同),改用身份比较。按 HEAD^1 的 lockfile 拓扑用硬链接重建了一棵 base 依赖树,并逐项与 lockfile 核对一致。结果:base 侧 4/4 个直接 import 站点分裂playwright@1.58.2 → chromium 1208@playwright/test → chromium 1228);head 侧 4/4 一致(均 1228)。本容器只安装了 chromium-1228,因此 base 侧的直接 import 在此根本无法启动浏览器(实测报 Executable doesn't exist at .../chromium_headless_shell-1208/...)—— 说明这个分裂是真实故障而非版本号差异。详见 02-playwright-parity-base-vs-head.png03-workflow-two-tree-premise-base-vs-head.png

测试非空转(mutation 矩阵):12 行全部符合预期(29/29 断言)。要点:以保留接口的方式回退 realpath 语义,恰好只有软链那条测试变红、另两条保持绿 —— 证明新测试是承重的;而「整文件回退」会在 import 阶段就崩,是无效的空转检查(两者都记录在矩阵里)。分类器两个 fixture 不冗余:把 camelCase 边界当作分隔符的变异,只有本 PR 新增的 shellCommandProcessor.ts 能抓到,main 的 Shellfish.tsx 抓不到;反之「关键字出现在段内任意位置」的变异只有 Shellfish.tsx 能抓到 —— 两者覆盖正交。详见 04-mutation-matrix.png

findings(均非阻塞):① playwright 精确固定与 @playwright/test 的 caret 范围之间的一致性没有任何门禁保障,PR 自己在 Risk 里承认「必须一起升级」,但 check-lockfile.js、lint、测试都不校验;② qwen-triage.yml 里「Install evidence browser」步骤的注释前提(「本 lockfile 有两棵 Playwright 树……固定了不同的 chromium revision」)被本 PR 变为过时(workaround 代码本身仍应保留);③ isMainModulerealpathSync try/catch 兜底无测试覆盖(已分类为覆盖缺口,不是死代码)。

未覆盖范围:本 checkout 深度为 2,git rev-list HEAD^1..HEAD^2 只返回 1 个提交,而快照记录 10 个 —— 逐提交归因不可达,只验证了聚合 diff;packages/climanifest-repository-context.committed.test.ts 在本容器内无法跑绿(.qwen/review-context.json 是 root 只读且被冻结在 base 内容),已用其他方式验证;Windows/macOS 未测;未跑全仓测试套件。

Scope

Central claim. One production-code change carries this PR: the exported-JSONL→HTML helper decided whether it was the main module by comparing path.resolve(process.argv[1]) against fileURLToPath(import.meta.url). Node realpath-resolves import.meta.url for an ESM main module but leaves argv[1] as invoked, so any symlinked invocation skipped main() and exited 0 having written nothing. The PR replaces that with an exported, realpath-based isMainModule().

Secondary claims (both verified).

  1. Pinning playwright to exact 1.61.1 restores version parity with @playwright/test, so direct-import tests drive the same browser revision as the runner suite.
  2. The restored regression tests actually hold down what they claim to (platform-sensitivity fixture, file:-scheme rejection, exporter main-module check), and the new .qwen/review-context.json rule has an observable effect.

Out of scope by choice: the full repo test suite; Windows/macOS; per-commit attribution (unreachable, see Not covered); the #11076 issue thread itself (no GitHub token in this job).

Central claim A/B

Two arms byte-identical except the main-module hunk (diff between them is exactly the fileURLToPathpathToFileURL import plus the isMainModule block — quoted in logs/ab-run.txt). Real child processes, real filesystem, the real @qwen-code/qwen-code/export API resolved from the installed+built tree. Nothing is stubbed.

arm invocation exit wrote HTML bytes stdout stderr
base direct 0 yes 6968 Wrote HTML export to: … (empty)
base symlink 0 no 0 (empty) (empty)
head direct 0 yes 6968 Wrote HTML export to: … (empty)
head symlink 0 yes 6968 Wrote HTML export to: … (empty)

base wrote 1/2 cells; head wrote 2/2. The base failure mode is the dangerous shape: exit 0, no stdout, no stderr, no artifact — a silent no-op that any harness reading an exit code scores as success.

15/15 assertions, including:

  • Fidelity — the artifact written through the symlinked invocation actually renders the fixture (<title>Qwen Code Chat Export</title>, CSP header, "promptCount":2, "projectName":"work", and startedAt = the earliest record timestamp rather than the first in file order), not merely that a file appeared.
  • No regression — head+direct and base+direct produce the same document once normalised.
  • Normaliser controls — the exporter is intentionally non-deterministic (a fresh CSP nonce in three spellings plus a wall-clock exportedAt), so raw sha256 equality is an invalid oracle. Measured: two runs writing the same path differ. The normaliser is proven live by three controls — two independent renders are raw-different, collapse to one digest after normalisation, and retain every load-bearing marker with a 141-byte delta out of 6968. An earlier revision of this harness normalised only two of the three nonce spellings and the control caught it.

Witness: 01-ab-symlinked-invocation-base-vs-head.png.

Secondary claim 1 — the Playwright pin

A dependency pin has no A/B (both arms run identical code), so the oracle is an identity comparison. Base topology was reconstructed from HEAD^1:package-lock.json by hardlink-copying the already-installed packages into a scratch tree, then cross-checked entry-by-entry against that lockfile (BASE TREE FAITHFUL: True, all 5 entries).

arm site playwright chromium rev @playwright/test drives chromium rev match
base all 4 direct-import sites 1.58.2 1208 1.61.1 1228 NO
head all 4 direct-import sites 1.61.1 1228 1.61.1 1228 YES

Base 4/4 split, head 4/4 identical. The four sites are integration-tests/chat-transcript-document.test.ts, integration-tests/terminal-capture/terminal-capture.ts, plus two non-module grep hits (a YAML comment and a test asserting on that comment) whose resolution is reported for completeness.

The split was a real breakage, not a cosmetic version diff. This container has only chromium-1228 / chromium_headless_shell-1228 installed (put there by @playwright/test@1.61.1). Launching through head's playwright succeeds (Chromium 149.0.7827.55); launching through base's playwright@1.58.2 fails hard:

browserType.launch: Executable doesn't exist at
  /__w/_temp/pw-browsers/chromium_headless_shell-1208/chrome-headless-shell-linux64/chrome-headless-shell

Witness: 02-playwright-parity-base-vs-head.png.

A consequence the PR description does not claim

.github/workflows/qwen-triage.yml's Install evidence browser step carries a workaround whose comment states the split as its premise, verbatim: "This lockfile has TWO Playwright trees: terminal-capture.ts imports playwright, but node_modules/.bin/playwright is @​playwright/test's CLI, which pins a different chromium revision — so npx playwright install would download a browser the harness cannot launch."

That premise is falsifiable, so it was measured on both arms (run-workflow-premise.mjs, 5/5):

arm harness from 'playwright' .bin/playwright (= @playwright/test's CLI) revisions
base 1.58.2 → 1208 1.61.1 → 1228 DIFFER
head 1.61.1 → 1228 1.61.1 → 1228 AGREE

So this PR removes the condition the workflow documents a workaround for. The workaround itself remains correct and should stay — it resolves the CLI from the harness's own directory, which is robust to hoist-layout changes and still lands on the same package. See Findings F2 for the now-stale comment. Witness: 03-workflow-two-tree-premise-base-vs-head.png.

Residual divergence, accounted for. packages/mobile-mcp/node_modules/playwright@1.58.2 remains, and root node_modules/playwright-core is still 1.58.2 (hoisted for it). This is forced, not a defect: mobilewright@0.0.53 declares an exact playwright: "1.58.2" dependency, so npm cannot dedupe it. No repo file under packages/mobile-mcp imports playwright directly (the census above found zero such sites), so it is off every path this PR is about. node scripts/check-lockfile.js passes on the pinned package.json + lockfile.

Secondary claim 2 — the restored tests hold down what they claim

Mutation/vacuity matrix, 12 rows, all matched expectation, 29/29 assertions. Every mutation ran in a scratch copy; the repository tree was never modified. Witness: 04-mutation-matrix.png.

subject mutation expected observed
isMainModule CONTROL unmutated head survived SURVIVED (3/3 green)
isMainModule M1 revert realpath semantics, interface preserved killed KILLED — exactly recognizes a main-module invocation through a symlinked path red, other 2 green
isMainModule M1b whole-file revert to base killed KILLED — but at import, 1 file-level failure
isMainModule M2 delete typeof argv1 !== 'string' guard killed KILLED — negative test red
isMainModule M3 delete the realpathSync try/catch fallback survived SURVIVED → see F3
isMainModule M4 compare a file: URL to a plain path killed KILLED — 2 red
classifier CONTROL unmutated head survived SURVIVED (12/12 green)
classifier M5 treat a camelCase boundary as a stem separator, both fixtures killed KILLED
classifier M5 without this PR's fixture (main's Shellfish.tsx only) survived SURVIVED (12/12 green)
classifier M5 without main's fixture (this PR's shellCommandProcessor.ts only) killed KILLED
classifier M6 keyword matches anywhere in a segment, both fixtures killed KILLED
classifier M6 without this PR's fixture killed KILLED

Two results deserve emphasis.

M1b is the vacuity check a naive round would run, and it is the wrong one. Reverting the whole file removes the isMainModule export, so the test file dies at load and reports a single file-level failure — a red that would look like "non-vacuous" while never reaching an assertion. M1 keeps the export and restores base's semantics, and fails precisely the symlink expectation. Both rows are recorded so the distinction is visible.

The two classifier fixtures are orthogonal, not redundant. M5 (camelCase counts as a compound separator — a plausible completion of a rule that already counts pty-host.ts and shell_exec.ts) is caught only by this PR's shellCommandProcessor.ts; with main's Shellfish.tsx alone the whole suite stays green. M6 (keyword anywhere in a segment) is caught by Shellfish.tsx with or without this PR's fixture. The new fixture therefore pins a trap nothing on main could see. Two supporting facts: packages/cli/src/ui/hooks/shellCommandProcessor.ts exists in the repo while packages/web-shell/client/components/Shellfish.tsx does not — that is what the commit means by "a live path" — and SUBSYSTEM_STEM_HEAD separates on [-_] only, so a camelCase head is genuinely a distinct near-miss.

A note on harness discipline: an earlier revision of M5 mutated the regex to (?:[-_]|(?=[A-Z])). That is a strawman — the regex is built with the i flag, so [A-Z] matches lowercase too and the mutation degenerates into M6. The matrix flagged it as an unexpected survival, which is what exposed it; the replacement normalises the path at case boundaries and is case-sensitive by construction.

file:-scheme rejection (web-shell)

Mutated SAFE_HREF_SCHEMES from /^(https?:|mailto:)/i to /^(https?:|mailto:|file:)/i in place under an EXIT trap with sha256 verification (restore confirmed byte-exact, b816f69d… before and after). Control 85/85 green; mutated exactly 2 redisSafeHref > blocks file: scheme and isSafeImageSrc > blocks file: scheme — with the other 83 green. The restored tests are load-bearing and specific. 7/7 assertions.

New .qwen/review-context.json rule

The provider matches with rules.filter(...) then flatMap + sortedUnique — a union of every matching rule, not first-match-wins — so the broader integration-tests/** rule that precedes it cannot shadow it. Verified by driving the real provider from the built dist/ with each arm's committed manifest content (run-manifest-effect.mjs, 11/11):

arm recommendedTests for integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs
base ["integration-tests"]
head ["helper-tests","integration-tests"]

Also asserted: reversing all 10 rules yields identical output (order-independence proven, not assumed); helper-tests is emitted for integration-tests/concurrent-runner/runner.py and not for integration-tests/chat-transcript-document.test.ts or packages/cli/src/index.ts (no over-reach); the manifest still passes the provider's fail-closed schema validation.

Theme-class assertion (chat-transcript-document.test.ts)

expect(await …getAttribute('class')).toContain('light')await expect.poll(…).toBe('light'). Two separate questions, both answered (6/6 assertions):

  • Strengthening is real. toContain('light') accepts "dark light", "light dark" and even "lightish"; .toBe('light') rejects all three and agrees on the correct value.
  • Exactness is safe. document-main.tsx has exactly two documentElement.classList writes, mutually exclusive toggles on dark/light, it is the only writer in the export bundle (git grep census over packages/web-templates/src/export-html/** and packages/cli/src/ui/utils/export/**), and document-index.html ships <html lang="en" class="dark"> — one class, no third token for .toBe to trip on. So the attribute is always exactly dark or light, and dark light is unreachable today: this is defence against a future regression, not a live bug fix.

Reviewer Test Plan, walked step by step

# step as written result
1 node --test .github/scripts/ci/classify-platform-sensitivity.test.mjs 12/12 pass
2 node --test integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs 3/3 pass (direct, symlinked, imported)
3 npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/export-html-from-chatrecord-jsonl.test.js 10/10 pass — see Corrections: this file is not added by this PR
4 cd packages/web-shell && npx vitest run client/components/messages/Markdown.test.ts 85/85 pass
5 npm run typecheck && npm run lint typecheck exit 0 (including typecheck:integration); lint exit 0
6 npm ls playwright @playwright/test both 1.61.1, deduped — see the parity table above

Step 5's typecheck:integration is the gate the PR's second commit says failed with TS2339 on toHaveClass; it is green here.

Every step was performable. In addition, the plan's own out-of-scope item was executed: "the chat-transcript-document.test.ts browser-gate changes require a full build plus a Chromium install, so they were not run locally; CI covers them." This container has both, so the gate was run — 5/5 pass in 37.4 s, the changed test taking 25.5 s (logs/gate-head.txt), with --retry=0 so no retry budget absorbed anything.

Witness for this table and the gates below: 05-reviewer-test-plan-gates.png.

Two further gates the PR's ci.yml change implicates, both green: .github/scripts/check-workflow-size.sh exits 0 with WORKFLOW_SIZE_BASE_SHA=HEAD^1 (ci.yml grew 79 bytes — 135234 → 135313, 1.9% of the 4096-byte baseline budget), and its vitest mirror passes 202/202. The HELPER_TESTS battery claim was checked mechanically rather than by eye: base 25 files / head 26, no duplicates, every listed path exists on disk, and the adjacent comment's 25-file26-file edit matches the actual count on both sides.

Corrections to the PR description

These are corrections to the description, not requests to change code.

  1. Test-plan step 3 credits this PR with coverage it does not add. scripts/tests/export-html-from-chatrecord-jsonl.test.js is absent from git diff HEAD^1..HEAD. Per the merge commit, main extracted the same input gate as selectChatRecords and added renderHtmlFromObjects, and this branch dropped its duplicate assertRenderableJsonl. The suite is genuinely good coverage and the step is worth running — but it landed via main, not here. The body's "Cover the legacy exported JSONL rejection and ChatRecord happy path" (commit 1's message) was superseded by that merge.
  2. The aggregate diff is smaller than the commit list implies. Commit 1 also claims an 01-architecture.md adapter-diagram path fix and a useDaemonFollowupSuggestion doc rename; neither file appears in HEAD^1..HEAD, because the merge from main already carried equivalent changes (the merge commit says main's version was a superset and was taken). Commit 1's "Pin the historical sessionStorage key prefix with a literal assertion" was likewise added then removed by commit 7 as a byte-equivalent twin of what fix(web-shell): close the four deferred #9812 review follow-ups #11107 landed — correctly, and the body already says so. A reader auditing per-commit claims against the final diff will find gaps that are merges, not omissions.

Findings

All three are non-blocking. None is a merge condition, and none reproduced a defect in the PR's own behaviour.

F1 — Suggestion: the Playwright parity invariant has no enforcement

Root package.json now pins playwright to exact 1.61.1, while packages/web-shell/package.json declares @playwright/test: "^1.57.0" and packages/mobile-mcp/package.json declares "^1.49.0". The parity this PR restores therefore rests on a caret range happening to resolve to the pinned version today. Nothing holds it: scripts/check-lockfile.js validates lockfile↔package.json consistency (it passes, and would keep passing), and no lint rule, test, or CI step asserts that the two resolve to the same version — a repo-wide grep for a parity guard found none.

Failure scenario: Playwright publishes 1.62.x, someone bumps or regenerates @playwright/test, the lockfile moves it to 1.62.x while root playwright stays exact at 1.61.1, and the chromium revisions split again (1228 vs whatever 1.62 pins) — silently, with every gate green. The PR's own Risk section discloses the tradeoff ("a future Playwright bump must move both together"), so this is about the absence of a mechanism, not an undisclosed risk. Cheapest closing move: pin @playwright/test to the same exact version, or add an assertion to check-lockfile.js that the two resolve identically.

F2 — Nit: this PR falsifies the premise of a comment in qwen-triage.yml

The Install evidence browser step (around line 3950) justifies its directory-scoped CLI resolution with "This lockfile has TWO Playwright trees … which pins a different chromium revision — so npx playwright install would download a browser the harness cannot launch." Measured above: at head both paths resolve to 1.61.1 / chromium 1228, so for the harness path the two-tree premise no longer holds. (A second tree does still exist under packages/mobile-mcp, forced by mobilewright's exact pin, but it is not on any path this step resolves.)

The code should stay — resolving from the harness's own directory is correct regardless and is robust to hoist changes. Only the rationale is now stale, and a future reader will mis-infer why the indirection exists. Note when editing: scripts/tests/qwen-triage-workflow.test.js:5593 asserts expect(capture).toMatch(/from 'playwright'/) against this text, so the comment and that assertion must move together.

F3 — Nit (coverage gap, classified): isMainModule's realpath fallback is untested

M3 (delete the try { fs.realpathSync(argv1) } catch { resolved = path.resolve(argv1) } fallback) survived all 3 tests. Classified per the matrix protocol:

  • Not dead code. Driven directly, isMainModule('/definitely/not/here.js', …) returns false at head and throws with the fallback removed — the clause decides an outcome.
  • Not redundant defence. No sibling hunk in this PR covers a nonexistent argv1.
  • A coverage gap. The negative-case fixture points at integration-tests/concurrent-runner/runner.py, which exists, so realpathSync succeeds and the catch is never entered. The other negative input, undefined, is stopped earlier by the typeof guard.

One-line fixture that would pin it: assert.equal(isMainModule(path.join(dir, 'does-not-exist.js'), exporterUrl), false). For the module's own invocation the branch is near-unreachable (Node just loaded argv[1]), so this is worth a test only because the predicate is exported.

Not covered

  • Per-commit attribution. The checkout is depth 2 and git rev-parse --is-shallow-repository is true. git rev-list HEAD^1..HEAD^2 returns 1 while the metadata snapshot lists 10 commits — the shallow boundary silently undercounts rather than erroring, so the two were compared explicitly. Only the aggregate HEAD^1..HEAD diff was verified; no per-commit table is presented. Note also that the snapshot's baseRefOid (00fe6904…) is older than HEAD^1 (1a86cd6c…), i.e. the merge ref was cut against a newer main; HEAD^1 was used as base throughout, as the merge-ref contract requires.
  • packages/cli manifest-repository-context.committed.test.ts could not be run green in this container — environmental, and proven so. The container froze .qwen/review-context.json as -r--r--r-- root root (directory .qwen is dr-xr-xr-x root root) holding the base content with this PR's new rule removed, while the agent runs as uid=1000(node). The test's readCommittedFile() — despite its name — does readFileSync(join(repoRoot, …)) against the working tree, so it reads the frozen base content and fails 1/5. An attempt to restore the committed content failed with Permission denied; sha256 confirmed the file was never modified (a71b02dc… before and after). The assertion was verified two other ways instead: the committed manifest and the test's expectedManifest literal contain a byte-identical rule object, and the real provider was driven directly on both arms' committed content (11/11, above). A pristine git worktree at HEAD was also tried and hit the repo's own globalSetup guard for unbuilt workspace packages.
  • Trial merge into current main. No network/refs beyond the depth-2 checkout, so the merge this PR would land as could not be re-measured. The head already contains two merges from main (the most recent resolving fix(web-shell): close the four deferred #9812 review follow-ups #11107's overlap), which is the same risk retired from the other direction.
  • The full repo test suite and the whole 26-file HELPER_TESTS battery. Only the files this PR touches or implicates were run (steps 1–4 above, plus the size-ratchet mirror). The PR's own CI covers the rest from a clean checkout.
  • Windows and macOS. Linux container only; the PR body likewise marks both untested. The realpath fix is POSIX-symlink-shaped — on Windows the equivalent would involve junctions/reparse points, which was not exercised.
  • A live npm install of the pinned dependency. Parity was measured against the already-installed tree plus a hardlink reconstruction of base's lockfile topology, not by resolving 1.61.1 from the registry.
  • Browser-gate timing margins. expect.poll uses vitest's default 1000 ms budget against a click-driven class flip. It passed at --retry=0, but the natural flip latency was not measured N times, so no margin assertion is reported. This job runs on a shared, loaded runner — the regime where such a test passes; a fast-machine failure could not be reproduced here by repetition.
  • lint initially reported 4215 errors, all self-inflicted. Every one was under tmp/ — 611 files from a scratch git worktree and 22 from this artifact directory — with zero in the repository. The worktree was removed (git worktree remove --force), the artifact directory was staged outside the repo, and npm run lint was re-run on the pristine tree: exit 0. Recorded so the first number is not mistaken for a PR result.

Methodology

Linux container (node:22-bookworm lane), Node v22.23.2 / npm 10.9.8, working tree at the merge commit b7d6660c with npm ci + npm run build already completed by the workflow. The PR's effective diff is git diff HEAD^1..HEAD; the verified head is HEAD^2 = c5a45ce4, matching the snapshot's headRefOid.

Every harness is a .mjs file under this artifact directory and drives compiled or source-real code, never a stub of the unit under test: the A/B spawns real node child processes against two exporter copies that differ by exactly one hunk and asserts on the child's exit code, stdout, stderr and the artifact it leaves on disk; the parity harnesses resolve packages with createRequire from each site's own directory and read the chromium revision out of each playwright-core's shipped browsers.json; the manifest harness calls the real manifestRepositoryContextProvider.provide() from packages/cli/dist with each arm's committed manifest supplied through readIdentityFile; the mutation matrix runs node --test and vitest against scratch copies. The base dependency tree was built by hardlink-copying already-installed packages into the layout HEAD^1:package-lock.json describes, then validated entry-by-entry against that lockfile before any cell was trusted — hardlinks rather than symlinks because Node realpath-resolves symlinks and would have escaped the scratch tree back into head's node_modules, the exact confound the A/B exists to avoid.

The one in-place mutation (Markdown.tsx) ran under a bash EXIT trap with sha256 verification before and after; git status --porcelain at the end of the round shows only the pre-existing root-owned .qwen/review-context.json drift, which was never modified (an attempt to restore it was refused by permissions and the sha confirms it is untouched). Scratch worktrees were removed and the artifact directory was staged outside the repository while npm run lint ran, so no scratch file could be attributed to the PR.

The scratch base dependency tree was removed after its cells were captured (it was 32 MB of hardlinks into node_modules); logs/parity-base.txt and logs/workflow-premise.json hold its full output, and parity/run-parity.mjs rebuilds nothing on its own — to re-run the base arm, recreate parity/base-tree/node_modules by hardlink-copying (cp -al) packages/mobile-mcp/node_modules/playwrightplaywright, node_modules/playwright-coreplaywright-core, node_modules/@playwright/test@playwright/test, node_modules/playwright@playwright/test/node_modules/playwright, and node_modules/playwright/node_modules/playwright-core@playwright/test/node_modules/playwright-core, then validate each version against git show HEAD^1:package-lock.json as the harness's first run did.

Raw per-cell stdout/stderr, build-free gate logs and every harness's JSON summary live in logs/; harnesses in ab/, parity/, manifest/, mutate/; image evidence in evidence/. Assertion counts in assertions.json were produced by aggregate.mjs, which re-parses those recorded logs rather than trusting any summary line, and counts only checks that actually executed — with fail reserved for unexpected outcomes, so base-arm cells that broke exactly as predicted are recorded as passes.

Flakiness gate log

integration test, out of gate scope: integration-tests/chat-transcript-document.test.ts
integration test, out of gate scope: integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs
rounds=5 files=3 skipped=2
file .github/scripts/ci/classify-platform-sensitivity.test.mjs: (cd .) node --test ./.github/scripts/ci/classify-platform-sensitivity.test.mjs
file packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/review/lib/manifest-repository-context.committed.test.ts
file packages/web-shell/client/components/messages/Markdown.test.ts: (cd packages/web-shell) npx --no-install vitest run ./client/components/messages/Markdown.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  .github/scripts/ci/classify-platform-sensitivity.test.mjs: PPPPP
  packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts: PPPPP
  packages/web-shell/client/components/messages/Markdown.test.ts: PPPPP

verdict: pass
summary: 3 changed test file(s) x 5 identical rounds, no divergence

--- per-invocation detail (full copy in the artifact) ---
round 1 · .github/scripts/ci/classify-platform-sensitivity.test.mjs: P (exit 0)
round 1 · packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts: P (exit 0)
round 1 · packages/web-shell/client/components/messages/Markdown.test.ts: P (exit 0)
round 2 · .github/scripts/ci/classify-platform-sensitivity.test.mjs: P (exit 0)
round 2 · packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts: P (exit 0)
round 2 · packages/web-shell/client/components/messages/Markdown.test.ts: P (exit 0)
round 3 · .github/scripts/ci/classify-platform-sensitivity.test.mjs: P (exit 0)
round 3 · packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts: P (exit 0)
round 3 · packages/web-shell/client/components/messages/Markdown.test.ts: P (exit 0)
round 4 · .github/scripts/ci/classify-platform-sensitivity.test.mjs: P (exit 0)
round 4 · packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts: P (exit 0)
round 4 · packages/web-shell/client/components/messages/Markdown.test.ts: P (exit 0)
round 5 · .github/scripts/ci/classify-platform-sensitivity.test.mjs: P (exit 0)
round 5 · packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts: P (exit 0)
round 5 · packages/web-shell/client/components/messages/Markdown.test.ts: P (exit 0)

Evidence images

01-ab-symlinked-invocation-base-vs-head

02-playwright-parity-base-vs-head

03-workflow-two-tree-premise-base-vs-head

04-mutation-matrix

05-reviewer-test-plan-gates

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot's only review on c5a45ce45cb36a144826ce96c2921c933f0f3a32 is a COMMENTED one, which carries no vote — so it has no verdict of its own on this commit, and main needs two approving reviews: an approval left by another account is a separate vote and does not count as the bot's own. Two different things look like this, and the stage-3 comment above says which: the triage skill deferring on purpose at 3/5 — a fork refactor hitting the approval guardrail, or a core change escalated for maintainer awareness, both normal outcomes — or an earlier approval that a push dismissed, leaving only the comment behind, which needs a fresh review.

⚠️ 机器人在 c5a45ce45cb36a144826ce96c2921c933f0f3a32 上唯一的评审是 COMMENTED不带票 —— 因此它在该 commit 上没有自己的裁决,而 main 需要两个批准(其他账号的批准是另一张票)。有两种情况长这样,上方的 stage-3 评论会说明是哪一种:triage skill 在 3/5 时有意 defer(fork refactor 命中审批护栏,或核心改动被升级交由维护者把关,两者都是正常结果);或者更早的批准被一次推送作废、只剩下这条评论,此时需要重新评审。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

@yiliang114

Copy link
Copy Markdown
Collaborator Author

CI attribution for the two red checks at head c5a45ce45c — neither is caused by this PR.

gh pr checks 11101 shows two non-passing checks. Both are infrastructure, and I am deliberately not "fixing" either inside this PR:

1. web-shell E2E Smoke (ubuntu-latest, Node 22.x) — job conclusion is cancelled, not failure.

job 101511286923  started 2026-09-06T15:27:24Z  completed 15:47:48Z  conclusion=cancelled

gh run view --job 101511286923 --log-failed returns empty — there is no failure output at all, because no test failed; the job was killed at a ~20m24s wall.

The same check is cancelled at the same wall on two unrelated PRs, which is what rules out this diff:

PR job started → completed conclusion PR touches
#11101 101511286923 15:27:24 → 15:47:48 (20m24s) cancelled web-shell Markdown test + CI scripts
#11080 101567321344 22:09:54 → 22:30:17 (20m23s) cancelled .github/scripts/ only — cannot reach web-shell E2E
#9402 101477471687 11:19:54 → 11:40:23 (20m29s) cancelled agent board

Three unrelated diffs, one of which cannot touch the web-shell E2E surface at all, all killed at ~20m2x with no log. That is a job-level timeout / runner ceiling, not a regression.

2. review-pr — fail after 3h0m57s. This is the review bot's own pipeline (informational); it is not a required check and never gates merge. Its earlier qwen-review-fallback comment on this branch already reported the pipeline failing to complete.

Everything that actually gates is green at this head: Lint & Static, Integration Tests (no-AK), Desktop Shell (ubuntu-22.04 / windows-2022), TUI parity snapshots, OpenTUI no-flicker gate, Capture web-shell visuals, Real daemon E2E / Java 11, Classify PR.

So the state of this PR is: 0 unresolved review threads (12/12 resolved), the bot's own triage verification ✅ passed — merge-ready (106/106 scripted assertions, flakiness gate clean over 3 changed test files × 5 rounds), and the standing CHANGES_REQUESTED is the bot's from the superseded head 503a5b4f. The triage re-run confirmed its only review at c5a45ce45c is COMMENTED, which carries no vote — so the stale request-changes needs a human reviewer to clear. No human has reviewed this PR yet and reviewRequests is empty; flagging for maintainer attention rather than re-triggering triage a third time on the same head.

@qwen-code-ci-bot qwen-code-ci-bot 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.

Not explored to full depth (tool budget reached): "agent 6b": did not verify whether integration-tests/concurrent-runner/runner.py exists — it decides only which branch of isMainModule the third test exercises (the as…; "agent 6b": did not verify the doc's second added sentence ("runs after every turn that meets the conditions above") against the daemon's full condition list — I confirmed …; "agent 6b": did not grep for --preserve-symlinks-main / NODE_OPTIONS across CI and npm scripts, which is what would have turned the unfilable isMainModule candidate a….

Not linted (tool limitation, not a blocker): .github/workflows/ci.yml — actionlint embedded-shell source mapping is not yet supported.

Test Plan (not a blocker): client/daemon/session/clientLifecycle.test.tsno such file or directory.

Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round:

  • integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.js:201 — [review] isMainModule realpaths only argv[1], so under --preserve-symlinks-main — the one configuration where the deleted comparison worked — a symlinked invocat…
  • integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.js:193 — [review] a third exported isMainModule with a third arity, while scripts/release-script-utils.js:61 already exports one lacking the realpath step; picking the -u…
  • integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs:25 — [probe] nothing pins that the module's own guard calls isMainModule — the guard mutant gives exit 0 with 0 output bytes while the .mjs suite stays 3 pass an…
  • package.json:163 — [review] the exact playwright pin leaves the parity invariant unowned (twin still on a caret, so npm re-nests the deleted tree with no check going red) and falsifies the 'TWO Playwright trees' rationale qwen-triage.yml:39…
  • .github/scripts/ci/classify-platform-sensitivity.test.mjs:127 — [review] the only real path in a loop of five invented substring fixtures genuinely branches on os.platform(), so the pin cements the classifier's camelCase false negative unde…
  • integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs:16 — [probe] the comment's reason for a second lane ('cannot exercise without spawning') is measurably false — the assertions graft into the existing vitest suit…

[Critical] R5-1: [certifies-falsely] [new-surface] An existing blocker on this PR still stands and this review is re-asserting it rather than filing a new claim. The PR description still carries Closes #11076, but #11076 is CLOSED (closedAt 2026-09-05T23:41:17Z) and all four of its tracked items are satisfied at the reviewed commit by files this diff does not touch: the literal compat pin at packages/web-shell/client/daemon/session/clientLifecycle.test.ts:98,105,120; the legacy-JSONL rejection plus ChatRecord happy path at scripts/tests/export-html-from-chatrecord-jsonl.test.js:78-85,140 (that file, blob 4a60f165, and selectChatRecords at base line 143 both exist AT THE MERGE BASE — git diff --stat <merge-base> HEAD -- scripts/tests/export-html-from-chatrecord-jsonl.test.js is empty); the Shellfish.tsx classifier fixture at .github/scripts/ci/classify-platform-sensitivity.test.mjs:126, which appears in this diff only as a context line (the sole added line is 127); and zero remaining InputForm references under packages/web-shell. Merging therefore fires a close keyword at an already-closed issue and closes nothing, while the description's '#11107 has since restored the sessionStorage key coverage on main; this PR restores the rest' has no rest to restore and 'fixes two stale documentation references that pointed at renamed or deleted artifacts' describes work the delta does not contain — the only doc file touched is docs/users/features/followup-suggestions.md, whose two hunks are the end_turn stop-reason gate and the headless/SDK opt-out note, and whose merge-base version carries no InputForm or webui reference at all. Meanwhile the real delta (the isMainModule realpath fix and its suite, the file: scheme rejections, the camelCase classifier fixture, the theme-poll assertion, the playwright pin, the concurrent-runner domain mapping) has no issue describing it, so none of it is traceable. This is the same ask the triage gate made at review 5124792569: ask 1 (merge current main) landed at c5a45ce, ask 2 (drop the closing keyword and rescope the description) has not. Witness: GitHub's own closing-reference resolver, reached by raw GraphQL after the platform CLI's --json field proved unavailable — pullRequest(number:11101){ closingIssuesReferences{ nodes{...} } } returned {"state":"OPEN","closingIssuesReferences":{"nodes":[{"number":11076,"state":"CLOSED","closedAt":"2026-09-05T23:41:17Z"}]}}; per item at the reviewed commit, grep -c 'qwen-code-webui-client-id:session:session-a' clientLifecycle.test.ts gives 3 at both head and merge base with the file absent from the 11 diff paths, selectChatRecords sits at BASE line 143, diff hunk @@ -124,6 +124,7 @@ shows Shellfish.tsx as context with 127 the only added line, grep -rn InputForm packages/web-shell --include='*.ts*' | wc -l gives 0, and git merge-base main HEAD gives a4a6225, which is main's tip. The PR body fetched live still carries Closes #11076 in both the English and the Chinese blocks. Fix: drop Closes #11076 from Linked Issues and correct the 'this PR restores the rest' and 'fixes two stale documentation references' sentences, retitling and redescribing the PR as the delta the triage gate enumerated (its seven bullets were each re-verified as absent from main), keeping every code change; if a link is wanted, reference #11076 and #9812 as motivating prior work without a closing keyword. No fix witness applies — the fix is PR metadata and description only, so it adds no guard, branch or behaviour a test can pin.

中文说明

未探索到全部深度(达到工具调用预算):"agent 6b"did not verify whether integration-tests/concurrent-runner/runner.py exists — it decides only which branch of isMainModule the third test exercises (the as…"agent 6b"did not verify the doc's second added sentence ("runs after every turn that meets the conditions above") against the daemon's full condition list — I confirmed …"agent 6b"did not grep for --preserve-symlinks-main / NODE_OPTIONS across CI and npm scripts, which is what would have turned the unfilable isMainModule candidate a…

未检查(工具限制,非阻断):.github/workflows/ci.yml——actionlint 对 workflow 内嵌 shell 的源映射尚未支持。

Test Plan(非阻断):client/daemon/session/clientLifecycle.test.tsno such file or directory

收敛姿态下延后(第 6 轮,非阻断)——已记录,本轮不要求修改:共 6 条(原文未翻译,列表见上方英文部分)。

[Critical] R5-1: [certifies-falsely] [new-surface] An existing blocker on this PR still stands and this review is re-asserting it rather than filing a new claim. The PR description still carries Closes #11076, but #11076 is CLOSED (closedAt 2026-09-05T23:41:17Z) and all four of its tracked items are satisfied at the reviewed commit by files this diff does not touch: the literal compat pin at packages/web-shell/client/daemon/session/clientLifecycle.test.ts:98,105,120; the legacy-JSONL rejection plus ChatRecord happy path at scripts/tests/export-html-from-chatrecord-jsonl.test.js:78-85,140 (that file, blob 4a60f165, and selectChatRecords at base line 143 both exist AT THE MERGE BASE — git diff --stat <merge-base> HEAD -- scripts/tests/export-html-from-chatrecord-jsonl.test.js is empty); the Shellfish.tsx classifier fixture at .github/scripts/ci/classify-platform-sensitivity.test.mjs:126, which appears in this diff only as a context line (the sole added line is 127); and zero remaining InputForm references under packages/web-shell. Merging therefore fires a close keyword at an already-closed issue and closes nothing, while the description's '#11107 has since restored the sessionStorage key coverage on main; this PR restores the rest' has no rest to restore and 'fixes two stale documentation references that pointed at renamed or deleted artifacts' describes work the delta does not contain — the only doc file touched is docs/users/features/followup-suggestions.md, whose two hunks are the end_turn stop-reason gate and the headless/SDK opt-out note, and whose merge-base version carries no InputForm or webui reference at all. Meanwhile the real delta (the isMainModule realpath fix and its suite, the file: scheme rejections, the camelCase classifier fixture, the theme-poll assertion, the playwright pin, the concurrent-runner domain mapping) has no issue describing it, so none of it is traceable. This is the same ask the triage gate made at review 5124792569: ask 1 (merge current main) landed at c5a45ce, ask 2 (drop the closing keyword and rescope the description) has not. Witness: GitHub's own closing-reference resolver, reached by raw GraphQL after the platform CLI's --json field proved unavailable — pullRequest(number:11101){ closingIssuesReferences{ nodes{...} } } returned {"state":"OPEN","closingIssuesReferences":{"nodes":[{"number":11076,"state":"CLOSED","closedAt":"2026-09-05T23:41:17Z"}]}}; per item at the reviewed commit, grep -c 'qwen-code-webui-client-id:session:session-a' clientLifecycle.test.ts gives 3 at both head and merge base with the file absent from the 11 diff paths, selectChatRecords sits at BASE line 143, diff hunk @@ -124,6 +124,7 @@ shows Shellfish.tsx as context with 127 the only added line, grep -rn InputForm packages/web-shell --include='*.ts*' | wc -l gives 0, and git merge-base main HEAD gives a4a6225, which is main's tip. The PR body fetched live still carries Closes #11076 in both the English and the Chinese blocks. Fix: drop Closes #11076 from Linked Issues and correct the 'this PR restores the rest' and 'fixes two stale documentation references' sentences, retitling and redescribing the PR as the delta the triage gate enumerated (its seven bullets were each re-verified as absent from main), keeping every code change; if a link is wanted, reference #11076 and #9812 as motivating prior work without a closing keyword. No fix witness applies — the fix is PR metadata and description only, so it adds no guard, branch or behaviour a test can pin.

— qwen3.8-max via Qwen Code /review (v0.23.0)

@yiliang114

Copy link
Copy Markdown
Collaborator Author

State at head a3ddb0606d — nothing actionable stands, and one gap disclosure in the current request-changes review is a false negative.

Threads: 0 unresolved of 12 (paginated to hasNextPage=false). Resolution-integrity audit clean — the single resolved [Critical] carries a substantive author reply, so nothing was silently resolved.

Checks: 23 pass / 27 skipping / 0 fail, 0 pending. The two reds attributed to infrastructure at the previous head c5a45ce45c (see the CI-attribution comment above) are gone at this head; there is no CI failure left to explain.

The standing CHANGES_REQUESTED is the bot's own review submitted at 2026-09-07T18:28:02Z on this same head. It is explicitly a partial review — "Not explored to full depth (tool budget reached)" — and it carries no inline findings. Its qwen-review-deferred block says so itself: the three items are "Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round". So the decision is not backed by a single requested change at this head.

One correction to that review. It discloses:

Test Plan (not a blocker): client/daemon/session/clientLifecycle.test.tsno such file or directory

That file exists at this head. Verified against the tree at ref a3ddb0606d:

  • packages/web-shell/client/daemon/session/clientLifecycle.test.ts — resolves
  • packages/web-shell/client/daemon/session/clientLifecycle.ts — resolves (the implementation it pins)

So the Test Plan reference in the description is correct and that disclosure is a sandbox miss on the review's side, not a defect here.

The gaps that review does disclose are honest, and none of them was requested as a change: runner.py's existence was not checked (it only decides which isMainModule branch the third test exercises), the doc's second added sentence was not checked against the daemon's full condition list, there was no grep for --preserve-symlinks-main / NODE_OPTIONS across CI and npm scripts, and ci.yml was not linted because actionlint's embedded-shell source mapping is unsupported. The three deferred items are all variants of one shape — isMainModule realpaths only argv[1], a third exported isMainModule with a third arity next to scripts/release-script-utils.js:61, and nothing pinning that the module's own guard can be exercised.

Firing /triage in a separate comment to re-judge this head. The previous /triage ran at c5a45ce45c on 2026-09-06T17:01Z and completed without superseding the bot's review; the head has moved since, so that run says nothing about a3ddb0606d.

@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 109 passed · 0 failed · 109 total

Flakiness gate: ✅ 3 changed test file(s) x 5 identical rounds, no divergence

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:109 通过 · 0 失败 · 109 总计

抖动门:✅ 3 changed test file(s) x 5 identical rounds, no divergence

Verification report

PR #11101 — Deep Verification Report (follow-up round)

Verdict: merge-ready — 109 scripted assertions executed, 0 unexpected failures.
Verified head OID: a3ddb0606d8cd92a42f70ab339e07c685eadbecf (git rev-parse HEAD^2, matches the snapshot's headRefOid).
Base arm for every A/B: a4a62258b1fcc58537a5c5e76fb59ed95eb21e16 (HEAD^1, the merge-ref base tip — and this round it also equals the snapshot's baseRefOid, unlike last round).
This is a follow-up round: the previous report ran at head c5a45ce4 against base 1a86cd6c. Neither OID is reachable in this depth-2 checkout, so every carried-forward measurement was rebuilt and re-run here — nothing was diffed out of the old report.

Three findings carry forward and all three still stand (F1 Suggestion, F2 Nit, F3 Nit). One claim the previous round never covered — the followup-suggestions.md rewrite — was verified this round: its stop-reason enumeration matches the shipped protocol schema exactly, but the same edit introduces one new inaccuracy of its own (F4, Suggestion), in the sentence that points a headless/SDK reader at a condition list that does not describe the daemon. No finding is a merge condition and none is a defect in this PR's executable behaviour.

中文摘要

结论:merge-ready —— 共执行 109 条脚本化断言,0 条非预期失败

本轮性质:这是跟进轮。上一轮在 head c5a45ce4 / base 1a86cd6c 上执行,两个 OID 在本次深度为 2 的 checkout 中都不可达,因此所有沿用的测量都在新 head 上重建并重跑,没有从旧报告里 diff 任何结论。本轮 base 与快照的 baseRefOid 首次一致(上一轮不一致)。

A/B 结论(中心主张,重测):本 PR 唯一的生产代码改动仍是导出脚本的 main-module 判定(改为比较 realpath)。两个只差这一个 hunk 的构建(改动行逐字比对,2 个 hunk、25 行,无一行越界)做 A/B,以「是否真的写出 HTML」为 oracle:base 2 种调用方式只成功 1 种,head 2/2。base 经软链调用时 main() 被跳过、退出码 0、stdout/stderr 全空、不写任何文件。三个写出单元格规范化后折叠为同一摘要 fcb65b1ac7490d5f,且文档含 3 个 block、startedAt 取最早时间戳而非文件首行。见下表 Central claim A/B01-ab-symlinked-invocation-base-vs-head.png

本轮新增,两项上一轮没有的证据

  1. 兄弟形状扫描:同一根因(argv[1] 未 realpath 而 import.meta.url 已 realpath)不止「软链文件」一扇门。枚举 7 种调用形状 × 2 个构建:base 在 3/7 上静默空转(软链文件、经软链目录、从无关 cwd 用软链绝对路径),head 在 0/7 上。同时划清边界:从「经软链进入的 cwd」用相对路径调用不会触发(Node 的 process.cwd() 是物理路径,已用独立探针证实),硬链接也不会触发。见 02-sibling-sweep-invocation-shapes.png
  2. 真实消费方端到端runner.py 第 340 行仅凭 returncode == 0 就打印「Rendered chat HTML saved」。把 runner.py 自己的调用块与 _run_command 按内容标记逐字抽取(抽取结果与源文件同区间 sha256 相同)后执行:base+软链目录下,它打印了「已保存」、没有任何 warning、而目录里只有 .jsonl,HTML 根本不存在;head 下声明与文件系统一致。见 03-runner-py-consumer-claims-vs-filesystem.png

依赖固定的 A/B(重测):版本一致性无法用 A/B 衡量(两侧代码相同),改用身份比较。按 HEAD^1 的 lockfile 拓扑用硬链接重建 base 依赖树,5/5 条目逐项与 lockfile 核对一致。以两个独立instrument读取 revision(各自包的 live chromium.executablePath(),与磁盘上 playwright-core/browsers.json),两者在每个单元格都一致:base 侧 2/2 个直接 import 站点分裂(直接 import → chromium 1208,runner → 1228),head 侧 2/2 一致(均 1228)。本容器只装了 chromium-1228,故 base 侧直接 import 实测无法启动浏览器;head 侧成功启动 Chromium 149.0.7827.55 并渲染出探针页面。见 04-playwright-parity-base-vs-head.png

上一轮 findings 状态(全部重测,三条全部 stands);本轮另新增 F4(见上):F1 stands(且本轮把「假设的未来」变成「今天就能复现」:caret 范围今天解析到 1.63.0,其 playwright-core 钉 chromium 1243 ≠ 固定的 1.61.1 的 1228,而 check-lockfile.js 照样通过、仓库内 0 处 parity 断言);F2 stands(用 workflow 自己的命令实测:base 下 PW_CLI 装 1208 而 .bin/playwright 钉 1228,head 下两者都是 1228 —— 注释前提已不成立,但代码仍应保留);F3 stands(M3 删除 realpath 兜底后 3/3 仍绿;正对照证明本 harness 能让该套件变红;候选 fixture 能杀掉该 mutant,且在未变异 head 上 4/4 绿 —— 即该轴当前无任何测试钉住)。

本轮新覆盖的文档主张(并因此新增一条 finding F4)followup-suggestions.md 新写的「end_turn 才生成,cancelled/refusal/max_tokens/max_turn_requests 不生成」是完整性主张。以依赖包自带的类型为 oracle(逐字取出,非手写期望):ACP SDK 声明 StopReason 恰为 5 个成员,代码里的门是 Session.ts:5090 if (result.stopReason !== 'end_turn') return;(allowlist),文档点名的 4 个恰为其补集 —— 不多不少、无虚构值,另有 zod 校验器、Java SDK enum、schema.json 三处佐证。但补测发现两点:① refusalmax_turn_requestspackages/cli/src从未被赋值(实际只产生 end_turn/cancelled/max_tokens),文档忠于协议但点名了两个本实现到不了的状态;② F4(Suggestion,本轮新增):PR 新加的那句「In the daemon … runs after every turn that meets the conditions above」把读者指向上面的条件列表,而该列表描述的是 CLI 而非 daemon —— 「最近响应无错误」「无待确认对话框」两条只在 AppContainer.tsx:3610-3630 检查,daemon 完全不查;daemon 另有三条(todoStopGuardgetLlmClient()?.getChat()、最后一条历史须为 role === 'model')列表里没有。归因分清:列表是既有的(不在本 PR diff 内),新增的是那句指回列表的话;而本 PR 也正是第一次把 daemon 写成该段的主语,才让既有的混写变得误导。该句给 headless/SDK 的成本建议本身是准确的(已实测:generatePromptSuggestion 只有两个非测试调用方,nonInteractive/ 无任何建议代码,daemon 路径无交互性门)。

未覆盖范围:逐提交归因不可达(深度 2,git rev-list HEAD^1..HEAD^2 返回 1 而快照记录 11 个提交);未跑全仓测试套件;Windows/macOS 未测;npm run lint 全仓未跑(只跑了针对改动文件的 eslint);上一轮 mutation 矩阵中针对 classifier 与 Markdown.tsx 的行本轮未重跑(预算),这两处生产文件不在本 PR diff 内。

Previous-finding status

Re-measured at the new head; every number below comes from a harness that ran this round (parity/run-findings.mjs, 21/21).

# finding severity status at a3ddb060 re-measured evidence
F1 Playwright parity invariant has no enforcement Suggestion stands — and is now reproducible today, not hypothetical check-lockfile.js has 0 playwright logic and exits 0; repo-wide census over scripts/, .github/scripts/, eslint-rules/, eslint.config.js finds 25 textual playwright hits and 0 asserting a version/parity relationship. @playwright/test@^1.57.0 resolves today to 1.63.0, whose playwright-core pins chromium 1243 (npm pack playwright-core@1.63.0, read from its own browsers.json) against the exact-pinned 1.61.1's 1228.
F2 qwen-triage.yml "Install evidence browser" comment premise falsified Nit stands Ran the step's own command verbatim on both arms: base PW_CLIplaywright@1.58.2, installs chromium 1208, while node_modules/.bin/playwright (→ @playwright/test/cli.js) pins 1228 — premise true. At head both are 1228 — premise false. The code should stay (F2.5); the comment must move together with scripts/tests/qwen-triage-workflow.test.js:5593, which asserts on its text.
F3 isMainModule's realpath fallback untested Nit (coverage gap) stands, and now bounded from both sides M3 (delete the try/catch fallback) survives 3/3. Positive control in the same file (invert the typeof guard) kills 1 — the harness is live. Driven directly, isMainModule('/definitely/not/here.js', …) returns false at head and throws with the fallback removed → not dead code. Candidate fixture kills M3 (not ok 4 - returns false for a nonexistent argv1 instead of throwing) while the intact suite is green with and without it (4/4) → the axis is pinned by nothing today.

| F4 | NEW this round — the doc's new sentence points a daemon reader at a condition list that describes the CLI, not the daemon | Suggestion | new | See Findings below. 5 scripted checks in manifest/run-docs-manifest.mjs. |

I agree with all three carried-forward severities as filed. Nothing was declined by the author, so there are no declined rows to adjudicate. F4 is new this round and is described in full under Findings.

Witness for all 21 rows below, including the positive control and the candidate fixture: 05-carried-forward-findings-f1-f2-f3.png.

One methodological catch worth recording, because it changes how F3 must be reproduced: my first M3 run killed the mutant, which contradicted the previous round. The cause was my scratch directory, not the code — the suite's negative case points at runner.py in the module's own directory, and a scratch variant that omits it changes the fixture as well as the mutation, making the realpathSync catch load-bearing for an unrelated reason. Mirroring the repository precondition (copying runner.py in, and asserting it is present) restores M3's survival. Both rows are in the harness (F3.3 and F3.3b) so the distinction is visible. Side observation for the author, not a finding: that test's green therefore depends on runner.py continuing to exist beside the module.

Scope

Central claim. One production-code change carries this PR: the exported-JSONL→HTML helper decided whether it was the main module by comparing path.resolve(process.argv[1]) against fileURLToPath(import.meta.url). Node realpath-resolves import.meta.url for an ESM main module but leaves argv[1] as invoked, so a symlinked spelling skipped main() and exited 0 having written nothing. The PR replaces that with an exported, realpath-based isMainModule().

Secondary claims (both re-verified).

  1. Pinning playwright to exact 1.61.1 restores version parity with @playwright/test, so direct-import tests drive the same chromium revision as the runner suite.
  2. The restored tests hold down what they claim to, and the new .qwen/review-context.json rule has an observable effect.

Claim added this round (not covered previously). The followup-suggestions.md rewrite asserts a stop-reason enumeration. Verified against the shipped ACP SDK's own type — see Docs claim below.

Out of scope by choice: the full repo test suite; Windows/macOS; per-commit attribution (unreachable); the #11076 thread itself (no token in this job).

Central claim A/B

Two arms side by side in ab/, differing by exactly the main-module hunk. The changed-line set is compared literally (not by pattern) against the expected 25 lines across 2 hunks, so any extra delta would fail the run. Real child processes, real filesystem, real @qwen-code/qwen-code/export resolved from the installed+built tree.

The internal workspace closure is asserted, not assumed: @qwen-code/qwen-code → <repo>/packages/cli, @qwen-code/qwen-code-core → <repo>/packages/core. Both arms share it, and the PR changes no production code under packages/, so it is not part of the change under test.

arm invocation exit wrote HTML bytes rawSha normSha stdout stderr
base direct 0 yes 7389 ec59d665… fcb65b1a… Wrote HTML export to: … (empty)
base symlink 0 no 0 (empty) (empty)
head direct 0 yes 7389 58e5af47… fcb65b1a… Wrote HTML export to: … (empty)
head symlink 0 yes 7389 d6ec943f… fcb65b1a… Wrote HTML export to: … (empty)

base wrote 1/2 cells; head wrote 2/2 (22/22 assertions). The base failure mode is the dangerous shape: exit 0, no stdout, no stderr, no artifact — a silent no-op that any harness reading an exit code scores as success.

Fidelity, not just "a file appeared": the symlinked artifact carries all 3 transcript blocks (user/assistant/user), diagnostics: [], "projectName":"work", "promptCount":2, and "startedAt":"2026-03-04T05:06:01.000Z" — the earliest timestamp, which sits on the last line of the fixture, so startTimeFor() demonstrably scanned rather than taking records[0].

Normaliser controls. Raw sha256 is an invalid oracle here, and this round measured exactly why: two renders of identical input differ in precisely two literal strings — a fresh CSP nonce appearing in five positions (including element.setAttribute('nonce', '…'), which a spelling-by-spelling normaliser misses) and a wall-clock exportedAt. The normaliser therefore extracts each literal and replaces every occurrence. Controls: two independent renders are raw-different, collapse to one digest (fcb65b1a…, shared by all three writing cells), the delta is 33 bytes of 7389, and every fidelity marker survives normalisation.

Witness: 01-ab-symlinked-invocation-base-vs-head.png.

Sibling sweep — the same root cause admits more than one shape

The reported shape is a symlinked file. The mechanism is "argv[1]'s spelling is not physical", so seven shapes were driven through both arms, with predictions written down before running (20/20 matched).

# invocation shape base head
S1 direct absolute realpath-clean path (positive control) writes writes
S2 symlink to the script file (the reported shape) silent no-op writes
S3 script reached through a symlinked directory (the macOS /tmp/private/tmp shape) silent no-op writes
S4 relative ./script from a realpath-clean cwd writes writes
S5 relative ./script from a cwd reached through a symlink writes writes
S6 hard link to the script writes writes
S7 absolute symlinked path from an unrelated cwd silent no-op writes
S8 imported as a module (negative control) does not run main() does not run main()

base silently no-ops on 3 of 7; head on 0 of 7. All 11 writing cells rendered the same document.

Two consequences are disproved, and bounding them is the point: S5 does not break base because Node's process.cwd() is physical — corroborated by an independent probe (invoked as dirlink/cwdprobe.mjs, node reported argv1 with the logical path while cwd was the realpath), which is the mechanism instrumented without going through the exporter. S6 does not break base because a hard link is a real directory entry, not something realpath resolves away. S8 also shows head exports isMainModule while base exports no such symbol — the test lane's import surface is new, and importing runs nothing on either arm.

Witness: 02-sibling-sweep-invocation-shapes.png.

The consumer announces an artifact that does not exist

runner.py is the real caller. It resolves the exporter as a sibling of __file__ and decides success from the return code alone:

if result.returncode == 0:
    self.console.print(f"[dim]Rendered chat HTML saved: {rendered_html_path.name}[/dim]")
else:
    self.console.print(f"[yellow]Warning: HTML exporter failed: {result.stderr}[/yellow]")

So the base arm's silent no-op is not a missing file — it is a false success message with no warning. This was driven end to end rather than argued: the exporter block (lines 329–348) and _run_command (lines 351–378) were extracted by content marker and their sha256 compared against the same line range re-read from runner.py (4e44936da41fd5ea, 727272ef142609ad), then exec'd with only console.print redirected. rich is not installed in this container, so this is a verbatim replay of the call site, not a full runner.py execution — the shape and the decision logic are the real ones; the surrounding orchestration is not.

arm __file__ path printed warned artifact on disk
base clean Rendered chat HTML saved: sess-consumer.html no present
base through a symlinked dir Rendered chat HTML saved: sess-consumer.html no ABSENT — dir holds only sess-consumer.jsonl
head clean same no present
head through a symlinked dir same no present

Claim-vs-filesystem agreement: base 1 of 2, head 2 of 2 (10/10 assertions). The head artifact renders the conversation (consumer user line, consumer assistant line, "projectName":"consumerwork"), so the fix restores content, not just a file.

Witness: 03-runner-py-consumer-claims-vs-filesystem.png.

Secondary claim 1 — the Playwright pin

A dependency pin has no A/B (both arms run identical code), so the oracle is an identity comparison. Base topology was reconstructed from HEAD^1:package-lock.json by hardlink-copying already-installed packages — hardlinks because Node realpath-resolves symlinks and a symlinked base tree would escape back into head's node_modules, the exact confound this comparison exists to avoid. All 5 base entries and all 5 head entries were validated against their lockfile (FAITHFUL on every row), and the trees were asserted to contain no symlinks.

arm site playwright direct rev @playwright/test runner rev match direct exe exists launch
base integration-tests/chat-transcript-document.test.ts 1.58.2 1208 1.61.1 1228 NO false FAILEDExecutable doesn't exist at …/chromium-1208/…
base integration-tests/terminal-capture/terminal-capture.ts 1.58.2 1208 1.61.1 1228 NO false FAILED — same
head integration-tests/chat-transcript-document.test.ts 1.61.1 1228 1.61.1 1228 YES true OK Chromium 149.0.7827.55, rendered h1
head integration-tests/terminal-capture/terminal-capture.ts 1.61.1 1228 1.61.1 1228 YES true OK same

base splits 2/2, head agrees 2/2 (18/18 assertions). The census is a fresh git grep, not carried forward: exactly 2 module-level direct-import sites, plus 2 textual mentions (the qwen-triage.yml comment and the test asserting on it).

Two independent instruments agree on every cell — each package's own live chromium.executablePath() (instrument a) and the shipped browsers.json of the playwright-core it resolves (instrument b): base 1208/1208, head 1228/1228. A file-based lookup alone would have given the wrong answer at head for .bin/playwright, because @playwright/test no longer nests a playwright-core and the hoisted root one is 1.58.2/1208 — so the runner's revision was read by asking the package itself.

The split was a real breakage, not a cosmetic version diff. This container has only chromium-1228 and chromium_headless_shell-1228 at PLAYWRIGHT_BROWSERS_PATH=/__w/_temp/pw-browsers — which is itself a third witness that the workflow's install step ran against head's tree.

Residual divergence, accounted for. Head keeps packages/mobile-mcp/node_modules/playwright@1.58.2 and a hoisted root playwright-core@1.58.2. This is forced: mobilewright@0.0.53 declares an exact playwright: "1.58.2", verified from the lockfile entry itself. It is off every path this PR is about — the direct-import path resolves playwright-core@1.61.1, and the census finds zero module-level playwright imports under packages/mobile-mcp. Also noted: integration-tests/terminal-capture/package.json declares its own playwright: "^1.50.0", but integration-tests is not a root workspace, so that declaration is never installed as a unit and root's exact 1.61.1 is what resolves.

Witness: 04-playwright-parity-base-vs-head.png.

Docs claim (new this round) — the stop-reason enumeration is complete and exact

The rewritten sentence names a end_turn gate plus four reasons that get no suggestion. A doc enumeration is a completeness claim, so the oracle is the shipped dependency's own type lifted verbatim, not a hand-written expectation. node_modules/@agentclientprotocol/sdk/dist/schema/types.gen.d.ts:2395:

export type StopReason = "end_turn" | "max_tokens" | "max_turn_requests" | "refusal" | "cancelled";

Exactly five members. The code's gate is an allowlist, packages/cli/src/acp-integration/session/Session.ts:5090:

if (result.stopReason !== 'end_turn') return;

so the complement is exactly the four the doc names — no invented values, none missing. The union is corroborated in three more places: the SDK's runtime zod validator (zod.gen.js:1029), the in-repo Java SDK enum (StopReason.java), and its schema.json.

Two refinements the enumeration alone does not settle, both scripted (18/18 assertions across docs + manifest):

  • Two of the four are unreachable in this codebase. git grep -oE "stopReason: '[a-z_]+'" over packages/cli/src finds only end_turn (153 sites), cancelled (54) and max_tokens (16) — plus reconstructed_from_transcript and shutdown, which belong to other locally-typed variables and are not StopReason members. refusal and max_turn_requests are never assigned. So the doc's list is faithful to the protocol and names two states this daemon cannot produce. That is not an error — describing the contract is a legitimate choice — but a reader who greps for refusal will find nothing.
  • The list directly above the new sentence does not describe the daemon. See F4.

Witness for this section and the manifest table that follows: 06-docs-stop-reason-and-manifest-effect.png. Supporting checks: the ui.enableFollowupSuggestions opt-out the doc points headless/SDK clients at is a real gate on the same path (Session.ts:5100, downstream of the stop-reason gate); the daemon gate region consults no interactivity flag, which is what makes the doc's new "a headless or SDK client … should set ui.enableFollowupSuggestions to false to avoid the per-turn LLM cost" advice correct rather than reassuring; the CLI-side gate lives separately in AppContainer.tsx:3583; and the stale unconditional wording ("after each completed turn the daemon emits") is gone.

Secondary claim 2 — the restored tests and the new routing rule

.qwen/review-context.json rule, driven through the real provider. The provider matches with rules.filter(...) then flatMap + cappedSortedUnique — a union of every matching rule, so the broader integration-tests/** rule that precedes the new one cannot shadow it. Driving manifestRepositoryContextProvider.provide() from packages/cli/dist with each arm's committed manifest:

arm recommendedTests for integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs
base ["integration-tests"]
head ["helper-tests","integration-tests"]

No over-reach: helper-tests is emitted for integration-tests/concurrent-runner/runner.py, and not for integration-tests/chat-transcript-document.test.ts (["integration-tests"]) or packages/cli/src/index.ts ([]).

ci.yml HELPER_TESTS battery, counted mechanically rather than by eye: base 25 files → head 26; added exactly integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs; removed none; no duplicates; every listed path exists on disk. The adjacent comment's 25-file26-file edit matches the actual count on both sides. The new entry is in HELPER_TESTS (dependency-installed lane) and not in HELPER_TESTS_DEP_FREE — the conservative choice, since the module's import surface is node builtins only. check-workflow-size.sh exits 0 with WORKFLOW_SIZE_BASE_SHA=HEAD^1: ci.yml grew 79 bytes (135987 → 136066), 1.9% of the 4096-byte baseline budget.

Mutation rows re-run this round are in the F3 table above, including the positive control that proves the harness can fail that suite.

Reviewer Test Plan, walked step by step

# step as written result
1 node --test .github/scripts/ci/classify-platform-sensitivity.test.mjs 12/12 pass
2 node --test integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs 3/3 pass (direct, symlinked, imported)
3 npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/export-html-from-chatrecord-jsonl.test.js 10/10 pass
4 cd packages/web-shell && npx vitest run client/components/messages/Markdown.test.ts 85/85 pass
5 npm run typecheck && npm run lint typecheck exit 0 (including typecheck:integration). Lint: the 6 changed lintable files exit 0, and eslint integration-tests (the second half of npm run lint) exit 0; whole-repo eslint . not run — see Not covered
6 npm ls playwright @playwright/test both 1.61.1, deduped

Every step was performable, and step 5's lint half was proven live rather than accepted as green: an unused variable planted in the PR's own new test file was reported (52:7 error 'unusedLintProbe' is assigned a value but never used @typescript-eslint/no-unused-vars, ✖ 1 problem, exit 1) and the file restored sha256-byte-identical.

Step 6's output also carries a pre-existing, unrelated invalid: marker — @vitest/browser@3.2.7 invalid: "1.6.1" from packages/sdk-typescript/node_modules/vitest. It concerns vitest, not playwright, and the two packages the step asks about do resolve identically. Noted so the step is not read as having printed a wholly clean tree.

The plan's own out-of-scope item was executed anyway: "the chat-transcript-document.test.ts browser-gate changes require a full build plus a Chromium install, so they were not run locally." This container has both, so the gate ran — 5/5 pass in 29.2 s with --retry=0. The changed assertion (chat-transcript-document.test.ts:476) sits inside opens, searches, copies, and prints the maximum document with its pinned npm runtime (declared at line 418), which took 20 986 ms of that run — so the changed line really executed in a real browser, rather than the file merely collecting green. No retry budget absorbed anything.

Step 4's file:-scheme assertions are green, and step 1's new shellCommandProcessor.ts fixture is green; the mutations that prove those two bite were not re-run this round (see Not covered).

Witness for this whole table, including the gates the PR implicates beyond the plan: 07-reviewer-test-plan-gates.png.

Findings

No blocking finding. The three carried-forward findings all stand, with the re-measured evidence in the status table at the top. One new Suggestion (F4) was found this round in the docs edit.

F4 — Suggestion (new): the new sentence sends a daemon reader to a list that describes the CLI

This PR adds, verbatim:

Suggestions will not appear in the CLI's non-interactive mode (e.g., headless/SDK mode). In the daemon, generation is server-side and runs after every turn that meets the conditions above, so a headless or SDK client that cannot render the suggestion should set ui.enableFollowupSuggestions to false to avoid the per-turn LLM cost.

The advice itself is correct and worth shipping — it was measured: generatePromptSuggestion has exactly two non-test callers (AppContainer.tsx:60, Session.ts:84), packages/cli/src/nonInteractive/ contains no suggestion code at all, the CLI path gates on config.isInteractive() && !config.getSdkMode() while the daemon path has no interactivity gate, so an SDK client really does pay the per-turn LLM cost unless it opts out.

The problem is the referent. "the conditions above" is the ## When Suggestions Appear bullet list, and there are two independent implementations that the list blends:

doc bullet CLI (AppContainer.tsx:3610-3630) daemon (Session.ts:5089-5120)
"no errors in the most recent response" checked — type !== 'error' and !pendingLlmHistoryItems.some(item => item.type === 'error') not checked
"no confirmation dialogs are pending" checked — five separate flags (shellConfirmationRequest, confirmationRequest, loopDetectionConfirmationRequest, isPermissionsDialogOpen, settingInputRequests) not checked — no dialog state exists server-side
"model has completed its response" Responding → Idle streaming transition stopReason === 'end_turn'
"at least 2 model turns" shared: MIN_ASSISTANT_TURNS = 2 in packages/core/src/followup/suggestionGenerator.ts:83 same
"approval mode is not plan" checked checked
"feature is enabled" checked checked

And the daemon has three conditions the list omits entirely: todoStopGuard.blocksUnrelatedAutomaticTurns || todoStopGuardQueuedPromptPriority (Session.ts:5091-5096), getLlmClient()?.getChat() being present (5103-5104), and the last curated-history entry having role === 'model' (5114-5120).

So a headless/SDK integrator — precisely the reader this new sentence addresses — is told the daemon fires on "every turn that meets the conditions above", and two of those conditions are things the daemon never looks at while three things it does look at are missing from the list.

Attribution, kept separate. The bullet list is pre-existing: git diff HEAD^1..HEAD on this file does not touch No confirmation dialogs are pending. What is new is the sentence that frames the daemon and refers back to that list. The PR's contribution is the mis-referral, not the blended list — though the PR is also the first edit to make the daemon the explicit subject of that paragraph, which is what makes the pre-existing blend newly misleading.

Minimal suggested fix (preserves the commit's intent; <b>not applied, not measured</b>)

Scope the referent instead of widening the list — the sentence only needs the conditions the daemon actually applies:

Suggestions will not appear in the CLI's non-interactive mode (e.g., headless/SDK mode). In the daemon, generation is server-side: it runs after every turn that ends with the `end_turn` stop reason and meets the enabled/approval-mode conditions above (the error and pending-dialog conditions are CLI-only), so a headless or SDK client that cannot render the suggestion should set `ui.enableFollowupSuggestions` to `false` to avoid the per-turn LLM cost.

Per the contract this is not a measured fix: it was not applied to a scratch copy and driven through a harness, because the oracle here is prose accuracy rather than an executable assertion. The five scripted checks that establish the underlying facts are measured, and are listed in logs/docs-and-manifest.txt.

Their suggested closings are unchanged and were not applied, so none is "measured" in the sense the contract requires of a fix I ship — they are reported as findings, not as patches.

For completeness, the sharpest form of F1 as measured this round: the invariant is not merely unenforced, it is one lockfile regeneration away from breaking today. @playwright/test@^1.57.0 resolves to 1.63.0 right now, which pins chromium 1243; root playwright is exact-pinned at 1.61.11228. node scripts/check-lockfile.js passes on the pinned tree and would keep passing after such a bump, because it validates lockfile↔package.json consistency and contains no playwright logic at all. Cheapest closing move remains: pin @playwright/test to the same exact version, or assert the two resolve identically in check-lockfile.js.

What was tested and did NOT hold. No defect was found in this PR's executable behaviour; F4 is a prose inaccuracy in the docs edit, not a code fault. Specifically disproved rather than merely unobserved: the fix does not regress direct or relative invocation (S1/S4/S5/S6 write on both arms, and head+direct normalises to the same digest as base+direct); importing the module still runs nothing on either arm (S8, with cwd left empty); the new routing rule does not over-reach beyond integration-tests/concurrent-runner/**; the doc's stop-reason list invents nothing and omits nothing; and the residual second Playwright tree is forced by an upstream exact pin and sits off every path this PR touches.

Not covered

  • Per-commit attribution. Depth-2 checkout; git rev-parse --is-shallow-repository is true. git rev-list HEAD^1..HEAD^2 returns 1 while the snapshot lists 11 commits — the shallow boundary undercounts silently rather than erroring, so the two were compared explicitly. The previous round's OIDs (c5a45ce4, 1a86cd6c) are not reachable at all (git cat-file -t fails on both), so no diff-against-last-round was possible and nothing was carried forward on prose. Only the aggregate HEAD^1..HEAD diff was verified.
  • npm run lint in full (its first half, eslint .). Not run. tmp/ is gitignored but not in eslint.config.js's global ignores, and this round's artifact directory lives there, so a whole-repo eslint . would report thousands of self-inflicted errors — the previous round hit exactly this and had to relocate its artifacts. Rather than present a number that looks like a PR result, lint was scoped: the 6 changed lintable files exit 0 and eslint integration-tests (the second half of the same npm script) exits 0, with a planted violation proving the gate is live. packages/ was not linted this round; the PR's only change there is one test file, which was linted individually.
  • Mutation rows for the classifier fixture and Markdown.tsx. The previous round's M5/M6 (classifier camelCase-boundary orthogonality) and the SAFE_HREF_SCHEMES file: mutation were not re-run this round — budget. Both suites are green at head (12/12 and 85/85) and neither production file (classify-platform-sensitivity.mjs, Markdown.tsx) appears in this PR's diff; whether main moved them between rounds cannot be checked, because the previous base OID is unreachable. Treat those two rows as not re-measured, not as re-measured and unchanged.
  • packages/cli manifest-repository-context.committed.test.ts in the main working tree — environmental, and proven so this round. It fails 1/5 here because the container froze .qwen/review-context.json as -r--r--r-- root root holding the base content (this PR's rule absent) while the agent runs as uid=1000(node); readCommittedFile() reads the working tree, not the index. An A/A control settles it: a scratch git worktree at HEAD, where the file is node-owned and carries head content, runs the same test file to 5/5 pass once the guard's prerequisites are mirrored (18 dist symlinks plus packages/cli/src/generated/git-commit.ts). So the failure is the frozen file, not the PR. git status shows the same single pre-existing M .qwen/review-context.json drift the previous round recorded, and the worktree was removed afterwards.
  • Trial merge into current main. No refs beyond the depth-2 checkout. The head contains three merges from main, the newest being this round's only new commit.
  • The full repo test suite and the whole 26-file HELPER_TESTS battery. Only files this PR touches or implicates were run (steps 1–4, 6, the browser gate, the size ratchet). The PR's own CI covers the rest from a clean checkout.
  • Windows and macOS. Linux container only; the PR body marks both untested. The realpath fix is POSIX-symlink-shaped — S3 models the macOS /tmp/private/tmp case mechanically, but nothing was executed on Darwin, and Windows junctions/reparse points were not exercised at all.
  • A live npm install of the pinned dependency. Parity was measured against the already-installed tree plus a hardlink reconstruction of base's lockfile topology. The registry was consulted read-only this round (npm view, npm pack playwright-core@1.63.0) for F1, but no install was performed.
  • runner.py end-to-end as a whole. The consumer replay executes the exporter call site and _run_command verbatim; the surrounding orchestration (session log copying, worktree lifecycle, status tracking) was not run, and rich is not installed here. This reproduces the decision logic and its consequence, not the full runner.
  • Browser-gate timing margins. expect.poll(...).toBe('light') replaced toContain('light'). The strengthening is real and the exactness is safe (the toggle is mutually exclusive), but the poll's timeout margin against the theme transition was not measured — this runner is shared and loaded, which is the regime where such a test passes.

Evidence images

file what it witnesses
01-ab-symlinked-invocation-base-vs-head.png the central-claim A/B cell table as printed: base skips main() on the symlinked invocation, head writes it
02-sibling-sweep-invocation-shapes.png 7 invocation shapes x 2 arms, with the process.cwd() physicality probe that explains why S5 does not break
03-runner-py-consumer-claims-vs-filesystem.png runner.py's own call site claiming a saved artifact that does not exist on base
04-playwright-parity-base-vs-head.png the parity identity table, both instruments, and the base launch failure
05-carried-forward-findings-f1-f2-f3.png F1/F2/F3 re-measured, plus the mutation rows and their positive control
06-docs-stop-reason-and-manifest-effect.png the shipped ACP union against the doc's list, and the new routing rule's effect
07-reviewer-test-plan-gates.png every test-plan step and the gates beyond it

Methodology

Linux container (node:22-bookworm lane), Node v22.23.2 / npm 10.9.8, working tree at the merge commit 2039f816 with npm ci + npm run build already completed by the workflow. The PR's effective diff is git diff HEAD^1..HEAD (11 files, 155 insertions, 63 deletions); the verified head is HEAD^2 = a3ddb060.

Every harness is a .mjs/.py/.sh file under this artifact directory and drives compiled or source-real code, never a stub of the unit under test: the A/B spawns real node child processes against two exporter copies that differ by exactly one hunk and asserts on exit code, stdout, stderr and the artifact left on disk; the sibling sweep adds symlinked files, a symlinked directory, hard links and relative invocations, plus an independent process.cwd() physicality probe; the consumer replay extracts runner.py's own call site by content marker with sha256 verification and execs it with only console.print redirected; the parity harnesses resolve packages from each site's own directory in hardlink-reconstructed trees validated entry-by-entry against each arm's lockfile, read the chromium revision with two independent instruments, and actually launch a browser; the docs harness lifts the StopReason union verbatim out of the shipped ACP SDK and reads the gate out of Session.ts; the manifest harness calls the real manifestRepositoryContextProvider.provide() from packages/cli/dist with each arm's committed manifest supplied through readIdentityFile.

Mutation work ran in scratch copies under mutate/; the repository tree was never modified. The one working-tree drift, .qwen/review-context.json, is the container's own root-owned frozen copy — sha-verified untouched, and the A/A worktree control above is what proves the resulting test failure is environmental. Scratch worktrees were removed with git worktree remove --force, and the reconstructed dependency trees live under parity/.

Raw per-cell stdout/stderr and every harness's output live in logs/ (ab-run.txt, ab-cells.json, sibling-sweep.txt, consumer-replay.txt, parity-run.txt, findings.txt, docs-and-manifest.txt, plus one log per gate); harnesses in ab/, parity/, manifest/; image evidence in evidence/. Assertion counts in assertions.json were summed from the harnesses' own recorded pass/fail lines, and count only checks that actually executed — with fail reserved for unexpected outcomes, so the base-arm cells that broke exactly as predicted (silent no-op, split revisions, unlaunchable browser, consumer claiming a missing artifact) are recorded as passes.

Flakiness gate log

integration test, out of gate scope: integration-tests/chat-transcript-document.test.ts
integration test, out of gate scope: integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs
rounds=5 files=3 skipped=2
file .github/scripts/ci/classify-platform-sensitivity.test.mjs: (cd .) node --test ./.github/scripts/ci/classify-platform-sensitivity.test.mjs
file packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/review/lib/manifest-repository-context.committed.test.ts
file packages/web-shell/client/components/messages/Markdown.test.ts: (cd packages/web-shell) npx --no-install vitest run ./client/components/messages/Markdown.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  .github/scripts/ci/classify-platform-sensitivity.test.mjs: PPPPP
  packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts: PPPPP
  packages/web-shell/client/components/messages/Markdown.test.ts: PPPPP

verdict: pass
summary: 3 changed test file(s) x 5 identical rounds, no divergence

--- per-invocation detail (full copy in the artifact) ---
round 1 · .github/scripts/ci/classify-platform-sensitivity.test.mjs: P (exit 0)
round 1 · packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts: P (exit 0)
round 1 · packages/web-shell/client/components/messages/Markdown.test.ts: P (exit 0)
round 2 · .github/scripts/ci/classify-platform-sensitivity.test.mjs: P (exit 0)
round 2 · packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts: P (exit 0)
round 2 · packages/web-shell/client/components/messages/Markdown.test.ts: P (exit 0)
round 3 · .github/scripts/ci/classify-platform-sensitivity.test.mjs: P (exit 0)
round 3 · packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts: P (exit 0)
round 3 · packages/web-shell/client/components/messages/Markdown.test.ts: P (exit 0)
round 4 · .github/scripts/ci/classify-platform-sensitivity.test.mjs: P (exit 0)
round 4 · packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts: P (exit 0)
round 4 · packages/web-shell/client/components/messages/Markdown.test.ts: P (exit 0)
round 5 · .github/scripts/ci/classify-platform-sensitivity.test.mjs: P (exit 0)
round 5 · packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts: P (exit 0)
round 5 · packages/web-shell/client/components/messages/Markdown.test.ts: P (exit 0)

Evidence images

01-ab-symlinked-invocation-base-vs-head

02-sibling-sweep-invocation-shapes

03-runner-py-consumer-claims-vs-filesystem

04-playwright-parity-base-vs-head

05-carried-forward-findings-f1-f2-f3

06-docs-stop-reason-and-manifest-effect

07-reviewer-test-plan-gates

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@yiliang114 yiliang114 changed the title chore(web-shell): follow up #9812 review findings and deferred suggestions chore: restore regression coverage after WebUI retirement Sep 8, 2026
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Resolved the remaining Critical at head 966d948c16fc:

Per the repository's round-6 convergence rule, the four non-blocking findings from the final verification are deferred rather than widening this PR: Playwright manifest parity enforcement (F1), the stale evidence-browser comment (F2), the missing-path isMainModule fallback decision/test (F3), and the blended CLI/daemon condition wording (F4). They are tracked together in #11336.

Verification: git diff --check and latest-main ancestry passed; the two dependency-free focused files passed 15/15; npm and pnpm lockfile validation passed. A fresh-install npm ls was not rerun in the isolated worktree; the committed lockfile records both Playwright packages at 1.61.1, and CI will verify the merge head.

@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@yiliang114

Copy link
Copy Markdown
Collaborator Author

CI follow-up: Install (windows-latest) exposed a PR-caused stale root specifier in the newly merged pnpm-lock.yaml (^1.57.0 versus manifest 1.61.1). Commit 95a35e061506 regenerates the lockfile with the required command; the resulting diff is one line. corepack pnpm install --lockfile-only --frozen-lockfile and node scripts/check-lockfile.js now pass.

@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 143 passed · 0 failed · 143 total

Flakiness gate: ✅ 3 changed test file(s) x 5 identical rounds, no divergence

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:143 通过 · 0 失败 · 143 总计

抖动门:✅ 3 changed test file(s) x 5 identical rounds, no divergence

Verification report

PR #11101 — Deep Verification Report (follow-up round, 3rd)

Verdict: merge-ready — 143 scripted assertions executed, 0 unexpected failures.
Verified head OID: 95a35e0615068325336269c977e353bec1de4069 (git rev-parse HEAD^2, matches the snapshot's headRefOid).
Base arm for every A/B: 078b924989148f5ae689e63dbb66590047c693e5 (HEAD^1, the merge-ref base tip). The snapshot's baseRefOid is 6ba04fc2… and is not reachable in this depth-2 checkout, so it was not used; HEAD^1 is the correct control on a merge-ref checkout.

This is a follow-up round. The previous report ran at head a3ddb060 against base a4a62258; neither OID is reachable (git cat-file -t fails on both), so nothing was diffed out of the old report and every carried-forward measurement was rebuilt and re-run here.

The delta since the previous round is two commits: 966d948c (a merge from origin/main) and 95a35e06 "fix(deps): sync pnpm lockfile for Playwright pin" — a one-line change to pnpm-lock.yaml. The aggregate diff grew by exactly one file / one insertion / one deletion versus the previous round, which matches. That one line turned out to be the most consequential thing in the PR: it is the difference between a red and a green pnpm-worktree-smoke CI job (proved below with the shipped bootstrap run verbatim on three arms).

Four findings carry forward and all four still stand (F1 Suggestion, F2 Nit, F3 Nit, F4 Suggestion). One new finding (F5, Suggestion) came out of the sibling sweep the central fix invites: the repo ships three other isMainModule implementations that still compare the raw spellings, and one of them was driven live returning false on a symlinked invocation where this PR's returns true. No finding is a merge condition and none is a defect in this PR's executable behaviour.

中文摘要

结论:merge-ready —— 共执行 143 条脚本化断言,0 条非预期失败

本轮性质:第三轮跟进。上一轮 head a3ddb060 / base a4a62258 在本次深度为 2 的 checkout 中都不可达,因此所有沿用测量都在新 head 上重建重跑,未从旧报告 diff 任何结论。快照的 baseRefOid6ba04fc2)同样不可达,故 A/B 一律以 HEAD^1078b9249)为对照。

本轮 delta:自上轮只多了两个提交 —— 一个 main 合并,以及 95a35e06「同步 pnpm lockfile 以匹配 Playwright 固定版本」pnpm-lock.yaml 一行)。这一行是本 PR 影响最大的改动:以逐字执行仓库自带的 node scripts/setup-worktree.js(即 pnpm-worktree-smoke.yml 的那一步)在三个分支上做 A/B,打了这个补丁之前 exit=1(ERR_PNPM_OUTDATED_LOCKFILE,pnpm 自己点名 playwright (lockfile: ^1.57.0, manifest: 1.61.1),且明确说只有 1 个依赖不匹配 → 同步既必要又完整);打了之后 exit=0、lockfile 未被改写、工作树干净。见下表 Delta A/B01-pnpm-frozen-lockfile-three-arms.png

中心主张 A/B(重测):唯一的生产代码改动仍是导出脚本的 main-module 判定改为比较 realpath。两个只差这一个 hunk(4 hunks / −5 / +20 行,逐字比对预期行集)的构建,以「是否真的写出 HTML」为 oracle,驱动 8 种调用形状 × 2 个构建,预测先写下再跑,16/16 命中base 4/7 写出,head 7/7。base 的三个失败单元格都是最危险的形状 —— exit 0、stdout/stderr 全空、不写任何文件。11 个写出单元格规范化后折叠为同一摘要;真实提交 fixture 渲染出 4 个 block、合成 fixture 渲染出 3 个 block,startedAt 取最早时间戳(位于 fixture 最后一行)。见 02-ab-symlinked-invocation-base-vs-head.png

上轮 findings 状态(全部重测,四条全部 stands):F1 stands(caret 今天解析到 1.63.0 → chromium 1243 ≠ 固定 1.61.1 的 1228check-lockfile.js 含 0 处 playwright 逻辑;仓库内 0 处 parity 断言);F2 stands(两个独立 instrument 在两个站点都读到 1228,注释「pins a different chromium revision」前提已不成立);F3 stands(删除 realpath 兜底后 3/3 仍绿;同文件两个正对照变红证明 harness 有效;候选 fixture 能杀掉该 mutant,且未变异时 4/4 绿 → 该轴当前无任何测试钉住);F4 stands(文档新增句把 daemon 读者指回描述 CLI 的条件列表,逐行核对两处实现)。

本轮新增 F5(Suggestion)git grep isMainModule 找到 4 个定义,其中 3 个(scripts/release-script-utils.js:61build-standalone-release.js:178create-standalone-package.js:219)仍在比较原始拼写。把共享的那个与本 PR 修好的那个放进同一进程,用一次软链调用驱动:共享 helper 返回 false,本 PR 的返回 true —— 分歧是实测的而非推演的。并已划清边界:遍历 .github/package.jsonMakefile 后,这些脚本的调用点没有一处经软链(全是 node scripts/….js),故 F5 是潜伏而非现行问题 → Suggestion,不是阻塞项;也不是要求复用共享 helper(签名不同、且在 release 工具树内)。见 03-mutation-matrix-f3-and-sibling-sweep-f5.png

Reviewer Test Plan:6 步全部可执行且全部通过(12/12、3/3、10/10、85/85、typecheck 0 条 error TSnpm ls 两条 playwright 路径均 1.61.1)。另跑了计划外的门:check-lockfile.js exit 0、workflow-size ratchet exit 0、package-scripts.test.js 39 passed / 1 skipped(它钉住了 pnpm smoke 的 path filter)。见 05-reviewer-test-plan-and-gates.png

未覆盖范围:逐提交归因不可达(快照 13 个提交,git rev-list --count HEAD^1..HEAD^2 因浅克隆返回 1);全仓 eslint . 未跑;全仓测试套件与 26 文件的 HELPER_TESTS 电池未跑;Windows/macOS 未测;chat-transcript-document.test.ts 浏览器门本轮未重跑(上轮 5/5,本 PR 对该文件的改动与上轮相同,但按规则它属于「未重测」而非「重测未变」);manifest-repository-context.committed.test.ts 在主工作树中的环境性失败本轮未重跑 A/A 对照(只实测了前提仍在:该文件是 -r--r--r-- root root 且内容仍是 base 版、不含本 PR 的规则);与最新 main 的 trial merge 不可达。

Previous-finding status

Re-measured at the new head; every number comes from a harness that ran this round.

# finding severity status at 95a35e06 re-measured evidence
F1 Playwright parity invariant has no enforcement Suggestion stands — still one regeneration away from breaking scripts/check-lockfile.js contains 0 playwright mentions and exits 0 on the pinned tree. Census over scripts/, .github/scripts/, eslint-rules/, eslint.config.js: 6 files mention playwright, 0 assert a version/parity relationship; the only version-adjacent assertion pins the comment text (qwen-triage-workflow.test.js, "Install evidence browser"). Both surviving carets (@playwright/test@^1.57.0 in packages/web-shell, ^1.49.0 in packages/mobile-mcp) resolve today to 1.63.0, whose playwright-core pins chromium 1243 (npm pack playwright-core@1.63.0, read from its own browsers.json) against the exact-pinned 1.61.1's 1228.
F2 qwen-triage.yml "Install evidence browser" comment premise falsified Nit stands The comment still says the two trees pin "a different chromium revision". Measured at head with two independent instruments at two sites: live chromium.executablePath() and the playwright-core each site resolves. integration-tests/terminal-capture1228 / 1228; repo root → 1228 / 1228; .bin/playwright@playwright/test/cli.jsplaywright@1.61.1 → nested playwright-core@1.61.11228. Premise false. The code should still stay (it is correct, just no longer load-bearing for the stated reason), and any edit must move together with scripts/tests/qwen-triage-workflow.test.js:5594, which asserts on the step by name. qwen-triage.yml is not in this PR's diff.
F3 isMainModule's realpath fallback untested Nit (coverage gap) stands, bounded from both sides M3 (delete the catch { path.resolve(argv1) } fallback) survives 3/3. Two positive controls in the same file go red (PC_drop_type_guardnot ok 3, 2/1; PC_break_url_comparenot ok 1+not ok 2, 1/2), so the harness is live. Driven directly the fallback is not dead code: intact answers true/false, with it removed both calls throw ENOENT … lstat '/definitely'. Candidate fixture kills M3 (not ok 3 - returns false for a nonexistent argv1 instead of throwing) while the intact suite is green with and without it (4/4 vs 3/3) → the axis is pinned by nothing today. Classified a coverage gap, not dead code and not redundant defence (no sibling hunk in this PR closes the same hazard).
F4 The doc's new sentence points a daemon reader at a condition list that describes the CLI Suggestion stands Re-read both implementations at the new head. Session.ts:5089-5120 gates on stopReason !== 'end_turn', todoStopGuard, enableFollowupSuggestions === false, ApprovalMode.PLAN, getLlmClient()?.getChat(), lastEntry.role !== 'model' — it checks neither "no errors in the most recent response" nor "no confirmation dialogs are pending". Those two live only in AppContainer.tsx:3611-3628 (pendingLlmHistoryItems.some(item => item.type === 'error') plus five dialog flags). The doc bullet list is pre-existing (not in this PR's diff); what is new is the sentence referring a daemon reader back to it.
F5 NEW this round — three other isMainModule copies still have the bug this PR fixes Suggestion new See Findings. 12 scripted checks in mutate/run-f5.mjs, including a live drive where the shipped shared helper and the PR's fixed helper disagree in one process.

Nothing was declined by the author, so there are no declined rows to adjudicate. I agree with all four carried-forward severities as filed.

Witness: 03-mutation-matrix-f3-and-sibling-sweep-f5.png (F3 matrix + F5 sweep), 04-f1-f2-playwright-parity.png (F1/F2).

Scope

Central claim (unchanged from last round, re-measured). One production-code change carries this PR: the exported-JSONL→HTML helper decided whether it was the main module by comparing path.resolve(process.argv[1]) against fileURLToPath(import.meta.url). Node realpath-resolves import.meta.url for an ESM main module but leaves argv[1] as invoked, so a symlinked spelling skipped main() and exited 0 having written nothing. The PR replaces that with an exported, realpath-based isMainModule().

Delta claim (new this round). 95a35e06 syncs pnpm-lock.yaml's recorded specifier to the pinned manifest, so pnpm install --frozen-lockfile — the command scripts/setup-worktree.js runs and .github/workflows/pnpm-worktree-smoke.yml gates on — accepts the tree.

Secondary claims (re-verified). (1) Pinning playwright to exact 1.61.1 restores version parity with @playwright/test. (2) The restored tests hold down what they claim to, and the new .qwen/review-context.json routing rule has an observable effect.

Out of scope by choice: the full repo test suite; the whole 26-file HELPER_TESTS battery; Windows/macOS; per-commit attribution (unreachable); the #11076 / #11336 threads (no token in this job).

Delta A/B — the pnpm lockfile sync is load-bearing, and it is complete

.github/workflows/pnpm-worktree-smoke.yml triggers on package.json for pull_request, and its install step is node scripts/setup-worktree.jscorepack pnpm install --frozen-lockfile --offline, retrying with --prefer-offline. This PR changes package.json, so that job fires on the PR.

Three arms, differing only in the two files the PR touches inside that gate's input closure. The closure is asserted, not assumed: git diff --name-only HEAD^1..HEAD over root + all workspace manifests, pnpm-workspace.yaml, .pnpmfile.mjs, .npmrc and patches/ returns exactly package.json and pnpm-lock.yaml. The two pnpm-lock.yaml variants were also asserted to differ by exactly one line (line 144).

The shipped bootstrap was run verbatim in each arm, with the registry reachable:

arm package.json pnpm-lock specifier node scripts/setup-worktree.js pnpm's verdict time lockfile rewritten tracked files dirty
base (HEAD^1) ^1.57.0 ^1.57.0 exit 0 Lockfile is up to date, resolution step is skipped 6.3 s no
mid (PR before 95a35e06) 1.61.1 ^1.57.0 exit 1 ERR_PNPM_OUTDATED_LOCKFILE 12.6 s no
head (PR at HEAD) 1.61.1 1.61.1 exit 0 Lockfile is up to date + Done in 55.1s using pnpm v11.24.0 99.5 s no 0 (clean)

base 0, mid 1, head 0 (14/14 assertions in pnpm/assert-online.mjs, plus 14/14 for the offline stage). The mid arm's own diagnosis, quoted from pnpm:

[ERR_PNPM_OUTDATED_LOCKFILE] Cannot install with "frozen-lockfile" because pnpm-lock.yaml is not up to date with <ROOT>/package.json
  Failure reason:
  specifiers in the lockfile don't match specifiers in package.json:
* 1 dependencies are mismatched:
  - playwright (lockfile: ^1.57.0, manifest: 1.61.1)

Two things fall out of that block, and both are asserted rather than read:

  • Necessary. Without this commit the pnpm-worktree-smoke job is red on all three OSes, at its install step (not its cleanliness step — the lockfile was not rewritten on mid either).
  • Complete. pnpm enumerates every mismatch and found exactly one. At head the same command exits 0, does not rewrite pnpm-lock.yaml (sha256 32aaaeb413a5da1e before and after), and leaves git status --porcelain clean — which is precisely the smoke job's third step, "Ensure bootstrap keeps the worktree clean".

What the sync does and does not change. The pnpm-lock resolved version for playwright is 1.61.1 on both arms; only the recorded specifier moved. So this commit changes no installed bytes — it changes whether the frozen gate accepts the tree. That is the right thing for it to do, and it is why no behavioural A/B applies.

Methodology note, recorded because it changed the result. The first offline run was non-discriminating: all three arms died on ERR_PNPM_NO_OFFLINE_META for @agentclientprotocol/sdk, because this container's pnpm metadata mirror was cold. That is an A/A control — base failed identically, so the failure was environmental, not the PR's. The offline discriminator there was the Lockfile is up to date line (present on base/head, absent on mid) and a 1.5 s vs 67–77 s bail. After the online head run warmed the store, the same offline command became fully discriminating (base exit 0, mid ERR_PNPM_OUTDATED_LOCKFILE, head exit 0), and that is the run reported. Both are in logs/.

Witness: 01-pnpm-frozen-lockfile-three-arms.png.

Central claim A/B — re-measured at the new head

Two arms differing by exactly the main-module hunk. The changed-line set is compared literally, not by pattern: git diff --no-index -U0 yields 4 hunks, −5 / +20 lines, and both the removed and the added line sequences are asserted equal, element by element, to the expected literal lists. Any extra delta fails the run. Real child processes, real filesystem, real @qwen-code/qwen-code/export.

The internal workspace closure is asserted: @qwen-code/qwen-code → packages/cli, @qwen-code/qwen-code-core → packages/core, and import.meta.resolve('@qwen-code/qwen-code/export') from inside an arm directory resolves to packages/cli/dist/src/export/index.js — so neither arm silently loads the other. git diff --name-only HEAD^1..HEAD -- packages returns only two .test.ts files, so the shared closure is not part of the change under test.

Predictions for all eight shapes were written down before running; 16/16 matched.

# invocation shape base head
S1 direct absolute realpath-clean path (positive control) writes writes
S2 symlink to the script file (the reported shape) silent no-op writes
S3 script reached through a symlinked directory (the macOS /tmp shape) silent no-op writes
S4 relative ./exporter.js from a realpath-clean cwd writes writes
S5 relative ./exporter.js from a cwd entered through a symlink writes writes
S6 hard link to the script writes writes
S7 double symlink chain (link → link → file) silent no-op writes
S8 imported as a module (negative control) does not run main() does not run main()

base wrote 4/7; head wrote 7/7 (60/60 assertions). Every base no-op is the dangerous shape: exit 0, empty stdout, empty stderr, no artifact — a harness that only reads the exit code scores all three as success.

Fidelity, not just "a file appeared." All 11 writing cells produce the same byte count and collapse to one normalised digest (a20b07872bf547d3), while remaining 11 distinct raw digests — raw sha256 is an invalid oracle here, and the harness proves why before relying on the normaliser (a fresh CSP nonce in several positions plus a wall-clock exportedAt). Content is really there: driven through the repository's own committed contract fixture (integration-tests/fixtures/chat-transcript-contract/v1/cases/representative/chat-records.jsonl — bytes off a real producer, blocks=4, contains Inspect the contract), and through a synthesized 3-record fixture whose earliest timestamp sits on the last line (blocks=3, startedAt = that earliest value, promptCount=2, model taken from the middle assistant record) — so startTimeFor() demonstrably scanned rather than taking records[0].

Two consequences are disproved, and bounding them is the point. S5 does not break base because a relative argv[1] is resolved against process.cwd(), which is physical — corroborated by an independent probe that never goes through the exporter: invoked as dirlink/cwdprobe.mjs, node reported argv[1] with the logical spelling (that is what breaks S3) but cwd as the realpath (that is why S5 writes). S6 does not break base because a hard link is a real directory entry, not something realpath resolves away. S8 also shows head exports isMainModule while base exports no such symbol; importing runs nothing on either arm.

Witness: 02-ab-symlinked-invocation-base-vs-head.png.

Secondary claim 1 — the Playwright pin

A dependency pin has no A/B (both arms run identical code), so the oracle is an identity comparison — see the F1/F2 rows in the status table and 04-f1-f2-playwright-parity.png. Two points worth stating separately from those findings:

  • Parity holds at head, in the npm tree. npm ls playwright @playwright/test (the PR's own plan step 6) shows root playwright@1.61.1, @playwright/test@1.61.1 under both packages/mobile-mcp and packages/web-shell, and @vitest/browser → playwright@1.61.1 deduped. On disk, node_modules/playwright@1.61.1 carries a nested playwright-core@1.61.1 (chromium 1228), matching the only browsers this container has (chromium-1228, chromium_headless_shell-1228 at PLAYWRIGHT_BROWSERS_PATH=/__w/_temp/pw-browsers) — itself a witness that the workflow's install step ran against head's tree.
  • Parity also holds in the pnpm tree. pnpm-lock.yaml records @playwright/test at version 1.61.1 for both importers (packages/web-shell specifier ^1.57.0, packages/mobile-mcp specifier ^1.49.0) and root playwright at 1.61.1. The residual playwright@1.58.2 / playwright-core@1.58.2 snapshots in both lockfiles are forced by mobilewright@0.0.53's exact upstream pin and sit off every path this PR is about.
  • A trap worth recording for the next reader. Resolving playwright-core directly from node_modules/@playwright/test/ gives 1.58.2 / chromium 1208, because @playwright/test@1.61.1 nests no playwright-core and the hoisted root one belongs to mobilewright's tree. That is the wrong answer — @playwright/test's CLI reaches core through its own playwright@1.61.1 dependency (1228). My first instrument (b) hit exactly this and reported 1208 where the live package said 1228; the fix was to re-anchor the require at the resolved playwright/package.json. Both instruments now agree on every cell.

Reviewer Test Plan, walked step by step

# step as written result
1 node --test .github/scripts/ci/classify-platform-sensitivity.test.mjs 12 pass / 0 fail
2 node --test integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs 3 pass / 0 fail (direct, symlinked, imported)
3 npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/export-html-from-chatrecord-jsonl.test.js 10/10 pass
4 cd packages/web-shell && npx vitest run client/components/messages/Markdown.test.ts 85/85 pass
5 npm run typecheck && npm run lint typecheck: 0 error TS lines across every project. Lint scoped — all 6 changed lintable files exit 0 and eslint integration-tests exit 0; whole-repo eslint . not run (see Not covered)
6 npm ls playwright @playwright/test both 1.61.1 on every path this PR touches

Every step was performable. Step 6's output still carries a pre-existing, unrelated invalid: marker (@vitest/browser@3.2.7 invalid: "1.6.1" from packages/sdk-typescript/node_modules/vitest) — vitest, not playwright; noted so the step is not read as having printed a wholly clean tree.

Gates the PR implicates beyond the plan: node scripts/check-lockfile.js exit 0 (both lockfiles); .github/scripts/check-workflow-size.sh with WORKFLOW_SIZE_BASE_SHA=HEAD^1 exit 0 (ci.yml within the 4096-byte baseline budget; the pre-existing 90 %-of-GitHub's-limit warning is about qwen-autofix.yml, untouched here); scripts/tests/package-scripts.test.js 39 passed / 1 skipped — worth naming because it is the test that pins pnpm-worktree-smoke.yml's path filter, i.e. the trigger that makes the Delta A/B above a CI gate rather than a local curiosity.

Step 2's suite is not vacuous (measured, not assumed): reverting only the realpath semantics while keeping the export intact — so no import or compile break — reddens exactly the test it exists for, not ok 2 - recognizes a main-module invocation through a symlinked path, AssertionError: expected true, actual false.

Witness: 05-reviewer-test-plan-and-gates.png.

Findings

No blocking finding. The four carried-forward findings all stand (status table above). One new Suggestion (F5) was found this round.

F5 — Suggestion (new): three other isMainModule copies still have the bug this PR fixes

The PR's own commit message states the mechanism exactly, and the mechanism is not specific to this file. git grep -n isMainModule finds 4 definitions across 10 files:

implementation compares on a symlinked invocation
integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.js (this PR) realpaths true
scripts/release-script-utils.js:61 — imported by generate-changelog.js, generate-release-notes.js, upload-aliyun-oss-assets.js, verify-installation-release.js path.resolve(process.argv[1]) === filename, filename = fileURLToPath(importMetaUrl) false
scripts/build-standalone-release.js:178 path.resolve(process.argv[1]) === __filename, __filename = fileURLToPath(import.meta.url) same expression
scripts/create-standalone-package.js:219 identical to the above same expression

Driven live, both helpers imported into one process and one symlinked invocation of the probe:

node /…/sibling/sibling-link.mjs        (symlink → sibling-probe.mjs)
  argv[1]     = …/sibling-link.mjs      ← LOGICAL spelling
  import.meta = …/sibling-probe.mjs     ← realpath-resolved
  shared helper (release-script-utils) → false      PR's fixed helper → true
direct invocation control:             shared → true,  fixed → true

So the divergence is measured, not inferred: the shipped shared helper skips the main block on a symlinked invocation and the process exits 0 having done nothing — exactly the failure mode this PR's commit message describes, in a file this PR does not touch. No test anywhere covers it (git grep isMainModule finds no scripts/**/*.test.* hit).

Bounded, and the bound is what keeps this a Suggestion. Every invocation site found by grepping .github/, package.json and Makefile is a plain node scripts/….js (finalize-release.yml:129,196, package.json:91,92,95, web-shell-visuals-publish.yml); 0 reach these through a symlink. The bug is therefore latent, not live.

This is not a request to reuse the shared helper. Its signature differs (isMainModule(importMetaUrl) reads process.argv[1] itself) and it lives in release tooling, so importing it from integration-tests/concurrent-runner/ would create a new cross-tree dependency for a 9-line predicate. The finding is narrower: three copies of a bug this PR just fixed remain in the repo, and the cheapest closing move is a follow-up that fixes scripts/release-script-utils.js in place (the two file-local copies are byte-identical in shape) rather than anything in this PR.

F1, sharpest form as measured this round

The invariant is not merely unenforced, it is one lockfile regeneration away from breaking today: @playwright/test@^1.57.0 resolves to 1.63.0 right now, which pins chromium 1243, while root playwright is exact-pinned at 1.61.11228. node scripts/check-lockfile.js passes on the pinned tree and would keep passing after such a bump, because it validates lockfile↔manifest consistency and contains no playwright logic. Cheapest closing move remains: pin @playwright/test to the same exact version, or assert the two resolve identically in check-lockfile.js. The PR body already tracks this as #11336.

What was tested and did NOT hold. No defect was found in this PR's executable behaviour. Specifically disproved rather than merely unobserved: the pnpm sync is not partial (pnpm enumerated exactly one mismatch, and head is green with no lockfile rewrite); the realpath fix does not regress direct, relative, symlinked-cwd or hard-link invocation (S1/S4/S5/S6 write on both arms, and head+direct normalises to the same digest as base+direct); importing the module still runs nothing on either arm; the PR's central new test is not vacuous; the two isMainModule helpers agree whenever nothing is symlinked; and the residual second Playwright tree is forced by an upstream exact pin and sits off every path this PR touches.

Not covered

  • Per-commit attribution. Depth-2 checkout, git rev-parse --is-shallow-repository = true. git rev-list HEAD^1..HEAD^2 returns 1 while the snapshot lists 13 commits — the shallow boundary undercounts silently rather than erroring, so the two were compared explicitly. The previous round's OIDs (a3ddb060, a4a62258) and the snapshot's baseRefOid (6ba04fc2) are all unreachable (git cat-file -t fails), so no diff-against-last-round was possible. Only the aggregate HEAD^1..HEAD diff was verified; the two-commit delta was inferred from the snapshot's commit list plus the aggregate diff growing by exactly pnpm-lock.yaml (1 file, 1 insertion, 1 deletion).
  • npm run lint in full (its first half, eslint .). Not run: tmp/ is gitignored but is not in eslint.config.js's global ignores, and this round's artifact directory lives there, so a whole-repo eslint . would report thousands of self-inflicted errors. Lint was scoped instead to all 6 changed lintable files (exit 0) and eslint integration-tests (exit 0). packages/ was not linted as a whole; the PR's only changes there are two test files, both linted individually. The lint gate was not proven live this round — the previous round planted a violation and confirmed it was reported; that plant-and-restore was not repeated here, so treat "live" as carried-forward prose, not as re-measured.
  • The browser gate (integration-tests/chat-transcript-document.test.ts, whose toContain('light')expect.poll(...).toBe('light') change is in this diff). Not re-run this round — budget went to the new pnpm surface. The previous round ran it 5/5 in 29.2 s with --retry=0 and confirmed the changed line executed inside a real browser (20 986 ms of the run). The PR's edit to that file is unchanged in the aggregate diff, but per the re-measurement rule this is not re-measured, not "re-measured and unchanged". Its poll timeout margin against the theme transition was never measured on any round, and this runner is shared and loaded — the regime where such a test passes.
  • Mutation rows for the classifier fixture and Markdown.tsx. Not re-run (budget). Both suites are green at head (12/12 and 85/85) and neither production file (classify-platform-sensitivity.mjs, Markdown.tsx) appears in this PR's diff.
  • packages/cli manifest-repository-context.committed.test.ts in the main working tree. Not re-run, and the A/A worktree control the previous round used was not repeated. The precondition it diagnosed was re-measured and still holds: .qwen/review-context.json is -r--r--r-- root root in the working tree and its content is the base manifest (a scripted read confirms it does not contain integration-tests/concurrent-runner/**), while the agent runs as uid=1000(node); readCommittedFile() reads the working tree, not the index. git status shows the same single pre-existing M .qwen/review-context.json drift. So a failure there would be environmental, but this round asserts only the precondition, not the outcome. The routing rule's effect was therefore not driven through the real provider this round either — the previous round measured base ["integration-tests"] → head ["helper-tests","integration-tests"]; treat that as carried-forward prose.
  • Trial merge into current main. No refs beyond the depth-2 checkout.
  • The full repo test suite and the whole 26-file HELPER_TESTS battery. Only files this PR touches or implicates were run. The ci.yml battery contents were not re-counted this round (the previous round counted base 25 → head 26 mechanically); the diff hunk adding the 26th entry and the matching 25-file26-file comment edit are both visible in git diff HEAD^1..HEAD.
  • Windows and macOS. Linux container only; the PR body marks Windows untested. The realpath fix is POSIX-symlink-shaped — S3 models the macOS /tmp/private/tmp case mechanically, but nothing ran on Darwin and Windows junctions/reparse points were not exercised at all.
  • A pnpm install from a genuinely cold store. The head arm's online run reused this container's warm /__w/.pnpm-store/v11, and the base arm ran after head had populated it. Both are green, but a first-ever cold install on a runner was not reproduced. The shape of the gate failure is reproduced (real pnpm, real manifests, real lockfiles, verbatim shipped command); the smoke job's behaviour on a fresh runner is the PR's own CI's business, not this round's.

Methodology

Linux container (node:22-bookworm lane), Node v22.23.2 / npm 10.9.8 / corepack pnpm 11.24.0, working tree at the merge commit 7ee68f99 with npm ci + npm run build already completed by the workflow. The PR's effective diff is git diff HEAD^1..HEAD (12 files, 156 insertions, 64 deletions); the verified head is HEAD^2 = 95a35e06.

Every harness is a .mjs/.sh file under this artifact directory and drives compiled or source-real code, never a stub of the unit under test. The pnpm A/B runs the repository's own scripts/setup-worktree.js verbatim — the exact command the smoke workflow runs — in a scratch git worktree at HEAD whose two varying files are supplied from git show HEAD^1:… / git show HEAD:…, and asserts on exit status, pnpm's own error code and diagnosis text, the lockfile's sha256 before and after, and git status --porcelain. The central A/B spawns real node child processes against two exporter copies that differ by exactly one hunk (the changed-line set compared literally against the expected −5/+20) and asserts on exit code, stdout, stderr and the artifact left on disk, across symlinked files, a symlinked directory, a double symlink chain, hard links and relative invocations, plus an independent argv[1]-vs-cwd spelling probe. The parity harnesses read each package's live chromium.executablePath() and its resolved playwright-core/browsers.json as two independent instruments, and lift today's caret resolution from the registry (npm view) and npm pack playwright-core@1.63.0. The mutation work runs in scratch mirrors under mutate/scratch/ that copy runner.py in beside the module to reproduce the repository precondition. The repository tree was never modified.

The one working-tree drift, .qwen/review-context.json, is the container's own root-owned frozen base-content copy, re-measured and described under Not covered. The scratch worktree at pnpm/tree still holds a pnpm-installed node_modules from the head arm.

Raw per-cell output and every harness's log live in logs/ (ab-run.txt, pnpm-frozen-ab.txt, pnpm-online-{base,mid,head}.txt, pnpm-online-assert.txt, findings-f1f2.txt, findings-f3.txt, findings-f5.txt, gates.txt, typecheck.txt, plan-summary.txt); harnesses in ab/, pnpm/, parity/, mutate/; recorded cells in ab/cells.json, pnpm/cells.json, pnpm/online-*.json, parity/f1f2.json, mutate/f3.json, mutate/f5.json; rendered artifacts in ab/artifact-*.html; image evidence in evidence/.

Assertion counts in assertions.json (143 pass / 0 fail / 143 total) were summed from the six harnesses' own recorded pass/fail lines and count only checks that actually executed. fail is reserved for unexpected outcomes, so the base- and mid-arm cells that broke exactly as predicted (silent no-op, ERR_PNPM_OUTDATED_LOCKFILE) are recorded as passes. Four harness-expectation bugs found and fixed during this round are recorded rather than hidden: the offline pnpm discriminator was first asserted as an error code that a cold metadata mirror makes unreachable on every arm; a require used inside --input-type=module -e threw and a catch silently fell back to the hoisted playwright-core, reporting chromium 1208 where the live package says 1228; npm view <spec> version output was parsed with the wrong token; and playwright/cli.js is not in that package's exports map (ERR_PACKAGE_PATH_NOT_EXPORTED), so the require had to be re-anchored at playwright/package.json.

Evidence images

file what it witnesses
01-pnpm-frozen-lockfile-three-arms.png the Delta A/B as printed: the shipped bootstrap's exit status per arm, pnpm's own one-line mismatch diagnosis, and the smoke job's cleanliness step
02-ab-symlinked-invocation-base-vs-head.png the central-claim A/B: 8 invocation shapes × 2 arms, base 4/7 vs head 7/7, and the argv[1]-vs-cwd spelling probe that explains S3 vs S5
03-mutation-matrix-f3-and-sibling-sweep-f5.png the F3 mutation matrix with its same-file positive controls and vacuity row, and the F5 sibling sweep with its live divergence
04-f1-f2-playwright-parity.png F1/F2 re-measured: the enforcement census, today's caret resolution against the pin, and both parity instruments at both sites
05-reviewer-test-plan-and-gates.png every test-plan step's own exit code and counts, plus the gates the PR implicates beyond the plan
Flakiness gate log

integration test, out of gate scope: integration-tests/chat-transcript-document.test.ts
integration test, out of gate scope: integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs
rounds=5 files=3 skipped=2
file .github/scripts/ci/classify-platform-sensitivity.test.mjs: (cd .) node --test ./.github/scripts/ci/classify-platform-sensitivity.test.mjs
file packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/review/lib/manifest-repository-context.committed.test.ts
file packages/web-shell/client/components/messages/Markdown.test.ts: (cd packages/web-shell) npx --no-install vitest run ./client/components/messages/Markdown.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  .github/scripts/ci/classify-platform-sensitivity.test.mjs: PPPPP
  packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts: PPPPP
  packages/web-shell/client/components/messages/Markdown.test.ts: PPPPP

verdict: pass
summary: 3 changed test file(s) x 5 identical rounds, no divergence

--- per-invocation detail (full copy in the artifact) ---
round 1 · .github/scripts/ci/classify-platform-sensitivity.test.mjs: P (exit 0)
round 1 · packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts: P (exit 0)
round 1 · packages/web-shell/client/components/messages/Markdown.test.ts: P (exit 0)
round 2 · .github/scripts/ci/classify-platform-sensitivity.test.mjs: P (exit 0)
round 2 · packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts: P (exit 0)
round 2 · packages/web-shell/client/components/messages/Markdown.test.ts: P (exit 0)
round 3 · .github/scripts/ci/classify-platform-sensitivity.test.mjs: P (exit 0)
round 3 · packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts: P (exit 0)
round 3 · packages/web-shell/client/components/messages/Markdown.test.ts: P (exit 0)
round 4 · .github/scripts/ci/classify-platform-sensitivity.test.mjs: P (exit 0)
round 4 · packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts: P (exit 0)
round 4 · packages/web-shell/client/components/messages/Markdown.test.ts: P (exit 0)
round 5 · .github/scripts/ci/classify-platform-sensitivity.test.mjs: P (exit 0)
round 5 · packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts: P (exit 0)
round 5 · packages/web-shell/client/components/messages/Markdown.test.ts: P (exit 0)

Evidence images

01-pnpm-frozen-lockfile-three-arms

02-ab-symlinked-invocation-base-vs-head

03-mutation-matrix-f3-and-sibling-sweep-f5

04-f1-f2-playwright-parity

05-reviewer-test-plan-and-gates

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@yiliang114
yiliang114 enabled auto-merge September 8, 2026 07:45

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

Review — chore: restore regression coverage after WebUI retirement

Round 1 · head 95a35e06 · base 6ba04fc2


Scope

Reviewed all 12 changed files at head. Static analysis only — no local execution environment available (Playwright/Chromium browser gate, Node/Vitest suites not run). Cross-checked against all prior rounds from qwen-code-ci-bot.

Not reviewed / not run:

  • Browser gate in integration-tests/chat-transcript-document.test.ts (requires Playwright + built asset)
  • Windows behavior (not verified locally; CI covers Linux; macOS confirmed by author in PR body)
  • --preserve-symlinks-main / NODE_OPTIONS sweep across all npm scripts (CI workflow grep returned no matches; edge-case impact only)

Prior-round findings status

All confirmed blockers and suggestions from prior rounds (R1–D4) have been resolved at this head:

ID Summary Status
R1-1 toHaveClass incompatible with vitest expect Fixed — .poll().toBe('light')
R1-2 Inert synthetic-unhandledrejection block Fixed — block deleted
R1-3 isMain path-spelling guard fails on symlinks Fixed — isMainModule with realpathSync
R1-5 Stop-reason enumeration named interrupted Fixed — cancelled, refusal, max_tokens, max_turn_requests
R1-6 "unconditional" contradicts six early-return conditions Fixed — wording removed
R1-8 throw in assertRenderableJsonl unreachable from test Fixed — assertRenderableJsonl exported and called directly
R1-9 Test factory hand-rolled synthetic ChatRecord shape Fixed — factory removed; .mjs now covers only isMainModule
R1-10 Stop-reason gate on negative side has no test Deferred to #11159 — agreed split, not a blocker here
R1-11 node:test .mjs file not routed to correct CI lane Fixed — .qwen/review-context.json narrower rule added
R2-1 .toMatch(/light/) matches dark light Fixed — .toBe('light')
R3-1 PR body test plan inconsistent with actual .mjs scope Fixed — PR body reworded
R3-2 Duplicate persists under the literal historical key prefix block Fixed — block deleted
D4-6 HELPER_TESTS hand-edited list lacked the new .mjs Fixed — .mjs added, count comment updated 25→26

New findings at this head

No new blockers. No new major findings.

Minor observations (body-only, no inline threads per round-1 policy for non-blockers already addressed):

  • shellCommandProcessor.ts in the PLATFORM_INSENSITIVE assertion: the file contains os.platform() === 'win32' and conditional path logic, but the classifier intentionally cannot see into source internals — its own comment documents this and names the nightly scheduled run as the backstop. The test addition correctly pins the existing classifier boundary (camelCase-embedded keyword is not a segment or stem-head match). No action needed here; the design trade-off is explicitly stated in the classifier.

  • Playwright pin (1.61.1 exact vs. caret ^1.57.0): the @playwright/test/node_modules override that duplicated the 1.61.1 sub-tree is cleanly removed; root playwright resolves to 1.61.1 for both the test runner and the root package. The long-term manifest-level parity enforcement is tracked in #11336 as noted in the PR.


Cross-file checks performed

  • isMainModule: verified runner.py exists in integration-tests/concurrent-runner/ — the third test case (non-main path) uses a real file and is sound.
  • isSafeHref / isSafeImageSrc: confirmed at head that SAFE_HREF_SCHEMES = /^(https?:|mailto:)/ifile: URIs are already rejected; the new tests pin correct existing behavior.
  • ACP StopReason: confirmed end_turn, cancelled, max_tokens are used in Session.ts; refusal and max_turn_requests appear in the doc as valid non-end_turn suppressors — consistent with result.stopReason !== 'end_turn' guard at line 5090.
  • .qwen/review-context.json narrower rule: the manifest-repository-context.ts code merges all matching rules rather than first-match; the committed test snapshot was updated to match; both sides are consistent.
  • HELPER_TESTS count: old list had 25 entries; new list adds integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs; comment updated to 26. Count verified by inspection.

Verdict

No blocking findings. No approval blockers.

Approving.

Reviewed with AI assistance.

@qwen-code-dev-bot qwen-code-dev-bot 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.

Approved at head 95a35e0615.

The one thing that was blocking this — R5-1 — is resolved in the description at this head, and I checked the description rather than the claim about it. The Closes #11076 keyword is gone; the body now states plainly that "the four items originally tracked in #11076 were completed separately by #11107; this PR neither reimplements nor closes #11076", which is what the earlier rounds asked for twice. That also removes the merge-time hazard (a close keyword firing at an issue that has been closed since 2026-09-05) and the mis-scoped coverage claims, and the deferred non-blocking suggestions now have a home (#11336) instead of living only in review threads.

The code side holds up on its own terms. The earlier Critical about toHaveClass — a @playwright/test web-first matcher used under vitest's expect, which typechecked red and threw Invalid Chai property at runtime — is gone: no toHaveClass remains anywhere under integration-tests/, and the theme check is now await expect.poll(() => page.locator('html').getAttribute('class')).toBe('light') at chat-transcript-document.test.ts:474-476, sitting under a comment that explains why the attribute is exactly one of two values. The typecheck:integration command that CI runs inside the no-AK job is green there (24m38s / 12m39s on this head), so the author's note about errors "remaining locally, all in packages/**" is a local-tree artifact and not this lane.

The package.json1.61.1 Playwright pin is exact rather than a range, and the newest commit (95a35e0615) exists only to bring pnpm-lock.yaml in step with it — which is the failure mode #10449's dual-lockfile arrangement creates whenever a pin moves on one side, and it is enforced by the lockfile check that runs in the green Lint & Static lane.

I ran the new lane myself. node --test integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs at this head: 3 passed, 0 failed, including recognizes a main-module invocation through a symlinked path — the exact shape that motivated the isMainModule realpath fix, where a symlinked invocation used to exit successfully having written nothing. The wiring around it is consistent too: the file is appended to HELPER_TESTS in ci.yml:90, the "25-file" comment two lines below became 26, and .qwen/review-context.json gained a matching integration-tests/concurrent-runner/**helper-tests/node22 route, with classify-platform-sensitivity.test.mjs picking up its one line.

One wording nit, not blocking. The comment at export-html-from-chatrecord-jsonl.test.mjs:15-17 justifies the second lane by saying the vitest suite "cannot exercise [the main-module check] without spawning". That reason is wrong in the direction that matters for future maintainers: the assertions call isMainModule(exporterPath, exporterUrl) with computed arguments, so no spawn is needed and they could live in the existing suite — the real reason for the file is its node:test form and the HELPER_TESTS battery. Say that instead the next time this file is touched, so nobody deletes a "duplicate" on the strength of the current comment.

CI facts. At this head Lint & Static 24m38s and Integration Tests (no-AK) 12m39s are green; Test (ubuntu-latest), review-pr, assign and delay-automatic-review were still queued or running when I submitted, and nothing has reported red. All 12 review threads are resolved, and chiga0's approval is on this head; the page's CHANGES_REQUESTED row is ci-bot's, anchored at the pre-description-fix commit, so a @qwen-code /triage re-run is what clears it.

@yiliang114
yiliang114 added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit 32f51a3 Sep 8, 2026
69 of 71 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ⚠️ not run — skipped - workflow run

Skipped because the PR is not open for verification (state=MERGED, draft=false).

中文 — 判定:⚠️ 未运行 · 已跳过

跳过原因:the PR is not open for verification (state=MERGED, draft=false)。

Qwen Code · sandboxed verification

yiliang114 added a commit that referenced this pull request Sep 8, 2026
Works the four items tracked in #11336:

- Pin Web Shell's @playwright/test to the same exact version as the root
  playwright dependency, and fail `npm run check:lockfile` when the two
  direct manifests drift apart or their resolved root trees stop matching
  the pin. The check deliberately tolerates the packages/mobile-mcp tree:
  mobilewright depends on playwright and playwright-core at an exact
  older revision, so no manifest edit in this repository can dedupe it.
- Correct the "Install evidence browser" rationale in qwen-triage.yml and
  its restatement in the workflow test. The two-trees premise no longer
  holds now that both manifests are pinned; the resolution behavior is
  unchanged, since resolving from the harness's own directory still keeps
  the binary tied to the package it imports.
- Cover the ENOENT fallback of the exported isMainModule helper. The
  existing runner.py assertion takes the realpath success path because
  that file exists, so the fallback stayed unpinned.
- Separate the CLI-only from the daemon-only followup suggestion gates in
  the documentation instead of pointing daemon clients at the blended
  CLI-oriented list, keeping the headless/SDK opt-out guidance.
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.23.1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/self-reported The linked issue was opened by the PR author (self-reported)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

chore(web-shell): follow up deferred #9812 review suggestions

4 participants