mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
test: remove low-value main-process suites and browser probes (#33889)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -1089,10 +1089,8 @@ jobs:
|
||||
xvfb-run --auto-servernum pnpm exec vitest run --config config/vitest.config.ts
|
||||
--no-file-parallelism
|
||||
src/main/browser/browser-client-page-renderer-lifecycle.electron.test.ts
|
||||
src/main/browser/browser-route-tcp-egress.electron.test.ts
|
||||
src/main/browser/browser-route-webrtc-egress.electron.test.ts
|
||||
src/main/browser/browser-route-h3-egress.electron.test.ts
|
||||
src/main/browser/browser-route-dns-prefetch.electron.test.ts
|
||||
src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts
|
||||
src/main/browser/popup-origin-chrome.electron.test.ts
|
||||
src/main/browser/browser-link-materialization.electron.test.ts
|
||||
|
||||
@@ -1194,10 +1194,7 @@
|
||||
1 test-timeout-needs-reason:src/main/browser/agent-browser-bridge-raw-process.test.ts
|
||||
1 test-timeout-needs-reason:src/main/browser/browser-client-page-renderer-lifecycle.electron.test.ts
|
||||
1 test-timeout-needs-reason:src/main/browser/browser-find-frame-traversal.electron.test.ts
|
||||
1 test-timeout-needs-reason:src/main/browser/browser-route-dns-prefetch.electron.test.ts
|
||||
1 test-timeout-needs-reason:src/main/browser/browser-route-h3-egress.electron.test.ts
|
||||
1 test-timeout-needs-reason:src/main/browser/browser-route-persisted-worker-egress.electron.test.ts
|
||||
1 test-timeout-needs-reason:src/main/browser/browser-route-tcp-egress.electron.test.ts
|
||||
1 test-timeout-needs-reason:src/main/browser/browser-route-webrtc-egress.electron.test.ts
|
||||
1 test-timeout-needs-reason:src/main/browser/browser-session-ua-cloudflare-live.electron.test.ts
|
||||
2 test-timeout-needs-reason:src/main/browser/browser-session-ua-wire-identity-cross-context.electron.test.ts
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
2 src/main/browser/cookies/sources/browser-cookie-key.ts
|
||||
1 src/main/browser/browser-route-egress-electron-launch.ts
|
||||
1 src/main/browser/browser-route-partition-binding-store.ts
|
||||
1 src/main/browser/browser-route-persisted-worker-electron-process.ts
|
||||
1 src/main/claude-accounts/runtime-auth-service.ts
|
||||
3 src/main/codex-accounts/fs-utils.ts
|
||||
2 src/main/codex-accounts/legacy-wsl-runtime-auth-drain-recovery-script-harness.ts
|
||||
|
||||
@@ -20,7 +20,6 @@ src/main/browser/agent-browser-bridge-raw-process.ts
|
||||
src/main/browser/agent-browser-bridge-state.ts
|
||||
src/main/browser/agent-browser-bridge-types.ts
|
||||
src/main/browser/browser-route-egress-electron-launch.ts
|
||||
src/main/browser/browser-route-persisted-worker-electron-process.ts
|
||||
src/main/codex-accounts/legacy-wsl-runtime-auth-drain-recovery-script-harness.ts
|
||||
src/main/codex-accounts/legacy-wsl-runtime-auth-drain-script-harness.ts
|
||||
src/main/codex-accounts/service.ts
|
||||
|
||||
@@ -5286,7 +5286,7 @@
|
||||
"providers": ["local", "daemon", "ssh"],
|
||||
"coveredPlatforms": ["macos"],
|
||||
"coveredProviders": ["local", "daemon", "ssh"],
|
||||
"coverageNotes": "Deterministic unit coverage exercises activation gating, single-instance ownership, quit policy, local/remote CLI status, headless binding persistence, local daemon identity, SSH identity transfer, the promoted renderer's agent-resume accounting, and the macOS serve update handoff from staged installer through atomic bundle replacement and target-version readiness. Supervisor settlement coverage prevents a late handoff-completion failure from rearming termination after the replacement child exits. A macOS Electron journey covers headless promotion and persistent PTY identity. A disposable locally signed Electron canary exercised real ShipIt and a temporary LaunchAgent with the compiled production supervisor; full packaged Orca and Linux/Windows serve updates remain uncollected.",
|
||||
"coverageNotes": "Deterministic unit coverage exercises activation gating, single-instance ownership, local/remote CLI status, headless binding persistence, local daemon identity, SSH identity transfer, the promoted renderer's agent-resume accounting, and the macOS serve update handoff from staged installer through atomic bundle replacement and target-version readiness. Supervisor settlement coverage prevents a late handoff-completion failure from rearming termination after the replacement child exits. A macOS Electron journey covers headless promotion and persistent PTY identity. A disposable locally signed Electron canary exercised real ShipIt and a temporary LaunchAgent with the compiled production supervisor; full packaged Orca and Linux/Windows serve updates remain uncollected.",
|
||||
"motivatingLinks": [
|
||||
"https://github.com/stablyai/orca/issues/8457",
|
||||
"https://github.com/stablyai/orca/issues/9563"
|
||||
@@ -5296,7 +5296,7 @@
|
||||
"commands": [
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/cli/runtime/launch.test.ts src/main/serve-update-handoff.test.ts src/main/updater.headless-serve-install.test.ts src/main/updater.mac-install.test.ts src/main/window/attach-main-window-services.test.ts",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/cli/runtime/serve-signal-exit-diagnostic.test.ts",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/main/startup/serve-desktop-activation.test.ts src/main/startup/single-instance-lock.test.ts src/main/startup/window-all-closed-quit-policy.test.ts src/cli/runtime-client.test.ts src/cli/runtime/websocket-transport.test.ts src/main/runtime/orca-runtime.test.ts",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/main/startup/serve-desktop-activation.test.ts src/main/startup/single-instance-lock.test.ts src/cli/runtime-client.test.ts src/cli/runtime/websocket-transport.test.ts src/main/runtime/orca-runtime.test.ts",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/lib/serve-desktop-promotion-session-continuity.test.ts",
|
||||
"pnpm exec electron-vite build --mode e2e",
|
||||
"pnpm run test:e2e -- tests/e2e/headless-serve-desktop-activation.spec.ts --workers=1"
|
||||
@@ -5308,7 +5308,6 @@
|
||||
"src/cli/runtime/serve-signal-exit-diagnostic.test.ts",
|
||||
"src/main/startup/serve-desktop-activation.test.ts",
|
||||
"src/main/startup/single-instance-lock.test.ts",
|
||||
"src/main/startup/window-all-closed-quit-policy.test.ts",
|
||||
"src/cli/runtime-client.test.ts",
|
||||
"src/cli/runtime/websocket-transport.test.ts",
|
||||
"src/main/runtime/orca-runtime.test.ts",
|
||||
@@ -5363,12 +5362,6 @@
|
||||
"the isolated E2E profile can opt into the production ownership path"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/main/startup/window-all-closed-quit-policy.test.ts",
|
||||
"assertions": [
|
||||
"a promoted serve owner remains alive after an ordinary window close but exits after a committed quit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/cli/runtime-client.test.ts",
|
||||
"assertions": [
|
||||
@@ -7634,30 +7627,25 @@
|
||||
"providers": ["remote-runtime", "ssh", "wsl"],
|
||||
"coveredPlatforms": ["macos", "linux"],
|
||||
"coveredProviders": ["ssh", "remote-runtime"],
|
||||
"coverageNotes": "Deterministic main-process tests cover versioned delimiter-safe aggregate partition derivation, path-safe opaque names, durable collision metadata, oversized or corrupt metadata refusal, missing-sidecar Chromium-data refusal, bounded binding/live-page admission, immediate SOCKS5 setup with Chromium loopback bypass disabled, inherited-connection closure, exact proxy verification before allowlisting, concurrent setup coalescing, live proxy-retarget refusal, token-safe page replacement, existing browser-profile policy installation, active Orca-profile storage scoping, blank-only initial attachment, arbitrary initial-navigation denial, and fail-closed per-guest WebRTC policy through delayed or failed cleanup. The production client-page executor prepares, registers, and grants the exact route page. A real Electron A/B capture proves HTTP, HTTPS, WebSocket, redirects, subresources, downloads, and a `.test` hostname traverse SOCKS with no direct target connection. A two-launch control proves immediate setProxy routes a forced persisted-worker wake and later worker fetch. A separate capture proves the protected guest sends zero direct STUN packets. A further capture proves non-WebRTC UDP is also contained: a WebTransport session and a fetch forced onto QUIC both reach the desktop directly in the control arm and emit zero datagrams through the route partition, and the shipped disable-features list hides the Direct Sockets constructors whose mere construction kills a control-arm renderer. DNS prefetch is a tripwire over an accepted residual rather than a guard: Electron 43 inherits Chromium's PrefetchDNS, so a `<link rel=\"dns-prefetch\">` host resolves on the desktop resolver outside the tunnel, and a source census keeps any DoH host-resolver mode from widening that leak. Network-service restart and WSL provider journeys remain uncovered. Four Linux Docker SSH browser baseline scenarios passed: direct-host routing, unavailable-host local escape, forwarding refusal, and paired client-hosted reconnect. All four scenarios subsequently passed three repetitions each (12 passes, no skips or retries) in Linux CI run 34040309638, with unchanged assertions and timeouts.",
|
||||
"coverageNotes": "Deterministic main-process tests cover versioned delimiter-safe aggregate partition derivation, path-safe opaque names, durable collision metadata, oversized or corrupt metadata refusal, missing-sidecar Chromium-data refusal, bounded binding/live-page admission, immediate SOCKS5 setup with Chromium loopback bypass disabled, inherited-connection closure, exact proxy verification before allowlisting, concurrent setup coalescing, live proxy-retarget refusal, token-safe page replacement, existing browser-profile policy installation, active Orca-profile storage scoping, blank-only initial attachment, arbitrary initial-navigation denial, and fail-closed per-guest WebRTC policy through delayed or failed cleanup. The production client-page executor prepares, registers, and grants the exact route page. Archived standalone Electron experiments measured TCP routing and a forced persisted-worker wake using fixture-owned proxy setup; they did not exercise production route preparation and are no longer current test coverage. A separate capture proves the protected guest sends zero direct STUN packets. A further capture proves non-WebRTC UDP is also contained: a WebTransport session and a fetch forced onto QUIC both reach the desktop directly in the control arm and emit zero datagrams through the route partition, and the shipped disable-features list hides the Direct Sockets constructors whose mere construction kills a control-arm renderer. An archived DNS-prefetch experiment observed an accepted Electron 43 residual: prefetched hostnames resolved on the desktop outside the tunnel. It asserted the leak and was removed; current production-path TCP, persisted-worker, and DNS-prefetch egress coverage remains incomplete. Network-service restart and WSL provider journeys remain uncovered. Four Linux Docker SSH browser baseline scenarios passed: direct-host routing, unavailable-host local escape, forwarding refusal, and paired client-hosted reconnect. All four scenarios subsequently passed three repetitions each (12 passes, no skips or retries) in Linux CI run 34040309638, with unchanged assertions and timeouts.",
|
||||
"motivatingLinks": [
|
||||
"https://linear.app/stably/issue/STA-4150/refactor-remote-browser-to-client-hosted-electron-webviews",
|
||||
"https://github.com/stablyai/orca/actions/runs/34039986047",
|
||||
"https://github.com/stablyai/orca/actions/runs/34040309638"
|
||||
],
|
||||
"invariant": "A client-hosted partition is derived only in main from stable Orca-profile, browser-profile, authority-connection, and execution-host identities. Raw identities and individually linkable component hashes never enter its path-safe partition name. Durable binding metadata must match and precede Chromium partition data before reuse. One live partition never changes execution host or proxy endpoint. Fixed SOCKS5 setup starts immediately after Session creation, before policy installation can yield or a persisted worker is awakened; no partition enters the webview allowlist until browser policy is installed, inherited connections are closed, and resolveProxy returns exactly that one listener. Initial route-partition attachment is blank-only. Its exact WebContents is quarantined before applying non-proxied WebRTC denial and remains navigation- and popup-denied if policy application or cleanup fails. Distinct live partitions, retained logical page generations, durable bindings, and binding-file reads remain bounded. No UDP transport a route-partition page can reach — WebRTC, WebTransport, or forced QUIC — emits a datagram to the desktop, the Direct Sockets constructors stay absent from every guest so no page can kill its renderer, and the process never enables a DoH host-resolver mode.",
|
||||
"oracle": "Derive two delimiter-adversarial identities and require distinct full-digest path-safe partitions with no raw IDs or component hashes. Persist one binding, reload it, and reject replacement, malformed or oversized state, Chromium data without matching metadata, and the 513th binding. Prepare one partition and require setProxy with <-loopback> to be invoked immediately after getSession and before policy setup, then closeAllConnections and exact SOCKS5 resolveProxy while isAllowedPartition remains false; only then may it become live. Under real Electron, require direct controls for HTTP, HTTPS, WebSocket, redirects, subresources, and downloads, then require the fixed SOCKS session to route every equivalent request plus an otherwise-unresolvable `.test` hostname with zero direct target connections. Across two Electron launches, require immediate setProxy to route a forced worker wake and post-verification fetch. Reject DIRECT, endpoint retargeting, and capacity overflow. Require quarantine before disable_non_proxied_udp and admission; under real Electron require the unprotected control to emit STUN and the protected guest to emit zero direct UDP packets. Under real Electron require a direct control to emit WebTransport and forced-QUIC datagrams and the SOCKS partition to emit none, require an explicitly enabled Direct Sockets control to expose the constructors and die on construction, and require the shipped disable-features list to leave them undefined with the renderer alive. Capture a route partition's netLog across a dns-prefetch load and require the prefetched host to appear on a local resolver task while an unreferenced control host appears nowhere; require no source file to set a non-'off' secureDnsMode.",
|
||||
"oracle": "Derive two delimiter-adversarial identities and require distinct full-digest path-safe partitions with no raw IDs or component hashes. Persist one binding, reload it, and reject replacement, malformed or oversized state, Chromium data without matching metadata, and the 513th binding. Prepare one partition and require setProxy with <-loopback> to be invoked immediately after getSession and before policy setup, then closeAllConnections and exact SOCKS5 resolveProxy while isAllowedPartition remains false; only then may it become live. Reject DIRECT, endpoint retargeting, and capacity overflow. Require quarantine before disable_non_proxied_udp and admission; under real Electron require the unprotected control to emit STUN and the protected guest to emit zero direct UDP packets. Under real Electron require a direct control to emit WebTransport and forced-QUIC datagrams and the SOCKS partition to emit none, require an explicitly enabled Direct Sockets control to expose the constructors and die on construction, and require the shipped disable-features list to leave them undefined with the renderer alive.",
|
||||
"commands": [
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/main/browser/browser-route-identity.test.ts src/main/browser/browser-route-partition-binding-store.test.ts src/main/browser/browser-route-session-registry.test.ts src/main/browser/browser-session-registry.test.ts src/main/browser/browser-session-startup.test.ts src/main/window/createMainWindow.test.ts",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/main/browser/browser-route-webcontents-registry.test.ts src/main/browser/browser-route-webrtc-egress.electron.test.ts",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/main/browser/browser-route-session-registry.test.ts src/main/browser/browser-route-persisted-worker-egress.electron.test.ts src/main/browser/browser-route-webrtc-egress.electron.test.ts",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/main/browser/browser-route-tcp-egress.electron.test.ts",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/main/browser/browser-route-h3-egress.electron.test.ts src/main/browser/browser-route-dns-prefetch.electron.test.ts"
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/main/browser/browser-route-h3-egress.electron.test.ts"
|
||||
],
|
||||
"testFiles": [
|
||||
"src/main/browser/browser-route-identity.test.ts",
|
||||
"src/main/browser/browser-route-partition-binding-store.test.ts",
|
||||
"src/main/browser/browser-route-session-registry.test.ts",
|
||||
"src/main/browser/browser-route-persisted-worker-egress.electron.test.ts",
|
||||
"src/main/browser/browser-route-tcp-egress.electron.test.ts",
|
||||
"src/main/browser/browser-route-webrtc-egress.electron.test.ts",
|
||||
"src/main/browser/browser-route-h3-egress.electron.test.ts",
|
||||
"src/main/browser/browser-route-dns-prefetch.electron.test.ts",
|
||||
"src/main/browser/browser-route-webcontents-registry.test.ts",
|
||||
"src/main/browser/browser-session-registry.test.ts",
|
||||
"src/main/browser/browser-session-startup.test.ts",
|
||||
@@ -7694,23 +7682,6 @@
|
||||
"stale page cleanup cannot retire its replacement"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/main/browser/browser-route-persisted-worker-egress.electron.test.ts",
|
||||
"assertions": [
|
||||
"a forced persisted-worker wake before proxy setup reaches the direct desktop control endpoint",
|
||||
"getAllRunning is empty immediately after persistent Session creation in either arm",
|
||||
"invoking setProxy before the forced wake routes it through SOCKS with no direct target request",
|
||||
"a later worker message fetch remains routed after exact proxy verification"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/main/browser/browser-route-tcp-egress.electron.test.ts",
|
||||
"assertions": [
|
||||
"the unprotected Electron control reaches HTTP, HTTPS, WebSocket, redirect, subresource, and download targets directly",
|
||||
"the fixed SOCKS session routes every equivalent TCP surface with zero direct target connections",
|
||||
"an otherwise-unresolvable `.test` hostname is carried to SOCKS rather than desktop DNS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/main/browser/browser-route-webrtc-egress.electron.test.ts",
|
||||
"assertions": [
|
||||
@@ -7728,14 +7699,6 @@
|
||||
"the shipped disable-features list leaves every Direct Sockets constructor undefined and the renderer alive"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/main/browser/browser-route-dns-prefetch.electron.test.ts",
|
||||
"assertions": [
|
||||
"a dns-prefetch hostname in a SOCKS route partition still opens a local host-resolver job",
|
||||
"that job runs as a system or built-in DNS task, neither of which traverses the partition proxy",
|
||||
"a hostname the page never references produces no host-resolver events"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/main/browser/browser-route-webcontents-registry.test.ts",
|
||||
"assertions": [
|
||||
@@ -7752,24 +7715,6 @@
|
||||
}
|
||||
],
|
||||
"evidenceRuns": [
|
||||
{
|
||||
"date": "2026-08-17",
|
||||
"runner": "local",
|
||||
"platform": "macos",
|
||||
"command": "pnpm exec vitest run --config config/vitest.config.ts src/main/browser/browser-route-tcp-egress.electron.test.ts",
|
||||
"result": "passed",
|
||||
"durationSeconds": 1,
|
||||
"summary": "The real Electron direct control reached all six TCP request surfaces; the fixed SOCKS session carried HTTP, HTTPS, WebSocket, redirects, subresources, downloads, and remote DNS with zero direct target connections."
|
||||
},
|
||||
{
|
||||
"date": "2026-08-16",
|
||||
"runner": "local",
|
||||
"platform": "macos",
|
||||
"command": "pnpm exec vitest run --config config/vitest.config.ts src/main/browser/browser-route-session-registry.test.ts src/main/browser/browser-route-persisted-worker-egress.electron.test.ts src/main/browser/browser-route-webrtc-egress.electron.test.ts",
|
||||
"result": "passed",
|
||||
"durationSeconds": 10,
|
||||
"summary": "Three files passed 28 tests. The two-launch worker control reached desktop localhost before proxy setup; immediate setProxy invocation routed the same wake and a later worker fetch through SOCKS with path-attributed direct counts remaining zero. Both resolved and rejected proxy setup keep failed-policy retries joined through connection cleanup. The protected WebRTC guest emitted zero direct UDP packets."
|
||||
},
|
||||
{
|
||||
"date": "2026-08-16",
|
||||
"runner": "local",
|
||||
@@ -7799,7 +7744,7 @@
|
||||
},
|
||||
"redGreenEvidence": {
|
||||
"status": "partial",
|
||||
"evidence": "The TCP A/B control first reached all six target paths directly, while the fixed SOCKS arm routed the same HTTP, HTTPS, WebSocket, redirect, subresource, and download traffic plus remote DNS with zero direct target connections. The two-launch worker control reached desktop localhost before setProxy; immediate setProxy routed the same wake and later fetch through SOCKS. The WebRTC control emitted four direct STUN packets until disable_non_proxied_udp reduced the capture to zero. Non-WebRTC UDP and network-service restart remain explicit residuals. Historical receipts before removing the source-only tests, preserved verbatim with their originally executed commands (the narrowed commands were not rerun here): [{\"date\":\"2026-08-20\",\"runner\":\"local\",\"platform\":\"macos\",\"command\":\"pnpm exec vitest run --config config/vitest.config.ts src/main/browser/browser-route-h3-egress.electron.test.ts src/main/browser/browser-route-dns-prefetch.electron.test.ts src/main/startup/secure-dns-census.test.ts\",\"result\":\"passed\",\"durationSeconds\":21,\"summary\":\"The direct control emitted 5 WebTransport and 6 forced-QUIC datagrams while the SOCKS route partition emitted zero of each. With Direct Sockets explicitly enabled the control exposed TCPSocket/UDPSocket/TCPServerSocket and constructing one killed the guest renderer; the shipped disable-features list left all three undefined and the renderer alive. The dns-prefetch tripwire recorded HOST_RESOLVER_SYSTEM_TASK for the prefetched host with zero events for an unreferenced control host, documenting the accepted local-resolution residual.\",\"notes\":\"The dns-prefetch expectation intentionally asserts the leak. Flip it to an empty event list when the upstream Electron PrefetchDNS fix lands.\"}]"
|
||||
"evidence": "The TCP A/B control first reached all six target paths directly, while the fixed SOCKS arm routed the same HTTP, HTTPS, WebSocket, redirect, subresource, and download traffic plus remote DNS with zero direct target connections. The two-launch worker control reached desktop localhost before setProxy; immediate setProxy routed the same wake and later fetch through SOCKS. The WebRTC control emitted four direct STUN packets until disable_non_proxied_udp reduced the capture to zero. Non-WebRTC UDP and network-service restart remain explicit residuals. Historical receipts before removing the source-only tests, preserved verbatim with their originally executed commands (the narrowed commands were not rerun here): [{\"date\":\"2026-08-20\",\"runner\":\"local\",\"platform\":\"macos\",\"command\":\"pnpm exec vitest run --config config/vitest.config.ts src/main/browser/browser-route-h3-egress.electron.test.ts src/main/browser/browser-route-dns-prefetch.electron.test.ts src/main/startup/secure-dns-census.test.ts\",\"result\":\"passed\",\"durationSeconds\":21,\"summary\":\"The direct control emitted 5 WebTransport and 6 forced-QUIC datagrams while the SOCKS route partition emitted zero of each. With Direct Sockets explicitly enabled the control exposed TCPSocket/UDPSocket/TCPServerSocket and constructing one killed the guest renderer; the shipped disable-features list left all three undefined and the renderer alive. The dns-prefetch tripwire recorded HOST_RESOLVER_SYSTEM_TASK for the prefetched host with zero events for an unreferenced control host, documenting the accepted local-resolution residual.\",\"notes\":\"The dns-prefetch expectation intentionally asserts the leak. Flip it to an empty event list when the upstream Electron PrefetchDNS fix lands.\"}] Archived provider-experiment receipts after removing the standalone TCP and persisted-worker suites, preserved verbatim with their originally executed commands (remaining and narrowed commands were not rerun here): [\n {\n \"date\": \"2026-08-17\",\n \"runner\": \"local\",\n \"platform\": \"macos\",\n \"command\": \"pnpm exec vitest run --config config/vitest.config.ts src/main/browser/browser-route-tcp-egress.electron.test.ts\",\n \"result\": \"passed\",\n \"durationSeconds\": 1,\n \"summary\": \"The real Electron direct control reached all six TCP request surfaces; the fixed SOCKS session carried HTTP, HTTPS, WebSocket, redirects, subresources, downloads, and remote DNS with zero direct target connections.\"\n },\n {\n \"date\": \"2026-08-16\",\n \"runner\": \"local\",\n \"platform\": \"macos\",\n \"command\": \"pnpm exec vitest run --config config/vitest.config.ts src/main/browser/browser-route-session-registry.test.ts src/main/browser/browser-route-persisted-worker-egress.electron.test.ts src/main/browser/browser-route-webrtc-egress.electron.test.ts\",\n \"result\": \"passed\",\n \"durationSeconds\": 10,\n \"summary\": \"Three files passed 28 tests. The two-launch worker control reached desktop localhost before proxy setup; immediate setProxy invocation routed the same wake and a later worker fetch through SOCKS with path-attributed direct counts remaining zero. Both resolved and rejected proxy setup keep failed-policy retries joined through connection cleanup. The protected WebRTC guest emitted zero direct UDP packets.\"\n }\n ]"
|
||||
},
|
||||
"performanceBudget": {
|
||||
"required": true,
|
||||
@@ -7814,7 +7759,7 @@
|
||||
],
|
||||
"knownGaps": [
|
||||
"The production client-page executor calls preparePage before renderer mounting and navigation grant.",
|
||||
"A forced persisted worker is routed once setProxy is invoked immediately after Session creation; spontaneous background-event wake timing beyond the two-launch oracle still needs cross-platform soak evidence.",
|
||||
"Archived standalone experiments routed a manually forced persisted-worker wake after fixture-owned setProxy invocation; production-path TCP routing, spontaneous worker wake timing, and DNS-prefetch containment still require direct application coverage across platforms.",
|
||||
"The route partition permits only blank initial attachment; the production executor registers and grants only the exact main-observed WebContents.",
|
||||
"Network-service recovery, speculative requests, and popup traffic remain incompletely captured.",
|
||||
"QUIC, DoH, WebTransport, and non-WebRTC UDP/direct-network paths are not yet disabled or proven fail closed.",
|
||||
|
||||
@@ -47,10 +47,8 @@ export const UNIT_EXCLUDE = [
|
||||
'src/main/browser/popup-origin-chrome.electron.test.ts',
|
||||
'src/main/browser/browser-link-materialization.electron.test.ts',
|
||||
'src/main/browser/browser-client-page-renderer-lifecycle.electron.test.ts',
|
||||
'src/main/browser/browser-route-tcp-egress.electron.test.ts',
|
||||
'src/main/browser/browser-route-webrtc-egress.electron.test.ts',
|
||||
'src/main/browser/browser-route-h3-egress.electron.test.ts',
|
||||
'src/main/browser/browser-route-dns-prefetch.electron.test.ts',
|
||||
'src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts',
|
||||
'src/main/browser/browser-popup-compatibility.electron.test.ts',
|
||||
'src/main/browser/browser-popup-adapter-compatibility.electron.test.ts',
|
||||
|
||||
@@ -6,10 +6,8 @@ export const LINUX_PACKAGE_TESTS = [
|
||||
'src/main/browser/popup-origin-chrome.electron.test.ts',
|
||||
'src/main/browser/browser-link-materialization.electron.test.ts',
|
||||
'src/main/browser/browser-client-page-renderer-lifecycle.electron.test.ts',
|
||||
'src/main/browser/browser-route-tcp-egress.electron.test.ts',
|
||||
'src/main/browser/browser-route-webrtc-egress.electron.test.ts',
|
||||
'src/main/browser/browser-route-h3-egress.electron.test.ts',
|
||||
'src/main/browser/browser-route-dns-prefetch.electron.test.ts',
|
||||
'src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts',
|
||||
'src/main/browser/browser-popup-compatibility.electron.test.ts',
|
||||
'src/main/browser/browser-popup-adapter-compatibility.electron.test.ts',
|
||||
@@ -44,10 +42,8 @@ export const WINDOWS_BOUNDARY_TESTS = [
|
||||
'config/scripts/package-electron-runtime-contract.test.mjs',
|
||||
'config/scripts/electron-builder-runtime-resources.test.mjs',
|
||||
'src/main/browser/browser-client-page-renderer-lifecycle.electron.test.ts',
|
||||
'src/main/browser/browser-route-tcp-egress.electron.test.ts',
|
||||
'src/main/browser/browser-route-webrtc-egress.electron.test.ts',
|
||||
'src/main/browser/browser-route-h3-egress.electron.test.ts',
|
||||
'src/main/browser/browser-route-dns-prefetch.electron.test.ts',
|
||||
'src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts',
|
||||
'src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts',
|
||||
'src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts',
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('unit support routing', () => {
|
||||
'src/renderer/src/components/native-chat/NativeChatStructuredSession.next-test-harness.tsx',
|
||||
'src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx',
|
||||
'src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts',
|
||||
'src/main/browser/browser-route-tcp-egress.electron.test.ts'
|
||||
'src/main/browser/browser-route-webrtc-egress.electron.test.ts'
|
||||
]) {
|
||||
const result = classifyPrJobs([unitHarness, file])
|
||||
expect(result.should_run, file).toBe(true)
|
||||
|
||||
@@ -6,13 +6,6 @@ import {
|
||||
} from './browser-page-initiated-tab-budget'
|
||||
|
||||
describe('page-initiated tab budget', () => {
|
||||
it('absorbs a window.open loop fired from a single activation', () => {
|
||||
const budget = createPageInitiatedTabBudget()
|
||||
const granted = Array.from({ length: 12 }, () => budget.tryConsume(1_000)).filter(Boolean)
|
||||
|
||||
expect(granted).toHaveLength(MAX_PAGE_INITIATED_TABS_PER_WINDOW)
|
||||
})
|
||||
|
||||
it('refills once the rolling window has passed, so real browsing is unaffected', () => {
|
||||
const budget = createPageInitiatedTabBudget()
|
||||
for (let i = 0; i < MAX_PAGE_INITIATED_TABS_PER_WINDOW; i++) {
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
export function browserRouteDnsPrefetchElectronMain(): string {
|
||||
return String.raw`
|
||||
const { app, BrowserWindow, protocol, session } = require('electron')
|
||||
const { readFileSync, writeFileSync } = require('node:fs')
|
||||
const config = JSON.parse(readFileSync(process.argv[2], 'utf8'))
|
||||
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{ scheme: 'probe', privileges: { standard: true, secure: true, supportFetchAPI: true } }
|
||||
])
|
||||
|
||||
function settle(milliseconds) {
|
||||
return new Promise((resolve) => setTimeout(resolve, milliseconds))
|
||||
}
|
||||
|
||||
async function probe() {
|
||||
const partition = 'persist:dns-prefetch-' + Date.now()
|
||||
const routeSession = session.fromPartition(partition, { cache: false })
|
||||
await routeSession.setProxy({
|
||||
mode: 'fixed_servers',
|
||||
proxyRules: 'socks5://127.0.0.1:' + config.socksPort,
|
||||
proxyBypassRules: '<-loopback>'
|
||||
})
|
||||
await routeSession.closeAllConnections()
|
||||
const resolvedProxy = await routeSession.resolveProxy('https://' + config.probeHost + '/')
|
||||
routeSession.protocol.handle('probe', () =>
|
||||
new Response(
|
||||
'<!doctype html><title>DNS prefetch probe</title>' +
|
||||
'<link rel="dns-prefetch" href="https://' + config.probeHost + '">',
|
||||
{ headers: { 'Content-Type': 'text/html' } }
|
||||
)
|
||||
)
|
||||
|
||||
await routeSession.netLog.startLogging(config.netLogPath)
|
||||
const window = new BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: { partition, sandbox: true, nodeIntegration: false, contextIsolation: true }
|
||||
})
|
||||
await window.loadURL('probe://page/')
|
||||
// DNS prefetch has no completion signal; allow time for the resolver job to appear in netLog.
|
||||
await settle(4000)
|
||||
window.destroy()
|
||||
await routeSession.netLog.stopLogging()
|
||||
return { resolvedProxy }
|
||||
}
|
||||
|
||||
// Prevent automatic quit while stopLogging() yields before the result is written.
|
||||
app.on('window-all-closed', () => {})
|
||||
|
||||
async function run() {
|
||||
const timeout = setTimeout(() => app.exit(2), 25000)
|
||||
await app.whenReady()
|
||||
const result = await probe()
|
||||
writeFileSync(config.resultPath, JSON.stringify(result))
|
||||
clearTimeout(timeout)
|
||||
app.quit()
|
||||
}
|
||||
|
||||
run().catch((error) => {
|
||||
writeFileSync(config.resultPath, JSON.stringify({ error: String(error?.stack || error) }))
|
||||
app.exit(1)
|
||||
})
|
||||
`
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { createServer, type Server, type Socket } from 'node:net'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { runBrowserRouteEgressElectron } from './browser-route-egress-electron-launch'
|
||||
import { browserRouteDnsPrefetchElectronMain } from './browser-route-dns-prefetch-electron-main'
|
||||
|
||||
export type BrowserRouteDnsPrefetchProbeResult = {
|
||||
resolvedProxy: string
|
||||
probeHost: string
|
||||
controlHost: string
|
||||
probeHostResolverEvents: string[]
|
||||
controlHostResolverEvents: string[]
|
||||
}
|
||||
|
||||
export async function runBrowserRouteDnsPrefetchProbe(): Promise<BrowserRouteDnsPrefetchProbeResult> {
|
||||
const root = mkdtempSync(join(tmpdir(), 'orca-browser-dns-prefetch-'))
|
||||
const unique = `${Date.now().toString(36)}${Math.floor(Math.random() * 1e6).toString(36)}`
|
||||
const probeHost = `orca-prefetch-${unique}.invalid`
|
||||
const controlHost = `orca-control-${unique}.invalid`
|
||||
const sockets = new Set<Socket>()
|
||||
const socks = createServer((socket) => socket.destroy())
|
||||
let result: BrowserRouteDnsPrefetchProbeResult | null = null
|
||||
let primaryFailure: unknown = null
|
||||
try {
|
||||
const socksPort = await listen(socks, sockets)
|
||||
const netLogPath = join(root, 'netlog.json')
|
||||
const resultPath = join(root, 'result.json')
|
||||
const mainPath = join(root, 'main.cjs')
|
||||
writeFileSync(mainPath, browserRouteDnsPrefetchElectronMain())
|
||||
writeFileSync(
|
||||
join(root, 'config.json'),
|
||||
JSON.stringify({ netLogPath, probeHost, resultPath, socksPort })
|
||||
)
|
||||
const parsed = await runBrowserRouteEgressElectron(root, mainPath)
|
||||
if (typeof parsed.resolvedProxy !== 'string') {
|
||||
throw new Error(`browser_route_dns_prefetch_result_invalid:${JSON.stringify(parsed)}`)
|
||||
}
|
||||
const netLog = readFileSync(netLogPath, 'utf8')
|
||||
result = {
|
||||
resolvedProxy: parsed.resolvedProxy,
|
||||
probeHost,
|
||||
controlHost,
|
||||
probeHostResolverEvents: findHostResolverEvents(netLog, probeHost),
|
||||
controlHostResolverEvents: findHostResolverEvents(netLog, controlHost)
|
||||
}
|
||||
} catch (error) {
|
||||
primaryFailure = error
|
||||
}
|
||||
const cleanupFailures = await cleanup(root, socks, sockets)
|
||||
if (primaryFailure || cleanupFailures.length > 0) {
|
||||
throw new AggregateError(
|
||||
[...(primaryFailure ? [primaryFailure] : []), ...cleanupFailures],
|
||||
primaryFailure instanceof Error
|
||||
? primaryFailure.message
|
||||
: 'browser_route_dns_prefetch_probe_failed'
|
||||
)
|
||||
}
|
||||
if (!result) {
|
||||
throw new Error('browser_route_dns_prefetch_probe_result_missing')
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
type NetLog = {
|
||||
constants?: { logEventTypes?: Record<string, number>; logSourceType?: Record<string, number> }
|
||||
events?: { type?: number; source?: { type?: number; id?: number }; params?: unknown }[]
|
||||
}
|
||||
|
||||
// Correlate by source id: resolver task events may omit the hostname.
|
||||
function findHostResolverEvents(netLog: string, host: string): string[] {
|
||||
const parsed = parseNetLog(netLog)
|
||||
const eventTypeNames = invert(parsed.constants?.logEventTypes ?? {})
|
||||
const events = parsed.events ?? []
|
||||
const hostSourceIds = new Set<number>()
|
||||
for (const event of events) {
|
||||
const sourceId = event.source?.id
|
||||
if (sourceId !== undefined && JSON.stringify(event.params ?? null).includes(host)) {
|
||||
hostSourceIds.add(sourceId)
|
||||
}
|
||||
}
|
||||
const names = new Set<string>()
|
||||
for (const event of events) {
|
||||
const sourceId = event.source?.id
|
||||
if (sourceId === undefined || !hostSourceIds.has(sourceId) || event.type === undefined) {
|
||||
continue
|
||||
}
|
||||
const name = eventTypeNames.get(event.type)
|
||||
if (name?.startsWith('HOST_RESOLVER')) {
|
||||
names.add(name)
|
||||
}
|
||||
}
|
||||
return [...names].sort()
|
||||
}
|
||||
|
||||
function parseNetLog(netLog: string): NetLog {
|
||||
try {
|
||||
return JSON.parse(netLog) as NetLog
|
||||
} catch {
|
||||
// Incremental netLog writes may leave the final array/object unclosed.
|
||||
return JSON.parse(`${netLog.replace(/,\s*$/, '')}]}`) as NetLog
|
||||
}
|
||||
}
|
||||
|
||||
function invert(record: Record<string, number>): Map<number, string> {
|
||||
return new Map(Object.entries(record).map(([name, id]) => [id, name]))
|
||||
}
|
||||
|
||||
function listen(server: Server, sockets: Set<Socket>): Promise<number> {
|
||||
server.on('connection', (socket) => {
|
||||
sockets.add(socket)
|
||||
socket.on('error', () => socket.destroy())
|
||||
socket.once('close', () => sockets.delete(socket))
|
||||
})
|
||||
return new Promise((resolve, reject) => {
|
||||
server.once('error', reject)
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
server.off('error', reject)
|
||||
const address = server.address()
|
||||
if (!address || typeof address === 'string') {
|
||||
reject(new Error('browser_route_dns_prefetch_listener_unavailable'))
|
||||
return
|
||||
}
|
||||
resolve(address.port)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function cleanup(root: string, socks: Server, sockets: Set<Socket>): Promise<unknown[]> {
|
||||
const failures: unknown[] = []
|
||||
for (const socket of sockets) {
|
||||
socket.destroy()
|
||||
}
|
||||
sockets.clear()
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
socks.close((error) => (error ? reject(error) : resolve()))
|
||||
)
|
||||
} catch (error) {
|
||||
failures.push(error)
|
||||
}
|
||||
try {
|
||||
rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })
|
||||
} catch (error) {
|
||||
failures.push(error)
|
||||
}
|
||||
return failures
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { runBrowserRouteDnsPrefetchProbe } from './browser-route-dns-prefetch-fixture'
|
||||
|
||||
// This records the Electron 43 DNS-prefetch leak outside SOCKS; invert the leak assertions after an upstream fix.
|
||||
describe('browser route DNS prefetch under Electron', () => {
|
||||
it('leaks a dns-prefetch hostname to the desktop resolver despite the SOCKS partition', async () => {
|
||||
const probe = await runBrowserRouteDnsPrefetchProbe()
|
||||
|
||||
expect(probe.resolvedProxy).toMatch(/^SOCKS5 127\.0\.0\.1:\d+$/)
|
||||
|
||||
expect(probe.probeHostResolverEvents).toContain('HOST_RESOLVER_MANAGER_CREATE_JOB')
|
||||
expect(
|
||||
probe.probeHostResolverEvents.some((name) => /^HOST_RESOLVER_(SYSTEM|DNS)_TASK$/.test(name))
|
||||
).toBe(true)
|
||||
|
||||
expect(probe.controlHostResolverEvents).toEqual([])
|
||||
}, 90_000)
|
||||
})
|
||||
@@ -1,23 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { runPersistedWorkerProbe } from './browser-route-persisted-worker-fixture'
|
||||
|
||||
describe('browser route persisted-worker egress under Electron', () => {
|
||||
it('routes a forced worker wake once proxy setup has started', async () => {
|
||||
const baseline = await runPersistedWorkerProbe(false)
|
||||
const protectedSession = await runPersistedWorkerProbe(true)
|
||||
|
||||
expect(baseline.preProxyWorkerRequests).toBeGreaterThan(0)
|
||||
expect(baseline.workerRunningBeforeForcedWake).toBe(false)
|
||||
expect(protectedSession.workerRunningBeforeForcedWake).toBe(false)
|
||||
expect(baseline.directWorkerStartRequests).toBeGreaterThan(0)
|
||||
expect(baseline.socksWorkerStartRequests).toBe(0)
|
||||
expect(baseline.directWorkerAfterProxyRequests).toBe(0)
|
||||
expect(baseline.socksWorkerAfterProxyRequests).toBeGreaterThan(0)
|
||||
expect(protectedSession.directWorkerStartRequests).toBe(0)
|
||||
expect(protectedSession.directWorkerAfterProxyRequests).toBe(0)
|
||||
expect(protectedSession.socksWorkerStartRequests).toBeGreaterThan(0)
|
||||
expect(protectedSession.socksWorkerAfterProxyRequests).toBeGreaterThan(0)
|
||||
expect(protectedSession.resolvedProxy).toMatch(/^SOCKS5 127\.0\.0\.1:\d+$/)
|
||||
expect(protectedSession.postProxyWorkerRequests).toBeGreaterThan(0)
|
||||
}, 180_000)
|
||||
})
|
||||
@@ -1,61 +0,0 @@
|
||||
export function persistedWorkerElectronMain(): string {
|
||||
return String.raw`
|
||||
const { app, BrowserWindow, session } = require('electron')
|
||||
const { existsSync, readFileSync, writeFileSync } = require('node:fs')
|
||||
|
||||
const config = JSON.parse(readFileSync(process.argv[2], 'utf8'))
|
||||
const mode = process.argv[3]
|
||||
const partition = 'persist:worker-egress'
|
||||
const scope = 'http://localhost:' + config.targetPort + '/'
|
||||
|
||||
async function waitForContinue() {
|
||||
const deadline = Date.now() + 10000
|
||||
while (!existsSync(config.continuePath)) {
|
||||
if (Date.now() >= deadline) throw new Error('worker_probe_continue_barrier_missing')
|
||||
await new Promise((resolve) => setTimeout(resolve, 20))
|
||||
}
|
||||
}
|
||||
|
||||
async function setup() {
|
||||
const routeSession = session.fromPartition(partition)
|
||||
const window = new BrowserWindow({ show: false, webPreferences: { partition, sandbox: true } })
|
||||
await window.loadURL(scope)
|
||||
await window.webContents.executeJavaScript("navigator.serviceWorker.register('/sw.js').then(() => navigator.serviceWorker.ready).then(() => true)")
|
||||
window.destroy()
|
||||
return { registered: true }
|
||||
}
|
||||
|
||||
async function probe() {
|
||||
const routeSession = session.fromPartition(partition)
|
||||
const workerRunningBeforeForcedWake = Object.keys(routeSession.serviceWorkers.getAllRunning()).length > 0
|
||||
const proxySetup = config.protectedSession
|
||||
? routeSession.setProxy({ mode: 'fixed_servers', proxyRules: 'socks5://127.0.0.1:' + config.socksPort, proxyBypassRules: '<-loopback>' })
|
||||
: null
|
||||
await routeSession.serviceWorkers.startWorkerForScope(scope)
|
||||
writeFileSync(config.workerStartedPath, '')
|
||||
await waitForContinue()
|
||||
await (proxySetup ?? routeSession.setProxy({ mode: 'fixed_servers', proxyRules: 'socks5://127.0.0.1:' + config.socksPort, proxyBypassRules: '<-loopback>' }))
|
||||
await routeSession.closeAllConnections()
|
||||
const resolvedProxy = await routeSession.resolveProxy(scope)
|
||||
const window = new BrowserWindow({ show: false, webPreferences: { partition, sandbox: true } })
|
||||
await window.loadURL(scope)
|
||||
await window.webContents.executeJavaScript("new Promise(async (resolve, reject) => { const registration = await navigator.serviceWorker.ready; const worker = navigator.serviceWorker.controller || registration.active; const timeout = setTimeout(() => reject(new Error('worker message timeout')), 5000); navigator.serviceWorker.addEventListener('message', event => { if (event.data === 'done') { clearTimeout(timeout); resolve(true) } }, { once: true }); worker.postMessage('probe') })")
|
||||
window.destroy()
|
||||
return { resolvedProxy, workerRunningBeforeForcedWake }
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const timeout = setTimeout(() => app.exit(2), 20000)
|
||||
await app.whenReady()
|
||||
const result = mode === 'setup' ? await setup() : await probe()
|
||||
writeFileSync(config.resultPath, JSON.stringify(result))
|
||||
clearTimeout(timeout)
|
||||
app.quit()
|
||||
}
|
||||
|
||||
run().catch((error) => {
|
||||
writeFileSync(config.resultPath, JSON.stringify({ error: String(error?.stack || error) }))
|
||||
app.exit(1)
|
||||
})
|
||||
`
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
import { spawn, spawnSync, type ChildProcess } from 'node:child_process'
|
||||
import { existsSync, readFileSync, rmSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { join } from 'node:path'
|
||||
import { resolveElectronProbeLaunch } from './electron-probe-display-launch'
|
||||
|
||||
const electronBinary = createRequire(import.meta.url)('electron') as string
|
||||
|
||||
type PersistedWorkerElectronResult = {
|
||||
workerRunningBeforeForcedWake: boolean
|
||||
resolvedProxy: string
|
||||
}
|
||||
|
||||
export async function runPersistedWorkerElectron(
|
||||
root: string,
|
||||
mainPath: string,
|
||||
configPath: string,
|
||||
mode: 'setup' | 'probe',
|
||||
onBarrier?: () => Promise<void>
|
||||
): Promise<PersistedWorkerElectronResult> {
|
||||
const resultPath = join(root, 'result.json')
|
||||
rmSync(resultPath, { force: true })
|
||||
const electronArgs = [mainPath, configPath, mode, `--user-data-dir=${join(root, 'profile')}`]
|
||||
const { ELECTRON_RUN_AS_NODE: _electronRunAsNode, ...env } = process.env
|
||||
const { executable, args } = resolveElectronProbeLaunch({
|
||||
electronBinary,
|
||||
electronArgs,
|
||||
platform: process.platform,
|
||||
display: env.DISPLAY
|
||||
})
|
||||
const child = spawn(executable, args, {
|
||||
detached: true,
|
||||
env,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
windowsHide: true
|
||||
})
|
||||
const exit = waitForExit(child)
|
||||
let timedOut = false
|
||||
let termination: Promise<void> | null = null
|
||||
const terminate = (): Promise<void> => {
|
||||
termination ??= terminateProcessTree(child)
|
||||
return termination
|
||||
}
|
||||
const timeout = setTimeout(() => {
|
||||
timedOut = true
|
||||
void terminate()
|
||||
}, 30_000)
|
||||
try {
|
||||
const [, output] = await Promise.all([onBarrier?.() ?? Promise.resolve(), exit])
|
||||
const rawResult = existsSync(resultPath) ? readFileSync(resultPath, 'utf8') : 'no result'
|
||||
if (timedOut || output.code !== 0 || rawResult === 'no result') {
|
||||
throw new Error(`${timedOut ? 'timeout' : output.code}\n${rawResult}\n${output.output}`)
|
||||
}
|
||||
const result = JSON.parse(rawResult) as Record<string, unknown>
|
||||
if (typeof result.error === 'string') {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
if (mode === 'setup') {
|
||||
if (result.registered !== true) {
|
||||
throw new Error('worker_probe_setup_result_invalid')
|
||||
}
|
||||
return { workerRunningBeforeForcedWake: false, resolvedProxy: '' }
|
||||
}
|
||||
if (
|
||||
!Object.hasOwn(result, 'workerRunningBeforeForcedWake') ||
|
||||
typeof result.workerRunningBeforeForcedWake !== 'boolean' ||
|
||||
!Object.hasOwn(result, 'resolvedProxy') ||
|
||||
typeof result.resolvedProxy !== 'string'
|
||||
) {
|
||||
throw new Error('worker_probe_result_invalid')
|
||||
}
|
||||
return {
|
||||
workerRunningBeforeForcedWake: result.workerRunningBeforeForcedWake,
|
||||
resolvedProxy: result.resolvedProxy
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
if (termination || (child.exitCode === null && child.signalCode === null)) {
|
||||
await terminate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function waitForExit(child: ChildProcess): Promise<{ code: number | null; output: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let output = ''
|
||||
child.stdout?.on('data', (chunk) => (output += String(chunk)))
|
||||
child.stderr?.on('data', (chunk) => (output += String(chunk)))
|
||||
child.once('error', reject)
|
||||
child.once('exit', (code) => resolve({ code, output }))
|
||||
})
|
||||
}
|
||||
|
||||
async function terminateProcessTree(child: ChildProcess): Promise<void> {
|
||||
if (!child.pid) {
|
||||
return
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
spawnSync('taskkill', ['/pid', String(child.pid), '/t', '/f'], {
|
||||
stdio: 'ignore',
|
||||
windowsHide: true
|
||||
})
|
||||
await waitForTermination(child, 5_000)
|
||||
return
|
||||
}
|
||||
try {
|
||||
process.kill(-child.pid, 'SIGTERM')
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await waitForTermination(child, 2_000)
|
||||
try {
|
||||
process.kill(-child.pid, 0)
|
||||
process.kill(-child.pid, 'SIGKILL')
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await waitForTermination(child, 5_000)
|
||||
}
|
||||
|
||||
function waitForTermination(child: ChildProcess, timeoutMs: number): Promise<void> {
|
||||
if (child.exitCode !== null || child.signalCode !== null) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
const timeout = setTimeout(done, timeoutMs)
|
||||
child.once('exit', done)
|
||||
function done(): void {
|
||||
clearTimeout(timeout)
|
||||
child.off('exit', done)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,297 +0,0 @@
|
||||
import { createServer as createHttpServer } from 'node:http'
|
||||
import { createServer as createNetServer, type Server, Socket } from 'node:net'
|
||||
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { persistedWorkerElectronMain } from './browser-route-persisted-worker-electron-main'
|
||||
import { runPersistedWorkerElectron } from './browser-route-persisted-worker-electron-process'
|
||||
|
||||
export type PersistedWorkerProbeResult = {
|
||||
workerRunningBeforeForcedWake: boolean
|
||||
preProxyWorkerRequests: number
|
||||
postProxyWorkerRequests: number
|
||||
directWorkerStartRequests: number
|
||||
directWorkerAfterProxyRequests: number
|
||||
socksWorkerStartRequests: number
|
||||
socksWorkerAfterProxyRequests: number
|
||||
resolvedProxy: string
|
||||
}
|
||||
|
||||
type ProbeCounts = Omit<
|
||||
PersistedWorkerProbeResult,
|
||||
'workerRunningBeforeForcedWake' | 'resolvedProxy'
|
||||
>
|
||||
|
||||
type TargetObservation = Readonly<{ path: string; remotePort: number }>
|
||||
|
||||
export async function runPersistedWorkerProbe(
|
||||
protectedSession: boolean
|
||||
): Promise<PersistedWorkerProbeResult> {
|
||||
const root = mkdtempSync(join(tmpdir(), 'orca-browser-worker-egress-'))
|
||||
const observations: TargetObservation[] = []
|
||||
const routedSourcePorts = new Set<number>()
|
||||
const sockets = new Set<Socket>()
|
||||
const target = createTargetServer(observations)
|
||||
trackAcceptedSockets(target, sockets)
|
||||
let socks: Server | null = null
|
||||
let result: PersistedWorkerProbeResult | null = null
|
||||
let primaryFailure: { error: unknown } | null = null
|
||||
try {
|
||||
const targetPort = await listen(target)
|
||||
socks = createSocksRecorder(targetPort, routedSourcePorts, sockets)
|
||||
const socksPort = await listen(socks)
|
||||
const mainPath = join(root, 'main.cjs')
|
||||
const configPath = join(root, 'config.json')
|
||||
const resultPath = join(root, 'result.json')
|
||||
const workerStartedPath = join(root, 'worker-started')
|
||||
const continuePath = join(root, 'continue')
|
||||
writeFileSync(mainPath, persistedWorkerElectronMain())
|
||||
writeFileSync(
|
||||
configPath,
|
||||
JSON.stringify({
|
||||
continuePath,
|
||||
protectedSession,
|
||||
resultPath,
|
||||
socksPort,
|
||||
targetPort,
|
||||
workerStartedPath
|
||||
})
|
||||
)
|
||||
await runPersistedWorkerElectron(root, mainPath, configPath, 'setup')
|
||||
observations.length = 0
|
||||
routedSourcePorts.clear()
|
||||
const probe = await runPersistedWorkerElectron(
|
||||
root,
|
||||
mainPath,
|
||||
configPath,
|
||||
'probe',
|
||||
async () => {
|
||||
await waitFor(() => existsSync(workerStartedPath), 'worker_probe_start_barrier_missing')
|
||||
await waitFor(
|
||||
() => observations.some((observation) => observation.path === '/worker-start'),
|
||||
'worker_probe_control_fetch_missing'
|
||||
)
|
||||
writeFileSync(continuePath, '')
|
||||
}
|
||||
)
|
||||
result = { ...classifyProbeRequests(observations, routedSourcePorts), ...probe }
|
||||
} catch (error) {
|
||||
primaryFailure = { error }
|
||||
}
|
||||
const cleanupFailures = await cleanupProbeResources(root, target, socks, sockets)
|
||||
if (primaryFailure) {
|
||||
if (cleanupFailures.length > 0) {
|
||||
const message =
|
||||
primaryFailure.error instanceof Error
|
||||
? primaryFailure.error.message
|
||||
: String(primaryFailure.error)
|
||||
throw new AggregateError([primaryFailure.error, ...cleanupFailures], message)
|
||||
}
|
||||
throw primaryFailure.error
|
||||
}
|
||||
if (cleanupFailures.length > 0) {
|
||||
throw new AggregateError(cleanupFailures, 'worker_probe_cleanup_failed')
|
||||
}
|
||||
if (!result) {
|
||||
throw new Error('worker_probe_result_missing')
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async function cleanupProbeResources(
|
||||
root: string,
|
||||
target: Server,
|
||||
socks: Server | null,
|
||||
sockets: Set<Socket>
|
||||
): Promise<unknown[]> {
|
||||
const failures: unknown[] = []
|
||||
for (const server of socks ? [socks, target] : [target]) {
|
||||
try {
|
||||
await closeServer(server, sockets)
|
||||
} catch (error) {
|
||||
failures.push(error)
|
||||
}
|
||||
}
|
||||
try {
|
||||
rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })
|
||||
} catch (error) {
|
||||
failures.push(error)
|
||||
}
|
||||
return failures
|
||||
}
|
||||
|
||||
function createProbeCounts(): ProbeCounts {
|
||||
return {
|
||||
preProxyWorkerRequests: 0,
|
||||
postProxyWorkerRequests: 0,
|
||||
directWorkerStartRequests: 0,
|
||||
directWorkerAfterProxyRequests: 0,
|
||||
socksWorkerStartRequests: 0,
|
||||
socksWorkerAfterProxyRequests: 0
|
||||
}
|
||||
}
|
||||
|
||||
function classifyProbeRequests(
|
||||
observations: TargetObservation[],
|
||||
routedSourcePorts: Set<number>
|
||||
): ProbeCounts {
|
||||
const counts = createProbeCounts()
|
||||
for (const observation of observations) {
|
||||
const routed = routedSourcePorts.has(observation.remotePort)
|
||||
if (observation.path === '/worker-start') {
|
||||
counts.preProxyWorkerRequests += 1
|
||||
counts[routed ? 'socksWorkerStartRequests' : 'directWorkerStartRequests'] += 1
|
||||
}
|
||||
if (observation.path === '/worker-after-proxy') {
|
||||
counts.postProxyWorkerRequests += 1
|
||||
counts[routed ? 'socksWorkerAfterProxyRequests' : 'directWorkerAfterProxyRequests'] += 1
|
||||
}
|
||||
}
|
||||
return counts
|
||||
}
|
||||
|
||||
function createTargetServer(observations: TargetObservation[]): Server {
|
||||
return createHttpServer((request, response) => {
|
||||
observations.push({ path: request.url ?? '', remotePort: request.socket.remotePort ?? -1 })
|
||||
if (request.url === '/sw.js') {
|
||||
response.writeHead(200, {
|
||||
'Cache-Control': 'no-store',
|
||||
'Content-Type': 'application/javascript',
|
||||
'Service-Worker-Allowed': '/'
|
||||
})
|
||||
response.end(workerScript())
|
||||
return
|
||||
}
|
||||
response.writeHead(200, { 'Cache-Control': 'no-store', 'Content-Type': 'text/html' })
|
||||
response.end('<!doctype html><title>worker probe</title>')
|
||||
})
|
||||
}
|
||||
|
||||
function workerScript(): string {
|
||||
return [
|
||||
"fetch('/worker-start').catch(() => {})",
|
||||
"self.addEventListener('install', event => event.waitUntil(fetch('/worker-start').then(() => self.skipWaiting())))",
|
||||
"self.addEventListener('activate', event => event.waitUntil(self.clients.claim()))",
|
||||
"self.addEventListener('message', event => {",
|
||||
" if (event.data !== 'probe') return",
|
||||
" event.waitUntil(fetch('/worker-after-proxy').then(() => event.source.postMessage('done')))",
|
||||
'})'
|
||||
].join(';')
|
||||
}
|
||||
|
||||
function createSocksRecorder(
|
||||
targetPort: number,
|
||||
routedSourcePorts: Set<number>,
|
||||
sockets: Set<Socket>
|
||||
): Server {
|
||||
return createNetServer((client) => {
|
||||
trackSocket(client, sockets)
|
||||
let buffer = Buffer.alloc(0)
|
||||
let greeted = false
|
||||
const onData = (chunk: Buffer): void => {
|
||||
buffer = Buffer.concat([buffer, chunk])
|
||||
if (!greeted) {
|
||||
if (buffer.length < 2) {
|
||||
return
|
||||
}
|
||||
const greetingLength = 2 + (buffer[1] ?? 0)
|
||||
if (buffer.length < greetingLength) {
|
||||
return
|
||||
}
|
||||
buffer = buffer.subarray(greetingLength)
|
||||
greeted = true
|
||||
client.write(Buffer.from([5, 0]))
|
||||
}
|
||||
if (buffer.length < 4) {
|
||||
return
|
||||
}
|
||||
if (buffer[3] === 3 && buffer.length < 5) {
|
||||
return
|
||||
}
|
||||
const addressLength = buffer[3] === 1 ? 4 : buffer[3] === 4 ? 16 : (buffer[4] ?? 0) + 1
|
||||
const requestLength = 4 + addressLength + 2
|
||||
if (buffer.length < requestLength) {
|
||||
return
|
||||
}
|
||||
const remainder = buffer.subarray(requestLength)
|
||||
client.off('data', onData)
|
||||
connectSocksUpstream(client, remainder, targetPort, routedSourcePorts, sockets)
|
||||
}
|
||||
client.on('data', onData)
|
||||
})
|
||||
}
|
||||
|
||||
function connectSocksUpstream(
|
||||
client: Socket,
|
||||
remainder: Buffer,
|
||||
targetPort: number,
|
||||
routedSourcePorts: Set<number>,
|
||||
sockets: Set<Socket>
|
||||
): void {
|
||||
const upstream = new Socket()
|
||||
trackSocket(upstream, sockets)
|
||||
upstream.connect(targetPort, '127.0.0.1', () => {
|
||||
routedSourcePorts.add(upstream.localPort ?? -1)
|
||||
client.write(Buffer.from([5, 0, 0, 1, 127, 0, 0, 1, 0, 0]))
|
||||
if (remainder.length > 0) {
|
||||
upstream.write(remainder)
|
||||
}
|
||||
client.pipe(upstream).pipe(client)
|
||||
})
|
||||
upstream.on('error', () => client.destroy())
|
||||
client.on('error', () => upstream.destroy())
|
||||
client.once('close', () => upstream.destroy())
|
||||
}
|
||||
|
||||
function trackAcceptedSockets(server: Server, sockets: Set<Socket>): void {
|
||||
server.on('connection', (socket) => trackSocket(socket, sockets))
|
||||
}
|
||||
|
||||
function trackSocket(socket: Socket, sockets: Set<Socket>): void {
|
||||
sockets.add(socket)
|
||||
socket.on('error', () => socket.destroy())
|
||||
socket.once('close', () => sockets.delete(socket))
|
||||
}
|
||||
|
||||
function listen(server: Server): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
server.once('error', reject)
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
server.off('error', reject)
|
||||
const address = server.address()
|
||||
if (!address || typeof address === 'string') {
|
||||
reject(new Error('worker_probe_listener_unavailable'))
|
||||
return
|
||||
}
|
||||
resolve(address.port)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function closeServer(server: Server, sockets: Set<Socket>): Promise<void> {
|
||||
for (const socket of sockets) {
|
||||
socket.destroy()
|
||||
}
|
||||
if (!server.listening) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
return new Promise((resolve) => server.close(() => resolve()))
|
||||
}
|
||||
|
||||
function waitFor(predicate: () => boolean, code: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const deadline = Date.now() + 10_000
|
||||
const check = (): void => {
|
||||
if (predicate()) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
if (Date.now() >= deadline) {
|
||||
reject(new Error(code))
|
||||
return
|
||||
}
|
||||
setTimeout(check, 20)
|
||||
}
|
||||
check()
|
||||
})
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { runBrowserRouteEgressElectron } from './browser-route-egress-electron-launch'
|
||||
|
||||
export async function runBrowserRouteTcpEgressElectron(
|
||||
root: string,
|
||||
mainPath: string
|
||||
): Promise<string> {
|
||||
const parsed = await runBrowserRouteEgressElectron(root, mainPath)
|
||||
if (typeof parsed.resolvedProxy !== 'string') {
|
||||
throw new Error('browser_route_tcp_probe_result_invalid')
|
||||
}
|
||||
return parsed.resolvedProxy
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { rmSync } from 'node:fs'
|
||||
import type { Server, Socket } from 'node:net'
|
||||
import type { WebSocketServer } from 'ws'
|
||||
|
||||
export async function cleanupBrowserRouteTcpEgressFixture(
|
||||
root: string,
|
||||
servers: (Server | null)[],
|
||||
webSocket: WebSocketServer,
|
||||
sockets: Set<Socket>
|
||||
): Promise<unknown[]> {
|
||||
const failures: unknown[] = []
|
||||
for (const client of webSocket.clients) {
|
||||
client.terminate()
|
||||
}
|
||||
try {
|
||||
await closeWithTimeout(
|
||||
new Promise<void>((resolve, reject) =>
|
||||
webSocket.close((error) => (error ? reject(error) : resolve()))
|
||||
),
|
||||
'browser_route_tcp_probe_websocket_close_timeout'
|
||||
)
|
||||
} catch (error) {
|
||||
failures.push(error)
|
||||
}
|
||||
for (const socket of sockets) {
|
||||
socket.destroy()
|
||||
}
|
||||
for (const server of servers) {
|
||||
if (!server?.listening) {
|
||||
continue
|
||||
}
|
||||
try {
|
||||
await closeWithTimeout(
|
||||
new Promise<void>((resolve, reject) =>
|
||||
server.close((error) => (error ? reject(error) : resolve()))
|
||||
),
|
||||
'browser_route_tcp_probe_server_close_timeout'
|
||||
)
|
||||
} catch (error) {
|
||||
failures.push(error)
|
||||
}
|
||||
}
|
||||
try {
|
||||
rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })
|
||||
} catch (error) {
|
||||
failures.push(error)
|
||||
}
|
||||
return failures
|
||||
}
|
||||
|
||||
function closeWithTimeout(operation: Promise<void>, message: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error(message)), 5000)
|
||||
operation.then(
|
||||
() => {
|
||||
clearTimeout(timeout)
|
||||
resolve()
|
||||
},
|
||||
(error) => {
|
||||
clearTimeout(timeout)
|
||||
reject(error)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
import { mkdtempSync, writeFileSync } from 'node:fs'
|
||||
import { lookup } from 'node:dns/promises'
|
||||
import { createServer as createHttpServer, type Server as HttpServer } from 'node:http'
|
||||
import { createServer as createHttpsServer } from 'node:https'
|
||||
import type { Server, Socket } from 'node:net'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { WebSocketServer } from 'ws'
|
||||
import {
|
||||
LOCAL_HTTPS_TEST_CERTIFICATE,
|
||||
LOCAL_HTTPS_TEST_PRIVATE_KEY
|
||||
} from './browser-local-https-test-certificate'
|
||||
import { runBrowserRouteTcpEgressElectron } from './browser-route-tcp-egress-electron-process'
|
||||
import { cleanupBrowserRouteTcpEgressFixture } from './browser-route-tcp-egress-fixture-cleanup'
|
||||
import { createBrowserRouteTcpEgressSocksRecorder } from './browser-route-tcp-egress-socks-recorder'
|
||||
|
||||
const REMOTE_HOST = 'remote-browser.test'
|
||||
|
||||
type TargetObservation = Readonly<{ path: string; remotePort: number; routed: boolean }>
|
||||
|
||||
export type BrowserRouteTcpEgressProbeResult = {
|
||||
resolvedProxy: string
|
||||
directPaths: string[]
|
||||
routedPaths: string[]
|
||||
socksHosts: string[]
|
||||
}
|
||||
|
||||
export async function runBrowserRouteTcpEgressProbe(
|
||||
protectedSession: boolean
|
||||
): Promise<BrowserRouteTcpEgressProbeResult> {
|
||||
const root = mkdtempSync(join(tmpdir(), 'orca-browser-tcp-egress-'))
|
||||
const observations: TargetObservation[] = []
|
||||
const routedSourcePorts = new Set<number>()
|
||||
const socksHosts = new Set<string>()
|
||||
const sockets = new Set<Socket>()
|
||||
const http = createHttpTarget(observations, routedSourcePorts)
|
||||
const https = createHttpsTarget(observations, routedSourcePorts)
|
||||
const webSocket = attachWebSocketTarget(http, observations, routedSourcePorts)
|
||||
let socks: Server | null = null
|
||||
let result: BrowserRouteTcpEgressProbeResult | null = null
|
||||
let primaryFailure: unknown = null
|
||||
try {
|
||||
if (protectedSession) {
|
||||
await assertLocallyUnresolvable(REMOTE_HOST)
|
||||
}
|
||||
const [httpPort, httpsPort] = await Promise.all([listen(http, sockets), listen(https, sockets)])
|
||||
socks = createBrowserRouteTcpEgressSocksRecorder(
|
||||
new Set([httpPort, httpsPort]),
|
||||
routedSourcePorts,
|
||||
socksHosts,
|
||||
sockets
|
||||
)
|
||||
const socksPort = await listen(socks, sockets)
|
||||
const mainPath = join(root, 'main.cjs')
|
||||
const resultPath = join(root, 'result.json')
|
||||
writeFileSync(mainPath, electronMain())
|
||||
writeFileSync(
|
||||
join(root, 'config.json'),
|
||||
JSON.stringify({ httpPort, httpsPort, protectedSession, resultPath, socksPort })
|
||||
)
|
||||
const resolvedProxy = await runBrowserRouteTcpEgressElectron(root, mainPath)
|
||||
const classified = classifyObservations(observations)
|
||||
result = { resolvedProxy, ...classified, socksHosts: [...socksHosts].sort() }
|
||||
} catch (error) {
|
||||
primaryFailure = error
|
||||
}
|
||||
const cleanupFailures = await cleanupBrowserRouteTcpEgressFixture(
|
||||
root,
|
||||
[socks, https, http],
|
||||
webSocket,
|
||||
sockets
|
||||
)
|
||||
if (primaryFailure || cleanupFailures.length > 0) {
|
||||
throw new AggregateError(
|
||||
[...(primaryFailure ? [primaryFailure] : []), ...cleanupFailures],
|
||||
primaryFailure instanceof Error ? primaryFailure.message : 'browser_route_tcp_probe_failed'
|
||||
)
|
||||
}
|
||||
if (!result) {
|
||||
throw new Error('browser_route_tcp_probe_result_missing')
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async function assertLocallyUnresolvable(host: string): Promise<void> {
|
||||
try {
|
||||
await lookup(host)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
throw new Error(`browser_route_tcp_probe_host_resolved_locally:${host}`)
|
||||
}
|
||||
|
||||
function electronMain(): string {
|
||||
return String.raw`
|
||||
const { app, BrowserWindow, session } = require('electron')
|
||||
const { readFileSync, writeFileSync } = require('node:fs')
|
||||
const config = JSON.parse(readFileSync(process.argv[2], 'utf8'))
|
||||
|
||||
app.on('certificate-error', (event, _webContents, _url, _error, _certificate, callback) => {
|
||||
event.preventDefault()
|
||||
callback(true)
|
||||
})
|
||||
|
||||
function waitForDownload(window) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('tcp_probe_download_timeout')), 5000)
|
||||
window.webContents.session.once('will-download', (_event, item) => {
|
||||
clearTimeout(timeout)
|
||||
item.cancel()
|
||||
resolve()
|
||||
})
|
||||
window.webContents.downloadURL('http://' + config.host + ':' + config.httpPort + '/download')
|
||||
})
|
||||
}
|
||||
|
||||
async function probe() {
|
||||
const partition = 'persist:tcp-egress-' + config.protectedSession + '-' + Date.now()
|
||||
const routeSession = session.fromPartition(partition, { cache: false })
|
||||
routeSession.setCertificateVerifyProc((_request, callback) => callback(0))
|
||||
await routeSession.setProxy(
|
||||
config.protectedSession
|
||||
? {
|
||||
mode: 'fixed_servers',
|
||||
proxyRules: 'socks5://127.0.0.1:' + config.socksPort,
|
||||
proxyBypassRules: '<-loopback>'
|
||||
}
|
||||
: { mode: 'direct' }
|
||||
)
|
||||
await routeSession.closeAllConnections()
|
||||
const resolvedProxy = await routeSession.resolveProxy('http://' + config.host + '/')
|
||||
const window = new BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: { partition, sandbox: true, nodeIntegration: false, contextIsolation: true }
|
||||
})
|
||||
await window.loadURL('http://' + config.host + ':' + config.httpPort + '/redirect')
|
||||
await window.webContents.executeJavaScript(
|
||||
"new Promise((resolve, reject) => { const image = document.querySelector('img'); if (image.complete) return resolve(true); image.addEventListener('load', () => resolve(true), { once: true }); image.addEventListener('error', () => reject(new Error('asset failed')), { once: true }) })"
|
||||
)
|
||||
await window.webContents.executeJavaScript(
|
||||
"new Promise((resolve, reject) => { const socket = new WebSocket('ws://" + config.host + ":" + config.httpPort + "/socket'); const timeout = setTimeout(() => reject(new Error('socket timeout')), 5000); socket.addEventListener('message', () => { clearTimeout(timeout); socket.close(); resolve(true) }, { once: true }); socket.addEventListener('error', () => reject(new Error('socket failed')), { once: true }) })"
|
||||
)
|
||||
await window.webContents.executeJavaScript(
|
||||
"fetch('https://" + config.host + ":" + config.httpsPort + "/secure').then(response => { if (!response.ok) throw new Error('secure fetch failed'); return response.text() })"
|
||||
)
|
||||
await waitForDownload(window)
|
||||
window.destroy()
|
||||
return resolvedProxy
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const timeout = setTimeout(() => app.exit(2), 20000)
|
||||
await app.whenReady()
|
||||
config.host = config.protectedSession ? '${REMOTE_HOST}' : '127.0.0.1'
|
||||
const resolvedProxy = await probe()
|
||||
writeFileSync(config.resultPath, JSON.stringify({ resolvedProxy }))
|
||||
clearTimeout(timeout)
|
||||
app.quit()
|
||||
}
|
||||
|
||||
run().catch((error) => {
|
||||
writeFileSync(config.resultPath, JSON.stringify({ error: String(error?.stack || error) }))
|
||||
app.exit(1)
|
||||
})
|
||||
`
|
||||
}
|
||||
|
||||
function createHttpTarget(
|
||||
observations: TargetObservation[],
|
||||
routedSourcePorts: Set<number>
|
||||
): HttpServer {
|
||||
return createHttpServer((request, response) => {
|
||||
observeRequest(observations, request.url, request.socket.remotePort, routedSourcePorts)
|
||||
if (request.url === '/redirect') {
|
||||
response.writeHead(302, { Location: '/page' })
|
||||
response.end()
|
||||
return
|
||||
}
|
||||
if (request.url === '/page') {
|
||||
response.writeHead(200, { 'Content-Type': 'text/html' })
|
||||
response.end('<!doctype html><title>TCP probe</title><img src="/asset">')
|
||||
return
|
||||
}
|
||||
if (request.url === '/asset') {
|
||||
response.writeHead(200, { 'Content-Type': 'image/svg+xml' })
|
||||
response.end('<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>')
|
||||
return
|
||||
}
|
||||
if (request.url === '/download') {
|
||||
response.writeHead(200, {
|
||||
'Content-Disposition': 'attachment; filename="probe.txt"',
|
||||
'Content-Type': 'text/plain'
|
||||
})
|
||||
response.end('download probe')
|
||||
return
|
||||
}
|
||||
response.writeHead(404)
|
||||
response.end()
|
||||
})
|
||||
}
|
||||
|
||||
function createHttpsTarget(
|
||||
observations: TargetObservation[],
|
||||
routedSourcePorts: Set<number>
|
||||
): HttpServer {
|
||||
return createHttpsServer(
|
||||
{ key: LOCAL_HTTPS_TEST_PRIVATE_KEY, cert: LOCAL_HTTPS_TEST_CERTIFICATE },
|
||||
(request, response) => {
|
||||
observeRequest(observations, request.url, request.socket.remotePort, routedSourcePorts)
|
||||
response.writeHead(request.url === '/secure' ? 200 : 404, {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Content-Type': 'text/plain'
|
||||
})
|
||||
response.end(request.url === '/secure' ? 'secure probe' : 'not found')
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function attachWebSocketTarget(
|
||||
server: HttpServer,
|
||||
observations: TargetObservation[],
|
||||
routedSourcePorts: Set<number>
|
||||
): WebSocketServer {
|
||||
const webSocket = new WebSocketServer({ noServer: true })
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
observeRequest(observations, request.url, request.socket.remotePort, routedSourcePorts)
|
||||
if (request.url !== '/socket') {
|
||||
socket.destroy()
|
||||
return
|
||||
}
|
||||
webSocket.handleUpgrade(request, socket, head, (client) => client.send('ready'))
|
||||
})
|
||||
return webSocket
|
||||
}
|
||||
|
||||
function observeRequest(
|
||||
observations: TargetObservation[],
|
||||
path: string | undefined,
|
||||
remotePort: number | undefined,
|
||||
routedSourcePorts: Set<number>
|
||||
): void {
|
||||
const sourcePort = remotePort ?? -1
|
||||
observations.push({
|
||||
path: path ?? '',
|
||||
remotePort: sourcePort,
|
||||
routed: routedSourcePorts.has(sourcePort)
|
||||
})
|
||||
}
|
||||
|
||||
function classifyObservations(
|
||||
observations: TargetObservation[]
|
||||
): Pick<BrowserRouteTcpEgressProbeResult, 'directPaths' | 'routedPaths'> {
|
||||
const directPaths = new Set<string>()
|
||||
const routedPaths = new Set<string>()
|
||||
for (const observation of observations) {
|
||||
const target = observation.routed ? routedPaths : directPaths
|
||||
target.add(observation.path)
|
||||
}
|
||||
return { directPaths: [...directPaths].sort(), routedPaths: [...routedPaths].sort() }
|
||||
}
|
||||
|
||||
function listen(server: Server | HttpServer, sockets: Set<Socket>): Promise<number> {
|
||||
server.on('connection', (socket) => trackSocket(socket, sockets))
|
||||
return new Promise((resolve, reject) => {
|
||||
server.once('error', reject)
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
server.off('error', reject)
|
||||
const address = server.address()
|
||||
if (!address || typeof address === 'string') {
|
||||
reject(new Error('browser_route_tcp_probe_listener_unavailable'))
|
||||
return
|
||||
}
|
||||
resolve(address.port)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function trackSocket(socket: Socket, sockets: Set<Socket>): void {
|
||||
sockets.add(socket)
|
||||
socket.on('error', () => socket.destroy())
|
||||
socket.once('close', () => sockets.delete(socket))
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
import { createServer, type Server, Socket } from 'node:net'
|
||||
|
||||
const SOCKS_VERSION = 5
|
||||
const SOCKS_NO_AUTH = 0
|
||||
const SOCKS_CONNECT = 1
|
||||
const SOCKS_GENERAL_FAILURE = 1
|
||||
const SOCKS_COMMAND_NOT_SUPPORTED = 7
|
||||
|
||||
export function createBrowserRouteTcpEgressSocksRecorder(
|
||||
allowedPorts: Set<number>,
|
||||
routedSourcePorts: Set<number>,
|
||||
hosts: Set<string>,
|
||||
sockets: Set<Socket>
|
||||
): Server {
|
||||
return createServer((client) => {
|
||||
trackSocket(client, sockets)
|
||||
let buffer = Buffer.alloc(0) as Buffer<ArrayBufferLike>
|
||||
let greeted = false
|
||||
const onData = (chunk: Buffer): void => {
|
||||
buffer = Buffer.concat([buffer, chunk])
|
||||
if (!greeted) {
|
||||
const greeting = parseSocksGreeting(buffer)
|
||||
if (!greeting) {
|
||||
return
|
||||
}
|
||||
if (greeting.invalid) {
|
||||
closeWithSocksError(client, 0xff)
|
||||
return
|
||||
}
|
||||
buffer = greeting.remainder
|
||||
greeted = true
|
||||
client.write(Buffer.from([SOCKS_VERSION, SOCKS_NO_AUTH]))
|
||||
}
|
||||
const request = parseSocksRequest(buffer)
|
||||
if (!request) {
|
||||
return
|
||||
}
|
||||
if (request.invalid) {
|
||||
closeWithSocksError(client, request.status)
|
||||
return
|
||||
}
|
||||
client.off('data', onData)
|
||||
hosts.add(request.host)
|
||||
if (!allowedPorts.has(request.port)) {
|
||||
client.end(Buffer.from([5, 2, 0, 1, 0, 0, 0, 0, 0, 0]))
|
||||
return
|
||||
}
|
||||
connectSocksUpstream(client, request.port, request.remainder, routedSourcePorts, sockets)
|
||||
}
|
||||
client.on('data', onData)
|
||||
})
|
||||
}
|
||||
|
||||
function parseSocksGreeting(
|
||||
buffer: Buffer
|
||||
): { invalid: true } | { invalid: false; remainder: Buffer } | null {
|
||||
if (buffer.length < 2) {
|
||||
return null
|
||||
}
|
||||
const methodCount = buffer[1] ?? 0
|
||||
const greetingLength = 2 + methodCount
|
||||
if (buffer.length < greetingLength) {
|
||||
return null
|
||||
}
|
||||
if (buffer[0] !== SOCKS_VERSION || !buffer.subarray(2, greetingLength).includes(SOCKS_NO_AUTH)) {
|
||||
return { invalid: true }
|
||||
}
|
||||
return { invalid: false, remainder: buffer.subarray(greetingLength) }
|
||||
}
|
||||
|
||||
function parseSocksRequest(
|
||||
buffer: Buffer
|
||||
):
|
||||
| { invalid: false; host: string; port: number; remainder: Buffer }
|
||||
| { invalid: true; status: number }
|
||||
| null {
|
||||
if (buffer.length < 5) {
|
||||
return null
|
||||
}
|
||||
if (buffer[0] !== SOCKS_VERSION || buffer[2] !== 0) {
|
||||
return { invalid: true, status: SOCKS_GENERAL_FAILURE }
|
||||
}
|
||||
if (buffer[1] !== SOCKS_CONNECT) {
|
||||
return { invalid: true, status: SOCKS_COMMAND_NOT_SUPPORTED }
|
||||
}
|
||||
const type = buffer[3]
|
||||
const addressLength = type === 1 ? 4 : type === 4 ? 16 : type === 3 ? (buffer[4] ?? 0) + 1 : 0
|
||||
const requestLength = 4 + addressLength + 2
|
||||
if (addressLength === 0 || buffer.length < requestLength) {
|
||||
if (addressLength === 0) {
|
||||
return { invalid: true, status: SOCKS_GENERAL_FAILURE }
|
||||
}
|
||||
return null
|
||||
}
|
||||
const host =
|
||||
type === 3
|
||||
? buffer.subarray(5, 5 + (buffer[4] ?? 0)).toString('utf8')
|
||||
: type === 1
|
||||
? [...buffer.subarray(4, 8)].join('.')
|
||||
: 'ipv6'
|
||||
return {
|
||||
invalid: false,
|
||||
host,
|
||||
port: buffer.readUInt16BE(requestLength - 2),
|
||||
remainder: buffer.subarray(requestLength)
|
||||
}
|
||||
}
|
||||
|
||||
function closeWithSocksError(client: Socket, status: number): void {
|
||||
client.end(Buffer.from([SOCKS_VERSION, status, 0, 1, 0, 0, 0, 0, 0, 0]))
|
||||
}
|
||||
|
||||
function connectSocksUpstream(
|
||||
client: Socket,
|
||||
port: number,
|
||||
remainder: Buffer,
|
||||
routedSourcePorts: Set<number>,
|
||||
sockets: Set<Socket>
|
||||
): void {
|
||||
const upstream = new Socket()
|
||||
trackSocket(upstream, sockets)
|
||||
upstream.connect(port, '127.0.0.1', () => {
|
||||
const sourcePort = upstream.localPort ?? -1
|
||||
if (sourcePort > 0) {
|
||||
routedSourcePorts.add(sourcePort)
|
||||
}
|
||||
const removeSourcePort = (): void => {
|
||||
if (sourcePort > 0) {
|
||||
routedSourcePorts.delete(sourcePort)
|
||||
}
|
||||
}
|
||||
upstream.once('close', removeSourcePort)
|
||||
client.once('close', removeSourcePort)
|
||||
client.write(Buffer.from([5, 0, 0, 1, 127, 0, 0, 1, 0, 0]))
|
||||
if (remainder.length > 0) {
|
||||
upstream.write(remainder)
|
||||
}
|
||||
client.pipe(upstream).pipe(client)
|
||||
})
|
||||
upstream.on('error', () => client.destroy())
|
||||
client.on('error', () => upstream.destroy())
|
||||
client.once('close', () => upstream.destroy())
|
||||
}
|
||||
|
||||
function trackSocket(socket: Socket, sockets: Set<Socket>): void {
|
||||
sockets.add(socket)
|
||||
socket.on('error', () => socket.destroy())
|
||||
socket.once('close', () => sockets.delete(socket))
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { runBrowserRouteTcpEgressProbe } from './browser-route-tcp-egress-fixture'
|
||||
|
||||
const EXPECTED_PATHS = ['/asset', '/download', '/page', '/redirect', '/secure', '/socket']
|
||||
|
||||
describe('browser route TCP egress under Electron', () => {
|
||||
it('routes browser TCP surfaces and remote DNS through the fixed SOCKS session', async () => {
|
||||
const baseline = await runBrowserRouteTcpEgressProbe(false)
|
||||
const protectedSession = await runBrowserRouteTcpEgressProbe(true)
|
||||
|
||||
expect(baseline.resolvedProxy).toBe('DIRECT')
|
||||
expect(baseline.directPaths).toEqual(EXPECTED_PATHS)
|
||||
expect(baseline.routedPaths).toEqual([])
|
||||
expect(protectedSession.resolvedProxy).toMatch(/^SOCKS5 127\.0\.0\.1:\d+$/)
|
||||
expect(protectedSession.directPaths).toEqual([])
|
||||
expect(protectedSession.routedPaths).toEqual(EXPECTED_PATHS)
|
||||
// Check the target host; Chromium may send unrelated background requests through this proxy.
|
||||
expect(protectedSession.socksHosts).toContain('remote-browser.test')
|
||||
}, 60_000)
|
||||
})
|
||||
@@ -42,7 +42,7 @@ export function isDocPreviewSession(candidate: Electron.Session): boolean {
|
||||
}
|
||||
|
||||
// Block outbound requests that could exfiltrate grant-readable files.
|
||||
// Electron 43 DNS-prefetch bypasses these hooks; see browser-route-dns-prefetch.electron.test.ts.
|
||||
// Electron 43 DNS-prefetch bypasses these request hooks.
|
||||
const DOC_PREVIEW_CONTENT_SECURITY_POLICY = [
|
||||
"default-src 'self'",
|
||||
"script-src 'self' 'unsafe-inline'",
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { codexStructuredPermissionPolicy } from './codex-structured-permission-policy'
|
||||
|
||||
const FULL_ACCESS = {
|
||||
approvalPolicy: 'never',
|
||||
sandbox: 'danger-full-access',
|
||||
approvalsReviewer: 'user'
|
||||
}
|
||||
const ASK = { approvalPolicy: 'on-request', sandbox: 'read-only', approvalsReviewer: 'user' }
|
||||
|
||||
describe('codexStructuredPermissionPolicy', () => {
|
||||
it('maps Full access to no prompts and no sandbox', () => {
|
||||
expect(codexStructuredPermissionPolicy('full-access')).toEqual(FULL_ACCESS)
|
||||
})
|
||||
|
||||
it('states every Ask field, the human reviewer too', () => {
|
||||
expect(codexStructuredPermissionPolicy('ask')).toEqual(ASK)
|
||||
})
|
||||
|
||||
it('keeps Approve for me in the configured workspace sandbox', () => {
|
||||
expect(codexStructuredPermissionPolicy('auto')).toEqual({
|
||||
approvalPolicy: 'on-request',
|
||||
sandbox: 'workspace-write',
|
||||
approvalsReviewer: 'auto_review'
|
||||
})
|
||||
})
|
||||
|
||||
it('names a sandbox mode for every mode', () => {
|
||||
for (const mode of ['ask', 'auto', 'full-access'] as const) {
|
||||
expect(codexStructuredPermissionPolicy(mode)).not.toHaveProperty('sandboxPolicy')
|
||||
expect(typeof codexStructuredPermissionPolicy(mode).sandbox).toBe('string')
|
||||
}
|
||||
})
|
||||
|
||||
it('asks for a mode Codex does not have', () => {
|
||||
expect(codexStructuredPermissionPolicy('accept-edits')).toEqual(ASK)
|
||||
})
|
||||
})
|
||||
@@ -1,37 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { hasCommitObjectViaGitExec, isFullGitObjectId } from './commit-object-ref'
|
||||
|
||||
describe('commit object refs', () => {
|
||||
it('recognizes only complete git object IDs', () => {
|
||||
expect(isFullGitObjectId('a'.repeat(40))).toBe(true)
|
||||
expect(isFullGitObjectId('A'.repeat(40))).toBe(true)
|
||||
expect(isFullGitObjectId('abc123')).toBe(false)
|
||||
expect(isFullGitObjectId('origin/main')).toBe(false)
|
||||
expect(isFullGitObjectId('g'.repeat(40))).toBe(false)
|
||||
})
|
||||
|
||||
it('verifies full commit objects and rejects missing objects', async () => {
|
||||
const gitExec = vi.fn().mockResolvedValue({ stdout: 'a'.repeat(40), stderr: '' })
|
||||
|
||||
await expect(hasCommitObjectViaGitExec(gitExec, 'a'.repeat(40))).resolves.toBe(true)
|
||||
|
||||
expect(gitExec).toHaveBeenCalledWith([
|
||||
'rev-parse',
|
||||
'--verify',
|
||||
'--quiet',
|
||||
`${'a'.repeat(40)}^{commit}`
|
||||
])
|
||||
|
||||
gitExec.mockRejectedValueOnce(new Error('missing'))
|
||||
await expect(hasCommitObjectViaGitExec(gitExec, 'b'.repeat(40))).resolves.toBe(false)
|
||||
})
|
||||
|
||||
it('does not shell out for branch names or short SHAs', async () => {
|
||||
const gitExec = vi.fn()
|
||||
|
||||
await expect(hasCommitObjectViaGitExec(gitExec, 'abc123')).resolves.toBe(false)
|
||||
await expect(hasCommitObjectViaGitExec(gitExec, 'origin/main')).resolves.toBe(false)
|
||||
|
||||
expect(gitExec).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,46 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { gitExecFileAsyncMock } = vi.hoisted(() => ({
|
||||
gitExecFileAsyncMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('./runner', () => ({
|
||||
gitExecFileAsync: gitExecFileAsyncMock,
|
||||
gitExecFileSync: vi.fn()
|
||||
}))
|
||||
|
||||
import { getBaseRefDefault } from './repo'
|
||||
|
||||
describe('getBaseRefDefault async subprocess bounds', () => {
|
||||
beforeEach(() => {
|
||||
gitExecFileAsyncMock.mockReset()
|
||||
})
|
||||
|
||||
it('bounds every local probe and degrades a timeout to no default', async () => {
|
||||
gitExecFileAsyncMock.mockRejectedValue(new Error('git timed out.'))
|
||||
|
||||
await expect(getBaseRefDefault('/repo')).resolves.toBeNull()
|
||||
|
||||
expect(gitExecFileAsyncMock).toHaveBeenCalled()
|
||||
for (const [, options] of gitExecFileAsyncMock.mock.calls) {
|
||||
expect(options).toEqual({ cwd: '/repo', timeout: 15_000 })
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves the same timeout when routing probes through WSL', async () => {
|
||||
gitExecFileAsyncMock.mockRejectedValue(new Error('git timed out.'))
|
||||
|
||||
await expect(
|
||||
getBaseRefDefault('\\\\wsl.localhost\\Ubuntu\\repo', { wslDistro: 'Ubuntu' })
|
||||
).resolves.toBeNull()
|
||||
|
||||
expect(gitExecFileAsyncMock).toHaveBeenCalled()
|
||||
for (const [, options] of gitExecFileAsyncMock.mock.calls) {
|
||||
expect(options).toEqual({
|
||||
cwd: '\\\\wsl.localhost\\Ubuntu\\repo',
|
||||
timeout: 15_000,
|
||||
wslDistro: 'Ubuntu'
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,31 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { detachedBranchUse } = vi.hoisted(() => ({ detachedBranchUse: vi.fn(async () => false) }))
|
||||
vi.mock('../../shared/git-worktree-admin', () => ({
|
||||
isBranchReservedByWorktreeOperation: detachedBranchUse
|
||||
}))
|
||||
vi.mock('./local-repo-ref-maintenance', () => ({
|
||||
withRepoRefMaintenancePaused: (_reason: string, run: () => unknown) => run()
|
||||
}))
|
||||
|
||||
import { forceDeleteLocalBranch } from './worktree-branch-removal'
|
||||
|
||||
describe('preserved branch cleanup execution-host options', () => {
|
||||
it('uses the same WSL distro for both detached admin guards and the ref mutation', async () => {
|
||||
const repoPath = String.raw`\\wsl.localhost\Ubuntu\home\user\repo`
|
||||
const options = { wslDistro: 'Ubuntu' }
|
||||
const runGit = vi.fn(async (argv: string[]) => ({
|
||||
stdout:
|
||||
argv[0] === 'worktree'
|
||||
? 'worktree /home/user/repo\nHEAD abc\nbranch refs/heads/main\n\nworktree /home/user/detached\nHEAD abc\ndetached\n'
|
||||
: '',
|
||||
stderr: ''
|
||||
}))
|
||||
await forceDeleteLocalBranch(repoPath, 'feature', 'abc', runGit, options)
|
||||
expect(detachedBranchUse).toHaveBeenCalledTimes(2)
|
||||
for (const call of detachedBranchUse.mock.calls) {
|
||||
expect(call).toEqual([repoPath, 'feature', expect.any(Array), options])
|
||||
}
|
||||
expect(runGit).toHaveBeenCalledWith(['update-ref', '-d', 'refs/heads/feature', 'abc'], repoPath)
|
||||
})
|
||||
})
|
||||
@@ -2,37 +2,6 @@ import { describe, expect, it } from 'vitest'
|
||||
import { parseKeyboardLayoutSnapshot } from './macos-keyboard-layout-snapshot'
|
||||
|
||||
describe('parseKeyboardLayoutSnapshot', () => {
|
||||
it('accepts input-source identity and per-modifier characters', () => {
|
||||
expect(
|
||||
parseKeyboardLayoutSnapshot(
|
||||
JSON.stringify({
|
||||
inputSourceId: 'com.apple.keylayout.Latvian',
|
||||
layoutSourceId: 'com.apple.keylayout.Latvian',
|
||||
keyCharacters: {
|
||||
Digit2: {
|
||||
unmodified: '2',
|
||||
shifted: '@'
|
||||
},
|
||||
KeyE: { unmodified: 'e', shifted: null }
|
||||
}
|
||||
})
|
||||
)
|
||||
).toEqual({
|
||||
inputSourceId: 'com.apple.keylayout.Latvian',
|
||||
layoutSourceId: 'com.apple.keylayout.Latvian',
|
||||
keyCharacters: {
|
||||
Digit2: {
|
||||
unmodified: '2',
|
||||
shifted: '@'
|
||||
},
|
||||
KeyE: {
|
||||
unmodified: 'e',
|
||||
shifted: null
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects malformed snapshots and skips malformed key entries', () => {
|
||||
expect(parseKeyboardLayoutSnapshot('not json')).toBeNull()
|
||||
expect(parseKeyboardLayoutSnapshot('{}')).toBeNull()
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { hydrateMock, registerHandlersMock } = vi.hoisted(() => ({
|
||||
hydrateMock: vi.fn(),
|
||||
registerHandlersMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('./register-handlers', () => ({
|
||||
registerPtyHandlers: registerHandlersMock
|
||||
}))
|
||||
|
||||
vi.mock('../../memory/hydrate-local-pty-registry', () => ({
|
||||
hydrateLocalPtyRegistryAtBoot: hydrateMock
|
||||
}))
|
||||
|
||||
import { registerHeadlessPtyRuntime } from './register-headless-runtime'
|
||||
|
||||
describe('registerHeadlessPtyRuntime', () => {
|
||||
it('registers handlers before awaiting registry hydration', async () => {
|
||||
let resolveHydration!: () => void
|
||||
const hydration = new Promise<void>((resolve) => {
|
||||
resolveHydration = resolve
|
||||
})
|
||||
const events: string[] = []
|
||||
registerHandlersMock.mockImplementation(() => events.push('handlers'))
|
||||
hydrateMock.mockImplementation(() => {
|
||||
events.push('hydrate')
|
||||
return hydration
|
||||
})
|
||||
const store = {} as never
|
||||
|
||||
const ready = registerHeadlessPtyRuntime({} as never, undefined, undefined, undefined, store)
|
||||
|
||||
expect(events).toEqual(['handlers', 'hydrate'])
|
||||
expect(registerHandlersMock).toHaveBeenCalledOnce()
|
||||
expect(registerHandlersMock.mock.calls[0]?.[0]).toBeUndefined()
|
||||
expect(hydrateMock).toHaveBeenCalledWith(store)
|
||||
|
||||
resolveHydration()
|
||||
await ready
|
||||
})
|
||||
})
|
||||
@@ -1,42 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { PtyExitProof, PtyRetirementWithoutProof } from '../../../pty-exit-proof'
|
||||
import { publishSyntheticExit } from './kill'
|
||||
|
||||
function harness() {
|
||||
const calls: string[] = []
|
||||
const runtime = {
|
||||
markPtyLivenessUnverifiable: (_id: string, reason: string) => {
|
||||
calls.push(`unverifiable:${reason}`)
|
||||
},
|
||||
onPtyExit: () => {
|
||||
calls.push('exit')
|
||||
}
|
||||
}
|
||||
const sendPtyExitToRenderer = vi.fn()
|
||||
const deps = {
|
||||
runtime,
|
||||
rememberSyntheticKillExit: vi.fn(),
|
||||
sendPtyExitToRenderer
|
||||
}
|
||||
return { calls, deps, sendPtyExitToRenderer }
|
||||
}
|
||||
|
||||
describe('a synthetic PTY exit', () => {
|
||||
it('reads unverifiable, before the exit lands, when it rests on no proof', () => {
|
||||
const { calls, deps, sendPtyExitToRenderer } = harness()
|
||||
const unproven = PtyRetirementWithoutProof.because('gone')
|
||||
publishSyntheticExit(deps, 'pty-1', -1, 'inc-1', unproven)
|
||||
expect(calls).toEqual(['unverifiable:gone', 'exit'])
|
||||
expect(sendPtyExitToRenderer).toHaveBeenCalledWith({
|
||||
id: 'pty-1',
|
||||
code: -1,
|
||||
incarnationId: 'inc-1'
|
||||
})
|
||||
})
|
||||
|
||||
it('leaves the liveness verdict to the exit when the host proved it', () => {
|
||||
const { calls, deps } = harness()
|
||||
publishSyntheticExit(deps, 'pty-1', 0, undefined, PtyExitProof.from('absent', undefined))
|
||||
expect(calls).toEqual(['exit'])
|
||||
})
|
||||
})
|
||||
@@ -1,38 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const getAllWindows = vi.hoisted(() => vi.fn())
|
||||
vi.mock('electron', () => ({ BrowserWindow: { getAllWindows } }))
|
||||
|
||||
import { RUNTIME_ENVIRONMENT_DIAGNOSTICS_CHANNEL } from '../../../shared/runtime-environment-diagnostics'
|
||||
import { publishRuntimeEnvironmentDiagnostics } from './runtime-environment-diagnostics-broadcast'
|
||||
|
||||
describe('runtime environment diagnostics broadcast', () => {
|
||||
beforeEach(() => getAllWindows.mockReset())
|
||||
|
||||
it('publishes to live renderer windows and skips destroyed windows', () => {
|
||||
const live = { isDestroyed: () => false, webContents: { send: vi.fn() } }
|
||||
const destroyed = { isDestroyed: () => true, webContents: { send: vi.fn() } }
|
||||
getAllWindows.mockReturnValue([live, destroyed])
|
||||
const event = {
|
||||
environmentId: 'env-a',
|
||||
transportGeneration: 2,
|
||||
diagnostics: {
|
||||
state: 'reconnecting' as const,
|
||||
pendingRequestCount: 0,
|
||||
subscriptionCount: 1,
|
||||
reconnectAttempt: 1,
|
||||
lastConnectedAt: 1,
|
||||
lastClose: null,
|
||||
lastError: 'offline'
|
||||
}
|
||||
}
|
||||
|
||||
publishRuntimeEnvironmentDiagnostics(event)
|
||||
|
||||
expect(live.webContents.send).toHaveBeenCalledWith(
|
||||
RUNTIME_ENVIRONMENT_DIAGNOSTICS_CHANNEL,
|
||||
event
|
||||
)
|
||||
expect(destroyed.webContents.send).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,38 +0,0 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { resolveOrcadWebClientRoot } from './orcad-command-arguments'
|
||||
|
||||
describe('resolveOrcadWebClientRoot', () => {
|
||||
const roots: string[] = []
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
function scratch(): string {
|
||||
const root = mkdtempSync(join(tmpdir(), 'orcad-web-root-'))
|
||||
roots.push(root)
|
||||
return root
|
||||
}
|
||||
|
||||
it('serves a directory that holds the web client index', () => {
|
||||
const root = scratch()
|
||||
writeFileSync(join(root, 'web-index.html'), '<html></html>')
|
||||
expect(resolveOrcadWebClientRoot(root)).toBe(root)
|
||||
})
|
||||
|
||||
it('refuses a directory without the index, saying so, rather than advertising a dead URL', () => {
|
||||
const warn = vi.fn()
|
||||
expect(resolveOrcadWebClientRoot(scratch(), warn)).toBeUndefined()
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringContaining('web-index.html'))
|
||||
})
|
||||
|
||||
it('stays without a web client when none was handed over', () => {
|
||||
const warn = vi.fn()
|
||||
expect(resolveOrcadWebClientRoot(undefined, warn)).toBeUndefined()
|
||||
expect(warn).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -9,27 +9,6 @@ vi.mock('electron', () => ({
|
||||
}))
|
||||
|
||||
describe('managed skill startup recovery', () => {
|
||||
it('waits for startup recovery before reading local managed installs', async () => {
|
||||
let finishRecovery!: () => void
|
||||
const recovery = new Promise<void>((resolve) => {
|
||||
finishRecovery = resolve
|
||||
})
|
||||
const runtime = new OrcaRuntimeService(null, undefined, {
|
||||
skillTransactionRecovery: recovery
|
||||
})
|
||||
const listing = runtime.listManagedSkillInstalls()
|
||||
let settled = false
|
||||
void listing.finally(() => {
|
||||
settled = true
|
||||
})
|
||||
|
||||
await Promise.resolve()
|
||||
expect(settled).toBe(false)
|
||||
|
||||
finishRecovery()
|
||||
await expect(listing).resolves.toEqual([])
|
||||
})
|
||||
|
||||
it('continues skill management after startup recovery fails', async () => {
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
||||
const runtime = new OrcaRuntimeService(null, undefined, {
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { OrcaRuntimeService } from '../orca-runtime-test-mocks.spec'
|
||||
import { TEST_REPO_ID, store } from '../orca-runtime-test-fixtures.spec'
|
||||
|
||||
describe('OrcaRuntimeService', () => {
|
||||
it('uses remote path joins for SSH issue-command files and touches no filesystem', async () => {
|
||||
const remoteStore = {
|
||||
...store,
|
||||
getRepos: () => [
|
||||
{
|
||||
id: TEST_REPO_ID,
|
||||
path: 'C:/remote/repo',
|
||||
displayName: 'repo',
|
||||
badgeColor: 'blue',
|
||||
addedAt: 1,
|
||||
connectionId: 'ssh-1'
|
||||
}
|
||||
]
|
||||
}
|
||||
const runtime = new OrcaRuntimeService(remoteStore as never)
|
||||
|
||||
await expect(runtime.checkRepoHooks('id:repo-1')).resolves.toMatchObject({
|
||||
status: 'error',
|
||||
hasHooks: false
|
||||
})
|
||||
await expect(runtime.readRepoIssueCommand('id:repo-1')).resolves.toMatchObject({
|
||||
localContent: null,
|
||||
effectiveContent: null,
|
||||
localFilePath: 'C:\\remote\\repo\\.orca\\issue-command'
|
||||
})
|
||||
await expect(runtime.writeRepoIssueCommand('id:repo-1', 'Ship it')).resolves.toEqual({
|
||||
ok: true
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -24,7 +24,6 @@ await import('./orca-runtime-tests/local-worktree-creation-part-02.spec')
|
||||
await import('./orca-runtime-tests/worktree-create-execution-host.spec')
|
||||
await import('./orca-runtime-tests/ssh-worktree-lifecycle.spec')
|
||||
await import('./orca-runtime-tests/ssh-worktree-lifecycle-part-02.spec')
|
||||
await import('./orca-runtime-tests/ssh-worktree-lifecycle-part-03.spec')
|
||||
await import('./orca-runtime-tests/hooks-and-hosted-review.spec')
|
||||
await import('./orca-runtime-tests/hooks-and-hosted-review-part-02.spec')
|
||||
await import('./orca-runtime-tests/hooks-and-hosted-review-part-03.spec')
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
decideWorkerStartMode,
|
||||
resolveWorkerStartModeOnHost
|
||||
} from './orchestration-worker-start-mode'
|
||||
|
||||
const mode = decideWorkerStartMode({
|
||||
params: { agent: 'claude' },
|
||||
settings: {
|
||||
experimentalNativeChat: true
|
||||
}
|
||||
})
|
||||
|
||||
describe('host support evidence', () => {
|
||||
it('distinguishes an unanswered host from an explicit refusal without creating a session', async () => {
|
||||
const getStructuredAgentSessionCreateSupport = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('disconnected'))
|
||||
const runtime = { getStructuredAgentSessionCreateSupport }
|
||||
const unknown = await resolveWorkerStartModeOnHost(runtime, mode, 'workspace-1', 'claude')
|
||||
expect(unknown).toMatchObject({
|
||||
mode: 'terminal',
|
||||
preferred: 'structured',
|
||||
reason: 'structured_support_unknown'
|
||||
})
|
||||
expect(unknown.detail).toContain('has not established')
|
||||
expect(getStructuredAgentSessionCreateSupport).toHaveBeenCalledWith(
|
||||
'id:workspace-1',
|
||||
'claude',
|
||||
{
|
||||
requireInstalledChatProgram: true
|
||||
}
|
||||
)
|
||||
getStructuredAgentSessionCreateSupport.mockResolvedValue({ supported: false })
|
||||
const refusal = await resolveWorkerStartModeOnHost(runtime, mode, 'workspace-1', 'claude')
|
||||
expect(refusal.reason).toBe('structured_unsupported_on_host')
|
||||
expect(refusal.detail).toContain('cannot create')
|
||||
})
|
||||
})
|
||||
@@ -1,49 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { inspectionShowsShellAlone } from './run-terminal-shell-alone'
|
||||
|
||||
describe('inspectionShowsShellAlone (the Windows proof)', () => {
|
||||
it('proves a shell alone only on an observed empty child census', () => {
|
||||
expect(
|
||||
inspectionShowsShellAlone({
|
||||
foregroundProcess: 'pwsh.exe',
|
||||
hasChildProcesses: false,
|
||||
childProcessEvidence: 'no-children'
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
[
|
||||
'a child still runs',
|
||||
{
|
||||
foregroundProcess: 'node.exe',
|
||||
hasChildProcesses: true,
|
||||
childProcessEvidence: 'children' as const
|
||||
}
|
||||
],
|
||||
[
|
||||
'the host could not read its process table',
|
||||
{
|
||||
foregroundProcess: 'pwsh.exe',
|
||||
hasChildProcesses: false,
|
||||
childProcessEvidence: 'unverifiable' as const
|
||||
}
|
||||
],
|
||||
[
|
||||
'an older host sent no child census',
|
||||
{ foregroundProcess: 'pwsh.exe', hasChildProcesses: false }
|
||||
],
|
||||
[
|
||||
'the host could not be reached',
|
||||
{
|
||||
foregroundProcess: null,
|
||||
hasChildProcesses: false as const,
|
||||
verdict: 'unverifiable' as const,
|
||||
reason: 'timeout'
|
||||
}
|
||||
]
|
||||
])('stays unproven when %s', (_case, inspection) => {
|
||||
expect(inspectionShowsShellAlone(inspection)).toBe(false)
|
||||
expect(inspectionShowsShellAlone(null)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,37 +0,0 @@
|
||||
import { join } from 'node:path'
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { afterEach, expect, it } from 'vitest'
|
||||
import { createStructuredAgentSessionLogger } from '../native-chat/agent-session-wire/structured-agent-session-logger'
|
||||
import {
|
||||
ensureStructuredAgentSessionHost,
|
||||
stopStructuredAgentSessionRuntime,
|
||||
STRUCTURED_AGENT_LAUNCH_ARGS_REQUIRED
|
||||
} from './structured-agent-session-runtime'
|
||||
|
||||
let stateDirectory: string | undefined
|
||||
|
||||
afterEach(async () => {
|
||||
await stopStructuredAgentSessionRuntime()
|
||||
if (stateDirectory) {
|
||||
await rm(stateDirectory, { recursive: true, force: true })
|
||||
stateDirectory = undefined
|
||||
}
|
||||
})
|
||||
|
||||
it('refuses installation when the host omits the saved Arguments source', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'orca-launch-args-wiring-'))
|
||||
stateDirectory = root
|
||||
|
||||
await expect(
|
||||
// @ts-expect-error Exercise an unchecked caller that dropped the required resolver.
|
||||
ensureStructuredAgentSessionHost({
|
||||
stateDirectory: root,
|
||||
hostId: 'local',
|
||||
claimKeyId: 'key-1',
|
||||
resolveWorkspacePath: async () => root,
|
||||
resolveClaudeAuthPolicy: () => ({ account: 'managed' }),
|
||||
logger: createStructuredAgentSessionLogger()
|
||||
})
|
||||
).rejects.toThrow(STRUCTURED_AGENT_LAUNCH_ARGS_REQUIRED)
|
||||
})
|
||||
@@ -1,40 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
STRUCTURED_AGENT_RUNTIME_METADATA,
|
||||
structuredAgentRuntimeMetadata
|
||||
} from './structured-agent-runtime-metadata'
|
||||
|
||||
vi.mock('./structured-agent-runtime-registrations', () => {
|
||||
throw new Error('Metadata reads must not load adapter factories')
|
||||
})
|
||||
vi.mock('../codex/codex-structured-session-adapter', () => {
|
||||
throw new Error('Metadata reads must not load the Codex adapter')
|
||||
})
|
||||
vi.mock('./structured-claude-runtime-adapter', () => {
|
||||
throw new Error('Metadata reads must not load the Claude adapter')
|
||||
})
|
||||
vi.mock('../acp/acp-structured-session-adapter', () => {
|
||||
throw new Error('Metadata reads must not load the ACP adapter')
|
||||
})
|
||||
vi.mock('../pi/rpc-session-adapter', () => {
|
||||
throw new Error('Metadata reads must not load the Pi adapter')
|
||||
})
|
||||
|
||||
describe('structured agent metadata without adapter implementations', () => {
|
||||
it('lists the registered agents before any host or factory is loaded', () => {
|
||||
expect(STRUCTURED_AGENT_RUNTIME_METADATA.map(({ definition }) => definition.agent)).toEqual([
|
||||
'pi',
|
||||
'codex',
|
||||
'claude',
|
||||
'grok',
|
||||
'opencode',
|
||||
'opencode2',
|
||||
'omp',
|
||||
'antigravity'
|
||||
])
|
||||
expect(
|
||||
STRUCTURED_AGENT_RUNTIME_METADATA.every((metadata) => !('createAdapter' in metadata))
|
||||
).toBe(true)
|
||||
expect(structuredAgentRuntimeMetadata('cursor')).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,37 +0,0 @@
|
||||
import { join } from 'node:path'
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
CLAUDE_STRUCTURED_AUTH_POLICY_REQUIRED,
|
||||
ensureStructuredAgentSessionHost,
|
||||
stopStructuredAgentSessionRuntime
|
||||
} from './structured-agent-session-runtime'
|
||||
|
||||
/** Require production auth-policy wiring so failed sign-in cannot name the wrong account. */
|
||||
describe('structured Claude auth policy wiring', () => {
|
||||
describe('installing without one', () => {
|
||||
let stateDirectory: string | null = null
|
||||
|
||||
afterEach(async () => {
|
||||
await stopStructuredAgentSessionRuntime()
|
||||
if (stateDirectory) {
|
||||
await rm(stateDirectory, { recursive: true, force: true })
|
||||
stateDirectory = null
|
||||
}
|
||||
})
|
||||
|
||||
it('refuses loudly rather than defaulting to a guess', async () => {
|
||||
stateDirectory = await mkdtemp(join(tmpdir(), 'orca-auth-policy-wiring-'))
|
||||
|
||||
await expect(
|
||||
ensureStructuredAgentSessionHost({
|
||||
stateDirectory,
|
||||
hostId: 'local',
|
||||
claimKeyId: 'key-1',
|
||||
resolveWorkspacePath: async () => stateDirectory as string
|
||||
} as unknown as Parameters<typeof ensureStructuredAgentSessionHost>[0])
|
||||
).rejects.toThrow(CLAUDE_STRUCTURED_AUTH_POLICY_REQUIRED)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,19 +1,8 @@
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { appendNormalizedToTailBuffer } from './terminal-tail-buffer'
|
||||
import { trimTerminalLineRight } from './terminal-tail-line-controls'
|
||||
|
||||
describe('terminal redraw whitespace', () => {
|
||||
it.each([
|
||||
['hello \t', 'hello'],
|
||||
[' \thello \t world \t', ' \thello \t world'],
|
||||
[' \t', ''],
|
||||
['hello\u00a0 \t', 'hello\u00a0'],
|
||||
['hello\n', 'hello\n']
|
||||
])('preserves terminal text while trimming spaces and tabs: %j', (input, expected) => {
|
||||
expect(trimTerminalLineRight(input)).toBe(expected)
|
||||
})
|
||||
|
||||
it.each([2, 20])('handles padded redraws across %i retained rows without stalling', (rows) => {
|
||||
const padded = `${' '.repeat(32_000)}marker \t`
|
||||
const previousLines = Array.from({ length: rows }, (_, index) =>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { TerminalCreateParams } from '../../../../shared/rpc-contract/terminal-unary-params'
|
||||
import { setupLayoutOwnerRuntime, WORKTREE_ID } from '../../orca-runtime-layout-owner-start-fixture'
|
||||
|
||||
// Why: pinned off, so a run with ORCA_LAYOUT_OWNER=main still checks the switch-off refusal.
|
||||
@@ -14,13 +13,4 @@ describe("the window's new-tab start with the layout-owner build off", () => {
|
||||
).rejects.toThrow('host_not_owned')
|
||||
expect(spawn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('leaves an older caller’s terminal.create request as it parsed before', () => {
|
||||
const older = { worktree: `id:${WORKTREE_ID}`, command: 'ls', tabId: 't', leafId: 'l' }
|
||||
expect(TerminalCreateParams.parse(older)).toEqual(older)
|
||||
expect(
|
||||
TerminalCreateParams.parse({ ...older, windowTab: true, cwd: '/workspace/sub' })
|
||||
).toMatchObject({ windowTab: true, cwd: '/workspace/sub' })
|
||||
expect(() => TerminalCreateParams.parse({ ...older, windowTab: false })).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { exitedSplitPaneOutcome, type ExitedSplitPaneFacts } from './exited-split-pane-rule'
|
||||
|
||||
const used: ExitedSplitPaneFacts = {
|
||||
exitCode: 0,
|
||||
freshSpawn: true,
|
||||
inputSeen: true,
|
||||
outputSeen: true,
|
||||
shown: true
|
||||
}
|
||||
|
||||
describe('exitedSplitPaneOutcome', () => {
|
||||
it('closes a split pane whose shell exited cleanly', () => {
|
||||
expect(exitedSplitPaneOutcome(used)).toBe('close')
|
||||
})
|
||||
|
||||
it('keeps a failed process behind its card', () => {
|
||||
expect(exitedSplitPaneOutcome({ ...used, exitCode: 1 })).toBe('keep')
|
||||
})
|
||||
|
||||
it('keeps a pane on any negative code, as the window does: the code proves no death', () => {
|
||||
expect(exitedSplitPaneOutcome({ ...used, exitCode: -1 })).toBe('keep')
|
||||
expect(exitedSplitPaneOutcome({ ...used, exitCode: -1 }, true)).toBe('keep')
|
||||
})
|
||||
|
||||
it('keeps a newborn split that died while shown before any input or output', () => {
|
||||
const newborn = { ...used, inputSeen: false, outputSeen: false }
|
||||
expect(exitedSplitPaneOutcome(newborn)).toBe('keep')
|
||||
expect(exitedSplitPaneOutcome({ ...newborn, shown: false })).toBe('close')
|
||||
expect(exitedSplitPaneOutcome({ ...newborn, freshSpawn: false })).toBe('close')
|
||||
expect(exitedSplitPaneOutcome({ ...newborn, inputSeen: true })).toBe('close')
|
||||
expect(exitedSplitPaneOutcome({ ...newborn, outputSeen: true })).toBe('close')
|
||||
})
|
||||
})
|
||||
@@ -1,31 +0,0 @@
|
||||
import { readdir, readFile } from 'node:fs/promises'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { BUNDLED_SKILL_SOURCES } from './bundled-skill-sources'
|
||||
import { bundledSkillTextDigest } from './classify'
|
||||
|
||||
const REPO_ROOT = resolve(import.meta.dirname, '..', '..', '..', '..')
|
||||
|
||||
describe('embedded bundled skill sources', () => {
|
||||
it('carry exactly the repo’s skills/*/SKILL.md, LF-normalised', async () => {
|
||||
const names = (await readdir(join(REPO_ROOT, 'skills'), { withFileTypes: true }))
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name)
|
||||
.sort()
|
||||
expect(BUNDLED_SKILL_SOURCES.map((source) => source.name)).toEqual(names)
|
||||
for (const source of BUNDLED_SKILL_SOURCES) {
|
||||
const onDisk = await readFile(join(REPO_ROOT, 'skills', source.name, 'SKILL.md'), 'utf8')
|
||||
expect(source.text).toBe(onDisk.replace(/\r\n?/gu, '\n'))
|
||||
}
|
||||
})
|
||||
|
||||
it('list this build’s digest and agree with the released ledger', async () => {
|
||||
const ledger = JSON.parse(
|
||||
await readFile(join(REPO_ROOT, 'resources', 'skills', 'released-digests.json'), 'utf8')
|
||||
)
|
||||
for (const source of BUNDLED_SKILL_SOURCES) {
|
||||
expect(source.releasedDigests).toContain(bundledSkillTextDigest(Buffer.from(source.text)))
|
||||
expect(source.releasedDigests).toEqual(ledger.skills[source.name])
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,44 +0,0 @@
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { listManagedSkillInstalls, writeSkillInstallReceipt } from './skill-install-provenance'
|
||||
|
||||
describe('managed skill install receipts', () => {
|
||||
let root = ''
|
||||
|
||||
beforeEach(async () => {
|
||||
root = await mkdtemp(join(tmpdir(), 'orca-skill-receipts-test-'))
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('lists bounded receipt metadata without exposing package contents', async () => {
|
||||
await writeSkillInstallReceipt(root, {
|
||||
schemaVersion: 1,
|
||||
packageId: 'package-1',
|
||||
versionId: 'version-1',
|
||||
packageDigest: 'a'.repeat(64),
|
||||
archiveSha256: 'b'.repeat(64),
|
||||
scope: 'global',
|
||||
destinationIdentity: 'global:runtime-1',
|
||||
canonicalPath: join(root, 'missing', 'example'),
|
||||
placements: [],
|
||||
providers: ['codex', 'claude'],
|
||||
installedAt: '2026-08-11T00:00:00.000Z',
|
||||
hostIdentity: 'runtime-1'
|
||||
})
|
||||
|
||||
await expect(listManagedSkillInstalls(root)).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
name: 'example',
|
||||
packageId: 'package-1',
|
||||
versionId: 'version-1',
|
||||
providers: ['codex', 'claude'],
|
||||
state: 'missing'
|
||||
})
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -1,38 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { orcadMigrationConflictGuidance } from './orcad-migration-conflict-guidance'
|
||||
|
||||
const NO_DESKTOP_STEP = [
|
||||
'project_group_id',
|
||||
'folder_workspace_id',
|
||||
'dormant_id',
|
||||
'dormant_focus',
|
||||
'client_state',
|
||||
'close_intent_destination',
|
||||
'snapshot_destination'
|
||||
]
|
||||
|
||||
describe('orcad migration conflict guidance', () => {
|
||||
// This guidance also reaches clients without a plain SSH terminal.
|
||||
it('names the code for every conflict retrying can’t clear, and no client-only action', () => {
|
||||
for (const kind of NO_DESKTOP_STEP) {
|
||||
const text = orcadMigrationConflictGuidance(
|
||||
new Error(`orcad_migration_${kind}_conflict:ui:workspaceHostScope`)
|
||||
)
|
||||
expect(text).toContain('Retrying won’t clear it.')
|
||||
expect(text).not.toContain('plain SSH')
|
||||
expect(text).toContain(`orcad_migration_${kind}_conflict`)
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps the specific way out where the desktop has one', () => {
|
||||
expect(
|
||||
orcadMigrationConflictGuidance(
|
||||
new Error('orcad_migration_repository_path_conflict:/home/me/app')
|
||||
)
|
||||
).toContain('remove that project from this desktop and connect again')
|
||||
})
|
||||
|
||||
it('is null for anything that is not a conflict', () => {
|
||||
expect(orcadMigrationConflictGuidance(new Error('ECONNRESET'))).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,30 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
createSshDisposalError,
|
||||
isSshRequestOutcomeUnverifiable,
|
||||
SSH_MUX_REQUEST_TIMEOUT_CODE
|
||||
} from './ssh-channel-multiplexer'
|
||||
|
||||
// Use live/unverifiable/exited vocabulary: lost contact never proves absence (docs/reference/ssh-execution-boundary.md).
|
||||
describe('SSH request outcome verdict', () => {
|
||||
it('treats a response deadline as unverifiable', () => {
|
||||
const timedOut = Object.assign(new Error('Request "x" timed out after 30000ms'), {
|
||||
code: SSH_MUX_REQUEST_TIMEOUT_CODE
|
||||
})
|
||||
expect(isSshRequestOutcomeUnverifiable(timedOut)).toBe(true)
|
||||
})
|
||||
|
||||
it('treats a link declared lost as unverifiable, not as absence', () => {
|
||||
// Wedged links now report CONNECTION_LOST instead of request timeout; both must retain possibly-running host outcome.
|
||||
expect(isSshRequestOutcomeUnverifiable(createSshDisposalError('connection_lost'))).toBe(true)
|
||||
})
|
||||
|
||||
it('does not claim unverifiable for a deliberate shutdown', () => {
|
||||
expect(isSshRequestOutcomeUnverifiable(createSshDisposalError('shutdown'))).toBe(false)
|
||||
})
|
||||
|
||||
it('does not claim unverifiable for an ordinary failure', () => {
|
||||
expect(isSshRequestOutcomeUnverifiable(new Error('boom'))).toBe(false)
|
||||
expect(isSshRequestOutcomeUnverifiable(undefined)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,54 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { shouldQuitWhenAllWindowsClosed } from './window-all-closed-quit-policy'
|
||||
|
||||
describe('shouldQuitWhenAllWindowsClosed', () => {
|
||||
it('keeps headless serve alive when its offscreen browser windows close', () => {
|
||||
expect(
|
||||
shouldQuitWhenAllWindowsClosed({
|
||||
platform: 'linux',
|
||||
isQuitting: false,
|
||||
isServeMode: true
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps normal macOS close-all behavior outside quit', () => {
|
||||
expect(
|
||||
shouldQuitWhenAllWindowsClosed({
|
||||
platform: 'darwin',
|
||||
isQuitting: false,
|
||||
isServeMode: false
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('quits desktop Linux when all windows close', () => {
|
||||
expect(
|
||||
shouldQuitWhenAllWindowsClosed({
|
||||
platform: 'linux',
|
||||
isQuitting: false,
|
||||
isServeMode: false
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('continues a committed quit on macOS after all windows close', () => {
|
||||
expect(
|
||||
shouldQuitWhenAllWindowsClosed({
|
||||
platform: 'darwin',
|
||||
isQuitting: true,
|
||||
isServeMode: false
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('continues a committed quit after a serve owner was promoted to desktop', () => {
|
||||
expect(
|
||||
shouldQuitWhenAllWindowsClosed({
|
||||
platform: 'darwin',
|
||||
isQuitting: true,
|
||||
isServeMode: true
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,34 +0,0 @@
|
||||
/** #11375: choose rules from the execution host; a Windows client can still run a POSIX command remotely. */
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { commandBackslashMode } from './commit-message-text-generation'
|
||||
|
||||
const LOCAL = { kind: 'local' as const, cwd: 'C:\\repo' }
|
||||
const REMOTE = {
|
||||
kind: 'remote' as const,
|
||||
cwd: '/repo',
|
||||
execute: async () => ({ ok: true }) as never,
|
||||
missingBinaryLocation: 'the remote host'
|
||||
}
|
||||
|
||||
describe('commandBackslashMode', () => {
|
||||
it('reads backslashes literally only for a native Windows local target', () => {
|
||||
expect(commandBackslashMode(LOCAL, 'win32')).toBe('literal')
|
||||
})
|
||||
|
||||
it.each<NodeJS.Platform>(['darwin', 'linux'])('keeps POSIX escaping on %s', (platform) => {
|
||||
expect(commandBackslashMode(LOCAL, platform)).toBe('escape')
|
||||
})
|
||||
|
||||
it('keeps POSIX escaping for a WSL target, which runs a Linux binary', () => {
|
||||
expect(commandBackslashMode({ ...LOCAL, wslDistro: 'Ubuntu' }, 'win32')).toBe('escape')
|
||||
})
|
||||
|
||||
it('keeps POSIX escaping for a remote target, whose platform we cannot see', () => {
|
||||
expect(commandBackslashMode(REMOTE, 'win32')).toBe('escape')
|
||||
})
|
||||
|
||||
it('defaults to this process platform when none is given', () => {
|
||||
const expected = process.platform === 'win32' ? 'literal' : 'escape'
|
||||
expect(commandBackslashMode(LOCAL)).toBe(expected)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user