Skip to content

perf(cli): import core modules directly where no test mocks the package - #10946

Closed
yiliang114 wants to merge 2 commits into
perf/core-subpath-importsfrom
perf/core-subpath-imports-batch1
Closed

perf(cli): import core modules directly where no test mocks the package#10946
yiliang114 wants to merge 2 commits into
perf/core-subpath-importsfrom
perf/core-subpath-imports-batch1

Conversation

@yiliang114

Copy link
Copy Markdown
Collaborator

Stacked on #10917 — that PR adds the resolver mapping these imports depend on, so this one is based on it and will retarget to main once it lands.

What this PR does

Moves 130 cli files from importing the core package root to importing the specific modules they use. Only import statements change; every other line is byte-identical to its current contents.

Why it's needed

Importing from the package root evaluates core's whole export graph, a bit over six hundred modules, however little of it a file actually uses. The cost shows up as collection rather than assertions: on the release lane the cli workspace reported 2223s collecting against 1372s running tests, and on the main lane it now takes 84 minutes on its own with collection running roughly three times the test time. A file that imports the package root costs about 11.5s before its first assertion; importing one module instead costs about 2s.

Sharding cannot reach this. The three release shards finish within a minute of each other, so the split is already balanced and adding shards only divides a fixed per-file cost — a cost that grows with every test file added, and the suite has grown 87% in ten weeks.

Background and measurements are in #10908.

How these files were chosen

The constraint is not which files import the package root — most of them do — but which ones can move without silently changing what a test exercises. For every test whose module graph reaches a candidate file, this checks how that test treats the core package:

  • A test that replaces the package wholesale with a factory never evaluates the real one. Once the code under test imports a module directly, the mock stops intercepting and the real implementation loads instead. Those files are excluded.
  • A test that spreads the real package and overrides a few names already gives the real implementation for everything else, so it only matters when an overridden name is one the file imports. Those files are excluded too.

What is left is 130 files where no test's treatment of the package changes. The excluded ones need their mocks moved in the same commit as the code, which is a separate change.

Reviewer Test Plan

How to verify

A wrong specifier fails at import time rather than subtly, so a green cli suite is the signal. The mechanical property worth confirming is that nothing outside an import statement moved — comparing each file with its parent outside the import block should come back identical.

The judgement worth a reviewer's eye is the exclusion rule above: whether "a spread mock that does not override the imported name is safe to leave alone" holds for how this codebase writes mocks.

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: a file could import a name whose module has an initialization side effect that previously ran as part of the package root being evaluated. Nothing found while preparing this, but it is the failure mode to watch for, and it would surface as a test failure rather than as wrong behavior in production.
  • Not validated / out of scope: the excluded files, which are the larger half. The bundle is unaffected in shape — the build already resolves these specifiers through the same path mapping — but its size was not measured here.
  • Breaking changes / migration notes: none. The published CLI is a single bundle, so these specifiers do not exist in the shipped artifact.

Linked Issues

Refs #10908

中文说明

本 PR 基于 #10917(那个 PR 提供了这些导入所依赖的解析映射),待其合并后会自动重定向到 main。

这个 PR 做了什么

把 130 个 cli 文件从「导入 core 包根」改为「导入实际用到的具体模块」。只有 import 语句变化,其余每一行与当前内容逐字节相同。

为什么需要

从包根导入会求值 core 的整个导出图(六百多个模块),无论调用方实际只用了多少。代价体现在模块收集而非断言上:release lane 上 cli 收集耗时 2223s、跑测试 1372s;main lane 上 cli 现在单独就要 84 分钟,收集时间约为测试时间的三倍。一个从包根导入的文件在第一条断言前要花约 11.5s,改成按模块导入只要约 2s。

分片解决不了这个问题。三个 release 分片耗时相差不到一分钟,说明切分已经均衡,继续加分片只是在摊薄一个固定的单文件成本——而这个成本随每个新增测试文件线性增长,套件在十周内长了 87%。

背景与测量数据见 #10908

这些文件是怎么挑的

约束不在于「哪些文件导入了包根」(大多数都导入了),而在于「哪些能在不悄悄改变测试所验证内容的前提下迁移」。对每个候选文件,检查所有模块图能到达它的测试如何处理 core 包:

  • 用工厂整体替换该包的测试,从不求值真实包。一旦被测代码直接按模块导入,mock 就不再拦截,真实实现会被加载。这类文件被排除。
  • 展开真实包、只覆盖少数名字的测试,本来就对其余名字给出真实实现,因此只有当被覆盖的名字正是该文件所导入的时才有影响。这类文件同样被排除。

剩下的 130 个文件,没有任何测试对该包的处理方式会因此改变。被排除的那些需要在同一个提交里同步搬迁 mock,属于另一次改动。

风险与范围

  • 主要风险或权衡:某个文件可能导入了一个「其所在模块带初始化副作用、而该副作用原先随包根求值一并发生」的名字。准备本 PR 时未发现此类情况,但这是需要留意的失效模式;它会表现为测试失败,而不是生产行为出错。
  • 未验证 / 超出范围:被排除的那批文件(数量更多的一半)。bundle 形态不受影响——构建本来就通过同一套路径映射解析这些说明符——但其体积变化未在此测量。
  • 破坏性变更 / 迁移说明:无。发布的 CLI 是单一 bundle,这些说明符在产物中并不存在。

Importing from the core package root evaluates its whole export graph — a bit
over six hundred modules — however little of it a file uses. On the release
lane the cli workspace spends more time collecting modules than running tests,
and on the main lane it now takes 84 minutes on its own, most of it collection.

These 130 files ask for named modules instead. They were chosen by checking,
for every test whose module graph reaches them, whether that test replaces the
core package with a mock: a test that swaps the package wholesale stops
intercepting once the code under test imports a module directly, and a test
that spreads the real package and overrides a few names only matters if one of
those names is what the file imports. Files with either kind of coupling are
left for a later change that moves the mocks at the same time.

Only import statements move; every other 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 — and for writing the measurements down in #10908 first, that makes this easy to evaluate.

Template looks good ✓

Problem: observed and measured, not theoretical. #10908 is open with concrete numbers from a real run (cli collecting 2223s against 1372s of assertions; ~11.5s per barrel-importing file vs ~2.1s for a direct module import), and the sharding argument is answered with actual shard timings. This clears the "does the problem exist" bar comfortably.

Direction: aligned, and more so than the PR body claims. The repo already forbids this pattern inside core — eslint-rules/no-core-root-barrel-import.js reports any core production module that imports the root barrel, with the message "must import symbols from their direct owner modules". This PR applies that same discipline to packages/cli, which is the other half of the same argument. CHANGELOG: no direct reference, and I wouldn't expect one — this is CI throughput, not a user-facing surface.

