Skip to content

refactor(web-shell): share sidebar test harness - #9797

Open
bluefateludi wants to merge 1 commit into
QwenLM:mainfrom
bluefateludi:codex/refactor-9701-web-shell-sidebar-harness
Open

refactor(web-shell): share sidebar test harness#9797
bluefateludi wants to merge 1 commit into
QwenLM:mainfrom
bluefateludi:codex/refactor-9701-web-shell-sidebar-harness

Conversation

@bluefateludi

Copy link
Copy Markdown

What this PR does

Consolidates the stable test setup shared by three Web Shell sidebar suites into one collocated harness. Session-page resolution, DOM shims, session fixtures, async flushing, and click dispatch now have one implementation, while suite-specific catalog controllers and rendering options remain local.

Why it's needed

The copied setup had already diverged on the unloaded-page contract: an explicitly unloaded catalog page could expose stale fixture sessions in one suite while remaining empty in another. Sharing that contract prevents future catalog changes from being applied inconsistently and removes repeated test plumbing without changing production behavior.

Reviewer Test Plan

How to verify

Run the three Web Shell sidebar suites together and confirm all 128 tests pass. Confirm the Web Shell and repository type checks pass, the repository build succeeds, and no production file or test expectation changed.

Evidence (Before & After)

N/A — internal test refactor with no user-facing change.

Tested on

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

Environment (optional)

Node.js 22 on Windows. Targeted Vitest suites: 128 passed. Repository build and typecheck passed. Changed TypeScript files passed ESLint and all changed files passed Prettier.

Risk & Scope

  • Main risk or tradeoff: an overly broad harness could hide suite-specific behavior, so only stable shared behavior moved and specialized catalog controllers remain local.
  • Not validated / out of scope: the full unit-test suite and browser E2E; this change has no production behavior.
  • Breaking changes / migration notes: none.

Linked Issues

Fixes #9701

中文说明

这个 PR 做了什么

将三套 Web Shell 侧边栏测试中稳定且重复的测试设置合并到一个同目录共享 harness。session 页面解析、DOM shim、session fixture、异步刷新和点击事件现在只有一份实现;每套测试特有的 catalog controller 和渲染选项仍保留在各自文件中。

为什么需要

重复的测试设置已经在未加载页面契约上产生分歧:显式处于未加载状态的 catalog 页面,在一套测试里可能暴露过期 fixture session,而在另一套测试里保持为空。共享这一契约可以防止后续 catalog 改动在三套测试中应用不一致,并在不改变生产行为的前提下删除重复测试样板。

审阅者测试计划

如何验证

一起运行三套 Web Shell 侧边栏测试并确认 128 项测试全部通过。确认 Web Shell 和整个仓库的类型检查通过、仓库构建成功,并确认没有生产文件或测试断言发生变化。

证据(改动前后)

不适用——内部测试重构,没有用户可见变化。

测试环境

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

环境(可选)

Windows 上的 Node.js 22。定向 Vitest 测试 128 项通过。仓库构建和类型检查通过。改动的 TypeScript 文件通过 ESLint,所有改动文件通过 Prettier。

风险与范围

  • 主要风险或权衡:范围过大的 harness 可能掩盖各测试套件特有的行为,因此只移动稳定的共享行为,特殊 catalog controller 仍保留在本地。
  • 未验证或超出范围:完整单元测试套件和浏览器 E2E;本改动不涉及生产行为。
  • 破坏性改动或迁移说明:无。

关联 Issue

Fixes #9701

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 23, 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 Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Re-run note: this gate was first posted on Aug 23 and re-verified on Aug 25 — no new commits have landed (same head SHA), and every finding below still holds.

  • Template: complete ✓ — all sections filled in, bilingual body, and the N/A under Before/After is the right call for a test-only refactor.
  • Problem: real and documented. This implements Extract shared mock harness for WebShellSidebar test suites (web-shell) #9701, which was filed by a maintainer as a follow-up to the fix(web-shell): stable pinned-section order and instant pin feedback #9560 review: three near-verbatim copies (~150 lines each) of the sidebar mock harness that had already diverged on the useWebShellSessions unloaded-page contract. Not theoretical — the divergence is quoted line-by-line in the issue.
  • Direction: aligned. The issue explicitly requests this exact extraction, including the file name and the "dedicated refactor PR with no behavior change" scope. CHANGELOG reference not applicable for a test-infrastructure refactor.
  • Size: no core infrastructure paths touched (packages/core, auth/providers/models/config/tools/services — none). Production logic lines: 0 — the diff is 3 test suites, 1 test harness, and a 21-line design doc under docs/design/ (which follows the repo convention for multi-file changes). Stage 0 gate not applicable.
  • Approach: scope matches the issue's suggested scope exactly — shared resolver, DOM shims, fixtures, flush and click helpers move to the collocated harness, while suite-specific catalog controllers and render options stay local. That split is the right call: the workspace-removal suite genuinely models more (catalog invalidation, channels, multi-workspace routes), so forcing it through a shared controller would have been the over-abstraction to avoid. No drive-by changes in the diff.
  • Risk: no elevated risk signals — no revert-correlated paths matched; zero production files touched.

