From 653aa2db20a3b8239c7ab2e1bcd4d5d3f686ccae Mon Sep 17 00:00:00 2001 From: Neil Date: Sat, 17 Oct 2026 23:25:25 -0700 Subject: [PATCH] test(orcad): orca serve's argv makes orcad serve the web client and 404 other paths (#30120) --- src/cli/runtime/launch.ts | 8 +-- src/cli/runtime/serve-orcad-launch.test.ts | 3 +- src/cli/runtime/serve-orcad-launch.ts | 39 +------------ .../orcad-serve-parity.integration.test.ts | 57 +++++++++++++++---- src/shared/orcad-serve-argv.ts | 41 +++++++++++++ 5 files changed, 93 insertions(+), 55 deletions(-) create mode 100644 src/shared/orcad-serve-argv.ts diff --git a/src/cli/runtime/launch.ts b/src/cli/runtime/launch.ts index 94a2aa7a06b7..45797d858cca 100644 --- a/src/cli/runtime/launch.ts +++ b/src/cli/runtime/launch.ts @@ -26,12 +26,8 @@ import { import { RuntimeClientError } from './types' import { SERVE_RUNTIME_ELECTRON, SERVE_RUNTIME_ENV } from '../../shared/orcad-local-serve-selection' import { findBundledWebClientRoot } from '../../shared/bundled-web-client-root' -import { - resolveLocalServeRuntime, - serveOptionArgs, - serveWithOrcad, - type ServeOrcaAppArgs -} from './serve-orcad-launch' +import { resolveLocalServeRuntime, serveWithOrcad } from './serve-orcad-launch' +import { serveOptionArgs, type ServeOrcaAppArgs } from '../../shared/orcad-serve-argv' import { waitForRecipeJson } from './serve-recipe-json' import { preflightElectronServe, diff --git a/src/cli/runtime/serve-orcad-launch.test.ts b/src/cli/runtime/serve-orcad-launch.test.ts index 534105fe9274..1bc1bf4b654e 100644 --- a/src/cli/runtime/serve-orcad-launch.test.ts +++ b/src/cli/runtime/serve-orcad-launch.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it, vi } from 'vitest' import { formatServeRuntimeSelection } from '../../shared/orcad-local-serve-selection' import type { runProcess } from '@orca/process-host' -import { orcadServeArgs, resolveLocalServeRuntime, serveOptionArgs } from './serve-orcad-launch' +import { resolveLocalServeRuntime } from './serve-orcad-launch' +import { orcadServeArgs, serveOptionArgs } from '../../shared/orcad-serve-argv' type RunProcess = typeof runProcess diff --git a/src/cli/runtime/serve-orcad-launch.ts b/src/cli/runtime/serve-orcad-launch.ts index 130142c2516a..302c83d2e820 100644 --- a/src/cli/runtime/serve-orcad-launch.ts +++ b/src/cli/runtime/serve-orcad-launch.ts @@ -15,21 +15,7 @@ import { waitForRecipeJson } from './serve-recipe-json' import { superviseForegroundServe } from './serve-update-supervisor' import { isSupervisedSpawnHeld } from './serve-supervised-stop' import { redactingSpawn, shouldRedactSupervisedStdout } from './serve-pairing-redaction' - -export type ServeOrcaAppArgs = { - json?: boolean - port?: string | null - pairingAddress?: string | null - noPairing?: boolean - mobilePairing?: boolean - grantDesktopControl?: boolean - recipeJson?: boolean - projectRoot?: string | null - /** Starts Electron serve despite a remembered macOS startup abort in this environment. */ - force?: boolean - /** Restart the server under a service manager instead of exiting with it; never inferred. */ - supervise?: boolean -} +import { orcadServeArgs, type ServeOrcaAppArgs } from '../../shared/orcad-serve-argv' /** A first run may download and verify the pinned Node; bound it well past that. */ const SELECTION_TIMEOUT_MS = 10 * 60_000 @@ -119,26 +105,3 @@ export function serveWithOrcad( expectedHandoff: null }) } - -export function serveOptionArgs(args: ServeOrcaAppArgs, prefix: '--' | '--serve-'): string[] { - return [ - ...(args.json ? [`${prefix}json`] : []), - ...(args.port ? [`${prefix}port`, args.port] : []), - ...(args.pairingAddress ? [`${prefix}pairing-address`, args.pairingAddress] : []), - ...(args.noPairing ? [`${prefix}no-pairing`] : []), - ...(args.mobilePairing ? [`${prefix}mobile-pairing`] : []), - ...(args.grantDesktopControl ? [`${prefix}grant-desktop-control`] : []), - ...(args.recipeJson && args.projectRoot - ? [`${prefix}recipe-json`, `${prefix}project-root`, args.projectRoot] - : []) - ] -} - -export function orcadServeArgs(args: ServeOrcaAppArgs, webClientRoot?: string): string[] { - return [ - '--bind', - '0.0.0.0', - ...serveOptionArgs(args, '--'), - ...(webClientRoot ? ['--web-client-root', webClientRoot] : []) - ] -} diff --git a/src/main/orcad/orcad-serve-parity.integration.test.ts b/src/main/orcad/orcad-serve-parity.integration.test.ts index 942c71f2c19d..34285f83c35c 100644 --- a/src/main/orcad/orcad-serve-parity.integration.test.ts +++ b/src/main/orcad/orcad-serve-parity.integration.test.ts @@ -1,14 +1,19 @@ /** * `orca serve` on orcad, against the packaged slot: the native preflight the launcher asks - * first, desktop-serve flag parity on stdout (the readiness contract), and the shared-profile - * refusal while the desktop app holds the profile. + * first, desktop-serve flag parity on stdout (the readiness contract), the browser client the + * CLI hands orcad, and the shared-profile refusal while the desktop app holds the profile. */ -import { existsSync, mkdtempSync, realpathSync, writeFileSync } from 'node:fs' +import { existsSync, mkdirSync, mkdtempSync, realpathSync, writeFileSync } from 'node:fs' import { tmpdir, userInfo } from 'node:os' import { join, resolve } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { runProcess, spawnProcess } from '@orca/process-host' import { ORCAD_NODE_RUNTIME_MARKER_FILENAME } from '../../shared/orcad-artifacts' +import { + findBundledWebClientRoot, + WEB_CLIENT_INDEX_FILENAME +} from '../../shared/bundled-web-client-root' +import { orcadServeArgs } from '../../shared/orcad-serve-argv' import { ORCAD_NATIVE_PREFLIGHT_FLAG, parseOrcadNativePreflightReport @@ -189,6 +194,32 @@ describe.skipIf(skip)('orca serve on orcad', () => { expect(result.stderr).toContain('The Orca desktop app') }, 90_000) + // #30120: the CLI never handed orcad the app's web client, so no URL was printed and GETs hung. + it('serves the web client the CLI hands it, and answers other paths with 404', async () => { + const userData = realpathSync( + mkdtempSync(join(process.platform === 'win32' ? tmpdir() : '/tmp', 'orca-serve-web-')) + ) + roots.push(userData) + const appRoot = profile() + mkdirSync(join(appRoot, 'out', 'web'), { recursive: true }) + writeFileSync(join(appRoot, 'out', 'web', WEB_CLIENT_INDEX_FILENAME), 'orca web') + // The argv `orca serve` builds, but on loopback: a test must not listen on every interface. + const args = orcadServeArgs({ json: true, port: '0' }, findBundledWebClientRoot(appRoot)) + args.splice(args.indexOf('0.0.0.0'), 1, '127.0.0.1') + try { + const readiness = await serveOnce(userData, args) + const webClientUrl: unknown = readiness.pairing?.webClientUrl + expect(webClientUrl).toEqual(expect.stringMatching(/^http:\/\/127\.0\.0\.1:\d+\//u)) + const index = await fetch(new URL(`/${WEB_CLIENT_INDEX_FILENAME}`, String(webClientUrl))) + expect(index.status).toBe(200) + expect(await index.text()).toBe('orca web') + const unknown = await fetch(new URL('/no-such-page', String(webClientUrl))) + expect(unknown.status).toBe(404) + } finally { + await stopServeProfile(userData, null) + } + }, 150_000) + // A short /tmp root keeps the POSIX daemon socket path legal; Windows uses a named pipe. it('keeps a live terminal across a serve restart on the shared profile (D7)', async () => { const userData = realpathSync( @@ -207,7 +238,7 @@ describe.skipIf(skip)('orca serve on orcad', () => { expect(result.code, result.stderr).toBe(0) return JSON.parse(result.stdout.trim().split('\n').at(-1) ?? '{}') } - const first = await serveOnce(userData) + const first = await serveDaemonPid(userData) let shellPid: number | null = null try { const created = await session('create', 'BEFORE') @@ -215,7 +246,7 @@ describe.skipIf(skip)('orca serve on orcad', () => { expect(created).toMatchObject({ isReattach: false, output: true }) await stopServe(userData) // The same daemon, in the same \`/daemon\` Electron serve uses, is adopted. - expect(await serveOnce(userData)).toBe(first) + expect(await serveDaemonPid(userData)).toBe(first) expect(await session('attach', 'AFTER')).toEqual({ pid: created.pid, isReattach: true, @@ -230,10 +261,18 @@ describe.skipIf(skip)('orca serve on orcad', () => { const running = new Map>() /** Starts `orca serve --json` on orcad and returns the terminal daemon pid its readiness names. */ -async function serveOnce(userData: string): Promise { +async function serveDaemonPid(userData: string): Promise { + const readiness = await serveOnce(userData, ['--bind', '127.0.0.1', '--port', '0', '--json']) + const daemon = readiness.health?.terminalDaemon + expect(daemon?.state).toBe('live') + return daemon.pid +} + +/** Starts orcad with `args` and returns its parsed readiness line. */ +async function serveOnce(userData: string, args: string[]) { const child = spawnProcess({ program: runtime!, - args: [join(slotDir, 'orcad.js'), '--bind', '127.0.0.1', '--port', '0', '--json'], + args: [join(slotDir, 'orcad.js'), ...args], env: serveEnv(userData), timeoutMs: null }) @@ -252,9 +291,7 @@ async function serveOnce(userData: string): Promise { } }) }) - const daemon = JSON.parse(line).health?.terminalDaemon - expect(daemon?.state, stderr).toBe('live') - return daemon.pid + return JSON.parse(line) } /** SIGTERM is serve's graceful stop on POSIX; it leaves the daemon running by design. */ diff --git a/src/shared/orcad-serve-argv.ts b/src/shared/orcad-serve-argv.ts new file mode 100644 index 000000000000..5abf293d1175 --- /dev/null +++ b/src/shared/orcad-serve-argv.ts @@ -0,0 +1,41 @@ +/** + * The argv `orca serve` hands a serve host: Electron as `--serve-*`, orcad as plain flags. Shared so + * orcad's own tests run the exact command line the CLI builds (#30120 dropped one across it). + */ +export type ServeOrcaAppArgs = { + json?: boolean + port?: string | null + pairingAddress?: string | null + noPairing?: boolean + mobilePairing?: boolean + grantDesktopControl?: boolean + recipeJson?: boolean + projectRoot?: string | null + /** Starts Electron serve despite a remembered macOS startup abort in this environment. */ + force?: boolean + /** Restart the server under a service manager instead of exiting with it; never inferred. */ + supervise?: boolean +} + +export function serveOptionArgs(args: ServeOrcaAppArgs, prefix: '--' | '--serve-'): string[] { + return [ + ...(args.json ? [`${prefix}json`] : []), + ...(args.port ? [`${prefix}port`, args.port] : []), + ...(args.pairingAddress ? [`${prefix}pairing-address`, args.pairingAddress] : []), + ...(args.noPairing ? [`${prefix}no-pairing`] : []), + ...(args.mobilePairing ? [`${prefix}mobile-pairing`] : []), + ...(args.grantDesktopControl ? [`${prefix}grant-desktop-control`] : []), + ...(args.recipeJson && args.projectRoot + ? [`${prefix}recipe-json`, `${prefix}project-root`, args.projectRoot] + : []) + ] +} + +export function orcadServeArgs(args: ServeOrcaAppArgs, webClientRoot?: string): string[] { + return [ + '--bind', + '0.0.0.0', + ...serveOptionArgs(args, '--'), + ...(webClientRoot ? ['--web-client-root', webClientRoot] : []) + ] +}