CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes48downloads
fast-path.test.js1199 linesDownload Raw Back to serve
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
basant307/AI_Governance_Project · CoolFace