basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Qwen Team4 * SPDX-License-Identifier: Apache-2.05 */6import { afterEach, describe, expect, it, vi } from 'vitest';7import yargs, {} from 'yargs';8import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync, } from 'node:fs';9import * as os from 'node:os';10import { dirname, join, relative, resolve } from 'node:path';11import * as ts from 'typescript';12import { QWEN_DIR, Storage } from '@qwen-code/qwen-code-core';13import { bootstrapServeFastPathEnvironment, parseServeFastPathArgs, tryRunServeFastPath, waitForServeRuntimeOrExit, } from './fast-path.js';14import { loadServeFastPathEnvironment, loadServeFastPathSettings, preResolveServeFastPathHomeEnvOverrides, resetServeFastPathHomeEnvBootstrapForTesting, } from './fast-path-settings.js';15import { getGlobalQwenDirLite, SETTINGS_DIRECTORY_NAME, } from '../config/storage-paths-lite.js';16import { RUNTIME_STARTUP_CANCELLED_MESSAGE } from './runtime-startup-errors.js';17import { resetTrustedFoldersForTesting, TrustLevel, } from '../config/trustedFolders.js';18import { HEADLESS_YOLO_NO_SANDBOX_WARNING } from '../utils/headlessSafetyWarnings.js';19import * as runQwenServeModule from './run-qwen-serve.js';20import { serveCommand } from '../commands/serve.js';21let tempWorkspace;22let tempLaunchCwd;23let tempQwenHome;24let tempSymlink;25const originalToken = process.env['QWEN_SERVER_TOKEN'];26const originalQwenHome = process.env['QWEN_HOME'];27const originalHome = process.env['HOME'];28const originalUserProfile = process.env['USERPROFILE'];29const originalQwenRuntimeDir = process.env['QWEN_RUNTIME_DIR'];30const originalMcpApprovalsPath = process.env['QWEN_CODE_MCP_APPROVALS_PATH'];31const originalSystemSettingsPath = process.env['QWEN_CODE_SYSTEM_SETTINGS_PATH'];32const originalSystemDefaultsPath = process.env['QWEN_CODE_SYSTEM_DEFAULTS_PATH'];33const originalTrustedFoldersPath = process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'];34const originalReferencedToken = process.env['FAST_PATH_REFERENCED_TOKEN'];35const originalRateLimit = process.env['QWEN_SERVE_RATE_LIMIT'];36const originalRateLimitPrompt = process.env['QWEN_SERVE_RATE_LIMIT_PROMPT'];37const originalCloudShell = process.env['CLOUD_SHELL'];38const originalGoogleCloudProject = process.env['GOOGLE_CLOUD_PROJECT'];39const originalCwd = process.cwd();40const cliPackageRoot = process.cwd();41function normalizePathForTest(filePath) {42 return filePath.replace(/\\/g, '/');43}44function moduleSpecifierText(specifier) {45 if (!specifier || !ts.isStringLiteral(specifier))46 return undefined;47 return specifier.text;48}49function importDeclarationHasRuntimeValue(node) {50 const clause = node.importClause;51 if (!clause)52 return true;53 if (clause.isTypeOnly)54 return false;55 if (clause.name)56 return true;57 const bindings = clause.namedBindings;58 if (!bindings)59 return false;60 if (ts.isNamespaceImport(bindings))61 return true;62 if (bindings.elements.length === 0)63 return true;64 return bindings.elements.some((element) => !element.isTypeOnly);65}66function exportDeclarationHasRuntimeValue(node) {67 if (node.isTypeOnly)68 return false;69 const clause = node.exportClause;70 if (!clause)71 return true;72 if (ts.isNamespaceExport(clause))73 return true;74 if (clause.elements.length === 0)75 return true;76 return clause.elements.some((element) => !element.isTypeOnly);77}78function resolveLocalSourceImport(importer, specifier) {79 const basePath = resolve(dirname(importer), specifier);80 const candidates = specifier.endsWith('.js')81 ? [`${basePath.slice(0, -3)}.ts`, `${basePath.slice(0, -3)}.tsx`]82 : [83 `${basePath}.ts`,84 `${basePath}.tsx`,85 join(basePath, 'index.ts'),86 join(basePath, 'index.tsx'),87 ];88 return candidates.find((candidate) => existsSync(candidate));89}90function collectStaticSourceGraph(entryFile) {91 const visited = new Set();92 const localFiles = new Set();93 const externalValueImports = new Set();94 const unresolvedLocalImports = [];95 function visit(filePath) {96 const normalizedFilePath = resolve(filePath);97 if (visited.has(normalizedFilePath))98 return;99 visited.add(normalizedFilePath);100 localFiles.add(normalizePathForTest(relative(cliPackageRoot, normalizedFilePath)));101 const sourceText = readFileSync(normalizedFilePath, 'utf8');102 const sourceFile = ts.createSourceFile(normalizedFilePath, sourceText, ts.ScriptTarget.Latest, true, normalizedFilePath.endsWith('.tsx')103 ? ts.ScriptKind.TSX104 : ts.ScriptKind.TS);105 for (const statement of sourceFile.statements) {106 let specifier;107 let hasRuntimeValue = false;108 if (ts.isImportDeclaration(statement)) {109 specifier = moduleSpecifierText(statement.moduleSpecifier);110 hasRuntimeValue = importDeclarationHasRuntimeValue(statement);111 }112 else if (ts.isExportDeclaration(statement)) {113 specifier = moduleSpecifierText(statement.moduleSpecifier);114 hasRuntimeValue = exportDeclarationHasRuntimeValue(statement);115 }116 if (!specifier || !hasRuntimeValue)117 continue;118 if (!specifier.startsWith('.')) {119 externalValueImports.add(specifier);120 continue;121 }122 const resolvedImport = resolveLocalSourceImport(normalizedFilePath, specifier);123 if (!resolvedImport) {124 unresolvedLocalImports.push(`${normalizePathForTest(relative(cliPackageRoot, normalizedFilePath))} -> ${specifier}`);125 continue;126 }127 visit(resolvedImport);128 }129 }130 visit(entryFile);131 return { localFiles, externalValueImports, unresolvedLocalImports };132}133function useTempQwenHome() {134 tempQwenHome = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-home-')));135 process.env['QWEN_HOME'] = tempQwenHome;136 process.env['QWEN_CODE_SYSTEM_SETTINGS_PATH'] = join(tempQwenHome, 'system-settings.json');137 process.env['QWEN_CODE_SYSTEM_DEFAULTS_PATH'] = join(tempQwenHome, 'system-defaults.json');138 return tempQwenHome;139}140function buildServeCommandParser() {141 return serveCommand.builder(yargs([]).exitProcess(false).fail(false).locale('en'));142}143function pickServeFastPathComparable(settings) {144 const out = {};145 if (settings.env) {146 out.env = settings.env;147 }148 if (settings.advanced?.excludedEnvVars !== undefined) {149 out.advanced = {150 ...(out.advanced ?? {}),151 excludedEnvVars: settings.advanced.excludedEnvVars,152 };153 }154 if (settings.advanced?.runtimeOutputDir !== undefined) {155 out.advanced = {156 ...(out.advanced ?? {}),157 runtimeOutputDir: settings.advanced.runtimeOutputDir,158 };159 }160 if (settings.security?.folderTrust?.enabled !== undefined) {161 out.security = {162 folderTrust: { enabled: settings.security.folderTrust.enabled },163 };164 }165 if (settings.tools?.approvalMode !== undefined) {166 out.tools = {167 ...(out.tools ?? {}),168 approvalMode: settings.tools.approvalMode,169 };170 }171 if (settings.tools?.sandbox !== undefined) {172 out.tools = { ...(out.tools ?? {}), sandbox: settings.tools.sandbox };173 }174 if (settings.context?.fileName !== undefined) {175 out.context = {176 ...(out.context ?? {}),177 fileName: settings.context.fileName,178 };179 }180 if (settings.context?.fileFiltering?.customIgnoreFiles !== undefined) {181 out.context = {182 ...(out.context ?? {}),183 fileFiltering: {184 customIgnoreFiles: settings.context.fileFiltering.customIgnoreFiles,185 },186 };187 }188 if (settings.policy?.permissionStrategy !== undefined) {189 out.policy = {190 ...(out.policy ?? {}),191 permissionStrategy: settings.policy.permissionStrategy,192 };193 }194 if (settings.policy?.consensusQuorum !== undefined) {195 out.policy = {196 ...(out.policy ?? {}),197 consensusQuorum: settings.policy.consensusQuorum,198 };199 }200 return out;201}202afterEach(() => {203 vi.restoreAllMocks();204 process.chdir(originalCwd);205 if (originalToken === undefined) {206 delete process.env['QWEN_SERVER_TOKEN'];207 }208 else {209 process.env['QWEN_SERVER_TOKEN'] = originalToken;210 }211 if (originalQwenHome === undefined) {212 delete process.env['QWEN_HOME'];213 }214 else {215 process.env['QWEN_HOME'] = originalQwenHome;216 }217 if (originalHome === undefined) {218 delete process.env['HOME'];219 }220 else {221 process.env['HOME'] = originalHome;222 }223 if (originalUserProfile === undefined) {224 delete process.env['USERPROFILE'];225 }226 else {227 process.env['USERPROFILE'] = originalUserProfile;228 }229 if (originalSystemSettingsPath === undefined) {230 delete process.env['QWEN_CODE_SYSTEM_SETTINGS_PATH'];231 }232 else {233 process.env['QWEN_CODE_SYSTEM_SETTINGS_PATH'] = originalSystemSettingsPath;234 }235 if (originalSystemDefaultsPath === undefined) {236 delete process.env['QWEN_CODE_SYSTEM_DEFAULTS_PATH'];237 }238 else {239 process.env['QWEN_CODE_SYSTEM_DEFAULTS_PATH'] = originalSystemDefaultsPath;240 }241 if (originalTrustedFoldersPath === undefined) {242 delete process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'];243 }244 else {245 process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'] = originalTrustedFoldersPath;246 }247 if (originalReferencedToken === undefined) {248 delete process.env['FAST_PATH_REFERENCED_TOKEN'];249 }250 else {251 process.env['FAST_PATH_REFERENCED_TOKEN'] = originalReferencedToken;252 }253 if (originalRateLimit === undefined) {254 delete process.env['QWEN_SERVE_RATE_LIMIT'];255 }256 else {257 process.env['QWEN_SERVE_RATE_LIMIT'] = originalRateLimit;258 }259 if (originalRateLimitPrompt === undefined) {260 delete process.env['QWEN_SERVE_RATE_LIMIT_PROMPT'];261 }262 else {263 process.env['QWEN_SERVE_RATE_LIMIT_PROMPT'] = originalRateLimitPrompt;264 }265 if (originalCloudShell === undefined) {266 delete process.env['CLOUD_SHELL'];267 }268 else {269 process.env['CLOUD_SHELL'] = originalCloudShell;270 }271 if (originalGoogleCloudProject === undefined) {272 delete process.env['GOOGLE_CLOUD_PROJECT'];273 }274 else {275 process.env['GOOGLE_CLOUD_PROJECT'] = originalGoogleCloudProject;276 }277 if (originalQwenRuntimeDir === undefined) {278 delete process.env['QWEN_RUNTIME_DIR'];279 }280 else {281 process.env['QWEN_RUNTIME_DIR'] = originalQwenRuntimeDir;282 }283 if (originalMcpApprovalsPath === undefined) {284 delete process.env['QWEN_CODE_MCP_APPROVALS_PATH'];285 }286 else {287 process.env['QWEN_CODE_MCP_APPROVALS_PATH'] = originalMcpApprovalsPath;288 }289 resetServeFastPathHomeEnvBootstrapForTesting();290 resetTrustedFoldersForTesting();291 if (tempWorkspace) {292 rmSync(tempWorkspace, { recursive: true, force: true });293 tempWorkspace = undefined;294 }295 if (tempLaunchCwd) {296 rmSync(tempLaunchCwd, { recursive: true, force: true });297 tempLaunchCwd = undefined;298 }299 if (tempQwenHome) {300 rmSync(tempQwenHome, { recursive: true, force: true });301 tempQwenHome = undefined;302 }303 if (tempSymlink) {304 rmSync(tempSymlink, { force: true });305 tempSymlink = undefined;306 }307});308describe('CLI entry import boundary', () => {309 it('does not statically import the full gemini entry before the serve fast path can run', () => {310 const cliSource = readFileSync('src/cli.ts', 'utf8');311 expect(cliSource).not.toContain("import './gemini.js'");312 expect(cliSource).not.toContain("import { main } from './gemini.js'");313 expect(cliSource).not.toContain("process.argv[2] === 'serve'");314 expect(cliSource).toContain("await import('./serve/fast-path.js')");315 });316 it('does not import the full settings loader on the serve fast path', () => {317 const fastPathSource = readFileSync('src/serve/fast-path.ts', 'utf8');318 expect(fastPathSource).not.toContain('../config/settings.js');319 expect(fastPathSource).not.toContain('../config/environment.js');320 expect(fastPathSource).not.toContain('@qwen-code/qwen-code-core');321 expect(fastPathSource).toContain('bootSettings: settings');322 expect(fastPathSource).toContain('resolveOnListen: true');323 expect(fastPathSource).toContain('deferRuntimeUntilFirstHealth: !parsed.open');324 });325 it('uses the shared headless yolo warning helper on the serve fast path', () => {326 const fastPathSource = readFileSync('src/serve/fast-path.ts', 'utf8');327 expect(fastPathSource).toContain('getHeadlessYoloSafetyWarning');328 expect(fastPathSource).not.toContain("settings.tools?.approvalMode === 'yolo'");329 });330 it('keeps headless yolo warning helper free of runtime core imports', () => {331 const helperSource = readFileSync('src/utils/headlessSafetyWarnings.ts', 'utf8');332 expect(helperSource).not.toMatch(/import\s+(?!type\b)[^;]*from ['"]@qwen-code\/qwen-code-core['"]/);333 });334 it('keeps settings free of UI imports used before serve can listen', () => {335 const settingsSource = readFileSync('src/config/settings.ts', 'utf8');336 expect(settingsSource).not.toContain('../ui/');337 });338 it('keeps extension command parsing free of UI state imports', () => {339 const updateCommandSource = readFileSync('src/commands/extensions/update.ts', 'utf8');340 expect(updateCommandSource).not.toContain('../../ui/');341 });342 it('keeps runQwenServe from statically loading the full server and ACP runtime', () => {343 const runServeSource = readFileSync('src/serve/run-qwen-serve.ts', 'utf8');344 expect(runServeSource).not.toMatch(/from ['"]\.\/server\.js['"]/);345 expect(runServeSource).not.toMatch(/from ['"]\.\/web-shell-static\.js['"]/);346 expect(runServeSource).not.toMatch(/from ['"]\.\/acp-session-bridge\.js['"]/);347 expect(runServeSource).not.toMatch(/from ['"]@qwen-code\/acp-bridge\/bridge['"]/);348 expect(runServeSource).not.toMatch(/from ['"]@qwen-code\/acp-bridge\/spawnChannel['"]/);349 expect(runServeSource).toContain("import('./server.js')");350 expect(runServeSource).toContain("import('@qwen-code/acp-bridge/bridge')");351 });352 it('keeps request helpers from value-importing the ACP compatibility shim', () => {353 const requestHelpersSource = readFileSync('src/serve/server/request-helpers.ts', 'utf8');354 expect(requestHelpersSource).not.toMatch(/from ['"]\.\.\/acp-session-bridge\.js['"]/);355 expect(requestHelpersSource).toContain("import type { AcpSessionBridge } from '@qwen-code/acp-bridge/bridgeTypes';");356 expect(requestHelpersSource).toContain("import { MAX_WORKSPACE_PATH_LENGTH } from '@qwen-code/acp-bridge/workspacePaths';");357 });358 it('keeps the runQwenServe static source graph free of ACP runtime modules', () => {359 const graph = collectStaticSourceGraph(resolve(cliPackageRoot, 'src/serve/run-qwen-serve.ts'));360 expect(graph.unresolvedLocalImports).toEqual([]);361 const forbiddenLocalFiles = [...graph.localFiles].filter((filePath) => filePath === 'src/serve/acp-session-bridge.ts');362 expect(forbiddenLocalFiles, `Unexpected static source graph files:\n${forbiddenLocalFiles.join('\n')}`).toEqual([]);363 const forbiddenExternalImports = [364 '@qwen-code/acp-bridge',365 '@qwen-code/acp-bridge/bridge',366 '@qwen-code/acp-bridge/spawnChannel',367 '@qwen-code/acp-bridge/bridgeClient',368 '@qwen-code/acp-bridge/bridgeErrors',369 ];370 const forbiddenImports = [...graph.externalValueImports].filter((specifier) => forbiddenExternalImports.includes(specifier));371 expect(forbiddenImports, `Unexpected ACP runtime imports:\n${forbiddenImports.join('\n')}`).toEqual([]);372 });373});374describe('serve fast path argument parsing', () => {375 it('parses the common daemon startup flags without loading the full CLI parser', () => {376 const parsed = parseServeFastPathArgs([377 'serve',378 '--port',379 '0',380 '--hostname',381 '127.0.0.1',382 '--workspace',383 '/tmp/workspace',384 '--no-web',385 '--no-open',386 ]);387 expect(parsed).toEqual({388 kind: 'serve',389 httpBridge: true,390 open: false,391 options: {392 hostname: '127.0.0.1',393 mcpBudgetMode: 'off',394 mode: 'http-bridge',395 port: 0,396 serveWebShell: false,397 workspace: '/tmp/workspace',398 },399 });400 });401 it('parses --tls-cert and --tls-key on the fast path', () => {402 const parsed = parseServeFastPathArgs([403 'serve',404 '--tls-cert',405 '/tmp/cert.pem',406 '--tls-key',407 '/tmp/key.pem',408 ]);409 expect(parsed).toMatchObject({410 kind: 'serve',411 options: {412 tlsCert: '/tmp/cert.pem',413 tlsKey: '/tmp/key.pem',414 },415 });416 });417 it('parses bundled entrypoint argv before serve', () => {418 const parsed = parseServeFastPathArgs([419 '/repo/dist/cli.js',420 'serve',421 '--port',422 '0',423 ]);424 expect(parsed).toMatchObject({425 kind: 'serve',426 options: { port: 0 },427 });428 });429 it('falls back to the full parser for repeatable --workspace values', () => {430 expect(parseServeFastPathArgs([431 'serve',432 '--workspace',433 '/tmp/primary',434 '--workspace',435 '/tmp/secondary',436 ])).toEqual({ kind: 'fallback' });437 });438 it('falls back to the full parser for empty --workspace values', () => {439 expect(parseServeFastPathArgs(['serve', '--workspace='])).toEqual({440 kind: 'fallback',441 });442 expect(parseServeFastPathArgs(['serve', '--workspace', ''])).toEqual({443 kind: 'fallback',444 });445 });446 it('parses Windows bundled entrypoint argv before serve', () => {447 const parsed = parseServeFastPathArgs([448 'C:\\repo\\dist\\cli.js',449 'serve',450 '--port',451 '0',452 ]);453 expect(parsed).toMatchObject({454 kind: 'serve',455 options: { port: 0 },456 });457 });458 it('falls back to the full parser for help and unknown options', () => {459 expect(parseServeFastPathArgs(['serve', '--help'])).toEqual({460 kind: 'fallback',461 });462 expect(parseServeFastPathArgs(['serve', '--unknown-option'])).toEqual({463 kind: 'fallback',464 });465 });466 it('falls back to the full parser for daemon-managed channels', () => {467 expect(parseServeFastPathArgs(['serve', '--channel', 'telegram'])).toEqual({468 kind: 'fallback',469 });470 });471 it('handles every yargs serve long option or explicitly falls back', () => {472 const options = buildServeCommandParser().getOptions();473 const longOptionNames = Object.keys(options.key).filter((name) => name.length > 1 && !options.alias[name]?.length);474 const sampleArgvByOption = new Map([475 ['port', ['--port', '0']],476 ['hostname', ['--hostname', '127.0.0.1']],477 ['token', ['--token', 'token']],478 ['max-sessions', ['--max-sessions', '10']],479 ['max-total-sessions', ['--max-total-sessions', '20']],480 [481 'max-pending-prompts-per-session',482 ['--max-pending-prompts-per-session', '5'],483 ],484 ['max-connections', ['--max-connections', '256']],485 ['event-ring-size', ['--event-ring-size', '8000']],486 ['workspace', ['--workspace', process.cwd()]],487 ['require-auth', ['--require-auth']],488 ['enable-session-shell', ['--enable-session-shell']],489 ['tls-cert', ['--tls-cert', '/tmp/cert.pem']],490 ['tls-key', ['--tls-key', '/tmp/key.pem']],491 ['web', ['--no-web']],492 ['open', ['--open']],493 ['http-bridge', ['--no-http-bridge']],494 ['mcp-client-budget', ['--mcp-client-budget', '10']],495 ['mcp-budget-mode', ['--mcp-budget-mode', 'warn']],496 ['allow-origin', ['--allow-origin', 'http://localhost:3000']],497 ['allow-private-auth-base-url', ['--allow-private-auth-base-url']],498 ['prompt-deadline-ms', ['--prompt-deadline-ms', '1000']],499 ['writer-idle-timeout-ms', ['--writer-idle-timeout-ms', '1000']],500 ['channel-idle-timeout-ms', ['--channel-idle-timeout-ms', '1000']],501 ['session-reap-interval-ms', ['--session-reap-interval-ms', '1000']],502 ['session-idle-timeout-ms', ['--session-idle-timeout-ms', '1000']],503 [504 'permission-response-timeout-ms',505 ['--permission-response-timeout-ms', '1000'],506 ],507 ['rate-limit', ['--rate-limit']],508 ['rate-limit-prompt', ['--rate-limit-prompt', '10']],509 ['rate-limit-mutation', ['--rate-limit-mutation', '30']],510 ['rate-limit-read', ['--rate-limit-read', '120']],511 ['rate-limit-window-ms', ['--rate-limit-window-ms', '60000']],512 ['experimental-lsp', ['--experimental-lsp']],513 ['channel', ['--channel', 'telegram']],514 ['help', ['--help']],515 ['version', ['--version']],516 ]);517 const expectedFallbackOptions = new Set(['channel', 'help', 'version']);518 expect(longOptionNames.sort()).toEqual([...sampleArgvByOption.keys()].sort());519 for (const [optionName, sampleArgv] of sampleArgvByOption) {520 const parsed = parseServeFastPathArgs(['serve', ...sampleArgv]);521 if (parsed.kind === 'fallback') {522 expect(expectedFallbackOptions.has(optionName)).toBe(true);523 }524 else {525 expect(expectedFallbackOptions.has(optionName)).toBe(false);526 }527 }528 });529 it('matches yargs defaults for options materialized before runQwenServe', () => {530 const yargsParsed = buildServeCommandParser().parseSync('');531 const fastPathParsed = parseServeFastPathArgs(['serve']);532 expect(fastPathParsed).toMatchObject({533 kind: 'serve',534 options: {535 hostname: yargsParsed['hostname'],536 mode: 'http-bridge',537 port: yargsParsed['port'],538 },539 });540 expect(fastPathParsed).not.toHaveProperty('options.maxSessions');541 expect(fastPathParsed).not.toHaveProperty('options.maxTotalSessions');542 expect(fastPathParsed).not.toHaveProperty('options.maxConnections');543 expect(fastPathParsed).not.toHaveProperty('options.eventRingSize');544 expect(fastPathParsed).not.toHaveProperty('options.maxPendingPromptsPerSession');545 });546 it('keeps --experimental-lsp on the fast path', () => {547 const parsed = parseServeFastPathArgs(['serve', '--experimental-lsp']);548 expect(parsed).toMatchObject({549 kind: 'serve',550 options: { experimentalLsp: true },551 });552 });553 it('returns false to let the full CLI handle fallback cases', async () => {554 await expect(tryRunServeFastPath(['serve', '--help'])).resolves.toBe(false);555 });556 it('prints a breadcrumb when settings bootstrap falls back to the full CLI', async () => {557 tempWorkspace = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-fallback-')));558 mkdirSync(join(tempWorkspace, '.qwen'));559 writeFileSync(join(tempWorkspace, '.qwen', 'settings.json'), '{');560 const stderrWrites = [];561 vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {562 stderrWrites.push(String(chunk));563 return true;564 });565 await expect(tryRunServeFastPath(['serve', '--workspace', tempWorkspace])).resolves.toBe(false);566 expect(stderrWrites.join('')).toContain('qwen serve: fast-path bootstrap failed, falling back to full startup:');567 });568 it.each([569 [570 ['serve', '--mcp-client-budget', '0'],571 'qwen serve: --mcp-client-budget must be a positive integer.',572 ],573 [574 ['serve', '--mcp-budget-mode', 'enforce'],575 'qwen serve: --mcp-budget-mode=enforce requires --mcp-client-budget=N.',576 ],577 [578 ['serve', '--max-pending-prompts-per-session=-1'],579 'qwen serve: --max-pending-prompts-per-session must be a non-negative integer (0 / Infinity = unlimited).',580 ],581 [582 ['serve', '--rate-limit', '--rate-limit-prompt=0'],583 'qwen serve: --rate-limit-prompt must be a positive integer.',584 ],585 ])('validates %s before bootstrapping settings and environment', async (argv, message) => {586 const qwenHome = useTempQwenHome();587 writeFileSync(join(qwenHome, 'settings.json'), '{');588 const stderrWrites = [];589 vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {590 stderrWrites.push(String(chunk));591 return true;592 });593 vi.spyOn(process, 'exit').mockImplementation(((code) => {594 throw new Error(`process.exit(${code})`);595 }));596 await expect(tryRunServeFastPath(argv)).rejects.toThrow('process.exit(1)');597 expect(stderrWrites.join('')).toContain(message);598 });599 it('does not enable rate limiting just because tuning flags are present', () => {600 const parsed = parseServeFastPathArgs([601 'serve',602 '--rate-limit-prompt',603 '0',604 '--rate-limit-window-ms',605 '1',606 ]);607 expect(parsed.kind).toBe('serve');608 if (parsed.kind !== 'serve')609 return;610 expect(parsed.options).not.toHaveProperty('rateLimit');611 expect(parsed.options.rateLimitPrompt).toBe(0);612 expect(parsed.options.rateLimitWindowMs).toBe(1);613 });614 it('enables rate limiting from env and applies env tuning values', () => {615 const parsed = parseServeFastPathArgs(['serve'], {616 QWEN_SERVE_RATE_LIMIT: '1',617 QWEN_SERVE_RATE_LIMIT_PROMPT: '10',618 });619 expect(parsed.kind).toBe('serve');620 if (parsed.kind !== 'serve')621 return;622 expect(parsed.options.rateLimit).toBe(true);623 expect(parsed.options.rateLimitPrompt).toBe(10);624 });625 it('discards rate limit env tuning when rate limiting is disabled', () => {626 const parsed = parseServeFastPathArgs(['serve'], {627 QWEN_SERVE_RATE_LIMIT_PROMPT: '10',628 });629 expect(parsed.kind).toBe('serve');630 if (parsed.kind !== 'serve')631 return;632 expect(parsed.options).not.toHaveProperty('rateLimit');633 expect(parsed.options.rateLimitPrompt).toBeUndefined();634 });635 it('rejects unsafe rate limit env integers instead of rounding them', () => {636 const parsed = parseServeFastPathArgs(['serve', '--rate-limit'], {637 QWEN_SERVE_RATE_LIMIT_PROMPT: String(Number.MAX_SAFE_INTEGER + 1),638 });639 expect(parsed.kind).toBe('serve');640 if (parsed.kind !== 'serve')641 return;642 expect(parsed.options.rateLimitPrompt).toBeNaN();643 });644});645describe('serve fast path environment bootstrap', () => {646 it('keeps the lite settings directory name in sync with core QWEN_DIR', () => {647 expect(SETTINGS_DIRECTORY_NAME).toBe(QWEN_DIR);648 });649 it('matches Storage.getGlobalQwenDir path resolution', () => {650 tempWorkspace = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-storage-cwd-')));651 tempQwenHome = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-storage-home-')));652 process.chdir(tempWorkspace);653 for (const qwenHome of [654 undefined,655 tempQwenHome,656 '~',657 '~/qwen-fast-path',658 '~\\qwen-fast-path',659 'relative-qwen-home',660 ]) {661 if (qwenHome === undefined) {662 delete process.env['QWEN_HOME'];663 }664 else {665 process.env['QWEN_HOME'] = qwenHome;666 }667 expect(getGlobalQwenDirLite()).toBe(Storage.getGlobalQwenDir());668 }669 });670 it('closes the listener and exits when runtime startup fails after listen', async () => {671 const stderrWrites = [];672 const close = vi.fn().mockResolvedValue(undefined);673 vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {674 stderrWrites.push(String(chunk));675 return true;676 });677 vi.spyOn(process, 'exit').mockImplementation(((code) => {678 throw new Error(`process.exit(${code})`);679 }));680 await expect(waitForServeRuntimeOrExit({681 runtimeReady: Promise.reject(new Error('runtime boom')),682 close,683 })).rejects.toThrow('process.exit(1)');684 expect(close).toHaveBeenCalledTimes(1);685 expect(stderrWrites.join('')).toContain('qwen serve: runtime startup failed after listener was ready: runtime boom');686 expect(process.exit).toHaveBeenCalledWith(1);687 });688 it('does not report startup failure when runtime startup is cancelled by close', async () => {689 const stderrWrites = [];690 const close = vi.fn().mockResolvedValue(undefined);691 vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {692 stderrWrites.push(String(chunk));693 return true;694 });695 const exit = vi.spyOn(process, 'exit').mockImplementation(((code) => {696 throw new Error(`process.exit(${code})`);697 }));698 await expect(waitForServeRuntimeOrExit({699 runtimeReady: Promise.reject(new Error(RUNTIME_STARTUP_CANCELLED_MESSAGE)),700 close,701 })).resolves.toBeUndefined();702 expect(close).not.toHaveBeenCalled();703 expect(stderrWrites.join('')).not.toContain('runtime startup failed after listener was ready');704 expect(exit).not.toHaveBeenCalled();705 });706 it('validates rate limit env after settings bootstrap enables rate limiting', async () => {707 useTempQwenHome();708 tempWorkspace = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-rate-limit-env-')));709 mkdirSync(join(tempWorkspace, '.qwen'));710 writeFileSync(join(tempWorkspace, '.qwen', 'settings.json'), JSON.stringify({711 env: {712 QWEN_SERVE_RATE_LIMIT: '1',713 QWEN_SERVE_RATE_LIMIT_PROMPT: '0',714 },715 }));716 const stderrWrites = [];717 vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {718 stderrWrites.push(String(chunk));719 return true;720 });721 vi.spyOn(process, 'exit').mockImplementation(((code) => {722 throw new Error(`process.exit(${code})`);723 }));724 await expect(tryRunServeFastPath([725 'serve',726 '--workspace',727 tempWorkspace,728 '--port',729 '0',730 '--hostname',731 '127.0.0.1',732 '--no-open',733 '--no-web',734 ])).rejects.toThrow('process.exit(1)');735 expect(stderrWrites.join('')).toContain('qwen serve: --rate-limit-prompt must be a positive integer.');736 expect(process.exit).toHaveBeenCalledWith(1);737 });738 it('exits when runQwenServe fails after settings bootstrap succeeds', async () => {739 useTempQwenHome();740 vi.spyOn(runQwenServeModule, 'runQwenServe').mockRejectedValue(new Error('listen boom'));741 const stderrWrites = [];742 vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {743 stderrWrites.push(String(chunk));744 return true;745 });746 vi.spyOn(process, 'exit').mockImplementation(((code) => {747 throw new Error(`process.exit(${code})`);748 }));749 await expect(tryRunServeFastPath([750 'serve',751 '--port',752 '0',753 '--hostname',754 '127.0.0.1',755 '--no-open',756 '--no-web',757 ])).rejects.toThrow('process.exit(1)');758 expect(stderrWrites.join('')).toContain('qwen serve: listen boom');759 expect(process.exit).toHaveBeenCalledWith(1);760 });761 it('keeps headless yolo warning best-effort after listening', async () => {762 const originalSandbox = process.env['SANDBOX'];763 const originalSuppress = process.env['QWEN_CODE_SUPPRESS_YOLO_WARNING'];764 delete process.env['SANDBOX'];765 delete process.env['QWEN_CODE_SUPPRESS_YOLO_WARNING'];766 const qwenHome = useTempQwenHome();767 writeFileSync(join(qwenHome, 'settings.json'), JSON.stringify({ tools: { approvalMode: 'yolo', sandbox: false } }));768 const runtimeReady = Promise.reject(new Error('runtime boom'));769 void runtimeReady.catch(() => undefined);770 const close = vi.fn().mockResolvedValue(undefined);771 vi.spyOn(runQwenServeModule, 'runQwenServe').mockResolvedValue({772 runtimeReady,773 close,774 });775 const stderrWrites = [];776 vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {777 const text = String(chunk);778 stderrWrites.push(text);779 if (text.includes(HEADLESS_YOLO_NO_SANDBOX_WARNING)) {780 throw new Error('stderr closed');781 }782 return true;783 });784 vi.spyOn(process, 'exit').mockImplementation(((code) => {785 throw new Error(`process.exit(${code})`);786 }));787 try {788 await expect(tryRunServeFastPath(['serve', '--port', '0', '--no-open', '--no-web'])).rejects.toThrow('process.exit(1)');789 expect(stderrWrites.join('')).toContain(HEADLESS_YOLO_NO_SANDBOX_WARNING);790 expect(stderrWrites.join('')).toContain('qwen serve: runtime startup failed after listener was ready: runtime boom');791 expect(stderrWrites.join('')).not.toContain('qwen serve: stderr closed');792 expect(close).toHaveBeenCalledTimes(1);793 expect(process.exit).toHaveBeenCalledWith(1);794 }795 finally {796 if (originalSandbox === undefined) {797 delete process.env['SANDBOX'];798 }799 else {800 process.env['SANDBOX'] = originalSandbox;801 }802 if (originalSuppress === undefined) {803 delete process.env['QWEN_CODE_SUPPRESS_YOLO_WARNING'];804 }805 else {806 process.env['QWEN_CODE_SUPPRESS_YOLO_WARNING'] = originalSuppress;807 }808 }809 });810 it('rejects malformed user settings so the full settings loader can handle it', async () => {811 const qwenHome = useTempQwenHome();812 writeFileSync(join(qwenHome, 'settings.json'), '{');813 await expect(bootstrapServeFastPathEnvironment(undefined)).rejects.toThrow(/settings/i);814 }, 10_000);815 it('falls back to the full CLI when fast-path settings bootstrap fails', async () => {816 const qwenHome = useTempQwenHome();817 writeFileSync(join(qwenHome, 'settings.json'), '{');818 await expect(tryRunServeFastPath(['serve', '--port', '0', '--no-open', '--no-web'])).resolves.toBe(false);819 }, 10_000);820 it.each([821 [822 'advanced.excludedEnvVars',823 { advanced: { excludedEnvVars: 'QWEN_SERVER_TOKEN' } },824 ],825 [826 'advanced.runtimeOutputDir',827 { advanced: { runtimeOutputDir: ['.qwen-runtime'] } },828 ],829 [830 'security.folderTrust.enabled',831 { security: { folderTrust: { enabled: 'true' } } },832 ],833 ])('falls back to the full CLI when %s has an incompatible shape', async (_field, settingsJson) => {834 const qwenHome = useTempQwenHome();835 writeFileSync(join(qwenHome, 'settings.json'), JSON.stringify(settingsJson));836 await expect(tryRunServeFastPath(['serve', '--port', '0', '--no-open', '--no-web'])).resolves.toBe(false);837 });838 it('loads QWEN_SERVER_TOKEN from the workspace .env before the daemon starts', async () => {839 delete process.env['QWEN_SERVER_TOKEN'];840 useTempQwenHome();841 tempWorkspace = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-env-')));842 mkdirSync(join(tempWorkspace, '.qwen'));843 writeFileSync(join(tempWorkspace, '.qwen', '.env'), 'QWEN_SERVER_TOKEN=from-workspace-env\n');844 process.chdir(tempWorkspace);845 await bootstrapServeFastPathEnvironment(tempWorkspace);846 expect(process.env['QWEN_SERVER_TOKEN']).toBe('from-workspace-env');847 });848 it('loads .env from --workspace even when launched from another directory', async () => {849 delete process.env['QWEN_SERVER_TOKEN'];850 useTempQwenHome();851 tempWorkspace = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-workspace-env-')));852 tempLaunchCwd = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-launch-cwd-')));853 mkdirSync(join(tempWorkspace, '.qwen'));854 writeFileSync(join(tempWorkspace, '.qwen', '.env'), 'QWEN_SERVER_TOKEN=from-explicit-workspace-env\n');855 process.chdir(tempLaunchCwd);856 await bootstrapServeFastPathEnvironment(tempWorkspace);857 expect(process.env['QWEN_SERVER_TOKEN']).toBe('from-explicit-workspace-env');858 });859 it('loads home .env after workspace .env for daemon boot-time keys', async () => {860 delete process.env['QWEN_SERVER_TOKEN'];861 delete process.env['QWEN_SERVE_RATE_LIMIT'];862 delete process.env['QWEN_SERVE_RATE_LIMIT_PROMPT'];863 const qwenHome = useTempQwenHome();864 tempWorkspace = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-layered-env-')));865 writeFileSync(join(tempWorkspace, '.env'), 'QWEN_SERVE_RATE_LIMIT_PROMPT=123\n');866 writeFileSync(join(qwenHome, '.env'), ['QWEN_SERVER_TOKEN=from-home-env', 'QWEN_SERVE_RATE_LIMIT=1'].join('\n'));867 await bootstrapServeFastPathEnvironment(tempWorkspace);868 expect(process.env['QWEN_SERVE_RATE_LIMIT_PROMPT']).toBe('123');869 expect(process.env['QWEN_SERVER_TOKEN']).toBe('from-home-env');870 expect(process.env['QWEN_SERVE_RATE_LIMIT']).toBe('1');871 });872 it('applies legacy excludedProjectEnvVars before loading workspace .env', async () => {873 delete process.env['QWEN_SERVER_TOKEN'];874 const qwenHome = useTempQwenHome();875 writeFileSync(join(qwenHome, 'settings.json'), JSON.stringify({ excludedProjectEnvVars: ['QWEN_SERVER_TOKEN'] }));876 tempWorkspace = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-legacy-env-')));877 writeFileSync(join(tempWorkspace, '.env'), 'QWEN_SERVER_TOKEN=from-workspace-env\n');878 process.chdir(tempWorkspace);879 await bootstrapServeFastPathEnvironment(tempWorkspace);880 expect(process.env['QWEN_SERVER_TOKEN']).toBeUndefined();881 });882 it('loads QWEN_SERVER_TOKEN from workspace settings.env without the full settings loader', async () => {883 delete process.env['QWEN_SERVER_TOKEN'];884 useTempQwenHome();885 tempWorkspace = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-settings-env-')));886 mkdirSync(join(tempWorkspace, '.qwen'));887 writeFileSync(join(tempWorkspace, '.qwen', 'settings.json'), JSON.stringify({888 env: { QWEN_SERVER_TOKEN: 'from-workspace-settings-env' },889 }));890 process.chdir(tempWorkspace);891 await bootstrapServeFastPathEnvironment(tempWorkspace);892 expect(process.env['QWEN_SERVER_TOKEN']).toBe('from-workspace-settings-env');893 });894 it('pre-resolves home env overrides in the same order as the full loader', () => {895 delete process.env['QWEN_HOME'];896 delete process.env['QWEN_RUNTIME_DIR'];897 delete process.env['QWEN_CODE_MCP_APPROVALS_PATH'];898 delete process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'];899 tempLaunchCwd = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-fake-home-')));900 process.env['HOME'] = tempLaunchCwd;901 process.env['USERPROFILE'] = tempLaunchCwd;902 tempQwenHome = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-discovered-home-')));903 mkdirSync(join(tempLaunchCwd, '.qwen'), { recursive: true });904 writeFileSync(join(tempLaunchCwd, '.qwen', '.env'), `QWEN_HOME=${tempQwenHome}\n`);905 writeFileSync(join(tempLaunchCwd, '.env'), 'QWEN_RUNTIME_DIR=from-home-env\n');906 writeFileSync(join(tempQwenHome, '.env'), [907 'QWEN_CODE_MCP_APPROVALS_PATH=from-discovered-home',908 'QWEN_CODE_TRUSTED_FOLDERS_PATH=from-discovered-trust',909 ].join('\n'));910 preResolveServeFastPathHomeEnvOverrides();911 expect(process.env['QWEN_HOME']).toBe(tempQwenHome);912 expect(process.env['QWEN_RUNTIME_DIR']).toBe('from-home-env');913 expect(process.env['QWEN_CODE_MCP_APPROVALS_PATH']).toBe('from-discovered-home');914 expect(process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH']).toBe('from-discovered-trust');915 });916 it('still pre-resolves missing home-scoped keys when QWEN_HOME and runtime are already set', () => {917 delete process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'];918 const qwenHome = useTempQwenHome();919 process.env['QWEN_RUNTIME_DIR'] = join(qwenHome, 'runtime');920 writeFileSync(join(qwenHome, '.env'), 'QWEN_CODE_TRUSTED_FOLDERS_PATH=from-existing-home\n');921 preResolveServeFastPathHomeEnvOverrides();922 expect(process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH']).toBe('from-existing-home');923 });924 it('applies legacy settings keys consumed by the serve fast path', () => {925 const qwenHome = useTempQwenHome();926 tempWorkspace = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-legacy-settings-')));927 writeFileSync(join(qwenHome, 'settings.json'), JSON.stringify({928 approvalMode: 'yolo',929 contextFileName: 'LEGACY.md',930 excludedProjectEnvVars: ['QWEN_SERVER_TOKEN'],931 fileFiltering: { customIgnoreFiles: ['.legacy-ignore'] },932 folderTrust: true,933 sandbox: false,934 }));935 const settings = loadServeFastPathSettings(tempWorkspace);936 expect(settings).toMatchObject({937 advanced: { excludedEnvVars: ['QWEN_SERVER_TOKEN'] },938 context: {939 fileName: 'LEGACY.md',940 fileFiltering: { customIgnoreFiles: ['.legacy-ignore'] },941 },942 security: { folderTrust: { enabled: true } },943 tools: { approvalMode: 'yolo', sandbox: false },944 });945 });946 it('matches the full settings loader for fields consumed before listen', async () => {947 const qwenHome = useTempQwenHome();948 tempWorkspace = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-settings-parity-')));949 mkdirSync(join(tempWorkspace, '.qwen'));950 const { SETTINGS_VERSION, loadSettings } = await import('../config/settings.js');951 const versioned = (settings) => ({952 $version: SETTINGS_VERSION,953 ...settings,954 });955 writeFileSync(process.env['QWEN_CODE_SYSTEM_DEFAULTS_PATH'], JSON.stringify(versioned({956 env: {957 FAST_PATH_DEFAULT_ONLY: 'default',958 FAST_PATH_OVERLAP: 'default',959 },960 advanced: {961 excludedEnvVars: ['FAST_PATH_DEFAULT_EXCLUDED'],962 runtimeOutputDir: '.default-runtime',963 },964 context: {965 fileName: 'DEFAULT.md',966 fileFiltering: { customIgnoreFiles: ['.default-ignore'] },967 },968 security: { folderTrust: { enabled: false } },969 tools: { approvalMode: 'default' },970 })));971 writeFileSync(join(qwenHome, 'settings.json'), JSON.stringify(versioned({972 env: {973 FAST_PATH_USER_ONLY: 'user',974 FAST_PATH_OVERLAP: 'user',975 },976 advanced: {977 excludedEnvVars: ['FAST_PATH_USER_EXCLUDED'],978 },979 security: { folderTrust: { enabled: true } },980 tools: { approvalMode: 'auto' },981 })));982 writeFileSync(join(tempWorkspace, '.qwen', 'settings.json'), JSON.stringify(versioned({983 env: {984 FAST_PATH_WORKSPACE_ONLY: 'workspace',985 FAST_PATH_OVERLAP: 'workspace',986 },987 advanced: { runtimeOutputDir: '.workspace-runtime' },988 context: {989 fileName: 'WORKSPACE.md',990 fileFiltering: { customIgnoreFiles: ['.workspace-ignore'] },991 },992 policy: { permissionStrategy: 'consensus', consensusQuorum: 3 },993 tools: { sandbox: true },994 })));995 writeFileSync(process.env['QWEN_CODE_SYSTEM_SETTINGS_PATH'], JSON.stringify(versioned({996 env: {997 FAST_PATH_SYSTEM_ONLY: 'system',998 FAST_PATH_OVERLAP: 'system',999 },1000 context: { fileName: 'SYSTEM.md' },1001 tools: { approvalMode: 'yolo' },1002 })));1003 expect(loadServeFastPathSettings(tempWorkspace)).toEqual(pickServeFastPathComparable(loadSettings(tempWorkspace, { skipLoadEnvironment: true }).merged));1004 });1005 it('loads runtimeOutputDir for daemon startup artifacts', () => {1006 const qwenHome = useTempQwenHome();1007 tempWorkspace = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-runtime-dir-')));1008 writeFileSync(join(qwenHome, 'settings.json'), JSON.stringify({1009 advanced: { runtimeOutputDir: '.qwen-runtime' },1010 }));1011 const settings = loadServeFastPathSettings(tempWorkspace);1012 expect(settings.advanced?.runtimeOutputDir).toBe('.qwen-runtime');1013 });1014 it('ignores stale legacy keys in current-version settings files', () => {1015 const qwenHome = useTempQwenHome();1016 tempWorkspace = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-stale-legacy-settings-')));1017 writeFileSync(join(qwenHome, 'settings.json'), JSON.stringify({1018 $version: 5,1019 approvalMode: 'yolo',1020 contextFileName: 'LEGACY.md',1021 excludedProjectEnvVars: ['QWEN_SERVER_TOKEN'],1022 fileFiltering: { customIgnoreFiles: ['.legacy-ignore'] },1023 folderTrust: true,1024 sandbox: false,1025 }));1026 const settings = loadServeFastPathSettings(tempWorkspace);1027 expect(settings.advanced).toBeUndefined();1028 expect(settings.context).toBeUndefined();1029 expect(settings.security).toBeUndefined();1030 expect(settings.tools).toBeUndefined();1031 });1032 it('uses trusted-folders path from home .env before loading workspace env', async () => {1033 delete process.env['QWEN_SERVER_TOKEN'];1034 delete process.env['QWEN_RUNTIME_DIR'];1035 delete process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'];1036 const qwenHome = useTempQwenHome();1037 const customTrustedFoldersPath = join(qwenHome, 'custom-trusted.json');1038 tempWorkspace = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-home-trust-env-')));1039 writeFileSync(join(qwenHome, '.env'), `QWEN_CODE_TRUSTED_FOLDERS_PATH=${customTrustedFoldersPath}\n`);1040 writeFileSync(join(qwenHome, 'settings.json'), JSON.stringify({ security: { folderTrust: { enabled: true } } }));1041 writeFileSync(customTrustedFoldersPath, JSON.stringify({ [tempWorkspace]: TrustLevel.DO_NOT_TRUST }));1042 writeFileSync(join(tempWorkspace, '.env'), 'QWEN_SERVER_TOKEN=from-untrusted-workspace-env\n');1043 await bootstrapServeFastPathEnvironment(tempWorkspace);1044 expect(process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH']).toBe(customTrustedFoldersPath);1045 expect(process.env['QWEN_SERVER_TOKEN']).toBeUndefined();1046 });1047 it('uses legacy folderTrust before loading workspace env', async () => {1048 delete process.env['QWEN_SERVER_TOKEN'];1049 const qwenHome = useTempQwenHome();1050 tempWorkspace = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-legacy-trust-')));1051 writeFileSync(join(qwenHome, 'settings.json'), JSON.stringify({ folderTrust: true }));1052 process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'] = join(qwenHome, 'trustedFolders.json');1053 writeFileSync(process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'], JSON.stringify({ [tempWorkspace]: TrustLevel.DO_NOT_TRUST }));1054 writeFileSync(join(tempWorkspace, '.env'), 'QWEN_SERVER_TOKEN=from-untrusted-workspace-env\n');1055 await bootstrapServeFastPathEnvironment(tempWorkspace);1056 expect(process.env['QWEN_SERVER_TOKEN']).toBeUndefined();1057 });1058 it('caches trusted folders during a single fast-path bootstrap', () => {1059 delete process.env['QWEN_SERVER_TOKEN'];1060 const qwenHome = useTempQwenHome();1061 tempWorkspace = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-trust-cache-')));1062 writeFileSync(join(qwenHome, 'settings.json'), JSON.stringify({ security: { folderTrust: { enabled: true } } }));1063 process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'] = join(qwenHome, 'trustedFolders.json');1064 writeFileSync(process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'], JSON.stringify({ [tempWorkspace]: TrustLevel.TRUST_FOLDER }));1065 writeFileSync(join(tempWorkspace, '.env'), 'QWEN_SERVER_TOKEN=trusted\n');1066 const settings = loadServeFastPathSettings(tempWorkspace);1067 writeFileSync(process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'], JSON.stringify({ [tempWorkspace]: TrustLevel.DO_NOT_TRUST }));1068 loadServeFastPathEnvironment(settings, tempWorkspace);1069 expect(process.env['QWEN_SERVER_TOKEN']).toBe('trusted');1070 });1071 it('prioritizes trusted parent folders over nested distrust rules', async () => {1072 delete process.env['QWEN_SERVER_TOKEN'];1073 const qwenHome = useTempQwenHome();1074 tempWorkspace = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-trust-precedence-')));1075 const childWorkspace = join(tempWorkspace, 'child');1076 mkdirSync(childWorkspace);1077 writeFileSync(join(qwenHome, 'settings.json'), JSON.stringify({ security: { folderTrust: { enabled: true } } }));1078 process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'] = join(qwenHome, 'trustedFolders.json');1079 writeFileSync(process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'], JSON.stringify({1080 [tempWorkspace]: TrustLevel.TRUST_FOLDER,1081 [childWorkspace]: TrustLevel.DO_NOT_TRUST,1082 }));1083 writeFileSync(join(childWorkspace, '.env'), 'QWEN_SERVER_TOKEN=trusted\n');1084 await bootstrapServeFastPathEnvironment(childWorkspace);1085 expect(process.env['QWEN_SERVER_TOKEN']).toBe('trusted');1086 });1087 it('treats TRUST_PARENT as trusting the containing folder', async () => {1088 delete process.env['QWEN_SERVER_TOKEN'];1089 const qwenHome = useTempQwenHome();1090 tempWorkspace = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-trust-parent-')));1091 writeFileSync(join(qwenHome, 'settings.json'), JSON.stringify({ security: { folderTrust: { enabled: true } } }));1092 process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'] = join(qwenHome, 'trustedFolders.json');1093 writeFileSync(process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'], JSON.stringify({1094 [join(tempWorkspace, 'marker')]: TrustLevel.TRUST_PARENT,1095 }));1096 writeFileSync(join(tempWorkspace, '.env'), 'QWEN_SERVER_TOKEN=trusted\n');1097 await bootstrapServeFastPathEnvironment(tempWorkspace);1098 expect(process.env['QWEN_SERVER_TOKEN']).toBe('trusted');1099 });1100 it('matches Cloud Shell default project behavior for empty env values', async () => {1101 delete process.env['GOOGLE_CLOUD_PROJECT'];1102 process.env['CLOUD_SHELL'] = 'true';1103 useTempQwenHome();1104 tempWorkspace = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-cloud-shell-')));1105 writeFileSync(join(tempWorkspace, '.env'), 'GOOGLE_CLOUD_PROJECT=\n');1106 await bootstrapServeFastPathEnvironment(tempWorkspace);1107 expect(process.env['GOOGLE_CLOUD_PROJECT']).toBe('cloudshell-gca');1108 });1109 it('expands process environment placeholders in workspace settings.env', async () => {1110 delete process.env['QWEN_SERVER_TOKEN'];1111 useTempQwenHome();1112 process.env['FAST_PATH_REFERENCED_TOKEN'] = 'from-referenced-env';1113 tempWorkspace = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-settings-env-')));1114 mkdirSync(join(tempWorkspace, '.qwen'));1115 writeFileSync(join(tempWorkspace, '.qwen', 'settings.json'), JSON.stringify({1116 env: { QWEN_SERVER_TOKEN: '${FAST_PATH_REFERENCED_TOKEN}' },1117 }));1118 process.chdir(tempWorkspace);1119 await bootstrapServeFastPathEnvironment(tempWorkspace);1120 expect(process.env['QWEN_SERVER_TOKEN']).toBe('from-referenced-env');1121 });1122 it('expands home .env fallback placeholders in workspace settings.env', async () => {1123 delete process.env['QWEN_SERVER_TOKEN'];1124 delete process.env['FAST_PATH_REFERENCED_TOKEN'];1125 const qwenHome = useTempQwenHome();1126 writeFileSync(join(qwenHome, '.env'), 'FAST_PATH_REFERENCED_TOKEN=from-home-env\n');1127 tempWorkspace = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-settings-env-')));1128 mkdirSync(join(tempWorkspace, '.qwen'));1129 writeFileSync(join(tempWorkspace, '.qwen', 'settings.json'), JSON.stringify({1130 env: { QWEN_SERVER_TOKEN: '${FAST_PATH_REFERENCED_TOKEN}' },1131 }));1132 process.chdir(tempWorkspace);1133 await bootstrapServeFastPathEnvironment(tempWorkspace);1134 expect(process.env['QWEN_SERVER_TOKEN']).toBe('from-home-env');1135 });1136 it.each([1137 ['malformed JSON', '{ "env": { "QWEN_SERVER_TOKEN": "broken" }'],1138 ['non-object JSON', '[]'],1139 ])('rejects %s workspace settings so the full settings loader can handle it', async (_name, settingsJson) => {1140 delete process.env['QWEN_SERVER_TOKEN'];1141 useTempQwenHome();1142 tempWorkspace = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-bad-settings-')));1143 mkdirSync(join(tempWorkspace, '.qwen'));1144 writeFileSync(join(tempWorkspace, '.qwen', 'settings.json'), settingsJson);1145 process.chdir(tempWorkspace);1146 await expect(bootstrapServeFastPathEnvironment(tempWorkspace)).rejects.toThrow(/settings/i);1147 expect(process.env['QWEN_SERVER_TOKEN']).toBeUndefined();1148 });1149 it('still reads invalid workspace settings before dropping an untrusted workspace from the merge', () => {1150 const qwenHome = useTempQwenHome();1151 tempWorkspace = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-untrusted-settings-')));1152 mkdirSync(join(tempWorkspace, '.qwen'));1153 writeFileSync(join(qwenHome, 'settings.json'), JSON.stringify({ security: { folderTrust: { enabled: true } } }));1154 process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'] = join(qwenHome, 'trustedFolders.json');1155 writeFileSync(process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'], JSON.stringify({ [tempWorkspace]: TrustLevel.DO_NOT_TRUST }));1156 writeFileSync(join(tempWorkspace, '.qwen', 'settings.json'), '[]');1157 process.chdir(tempWorkspace);1158 expect(() => loadServeFastPathSettings(tempWorkspace)).toThrow(/settings/i);1159 });1160 it('does not load env from an explicit untrusted workspace when launched elsewhere', async () => {1161 delete process.env['QWEN_SERVER_TOKEN'];1162 const qwenHome = useTempQwenHome();1163 tempWorkspace = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-untrusted-env-')));1164 tempLaunchCwd = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-trusted-launch-')));1165 mkdirSync(join(tempWorkspace, '.qwen'));1166 writeFileSync(join(qwenHome, 'settings.json'), JSON.stringify({ security: { folderTrust: { enabled: true } } }));1167 process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'] = join(qwenHome, 'trustedFolders.json');1168 writeFileSync(process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'], JSON.stringify({1169 [tempLaunchCwd]: TrustLevel.TRUST_FOLDER,1170 [tempWorkspace]: TrustLevel.DO_NOT_TRUST,1171 }));1172 writeFileSync(join(tempWorkspace, '.env'), 'QWEN_SERVER_TOKEN=from-untrusted-workspace-env\n');1173 writeFileSync(join(tempWorkspace, '.qwen', 'settings.json'), JSON.stringify({1174 env: { QWEN_SERVER_TOKEN: 'from-untrusted-workspace-settings' },1175 }));1176 process.chdir(tempLaunchCwd);1177 await bootstrapServeFastPathEnvironment(tempWorkspace);1178 expect(process.env['QWEN_SERVER_TOKEN']).toBeUndefined();1179 });1180 it('checks trust against the canonical explicit workspace path', async () => {1181 delete process.env['QWEN_SERVER_TOKEN'];1182 const qwenHome = useTempQwenHome();1183 tempWorkspace = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-real-untrusted-env-')));1184 tempLaunchCwd = realpathSync(mkdtempSync(join(os.tmpdir(), 'qws-fast-path-symlink-launch-')));1185 tempSymlink = join(tempLaunchCwd, 'workspace-link');1186 symlinkSync(tempWorkspace, tempSymlink, 'dir');1187 writeFileSync(join(qwenHome, 'settings.json'), JSON.stringify({ security: { folderTrust: { enabled: true } } }));1188 process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'] = join(qwenHome, 'trustedFolders.json');1189 writeFileSync(process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'], JSON.stringify({1190 [tempLaunchCwd]: TrustLevel.TRUST_FOLDER,1191 [tempWorkspace]: TrustLevel.DO_NOT_TRUST,1192 }));1193 writeFileSync(join(tempWorkspace, '.env'), 'QWEN_SERVER_TOKEN=from-symlinked-untrusted-workspace-env\n');1194 process.chdir(tempLaunchCwd);1195 await bootstrapServeFastPathEnvironment(tempSymlink);1196 expect(process.env['QWEN_SERVER_TOKEN']).toBeUndefined();1197 });1198});1199//# sourceMappingURL=fast-path.test.js.map