mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 16:03:26 +00:00
* 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>
352 lines
11 KiB
TypeScript
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()
|
|
})
|
|
})
|