Files
orca/src/cli/runtime/launch.test.ts
a5faf19631 fix(cli): wait for valid serve recipe JSON (#8361)
* fix(cli): wait for valid serve recipe JSON

* fix(cli): harden recipe output diagnostics

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Siddharth Ahire <siddharth@Siddharths-MacBook-Air.local>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-12 21:17:46 -07:00

352 lines
11 KiB
TypeScript

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()
})
})