import { EventEmitter } from 'node:events' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const { execFileMock, execFileSyncMock, spawnMock } = vi.hoisted(() => ({ execFileMock: vi.fn(), execFileSyncMock: vi.fn(), spawnMock: vi.fn() })) vi.mock('node:child_process', () => ({ execFile: execFileMock, execFileSync: execFileSyncMock, spawn: spawnMock })) import { commandExecFileAsync, ghExecFileAsync, gitExecFileAsync, gitStreamStdout, translateWslOutputPaths, wslAwareSpawn } from './runner' type MockChildProcess = EventEmitter & { stdout: EventEmitter stderr: EventEmitter pid: number kill: ReturnType unref?: ReturnType } function createMockChildProcess(pid: number): MockChildProcess { const child = new EventEmitter() as MockChildProcess child.stdout = new EventEmitter() child.stderr = new EventEmitter() child.pid = pid child.kill = vi.fn() return child } function createMockTaskkillProcess(): MockChildProcess { const child = createMockChildProcess(9000) child.unref = vi.fn() return child } async function withPlatform(platform: NodeJS.Platform, fn: () => Promise): Promise { const original = process.platform Object.defineProperty(process, 'platform', { configurable: true, value: platform }) try { return await fn() } finally { Object.defineProperty(process, 'platform', { configurable: true, value: original }) } } describe('commandExecFileAsync Windows command shims', () => { const originalComSpec = process.env.ComSpec beforeEach(() => { process.env.ComSpec = 'C:\\Windows\\System32\\cmd.exe' execFileMock.mockReset() execFileSyncMock.mockReset() spawnMock.mockReset() }) afterEach(() => { vi.useRealTimers() if (originalComSpec === undefined) { delete process.env.ComSpec } else { process.env.ComSpec = originalComSpec } }) it('kills aborted Windows .cmd shim executions as a process tree', async () => { await withPlatform('win32', async () => { const command = createMockChildProcess(1234) const taskkill = createMockTaskkillProcess() spawnMock.mockImplementation((cmd: string) => (cmd === 'taskkill' ? taskkill : command)) const controller = new AbortController() const promise = commandExecFileAsync('C:\\tools\\pnpm.cmd', ['--version'], { cwd: 'C:\\repo', signal: controller.signal }) const rejection = expect(promise).rejects.toMatchObject({ name: 'AbortError' }) controller.abort() await rejection expect(spawnMock).toHaveBeenCalledWith( 'taskkill', ['/pid', '1234', '/t', '/f'], expect.objectContaining({ stdio: 'ignore', windowsHide: true }) ) expect(command.kill).not.toHaveBeenCalled() }) }) it('kills timed-out Windows .cmd shim executions as a process tree', async () => { vi.useFakeTimers() await withPlatform('win32', async () => { const command = createMockChildProcess(1234) const taskkill = createMockTaskkillProcess() spawnMock.mockImplementation((cmd: string) => (cmd === 'taskkill' ? taskkill : command)) const promise = commandExecFileAsync('C:\\tools\\pnpm.cmd', ['store', 'prune'], { cwd: 'C:\\repo', timeout: 1000 }) const rejection = expect(promise).rejects.toThrow('C:\\tools\\pnpm.cmd timed out.') await vi.advanceTimersByTimeAsync(1000) await rejection expect(spawnMock).toHaveBeenCalledWith( 'taskkill', ['/pid', '1234', '/t', '/f'], expect.objectContaining({ stdio: 'ignore', windowsHide: true }) ) expect(command.kill).not.toHaveBeenCalled() expect(command.stdout.listenerCount('data')).toBe(0) expect(command.stderr.listenerCount('data')).toBe(0) expect(command.listenerCount('error')).toBe(0) expect(command.listenerCount('close')).toBe(0) }) }) it('kills over-buffer Windows .cmd shim executions as a process tree', async () => { await withPlatform('win32', async () => { const command = createMockChildProcess(1234) const taskkill = createMockTaskkillProcess() spawnMock.mockImplementation((cmd: string) => (cmd === 'taskkill' ? taskkill : command)) const promise = commandExecFileAsync('C:\\tools\\pnpm.cmd', ['store', 'prune'], { cwd: 'C:\\repo', maxBuffer: 2 }) const rejection = expect(promise).rejects.toThrow( 'C:\\tools\\pnpm.cmd stdout exceeded maxBuffer.' ) command.stdout.emit('data', Buffer.from('too much output')) await rejection expect(spawnMock).toHaveBeenCalledWith( 'taskkill', ['/pid', '1234', '/t', '/f'], expect.objectContaining({ stdio: 'ignore', windowsHide: true }) ) expect(command.kill).not.toHaveBeenCalled() expect(command.stdout.listenerCount('data')).toBe(0) expect(command.stderr.listenerCount('data')).toBe(0) expect(command.listenerCount('error')).toBe(0) expect(command.listenerCount('close')).toBe(0) }) }) it('removes listeners after successful Windows .cmd shim executions', async () => { await withPlatform('win32', async () => { const command = createMockChildProcess(1234) spawnMock.mockReturnValue(command) const promise = commandExecFileAsync('C:\\tools\\pnpm.cmd', ['--version'], { cwd: 'C:\\repo' }) command.stdout.emit('data', Buffer.from('9.1.0\n')) command.stderr.emit('data', Buffer.from('notice\n')) command.emit('close', 0) await expect(promise).resolves.toEqual({ stdout: '9.1.0\n', stderr: 'notice\n' }) expect(command.stdout.listenerCount('data')).toBe(0) expect(command.stderr.listenerCount('data')).toBe(0) expect(command.listenerCount('error')).toBe(0) expect(command.listenerCount('close')).toBe(0) }) }) }) describe('runner execFile timeout handling', () => { beforeEach(() => { execFileMock.mockReset() spawnMock.mockReset() execFileSyncMock.mockReset() vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('rejects command executions when execFile never calls back after timeout', async () => { const child = createMockChildProcess(1234) execFileMock.mockReturnValue(child) const promise = commandExecFileAsync('git', ['status'], { cwd: '/repo', timeout: 1000 }) const rejection = expect(promise).rejects.toThrow(/git(?:\.exe)? timed out\./i) await vi.advanceTimersByTimeAsync(1000) await rejection expect(child.kill).toHaveBeenCalled() }) it('rejects git executions when execFile never calls back after timeout', async () => { const child = createMockChildProcess(1234) execFileMock.mockReturnValue(child) const promise = gitExecFileAsync(['status'], { cwd: '/repo', timeout: 1000 }) const rejection = expect(promise).rejects.toThrow('git timed out.') await vi.advanceTimersByTimeAsync(1000) await rejection expect(child.kill).toHaveBeenCalled() }) it('rejects gh executions that never call back using the default timeout', async () => { const child = createMockChildProcess(1234) execFileMock.mockReturnValue(child) const promise = ghExecFileAsync(['api', 'repos/stablyai/orca/issues/5388'], { cwd: '/repo' }) const rejection = expect(promise).rejects.toThrow('gh timed out.') await vi.advanceTimersByTimeAsync(30_000) await rejection expect(child.kill).toHaveBeenCalled() }) it('kills an active gh execution when its caller aborts', async () => { const child = createMockChildProcess(1234) execFileMock.mockReturnValue(child) const controller = new AbortController() const promise = ghExecFileAsync(['api', 'repos/stablyai/orca/issues/5388'], { cwd: '/repo', signal: controller.signal }) const rejection = expect(promise).rejects.toMatchObject({ name: 'AbortError' }) controller.abort() await rejection expect(child.kill).toHaveBeenCalled() }) it('honors explicit gh timeouts', async () => { const child = createMockChildProcess(1234) execFileMock.mockReturnValue(child) const promise = ghExecFileAsync(['api', 'repos/stablyai/orca/issues/5388'], { cwd: '/repo', timeout: 1234 }) const rejection = expect(promise).rejects.toThrow('gh timed out.') await vi.advanceTimersByTimeAsync(1233) expect(child.kill).not.toHaveBeenCalled() await vi.advanceTimersByTimeAsync(1) await rejection expect(child.kill).toHaveBeenCalled() }) it('runs gh non-interactively while preserving explicit env', async () => { const child = createMockChildProcess(1234) let capturedEnv: NodeJS.ProcessEnv | undefined execFileMock.mockImplementation((_cmd, _args, opts, cb) => { capturedEnv = opts.env cb(null, 'ok', '') return child }) await ghExecFileAsync(['api', 'user'], { cwd: '/repo', env: { ...process.env, GH_PROMPT_DISABLED: '0', ORCA_TEST_ENV: 'kept' }, timeout: 1234 }) expect(capturedEnv?.GH_PROMPT_DISABLED).toBe('0') expect(capturedEnv?.ORCA_TEST_ENV).toBe('kept') }) // Issue #5308: git read-path calls must be forced non-interactive so a // credential / SSH host-key prompt fails fast instead of blocking forever on // stdin and wedging the serve runtime for all clients. it('runs git non-interactively so a prompt fails fast instead of hanging', async () => { const child = createMockChildProcess(1234) let capturedEnv: NodeJS.ProcessEnv | undefined execFileMock.mockImplementation((_cmd, _args, opts, cb) => { capturedEnv = opts.env cb(null, '', '') return child }) await gitExecFileAsync(['worktree', 'list', '--porcelain', '-z'], { cwd: '/home5/Brian' }) expect(capturedEnv?.GIT_TERMINAL_PROMPT).toBe('0') expect(capturedEnv?.GIT_ASKPASS).toBe('') expect(capturedEnv?.SSH_ASKPASS).toBe('') expect(capturedEnv?.GIT_SSH_COMMAND).toContain('BatchMode=yes') }) it('probes core.sshCommand for opted-in network git calls', async () => { const child = createMockChildProcess(1234) const calls: { args: string[]; env: NodeJS.ProcessEnv }[] = [] execFileMock.mockImplementation((_cmd, args, opts, cb) => { calls.push({ args, env: opts.env }) cb(null, args[0] === 'config' ? 'ssh -F ~/.ssh/github-work -i ~/.ssh/work_key\n' : '', '') return child }) await gitExecFileAsync(['fetch', 'origin'], { cwd: '/repo', env: {}, useConfiguredSshCommandForNetwork: true }) expect(calls[0]?.args).toEqual(['config', '--get', 'core.sshCommand']) expect(calls[0]?.env.GIT_TERMINAL_PROMPT).toBe('0') expect(calls[0]?.env.GIT_SSH_COMMAND).toBeUndefined() expect(calls[1]?.args).toEqual(['fetch', 'origin']) expect(calls[1]?.env.GIT_SSH_COMMAND).toBe( 'ssh -F ~/.ssh/github-work -i ~/.ssh/work_key -o BatchMode=yes' ) }) it('replaces configured BatchMode for opted-in mergeable OpenSSH commands', async () => { const child = createMockChildProcess(1234) let capturedEnv: NodeJS.ProcessEnv | undefined execFileMock.mockImplementation((_cmd, args, opts, cb) => { if (args[0] === 'config') { cb(null, 'ssh -o BatchMode=no -i ~/.ssh/personal\n', '') } else { capturedEnv = opts.env cb(null, '', '') } return child }) await gitExecFileAsync(['fetch', 'origin'], { cwd: '/repo', env: {}, useConfiguredSshCommandForNetwork: true }) expect(capturedEnv?.GIT_SSH_COMMAND).toBe('ssh -i ~/.ssh/personal -o BatchMode=yes') }) it('merges quoted ssh.exe command shapes for opted-in network calls', async () => { const child = createMockChildProcess(1234) let capturedEnv: NodeJS.ProcessEnv | undefined execFileMock.mockImplementation((_cmd, args, opts, cb) => { if (args[0] === 'config') { cb(null, '"C:/Program Files/Git/usr/bin/ssh.exe" -F ~/.ssh/config\n', '') } else { capturedEnv = opts.env cb(null, '', '') } return child }) await gitExecFileAsync(['fetch', 'origin'], { cwd: '/repo', env: {}, useConfiguredSshCommandForNetwork: true }) expect(capturedEnv?.GIT_SSH_COMMAND).toBe( "'C:/Program Files/Git/usr/bin/ssh.exe' -F ~/.ssh/config -o BatchMode=yes" ) }) it('merges unquoted Windows ssh.exe paths for opted-in network calls', async () => { const child = createMockChildProcess(1234) let capturedEnv: NodeJS.ProcessEnv | undefined execFileMock.mockImplementation((_cmd, args, opts, cb) => { if (args[0] === 'config') { cb(null, `${String.raw`C:\Git\usr\bin\ssh.exe -i C:\Users\me\.ssh\work_key`}\n`, '') } else { capturedEnv = opts.env cb(null, '', '') } return child }) await gitExecFileAsync(['fetch', 'origin'], { cwd: '/repo', env: {}, useConfiguredSshCommandForNetwork: true }) expect(capturedEnv?.GIT_SSH_COMMAND).toBe( String.raw`'C:\Git\usr\bin\ssh.exe' -i 'C:\Users\me\.ssh\work_key' -o BatchMode=yes` ) }) it('passes through unmergeable core.sshCommand wrappers without generic fallback', async () => { const child = createMockChildProcess(1234) let capturedEnv: NodeJS.ProcessEnv | undefined execFileMock.mockImplementation((_cmd, args, opts, cb) => { if (args[0] === 'config') { cb(null, '/usr/local/bin/work-ssh-wrapper --account work\n', '') } else { capturedEnv = opts.env cb(null, '', '') } return child }) await gitExecFileAsync(['fetch', 'origin'], { cwd: '/repo', env: {}, useConfiguredSshCommandForNetwork: true }) expect(capturedEnv?.GIT_TERMINAL_PROMPT).toBe('0') expect(capturedEnv?.GIT_ASKPASS).toBe('') expect(capturedEnv?.SSH_ASKPASS).toBe('') expect(capturedEnv?.GIT_SSH_COMMAND).toBeUndefined() }) it('passes through shell-expanding OpenSSH configs without changing expansion semantics', async () => { const child = createMockChildProcess(1234) let capturedEnv: NodeJS.ProcessEnv | undefined execFileMock.mockImplementation((_cmd, args, opts, cb) => { if (args[0] === 'config') { cb(null, 'ssh -i "$HOME/.ssh/work_key"\n', '') } else { capturedEnv = opts.env cb(null, '', '') } return child }) await gitExecFileAsync(['fetch', 'origin'], { cwd: '/repo', env: {}, useConfiguredSshCommandForNetwork: true }) expect(capturedEnv?.GIT_TERMINAL_PROMPT).toBe('0') expect(capturedEnv?.GIT_SSH_COMMAND).toBeUndefined() }) it('falls back to generic batch-mode SSH when opted-in config is unset', async () => { const child = createMockChildProcess(1234) let capturedEnv: NodeJS.ProcessEnv | undefined execFileMock.mockImplementation((_cmd, args, opts, cb) => { if (args[0] === 'config') { cb(Object.assign(new Error('missing'), { code: 1 }), '', '') } else { capturedEnv = opts.env cb(null, '', '') } return child }) await gitExecFileAsync(['fetch', 'origin'], { cwd: '/repo', env: {}, useConfiguredSshCommandForNetwork: true }) expect(capturedEnv?.GIT_SSH_COMMAND).toBe('ssh -o BatchMode=yes') }) it('preserves explicit GIT_SSH_COMMAND and skips the opted-in config probe', async () => { const child = createMockChildProcess(1234) let capturedEnv: NodeJS.ProcessEnv | undefined execFileMock.mockImplementation((_cmd, _args, opts, cb) => { capturedEnv = opts.env cb(null, '', '') return child }) await gitExecFileAsync(['fetch', 'origin'], { cwd: '/repo', env: { GIT_SSH_COMMAND: 'custom-ssh -o IdentityAgent=none' }, useConfiguredSshCommandForNetwork: true }) expect(execFileMock).toHaveBeenCalledTimes(1) expect(capturedEnv?.GIT_SSH_COMMAND).toBe('custom-ssh -o IdentityAgent=none') expect(capturedEnv?.GIT_TERMINAL_PROMPT).toBe('0') }) it('routes git through the selected WSL distro login shell when requested', async () => { await withPlatform('win32', async () => { const child = createMockChildProcess(1234) execFileMock.mockImplementation((_cmd, _args, _opts, cb) => { cb(null, 'ok', '') return child }) await gitExecFileAsync(['status', '--short'], { cwd: String.raw`C:\repo`, wslDistro: 'Ubuntu' }) expect(execFileMock).toHaveBeenCalledWith( 'wsl.exe', ['-d', 'Ubuntu', '--', 'sh', '-lc', expect.any(String)], expect.objectContaining({ cwd: undefined }), expect.any(Function) ) const shellCommand = execFileMock.mock.calls[0]?.[1]?.[5] as string expect(shellCommand).toContain('getent passwd') expect(shellCommand).toContain('exec "\\$_orca_wsl_shell" -ilc') expect(shellCommand).toContain('/mnt/c/repo') expect(shellCommand).toContain("'git'") expect(shellCommand).toContain('status') expect(shellCommand).toContain('--short') }) }) it('routes fixed commands through an explicitly selected WSL distro', async () => { await withPlatform('win32', async () => { const child = createMockChildProcess(1234) execFileMock.mockImplementation((_cmd, _args, _opts, cb) => { cb(null, 'hostname github.com\n', '') return child }) await commandExecFileAsync('ssh', ['-G', '--', 'github-work'], { cwd: String.raw`C:\repo`, timeout: 5_000, wslDistro: 'Ubuntu' }) expect(execFileMock).toHaveBeenCalledWith( 'wsl.exe', ['-d', 'Ubuntu', '--', 'bash', '-c', expect.any(String)], expect.objectContaining({ cwd: undefined }), expect.any(Function) ) const shellCommand = execFileMock.mock.calls[0]?.[1]?.[5] as string expect(shellCommand).toContain('/mnt/c/repo') expect(shellCommand).toContain("'ssh' '-G' '--' 'github-work'") }) }) it('forwards synthesized network SSH policy into the selected WSL distro', async () => { await withPlatform('win32', async () => { const child = createMockChildProcess(1234) let capturedEnv: NodeJS.ProcessEnv | undefined execFileMock.mockImplementation((_cmd, args, opts, cb) => { const shellCommand = args[5] as string if (shellCommand.includes("'config'")) { cb(Object.assign(new Error('missing'), { code: 1 }), '', '') } else { capturedEnv = opts.env cb(null, '', '') } return child }) await gitExecFileAsync(['fetch', 'origin'], { cwd: String.raw`C:\repo`, env: {}, wslDistro: 'Ubuntu', useConfiguredSshCommandForNetwork: true }) expect(capturedEnv?.GIT_SSH_COMMAND).toBe('ssh -o BatchMode=yes') expect((capturedEnv?.WSLENV ?? '').split(':')).toContain('GIT_SSH_COMMAND') }) }) it('forwards synthesized network SSH policy when a UNC cwd selects WSL', async () => { await withPlatform('win32', async () => { const child = createMockChildProcess(1234) let capturedEnv: NodeJS.ProcessEnv | undefined execFileMock.mockImplementation((_cmd, args, opts, cb) => { const shellCommand = args[5] as string if (shellCommand.includes("'config'")) { cb(Object.assign(new Error('missing'), { code: 1 }), '', '') } else { capturedEnv = opts.env cb(null, '', '') } return child }) await gitExecFileAsync(['fetch', 'origin'], { cwd: String.raw`\\wsl.localhost\Ubuntu\home\me\repo`, env: {}, useConfiguredSshCommandForNetwork: true }) expect(capturedEnv?.GIT_SSH_COMMAND).toBe('ssh -o BatchMode=yes') expect((capturedEnv?.WSLENV ?? '').split(':')).toContain('GIT_SSH_COMMAND') }) }) it('quotes WSL-routed executables before entering the shell', async () => { await withPlatform('win32', async () => { const child = createMockChildProcess(1234) spawnMock.mockReturnValue(child) wslAwareSpawn('codex; touch /tmp/pwned', ['--version'], { cwd: String.raw`C:\repo`, stdio: ['pipe', 'pipe', 'pipe'], wslDistro: 'Ubuntu', useWslLoginShell: true }) const shellCommand = spawnMock.mock.calls[0]?.[1]?.[5] as string expect(shellCommand).toContain(String.raw`'\''codex; touch /tmp/pwned'\'' '\''--version'\''`) }) }) }) describe('gitStreamStdout', () => { beforeEach(() => { spawnMock.mockReset() }) it('streams chunks to onStdout and resolves cleanly on a zero exit', async () => { const child = createMockChildProcess(1234) spawnMock.mockReturnValue(child) const chunks: string[] = [] const promise = gitStreamStdout(['status', '--porcelain=v2'], { cwd: '/repo', onStdout: (chunk) => { chunks.push(chunk) } }) child.stdout.emit('data', Buffer.from('? a.txt\n')) child.stdout.emit('data', Buffer.from('? b.txt\n')) child.emit('close', 0) await expect(promise).resolves.toEqual({ stoppedEarly: false }) expect(chunks).toEqual(['? a.txt\n', '? b.txt\n']) expect(child.kill).not.toHaveBeenCalled() }) it('kills git early and resolves stoppedEarly when onStdout requests a stop', async () => { const child = createMockChildProcess(1234) spawnMock.mockReturnValue(child) let calls = 0 const promise = gitStreamStdout(['status'], { cwd: '/repo', // Stop after the first chunk — mirrors a parser hitting its entry limit. onStdout: () => { calls += 1 return true } }) child.stdout.emit('data', Buffer.from('? a.txt\n')) await expect(promise).resolves.toEqual({ stoppedEarly: true }) expect(child.kill).toHaveBeenCalled() expect(calls).toBe(1) }) it('rejects when stdout exceeds the maxBuffer backstop', async () => { const child = createMockChildProcess(1234) spawnMock.mockReturnValue(child) const promise = gitStreamStdout(['status'], { cwd: '/repo', maxBuffer: 4, onStdout: () => {} }) const rejection = expect(promise).rejects.toThrow('git stdout exceeded maxBuffer.') child.stdout.emit('data', Buffer.from('way too much')) await rejection expect(child.kill).toHaveBeenCalled() }) it('rejects on a non-zero exit with stderr context', async () => { const child = createMockChildProcess(1234) spawnMock.mockReturnValue(child) const promise = gitStreamStdout(['status'], { cwd: '/repo', onStdout: () => {} }) const rejection = expect(promise).rejects.toThrow('git exited with 128') child.stderr.emit('data', Buffer.from('fatal: not a git repository')) child.emit('close', 128) await rejection }) it('rejects (not crashes) when the onStdout callback throws', async () => { const child = createMockChildProcess(1234) spawnMock.mockReturnValue(child) const promise = gitStreamStdout(['status'], { cwd: '/repo', onStdout: () => { throw new Error('parser blew up') } }) const rejection = expect(promise).rejects.toThrow('parser blew up') child.stdout.emit('data', Buffer.from('? a.txt\n')) await rejection expect(child.kill).toHaveBeenCalled() }) it('handles a late spawn error after cancellation', async () => { const child = createMockChildProcess(0) spawnMock.mockReturnValue(child) const controller = new AbortController() const promise = gitStreamStdout(['status'], { cwd: '/repo', signal: controller.signal, onStdout: () => {} }) controller.abort() await expect(promise).rejects.toMatchObject({ name: 'AbortError' }) expect(() => child.emit('error', new Error('spawn ENOENT'))).not.toThrow() }) }) describe('translateWslOutputPaths', () => { it('translates WSL output paths with an explicit distro for Windows cwd routing', () => { expect( translateWslOutputPaths('worktree /mnt/c/Users/me/repo-feature\n', 'C:\\Users\\me\\repo', { wslDistro: 'Ubuntu' }) ).toBe('worktree C:\\Users\\me\\repo-feature\n') }) })