Size: 130 files, 996 production lines (382 added / 614 deleted), 0 test lines, 0 generated/schema lines. It does touch core paths as the gate defines them — packages/cli/src/config/** (2 files) and packages/cli/src/services/** (3 files) — so I ran the two-tier check: the title type is perf, not refactor, so the Tier 1 hard block doesn't apply; and you have admin on this repo, so per AGENTS.md this is a maintainer-authored PR and the external-PR gate is exempt. Worth saying explicitly because 130 files looks like the thing that rule exists to stop — but it's a uniform ~7.7-line sweep, which is the "breadth ≠ size" case, and the five files under core paths change one or two import lines each. 996 also sits just under the 1000-line large-PR advisory, and given the shape I don't think splitting further would help a reviewer.

Approach: the scope feels right, and I checked the minimality claim rather than taking it — see the Stage 2 comment for the detail. Short version: it holds. Every added and removed line in the diff is an import statement, and the set of 248 names removed from the barrel is exactly the set of 248 re-added through subpaths, so nothing was dropped, added, or drive-by refactored along the way. Deferring the mock-coupled half to a follow-up is the right cut, not an arbitrary one.

Two things I'd flag before the code review, neither of them about the transform itself:

  1. No CI lane runs against this PR. The base is perf/core-subpath-imports, and Qwen Code CI (Test, and Lint & Static which carries ESLint + Prettier), Security Checks and serve-ab all filter pull_request to main / release/**. Your stated oracle — "a wrong specifier fails at import time rather than subtly, so a green cli suite is the signal" — is the right oracle, but it has not run and cannot run on this base.
  2. CI test time is bound by module import cost, not scheduling #10908 lists the exports map question as an unverified precondition ("must be settled before the codemod runs, since it decides whether subpaths point at src or dist"). The PR body says the build already resolves these specifiers through the same path mapping; I think that mapping is the tsconfig one, and it does not cover plain Node. Details in Stage 2.

Risk: Stage 1e matched one revert-correlated path — packages/cli/src/serve/sandbox.ts. Not a blocker and the change there is two import lines, but it means I'm not skipping any review depth and I do want CI evidence before this is approved.

Moving on to code review. 🔍

中文说明

感谢贡献——也感谢先在 #10908 里把测量数据写清楚,这让评估变得很容易。

模板完整 ✓

问题: 已观测且有测量,不是理论性加固。#10908 处于 open 状态,带有真实 run 的具体数字(cli 收集 2223s、断言只有 1372s;单个导入 barrel 的文件约 11.5s,改成直接导入模块约 2.1s),并且用真实的分片耗时回答了「加分片行不行」。这一条完全过了「问题是否存在」的门槛。

方向: 对齐,而且比 PR 正文说的更对齐。仓库内部本来就已经禁止这个模式——eslint-rules/no-core-root-barrel-import.js 会报告任何导入 root barrel 的 core 生产模块,提示语是「必须从直接持有该符号的模块导入」。这个 PR 把同一套纪律应用到 packages/cli,是同一个论证的另一半。CHANGELOG:没有直接对应条目,也不该有——这是 CI 吞吐,不是用户可见面。

规模: 130 个文件,996 行生产代码(新增 382 / 删除 614),测试行 0,生成/schema 行 0。按 gate 的定义确实触及核心路径——packages/cli/src/config/**(2 个文件)和 packages/cli/src/services/**(3 个文件)——所以我跑了两级检查:标题类型是 perf 而不是 refactor,Tier 1 硬拦截不适用;你在本仓库有 admin 权限,按 AGENTS.md 属于维护者自己提的 PR,外部 PR 的 gate 豁免。这点要写明,因为 130 个文件看起来正是那条规则要拦的东西——但它是一次均匀的、每文件约 7.7 行的清扫,属于「广度 ≠ 规模」的情形,而落在核心路径下的那 5 个文件每个只改了一两行 import。996 也刚好在 1000 行大 PR 提醒线之下;考虑到改动形态,我认为再拆也不会让 review 更好做。

方案: 范围合理,而且我没有直接采信「改动最小」的说法,是核对过的——细节见 Stage 2 评论。简版结论:成立。diff 里每一条新增和删除的行都是 import 语句;从 barrel 移除的 248 个名字,与通过子路径重新加回来的 248 个名字完全一致,所以没有丢名字、没有加名字、也没有夹带顺手重构。把与 mock 耦合的那一半留给后续 PR,是正确的切分而不是随意切分。

进入代码审查前有两点要先说,都不是针对这次转换本身:

  1. 这个 PR 没有任何 CI lane 会跑。 base 是 perf/core-subpath-imports,而 Qwen Code CI(Test,以及承载 ESLint + Prettier 的 Lint & Static)、Security Checksserve-abpull_request 触发都过滤到 main / release/**。你写的判据——「错误的说明符会在 import 时就失败,所以 cli 套件全绿就是信号」——判据本身是对的,但它没有跑过,而且在这个 base 上跑不了。
  2. CI test time is bound by module import cost, not scheduling #10908 自己把 exports map 这个问题列为未验证的前置条件(「必须在 codemod 跑之前查清,因为它决定子路径指向 src 还是 dist」)。PR 正文说构建已经通过同一套路径映射解析这些说明符;我认为那是 tsconfig 的映射,而它覆盖不到纯 Node 解析。细节见 Stage 2。

风险: Stage 1e 命中一条与 revert 相关的路径——packages/cli/src/serve/sandbox.ts。不是拦截项,那里也只改了两行 import,但意味着我不会降低 review 深度,并且在批准前确实需要 CI 证据。

进入代码审查 🔍

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

Reviewed at 028826b58db0c9951d269b1954287f5bc3222b17 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code review

My independent proposal first. Reading only the title and "Why it's needed", I'd have reached for the same fix — a codemod from the root barrel to per-module subpaths, staged — because the alternatives are worse: making the barrel itself lazy is higher-leverage per file but changes core semantics, aliasing the barrel to a stub inside vitest is cheap but quietly changes what tests exercise, and more sharding is already ruled out by the shard timings in #10908. Where I'd have been insistent is on two preconditions before the codemod runs: settle how the new specifiers resolve under plain Node (tsc paths and Node exports are different resolvers), and run the formatter. Those are the two places this PR and my proposal diverge.

The transform itself is clean — I checked it rather than trusting it. Three static checks over the full diff:

  • Lossless. The 248 names removed from @qwen-code/qwen-code-core barrel imports and the 248 names re-added through subpath specifiers are the same setcomm in both directions is empty. Nothing was dropped, nothing was smuggled in.
  • Nothing but imports moved. Filtering every added and removed line against import-statement shapes leaves zero lines unaccounted for, so the "every other line is byte-identical" claim holds.
  • Every specifier is real. All 102 distinct new specifiers map to an existing packages/core/src/** module; no dangling paths.

Type/value classification is preserved too, which matters under verbatimModuleSyntax: true. In systemController.ts the inline type MCPOAuthConfig and type ReasoningEffortOverride became import type, while AuthProviderType (an export enum, packages/core/src/config/mcp-server-config.ts:100) and MCPServerConfig (an export class, :106) correctly stayed in a value import. That's a codemod that understood the difference, not a regex.

So: no correctness finding in the rewrite. My three concerns are all about what happens around it.

1. No CI lane runs against this PR, so the stated oracle never executed (blocking)

The safety argument for a 130-file import rewrite is "a wrong specifier fails at import time rather than subtly, so a green cli suite is the signal." That property is true, but it only produces a signal if something imports the files. On this base, nothing does: .github/workflows/ci.yml triggers pull_request on branches: [main, release/**], and this PR's base is perf/core-subpath-imports. security-checks.yml and serve-ab.yml carry the same filter. The 18 check-runs on 028826b5 confirm it — the only PR-CI jobs are the two tui-parity ones, and there is no Test (ubuntu-latest, Node 22.x), no Lint & Static (which is where ESLint and Prettier live), no Classify PR.

There's no green suite on the PR this one depends on either. #10917 at a1a84a25 shows Test (ubuntu-latest, Node 22.x) as failure, but that failure is infra, not tests: the job's step 16 Run tests and generate reports has a null conclusion (it never completed) and the only annotation on the check reads "The self-hosted runner lost communication with the server." The log blob is already gone (HTTP 404 BlobNotFound), so I can't quote an excerpt — I'm classifying it as infra from the step conclusions and the annotation, not from log text.

This matters most for the part you explicitly asked a reviewer to judge: whether "a spread mock that does not override the imported name is safe to leave alone" holds for how this codebase writes mocks. That is precisely the kind of assumption that shows up as a red test and stays invisible in a diff read. #10908 counts 135 cli test files calling vi.mock('@qwen-code/qwen-code-core', factory); the exclusion rule is a good argument for why the 130 are safe, but it's an argument, and the one thing that would turn it into evidence hasn't run.

2. The new specifier form is not resolvable by plain Node, and two consumers execute the unbundled output (blocking)

At this PR's base commit a1a84a25, packages/core/package.json exports contains: ., the eight named subpaths (./transcriptRecords, ./envVarResolver, ./goalWire, ./memoryScopes, ./subSessionConstants, ./toolWriteOrigin, ./userPromptSubmitContext, ./noFollowOpen), ./package.json, ./dist/*, ./src/*. I read that at the base ref rather than at main#10917 touches three files and none of them is that manifest.

None of this PR's 102 specifiers match any of those keys. @qwen-code/qwen-code-core/utils/debugLogger.js is not ., not one of the eight, and not under ./src/ or ./dist/. They resolve everywhere a tsconfig-aware resolver is in play — tsc (packages/cli/tsconfig.json maps "@qwen-code/qwen-code-core/*": ["../core/src/*"], and paths wins over node resolution), vitest (the aliases #10917 adds), tsx, and esbuild. But packages/cli's build is tsc --build (scripts/build_package.js:38) and tsc emits specifiers verbatim — it never rewrites paths. So packages/cli/dist/**.js literally contains @qwen-code/qwen-code-core/utils/debugLogger.js, and plain Node then applies the exports map, which rejects it: ERR_PACKAGE_PATH_NOT_EXPORTED.

Two consumers run that unbundled output under plain Node:

  • npm startscripts/start.js spawns node <root>/packages/cli, which resolves main: dist/index.js. Its nodeArgs carry only --expose-gc (plus --inspect-brk under DEBUG) — no --import, no loader hook, nothing that would teach Node about tsconfig paths.
  • .github/workflows/serve-ab.yml, lines 294 and 307: node … packages/cli/dist/index.js.

Unaffected: the published package (scripts/prepare-package.js repoints main to cli.js and bin at the esbuild bundle, which esbuild.config.js writes to the root dist/, outdir: 'dist', entry cli — it does not overwrite the tsc output) and npm run dev (tsx). So the PR body is right that the shipped artifact doesn't contain these specifiers; the exposure is the unbundled dev/A-B path, not the release.

Two things make me treat this as a real gap rather than a theoretical one. This repo has been bitten by exactly this divergence before — packages/cli/src/commands/serve.test.ts treats ERR_PACKAGE_PATH_NOT_EXPORTED on a spawned child's stderr as a hard failure. And #10908 already names it as an open precondition: "Unverified: the barrel currently resolves to ~612 packages/core/src/*.ts files and zero dist/ files, which contradicts the exports map. This must be settled before the codemod runs, since it decides whether subpaths point at src or dist." The PR body's "the build already resolves these specifiers through the same path mapping" reads as the tsconfig mapping — and tsconfig paths and the exports map only agree today because every existing subpath import happens to be one of the eight registered names. I checked: there are zero multi-segment @qwen-code/qwen-code-core/<dir>/<file>.js imports anywhere in packages/** on main, so this PR is the first to depend on the unregistered form.

To be explicit about the limits of this finding: it is static reasoning. Triage never executes PR-derived code, so I did not run npm start or a build, and I have not confirmed that esbuild applies the .js.ts substitution after a paths substitution (no lane builds the bundle on this base either). One real run settles it. The fix is small in either direction — widen core's exports to cover these roots, or decide that the unbundled dist is not a supported runtime and adjust scripts/start.js / serve-ab.yml to match. And to be fair about ownership: #10917 opens the same gap for 2 files, so this is a stack-level decision rather than a defect unique to this PR — but this PR is what scales it from 2 files to 130, which is also what makes it worth settling now.

3. The diff is not Prettier-formatted (blocking, one command to fix)

.prettierrc.json sets printWidth: 80 and .prettierignore does not exclude packages/cli/src. 279 of the 382 added lines exceed 80 characters, and at least 39 of them are multi-name import lists that Prettier would expand one-per-line. The two longest:

  • packages/cli/src/startup/worktreeStartup.tsimport { GitWorktreeService, readWorktreeSessionMarker, worktreeBranchForSlug, writeWorktreeSessionMarker } from '@qwen-code/qwen-code-core/services/gitWorktreeService.js'; (172 chars)
  • packages/cli/src/ui/hooks/useProviderUpdates.ts — the nine-name provider-config.js import (~250 chars)

The Run Prettier step in Lint & Static would fail on this as posted, but that lane doesn't run on this base (finding 1), so nothing caught it. npm run format fixes it — worth doing before the retarget so the eventual main-based run isn't red for a cosmetic reason and the real signal isn't buried.

Non-blocking

  • No regression guard ships with the migration. CI test time is bound by module import cost, not scheduling #10908's phase ① is "Migrate barrel imports to subpaths + lint rule". architecture/no-core-root-barrel-import is registered as 'error' only for files: ['packages/core/src/**/*.{ts,tsx}'] (eslint.config.js:230–240) — there is no cli equivalent, so nothing stops these 130 files drifting back to the barrel on the next feature PR. Fine to defer to a later batch; worth confirming it's deferred rather than dropped, since without it the win decays at ~110 new test files a week.
  • Batch 1 of 2 is the right cut. Deferring the mock-coupled files (which need mocks moved in the same commit) keeps this diff reviewable and revertible.
  • The 996-line size sits just under the 1000-line advisory, and given that it's a uniform sweep I don't think splitting further would help anyone.
