fix(cli): Guard serve fast-path bundle closure (#5995)

* fix(cli): guard serve fast-path bundle closure

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(cli): tighten serve fast-path bundle guard

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5995)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5995)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5995)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
jinye 2026-06-29 22:38:32 +08:00 committed by GitHub
parent f11bb31df2
commit 565e4bc437
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 725 additions and 87 deletions

View file

@ -247,6 +247,10 @@ jobs:
fi
echo "Settings schema is up-to-date"
- name: 'Check serve fast-path bundle closure'
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"
run: 'npm run check:serve-fast-path-bundle'
- name: 'Run tests and generate reports'
id: 'unit_tests'
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"
@ -428,7 +432,6 @@ jobs:
os: '${{ matrix.os }}'
github_token: '${{ secrets.GITHUB_TOKEN }}'
# Integration tests run only in the merge queue, not on every PR push.
# They are the suite that previously ran *only* in the nightly Release
# pipeline (`release.yml`), so regressions stayed hidden until release

View file

@ -80,6 +80,7 @@
"telemetry": "node scripts/telemetry.js",
"check:lockfile": "node scripts/check-lockfile.js",
"check:desktop-isolation": "node scripts/check-desktop-isolation.js",
"check:serve-fast-path-bundle": "npm run build -- --cli-only && cross-env DEV=true npm run bundle && node scripts/check-serve-fast-path-bundle.js",
"desktop-openwork-sync": "bun run scripts/desktop-openwork-sync.ts",
"clean": "node scripts/clean.js",
"pre-commit": "node scripts/pre-commit.js"

View file

