mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 17:27:54 +00:00
346 lines
9.9 KiB
TypeScript
346 lines
9.9 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import {
|
|
vi,
|
|
describe,
|
|
it,
|
|
expect,
|
|
beforeEach,
|
|
afterEach,
|
|
type MockInstance,
|
|
} from 'vitest';
|
|
import { EventEmitter } from 'node:events';
|
|
import { RELAUNCH_EXIT_CODE } from './processUtils.js';
|
|
import type { ChildProcess } from 'node:child_process';
|
|
import { spawn } from 'node:child_process';
|
|
|
|
vi.mock('node:child_process', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('node:child_process')>();
|
|
return {
|
|
...actual,
|
|
spawn: vi.fn(),
|
|
};
|
|
});
|
|
|
|
const mockedSpawn = vi.mocked(spawn);
|
|
|
|
// Import the functions initially
|
|
import { relaunchAppInChildProcess, relaunchOnExitCode } from './relaunch.js';
|
|
|
|
describe('relaunchOnExitCode', () => {
|
|
let processExitSpy: MockInstance;
|
|
let consoleErrorSpy: MockInstance;
|
|
let stdinResumeSpy: MockInstance;
|
|
|
|
beforeEach(() => {
|
|
processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
|
|
throw new Error('PROCESS_EXIT_CALLED');
|
|
});
|
|
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
stdinResumeSpy = vi
|
|
.spyOn(process.stdin, 'resume')
|
|
.mockImplementation(() => process.stdin);
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
processExitSpy.mockRestore();
|
|
consoleErrorSpy.mockRestore();
|
|
stdinResumeSpy.mockRestore();
|
|
});
|
|
|
|
it('should exit with non-RELAUNCH_EXIT_CODE', async () => {
|
|
const runner = vi.fn().mockResolvedValue(0);
|
|
|
|
await expect(relaunchOnExitCode(runner)).rejects.toThrow(
|
|
'PROCESS_EXIT_CALLED',
|
|
);
|
|
|
|
expect(runner).toHaveBeenCalledTimes(1);
|
|
expect(processExitSpy).toHaveBeenCalledWith(0);
|
|
});
|
|
|
|
it('should continue running when RELAUNCH_EXIT_CODE is returned', async () => {
|
|
let callCount = 0;
|
|
const runner = vi.fn().mockImplementation(async () => {
|
|
callCount++;
|
|
if (callCount === 1) return RELAUNCH_EXIT_CODE;
|
|
if (callCount === 2) return RELAUNCH_EXIT_CODE;
|
|
return 0; // Exit on third call
|
|
});
|
|
|
|
await expect(relaunchOnExitCode(runner)).rejects.toThrow(
|
|
'PROCESS_EXIT_CALLED',
|
|
);
|
|
|
|
expect(runner).toHaveBeenCalledTimes(3);
|
|
expect(processExitSpy).toHaveBeenCalledWith(0);
|
|
});
|
|
|
|
it('should handle runner errors', async () => {
|
|
const error = new Error('Runner failed');
|
|
const runner = vi.fn().mockRejectedValue(error);
|
|
|
|
await expect(relaunchOnExitCode(runner)).rejects.toThrow(
|
|
'PROCESS_EXIT_CALLED',
|
|
);
|
|
|
|
expect(runner).toHaveBeenCalledTimes(1);
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
'Fatal error: Failed to relaunch the CLI process.',
|
|
error,
|
|
);
|
|
expect(stdinResumeSpy).toHaveBeenCalled();
|
|
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
});
|
|
});
|
|
|
|
describe('relaunchAppInChildProcess', () => {
|
|
let processExitSpy: MockInstance;
|
|
let consoleErrorSpy: MockInstance;
|
|
let stdinPauseSpy: MockInstance;
|
|
let stdinResumeSpy: MockInstance;
|
|
|
|
// Store original values to restore later
|
|
const originalEnv = { ...process.env };
|
|
const originalExecArgv = [...process.execArgv];
|
|
const originalArgv = [...process.argv];
|
|
const originalExecPath = process.execPath;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
|
|
process.env = { ...originalEnv };
|
|
delete process.env['GEMINI_CLI_NO_RELAUNCH'];
|
|
|
|
process.execArgv = [...originalExecArgv];
|
|
process.argv = [...originalArgv];
|
|
process.execPath = '/usr/bin/node';
|
|
|
|
processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
|
|
throw new Error('PROCESS_EXIT_CALLED');
|
|
});
|
|
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
stdinPauseSpy = vi
|
|
.spyOn(process.stdin, 'pause')
|
|
.mockImplementation(() => process.stdin);
|
|
stdinResumeSpy = vi
|
|
.spyOn(process.stdin, 'resume')
|
|
.mockImplementation(() => process.stdin);
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.env = { ...originalEnv };
|
|
process.execArgv = [...originalExecArgv];
|
|
process.argv = [...originalArgv];
|
|
process.execPath = originalExecPath;
|
|
|
|
processExitSpy.mockRestore();
|
|
consoleErrorSpy.mockRestore();
|
|
stdinPauseSpy.mockRestore();
|
|
stdinResumeSpy.mockRestore();
|
|
});
|
|
|
|
describe('when GEMINI_CLI_NO_RELAUNCH is set', () => {
|
|
it('should return early without spawning a child process', async () => {
|
|
process.env['GEMINI_CLI_NO_RELAUNCH'] = 'true';
|
|
|
|
await relaunchAppInChildProcess(['--test'], ['--verbose']);
|
|
|
|
expect(mockedSpawn).not.toHaveBeenCalled();
|
|
expect(processExitSpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('when GEMINI_CLI_NO_RELAUNCH is not set', () => {
|
|
beforeEach(() => {
|
|
delete process.env['GEMINI_CLI_NO_RELAUNCH'];
|
|
});
|
|
|
|
it('should construct correct node arguments from execArgv, additionalNodeArgs, script, additionalScriptArgs, and argv', () => {
|
|
// Test the argument construction logic directly by extracting it into a testable function
|
|
// This tests the same logic that's used in relaunchAppInChildProcess
|
|
|
|
// Setup test data to verify argument ordering
|
|
const mockExecArgv = ['--inspect=9229', '--trace-warnings'];
|
|
const mockArgv = [
|
|
'/usr/bin/node',
|
|
'/path/to/cli.js',
|
|
'command',
|
|
'--flag=value',
|
|
'--verbose',
|
|
];
|
|
const additionalNodeArgs = [
|
|
'--max-old-space-size=4096',
|
|
'--experimental-modules',
|
|
];
|
|
const additionalScriptArgs = ['--model', 'gemini-1.5-pro', '--debug'];
|
|
|
|
// Extract the argument construction logic from relaunchAppInChildProcess
|
|
const script = mockArgv[1];
|
|
const scriptArgs = mockArgv.slice(2);
|
|
|
|
const nodeArgs = [
|
|
...mockExecArgv,
|
|
...additionalNodeArgs,
|
|
script,
|
|
...additionalScriptArgs,
|
|
...scriptArgs,
|
|
];
|
|
|
|
// Verify the argument construction follows the expected pattern:
|
|
// [...process.execArgv, ...additionalNodeArgs, script, ...additionalScriptArgs, ...scriptArgs]
|
|
const expectedArgs = [
|
|
// Original node execution arguments
|
|
'--inspect=9229',
|
|
'--trace-warnings',
|
|
// Additional node arguments passed to function
|
|
'--max-old-space-size=4096',
|
|
'--experimental-modules',
|
|
// The script path
|
|
'/path/to/cli.js',
|
|
// Additional script arguments passed to function
|
|
'--model',
|
|
'gemini-1.5-pro',
|
|
'--debug',
|
|
// Original script arguments (everything after the script in process.argv)
|
|
'command',
|
|
'--flag=value',
|
|
'--verbose',
|
|
];
|
|
|
|
expect(nodeArgs).toEqual(expectedArgs);
|
|
});
|
|
|
|
it('should handle empty additional arguments correctly', () => {
|
|
// Test edge cases with empty arrays
|
|
const mockExecArgv = ['--trace-warnings'];
|
|
const mockArgv = ['/usr/bin/node', '/app/cli.js', 'start'];
|
|
const additionalNodeArgs: string[] = [];
|
|
const additionalScriptArgs: string[] = [];
|
|
|
|
// Extract the argument construction logic
|
|
const script = mockArgv[1];
|
|
const scriptArgs = mockArgv.slice(2);
|
|
|
|
const nodeArgs = [
|
|
...mockExecArgv,
|
|
...additionalNodeArgs,
|
|
script,
|
|
...additionalScriptArgs,
|
|
...scriptArgs,
|
|
];
|
|
|
|
const expectedArgs = ['--trace-warnings', '/app/cli.js', 'start'];
|
|
|
|
expect(nodeArgs).toEqual(expectedArgs);
|
|
});
|
|
|
|
it('should handle complex argument patterns', () => {
|
|
// Test with various argument types including flags with values, boolean flags, etc.
|
|
const mockExecArgv = ['--max-old-space-size=8192'];
|
|
const mockArgv = [
|
|
'/usr/bin/node',
|
|
'/cli.js',
|
|
'--config=/path/to/config.json',
|
|
'--verbose',
|
|
'subcommand',
|
|
'--output',
|
|
'file.txt',
|
|
];
|
|
const additionalNodeArgs = ['--inspect-brk=9230'];
|
|
const additionalScriptArgs = ['--model=gpt-4', '--temperature=0.7'];
|
|
|
|
const script = mockArgv[1];
|
|
const scriptArgs = mockArgv.slice(2);
|
|
|
|
const nodeArgs = [
|
|
...mockExecArgv,
|
|
...additionalNodeArgs,
|
|
script,
|
|
...additionalScriptArgs,
|
|
...scriptArgs,
|
|
];
|
|
|
|
const expectedArgs = [
|
|
'--max-old-space-size=8192',
|
|
'--inspect-brk=9230',
|
|
'/cli.js',
|
|
'--model=gpt-4',
|
|
'--temperature=0.7',
|
|
'--config=/path/to/config.json',
|
|
'--verbose',
|
|
'subcommand',
|
|
'--output',
|
|
'file.txt',
|
|
];
|
|
|
|
expect(nodeArgs).toEqual(expectedArgs);
|
|
});
|
|
|
|
// Note: Additional integration tests for spawn behavior are complex due to module mocking
|
|
// limitations with ES modules. The core logic is tested in relaunchOnExitCode tests.
|
|
|
|
it('should handle null exit code from child process', async () => {
|
|
process.argv = ['/usr/bin/node', '/app/cli.js'];
|
|
|
|
const mockChild = createMockChildProcess(0, false); // Don't auto-close
|
|
mockedSpawn.mockImplementation(() => {
|
|
// Emit close with null code immediately
|
|
setImmediate(() => {
|
|
mockChild.emit('close', null);
|
|
});
|
|
return mockChild;
|
|
});
|
|
|
|
// Start the relaunch process
|
|
const promise = relaunchAppInChildProcess([], []);
|
|
|
|
await expect(promise).rejects.toThrow('PROCESS_EXIT_CALLED');
|
|
|
|
// Should default to exit code 1
|
|
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
});
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Creates a mock child process that emits events asynchronously
|
|
*/
|
|
function createMockChildProcess(
|
|
exitCode: number = 0,
|
|
autoClose: boolean = false,
|
|
): ChildProcess {
|
|
const mockChild = new EventEmitter() as ChildProcess;
|
|
|
|
Object.assign(mockChild, {
|
|
stdin: null,
|
|
stdout: null,
|
|
stderr: null,
|
|
stdio: [null, null, null],
|
|
pid: 12345,
|
|
killed: false,
|
|
exitCode: null,
|
|
signalCode: null,
|
|
spawnargs: [],
|
|
spawnfile: '',
|
|
kill: vi.fn(),
|
|
send: vi.fn(),
|
|
disconnect: vi.fn(),
|
|
unref: vi.fn(),
|
|
ref: vi.fn(),
|
|
});
|
|
|
|
if (autoClose) {
|
|
setImmediate(() => {
|
|
mockChild.emit('close', exitCode);
|
|
});
|
|
}
|
|
|
|
return mockChild;
|
|
}
|