Dependency surface this PR creates (18 subpath roots)

Useful for finding 2 — each root is a key packages/core/package.json exports would need to cover for plain Node to resolve it.

Subpath root Import statements
@qwen-code/qwen-code-core/utils 89
@qwen-code/qwen-code-core/config 76
@qwen-code/qwen-code-core/agents 43
@qwen-code/qwen-code-core/tools 40
@qwen-code/qwen-code-core/extension 33
@qwen-code/qwen-code-core/services 27
@qwen-code/qwen-code-core/core 27
@qwen-code/qwen-code-core/providers 8
@qwen-code/qwen-code-core/mcp 7
@qwen-code/qwen-code-core/telemetry 6
@qwen-code/qwen-code-core/hooks 6
@qwen-code/qwen-code-core/subagents 5
@qwen-code/qwen-code-core/permissions 5
@qwen-code/qwen-code-core/goals 3
@qwen-code/qwen-code-core/output 2
@qwen-code/qwen-code-core/memory 2
@qwen-code/qwen-code-core/ide 2
@qwen-code/qwen-code-core/models 1

102 distinct module specifiers across those roots; all 102 verified to point at an existing file under packages/core/src/.

Testing

Which evidence this comment carries: the PR's own CI check results, read through the GitHub API for the reviewed commit. This is an unattended CI run, so per the triage rules I did not build, run, or test any PR-derived code, and I drove no real-scenario tmux session — that lane is local-invocation only. Everything above about the rewrite's correctness is static analysis of the diff text plus reads of the base tree.

