Skip to content

perf(cli): import core modules directly in another 114 files - #10956

Closed
yiliang114 wants to merge 1 commit into
perf/core-subpath-imports-batch1from
perf/core-subpath-imports-batch2
Closed

perf(cli): import core modules directly in another 114 files#10956
yiliang114 wants to merge 1 commit into
perf/core-subpath-imports-batch1from
perf/core-subpath-imports-batch2

Conversation

@yiliang114

Copy link
Copy Markdown
Collaborator

Stacked on #10946, which is stacked on #10917. Each will retarget as the one below it lands.

What this PR does

The same mechanical change as #10946, over 114 more files: they import the specific core modules they use instead of the package root. Only import statements change.

Why these files were not in the first batch

They should have been. The analysis that produced the first batch decided whether a test replaces the core package by looking for the text of such a call anywhere in the file — including inside comments. One test only mentions the pattern in a doc comment, explaining why it deliberately avoids the heavy mock its neighbour uses, and counting it as a blocker ruled out 217 modules that nothing actually blocks.

Ignoring comments when detecting the call changes the picture substantially:

before after
movable without touching any test 141 260
needing a mock moved first 310 191

The first batch took 130 of those; this takes 114 more. What remains genuinely does need its mocks moved in the same change.

Reviewer Test Plan

How to verify

Every generated specifier is checked against the exports its named module actually has, following that module's own re-exports — 865 symbol/module pairs here, none wrong. That check exists because the first batch shipped one bad pair: a symbol map built from re-export chains had named the module that re-exports a symbol rather than the one declaring it, and the build caught it.

Outside import statements every line is byte-identical to its parent, which is worth confirming independently since it is the property that makes this reviewable at this size.

Evidence (Before & After)

N/A — no user-visible behavior changes.

Tested on

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

Not run locally; relying on CI.

Risk & Scope

  • Main risk or tradeoff: as before, a module could carry an initialization side effect that previously ran because the package root was evaluated. Nothing found, and it would surface as a test failure rather than as wrong behavior.
  • Not validated / out of scope: the 191 files that still need their mocks moved alongside the code.
  • Breaking changes / migration notes: none.

Linked Issues

Refs #10908

中文说明

本 PR 基于 #10946,后者基于 #10917。下层合并后会逐级自动重定向。

这个 PR 做了什么

#10946 相同的机械改动,覆盖另外 114 个文件:把「导入 core 包根」改为「导入实际用到的具体模块」。只有 import 语句变化。

为什么这些文件不在第一批里

它们本就应该在。产生第一批的分析在判断「某测试是否整体替换了 core 包」时,是在整个文件里搜索该调用的文本——包括注释内部。有一个测试仅在文档注释里提到这个写法(说明它为何刻意避开邻居那个笨重的 mock),把它当成阻塞点,就排除掉了 217 个实际上没有任何阻塞的模块。

检测该调用时忽略注释后,结论变化很大:

修正前 修正后
无需改动任何测试即可迁移 141 260
需先搬迁 mock 310 191

第一批取了其中 130 个,本批再取 114 个。剩下的确实需要在同一次改动里搬迁 mock。

审查者验证计划

每条生成的说明符都对照其目标模块的真实导出集合做了校验(跟随该模块自身的 re-export),本批 865 个符号/模块组合,无一错误。这道校验的由来是第一批漏过了一个错误组合:基于 re-export 链构建的符号表把某符号指向了再导出它的模块,而非声明它的模块,被构建捕获。

import 语句之外的每一行与父提交逐字节相同——在这个体量下,这条性质是可审查性的基础,值得独立确认。

风险与范围

  • 主要风险或权衡:同上,某个模块可能带有「原先随包根求值而发生」的初始化副作用。未发现此类情况,且它会表现为测试失败而非生产行为错误。
  • 未验证 / 超出范围:仍需连同 mock 一起搬迁的 191 个文件。
  • 破坏性变更 / 迁移说明:无。

The same mechanical change as the previous commit, over the files a corrected
reading of the test suite showed were always safe to move.

The earlier pass classified a test as replacing the core package if the text
of such a call appeared anywhere in it, including inside a comment. One file
only mentions the pattern in a doc comment explaining why it deliberately
avoids it, and being counted as a blocker there ruled out 217 modules that
nothing actually blocks. Ignoring comments when detecting the call raises the
number of files movable without touching a single test from 141 to 260.

Every generated specifier is checked against the exports its named module
really has, following that module's own re-exports — 865 pairs here, none
wrong. Outside import statements every line is byte-identical.
@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 3, 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

Copy link
Copy Markdown
Collaborator

Thanks for the PR! Stacked on #10946#10917, base perf/core-subpath-imports-batch1, so the duplicate/already-fixed check does not apply here (non-default base) and this is reviewed against that base.

Template — all required sections present. One deviation worth naming rather than blocking on: the body carries ## Why these files were not in the first batch where the template asks for ## Why it's needed. The motivation the heading exists to capture is present and linked to #10908, so I did not treat this as a template failure.

Problem — observed and measured, not theoretical. #10908 carries hard numbers: the cli workspace spent 2223s in collect against 1372s in tests; the core barrel re-exports a closure of ~612 modules; 871/1242 cli source files import it; rewriting a file's import to the defining module dropped it from ~11.5s to ~2.1s across 15 measured files; an empty test file costs 1.97s against 12.34s for one that imports only the barrel. This PR is Phase ① of that plan.

Direction — aligned, and it is a maintainer-filed plan rather than a drive-by. One genuine question: #10908 scopes Phase ① as "migrate barrel imports to subpaths + lint rule", and credits the lint rule with keeping the win from decaying as test files grow (~110/week). Neither this PR nor #10917/#10946 adds a rule. Presumably it cannot land until the remaining 191 files are done — worth saying where it lands so the migration does not stop regressing the moment it finishes.