Moving on to code review. 🔍

中文说明

Re-run 说明:本门禁首发于 8 月 23 日,8 月 25 日复核——没有新提交(head SHA 不变),下列结论全部依然成立。

  • 模板: 完整 ✓ —— 各部分齐全,中英双语,Before/After 写 N/A 对纯测试重构是正确处理。
  • 问题: 真实且有据可查。本 PR 实现 Extract shared mock harness for WebShellSidebar test suites (web-shell) #9701,该 issue 由 maintainer 在 fix(web-shell): stable pinned-section order and instant pin feedback #9560 评审后提出:三套几乎逐字复制(各约 150 行)的侧边栏 mock harness,且在 useWebShellSessions 未加载页面契约上已经产生分歧。不是理论问题——issue 里逐行列出了分歧点。
  • 方向: 对齐。issue 明确要求这次抽取,连文件名和"独立重构 PR、不改变行为"的范围都写明了。CHANGELOG 对测试基础设施重构不适用。
  • 规模: 未触及核心基础设施路径(packages/coreauth/providers/models/config/tools/services 均未涉及)。生产逻辑行数为 0 —— diff 由 3 个测试套件、1 个测试 harness 和一份 21 行的 docs/design/ 设计文档(符合仓库对多文件改动的约定)组成。Stage 0 门槛不适用。
  • 方案: 范围与 issue 建议完全一致——共享的 resolver、DOM shim、fixture、flush 和 click 助手移入同目录 harness;各套件特有的 catalog controller 和渲染选项保留在本地。这个拆分是对的:workspace-removal 套件确实建模了更多内容(catalog 失效、channels、多工作区路由),强行共用 controller 反而是过度抽象。diff 中无顺手改动。
  • 风险: 无升级风险信号——未命中高回滚相关路径;零生产文件改动。

进入代码审查。🔍

Qwen Code · qwen3.8-max

Reviewed at 08b76029aa7200905c4d5be80fcbc90714a2ca90 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Code review

Re-run on Aug 25: no new commits since the first pass, and I re-verified the findings against the diff and the base tree rather than trusting the earlier write-up. Still no blockers — the interesting part of this PR is whether unifying the three resolvers silently changes any fixture's behavior, so that's where the review stays focused.

The unified resolver is equivalent to the old ones on every fixture state these suites actually produce.

  • The session-pinning and collapse-persist fixtures always carry their own data key, so Object.hasOwn picks state.data — and the sessions: data === undefined ? [] : state.sessions expression is exactly session-pinning's original one. The pinned.data = undefined unload cases there produce sessions: [] under both old and new contracts.
  • The workspace-removal fixture has no data key in its base shape, so the hasOwn fallback data = state.sessions matches its old 'data' in state branch. I re-checked its two unloaded-page tests in the base tree: both set active.sessions = [] before spreading { ...state, data: undefined }, so old and new resolvers are observably identical there too.
  • The one real semantic change is deliberate and correct: collapse-persist's old ?? fallback exposed stale fixture sessions on an explicitly unloaded page. The unified contract matches the real hook — useScopedSessions returns page?.sessions ?? [] and data: page ? page.sessions : undefined, and WebShellSidebar consumes .sessions. That's precisely the drift Extract shared mock harness for WebShellSidebar test suites (web-shell) #9701 asked to eliminate, now pinned in one place.

Everything else checks out: the workspace-removal click delegate with includePointerEvents=true dispatches the same pointerdown(button 0) + pointerup + click sequence as before, the other two suites keep plain click dispatch, no orphaned imports remain, and zero expect(...) lines were touched — consistent with the "no test expectation changed" claim. One structural check added this pass: WebShellSidebar.test-harness.ts does not match Vitest's **/*.{test,spec}.* collection pattern in the package config, so the harness is imported by the suites but never collected as one. Suite-specific controllers staying local is the right boundary.

Test evidence

The previous pass found all three fork CI workflows sitting in action_required; a maintainer has since approved them, and all CI on the reviewed commit is now green. The three pull_request workflows — Qwen Code CI, Web-shell Visuals, and Security Checks — all completed with conclusion success on 08b76029aa7200905c4d5be80fcbc90714a2ca90, and there are no failed check-runs on this commit. Test (ubuntu-latest, Node 22.x) (the unit suite including the three sidebar suites) and web-shell E2E Smoke (ubuntu-latest, Node 22.x) both passed; the macOS/Windows test legs and CLI integration jobs were skipped by this PR's CI profile (a web-shell-only change), not failed.