Check Conclusion
OpenTUI no-flicker gate in progress
TUI parity snapshots (ink vs opentui) in progress
triage in progress
delay-automatic-review waiting
authorize success
label success
assign success
verify skipped
tmux-testing skipped
publish-verify skipped
publish-tmux skipped
publish-resolution skipped
resolve-pr skipped
precheck-pr skipped
review-config skipped
ack-review-request skipped

Read that table as an absence, not as a pass. There is no Test, no Lint & Static, no typecheck and no bundle build in it, because the base branch filters those workflows out — the two in-progress rows are the whole of this PR's own CI. Nothing has imported these 130 files yet.

Not verified, and why:

  • Not verified: that the cli suite passes with these imports — no lane runs it on this base, and the base PR's Test lane died on a runner disconnect before executing.
  • Not verified: that ESLint and Prettier pass — same reason. I checked Prettier by hand against .prettierrc.json instead, and it does not (finding 3).
  • Not verified: that tsc --build accepts all 102 specifiers and that every imported name is exported by the module it now points at. I confirmed each module file exists and that the name set is preserved, but only a typechecker settles per-name export visibility.
  • Not verified: that the esbuild bundle still builds and does not duplicate modules — the module-identity risk CI test time is bound by module import cost, not scheduling #10908 calls out. No lane bundles here.
  • Not verified: the actual collection-time win. The per-file numbers come from CI test time is bound by module import cost, not scheduling #10908 and are the author's measurements, clearly attributed as such, not something this run re-measured.
  • Not verified: whether plain Node can load packages/cli/dist after this change (finding 2) — that needs one real run, which triage does not perform.

Sandboxed verification would settle most of that, and you have write access so both lanes are available directly rather than sponsored: @qwen-code /verify — that the 130 rewritten files still import what they use (nothing on this base ran the cli suite, so the mock-exclusion judgement is still an argument rather than evidence), and that the collection-time win is real A/B against the base build; @qwen-code /tmux — that the CLI still starts and renders, which is also the cheapest way to find out whether the unbundled packages/cli/dist resolution gap in finding 2 bites a real run. Alternatively, retargeting to main once #10917 lands gets the same answer from the normal lanes without a manual trigger.

中文说明

代码审查

先说我自己的方案。 只看标题和「为什么需要」,我会选同样的做法——把 root barrel 导入按模块改写成子路径,并且分批推进——因为其他选项更差:把 barrel 本身改成惰性导入,单文件收益更高但会改动 core 语义;在 vitest 里把 barrel 别名成 stub 很便宜,但会悄悄改变测试所验证的东西;继续加分片已经被 #10908 里的分片耗时排除。我会坚持的两个前置条件是:先查清这些新说明符在纯 Node 下怎么解析(tsc 的 paths 和 Node 的 exports 是两套解析器),以及先跑格式化工具。这也正是本 PR 与我的方案分叉的两处。

转换本身是干净的——我是核对过的,不是采信。 对完整 diff 做了三项静态检查:

  • 无丢失。@qwen-code/qwen-code-core barrel 导入中移除的 248 个名字,与通过子路径说明符重新加回来的 248 个名字,是同一个集合——两个方向的 comm 都是空的。没丢名字,也没夹带新东西。
  • 除 import 外没有任何改动。 把所有新增行和删除行按 import 语句形态过滤后,剩余行无法归类,所以「其余每一行逐字节相同」的说法成立。
  • 每个说明符都真实存在。 102 个不同的新说明符全部指向 packages/core/src/** 下已存在的模块,没有悬空路径。

type / value 的分类也保留了,这在 verbatimModuleSyntax: true 下很关键。systemController.ts 里内联的 type MCPOAuthConfigtype ReasoningEffortOverride 变成了 import type,而 AuthProviderTypeexport enumpackages/core/src/config/mcp-server-config.ts:100)和 MCPServerConfigexport class:106)正确地留在了值导入里。这说明 codemod 理解两者区别,不是正则替换。

所以改写本身没有正确性问题。我的三点顾虑都在它周围。

1. 这个 PR 没有任何 CI lane 会跑,所以正文里的判据从未执行(拦截项)

130 个文件的 import 改写,其安全论证是「错误的说明符会在 import 时就失败,所以 cli 套件全绿就是信号」。这个性质成立,但只有在真的有东西去 import 这些文件时才会产生信号。在这个 base 上没有:.github/workflows/ci.ymlpull_request 触发过滤到 branches: [main, release/**],而本 PR 的 base 是 perf/core-subpath-importssecurity-checks.ymlserve-ab.yml 是同样的过滤。028826b5 上的 18 条 check-run 印证了这点——属于 PR 自身 CI 的只有两条 tui-parity,没有 Test (ubuntu-latest, Node 22.x)、没有 Lint & Static(ESLint 和 Prettier 都在那里)、没有 Classify PR

它所依赖的那个 PR 也没有绿套件。#10917a1a84a25Test (ubuntu-latest, Node 22.x) 显示 failure,但那是基础设施故障而非测试失败:该 job 的第 16 步 Run tests and generate reports 结论是 null(从未跑完),check 上唯一的 annotation 写着 "The self-hosted runner lost communication with the server."。日志 blob 已经不存在(HTTP 404 BlobNotFound),所以我无法引用日志片段——我把它归类为基础设施问题,依据是步骤结论和 annotation,不是日志正文。

这一点对你明确请 reviewer 判断的那部分最关键:「展开式 mock 只要没覆盖被导入的名字就可以不动」这个规则,在本仓库的 mock 写法下是否成立。这正是那种会以红色测试暴露、而在读 diff 时完全看不见的假设。#10908 统计有 135 个 cli 测试文件调用 vi.mock('@qwen-code/qwen-code-core', factory);排除规则是一个很好的论证,说明这 130 个是安全的,但它目前只是论证,而唯一能把它变成证据的那一步没有跑。

2. 新的说明符形态在纯 Node 下无法解析,而有两个消费方会执行未打包的产物(拦截项)

在本 PR 的 base 提交 a1a84a25packages/core/package.jsonexports 包含:.、八个具名子路径(./transcriptRecords./envVarResolver./goalWire./memoryScopes./subSessionConstants./toolWriteOrigin./userPromptSubmitContext./noFollowOpen)、./package.json./dist/*./src/*。我是在 base ref 上读的,不是在 main 上——#10917 只改了三个文件,其中没有这个 manifest。

本 PR 的 102 个说明符没有一个能匹配上述任何 key。@qwen-code/qwen-code-core/utils/debugLogger.js 既不是 .,不是八个具名之一,也不在 ./src/./dist/ 下。它们在所有认 tsconfig 的解析器里都能解析——tsc(packages/cli/tsconfig.json 映射 "@qwen-code/qwen-code-core/*": ["../core/src/*"],且 paths 优先于 node 解析)、vitest(#10917 新增的 alias)、tsx、esbuild。但 packages/cli 的构建是 tsc --buildscripts/build_package.js:38),而 tsc 原样输出说明符——它从不重写 paths。所以 packages/cli/dist/**.js 里字面上就是 @qwen-code/qwen-code-core/utils/debugLogger.js,纯 Node 接着套用 exports map,结果是 ERR_PACKAGE_PATH_NOT_EXPORTED

有两个消费方会在纯 Node 下执行这份未打包产物:

  • npm startscripts/start.js 派生 node <root>/packages/cli,解析到 main: dist/index.js。它的 nodeArgs 只带 --expose-gcDEBUG 下再加 --inspect-brk)——没有 --import、没有 loader hook,没有任何能让 Node 认识 tsconfig paths 的东西。
  • .github/workflows/serve-ab.yml 第 294、307 行:node … packages/cli/dist/index.js

不受影响的:发布包(scripts/prepare-package.jsmain 改成 cli.jsbin 指向 esbuild bundle,而 esbuild.config.js 把 bundle 写到目录 dist/outdir: 'dist'、入口 cli——不会覆盖 tsc 产物),以及 npm run dev(tsx)。所以 PR 正文说发布产物里不存在这些说明符是对的;暴露面是未打包的开发/A-B 路径,不是发布路径。

有两点让我不把它当理论问题。这个仓库以前就踩过同样的分歧——packages/cli/src/commands/serve.test.ts 把派生子进程 stderr 里的 ERR_PACKAGE_PATH_NOT_EXPORTED 当作硬失败。而 #10908 已经把它列为未解决的前置条件:「未验证: barrel 目前解析到约 612 个 packages/core/src/*.ts 文件、零个 dist/ 文件,这与 exports map 矛盾。必须在 codemod 跑之前查清,因为它决定子路径指向 src 还是 dist。」PR 正文的「构建本来就通过同一套路径映射解析这些说明符」读起来指的是 tsconfig 映射——而 tsconfig pathsexports map 今天之所以一致,只是因为现有的子路径导入恰好都是那八个已注册的名字之一。我查过:mainpackages/** 里多段形态 @qwen-code/qwen-code-core/<dir>/<file>.js 的导入数量是,所以本 PR 是第一个依赖这种未注册形态的。

明确说明这条的边界:它是静态推理。triage 从不执行 PR 带来的代码,所以我没有跑 npm start、没有跑构建,也没有确认 esbuild 在 paths 替换之后是否会做 .js.ts 替换(这个 base 上也没有 lane 会打 bundle)。一次真实运行就能定论。两个方向的修法都很小——要么扩展 core 的 exports 覆盖这些 root,要么明确认定未打包的 dist 不是受支持的运行时,并相应调整 scripts/start.js / serve-ab.yml。也说明归属:#10917 已经为 2 个文件开了同样的口子,所以这是整条 stack 层面的决定,不是本 PR 独有的缺陷——但本 PR 把它从 2 个文件放大到 130 个,这也是现在就该定下来的原因。

3. diff 没有经过 Prettier 格式化(拦截项,一条命令可修)

.prettierrc.json 设了 printWidth: 80,而 .prettierignore 没有排除 packages/cli/src382 行新增里有 279 行超过 80 字符,其中至少 39 行是 Prettier 会展开成每行一个名字的多名导入。最长的两处:

  • packages/cli/src/startup/worktreeStartup.ts —— import { GitWorktreeService, readWorktreeSessionMarker, worktreeBranchForSlug, writeWorktreeSessionMarker } from '@qwen-code/qwen-code-core/services/gitWorktreeService.js';(172 字符)
  • packages/cli/src/ui/hooks/useProviderUpdates.ts —— 九个名字的 provider-config.js 导入(约 250 字符)

Lint & Static 里的 Run Prettier 步骤按现状会失败,但这个 base 上该 lane 不跑(见第 1 点),所以没有任何东西发现它。npm run format 就能修——建议在重定向 base 之前做掉,免得将来基于 main 的那次运行因为格式问题变红,把真正的信号埋掉。

非拦截项

  • 迁移没有附带防回归的护栏。 CI test time is bound by module import cost, not scheduling #10908 的阶段 ① 是「把 barrel 导入迁移到子路径 + lint 规则」。architecture/no-core-root-barrel-import 只在 files: ['packages/core/src/**/*.{ts,tsx}'] 上注册为 'error'eslint.config.js:230–240),cli 侧没有对应规则,所以没有任何东西阻止这 130 个文件在下一次功能 PR 里又退回 barrel。留到后续批次没问题,但值得确认是「延后」而不是「丢掉」——没有它,这份收益会以每周约 110 个新测试文件的速度衰减。
  • 分两批是正确的切分。 把与 mock 耦合的那批文件(需要在同一个提交里搬迁 mock)留到后面,让这次的 diff 保持可审、可回滚。
  • 996 行的规模刚好在 1000 行提醒线之下;考虑到它是一次均匀清扫,我认为再拆对谁都没有帮助。

