Files
orca/src/main/startup/single-instance-lock.test.ts
T
Wooseong KimandJinwoo-H f057cbc85f fix(serve): recognize CLI-form serve args on the Electron process (#12818)
* 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>
2026-08-06 23:56:34 -07:00

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