Size — core paths are touched under the gate's patterns: packages/cli/src/config/** (6 files) and packages/cli/src/services/** (5 files). All 114 changed files are production code — no *.test.ts, __tests__/**, or generated/schema files — so the breakdown is 710 production lines (356 additions / 354 deletions), 0 test lines, 0 generated. The title type is perf, not refactor, so the large-core-refactor hard block does not apply; the author also holds admin on this repo, and maintainer-authored PRs are exempt from the two-tier core gate. 710 is under the 1000-line advisory. Breadth is not size here: a uniform import-path sweep averaging ~6 lines per file.

Approach — minimal and honest about it. I checked the claim mechanically rather than taking it on trust: all 710 changed lines are import/export-from statement lines, there is exactly one hunk per file (114 hunks, 114 files), and the deepest hunk starts at old-side line 51 with an import as its context heading. Nothing outside the import region moves. The stack itself is the scope split, which is the right shape for a 451-file migration.

Risk — Stage 1e matched: 14 files under packages/cli/src/acp-integration/, which is on the revert-correlated path list. That said, an import-only change cannot alter runtime behaviour if the imports resolve, and tui-parity already exercises the TUI surface on this PR automatically, so I did not treat the match as a reason to deepen the review beyond verifying resolution — which is what Stage 2 does.

Moving on to code review. 🔍

中文说明

感谢贡献!本 PR 基于 #10946#10917,base 为 perf/core-subpath-imports-batch1,因此「重复/已修复」检查不适用(非默认分支),审查以该 base 为基准。

模板 —— 必需章节齐全。有一处偏差,点出来但不作为阻塞:正文用的是 ## Why these files were not in the first batch,而模板要求的是 ## Why it's needed。该标题本应承载的动机信息实际存在,并链接到 #10908,因此未按模板缺失处理。

问题 —— 已观测且有实测数据,不是理论性问题。#10908 给出了硬数字:cli workspace 的 collect 耗时 2223s,而 tests 仅 1372s;core barrel 再导出约 612 个模块的闭包;1242 个 cli 源文件中有 871 个这样导入;把 15 个文件的导入改为定义模块后,单文件从约 11.5s 降到约 2.1s;空测试文件耗时 1.97s,而只导入 barrel 的文件耗时 12.34s。本 PR 是该计划的阶段 ①。

方向 —— 对齐,且这是维护者自己提出的计划,不是顺手改动。有一个真实的问题:#10908 把阶段 ① 定义为「迁移 barrel 导入到子路径 + lint 规则」,并认为 lint 规则是防止收益随测试文件增长(约每周 110 个)而衰减的关键。本 PR 及 #10917/#10946 都没有加入该规则。推测是要等剩下 191 个文件完成后才能落地——建议说明它将落在哪里,否则迁移一结束就会开始回退。