Check Conclusion
Capture web-shell visuals (ubuntu-latest, Node 22.x) ✅ success
Classify PR ✅ success
Dependency CVE audit ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Secret scan (TruffleHog) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success

The author's "128 targeted tests pass" claim was unverified last pass (Windows-only, author-reported); CI now independently runs the same suites on Linux/Node 22 and passes. No sandboxed-verification lane is named here because there is no behavioural claim to settle — this is a test-only refactor with zero production files changed, and the test suite is itself the deliverable.

Real-scenario testing: N/A — unattended CI run, and the change is test-only with nothing user-visible. Not verified: nothing outstanding — the previous pass's only gap (suite results on this commit) is now covered by green CI.

中文说明

代码审查

8 月 25 日 re-run:自首轮审查以来没有新提交,我对照 diff 与基线树重新核实了每项结论,而非沿用旧记录。仍无阻塞问题——本 PR 的关键在于统一三个 resolver 是否会悄悄改变某个 fixture 的行为,审查集中在这里。

  • session-pinning 与 collapse-persist 的 fixture 始终带有自己的 data 键,Object.hasOwn 会取 state.data——sessions 表达式与 session-pinning 原实现完全一致;pinned.data = undefined 的未加载场景在新旧契约下都得到 sessions: []
  • workspace-removal 的基础 fixture 没有 data 键,hasOwn 回退 data = state.sessions 与旧版 'data' in state 分支一致。本轮在基线树中复核了它的两个未加载页面用例:都先设 active.sessions = [] 再展开 { ...state, data: undefined },新旧 resolver 的可观察行为完全相同。
  • 唯一的真实语义变化是有意且正确的:collapse-persist 旧版的 ?? 回退会在显式未加载页面上暴露过期 fixture sessions。统一后的契约与真实 hook 一致——useScopedSessions 返回 page?.sessions ?? []data: page ? page.sessions : undefined,且 WebShellSidebar 消费 .sessions。这正是 Extract shared mock harness for WebShellSidebar test suites (web-shell) #9701 要消除的分歧,现在固定在一处。

