qwen-code/scripts/tests/cli-entry.test.js
jinye 2b6de671d1
feat(managed-agent): Add SSH Tool Runtime support (#14318)
* feat(managed-agent): add SSH Tool Runtime support

Add administrator-configured SSH placement, remote Workspace registration,
Session-isolated workers and strict retirement over the existing HTTP protocol.
Keep new admission disabled by default and preserve original-worker identity.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(managed-agent): clean up SSH runtime transport lifecycle

Keep bootstrap work outside the endpoint lock and release unused forwards and stopped direct routes without disconnecting durable observers. Bound control work and complete queued futures during shutdown.

Align SSH registration with the shared SQL clock, reuse storage identity and dependencies, and fix the environment inventory and transcript budget. Normalize inherited CSI test formatting for Java verification.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(managed-agent): align lifecycle and timestamp test fixtures

Use canonical SQL timestamp columns and database milliseconds in fault-gate evidence, and require parent close to cancel a held child Turn as current main specifies. Preserve production behavior and capability boundaries.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(managed-agent): Bind environment query IDs as binary

Preserve canonical Session and Turn identity types when H2 recompiles the
latest environment query. Force recompilation in the existing regression
without changing its admission-order or environment assertions.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-10-16 18:18:04 +00:00

651 lines
25 KiB
JavaScript

/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
const {
spawnSyncMock,
existsSyncMock,
homedirMock,
tmpdirMock,
enableCompileCacheMock,
} = vi.hoisted(() => ({
spawnSyncMock: vi.fn(() => ({ status: 0, signal: null })),
existsSyncMock: vi.fn(() => false),
homedirMock: vi.fn(() => '/home/test-user'),
tmpdirMock: vi.fn(() => '/tmp'),
enableCompileCacheMock: vi.fn(() => ({
status: 1,
directory: '/tmp/node-compile-cache',
})),
}));
vi.mock('node:child_process', () => ({
spawnSync: spawnSyncMock,
}));
vi.mock('node:fs', async (importOriginal) => ({
...(await importOriginal()),
existsSync: existsSyncMock,
realpathSync: vi.fn((p) => p),
readFileSync: vi.fn(() => JSON.stringify({ version: '0.0.0-test' })),
}));
vi.mock('node:os', async (importOriginal) => ({
...(await importOriginal()),
homedir: homedirMock,
tmpdir: tmpdirMock,
}));
// Mocked so the launcher never enables a real compile cache in the test worker.
vi.mock('node:module', () => ({
default: {
enableCompileCache: enableCompileCacheMock,
constants: { compileCacheStatus: { ENABLED: 1, ALREADY_ENABLED: 2 } },
},
}));
const normalizePath = (path) => String(path).replaceAll('\\', '/');
describe('scripts/cli-entry.js production entry', () => {
const originalArgv = process.argv;
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
let exitSpy;
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
homedirMock.mockReturnValue('/home/test-user');
tmpdirMock.mockReturnValue('/tmp');
// Every import stamps these into the real process.env, and the pin's
// bootstrap guard cannot tell two imports of this file apart — Vitest drops
// the cache-buster query from import.meta.url — so a leftover pin would be
// honoured as an inherited managed-version pin and its updateRoot would win
// over the home this test is trying to observe. Each test here drives a
// top-level invocation, which starts with neither.
delete process.env.QWEN_CODE_MANAGED_NPM_PIN;
delete process.env.QWEN_CODE_MANAGED_NPM_ROOT;
// A non-fast-path command on Windows, so the entry takes the spawnSync
// branch (mocked) instead of importing the real dist/cli.js in-process.
process.argv = ['node', 'scripts/cli-entry.js', 'review', 'check'];
Object.defineProperty(process, 'platform', { value: 'win32' });
// The entry exits after its child returns; the import must survive that.
exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined);
});
afterEach(() => {
process.argv = originalArgv;
Object.defineProperty(process, 'platform', originalPlatform);
exitSpy.mockRestore();
});
it('stamps QWEN_CODE_CLI with its own path, overriding an inherited one', async () => {
// The dev and start launchers had this pin; the production entry — the one
// every npm install actually runs — did not, so a regression back to
// honouring an inherited value would route installed review subprocesses to
// an outer or stale CLI with every test green.
const inherited = process.env.QWEN_CODE_CLI;
process.env.QWEN_CODE_CLI = '/somewhere/else/entirely/qwen';
try {
await import('../cli-entry.js?stamps-own-cli');
expect(normalizePath(process.env.QWEN_CODE_CLI)).toMatch(
/scripts\/cli-entry\.js$/,
);
} finally {
if (inherited === undefined) delete process.env.QWEN_CODE_CLI;
else process.env.QWEN_CODE_CLI = inherited;
}
});
it('preserves the startup version in child review commands', async () => {
const inherited = process.env.QWEN_CODE_STARTUP_VERSION;
process.env.QWEN_CODE_STARTUP_VERSION = '0.21.3';
try {
await import('../cli-entry.js?stamps-version');
expect(process.env.QWEN_CODE_STARTUP_VERSION).toBe('0.21.3');
// And the value rides the spawned child's env — the hop that actually
// reaches the submit handler — not merely the parent's copy.
const spawnEnv = spawnSyncMock.mock.calls.at(-1)?.[2]?.env;
expect(spawnEnv.QWEN_CODE_STARTUP_VERSION).toBe('0.21.3');
} finally {
if (inherited === undefined) delete process.env.QWEN_CODE_STARTUP_VERSION;
else process.env.QWEN_CODE_STARTUP_VERSION = inherited;
}
});
it('initializes the version for a fresh CLI process', async () => {
const inherited = process.env.QWEN_CODE_STARTUP_VERSION;
delete process.env.QWEN_CODE_STARTUP_VERSION;
try {
await import('../cli-entry.js?initializes-version');
expect(process.env.QWEN_CODE_STARTUP_VERSION).toBe('0.0.0-test');
} finally {
if (inherited === undefined) delete process.env.QWEN_CODE_STARTUP_VERSION;
else process.env.QWEN_CODE_STARTUP_VERSION = inherited;
}
});
it('clears the startup version before a managed-update relaunch', async () => {
// Behavioural, not source-text: the update child exits 44, and the
// relaunch through the launcher must NOT inherit the old session's
// stamp, so the new build stamps its own version.
const inherited = process.env.QWEN_CODE_STARTUP_VERSION;
const inheritedShim = process.env.QWEN_CODE_LAUNCHER_PATH;
process.env.QWEN_CODE_STARTUP_VERSION = '0.21.3';
process.env.QWEN_CODE_LAUNCHER_PATH = '/opt/qwen-standalone/bin/qwen';
existsSyncMock.mockImplementation(
(p) => normalizePath(p) === '/opt/qwen-standalone/bin/qwen',
);
// Snapshot the env AT call time: the mock records the object by
// reference, so asserting on it later would let a delete that happens
// AFTER the spawn mutate the record and hide the regression.
const spawnEnvs = [];
const spawnImpl = spawnSyncMock.getMockImplementation();
spawnSyncMock.mockImplementation((_cmd, _args, opts) => {
spawnEnvs.push({ ...opts.env });
return spawnEnvs.length === 1
? { status: 44, signal: null }
: { status: 0, signal: null };
});
try {
await import('../cli-entry.js?clears-version-on-relaunch');
expect(spawnEnvs).toHaveLength(2);
// The pre-update child inherits the session's stamp...
expect(spawnEnvs[0].QWEN_CODE_STARTUP_VERSION).toBe('0.21.3');
// ...and the post-update relaunch does not.
expect('QWEN_CODE_STARTUP_VERSION' in spawnEnvs[1]).toBe(false);
} finally {
spawnSyncMock.mockImplementation(spawnImpl);
existsSyncMock.mockImplementation(() => false);
if (inherited === undefined) delete process.env.QWEN_CODE_STARTUP_VERSION;
else process.env.QWEN_CODE_STARTUP_VERSION = inherited;
if (inheritedShim === undefined)
delete process.env.QWEN_CODE_LAUNCHER_PATH;
else process.env.QWEN_CODE_LAUNCHER_PATH = inheritedShim;
}
});
it('hands the post-update relaunch command line to cmd.exe verbatim', async () => {
// #12687: the ""…"" idiom only reaches cmd intact when the spawn passes
// windowsVerbatimArguments — otherwise Node's MSVCRT escaping rewrites
// every embedded quote as \" and cmd /s rule 2 strips the line down to
// a literal \"\"path\"\" program name, which is exactly the reporter's
// error. The standalone Windows shim stamps an un-normalized
// bin\..\bin launcher path, so the fixture carries one.
const inheritedShim = process.env.QWEN_CODE_LAUNCHER_PATH;
const launcher =
'C:\\Users\\test\\AppData\\Local\\qwen-code\\qwen-code\\bin\\..\\bin\\qwen.cmd';
process.env.QWEN_CODE_LAUNCHER_PATH = launcher;
existsSyncMock.mockImplementation((p) => p === launcher);
let spawnCount = 0;
const spawnImpl = spawnSyncMock.getMockImplementation();
spawnSyncMock.mockImplementation(() => {
spawnCount += 1;
return spawnCount === 1
? { status: 44, signal: null }
: { status: 0, signal: null };
});
try {
await import('../cli-entry.js?verbatim-cmd-relaunch');
// First spawn is the managed-update child (exit 44), second is the
// relaunch through the standalone shim. The interpreter mirrors the
// production `process.env['ComSpec'] ?? 'cmd.exe'` so the assertion
// also holds on a real Windows host, where ComSpec is always set.
expect(spawnSyncMock).toHaveBeenCalledTimes(2);
expect(spawnSyncMock).toHaveBeenLastCalledWith(
process.env['ComSpec'] ?? 'cmd.exe',
['/d', '/s', '/c', `""${launcher}""`],
expect.objectContaining({
stdio: 'inherit',
windowsVerbatimArguments: true,
}),
);
} finally {
spawnSyncMock.mockImplementation(spawnImpl);
existsSyncMock.mockImplementation(() => false);
if (inheritedShim === undefined)
delete process.env.QWEN_CODE_LAUNCHER_PATH;
else process.env.QWEN_CODE_LAUNCHER_PATH = inheritedShim;
}
});
it('refuses a verbatim cmd relaunch when the launcher has cmd metacharacters', async () => {
// windowsVerbatimArguments drops Node's escaping safety net, so a
// launcher path containing cmd metacharacters would be re-tokenized
// into extra commands. The update itself already landed at that point,
// so the entry must skip the relaunch instead of spawning a malformed
// command line.
const inheritedShim = process.env.QWEN_CODE_LAUNCHER_PATH;
const launcher = 'C:\\evil&whoami\\bin\\qwen.cmd';
process.env.QWEN_CODE_LAUNCHER_PATH = launcher;
existsSyncMock.mockImplementation((p) => p === launcher);
let spawnCount = 0;
const spawnImpl = spawnSyncMock.getMockImplementation();
spawnSyncMock.mockImplementation(() => {
spawnCount += 1;
return spawnCount === 1
? { status: 44, signal: null }
: { status: 0, signal: null };
});
const stderrSpy = vi
.spyOn(process.stderr, 'write')
.mockImplementation(() => true);
try {
await import('../cli-entry.js?unsafe-cmd-launcher');
// Only the managed-update child ran; no cmd.exe relaunch followed.
expect(spawnSyncMock).toHaveBeenCalledTimes(1);
expect(stderrSpy).toHaveBeenCalledWith(
expect.stringContaining('next run'),
);
expect(exitSpy).toHaveBeenCalledWith(0);
} finally {
stderrSpy.mockRestore();
spawnSyncMock.mockImplementation(spawnImpl);
existsSyncMock.mockImplementation(() => false);
if (inheritedShim === undefined)
delete process.env.QWEN_CODE_LAUNCHER_PATH;
else process.env.QWEN_CODE_LAUNCHER_PATH = inheritedShim;
}
});
it('reports a spawn failure of the CLI child instead of exiting silently', async () => {
// #13076: a child that never starts comes back as
// { status: null, signal: null, error }, which used to collapse into a
// bare process.exit(1) with nothing on the console.
const spawnImpl = spawnSyncMock.getMockImplementation();
spawnSyncMock.mockImplementation(() => ({
status: null,
signal: null,
error: Object.assign(new Error('spawnSync node.exe EACCES'), {
code: 'EACCES',
errno: -4092,
syscall: 'spawnSync node.exe',
}),
}));
const stderrSpy = vi
.spyOn(process.stderr, 'write')
.mockImplementation(() => true);
try {
await import('../cli-entry.js?spawn-error');
const output = stderrSpy.mock.calls.map(([chunk]) => chunk).join('');
expect(output).toContain('EACCES');
expect(output).toContain('-4092');
// The message already carries the code and syscall: not repeated.
expect(output.match(/EACCES/g)).toHaveLength(1);
expect(output.match(/spawnSync node\.exe/g)).toHaveLength(1);
expect(output).toContain('errno -4092');
expect(output).not.toContain('next run');
expect(output).toContain(process.execPath);
expect(output).toContain('cli.js');
expect(exitSpy).toHaveBeenCalledTimes(1);
const exitCode = exitSpy.mock.calls[0][0];
// Distinguishable from a CLI that itself exits 1, and from the
// managed-update code 44 that triggers a relaunch.
expect(exitCode).toBe(126);
} finally {
stderrSpy.mockRestore();
spawnSyncMock.mockImplementation(spawnImpl);
}
});
it('keeps the errno when the path happens to contain its digits', async () => {
const spawnImpl = spawnSyncMock.getMockImplementation();
spawnSyncMock.mockImplementation(() => ({
status: null,
signal: null,
error: Object.assign(
new Error('spawnSync /srv/app-2/dist/cli.js ENOENT'),
{
code: 'ENOENT',
errno: -2,
syscall: 'spawnSync /srv/app-2/dist/cli.js',
},
),
}));
const stderrSpy = vi
.spyOn(process.stderr, 'write')
.mockImplementation(() => true);
try {
await import('../cli-entry.js?errno-in-path');
const output = stderrSpy.mock.calls.map(([chunk]) => chunk).join('');
expect(output).toContain('(errno -2)');
expect(output.match(/ENOENT/g)).toHaveLength(1);
} finally {
stderrSpy.mockRestore();
spawnSyncMock.mockImplementation(spawnImpl);
}
});
it('prints no empty detail for a partial spawn error', async () => {
const spawnImpl = spawnSyncMock.getMockImplementation();
spawnSyncMock.mockImplementation(() => ({
status: null,
signal: null,
error: new Error('spawn failed'),
}));
const stderrSpy = vi
.spyOn(process.stderr, 'write')
.mockImplementation(() => true);
try {
await import('../cli-entry.js?partial-error');
const output = stderrSpy.mock.calls.map(([chunk]) => chunk).join('');
expect(output).toContain('spawn failed');
expect(output).not.toMatch(/\(\s*\)/);
expect(output).not.toContain('undefined');
} finally {
stderrSpy.mockRestore();
spawnSyncMock.mockImplementation(spawnImpl);
}
});
it('reports a spawn failure of the post-update relaunch', async () => {
const inheritedShim = process.env.QWEN_CODE_LAUNCHER_PATH;
const launcher =
'C:\\Users\\test\\AppData\\Local\\qwen-code\\bin\\qwen.cmd';
process.env.QWEN_CODE_LAUNCHER_PATH = launcher;
existsSyncMock.mockImplementation((p) => p === launcher);
let spawnCount = 0;
const spawnImpl = spawnSyncMock.getMockImplementation();
spawnSyncMock.mockImplementation(() => {
spawnCount += 1;
return spawnCount === 1
? { status: 44, signal: null }
: {
status: null,
signal: null,
error: Object.assign(new Error('spawnSync cmd.exe ENOENT'), {
code: 'ENOENT',
errno: -4058,
syscall: 'spawnSync cmd.exe',
}),
};
});
const stderrSpy = vi
.spyOn(process.stderr, 'write')
.mockImplementation(() => true);
try {
await import('../cli-entry.js?relaunch-spawn-error');
expect(spawnSyncMock).toHaveBeenCalledTimes(2);
const output = stderrSpy.mock.calls.map(([chunk]) => chunk).join('');
expect(output).toContain('ENOENT');
expect(output).toContain(launcher);
expect(output).toContain('next run');
expect(exitSpy).toHaveBeenCalledTimes(1);
const exitCode = exitSpy.mock.calls[0][0];
expect(exitCode).toBe(126);
} finally {
stderrSpy.mockRestore();
spawnSyncMock.mockImplementation(spawnImpl);
existsSyncMock.mockImplementation(() => false);
if (inheritedShim === undefined)
delete process.env.QWEN_CODE_LAUNCHER_PATH;
else process.env.QWEN_CODE_LAUNCHER_PATH = inheritedShim;
}
});
it('leaves the startup version unset when package metadata is unreadable', async () => {
const inherited = process.env.QWEN_CODE_STARTUP_VERSION;
delete process.env.QWEN_CODE_STARTUP_VERSION;
const readFileSyncMock = vi.mocked(readFileSync);
const impl = readFileSyncMock.getMockImplementation();
readFileSyncMock.mockImplementation(() => {
throw new Error('unreadable');
});
try {
await import('../cli-entry.js?unreadable-metadata');
expect(process.env.QWEN_CODE_STARTUP_VERSION).toBeUndefined();
} finally {
readFileSyncMock.mockImplementation(impl);
if (inherited === undefined) delete process.env.QWEN_CODE_STARTUP_VERSION;
else process.env.QWEN_CODE_STARTUP_VERSION = inherited;
}
});
it('stamps unknown when the package metadata has no version', async () => {
const inherited = process.env.QWEN_CODE_STARTUP_VERSION;
delete process.env.QWEN_CODE_STARTUP_VERSION;
const readFileSyncMock = vi.mocked(readFileSync);
const impl = readFileSyncMock.getMockImplementation();
readFileSyncMock.mockImplementation(() => '{}');
try {
await import('../cli-entry.js?versionless-metadata');
expect(process.env.QWEN_CODE_STARTUP_VERSION).toBe('unknown');
} finally {
readFileSyncMock.mockImplementation(impl);
if (inherited === undefined) delete process.env.QWEN_CODE_STARTUP_VERSION;
else process.env.QWEN_CODE_STARTUP_VERSION = inherited;
}
});
it('prefers the standalone launcher shim, which carries the bundled Node', async () => {
// The standalone package launches this file through `bin/qwen`, a shim that
// selects the BUNDLED Node — the host may have none — and announces itself
// via QWEN_CODE_LAUNCHER_PATH. There, stamping this file would hand every
// subprocess a `#!/usr/bin/env node` script on a machine where that resolves
// to nothing. The shim is the entry that reaches this build; stamp it.
const inheritedCli = process.env.QWEN_CODE_CLI;
const inheritedShim = process.env.QWEN_CODE_LAUNCHER_PATH;
process.env.QWEN_CODE_LAUNCHER_PATH = '/opt/qwen-standalone/bin/qwen';
delete process.env.QWEN_CODE_CLI;
existsSyncMock.mockImplementation(
(p) => normalizePath(p) === '/opt/qwen-standalone/bin/qwen',
);
try {
await import('../cli-entry.js?stamps-shim');
expect(process.env.QWEN_CODE_CLI).toBe('/opt/qwen-standalone/bin/qwen');
// And the hint is CONSUMED, not leaked: the serve/mcp fast path never
// reaches the spawn branch that used to delete it, and a child qwen from
// a different checkout would read the leftover shim and republish it as
// its own entry — the wrong build, wearing this one's stamp.
expect('QWEN_CODE_LAUNCHER_PATH' in process.env).toBe(false);
} finally {
if (inheritedCli === undefined) delete process.env.QWEN_CODE_CLI;
else process.env.QWEN_CODE_CLI = inheritedCli;
if (inheritedShim === undefined)
delete process.env.QWEN_CODE_LAUNCHER_PATH;
else process.env.QWEN_CODE_LAUNCHER_PATH = inheritedShim;
}
});
it('hands the compile cache to the spawned CLI', async () => {
// Without it every spawned run — one-shot `-p` included — recompiles the
// whole bundle; only the in-process fast paths used to enable the cache.
const inherited = process.env.NODE_COMPILE_CACHE;
delete process.env.NODE_COMPILE_CACHE;
try {
await import('../cli-entry.js?compile-cache');
const spawnEnv = spawnSyncMock.mock.calls.at(-1)?.[2]?.env;
expect(spawnEnv.NODE_COMPILE_CACHE).toBe('/tmp/node-compile-cache');
} finally {
if (inherited === undefined) delete process.env.NODE_COMPILE_CACHE;
else process.env.NODE_COMPILE_CACHE = inherited;
}
});
it('keeps an inherited compile cache and a disabled one', async () => {
const inherited = process.env.NODE_COMPILE_CACHE;
process.env.NODE_COMPILE_CACHE = '/custom/cache';
try {
await import('../cli-entry.js?inherited-compile-cache');
expect(spawnSyncMock.mock.calls.at(-1)?.[2]?.env.NODE_COMPILE_CACHE).toBe(
'/custom/cache',
);
delete process.env.NODE_COMPILE_CACHE;
// NODE_DISABLE_COMPILE_CACHE=1 makes Node report the cache as disabled.
enableCompileCacheMock.mockReturnValueOnce({ status: 3 });
vi.resetModules();
await import('../cli-entry.js?disabled-compile-cache');
expect(
'NODE_COMPILE_CACHE' in spawnSyncMock.mock.calls.at(-1)[2].env,
).toBe(false);
} finally {
if (inherited === undefined) delete process.env.NODE_COMPILE_CACHE;
else process.env.NODE_COMPILE_CACHE = inherited;
}
});
it('falls back to tmpdir for tilde QWEN_HOME when homedir is unavailable', async () => {
const inheritedHome = process.env.QWEN_HOME;
homedirMock.mockImplementation(() => {
throw new Error('homedir unavailable');
});
process.env.QWEN_HOME = '~';
try {
await import('../cli-entry.js?tilde-home-fallback');
expect(normalizePath(process.env.QWEN_CODE_MANAGED_NPM_ROOT)).toBe(
'/tmp/updates/npm',
);
} finally {
if (inheritedHome === undefined) delete process.env.QWEN_HOME;
else process.env.QWEN_HOME = inheritedHome;
}
});
describe('outside Windows', () => {
const cliPath = fileURLToPath(new URL('../cli.js', import.meta.url));
const inheritedCompileCache = process.env.NODE_COMPILE_CACHE;
beforeEach(() => {
Object.defineProperty(process, 'platform', { value: 'linux' });
vi.doMock(cliPath, () => ({}));
delete process.env.NODE_COMPILE_CACHE;
});
afterEach(() => {
vi.doUnmock(cliPath);
if (inheritedCompileCache === undefined) {
delete process.env.NODE_COMPILE_CACHE;
} else {
process.env.NODE_COMPILE_CACHE = inheritedCompileCache;
}
});
it('runs the CLI in this process with gc exposed', async () => {
await import('../cli-entry.js?in-process');
expect(spawnSyncMock).not.toHaveBeenCalled();
expect(process.argv.slice(1)).toEqual([cliPath, 'review', 'check']);
expect(typeof globalThis.gc).toBe('function');
// Supervised relaunches and tool subprocesses inherit it from here.
expect(process.env.NODE_COMPILE_CACHE).toBe('/tmp/node-compile-cache');
});
it.each([
['managed-runtime-bootstrap'],
['managed-runtime-bootstrap-worker', '/private/runtime/resource'],
])(
'keeps pinned %s commands in this process and skips active updates',
async (...args) => {
process.argv = ['node', 'scripts/cli-entry.js', ...args];
await import('../cli-entry.js?pinned-runtime');
expect(spawnSyncMock).not.toHaveBeenCalled();
expect(process.argv.slice(1)).toEqual([cliPath, ...args]);
expect(
readFileSync.mock.calls.some(([file]) =>
normalizePath(file).endsWith('/active.json'),
),
).toBe(false);
expect(
JSON.parse(process.env.QWEN_CODE_MANAGED_NPM_PIN).version,
).toBeNull();
},
);
it('keeps the spawned child with --expose-gc under Bun', async () => {
Object.defineProperty(process.versions, 'bun', {
value: '1.3.14',
configurable: true,
});
try {
await import('../cli-entry.js?bun');
expect(spawnSyncMock).toHaveBeenCalledWith(
process.execPath,
['--expose-gc', expect.stringMatching(/cli\.js$/), 'review', 'check'],
expect.anything(),
);
} finally {
delete process.versions.bun;
}
});
it('relaunches through the launcher when the CLI exits after an update', async () => {
const exitListeners = process.listeners('exit');
process.env.QWEN_CODE_LAUNCHER_PATH = '/opt/qwen-standalone/bin/qwen';
existsSyncMock.mockImplementation(
(p) => normalizePath(p) === '/opt/qwen-standalone/bin/qwen',
);
try {
await import('../cli-entry.js?in-process-update');
const hook = process
.listeners('exit')
.find((l) => !exitListeners.includes(l));
hook(0);
expect(spawnSyncMock).not.toHaveBeenCalled();
hook(44);
expect(spawnSyncMock).toHaveBeenCalledWith(
'/opt/qwen-standalone/bin/qwen',
[],
expect.objectContaining({
env: expect.objectContaining({
QWEN_CODE_RELAUNCH_ARGS: JSON.stringify(['review', 'check']),
}),
}),
);
process.removeListener('exit', hook);
} finally {
delete process.env.QWEN_CODE_LAUNCHER_PATH;
}
});
it('reports a failed relaunch from the exit hook and exits 126', async () => {
const exitListeners = process.listeners('exit');
process.env.QWEN_CODE_LAUNCHER_PATH = '/opt/qwen-standalone/bin/qwen';
existsSyncMock.mockImplementation(
(p) => normalizePath(p) === '/opt/qwen-standalone/bin/qwen',
);
const spawnImpl = spawnSyncMock.getMockImplementation();
spawnSyncMock.mockImplementation(() => ({
status: null,
signal: null,
error: Object.assign(
new Error('spawnSync /opt/qwen-standalone/bin/qwen ENOENT'),
{
code: 'ENOENT',
errno: -2,
syscall: 'spawnSync /opt/qwen-standalone/bin/qwen',
},
),
}));
const stderrSpy = vi
.spyOn(process.stderr, 'write')
.mockImplementation(() => true);
let hook;
try {
await import('../cli-entry.js?in-process-relaunch-error');
hook = process
.listeners('exit')
.find((l) => !exitListeners.includes(l));
hook(44);
const output = stderrSpy.mock.calls.map(([chunk]) => chunk).join('');
expect(output).toContain('ENOENT');
expect(output).toContain('next run');
expect(exitSpy).toHaveBeenCalledWith(126);
} finally {
if (hook) process.removeListener('exit', hook);
stderrSpy.mockRestore();
spawnSyncMock.mockImplementation(spawnImpl);
delete process.env.QWEN_CODE_LAUNCHER_PATH;
}
});
});
});