Skip to content

perf(cli): let tests resolve core modules individually - #10917

Open
yiliang114 wants to merge 15 commits into
mainfrom
perf/core-subpath-imports
Open

perf(cli): let tests resolve core modules individually#10917
yiliang114 wants to merge 15 commits into
mainfrom
perf/core-subpath-imports

Conversation

@yiliang114

Copy link
Copy Markdown
Collaborator

What this PR does

Teaches the cli test runner to resolve individual core modules, and moves two files off the core package root as a first end-to-end check that the mapping works.

The runner's alias list is expressed as an ordered array so it can carry a pattern entry, mirroring the wildcard subpath rule cli's tsconfig already has. The package root becomes an exact match in the process — spelled as a string it would also match everything beneath it and rewrite each subpath into a path under index.ts.

Why it's needed

Importing from the core package root pulls in its entire export graph, a bit over six hundred modules, however little of it a file actually uses. In release run 33713579913 the cli workspace reported 2223s collecting modules against 1372s running tests; core reported 546s against 251s. A file that imports the package root costs roughly 11.5s before it reaches its first assertion, where the same file importing a single module costs about 2s — and the suites that already replace the package with a mock factory, and so never evaluate it, have always run at about 1.9s.

esbuild reads tsconfig paths, so the bundle already resolves per-module imports. The test runner does not read them, and the alias list standing in for them named only four subpaths, so per-module imports did not resolve under test at all. That gap is what this PR closes; the two migrated files exist to prove it end to end before anything larger moves.

Background and measurements are in #10908.

Reviewer Test Plan

How to verify

The two migrated files should behave identically — the change is which module the same symbols come from. Their own suites cover them, and the rest of the cli suite exercises the alias change, since every test in the package now resolves the package root through a pattern entry rather than a string one. A resolution mistake here fails loudly at import time rather than subtly, so a green cli run is the signal.

Worth a reviewer's eye: the ordering in the alias array. The four named subpaths must stay ahead of the pattern entry because their targets are not derivable from their names, and the package root must remain an exact match.

Evidence (Before & After)

N/A — no user-visible behavior changes.

Tested on

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

Not run locally; relying on CI across the three platforms.

Risk & Scope

  • Main risk or tradeoff: the alias change affects how every cli test resolves the core package. It is a like-for-like mapping onto the same sources the previous entry pointed at, so a mistake surfaces as an import failure rather than as wrong behavior.
  • Not validated / out of scope: only two files move here. Files whose dependents replace the core package with a mock factory are deliberately untouched — once the code under test imports a module directly, such a mock stops intercepting, and those call sites need their mocks moved in the same change. About thirty test files sit in that category for the next batch alone, so they are being handled separately rather than folded in here.
  • Breaking changes / migration notes: none.

Linked Issues

Refs #10908

中文说明

这个 PR 做了什么

让 cli 的测试运行器能够解析 core 的单个模块,并把两个文件从包根导入改成按模块导入,作为端到端的第一次验证。

alias 列表改成有序数组形式,以便携带一条通配规则,对齐 cli tsconfig 里已有的通配 subpath 映射。包根在此过程中必须改成精确匹配——写成字符串时它同样会匹配其下所有子路径,把每个子路径重写成 index.ts 下的路径。

为什么需要

从 core 包根导入会拉进它的整个导出图,六百多个模块,无论调用方实际只用了多少。在 release run 33713579913 中,cli 的模块收集耗时 2223s、跑测试 1372s;core 是 546s 对 251s。一个从包根导入的文件在到达第一条断言之前要花约 11.5s,而同一文件改成按模块导入只要约 2s——那些本来就用 mock 工厂替换整个包、因而从不求值它的用例,一直是约 1.9s。

esbuild 会读 tsconfig 的 paths,所以打包时已经能解析按模块导入。测试运行器不读 paths,而代替它的 alias 列表只列了四个具名 subpath,因此按模块导入在测试里根本解析不了。本 PR 补的就是这个缺口;两个迁移文件的作用是在更大范围改动之前把链路走通。

背景和测量数据见 #10908

审查者验证计划

如何验证

两个迁移文件的行为应完全不变——变的只是同一批符号来自哪个模块。它们各自的用例覆盖了自身,而整个 cli 套件则检验了 alias 改动,因为现在包中每个测试都通过通配规则而非字符串规则解析包根。这里若有解析错误会在导入期直接报错而不是悄悄改变行为,所以 cli 跑绿就是信号。

值得审查者留意的是数组里的顺序:四个具名 subpath 必须排在通配规则之前(它们的目标路径无法从名字推导),包根必须保持精确匹配。

证据(前后对比)

N/A —— 无用户可见行为变化。

风险与范围

  • 主要风险或权衡:alias 改动影响 cli 每个测试解析 core 包的方式。它与原先那条规则指向同一批源文件,是等价映射,出错会表现为导入失败而非行为错误。
  • 未验证 / 超出范围:本 PR 只迁移两个文件。那些「依赖方用 mock 工厂替换整个 core 包」的文件被刻意跳过——一旦被测代码直接按模块导入,这类 mock 就不再拦截,这些调用点需要在同一次改动里同步迁移 mock。仅下一批就有约三十个测试文件属于这种情况,因此单独处理而不并入本 PR。
  • 破坏性变更 / 迁移说明:无。

关联 Issue

Refs #10908

…files importing the whole package

Importing from the core package root pulls in its entire export graph — a bit
over six hundred modules — however little of it a file actually uses. In a
release run the cli workspace spent 2223s collecting modules against 1372s
running tests, and a file that imports the package root costs about 11.5s
before its first assertion where one importing a single module costs about 2s.

cli's tsconfig already maps a wildcard subpath onto core's sources, so esbuild
resolves per-module imports when it bundles. Vitest does not read tsconfig
paths, and the alias list that stands in for them named only four subpaths, so
those imports did not resolve under test at all. This adds the wildcard there.

Expressing the alias list as an ordered array is what allows a pattern entry.
The package root has to become an exact match in the process: as a string it
would also match everything beneath it and rewrite each subpath into a path
under index.ts.

Two files move to per-module imports as a first check that the mapping holds
end to end. Both were picked because nothing that depends on them replaces the
core package with a mock factory — where a test does that, the mock stops
intercepting once the code under test imports the module directly, so those
call sites need their mocks moved in the same change and are left alone here.
@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

⚠️ Qwen Triage ended earlyview run. It stopped before finishing; check the run log.

⚠️ Qwen Triage 提前结束 —— 查看运行。未跑完,请查看运行日志。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR — the motivation here is real and well measured.

Template ✓ — all sections present, bilingual body complete.

Problem: observed, not theoretical. #10908 documents concrete numbers (release run 33713579913: cli spends 2223s collecting vs 1372s running; a package-root import costs ~11.5s before the first assertion vs ~2s for a per-module import). Quantified and reproducible from CI data.

