CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes45downloads
relaunch.test.ts369 linesDownload Raw Back to utils
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 
basant307/AI_Governance_Project · CoolFace