qwen-code/packages/cli/src/serve/fast-path.test.ts
ChiGao 53243de0c0
feat(daemon): persist session artifacts across restarts (#6557)
* feat(daemon): persist session artifact metadata

* fix(daemon): address artifact restore review findings

* fix(daemon): harden artifact persistence restore

* fix(daemon): align artifact persistence review decisions

* fix(daemon): address artifact persistence review gaps

* fix(daemon): harden artifact persistence recovery

* fix(daemon): align artifact ownership capability

* fix(daemon): preserve marker identity during fork

* fix(daemon): roll back durable replacement removals

* fix(daemon): surface artifact rollback warnings

* fix(daemon): surface restore warning details

* fix(daemon): preserve artifact marker metadata safely

* fix(daemon): sanitize fork marker metadata

* fix(daemon): harden artifact restore boundaries

* fix(daemon): omit orphaned sticky snapshot markers

* fix(daemon): preserve artifact tombstone and rewind warnings

* fix(daemon): address artifact fork review blockers

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: 易良 <1204183885@qq.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-09 13:04:16 +00:00

1780 lines
58 KiB
TypeScript

/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { afterEach, describe, expect, it, vi } from 'vitest';
import yargs, { type Argv } from 'yargs';
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
realpathSync,
rmSync,
symlinkSync,
writeFileSync,
} from 'node:fs';
import * as os from 'node:os';
import { dirname, join, relative, resolve } from 'node:path';
import * as ts from 'typescript';
import { QWEN_DIR, Storage } from '@qwen-code/qwen-code-core';
import {
bootstrapServeFastPathEnvironment,
parseServeFastPathArgs,
tryRunServeFastPath,
waitForServeRuntimeOrExit,
} from './fast-path.js';
import {
loadServeFastPathEnvironment,
loadServeFastPathSettings,
preResolveServeFastPathHomeEnvOverrides,
resetServeFastPathHomeEnvBootstrapForTesting,
} from './fast-path-settings.js';
import {
getGlobalQwenDirLite,
SETTINGS_DIRECTORY_NAME,
} from '../config/storage-paths-lite.js';
import { RUNTIME_STARTUP_CANCELLED_MESSAGE } from './runtime-startup-errors.js';
import {
resetTrustedFoldersForTesting,
TrustLevel,
} from '../config/trustedFolders.js';
import { HEADLESS_YOLO_NO_SANDBOX_WARNING } from '../utils/headlessSafetyWarnings.js';
import * as runQwenServeModule from './run-qwen-serve.js';
import type { ServeFastPathSettings } from './fast-path-settings.js';
import type { Settings } from '../config/settingsSchema.js';
import { serveCommand } from '../commands/serve.js';
let tempWorkspace: string | undefined;
let tempLaunchCwd: string | undefined;
let tempQwenHome: string | undefined;
let tempSymlink: string | undefined;
const originalToken = process.env['QWEN_SERVER_TOKEN'];
const originalQwenHome = process.env['QWEN_HOME'];
const originalHome = process.env['HOME'];
const originalUserProfile = process.env['USERPROFILE'];
const originalQwenRuntimeDir = process.env['QWEN_RUNTIME_DIR'];
const originalMcpApprovalsPath = process.env['QWEN_CODE_MCP_APPROVALS_PATH'];
const originalSystemSettingsPath =
process.env['QWEN_CODE_SYSTEM_SETTINGS_PATH'];
const originalSystemDefaultsPath =
process.env['QWEN_CODE_SYSTEM_DEFAULTS_PATH'];
const originalTrustedFoldersPath =
process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'];
const originalReferencedToken = process.env['FAST_PATH_REFERENCED_TOKEN'];
const originalRateLimit = process.env['QWEN_SERVE_RATE_LIMIT'];
const originalRateLimitPrompt = process.env['QWEN_SERVE_RATE_LIMIT_PROMPT'];
const originalCloudShell = process.env['CLOUD_SHELL'];
const originalGoogleCloudProject = process.env['GOOGLE_CLOUD_PROJECT'];
const originalCwd = process.cwd();
const cliPackageRoot = process.cwd();
interface StaticSourceGraph {
localFiles: Set<string>;
externalValueImports: Set<string>;
unresolvedLocalImports: string[];
}
function normalizePathForTest(filePath: string): string {
return filePath.replace(/\\/g, '/');
}
function moduleSpecifierText(
specifier: ts.Expression | undefined,
): string | undefined {
if (!specifier || !ts.isStringLiteral(specifier)) return undefined;
return specifier.text;
}
function importDeclarationHasRuntimeValue(node: ts.ImportDeclaration): boolean {
const clause = node.importClause;
if (!clause) return true;
if (clause.isTypeOnly) return false;
if (clause.name) return true;
const bindings = clause.namedBindings;
if (!bindings) return false;
if (ts.isNamespaceImport(bindings)) return true;
if (bindings.elements.length === 0) return true;
return bindings.elements.some((element) => !element.isTypeOnly);
}
function exportDeclarationHasRuntimeValue(node: ts.ExportDeclaration): boolean {
if (node.isTypeOnly) return false;
const clause = node.exportClause;
if (!clause) return true;
if (ts.isNamespaceExport(clause)) return true;
if (clause.elements.length === 0) return true;
return clause.elements.some((element) => !element.isTypeOnly);
}
function resolveLocalSourceImport(
importer: string,
specifier: string,
): string | undefined {
const basePath = resolve(dirname(importer), specifier);
const candidates = specifier.endsWith('.js')
? [`${basePath.slice(0, -3)}.ts`, `${basePath.slice(0, -3)}.tsx`]
: [
`${basePath}.ts`,
`${basePath}.tsx`,
join(basePath, 'index.ts'),
join(basePath, 'index.tsx'),
];
return candidates.find((candidate) => existsSync(candidate));
}
function collectStaticSourceGraph(entryFile: string): StaticSourceGraph {
const visited = new Set<string>();
const localFiles = new Set<string>();
const externalValueImports = new Set<string>();
const unresolvedLocalImports: string[] = [];
function visit(filePath: string): void {
const normalizedFilePath = resolve(filePath);
if (visited.has(normalizedFilePath)) return;
visited.add(normalizedFilePath);
localFiles.add(
normalizePathForTest(relative(cliPackageRoot, normalizedFilePath)),
);
const sourceText = readFileSync(normalizedFilePath, 'utf8');
const sourceFile = ts.createSourceFile(
normalizedFilePath,
sourceText,
ts.ScriptTarget.Latest,
true,
normalizedFilePath.endsWith('.tsx')
? ts.ScriptKind.TSX
: ts.ScriptKind.TS,
);
for (const statement of sourceFile.statements) {
let specifier: string | undefined;
let hasRuntimeValue = false;
if (ts.isImportDeclaration(statement)) {
specifier = moduleSpecifierText(statement.moduleSpecifier);
hasRuntimeValue = importDeclarationHasRuntimeValue(statement);
} else if (ts.isExportDeclaration(statement)) {
specifier = moduleSpecifierText(statement.moduleSpecifier);
hasRuntimeValue = exportDeclarationHasRuntimeValue(statement);
}
if (!specifier || !hasRuntimeValue) continue;
if (!specifier.startsWith('.')) {
externalValueImports.add(specifier);
continue;
}
const resolvedImport = resolveLocalSourceImport(
normalizedFilePath,
specifier,
);
if (!resolvedImport) {
unresolvedLocalImports.push(
`${normalizePathForTest(relative(cliPackageRoot, normalizedFilePath))} -> ${specifier}`,
);
continue;
}
visit(resolvedImport);
}
}
visit(entryFile);
return { localFiles, externalValueImports, unresolvedLocalImports };
}
function useTempQwenHome(): string {
tempQwenHome = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-home-')),
);
process.env['QWEN_HOME'] = tempQwenHome;
process.env['QWEN_CODE_SYSTEM_SETTINGS_PATH'] = join(
tempQwenHome,
'system-settings.json',
);
process.env['QWEN_CODE_SYSTEM_DEFAULTS_PATH'] = join(
tempQwenHome,
'system-defaults.json',
);
return tempQwenHome;
}
function buildServeCommandParser(): Argv {
return (serveCommand.builder as (argv: Argv) => Argv)(
yargs([]).exitProcess(false).fail(false).locale('en'),
);
}
function pickServeFastPathComparable(
settings: Settings,
): ServeFastPathSettings {
const out: ServeFastPathSettings = {};
if (settings.env) {
out.env = settings.env;
}
if (settings.general?.chatRecording !== undefined) {
out.general = { chatRecording: settings.general.chatRecording };
}
if (settings.advanced?.excludedEnvVars !== undefined) {
out.advanced = {
...(out.advanced ?? {}),
excludedEnvVars: settings.advanced.excludedEnvVars,
};
}
if (settings.advanced?.runtimeOutputDir !== undefined) {
out.advanced = {
...(out.advanced ?? {}),
runtimeOutputDir: settings.advanced.runtimeOutputDir,
};
}
if (settings.security?.folderTrust?.enabled !== undefined) {
out.security = {
folderTrust: { enabled: settings.security.folderTrust.enabled },
};
}
if (settings.tools?.approvalMode !== undefined) {
out.tools = {
...(out.tools ?? {}),
approvalMode: settings.tools.approvalMode,
};
}
if (settings.tools?.sandbox !== undefined) {
out.tools = { ...(out.tools ?? {}), sandbox: settings.tools.sandbox };
}
if (settings.context?.fileName !== undefined) {
out.context = {
...(out.context ?? {}),
fileName: settings.context.fileName,
};
}
if (settings.context?.fileFiltering?.customIgnoreFiles !== undefined) {
out.context = {
...(out.context ?? {}),
fileFiltering: {
customIgnoreFiles: settings.context.fileFiltering.customIgnoreFiles,
},
};
}
if (settings.policy?.permissionStrategy !== undefined) {
out.policy = {
...(out.policy ?? {}),
permissionStrategy: settings.policy.permissionStrategy,
};
}
if (settings.policy?.consensusQuorum !== undefined) {
out.policy = {
...(out.policy ?? {}),
consensusQuorum: settings.policy.consensusQuorum,
};
}
return out;
}
afterEach(() => {
vi.restoreAllMocks();
process.chdir(originalCwd);
if (originalToken === undefined) {
delete process.env['QWEN_SERVER_TOKEN'];
} else {
process.env['QWEN_SERVER_TOKEN'] = originalToken;
}
if (originalQwenHome === undefined) {
delete process.env['QWEN_HOME'];
} else {
process.env['QWEN_HOME'] = originalQwenHome;
}
if (originalHome === undefined) {
delete process.env['HOME'];
} else {
process.env['HOME'] = originalHome;
}
if (originalUserProfile === undefined) {
delete process.env['USERPROFILE'];
} else {
process.env['USERPROFILE'] = originalUserProfile;
}
if (originalSystemSettingsPath === undefined) {
delete process.env['QWEN_CODE_SYSTEM_SETTINGS_PATH'];
} else {
process.env['QWEN_CODE_SYSTEM_SETTINGS_PATH'] = originalSystemSettingsPath;
}
if (originalSystemDefaultsPath === undefined) {
delete process.env['QWEN_CODE_SYSTEM_DEFAULTS_PATH'];
} else {
process.env['QWEN_CODE_SYSTEM_DEFAULTS_PATH'] = originalSystemDefaultsPath;
}
if (originalTrustedFoldersPath === undefined) {
delete process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'];
} else {
process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'] = originalTrustedFoldersPath;
}
if (originalReferencedToken === undefined) {
delete process.env['FAST_PATH_REFERENCED_TOKEN'];
} else {
process.env['FAST_PATH_REFERENCED_TOKEN'] = originalReferencedToken;
}
if (originalRateLimit === undefined) {
delete process.env['QWEN_SERVE_RATE_LIMIT'];
} else {
process.env['QWEN_SERVE_RATE_LIMIT'] = originalRateLimit;
}
if (originalRateLimitPrompt === undefined) {
delete process.env['QWEN_SERVE_RATE_LIMIT_PROMPT'];
} else {
process.env['QWEN_SERVE_RATE_LIMIT_PROMPT'] = originalRateLimitPrompt;
}
if (originalCloudShell === undefined) {
delete process.env['CLOUD_SHELL'];
} else {
process.env['CLOUD_SHELL'] = originalCloudShell;
}
if (originalGoogleCloudProject === undefined) {
delete process.env['GOOGLE_CLOUD_PROJECT'];
} else {
process.env['GOOGLE_CLOUD_PROJECT'] = originalGoogleCloudProject;
}
if (originalQwenRuntimeDir === undefined) {
delete process.env['QWEN_RUNTIME_DIR'];
} else {
process.env['QWEN_RUNTIME_DIR'] = originalQwenRuntimeDir;
}
if (originalMcpApprovalsPath === undefined) {
delete process.env['QWEN_CODE_MCP_APPROVALS_PATH'];
} else {
process.env['QWEN_CODE_MCP_APPROVALS_PATH'] = originalMcpApprovalsPath;
}
resetServeFastPathHomeEnvBootstrapForTesting();
resetTrustedFoldersForTesting();
if (tempWorkspace) {
rmSync(tempWorkspace, { recursive: true, force: true });
tempWorkspace = undefined;
}
if (tempLaunchCwd) {
rmSync(tempLaunchCwd, { recursive: true, force: true });
tempLaunchCwd = undefined;
}
if (tempQwenHome) {
rmSync(tempQwenHome, { recursive: true, force: true });
tempQwenHome = undefined;
}
if (tempSymlink) {
rmSync(tempSymlink, { force: true });
tempSymlink = undefined;
}
});
describe('CLI entry import boundary', () => {
it('does not statically import the full gemini entry before the serve fast path can run', () => {
const cliSource = readFileSync('src/cli.ts', 'utf8');
expect(cliSource).not.toContain("import './gemini.js'");
expect(cliSource).not.toContain("import { main } from './gemini.js'");
expect(cliSource).not.toContain("process.argv[2] === 'serve'");
expect(cliSource).toContain("await import('./serve/fast-path.js')");
});
it('does not import the full settings loader on the serve fast path', () => {
const fastPathSource = readFileSync('src/serve/fast-path.ts', 'utf8');
expect(fastPathSource).not.toContain('../config/settings.js');
expect(fastPathSource).not.toContain('../config/environment.js');
expect(fastPathSource).not.toContain('@qwen-code/qwen-code-core');
expect(fastPathSource).toContain('bootSettings: settings');
expect(fastPathSource).toContain('resolveOnListen: true');
expect(fastPathSource).toContain(
'deferRuntimeUntilFirstHealth: !parsed.open',
);
});
it('uses the shared headless yolo warning helper on the serve fast path', () => {
const fastPathSource = readFileSync('src/serve/fast-path.ts', 'utf8');
expect(fastPathSource).toContain('getHeadlessYoloSafetyWarning');
expect(fastPathSource).not.toContain(
"settings.tools?.approvalMode === 'yolo'",
);
});
it('keeps headless yolo warning helper free of runtime core imports', () => {
const helperSource = readFileSync(
'src/utils/headlessSafetyWarnings.ts',
'utf8',
);
expect(helperSource).not.toMatch(
/import\s+(?!type\b)[^;]*from ['"]@qwen-code\/qwen-code-core['"]/,
);
});
it('keeps settings free of UI imports used before serve can listen', () => {
const settingsSource = readFileSync('src/config/settings.ts', 'utf8');
expect(settingsSource).not.toContain('../ui/');
});
it('keeps extension command parsing free of UI state imports', () => {
const updateCommandSource = readFileSync(
'src/commands/extensions/update.ts',
'utf8',
);
expect(updateCommandSource).not.toContain('../../ui/');
});
it('keeps runQwenServe from statically loading the full server and ACP runtime', () => {
const runServeSource = readFileSync('src/serve/run-qwen-serve.ts', 'utf8');
expect(runServeSource).not.toMatch(/from ['"]\.\/server\.js['"]/);
expect(runServeSource).not.toMatch(/from ['"]\.\/web-shell-static\.js['"]/);
expect(runServeSource).not.toMatch(
/from ['"]\.\/acp-session-bridge\.js['"]/,
);
expect(runServeSource).not.toMatch(
/from ['"]@qwen-code\/acp-bridge\/bridge['"]/,
);
expect(runServeSource).not.toMatch(
/from ['"]@qwen-code\/acp-bridge\/spawnChannel['"]/,
);
expect(runServeSource).toContain("import('./server.js')");
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'),
);
expect(graph.unresolvedLocalImports).toEqual([]);
const forbiddenLocalFiles = [...graph.localFiles].filter(
(filePath) => filePath === 'src/serve/acp-session-bridge.ts',
);
expect(
forbiddenLocalFiles,
`Unexpected static source graph files:\n${forbiddenLocalFiles.join('\n')}`,
).toEqual([]);
const forbiddenExternalImports = [
'@qwen-code/acp-bridge',
'@qwen-code/acp-bridge/bridge',
'@qwen-code/acp-bridge/spawnChannel',
'@qwen-code/acp-bridge/bridgeClient',
'@qwen-code/acp-bridge/bridgeErrors',
];
const forbiddenImports = [...graph.externalValueImports].filter(
(specifier) => forbiddenExternalImports.includes(specifier),
);
expect(
forbiddenImports,
`Unexpected ACP runtime imports:\n${forbiddenImports.join('\n')}`,
).toEqual([]);
});
});
describe('serve fast path argument parsing', () => {
it('parses the common daemon startup flags without loading the full CLI parser', () => {
const parsed = parseServeFastPathArgs([
'serve',
'--port',
'0',
'--hostname',
'127.0.0.1',
'--workspace',
'/tmp/workspace',
'--no-web',
'--no-open',
]);
expect(parsed).toEqual({
kind: 'serve',
httpBridge: true,
open: false,
options: {
hostname: '127.0.0.1',
mcpBudgetMode: 'off',
mode: 'http-bridge',
port: 0,
serveWebShell: false,
workspace: '/tmp/workspace',
},
});
});
it('parses --tls-cert and --tls-key on the fast path', () => {
const parsed = parseServeFastPathArgs([
'serve',
'--tls-cert',
'/tmp/cert.pem',
'--tls-key',
'/tmp/key.pem',
]);
expect(parsed).toMatchObject({
kind: 'serve',
options: {
tlsCert: '/tmp/cert.pem',
tlsKey: '/tmp/key.pem',
},
});
});
it('parses bundled entrypoint argv before serve', () => {
const parsed = parseServeFastPathArgs([
'/repo/dist/cli.js',
'serve',
'--port',
'0',
]);
expect(parsed).toMatchObject({
kind: 'serve',
options: { port: 0 },
});
});
it('falls back to the full parser for repeatable --workspace values', () => {
expect(
parseServeFastPathArgs([
'serve',
'--workspace',
'/tmp/primary',
'--workspace',
'/tmp/secondary',
]),
).toEqual({ kind: 'fallback' });
});
it('falls back to the full parser for empty --workspace values', () => {
expect(parseServeFastPathArgs(['serve', '--workspace='])).toEqual({
kind: 'fallback',
});
expect(parseServeFastPathArgs(['serve', '--workspace', ''])).toEqual({
kind: 'fallback',
});
});
it('parses Windows bundled entrypoint argv before serve', () => {
const parsed = parseServeFastPathArgs([
'C:\\repo\\dist\\cli.js',
'serve',
'--port',
'0',
]);
expect(parsed).toMatchObject({
kind: 'serve',
options: { port: 0 },
});
});
it('falls back to the full parser for help and unknown options', () => {
expect(parseServeFastPathArgs(['serve', '--help'])).toEqual({
kind: 'fallback',
});
expect(parseServeFastPathArgs(['serve', '--unknown-option'])).toEqual({
kind: 'fallback',
});
});
it('falls back to the full parser for daemon-managed channels', () => {
expect(parseServeFastPathArgs(['serve', '--channel', 'telegram'])).toEqual({
kind: 'fallback',
});
});
it('handles every yargs serve long option or explicitly falls back', () => {
const options = (
buildServeCommandParser() as unknown as {
getOptions(): {
key: Record<string, boolean>;
alias: Record<string, string[]>;
};
}
).getOptions();
const longOptionNames = Object.keys(options.key).filter(
(name) => name.length > 1 && !options.alias[name]?.length,
);
const sampleArgvByOption = new Map<string, string[]>([
['port', ['--port', '0']],
['hostname', ['--hostname', '127.0.0.1']],
['token', ['--token', 'token']],
['max-sessions', ['--max-sessions', '10']],
['max-total-sessions', ['--max-total-sessions', '20']],
[
'max-pending-prompts-per-session',
['--max-pending-prompts-per-session', '5'],
],
['max-connections', ['--max-connections', '256']],
['event-ring-size', ['--event-ring-size', '8000']],
[
'compacted-replay-max-bytes',
['--compacted-replay-max-bytes', '4194304'],
],
['workspace', ['--workspace', process.cwd()]],
['require-auth', ['--require-auth']],
['enable-session-shell', ['--enable-session-shell']],
['tls-cert', ['--tls-cert', '/tmp/cert.pem']],
['tls-key', ['--tls-key', '/tmp/key.pem']],
['web', ['--no-web']],
['open', ['--open']],
['http-bridge', ['--no-http-bridge']],
['mcp-client-budget', ['--mcp-client-budget', '10']],
['mcp-budget-mode', ['--mcp-budget-mode', 'warn']],
['allow-origin', ['--allow-origin', 'http://localhost:3000']],
['allow-private-auth-base-url', ['--allow-private-auth-base-url']],
['prompt-deadline-ms', ['--prompt-deadline-ms', '1000']],
['writer-idle-timeout-ms', ['--writer-idle-timeout-ms', '1000']],
['channel-idle-timeout-ms', ['--channel-idle-timeout-ms', '1000']],
['session-reap-interval-ms', ['--session-reap-interval-ms', '1000']],
['session-idle-timeout-ms', ['--session-idle-timeout-ms', '1000']],
[
'permission-response-timeout-ms',
['--permission-response-timeout-ms', '1000'],
],
['rate-limit', ['--rate-limit']],
['rate-limit-prompt', ['--rate-limit-prompt', '10']],
['rate-limit-mutation', ['--rate-limit-mutation', '30']],
['rate-limit-read', ['--rate-limit-read', '120']],
['rate-limit-window-ms', ['--rate-limit-window-ms', '60000']],
['experimental-lsp', ['--experimental-lsp']],
['channel', ['--channel', 'telegram']],
['help', ['--help']],
['version', ['--version']],
]);
const expectedFallbackOptions = new Set(['channel', 'help', 'version']);
expect(longOptionNames.sort()).toEqual(
[...sampleArgvByOption.keys()].sort(),
);
for (const [optionName, sampleArgv] of sampleArgvByOption) {
const parsed = parseServeFastPathArgs(['serve', ...sampleArgv]);
if (parsed.kind === 'fallback') {
expect(expectedFallbackOptions.has(optionName)).toBe(true);
} else {
expect(expectedFallbackOptions.has(optionName)).toBe(false);
}
}
});
it('matches yargs defaults for options materialized before runQwenServe', () => {
const yargsParsed = buildServeCommandParser().parseSync('');
const fastPathParsed = parseServeFastPathArgs(['serve']);
expect(fastPathParsed).toMatchObject({
kind: 'serve',
options: {
hostname: yargsParsed['hostname'],
mode: 'http-bridge',
port: yargsParsed['port'],
},
});
expect(fastPathParsed).not.toHaveProperty('options.maxSessions');
expect(fastPathParsed).not.toHaveProperty('options.maxTotalSessions');
expect(fastPathParsed).not.toHaveProperty('options.maxConnections');
expect(fastPathParsed).not.toHaveProperty('options.eventRingSize');
expect(fastPathParsed).not.toHaveProperty(
'options.compactedReplayMaxBytes',
);
expect(fastPathParsed).not.toHaveProperty(
'options.maxPendingPromptsPerSession',
);
});
it('parses --compacted-replay-max-bytes on the fast path', () => {
const parsed = parseServeFastPathArgs([
'serve',
'--compacted-replay-max-bytes',
'1048576',
]);
expect(parsed).toMatchObject({
kind: 'serve',
options: { compactedReplayMaxBytes: 1024 * 1024 },
});
});
it('keeps --experimental-lsp on the fast path', () => {
const parsed = parseServeFastPathArgs(['serve', '--experimental-lsp']);
expect(parsed).toMatchObject({
kind: 'serve',
options: { experimentalLsp: true },
});
});
it('returns false to let the full CLI handle fallback cases', async () => {
await expect(tryRunServeFastPath(['serve', '--help'])).resolves.toBe(false);
});
it('prints a breadcrumb when settings bootstrap falls back to the full CLI', async () => {
tempWorkspace = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-fallback-')),
);
mkdirSync(join(tempWorkspace, '.qwen'));
writeFileSync(join(tempWorkspace, '.qwen', 'settings.json'), '{');
const stderrWrites: string[] = [];
vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
stderrWrites.push(String(chunk));
return true;
});
await expect(
tryRunServeFastPath(['serve', '--workspace', tempWorkspace]),
).resolves.toBe(false);
expect(stderrWrites.join('')).toContain(
'qwen serve: fast-path bootstrap failed, falling back to full startup:',
);
});
it.each([
[
['serve', '--mcp-client-budget', '0'],
'qwen serve: --mcp-client-budget must be a positive integer.',
],
[
['serve', '--mcp-budget-mode', 'enforce'],
'qwen serve: --mcp-budget-mode=enforce requires --mcp-client-budget=N.',
],
[
['serve', '--max-pending-prompts-per-session=-1'],
'qwen serve: --max-pending-prompts-per-session must be a non-negative integer (0 / Infinity = unlimited).',
],
[
['serve', '--compacted-replay-max-bytes=0'],
'qwen serve: --compacted-replay-max-bytes must be a positive safe integer in [1, 268435456].',
],
[
['serve', '--rate-limit', '--rate-limit-prompt=0'],
'qwen serve: --rate-limit-prompt must be a positive integer.',
],
])(
'validates %s before bootstrapping settings and environment',
async (argv, message) => {
const qwenHome = useTempQwenHome();
writeFileSync(join(qwenHome, 'settings.json'), '{');
const stderrWrites: string[] = [];
vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
stderrWrites.push(String(chunk));
return true;
});
vi.spyOn(process, 'exit').mockImplementation(((
code?: string | number | null,
) => {
throw new Error(`process.exit(${code})`);
}) as typeof process.exit);
await expect(tryRunServeFastPath(argv)).rejects.toThrow(
'process.exit(1)',
);
expect(stderrWrites.join('')).toContain(message);
},
);
it('does not enable rate limiting just because tuning flags are present', () => {
const parsed = parseServeFastPathArgs([
'serve',
'--rate-limit-prompt',
'0',
'--rate-limit-window-ms',
'1',
]);
expect(parsed.kind).toBe('serve');
if (parsed.kind !== 'serve') return;
expect(parsed.options).not.toHaveProperty('rateLimit');
expect(parsed.options.rateLimitPrompt).toBe(0);
expect(parsed.options.rateLimitWindowMs).toBe(1);
});
it('enables rate limiting from env and applies env tuning values', () => {
const parsed = parseServeFastPathArgs(['serve'], {
QWEN_SERVE_RATE_LIMIT: '1',
QWEN_SERVE_RATE_LIMIT_PROMPT: '10',
});
expect(parsed.kind).toBe('serve');
if (parsed.kind !== 'serve') return;
expect(parsed.options.rateLimit).toBe(true);
expect(parsed.options.rateLimitPrompt).toBe(10);
});
it('discards rate limit env tuning when rate limiting is disabled', () => {
const parsed = parseServeFastPathArgs(['serve'], {
QWEN_SERVE_RATE_LIMIT_PROMPT: '10',
});
expect(parsed.kind).toBe('serve');
if (parsed.kind !== 'serve') return;
expect(parsed.options).not.toHaveProperty('rateLimit');
expect(parsed.options.rateLimitPrompt).toBeUndefined();
});
it('rejects unsafe rate limit env integers instead of rounding them', () => {
const parsed = parseServeFastPathArgs(['serve', '--rate-limit'], {
QWEN_SERVE_RATE_LIMIT_PROMPT: String(Number.MAX_SAFE_INTEGER + 1),
});
expect(parsed.kind).toBe('serve');
if (parsed.kind !== 'serve') return;
expect(parsed.options.rateLimitPrompt).toBeNaN();
});
});
describe('serve fast path environment bootstrap', () => {
it('keeps the lite settings directory name in sync with core QWEN_DIR', () => {
expect(SETTINGS_DIRECTORY_NAME).toBe(QWEN_DIR);
});
it('matches Storage.getGlobalQwenDir path resolution', () => {
tempWorkspace = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-storage-cwd-')),
);
tempQwenHome = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-storage-home-')),
);
process.chdir(tempWorkspace);
for (const qwenHome of [
undefined,
tempQwenHome,
'~',
'~/qwen-fast-path',
'~\\qwen-fast-path',
'relative-qwen-home',
]) {
if (qwenHome === undefined) {
delete process.env['QWEN_HOME'];
} else {
process.env['QWEN_HOME'] = qwenHome;
}
expect(getGlobalQwenDirLite()).toBe(Storage.getGlobalQwenDir());
}
});
it('closes the listener and exits when runtime startup fails after listen', async () => {
const stderrWrites: string[] = [];
const close = vi.fn().mockResolvedValue(undefined);
vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
stderrWrites.push(String(chunk));
return true;
});
vi.spyOn(process, 'exit').mockImplementation(((
code?: string | number | null,
) => {
throw new Error(`process.exit(${code})`);
}) as typeof process.exit);
await expect(
waitForServeRuntimeOrExit({
runtimeReady: Promise.reject(new Error('runtime boom')),
close,
}),
).rejects.toThrow('process.exit(1)');
expect(close).toHaveBeenCalledTimes(1);
expect(stderrWrites.join('')).toContain(
'qwen serve: runtime startup failed after listener was ready: runtime boom',
);
expect(process.exit).toHaveBeenCalledWith(1);
});
it('does not report startup failure when runtime startup is cancelled by close', async () => {
const stderrWrites: string[] = [];
const close = vi.fn().mockResolvedValue(undefined);
vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
stderrWrites.push(String(chunk));
return true;
});
const exit = vi.spyOn(process, 'exit').mockImplementation(((
code?: string | number | null,
) => {
throw new Error(`process.exit(${code})`);
}) as typeof process.exit);
await expect(
waitForServeRuntimeOrExit({
runtimeReady: Promise.reject(
new Error(RUNTIME_STARTUP_CANCELLED_MESSAGE),
),
close,
}),
).resolves.toBeUndefined();
expect(close).not.toHaveBeenCalled();
expect(stderrWrites.join('')).not.toContain(
'runtime startup failed after listener was ready',
);
expect(exit).not.toHaveBeenCalled();
});
it('validates rate limit env after settings bootstrap enables rate limiting', async () => {
useTempQwenHome();
tempWorkspace = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-rate-limit-env-')),
);
mkdirSync(join(tempWorkspace, '.qwen'));
writeFileSync(
join(tempWorkspace, '.qwen', 'settings.json'),
JSON.stringify({
env: {
QWEN_SERVE_RATE_LIMIT: '1',
QWEN_SERVE_RATE_LIMIT_PROMPT: '0',
},
}),
);
const stderrWrites: string[] = [];
vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
stderrWrites.push(String(chunk));
return true;
});
vi.spyOn(process, 'exit').mockImplementation(((
code?: string | number | null,
) => {
throw new Error(`process.exit(${code})`);
}) as typeof process.exit);
await expect(
tryRunServeFastPath([
'serve',
'--workspace',
tempWorkspace,
'--port',
'0',
'--hostname',
'127.0.0.1',
'--no-open',
'--no-web',
]),
).rejects.toThrow('process.exit(1)');
expect(stderrWrites.join('')).toContain(
'qwen serve: --rate-limit-prompt must be a positive integer.',
);
expect(process.exit).toHaveBeenCalledWith(1);
});
it('exits when runQwenServe fails after settings bootstrap succeeds', async () => {
useTempQwenHome();
vi.spyOn(runQwenServeModule, 'runQwenServe').mockRejectedValue(
new Error('listen boom'),
);
const stderrWrites: string[] = [];
vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
stderrWrites.push(String(chunk));
return true;
});
vi.spyOn(process, 'exit').mockImplementation(((
code?: string | number | null,
) => {
throw new Error(`process.exit(${code})`);
}) as typeof process.exit);
await expect(
tryRunServeFastPath([
'serve',
'--port',
'0',
'--hostname',
'127.0.0.1',
'--no-open',
'--no-web',
]),
).rejects.toThrow('process.exit(1)');
expect(stderrWrites.join('')).toContain('qwen serve: listen boom');
expect(process.exit).toHaveBeenCalledWith(1);
});
it('keeps headless yolo warning best-effort after listening', async () => {
const originalSandbox = process.env['SANDBOX'];
const originalSuppress = process.env['QWEN_CODE_SUPPRESS_YOLO_WARNING'];
delete process.env['SANDBOX'];
delete process.env['QWEN_CODE_SUPPRESS_YOLO_WARNING'];
const qwenHome = useTempQwenHome();
writeFileSync(
join(qwenHome, 'settings.json'),
JSON.stringify({ tools: { approvalMode: 'yolo', sandbox: false } }),
);
const runtimeReady = Promise.reject(new Error('runtime boom'));
void runtimeReady.catch(() => undefined);
const close = vi.fn().mockResolvedValue(undefined);
vi.spyOn(runQwenServeModule, 'runQwenServe').mockResolvedValue({
runtimeReady,
close,
} as unknown as Awaited<
ReturnType<typeof runQwenServeModule.runQwenServe>
>);
const stderrWrites: string[] = [];
vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
const text = String(chunk);
stderrWrites.push(text);
if (text.includes(HEADLESS_YOLO_NO_SANDBOX_WARNING)) {
throw new Error('stderr closed');
}
return true;
});
vi.spyOn(process, 'exit').mockImplementation(((
code?: string | number | null,
) => {
throw new Error(`process.exit(${code})`);
}) as typeof process.exit);
try {
await expect(
tryRunServeFastPath(['serve', '--port', '0', '--no-open', '--no-web']),
).rejects.toThrow('process.exit(1)');
expect(stderrWrites.join('')).toContain(HEADLESS_YOLO_NO_SANDBOX_WARNING);
expect(stderrWrites.join('')).toContain(
'qwen serve: runtime startup failed after listener was ready: runtime boom',
);
expect(stderrWrites.join('')).not.toContain('qwen serve: stderr closed');
expect(close).toHaveBeenCalledTimes(1);
expect(process.exit).toHaveBeenCalledWith(1);
} finally {
if (originalSandbox === undefined) {
delete process.env['SANDBOX'];
} else {
process.env['SANDBOX'] = originalSandbox;
}
if (originalSuppress === undefined) {
delete process.env['QWEN_CODE_SUPPRESS_YOLO_WARNING'];
} else {
process.env['QWEN_CODE_SUPPRESS_YOLO_WARNING'] = originalSuppress;
}
}
});
it('rejects malformed user settings so the full settings loader can handle it', async () => {
const qwenHome = useTempQwenHome();
writeFileSync(join(qwenHome, 'settings.json'), '{');
await expect(bootstrapServeFastPathEnvironment(undefined)).rejects.toThrow(
/settings/i,
);
}, 10_000);
it('falls back to the full CLI when fast-path settings bootstrap fails', async () => {
const qwenHome = useTempQwenHome();
writeFileSync(join(qwenHome, 'settings.json'), '{');
await expect(
tryRunServeFastPath(['serve', '--port', '0', '--no-open', '--no-web']),
).resolves.toBe(false);
}, 10_000);
it.each([
[
'advanced.excludedEnvVars',
{ advanced: { excludedEnvVars: 'QWEN_SERVER_TOKEN' } },
],
[
'advanced.runtimeOutputDir',
{ advanced: { runtimeOutputDir: ['.qwen-runtime'] } },
],
[
'security.folderTrust.enabled',
{ security: { folderTrust: { enabled: 'true' } } },
],
])(
'falls back to the full CLI when %s has an incompatible shape',
async (_field, settingsJson) => {
const qwenHome = useTempQwenHome();
writeFileSync(
join(qwenHome, 'settings.json'),
JSON.stringify(settingsJson),
);
await expect(
tryRunServeFastPath(['serve', '--port', '0', '--no-open', '--no-web']),
).resolves.toBe(false);
},
);
it('loads QWEN_SERVER_TOKEN from the workspace .env before the daemon starts', async () => {
delete process.env['QWEN_SERVER_TOKEN'];
useTempQwenHome();
tempWorkspace = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-env-')),
);
mkdirSync(join(tempWorkspace, '.qwen'));
writeFileSync(
join(tempWorkspace, '.qwen', '.env'),
'QWEN_SERVER_TOKEN=from-workspace-env\n',
);
process.chdir(tempWorkspace);
await bootstrapServeFastPathEnvironment(tempWorkspace);
expect(process.env['QWEN_SERVER_TOKEN']).toBe('from-workspace-env');
});
it('loads .env from --workspace even when launched from another directory', async () => {
delete process.env['QWEN_SERVER_TOKEN'];
useTempQwenHome();
tempWorkspace = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-workspace-env-')),
);
tempLaunchCwd = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-launch-cwd-')),
);
mkdirSync(join(tempWorkspace, '.qwen'));
writeFileSync(
join(tempWorkspace, '.qwen', '.env'),
'QWEN_SERVER_TOKEN=from-explicit-workspace-env\n',
);
process.chdir(tempLaunchCwd);
await bootstrapServeFastPathEnvironment(tempWorkspace);
expect(process.env['QWEN_SERVER_TOKEN']).toBe(
'from-explicit-workspace-env',
);
});
it('loads home .env after workspace .env for daemon boot-time keys', async () => {
delete process.env['QWEN_SERVER_TOKEN'];
delete process.env['QWEN_SERVE_RATE_LIMIT'];
delete process.env['QWEN_SERVE_RATE_LIMIT_PROMPT'];
const qwenHome = useTempQwenHome();
tempWorkspace = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-layered-env-')),
);
writeFileSync(
join(tempWorkspace, '.env'),
'QWEN_SERVE_RATE_LIMIT_PROMPT=123\n',
);
writeFileSync(
join(qwenHome, '.env'),
['QWEN_SERVER_TOKEN=from-home-env', 'QWEN_SERVE_RATE_LIMIT=1'].join('\n'),
);
await bootstrapServeFastPathEnvironment(tempWorkspace);
expect(process.env['QWEN_SERVE_RATE_LIMIT_PROMPT']).toBe('123');
expect(process.env['QWEN_SERVER_TOKEN']).toBe('from-home-env');
expect(process.env['QWEN_SERVE_RATE_LIMIT']).toBe('1');
});
it('applies legacy excludedProjectEnvVars before loading workspace .env', async () => {
delete process.env['QWEN_SERVER_TOKEN'];
const qwenHome = useTempQwenHome();
writeFileSync(
join(qwenHome, 'settings.json'),
JSON.stringify({ excludedProjectEnvVars: ['QWEN_SERVER_TOKEN'] }),
);
tempWorkspace = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-legacy-env-')),
);
writeFileSync(
join(tempWorkspace, '.env'),
'QWEN_SERVER_TOKEN=from-workspace-env\n',
);
process.chdir(tempWorkspace);
await bootstrapServeFastPathEnvironment(tempWorkspace);
expect(process.env['QWEN_SERVER_TOKEN']).toBeUndefined();
});
it('loads QWEN_SERVER_TOKEN from workspace settings.env without the full settings loader', async () => {
delete process.env['QWEN_SERVER_TOKEN'];
useTempQwenHome();
tempWorkspace = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-settings-env-')),
);
mkdirSync(join(tempWorkspace, '.qwen'));
writeFileSync(
join(tempWorkspace, '.qwen', 'settings.json'),
JSON.stringify({
env: { QWEN_SERVER_TOKEN: 'from-workspace-settings-env' },
}),
);
process.chdir(tempWorkspace);
await bootstrapServeFastPathEnvironment(tempWorkspace);
expect(process.env['QWEN_SERVER_TOKEN']).toBe(
'from-workspace-settings-env',
);
});
it('pre-resolves home env overrides in the same order as the full loader', () => {
delete process.env['QWEN_HOME'];
delete process.env['QWEN_RUNTIME_DIR'];
delete process.env['QWEN_CODE_MCP_APPROVALS_PATH'];
delete process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'];
tempLaunchCwd = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-fake-home-')),
);
process.env['HOME'] = tempLaunchCwd;
process.env['USERPROFILE'] = tempLaunchCwd;
tempQwenHome = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-discovered-home-')),
);
mkdirSync(join(tempLaunchCwd, '.qwen'), { recursive: true });
writeFileSync(
join(tempLaunchCwd, '.qwen', '.env'),
`QWEN_HOME=${tempQwenHome}\n`,
);
writeFileSync(
join(tempLaunchCwd, '.env'),
'QWEN_RUNTIME_DIR=from-home-env\n',
);
writeFileSync(
join(tempQwenHome, '.env'),
[
'QWEN_CODE_MCP_APPROVALS_PATH=from-discovered-home',
'QWEN_CODE_TRUSTED_FOLDERS_PATH=from-discovered-trust',
].join('\n'),
);
preResolveServeFastPathHomeEnvOverrides();
expect(process.env['QWEN_HOME']).toBe(tempQwenHome);
expect(process.env['QWEN_RUNTIME_DIR']).toBe('from-home-env');
expect(process.env['QWEN_CODE_MCP_APPROVALS_PATH']).toBe(
'from-discovered-home',
);
expect(process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH']).toBe(
'from-discovered-trust',
);
});
it('still pre-resolves missing home-scoped keys when QWEN_HOME and runtime are already set', () => {
delete process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'];
const qwenHome = useTempQwenHome();
process.env['QWEN_RUNTIME_DIR'] = join(qwenHome, 'runtime');
writeFileSync(
join(qwenHome, '.env'),
'QWEN_CODE_TRUSTED_FOLDERS_PATH=from-existing-home\n',
);
preResolveServeFastPathHomeEnvOverrides();
expect(process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH']).toBe(
'from-existing-home',
);
});
it('applies legacy settings keys consumed by the serve fast path', () => {
const qwenHome = useTempQwenHome();
tempWorkspace = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-legacy-settings-')),
);
writeFileSync(
join(qwenHome, 'settings.json'),
JSON.stringify({
approvalMode: 'yolo',
contextFileName: 'LEGACY.md',
excludedProjectEnvVars: ['QWEN_SERVER_TOKEN'],
fileFiltering: { customIgnoreFiles: ['.legacy-ignore'] },
folderTrust: true,
sandbox: false,
}),
);
const settings = loadServeFastPathSettings(tempWorkspace);
expect(settings).toMatchObject({
advanced: { excludedEnvVars: ['QWEN_SERVER_TOKEN'] },
context: {
fileName: 'LEGACY.md',
fileFiltering: { customIgnoreFiles: ['.legacy-ignore'] },
},
security: { folderTrust: { enabled: true } },
tools: { approvalMode: 'yolo', sandbox: false },
});
});
it('matches the full settings loader for fields consumed before listen', async () => {
const qwenHome = useTempQwenHome();
tempWorkspace = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-settings-parity-')),
);
mkdirSync(join(tempWorkspace, '.qwen'));
const { SETTINGS_VERSION, loadSettings } = await import(
'../config/settings.js'
);
const versioned = (settings: Record<string, unknown>) => ({
$version: SETTINGS_VERSION,
...settings,
});
writeFileSync(
process.env['QWEN_CODE_SYSTEM_DEFAULTS_PATH']!,
JSON.stringify(
versioned({
env: {
FAST_PATH_DEFAULT_ONLY: 'default',
FAST_PATH_OVERLAP: 'default',
},
advanced: {
excludedEnvVars: ['FAST_PATH_DEFAULT_EXCLUDED'],
runtimeOutputDir: '.default-runtime',
},
context: {
fileName: 'DEFAULT.md',
fileFiltering: { customIgnoreFiles: ['.default-ignore'] },
},
security: { folderTrust: { enabled: false } },
tools: { approvalMode: 'default' },
}),
),
);
writeFileSync(
join(qwenHome, 'settings.json'),
JSON.stringify(
versioned({
env: {
FAST_PATH_USER_ONLY: 'user',
FAST_PATH_OVERLAP: 'user',
},
advanced: {
excludedEnvVars: ['FAST_PATH_USER_EXCLUDED'],
},
security: { folderTrust: { enabled: true } },
tools: { approvalMode: 'auto' },
}),
),
);
writeFileSync(
join(tempWorkspace, '.qwen', 'settings.json'),
JSON.stringify(
versioned({
env: {
FAST_PATH_WORKSPACE_ONLY: 'workspace',
FAST_PATH_OVERLAP: 'workspace',
},
advanced: { runtimeOutputDir: '.workspace-runtime' },
general: { chatRecording: false },
context: {
fileName: 'WORKSPACE.md',
fileFiltering: { customIgnoreFiles: ['.workspace-ignore'] },
},
policy: { permissionStrategy: 'consensus', consensusQuorum: 3 },
tools: { sandbox: true },
}),
),
);
writeFileSync(
process.env['QWEN_CODE_SYSTEM_SETTINGS_PATH']!,
JSON.stringify(
versioned({
env: {
FAST_PATH_SYSTEM_ONLY: 'system',
FAST_PATH_OVERLAP: 'system',
},
context: { fileName: 'SYSTEM.md' },
tools: { approvalMode: 'yolo' },
}),
),
);
expect(loadServeFastPathSettings(tempWorkspace)).toEqual(
pickServeFastPathComparable(
loadSettings(tempWorkspace, { skipLoadEnvironment: true }).merged,
),
);
});
it('loads runtimeOutputDir for daemon startup artifacts', () => {
const qwenHome = useTempQwenHome();
tempWorkspace = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-runtime-dir-')),
);
writeFileSync(
join(qwenHome, 'settings.json'),
JSON.stringify({
advanced: { runtimeOutputDir: '.qwen-runtime' },
}),
);
const settings = loadServeFastPathSettings(tempWorkspace);
expect(settings.advanced?.runtimeOutputDir).toBe('.qwen-runtime');
});
it('ignores stale legacy keys in current-version settings files', () => {
const qwenHome = useTempQwenHome();
tempWorkspace = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-stale-legacy-settings-')),
);
writeFileSync(
join(qwenHome, 'settings.json'),
JSON.stringify({
$version: 5,
approvalMode: 'yolo',
contextFileName: 'LEGACY.md',
excludedProjectEnvVars: ['QWEN_SERVER_TOKEN'],
fileFiltering: { customIgnoreFiles: ['.legacy-ignore'] },
folderTrust: true,
sandbox: false,
}),
);
const settings = loadServeFastPathSettings(tempWorkspace);
expect(settings.advanced).toBeUndefined();
expect(settings.context).toBeUndefined();
expect(settings.security).toBeUndefined();
expect(settings.tools).toBeUndefined();
});
it('uses trusted-folders path from home .env before loading workspace env', async () => {
delete process.env['QWEN_SERVER_TOKEN'];
delete process.env['QWEN_RUNTIME_DIR'];
delete process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'];
const qwenHome = useTempQwenHome();
const customTrustedFoldersPath = join(qwenHome, 'custom-trusted.json');
tempWorkspace = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-home-trust-env-')),
);
writeFileSync(
join(qwenHome, '.env'),
`QWEN_CODE_TRUSTED_FOLDERS_PATH=${customTrustedFoldersPath}\n`,
);
writeFileSync(
join(qwenHome, 'settings.json'),
JSON.stringify({ security: { folderTrust: { enabled: true } } }),
);
writeFileSync(
customTrustedFoldersPath,
JSON.stringify({ [tempWorkspace]: TrustLevel.DO_NOT_TRUST }),
);
writeFileSync(
join(tempWorkspace, '.env'),
'QWEN_SERVER_TOKEN=from-untrusted-workspace-env\n',
);
await bootstrapServeFastPathEnvironment(tempWorkspace);
expect(process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH']).toBe(
customTrustedFoldersPath,
);
expect(process.env['QWEN_SERVER_TOKEN']).toBeUndefined();
});
it('uses legacy folderTrust before loading workspace env', async () => {
delete process.env['QWEN_SERVER_TOKEN'];
const qwenHome = useTempQwenHome();
tempWorkspace = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-legacy-trust-')),
);
writeFileSync(
join(qwenHome, 'settings.json'),
JSON.stringify({ folderTrust: true }),
);
process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'] = join(
qwenHome,
'trustedFolders.json',
);
writeFileSync(
process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'],
JSON.stringify({ [tempWorkspace]: TrustLevel.DO_NOT_TRUST }),
);
writeFileSync(
join(tempWorkspace, '.env'),
'QWEN_SERVER_TOKEN=from-untrusted-workspace-env\n',
);
await bootstrapServeFastPathEnvironment(tempWorkspace);
expect(process.env['QWEN_SERVER_TOKEN']).toBeUndefined();
});
it('caches trusted folders during a single fast-path bootstrap', () => {
delete process.env['QWEN_SERVER_TOKEN'];
const qwenHome = useTempQwenHome();
tempWorkspace = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-trust-cache-')),
);
writeFileSync(
join(qwenHome, 'settings.json'),
JSON.stringify({ security: { folderTrust: { enabled: true } } }),
);
process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'] = join(
qwenHome,
'trustedFolders.json',
);
writeFileSync(
process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'],
JSON.stringify({ [tempWorkspace]: TrustLevel.TRUST_FOLDER }),
);
writeFileSync(join(tempWorkspace, '.env'), 'QWEN_SERVER_TOKEN=trusted\n');
const settings = loadServeFastPathSettings(tempWorkspace);
writeFileSync(
process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'],
JSON.stringify({ [tempWorkspace]: TrustLevel.DO_NOT_TRUST }),
);
loadServeFastPathEnvironment(settings, tempWorkspace);
expect(process.env['QWEN_SERVER_TOKEN']).toBe('trusted');
});
it('prioritizes trusted parent folders over nested distrust rules', async () => {
delete process.env['QWEN_SERVER_TOKEN'];
const qwenHome = useTempQwenHome();
tempWorkspace = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-trust-precedence-')),
);
const childWorkspace = join(tempWorkspace, 'child');
mkdirSync(childWorkspace);
writeFileSync(
join(qwenHome, 'settings.json'),
JSON.stringify({ security: { folderTrust: { enabled: true } } }),
);
process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'] = join(
qwenHome,
'trustedFolders.json',
);
writeFileSync(
process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'],
JSON.stringify({
[tempWorkspace]: TrustLevel.TRUST_FOLDER,
[childWorkspace]: TrustLevel.DO_NOT_TRUST,
}),
);
writeFileSync(join(childWorkspace, '.env'), 'QWEN_SERVER_TOKEN=trusted\n');
await bootstrapServeFastPathEnvironment(childWorkspace);
expect(process.env['QWEN_SERVER_TOKEN']).toBe('trusted');
});
it('treats TRUST_PARENT as trusting the containing folder', async () => {
delete process.env['QWEN_SERVER_TOKEN'];
const qwenHome = useTempQwenHome();
tempWorkspace = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-trust-parent-')),
);
writeFileSync(
join(qwenHome, 'settings.json'),
JSON.stringify({ security: { folderTrust: { enabled: true } } }),
);
process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'] = join(
qwenHome,
'trustedFolders.json',
);
writeFileSync(
process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'],
JSON.stringify({
[join(tempWorkspace, 'marker')]: TrustLevel.TRUST_PARENT,
}),
);
writeFileSync(join(tempWorkspace, '.env'), 'QWEN_SERVER_TOKEN=trusted\n');
await bootstrapServeFastPathEnvironment(tempWorkspace);
expect(process.env['QWEN_SERVER_TOKEN']).toBe('trusted');
});
it('matches Cloud Shell default project behavior for empty env values', async () => {
delete process.env['GOOGLE_CLOUD_PROJECT'];
process.env['CLOUD_SHELL'] = 'true';
useTempQwenHome();
tempWorkspace = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-cloud-shell-')),
);
writeFileSync(join(tempWorkspace, '.env'), 'GOOGLE_CLOUD_PROJECT=\n');
await bootstrapServeFastPathEnvironment(tempWorkspace);
expect(process.env['GOOGLE_CLOUD_PROJECT']).toBe('cloudshell-gca');
});
it('expands process environment placeholders in workspace settings.env', async () => {
delete process.env['QWEN_SERVER_TOKEN'];
useTempQwenHome();
process.env['FAST_PATH_REFERENCED_TOKEN'] = 'from-referenced-env';
tempWorkspace = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-settings-env-')),
);
mkdirSync(join(tempWorkspace, '.qwen'));
writeFileSync(
join(tempWorkspace, '.qwen', 'settings.json'),
JSON.stringify({
env: { QWEN_SERVER_TOKEN: '${FAST_PATH_REFERENCED_TOKEN}' },
}),
);
process.chdir(tempWorkspace);
await bootstrapServeFastPathEnvironment(tempWorkspace);
expect(process.env['QWEN_SERVER_TOKEN']).toBe('from-referenced-env');
});
it('expands home .env fallback placeholders in workspace settings.env', async () => {
delete process.env['QWEN_SERVER_TOKEN'];
delete process.env['FAST_PATH_REFERENCED_TOKEN'];
const qwenHome = useTempQwenHome();
writeFileSync(
join(qwenHome, '.env'),
'FAST_PATH_REFERENCED_TOKEN=from-home-env\n',
);
tempWorkspace = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-settings-env-')),
);
mkdirSync(join(tempWorkspace, '.qwen'));
writeFileSync(
join(tempWorkspace, '.qwen', 'settings.json'),
JSON.stringify({
env: { QWEN_SERVER_TOKEN: '${FAST_PATH_REFERENCED_TOKEN}' },
}),
);
process.chdir(tempWorkspace);
await bootstrapServeFastPathEnvironment(tempWorkspace);
expect(process.env['QWEN_SERVER_TOKEN']).toBe('from-home-env');
});
it.each([
['malformed JSON', '{ "env": { "QWEN_SERVER_TOKEN": "broken" }'],
['non-object JSON', '[]'],
])(
'rejects %s workspace settings so the full settings loader can handle it',
async (_name, settingsJson) => {
delete process.env['QWEN_SERVER_TOKEN'];
useTempQwenHome();
tempWorkspace = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-bad-settings-')),
);
mkdirSync(join(tempWorkspace, '.qwen'));
writeFileSync(
join(tempWorkspace, '.qwen', 'settings.json'),
settingsJson,
);
process.chdir(tempWorkspace);
await expect(
bootstrapServeFastPathEnvironment(tempWorkspace),
).rejects.toThrow(/settings/i);
expect(process.env['QWEN_SERVER_TOKEN']).toBeUndefined();
},
);
it('still reads invalid workspace settings before dropping an untrusted workspace from the merge', () => {
const qwenHome = useTempQwenHome();
tempWorkspace = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-untrusted-settings-')),
);
mkdirSync(join(tempWorkspace, '.qwen'));
writeFileSync(
join(qwenHome, 'settings.json'),
JSON.stringify({ security: { folderTrust: { enabled: true } } }),
);
process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'] = join(
qwenHome,
'trustedFolders.json',
);
writeFileSync(
process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'],
JSON.stringify({ [tempWorkspace]: TrustLevel.DO_NOT_TRUST }),
);
writeFileSync(join(tempWorkspace, '.qwen', 'settings.json'), '[]');
process.chdir(tempWorkspace);
expect(() => loadServeFastPathSettings(tempWorkspace!)).toThrow(
/settings/i,
);
});
it('does not load env from an explicit untrusted workspace when launched elsewhere', async () => {
delete process.env['QWEN_SERVER_TOKEN'];
const qwenHome = useTempQwenHome();
tempWorkspace = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-untrusted-env-')),
);
tempLaunchCwd = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-trusted-launch-')),
);
mkdirSync(join(tempWorkspace, '.qwen'));
writeFileSync(
join(qwenHome, 'settings.json'),
JSON.stringify({ security: { folderTrust: { enabled: true } } }),
);
process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'] = join(
qwenHome,
'trustedFolders.json',
);
writeFileSync(
process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'],
JSON.stringify({
[tempLaunchCwd]: TrustLevel.TRUST_FOLDER,
[tempWorkspace]: TrustLevel.DO_NOT_TRUST,
}),
);
writeFileSync(
join(tempWorkspace, '.env'),
'QWEN_SERVER_TOKEN=from-untrusted-workspace-env\n',
);
writeFileSync(
join(tempWorkspace, '.qwen', 'settings.json'),
JSON.stringify({
env: { QWEN_SERVER_TOKEN: 'from-untrusted-workspace-settings' },
}),
);
process.chdir(tempLaunchCwd);
await bootstrapServeFastPathEnvironment(tempWorkspace);
expect(process.env['QWEN_SERVER_TOKEN']).toBeUndefined();
});
it('checks trust against the canonical explicit workspace path', async () => {
delete process.env['QWEN_SERVER_TOKEN'];
const qwenHome = useTempQwenHome();
tempWorkspace = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-real-untrusted-env-')),
);
tempLaunchCwd = realpathSync(
mkdtempSync(join(os.tmpdir(), 'qws-fast-path-symlink-launch-')),
);
tempSymlink = join(tempLaunchCwd, 'workspace-link');
symlinkSync(tempWorkspace, tempSymlink, 'dir');
writeFileSync(
join(qwenHome, 'settings.json'),
JSON.stringify({ security: { folderTrust: { enabled: true } } }),
);
process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'] = join(
qwenHome,
'trustedFolders.json',
);
writeFileSync(
process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'],
JSON.stringify({
[tempLaunchCwd]: TrustLevel.TRUST_FOLDER,
[tempWorkspace]: TrustLevel.DO_NOT_TRUST,
}),
);
writeFileSync(
join(tempWorkspace, '.env'),
'QWEN_SERVER_TOKEN=from-symlinked-untrusted-workspace-env\n',
);
process.chdir(tempLaunchCwd);
await bootstrapServeFastPathEnvironment(tempSymlink);
expect(process.env['QWEN_SERVER_TOKEN']).toBeUndefined();
});
});