测试

本评论携带的证据类型: 通过 GitHub API 读取的、本 PR 自身在所审提交上的 CI check 结果。这是无人值守的 CI 运行,按 triage 规则我没有构建、运行或测试任何 PR 带来的代码,也没有驱动真实场景的 tmux 会话——那条 lane 只适用于本地调用。上面所有关于改写正确性的内容,都是对 diff 文本的静态分析加上对 base 树的读取。

把上面那张表当作「缺失」来读,不要当作「通过」。表里没有 Test、没有 Lint & Static、没有 typecheck、没有 bundle 构建,因为 base 分支把这些 workflow 过滤掉了——两条 in-progress 就是本 PR 自身 CI 的全部。到目前为止没有任何东西 import 过这 130 个文件。

未验证项及原因:

  • 未验证:cli 套件在这些导入下是否通过——这个 base 上没有 lane 会跑它,而 base PR 的 Test lane 在执行前就因 runner 断连死掉了。
  • 未验证:ESLint 与 Prettier 是否通过——同样原因。Prettier 我改为对着 .prettierrc.json 手工核对,结论是不通过(第 3 点)。
  • 未验证:tsc --build 是否接受全部 102 个说明符、以及每个被导入的名字是否真的由它现在指向的模块导出。我确认了模块文件都存在、名字集合保持一致,但逐个名字的导出可见性只有类型检查器能定论。
  • 未验证:esbuild bundle 是否仍能构建、是否不会重复模块——即 CI test time is bound by module import cost, not scheduling #10908 点出的模块同一性风险。这里没有 lane 会打 bundle。
  • 未验证:实际的收集耗时收益。单文件数字来自 CI test time is bound by module import cost, not scheduling #10908,是作者的测量,明确归属于作者,不是本次运行重新测的。
  • 未验证:改动之后纯 Node 能否加载 packages/cli/dist(第 2 点)——这需要一次真实运行,而 triage 不执行。

沙箱化验证可以解决其中大部分,而且你有写权限,两条 lane 都可以直接触发而不是赞助运行:@qwen-code /verify —— 验证这 130 个改写后的文件仍然导入了它们所用的东西(这个 base 上没有任何东西跑过 cli 套件,所以 mock 排除规则目前还是论证而非证据),以及收集耗时的收益相对 base 构建做 A/B 是否真实;@qwen-code /tmux —— 验证 CLI 仍能启动并渲染,这也是判断第 2 点里未打包 packages/cli/dist 的解析缺口是否会在真实运行中咬人的最便宜方式。另一个选择是:等 #10917 合并后把 base 重定向到 main,不用手动触发就能从常规 lane 得到同样的答案。

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

Reviewed at 028826b58db0c9951d269b1954287f5bc3222b17 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 2/5 — the rewrite itself is the cleanest part of this; what I can't get past is that nothing has executed it, and one static check says a real runtime path breaks.

Stepping back. The transform is genuinely good work, and better than a codemod has any right to be: the name set is provably lossless, type-vs-value classification survived verbatimModuleSyntax, all 102 specifiers point at real modules, and not one line outside an import statement moved. If the question were "did the author do this carefully?", the answer is clearly yes and I'd be writing a different comment.

