Files
orca/src/main/runtime/runtime-browser-commands-factory.ts
T
Neil e217fdd10f build(orcad): gate orcad's own graph, and prove it loads under plain Node (#16368)
* fix(orcad): close the browser-provider gaps

The providers landed without enforced coverage, so a regression in either path
would have landed silently.

- CI: the external-Chromium integration test was gated on ORCA_BROWSER_EXECUTABLE
  and nothing ever set it, so it skipped forever. It now runs in its own job
  against the runner's Chrome and FAILS when Chrome is absent rather than
  skipping, because an unset variable is exactly how it went uncovered. Timeout
  raised to 120s: a warm run is ~7s but the first launch against an unseeded
  profile took 30s and hit Vitest's default, and CI is always that cold case.
- Electron provider had no test at all. It is the path anyone with the desktop
  app hits.
- Browser unavailability reported one message for four causes, including telling
  an operator to set a variable they had already set.

Fixes a live defect found while covering it: the runtime advertises
browser.tabCreate.known-id.v1 unconditionally, so a web client sends a
provisional page id for a page that does not exist yet — and the sidecar's
generic requestedPageId branch ran require() on it first and threw. Every
known-id create against the Electron provider failed. The adoption logic was
already there; only the ordering was wrong.

Also updates the workflow-parallelism guard, which correctly caught the new job
missing from verify's required-check list, and asserts verify actually reads it.

* build(orcad): gate orcad's own graph, and prove it loads under plain Node

Two gaps the artifact's own comment asked for.

The ratchet measured only orca-runtime + runtime-rpc, but orcad imports ipc/pty
directly to install the PTY controller, so its graph is strictly larger. The gate
could read zero while the shipped artifact regressed. orcad's entry is now a
ratchet entry point, and the baseline stays empty with it included.

orcad cannot join plain-node-entry-guard — that is a rollup plugin keyed on
electron-vite input names, and orcad is an esbuild artifact. But the half that
matters here is the guard's smoke-load: scanning the metafile proves no module
NAMES electron, not that the graph resolves under plain Node. A dynamic require,
a missing native or a top-level throw all pass the scan and fail at runtime.
build-orcad now runs the bundle with a bogus flag and requires the argv rejection
that only a fully loaded graph can produce.

Verified: a bundle that builds but throws on load fails the gate.
2026-08-24 22:21:26 -07:00

117 lines
3.9 KiB
TypeScript

import { BrowserError } from '../browser/browser-error'
import {
BROWSER_UNAVAILABLE_ERROR_CODE,
type RuntimeBrowserUnavailableReason
} from '../../shared/runtime-types'
import type { RuntimeBrowserCommandHost, RuntimeBrowserCommands } from './orca-runtime-browser'
/**
* How `OrcaRuntimeService` obtains its browser-automation commands.
*
* Why a factory rather than a direct import: `orca-runtime-browser.ts` reaches the
* whole Chromium cluster — `BrowserWindow`, `session`, `webContents`, cookie jars —
* 15 modules that a Node host cannot load at all. Importing the class for its *type*
* is free; constructing it is what drags the cluster in.
*
* The desktop installs the Electron factory. A Node host installs an external provider
* when one resolves; otherwise every browser RPC rejects with the closed
* `browser_unavailable` code and capability filtering hides the affordance.
*
* Deliberately NOT a stub object with silently-succeeding methods: that is the
* "looks fine, returns a lie" shape this codebase rejects. Absent means rejected.
*/
export type RuntimeBrowserCommandsFactory = (
host: RuntimeBrowserCommandHost
) => RuntimeBrowserCommands
export type RuntimeBrowserCommandsFactoryOptions = {
/** The provider owns browser pages without a renderer window. */
headless?: boolean
/** Live health probe; failure must remove advertised capability. */
isAvailable?: () => boolean
}
let currentFactory: RuntimeBrowserCommandsFactory | null = null
let currentOptions: RuntimeBrowserCommandsFactoryOptions = {}
export function setRuntimeBrowserCommandsFactory(
factory: RuntimeBrowserCommandsFactory | null,
options: RuntimeBrowserCommandsFactoryOptions = {}
): void {
currentFactory = factory
currentOptions = factory ? options : {}
}
export function runtimeBrowserCommandsFactoryIsAvailable(): boolean {
if (!currentFactory) {
return false
}
try {
return currentOptions.isAvailable?.() !== false
} catch {
return false
}
}
export function runtimeBrowserCommandsFactoryIsHeadless(): boolean {
return runtimeBrowserCommandsFactoryIsAvailable() && currentOptions.headless === true
}
export type RuntimeBrowserUnavailableCause = {
reason: RuntimeBrowserUnavailableReason
detail?: string
}
let unavailableCause: RuntimeBrowserUnavailableCause | null = null
/**
* Recorded by whoever tried to resolve a provider, because only that code can tell a
* missing driver from an unset env var from a launch that threw. Null once one resolves.
*/
export function setRuntimeBrowserUnavailableCause(
cause: RuntimeBrowserUnavailableCause | null
): void {
unavailableCause = cause
}
/**
* The cause this process can prove. An installed factory outranks any recorded note: it
* means resolution succeeded, so the failure is later and elsewhere — a dead provider, or
* a renderer-backed factory with no window. Unknown beats guessing.
*/
export function runtimeBrowserUnavailableCause(): RuntimeBrowserUnavailableCause {
if (currentFactory) {
return runtimeBrowserCommandsFactoryIsAvailable()
? { reason: 'desktop_window_unavailable' }
: { reason: 'provider_unhealthy' }
}
return unavailableCause ?? { reason: 'unknown' }
}
/**
* Build the commands, or a rejecting proxy when this host has no browser. The proxy
* throws per call rather than at construction so the runtime still starts — the
* capability is simply not advertised.
*/
export function createRuntimeBrowserCommands(
host: RuntimeBrowserCommandHost
): RuntimeBrowserCommands {
if (currentFactory) {
return currentFactory(host)
}
return new Proxy({} as RuntimeBrowserCommands, {
get: (_target, property) => {
if (property === 'then') {
// Why: an awaited undefined must not look like a thenable.
return undefined
}
return () => {
throw new BrowserError(
BROWSER_UNAVAILABLE_ERROR_CODE,
'Browser automation is unavailable on this host.'
)
}
}
})
}