Files
orca/src/main/startup/browser-process-user-agent-ordering.test.ts
T
OrcaWinandm4air bc519c57fa fix(serve): say what headless serve actually needs, and stop repeating the supervisor's restart line (#28230)
- A headless serve with no X display told the user to run `orca-ide serve`, which they just
  ran. Serve now gets its own message: it needs an X display, Wayland alone is not enough, and
  which X virtual framebuffer package to install (now including Arch's xorg-server-xvfb).
- The supervisor's restart line read 'Orca serve Orca serve exited via SIGKILL.; restarting'.
- The supervisor and 'serve service status' gave different advice after giving up; both now
  give the same, correct advice.

Fixes BUG-2 and BUG-9 from the real-host Linux test run.

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-13 01:28:02 -07:00

496 lines
19 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => {
const events: string[] = []
// Why a two-word app token: this file sets the dev app name to "Orca Development", and Electron
// builds the app token from that name. A single-token fixture could not exhibit the multi-word
// leak the cleaner exists to handle, so it disagreed with the scenario it set up.
// Why the engine comment: a real app.userAgentFallback always carries it, and the cleaner only
// touches identities that do — a fixture without it models a string Electron cannot produce.
let userAgent =
'Mozilla/5.0 (Test) AppleWebKit/537.36 (KHTML, like Gecko) Orca Development/0.0.0 Chrome/150.0.0.0 Electron/43.0.0 Safari/537.36'
const app = {
isPackaged: false,
exit: vi.fn(),
// Real `app` always carries this; preflight reads it to decide the Linux keyring backend.
commandLine: { hasSwitch: vi.fn(() => false), appendSwitch: vi.fn() },
getVersion: vi.fn(() => '1.0.0'),
getPath: vi.fn(() => '/canonical-user-data'),
get userAgentFallback(): string {
events.push('read-user-agent')
return userAgent
},
set userAgentFallback(value: string) {
events.push('write-user-agent')
userAgent = value
},
isReady: vi.fn(() => {
events.push('is-ready')
return false
}),
whenReady: vi.fn(() => Promise.resolve()),
setName: vi.fn((name: string) => {
events.push(`set-name:${name}`)
})
}
return {
app,
events,
userAgent: () => userAgent,
showErrorBox: vi.fn(),
backgroundLaunch: vi.fn(() => true),
admission: vi.fn(),
lock: vi.fn(() => true),
afterIdentity: vi.fn((): void => {
throw new Error('preflight-test-stop')
}),
recoverMoves: vi.fn(),
profileLock: vi.fn((): { state: string; message?: string } => ({ state: 'unavailable' }))
}
})
vi.mock('electron', () => ({
app: mocks.app,
dialog: { showErrorBox: mocks.showErrorBox },
ipcMain: {},
powerMonitor: {},
session: { defaultSession: {} }
}))
vi.mock('../window/foreground-activation-policy', () => ({
isBackgroundLaunch: mocks.backgroundLaunch
}))
vi.mock('@electron-toolkit/utils', () => ({ is: { dev: true } }))
vi.mock('./cli-launch-redirect', () => ({
maybeRedirectCliLaunch: () => ({ redirected: false, status: 0 })
}))
vi.mock('./serve-mode-argv', () => ({
argvRequestsServeMode: () => false,
normalizeServeModeArgv: (argv: string[]) => argv
}))
vi.mock('./configure-process', () => ({
configureDevUserDataPath: vi.fn(),
configureElectronNetworkCompatibility: vi.fn(),
configureOrcaUserDataPathEnv: vi.fn(),
disableUnsupportedChromiumFeatures: vi.fn(),
enableMainProcessGpuFeatures: vi.fn(),
installDevParentDisconnectQuit: vi.fn(),
installDevParentSignalQuit: vi.fn(),
installDevParentWatchdog: vi.fn(),
optOutOfHiddenPageWakeUpThrottling: vi.fn(),
patchPackagedProcessPath: vi.fn()
}))
vi.mock('../serve-update-handoff', () => ({ installServeSupervisorDisconnectQuit: vi.fn() }))
vi.mock('./main-process-error-guards', () => ({
installUncaughtPipeErrorGuard: vi.fn(),
installUnhandledRejectionLogging: vi.fn()
}))
vi.mock('./hydrate-shell-path')
vi.mock('../runtime/remote-server-updater', () => ({ configureRemoteServerUpdater: vi.fn() }))
vi.mock('../updater', () => ({
getRemoteServerUpdaterSnapshot: vi.fn(),
checkForRemoteServerUpdate: vi.fn(),
downloadRemoteServerUpdate: vi.fn(),
installRemoteServerUpdate: vi.fn(),
isQuittingForUpdate: () => false
}))
vi.mock('./dev-instance-identity', () => ({
getDevInstanceIdentity: () => ({
isDev: true,
appName: 'Orca Development',
appUserModelId: 'com.orca.development'
}),
shouldApplyPreReadyAppName: () => true
}))
vi.mock('./renderer-heap-headroom')
vi.mock('./startup-diagnostics', () => ({
isStartupDiagnosticsEnabled: () => false,
logStartupDiagnostic: vi.fn()
}))
vi.mock('./event-loop-stall-probe')
vi.mock('../diagnostics/main-thread-churn-probe')
vi.mock('../git/source-control/git-read-cache-invalidation', () => ({
settledDiffCache: { stats: vi.fn() }
}))
vi.mock('../server/serve-stdout-boundary')
vi.mock('./serve-desktop-activation', () => ({
createServeDesktopActivationGate: () => ({})
}))
vi.mock('./desktop-profile-instance-lock', () => ({
acquireDesktopProfileInstanceLock: mocks.profileLock
}))
vi.mock('./single-instance-lock', () => ({
shouldBypassSingleInstanceLock: () => false,
shouldSkipSingleInstanceLock: () => false,
acquireSingleInstanceLock: () => {
mocks.events.push('single-instance-lock')
return mocks.lock()
},
logSingleInstanceLockBypass: vi.fn(),
logSingleInstanceLockFailure: vi.fn(),
SINGLE_INSTANCE_ALREADY_RUNNING_EXIT_CODE: 3
}))
vi.mock('../../shared/app-environment', () => ({ setAppEnvironment: vi.fn() }))
vi.mock('../host/electron-app-environment', () => ({ ElectronAppEnvironment: class {} }))
vi.mock('../own-chromium-tree-kill-guard')
vi.mock('../../shared/secret-store', () => ({
setSecretStore: () => {
mocks.events.push('continued-after-browser-identity')
mocks.afterIdentity()
}
}))
vi.mock('../host/electron-secret-store')
vi.mock('../ipc/pty-host-bindings')
vi.mock('../host/electron-runtime-desktop-surface')
vi.mock('../runtime/runtime-desktop-surface')
vi.mock('../host/electron-browser-commands')
vi.mock('../runtime/runtime-browser-commands-factory')
vi.mock('../host/electron-http-client')
vi.mock('../network/http-client')
vi.mock('../host/electron-speech-services')
vi.mock('../speech/speech-runtime-service')
vi.mock('../ipc/worktree-watcher-removal')
vi.mock('../ipc/filesystem-watcher')
vi.mock('../network/proxy-settings')
vi.mock('../persistence', () => ({
initDataPath: () => mocks.events.push('init-data-path'),
getCanonicalUserDataPath: () => '/canonical-user-data'
}))
vi.mock('../persistence/profile-state/profile-state-access', () => ({
acquireProfileStateRuntimeAdmission: (root: string) => {
mocks.events.push(`admission:${root}`)
return mocks.admission()
}
}))
vi.mock('../macos-press-and-hold-default')
vi.mock('../ai-vault/session-parse-cache-persistence')
vi.mock('../orca-profiles/profile-index-store', () => ({ initOrcaProfilePaths: vi.fn() }))
vi.mock('../orca-profiles/profile-storage-paths', () => ({
getProfileUserDataPath: () => '/canonical-user-data'
}))
vi.mock('../orca-profiles/profile-project-move-intent', () => ({
recoverPendingProfileProjectMoves: mocks.recoverMoves
}))
vi.mock('../persistence/profile-state/profile-state-active-location', () => ({
getActiveProfileStateLocation: () => ({ profileId: 'active-profile' })
}))
vi.mock('../stats/collector')
vi.mock('../claude-usage/store')
vi.mock('../codex-usage/store')
vi.mock('../opencode-usage/store')
vi.mock('../browser/doc-preview-protocol')
vi.mock('../crash-reporting/crashpad-capture')
vi.mock('../crash-reporting/crash-report-store')
vi.mock('../crash-reporting/crash-breadcrumb-store')
vi.mock('../crash-reporting/durable-crash-breadcrumb')
vi.mock('../crash-reporting/gpu-crash-diagnostics')
vi.mock('../crash-reporting/main-process-lifecycle-identity')
vi.mock('./ensure-virtual-display', () => ({
ensureVirtualDisplayForHeadlessServe: vi.fn(),
hasUsableLinuxDisplay: () => true,
missingLinuxDisplayMessage: () => 'missing display'
}))
vi.mock('./gpu-lifecycle')
vi.mock('./main-process-state', () => ({ mainProcessState: {} }))
vi.mock('./synthetic-title-runtime')
vi.mock('../browser/browser-identity-mode-store', () => ({
initializeBrowserIdentityModeStore: (path: string) => {
mocks.events.push(`read-mode:${path}`)
return {
state: 'valid',
appliedMode: 'clean',
configuredMode: 'clean',
explicitSelection: true,
migrationNoticePending: false
}
}
}))
describe('browser process user-agent startup ordering', () => {
it('explains admission refusal before a desktop launch exits', async () => {
const { runMainProcessPreflight } = await import('./main-process-preflight')
const platform = vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
mocks.backgroundLaunch.mockReturnValueOnce(false)
mocks.admission.mockImplementationOnce(() => {
throw new Error('Stop Orca and orcad before retrying profile recovery')
})
const error = vi.spyOn(console, 'error').mockImplementation(() => {})
try {
expect(
runMainProcessPreflight({ focusExistingWindow: vi.fn(), requestDesktopActivation: vi.fn() })
).toBe(false)
expect(mocks.showErrorBox).toHaveBeenCalledWith(
'Orca could not start',
expect.stringContaining('Stop Orca and orcad before retrying profile recovery')
)
expect(mocks.app.isReady).not.toHaveBeenCalled()
} finally {
error.mockRestore()
platform.mockRestore()
mocks.showErrorBox.mockClear()
mocks.events.length = 0
}
})
it('explains an orcad holding the profile instead of exiting silently', async () => {
const { runMainProcessPreflight } = await import('./main-process-preflight')
const platform = vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
mocks.backgroundLaunch.mockReturnValueOnce(false)
mocks.profileLock.mockReturnValueOnce({ state: 'held', message: 'Another orcad (pid 7)' })
mocks.app.exit.mockClear()
mocks.events.length = 0
const error = vi.spyOn(console, 'error').mockImplementation(() => {})
try {
expect(
runMainProcessPreflight({ focusExistingWindow: vi.fn(), requestDesktopActivation: vi.fn() })
).toBe(false)
expect(mocks.showErrorBox).toHaveBeenCalledWith(
'Orca could not start',
'Another orcad (pid 7)'
)
expect(mocks.app.exit.mock.calls).toEqual([[3]])
expect(mocks.events).not.toContain('admission:/canonical-user-data')
} finally {
error.mockRestore()
platform.mockRestore()
mocks.showErrorBox.mockClear()
mocks.app.exit.mockClear()
mocks.events.length = 0
}
})
it('does not acquire profile admission for a duplicate launch', async () => {
const { runMainProcessPreflight } = await import('./main-process-preflight')
mocks.events.length = 0
mocks.lock.mockReturnValueOnce(false)
expect(
runMainProcessPreflight({ focusExistingWindow: vi.fn(), requestDesktopActivation: vi.fn() })
).toBe(false)
expect(mocks.events).not.toContain('admission:/canonical-user-data')
expect(mocks.events).not.toContain('read-mode:/canonical-user-data')
expect(mocks.app.exit).toHaveBeenCalledWith(3)
mocks.events.length = 0
})
it('executes after the dev app name and before later preflight work', async () => {
const { getBrowserProcessUserAgentIdentity } =
await import('../browser/browser-process-user-agent')
const { runMainProcessPreflight } = await import('./main-process-preflight')
expect(
runMainProcessPreflight({
focusExistingWindow: vi.fn(),
requestDesktopActivation: vi.fn()
})
).toBe(false)
const nameIndex = mocks.events.indexOf('set-name:Orca Development')
const modeIndex = mocks.events.indexOf('read-mode:/canonical-user-data')
const writeIndex = mocks.events.indexOf('write-user-agent')
const continuationIndex = mocks.events.indexOf('continued-after-browser-identity')
expect(mocks.events.indexOf('init-data-path')).toBeLessThan(nameIndex)
expect(mocks.events.indexOf('init-data-path')).toBeLessThan(
mocks.events.indexOf('admission:/canonical-user-data')
)
expect(mocks.events.indexOf('admission:/canonical-user-data')).toBeLessThan(modeIndex)
expect(nameIndex).toBeLessThan(modeIndex)
expect(modeIndex).toBeLessThan(writeIndex)
expect(writeIndex).toBeLessThan(continuationIndex)
expect(getBrowserProcessUserAgentIdentity()).toEqual({
mode: 'clean',
userAgent: mocks.userAgent()
})
// Both app-name words must be gone, not just the last: a single \S+ would have left "Orca".
expect(mocks.userAgent()).not.toMatch(/Electron/)
expect(mocks.userAgent()).not.toMatch(/Orca|Development/)
})
it('exits without reading profile state or revealing a window when recovery holds admission', async () => {
const { runMainProcessPreflight } = await import('./main-process-preflight')
mocks.events.length = 0
mocks.admission.mockImplementationOnce(() => {
throw new Error('Profile recovery is in progress')
})
const error = vi.spyOn(console, 'error').mockImplementation(() => {})
const focusExistingWindow = vi.fn()
const requestDesktopActivation = vi.fn()
try {
expect(runMainProcessPreflight({ focusExistingWindow, requestDesktopActivation })).toBe(false)
expect(mocks.app.exit).toHaveBeenCalledWith(1)
expect(mocks.events).toEqual([
'init-data-path',
'set-name:Orca Development',
'single-instance-lock',
'admission:/canonical-user-data'
])
expect(focusExistingWindow).not.toHaveBeenCalled()
expect(requestDesktopActivation).not.toHaveBeenCalled()
expect(mocks.showErrorBox).not.toHaveBeenCalled()
} finally {
error.mockRestore()
}
})
})
it('exits and releases admission after pending profile move recovery fails', async () => {
const { resetBrowserProcessUserAgentForTests } =
await import('../browser/browser-process-user-agent')
resetBrowserProcessUserAgentForTests()
const release = vi.fn()
mocks.admission.mockReturnValueOnce({ release })
mocks.afterIdentity.mockImplementationOnce(() => undefined)
mocks.recoverMoves.mockImplementationOnce(() => {
throw new Error('unreadable move journal')
})
const { runMainProcessPreflight } = await import('./main-process-preflight')
expect(
runMainProcessPreflight({ focusExistingWindow: vi.fn(), requestDesktopActivation: vi.fn() })
).toBe(false)
expect(mocks.recoverMoves).toHaveBeenCalledWith('/canonical-user-data', 'active-profile')
expect(release).toHaveBeenCalledOnce()
expect(mocks.app.exit).toHaveBeenCalledWith(1)
})
it('defers a Linux desktop startup failure until Electron is ready', async () => {
const { runMainProcessPreflight } = await import('./main-process-preflight')
const { resetBrowserProcessUserAgentForTests } =
await import('../browser/browser-process-user-agent')
resetBrowserProcessUserAgentForTests()
const release = vi.fn()
let resolveReady: (() => void) | undefined
const ready = new Promise<void>((resolve) => {
resolveReady = resolve
})
mocks.admission.mockReturnValueOnce({ release })
mocks.afterIdentity.mockImplementationOnce(() => {
throw new Error('Linux pre-ready failure')
})
mocks.app.whenReady.mockReturnValueOnce(ready)
mocks.app.exit.mockClear()
mocks.showErrorBox.mockClear()
const platform = vi.spyOn(process, 'platform', 'get').mockReturnValue('linux')
mocks.backgroundLaunch.mockReturnValueOnce(false)
const error = vi.spyOn(console, 'error').mockImplementation(() => {})
try {
expect(
runMainProcessPreflight({ focusExistingWindow: vi.fn(), requestDesktopActivation: vi.fn() })
).toBe(false)
expect(release).toHaveBeenCalledOnce()
expect(mocks.app.whenReady).toHaveBeenCalledOnce()
expect(mocks.showErrorBox).not.toHaveBeenCalled()
expect(mocks.app.exit).not.toHaveBeenCalled()
resolveReady?.()
await ready
await Promise.resolve()
expect(mocks.showErrorBox).toHaveBeenCalledWith(
'Orca could not start',
expect.stringContaining('Linux pre-ready failure')
)
expect(mocks.app.exit).toHaveBeenCalledWith(1)
} finally {
error.mockRestore()
platform.mockRestore()
mocks.app.whenReady.mockClear()
}
})
it('exits after a Linux desktop readiness rejection without showing a dialog', async () => {
const { runMainProcessPreflight } = await import('./main-process-preflight')
const { resetBrowserProcessUserAgentForTests } =
await import('../browser/browser-process-user-agent')
resetBrowserProcessUserAgentForTests()
const release = vi.fn()
let rejectReady!: (error: Error) => void
const ready = new Promise<void>((_resolve, reject) => {
rejectReady = reject
})
mocks.admission.mockReturnValueOnce({ release })
mocks.afterIdentity.mockImplementationOnce(() => {
throw new Error('Linux pre-ready failure')
})
mocks.app.whenReady.mockReturnValueOnce(ready)
mocks.app.exit.mockClear()
mocks.showErrorBox.mockClear()
const platform = vi.spyOn(process, 'platform', 'get').mockReturnValue('linux')
mocks.backgroundLaunch.mockReturnValueOnce(false)
const error = vi.spyOn(console, 'error').mockImplementation(() => {})
try {
expect(
runMainProcessPreflight({ focusExistingWindow: vi.fn(), requestDesktopActivation: vi.fn() })
).toBe(false)
expect(release).toHaveBeenCalledOnce()
rejectReady(new Error('Electron readiness failed'))
await ready.catch(() => undefined)
await Promise.resolve()
expect(mocks.showErrorBox).not.toHaveBeenCalled()
expect(mocks.app.exit).toHaveBeenCalledWith(1)
} finally {
error.mockRestore()
platform.mockRestore()
mocks.app.whenReady.mockClear()
}
})
it('keeps Linux background startup failures console-only and immediate', async () => {
const { runMainProcessPreflight } = await import('./main-process-preflight')
const { resetBrowserProcessUserAgentForTests } =
await import('../browser/browser-process-user-agent')
resetBrowserProcessUserAgentForTests()
const release = vi.fn()
mocks.admission.mockReturnValueOnce({ release })
mocks.afterIdentity.mockImplementationOnce(() => {
throw new Error('Linux background failure')
})
mocks.app.whenReady.mockClear()
mocks.app.exit.mockClear()
mocks.showErrorBox.mockClear()
const platform = vi.spyOn(process, 'platform', 'get').mockReturnValue('linux')
mocks.backgroundLaunch.mockReturnValueOnce(true)
const error = vi.spyOn(console, 'error').mockImplementation(() => {})
try {
expect(
runMainProcessPreflight({ focusExistingWindow: vi.fn(), requestDesktopActivation: vi.fn() })
).toBe(false)
expect(release).toHaveBeenCalledOnce()
expect(mocks.app.whenReady).not.toHaveBeenCalled()
expect(mocks.showErrorBox).not.toHaveBeenCalled()
expect(mocks.app.exit).toHaveBeenCalledWith(1)
} finally {
error.mockRestore()
platform.mockRestore()
}
})
it('keeps Linux serve startup failures console-only and immediate', async () => {
const { runMainProcessPreflight } = await import('./main-process-preflight')
const { resetBrowserProcessUserAgentForTests } =
await import('../browser/browser-process-user-agent')
resetBrowserProcessUserAgentForTests()
const release = vi.fn()
const originalArgv = process.argv
process.argv = originalArgv.includes('--serve') ? [...originalArgv] : [...originalArgv, '--serve']
mocks.admission.mockReturnValueOnce({ release })
mocks.afterIdentity.mockImplementationOnce(() => {
throw new Error('Linux serve failure')
})
mocks.app.whenReady.mockClear()
mocks.app.exit.mockClear()
mocks.showErrorBox.mockClear()
const platform = vi.spyOn(process, 'platform', 'get').mockReturnValue('linux')
mocks.backgroundLaunch.mockReturnValueOnce(false)
const error = vi.spyOn(console, 'error').mockImplementation(() => {})
try {
expect(
runMainProcessPreflight({ focusExistingWindow: vi.fn(), requestDesktopActivation: vi.fn() })
).toBe(false)
expect(release).toHaveBeenCalledOnce()
expect(mocks.app.whenReady).not.toHaveBeenCalled()
expect(mocks.showErrorBox).not.toHaveBeenCalled()
expect(mocks.app.exit).toHaveBeenCalledWith(1)
} finally {
error.mockRestore()
platform.mockRestore()
process.argv = originalArgv
}
})