import { EventEmitter } from 'node:events' import { resolve } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { encodePairingOffer, PAIRING_OFFER_VERSION } from '../../shared/pairing' const { spawnMock } = vi.hoisted(() => ({ spawnMock: vi.fn() })) vi.mock('child_process', () => ({ spawn: spawnMock })) import { launchOrcaApp, serveOrcaApp } from './launch' class FakeChildProcess extends EventEmitter { stdout = new EventEmitter() kill = vi.fn() unref = vi.fn() } const RECIPE_JSON = JSON.stringify({ schemaVersion: 1, pairingCode: encodePairingOffer({ v: PAIRING_OFFER_VERSION, endpoint: 'wss://sandbox.example.com', deviceToken: 'token', publicKeyB64: 'public-key' }), projectRoot: '/workspace/repo' }) const SERVE_INSTALL_STATUS = '[serve] orca CLI install: installed' const SSH_PRIVATE_KEY = 'TOP-SECRET-PRIVATE-KEY' const SSH_AUTHORIZATION = 'Bearer TOP-SECRET-AUTHORIZATION' const SSH_PASSPHRASE = 'TOP-SECRET-PASSPHRASE' const SSH_COOKIE = 'session=TOP-SECRET-COOKIE' const SSH_RECIPE_JSON = JSON.stringify({ schemaVersion: 1, connection: { type: 'ssh', target: { label: 'Sandbox', host: 'sandbox.example.com', port: 22, username: 'root' }, projectRoot: '/workspace/repo' }, userData: { credentials: { privateKey: SSH_PRIVATE_KEY, authorization: SSH_AUTHORIZATION, passphrase: SSH_PASSPHRASE, cookie: SSH_COOKIE } } }) const INVALID_SSH_RECIPE_JSON = SSH_RECIPE_JSON.replace('/workspace/repo', 'relative/repo') const IGNORED_NON_RECIPE_STDOUT = '[serve] ignored non-recipe stdout' function startRecipeJsonServer() { const child = new FakeChildProcess() spawnMock.mockReturnValue(child) const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true) const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true) const result = serveOrcaApp({ recipeJson: true, projectRoot: '/workspace/repo' }) return { child, result, stdoutSpy, stderrSpy } } describe('serveOrcaApp', () => { beforeEach(() => { spawnMock.mockReset() process.env.ORCA_APP_EXECUTABLE = '/Applications/Orca.app/Contents/MacOS/Orca' }) afterEach(() => { vi.restoreAllMocks() 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('prints recipe JSON from a detached server child and exits', async () => { const child = new FakeChildProcess() spawnMock.mockReturnValue(child) const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true) const result = serveOrcaApp({ pairingAddress: 'wss://sandbox.example.com', recipeJson: true, projectRoot: '/workspace/repo' }) queueMicrotask(() => { child.stdout.emit('data', `${RECIPE_JSON}\n`) }) await expect(result).resolves.toBe(0) expect(spawnMock).toHaveBeenCalledWith( '/Applications/Orca.app/Contents/MacOS/Orca', [ '--serve', '--serve-pairing-address', 'wss://sandbox.example.com', '--serve-recipe-json', '--serve-project-root', '/workspace/repo' ], expect.objectContaining({ cwd: resolve(__dirname, '../../..'), detached: true, stdio: ['ignore', 'pipe', 'inherit'] }) ) expect(writeSpy).toHaveBeenCalledWith(`${RECIPE_JSON}\n`) expect(child.unref).toHaveBeenCalled() }) it('waits past startup status lines for valid recipe JSON', async () => { const { child, result, stdoutSpy, stderrSpy } = startRecipeJsonServer() queueMicrotask(() => { child.stdout.emit( 'data', `${SERVE_INSTALL_STATUS}\n${INVALID_SSH_RECIPE_JSON}\n${SSH_RECIPE_JSON}\n${RECIPE_JSON.slice(0, 40)}` ) child.stdout.emit('data', `${RECIPE_JSON.slice(40)}\n`) }) await expect(result).resolves.toBe(0) expect(stderrSpy).toHaveBeenNthCalledWith(1, `${IGNORED_NON_RECIPE_STDOUT}\n`) expect(stderrSpy).toHaveBeenNthCalledWith(2, `${IGNORED_NON_RECIPE_STDOUT}\n`) expect(stderrSpy).toHaveBeenNthCalledWith(3, `${IGNORED_NON_RECIPE_STDOUT}\n`) for (const secret of [SSH_PRIVATE_KEY, SSH_AUTHORIZATION, SSH_PASSPHRASE, SSH_COOKIE]) { expect(stderrSpy).not.toHaveBeenCalledWith(expect.stringContaining(secret)) } expect(stdoutSpy).toHaveBeenCalledTimes(1) expect(stdoutSpy).toHaveBeenCalledWith(`${RECIPE_JSON}\n`) expect(child.unref).toHaveBeenCalledOnce() }) it('preserves UTF-8 recipe JSON split across Buffer chunks', async () => { const { child, result, stdoutSpy } = startRecipeJsonServer() const unicodeRecipeJson = RECIPE_JSON.replace('/workspace/repo', '/workspace/café') const recipeBuffer = Buffer.from(`${unicodeRecipeJson}\n`) const splitIndex = recipeBuffer.indexOf(Buffer.from('é')) + 1 queueMicrotask(() => { child.stdout.emit('data', recipeBuffer.subarray(0, splitIndex)) child.stdout.emit('data', recipeBuffer.subarray(splitIndex)) }) await expect(result).resolves.toBe(0) expect(stdoutSpy).toHaveBeenCalledWith(`${unicodeRecipeJson}\n`) expect(child.unref).toHaveBeenCalledOnce() }) it('rejects when the server exits without valid recipe JSON', async () => { const { child, result, stdoutSpy, stderrSpy } = startRecipeJsonServer() const secrets = ['UPPER-SECRET', 'SLASH-SECRET', 'LEGACY-SECRET', 'PRIVATE-SECRET'] const untrustedLines = [ 'ORCA://pair?code=UPPER-SECRET', 'orca://pair/?code=SLASH-SECRET', 'orca://pair#LEGACY-SECRET', '"embedded privateKey PRIVATE-SECRET"', '{privateKey:"PRIVATE-SECRET"}' ].join('\n') queueMicrotask(() => { child.stdout.emit('data', `${untrustedLines}\n`) child.emit('exit', 0, null) child.emit('close', 0, null) }) await expect(result).rejects.toMatchObject({ code: 'runtime_serve_failed', message: 'Orca serve exited before printing valid recipe JSON with code 0.' }) expect(stdoutSpy).not.toHaveBeenCalled() expect(stderrSpy).toHaveBeenCalledTimes(5) expect(stderrSpy).toHaveBeenCalledWith(`${IGNORED_NON_RECIPE_STDOUT}\n`) for (const secret of secrets) { expect(stderrSpy).not.toHaveBeenCalledWith(expect.stringContaining(secret)) } expect(child.unref).not.toHaveBeenCalled() }) it('accepts valid recipe JSON at exit without a trailing newline', async () => { const { child, result, stdoutSpy } = startRecipeJsonServer() queueMicrotask(() => { child.emit('exit', 0, null) child.stdout.emit('data', RECIPE_JSON) child.emit('close', 0, null) }) await expect(result).resolves.toBe(0) expect(stdoutSpy).toHaveBeenCalledWith(`${RECIPE_JSON}\n`) expect(child.unref).toHaveBeenCalledOnce() }) 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() }) })