mirror of
https://github.com/stablyai/orca.git
synced 2026-10-26 16:02:42 +00:00
* 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.
117 lines
3.9 KiB
TypeScript
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.'
|
|
)
|
|
}
|
|
}
|
|
})
|
|
}
|