basant307/AI_Governance_Project
045
1/**2 * @license3 * Copyright 2025 Google LLC4 * SPDX-License-Identifier: Apache-2.05 */6 7import {8 vi,9 describe,10 it,11 expect,12 beforeEach,13 afterEach,14 type MockInstance,15} from 'vitest';16import { EventEmitter } from 'node:events';17import { RELAUNCH_EXIT_CODE } from './processUtils.js';18import type { ChildProcess } from 'node:child_process';19import { spawn } from 'node:child_process';20 21vi.mock('node:child_process', async (importOriginal) => {22 const actual = await importOriginal<typeof import('node:child_process')>();23 const mockSpawn = vi.fn();24 // Named re-exports must be spelled out for vitest ESM mocking to rebind them.25 return {26 ...actual,27 default: { ...actual, spawn: mockSpawn },28 spawn: mockSpawn,29 };30});31 32const mockedSpawn = vi.mocked(spawn);33 34// Import the functions initially35import { relaunchAppInChildProcess, relaunchOnExitCode } from './relaunch.js';36 37describe('relaunchOnExitCode', () => {38 let processExitSpy: MockInstance;39 let stdinResumeSpy: MockInstance;40 41 beforeEach(() => {42 processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {43 throw new Error('PROCESS_EXIT_CALLED');44 });45 stdinResumeSpy = vi46 .spyOn(process.stdin, 'resume')47 .mockImplementation(() => process.stdin);48 vi.clearAllMocks();49 });50 51 afterEach(() => {52 processExitSpy.mockRestore();53 stdinResumeSpy.mockRestore();54 });55 56 it('should exit with non-RELAUNCH_EXIT_CODE', async () => {57 const runner = vi.fn().mockResolvedValue(0);58 59 await expect(relaunchOnExitCode(runner)).rejects.toThrow(60 'PROCESS_EXIT_CALLED',61 );62 63 expect(runner).toHaveBeenCalledTimes(1);64 expect(processExitSpy).toHaveBeenCalledWith(0);65 });66 67 it('should continue running when RELAUNCH_EXIT_CODE is returned', async () => {68 let callCount = 0;69 const runner = vi.fn().mockImplementation(async () => {70 callCount++;71 if (callCount === 1) return RELAUNCH_EXIT_CODE;72 if (callCount === 2) return RELAUNCH_EXIT_CODE;73 return 0; // Exit on third call74 });75 76 await expect(relaunchOnExitCode(runner)).rejects.toThrow(77 'PROCESS_EXIT_CALLED',78 );79 80 expect(runner).toHaveBeenCalledTimes(3);81 expect(processExitSpy).toHaveBeenCalledWith(0);82 });83 84 it('should handle runner errors', async () => {85 const error = new Error('Runner failed');86 const runner = vi.fn().mockRejectedValue(error);87 88 await expect(relaunchOnExitCode(runner)).rejects.toThrow(89 'PROCESS_EXIT_CALLED',90 );91 92 expect(runner).toHaveBeenCalledTimes(1);93 expect(stdinResumeSpy).toHaveBeenCalled();94 expect(processExitSpy).toHaveBeenCalledWith(1);95 });96});97 98describe('relaunchAppInChildProcess', () => {99 let processExitSpy: MockInstance;100 let stdinPauseSpy: MockInstance;101 let stdinResumeSpy: MockInstance;102 103 // Store original values to restore later104 const originalEnv = { ...process.env };105 const originalExecArgv = [...process.execArgv];106 const originalArgv = [...process.argv];107 const originalExecPath = process.execPath;108 109 beforeEach(() => {110 vi.clearAllMocks();111 112 process.env = { ...originalEnv };113 delete process.env['QWEN_CODE_NO_RELAUNCH'];114 115 process.execArgv = [...originalExecArgv];116 process.argv = [...originalArgv];117 process.execPath = '/usr/bin/node';118 119 processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {120 throw new Error('PROCESS_EXIT_CALLED');121 });122 stdinPauseSpy = vi123 .spyOn(process.stdin, 'pause')124 .mockImplementation(() => process.stdin);125 stdinResumeSpy = vi126 .spyOn(process.stdin, 'resume')127 .mockImplementation(() => process.stdin);128 });129 130 afterEach(() => {131 process.env = { ...originalEnv };132 process.execArgv = [...originalExecArgv];133 process.argv = [...originalArgv];134 process.execPath = originalExecPath;135 136 processExitSpy.mockRestore();137 stdinPauseSpy.mockRestore();138 stdinResumeSpy.mockRestore();139 });140 141 describe('when QWEN_CODE_NO_RELAUNCH is set', () => {142 it('should return early without spawning a child process', async () => {143 process.env['QWEN_CODE_NO_RELAUNCH'] = 'true';144 145 await relaunchAppInChildProcess(['--test'], ['--verbose']);146 147 expect(mockedSpawn).not.toHaveBeenCalled();148 expect(processExitSpy).not.toHaveBeenCalled();149 });150 });151 152 describe('when QWEN_CODE_NO_RELAUNCH is not set', () => {153 beforeEach(() => {154 delete process.env['QWEN_CODE_NO_RELAUNCH'];155 });156 157 it('should construct correct node arguments from execArgv, additionalNodeArgs, script, additionalScriptArgs, and argv', () => {158 // Test the argument construction logic directly by extracting it into a testable function159 // This tests the same logic that's used in relaunchAppInChildProcess160 161 // Setup test data to verify argument ordering162 const mockExecArgv = ['--inspect=9229', '--trace-warnings'];163 const mockArgv = [164 '/usr/bin/node',165 '/path/to/cli.js',166 'command',167 '--flag=value',168 '--verbose',169 ];170 const additionalNodeArgs = [171 '--max-old-space-size=4096',172 '--experimental-modules',173 ];174 const additionalScriptArgs = ['--model', 'gemini-1.5-pro', '--debug'];175 176 // Extract the argument construction logic from relaunchAppInChildProcess177 const script = mockArgv[1];178 const scriptArgs = mockArgv.slice(2);179 180 const nodeArgs = [181 ...mockExecArgv,182 ...additionalNodeArgs,183 script,184 ...additionalScriptArgs,185 ...scriptArgs,186 ];187 188 // Verify the argument construction follows the expected pattern:189 // [...process.execArgv, ...additionalNodeArgs, script, ...additionalScriptArgs, ...scriptArgs]190 const expectedArgs = [191 // Original node execution arguments192 '--inspect=9229',193 '--trace-warnings',194 // Additional node arguments passed to function195 '--max-old-space-size=4096',196 '--experimental-modules',197 // The script path198 '/path/to/cli.js',199 // Additional script arguments passed to function200 '--model',201 'gemini-1.5-pro',202 '--debug',203 // Original script arguments (everything after the script in process.argv)204 'command',205 '--flag=value',206 '--verbose',207 ];208 209 expect(nodeArgs).toEqual(expectedArgs);210 });211 212 it('should handle empty additional arguments correctly', () => {213 // Test edge cases with empty arrays214 const mockExecArgv = ['--trace-warnings'];215 const mockArgv = ['/usr/bin/node', '/app/cli.js', 'start'];216 const additionalNodeArgs: string[] = [];217 const additionalScriptArgs: string[] = [];218 219 // Extract the argument construction logic220 const script = mockArgv[1];221 const scriptArgs = mockArgv.slice(2);222 223 const nodeArgs = [224 ...mockExecArgv,225 ...additionalNodeArgs,226 script,227 ...additionalScriptArgs,228 ...scriptArgs,229 ];230 231 const expectedArgs = ['--trace-warnings', '/app/cli.js', 'start'];232 233 expect(nodeArgs).toEqual(expectedArgs);234 });235 236 it('should handle complex argument patterns', () => {237 // Test with various argument types including flags with values, boolean flags, etc.238 const mockExecArgv = ['--max-old-space-size=8192'];239 const mockArgv = [240 '/usr/bin/node',241 '/cli.js',242 '--config=/path/to/config.json',243 '--verbose',244 'subcommand',245 '--output',246 'file.txt',247 ];248 const additionalNodeArgs = ['--inspect-brk=9230'];249 const additionalScriptArgs = ['--model=gpt-4', '--temperature=0.7'];250 251 const script = mockArgv[1];252 const scriptArgs = mockArgv.slice(2);253 254 const nodeArgs = [255 ...mockExecArgv,256 ...additionalNodeArgs,257 script,258 ...additionalScriptArgs,259 ...scriptArgs,260 ];261 262 const expectedArgs = [263 '--max-old-space-size=8192',264 '--inspect-brk=9230',265 '/cli.js',266 '--model=gpt-4',267 '--temperature=0.7',268 '--config=/path/to/config.json',269 '--verbose',270 'subcommand',271 '--output',272 'file.txt',273 ];274 275 expect(nodeArgs).toEqual(expectedArgs);276 });277 278 // Note: Additional integration tests for spawn behavior are complex due to module mocking279 // limitations with ES modules. The core logic is tested in relaunchOnExitCode tests.280 281 it('should invoke afterSpawn immediately after spawn, before waiting for child exit', async () => {282 process.argv = ['/usr/bin/node', '/app/cli.js'];283 284 const afterSpawn = vi.fn();285 let spawned = false;286 287 const mockChild = createMockChildProcess(0, false);288 mockedSpawn.mockImplementation(() => {289 spawned = true;290 return mockChild;291 });292 293 const promise = relaunchAppInChildProcess([], [], { afterSpawn });294 295 // Wait until spawn has been called296 await vi.waitFor(() => {297 expect(spawned).toBe(true);298 });299 300 // afterSpawn must have been called before child exits301 expect(afterSpawn).toHaveBeenCalledTimes(1);302 303 // Close the child so the promise resolves304 mockChild.emit('close', 0);305 await expect(promise).rejects.toThrow('PROCESS_EXIT_CALLED');306 307 // afterSpawn should still be called only once (first spawn)308 expect(afterSpawn).toHaveBeenCalledTimes(1);309 });310 311 it('should handle null exit code from child process', async () => {312 process.argv = ['/usr/bin/node', '/app/cli.js'];313 314 const mockChild = createMockChildProcess(0, false); // Don't auto-close315 mockedSpawn.mockImplementation(() => {316 // Emit close with null code immediately317 setImmediate(() => {318 mockChild.emit('close', null);319 });320 return mockChild;321 });322 323 // Start the relaunch process324 const promise = relaunchAppInChildProcess([], []);325 326 await expect(promise).rejects.toThrow('PROCESS_EXIT_CALLED');327 328 // Should default to exit code 1329 expect(processExitSpy).toHaveBeenCalledWith(1);330 });331 });332});333 334/**335 * Creates a mock child process that emits events asynchronously336 */337function createMockChildProcess(338 exitCode: number = 0,339 autoClose: boolean = false,340): ChildProcess {341 const mockChild = new EventEmitter() as ChildProcess;342 343 Object.assign(mockChild, {344 stdin: null,345 stdout: null,346 stderr: null,347 stdio: [null, null, null],348 pid: 12345,349 killed: false,350 exitCode: null,351 signalCode: null,352 spawnargs: [],353 spawnfile: '',354 kill: vi.fn(),355 send: vi.fn(),356 disconnect: vi.fn(),357 unref: vi.fn(),358 ref: vi.fn(),359 });360 361 if (autoClose) {362 setImmediate(() => {363 mockChild.emit('close', exitCode);364 });365 }366 367 return mockChild;368}369 