import { EventEmitter } from 'events' import { resolve } from 'path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const { spawnMock } = vi.hoisted(() => ({ spawnMock: vi.fn() })) vi.mock('child_process', () => ({ spawn: spawnMock })) import { launchOrcaApp, serveOrcaApp } from './launch' class FakeChildProcess extends EventEmitter { kill = vi.fn() unref = vi.fn() } describe('serveOrcaApp', () => { beforeEach(() => { spawnMock.mockReset() process.env.ORCA_APP_EXECUTABLE = '/Applications/Orca.app/Contents/MacOS/Orca' }) afterEach(() => { delete process.env.ORCA_APP_EXECUTABLE delete process.env.ORCA_APP_EXECUTABLE_NEEDS_APP_ROOT }) it('pins the Electron child cwd to the app root instead of the caller cwd', async () => { const child = { kill: vi.fn(), once: vi.fn( (event: string, handler: (code: number | null, signal: string | null) => void) => { if (event === 'exit') { queueMicrotask(() => handler(0, null)) } return child } ) } spawnMock.mockReturnValue(child) await expect(serveOrcaApp({ json: true })).resolves.toBe(0) expect(spawnMock).toHaveBeenCalledWith( '/Applications/Orca.app/Contents/MacOS/Orca', ['--serve', '--serve-json'], expect.objectContaining({ cwd: resolve(__dirname, '../../..') }) ) }) it('passes mobile pairing through to the foreground server child', async () => { const child = { kill: vi.fn(), once: vi.fn( (event: string, handler: (code: number | null, signal: string | null) => void) => { if (event === 'exit') { queueMicrotask(() => handler(0, null)) } return child } ) } spawnMock.mockReturnValue(child) await expect( serveOrcaApp({ json: true, port: '6768', pairingAddress: '100.64.1.20', mobilePairing: true }) ).resolves.toBe(0) expect(spawnMock).toHaveBeenCalledWith( '/Applications/Orca.app/Contents/MacOS/Orca', [ '--serve', '--serve-json', '--serve-port', '6768', '--serve-pairing-address', '100.64.1.20', '--serve-mobile-pairing' ], expect.objectContaining({ cwd: resolve(__dirname, '../../..') }) ) }) it('passes the app root before serve flags for dev Electron executables', async () => { process.env.ORCA_APP_EXECUTABLE = '/repo/node_modules/.bin/electron' process.env.ORCA_APP_EXECUTABLE_NEEDS_APP_ROOT = '1' const child = { kill: vi.fn(), once: vi.fn( (event: string, handler: (code: number | null, signal: string | null) => void) => { if (event === 'exit') { queueMicrotask(() => handler(0, null)) } return child } ) } spawnMock.mockReturnValue(child) await expect(serveOrcaApp({ json: true, port: '6768' })).resolves.toBe(0) expect(spawnMock).toHaveBeenCalledWith( '/repo/node_modules/.bin/electron', [resolve(__dirname, '../../..'), '--serve', '--serve-json', '--serve-port', '6768'], expect.objectContaining({ cwd: resolve(__dirname, '../../..') }) ) }) it('uses a shell when a Windows npm command shim is the Electron executable', async () => { const platformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform') Object.defineProperty(process, 'platform', { value: 'win32' }) process.env.ORCA_APP_EXECUTABLE = 'C:\\repo\\node_modules\\.bin\\electron.cmd' const child = { kill: vi.fn(), once: vi.fn( (event: string, handler: (code: number | null, signal: string | null) => void) => { if (event === 'exit') { queueMicrotask(() => handler(0, null)) } return child } ) } spawnMock.mockReturnValue(child) try { await expect(serveOrcaApp({ json: true })).resolves.toBe(0) expect(spawnMock).toHaveBeenCalledWith( 'C:\\repo\\node_modules\\.bin\\electron.cmd', ['--serve', '--serve-json'], expect.objectContaining({ shell: true }) ) } finally { if (platformDescriptor) { Object.defineProperty(process, 'platform', platformDescriptor) } } }) }) describe('launchOrcaApp', () => { beforeEach(() => { spawnMock.mockReset() }) afterEach(() => { delete process.env.ORCA_OPEN_COMMAND delete process.env.ORCA_APP_EXECUTABLE delete process.env.ORCA_APP_EXECUTABLE_NEEDS_APP_ROOT }) it('handles asynchronous detached spawn errors without throwing', async () => { process.env.ORCA_APP_EXECUTABLE = '/missing/Orca' const child = new FakeChildProcess() spawnMock.mockReturnValue(child) launchOrcaApp() child.emit('error', new Error('ENOENT')) await Promise.resolve() expect(child.unref).toHaveBeenCalled() }) })