Direction: aligned. CI collect-time is an active concern in this repo (#10908, and #10870 / #10869 landed in the same area this week). esbuild already resolves per-module imports through tsconfig paths while Vitest cannot — that gap is genuine, and closing it is the right move.

Size: ~6 production source lines (two import swaps in RemoteInputWatcher.ts and tipHistory.ts); the remaining ~340 changed lines are packages/cli/vitest.config.ts (test infrastructure). No size gate triggered.

Approach: the described change — ordered alias array, a wildcard mirroring the @qwen-code/qwen-code-core/* rule in tsconfig.json, package root kept as an exact match — is the right shape. But the diff carries materially more than the description says, and that's the concern:

  • The commit also drops settings that are live on main today: the RUNNER_NAME-conditioned ECS timeouts (60s) and maxWorkers: '25%', environment: 'node', the globalSetup fail-fast guard (Package-local unit tests do not run in a fresh checkout; the documented command fails with a misleading resolution error #9149, documented in AGENTS.md), the off-Linux dangerouslyIgnoreUnhandledErrors exemption, and the CI coverage gating. Several of those are pinned by scripts/tests/unit-vitest-configs.test.ts, and two run directly against this PR's own goal (jsdom for every file and always-on coverage both cost measurable wall time). The file in this commit looks like it may have been edited against an older snapshot of main — worth reconciling.
  • Four camelCase core aliases were removed (noFollowOpen, subSessionConstants, toolWriteOrigin, envVarResolver). The wildcard cannot derive them, and three still have live import sites on main (settings.ts, fast-path-settings.ts, workspace-registration-store.ts, acp-integration/service/filesystem.ts, serve/bridge-file-system-adapter.ts). The "four named subpaths" the description says must precede the pattern entry are actually at least seven.

Risk: no elevated-risk-path signals — none of the three changed files match the revert-correlated paths.

Flagging the scope mismatch before diving deeper — moving on to code review. 🔍

中文说明

感谢贡献!这个 PR 的动机是真实且有数据支撑的。

模板 ✓ —— 各节齐全,中英双语完整。

问题: 已观测到,而非理论问题。#10908 记录了具体数据(release run 33713579913:cli 模块收集 2223s 对比执行 1372s;包根导入到达第一条断言前约 11.5s,按模块导入约 2s)。量化且可从 CI 数据复现。

方向: 对齐。CI 收集耗时是仓库当前关注点(#10908,本周 #10870 / #10869 也落在同一领域)。esbuild 已通过 tsconfig paths 解析按模块导入,而 Vitest 不能——这个缺口真实存在,补上它是正确的。

规模: 约 6 行生产源码改动(两个文件的导入替换);其余约 340 行改动在 packages/cli/vitest.config.ts(测试基础设施)。未触发规模门槛。

方案: 描述中的改动——有序 alias 数组、对齐 tsconfig.json@qwen-code/qwen-code-core/* 规则的通配项、包根保持精确匹配——形态是对的。但 diff 实际携带的内容明显多于描述,这是顾虑所在:

  • 该提交同时删掉了当前 main 上生效的若干配置RUNNER_NAME 条件的 ECS 超时(60s)与 maxWorkers: '25%'environment: 'node'globalSetup 快速失败守卫(Package-local unit tests do not run in a fresh checkout; the documented command fails with a misleading resolution error #9149,AGENTS.md 有记载)、非 Linux 的 dangerouslyIgnoreUnhandledErrors 豁免、以及 CI 覆盖率门控。其中数项被 scripts/tests/unit-vitest-configs.test.ts 钉住,且有两项与本 PR 自身目标相悖(全量 jsdom 与常开覆盖率都会带来可测量的墙钟开销)。本提交里的这个文件看起来可能是基于较早的 main 快照编辑的——值得先对齐。
  • 删除了四个驼峰命名的 core alias(noFollowOpensubSessionConstantstoolWriteOriginenvVarResolver)。通配规则无法推导出它们,且其中三个在 main 上仍有活跃导入点(settings.tsfast-path-settings.tsworkspace-registration-store.tsacp-integration/service/filesystem.tsserve/bridge-file-system-adapter.ts)。描述中说"必须排在通配规则之前的四个具名 subpath",实际上至少有七个。

风险: 无高风险路径信号——三个改动文件均不命中与 revert 相关的路径。

先提出范围不一致的问题,再深入——进入代码审查。🔍

Qwen Code · qwen3.8-max

Reviewed at 8822672a847d87abfc463edb6ab3c380d072c044 · re-run with @qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

Independent baseline first: for "let tests resolve core modules individually", the minimal change is one wildcard alias mirroring the @qwen-code/qwen-code-core/* tsconfig rule, the package root kept as an exact match, every existing named alias left in place, plus one or two migrated files as proof — a ~30-line diff. The additive half of this PR matches that shape cleanly. The problem is everything else in packages/cli/vitest.config.ts.

Blockers

  1. Dropped aliases that still have consumers on main. noFollowOpen, toolWriteOrigin, and envVarResolver are removed, but five files on main import them (src/config/settings.ts, src/serve/fast-path-settings.ts, src/serve/workspace-registration-store.ts, src/acp-integration/service/filesystem.ts, src/serve/bridge-file-system-adapter.ts). The wildcard cannot derive these — it rewrites @qwen-code/qwen-code-core/envVarResolver to core/src/envVarResolver, while the file lives at core/src/utils/envVarResolver.ts (utils/no-follow-open.ts and services/tool-write-origin.ts likewise). Alias rewrites preempt normal resolution with no fallback, so every suite that transitively imports settings.ts fails at import time once this merges — exactly the "fails loudly at import time" behavior the PR description itself promises. (subSessionConstants has no import site on main, so dropping that one is fine.) The "four named subpaths" that must precede the pattern entry are actually seven.
  2. Settings deleted that are pinned by witness tests. scripts/tests/unit-vitest-configs.test.ts asserts, for the cli config: dangerouslyIgnoreUnhandledErrors === (platform !== 'linux') — the flag is deleted, so the pin fails on every platform (the exemption exists for the vitest RPC 60s-budget failure class behind Main CI failed: Qwen Code CI on 5ae363e2f906 #10438); and under RUNNER_NAME=ecs-qwen-parity the config must yield testTimeout 60000, hookTimeout 60000, maxWorkers '25%' — this PR hardcodes 15000 and deletes the rest. Both assertions fail deterministically; the scripts suite will be red.
  3. globalSetup fail-fast guard deleted. That guard is the documented fix for Package-local unit tests do not run in a fresh checkout; the documented command fails with a misleading resolution error #9149 (fresh clone / new worktree / deep clean) and is referenced in AGENTS.md's unit-test instructions. Removing it turns actionable setup errors into opaque import failures again. Nothing in the description mentions it.
  4. Two changes run directly against the PR's own perf goal. environment: 'node''jsdom' for every file — the comment on main records per-file jsdom costing 0.2–0.5s ("a tenth of the suite"), with DOM-needing files already opting in via // @vitest-environment jsdom pragmas; and coverage.enabled: true on every run — main gates coverage behind QWEN_CI_COVERAGE because v8 instrumentation costs about a fifth of suite wall time. Both make CI slower, which is the opposite of CI test time is bound by module import cost, not scheduling #10908. The new hardcoded minThreads: 8 / maxThreads: 16 also replaces the CPU-scaling default and the deliberate ECS '25%' cap ("ECS hosts run several jobs at once; leave capacity for neighboring jobs").
  5. The new import style doesn't resolve at runtime. @qwen-code/qwen-code-core/utils/debugLogger.js, /config/storage.js, /utils/atomicFileWrite.js resolve under Vitest (new wildcard alias) and in the bundle (esbuild reads tsconfig paths), but not under plain Node: core's exports map has no ./utils/* or ./config/* entry, and scripts/dev.js's loader intercepts only the exact package-root specifier. npm run dev hits ERR_PACKAGE_PATH_NOT_EXPORTED as soon as tipHistory (Tips / tipScheduler, TUI startup path) or RemoteInputWatcher loads. If path-style specifiers are the way forward, core's exports needs matching entries (and the two schemes reconciled); otherwise the named-subpath style is the established convention precisely because it resolves everywhere. The symbols themselves check out — createDebugLogger, Storage, and atomicWriteFileSync all exist at the target paths.

Minor

  • The drive-by comment edit in RemoteInputWatcher.ts goes the wrong way: useLlmStreamuseGeminiStream reverts to the pre-rename name; the hook is src/ui/hooks/use-llm-stream.js on main now. Unrelated to the alias change — suggest dropping it.
  • The overall shape — this commit's vitest.config.ts is an older state of main plus the alias array — suggests the file was edited against a stale snapshot. Rebasing alone won't reconstruct the intended diff; the deletion hunks need to be dropped, keeping only the alias restructuring and the two import migrations.

Testing

Evidence carried: the PR's own CI, read via the API at review time (unattended run — no PR code executed here). The unit suite is still in flight on this commit (~30-minute suite; not polling — the finalize job updates the table below once CI settles). Static read of scripts/tests/unit-vitest-configs.test.ts says the scripts suite fails on the deleted settings (blocker 2), and the dropped aliases should fail the cli suite at import time (blocker 1) — treat those as predictions until the run lands. Not verified: the collect-time improvement itself — it cannot be measured on this commit, since the diff re-adds the two biggest wall-time costs (jsdom-everywhere, always-on coverage) it set out to remove.

Final CI results for 8822672 (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Test (ubuntu-latest, Node 22.x) ❌ failure
Classify PR ✅ success
Dependency CVE audit ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Integration Tests (no-AK, No Sandbox) ✅ success
Lint & Static (ubuntu-latest, Node 22.x) ✅ success
OpenTUI no-flicker gate ✅ success
Secret scan (TruffleHog) ✅ success
TUI parity snapshots (ink vs opentui) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

Once the diff is reduced to the intended change, the perf claim's oracle is CI's own timing — the collect-vs-run split from #10908 — rather than a /verify A/B (which targets runtime behavior, not test-infrastructure timing).

中文说明

代码审查

先说独立基线:要让测试按模块解析 core,最小改动是加一条对齐 tsconfig @qwen-code/qwen-code-core/* 规则的通配 alias、包根保持精确匹配、其余具名 alias 原样保留,再迁移一两个文件作为验证——约 30 行的 diff。本 PR 的"加法"部分与这个形态完全吻合。问题出在 packages/cli/vitest.config.ts 里的其余内容。

阻塞项

  1. 删除了仍有消费者的 alias。 noFollowOpentoolWriteOriginenvVarResolver 被删除,但 main 上有五个文件在导入它们(src/config/settings.tssrc/serve/fast-path-settings.tssrc/serve/workspace-registration-store.tssrc/acp-integration/service/filesystem.tssrc/serve/bridge-file-system-adapter.ts)。通配规则无法推导出这些——它把 @qwen-code/qwen-code-core/envVarResolver 重写成 core/src/envVarResolver,而文件实际在 core/src/utils/envVarResolver.tsutils/no-follow-open.tsservices/tool-write-origin.ts 同理)。alias 重写优先于正常解析且无回退,因此合并后所有传递导入 settings.ts 的用例都会在导入期失败——正是 PR 描述自己承诺的"导入期直接报错"。(subSessionConstantsmain 上没有导入点,删它没问题。)"必须排在通配规则之前的具名 subpath"实际上是七个,不是四个。
  2. 删除了被见证测试钉住的配置。 scripts/tests/unit-vitest-configs.test.ts 对 cli 配置断言:dangerouslyIgnoreUnhandledErrors === (platform !== 'linux')——该标志被删除,此钉在所有平台都会失败(该豁免是为 Main CI failed: Qwen Code CI on 5ae363e2f906 #10438 背后的 vitest RPC 60 秒预算失败类而存在的);且在 RUNNER_NAME=ecs-qwen-parity 下配置必须给出 testTimeout 60000、hookTimeout 60000、maxWorkers '25%'——本 PR 硬编码 15000 并删除了其余项。两条断言都会确定性失败,scripts 套件会变红。
  3. globalSetup 快速失败守卫被删除。 该守卫是 Package-local unit tests do not run in a fresh checkout; the documented command fails with a misleading resolution error #9149(新克隆 / 新 worktree / 深度清理)的文档化修复,AGENTS.md 的单测说明里也引用了它。删除后,可操作的配置错误会重新变成难以理解的导入失败。描述中对此只字未提。
  4. 两处改动与本 PR 自身的性能目标直接相悖。 environment: 'node' → 全量 'jsdom'——main 上的注释记录每个文件的 jsdom 成本为 0.2–0.5 秒("十分之一的套件"),需要 DOM 的文件已通过 // @vitest-environment jsdom 声明按需启用;以及 coverage.enabled: true 全量开启——mainQWEN_CI_COVERAGE 门控覆盖率,因为 v8 插桩约占套件墙钟时间的五分之一。两者都让 CI 更慢,与 CI test time is bound by module import cost, not scheduling #10908 目标相反。新加的硬编码 minThreads: 8 / maxThreads: 16 还替换了按 CPU 伸缩的默认值和刻意的 ECS '25%' 上限("ECS 主机同时跑多个任务,要给相邻任务留容量")。
  5. 新导入风格在运行时无法解析。 @qwen-code/qwen-code-core/utils/debugLogger.js/config/storage.js/utils/atomicFileWrite.js 在 Vitest(新通配 alias)和打包(esbuild 读 tsconfig paths)下可解析,但在纯 Node 下不行:core 的 exports 映射没有 ./utils/*./config/* 条目,而 scripts/dev.js 的 loader 只拦截精确的包根说明符。npm run dev 会在 tipHistory(Tips / tipScheduler,TUI 启动路径)或 RemoteInputWatcher 加载时抛出 ERR_PACKAGE_PATH_NOT_EXPORTED。如果要以路径风格说明符作为方向,core 的 exports 需要补上对应条目(并统一两套方案);否则具名 subpath 风格之所以是既有约定,正是因为它在所有环境下都能解析。符号本身没问题——createDebugLoggerStorageatomicWriteFileSync 都存在于目标路径。

次要

  • RemoteInputWatcher.ts 里顺带的注释编辑方向反了:useLlmStreamuseGeminiStream 退回了改名前的旧名;main 上该 hook 现在是 src/ui/hooks/use-llm-stream.js。与 alias 改动无关——建议去掉。
  • 整体形态——本提交的 vitest.config.ts 等于"较早的 main 状态 + alias 数组"——表明该文件是基于过期快照编辑的。仅 rebase 无法重建预期 diff;需要删掉那些删除类 hunk,只保留 alias 重构和两个文件的导入迁移。

测试

证据说明:本节引用的是 PR 自身 CI 在审查时刻经 API 读取的真实状态(无人值守运行——此处未执行任何 PR 代码)。该提交的单测套件仍在运行(约 30 分钟;不轮询——finalize 任务会在 CI 结束后更新下表)。对 scripts/tests/unit-vitest-configs.test.ts 的静态阅读表明 scripts 套件会因被删配置而失败(阻塞项 2),被删 alias 应使 cli 套件在导入期失败(阻塞项 1)——在运行结果落地前请视为预测。未验证:收集耗时的改善本身——在本提交上无法测量,因为 diff 重新引入了它本要移除的两项最大墙钟开销(全量 jsdom、常开覆盖率)。

上表由 finalize 任务在 CI 结束后原地更新。待 diff 缩减为预期改动后,性能结论的判据是 CI 自身的时间数据(#10908 中"收集 vs 执行"的拆分),而非 /verify A/B(后者针对运行时行为,不针对测试基础设施耗时)。

Qwen Code · qwen3.8-max

Reviewed at 8822672a847d87abfc463edb6ab3c380d072c044 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 2/5 — the idea is right and the alias-array half is well done, but the diff as committed regresses several deliberate, recently-landed settings and breaks resolution in two places; it can't merge as-is.

Stepping back: the motivation is genuine — #10908's numbers are concrete, and the gap between esbuild (reads tsconfig paths) and Vitest (doesn't) is real. My independent proposal for this problem was almost exactly the additive half of this PR: one wildcard alias mirroring the tsconfig rule, the root kept exact, existing named aliases untouched, two files migrated as proof. That part here is clean and even comes with the right ordering rationale.

What I can't sign is the rest. This commit's vitest.config.ts is effectively an older snapshot of main with the alias array grafted on: it drops the ECS timeout/worker consideration, the node-default environment, the #9149 globalSetup guard, the coverage gating, and the unhandled-error exemption — settings that exist because of specific incidents (#10438, #9149) and are pinned by scripts/tests/unit-vitest-configs.test.ts precisely so they can't leave quietly. Two of the deletions (jsdom everywhere, always-on coverage) re-add the largest wall-time costs the suite carries, which is the opposite of what a perf PR for CI time should ship. The dropped noFollowOpen / toolWriteOrigin / envVarResolver aliases still have live import sites on main, and the new path-style imports don't resolve under npm run dev (no matching core exports entries; the dev loader only intercepts the package root). In six months this merge would be the commit everyone bisects their red lane back to.

The fix is small: rebuild the commit on current main, keep only the alias restructuring (with all seven non-derivable named entries ahead of the wildcard), the two import migrations, and decide the runtime-resolution question — either add core exports entries for the path style or migrate to the existing named-subpath style. Happy to re-review as soon as that lands.

Qwen Code · qwen3.8-max

Reviewed at 8822672a847d87abfc463edb6ab3c380d072c044 · 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 Needs some rethinking before this can merge — full notes in the review comment above. The short version: the alias-array change itself is good, but this commit also drops settings that are live on main (ECS timeouts/maxWorkers, node-default environment, the #9149 globalSetup guard, coverage gating, the unhandled-error exemption — several pinned by scripts/tests/unit-vitest-configs.test.ts), removes three core aliases that still have import sites, and the new path-style imports don't resolve under npm run dev (no core exports entries). Rebuilding the commit on current main with only the alias restructuring + the two migrations should get it home. 🙏

中文说明

合并前需要重新考虑——完整说明见上方审查评论。简版:alias 数组改动本身没问题,但本提交同时删掉了 main 上生效的配置(ECS 超时/maxWorkers、node 默认环境、#9149 的 globalSetup 守卫、覆盖率门控、未处理错误豁免——其中数项被 scripts/tests/unit-vitest-configs.test.ts 钉住),移除了三个仍有导入点的 core alias,且新的路径风格导入在 npm run dev 下无法解析(core exports 没有对应条目)。基于当前 main 重建提交,只保留 alias 重构和两个文件的迁移,应该就能过。🙏

The previous commit was assembled from a working copy that predated main by
several weeks, so it silently reverted this file to that older state. Four
named core subpaths added since — envVarResolver, noFollowOpen,
subSessionConstants and toolWriteOrigin — disappeared with it, and the new
wildcard then claimed those specifiers and pointed them at files that do not
exist. 257 test files failed to load as a result.

All eight named subpaths are restored and kept ahead of the wildcard, with a
comment saying why that order matters and what a contributor adding a ninth
has to do. None of the eight can be derived from its specifier, so none of
them can be folded into the pattern.

The two migrated source files are rebuilt on their current contents for the
same reason; one of them had also been reverted by a line.
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

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

Screenshots · before / after

⚠️ One or more scenarios failed to render on this head, so this preview may be missing views — see the workflow run. The composites below are the scenarios that did render.

workflow-page-running-dark before/after

workflow-page-running-light before/after

workflow-page-saved-dark before/after

workflow-page-saved-detail-dark before/after

workflow-page-saved-detail-light before/after

workflow-page-saved-light before/after

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

Qwen Code · web-shell visuals

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head 8b3d0b0, drove a fixed endpoint set against each, and diffed the JSON responses. Only fields that changed are shown.

No response changes against the PR base across 12 scenario(s).

Qwen Code · serve A/B

@yiliang114

Copy link
Copy Markdown
Collaborator Author

Cross-linking the plan: #10909 is the document this PR is phase ① of, and it names this exact step.

Its §6.1 (「迁移写法(已确定)」) records the resolution chain and then says the wildcard alias has to come first — "vitest alias:目前只有 4 个具名 subpath …… 没有通配。深路径导入今天在测试里会解析失败,必须先补一条通配 alias,这是 phase ① 的第一步". This PR is that step. It also confirms the §6.1 table under vitest rather than only from the tsconfig rule: ${CORE_SRC}/$1 resolves to packages/core/src/*, so the test chain lands on core's TypeScript sources and never on dist/ — which is the property §6.2 shows is load-bearing, since a /dist/... specifier misses the paths rule, falls back to exports, and produces two copies of the same module in the bundle.

I checked this PR against §6.3, the plan's largest correctness risk, and it is clean — but the reason is worth writing down.

§6.3's failure mode is that a test which mocks the core barrel silently stops intercepting once the code under test imports deeply: the suite stays green while testing something else. On this branch 138 cli test files carry vi.mock('@qwen-code/qwen-code-core', …). Of those, exactly one also references either migrated module:

  • packages/cli/src/ui/opentui/opentui-runtime.test.ts — mocks the barrel at line 54 (overriding createDebugLogger and writeRuntimeStatus) and references RemoteInputWatcher.

It is safe, and not by luck of ordering: line 42 replaces the whole ../../remoteInput/RemoteInputWatcher.js module with a stub class, so whether that module reaches createDebugLogger through the barrel or through a subpath never enters the test. The barrel mock there exists for opentui-runtime.ts's own imports, and this PR does not touch that file. The two mocks are disjoint.

The general rule that follows, for whoever writes the codemod: a barrel mock only breaks when the migrated module is the one the mock was meant to intercept through. A module that is itself stubbed is inert regardless of how it imports. That distinction is what makes the 138 tractable — the scope is not "138 files to audit" but "the subset whose barrel mock is intended to reach into a migrated module", and it should be computed per migration batch rather than up front.

Why the payoff is worth pushing on. #10909's collect > tests evidence comes from release run 33713579913. The same signature hit #10910's unit lane today (job 100630658328, ecs-qwen-hk3-16), cancelled at the 120-minute cap:

Duration 5013.58s (transform 549.37s, setup 1235.22s, collect 17994.55s, tests 6333.43s, ...)

collect at 2.8x tests, packages/cli alone taking 83 of the 120 minutes, and both of that run's two failures import-time rather than assertion-time (voice-keyterms-race.test.ts timed out at 20s inside a beforeAll whose only statement is a dynamic import). The host was not short of anything but CPU — DFSAMPLE showed load ~290 with 114–187 concurrent vitest processes on 128 cores, ~160 GB memory free, disk at 41% — and collect is the phase that starves. Raising caps, which is what #10915 and #10931 do, keeps those runs alive; this PR is the one that makes them cheaper.

Refs #10908, #10909.

中文说明

互相链接一下计划侧:#10909 就是本 PR 所属的那份文档的 phase ①,而它点名了这一步。

其 §6.1(「迁移写法(已确定)」)记录了解析链,然后说明通配 alias 必须先做 —— 「vitest alias:目前只有 4 个具名 subpath …… 没有通配。深路径导入今天在测试里会解析失败,必须先补一条通配 alias,这是 phase ① 的第一步」。本 PR 就是这一步。它同时在 vitest 下印证了 §6.1 的表格,而不只是从 tsconfig 规则推导:${CORE_SRC}/$1 解析到 packages/core/src/*,因此测试链落在 core 的 TypeScript 源码上、永不落到 dist/ —— 而这正是 §6.2 证明为关键的性质,因为 /dist/... 说明符匹配不到 paths 规则,会回落到 exports,从而在 bundle 里产生同一模块的两份副本。

我拿本 PR 对着 §6.3(该计划最大的正确性风险)核对过,结论是干净的 —— 但原因值得写下来。

§6.3 的失效模式是:一旦被测代码改成深路径导入,mock 了 core barrel 的测试就会静默失去拦截,套件仍然是绿的,但测的东西变了。在本分支上,有 138 个 cli 测试文件带 vi.mock('@qwen-code/qwen-code-core', …)。其中恰好只有一个同时引用了两个迁移模块之一:

  • packages/cli/src/ui/opentui/opentui-runtime.test.ts —— 在第 54 行 mock 了 barrel(覆盖 createDebugLoggerwriteRuntimeStatus),并且引用了 RemoteInputWatcher

它是安全的,而且不是靠顺序上的巧合:第 42 行把 ../../remoteInput/RemoteInputWatcher.js 整个模块替换成了一个 stub class,因此该模块究竟是经 barrel 还是经 subpath 拿到 createDebugLogger,根本不进入这个测试。那里的 barrel mock 是为 opentui-runtime.ts 自己的导入而存在的,而本 PR 没有触碰那个文件。两个 mock 互不相交。

由此得出的一般规则,供写 codemod 的人参考:barrel mock 只有在「被迁移的模块正是该 mock 意图经由其进行拦截的那个模块」时才会失效。 一个自身已被整体 stub 的模块,无论怎么导入都是惰性的。正是这个区分让 138 这个数字变得可处理 —— 范围不是「138 个文件要审」,而是「其 barrel mock 意图伸进某个被迁移模块的那个子集」,并且应当按每一批迁移分别计算,而不是一次性预估。

为什么这个收益值得推进。 #10909collect > tests 证据来自 release run 33713579913。同样的特征今天出现在 #10910 的单测通道(job 100630658328ecs-qwen-hk3-16),该 job 在 120 分钟上限被取消:

Duration 5013.58s (transform 549.37s, setup 1235.22s, collect 17994.55s, tests 6333.43s, ...)

collecttests2.8 倍packages/cli 单独吃掉 120 分钟里的 83 分钟,而那次运行的两个失败都发生在导入期而非断言期(voice-keyterms-race.test.ts 在一个唯一语句是动态 importbeforeAll 里 20 秒超时)。那台主机除了 CPU 之外什么都不缺 —— DFSAMPLE 显示 128 核上 load 约 290、并发 114–187 个 vitest 进程,内存空闲约 160 GB,磁盘 41% —— 而被饿死的正是 collect 阶段。抬高上限(#10915#10931 做的事)能让这些运行活下来;而本 PR 是让它们变便宜的那一个。

Refs #10908#10909

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not explored to full depth (tool budget reached): "agent 2": none.**.

中文说明

未探索到全部深度(达到工具调用预算):"agent 2"none.**

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

Comment thread packages/cli/src/services/tips/tipHistory.ts Outdated
Comment thread packages/cli/src/remoteInput/RemoteInputWatcher.ts Outdated
Comment thread packages/cli/vitest.config.ts
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Note on how this relates to the rest.

#10957 now carries the full change against main, this PR's contents included, and is green across the unit suite, lint, the integration gate and Serve A/B. #10946 and #10956 are closed — their branches still held the pre-revert set, which a full run showed breaks 16 suites.

This one stays open because it is the smallest reviewable piece and is independently correct: the resolver mapping plus two files as a smoke test. Merging it first simply shrinks #10957. Merging #10957 first makes this empty. Either order works.

Worth carrying across from what the larger run found: this PR's own checks are not the whole story either. A PR based on anything other than main or release/** runs no unit suite and no lint here, so "seven checks passing" on a stacked PR means considerably less than it looks.

中文说明

关于本 PR 与其余部分的关系。

#10957 现已基于 main 承载全部改动(含本 PR 内容),并在单元套件、lint、集成门禁与 Serve A/B 上全绿。#10946#10956 已关闭——它们的分支仍是回退前的内容,而完整运行证明那会挂掉 16 个用例。

本 PR 保持开启,因为它是最小的可审阅单元且本身正确:解析映射加两个文件作为冒烟验证。先合本 PR 只会让 #10957 的 diff 缩小;先合 #10957 则本 PR 变空。两种顺序都可以。

另外值得带走的一点:本 PR 自己的检查同样不能说明全部问题。在此仓库,base 不是 mainrelease/** 的 PR 不会运行单元测试和 lint,所以叠加 PR 上「七项检查通过」的含金量远低于它看起来的样子。

yiliang114 and others added 2 commits September 4, 2026 05:02
The tipHistory / RemoteInputWatcher migration used .js-suffixed subpath
specifiers that match no entry in packages/core/package.json exports, so
the built-but-unbundled CLI (npm start / build-and-start, whose tsc dist
keeps specifiers) crashed at module load with ERR_PACKAGE_PATH_NOT_EXPORTED
while typecheck (tsconfig paths), unit tests (vitest wildcard alias) and the
bundle (esbuild paths) all bypassed exports and stayed green. Switch the two
files to named subpaths (storage, atomicFileWrite, debugLogger), following
the convention of the eight existing entries, and add the matching exports
entries and vitest aliases.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtlz4ubdgq
Every core subpath specifier statically imported from packages/cli/src must
resolve through packages/core/package.json exports in a real child node
process — no vitest aliases, no tsconfig paths. Without the matching exports
entries the built-but-unbundled CLI dies with ERR_PACKAGE_PATH_NOT_EXPORTED
while every gate that bypasses exports stays green; this guard goes red the
moment an entry is removed.

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

@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 — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

中文说明

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

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

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

Comment thread packages/cli/src/remoteInput/RemoteInputWatcher.ts
Comment thread scripts/tests/core-subpath-exports-resolution.test.js Outdated
Comment thread scripts/tests/core-subpath-exports-resolution.test.js Outdated
The named subpaths cli sources import from @qwen-code/qwen-code-core had
no named `paths` entries, so the wildcard composed nonexistent files and
esbuild (bundle) plus the dev loader chain fell back to the exports map,
loading packages/core/dist copies while every package-root import loads
packages/core/src — two instances of barrel-exported, stateful modules
(debugLogger, storage, atomicFileWrite, envVarResolver, toolWriteOrigin,
memoryScopes) in one process. Add the six missing named entries beside the
existing ones so bundle and dev resolve all subpaths into the core src
tree, consistent with the vitest alias list.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtme4yqxhl
Complete the resolution guard and harden its probe:

- Cover every core subpath statically imported from packages/cli/src
  (adds toolWriteOrigin and memoryScopes) plus the subpaths npm start
  reaches through @qwen-code/acp-bridge (subSessionConstants, goalWire,
  transcriptRecords), instead of the previous five specifiers.
- Pin each specifier to its expected dist target: assert the resolved
  URL equals the pinned path and the target file exists, so a typo'd or
  redirected exports target fails the guard (import.meta.resolve alone
  accepts both). This makes a built core dist a prerequisite, which
  vitest-global-setup already fail-fasts on.
- Add a bundle-resolution guard: esbuild-bundle every cli/src core
  subpath under packages/cli/tsconfig.json and assert no input comes
  from packages/core/dist, pinning the tsconfig paths entries.

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

@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 — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not explored to full depth (tool budget reached): "agent 4": none — all planned checks completed within budget (~7 tool calls)..

Convergence: round 3 posted 3 inline comment(s), 3 of them reported for the first time; the previous round posted 3 (3 new). Findings keep coming back to the same files: scripts/tests/core-subpath-exports-resolution.test.js (findings in round 2; 3 more now). The rate of new findings is not falling. A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. Batching the remaining fixes and verifying them before the next push, or dropping this PR's reviews to --severity-floor critical, keeps the loop from re-deriving the same set. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

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

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

未探索到全部深度(达到工具调用预算):"agent 4"none — all planned checks completed within budget (~7 tool calls).

收敛情况:第 3 轮发布了 3 条行内评论,其中 3 条是首次提出;上一轮发布了 3 条(其中 3 条首次提出)。发现反复回到同一批文件:scripts/tests/core-subpath-exports-resolution.test.js(第 2 轮已出过发现,本轮又有 3 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,或将本 PR 的评审降到 --severity-floor critical,可以避免循环反复推导同一组发现。(仅为观察——本轮评审未因此扣留任何内容。)

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

Comment thread scripts/tests/core-subpath-exports-resolution.test.js Outdated
Comment thread scripts/tests/core-subpath-exports-resolution.test.js
Comment thread scripts/tests/core-subpath-exports-resolution.test.js Outdated
- Add goalWire to the bundle arm and named tsconfig paths entries in
  packages/cli and packages/acp-bridge: acp-bridge's transcript-replay
  imports @qwen-code/qwen-code-core/goalWire, and without a named entry
  the wildcard resolved to a nonexistent ../core/src/goalWire and fell
  back to the exports map, bundling packages/core/dist/src/goals/
  goal-wire.js next to src-resolved core modules — the module-identity
  split #10908's Known risks name. Verified with an esbuild metafile
  probe: dist input before the paths entry, src input after.
- Normalize esbuild metafile input keys to forward slashes before the
  dist-leak filter and src-target assertions so the guard behaves the
  same on Windows runners, where esbuild emits backslash separators.
- Correct the header: this lane's vitest config does not wire
  scripts/vitest-global-setup.js (it is a globalSetup only in the
  packages/core and packages/cli configs, and its DIST_PREREQUISITES
  has no key covering this lane), so a missing dist surfaces as the
  existence assertion naming the absent file.

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

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed. Suggestions are inline.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Convergence: round 4 posted 1 inline comment(s), 1 of them reported for the first time; the previous round posted 3 (3 new). Findings keep coming back to the same files: scripts/tests/core-subpath-exports-resolution.test.js (findings in round 3; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. No Critical finding is open on this round, so merging and moving the remaining Suggestion threads to a follow-up issue is available as an ending — a merged pull request cannot diverge further. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

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

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

收敛情况:第 4 轮发布了 1 条行内评论,其中 1 条是首次提出;上一轮发布了 3 条(其中 3 条首次提出)。发现反复回到同一批文件:scripts/tests/core-subpath-exports-resolution.test.js(第 3 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。本轮没有未决的 Critical,因此"合入后把剩余 Suggestion 线程转到后续 issue"是一个可选的结束方式——已合入的 PR 不会继续发散。(仅为观察——本轮评审未因此扣留任何内容。)

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

Comment thread scripts/tests/core-subpath-exports-resolution.test.js
The bundle arm seeded goalWire into the cli-routed buildSync case, which
passes an explicit packages/cli/tsconfig.json — but the shipped bundle
resolves that import under packages/acp-bridge/tsconfig.json (mainBuild in
esbuild.config.js carries no tsconfig option, so esbuild discovers the
nearest tsconfig per importing file, and goalWire is imported only from
packages/acp-bridge/src/transcript-replay.ts). The acp-bridge goalWire
paths entry was therefore guarded by no arm, and transcriptRecords /
subSessionConstants were probed by none at all: removing the acp-bridge
goalWire entry left the test green while a production-shaped build pulled
packages/core/dist inputs.

Add an acp-bridge-routed buildSync case — no tsconfig option, resolveDir
packages/acp-bridge/src, seeding goalWire, transcriptRecords,
subSessionConstants and noFollowOpen — asserting no input lands under
packages/core/dist and each expected core src target is present. With the
new arm, removing the acp-bridge goalWire entry goes red (4 dist leaks)
where previously nothing did.

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

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

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

  • R5-1 hand-maintained specifier coverage maps — already reported (R2-1 thread, comment 3929721370)

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not explored to full depth (tool budget reached): "agent 1a": executing scripts/tests/core-subpath-exports-resolution.test.js itself — the review worktree has no node_modules and a full monorepo npm ci + build exceed….

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

  • scripts/tests/core-subpath-exports-resolution.test.js:103 — [test] D5-1 missing core dist surfaces as a cryptic URL-spelling mismatch instead of the header's promised named-file diagnostic; the existsSync line is unreachable
中文说明

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

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

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

未探索到全部深度(达到工具调用预算):"agent 1a"executing scripts/tests/core-subpath-exports-resolution.test.js itself — the review worktree has no node_modules and a full monorepo npm ci + build exceed…

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

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

Take the union of core package exports (storage/atomicFileWrite/
debugLogger from this branch plus conversationsRuntimeMarker from
main) and register the conversationsRuntimeMarker vitest alias ahead
of the wildcard.

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

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

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

  • derive-the-specifier-list recommendation (scripts/tests/core-subpath-exports-resolution.test.js) — already reported (R2-1 thread, comment 3929721370)

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

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

  • scripts/tests/core-subpath-exports-resolution.test.js:209 — [review] D6-1 esbuild probe-and-assert block duplicated across the two guard arms; a future leak-filter/normalization change mirrored into only one arm leaves the other silently pa…
中文说明

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

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

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

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

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

Comment thread scripts/tests/core-subpath-exports-resolution.test.js Outdated
Comment thread scripts/tests/core-subpath-exports-resolution.test.js
yiliang114 and others added 4 commits September 5, 2026 05:49
R6-1: @qwen-code/qwen-code-core/conversationsRuntimeMarker is
statically imported from packages/cli/src (config/shared-env-keys.ts,
serve/run-qwen-serve.ts) but carried no named entry in
packages/cli/tsconfig.json paths and was seeded into neither guard
map. The wildcard composed a nonexistent ../core/src target and fell
back to the exports map, so the cli-routed bundle loaded a
packages/core/dist copy next to the core src copy while every guard
arm stayed green. Add the named paths entry beside the ones this PR
already adds and seed both guard maps with the specifier.

Co-authored-by: Qwen-Coder <qwen-coder@alibaba-cloud.com>
Patrol-Run: qwen-pr-closeout/jmtngpuscjl
R6-2: the cli bundle reaches sdk sources through the cli tsconfig
@qwen-code/sdk/* mapping (ui/utils/export/export-transcript-document.ts
imports @qwen-code/sdk/daemon/transcript, which re-exports from
daemon/ui/chat-record-transcript.ts), and chat-record-transcript.ts
imports @qwen-code/qwen-code-core/transcriptRecords. With no paths in
packages/sdk-typescript/tsconfig.json, mainBuild's per-importing-file
tsconfig discovery found no mapping and fell back to the exports map,
bundling a packages/core/dist copy while the guard suite stayed green.
Add the named entry to the sdk tsconfig, mirroring acp-bridge, and a
third guard arm probing the sdk route.

Co-authored-by: Qwen-Coder <qwen-coder@alibaba-cloud.com>
Patrol-Run: qwen-pr-closeout/jmtngpuscjl
The paths entry added for the sdk-routed guard resolves
@qwen-code/qwen-code-core/transcriptRecords to the core source file,
which in the composite tsc --build graph belongs to the core project.
Without a project reference the cli build failed with TS6059/TS6307;
declare the core reference, mirroring packages/acp-bridge/tsconfig.json.
Verified with npm run build in packages/cli and npm run typecheck in
packages/sdk-typescript.

Co-authored-by: Qwen-Coder <qwen-coder@alibaba-cloud.com>
Patrol-Run: qwen-pr-closeout/jmtngpuscjl
The paths entry added to tsconfig.json for the bundle's esbuild
discovery is inherited by tsconfig.build.json; in that plain
declaration build it pulled core sources into the program, re-rooted
the inferred rootDir above the package, and nested every emitted .d.ts
under dist/sdk-typescript/src/, dropping dist/daemon/index.d.ts that
web-shell imports (TS7016). Reset paths in tsconfig.build.json so the
declaration build resolves core through its exports map. Verified with
npm run build in packages/sdk-typescript: dist/daemon/index.d.ts
restored and the daemon browser bundle byte-identical (236252 bytes,
its pre-existing warning threshold breach is unchanged).

Co-authored-by: Qwen-Coder <qwen-coder@alibaba-cloud.com>
Patrol-Run: qwen-pr-closeout/jmtngpuscjl

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

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

  • the derive-the-specifier-list recommendation for the guard's hand-maintained maps (this round's instance: userPromptSubmitContext probed by no arm) — already reported (R2-1 thread, comment 3929721370)

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

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

  • packages/sdk-typescript/src/daemon/ui/chat-record-transcript.ts:11 — [probe] D7-1 sdk-routed acp-bridge subpath imports bundle dist copies while cli-routed imports bundle src — transcript-replay/bridgeTypes/mcpTimeouts doubled in the shippe…
  • packages/sdk-typescript/tsconfig.reference.json:8 — [review] D7-2 added comment justifies the ../core reference with a paths inheritance the same diff removes ("paths": {} in tsconfig.build.json); tsc --showConfig reports paths: {}
  • scripts/tests/core-subpath-exports-resolution.test.js:241 — [review] D7-3 the three esbuild guard arms are near-verbatim copies of one ~35-line block; a shared-logic fix must be applied in three places by hand
  • scripts/tests/core-subpath-exports-resolution.test.js:134 — [review] D7-4 the cli-routed arm probes goalWire through packages/cli/tsconfig.json but no production route uses it; the dead cli entry and the probe pin each other and block clean…
  • packages/sdk-typescript/tsconfig.build.json:18 — [review] D7-5 the "paths": {} reset guarding the sdk declaration dist layout has no witness; a regression ships broken ./daemon types with every lane green
  • scripts/tests/core-subpath-exports-resolution.test.js:108 — [review] D7-6 the header promises the existence assertion names the absent file, but the failure output carries no path

[Critical] R1-2: [fails-closed] [regression] The round-2 fix (b5465637b) closed this thread's bundle lane but the dev lane still stands, verified by probe at this commit: scripts/dev.js's generated loader intercepts only the exact package root ('@qwen-code/qwen-code-core'), and Node never consults the tsconfig paths entries that fix added, so under npm run dev the migrated subpath imports — '@qwen-code/qwen-code-core/debugLogger' in RemoteInputWatcher.ts, '/storage' and '/atomicFileWrite' in tipHistory.ts — fall through to the exports map and load packages/core/dist/** while every package-root import loads packages/core/src. Two instances of debugLogger, storage and atomicFileWrite in one dev process. Observable impact: Config binds the debug session on the src copy (setDebugLogSession(this), packages/core/src/config/config.ts), so RemoteInputWatcher's dist-copy REMOTE_INPUT logger reads an empty session and every debugLogger(...) call there silently no-ops whenever QWEN_DEBUG_LOG_FILE is enabled — it worked before this diff migrated the files. Storage carries split static state (runtimeBaseDir, AsyncLocalStorage), latent today only because tipHistory happens to call the stateless getGlobalQwenDir(). This violates the R1-1 thread's acceptance constraint (comment 3925934275). Witness: a dev-chain probe (loader replicating scripts/dev.js verbatim, run at HEAD) resolved ROOT to packages/core/index.ts (src) and SUBPATH to packages/core/dist/src/utils/debugLogger.js, with 'same module instance? false' and the REMOTE_INPUT (dist) line ABSENT from the session log (silently no-op'd); the flip check with the loader intercepting the subpath into src resolved SUBPATH to packages/core/src/utils/debugLogger.ts with 'same module instance? true' and both log lines written. Fix: extend scripts/dev.js's loader resolve hook so the core subpath specifiers short-circuit to the matching packages/core/src files, or revert the two migrated files to package-root imports and land the migrations together with the scheme reconciliation; keep the new exports entries (the built-but-unbundled CLI needs them) and the tsconfig paths entries (the bundle lane).

中文说明

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

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

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

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

[Critical] R1-2: [fails-closed] [regression] The round-2 fix (b5465637b) closed this thread's bundle lane but the dev lane still stands, verified by probe at this commit: scripts/dev.js's generated loader intercepts only the exact package root ('@qwen-code/qwen-code-core'), and Node never consults the tsconfig paths entries that fix added, so under npm run dev the migrated subpath imports — '@qwen-code/qwen-code-core/debugLogger' in RemoteInputWatcher.ts, '/storage' and '/atomicFileWrite' in tipHistory.ts — fall through to the exports map and load packages/core/dist/** while every package-root import loads packages/core/src. Two instances of debugLogger, storage and atomicFileWrite in one dev process. Observable impact: Config binds the debug session on the src copy (setDebugLogSession(this), packages/core/src/config/config.ts), so RemoteInputWatcher's dist-copy REMOTE_INPUT logger reads an empty session and every debugLogger(...) call there silently no-ops whenever QWEN_DEBUG_LOG_FILE is enabled — it worked before this diff migrated the files. Storage carries split static state (runtimeBaseDir, AsyncLocalStorage), latent today only because tipHistory happens to call the stateless getGlobalQwenDir(). This violates the R1-1 thread's acceptance constraint (comment 3925934275). Witness: a dev-chain probe (loader replicating scripts/dev.js verbatim, run at HEAD) resolved ROOT to packages/core/index.ts (src) and SUBPATH to packages/core/dist/src/utils/debugLogger.js, with 'same module instance? false' and the REMOTE_INPUT (dist) line ABSENT from the session log (silently no-op'd); the flip check with the loader intercepting the subpath into src resolved SUBPATH to packages/core/src/utils/debugLogger.ts with 'same module instance? true' and both log lines written. Fix: extend scripts/dev.js's loader resolve hook so the core subpath specifiers short-circuit to the matching packages/core/src files, or revert the two migrated files to package-root imports and land the migrations together with the scheme reconciliation; keep the new exports entries (the built-but-unbundled CLI needs them) and the tsconfig paths entries (the bundle lane).

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

@yiliang114

Copy link
Copy Markdown
Collaborator Author

CI attribution for head 4653a34 (Test ubuntu-latest cancelled after exactly 2h0m, run 33924707799): the 'Run tests and generate reports' step was cap-killed with no failing suite output — the shared self-hosted runner saturation shape seen on this branch before (vitest RPC timeouts under load, job hits its 2h ceiling). Not PR-caused; no diff files implicated. Triggering a --failed rerun.

@yiliang114

Copy link
Copy Markdown
Collaborator Author

CI attribution for Test (ubuntu-latest) attempt-2 failure on head 4653a34 (job 101218231243, run 33924707799): shared-runner disk pressure, not this PR. The job uploaded the runner's own disk-pressure-run-33924707799-attempt-2.zip artifact, the log carries ENOSPC mid-suite markers, and the trailing suites all passed (no failing vitest summary anywhere in the log) before the step exited 1 at 02:19Z. Re-running the failed job.

@yiliang114

Copy link
Copy Markdown
Collaborator Author

Test ubuntu attempt-3 attribution (run 33924707799): not PR-caused.

  • The junit report for the leg is fully green: 57885 passed, 0 failed, 100 skipped across packages/cli (28491), packages/core (23412), packages/web-shell (5982). No vitest failure summary appears anywhere in the log.
  • The step still exited 1 after the last suite batch (04:43:47Z suites done, diagnostics dump, ##[error]Process completed with exit code 1 at 04:43:48Z).
  • Runner host state at failure: load average 177.52 / 188.99 / 184.65 with hosttests[66] (66 concurrent test jobs on the shared pool). Disk and memory were fine at that moment (555G free, 179G MemAvailable), so this is contention on the shared pool, same lineage as attempt-2's mid-suite ENOSPC on the same pool (see prior comment).

Rerunning the failed jobs once more; if attempt-4 repeats the all-green-but-exit-1 shape, this leg needs the runner pool looked at, not another rerun.

@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run

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

Scripted assertions: 263 passed · 0 failed · 263 total

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

中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)

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

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

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

Verification report

PR #10917 — deep verification

Verdict: findings — 263 scripted assertions executed, 263 pass / 0 fail.
Verified head: 4653a3452a233da2a292f93708c68c2ed5c8edf5 (git rev-parse HEAD^2).
Merge-ref base tip used for every A/B control: 74fe3a659dde2859f152d6c860e04cfddca86d05 (HEAD^1).
Diff under test: 10 files, +532 / −153.

The central claim is proven load-bearing: 5 of 16 probe specifiers flip from unresolvable to resolved between the base and head alias configs, and nothing else moves. The perf premise is confirmed by measurement (11.30 s package-root premium vs 0.39 s for one module; 15.34 s saved on the migrated files' own suites). No regression reproduced. The findings below are all latent, coverage, or documentation items — none is a live break, and each is bounded with what does not hold.

中文摘要

结论:findings —— 共执行 263 条脚本化断言,263 通过 / 0 失败。验证 head:4653a34;A/B 对照基线为 merge-ref 的 base tip 74fe3a6HEAD^1)。

A/B 结论(核心主张成立):见下文「Central claim + A/B」表。base 与 head 两套 alias 配置下跑同一组 16 个探针 import,storageatomicFileWritedebugLogger 以及两个「只有通配规则才能解析」的 specifier 共 5 项由失败转为成功,其余 11 项两侧一致。四个变异臂进一步证明:具名条目必须排在通配条目之前(把通配前移后 12 个具名 subpath 全部解析到不存在的 core/src/<name>);而把包根改回字符串写法但保持原位时,16 个 specifier 的解析结果与 head 完全相同。

性能前提(实测支持):固定开销 2.95 s;单个 core 模块边际成本 0.39 s;包根边际成本 11.30 s(28.8 倍),与 PR 描述的「约 11.5 s / 约 2 s」吻合。两个迁移文件自身的 4 个用例:head 17.72 s vs 还原成 barrel 导入后 33.06 s,省 15.34 s,两侧均全绿。

Findings(均为潜在/覆盖率/文档问题,无线上破坏)

  1. 本 PR 的头条机制——vitest 的通配(pattern)条目——没有任何测试钉住它。删掉它后,两个迁移文件的用例仍 37/37 全绿,新守卫测试仍 14/14 全绿。
  2. 通配条目让「测试路径」接受的范围超过 packages/core/package.json exports 在运行时承认的范围。实测 core/utils/promptIdContextcore/config/models 在 vitest 下可解析,而 plain Node 抛 ERR_PACKAGE_PATH_NOT_EXPORTED。新守卫测试正是为此而写,但它的 specifier 列表是硬编码的(11 条),不是从源码推导的;今天 0 处不符,但下一批迁移可能悄悄踩中。已给出实测过的修复补丁。
  3. packages/sdk-typescript/tsconfig.reference.json 新增 references 的注释声称缺少它会导致 TS6059/TS6307 —— 该前提被同一个 PRtsconfig.build.json"paths": {})取消了。实测 tsc --showConfig 得到 paths: {},删掉 referencestsc --build --force 仍 exit 0、无任何 TS 错误。
  4. PR 描述里的数字与实际不符:说 alias 列表「只列了四个 subpath」,而 base tip 上有九个;Reviewer Test Plan 说「四个具名 subpath 必须排在通配前」,而 head 上有十二个。(结论本身正确,只是计数错。)
  5. 描述称两个迁移文件用于「端到端验证」通配规则;实测这三个 specifier 都各自获得了具名条目,因此这两个文件从未走过通配规则。

未覆盖范围:逐 commit 归因(快照列 14 个 commit,本地 shallow 只可达 1 个);完整 cli/core 套件;npm run bundle;macOS/Windows;与当前 main 的试合并;references 条目在 core dist 缺失的全新克隆上是否必要(需销毁 64 MB core dist,未做)。详见「Not covered」。

Scope

Central claim — the cli vitest alias list, restructured as an ordered array carrying a pattern entry, makes per-module core subpath imports resolve under test where they previously could not; the two migrated files behave identically.

Secondary claims

  • S1 — the three new exports entries plus the named tsconfig paths entries keep the other two resolution routes (plain-Node npm start, esbuild bundle) correct; guarded by the new scripts/tests/core-subpath-exports-resolution.test.js.
  • S2 — the sdk tsconfig changes keep the declaration build's dist layout stable, and the composite reference build needs references: [{path:"../core"}].
  • S3 — per-file module-collection cost drops from ~11.5 s (package root) to ~2 s (single module).

Explicitly out of scope: the full cli suite (1011 test files), the full core suite, npm run bundle, non-Linux platforms, the remaining ~555-file migration.

Central claim + A/B

h1-alias-ab.mjs ran 16 probe specifiers × 6 alias arms end to end through vitest (one probe file per specifier, real static imports — exactly what a migrated source file does). Only packages/cli/vitest.config.* differs between arms; the base arm is git show HEAD^1:packages/cli/vitest.config.ts byte-identically (verified by diff). Witness: 01-ab-six-arm-alias-matrix.png.

probe specifier group head base root-string-first root-string-in-place wildcard-first no-wildcard
core (bare root) root pass pass pass pass pass pass
9 subpaths named on base¹ namedOnBase pass pass fail pass fail pass
core/storage newOnHead pass fail fail pass fail pass
core/atomicFileWrite newOnHead pass fail fail pass fail pass
core/debugLogger newOnHead pass fail fail pass fail pass
core/utils/promptIdContext wildcardOnly pass fail fail pass pass fail
core/config/models wildcardOnly pass fail fail pass pass fail
core/src/utils/promptIdContext srcPrefixed fail fail fail fail fail pass

¹ noFollowOpen, subSessionConstants, goalWire, transcriptRecords, userPromptSubmitContext, memoryScopes, toolWriteOrigin, envVarResolver, conversationsRuntimeMarker.

The load-bearing number: 5 of 16 flip from broken to fixed (the three newOnHead plus the two wildcardOnly), and no cell regresses. 97/97 assertions.

Mechanism, observed rather than inferred

vitest's json reporter carries no message for a collection-time import error — the observable symptom on base is a bare Cannot find module '@qwen-code/qwen-code-core/storage' naming the original specifier. So h1d-resolver.mjs asked Vite's own pluginContainer.resolveId what each arm resolves each specifier to, and classified the result by whether the returned path exists. Witness: 02-resolver-mechanism-and-route-divergence.png. 42/42 assertions.

specifier head base root-string-first wildcard-first no-wildcard
core/storage core/src/config/storage.ts core/index.ts/storage core/index.ts/storage core/src/storage core/src/config/storage.ts
core/noFollowOpen core/src/utils/no-follow-open.ts same core/index.ts/noFollowOpen core/src/noFollowOpen same
core/utils/promptIdContext core/src/utils/promptIdContext.ts core/index.ts/utils/… core/index.ts/utils/… core/src/utils/promptIdContext.ts throws ERR_PACKAGE_PATH_NOT_EXPORTED

(✗ = the alias rewrote the specifier to a path that does not exist.)

This confirms the head config's comment precisely: a string root alias prefix-matches <pattern>/<sub> and appends the tail under index.ts. That was proved independently of the repo, in a scratch Vite root (h1d section A): the same alias with a file replacement yields …/target/index.ts/storage; with a directory replacement it yields …/target/storage.ts.

h1c-mechanism.mjs (18/18) re-confirms the direction end to end through the default reporter: base fails with Cannot find module '<specifier>', head passes, for three specifiers.

Mutation matrix

mutation result classification
root as a string, moved first all 12 subpaths rewritten under index.ts the arrangement the comment warns about — load-bearing
root as a string, left in place resolution identical to head on all 16 specifiers redundant defence — see Finding 5
pattern entry moved above the named entries all 12 named subpaths → nonexistent core/src/<name>; wildcard-only still resolve ordering is load-bearing, as the Test Plan says
pattern entry deleted named entries all fine; wildcard-only rejected by the exports map survivor — see Finding 1

Perf premise (S3) — confirmed

h5-perf.mjs, sequential single-file runs with --no-file-parallelism, min of 3. Witness: 03-perf-slice-and-migrated-files-ab.png. 8/8 assertions.

probe min wall marginal over fixed overhead
imports nothing from core 2.95 s — (fixed overhead)
imports core/debugLogger 3.35 s +0.39 s
imports core (package root) 14.25 s +11.30 s

Ratio 28.8×. The PR's "roughly 11.5 s" and "about 2 s" are corroborated (a first independent run measured 12.18 s and 0.43 s).

The real end-to-end delta: reverting only the two migrated source files to their base barrel imports, holding the config at head, and running the four suites that cover them (RemoteInputWatcher.test.ts, tipHistory.test.ts, tipScheduler.test.ts, Tips.test.ts):

arm exit wall
head sources (subpath imports) 0 17.72 s
base sources (barrel imports) 0 33.06 s

15.34 s saved, both arms green — behaviour unchanged, as claimed. Sources restored byte-identically (sha verified).

The PR's own named risk (mock-factory bypass) — ruled out, and demonstrated

The description says files whose dependents replace core with a mock factory were deliberately untouched, because a direct subpath import stops being intercepted. That is a real mechanism, and h6-identity.mjs demonstrates it: with a barrel factory that overrides Storage, the barrel sees the fake while core/storage loads the real module (3/3 assertions).

It does not bite this PR:

  • 141 packages/cli test files call vi.mock('@qwen-code/qwen-code-core', …); 0 mock a core subpath. (A 142nd grep hit, packages/cli/src/acp-integration/authPreflight.test.ts:23, is a comment — verified.)
  • Exactly 4 of them transitively reach a migrated file. All four use { ...actual, <unrelated overrides> }; none overrides createDebugLogger, Storage, or atomicWriteFileSync. The one that does override createDebugLogger (opentui-runtime.test.ts) also replaces ../../remoteInput/RemoteInputWatcher.js wholesale, cutting the chain.
  • The four direct suites (RemoteInputWatcher.test.ts, tipHistory.test.ts, tipScheduler.test.ts, Tips.test.ts) mock nothing in core at all.
  • Module identity holds: barrel.Storage === subpath.Storage, and likewise atomicWriteFileSync and createDebugLogger (3/3), including under the ...actual spread-mock shape (3/3). Both routes bottom out in the same file — core/index.ts is only export * from './src/index.js' — so no instanceof, singleton, or mock interception changes.

Pre-existing and not caused by this PR: packages/cli/test-setup.ts sets QWEN_DEBUG_LOG_FILE=0 (which fully neuters debug-logger writes) but does not redirect StorageQWEN_HOME is unset, so Storage.getGlobalQwenDir() resolves to the real os.homedir()/.qwen. Identical before and after, because the spread factories already delivered the real implementations.

Guard test is not vacuous (S1)

h3-guard-mutations.mjs, 46/46. Each mutation reverts one source hunk; the right arm goes red and only that arm. Witnesses: 05-guard-mutation-matrix.png, 04-nothing-pins-the-vitest-pattern-entry.png.

reverted hunk plain-Node arm esbuild cli arm esbuild acp arm esbuild sdk arm
none (baseline, 14 tests) 11 green green green green
3 new exports entries 3 red green green green
one exports import target typo'd 1 red green green green
named storage entry in cli tsconfig green 1 red green green
goalWire entry in acp-bridge tsconfig green green 1 red green
paths block in sdk tsconfig green green green 1 red

The typo row proves the pinned-URL check is load-bearing beyond mere file existence. The three new exports targets were also verified against the real built artifacts: all six files (storage/atomicFileWrite/debugLogger × .js/.d.ts) exist under packages/core/dist.

S2 — declaration build

h4-sdk-build.mjs, 15/15.

arm exit files emitted dist/daemon/index.d.ts nested above the package
head ("paths": {}) 0 69 present 0
paths inherited (mutant) 0 70 absent 69 under sdk-typescript/src/, plus core/src/utils/transcript-records.d.ts

The layout claim reproduces exactly, including a core source being emitted into the sdk output directory. Scratch --outDirs were used, so the shipped dist was never touched.

Corrections

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

  1. "the alias list standing in for them named only four subpaths" — at the verified base tip 74fe3a6 it named nine. Likewise the Reviewer Test Plan's "The four named subpaths must stay ahead of the pattern entry" — at head there are twelve. The substantive point is correct and load-bearing (moving the pattern entry first breaks all twelve); only the counts are wrong. The PR spans 14 commits with two merges of main, and only 1 commit is reachable locally, so the description was most likely written against an older base — I could not check.

  2. "the two migrated files exist to prove it end to end" — "it" is the pattern entry, but storage, atomicFileWrite and debugLogger each received an explicit named entry in all three places (vitest alias, cli tsconfig paths, core exports). The two migrated files therefore never exercise the pattern entry; deleting it leaves them green (Finding 1). They prove the named-subpath route end to end, which is still worth proving — just not the wildcard.

  3. packages/sdk-typescript/tsconfig.reference.json's new comment states the references entry is needed because "The paths entry inherited from tsconfig.json resolves core subpath imports to core sources … or the build fails TS6059/TS6307". At head that premise is void: the file extends tsconfig.**build**.json, and this same PR sets "paths": {} there. Measured — tsc --showConfig -p tsconfig.reference.json reports paths: {}, and deleting the references entry leaves tsc --build --force at exit 0 with no TS errors. See Finding 3.

Findings

Ordered by severity. None is a blocker; none reproduced a live break.

1. Nothing in the repo pins the pattern entry — the PR's headline mechanism (Suggestion, test-coverage)

Deleting the pattern entry from packages/cli/vitest.config.ts changes no test outcome anywhere:

with the pattern entry    : exit=0 suites=11 tests=37 passed=37 failed=0
without the pattern entry : exit=0 suites=11 tests=37 passed=37 failed=0
new guard test            : 14 green, 0 red

Reproduce: h3-guard-mutations.mjs (section "does anything pin the vitest pattern entry?").

Cause: all 12 named subpaths have explicit alias entries, and the census (h0-subpath-census.mjs) shows 0 specifiers in packages/{cli,acp-bridge,sdk-typescript}/src that need the wildcard. Classification: coverage gap, not dead code — the entry works (h1d shows it resolving core/utils/promptIdContext to a real file), it is simply unobserved. A future edit that drops it, or reorders it above the named entries (which breaks all 12 — see the matrix), fails nothing today. The head config's own comment warns about exactly that reordering and nothing enforces it.

The pin cannot live in the new guard test, which never reads the cli vitest config (its arms are plain-Node and esbuild). It belongs either in a cli-side test that imports a wildcard-only specifier, or in scripts/tests/unit-vitest-configs.test.ts, which already imports packages/cli/vitest.config.js as a module and could resolve one specifier through its alias array.

2. The pattern entry widens the test route past what exports admits at runtime (Suggestion, latent)

Two independent instruments on the same specifiers — Vite's resolver (the test route) and a real child node process from packages/cli (the npm start route):

specifier vitest at head plain Node
core/storage core/src/config/storage.ts OK → core/dist/src/config/storage.js
core/debugLogger core/src/utils/debugLogger.ts OK
core/utils/promptIdContext core/src/utils/promptIdContext.ts ERR_PACKAGE_PATH_NOT_EXPORTED
core/config/models core/src/config/models.ts ERR_PACKAGE_PATH_NOT_EXPORTED
core/src/utils/promptIdContext core/src/src/utils/promptIdContext OK (via ./src/*)

Reproduce: h1d-resolver.mjs (sections B and C).

This is precisely failure mode #1 in the new guard test's own header comment. The guard exists to catch it, but expectedDistTargets is a hardcoded list of 11 specifiers, not derived from the sources — so it pins today's set and cannot see tomorrow's. Before this PR the test route rejected any unnamed subpath, which caught the mistake at authoring time; now the wildcard admits it.

What does not hold (bounds):

  • No live break. The census over packages/{cli,acp-bridge,sdk-typescript}/src found 12 distinct core specifiers in use and 0 without an exports entry.
  • Typecheck and the bundle agree with tests, not with npm start: cli's tsconfig has the same @qwen-code/qwen-code-core/*../core/src/* wildcard, so all three resolve to core/src. Only the built-but-unbundled CLI (npm start, npm run build-and-start) breaks. The failure is loud — a load-time throw naming the specifier — not silent.
  • The last row is not a regression: core/src/* fails under vitest on base too (rewritten to index.ts/src/…), by a different mechanism.
Measured candidate fix (not applied to the PR)

Keep the pinned list — it pins the dist targets, which a census cannot derive — and add one arm that derives the in-use specifier set from the sources and requires it to equal the pinned set. Full patch: candidate-fix-guard-census.mjs.snippet; trial: h9-fix-trial.mjs. Witness: 06-candidate-fix-trial-hostile-fixture.png. 10/10 assertions.

state tests result
shipped guard, unmodified tree 14 14 green
patched guard, unmodified tree 15 15 green, 0 red — zero collateral, exactly one test added
patched guard + hostile fixture¹ 15 14 green, 1 red — the new arm only
shipped guard + same hostile fixture 14 14 green, 0 red, exit 0 — gap confirmed

¹ packages/cli/src/__verify_hostile__/imports-unexported-subpath.ts importing @qwen-code/qwen-code-core/utils/promptIdContext.

The suite is green both with and without the patch on the unmodified tree, which is the expected unpinned-axis signal: only the hostile fixture distinguishes them. The patch should ship together with a fixture that exercises it.

3. tsconfig.reference.json's references entry carries a rationale this PR voided (Nice to have)

The comment names TS6059/TS6307 as the consequence of omitting the entry; measured, the build succeeds without it (exit 0, no TS codes), because the same PR reset paths to {} in the parent config. See Correction 3.

The entry is not a no-op: tsc --build --dry --verbose shows it puts the core project in the build graph (head: ['../core/tsconfig.json', 'tsconfig.reference.json'], mutant: ['tsconfig.reference.json']). Standalone under --force that costs 100.9 s vs 14.2 s — but that is a worst case for a path the repo does not run: root scripts/build.js builds packages/core first by explicit order, and packages/cli/tsconfig.json already references ../core directly, so in the real build path the edge adds graph structure rather than work. Not measured: whether it is required on a fresh clone with no packages/core/dist (that would mean destroying the 64 MB built dist). Suggested action is to fix the comment, not to remove the entry.

4. One named subpath is in the vitest alias list but not in cli's tsconfig paths (Nice to have, pre-existing)

h7-route-census.mjs compared all four lists. 7/7 assertions.

specifier core exports cli tsconfig acp tsconfig sdk tsconfig cli vitest guard test
11 others X X (4 of them) (1) X X
userPromptSubmitContext X X

Consequence, measured: bundling that specifier under cli's tsconfig lands in core/dist/src/hooks/user-prompt-submit-context.js (plus a core/dist copy of transcript-records), with 0 files from core/src — the two-copies-of-one-module shape the guard's comment #2 names, while cli's vitest alias resolves it to core/src.

What does not hold: nothing under packages/{cli,acp-bridge,sdk-typescript}/src imports it (the only importers are integration-tests/, which has its own named tsconfig entry); and the module is stateless — pure functions and re-exported string constants, no AsyncLocalStorage, no top-level mutable state — so a duplicated copy is not observable in the way the comment fears. Pre-existing, not introduced here: base's cli tsconfig named only 3 subpaths. Worth noting because this PR is the change that systematically closed the gap — it added named cli tsconfig entries for goalWire, storage, atomicFileWrite, debugLogger, envVarResolver, toolWriteOrigin, memoryScopes and conversationsRuntimeMarker, and left this one behind.

5. The exact-match regex for the package root is redundant defence (no action)

Replacing /^@qwen-code\/qwen-code-core$/ with the string '@qwen-code/qwen-code-core' in the same position produces resolution identical to head on all 16 specifiers, because the pattern entry precedes it. The comment justifying the regex ("Spelled as a string it would also match everything beneath it") is mechanically true — proven in h1d section A — but only for an arrangement the shipped ordering does not have. Classification: redundant defence, correct as it stands; it is cheap insurance against a future reorder, and mut-root-string-first shows what that reorder would cost (all 12 subpaths). No change requested.

Not covered

  • Per-commit attribution. The snapshot lists 14 commits; git rev-list HEAD^1..HEAD^2 returns 1, and git rev-parse --is-shallow-repository is true (depth-2 merge-ref checkout). Only the aggregate HEAD^1..HEAD diff was verified. Note git rev-list --count returned a plausible 1 rather than erroring, which is exactly the silent-gap shape — it was cross-checked against the snapshot's commits array, and they disagree.
  • baseRefOid drift. The snapshot's baseRefOid is dff26740f7ff78065a2a6e4b81f8b86653866f7e, not the merge-ref base tip 74fe3a65… used for every control here. Per the CI contract HEAD^1 is authoritative for a merge-ref checkout; the difference is recorded rather than reconciled.
  • Trial merge into current main. Not done — shallow clone, no network. Whether main has touched any of these 10 files since the merge base is unknown.
  • Full cli suite (1011 test files) and full core suite. The PR's CI figures (cli 2223 s collect / 1372 s run, core 546 s / 251 s, release run 33713579913) are author claims and were not reproduced. The ~6269 s ceiling printed by h5-perf.mjs is arithmetic on a measured per-file premium × a measured file count, not a measurement, and is optimistic: files share module graphs and vitest evaluates a module once per worker, not once per file.
  • npm run bundle / check:serve-fast-path-bundle. Not run. The bundle route was exercised through the guard test's three real esbuild.buildSync arms instead, which is the resolution question the bundle raises — but no bundle artifact was produced or byte-compared.
  • macOS and Windows. The PR's own test matrix is ⚠️ on all three OSes. Only Linux was measured. The guard test's backslash normalization (input.replace(/\\/g, '/')) is untested here on a platform that produces backslashes.
  • Fresh-clone necessity of the references entry (Finding 3) — would require destroying and rebuilding the 64 MB packages/core/dist.
  • The ~/.qwen/tip_history.json write path in AppContainer.test.tsx was traced statically (by the mock-bypass sweep), not executed. It is pre-existing and unchanged by this PR either way.
  • packages/core unit tests were not run; this PR changes only packages/core/package.json there, and that change was verified through the plain-Node and esbuild routes.

Methodology

Everything ran in the CI verify container (node:22-bookworm, node v22.23.2, vitest 3.2.7) on the pre-built refs/pull/10917/merge tree at 4653a34. Ten assertion-bearing harnesses (.mjs, in this directory) plus a specifier census (h0) drove real code — no stub of anything under test: real vitest runs of real probe files, Vite's own pluginContainer.resolveId, real child node processes for import.meta.resolve, real esbuild.buildSync, real tsc. Controls differ from head by exactly one hunk each; the base arm is git show HEAD^1:<file> verified byte-identical by diff.

Internal workspace links were checked before any control was trusted: readlink -f node_modules/@qwen-code/{qwen-code-core,acp-bridge,sdk} all resolve into this tree (/__w/qwen-code/qwen-code/packages/…), so in-place mutations of packages/core/package.json and the tsconfigs are visible to the plain-Node and esbuild routes. package.json/package-lock.json dependency trees are untouched by this PR, so reusing the root node_modules for base arms is a clean control. Every harness that mutates the tree restores it and asserts a sha256 match afterwards; git status --porcelain is empty at the end and packages/cli/vitest.config.ts still hashes to bc2aae7f….

Expected base-arm reds are encoded as passing assertions inside the owning harness, so fail counts only unexpected outcomes — hence 0 fail with a findings verdict.

Harness defects found and fixed during the round, recorded because each initially produced false signal and a reader judging the counts is entitled to them:

  • a mutation emitted an object-form alias entry into an array-form list, so one arm's config failed to load and every cell read n/a (fixed; that arm now runs);
  • a bulk find-and-replace also rewrote a scoring condition, reporting fail=38 against a matrix in which every cell already matched its expectation (fixed; the same run then scored 97/0);
  • an expectation that the base arm's error text would name the index.ts rewrite — the rewrite is real but invisible in the symptom, so the mechanism was moved to a resolver-level instrument (h1d) that can see it;
  • a vi.mock factory closing over an unhoisted binding, the gotcha AGENTS.md documents (fixed with vi.hoisted());
  • two assertions encoding invariants stricter than the code declares — the PR's "about thirty … for the next batch" read as a total, and the new guard's hardcoded list held to every exports entry rather than its stated scope of specifiers actually imported from packages/cli/src. Both were reframed to assert the measured state, and the underlying observations are reported as Findings 2 and 4 rather than counted as failures.

h8-gates.mjs proves the typecheck gate live by planting a type error and confirming tsc names the file.

Raw per-cell logs, per-arm vitest json reports, tsc and esbuild outputs, and the six evidence PNGs are all in this directory. Assertion counts per harness: h1 97, h1c 18, h1d 42, h3 46, h4 15, h5 8, h6 11, h7 7, h8 9, h9 10 → 263.

Flakiness gate log

rounds=5 files=1 skipped=0
file scripts/tests/core-subpath-exports-resolution.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/core-subpath-exports-resolution.test.js


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  scripts/tests/core-subpath-exports-resolution.test.js: PPPPP

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

--- per-invocation detail (full copy in the artifact) ---
round 1 · scripts/tests/core-subpath-exports-resolution.test.js: P (exit 0)
round 2 · scripts/tests/core-subpath-exports-resolution.test.js: P (exit 0)
round 3 · scripts/tests/core-subpath-exports-resolution.test.js: P (exit 0)
round 4 · scripts/tests/core-subpath-exports-resolution.test.js: P (exit 0)
round 5 · scripts/tests/core-subpath-exports-resolution.test.js: P (exit 0)

Evidence images

01-ab-six-arm-alias-matrix

02-resolver-mechanism-and-route-divergence

03-perf-slice-and-migrated-files-ab

04-nothing-pins-the-vitest-pattern-entry

05-guard-mutation-matrix

06-candidate-fix-trial-hostile-fixture

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

Qwen Code · sandboxed verification

The dev loader intercepted only the exact package root, so a named core
subpath fell through to the package's exports map and loaded
packages/core/dist while every package-root import loaded
packages/core/src. One dev process then held two instances of the same
module: Config binds the debug session on the src copy
(setDebugLogSession(this)), so RemoteInputWatcher's dist-copy
REMOTE_INPUT logger read an empty session and every debugLogger(...) call
there silently no-oped with QWEN_DEBUG_LOG_FILE enabled. Storage split
its static state the same way.

Derive the interception map from the core exports map so every named
subpath short-circuits to its packages/core/src file and stays covered as
subpaths are added. The exports entries (built-but-unbundled CLI) and the
tsconfig paths entries (bundle lane) are untouched.

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

Copy link
Copy Markdown
Collaborator Author

Fixed the round-7 body-only Critical (ledger R7-1, labelled R1-2): the dev lane resolving core subpaths into packages/core/dist while the package root loaded packages/core/src.

Fix27185b6, scripts/dev.js + scripts/tests/dev.test.js only.

The loader's resolve hook now derives its interception map from core's own exports map, so every named subpath short-circuits to the matching packages/core/src file instead of falling through to dist. Deriving it rather than hardcoding the three specifiers named in the review closes the whole class: the same dual-instance exposure applied to every pre-existing named subpath (envVarResolver, goalWire, memoryScopes, subSessionConstants, toolWriteOrigin, userPromptSubmitContext, noFollowOpen, transcriptRecords, conversationsRuntimeMarker) reachable in a dev process, not just the two migrated files. The root and the string-valued wildcard entries (./dist/*, ./src/*, ./package.json) are excluded, and a mapping whose source file is absent is dropped rather than emitted.

Kept, as required: the new exports entries in packages/core/package.json (built-but-unbundled CLI) and the tsconfig paths entries (bundle lane) are untouched — git diff --name-only for the fix commit lists only the two scripts/ files.

Confirmed real at 4653a34, and confirmed flipped. Probes ran under the loader scripts/dev.js actually generates (captured from a real dev.js run with TMPDIR pinned, not a replica), through tsx, from inside packages/cli. packages/core/dist was built first so the fall-through lane could resolve at all.

Module resolution + identity:

# before (loader from 4653a3452a)
ROOT    resolved: packages/core/index.ts
SUBPATH resolved: packages/core/dist/src/utils/debugLogger.js   [DIST]
SUBPATH resolved: packages/core/dist/src/config/storage.js      [DIST]
SUBPATH resolved: packages/core/dist/src/utils/atomicFileWrite.js [DIST]
same module instance? false

# after (loader from 27185b6f84)
ROOT    resolved: packages/core/index.ts
SUBPATH resolved: packages/core/src/utils/debugLogger.ts        [SRC]
SUBPATH resolved: packages/core/src/config/storage.ts           [SRC]
SUBPATH resolved: packages/core/src/utils/atomicFileWrite.ts    [SRC]
same module instance? true

The observable failure reproduced and cleared too. Binding the session the way Config does (setDebugLogSession, on the package-root copy) and then creating the REMOTE_INPUT logger from the subpath specifier, with QWEN_DEBUG_LOG_FILE=1 and an isolated HOME:

# before                                   # after
REMOTE_INPUT isEnabled()? false            REMOTE_INPUT isEnabled()? true
ROOT_CONTROL line in log? true             ROOT_CONTROL line in log? true
REMOTE_INPUT line in log? false            REMOTE_INPUT line in log? true

So the silent no-op was real, and it is gone — not merely quieter.

Regression test — added to the existing scripts/tests/dev.test.js carrier. It executes the generated resolve hook (via a data: URL) rather than asserting on loader source text, pins the three migrated specifiers plus the root to hardcoded src paths, asserts completeness over every named subpath the exports map publishes, and asserts unrelated specifiers still reach Node's resolver.

Falsified: reverting scripts/dev.js to 4653a34 turns it red on the exact specifier — @qwen-code/qwen-code-core/debugLogger: expected false to be true — with the other four tests still green.

scripts/tests/dev.test.js                            5 passed
scripts/tests/core-subpath-exports-resolution.test.js 14 passed
eslint scripts/dev.js scripts/tests/dev.test.js       clean

No TypeScript was touched, so no package typecheck applies. Repo-wide build/lint/test were not run (this box cannot build packages/channels/feishu).

Test (ubuntu-latest, Node 22.x) on the previous head is red at exactly 2h0m50s — the job cap, not an assertion failure, and red on unrelated PRs; left to maintainers.

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

⚠️ Downgraded from Approve to Comment: CI failing: Test (ubuntu-latest, Node 22.x), web-shell E2E Smoke (ubuntu-latest, Node 22.x). Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not explored to full depth (tool budget reached): "agent 2": could not execute scripts/tests/dev.test.js (this review worktree has no node_modules/vitest ), so both findings are established by reading plus a direct fil…; "agent reverse-audit (round 5)": could not execute the real-spawn lane ( packages/cli serve.test.ts dev-entrypoint boundary, and npm run dev end-to-end) — the globalSetup guard stops the….

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

  • scripts/dev.js:94 — [test] Interception set defined by the derivation rule; the new test's completeness oracle mirrors that same rule, so an unmappable exports entry is silently dropped and invisible — both reject branches measured unpinned…
  • scripts/dev.js:85 — [review] Re-implements distEntryFiles (scripts/vitest-global-setup.js:108) with narrower condition handling; a third copy of the rule lives in eslint-rules/no-core-utils-upward-import.js:69-99
  • scripts/dev.js:95 — [probe] NODE_OPTIONS registration leaks the hook to every grandchild, turning the PR's own plain-Node guard red (11/11) when test:scripts runs inside a dev session
  • scripts/tests/dev.test.js:262 — [probe] Completeness arm asserts a location (/packages/core/src/) the launcher never guarantees, so a correctly intercepted root-level core export reds as a fake dist leak
  • scripts/tests/dev.test.js:237 — [probe] Nothing pins the generated loader's delivery to the child; dropping the --import registration leaves the suite 5/5 green while dev stops loading core from source
中文说明

⚠️ 已从批准降级为评论:CI failing: Test (ubuntu-latest, Node 22.x), web-shell E2E Smoke (ubuntu-latest, Node 22.x)。 仅完成部分审查,审查缺口已披露。

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

未探索到全部深度(达到工具调用预算):"agent 2"could not execute scripts/tests/dev.test.js (this review worktree has no node_modules/vitest ), so both findings are established by reading plus a direct fil…"agent reverse-audit (round 5)"could not execute the real-spawn lane ( packages/cli serve.test.ts dev-entrypoint boundary, and npm run dev end-to-end) — the globalSetup guard stops the…

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

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

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.

3 participants