fix(cli): scope startup warnings to dev sessions (#8456)

* fix(cli): scope startup warnings to dev sessions

* fix(cli): secure scoped startup warnings

Signed-off-by: MarkXian <[email protected]>

* test(cli): cover scoped startup warning cleanup

* test(cli): assert sandbox warning env flag

* test: cover build warning cleanup branches

* test(cli): cover startup warning cleanup timing

* test(cli): cover unset warnings file sandbox env

* test(cli): isolate warning file env in startup tests

* test(cli): harden startup warning file cleanup

* test(cli): cover warnings file path translation

* test: allow warnings file env in sandbox guard

---------

Signed-off-by: MarkXian <[email protected]>
Co-authored-by: 冼健聪 <[email protected]>
This commit is contained in:
Mark Xianand冼健聪 authored and GitHub committed 2026-08-29 13:22:34 +00:00
1 parent eaa2f5695f
commit 80b34ecbbb
12 files changed
+381 -72

No files matched your search

+3
View File
@@ -4902,6 +4902,7 @@ describe('Settings Loading and Merging', () => {
delete process.env['QWEN_RUNTIME_DIR'];
delete process.env['QWEN_CODE_MCP_APPROVALS_PATH'];
delete process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'];
delete process.env['QWEN_CODE_WARNINGS_FILE'];
const cwdSpy = vi
.spyOn(process, 'cwd')
@@ -4924,6 +4925,7 @@ describe('Settings Loading and Merging', () => {
'QWEN_RUNTIME_DIR=/tmp/hijack-runtime',
'QWEN_CODE_MCP_APPROVALS_PATH=/tmp/preapproved.json',
'QWEN_CODE_TRUSTED_FOLDERS_PATH=/tmp/trusted.json',
'QWEN_CODE_WARNINGS_FILE=/tmp/sensitive.txt',
'OTHER_VAR=ok',
].join('\n');
return '{}';
@@ -4937,6 +4939,7 @@ describe('Settings Loading and Merging', () => {
expect(process.env['QWEN_RUNTIME_DIR']).toBeUndefined();
expect(process.env['QWEN_CODE_MCP_APPROVALS_PATH']).toBeUndefined();
expect(process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH']).toBeUndefined();
expect(process.env['QWEN_CODE_WARNINGS_FILE']).toBeUndefined();
// Other vars from the same project .env still load.
expect(process.env['OTHER_VAR']).toEqual('ok');
@@ -40,6 +40,11 @@ describe('PROJECT_ENV_HARDCODED_EXCLUSIONS', () => {
);
});
it('excludes QWEN_CODE_WARNINGS_FILE so a project .env cannot redirect temp warnings', () => {
expect(PROJECT_ENV_HARDCODED_EXCLUSIONS).toContain(
'QWEN_CODE_WARNINGS_FILE',
);
});
it('keeps ACP repeated-tool-failure rollout policy operator-owned', () => {
expect(PROJECT_ENV_HARDCODED_EXCLUSIONS).toContain(
ENV_ACP_REPEATED_TOOL_FAILURE_GUARD,
@@ -27,6 +27,9 @@ export const PROJECT_ENV_HARDCODED_EXCLUSIONS = [
'QWEN_RUNTIME_DIR',
'QWEN_CODE_MCP_APPROVALS_PATH',
'QWEN_CODE_TRUSTED_FOLDERS_PATH',
// This points to a host temp file that carries build warnings. A project
// `.env` must not redirect it to an arbitrary file to read or delete.
'QWEN_CODE_WARNINGS_FILE',
// Runtime attribution markers are stamped by trusted launchers. A project
// `.env` must not spoof client channel telemetry.
QWEN_CODE_SERVE_ENV,
@@ -242,6 +242,7 @@ const allowedProcessEnvAccesses = normalizeAllowances([
'key:PYTHONPATH': 2,
'key:QWEN_CODE_INTEGRATION_TEST': 1,
'key:QWEN_CODE_MCP_APPROVALS_PATH': 2,
'key:QWEN_CODE_WARNINGS_FILE': 2,
'key:QWEN_CODE_SCRUB_ELECTRON_RUN_AS_NODE': 1,
'key:QWEN_CODE_TEST_VAR': 2,
'key:QWEN_SANDBOX_PROXY_COMMAND': 2,
+97
View File
@@ -5,6 +5,7 @@
*/
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { EventEmitter } from 'node:events';
import { pathToFileURL } from 'node:url';
@@ -50,6 +51,7 @@ afterEach(() => {
describe('start_sandbox', () => {
it('passes child environment variables into a container sandbox', async () => {
vi.stubEnv('SANDBOX_SET_UID_GID', 'false');
vi.stubEnv('QWEN_CODE_WARNINGS_FILE', '/tmp/qwen-code-warnings-test.txt');
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
vi.spyOn(fs, 'realpathSync').mockImplementation((filePath) =>
String(filePath),
@@ -87,6 +89,13 @@ describe('start_sandbox', () => {
'--env',
PRIVATE_ACP_CAPABILITY_ENV,
]);
const warningsFlagIndex = args.indexOf(
'QWEN_CODE_WARNINGS_FILE=/tmp/qwen-code-warnings-test.txt',
);
expect(args.slice(warningsFlagIndex - 1, warningsFlagIndex + 1)).toEqual([
'--env',
'QWEN_CODE_WARNINGS_FILE=/tmp/qwen-code-warnings-test.txt',
]);
expect(options).toEqual(
expect.objectContaining({
env: expect.objectContaining({
@@ -99,6 +108,94 @@ describe('start_sandbox', () => {
await expect(result).resolves.toBe(0);
});
it('omits the warnings file environment variable when it is unset', async () => {
vi.stubEnv('SANDBOX_SET_UID_GID', 'false');
vi.stubEnv('QWEN_CODE_WARNINGS_FILE', '');
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
vi.spyOn(fs, 'realpathSync').mockImplementation((filePath) =>
String(filePath),
);
execSyncMock.mockReturnValue(Buffer.from(''));
const imageCheck = Object.assign(new EventEmitter(), {
stdout: new EventEmitter(),
});
const child = new EventEmitter();
spawnMock
.mockImplementationOnce(() => {
queueMicrotask(() => {
imageCheck.stdout.emit('data', Buffer.from('image-id'));
imageCheck.emit('close', 0);
});
return imageCheck;
})
.mockReturnValueOnce(child);
const result = start_sandbox(
{ command: 'docker', image: 'example.com/qwen-code:latest' },
[],
undefined,
[process.execPath, '/path/to/cli.js'],
);
await vi.waitFor(() => expect(spawnMock).toHaveBeenCalledTimes(2));
const args = spawnMock.mock.calls[1]?.[1] as string[];
expect(
args.filter((arg) => arg.startsWith('QWEN_CODE_WARNINGS_FILE=')),
).toEqual([]);
child.emit('close', 0);
await expect(result).resolves.toBe(0);
});
it('translates the warnings file path before forwarding it on Windows', async () => {
vi.stubEnv('SANDBOX_SET_UID_GID', 'false');
vi.stubEnv(
'QWEN_CODE_WARNINGS_FILE',
'C:\\Users\\test\\Temp\\qwen-code-warnings-test.txt',
);
vi.spyOn(os, 'platform').mockReturnValue('win32');
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
vi.spyOn(fs, 'realpathSync').mockImplementation((filePath) =>
String(filePath),
);
execSyncMock.mockReturnValue(Buffer.from(''));
const imageCheck = Object.assign(new EventEmitter(), {
stdout: new EventEmitter(),
});
const child = new EventEmitter();
spawnMock
.mockImplementationOnce(() => {
queueMicrotask(() => {
imageCheck.stdout.emit('data', Buffer.from('image-id'));
imageCheck.emit('close', 0);
});
return imageCheck;
})
.mockReturnValueOnce(child);
const result = start_sandbox(
{ command: 'docker', image: 'example.com/qwen-code:latest' },
[],
undefined,
[process.execPath, '/path/to/cli.js'],
);
await vi.waitFor(() => expect(spawnMock).toHaveBeenCalledTimes(2));
const args = spawnMock.mock.calls[1]?.[1] as string[];
const warningsFlagIndex = args.indexOf(
'QWEN_CODE_WARNINGS_FILE=/c/Users/test/Temp/qwen-code-warnings-test.txt',
);
expect(args.slice(warningsFlagIndex - 1, warningsFlagIndex + 1)).toEqual([
'--env',
'QWEN_CODE_WARNINGS_FILE=/c/Users/test/Temp/qwen-code-warnings-test.txt',
]);
child.emit('close', 0);
await expect(result).resolves.toBe(0);
});
it('checks image presence with an offline inspect that sees digest references', async () => {
vi.stubEnv('SANDBOX_SET_UID_GID', 'false');
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
+8
View File
@@ -672,6 +672,14 @@ export async function start_sandbox(
)}`,
);
}
if (process.env['QWEN_CODE_WARNINGS_FILE']) {
args.push(
'--env',
`QWEN_CODE_WARNINGS_FILE=${getContainerPath(
process.env['QWEN_CODE_WARNINGS_FILE'],
)}`,
);
}
// copy GEMINI_API_KEY(s)
if (process.env['GEMINI_API_KEY']) {
+20 -4
View File
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { afterEach, describe, it, expect, vi, beforeEach } from 'vitest';
import { getStartupWarnings } from './startupWarnings.js';
import * as fs from 'node:fs/promises';
import { getErrorMessage } from '@qwen-code/qwen-code-core';
@@ -20,8 +20,15 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
});
describe('startupWarnings', () => {
const warningsFilePath = '/tmp/qwen-code-startup-warnings-test.txt';
beforeEach(() => {
vi.resetAllMocks();
vi.stubEnv('QWEN_CODE_WARNINGS_FILE', warningsFilePath);
});
afterEach(() => {
vi.unstubAllEnvs();
});
it('should return warnings from the file and delete it', async () => {
@@ -32,12 +39,21 @@ describe('startupWarnings', () => {
const warnings = await getStartupWarnings();
expect(fs.access).toHaveBeenCalled();
expect(fs.readFile).toHaveBeenCalled();
expect(fs.unlink).toHaveBeenCalled();
expect(fs.access).toHaveBeenCalledWith(warningsFilePath);
expect(fs.readFile).toHaveBeenCalledWith(warningsFilePath, 'utf-8');
expect(fs.unlink).toHaveBeenCalledWith(warningsFilePath);
expect(warnings).toEqual(['Warning 1', 'Warning 2']);
});
it('does not read a shared warnings file when no scoped path is provided', async () => {
delete process.env['QWEN_CODE_WARNINGS_FILE'];
const warnings = await getStartupWarnings();
expect(warnings).toEqual([]);
expect(fs.access).not.toHaveBeenCalled();
});
it('should return an empty array if the file does not exist', async () => {
const error = new Error('File not found');
(error as Error & { code: string }).code = 'ENOENT';
+3 -4
View File
@@ -5,13 +5,12 @@
*/
import fs from 'node:fs/promises';
import os from 'node:os';
import { join as pathJoin } from 'node:path';
import { getErrorMessage } from '@qwen-code/qwen-code-core';
const warningsFilePath = pathJoin(os.tmpdir(), 'qwen-code-warnings.txt');
export async function getStartupWarnings(): Promise<string[]> {
const warningsFilePath = process.env['QWEN_CODE_WARNINGS_FILE'];
if (!warningsFilePath) return [];
try {
await fs.access(warningsFilePath); // Check if file exists
const warningsContent = await fs.readFile(warningsFilePath, 'utf-8');
+27 -34
View File
@@ -6,7 +6,6 @@
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os'; // Import os module
// --- Configuration ---
const cliPackageDir = path.resolve('packages', 'cli'); // Base directory for the CLI package
@@ -17,9 +16,31 @@ const filesToWatch = [
path.join(cliPackageDir, 'tsconfig.json'),
]; // Specific files within the CLI package
const buildDir = path.join(cliPackageDir, 'dist'); // Build output directory within the CLI package
const warningsFilePath = path.join(os.tmpdir(), 'qwen-code-warnings.txt'); // Temp file for warnings
const warningsFilePath = process.env['QWEN_CODE_WARNINGS_FILE'];
// ---------------------
function clearWarningsFile() {
if (!warningsFilePath) return;
try {
if (fs.existsSync(warningsFilePath)) {
fs.unlinkSync(warningsFilePath);
}
} catch (err) {
console.warn(
`[Check Script] Warning: Could not delete previous warnings file: ${err.message}`,
);
}
}
function writeWarningsFile(content) {
if (!warningsFilePath) return;
try {
fs.writeFileSync(warningsFilePath, content, { mode: 0o600 });
} catch (err) {
console.error(`[Check Script] Error writing warnings file: ${err.message}`);
}
}
function getMtime(filePath) {
try {
return fs.statSync(filePath).mtimeMs; // Use mtimeMs for higher precision
@@ -58,28 +79,14 @@ function findSourceFiles(dir, allFiles = []) {
console.error('Checking build status...');
// Clean up old warnings file before check
try {
if (fs.existsSync(warningsFilePath)) {
fs.unlinkSync(warningsFilePath);
}
} catch (err) {
console.warn(
`[Check Script] Warning: Could not delete previous warnings file: ${err.message}`,
);
}
clearWarningsFile();
const buildMtime = getMtime(buildTimestampPath);
if (!buildMtime) {
// If build is missing, write that as a warning and exit(0) so app can display it
const errorMessage = `ERROR: Build timestamp file (${path.relative(process.cwd(), buildTimestampPath)}) not found. Run \`npm run build\` first.`;
console.error(errorMessage); // Still log error here
try {
fs.writeFileSync(warningsFilePath, errorMessage);
} catch (writeErr) {
console.error(
`[Check Script] Error writing missing build warning file: ${writeErr.message}`,
);
}
writeWarningsFile(errorMessage);
process.exit(0); // Allow app to start and show the error
}
@@ -129,25 +136,11 @@ if (newerSourceFileFound) {
console.warn(finalWarning);
// Write warnings to the temp file
try {
fs.writeFileSync(warningsFilePath, warningMessages.join('\n'));
// Removed debug log
} catch (err) {
console.error(`[Check Script] Error writing warnings file: ${err.message}`);
// Proceed without writing, app won't show warnings
}
writeWarningsFile(warningMessages.join('\n'));
} else {
console.error('Build is up-to-date.');
// Ensure no stale warning file exists if build is ok
try {
if (fs.existsSync(warningsFilePath)) {
fs.unlinkSync(warningsFilePath);
}
} catch (err) {
console.warn(
`[Check Script] Warning: Could not delete previous warnings file: ${err.message}`,
);
}
clearWarningsFile();
}
process.exit(0); // Always exit successfully so the app starts
+27 -13
View File
@@ -20,18 +20,38 @@
// limitations under the License.
import { spawn, execSync } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { readFileSync } from 'node:fs';
import { readFileSync, rmSync } from 'node:fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '..');
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8'));
const startupWarningsFile = join(
tmpdir(),
`qwen-code-warnings-${process.pid}-${randomUUID()}.txt`,
);
const env = {
...process.env,
CLI_VERSION: pkg.version,
DEV: 'true',
QWEN_CODE_WARNINGS_FILE: startupWarningsFile,
// The entry a `qwen …` subprocess should call to reach THIS build. This
// launcher runs `node packages/cli` directly — no bin wrapper in the chain, so
// nothing else publishes it, and a /review run from `npm start` would fall
// back to whatever `qwen` PATH resolves to. Assignment, not `||=`, for the
// same reason as scripts/dev.js: an inherited value is another session's CLI.
QWEN_CODE_CLI: fileURLToPath(import.meta.url),
};
// check build status, write warnings to file for app to display if needed
execSync('node ./scripts/check-build-status.js', {
stdio: 'inherit',
cwd: root,
env,
});
const nodeArgs = ['--expose-gc'];
@@ -60,18 +80,6 @@ if (process.env.DEBUG && !sandboxCommand) {
nodeArgs.push(join(root, 'packages', 'cli'));
nodeArgs.push(...process.argv.slice(2));
const env = {
...process.env,
CLI_VERSION: pkg.version,
DEV: 'true',
// The entry a `qwen …` subprocess should call to reach THIS build. This
// launcher runs `node packages/cli` directly — no bin wrapper in the chain, so
// nothing else publishes it, and a /review run from `npm start` would fall
// back to whatever `qwen` PATH resolves to. Assignment, not `||=`, for the
// same reason as scripts/dev.js: an inherited value is another session's CLI.
QWEN_CODE_CLI: fileURLToPath(import.meta.url),
};
if (process.env.DEBUG) {
// If this is not set, the debugger will pause on the outer process rather
// than the relaunched process making it harder to debug.
@@ -87,6 +95,12 @@ const child = spawn('node', nodeArgs, {
});
child.on('close', (code, signal) => {
try {
rmSync(startupWarningsFile, { force: true });
} catch {
// The checker or startup warnings consumer may already have removed it.
}
// Same contract as scripts/dev.js: this launcher is a QWEN_CODE_CLI entry, and
// a signal-killed child (`code === null`) must not exit 0 — `process.exit(null)`
// coerces to success. Re-raise the signal; fall back to a non-zero exit.
+134 -16
View File
@@ -6,12 +6,75 @@
import { describe, expect, it } from 'vitest';
import { execFile } from 'node:child_process';
import {
existsSync,
mkdtempSync,
mkdirSync,
readFileSync,
rmSync,
statSync,
utimesSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
describe('scripts/check-build-status.js', () => {
function createBuildFixture(cwd, { sourceIsNewer }) {
const cliDir = join(cwd, 'packages', 'cli');
const distDir = join(cliDir, 'dist');
const sourceDir = join(cliDir, 'src');
const buildTimestamp = join(distDir, '.last_build');
const sourceFile = join(sourceDir, 'fixture.ts');
const watchedFiles = [
join(cliDir, 'package.json'),
join(cliDir, 'tsconfig.json'),
sourceFile,
];
mkdirSync(distDir, { recursive: true });
mkdirSync(sourceDir, { recursive: true });
writeFileSync(watchedFiles[0], '{}');
writeFileSync(watchedFiles[1], '{}');
writeFileSync(sourceFile, 'export const fixture = true;');
writeFileSync(buildTimestamp, '');
const buildMtime = new Date(Date.now() - 1_000);
const oldMtime = new Date(buildMtime.getTime() - 1_000);
const sourceMtime = sourceIsNewer
? new Date(buildMtime.getTime() + 1_000)
: oldMtime;
utimesSync(buildTimestamp, buildMtime, buildMtime);
utimesSync(watchedFiles[0], oldMtime, oldMtime);
utimesSync(watchedFiles[1], oldMtime, oldMtime);
utimesSync(sourceFile, sourceMtime, sourceMtime);
return { warningsFile: join(cwd, 'warnings.txt') };
}
function runChecker(cwd, env = process.env) {
return new Promise((resolve, reject) => {
execFile(
process.execPath,
[join(root, 'scripts', 'check-build-status.js')],
{ cwd, env },
(err, stdout, stderr) => {
// The checker may exit non-zero without a spawn failure; the contract
// under test is the stream, not the verdict. A SPAWN failure must
// reject: execFile still hands back an empty-string stdout there, so
// resolving would let the empty-stdout assertion pass green on a run
// that never happened. Spawn-level errors (a missing node binary)
// carry a string code; process exit codes are numbers.
if (err && typeof err.code === 'string') reject(err);
else resolve({ stdout, stderr });
},
);
});
}
it('writes nothing to stdout — start.js runs it in front of piped review JSON', async () => {
// `scripts/start.js` executes this checker with `stdio: 'inherit'` before
// every spawn, and start.js is a QWEN_CODE_CLI entry whose stdout callers
@@ -19,23 +82,78 @@ describe('scripts/check-build-status.js', () => {
// whose first line is JSON. One `console.log` here — the shape this pins
// against — puts "Checking build status..." at the top of that file. Status
// and warnings belong on stderr, whatever build state the checker finds.
const { stdout } = await new Promise((resolve, reject) => {
execFile(
process.execPath,
[join(root, 'scripts', 'check-build-status.js')],
{ cwd: root },
(err, stdout, stderr) => {
// The checker may exit non-zero on an unbuilt tree; the contract under
// test is the stream, not the verdict. But a SPAWN failure (ENOENT —
// the script renamed or moved) must reject: execFile still hands back
// an empty-string stdout there, so the old stdout-type guard resolved
// and the empty-stdout assertion passed green on a script that never
// ran. Spawn-level errors carry a string code; exit codes are numbers.
if (err && typeof err.code === 'string') reject(err);
else resolve({ stdout, stderr });
},
);
const { stdout } = await runChecker(root, {
...process.env,
QWEN_CODE_WARNINGS_FILE: '',
});
expect(stdout).toBe('');
});
it('writes missing-build warnings to the configured file', async () => {
const cwd = mkdtempSync(join(tmpdir(), 'qwen-check-build-'));
const warningsFile = join(cwd, 'warnings.txt');
try {
await runChecker(cwd, {
...process.env,
QWEN_CODE_WARNINGS_FILE: warningsFile,
});
expect(readFileSync(warningsFile, 'utf8')).toContain(
'Build timestamp file',
);
if (process.platform !== 'win32') {
expect(statSync(warningsFile).mode & 0o777).toBe(0o600);
}
} finally {
rmSync(cwd, { recursive: true, force: true });
}
});
it('does not create a warnings file when the variable is unset', async () => {
const cwd = mkdtempSync(join(tmpdir(), 'qwen-check-build-'));
const warningsFile = join(cwd, 'warnings.txt');
const env = { ...process.env, TMPDIR: cwd, TMP: cwd, TEMP: cwd };
delete env.QWEN_CODE_WARNINGS_FILE;
try {
await runChecker(cwd, env);
expect(() => readFileSync(warningsFile)).toThrow();
expect(existsSync(join(cwd, 'qwen-code-warnings.txt'))).toBe(false);
} finally {
rmSync(cwd, { recursive: true, force: true });
}
});
it('deletes a stale warnings file when the build is up to date', async () => {
const cwd = mkdtempSync(join(tmpdir(), 'qwen-check-build-'));
const { warningsFile } = createBuildFixture(cwd, {
sourceIsNewer: false,
});
writeFileSync(warningsFile, 'stale warning');
try {
await runChecker(cwd, {
...process.env,
QWEN_CODE_WARNINGS_FILE: warningsFile,
});
expect(existsSync(warningsFile)).toBe(false);
} finally {
rmSync(cwd, { recursive: true, force: true });
}
});
it('writes source-newer warnings to the configured file', async () => {
const cwd = mkdtempSync(join(tmpdir(), 'qwen-check-build-'));
const { warningsFile } = createBuildFixture(cwd, {
sourceIsNewer: true,
});
try {
await runChecker(cwd, {
...process.env,
QWEN_CODE_WARNINGS_FILE: warningsFile,
});
expect(readFileSync(warningsFile, 'utf8')).toContain(
'modified since the last build',
);
} finally {
rmSync(cwd, { recursive: true, force: true });
}
});
});
+53 -1
View File
@@ -5,10 +5,13 @@
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { dirname, isAbsolute } from 'node:path';
import { tmpdir } from 'node:os';
const { spawnMock, execSyncMock } = vi.hoisted(() => ({
const { spawnMock, execSyncMock, rmSyncMock } = vi.hoisted(() => ({
spawnMock: vi.fn(() => ({ on: vi.fn() })),
execSyncMock: vi.fn(() => ''),
rmSyncMock: vi.fn(),
}));
vi.mock('node:child_process', () => ({
@@ -18,6 +21,7 @@ vi.mock('node:child_process', () => ({
vi.mock('node:fs', () => ({
readFileSync: vi.fn(() => JSON.stringify({ version: '0.0.0-test' })),
rmSync: rmSyncMock,
}));
const normalizePath = (path) => String(path).replaceAll('\\', '/');
@@ -76,4 +80,52 @@ describe('scripts/start.js launcher', () => {
else process.env.QWEN_CODE_CLI = inherited;
}
});
it('passes one scoped warnings file to the checker and child process', async () => {
await import('../start.js?scopes-startup-warnings');
const [, checkerOptions] = execSyncMock.mock.calls[0];
const [, , childOptions] = spawnMock.mock.calls[0];
const checkerPath = checkerOptions.env.QWEN_CODE_WARNINGS_FILE;
expect(checkerPath).toBe(childOptions.env.QWEN_CODE_WARNINGS_FILE);
expect(isAbsolute(checkerPath)).toBe(true);
expect(dirname(checkerPath)).toBe(tmpdir());
expect(normalizePath(checkerPath)).toMatch(
/qwen-code-warnings-\d+-[0-9a-f-]+\.txt$/,
);
await import('../start.js?scopes-startup-warnings-second');
const checkerCalls = execSyncMock.mock.calls.filter(
([command]) => command === 'node ./scripts/check-build-status.js',
);
expect(checkerCalls).toHaveLength(2);
expect(checkerCalls[1][1].env.QWEN_CODE_WARNINGS_FILE).not.toBe(
checkerPath,
);
});
it('cleans up the scoped warnings file when the child closes', async () => {
await import('../start.js?cleans-startup-warnings');
const child = spawnMock.mock.results[0].value;
const close = child.on.mock.calls.find(([ev]) => ev === 'close')[1];
const [, checkerOptions] = execSyncMock.mock.calls[0];
const warningsPath = checkerOptions.env.QWEN_CODE_WARNINGS_FILE;
const exitSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined);
try {
rmSyncMock.mockImplementationOnce(() => {
throw new Error('EBUSY');
});
expect(rmSyncMock).not.toHaveBeenCalled();
close(0, null);
expect(rmSyncMock).toHaveBeenCalledWith(warningsPath, { force: true });
expect(exitSpy).toHaveBeenCalledWith(0);
} finally {
exitSpy.mockRestore();
}
});
});