其余均核对无误:workspace-removal 的 click 委托发出与此前相同的 pointerdown(button 0) + pointerup + click 序列;另两套保持普通 click;无孤立 import;expect(...) 断言行零改动。本轮补充一项结构性检查:WebShellSidebar.test-harness.ts 不匹配包内 Vitest 的 **/*.{test,spec}.* 收集模式,只会被套件导入、不会被当作测试收集。套件特有的 controller 保留在本地是正确边界。

测试证据

上一轮时三个 fork CI 工作流都处于 action_required;maintainer 已批准,审查提交上的 CI 现在全部为绿。三个 pull_request 工作流——Qwen Code CIWeb-shell VisualsSecurity Checks——均在 08b76029aa7200905c4d5be80fcbc90714a2ca90 上以 success 结束,该提交无任何失败 check。Test (ubuntu-latest, Node 22.x)(含三套侧边栏测试的单测套件)与 web-shell E2E Smoke (ubuntu-latest, Node 22.x) 通过;macOS/Windows 测试分支与 CLI 集成任务因本 PR 的 CI profile(仅 web-shell 改动)被跳过,并非失败。

作者"128 项测试通过"的声明上一轮无法核实(仅 Windows、作者自述);CI 现已在 Linux/Node 22 上独立运行同样的套件并通过。此处不点名沙盒验证通道,因为没有需要落地的行为声明——这是零生产文件改动的纯测试重构,测试套件本身就是交付物。

真实场景测试:不适用——无人值守 CI 运行,且改动仅涉及测试,无用户可见变化。未验证项:无遗留——上一轮唯一的缺口(该提交上的套件结果)已由绿色 CI 覆盖。

Qwen Code · qwen3.8-max

Reviewed at 08b76029aa7200905c4d5be80fcbc90714a2ca90 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — clean review and green CI on the reviewed commit; the cap is pure policy — a fork refactor PR needs a maintainer's sign-off before merge, and the bot cannot give it.

Re-run reflection: nothing has changed in the code since the first pass (same head SHA), and re-verifying it only confirmed the earlier read. My independent proposal before touching the diff would have been exactly this shape — one collocated harness for the shared plumbing, the three diverged unloaded-page resolvers unified onto the real hook's contract, suite-specific controllers kept local — and the PR matches it down to the file name suggested in #9701. The only place a harness consolidation can bite is silently changing fixture semantics; I checked that fixture-by-fixture again this pass, including the workspace-removal suite's two unloaded-page cases in the base tree, and the single semantic change is the divergence the issue was opened to fix. Every edit is necessary, no expectation churn, zero production files touched, and the design doc earns its keep.

What changed since the first pass is the evidence. Back then the three fork CI workflows were stuck in action_required and the 128-test claim rested on the author's word; a maintainer approved the runs, and Qwen Code CI, Web-shell Visuals, and Security Checks all landed success on this exact commit, with the sidebar suites green on Linux/Node 22. So the one concrete gap from the previous review is closed.

The reason I'm still not approving is the one that doesn't go away with evidence: fork refactor PRs always need a human maintainer's eye under this repo's guardrails, so the approval here has to come from a person, not from me.

⏸️ Deferring to @wenshao — you triggered this re-run, and the code and CI now have nothing outstanding on my end. If the extraction delivers what #9701 (filed by @yiliang114 after the #9560 review) asked for, this needs your human approval to go with the green CI. I see nothing standing in the way of merge.

中文说明

置信度:3/5 —— 审查干净、审查提交上的 CI 全绿;压到 3 分纯属策略——fork 的 refactor PR 合并前需要 maintainer 签核,而机器人不能代签。

Re-run 复盘:首轮以来代码没有变化(head SHA 相同),重新核实只是确认了先前判断。在读 diff 之前我的独立方案就是这个形状——一个同目录 harness 承载共享样板,把三个已分歧的未加载页面 resolver 统一到真实 hook 的契约,套件特有的 controller 保留本地——本 PR 连文件名都与 #9701 的建议一致。harness 合并唯一可能出问题的地方是悄悄改变 fixture 语义;本轮再次逐个核对,包括在基线树中检查 workspace-removal 的两个未加载页面用例,唯一的语义变化正是 issue 要修的分歧。每处改动都必要,断言零变化,零生产文件改动,设计文档言简意赅。

与首轮相比变化的是证据。当时三个 fork CI 工作流卡在 action_required,128 项测试的声明只有作者自述;maintainer 批准后,Qwen Code CIWeb-shell VisualsSecurity Checks 在该提交上全部 success,侧边栏套件在 Linux/Node 22 上通过。上一轮的唯一缺口已经闭合。

仍然不批准的原因与证据无关:按本仓库护栏,fork 的 refactor PR 必须由人类 maintainer 把关,这里的批准必须来自人,而不是我。

⏸️ 转交 @wenshao —— 这次 re-run 由你触发,代码与 CI 在我这里已无遗留。如果这次抽取兑现了 #9701@yiliang114#9560 评审后提出)的要求,需要你的人工批准与绿色 CI 配套。我看不到合并的阻碍。

Qwen Code · qwen3.8-max

Reviewed at 08b76029aa7200905c4d5be80fcbc90714a2ca90 · 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.

Reviewed — no blockers. Suggestions are inline.

Test Plan (not a blocker): 128 tests pass — this review observed 4159 passed; 128 passed — this review observed 4159 passed.

中文说明

已审查——无阻断问题。 建议见行内评论。

Test Plan(非阻断):128 tests pass — this review observed 4159 passed; 128 passed — this review observed 4159 passed

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

Comment on lines +1 to +2
import { act } from 'react';
import type { DaemonSessionSummary } from '@qwen-code/sdk/daemon';

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.

[Suggestion] The new WebShellSidebar.test-harness.ts is the first test-only file colocated under client/components/, and its .test-harness.ts suffix slips past every exclusion the package has for test code. tsconfig.lib.json includes client/**/*.ts and excludes only client/**/*.test.ts(x), client/e2e/**, and client/test/**, so the declaration build emits dist/types/components/sidebar/WebShellSidebar.test-harness.d.ts, and package.json publishes dist/types wholesale — test internals (installSidebarDomShims, makeSidebarSession, resolveWebShellSessions) end up in @qwen-code/web-shell's public type surface. Separately, the vitest.config.ts coverage excludes (**/*.test.{ts,tsx}, **/test/**, **/e2e/**, **/*.d.ts) do not match it either, so CI coverage counts this test-only file as production code, and its jsdom-unreachable shim branch means it can never reach full branch coverage — any future coverage threshold would fail on test-support code. Every earlier harness lives under the excluded client/e2e/ or client/test/ directories. Verified in a scratch tree: before the fix the lib build emits the .d.ts and coverage counts 268 files including the harness (97.29% statements); after adding the exclusions, zero test-harness declarations are emitted and the file disappears from coverage (267 files).

Fix — either add exclusions:

"client/**/*.test-harness.ts"   → exclude in packages/web-shell/tsconfig.lib.json
'**/*.test-harness.{ts,tsx}'    → coverage.exclude in packages/web-shell/vitest.config.ts