But the question the gate has to answer is whether this is ready to merge, and there I keep landing on the same thing: this PR has never been imported by anything. The stated oracle is exactly right — a wrong specifier fails loudly at import time — but the base branch filters out every lane that would have triggered it, and the base PR's own Test lane died on a runner disconnect before executing a single test. So the safety argument is sound in principle and unevidenced in fact. That's not a small gap for a 130-file change whose whole justification is mechanical reliability, and it's the gap that also swallows the two things I'd most want a run to answer: whether tsc accepts all 102 specifiers name-by-name, and whether the mock-exclusion judgement you explicitly asked a reviewer to weigh holds against 135 vi.mock call sites. Reading a diff cannot settle either. I'd rather say that plainly than let a clean-looking mechanical change carry an unexamined assumption into main.

The Node-resolution finding is the one I'd want answered before anything else, and I want to be careful not to overstate it: it's static reasoning, I did not run npm start, and it may well have a one-line answer I can't see from here. What makes me raise it as blocking rather than as a question is that #10908 already lists it as an unverified precondition that "must be settled before the codemod runs", the PR body reads as settling it via the tsconfig mapping, and tsconfig paths is not the resolver plain Node uses. The two only agree today because all eight existing subpath imports are registered in exports. This PR is the first to depend on a form that isn't, at a scale of 130 files, and scripts/start.js plus serve-ab.yml both hand that output to bare node. If the answer is "the unbundled dist isn't a supported runtime", that's a completely reasonable position — but it should be stated and the two callers adjusted, not left for whoever next runs npm start to discover.

The Prettier finding is trivial and I almost left it out; I didn't, because it's the one item here that is certain rather than reasoned, and because it would turn the first main-based CI run red for a cosmetic reason and bury the signal that actually matters.

On the things I checked myself rather than took on faith: I confirmed the minimality claim instead of assuming it, and I looked for a simpler path — there isn't one. Making the barrel lazy touches core semantics, stubbing it in vitest changes what tests exercise, and more sharding is already answered by your own shard timings. Staging it 130-then-the-rest is the right cut, and deferring the mock-coupled half is a judgement call I agree with. If I were maintaining this in six months I'd thank you for the migration; I'd also want the lint rule from phase ① so the 130 files don't quietly drift back.

So: not approving, and not because of doubt about intent or craft. Requesting changes on three specific items — get one real run's worth of evidence, settle the exports question in whichever direction you choose, and run the formatter. None of them are large, and two of the three are one command or one manifest edit. I'd happily re-run triage on the retargeted PR.

中文说明

Confidence: 2/5 —— 这次改写本身是整个 PR 里最干净的部分;我过不去的是:它从未被执行过,而一项静态检查指出有一条真实的运行时路径会断。

退一步看整体。转换本身做得很好,甚至比一个 codemod 应有的水平更好:名字集合可证明无丢失、type 与 value 的分类在 verbatimModuleSyntax 下保住了、102 个说明符全部指向真实模块、除 import 语句外没有任何一行发生移动。如果问题是「作者做得是否细致」,答案显然是肯定的,那我写的会是另一份评论。

但 gate 要回答的问题是它是否可以合并,而在这点上我反复落到同一件事:这个 PR 到目前为止没有被任何东西 import 过。 你写的判据完全正确——错误的说明符会在 import 时大声失败——但 base 分支把每一个会触发它的 lane 都过滤掉了,而 base PR 自己的 Test lane 在执行任何一个测试之前就因 runner 断连死掉了。所以这个安全论证在原理上成立、在事实上没有证据。对一个 130 个文件、全部正当性都建立在「机械可靠性」上的改动来说,这个缺口不小;而且它同时吞掉了我最希望由一次真实运行来回答的两件事:tsc 是否逐个名字地接受全部 102 个说明符,以及你明确请 reviewer 判断的 mock 排除规则在 135 处 vi.mock 调用点上是否成立。读 diff 无法定论这两件事。我宁愿把这点说清楚,也不愿意让一个看起来干净的机械改动把一个未经检验的假设带进 main

Node 解析那条是我最希望在其他一切之前得到回答的,同时我也想小心不要夸大它:它是静态推理,我没有运行 npm start,而且很可能有一个我在这里看不到的、一行就能解决的答案。让我把它作为拦截项而不是提问提出的原因是:#10908 已经把它列为「必须在 codemod 跑之前查清」的未验证前置条件,而 PR 正文读起来像是用 tsconfig 映射把它算作已解决——但 tsconfig paths 不是纯 Node 使用的解析器。这两套今天之所以一致,只是因为现有的八个子路径导入都注册在 exports 里。本 PR 是第一个依赖未注册形态的,规模是 130 个文件,而 scripts/start.jsserve-ab.yml 都会把这份产物直接交给裸 node。如果答案是「未打包的 dist 不是受支持的运行时」,那是完全合理的立场——但它应该被写明,并相应调整那两个调用方,而不是留给下一个运行 npm start 的人去发现。

Prettier 那条很琐碎,我几乎没写进去;最终写了,因为它是这里唯一「确定」而非「推理」得出的一项,也因为它会让第一次基于 main 的 CI 运行因为格式问题变红,从而埋掉真正重要的信号。

关于我自己核对而没有直接采信的部分:我验证了「改动最小」的说法而没有假设它成立,也找过是否存在更简的路径——没有。把 barrel 改成惰性会触及 core 语义,在 vitest 里替换成 stub 会改变测试所验证的内容,继续加分片已经被你自己的分片耗时回答过了。分成「先 130 个、剩下的后续」是正确的切分,把与 mock 耦合的那一半延后,我同意这个判断。如果六个月后由我来维护,我会感谢这次迁移;同时我也会想要阶段 ① 里的那条 lint 规则,以免这 130 个文件悄悄退回原状。

所以:不批准,而且不是因为对意图或工艺有疑问。就三项具体内容请求修改——拿到一次真实运行的证据、按你选定的方向把 exports 问题解决掉、跑一遍格式化工具。这三项都不大,其中两项是一条命令或一处 manifest 修改。重定向 base 之后,我很乐意再跑一次 triage。

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

Reviewed at 028826b58db0c9951d269b1954287f5bc3222b17 · 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.

