import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { spawn, resolveShellConfig } from '../src/comm/child_process.js'; describe('process spawn options', () => { const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform'); const originalEnv = process.env; const tempDirs: string[] = []; afterEach(() => { jest.clearAllMocks(); process.env = originalEnv; if (originalPlatform) { Object.defineProperty(process, 'platform', originalPlatform); } tempDirs.splice(0).forEach(dir => fs.rmSync(dir, { recursive: true, force: true })); }); it('spawn automatically injects windowsHide: true', () => { const spawnMock = jest.fn().mockReturnValue(createFakeChild()); jest.resetModules(); jest.doMock('child_process', () => ({ ...jest.requireActual('child_process'), spawn: spawnMock, })); let wrappedSpawn: typeof spawn; jest.isolateModules(() => { ({ spawn: wrappedSpawn } = require('../src/comm/child_process.js')); }); wrappedSpawn!('echo', ['hello'], { stdio: 'ignore' }); expect(spawnMock).toHaveBeenCalledWith( 'echo', ['hello'], expect.objectContaining({ stdio: 'ignore', windowsHide: true }) ); }); it('uses Git Bash before cmd.exe on Windows when available', () => { Object.defineProperty(process, 'platform', { value: 'win32' }); const root = fs.mkdtempSync(path.join(os.tmpdir(), 'neta-git-bash-')); tempDirs.push(root); const gitBash = path.join(root, 'Git', 'bin', 'bash.exe'); fs.mkdirSync(path.dirname(gitBash), { recursive: true }); fs.writeFileSync(gitBash, '', 'utf8'); process.env = { ...originalEnv, ProgramFiles: root, 'ProgramFiles(x86)': '', ComSpec: 'C:\\Windows\\System32\\cmd.exe', }; const config = resolveShellConfig(); expect(config).toEqual({ shell: gitBash, args: ['-lc'], }); }); it('hides subagent worker process windows', async () => { const fakeChild = createFakeChild(); const spawnMock = jest.fn().mockReturnValue(fakeChild); jest.resetModules(); jest.doMock('child_process', () => ({ spawn: spawnMock, })); let runner: any; let protocolVersion: number; await jest.isolateModulesAsync(async () => { ({ SubagentProcessRunner: runner } = await import('../src/modules/netaclaw/subagent/process_runner.js')); ({ SUBAGENT_WORKER_PROTOCOL_VERSION: protocolVersion } = await import('../src/modules/netaclaw/subagent/process_protocol.js')); }); const instance = new runner({ workerPath: 'worker.js' }); void instance.run({ protocolVersion, runId: 'run-hidden', sessionId: 'session-1', parentMessageId: 1, parentToolCallId: 'tool-1', parentAgentId: 1, task: { goal: 'test' }, agentConfig: { name: 'agent', systemPrompt: 'test', model: 'openai:gpt-5.4', apiKey: 'test-key' }, toolNames: [], }).catch(() => undefined); expect(spawnMock).toHaveBeenCalledWith( expect.any(String), expect.any(Array), expect.objectContaining({ windowsHide: true }) ); }); }); function createFakeChild() { const { PassThrough } = require('stream'); const { EventEmitter } = require('events'); const child = new EventEmitter(); child.stdin = new PassThrough(); child.stdout = new PassThrough(); child.stderr = new PassThrough(); child.killed = false; child.kill = jest.fn(() => { child.killed = true; child.emit('close', 0); return true; }); return child; }