or move the harness into packages/web-shell/client/test/ (the package's existing convention, e.g. reactHarness.tsx).

中文说明

新的 WebShellSidebar.test-harness.ts 是第一个放在 client/components/ 下的测试专用文件,其 .test-harness.ts 后缀绕过了包内所有针对测试代码的排除规则。tsconfig.lib.jsonincludeclient/**/*.ts,只排除了 client/**/*.test.ts(x)client/e2e/**client/test/**,因此声明构建会输出 dist/types/components/sidebar/WebShellSidebar.test-harness.d.ts,而 package.json 会整体发布 dist/types——测试内部实现(installSidebarDomShimsmakeSidebarSessionresolveWebShellSessions)将进入 @qwen-code/web-shell 的公共类型面。另外,vitest.config.ts 的覆盖率排除项(**/*.test.{ts,tsx}**/test/****/e2e/****/*.d.ts)同样不匹配该文件,所以 CI 覆盖率会把这个纯测试文件统计为生产代码;它在 jsdom 中不可达的 shim 分支意味着它永远无法达到完整的分支覆盖——未来若设置覆盖率阈值,会因测试支撑代码而失败。此前所有 harness 都放在被排除的 client/e2e/client/test/ 目录下。已在临时树中验证:修复前,库构建会输出该 .d.ts,覆盖率统计 268 个文件且包含该 harness(语句覆盖 97.29%);加上排除项后,不再输出任何 test-harness 声明,该文件也从覆盖率报告中消失(267 个文件)。

修复方式——添加排除项:

"client/**/*.test-harness.ts"   → packages/web-shell/tsconfig.lib.json 的 exclude
'**/*.test-harness.{ts,tsx}'    → packages/web-shell/vitest.config.ts 的 coverage.exclude

或者将 harness 移到 packages/web-shell/client/test/(包内现有约定,如 reactHarness.tsx)。

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

Comment on lines +79 to +84
export async function flushSidebar(): Promise<void> {
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
}

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.

[Suggestion] flushSidebar is a byte-for-byte copy of the existing shared helper flushReact() in packages/web-shell/client/test/reactHarness.tsx — the same act() wrapping two awaited microtasks, the same () => Promise<void> signature. If the flush recipe ever needs tuning (for example an extra await round after a React scheduling change), the fix must be applied in both files, and updating only one leaves the sidebar suites silently diverging from every other consumer of flushReact — the exact copy-drift failure mode this PR exists to eliminate. Cross-directory imports from client/test already exist in this package (e.g. EnhancedMarkdownTable.test.tsx imports reactHarness).

Suggested change
export async function flushSidebar(): Promise<void> {
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
}
export { flushReact as flushSidebar } from '../../test/reactHarness';
中文说明

flushSidebarpackages/web-shell/client/test/reactHarness.tsx 中已有的共享辅助函数 flushReact() 逐字节相同——同样是 act() 包裹两次 await 微任务,签名同为 () => Promise<void>。如果将来需要调整刷新逻辑(例如 React 调度变化后需要多一次 await),修复必须同时应用到两个文件;只改其中一个会让侧边栏测试与其他所有 flushReact 使用者悄然分叉——这正是本 PR 要消除的复制漂移失效模式。包内已有跨目录引用 client/test 的先例(如 EnhancedMarkdownTable.test.tsx 引入 reactHarness)。

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

Comment on lines +9 to +15
import {
clickSidebarElement as click,
flushSidebar,
installSidebarDomShims,
makeSidebarSession as makeSession,
resolveWebShellSessions,
} from './WebShellSidebar.test-harness';

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.

[Suggestion] The consolidation of click gestures is incomplete within this PR's own scope: this suite already imports clickSidebarElement as click (and uses it for its plain clicks), but still emits the full inline pointerdown+pointerup+click triple at three call sites — groupItem! (~lines 1071-1078), renameItem! (~lines 1385-1392), and document.body (~lines 1415-1422). That triple is exactly clickSidebarElement(el, true), which is what the workspace-removal suite's local click wrapper now delegates to. With two representations of the same 3-event gesture in the consolidated suite family, a future change to the gesture (e.g. Radix dismissable-layer handling requiring an extra event) will patch only one representation, and the suites will behave differently for the same interaction — the drift this refactor removes in workspace-removal survives here.

Replace each inline triple with the already-imported alias, passing true so the pointer events are preserved:

click(groupItem!, true);
// ...
click(renameItem!, true);
// ...
click(document.body, true);

Note: calling click(el) with the default includePointerEvents = false would drop the pointer events and change behavior.

中文说明

点击手势的合并在本 PR 自身范围内并不彻底:本测试文件已经引入 clickSidebarElement as click(并在普通点击中使用),但仍在三处调用点内联发出完整的 pointerdown+pointerup+click 三连事件——groupItem!(约 1071-1078 行)、renameItem!(约 1385-1392 行)、document.body(约 1415-1422 行)。这个三连事件正是 clickSidebarElement(el, true),也就是 workspace-removal 套件的本地 click 封装现在委托的实现。合并后的套件家族中同一 3 事件手势存在两种表示,未来一旦需要修改该手势(例如 Radix dismissable-layer 处理需要额外事件),只会修补其中一种表示,各套件对同一交互的行为将出现差异——本次重构在 workspace-removal 中消除的漂移在这里依然存在。