@ -6,7 +6,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import yargs, { type Argv } from 'yargs';
import { execFileSync } from 'node:child_process';
import {
existsSync,
mkdirSync,
@ -70,7 +69,6 @@ const originalCloudShell = process.env['CLOUD_SHELL'];
const originalGoogleCloudProject = process.env['GOOGLE_CLOUD_PROJECT'];
const originalCwd = process.cwd();
const cliPackageRoot = process.cwd();
const repoRoot = resolve(cliPackageRoot, '../..');
interface StaticSourceGraph {
localFiles: Set<string>;
@ -78,18 +76,6 @@ interface StaticSourceGraph {
unresolvedLocalImports: string[];
}
interface EsbuildMetafileOutput {
inputs?: Record<string, unknown>;
imports?: Array<{
path: string;
kind?: string;
}>;
}
interface EsbuildMetafile {
outputs: Record<string, EsbuildMetafileOutput>;
}
function normalizePathForTest(filePath: string): string {
return filePath.replace(/\\/g, '/');
}
@ -196,69 +182,6 @@ function collectStaticSourceGraph(entryFile: string): StaticSourceGraph {
return { localFiles, externalValueImports, unresolvedLocalImports };
}
function collectBundledRunServeStaticRuntimeOffenders(): string[] {
const metafilePath = resolve(repoRoot, 'dist/esbuild.json');
rmSync(metafilePath, { force: true });
execFileSync(process.execPath, [resolve(repoRoot, 'esbuild.config.js')], {
cwd: repoRoot,
env: { ...process.env, DEV: 'true' },
stdio: 'pipe',
timeout: 30_000,
});
expect(existsSync(metafilePath)).toBe(true);
const metafile = JSON.parse(
readFileSync(metafilePath, 'utf8'),
) as EsbuildMetafile;
const outputs = new Map(
Object.entries(metafile.outputs).map(([outputPath, output]) => [
normalizePathForTest(outputPath),
output,
]),
);
const runServeOutput = [...outputs.entries()].find(([, output]) =>
Object.keys(output.inputs ?? {}).some(
(input) =>
normalizePathForTest(input) ===
'packages/cli/src/serve/run-qwen-serve.ts',
),
);
expect(runServeOutput).toBeDefined();
const queue = [runServeOutput![0]];
const staticClosure = new Set(queue);
for (let i = 0; i < queue.length; i++) {
const output = outputs.get(queue[i]);
for (const bundledImport of output?.imports ?? []) {
if (bundledImport.kind !== 'import-statement') continue;
const importedOutput = normalizePathForTest(bundledImport.path);
if (!outputs.has(importedOutput) || staticClosure.has(importedOutput)) {
continue;
}
staticClosure.add(importedOutput);
queue.push(importedOutput);
}
}
const forbiddenInputs = new Set([
'packages/cli/src/serve/acp-session-bridge.ts',
'packages/acp-bridge/src/bridge.ts',
'packages/acp-bridge/src/bridgeClient.ts',
'packages/acp-bridge/src/spawnChannel.ts',
]);
const offenders: string[] = [];
for (const outputPath of staticClosure) {
const output = outputs.get(outputPath);
for (const input of Object.keys(output?.inputs ?? {})) {
const normalizedInput = normalizePathForTest(input);
if (forbiddenInputs.has(normalizedInput)) {
offenders.push(`${outputPath} -> ${normalizedInput}`);
}
}
}
return offenders;
}
function useTempQwenHome(): string {
tempQwenHome = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-home-')),
@ -490,6 +413,23 @@ describe('CLI entry import boundary', () => {
expect(runServeSource).toContain("import('@qwen-code/acp-bridge/bridge')");
});
it('keeps request helpers from value-importing the ACP compatibility shim', () => {
const requestHelpersSource = readFileSync(
'src/serve/server/request-helpers.ts',
'utf8',
);
expect(requestHelpersSource).not.toMatch(
/from ['"]\.\.\/acp-session-bridge\.js['"]/,
);
expect(requestHelpersSource).toContain(
"import type { AcpSessionBridge } from '@qwen-code/acp-bridge/bridgeTypes';",
);
expect(requestHelpersSource).toContain(
"import { MAX_WORKSPACE_PATH_LENGTH } from '@qwen-code/acp-bridge/workspacePaths';",
);
});
it('keeps the runQwenServe static source graph free of ACP runtime modules', () => {
const graph = collectStaticSourceGraph(
resolve(cliPackageRoot, 'src/serve/run-qwen-serve.ts'),
@ -519,15 +459,6 @@ describe('CLI entry import boundary', () => {
`Unexpected ACP runtime imports:\n${forbiddenImports.join('\n')}`,
).toEqual([]);
});
it('keeps bundled runQwenServe static imports free of ACP runtime modules', () => {
const offenders = collectBundledRunServeStaticRuntimeOffenders();
expect(
offenders,
`Unexpected ACP runtime inputs in static bundle closure:\n${offenders.join('\n')}`,
).toEqual([]);
}, 30_000);
});
describe('serve fast path argument parsing', () => {

View file

@ -57,6 +57,10 @@ export default defineConfig({
__dirname,
'../acp-bridge/src/bridgeFileSystem.ts',
),
'@qwen-code/acp-bridge/eventBus': path.resolve(
__dirname,
'../acp-bridge/src/eventBus.ts',
),
'@qwen-code/acp-bridge/workspacePaths': path.resolve(
__dirname,
'../acp-bridge/src/workspacePaths.ts',

View file

@ -0,0 +1,315 @@
#!/usr/bin/env node
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const DEFAULT_METAFILE_PATH = resolve('dist/esbuild.json');
const METAFILE_BUILD_COMMAND =
'npm run build -- --cli-only && npx cross-env DEV=true npm run bundle';
const SERVE_PRE_LISTEN_ROOTS = [
{
label: 'serve fast path entry',
suffixes: [
'packages/cli/src/serve/fast-path.ts',
'packages/cli/dist/src/serve/fast-path.js',
],
},
{
label: 'serve fast path settings',
suffixes: [
'packages/cli/src/serve/fast-path-settings.ts',
'packages/cli/dist/src/serve/fast-path-settings.js',
],
},
{
label: 'run qwen serve entry',
suffixes: [
'packages/cli/src/serve/run-qwen-serve.ts',
'packages/cli/dist/src/serve/run-qwen-serve.js',
],
},
];
const FORBIDDEN_SOURCE_INPUTS = [
{
label: 'Serve ACP compatibility shim',
suffixes: [
'packages/cli/src/serve/acp-session-bridge.ts',
'packages/cli/dist/src/serve/acp-session-bridge.js',
],
},
{
label: 'ACP bridge runtime',
suffixes: [
'packages/acp-bridge/src/bridge.ts',
'packages/acp-bridge/dist/bridge.js',
],
},
{
label: 'ACP bridge client runtime',
suffixes: [
'packages/acp-bridge/src/bridgeClient.ts',
'packages/acp-bridge/dist/bridgeClient.js',
],
},
{
label: 'ACP spawnChannel runtime',
suffixes: [
'packages/acp-bridge/src/spawnChannel.ts',
'packages/acp-bridge/dist/spawnChannel.js',
],
},
{
label: 'ACP permission mediator runtime',
suffixes: [
'packages/acp-bridge/src/permissionMediator.ts',
'packages/acp-bridge/dist/permissionMediator.js',
],
},
{
label: 'ACP compaction engine runtime',
suffixes: [
'packages/acp-bridge/src/compactionEngine.ts',
'packages/acp-bridge/dist/compactionEngine.js',
],
},
{
label: 'Core shell tool runtime',
suffixes: [
'packages/core/src/tools/shell.ts',
'packages/core/dist/src/tools/shell.js',
],
},
];
const FORBIDDEN_VENDOR_PACKAGES = [
{ label: 'glob vendor package', packageName: 'glob' },
{ label: 'chokidar vendor package', packageName: 'chokidar' },
{ label: '@iarna/toml vendor package', packageName: '@iarna/toml' },
{ label: 'fzf vendor package', packageName: 'fzf' },
];
export function normalizeMetafilePath(filePath) {
return filePath.replace(/\\/g, '/').replace(/^\.\//, '');
}
function inputMatchesSuffix(input, suffix) {
const normalizedInput = normalizeMetafilePath(input);
return normalizedInput === suffix || normalizedInput.endsWith(`/${suffix}`);
}
function inputMatchesAnySuffix(input, suffixes) {
return suffixes.some((suffix) => inputMatchesSuffix(input, suffix));
}
function inputMatchesPackage(input, packageName) {
const normalizedInput = normalizeMetafilePath(input);
const marker = `node_modules/${packageName}/`;
return (
normalizedInput === `node_modules/${packageName}` ||
normalizedInput.includes(marker)
);
}
function normalizeOutputs(metafile) {
return new Map(
Object.entries(metafile.outputs ?? {}).map(([outputPath, output]) => [
normalizeMetafilePath(outputPath),
output,
]),
);
}
function findServePreListenRootOutputs(outputs) {
const rootOutputs = [];
const missingRoots = [];
for (const root of SERVE_PRE_LISTEN_ROOTS) {
let matchedOutput;
for (const [outputPath, output] of outputs) {
for (const input of Object.keys(output.inputs ?? {})) {
if (inputMatchesAnySuffix(input, root.suffixes)) {
matchedOutput = outputPath;
break;
}
}
if (matchedOutput) break;
}
if (matchedOutput) {
rootOutputs.push(matchedOutput);
} else {
missingRoots.push(`${root.label} (${root.suffixes.join(' or ')})`);
}
}
if (missingRoots.length > 0) {
throw new Error(
'Could not find bundled outputs for serve pre-listen roots:\n' +
missingRoots.map((root) => `- ${root}`).join('\n') +
`\nRun \`${METAFILE_BUILD_COMMAND}\` to produce the metafile.`,
);
}
return [...new Set(rootOutputs)];
}
function collectStaticClosure(outputs, entryOutputs) {
const queue = [...entryOutputs];
const closure = new Set(queue);
const parent = new Map();
for (let i = 0; i < queue.length; i++) {
const outputPath = queue[i];
const output = outputs.get(outputPath);
for (const bundledImport of output?.imports ?? []) {
if (bundledImport.external) continue;
if (bundledImport.kind === 'dynamic-import') continue;
const importedOutput = normalizeMetafilePath(bundledImport.path);
if (!outputs.has(importedOutput) || closure.has(importedOutput)) {
continue;
}
closure.add(importedOutput);
parent.set(importedOutput, outputPath);
queue.push(importedOutput);
}
}
return { closure, parent };
}
function buildImportPath(entryOutputs, outputPath, parent) {
const roots = new Set(entryOutputs);
const reversed = [outputPath];
let current = outputPath;
while (!roots.has(current)) {
current = parent.get(current);
if (!current) break;
reversed.push(current);
}
return reversed.reverse();
}
export function findServeFastPathBundleOffenders(metafile) {
const outputs = normalizeOutputs(metafile);
const entryOutputs = findServePreListenRootOutputs(outputs);
const { closure, parent } = collectStaticClosure(outputs, entryOutputs);
const offenders = [];
const seen = new Set();
for (const outputPath of closure) {
const output = outputs.get(outputPath);
const inputs = Object.keys(output?.inputs ?? {});
for (const input of inputs) {
const sourceMatch = FORBIDDEN_SOURCE_INPUTS.find(({ suffixes }) =>
inputMatchesAnySuffix(input, suffixes),
);
if (sourceMatch) {
addOffender(
sourceMatch.label,
normalizeMetafilePath(input),
outputPath,
);
}
const vendorMatch = FORBIDDEN_VENDOR_PACKAGES.find(({ packageName }) =>
inputMatchesPackage(input, packageName),
);
if (vendorMatch) {
addOffender(
vendorMatch.label,
normalizeMetafilePath(input),
outputPath,
);
}
}
}
return offenders;
function addOffender(label, matchedInput, outputPath) {
const key = `${label}\0${matchedInput}\0${outputPath}`;
if (seen.has(key)) return;
seen.add(key);
offenders.push({
label,
matchedInput,
outputPath,
bytes: outputs.get(outputPath)?.bytes ?? 0,
importPath: buildImportPath(entryOutputs, outputPath, parent),
});
}
}
export function formatServeFastPathBundleOffenders(offenders) {
return offenders
.map((offender) => {
const importPath = offender.importPath.join(' -> ');
return [
`- ${offender.label}`,
` input: ${offender.matchedInput}`,
` output: ${offender.outputPath} (${offender.bytes} bytes)`,
` static path: ${importPath}`,
].join('\n');
})
.join('\n');
}
export function checkServeFastPathBundle({
metafilePath = DEFAULT_METAFILE_PATH,
} = {}) {
if (!existsSync(metafilePath)) {
throw new Error(
`Missing esbuild metafile at ${metafilePath}. ` +
`Run \`${METAFILE_BUILD_COMMAND}\` to produce it.`,
);
}
let metafile;
try {
metafile = JSON.parse(readFileSync(metafilePath, 'utf8'));
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
throw new Error(
`Invalid esbuild metafile at ${metafilePath}: ${reason}. ` +
`Run \`${METAFILE_BUILD_COMMAND}\` to regenerate it.`,
);
}
const offenders = findServeFastPathBundleOffenders(metafile);
return { ok: offenders.length === 0, offenders };
}
function main() {
try {
const result = checkServeFastPathBundle();
if (result.ok) {
console.log('Serve fast-path bundle closure check passed.');
return;
}
console.error(
'Serve fast-path bundle closure includes pre-listen runtime modules:\n' +
formatServeFastPathBundleOffenders(result.offenders),
);
process.exitCode = 1;
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
}
if (
process.argv[1] &&
fileURLToPath(import.meta.url) === resolve(process.argv[1])
) {
main();
}

View file

@ -0,0 +1,384 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect, it } from 'vitest';
import { execFileSync } from 'node:child_process';
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import {
checkServeFastPathBundle,
findServeFastPathBundleOffenders,
formatServeFastPathBundleOffenders,
normalizeMetafilePath,
} from '../check-serve-fast-path-bundle.js';
const checkScriptPath = fileURLToPath(
new URL('../check-serve-fast-path-bundle.js', import.meta.url),
);
function makeMetafile(outputs) {
return {
outputs: {
'dist/chunks/fast-path.js': output({
inputs: ['packages/cli/src/serve/fast-path.ts'],
}),
'dist/chunks/fast-path-settings.js': output({
inputs: ['packages/cli/src/serve/fast-path-settings.ts'],
}),
'dist/chunks/run-qwen-serve.js': output({
inputs: ['packages/cli/src/serve/run-qwen-serve.ts'],
}),
...outputs,
},
};
}
function output({ inputs = [], imports = [], bytes = 1 } = {}) {
return {
bytes,
inputs: Object.fromEntries(
inputs.map((input) => [input, { bytesInOutput: 1 }]),
),
imports,
};
}
function writeMetafile(tempDir, metafile) {
mkdirSync(join(tempDir, 'dist'));
const metafilePath = join(tempDir, 'dist', 'esbuild.json');
writeFileSync(metafilePath, JSON.stringify(metafile));
return metafilePath;
}
function staticImport(path) {
return { path, kind: 'import-statement' };
}
function dynamicImport(path) {
return { path, kind: 'dynamic-import' };
}
describe('serve fast-path bundle check', () => {
it('reports forbidden source files reached through static imports', () => {
const metafile = makeMetafile({
'dist/chunks/run-qwen-serve.js': output({
inputs: ['packages/cli/src/serve/run-qwen-serve.ts'],
imports: [staticImport('dist/chunks/acp-runtime.js')],
}),
'dist/chunks/acp-runtime.js': output({
bytes: 179_129,
inputs: ['packages/acp-bridge/src/bridgeClient.ts'],
}),
});
const offenders = findServeFastPathBundleOffenders(metafile);
const diagnostic = formatServeFastPathBundleOffenders(offenders);
expect(offenders).toEqual([
expect.objectContaining({
label: 'ACP bridge client runtime',
matchedInput: 'packages/acp-bridge/src/bridgeClient.ts',
outputPath: 'dist/chunks/acp-runtime.js',
bytes: 179_129,
importPath: [
'dist/chunks/run-qwen-serve.js',
'dist/chunks/acp-runtime.js',
],
}),
]);
expect(diagnostic).toContain('- ACP bridge client runtime');
expect(diagnostic).toContain(
'output: dist/chunks/acp-runtime.js (179129 bytes)',
);
expect(diagnostic).toContain(
'static path: dist/chunks/run-qwen-serve.js -> dist/chunks/acp-runtime.js',
);
});
it('reports forbidden built package files reached through static imports', () => {
const metafile = makeMetafile({
'dist/chunks/run-qwen-serve.js': output({
inputs: ['packages/cli/dist/src/serve/run-qwen-serve.js'],
imports: [staticImport('dist/chunks/acp-runtime.js')],
}),
'dist/chunks/acp-runtime.js': output({
inputs: ['packages/acp-bridge/dist/bridge.js'],
}),
});
expect(findServeFastPathBundleOffenders(metafile)).toEqual([
expect.objectContaining({
label: 'ACP bridge runtime',
matchedInput: 'packages/acp-bridge/dist/bridge.js',
}),
]);
});
it('checks fast-path modules that run before runQwenServe listens', () => {
const metafile = makeMetafile({
'dist/chunks/fast-path.js': output({
inputs: ['packages/cli/src/serve/fast-path.ts'],
imports: [staticImport('dist/chunks/core-runtime.js')],
}),
'dist/chunks/core-runtime.js': output({
inputs: ['packages/core/dist/src/tools/shell.js'],
}),
});
const offenders = findServeFastPathBundleOffenders(metafile);
expect(offenders).toEqual([
expect.objectContaining({
label: 'Core shell tool runtime',
matchedInput: 'packages/core/dist/src/tools/shell.js',
importPath: ['dist/chunks/fast-path.js', 'dist/chunks/core-runtime.js'],
}),
]);
});
it('allows forbidden runtime files behind dynamic imports', () => {
const metafile = makeMetafile({
'dist/chunks/run-qwen-serve.js': output({
inputs: ['packages/cli/src/serve/run-qwen-serve.ts'],
imports: [dynamicImport('dist/chunks/bridge.js')],
}),
'dist/chunks/bridge.js': output({
inputs: ['packages/acp-bridge/src/bridge.ts'],
}),
});
expect(findServeFastPathBundleOffenders(metafile)).toEqual([]);
});
it('ignores external imports in the static closure', () => {
const metafile = makeMetafile({
'dist/chunks/run-qwen-serve.js': output({
inputs: ['packages/cli/src/serve/run-qwen-serve.ts'],
imports: [
{
path: 'dist/chunks/acp-runtime.js',
kind: 'import-statement',
external: true,
},
],
}),
'dist/chunks/acp-runtime.js': output({
inputs: ['packages/acp-bridge/src/bridge.ts'],
}),
});
expect(findServeFastPathBundleOffenders(metafile)).toEqual([]);
});
it('reports vendor packages reached through the core runtime chunk', () => {
const metafile = makeMetafile({
'dist/chunks/run-qwen-serve.js': output({
inputs: ['packages/cli/src/serve/run-qwen-serve.ts'],
imports: [staticImport('dist/chunks/core-runtime.js')],
}),
'dist/chunks/core-runtime.js': output({
bytes: 6_015_919,
inputs: [
'packages/core/src/tools/shell.ts',
'node_modules/.pnpm/glob@10.5.0/node_modules/glob/dist/esm/index.js',
'node_modules/.pnpm/@iarna+toml@2.2.5/node_modules/@iarna/toml/toml.js',
'node_modules/chokidar/esm/index.js',
'node_modules/fzf/dist/fzf.es.js',
],
}),
});
const offenders = findServeFastPathBundleOffenders(metafile);
expect(offenders.map((offender) => offender.label)).toEqual([
'Core shell tool runtime',
'glob vendor package',
'@iarna/toml vendor package',
'chokidar vendor package',
'fzf vendor package',
]);
expect(offenders[0].importPath).toEqual([
'dist/chunks/run-qwen-serve.js',
'dist/chunks/core-runtime.js',
]);
});
it('matches normalized source suffixes without accepting partial names', () => {
const metafile = makeMetafile({
'dist\\chunks\\run-qwen-serve.js': output({
inputs: ['..\\..\\packages\\cli\\src\\serve\\run-qwen-serve.ts'],
imports: [staticImport('dist\\chunks\\false-positive.js')],
}),
'dist\\chunks\\false-positive.js': output({
inputs: ['packages/acp-bridge/src/not-bridge.ts'],
}),
});
expect(normalizeMetafilePath('dist\\chunks\\run-qwen-serve.js')).toBe(
'dist/chunks/run-qwen-serve.js',
);
expect(findServeFastPathBundleOffenders(metafile)).toEqual([]);
});
it('throws when serve pre-listen roots are missing', () => {
const metafile = {
outputs: {
'dist/chunks/unrelated.js': output({
inputs: ['packages/cli/src/unrelated.ts'],
}),
},
};
expect(() => findServeFastPathBundleOffenders(metafile)).toThrow(
/Could not find bundled outputs for serve pre-listen roots/,
);
expect(() => findServeFastPathBundleOffenders(metafile)).toThrow(
/npm run build -- --cli-only && npx cross-env DEV=true npm run bundle/,
);
});
it('reports each matched input for the same forbidden label and output', () => {
const metafile = makeMetafile({
'dist/chunks/run-qwen-serve.js': output({
inputs: ['packages/cli/src/serve/run-qwen-serve.ts'],
imports: [staticImport('dist/chunks/acp-runtime.js')],
}),
'dist/chunks/acp-runtime.js': output({
inputs: [
'packages/acp-bridge/src/bridge.ts',
'packages/acp-bridge/dist/bridge.js',
],
}),
});
const offenders = findServeFastPathBundleOffenders(metafile);
expect(offenders).toEqual([
expect.objectContaining({
label: 'ACP bridge runtime',
matchedInput: 'packages/acp-bridge/src/bridge.ts',
}),
expect.objectContaining({
label: 'ACP bridge runtime',
matchedInput: 'packages/acp-bridge/dist/bridge.js',
}),
]);
});
it('reads a metafile path and returns bundle offenders', () => {
const tempDir = mkdtempSync(join(tmpdir(), 'serve-fast-path-bundle-'));
try {
const metafilePath = writeMetafile(
tempDir,
makeMetafile({
'dist/chunks/run-qwen-serve.js': output({
inputs: ['packages/cli/src/serve/run-qwen-serve.ts'],
imports: [staticImport('dist/chunks/acp-runtime.js')],
}),
'dist/chunks/acp-runtime.js': output({
inputs: ['packages/acp-bridge/src/bridge.ts'],
}),
}),
);
expect(checkServeFastPathBundle({ metafilePath })).toEqual({
ok: false,
offenders: [
expect.objectContaining({
label: 'ACP bridge runtime',
matchedInput: 'packages/acp-bridge/src/bridge.ts',
}),
],
});
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
it('throws when the checked metafile path is missing', () => {
const tempDir = mkdtempSync(join(tmpdir(), 'serve-fast-path-bundle-'));
try {
expect(() =>
checkServeFastPathBundle({
metafilePath: join(tempDir, 'dist', 'esbuild.json'),
}),
).toThrow(/Missing esbuild metafile/);
expect(() =>
checkServeFastPathBundle({
metafilePath: join(tempDir, 'dist', 'esbuild.json'),
}),
).toThrow(
/npm run build -- --cli-only && npx cross-env DEV=true npm run bundle/,
);
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
it('throws with context when the metafile is invalid JSON', () => {
const tempDir = mkdtempSync(join(tmpdir(), 'serve-fast-path-bundle-'));
try {
mkdirSync(join(tempDir, 'dist'));
const metafilePath = join(tempDir, 'dist', 'esbuild.json');
writeFileSync(metafilePath, '{');
expect(() => checkServeFastPathBundle({ metafilePath })).toThrow(
/Invalid esbuild metafile at .*dist[/\\]esbuild\.json/,
);
expect(() => checkServeFastPathBundle({ metafilePath })).toThrow(
/Run `npm run build -- --cli-only && npx cross-env DEV=true npm run bundle` to regenerate it/,
);
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
it('exits non-zero with CLI diagnostics for bundle offenders', () => {
const tempDir = mkdtempSync(join(tmpdir(), 'serve-fast-path-bundle-'));
try {
writeMetafile(
tempDir,
makeMetafile({
'dist/chunks/run-qwen-serve.js': output({
inputs: ['packages/cli/src/serve/run-qwen-serve.ts'],
imports: [staticImport('dist/chunks/acp-runtime.js')],
}),
'dist/chunks/acp-runtime.js': output({
bytes: 179_129,
inputs: ['packages/acp-bridge/src/bridge.ts'],
}),
}),
);
expect(() =>
execFileSync(process.execPath, [checkScriptPath], {
cwd: tempDir,
encoding: 'utf8',
stdio: 'pipe',
}),
).toThrow(/Serve fast-path bundle closure includes pre-listen runtime/);
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
it('exits non-zero when the metafile is missing', () => {
const tempDir = mkdtempSync(join(tmpdir(), 'serve-fast-path-bundle-'));
try {
expect(() =>
execFileSync(process.execPath, [checkScriptPath], {
cwd: tempDir,
encoding: 'utf8',
stdio: 'pipe',
}),
).toThrow(/Missing esbuild metafile/);
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
});