qwen-code/scripts/tests/core-subpath-exports-resolution.test.js
易良 49e3ef6269
perf(cli): let tests resolve core modules individually (#10917)
* perf(cli): let tests resolve core modules individually, and stop two 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.

* fix(cli): restore the named core subpaths the previous commit dropped

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.

* fix(cli): name the migrated core imports so plain Node resolves them

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

* test(scripts): guard core subpath exports resolution under plain Node

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

* fix(cli): name every core subpath import in cli tsconfig paths

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

* test(scripts): pin subpath exports targets and guard bundle resolution

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

* fix: cover goalWire in the core-subpath guard and make it OS-independent

- 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

* test: guard acp-bridge-routed core subpaths in the bundle arm

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

* fix(cli): guard the conversationsRuntimeMarker subpath route

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

* fix(sdk): guard the sdk-routed core subpath resolution

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

* fix(sdk): reference core from the sdk composite reference project

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

* fix(sdk): keep the declaration build on the exports map

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

* fix(scripts): route core subpaths to src in the dev loader

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibaba-cloud.com>
2026-09-06 10:20:05 +00:00

275 lines
12 KiB
JavaScript

/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
// Resolution guards for the core subpath scheme.
//
// cli sources import individual core modules through subpath specifiers
// (e.g. `@qwen-code/qwen-code-core/storage`). Typecheck resolves them via
// tsconfig `paths`, unit tests via the vitest alias list, and the bundle via
// esbuild's paths reading — none of which consults `exports`. Two failure
// modes stay invisible to all of those gates:
//
// 1. The built-but-unbundled CLI (`npm start`, `npm run build-and-start`)
// resolves subpaths through `packages/core/package.json` `exports` alone,
// so a specifier with no exports entry — or an entry whose import target
// is typo'd or redirected — crashes or misloads at module load. The
// plain-Node guard below resolves every specifier in a real child `node`
// process (no vitest aliases, no tsconfig paths), pins the resolved URL
// to the expected dist target, and asserts that target exists.
// 2. The bundle chain reads `packages/cli/tsconfig.json` `paths`; a subpath
// without a named entry falls through the wildcard to a nonexistent file
// and then back to the `exports` map, bundling a `packages/core/dist/**`
// copy while every package-root import loads `packages/core/src/**` —
// two instances of barrel-exported, stateful modules in one process, the
// module-identity failure #10908's Known risks name. The esbuild guard
// below bundles every core subpath statically imported from
// packages/cli/src — plus the subpaths the bundle reaches through
// @qwen-code/acp-bridge and @qwen-code/sdk — under the tsconfig each
// route's importing file discovers, and asserts every input lands under
// packages/core/src, never packages/core/dist.
//
// The pinned-target check makes a built core `dist` a prerequisite of the
// plain-Node guard (`import.meta.resolve` alone deliberately does not need
// one). This lane's vitest config does not wire
// `scripts/vitest-global-setup.js` (that guard 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 below naming the absent file — run `npm run build` from the
// repository root to produce it (#9149).
import { execFileSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { buildSync } from 'esbuild';
import { describe, expect, it } from 'vitest';
const root = join(dirname(fileURLToPath(import.meta.url)), '../..');
// Every core subpath specifier statically imported from packages/cli/src
// (storage, atomicFileWrite, debugLogger, noFollowOpen, envVarResolver,
// toolWriteOrigin, memoryScopes, conversationsRuntimeMarker), plus the
// subpaths `npm start` reaches through @qwen-code/acp-bridge
// (subSessionConstants, goalWire, transcriptRecords). Values are the dist
// targets pinned by the exports map in packages/core/package.json. Probes
// run from packages/cli; acp-bridge is a `file:` dependency there, so
// acp-bridge-routed specifiers resolve identically from that cwd.
const expectedDistTargets = {
'@qwen-code/qwen-code-core/storage':
'packages/core/dist/src/config/storage.js',
'@qwen-code/qwen-code-core/atomicFileWrite':
'packages/core/dist/src/utils/atomicFileWrite.js',
'@qwen-code/qwen-code-core/debugLogger':
'packages/core/dist/src/utils/debugLogger.js',
'@qwen-code/qwen-code-core/noFollowOpen':
'packages/core/dist/src/utils/no-follow-open.js',
'@qwen-code/qwen-code-core/envVarResolver':
'packages/core/dist/src/utils/envVarResolver.js',
'@qwen-code/qwen-code-core/toolWriteOrigin':
'packages/core/dist/src/services/tool-write-origin.js',
'@qwen-code/qwen-code-core/memoryScopes':
'packages/core/dist/src/memory/scopes.js',
'@qwen-code/qwen-code-core/conversationsRuntimeMarker':
'packages/core/dist/src/utils/conversations-runtime-marker.js',
'@qwen-code/qwen-code-core/subSessionConstants':
'packages/core/dist/src/tools/sub-session-constants.js',
'@qwen-code/qwen-code-core/goalWire':
'packages/core/dist/src/goals/goal-wire.js',
'@qwen-code/qwen-code-core/transcriptRecords':
'packages/core/dist/src/utils/transcript-records.js',
};
function probe(specifier) {
const script = `
try {
console.log('OK ' + import.meta.resolve(${JSON.stringify(specifier)}));
} catch (error) {
console.log('FAIL ' + (error.code ?? error.name));
}
`;
return execFileSync(process.execPath, ['--input-type=module', '-e', script], {
cwd: join(root, 'packages', 'cli'),
encoding: 'utf8',
}).trim();
}
describe('core subpath specifiers resolve under plain Node', () => {
it.each(Object.entries(expectedDistTargets))(
'%s resolves to its pinned exports target',
(specifier, expectedTarget) => {
const output = probe(specifier);
expect(output).toMatch(/^OK /);
const url = output.slice('OK '.length);
expect(url).toBe(pathToFileURL(join(root, expectedTarget)).href);
expect(existsSync(fileURLToPath(url))).toBe(true);
},
);
});
// The core subpath specifiers the cli bundle resolves under
// packages/cli/tsconfig.json `paths` — everything statically imported from
// packages/cli/src, plus goalWire, which the bundle reaches through
// @qwen-code/acp-bridge's transcript-replay — each mapped to the core source
// file the matching named `paths` entry must resolve it to.
const expectedSrcTargets = {
'@qwen-code/qwen-code-core/storage': 'packages/core/src/config/storage.ts',
'@qwen-code/qwen-code-core/atomicFileWrite':
'packages/core/src/utils/atomicFileWrite.ts',
'@qwen-code/qwen-code-core/debugLogger':
'packages/core/src/utils/debugLogger.ts',
'@qwen-code/qwen-code-core/noFollowOpen':
'packages/core/src/utils/no-follow-open.ts',
'@qwen-code/qwen-code-core/envVarResolver':
'packages/core/src/utils/envVarResolver.ts',
'@qwen-code/qwen-code-core/toolWriteOrigin':
'packages/core/src/services/tool-write-origin.ts',
'@qwen-code/qwen-code-core/memoryScopes':
'packages/core/src/memory/scopes.ts',
'@qwen-code/qwen-code-core/conversationsRuntimeMarker':
'packages/core/src/utils/conversations-runtime-marker.ts',
'@qwen-code/qwen-code-core/goalWire': 'packages/core/src/goals/goal-wire.ts',
};
describe('core subpath specifiers bundle from the core src tree', () => {
it('resolves every bundled core subpath under packages/core/src', () => {
const result = buildSync({
absWorkingDir: root,
stdin: {
contents: Object.keys(expectedSrcTargets)
.map((specifier) => `import ${JSON.stringify(specifier)};`)
.join('\n'),
resolveDir: join(root, 'packages', 'cli'),
sourcefile: 'core-subpath-bundle-probe.ts',
loader: 'ts',
},
bundle: true,
write: false,
metafile: true,
platform: 'node',
format: 'esm',
logLevel: 'silent',
tsconfig: join(root, 'packages', 'cli', 'tsconfig.json'),
});
// esbuild emits metafile input keys with the platform path separator
// (backslash on Windows); normalize to forward slashes so the target
// literals above and the leak filter below compare identically on every
// runner.
const inputs = Object.keys(result.metafile.inputs).map((input) =>
input.replace(/\\/g, '/'),
);
expect(
inputs.filter((input) => input.includes('packages/core/dist/')),
).toEqual([]);
for (const target of Object.values(expectedSrcTargets)) {
expect(inputs).toContain(target);
}
});
});
// The core subpath specifiers the bundle reaches through @qwen-code/acp-bridge
// — goalWire and transcriptRecords from transcript-replay.ts,
// subSessionConstants from bridgeOptions.ts, noFollowOpen from
// sessionArtifacts.ts — each mapped to the core source file the matching
// named `paths` entry in packages/acp-bridge/tsconfig.json must resolve it
// to. esbuild.config.js's mainBuild passes no `tsconfig` option, so the
// shipped bundle resolves these by discovering packages/acp-bridge/tsconfig.json
// per importing file, not the cli tsconfig pinned above — a guard arm that
// only probes the cli route is blind to mutations of these entries.
const expectedAcpBridgeSrcTargets = {
'@qwen-code/qwen-code-core/goalWire': 'packages/core/src/goals/goal-wire.ts',
'@qwen-code/qwen-code-core/transcriptRecords':
'packages/core/src/utils/transcript-records.ts',
'@qwen-code/qwen-code-core/subSessionConstants':
'packages/core/src/tools/sub-session-constants.ts',
'@qwen-code/qwen-code-core/noFollowOpen':
'packages/core/src/utils/no-follow-open.ts',
};
describe('acp-bridge-routed core subpaths bundle from the core src tree', () => {
it('resolves every acp-bridge-routed subpath under packages/core/src', () => {
const result = buildSync({
absWorkingDir: root,
stdin: {
contents: Object.keys(expectedAcpBridgeSrcTargets)
.map((specifier) => `import ${JSON.stringify(specifier)};`)
.join('\n'),
resolveDir: join(root, 'packages', 'acp-bridge', 'src'),
sourcefile: 'core-subpath-bundle-probe-acp-bridge.ts',
loader: 'ts',
},
bundle: true,
write: false,
metafile: true,
platform: 'node',
format: 'esm',
logLevel: 'silent',
// Deliberately no `tsconfig` option: mainBuild carries none, so esbuild
// discovers packages/acp-bridge/tsconfig.json per importing file — the
// resolution route the shipped bundle actually uses for these imports.
});
// Same backslash normalization as the cli-routed arm above.
const inputs = Object.keys(result.metafile.inputs).map((input) =>
input.replace(/\\/g, '/'),
);
expect(
inputs.filter((input) => input.includes('packages/core/dist/')),
).toEqual([]);
for (const target of Object.values(expectedAcpBridgeSrcTargets)) {
expect(inputs).toContain(target);
}
});
});
// The core subpath specifiers the bundle reaches through @qwen-code/sdk —
// transcriptRecords from daemon/ui/chat-record-transcript.ts, pulled in via
// the cli tsconfig `@qwen-code/sdk/*` mapping (cli sources such as
// ui/utils/export/export-transcript-document.ts import
// `@qwen-code/sdk/daemon/transcript`). mainBuild passes no `tsconfig`
// option, so the shipped bundle resolves these by discovering
// packages/sdk-typescript/tsconfig.json per importing file — a guard arm
// that only probes the cli and acp-bridge routes is blind to mutations of
// the named entry there.
const expectedSdkSrcTargets = {
'@qwen-code/qwen-code-core/transcriptRecords':
'packages/core/src/utils/transcript-records.ts',
};
describe('sdk-routed core subpaths bundle from the core src tree', () => {
it('resolves every sdk-routed subpath under packages/core/src', () => {
const result = buildSync({
absWorkingDir: root,
stdin: {
contents: Object.keys(expectedSdkSrcTargets)
.map((specifier) => `import ${JSON.stringify(specifier)};`)
.join('\n'),
resolveDir: join(root, 'packages', 'sdk-typescript', 'src'),
sourcefile: 'core-subpath-bundle-probe-sdk.ts',
loader: 'ts',
},
bundle: true,
write: false,
metafile: true,
platform: 'node',
format: 'esm',
logLevel: 'silent',
// Deliberately no `tsconfig` option: mainBuild carries none, so esbuild
// discovers packages/sdk-typescript/tsconfig.json per importing file —
// the resolution route the shipped bundle actually uses for these
// imports.
});
// Same backslash normalization as the cli-routed arm above.
const inputs = Object.keys(result.metafile.inputs).map((input) =>
input.replace(/\\/g, '/'),
);
expect(
inputs.filter((input) => input.includes('packages/core/dist/')),
).toEqual([]);
for (const target of Object.values(expectedSdkSrcTargets)) {
expect(inputs).toContain(target);
}
});
});