将每处内联三连事件替换为已引入的别名,并传入 true 以保留指针事件(见上方代码块)。

注意:使用默认 includePointerEvents = falseclick(el) 会丢掉指针事件,从而改变行为。

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

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

Screenshots · before / after

⚠️ No preview: one or more scenarios failed to render on this head — see the workflow run. This is not "no visual change" — a scenario that times out or throws produces no image. Fix the failing scenario (or a genuine regression it caught) and the preview returns on the next push.

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

Qwen Code · web-shell visuals

@wenshao

wenshao commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 25, 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: 33 passed · 0 failed · 33 total

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

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

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

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

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

Verification report

PR 9797 Deep Verification — refactor(web-shell): share sidebar test harness

Verdict: merge-ready — assertions: 33 pass / 0 fail / 33 total
(4 gate checks + 22 mutation-matrix verdicts + 7 resolver-differential probe
tests). Verified head: 08b76029aa7200905c4d5be80fcbc90714a2ca90
(merge commit 59dff58, base tip 4a492bc).

中文摘要
  • 结论: merge-ready。33 项脚本化断言全部通过,0 失败。
  • A/B 结论: 见「Mutation A/B 矩阵」——9 个生产代码单点突变体在基线套件与 PR 套件上的击杀完全对称(7 个被双方击杀,2 个双方都存活),0 回归、0 新增击杀;共享 harness 的钉住能力与基线完全一致。解析器差分测试证明:三套测试实际使用的 fixture 形状上,新统一解析器与旧的三套实现逐格一致(0 分歧);11 个分歧格全部落在没有任何 fixture 产生的形状上,其中 collapse-persist 的分歧正是 PR 描述中声明要统一的动机。
  • Findings: 1 条 Suggestion 级、预先存在的覆盖缺口(乐观置顶覆盖层被同步 mock 掩盖,两个 arm 对称存活,与本 PR 无关)。无阻塞项。
  • 未覆盖: issue Extract shared mock harness for WebShellSidebar test suites (web-shell) #9701 正文(沙箱无 token)、三个套件之外的完整单测与浏览器 E2E、仓库级 build 由 CI 在本轮之前完成(未重跑)。

Central claim and scope

PR 9797 is a test-only refactor (single commit 08b7602, reachable:
git rev-list --count HEAD^1..HEAD^2 = 1, matching the metadata snapshot).
Three Web Shell sidebar suites — collapse-persist (31 tests),
session-pinning (17), workspace-removal (80) — give up their copied setup
(session-page resolution, DOM shims, session fixtures, async flushing, click
dispatch) in favor of one collocated harness,
WebShellSidebar.test-harness.ts.

Central claim: the consolidation is pinning-preserving — the head suites
pin the production behavior of WebShellSidebar/collapsedSessionSections
at least as hard as the base suites, and no test expectation changed.

Secondary claims: (1) 128 tests pass and no production file changed;
(2) the unified resolveWebShellSessions is behavior-identical to the three
old per-suite resolvers over every fixture shape the suites actually produce.

Explicitly out of scope (see Not covered): the full unit suite, browser
E2E, and issue #9701 contents.

Gates

Scripted in gate-assertions.sh (witness: 05-gates.png):

