mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
* fix(cli): handle ACP read_file local roots Allow ACP read_file calls to fall back to local reads for explicitly permitted local roots when the serve workspace boundary rejects them, and preserve useful messages for plain object read errors. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6021) Add the missing getUserAutoMemoryRoot export to the acpAgent test core mock so the updated acpAgent import resolves under Vitest. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6021) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6021) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(acp): Narrow local read fallback temp roots Remove the broad OS temp directory from default read_file allow roots and ACP local read fallback roots. Keep qwen-managed temp roots readable and reuse the shared isSubpath helper after realpath resolution for ACP fallback containment. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: fix CI failure on PR #6021 Add the serve fast-path bundle check script and root npm script that the current CI workflow invokes. This mirrors the already-merged mainline check without pulling unrelated workflow or test config changes into this PR. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): address ACP read error review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): harden ACP error normalization Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6021) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6021) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6021) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6021) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6021) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: fix Windows CI path expectation (#6021) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6021) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6021) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6021) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6021) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
384 lines
12 KiB
JavaScript
384 lines
12 KiB
JavaScript
/**
|
|
* @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 && 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 && 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 && 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 });
|
|
}
|
|
});
|
|
});
|