规模 —— 按本仓库 gate 的匹配规则确实触及核心路径:packages/cli/src/config/**(6 个文件)与 packages/cli/src/services/**(5 个文件)。114 个改动文件全部是生产代码——没有 *.test.ts__tests__/**、也没有生成/schema 文件——因此明细为 710 行生产代码(356 增 / 354 删),测试 0 行,生成 0 行。标题类型是 perf 而非 refactor,因此大规模核心重构的硬性拦截不适用;作者在本仓库拥有 admin 权限,而维护者自己提交的 PR 免于两层核心 gate。710 行低于 1000 行的大 PR 提示线。这里的「广度」不等于「规模」:这是一次统一的导入路径迁移,平均每文件约 6 行。

方案 —— 改动最小,且描述诚实。我没有直接采信,而是机械校验了这条声明:710 行改动全部是 import/export-from 语句行;每个文件恰好一个 hunk(114 个 hunk、114 个文件);最深的 hunk 起始于旧文件第 51 行,其上下文标题行本身就是一条 import。导入区域之外没有任何改动。这个 stack 本身就是范围拆分,对于 451 个文件的迁移来说是正确的形态。

风险 —— Stage 1e 命中:packages/cli/src/acp-integration/ 下 14 个文件,该路径在「与 revert 相关」的清单里。不过,只要导入能正确解析,纯导入改动不会改变运行时行为,而且本 PR 上 tui-parity 已经自动覆盖 TUI 面,因此我没有因为命中而额外加深审查,而是把重点放在验证解析上——这正是 Stage 2 做的事。

进入代码审查 🔍

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

Reviewed at 2e7b7eb2a1ba9062593beddedc8c0c17cfde9dd2 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code review

I reviewed this statically at 2e7b7eb2a1ba9062593beddedc8c0c17cfde9dd2 and executed nothing from the PR. The mechanical claims hold up; the batch-selection claim does not.

What checks out

The two properties that make a 114-file diff reviewable are both true, and I verified them rather than taking them on trust:

  • Nothing outside imports moves. All 710 changed lines are import / export … from statement lines. There are exactly 114 hunks for 114 files — one each — and the deepest hunk starts at old-side line 51. The four deepest hunks (lines 41, 48, 50, 51) each carry an import statement as their context heading, so even those sit inside the import block.
  • Every specifier resolves. All 107 distinct @qwen-code/qwen-code-core/… subpaths map to a real file under packages/core/src/ (.ts, .tsx, or index.ts). Zero missing.
  • Every symbol is where the PR says it is. Extracting pairs from the new side of each hunk — which matters, because retargeted multi-line imports keep their brace list as context lines and a naive read of only the + lines misses 60 of them — gives 235 distinct symbol/module pairs. All 235 are exported directly by the exact module named; none required following a re-export chain. That is precisely the defect that reached the first batch, and it is clean here. (My count is 235 pairs over 107 modules rather than the 865 in the description — a different counting unit, not a disagreement about correctness.)
  • Nothing was dropped. Zero symbols leave a core-root import without reappearing on a subpath.
  • Resolution works on every surface that matters. tsc and the esbuild bundle both read "@qwen-code/qwen-code-core/*": ["../core/src/*"] in packages/cli/tsconfig.json (with moduleResolution: nodenext handling the .js.ts step), and vitest reads the RegExp alias perf(cli): let tests resolve core modules individually #10917 adds on the base branch. Worth recording for later: packages/core/package.json exports has no ./* wildcard and lists none of these 107 subpaths, so these specifiers are valid only inside the monorepo build — never for an external consumer of the published core package. That is not a defect in this PR, since scripts/prepare-package.js ships a single bundle (main: 'cli.js') and Node never resolves them at runtime, but it is the constraint that keeps this pattern from spreading into packages/sdk-typescript or packages/acp-bridge, which do resolve core through exports.
  • The initialization-side-effect worry stays moot for now. 882 cli source files still import the barrel on main, so the root module is still evaluated at runtime and this PR converts only 114 of them. That risk arrives with the last batch, not this one — worth re-checking then.

Blocker: five files break their colocated unit tests

The description's core claim is that these 114 files are movable "without touching any test", and that what remains "genuinely does need its mocks moved in the same change". Eight of the 114 have a colocated test that calls vi.mock('@qwen-code/qwen-code-core', …) on the root package. For five of them, the test overrides exactly the symbols this PR moves to a subpath:

Source file Colocated test Symbols moved off the root What the test overrides
serve/local-control/service.ts service.test.ts:21 sleepInhibitorservices/sleepInhibitor.js a full replacement mock, { sleepInhibitor: sleepInhibitorMock }; the test drives isRunning at lines 44 and 156
serve/routes/workspace-github-prs.ts workspace-github-prs.test.ts:25 fetchGitHubPullRequests, createGitHubPullRequest, getDefaultBranchutils/github-prs.js all three, asserted through vi.mocked(…)
serve/routes/workspace-git-diff.ts workspace-git-diff.test.ts:27 fetchGitDiff, fetchGitDiffHunksForFileutils/gitDiff.js both
serve/routes/workspace-git-log.ts workspace-git-log.test.ts:23 fetchGitLog, fetchGitCommitDetail (plus MAX_LOG_LIMIT, DEFAULT_LOG_LIMIT) → utils/gitDiff.js both fetchers
ui/commands/curator-command.ts curator-command.test.ts:20 getAutoSkillCuratorStatus, runAutoSkillCurator, restoreArchivedAutoSkill, setAutoSkillPinnedskills/skill-curator.js all four, with expect(mocks.getStatus).toHaveBeenCalledWith('/project') and friends

The mechanism is the base branch's own alias table. packages/cli/vitest.config.ts maps the root to packages/core/index.ts and /^@qwen-code\/qwen-code-core\/(.*)$/ to packages/core/src/$1 — two distinct module ids. vi.mock is keyed on the resolved id, so a mock registered against the barrel does not intercept a subpath import. After this change the module under test calls the real implementation while the test asserts on its mock: toHaveBeenCalledWith fails, and in the curator case the real status/run/restore functions execute against whatever the test environment provides.

The other three look genuinely benign, for different reasons: serve/live/live-task-service.ts and ui/hooks/useAutoAcceptIndicator.ts only move symbols their tests import type-only (SessionService, Config), and ui/commands/goalCommand.ts moves emptyGoalSnapshot / GoalPersistenceUnavailableError, which its test does not override.

I confirmed this against the reviewed commit rather than inferring it: none of #10917, #10946 or #10956 touches any .test.ts file, so every test file at head is byte-identical to main, which is what I read.

The fix is small and mechanical — either drop these five files into the batch that moves mocks, or move the five mock calls onto the subpaths in the same PR. What I would not do is land it as-is and let the retarget find it, for the reason in the next section: no gate on this PR can see this defect.

Test evidence

This is an unattended CI run, so the evidence below is the PR's own check results read through the API. I built nothing, ran no test, and executed no PR-derived code.

The important part is what is absent. Qwen Code CI — the lane that runs lint, static analysis and the unit suite — has a branch-filtered pull_request trigger (main, release/**). This PR's base is perf/core-subpath-imports-batch1, so that workflow never starts. Qwen Live Host CI is green but is scoped to packages/live-host: every step carries working-directory: packages/live-host, and it triggered only because this PR touches serve/live/live-task-service.ts, which matches its path filter. It compiles nothing in packages/cli.

Check Conclusion
Qwen Code CI (lint / static analysis / unit tests) not triggered — base is not main or release/**
tui-parity → TUI parity snapshots (ink vs opentui) in progress — runs npm ci + repo npm run build
tui-parity → OpenTUI no-flicker gate in progress
Qwen Live Host CI → Live Host (macos-latest) success — scoped to packages/live-host only
10 bot-orchestration checks (triage, review, verify, tmux, label, assign, …) skipped / in progress — not code gates

Read that table against the blocker above and the gap is the finding: the only gate on this PR that compiles packages/cli is tui-parity's npm run build, and a mock that stops intercepting is not a type error. The imports all typecheck — I verified all 235 pairs resolve — so the build will go green with all five regressions intact. They stay invisible until this PR retargets to main and the unit suite runs for the first time. The description's "Not run locally; relying on CI" is worth revisiting, because on this base the CI it is relying on does not include the unit suite.

Not verified, with reasons:

  • cli unit suite — not verified: the workflow does not trigger on a non-main base. This is the gap that hides the five regressions.
  • npm run lint and npm run typecheck — not verified: same trigger filter. npm run build in tui-parity is compilation only and was still in progress at review time.
  • The per-file test-time win for this batch — not verified: the description reports N/A for before/after and no measurement for these 114 files. The underlying numbers in CI test time is bound by module import cost, not scheduling #10908 were measured on a 15-file sample from the first batch, so the direction is well-supported; the magnitude for this batch is not.

Sandboxed verification would settle the one thing static review cannot: @qwen-code /verify — whether those five colocated tests actually fail once their source imports move to subpaths. No unit-test gate runs on this base, and a green npm run build cannot detect a mock that stopped intercepting, so this is the specific claim currently resting on my reading of the alias table alone. The author has write access, so /verify can be triggered directly; /tmux would add nothing here, since an import-only change with all 235 specifiers verified has no TUI surface to settle and tui-parity already snapshots it.

中文说明

代码审查

我在 2e7b7eb2a1ba9062593beddedc8c0c17cfde9dd2 上做了静态审查,没有执行 PR 中的任何代码。机械性的声明都成立;批次筛选的声明不成立。

通过的部分

让 114 个文件的 diff 可审查的两条性质都是真的,我是校验过的,不是采信的:

  • 导入之外没有任何改动。 710 行改动全部是 import / export … from 语句行。114 个文件恰好 114 个 hunk(每文件一个),最深的 hunk 起始于旧文件第 51 行。最深的四个 hunk(第 41、48、50、51 行)其上下文标题行本身就是一条 import,因此连这些也在导入块内部。
  • 每个说明符都能解析。 107 个不同的 @qwen-code/qwen-code-core/… 子路径全部对应 packages/core/src/ 下真实存在的文件(.ts.tsxindex.ts),无一缺失。
  • 每个符号都在 PR 所说的位置。 从每个 hunk 的新侧提取符号对很关键——被重定向的多行 import,其花括号列表是上下文行,只读 + 行会漏掉其中 60 个——得到 235 个不同的符号/模块组合。全部 235 个都由所指名模块直接导出,无一需要沿再导出链追踪。这正是第一批出现过的问题类型,本批是干净的。(我统计到的是 107 个模块上的 235 个组合,而非描述中的 865 个——计数单位不同,不是对正确性的分歧。)
  • 没有符号被丢弃。 没有任何符号离开 core 根导入后未在某个子路径上重新出现。
  • 所有关键面都能解析。 tsc 和 esbuild 打包都读取 packages/cli/tsconfig.json 里的 "@qwen-code/qwen-code-core/*": ["../core/src/*"](由 moduleResolution: nodenext 完成 .js.ts),vitest 则读取 perf(cli): let tests resolve core modules individually #10917 在 base 分支加入的正则 alias。有一点值得记录:packages/core/package.jsonexports 没有 ./* 通配,也没有列出这 107 个子路径中的任何一个,因此这些说明符只在 monorepo 构建内部有效——对已发布 core 包的外部消费者永远无效。这对本 PR 不构成缺陷,因为 scripts/prepare-package.js 只发布单个 bundle(main: 'cli.js'),Node 运行时根本不会解析它们;但这条约束意味着该模式不能扩散到 packages/sdk-typescriptpackages/acp-bridge,那两个包确实通过 exports 解析 core。
  • 初始化副作用的担忧目前仍不成立。 main 上仍有 882 个 cli 源文件导入 barrel,因此根模块在运行时依然被求值,本 PR 只转换了其中 114 个。这个风险会在最后一批到来,而不是这一批——届时值得重新检查。

阻塞项:五个文件会破坏其同目录单元测试

描述的核心主张是这 114 个文件「无需改动任何测试」即可迁移,而剩下的「确实需要在同一次改动里搬迁 mock」。114 个文件中有 8 个,其同目录测试对包调用了 vi.mock('@qwen-code/qwen-code-core', …)。其中 5 个,测试所覆盖的恰好就是本 PR 移到子路径的那些符号:

源文件 同目录测试 从根移走的符号 测试覆盖的内容
serve/local-control/service.ts service.test.ts:21 sleepInhibitorservices/sleepInhibitor.js 完全替换式 mock,{ sleepInhibitor: sleepInhibitorMock };测试在第 44、156 行驱动 isRunning
serve/routes/workspace-github-prs.ts workspace-github-prs.test.ts:25 fetchGitHubPullRequestscreateGitHubPullRequestgetDefaultBranchutils/github-prs.js 三者全部,通过 vi.mocked(…) 断言
serve/routes/workspace-git-diff.ts workspace-git-diff.test.ts:27 fetchGitDifffetchGitDiffHunksForFileutils/gitDiff.js 两者
serve/routes/workspace-git-log.ts workspace-git-log.test.ts:23 fetchGitLogfetchGitCommitDetail(以及 MAX_LOG_LIMITDEFAULT_LOG_LIMIT)→ utils/gitDiff.js 两个 fetcher
ui/commands/curator-command.ts curator-command.test.ts:20 getAutoSkillCuratorStatusrunAutoSkillCuratorrestoreArchivedAutoSkillsetAutoSkillPinnedskills/skill-curator.js 四者全部,含 expect(mocks.getStatus).toHaveBeenCalledWith('/project')

机制就来自 base 分支自己的 alias 表。packages/cli/vitest.config.ts 把根映射到 packages/core/index.ts,把 /^@qwen-code\/qwen-code-core\/(.*)$/ 映射到 packages/core/src/$1——两个不同的模块 id。vi.mock 以解析后的 id 为键,因此注册在 barrel 上的 mock 不会拦截子路径导入。改动之后,被测模块调用的是真实实现,而测试断言的是它的 mock:toHaveBeenCalledWith 失败;在 curator 这一例中,真实的 status/run/restore 函数会针对测试环境实际执行。

另外三个看起来确实无害,原因各不相同:serve/live/live-task-service.tsui/hooks/useAutoAcceptIndicator.ts 只移动了其测试仅作为类型导入的符号(SessionServiceConfig);ui/commands/goalCommand.ts 移动的是 emptyGoalSnapshot / GoalPersistenceUnavailableError,其测试并未覆盖这两个。

我是针对被审查的 commit 确认的,不是推断的:#10917#10946#10956 都没有改动任何 .test.ts 文件,因此 head 上的每个测试文件与 main 逐字节相同,也就是我实际读到的内容。

修复很小且机械——要么把这五个文件挪到「搬迁 mock」的那一批,要么在同一个 PR 里把这五处 mock 调用改到子路径上。我不建议按现状合并、让重定向去发现问题,原因见下一节:本 PR 上没有任何 gate 能看见这个缺陷。

测试证据

这是无人值守的 CI 运行,因此下面的证据是通过 API 读取的 PR 自身检查结果。我没有构建、没有跑测试、没有执行任何 PR 派生代码。

关键在于缺失的部分。Qwen Code CI——运行 lint、静态分析和单元测试的那条流水线——其 pull_request 触发器带分支过滤(mainrelease/**)。本 PR 的 base 是 perf/core-subpath-imports-batch1,因此该 workflow 根本不会启动。Qwen Live Host CI 是绿的,但作用域限于 packages/live-host:每个步骤都带 working-directory: packages/live-host,它被触发只是因为本 PR 改动了 serve/live/live-task-service.ts,命中了它的 path filter。它不编译 packages/cli 的任何内容。

把上表和阻塞项对照着看,缺口就是结论本身:本 PR 上唯一会编译 packages/cli 的 gate 是 tui-paritynpm run build,而「mock 不再拦截」不是类型错误。所有导入都能通过类型检查——我验证了全部 235 个组合——因此构建会变绿,而五处回归原封不动。它们会一直不可见,直到本 PR 重定向到 main、单元测试第一次真正运行。描述里的「未在本地运行;依赖 CI」值得重新考虑,因为在这个 base 上,它所依赖的 CI 并不包含单元测试。

未验证项及原因:

  • cli 单元测试套件 —— 未验证:该 workflow 不会在非 main 的 base 上触发。这正是隐藏五处回归的缺口。
  • npm run lintnpm run typecheck —— 未验证:同样的触发过滤。tui-parity 里的 npm run build 只做编译,且审查时仍在进行中。
  • 本批次的单文件测试耗时收益 —— 未验证:描述中 before/after 记为 N/A,这 114 个文件没有实测数据。CI test time is bound by module import cost, not scheduling #10908 里的基础数字是在第一批的 15 个文件样本上测得的,因此方向有充分支撑;本批次的幅度没有。

沙箱验证可以确认静态审查无法确认的那一件事:@qwen-code /verify —— 当这五个文件的源码导入改到子路径后,其同目录测试是否真的失败。本 base 上没有单元测试 gate,而绿色的 npm run build 无法检测「mock 不再拦截」,因此这是目前唯一仅靠我阅读 alias 表得出的判断。作者有写权限,可以直接触发 /verify/tmux 在这里没有增量价值——纯导入改动、235 个说明符全部验证通过,没有需要确认的 TUI 面,而且 tui-parity 已经在做快照。

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

Reviewed at 2e7b7eb2a1ba9062593beddedc8c0c17cfde9dd2 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 2/5 — the migration itself is verifiably clean and I could not break it, but five files silently un-mock their own tests and no gate on this base is capable of seeing that.

Stepping back from the two stages above.

Read from the title and #10908 alone, before I looked at the diff, the proposal I would have written is close to what this is: a generated codemod over the barrel imports, batched, with the batch boundary drawn by which tests interfere. The one place I would have drawn the boundary differently turns out to be the place this PR is wrong, which is worth stating plainly because it is the whole finding. I would have asked "does any test that reaches this file override a symbol this file imports from core?" — an intersection question. The description asks "does a test replace the core package?" — a presence question. Those two come apart exactly when a test mocks the root partially: it spreads importOriginal and overrides four or five names, which reads as "replaces the package" to one question and as "blocks nothing" to a predicate that only asks whether the call is there. All five confirmed breakages are that shape. Widening the batch from 130 to 114 more files by relaxing the predicate (ignoring comments) is what let them through, and relaxing a presence predicate does not make it an intersection predicate.

What I want to be clear about, because a 2/5 can read as "the whole thing is bad" and it isn't: the generator's export verification is genuinely good. I rebuilt the pair set independently — from the new side of every hunk, so that retargeted multi-line imports whose brace lists survive as context lines are included, which is 60 pairs a naive +-only read misses — and all 235 symbols are exported directly by the exact module named, with zero re-export chasing. That is the defect class that reached the first batch and it is fixed here. Nothing outside an import statement moves, nothing is dropped, and every specifier resolves. Whoever built this did the hard part correctly.

The part I cannot get past is that the failure is invisible on this base. A mock that stops intercepting is not a type error, so tui-parity's npm run build goes green with all five regressions intact, and the unit suite does not run at all because Qwen Code CI filters on main / release/**. "Relying on CI" describes a safety net that is not there for this class of defect. That is why this is a request-changes rather than a defer: I am not uncertain, I have the five files, the mock lines, the moved symbols, and the alias table that makes root and subpath distinct module ids. Deferring would hand a maintainer a question I have already answered.

Two follow-ups that are not blockers on this PR but would be a shame to lose:

  • perf(cli): import core modules directly where no test mocks the package #10946 has the same blind spot. Running the same intersection over batch 1's 130 files turns up 10 whose colocated tests mock the core root (config/lsp-config-watcher, nonInteractive/control/controllers/systemController, startup/startup-prefetch, ui/auth/AuthDialog, ui/components/background-view/workflow-save-overlay, ui/components/extensions/tabs/DiscoverTab, ui/components/extensions/tabs/SourcesTab, ui/components/StandaloneSessionPicker, ui/hooks/useResumeCommand, ui/opentui/live-session). That is a candidate list, not a confirmed defect list — batch 2 had 8 candidates and 5 broke — but batch 1 also runs no unit tests on its own base, so it is worth the same pass before it retargets.
  • The lint rule is the part that makes this stick. CI test time is bound by module import cost, not scheduling #10908 scopes Phase ① as migration plus rule, and its own arithmetic is why: test files are growing ~110/week, so every new barrel import silently buys back the cost this stack removes. Landing 244 migrations without the rule leaves the win decaying from the day it merges.

If I were maintaining this in six months I would thank them for the migration and for the export verification, and I would want the five files and the rule. For reference on the guardrails, neither fired: this is a same-repo branch (GUARD = ok, not a fork refactor), and the core-module gate is exempt because the author holds admin — so the 2/5 is my read of the diff, not a policy cap. One pull_request-event workflow was still in flight at review time (tui-parity); no approval is posted and no deferred-approval marker is emitted, so nothing here lands automatically when it finishes.

Requesting changes on the five files above. Re-running the intersection check against the remaining 191 before batch 3 would be cheaper than finding them one retarget at a time.

中文说明

Confidence: 2/5 —— 迁移本身经得起校验,我没能把它证伪;但有五个文件悄悄地让自己的测试失去 mock,而这个 base 上没有任何 gate 能看见这件事。

从上面两个阶段退一步看。

只看标题和 #10908、还没看 diff 时,我会写出的方案与这个 PR 很接近:一个针对 barrel 导入的生成式 codemod,分批推进,批次边界由「哪些测试会干扰」来划。唯一我会划得不一样地方,恰好就是这个 PR 出错的地方,值得直说,因为这就是全部结论。我会问的是「有没有任何能触达这个文件的测试,覆盖了该文件从 core 导入的某个符号?」——这是一个交集问题。而描述问的是「有没有测试替换掉 core 包?」——这是一个存在性问题。当测试只部分 mock 根包时,两者就分道扬镳:它 spread 了 importOriginal,再覆盖四五个名字,这在存在性问题里读作「替换了整个包」,而在只问「这个调用在不在」的判据里读作「不构成阻塞」。已确认的五处破坏全是这个形态。把批次从 130 放宽到再多 114 个文件,靠的是放宽判据(忽略注释),而正是这一步放了它们进来——放宽一个存在性判据,并不会把它变成交集判据。

有一点我要讲清楚,因为 2/5 容易被读成「整个东西都不行」,而事实并非如此:生成器的导出校验是真的做得好。我独立重建了符号对集合——从每个 hunk 的新侧提取,这样那些被重定向的多行 import(其花括号列表以上下文行形式保留)也会被计入,而只读 + 行会漏掉其中 60 对——全部 235 个符号都由所指名模块直接导出,无一需要沿再导出链追踪。这正是第一批出现过的问题类型,在这里已经修好了。导入语句之外没有任何改动,没有符号被丢弃,每个说明符都能解析。做这个工具的人把难的部分做对了。

我过不去的那一点是:这个失败在当前 base 上是不可见的。「mock 不再拦截」不是类型错误,所以 tui-paritynpm run build 会变绿,而五处回归原封不动;单元测试则完全不运行,因为 Qwen Code CImain / release/** 过滤。「依赖 CI」描述的是一张对这类缺陷并不存在的安全网。这也是为什么我给的是 request-changes 而不是 defer:我并不处于不确定状态——我手上有那五个文件、mock 所在行、被移动的符号,以及那张让根与子路径成为两个不同模块 id 的 alias 表。defer 等于把我已经回答完的问题重新丢回给维护者。

两条不属于本 PR 阻塞项、但丢了可惜的后续:

  • perf(cli): import core modules directly where no test mocks the package #10946 有同样的盲点。 对 batch 1 的 130 个文件跑同一次交集检查,找出 10 个其同目录测试 mock 了 core 根包的文件(config/lsp-config-watchernonInteractive/control/controllers/systemControllerstartup/startup-prefetchui/auth/AuthDialogui/components/background-view/workflow-save-overlayui/components/extensions/tabs/DiscoverTabui/components/extensions/tabs/SourcesTabui/components/StandaloneSessionPickerui/hooks/useResumeCommandui/opentui/live-session)。这是候选清单,不是已确认的缺陷清单——batch 2 有 8 个候选、其中 5 个确实坏了——但 batch 1 在自己的 base 上同样不跑单元测试,因此在它重定向之前值得做同一遍检查。
  • lint 规则才是让收益留得住的部分。 CI test time is bound by module import cost, not scheduling #10908 把阶段 ① 定义为迁移规则,它自己给出的算术就是原因:测试文件以每周约 110 个的速度增长,因此每一个新增的 barrel 导入都会悄悄把这个 stack 省下的成本买回去。迁移了 244 处却没有规则,收益从合并那天起就开始衰减。

如果六个月后由我来维护这份代码,我会感谢他们做了这次迁移、也做了导出校验;同时我会想要那五个文件,和那条规则。关于两条 guardrail 供参考:都没有触发——这是同仓库分支(GUARD = ok,不是 fork 上的 refactor),核心模块 gate 也因作者持有 admin 而豁免——所以这个 2/5 是我对 diff 的判断,不是政策性封顶。审查时仍有一条 pull_request 事件的 workflow 在运行(tui-parity);本次不提交任何 approval,也不发出任何延迟批准标记,因此它跑完之后不会有任何东西自动合并。

针对上面五个文件请求修改。在 batch 3 之前,对剩下的 191 个文件重跑一次交集检查,会比一次次靠重定向去发现更便宜。

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

Reviewed at 2e7b7eb2a1ba9062593beddedc8c0c17cfde9dd2 · 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.

Requesting changes on five specific files — full reasoning in the Stage 2 and Stage 3 notes above.

The migration itself is clean and I verified it independently: all 710 changed lines are import statements (one hunk per file, nothing outside the import region), all 107 subpaths resolve to real files under packages/core/src/, and all 235 distinct symbol/module pairs are exported directly by the exact module named — no re-export chasing, which is the defect that reached the first batch. Nothing was dropped.

The problem is the batch-selection predicate. These five files have a colocated test that calls vi.mock('@qwen-code/qwen-code-core', …) on the root package and overrides exactly the symbols this PR moves to a subpath:

  • serve/local-control/service.tssleepInhibitor (full-replacement mock; test drives isRunning)
  • serve/routes/workspace-github-prs.tsfetchGitHubPullRequests, createGitHubPullRequest, getDefaultBranch
  • serve/routes/workspace-git-diff.tsfetchGitDiff, fetchGitDiffHunksForFile
  • serve/routes/workspace-git-log.tsfetchGitLog, fetchGitCommitDetail
  • ui/commands/curator-command.tsgetAutoSkillCuratorStatus, runAutoSkillCurator, restoreArchivedAutoSkill, setAutoSkillPinned

vi.mock is keyed on the resolved module id, and the base branch's own packages/cli/vitest.config.ts maps the root to packages/core/index.ts while /^@qwen-code\/qwen-code-core\/(.*)$/ maps to packages/core/src/$1. Two distinct ids, so the mock no longer intercepts: the module under test calls the real implementation while the test asserts on its mock.

No gate on this base can see it. A mock that stops intercepting is not a type error, so tui-parity's npm run build stays green, and Qwen Code CI (the unit suite) does not trigger because the base is not main or release/**.

Fix is mechanical: move these five into the batch that relocates mocks, or repoint the five vi.mock calls at the subpaths in this PR. Also worth re-running the same intersection over #10946 — 10 of its 130 files are candidates for the same shape.

中文说明

针对五个具体文件请求修改——完整推理见上面的 Stage 2 与 Stage 3 说明。

迁移本身是干净的,我做了独立校验:710 行改动全部是 import 语句(每文件一个 hunk,导入区域之外无任何改动),107 个子路径全部对应 packages/core/src/ 下真实存在的文件,235 个不同的符号/模块组合全部由所指名模块直接导出——无需沿再导出链追踪,而这正是第一批出现过的问题。没有符号被丢弃。

问题出在批次筛选判据上。以下五个文件的同目录测试对包调用了 vi.mock('@qwen-code/qwen-code-core', …),且覆盖的恰好就是本 PR 移到子路径的那些符号:

  • serve/local-control/service.ts —— sleepInhibitor(完全替换式 mock;测试驱动 isRunning
  • serve/routes/workspace-github-prs.ts —— fetchGitHubPullRequestscreateGitHubPullRequestgetDefaultBranch
  • serve/routes/workspace-git-diff.ts —— fetchGitDifffetchGitDiffHunksForFile
  • serve/routes/workspace-git-log.ts —— fetchGitLogfetchGitCommitDetail
  • ui/commands/curator-command.ts —— getAutoSkillCuratorStatusrunAutoSkillCuratorrestoreArchivedAutoSkillsetAutoSkillPinned

vi.mock 以解析后的模块 id 为键,而 base 分支自己的 packages/cli/vitest.config.ts 把根映射到 packages/core/index.ts,把 /^@qwen-code\/qwen-code-core\/(.*)$/ 映射到 packages/core/src/$1。两个不同的 id,因此 mock 不再拦截:被测模块调用真实实现,而测试断言的是它的 mock。

这个 base 上没有任何 gate 能看见它。「mock 不再拦截」不是类型错误,所以 tui-paritynpm run build 依然是绿的;而 Qwen Code CI(单元测试套件)不会触发,因为 base 不是 mainrelease/**

修复是机械的:把这五个文件挪到搬迁 mock 的那一批,或在本 PR 里把这五处 vi.mock 调用改指到子路径。另外建议对 #10946 重跑同一次交集检查——它 130 个文件里有 10 个属于同一形态的候选。

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.

Not reviewed: build-and-test — the 'tmux-testing' CI check was skipped at this commit and its interactive E2E suite did not run locally.

Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.

中文说明

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

未审查(原文为英文):build-and-test — the 'tmux-testing' CI check was skipped at this commit and its interactive E2E suite did not run locally.

未审查:反向审计——在 5 轮的反审轮数上限内未收敛。

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


import { Storage, type Config } from '@qwen-code/qwen-code-core';
import type { Config } from '@qwen-code/qwen-code-core/config/config.js';
import { Storage } from '@qwen-code/qwen-code-core/config/storage.js';

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] R1-1: Converting this file (and six others in this batch) to a subpath import silently breaks the tests that intercept it through package-root mocks. Vitest resolves vi.mock('@qwen-code/qwen-code-core') and the new @qwen-code/qwen-code-core/config/storage.js specifier to different module ids (root alias → core/index.ts, wildcard alias → core/src/), so the root-keyed mock no longer applies here: Storage is now the real implementation while acpAgent.test.ts seeds its fixtures under the fake getGlobalQwenDir(), and the failing run writes into the real user HOME. The same mechanism breaks six more suites — ui/commands/curator-command.test.ts (14/15, real skill-curator functions hit the filesystem), serve/routes/workspace-git-diff.test.ts (8/16), workspace-git-log.test.ts (11/16), workspace-github-prs.test.ts (13/19, spies record zero calls and real git/gh code runs against fake cwds), serve/local-control/service.test.ts (1/5, the real sleepInhibitor spawns systemd-inhibit during the test), services/McpPromptLoader.test.ts (21/47, root-namespace vi.spyOn never intercepts the subpath binding) — and leaves acpAgent.worktree.test.ts green while it silently exercises real core implementations (Storage, createDebugLogger, getMCPServerStatus, sessionIdContext, APPROVAL_MODES) instead of its factory overrides. All seven suites pass at the merge base and fail only on this commit, so this batch's selection criterion ("what remains genuinely needs its mocks moved") misclassified them: their mocks must move in the same change.

Witness:

test-delta (built merge base 0e476635 vs PR): 11 failing files on base, 18 on the PR
-- the 7 files above fail ONLY on the PR (netNew)
npm test --workspace=packages/cli at HEAD: exit 1, 18 failed files / 143 failed tests
targeted reruns of the 7 net-new files: 71 failing tests
curator-command.test.ts: EACCES: permission denied, mkdir '/project' (real fs reached)

Fix: either pull the seven coupled files (this one plus ui/commands/curator-command.ts, serve/routes/workspace-git-diff.ts, workspace-git-log.ts, workspace-github-prs.ts, serve/local-control/service.ts, services/McpPromptLoader.ts) out of this batch and let the change that moves their mocks convert them — the series' own selection rule — or move the mocks in this PR by mocking the exact subpath modules the sources now import, e.g. here:

vi.mock('@qwen-code/qwen-code-core/config/storage.js', () => ({
  // return the SAME Storage fake object the root mock uses
}));

…and …/utils/gitDiff.js, …/utils/github-prs.js, …/skills/skill-curator.js, …/prompts/mcp-prompts.js, …/services/sleepInhibitor.js in the other six suites. The mock specifier must spell the exact subpath with the .js suffix so it resolves to the same module id as the source import under the wildcard alias in packages/cli/vitest.config.ts, and the storage.js factory must reuse the SAME Storage fake object the root mock provides in acpAgent.test.ts — a second divergent fake reproduces the split that broke these tests. The seven failing suites pin this behavior themselves and must go green after the fix; removing the fix (re-migrating any of these files without its mock) turns them red again — e.g. rerun curator-command.test.ts after reverting the mock move and confirm it reds.

中文说明

把本文件(以及本批另外六个文件)转换为子路径导入后,通过包根 mock 拦截它们的测试会被悄悄绕过。Vitest 将 vi.mock('@qwen-code/qwen-code-core') 与新的 @qwen-code/qwen-code-core/config/storage.js 解析为不同的模块 id(根别名 → core/index.ts,通配别名 → core/src/),因此包根级的 mock 在这里不再生效:Storage 变成了真实实现,而 acpAgent.test.ts 仍把测试数据播种在假的 getGlobalQwenDir() 之下,失败的测试运行还会写入真实用户主目录。同一机制还破坏了另外六个套件:curator-command.test.ts(14/15,真实 skill-curator 函数直接操作文件系统)、workspace-git-diff.test.ts(8/16)、workspace-git-log.test.ts(11/16)、workspace-github-prs.test.ts(13/19,spy 记录到 0 次调用,真实 git/gh 代码在假的 cwd 上运行)、local-control/service.test.ts(1/5,真实 sleepInhibitor 在测试中拉起 systemd-inhibit)、McpPromptLoader.test.ts(21/47,对根命名空间的 vi.spyOn 拦不到子路径绑定);此外 acpAgent.worktree.test.ts 虽然仍是绿色,却已悄悄改为运行真实 core 实现(Storage、createDebugLogger、getMCPServerStatus、sessionIdContext、APPROVAL_MODES)而非其工厂覆写。这七个套件在合并基线上全部通过、仅在本提交失败,说明本批的筛选条件(「剩下的确实需要连同 mock 一起搬迁」)把它们误分类了:它们的 mock 必须在同一次改动里一起搬迁。

修复:要么把这七个耦合文件移出本批,交给搬迁 mock 的那次改动去转换(即本系列自己的筛选规则);要么在本 PR 里同时搬迁 mock —— 对源码现在导入的确切子路径模块打 mock(示例见英文部分代码块),其余六个套件分别 mock …/utils/gitDiff.js…/utils/github-prs.js…/skills/skill-curator.js…/prompts/mcp-prompts.js…/services/sleepInhibitor.js。mock 说明符必须写出带 .js 后缀的完整子路径,才能在 packages/cli/vitest.config.ts 的通配别名下解析到与源码导入相同的模块 id;storage.js 的工厂必须复用 acpAgent.test.ts 包根 mock 里的同一个 Storage 假对象——再造一个不同的假对象会重现导致这些测试坏掉的分裂。七个失败套件本身就是验收标准:修复后必须变绿;移除修复(在未搬 mock 的情况下重新迁移这些文件)会再次变红——例如回退 mock 搬迁后重跑 curator-command.test.ts 确认其变红。

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Verified real at head 2e7b7eb: ran the seven suites together (acpAgent, curator-command, workspace-git-diff, workspace-git-log, workspace-github-prs, local-control/service, McpPromptLoader) — 7 files failed, 70 tests failed, matching the reported signatures (spies record 0 calls, real implementations reach fs/git, 200 vs expected 502). This PR changes no test files, so the source import rewrites are the cause.

Not auto-fixing because both remedies exceed three files: (a) revert these seven source files to barrel imports and let the mock-migration change convert them (the series' own selection rule), or (b) move vi.mock in all seven test files to the exact subpath modules, reusing the same Storage fake in acpAgent.test.ts. @yiliang114 which direction? Leaving open until decided.

Comment thread packages/cli/src/acp-integration/active-work-reporter.ts
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Closing in favour of #10957, which carries the whole change and is green.

This branch still holds the pre-revert set. Once the stack was retargeted at main and the unit suite ran for the first time, 16 files and 127 tests failed — every one a stub that stopped intercepting because the code under test named a module instead of the package root. The fix was to restore every migrated module any failing suite reaches, and that revert landed on the top branch, not here.

So the diff on this PR no longer describes anything that should land. Merging it on its own would reintroduce exactly the modules that were shown to break, and because its base is not main, CI on this PR never ran the unit suite and would not have told you — it reports seven or eight passing checks that are only the TUI gates and the bot jobs.

#10957 is based on main, contains the reduced set that passes, and adds the two resolution fixes the integration gate turned out to need. That is the one to review.

中文说明

关闭,改由 #10957 承载全部改动——它已全绿。

本分支仍是回退之前的内容。整个栈改到 main 之后单元测试第一次真正跑起来,挂了 16 个文件、127 个测试,全部是同一形态:被测代码从「包根」改为「具名模块」后,用例装的 stub 不再拦截。修复方式是回退所有被失败用例触达的已迁移模块,而那次回退落在栈顶分支上,不在这里。

因此本 PR 的 diff 已不代表任何应该落地的内容。单独合并它会把已被证实会挂的那批模块重新带进来;而且由于它的 base 不是 main本 PR 的 CI 从未运行单元测试,也不会告诉你这一点——它显示的七八项通过只是 TUI 门禁和机器人任务。

#10957 基于 main,包含收缩后能通过的集合,并补上了集成门禁暴露出的两处解析修复。请审阅那个。

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.

2 participants