gate base (HEAD^1) head (HEAD^2)
three sidebar suites (vitest JSON reporter) 128/128 pass 128/128 pass (31 + 17 + 80)
web-shell typecheck (tsc -p tsconfig.json --noEmit) pass (exit 0)
repo-wide typecheck (npm run typecheck) pass (exit 0)
ESLint on the 4 changed TS files pass (exit 0)
production files touched (--name-status) 0 — only 3 .test.tsx, 1 test-harness.ts, 1 design doc
expect( assertion lines changed (diff grep) 0

The reviewer test plan's "repository build succeeds" is covered by the CI
environment contract (npm ci + npm run build completed at HEAD before
this round); I did not re-run the build. Head suites witness:
01-head-suites-128-of-128.png.

Resolver differential (wire-oracle style)

The three base suites implemented different useWebShellSessions mock
resolvers — collapse-persist used data: state.data ?? state.sessions
(explicit undefined exposed stale sessions), session-pinning used
sessions: state.data === undefined ? [] : state.sessions (absent-key and
explicit-undefined collapsed together), workspace-removal used
'data' in state ? state.data : state.sessions (never emptied sessions).
The PR unifies them into resolveWebShellSessions (absent key = settled
fixture; explicit undefined = unloaded page → empty sessions).

The probe (logs/, witness 03-resolver-differential.png) extracted the
three old resolvers verbatim from the base files (byte-level verbatim
guards asserted), imported the real new harness function, and drove all
36 corpus cells (3 session shapes × 6 data-key shapes × enabled ±) through
both, comparing {sessions, data} field by field:

suite divergent cells on EXERCISED fixture shapes divergent cells on unexercised shapes
collapse-persist 0 6 — explicit undefined/null data (old ?? kept stale sessions)
session-pinning 0 3 — absent data key (old emptied sessions)
workspace-removal 0 2 — explicit undefined with non-empty sessions

All 11 unexercised divergences are shapes no fixture produces (census
asserted in the probe: collapse-persist never assigns undefined/null
data; session-pinning fixtures always own a data key; workspace-removal's
makeSessions() creates states without data, and its two
explicit-undefined tests set sessions = [] first). The collapse-persist
divergence is exactly the contract mismatch the PR description names as its
motivation — the unification deliberately closes it, and nothing exercised
depended on the old branch. 7/7 probe tests pass.

Mutation A/B matrix (test-only-PR method)

Nine single-point mutants of the production files (untouched by the PR),
each applied identically in the base worktree and the head tree; each arm ran
the full three-suite filter. Matrix witness: 02-mutation-matrix.png;
scripted verdicts (22 checks): 04-matrix-verdicts.png.

mutant what it breaks base suites head suites
control (unmutated) 128/128 128/128
M1 collapsed-sections storage read killed (4 failing) killed (4 failing)
M2 collapsed-sections storage write killed (4 failing) killed (4 failing)
M3 loading notice: drop sessionsPage === undefined gate killed (2) killed (2)
M4 error/retry notice: drop sessionsPage === undefined gate killed (2) killed (2)
M5 pin toggle: skip applyOptimistic() survived survived
M6 non-live workspace list includes live workspaces killed (1) killed (1)
M7 retry button onClick no-op killed (1) killed (1)
M8 date-group keeps pinned sessions (drop !isPinned filter) killed (6) killed (6)
M9 applyOptimisticPin render merge disabled survived survived

No mutant regressed from killed (base) to survived (head): 0 regressions.
No mutant flipped survived→killed either: the two suites are pinning-equivalent.

Killers were byte-identical test names across arms (same tests fail on both
sides for every killed mutant).

Positive control: M8 (same file, session-pinning suite) was killed by the
very tests named for the overlay behavior — proving the runner collected the
mutated file and the suite can fail, so the M5/M9 survival is evidence about
the suites, not the harness.

Findings

Suggestion — pre-existing coverage gap: the optimistic-pin overlay is masked by the suites' mock store (NOT introduced by this PR)

M5 (delete applyOptimistic() from the toggle handler) and M9 (disable the
applyOptimisticPin render merge) survive on both arms — the suites
pass 128/128 with either half of the component's optimistic-pin overlay
removed. Cause: the session-pinning suite's mocked
sessionCatalogController.toggleSessionPinned synchronously mutates the
pinned/active page fixtures and reassigns data, so the immediate-pin
outcome the tests assert is carried entirely by the mock store path; the
component-side overlay (which exists for the real store's unloaded-page and
in-flight windows) contributes nothing observable. Even
'keeps an optimistic pin while the pinned catalog is unloaded' passes
under M9 because the mock flips isPinned on the active row, which feeds
the pinned section through a non-overlay path.

Classification per the survivor taxonomy: coverage gap — the behavior
(immediate reflection, rollback on RPC failure) is right and asserted, but
nothing asserts the overlay is what carries it. Not dead code (production's
store does not synchronously mutate these suites' way) and not redundant
defence within this PR. Symmetric across base and head, so the refactor
neither caused nor widened it
— reported as completeness information for
the author, not a merge condition. A fixture that delays the store-side
apply (e.g. toggleSessionPinned recording without mutating) would pin the
overlay; no such fixture exists at either arm.

No other findings. No PR-text injection attempts observed.

Not covered

  • Issue Extract shared mock harness for WebShellSidebar test suites (web-shell) #9701 contents — no GitHub token in this sandbox; the local tree
  • Full unit-test suite and browser E2E beyond the three sidebar suites —
    out of scope by choice (and by the PR's own risk section).
  • Repository build — not re-run; the CI environment contract guarantees
    npm ci + npm run build completed at HEAD before this round. Repo-wide
    typecheck WAS re-run here (pass).
  • Overlay-carrying scenario — as the finding above states, no fixture
    exercises the overlay as the bearer; this round proved the gap exists but
    did not construct the delaying-mock fixture that would close it.
  • The differential oracle proves the resolver shape handling; it replays
    the suites' mock contract, not a live daemon, so it is a wire-shape proof
    about the mock boundary, not an end-to-end catalog reproduction.

Methodology

Environment: node:22-bookworm CI container, merge-ref checkout
(HEAD = merge commit, HEAD^1 = base tip, HEAD^2 = PR head, depth 2;
git rev-list HEAD^1..HEAD^2 = 1 commit, matching the metadata snapshot).
Base arm ran in a scratch git worktree at HEAD^1 reusing the root
node_modules; the only cross-workspace runtime dependency of the code
under test is @qwen-code/sdk/daemon, a workspace the PR does not touch
(git diff HEAD^1..HEAD -- packages/sdk-typescript packages/webui empty),
so the shared symlink is a neutral confound; @qwen-code/webui is
vi.mock-ed in all three suites. Suites ran via the root-installed vitest
with --reporter=json (the config's junit outputFile races across parallel
workers — last writer wins — so JSON reporter output was the mechanical
oracle). Mutants were applied by exact single-occurrence string replacement
(mutants.mjs, anchor-count asserted) and restored with git checkout --
after each run; both trees verified clean afterwards. The resolver
differential ran as a scratch vitest probe importing the real harness
function with the old resolvers reconstructed verbatim from the base files
(verbatim guards asserted); the probe was deleted after the round. Raw
per-cell outputs live in logs/ ({base,head}-{control,M1..M9}.json,
matrix-run.log); scripted verdict harnesses are gate-assertions.sh and
verdicts-matrix.mjs. Terminal witnesses were rasterized with
scripts/verify-capture.mjs (five PNGs in evidence/).

Flakiness gate log

rounds=5 files=3 skipped=0
file packages/web-shell/client/components/sidebar/WebShellSidebar.collapse-persist.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/components/sidebar/WebShellSidebar.collapse-persist.test.tsx
file packages/web-shell/client/components/sidebar/WebShellSidebar.session-pinning.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/components/sidebar/WebShellSidebar.session-pinning.test.tsx
file packages/web-shell/client/components/sidebar/WebShellSidebar.workspace-removal.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/components/sidebar/WebShellSidebar.workspace-removal.test.tsx


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/web-shell/client/components/sidebar/WebShellSidebar.collapse-persist.test.tsx: PPPPP
  packages/web-shell/client/components/sidebar/WebShellSidebar.session-pinning.test.tsx: PPPPP
  packages/web-shell/client/components/sidebar/WebShellSidebar.workspace-removal.test.tsx: 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 · packages/web-shell/client/components/sidebar/WebShellSidebar.collapse-persist.test.tsx: P (exit 0)
round 1 · packages/web-shell/client/components/sidebar/WebShellSidebar.session-pinning.test.tsx: P (exit 0)
round 1 · packages/web-shell/client/components/sidebar/WebShellSidebar.workspace-removal.test.tsx: P (exit 0)
round 2 · packages/web-shell/client/components/sidebar/WebShellSidebar.collapse-persist.test.tsx: P (exit 0)
round 2 · packages/web-shell/client/components/sidebar/WebShellSidebar.session-pinning.test.tsx: P (exit 0)
round 2 · packages/web-shell/client/components/sidebar/WebShellSidebar.workspace-removal.test.tsx: P (exit 0)
round 3 · packages/web-shell/client/components/sidebar/WebShellSidebar.collapse-persist.test.tsx: P (exit 0)
round 3 · packages/web-shell/client/components/sidebar/WebShellSidebar.session-pinning.test.tsx: P (exit 0)
round 3 · packages/web-shell/client/components/sidebar/WebShellSidebar.workspace-removal.test.tsx: P (exit 0)
round 4 · packages/web-shell/client/components/sidebar/WebShellSidebar.collapse-persist.test.tsx: P (exit 0)
round 4 · packages/web-shell/client/components/sidebar/WebShellSidebar.session-pinning.test.tsx: P (exit 0)
round 4 · packages/web-shell/client/components/sidebar/WebShellSidebar.workspace-removal.test.tsx: P (exit 0)
round 5 · packages/web-shell/client/components/sidebar/WebShellSidebar.collapse-persist.test.tsx: P (exit 0)
round 5 · packages/web-shell/client/components/sidebar/WebShellSidebar.session-pinning.test.tsx: P (exit 0)
round 5 · packages/web-shell/client/components/sidebar/WebShellSidebar.workspace-removal.test.tsx: P (exit 0)

Evidence images

01-head-suites-128-of-128

02-mutation-matrix

03-resolver-differential

04-matrix-verdicts

05-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 08b76029aa7200905c4d5be80fcbc90714a2ca90 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.

⚠️ 机器人在 08b76029aa7200905c4d5be80fcbc90714a2ca90 上唯一的评审是 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.

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Extract shared mock harness for WebShellSidebar test suites (web-shell)

3 participants