@yiliang114 Requesting changes on three specific items — full reasoning in my Stage 2 and Stage 3 notes above, short version here.

  1. Nothing has executed this diff. The base is perf/core-subpath-imports, and Qwen Code CI / Security Checks / serve-ab all filter pull_request to main / release/**, so there is no Test, no Lint & Static (ESLint + Prettier) and no typecheck on this PR. #10917's Test (ubuntu-latest, Node 22.x) also never completed — runner disconnect, step 16 has a null conclusion. Your oracle ("a wrong specifier fails at import time") is right, but it hasn't run. Either retarget to main once #10917 lands, or trigger @qwen-code /verify / a workflow_dispatch CI run and paste the result.
  2. The new specifier form isn't in core's exports map. At base a1a84a25 it holds ., eight named subpaths, ./package.json, ./dist/*, ./src/* — none of the 102 <dir>/<file>.js specifiers match. They resolve via tsconfig paths (tsc/esbuild/tsx) and via #10917's vitest aliases, but tsc --build emits specifiers verbatim into packages/cli/dist, and scripts/start.js (npm start) plus .github/workflows/serve-ab.yml both hand that output to bare nodeERR_PACKAGE_PATH_NOT_EXPORTED. #10908 lists exactly this as an unverified precondition. Widen exports, or state that the unbundled dist isn't a supported runtime and adjust those two callers. Static reasoning only — I did not run it, so one real run settles it either way.
  3. Prettier. .prettierrc.json sets printWidth: 80; 279 of 382 added lines exceed it and ~39 are multi-name imports Prettier would expand. npm run format.

The rewrite itself needs no changes — I verified the 248 removed barrel names are exactly the 248 re-added through subpaths, all 102 specifiers point at real modules, and nothing outside an import statement moved. Non-blocking: #10908's phase ① pairs this migration with a lint rule, and architecture/no-core-root-barrel-import is registered only for packages/core/src/**, so nothing stops these 130 files drifting back.

中文说明

@yiliang114 就三项具体内容请求修改——完整推理见上面的 Stage 2 与 Stage 3 评论,这里是简版。

  1. 这份 diff 从未被执行过。 base 是 perf/core-subpath-imports,而 Qwen Code CI / Security Checks / serve-abpull_request 都过滤到 main / release/**,所以本 PR 上没有 Test、没有 Lint & Static(ESLint + Prettier)、也没有 typecheck。#10917Test (ubuntu-latest, Node 22.x) 同样从未跑完——runner 断连,第 16 步结论为 null。你的判据(「错误的说明符会在 import 时失败」)是对的,但它没有跑过。要么等 #10917 合并后把 base 重定向到 main,要么触发 @qwen-code /verify 或一次 workflow_dispatch CI 运行并把结果贴出来。
  2. 新的说明符形态不在 core 的 exports map 里。 在 base a1a84a25 上,它只包含 .、八个具名子路径、./package.json./dist/*./src/*——102 个 <dir>/<file>.js 说明符没有一个匹配。它们能通过 tsconfig paths(tsc/esbuild/tsx)和 #10917 的 vitest alias 解析,但 tsc --build 会把说明符原样输出到 packages/cli/dist,而 scripts/start.jsnpm start)和 .github/workflows/serve-ab.yml 都会把这份产物直接交给裸 nodeERR_PACKAGE_PATH_NOT_EXPORTED#10908 正是把这一条列为未验证的前置条件。要么扩展 exports,要么明确未打包的 dist 不是受支持的运行时并调整这两个调用方。这仅是静态推理——我没有运行它,所以一次真实运行就能定论。
  3. Prettier。 .prettierrc.json 设了 printWidth: 80;382 行新增里有 279 行超出,其中约 39 行是 Prettier 会展开的多名导入。跑一下 npm run format

改写本身不需要改动——我核对过:从 barrel 移除的 248 个名字与通过子路径重新加回来的 248 个完全一致,102 个说明符全部指向真实模块,除 import 语句外没有任何一行发生移动。非拦截项:#10908 的阶段 ① 把这次迁移与一条 lint 规则配套提出,而 architecture/no-core-root-barrel-import 只注册在 packages/core/src/** 上,所以没有任何东西阻止这 130 个文件退回原状。

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

Reviewed at 028826b58db0c9951d269b1954287f5bc3222b17 · re-run with @qwen-code /triage

…nd wrap long imports

Two problems with the previous commit, both found by CI.

The symbol map resolved a re-exported name to the module that re-exports it
rather than the one that declares it, so `ProviderModelConfig` was asked of
`models/types` when it is declared in `providers/types`, and the build failed
to typecheck. A checker now confirms, for every generated specifier, that the
named module really does export that symbol — following its own re-exports —
and it reports one bad pair out of 492.

The formatting pass that was supposed to run over these files had silently
done nothing: invoked from the repository root against paths outside it,
Prettier skips the files and still reports success, so long import statements
went out unwrapped. Rerunning it properly reflows 47 files.

Two files are left with an over-long line Prettier would wrap, because that
line is over-long on the base commit too and the lint gate does not flag it;
reformatting it here would be unrelated noise. Every other line outside an
import statement stays byte-identical.

@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): chunk 7: contents of scripts/start.js — whether root npm start / the release flow executes the esbuild bundle ( dist/cli.js ) or the raw tsc dist/index.js .; chunk 7: release/publish pipeline — what artifact actually ships as the qwen bin (raw tsc output vs bundle)..

中文说明

未探索到全部深度(达到工具调用预算):chunk 7:contents of scripts/start.js — whether root npm start / the release flow executes the esbuild bundle ( dist/cli.js ) or the raw tsc dist/index.js .;chunk 7:release/publish pipeline — what artifact actually ships as the qwen bin (raw tsc output vs bundle).

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

} from '@qwen-code/qwen-code-core';
import type { Config } from '@qwen-code/qwen-code-core/config/config.js';
import { IdeClient } from '@qwen-code/qwen-code-core/ide/ide-client.js';
import { initializeTelemetry } from '@qwen-code/qwen-code-core/telemetry/sdk.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: Rewriting this file (and eight more of the migrated files) from the root barrel to subpath specifiers silently breaks the colocated tests' vi.mock('@qwen-code/qwen-code-core', …) interception: vitest keys mocks by resolved module ID, and the subpath specifiers resolve — via the wildcard alias in packages/cli/vitest.config.ts — to different module IDs than the barrel the tests mock, so the real implementations now run against the test stubs. This file is the representative anchor; the measured pairing is: startup-prefetch.test.ts (8 failures), lsp-config-watcher.test.ts (5), systemController.test.ts (3, systemController.ts + baseController.ts), useResumeCommand.test.ts (9), session-swap-telemetry.test.ts (6, via session-switch.ts), live-session.test.ts (20 — the real CoreToolScheduler constructor throws TypeError: options.config.getToolRegistry is not a function against the stub Config), workflow-save-overlay.test.tsx (3 — the real saveWorkflowScript crashes against the stub config), DiscoverTab.test.tsx (1), SourcesTab.test.tsx (1): 56 PR-caused failures from a full packages/cli suite run attributed against the merge base. The PR's stated oracle is red, and the description's selection rule ("files where no test mocks the package") is violated for these nine — this is issue #10908's named risk ("tests stay green while no longer testing what they claim") materializing, except here the tests mostly go red: the worse shape is the ones that could stay green while testing something else (e.g. the real initializeTelemetry now executes inside unit tests and the "swallows telemetry initialization failures" test exercises no error path).

Witness:

npm test --workspace=packages/cli → Test Files 11 failed | 992 passed; Tests 72 failed | 28054 passed
test-delta rerun at merge base a1a84a25: failing file set = settings.test.ts, run-qwen-serve-live.test.ts only
  → the 9 files above are net-new failures on the PR (pre-existing ones excluded)
live-session.test.ts: TypeError: options.config.getToolRegistry is not a function (coreToolScheduler.ts:1480)
startup-prefetch.test.ts 'swallows telemetry initialization failures': mockWarn — Number of calls: 0

Fix: either revert these nine files to the barrel import (exclude them from batch 1, matching the PR's own selection rule), or co-migrate each affected test's vi.mock to the exact subpath specifiers its subject now imports (e.g. vi.mock('@qwen-code/qwen-code-core/utils/debugLogger.js', …)) in the same commit — the remedy the PR description itself prescribes for mock-coupled files. Note the fix must respect that packages/cli/vitest.config.ts:62-78 resolves the wildcard alias /^@qwen-code\/qwen-code-core\/(.*)$/ → core/src/$1 and the exact-match root alias to different module IDs (the file's own comment documents that importing one core module rather than the package root depends on it), so a co-migrated mock must spell exactly the specifier the source file imports. Acceptance: the nine failing test files above are red at HEAD and must return to green with the fix — and go red again if a subpath import is reintroduced without mock coverage.

中文说明

[Critical] R1-1:把这个文件(以及另外 8 个被迁移的文件)从根 barrel 改为子路径说明符,会悄悄破坏同目录测试里的 vi.mock('@qwen-code/qwen-code-core', …) 拦截:vitest 按解析后的模块 ID 建立 mock,而子路径说明符经由 packages/cli/vitest.config.ts 的通配 alias 解析到的模块 ID 与测试所 mock 的 barrel 不同,于是真实实现会直接跑在测试桩上。本文件是代表性锚点;实测对应关系为:startup-prefetch.test.ts(8 个失败)、lsp-config-watcher.test.ts(5)、systemController.test.ts(3,systemController.ts + baseController.ts)、useResumeCommand.test.ts(9)、session-swap-telemetry.test.ts(6,经由 session-switch.ts)、live-session.test.ts(20 —— 真实 CoreToolScheduler 构造函数对桩 Config 抛 TypeError: options.config.getToolRegistry is not a function)、workflow-save-overlay.test.tsx(3 —— 真实 saveWorkflowScript 对桩配置崩溃)、DiscoverTab.test.tsx(1)、SourcesTab.test.tsx(1):一次完整 packages/cli 套件运行并对照合并基线归因后,共 56 个由本 PR 导致的失败。PR 声明的判据(绿套件)是红的,且正文的筛选规则(「没有测试 mock 该包的文件」)在这 9 个文件上被违反——这正是 #10908 点名的风险(「测试仍然绿着、却不再测试它声称的东西」)成真;这里多数测试直接变红,更糟的形态是那些可能继续绿、但测的已经是别的东西的用例(例如真实 initializeTelemetry 现在会在单测里执行,而「吞掉 telemetry 初始化失败」的用例不再走任何错误路径)。

修复:要么把这 9 个文件还原为 barrel 导入(从 batch 1 中排除,符合 PR 自己的筛选规则),要么在同一个提交里把各测试的 vi.mock 同步迁移到源文件现在实际导入的子路径说明符(如 vi.mock('@qwen-code/qwen-code-core/utils/debugLogger.js', …))——即 PR 正文为 mock 耦合文件开出的药方。注意 packages/cli/vitest.config.ts:62-78 把通配 alias 与根精确匹配 alias 解析为不同的模块 ID(该文件注释也写明按模块导入依赖此映射),因此迁移后的 mock 必须与源文件的导入说明符逐字一致。验收标准:上述 9 个测试文件当前为红,修复后必须回绿;若在缺少 mock 覆盖的情况下重新引入子路径导入,它们应再次变红。

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

Comment on lines +8 to +9
import type { Config } from '@qwen-code/qwen-code-core/config/config.js';
import { createDebugLogger } from '@qwen-code/qwen-code-core/utils/debugLogger.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-2: [fails-closed] [regression] The 101 new <dir>/<file>.js subpath specifiers are not covered by packages/core/package.json exports (which holds only ., the eight named subpaths, ./package.json, ./dist/*, ./src/*), and tsc emits them verbatim into packages/cli/dist. Bare Node therefore rejects them at module load: npm start (scripts/start.js spawns node packages/cli → main: dist/index.js) crashes at startup with ERR_PACKAGE_PATH_NOT_EXPORTED before serving any input — and, new evidence beyond the existing thread's static analysis, npm run dev crashes too, because scripts/dev.js's loader intercepts only the exact bare specifier, letting subpaths fall through to Node's exports check. .github/workflows/serve-ab.yml:294/307 (node … packages/cli/dist/index.js) hits the same crash. Typecheck, vitest and the esbuild bundle stay green only because each has a private mapping (tsconfig paths / the vitest alias / esbuild reading that tsconfig), which is why no existing lane catches it; the published npm artifact is unaffected (prepare-package.js repoints at the bundle). This is the run the existing discussion asked for ("one real run settles it") — it settles it against: the CLI does not start outside the bundle on this branch.

Witness:

$ node --input-type=module -e "...import.meta.resolve('@qwen-code/qwen-code-core/utils/debugLogger.js')"  (cwd packages/cli)
ERR_PACKAGE_PATH_NOT_EXPORTED: Package subpath './utils/debugLogger.js' is not defined by "exports"
$ node packages/cli/dist/index.js -p 'hi'
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: ... imported from .../packages/cli/dist/src/config/lsp-config-watcher.js  (exit 1)
$ npm run dev -- -p 'hi'
same error from packages/cli/src/config/lsp-config-watcher.ts, via scripts/dev.js's resolve hook  (exit 1)
base form still resolves: import.meta.resolve('@qwen-code/qwen-code-core') → packages/core/dist/index.js

Fix: register the subpath roots in packages/core/package.json exports — e.g. a "./*" pattern mapping into dist/src/ (exact-match named entries keep precedence), or explicitly declare the unbundled dist an unsupported runtime and adjust scripts/start.js, scripts/dev.js and serve-ab.yml to match. Either way this settles the precondition #10908 named as "must be settled before the codemod runs"; the gap is partly owned by base #10917, but this PR scales the exposure from 2 files to 130. The fix must match the manifest's existing convention — entries pair types with import and map into ./dist/src/... (e.g. ./transcriptRecords./dist/src/utils/transcript-records.js), and the existing ./package.json, ./dist/*, ./src/* passthroughs must not be shadowed. Acceptance: a plain-Node smoke test (no tsx/vitest/esbuild) that builds and dynamically imports one cli-used core subpath specifier must pass with the exports entries and fail with ERR_PACKAGE_PATH_NOT_EXPORTED when they are removed.

中文说明

[Critical] R1-2: [fails-closed] [regression] 101 个新的 <dir>/<file>.js 子路径说明符都不在 packages/core/package.jsonexports map 里(现有键只有 .、八个具名子路径、./package.json./dist/*./src/*),而 tsc 会把说明符原样输出到 packages/cli/dist。因此裸 Node 在模块加载时就会拒绝它们:npm start(scripts/start.js 派生 node packages/cli → main: dist/index.js)在服务任何输入之前就带着 ERR_PACKAGE_PATH_NOT_EXPORTED 崩掉——并且,这是超出现有讨论静态推理的新证据:npm run dev 同样会崩,因为 scripts/dev.js 的 loader 只拦截精确的裸说明符,子路径会落到 Node 的 exports 检查上。.github/workflows/serve-ab.yml:294/307node … packages/cli/dist/index.js)会踩到同样的崩溃。typecheck、vitest 和 esbuild bundle 之所以绿,只是因为各自有私有映射(tsconfig paths / vitest alias / esbuild 读取该 tsconfig),这正是现有 lane 都发现不了它的原因;发布产物不受影响(prepare-package.js 会改指向 bundle)。已有讨论说「一次真实运行就能定论」——这条证据就是那次运行:结论是否定的,本分支上除 bundle 外的启动路径都起不来。

修复:在 packages/core/package.jsonexports 中注册这些子路径根——例如加一条映射到 dist/src/"./*" 通配(精确匹配的具名条目仍优先),或者明确未打包的 dist 不是受支持的运行时并相应调整 scripts/start.jsscripts/dev.jsserve-ab.yml。无论哪个方向,都等于落实 #10908 列为「必须在 codemod 前查清」的前置条件;该缺口部分归属于 base #10917,但本 PR 把暴露面从 2 个文件放大到 130 个。修复须符合 manifest 现有约定——条目成对携带 typesimport 并映射到 ./dist/src/...(如 ./transcriptRecords./dist/src/utils/transcript-records.js),且不能遮蔽现有的 ./package.json./dist/*./src/* 直通条目。验收标准:一个纯 Node(不经 tsx/vitest/esbuild)的冒烟测试,在构建后动态导入一个 cli 实际使用的 core 子路径说明符,加入 exports 条目后必须通过、移除后必须以 ERR_PACKAGE_PATH_NOT_EXPORTED 失败。

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

@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