mirror of
https://github.com/stablyai/orca.git
synced 2026-10-04 08:02:09 +00:00
* fix(serve): recognize CLI-form serve args on the Electron process When the binary is launched as `… serve --port …` without the CLI rewrite that injects `--serve`, normalize argv so isServeMode, headless GPU flags, and serve option parsing all engage. Preserves existing `--serve*` flag behavior for the CLI-spawned path. Fixes #12677 * fix(serve): treat only CLI subcommand position as serve Parse bare `serve` as the first positional token after flags/values so an option value named `serve` cannot enable headless mode. Addresses CodeRabbit on #12818. * fix(serve): keep CLI redirects ahead of the serve argv rewrite Rewriting argv before maybeRedirectAppImageCliLaunch replaced the `serve` positional with `--serve`, so the redirect's command-name lookup saw a port number and bailed — dropping AppImage serve launches out of the CLI path. Also translate `--port=6768` (the CLI accepts it, getServeOptions only reads the next token) and the mixed `--serve --port` form, so a security-shaped flag like `--no-pairing` can no longer read as accepted while pairing stays on. Map lookups replace `in` on object literals, which turned a stray `serve toString` positional into a function spliced onto argv. * fix(serve): close the CLI-form serve gaps found in review second-instance: shouldActivateDesktopForSecondInstance matched only `--serve`, so a duplicate `<binary> serve --port …` — the ExecStart shape documented in docs/reference/headless-linux-server.md — promoted the live headless server to a desktop window, un-fixing #11935 on exactly the launch shape this PR legitimizes. findServeSubcommandIndex consumed a flag's value unconditionally while the rewrite consumed it only when the next token was not flag-shaped. The two could disagree and swallow the `serve` token, leaving `--serve` uninjected: #12677 again in a new shape (`--port --port serve`, `--port -- serve`). Both scans now share one definition of value consumption. `<binary> serve --help` / `serve help` bound a network-exposed runtime server with pairing on and printed nothing; the AppImage redirect already routes those three tokens to the CLI, so refuse them here too. `--no-pairing=false` translated to `--serve-no-pairing` with the value dropped, disabling pairing for an operator who asked for the opposite. The CLI reads its serve booleans as `flags.get(name) === true`, so a boolean is now translated only in its bare form and the `=` form rides through as the CLI treats it. Tests: spec-derived parity between src/cli/specs/serve.ts and the rewrite, covering both ends of the contract (serveOrcaApp and getServeOptions); a source-text lock on the index.ts redirect/rewrite ordering, which reverted silently green before; an exhaustive self-consistency property test; and the real GUI launch argv shapes that must never enter serve mode. --------- Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
187 lines
6.6 KiB
TypeScript
187 lines
6.6 KiB
TypeScript
import type { App } from 'electron'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
acquireSingleInstanceLock,
|
|
logSingleInstanceLockBypass,
|
|
logSingleInstanceLockFailure,
|
|
shouldActivateDesktopForSecondInstance,
|
|
shouldBypassSingleInstanceLock,
|
|
shouldSkipSingleInstanceLock,
|
|
SINGLE_INSTANCE_ALREADY_RUNNING_EXIT_CODE,
|
|
SINGLE_INSTANCE_LOCK_BYPASS_MESSAGE,
|
|
SINGLE_INSTANCE_LOCK_FAILURE_MESSAGE
|
|
} from './single-instance-lock'
|
|
|
|
type Listener = (...args: unknown[]) => void
|
|
|
|
function makeFakeApp(lockResult: boolean): {
|
|
app: App
|
|
requestSingleInstanceLock: ReturnType<typeof vi.fn>
|
|
on: ReturnType<typeof vi.fn>
|
|
listeners: Record<string, Listener[]>
|
|
} {
|
|
const listeners: Record<string, Listener[]> = {}
|
|
const requestSingleInstanceLock = vi.fn(() => lockResult)
|
|
const on = vi.fn((event: string, cb: Listener) => {
|
|
listeners[event] = listeners[event] ?? []
|
|
listeners[event].push(cb)
|
|
})
|
|
const app = {
|
|
requestSingleInstanceLock,
|
|
on
|
|
} as unknown as App
|
|
return { app, requestSingleInstanceLock, on, listeners }
|
|
}
|
|
|
|
describe('acquireSingleInstanceLock', () => {
|
|
it('returns false and does NOT register second-instance when the lock is held', () => {
|
|
const onSecondInstance = vi.fn()
|
|
const fake = makeFakeApp(false)
|
|
|
|
const acquired = acquireSingleInstanceLock(fake.app, onSecondInstance)
|
|
|
|
expect(acquired).toBe(false)
|
|
expect(fake.requestSingleInstanceLock).toHaveBeenCalledTimes(1)
|
|
// Why: if we registered the listener on a losing process, focusing the
|
|
// existing window would become our job even though the primary owns
|
|
// that UX surface. Verify no listener was added.
|
|
expect(fake.on).not.toHaveBeenCalled()
|
|
expect(fake.listeners['second-instance']).toBeUndefined()
|
|
})
|
|
|
|
it('returns true and registers exactly one second-instance listener when the lock is acquired', () => {
|
|
const onSecondInstance = vi.fn()
|
|
const fake = makeFakeApp(true)
|
|
|
|
const acquired = acquireSingleInstanceLock(fake.app, onSecondInstance)
|
|
|
|
expect(acquired).toBe(true)
|
|
expect(fake.requestSingleInstanceLock).toHaveBeenCalledTimes(1)
|
|
expect(fake.on).toHaveBeenCalledTimes(1)
|
|
expect(fake.on).toHaveBeenCalledWith('second-instance', expect.any(Function))
|
|
expect(fake.listeners['second-instance']).toHaveLength(1)
|
|
})
|
|
|
|
it('forwards the second launch argv so the owner can decide whether to activate', () => {
|
|
const onSecondInstance = vi.fn()
|
|
const fake = makeFakeApp(true)
|
|
|
|
acquireSingleInstanceLock(fake.app, onSecondInstance)
|
|
|
|
const [registered] = fake.listeners['second-instance'] ?? []
|
|
expect(registered).toBeDefined()
|
|
registered?.({}, ['/opt/orca/orca-linux.AppImage', '--serve'], '/home/orca')
|
|
|
|
expect(onSecondInstance).toHaveBeenCalledTimes(1)
|
|
expect(onSecondInstance).toHaveBeenCalledWith(['/opt/orca/orca-linux.AppImage', '--serve'])
|
|
})
|
|
})
|
|
|
|
describe('shouldActivateDesktopForSecondInstance', () => {
|
|
it('ignores a duplicate serve launch but still activates for a desktop launch', () => {
|
|
// Why: a supervisor respawning `orca serve` must not open a window on a display-less host (#11935).
|
|
const serveArgv = ['/opt/orca/orca-linux.AppImage', '--serve']
|
|
expect(shouldActivateDesktopForSecondInstance(serveArgv)).toBe(false)
|
|
expect(shouldActivateDesktopForSecondInstance(['/Applications/Orca.app/orca'])).toBe(true)
|
|
})
|
|
|
|
it('ignores a duplicate CLI-form serve launch the CLI redirect never rewrote', () => {
|
|
// Why: the documented systemd unit is `<binary> serve --port 6768 …`; an extracted AppRun/binary
|
|
// start reaches Electron in that shape, so a flag-only check would open a window on the live server.
|
|
expect(
|
|
shouldActivateDesktopForSecondInstance([
|
|
'/opt/orca/squashfs-root/orca-ide',
|
|
'serve',
|
|
'--port',
|
|
'6768',
|
|
'--pairing-address',
|
|
'100.64.1.20'
|
|
])
|
|
).toBe(false)
|
|
// A path argument that merely contains `serve` is still a desktop launch.
|
|
expect(
|
|
shouldActivateDesktopForSecondInstance(['/opt/orca/orca-ide', '/home/u/serve-repo'])
|
|
).toBe(true)
|
|
})
|
|
|
|
it('fails open when no argv is available', () => {
|
|
expect(shouldActivateDesktopForSecondInstance([])).toBe(true)
|
|
expect(shouldActivateDesktopForSecondInstance()).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('SINGLE_INSTANCE_ALREADY_RUNNING_EXIT_CODE', () => {
|
|
it('stays 3 because the documented systemd unit keys RestartPreventExitStatus off it', () => {
|
|
expect(SINGLE_INSTANCE_ALREADY_RUNNING_EXIT_CODE).toBe(3)
|
|
})
|
|
})
|
|
|
|
describe('shouldSkipSingleInstanceLock', () => {
|
|
it('keeps ordinary dev multi-instance behavior but never skips for serve', () => {
|
|
expect(shouldSkipSingleInstanceLock({ isDev: true, isServeMode: false, env: {} })).toBe(true)
|
|
expect(shouldSkipSingleInstanceLock({ isDev: true, isServeMode: true, env: {} })).toBe(false)
|
|
expect(shouldSkipSingleInstanceLock({ isDev: false, isServeMode: false, env: {} })).toBe(false)
|
|
})
|
|
|
|
it('lets isolated E2E exercise the production single-instance path', () => {
|
|
expect(
|
|
shouldSkipSingleInstanceLock({
|
|
isDev: true,
|
|
isServeMode: false,
|
|
env: { ORCA_E2E_ENFORCE_SINGLE_INSTANCE_LOCK: '1' }
|
|
})
|
|
).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('logSingleInstanceLockFailure', () => {
|
|
it('emits a production-visible synchronous diagnostic for the early quit path', () => {
|
|
const write = vi.fn()
|
|
|
|
logSingleInstanceLockFailure(write)
|
|
|
|
expect(write).toHaveBeenCalledWith(2, `${SINGLE_INSTANCE_LOCK_FAILURE_MESSAGE}\n`)
|
|
expect(write.mock.calls[0]?.[1]).toContain('Electron/macOS single-instance lock failure')
|
|
})
|
|
})
|
|
|
|
describe('shouldBypassSingleInstanceLock', () => {
|
|
it('allows the hidden diagnostic bypass only for packaged macOS app launches', () => {
|
|
expect(
|
|
shouldBypassSingleInstanceLock({
|
|
env: { ORCA_BYPASS_SINGLE_INSTANCE_LOCK: '1' },
|
|
isDev: false,
|
|
isServeMode: false,
|
|
platform: 'darwin'
|
|
})
|
|
).toBe(true)
|
|
expect(
|
|
shouldBypassSingleInstanceLock({
|
|
env: { ORCA_BYPASS_SINGLE_INSTANCE_LOCK: '1' },
|
|
isDev: true,
|
|
isServeMode: false,
|
|
platform: 'darwin'
|
|
})
|
|
).toBe(false)
|
|
expect(
|
|
shouldBypassSingleInstanceLock({
|
|
env: { ORCA_BYPASS_SINGLE_INSTANCE_LOCK: '1' },
|
|
isDev: false,
|
|
isServeMode: false,
|
|
platform: 'linux'
|
|
})
|
|
).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('logSingleInstanceLockBypass', () => {
|
|
it('emits a warning when the diagnostic bypass is active', () => {
|
|
const write = vi.fn()
|
|
|
|
logSingleInstanceLockBypass(write)
|
|
|
|
expect(write).toHaveBeenCalledWith(2, `${SINGLE_INSTANCE_LOCK_BYPASS_MESSAGE}\n`)
|
|
expect(write.mock.calls[0]?.[1]).toContain('bypassing the packaged macOS single-instance lock')
|
|
})
|
|
})
|