diff --git a/config/flaky-test-pattern-baseline.txt b/config/flaky-test-pattern-baseline.txt index dff72ab0a01..b2e209b341e 100644 --- a/config/flaky-test-pattern-baseline.txt +++ b/config/flaky-test-pattern-baseline.txt @@ -211,7 +211,6 @@ 1 test-no-real-time-wait:src/main/browser/guest-lifecycle/popups/browser-popup-native-compatibility.electron.test.ts 1 test-no-real-time-wait:src/main/browser/identity/browser-session-ua-wire-identity-cross-context.electron.test.ts 1 test-no-real-time-wait:src/main/browser/identity/browser-session-ua-wire-identity.electron.test.ts -1 test-no-real-time-wait:src/main/browser/network/execution-routes/paired-runtime-browser-network-route.test.ts 1 test-no-real-time-wait:src/main/browser/network/tunnels/execution-route-socket-duplex.test.ts 1 test-no-real-time-wait:src/main/browser/network/tunnels/remote-browser-socks-server.test.ts 1 test-no-real-time-wait:src/main/claude-accounts/claude-login-session.test.ts @@ -663,7 +662,6 @@ 2 test-no-wall-clock:src/main/antigravity/posix-hook-invocation.test.ts 2 test-no-wall-clock:src/main/browser/cookies/browser-cookie-import-partition-reland.electron.test.ts 2 test-no-wall-clock:src/main/browser/network/execution-routes/wsl-browser-network-execution-route.test.ts -2 test-no-wall-clock:src/main/browser/sessions/browser-session-registry.test.ts 1 test-no-wall-clock:src/main/claude-accounts/claude-account-folder.test.ts 1 test-no-wall-clock:src/main/claude-usage/persisted-source-authority.test.ts 1 test-no-wall-clock:src/main/claude/claude-background-wake-retention.test.ts diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 1abf817c3b0..918251eb363 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -5426,7 +5426,7 @@ "providers": ["remote-runtime"], "coveredPlatforms": ["macos"], "coveredProviders": [], - "coverageNotes": "Audit1568 removes API inventories, supplied mock-value/forwarding assertions, implementation-shape tables and negative/setup cases that do not reach their intended boundary, without equivalent replacement claims. Retained changed definitions exercise owned manager lifetime/queue/refcount/clamp and real synthetic process/files/staging boundaries. Seventeen unchanged native Electron definitions across this combined audit are statically reviewed and not rerun. Changed native suites exercise local popup wire/cookie/session and clean/native process-identity behavior; known popup-admission flaws remain unfixed. Unit native/event/session/registry inputs remain injected. No actual OS focus, registry/host execution, CRX verification or durable restart proof. Receipts selecting changed tests are retired and unaffected receipts remain unchanged. Historical scope before this audit: Audit1564 retires copied cookie key recipes, mock bridge/preview forwarding and flag tables, source-spelling/script inventories and setup-only alarms without equivalent replacement coverage. Retained changed cases exercise actual scoped SQLite merge/rollback/schema refusal, owned cancellation and grant authority/canonical containment, ranged response consistency, replacement guest ownership and executed guest-script promise/DOM cleanup behavior. Session, host response, geometry/observer and transport producers remain injected where documented. One unchanged native extension definition is not rerun locally; no actual remote filesystem, native layout/focus or authentication claim. Receipts selecting modified tests are retired; unaffected exact-selector receipts remain unchanged. Historical scope before this audit: Audit1412 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): Audit1386 changes referenced sources; no new execution result validates reduced selectors. Retained unchanged receipts apply only to unchanged commands. Historical coverage: Audit1308 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): A macOS desktop host and a separate paired Electron client exercise the Explorer preview action end to end: the document renders inside the client-local orca-preview guest, the client gains exactly one browser workspace and it is the document one, the host's own page registry and tab snapshot gain nothing, the split does not take focus from the source editor, an explicit click activates the preview, and closing it with its X leaves the editor and terminal intact. Both host oracles are read in a run where an ordinary URL browser tab from the same client does arrive in them, because a host that cannot project browser pages at all would otherwise answer the absence question the same way. A second journey quits that client with a document tab open and relaunches it on the same profile, which is the restore the user's original report turned on: the same workspace row comes back blank and named by the document, renders again, and does so over a grant URL that differs from the one that was quit. The same journey pins the guest URL to the orca-preview scheme, so a regression back to file:// or to a host browser page is visible. The rewritten journey caught a live gap the old one could not see: since previews became browser pages they had no chrome to hand focus to their guest, so main refused every reported link click as coming from an unfocused guest and the one route out of a preview was dead. Main's own log answered it — registered true, grant live, focused false — after the embedder reported the anchor really taking six real presses. The pane now hands focus over when it is the reader's surface, and the journey routes. The preview's own components moved out of the editor folder into browser-pane/workspace-doc/ with this change, since a preview is a browser pane and no longer an editor one; their i18n keys keep the historical auto.components.editor.HtmlDocPreview prefix rather than dropping thirty translated strings across four locales for a tidier key path. Seven gate commands named those files by their old paths and were re-issued at the new ones, and the nine dated runs behind them were retired rather than left naming files that are no longer there, on the same principle this gate already applied twice: vitest treats a missing named file as a filter and exits 0, so such a command reports green while silently running only the half that still exists. Each retired record's finding is carried into the replacement run recorded below it, including the red half of the entry/subresource pair, which was re-measured by re-applying its mutant at the new path rather than being dropped. The fence over the page-management channels was decided channel by channel before it was built, and that decision record is: of the 23 channels that can name a page, 6 are allowed for a workspace-document page — the tool channels the reader drives, which reach it through the one authority that reads both halves of the registry — 9 refuse (download accept and cancel, devtools, certificate proceed, client page metadata publish, viewport override, WebAuthn account response, registration probe, registration repair), 3 cannot name a page at all because they take a profile or a worktree instead, and the rest are the registration and activation doors, refused on membership. What shipped is not the per-channel guard that classification implies: eleven call sites read the browsing map directly, so a guard per channel is exactly the caller-flag seam that has to be remembered at every future call site. The registry was split in two instead, which makes the classification structural — a document guest is absent from the browsing map, so every refusing channel refuses by absence rather than by a guard, and a channel added later inherits the refusal without being classified at all. Unit contracts cover grant scoping and path containment, per-owner read routing, refusal of a truncated or over-cap read, the old-server binary degradation, scheme admission on will-attach-webview, the guest navigation policy, the plan layer's local/SSH/paired/out-of-worktree routing, and the distinct sentence the reader sees for each failure reason. The paired browser-creation surfaces this gate still owns — capability rejection cleanup, canonical page identity, reconciliation rollback, close convergence — keep their existing headed and headless journeys. Linux and Windows live journeys remain gaps. A refused subresource is a notice on a rendered document rather than a failure panel, since only the entry document's failure leaves the reader with nothing; the entry document is served as text by every owner and cannot be refused for its format at all. Grants die with the renderer document that minted them, at window creation and on every reload, because the renderer holds the only record of which preview owns which grant. The mint and revoke channels answer only the trusted renderer, and a preview tab is never republished to mobile clients, which have no grant to read it with. External URLs leave the preview only via a trusted user click on an anchor while the preview is focused; script-driven navigation and popups reach nothing. The guest's navigation sinks are deny-only and route no URL at all, and the guest carries a preview-only preload, pinned by will-attach-webview and denied to every other attach path, whose capture-phase click handler reports an anchor's URL to main; main answers only a report from a preview guest with a live bound grant that is the focused contents, and only for http(s). Two earlier reader-facing records from 2026-08-26, which measured a shape where a subresource failure was filtered out and the panel still carried an unsupported-asset sentence, were retired with that shape rather than left contradicting the 10/10 runs of the command they share. Two more were retired on the same principle when the external-link design changed: a record showing that removing a recent-input gate let the unattended document open two browser tabs, and a record showing that dropping the webview's allowpopups attribute left a target=_blank press with nothing to route. Both measured machinery that no longer exists — the input gate could not tell a script's navigation from the click it followed, which is the case now recorded below, and the preload intercepts the press before Chromium considers a popup at all, so allowpopups was removed. WebRTC is fenced by the guest's IP-handling policy alone: this Chromium answers a webrtc content-security-policy directive as unrecognized and gathers candidates anyway, so the directive is deliberately absent from the served policy and a unit assertion keeps it out. A standalone Electron probe on the same version gathered 7 candidates including a STUN reflexive one without the policy and 0 with it. Two command paths this gate used to carry no longer exist, and their structured evidence was retired with them rather than left naming files that are no longer there: vitest treats a missing named file as a filter and exits 0, so such a command reports green while silently running only the half that still exists. The first is src/renderer/src/lib/file-preview-capability-cleanup.test.ts, deleted in STA-5557 when HTML previews stopped creating paired browser tabs: on 2026-08-12 its five-assertion STA-4063 oracle ran red 5/5 on scan e4c278eb52 and on main bc2e30000b, partially green 2/5 on pre-fix PR #14100, and green 5/5 on candidate 14b5877a75; the surviving half of that oracle is web-runtime-browser-capability-cleanup.test.ts, still commanded above. The second is tests/e2e/paired-remote-html-browser-focus.spec.ts, renamed in STA-5557 to paired-remote-html-preview-local-render.spec.ts because the journey no longer proves anything about browser focus: its 2026-08-12 runs established the owning-runtime routing and no-focus-steal behavior of the then-current browser-tab preview (red on main 346e59c879 and a90a18d43d, green on candidates d05217c40d and b7de9c0961, red again with only the side-preview focus or routing seam disabled), and its 2026-08-26 runs are recorded below under the current path. An adversarial pass after the journey landed found four coverage gaps rather than shipping defects, and all four are pinned here. The page url had a third door: the navigation commit wrote whatever url was reported, unfenced, and while nothing reaches it for a document page today it sat one report away from giving a document page a URL that persistence, the publish boundary and the address bar all read. The group projection was the load-bearing half of the mobile publish boundary that no test had: removing it alone left the whole suite green, because no test in it had tab groups, while the phone payload gained a group whose order and active tab named a document tab it was never sent. The focus handoff was covered only in the narrowing direction, so widening it — dropping the page-activity check, or either half of the reader-surface check — survived both suites. And the handoff itself was offered once and never again, so another app taking the front left the preview's one route out shut until the pane remounted, which is a desktop gap rather than a test-only one. The standalone capability-cleanup and browser-placement ledger suites were removed, including their mixed-version rejection, cleanup transfer, capacity and partition alarms. web-runtime-session.test.ts no longer supplies browser creation, rollback or recovery assertions. The grouped workspace-document exclusion remains; the separate no-group publication example was removed. Existing live/browser-store evidence does not establish equivalent replacement alarms. Earlier descriptions of the removed fixtures and their run results are historical. The client-owned-placement and layout-groups suites are removed. The broad mirror fixture no longer supplies preview placement/reservation/move assertions. Browser rows retain pre-response replay and real local-workspace reuse; focus intent retains response ordering and exact-leaf sibling ordering. Supplied browser-layout and placement/focus projection alarms are lost; retained evidence is narrower and not equivalent. The test-value audit retires renderer grant-release mock ledgers, direct creation/title/session-field copies, notification/onboarding calls and several placement/per-page projections. Canonical-ID rejection, document reopen/restore and preview URL fences remain; they do not prove close-to-registry revocation or replace every lost alarm. Historical execution evidence is unchanged. The test-value audit removed the direct legacy editor-preview hydration example from tabs-hydration.test.ts. Dropping legacy preview chrome while preserving the same-document ordinary editor no longer has that alarm. The surviving corrupt-id deduplication case does not cover that migration, and retained preview journeys are not equivalent replacement coverage. Historical run counts and migration mutation findings describe the earlier suite. Audit1308: src/renderer/src/components/editor/EditorPanelShell.header.test.tsx removed. Header visibility and mocked editor-surface inventory alarms disappear. No equivalent replacement coverage is claimed. Audit1312 removes low-value imported specs from aggregate test drivers; old whole-driver execution receipts are archived. Retained registrations are unchanged. No equivalent replacement coverage is claimed. Audit1383: src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts reduced. Lose command-list/receipt/default/background activation matrices whose provider spawn returns canned IDs. Retained test proves actual tab allocation separation, not terminal startup. No equivalent replacement coverage is claimed. Existing exact unaffected receipts and their platform/provider/budget claims remain scoped to unchanged selectors. Changed optional selectors have no new execution result. Audit1386: src/renderer/src/lib/doc-preview-grants.test.ts reduced. Repeated-mount mint count, ordinary release/remint and failed-mint cache retry standalone alarms removed; no actual grant server/access test. No equivalent replacement coverage is claimed. Audit1401: src/main/runtime/orca-runtime-tests/mobile-session-tabs-duplicate-pty-repair.spec.ts removed. Loses listing-on-already-repaired-data alarm; it never established runtime repair invocation. src/main/runtime/orca-runtime-tests/mobile-session-tabs-split-agent-tab.spec.ts removed. Loses actual mobile split sibling shell versus recorded-agent resume command alarms; no equivalent coverage asserted. src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts removed. Loses separate setup/startup tab result in that manually assembled path. src/main/runtime/orca-runtime.test.ts reduced. Driver registration/import of a whole-deleted selected spec is removed; directly authored cases remain unchanged. No extra authored removal credit. No equivalent replacement coverage is claimed. The existing primary-selector receipt and its claims remain scoped to their unchanged sources; the broad optional command has no new execution result. Audit1412: src/main/browser/guest-lifecycle/manager/browser-manager-guest-policy-profile.test.ts removed. Loses workspace-document in-grant navigation/popup refusal, cleanup/re-attach, cross-registry guest authorization and browsing-map separation alarms. No equivalent browser policy coverage is claimed. No equivalent replacement coverage is claimed. Audit1421 removes browser-address-bar-suggestions.test.ts. Document suggestion title/path matching, recency merging and path-prefix ranking alarms are lost. No equivalent replacement coverage is claimed. Historical receipts remain scoped to unchanged selectors; the reduced command has no new execution result.", + "coverageNotes": "Audit1571 removes mock forwarding, capability/schema inventories, private predicates, supplied error/result replays, and filesystem-mocked persistence checks without equivalent replacement claims. Retained changed tests also exercise real local permission-file reread, registry proxy rollback/credential retirement, site display recovery, and pending sender/credential/document admission with injected Electron/UI producers. Retained network tests exercise owned stream credit/cleanup, generation and retirement fences, real loopback proxy/SOCKS bytes and file-backed binding state. SSH/WSL/paired runtime, native sessions, authority producers and storage clears remain injected; simulated relaunch uses filesystem mocks. The unchanged profile-retirement suite was statically reviewed and not rerun. No actual SSH/WSL runtime, native Chromium jar deletion, durable process restart, remote DNS or external site proof. Receipts selecting changed tests are retired and unaffected receipts remain unchanged. Historical scope before this audit: Audit1568 removes API inventories, supplied mock-value/forwarding assertions, implementation-shape tables and negative/setup cases that do not reach their intended boundary, without equivalent replacement claims. Retained changed definitions exercise owned manager lifetime/queue/refcount/clamp and real synthetic process/files/staging boundaries. Seventeen unchanged native Electron definitions across this combined audit are statically reviewed and not rerun. Changed native suites exercise local popup wire/cookie/session and clean/native process-identity behavior; known popup-admission flaws remain unfixed. Unit native/event/session/registry inputs remain injected. No actual OS focus, registry/host execution, CRX verification or durable restart proof. Receipts selecting changed tests are retired and unaffected receipts remain unchanged. Historical scope before this audit: Audit1564 retires copied cookie key recipes, mock bridge/preview forwarding and flag tables, source-spelling/script inventories and setup-only alarms without equivalent replacement coverage. Retained changed cases exercise actual scoped SQLite merge/rollback/schema refusal, owned cancellation and grant authority/canonical containment, ranged response consistency, replacement guest ownership and executed guest-script promise/DOM cleanup behavior. Session, host response, geometry/observer and transport producers remain injected where documented. One unchanged native extension definition is not rerun locally; no actual remote filesystem, native layout/focus or authentication claim. Receipts selecting modified tests are retired; unaffected exact-selector receipts remain unchanged. Historical scope before this audit: Audit1412 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): Audit1386 changes referenced sources; no new execution result validates reduced selectors. Retained unchanged receipts apply only to unchanged commands. Historical coverage: Audit1308 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): A macOS desktop host and a separate paired Electron client exercise the Explorer preview action end to end: the document renders inside the client-local orca-preview guest, the client gains exactly one browser workspace and it is the document one, the host's own page registry and tab snapshot gain nothing, the split does not take focus from the source editor, an explicit click activates the preview, and closing it with its X leaves the editor and terminal intact. Both host oracles are read in a run where an ordinary URL browser tab from the same client does arrive in them, because a host that cannot project browser pages at all would otherwise answer the absence question the same way. A second journey quits that client with a document tab open and relaunches it on the same profile, which is the restore the user's original report turned on: the same workspace row comes back blank and named by the document, renders again, and does so over a grant URL that differs from the one that was quit. The same journey pins the guest URL to the orca-preview scheme, so a regression back to file:// or to a host browser page is visible. The rewritten journey caught a live gap the old one could not see: since previews became browser pages they had no chrome to hand focus to their guest, so main refused every reported link click as coming from an unfocused guest and the one route out of a preview was dead. Main's own log answered it — registered true, grant live, focused false — after the embedder reported the anchor really taking six real presses. The pane now hands focus over when it is the reader's surface, and the journey routes. The preview's own components moved out of the editor folder into browser-pane/workspace-doc/ with this change, since a preview is a browser pane and no longer an editor one; their i18n keys keep the historical auto.components.editor.HtmlDocPreview prefix rather than dropping thirty translated strings across four locales for a tidier key path. Seven gate commands named those files by their old paths and were re-issued at the new ones, and the nine dated runs behind them were retired rather than left naming files that are no longer there, on the same principle this gate already applied twice: vitest treats a missing named file as a filter and exits 0, so such a command reports green while silently running only the half that still exists. Each retired record's finding is carried into the replacement run recorded below it, including the red half of the entry/subresource pair, which was re-measured by re-applying its mutant at the new path rather than being dropped. The fence over the page-management channels was decided channel by channel before it was built, and that decision record is: of the 23 channels that can name a page, 6 are allowed for a workspace-document page — the tool channels the reader drives, which reach it through the one authority that reads both halves of the registry — 9 refuse (download accept and cancel, devtools, certificate proceed, client page metadata publish, viewport override, WebAuthn account response, registration probe, registration repair), 3 cannot name a page at all because they take a profile or a worktree instead, and the rest are the registration and activation doors, refused on membership. What shipped is not the per-channel guard that classification implies: eleven call sites read the browsing map directly, so a guard per channel is exactly the caller-flag seam that has to be remembered at every future call site. The registry was split in two instead, which makes the classification structural — a document guest is absent from the browsing map, so every refusing channel refuses by absence rather than by a guard, and a channel added later inherits the refusal without being classified at all. Unit contracts cover grant scoping and path containment, per-owner read routing, refusal of a truncated or over-cap read, the old-server binary degradation, scheme admission on will-attach-webview, the guest navigation policy, the plan layer's local/SSH/paired/out-of-worktree routing, and the distinct sentence the reader sees for each failure reason. The paired browser-creation surfaces this gate still owns — capability rejection cleanup, canonical page identity, reconciliation rollback, close convergence — keep their existing headed and headless journeys. Linux and Windows live journeys remain gaps. A refused subresource is a notice on a rendered document rather than a failure panel, since only the entry document's failure leaves the reader with nothing; the entry document is served as text by every owner and cannot be refused for its format at all. Grants die with the renderer document that minted them, at window creation and on every reload, because the renderer holds the only record of which preview owns which grant. The mint and revoke channels answer only the trusted renderer, and a preview tab is never republished to mobile clients, which have no grant to read it with. External URLs leave the preview only via a trusted user click on an anchor while the preview is focused; script-driven navigation and popups reach nothing. The guest's navigation sinks are deny-only and route no URL at all, and the guest carries a preview-only preload, pinned by will-attach-webview and denied to every other attach path, whose capture-phase click handler reports an anchor's URL to main; main answers only a report from a preview guest with a live bound grant that is the focused contents, and only for http(s). Two earlier reader-facing records from 2026-08-26, which measured a shape where a subresource failure was filtered out and the panel still carried an unsupported-asset sentence, were retired with that shape rather than left contradicting the 10/10 runs of the command they share. Two more were retired on the same principle when the external-link design changed: a record showing that removing a recent-input gate let the unattended document open two browser tabs, and a record showing that dropping the webview's allowpopups attribute left a target=_blank press with nothing to route. Both measured machinery that no longer exists — the input gate could not tell a script's navigation from the click it followed, which is the case now recorded below, and the preload intercepts the press before Chromium considers a popup at all, so allowpopups was removed. WebRTC is fenced by the guest's IP-handling policy alone: this Chromium answers a webrtc content-security-policy directive as unrecognized and gathers candidates anyway, so the directive is deliberately absent from the served policy and a unit assertion keeps it out. A standalone Electron probe on the same version gathered 7 candidates including a STUN reflexive one without the policy and 0 with it. Two command paths this gate used to carry no longer exist, and their structured evidence was retired with them rather than left naming files that are no longer there: vitest treats a missing named file as a filter and exits 0, so such a command reports green while silently running only the half that still exists. The first is src/renderer/src/lib/file-preview-capability-cleanup.test.ts, deleted in STA-5557 when HTML previews stopped creating paired browser tabs: on 2026-08-12 its five-assertion STA-4063 oracle ran red 5/5 on scan e4c278eb52 and on main bc2e30000b, partially green 2/5 on pre-fix PR #14100, and green 5/5 on candidate 14b5877a75; the surviving half of that oracle is web-runtime-browser-capability-cleanup.test.ts, still commanded above. The second is tests/e2e/paired-remote-html-browser-focus.spec.ts, renamed in STA-5557 to paired-remote-html-preview-local-render.spec.ts because the journey no longer proves anything about browser focus: its 2026-08-12 runs established the owning-runtime routing and no-focus-steal behavior of the then-current browser-tab preview (red on main 346e59c879 and a90a18d43d, green on candidates d05217c40d and b7de9c0961, red again with only the side-preview focus or routing seam disabled), and its 2026-08-26 runs are recorded below under the current path. An adversarial pass after the journey landed found four coverage gaps rather than shipping defects, and all four are pinned here. The page url had a third door: the navigation commit wrote whatever url was reported, unfenced, and while nothing reaches it for a document page today it sat one report away from giving a document page a URL that persistence, the publish boundary and the address bar all read. The group projection was the load-bearing half of the mobile publish boundary that no test had: removing it alone left the whole suite green, because no test in it had tab groups, while the phone payload gained a group whose order and active tab named a document tab it was never sent. The focus handoff was covered only in the narrowing direction, so widening it — dropping the page-activity check, or either half of the reader-surface check — survived both suites. And the handoff itself was offered once and never again, so another app taking the front left the preview's one route out shut until the pane remounted, which is a desktop gap rather than a test-only one. The standalone capability-cleanup and browser-placement ledger suites were removed, including their mixed-version rejection, cleanup transfer, capacity and partition alarms. web-runtime-session.test.ts no longer supplies browser creation, rollback or recovery assertions. The grouped workspace-document exclusion remains; the separate no-group publication example was removed. Existing live/browser-store evidence does not establish equivalent replacement alarms. Earlier descriptions of the removed fixtures and their run results are historical. The client-owned-placement and layout-groups suites are removed. The broad mirror fixture no longer supplies preview placement/reservation/move assertions. Browser rows retain pre-response replay and real local-workspace reuse; focus intent retains response ordering and exact-leaf sibling ordering. Supplied browser-layout and placement/focus projection alarms are lost; retained evidence is narrower and not equivalent. The test-value audit retires renderer grant-release mock ledgers, direct creation/title/session-field copies, notification/onboarding calls and several placement/per-page projections. Canonical-ID rejection, document reopen/restore and preview URL fences remain; they do not prove close-to-registry revocation or replace every lost alarm. Historical execution evidence is unchanged. The test-value audit removed the direct legacy editor-preview hydration example from tabs-hydration.test.ts. Dropping legacy preview chrome while preserving the same-document ordinary editor no longer has that alarm. The surviving corrupt-id deduplication case does not cover that migration, and retained preview journeys are not equivalent replacement coverage. Historical run counts and migration mutation findings describe the earlier suite. Audit1308: src/renderer/src/components/editor/EditorPanelShell.header.test.tsx removed. Header visibility and mocked editor-surface inventory alarms disappear. No equivalent replacement coverage is claimed. Audit1312 removes low-value imported specs from aggregate test drivers; old whole-driver execution receipts are archived. Retained registrations are unchanged. No equivalent replacement coverage is claimed. Audit1383: src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts reduced. Lose command-list/receipt/default/background activation matrices whose provider spawn returns canned IDs. Retained test proves actual tab allocation separation, not terminal startup. No equivalent replacement coverage is claimed. Existing exact unaffected receipts and their platform/provider/budget claims remain scoped to unchanged selectors. Changed optional selectors have no new execution result. Audit1386: src/renderer/src/lib/doc-preview-grants.test.ts reduced. Repeated-mount mint count, ordinary release/remint and failed-mint cache retry standalone alarms removed; no actual grant server/access test. No equivalent replacement coverage is claimed. Audit1401: src/main/runtime/orca-runtime-tests/mobile-session-tabs-duplicate-pty-repair.spec.ts removed. Loses listing-on-already-repaired-data alarm; it never established runtime repair invocation. src/main/runtime/orca-runtime-tests/mobile-session-tabs-split-agent-tab.spec.ts removed. Loses actual mobile split sibling shell versus recorded-agent resume command alarms; no equivalent coverage asserted. src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts removed. Loses separate setup/startup tab result in that manually assembled path. src/main/runtime/orca-runtime.test.ts reduced. Driver registration/import of a whole-deleted selected spec is removed; directly authored cases remain unchanged. No extra authored removal credit. No equivalent replacement coverage is claimed. The existing primary-selector receipt and its claims remain scoped to their unchanged sources; the broad optional command has no new execution result. Audit1412: src/main/browser/guest-lifecycle/manager/browser-manager-guest-policy-profile.test.ts removed. Loses workspace-document in-grant navigation/popup refusal, cleanup/re-attach, cross-registry guest authorization and browsing-map separation alarms. No equivalent browser policy coverage is claimed. No equivalent replacement coverage is claimed. Audit1421 removes browser-address-bar-suggestions.test.ts. Document suggestion title/path matching, recency merging and path-prefix ranking alarms are lost. No equivalent replacement coverage is claimed. Historical receipts remain scoped to unchanged selectors; the reduced command has no new execution result.", "motivatingLinks": [ "https://linear.app/stably/issue/STA-5557/remote-html-preview-render-workspace-files-locally-over-the-file", "https://linear.app/stably/issue/STA-3872/unable-to-open-html-files-in-remote-server", @@ -5991,6 +5991,7 @@ "Collect 100 consecutive focused CI passes or 14 days without an unexplained flake." ], "knownGaps": [ + "Audit1571 removes mock forwarding, capability/schema inventories, private predicates, supplied error/result replays, and filesystem-mocked persistence checks without equivalent replacement claims. Retained changed tests also exercise real local permission-file reread, registry proxy rollback/credential retirement, site display recovery, and pending sender/credential/document admission with injected Electron/UI producers. Retained network tests exercise owned stream credit/cleanup, generation and retirement fences, real loopback proxy/SOCKS bytes and file-backed binding state. SSH/WSL/paired runtime, native sessions, authority producers and storage clears remain injected; simulated relaunch uses filesystem mocks. The unchanged profile-retirement suite was statically reviewed and not rerun. No actual SSH/WSL runtime, native Chromium jar deletion, durable process restart, remote DNS or external site proof. Receipts selecting changed tests are retired and unaffected receipts remain unchanged.", "Audit1568 removes API inventories, supplied mock-value/forwarding assertions, implementation-shape tables and negative/setup cases that do not reach their intended boundary, without equivalent replacement claims. Retained changed definitions exercise owned manager lifetime/queue/refcount/clamp and real synthetic process/files/staging boundaries. Seventeen unchanged native Electron definitions across this combined audit are statically reviewed and not rerun. Changed native suites exercise local popup wire/cookie/session and clean/native process-identity behavior; known popup-admission flaws remain unfixed. Unit native/event/session/registry inputs remain injected. No actual OS focus, registry/host execution, CRX verification or durable restart proof. Receipts selecting changed tests are retired and unaffected receipts remain unchanged.", "Audit1564 retires copied cookie key recipes, mock bridge/preview forwarding and flag tables, source-spelling/script inventories and setup-only alarms without equivalent replacement coverage. Retained changed cases exercise actual scoped SQLite merge/rollback/schema refusal, owned cancellation and grant authority/canonical containment, ranged response consistency, replacement guest ownership and executed guest-script promise/DOM cleanup behavior. Session, host response, geometry/observer and transport producers remain injected where documented. One unchanged native extension definition is not rerun locally; no actual remote filesystem, native layout/focus or authentication claim. Receipts selecting modified tests are retired; unaffected exact-selector receipts remain unchanged.", "The standalone download-notice throttle suite was removed; its interval, per-grant, revocation-reset and unbound-guest alarms are intentionally lost. Remaining notice suites are not claimed as exact substitutes.", @@ -6052,7 +6053,7 @@ "providers": ["local", "remote-runtime", "ssh", "wsl"], "coveredPlatforms": ["macos", "linux"], "coveredProviders": [], - "coverageNotes": "Audit1563 retires mock cookie inventories, summary/flag/counter checks, historical replacement/replay narratives and setup-only alarms without equivalent replacement coverage. Retained changed cases exercise owned admission and cleanup, real staged-file lifetime, JSON/SQLite/WAL/schema compatibility, synthetic encryption and native Electron SameSite storage. Session/CDP/credential/write/renderer producers remain injected where recorded. Seven unchanged native definitions are not rerun locally; known stale registry-stub fixtures are not executable proof. Receipts selecting modified tests are retired; unaffected exact-selector receipts remain unchanged. Historical scope before this audit: Audit1559 removes mocked host/lease/reconciliation, event/flag/counter, forwarding, inventory and retry-budget alarms without equivalent replacement coverage. Retained cases establish owned promise, registry, rollback, admission and cleanup boundaries with injected Electron/renderer/host inputs; actual local sockets, file staging and Buffer collection are scoped in the PR. Receipts selecting modified tests are retired; unaffected exact-selector receipts remain historical evidence only. No live paired runtime, rendered browser/profile/TLS or device claim. Historical scope before this audit: Audit1401 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): Audit1340 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): Deterministic protocol, registry, injected-socket, and loopback-listener tests cover strict framing, remote-DNS targets, exact destination-write and source-consumption credit, at most 256 streams per route with pending opens counted inside that cap, 1024 admitted opens per 10-second monotonic window, an 8 MiB per-route application-buffer ledger, shared 32 MiB browser-host and 128 MiB process ledgers across application copies, encrypted client queues, and native WebSocket bufferedAmount, bounded byte/claim/socket-source counts, a four-frame queued-drain quantum, authority epochs, exact host selection, one host per authenticated connection, four hosts per paired device, eight global browser-host polls with four per authenticated paired device, a shared ask/host ceiling that retains one quarter for waits, bounded initial and reconnect runtime_busy recovery, long-poll metering and disconnect abort, monotonic host/page/route generations without page tombstones, two-phase exact page retirement with cancellation, connection-owned cleanup, exact client revocation, stale and replaced fences, retired stream IDs, half-close and close ordering, SOCKS CONNECT, bind/close races, listener wildcard normalization, unsupported commands, unavailable routes, and raw/terminal binary-handler isolation. Page commands use a separately echoed v1 attach negotiation, exact authority/host/page generations, bounded command IDs and sequences, and bounded create/navigate payloads; legacy attaches still receive only the unchanged ready/revoked event shapes. A second optional reconciliation subprotocol gates bounded reclaim, close, and restore payloads behind exact attach/ready echo, complete inventory, command negotiation, reconnect authority, and command-result authority; the production client advertises it only with the matching command and inventory capabilities. Exact guest or app-renderer loss marks one page generation outcome-unknown, coalesces a bounded negotiated inventory reattach, closes or retires the dead generation, and allocates a fresh generation before URL restore; explicit close is not misclassified as a crash. Mixed-version mutation tests project hidden client pages before activate, close, split, reorder, and move-to-group admission, preserve hidden raw order slots, translate visible insertion indices, and project mutation snapshots. A production server orchestrator consumes each immutable inventory once, reserves target generations without exposing placement, emits only negotiated ledger commands, commits after exact completed proof, preserves unrelated and server placements, aborts an attempt when connection authority enters reconnect grace, and requires fresh inventory after failure or abort. The production client dispatcher additionally proves per-page FIFO execution, exact payload-matched duplicate replay, frozen command/result snapshots, a global retired-generation floor, transactional admission, bounded pages/active commands/queues/per-page and global result cache/concurrency, create dependency failure, cancellation, deduplicated retirement joining, and bounded close without late-result overwrite. The server ledger owns issue order and immutable command/result snapshots, bounds outstanding commands, active pages, and per-page/global replay caches, releases active-page capacity after an exact completed close while retaining bounded result replay, validates the shared wire payload before admission, requires live delivery and exact placement, authenticates results to the negotiated connection and paired lease, rejects gaps and conflicting replay, and fences outstanding outcomes at exact retirement. Negotiated command results reuse the authenticated attach socket through bounded nested JSON requests; exact ID routing, reverse-order replies, unknown and duplicate IDs, timeout teardown, serialization failure, aggregate queue accounting, acknowledgement validation, and the unchanged non-v1 path are deterministic. A stable local listener rejects CONNECT while offline or reconnecting, retains its address across replacement, requires a strictly increasing tunnel generation, ignores late superseded callbacks, propagates tunnel protocol failure to the route owner, and recycles exhausted stream IDs only after generation replacement. Reconnect uses the unchanged native v1 attach payload and capability pair; SSH descriptors alone add an execution-host capability and require a runtime-minted grant bound to the exact browser-host lease. Exact SSH provider epoch and connection generation fence ssh2 forwardOut and one non-interactive standalone system-SSH dynamic forward per route. Unit tests preserve domain-form SOCKS requests, sanitize remote errors, bound stderr, cancel startup, release timed-out and synchronously failed sockets, and release routes once. An ephemeral Docker sshd resolves a container-only domain and returns a unique HTTP marker through both ssh2 and the actual system-OpenSSH dynamic-forward adapter without touching the user's SSH files; authority loss fences the ssh2 route. The execution runtime charges route application bytes to the same per-host/process policy; its existing E2EE owner separately caps native outbound buffers process-wide. Production-registered browser-host and paired-runtime methods lease one exact host, prove attach, command delivery, and result settlement share one exact connection identity, then carry SOCKS and HTTP bytes over a dedicated E2EE socket to the fenced execution-host revision and prove route close destroys the destination socket. The production desktop adapter now composes one exact host per environment pairing revision with the page executor, current renderer selector, route Session/WebContents registries, and one reference-counted route per canonical execution-host key. Negotiated same-client control reconnect retains exact authority, placements, grants, dispatcher dedupe, executor guests, and listener addresses; it fences tunnels immediately, blocks route admission, reattaches command delivery only after ready, and replays unsettled commands without repeating completed mutations. Terminal release, replacement, legacy disconnect, and reconnect-grace expiry make only the exact host generation's client placements non-cancellable retirement-pending while retaining capacity until exact cleanup; reconnect grace preserves them. Environment replacement and app shutdown still serialize transport closure before page cleanup and force-close every remaining route. The production placement preparation starts the exact desktop adapter and advertises host/tunnel capabilities only when the paired Electron client is eligible. Node stream-internal high-water bytes, strict cross-route scheduling, and physical cross-platform evidence remain uncovered. The two Docker remote-only SSH browser routing journeys now run in the dedicated Linux ssh-browser-network-route CI job on full runs and their mapped source/test changes; 2 baseline and 6 repeated cases passed with no skips/retries on 2026-09-06. The removed retry-arithmetic and attach-request inventory suites are not current coverage; retained lease and composition suites exercise admission retry, cleanup, inventory negotiation, and reconnect behavior. Test-value audit1293 removes src/main/browser/network/tunnels/browser-network-tunnel-client-memory-budget.test.ts (4 authored cases). Their isolated assertions no longer run; surviving tests are not claimed as equivalent coverage. Fresh execution against the candidate SHA is required. Audit1302: src/main/browser/client-pages/lifecycle/browser-client-page-unavailability.test.ts: Every route/guest lifecycle/currentness/navigation/renderer/automation authority is supplied by fake callbacks, then unavailability listener is invoked manually and private inventory/mock calls are checked. No guest destruction, renderer disconnect or actual reconciliation/close consumer. Lose generation fence/reconcile-once/missing-renderer-close branch alarms; no equivalent replacement. Reduced selectors require fresh validation; prior full-suite receipts do not validate removed or changed coverage. No equivalent replacement or physical-provider claim. Audit1318: src/shared/remote-runtime/requests/remote-runtime-subscription-request.test.ts: Retain actual local encrypted WebSocket subscription concurrency routing when host replies out of order, plus an exception thrown by a real ws consumer callback being turned into a subscription error. Server is a test-shaped handshake/response producer using production crypto, not actual runtime host/auth authority or independent cryptographic oracle. Remove happy second-request duplicate, unknown/duplicate-ID branches, fixed32 capacity, serialization/capacity/timeouts and fake memory release/admission call-count scenarios; intentionally lose those alarms. Reduced selectors need fresh validation; historical full-suite receipts do not validate the changed coverage. No equivalent replacement or physical-provider claim. Audit1340: src/main/runtime/runtime-binary-message-router.test.ts removed. Connection mode conflict exceptions and one terminal/raw callback each. No equivalent replacement coverage is claimed. Audit1352: focused Linux unit selections validated on source head b638e07d395b76af3ba6b03761a1a0fe7e5e026e. Historical statements above retain their original scope. Intentional test losses are recorded in knownGaps; these runs claim no physical-provider, p95, rendered E2E or cross-platform validation. Audit1360: focused Linux unit selections validated on source head 415c9cffc2d10024131ec4b6721767a22195de6d. Historical statements above retain their original scope. Intentional test losses are recorded in knownGaps; these runs claim no physical-provider, p95, rendered E2E or cross-platform validation. Audit1371 removes the reconciliation protocol/capability echo inventory from browser-client-host-reconciliation.test.ts. The pending command result refusal without exact authority and later accepted acknowledgement callback remains byte-identical. The narrow negotiation echo alarm is intentionally lost; no equivalent browser/authenticated transport proof is claimed. Existing receipts with unchanged selectors retain only their original scope. Audit1401: src/main/browser/client-pages/lifecycle/browser-client-page-inventory.test.ts reduced. Loses deterministic equal-length URL compaction tie-breaking and reversed-input invariance alarm. No equivalent replacement coverage is claimed.", + "coverageNotes": "Audit1571 removes mock forwarding, capability/schema inventories, private predicates, supplied error/result replays, and filesystem-mocked persistence checks without equivalent replacement claims. Retained changed tests also exercise real local permission-file reread, registry proxy rollback/credential retirement, site display recovery, and pending sender/credential/document admission with injected Electron/UI producers. Retained network tests exercise owned stream credit/cleanup, generation and retirement fences, real loopback proxy/SOCKS bytes and file-backed binding state. SSH/WSL/paired runtime, native sessions, authority producers and storage clears remain injected; simulated relaunch uses filesystem mocks. The unchanged profile-retirement suite was statically reviewed and not rerun. No actual SSH/WSL runtime, native Chromium jar deletion, durable process restart, remote DNS or external site proof. Receipts selecting changed tests are retired and unaffected receipts remain unchanged. Historical scope before this audit: Audit1563 retires mock cookie inventories, summary/flag/counter checks, historical replacement/replay narratives and setup-only alarms without equivalent replacement coverage. Retained changed cases exercise owned admission and cleanup, real staged-file lifetime, JSON/SQLite/WAL/schema compatibility, synthetic encryption and native Electron SameSite storage. Session/CDP/credential/write/renderer producers remain injected where recorded. Seven unchanged native definitions are not rerun locally; known stale registry-stub fixtures are not executable proof. Receipts selecting modified tests are retired; unaffected exact-selector receipts remain unchanged. Historical scope before this audit: Audit1559 removes mocked host/lease/reconciliation, event/flag/counter, forwarding, inventory and retry-budget alarms without equivalent replacement coverage. Retained cases establish owned promise, registry, rollback, admission and cleanup boundaries with injected Electron/renderer/host inputs; actual local sockets, file staging and Buffer collection are scoped in the PR. Receipts selecting modified tests are retired; unaffected exact-selector receipts remain historical evidence only. No live paired runtime, rendered browser/profile/TLS or device claim. Historical scope before this audit: Audit1401 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): Audit1340 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): Deterministic protocol, registry, injected-socket, and loopback-listener tests cover strict framing, remote-DNS targets, exact destination-write and source-consumption credit, at most 256 streams per route with pending opens counted inside that cap, 1024 admitted opens per 10-second monotonic window, an 8 MiB per-route application-buffer ledger, shared 32 MiB browser-host and 128 MiB process ledgers across application copies, encrypted client queues, and native WebSocket bufferedAmount, bounded byte/claim/socket-source counts, a four-frame queued-drain quantum, authority epochs, exact host selection, one host per authenticated connection, four hosts per paired device, eight global browser-host polls with four per authenticated paired device, a shared ask/host ceiling that retains one quarter for waits, bounded initial and reconnect runtime_busy recovery, long-poll metering and disconnect abort, monotonic host/page/route generations without page tombstones, two-phase exact page retirement with cancellation, connection-owned cleanup, exact client revocation, stale and replaced fences, retired stream IDs, half-close and close ordering, SOCKS CONNECT, bind/close races, listener wildcard normalization, unsupported commands, unavailable routes, and raw/terminal binary-handler isolation. Page commands use a separately echoed v1 attach negotiation, exact authority/host/page generations, bounded command IDs and sequences, and bounded create/navigate payloads; legacy attaches still receive only the unchanged ready/revoked event shapes. A second optional reconciliation subprotocol gates bounded reclaim, close, and restore payloads behind exact attach/ready echo, complete inventory, command negotiation, reconnect authority, and command-result authority; the production client advertises it only with the matching command and inventory capabilities. Exact guest or app-renderer loss marks one page generation outcome-unknown, coalesces a bounded negotiated inventory reattach, closes or retires the dead generation, and allocates a fresh generation before URL restore; explicit close is not misclassified as a crash. Mixed-version mutation tests project hidden client pages before activate, close, split, reorder, and move-to-group admission, preserve hidden raw order slots, translate visible insertion indices, and project mutation snapshots. A production server orchestrator consumes each immutable inventory once, reserves target generations without exposing placement, emits only negotiated ledger commands, commits after exact completed proof, preserves unrelated and server placements, aborts an attempt when connection authority enters reconnect grace, and requires fresh inventory after failure or abort. The production client dispatcher additionally proves per-page FIFO execution, exact payload-matched duplicate replay, frozen command/result snapshots, a global retired-generation floor, transactional admission, bounded pages/active commands/queues/per-page and global result cache/concurrency, create dependency failure, cancellation, deduplicated retirement joining, and bounded close without late-result overwrite. The server ledger owns issue order and immutable command/result snapshots, bounds outstanding commands, active pages, and per-page/global replay caches, releases active-page capacity after an exact completed close while retaining bounded result replay, validates the shared wire payload before admission, requires live delivery and exact placement, authenticates results to the negotiated connection and paired lease, rejects gaps and conflicting replay, and fences outstanding outcomes at exact retirement. Negotiated command results reuse the authenticated attach socket through bounded nested JSON requests; exact ID routing, reverse-order replies, unknown and duplicate IDs, timeout teardown, serialization failure, aggregate queue accounting, acknowledgement validation, and the unchanged non-v1 path are deterministic. A stable local listener rejects CONNECT while offline or reconnecting, retains its address across replacement, requires a strictly increasing tunnel generation, ignores late superseded callbacks, propagates tunnel protocol failure to the route owner, and recycles exhausted stream IDs only after generation replacement. Reconnect uses the unchanged native v1 attach payload and capability pair; SSH descriptors alone add an execution-host capability and require a runtime-minted grant bound to the exact browser-host lease. Exact SSH provider epoch and connection generation fence ssh2 forwardOut and one non-interactive standalone system-SSH dynamic forward per route. Unit tests preserve domain-form SOCKS requests, sanitize remote errors, bound stderr, cancel startup, release timed-out and synchronously failed sockets, and release routes once. An ephemeral Docker sshd resolves a container-only domain and returns a unique HTTP marker through both ssh2 and the actual system-OpenSSH dynamic-forward adapter without touching the user's SSH files; authority loss fences the ssh2 route. The execution runtime charges route application bytes to the same per-host/process policy; its existing E2EE owner separately caps native outbound buffers process-wide. Production-registered browser-host and paired-runtime methods lease one exact host, prove attach, command delivery, and result settlement share one exact connection identity, then carry SOCKS and HTTP bytes over a dedicated E2EE socket to the fenced execution-host revision and prove route close destroys the destination socket. The production desktop adapter now composes one exact host per environment pairing revision with the page executor, current renderer selector, route Session/WebContents registries, and one reference-counted route per canonical execution-host key. Negotiated same-client control reconnect retains exact authority, placements, grants, dispatcher dedupe, executor guests, and listener addresses; it fences tunnels immediately, blocks route admission, reattaches command delivery only after ready, and replays unsettled commands without repeating completed mutations. Terminal release, replacement, legacy disconnect, and reconnect-grace expiry make only the exact host generation's client placements non-cancellable retirement-pending while retaining capacity until exact cleanup; reconnect grace preserves them. Environment replacement and app shutdown still serialize transport closure before page cleanup and force-close every remaining route. The production placement preparation starts the exact desktop adapter and advertises host/tunnel capabilities only when the paired Electron client is eligible. Node stream-internal high-water bytes, strict cross-route scheduling, and physical cross-platform evidence remain uncovered. The two Docker remote-only SSH browser routing journeys now run in the dedicated Linux ssh-browser-network-route CI job on full runs and their mapped source/test changes; 2 baseline and 6 repeated cases passed with no skips/retries on 2026-09-06. The removed retry-arithmetic and attach-request inventory suites are not current coverage; retained lease and composition suites exercise admission retry, cleanup, inventory negotiation, and reconnect behavior. Test-value audit1293 removes src/main/browser/network/tunnels/browser-network-tunnel-client-memory-budget.test.ts (4 authored cases). Their isolated assertions no longer run; surviving tests are not claimed as equivalent coverage. Fresh execution against the candidate SHA is required. Audit1302: src/main/browser/client-pages/lifecycle/browser-client-page-unavailability.test.ts: Every route/guest lifecycle/currentness/navigation/renderer/automation authority is supplied by fake callbacks, then unavailability listener is invoked manually and private inventory/mock calls are checked. No guest destruction, renderer disconnect or actual reconciliation/close consumer. Lose generation fence/reconcile-once/missing-renderer-close branch alarms; no equivalent replacement. Reduced selectors require fresh validation; prior full-suite receipts do not validate removed or changed coverage. No equivalent replacement or physical-provider claim. Audit1318: src/shared/remote-runtime/requests/remote-runtime-subscription-request.test.ts: Retain actual local encrypted WebSocket subscription concurrency routing when host replies out of order, plus an exception thrown by a real ws consumer callback being turned into a subscription error. Server is a test-shaped handshake/response producer using production crypto, not actual runtime host/auth authority or independent cryptographic oracle. Remove happy second-request duplicate, unknown/duplicate-ID branches, fixed32 capacity, serialization/capacity/timeouts and fake memory release/admission call-count scenarios; intentionally lose those alarms. Reduced selectors need fresh validation; historical full-suite receipts do not validate the changed coverage. No equivalent replacement or physical-provider claim. Audit1340: src/main/runtime/runtime-binary-message-router.test.ts removed. Connection mode conflict exceptions and one terminal/raw callback each. No equivalent replacement coverage is claimed. Audit1352: focused Linux unit selections validated on source head b638e07d395b76af3ba6b03761a1a0fe7e5e026e. Historical statements above retain their original scope. Intentional test losses are recorded in knownGaps; these runs claim no physical-provider, p95, rendered E2E or cross-platform validation. Audit1360: focused Linux unit selections validated on source head 415c9cffc2d10024131ec4b6721767a22195de6d. Historical statements above retain their original scope. Intentional test losses are recorded in knownGaps; these runs claim no physical-provider, p95, rendered E2E or cross-platform validation. Audit1371 removes the reconciliation protocol/capability echo inventory from browser-client-host-reconciliation.test.ts. The pending command result refusal without exact authority and later accepted acknowledgement callback remains byte-identical. The narrow negotiation echo alarm is intentionally lost; no equivalent browser/authenticated transport proof is claimed. Existing receipts with unchanged selectors retain only their original scope. Audit1401: src/main/browser/client-pages/lifecycle/browser-client-page-inventory.test.ts reduced. Loses deterministic equal-length URL compaction tie-breaking and reversed-input invariance alarm. No equivalent replacement coverage is claimed.", "motivatingLinks": [ "https://linear.app/stably/issue/STA-4150/refactor-remote-browser-to-client-hosted-electron-webviews" ], @@ -6291,16 +6292,16 @@ { "file": "src/main/browser/network/client-routes/browser-client-network-route-registry.test.ts", "assertions": [ - "canonical execution-host keys share one route until the final page release", - "native routes must name the exact authority runtime", - "aborted startup releases without admitting a route handle", - "suspension blocks new retention and reconnect preserves the exact listener address", - "one transient route failure retries without closing healthy routes", - "a superseded route recovery retains no retry timer", - "persistent route failure exhausts bounded grace without leaking retry timers", - "authority retirement blocks admission and reconnect while retaining suspended SSH routes until exact page release", - "final shutdown force-closes retained retired routes", - "registry shutdown closes every route and rejects later admission" + "retains one exact route until its final page releases it", + "keeps an ambiguously closed route fenced until final registry cleanup", + "retires old authority routes without destroying them before exact page cleanup", + "aborts a stale route recovery without retaining its retry timer", + "fails bounded recovery after one route exhausts grace without leaking timers", + "mints a fresh route for a retain that lands while the previous one is closing", + "waits for a route still closing before reporting retirement", + "rejects a retain the moment its signal aborts mid-startup", + "refuses a retain that waited out a closing route onto a retired registry", + "reports a failed final cleanup to an awaiting retirement" ] }, { @@ -6362,12 +6363,12 @@ { "file": "src/main/browser/network/tunnels/browser-network-tunnel-session.test.ts", "assertions": [ - "destination writes replenish client credit only after settlement", - "stale generations and reused stream IDs cannot replace the current destination", - "late frames for retired or reserved stream IDs leave concurrent destinations alive while never-reserved IDs fail closed", - "admitted open rate remains bounded and recovers only after window expiry", - "both traffic directions share an 8 MiB application-retention ledger with exact settlement, retirement, stale-callback, and reentrant-close release", - "unsettled client-to-destination writes retain at most 256 chunks per stream independently of byte credit" + "replenishes client credit only after the destination write settles", + "keeps concurrent streams alive when late data arrives for a retired stream", + "flushes credited destination bytes before close", + "caps aggregate retained destination bytes and releases exact stream accounting", + "caps unsettled destination writes and releases retirement exactly once", + "releases queued bytes once when an accepted send closes reentrantly" ] }, { @@ -6380,13 +6381,13 @@ { "file": "src/main/browser/network/tunnels/browser-network-tunnel-client.test.ts", "assertions": [ - "source writes never exceed granted credit or the bounded data-frame size", - "destination credit tracks only browser-facing socket writes that settle after readable consumption", - "pending tiny frames stay chunk-bounded and data drains before remote close retirement", - "late local settlement after remote close sends no stale credit and leaves the tunnel usable", - "stale generations cannot open a stream and route close destroys every source", - "late frames for retired stream IDs leave concurrent sources alive while never-allocated IDs fail closed", - "stream IDs never repeat within one tunnel generation after the bounded ledger exhausts" + "preserves remote DNS and waits for an opened acknowledgement", + "sends only credited bytes in bounded frames and replenishes consumed input", + "ignores a retired stream frame but rejects a never-allocated stream ID", + "never reuses stream IDs within one generation after its bounded ledger exhausts", + "replenishes only bytes settled after the readable consumer takes them", + "fails closed when pending tiny destination frames exceed the chunk cap", + "drains data that precedes a remote close before retiring the socket" ] }, { @@ -6425,18 +6426,16 @@ { "file": "src/main/browser/network/execution-routes/paired-runtime-browser-network-route.test.ts", "assertions": [ - "route teardown closes a subscription that resolves after startup cancellation", - "close after subscription acquisition settles the pending readiness wait", - "post-auth readiness has a deadline and runtime close tears down the local route", - "listener teardown failures remain visible to the route owner", - "the route binds and releases its exact host/process memory lease and four-frame drain quantum", - "one listener stays fail-closed and address-stable through reconnect", - "replacement generations advance strictly and remain fenced after ready-close races", - "reconnect retains the exact existing native v1 attach payload and capability pair", - "only an SSH descriptor adds the execution-host capability", - "late superseded callbacks cannot replace or close the current transport", - "a current-generation protocol failure retires the transport and remains reconnectable", - "stream-ID exhaustion replaces the generation before recycling an ID" + "closes a subscription that resolves after route teardown", + "settles startup when close follows subscription acquisition before readiness", + "bounds the post-auth readiness wait", + "binds and releases the exact browser-host outbound memory lease", + "keeps one listener fail-closed and accepts an increasing replacement generation", + "ignores late callbacks from a superseded transport", + "recovers again after a second transport death", + "rejects a non-increasing replacement generation and remains retryable", + "fences a ready generation even when the replacement closes before its await resumes", + "replaces the tunnel generation before recycling a bounded stream ID" ] }, { @@ -6456,12 +6455,12 @@ { "file": "src/main/browser/network/execution-routes/ssh-browser-network-execution-route.test.ts", "assertions": [ - "missing and stale SSH provider authority is rejected before destination open", - "ssh2 forwardOut receives the exact domain and port without desktop resolution", - "a synchronous forwardOut disconnect releases the exact deferred socket once", - "provider rotation invalidates and releases the exact route once", - "one system-SSH dynamic forward is owned by the whole route", - "grant revocation aborts an in-progress system-SSH dynamic forward" + "does not adopt a replacement ssh2 client under the captured authority", + "releases a deferred socket when forwardOut throws during disconnect", + "aborts system SSH startup when route authorization is revoked", + "reaches external sites over ssh2 through the host proxy and keeps local names direct", + "dials directly while the host environment is unreadable and adopts it once a retry reads it", + "reaches external sites over system SSH through the host proxy" ] }, { @@ -6591,26 +6590,6 @@ "result": "passed", "durationSeconds": 5.58, "summary": "Test Files 4 passed (4); Tests 44 passed (44). Deterministic fixtures; no physical phone or deployed mixed-host claim." - }, - { - "date": "2026-11-04", - "runner": "local", - "platform": "linux", - "result": "passed", - "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm --config.verifyDepsBeforeRun=false test src/main/browser/network/execution-routes/paired-runtime-browser-network-route.test.ts src/main/browser/network/tunnels/browser-network-tunnel-client.test.ts src/main/runtime/browser-network-tunnel-paired-runtime.integration.test.ts src/main/runtime/rpc/methods/browser-network-tunnel.test.ts --maxWorkers=2", - "currentCommand": "pnpm exec vitest run --config config/vitest.config.ts src/main/browser/network/tunnels/browser-network-tunnel-client.test.ts src/main/browser/network/execution-routes/paired-runtime-browser-network-route.test.ts src/main/runtime/rpc/methods/browser-network-tunnel.test.ts src/main/runtime/browser/network/browser-network-tunnel-paired-runtime.integration.test.ts", - "durationSeconds": 9.31, - "summary": "Test Files 4 passed (4); Tests 50 passed (50). Focused Linux unit execution; no physical-provider, p95 or cross-platform claim. Source head b638e07d395b76af3ba6b03761a1a0fe7e5e026e." - }, - { - "date": "2026-11-04", - "runner": "local", - "platform": "linux", - "result": "passed", - "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm --config.verifyDepsBeforeRun=false test src/main/browser/network/execution-routes/paired-runtime-browser-network-route.test.ts src/main/browser/network/tunnels/browser-network-tunnel-client.test.ts src/main/runtime/browser-network-tunnel-paired-runtime.integration.test.ts src/main/runtime/rpc/methods/browser-network-tunnel.test.ts --maxWorkers=2", - "currentCommand": "pnpm exec vitest run --config config/vitest.config.ts src/main/browser/network/tunnels/browser-network-tunnel-client.test.ts src/main/browser/network/execution-routes/paired-runtime-browser-network-route.test.ts src/main/runtime/rpc/methods/browser-network-tunnel.test.ts src/main/runtime/browser/network/browser-network-tunnel-paired-runtime.integration.test.ts", - "durationSeconds": 9.07, - "summary": "Test Files 4 passed (4); Tests 50 passed (50). Focused Linux unit execution; no physical-provider, p95 or cross-platform claim. Source head 415c9cffc2d10024131ec4b6721767a22195de6d." } ], "runtimeBudget": { @@ -6638,6 +6617,7 @@ "Prove an Electron partition routes loopback, DNS, HTTP, HTTPS, WebSocket, workers, and speculative traffic without desktop fallback." ], "knownGaps": [ + "Audit1571 removes mock forwarding, capability/schema inventories, private predicates, supplied error/result replays, and filesystem-mocked persistence checks without equivalent replacement claims. Retained changed tests also exercise real local permission-file reread, registry proxy rollback/credential retirement, site display recovery, and pending sender/credential/document admission with injected Electron/UI producers. Retained network tests exercise owned stream credit/cleanup, generation and retirement fences, real loopback proxy/SOCKS bytes and file-backed binding state. SSH/WSL/paired runtime, native sessions, authority producers and storage clears remain injected; simulated relaunch uses filesystem mocks. The unchanged profile-retirement suite was statically reviewed and not rerun. No actual SSH/WSL runtime, native Chromium jar deletion, durable process restart, remote DNS or external site proof. Receipts selecting changed tests are retired and unaffected receipts remain unchanged.", "Audit1563 retires mock cookie inventories, summary/flag/counter checks, historical replacement/replay narratives and setup-only alarms without equivalent replacement coverage. Retained changed cases exercise owned admission and cleanup, real staged-file lifetime, JSON/SQLite/WAL/schema compatibility, synthetic encryption and native Electron SameSite storage. Session/CDP/credential/write/renderer producers remain injected where recorded. Seven unchanged native definitions are not rerun locally; known stale registry-stub fixtures are not executable proof. Receipts selecting modified tests are retired; unaffected exact-selector receipts remain unchanged.", "Audit1559 removes mocked host/lease/reconciliation, event/flag/counter, forwarding, inventory and retry-budget alarms without equivalent replacement coverage. Retained cases establish owned promise, registry, rollback, admission and cleanup boundaries with injected Electron/renderer/host inputs; actual local sockets, file staging and Buffer collection are scoped in the PR. Receipts selecting modified tests are retired; unaffected exact-selector receipts remain historical evidence only. No live paired runtime, rendered browser/profile/TLS or device claim.", "The mock-only browser-session startup suite was removed, dropping its once-per-module and retry-after-initialization-throw call-count alarms.", @@ -6838,7 +6818,7 @@ "providers": ["remote-runtime", "ssh", "wsl"], "coveredPlatforms": ["macos", "ios", "linux"], "coveredProviders": ["remote-runtime", "ssh", "wsl"], - "coverageNotes": "Fresh-build Playwright journeys run the same production store action against an isolated headed Electron server and a real headless orca serve host. They prove one immutable client placement owns one real retained guest on the viewing desktop, the server owns no duplicate guest, no screencast frame renders, browser.snapshot reaches the client guest, disabling the setting preserves that guest, and the next page uses the legacy server engine. Deterministic contracts cover omitted placement, missing capabilities, explicit server placement, exact renderer-store materialization after delayed publication, no fallback after client-create failure, folder workspaces, git worktrees, browserless hosts, native and WSL routes, exact connected SSH authority, reconnect command replay, lease replacement, imported-inventory cleanup, bounded retirement, and shared remote screencast fanout for multiple independent viewers. A published v1.4.184 package runs both skew directions: an old client omits placement against the current host, while a current client capability-downgrades against the old host; each creates one server guest, no client guest, and returns the exact snapshot marker. A current iOS Simulator client paired to that legacy packaged host visibly loads Example Domain through the preserved server-hosted surface. A Docker OpenSSH target proves container-only DNS and localhost through both ssh2 and system-SSH routes. Physical Windows/Linux Electron and physical mobile journeys remain gaps. The two Docker remote-only SSH browser routing journeys now run in the dedicated Linux ssh-browser-network-route CI job on full runs and their mapped source/test changes; 2 baseline and 6 repeated cases passed with no skips/retries on 2026-09-06. Schema-only placement/navigation acceptance is excluded from behavioral and mixed-version evidence. The mocked client-placement and immediate create-failure suites were removed, including parameter selection, delayed mocked materialization and no-fallback call ledgers. The remaining live journeys and contracts do not establish equivalent unit alarms. Earlier descriptions and executed results for those fixtures are historical.", + "coverageNotes": "Audit1571 removes mock forwarding, capability/schema inventories, private predicates, supplied error/result replays, and filesystem-mocked persistence checks without equivalent replacement claims. Retained changed tests also exercise real local permission-file reread, registry proxy rollback/credential retirement, site display recovery, and pending sender/credential/document admission with injected Electron/UI producers. Retained network tests exercise owned stream credit/cleanup, generation and retirement fences, real loopback proxy/SOCKS bytes and file-backed binding state. SSH/WSL/paired runtime, native sessions, authority producers and storage clears remain injected; simulated relaunch uses filesystem mocks. The unchanged profile-retirement suite was statically reviewed and not rerun. No actual SSH/WSL runtime, native Chromium jar deletion, durable process restart, remote DNS or external site proof. Receipts selecting changed tests are retired and unaffected receipts remain unchanged. Historical scope before this audit: Fresh-build Playwright journeys run the same production store action against an isolated headed Electron server and a real headless orca serve host. They prove one immutable client placement owns one real retained guest on the viewing desktop, the server owns no duplicate guest, no screencast frame renders, browser.snapshot reaches the client guest, disabling the setting preserves that guest, and the next page uses the legacy server engine. Deterministic contracts cover omitted placement, missing capabilities, explicit server placement, exact renderer-store materialization after delayed publication, no fallback after client-create failure, folder workspaces, git worktrees, browserless hosts, native and WSL routes, exact connected SSH authority, reconnect command replay, lease replacement, imported-inventory cleanup, bounded retirement, and shared remote screencast fanout for multiple independent viewers. A published v1.4.184 package runs both skew directions: an old client omits placement against the current host, while a current client capability-downgrades against the old host; each creates one server guest, no client guest, and returns the exact snapshot marker. A current iOS Simulator client paired to that legacy packaged host visibly loads Example Domain through the preserved server-hosted surface. A Docker OpenSSH target proves container-only DNS and localhost through both ssh2 and system-SSH routes. Physical Windows/Linux Electron and physical mobile journeys remain gaps. The two Docker remote-only SSH browser routing journeys now run in the dedicated Linux ssh-browser-network-route CI job on full runs and their mapped source/test changes; 2 baseline and 6 repeated cases passed with no skips/retries on 2026-09-06. Schema-only placement/navigation acceptance is excluded from behavioral and mixed-version evidence. The mocked client-placement and immediate create-failure suites were removed, including parameter selection, delayed mocked materialization and no-fallback call ledgers. The remaining live journeys and contracts do not establish equivalent unit alarms. Earlier descriptions and executed results for those fixtures are historical.", "motivatingLinks": [ "https://linear.app/stably/issue/STA-4150/refactor-remote-browser-to-client-hosted-electron-webviews" ], @@ -6999,6 +6979,7 @@ "Complete fresh architecture, compatibility, lifecycle, security, performance, and test reviews on the rebased landing stack." ], "knownGaps": [ + "Audit1571 removes mock forwarding, capability/schema inventories, private predicates, supplied error/result replays, and filesystem-mocked persistence checks without equivalent replacement claims. Retained changed tests also exercise real local permission-file reread, registry proxy rollback/credential retirement, site display recovery, and pending sender/credential/document admission with injected Electron/UI producers. Retained network tests exercise owned stream credit/cleanup, generation and retirement fences, real loopback proxy/SOCKS bytes and file-backed binding state. SSH/WSL/paired runtime, native sessions, authority producers and storage clears remain injected; simulated relaunch uses filesystem mocks. The unchanged profile-retirement suite was statically reviewed and not rerun. No actual SSH/WSL runtime, native Chromium jar deletion, durable process restart, remote DNS or external site proof. Receipts selecting changed tests are retired and unaffected receipts remain unchanged.", "Live production journeys are macOS-only; WSL is deterministic and SSH uses Docker Linux rather than physical Windows/Linux Electron.", "The packaged skew journey uses one published old package plus the current E2E artifact on macOS; packaged Windows/Linux and a second independently released post-feature package remain unrecorded.", "The live fixture proves routed HTTP, HTTPS, WebSocket, redirect, subresource, and download ownership plus remote DNS through the fixed SOCKS session; worker, speculative, WebRTC, UDP, and DoH containment remain unrecorded.", diff --git a/config/scripts/ci/sharding/ci-shard-timings.json b/config/scripts/ci/sharding/ci-shard-timings.json index 7bb82692c44..4dd66f56bc4 100644 --- a/config/scripts/ci/sharding/ci-shard-timings.json +++ b/config/scripts/ci/sharding/ci-shard-timings.json @@ -1224,7 +1224,6 @@ "src/main/browser/sessions/browser-session-proxy.test.ts": 857, "src/main/browser/sessions/browser-session-registry-identity.persistence.test.ts": 603, "src/main/browser/sessions/browser-session-registry-startup-metadata.test.ts": 478, - "src/main/browser/sessions/browser-session-registry.persistence.test.ts": 1132, "src/main/browser/sessions/browser-session-registry.test.ts": 883, "src/main/browser/identity/browser-session-ua-wire-identity-cross-context.electron.test.ts": 7167, "src/main/browser/identity/browser-session-ua-wire-identity.electron.test.ts": 8280, diff --git a/src/main/browser/network/client-routes/browser-client-network-route-registry.test.ts b/src/main/browser/network/client-routes/browser-client-network-route-registry.test.ts index f83bf21eec2..712bddadd6d 100644 --- a/src/main/browser/network/client-routes/browser-client-network-route-registry.test.ts +++ b/src/main/browser/network/client-routes/browser-client-network-route-registry.test.ts @@ -87,69 +87,6 @@ describe('BrowserClientNetworkRouteRegistry', () => { expect(route.close).toHaveBeenCalledTimes(3) }) - it('rejects native and WSL routes for a different authority runtime', async () => { - const routeFactory = vi.fn(() => createRoute()) - const registry = new BrowserClientNetworkRouteRegistry({ - authority, - authorityStorageKey, - createRoute: routeFactory - }) - const key = browserNetworkExecutionHostKey({ - kind: 'native', - runtimeId: 'runtime-b', - revision: 1 - }) - - await expect(registry.retain(key, signal())).rejects.toThrow( - 'browser_client_network_route_authority_mismatch' - ) - const wslKey = browserNetworkExecutionHostKey({ - kind: 'wsl', - runtimeId: 'runtime-b', - revision: 1, - distro: 'Ubuntu' - }) - await expect(registry.retain(wslKey, signal())).rejects.toThrow( - 'browser_client_network_route_authority_mismatch' - ) - expect(routeFactory).not.toHaveBeenCalled() - }) - - it('closes every retained route and rejects later admission', async () => { - const firstRoute = createRoute() - const secondRoute = createRoute() - const routeFactory = vi.fn().mockReturnValueOnce(firstRoute).mockReturnValueOnce(secondRoute) - const registry = new BrowserClientNetworkRouteRegistry({ - authority, - authorityStorageKey, - createRoute: routeFactory - }) - await registry.retain( - browserNetworkExecutionHostKey({ kind: 'native', runtimeId: 'runtime-a', revision: 1 }), - signal() - ) - await registry.retain( - browserNetworkExecutionHostKey({ - kind: 'ssh', - targetId: 'ssh-a', - providerEpoch: 'provider-a', - connectionGeneration: 3 - }), - signal() - ) - - await registry.close() - - expect(firstRoute.close).toHaveBeenCalledOnce() - expect(secondRoute.close).toHaveBeenCalledOnce() - await expect( - registry.retain( - browserNetworkExecutionHostKey({ kind: 'native', runtimeId: 'runtime-a', revision: 2 }), - signal() - ) - ).rejects.toThrow('browser_client_network_route_registry_closed') - }) - it('retires old authority routes without destroying them before exact page cleanup', async () => { const route = createRoute() const registry = new BrowserClientNetworkRouteRegistry({ @@ -181,92 +118,6 @@ describe('BrowserClientNetworkRouteRegistry', () => { expect(route.close).toHaveBeenCalledOnce() }) - it('force-closes retained retired routes during final shutdown', async () => { - const route = createRoute() - const registry = new BrowserClientNetworkRouteRegistry({ - authority, - authorityStorageKey, - createRoute: () => route - }) - await registry.retain( - browserNetworkExecutionHostKey({ kind: 'native', runtimeId: 'runtime-a', revision: 1 }), - signal() - ) - - void registry.retire() - await registry.close() - - expect(route.close).toHaveBeenCalledOnce() - }) - - it('suspends every retained transport and restores the same listener address', async () => { - const route = createRoute() - const registry = new BrowserClientNetworkRouteRegistry({ - authority, - authorityStorageKey, - createRoute: () => route - }) - const key = browserNetworkExecutionHostKey({ - kind: 'native', - runtimeId: 'runtime-a', - revision: 1 - }) - await registry.retain(key, signal()) - - registry.suspend() - await expect(registry.retain(key, signal())).rejects.toThrow( - 'browser_client_network_route_registry_suspended' - ) - await registry.reconnect() - - expect(route.suspend).toHaveBeenCalledOnce() - expect(route.reconnect).toHaveBeenCalledOnce() - await expect(registry.retain(key, signal())).resolves.toMatchObject({ - proxyEndpoint: { host: '127.0.0.1', port: 43123 } - }) - await registry.close() - }) - - it('retries one flaky route without closing healthy retained routes', async () => { - vi.useFakeTimers() - const flaky = createRoute() - flaky.reconnect - .mockRejectedValueOnce(new Error('transient tunnel failure')) - .mockResolvedValue({ host: '127.0.0.1', port: 43123 }) - const healthy = createRoute() - const registry = new BrowserClientNetworkRouteRegistry({ - authority, - authorityStorageKey, - reconnectGraceMs: 1_000, - reconnectRetryDelayMs: 10, - createRoute: vi.fn().mockReturnValueOnce(flaky).mockReturnValueOnce(healthy) - }) - await registry.retain( - browserNetworkExecutionHostKey({ kind: 'native', runtimeId: 'runtime-a', revision: 1 }), - signal() - ) - await registry.retain( - browserNetworkExecutionHostKey({ - kind: 'ssh', - targetId: 'ssh-a', - providerEpoch: 'provider-a', - connectionGeneration: 1 - }), - signal() - ) - - registry.suspend() - const reconnecting = registry.reconnect() - await vi.runAllTimersAsync() - await expect(reconnecting).resolves.toBeUndefined() - - expect(flaky.reconnect).toHaveBeenCalledTimes(2) - expect(healthy.reconnect).toHaveBeenCalledOnce() - expect(flaky.close).not.toHaveBeenCalled() - expect(healthy.close).not.toHaveBeenCalled() - await registry.close() - }) - it('aborts a stale route recovery without retaining its retry timer', async () => { vi.useFakeTimers() let rejectReconnect = (_error: Error): void => {} @@ -398,22 +249,6 @@ describe('BrowserClientNetworkRouteRegistry', () => { expect(retired).toBe(true) }) - it('refuses a retain whose signal aborted before admission', async () => { - const routeFactory = vi.fn(() => createRoute()) - const registry = new BrowserClientNetworkRouteRegistry({ - authority, - authorityStorageKey, - createRoute: routeFactory - }) - const controller = new AbortController() - controller.abort() - - await expect(registry.retain(nativeKey(1), controller.signal)).rejects.toThrow( - 'browser_client_network_route_aborted' - ) - expect(routeFactory).not.toHaveBeenCalled() - }) - it('rejects a retain the moment its signal aborts mid-startup', async () => { let resolveStart = (_address: { host: string; port: number }): void => {} const route = createRoute( @@ -461,111 +296,6 @@ describe('BrowserClientNetworkRouteRegistry', () => { expect(route.start).toHaveBeenCalledOnce() }) - it('keeps a fully released key clean so final cleanup closes it only once', async () => { - const route = createRoute() - const registry = new BrowserClientNetworkRouteRegistry({ - authority, - authorityStorageKey, - createRoute: vi.fn(() => route) - }) - const retained = await registry.retain(nativeKey(1), signal()) - - await retained.release() - expect(route.close).toHaveBeenCalledOnce() - - await registry.close() - expect(route.close).toHaveBeenCalledOnce() - }) - - it('never re-closes a route the registry already force-closed', async () => { - const route = createRoute() - const registry = new BrowserClientNetworkRouteRegistry({ - authority, - authorityStorageKey, - createRoute: vi.fn(() => route) - }) - const key = nativeKey(1) - const first = await registry.retain(key, signal()) - const second = await registry.retain(key, signal()) - - await registry.close() - await first.release() - await second.release() - - expect(route.close).toHaveBeenCalledOnce() - }) - - it('ignores a repeated release from the same page handle', async () => { - const route = createRoute() - const registry = new BrowserClientNetworkRouteRegistry({ - authority, - authorityStorageKey, - createRoute: vi.fn(() => route) - }) - const key = nativeKey(1) - const first = await registry.retain(key, signal()) - const second = await registry.retain(key, signal()) - - await first.release() - await first.release() - expect(route.close).not.toHaveBeenCalled() - - await second.release() - expect(route.close).toHaveBeenCalledOnce() - }) - - it('refuses a retain whose route came back on a different port', async () => { - const route = createRoute() - route.reconnect.mockResolvedValue({ host: '127.0.0.1', port: 43999 }) - const registry = new BrowserClientNetworkRouteRegistry({ - authority, - authorityStorageKey, - createRoute: vi.fn(() => route) - }) - const key = nativeKey(1) - await registry.retain(key, signal()) - - await expect(registry.retain(key, signal())).rejects.toThrow( - 'browser_client_network_route_address_changed' - ) - }) - - it('refuses a reconnect that moved a retained route to a different port', async () => { - const route = createRoute() - const registry = new BrowserClientNetworkRouteRegistry({ - authority, - authorityStorageKey, - createRoute: vi.fn(() => route) - }) - await registry.retain(nativeKey(1), signal()) - registry.suspend() - route.reconnect.mockResolvedValue({ host: '127.0.0.1', port: 43999 }) - - await expect(registry.reconnect()).rejects.toThrow( - 'browser_client_network_route_address_changed' - ) - }) - - it('fails retirement when a route refuses to suspend', async () => { - const route = createRoute() - route.suspend.mockImplementation(() => { - throw new Error('suspend refused') - }) - const registry = new BrowserClientNetworkRouteRegistry({ - authority, - authorityStorageKey, - createRoute: vi.fn(() => route) - }) - await registry.retain(nativeKey(1), signal()) - - expect(() => registry.retire()).toThrow('Browser client network route suspension failed') - - await expect(registry.retire()).rejects.toThrow( - 'Browser client network route suspension failed' - ) - expect(route.suspend).toHaveBeenCalledOnce() - }) - it('reports a failed final cleanup to an awaiting retirement', async () => { const route = createRoute() route.close.mockRejectedValue(new Error('route cleanup outcome unknown')) @@ -585,40 +315,6 @@ describe('BrowserClientNetworkRouteRegistry', () => { expect(retirementFailure).toBe('Browser client network route cleanup failed') }) - - it('refuses to report retirement for a registry whose close already failed', async () => { - const route = createRoute() - route.close.mockRejectedValue(new Error('route cleanup outcome unknown')) - const registry = new BrowserClientNetworkRouteRegistry({ - authority, - authorityStorageKey, - createRoute: vi.fn(() => route) - }) - await registry.retain(nativeKey(1), signal()) - - await expect(registry.close()).rejects.toThrow('Browser client network route cleanup failed') - - await expect(registry.retire()).rejects.toThrow('Browser client network route cleanup failed') - }) - - it('surfaces both the retain failure and its cleanup failure', async () => { - const route = createRoute() - route.start.mockRejectedValue(new Error('tunnel start failed')) - route.close.mockRejectedValue(new Error('route cleanup outcome unknown')) - const registry = new BrowserClientNetworkRouteRegistry({ - authority, - authorityStorageKey, - createRoute: vi.fn(() => route) - }) - - await expect(registry.retain(nativeKey(1), signal())).rejects.toMatchObject({ - message: 'tunnel start failed', - errors: [ - expect.objectContaining({ message: 'tunnel start failed' }), - expect.objectContaining({ message: 'route cleanup outcome unknown' }) - ] - }) - }) }) function nativeKey(revision: number): string { diff --git a/src/main/browser/network/execution-routes/paired-runtime-browser-network-route.test.ts b/src/main/browser/network/execution-routes/paired-runtime-browser-network-route.test.ts index dc36c4cb788..143f6640757 100644 --- a/src/main/browser/network/execution-routes/paired-runtime-browser-network-route.test.ts +++ b/src/main/browser/network/execution-routes/paired-runtime-browser-network-route.test.ts @@ -25,7 +25,6 @@ vi.mock('../../../../shared/remote-runtime/remote-runtime-client', () => ({ import { PairedRuntimeBrowserNetworkRoute } from './paired-runtime-browser-network-route' import { BrowserNetworkTunnelOutboundMemoryBudgetRegistry } from '../tunnels/browser-network-tunnel-outbound-memory-budget' -import { RemoteBrowserSocksServer } from '../tunnels/remote-browser-socks-server' const pairing = { v: 2, @@ -119,92 +118,6 @@ describe('PairedRuntimeBrowserNetworkRoute', () => { expect(closeSubscription).toHaveBeenCalledOnce() }) - it('surfaces listener teardown failures', async () => { - vi.spyOn(RemoteBrowserSocksServer.prototype, 'close').mockRejectedValueOnce( - new Error('listener close failed') - ) - const route = createRoute() - - await expect(route.close()).rejects.toThrow('listener close failed') - }) - - it('tears down the local route when the runtime closes the tunnel', async () => { - let callbacks: RemoteRuntimeSubscriptionCallbacks | undefined - const closeSubscription = vi.fn() - subscribeRemoteRuntimeRequestMock.mockImplementationOnce( - async (...args: unknown[]): Promise => { - callbacks = args[4] as RemoteRuntimeSubscriptionCallbacks - return { - requestId: 'runtime-closed-subscription', - close: closeSubscription, - sendBinary: () => true - } - } - ) - const onError = vi.fn() - const route = createRoute({ onError }) - const starting = route.start() - await vi.waitFor(() => expect(callbacks).toBeDefined()) - callbacks!.onResponse({ - id: 'runtime-closed-subscription', - ok: true, - result: { type: 'ready', tunnelGeneration: 7 }, - _meta: { runtimeId: 'runtime-a' } - }) - await starting - - callbacks!.onResponse({ - id: 'runtime-closed-subscription', - ok: true, - result: { type: 'closed', tunnelGeneration: 7 }, - _meta: { runtimeId: 'runtime-a' } - }) - - await vi.waitFor(() => expect(closeSubscription).toHaveBeenCalledOnce()) - expect(onError).toHaveBeenCalledWith( - expect.objectContaining({ message: 'Browser network route closed by the runtime' }) - ) - }) - - it('finishes failure cleanup when reporting and listener teardown both throw', async () => { - let callbacks: RemoteRuntimeSubscriptionCallbacks | undefined - const closeSubscription = vi.fn() - subscribeRemoteRuntimeRequestMock.mockImplementationOnce( - async (...args: unknown[]): Promise => { - callbacks = args[4] as RemoteRuntimeSubscriptionCallbacks - return { - requestId: 'cleanup-failure-subscription', - close: closeSubscription, - sendBinary: () => true - } - } - ) - const closeSocks = vi - .spyOn(RemoteBrowserSocksServer.prototype, 'close') - .mockRejectedValueOnce(new Error('listener close failed')) - const route = createRoute({ - onError: () => { - throw new Error('reporting failed') - } - }) - const starting = route.start() - await vi.waitFor(() => expect(callbacks).toBeDefined()) - callbacks!.onResponse({ - id: 'cleanup-failure-subscription', - ok: true, - result: { type: 'ready', tunnelGeneration: 7 }, - _meta: { runtimeId: 'runtime-a' } - }) - await starting - - callbacks!.onClose?.() - - await vi.waitFor(() => expect(closeSubscription).toHaveBeenCalledOnce()) - expect(closeSocks).not.toHaveBeenCalled() - await expect(route.close()).rejects.toThrow('listener close failed') - expect(closeSocks).toHaveBeenCalledOnce() - }) - it('binds and releases the exact browser-host outbound memory lease', async () => { let callbacks: RemoteRuntimeSubscriptionCallbacks | undefined let subscriptionOptions: Record | undefined @@ -235,14 +148,6 @@ describe('PairedRuntimeBrowserNetworkRoute', () => { expect(registry.evidence()).toMatchObject({ hosts: 0, leases: 0 }) }) - it('opens no subscription when browser-host memory admission is exhausted', async () => { - const registry = new BrowserNetworkTunnelOutboundMemoryBudgetRegistry({ processMaxLeases: 0 }) - const route = createRoute({ outboundMemoryBudgetRegistry: registry }) - - await expect(route.start()).rejects.toThrow('outbound memory admission failed') - expect(subscribeRemoteRuntimeRequestMock).not.toHaveBeenCalled() - }) - it('keeps one listener fail-closed and accepts an increasing replacement generation', async () => { const attempts = mockSubscriptionAttempts() const route = createRoute() @@ -274,111 +179,6 @@ describe('PairedRuntimeBrowserNetworkRoute', () => { await route.close() }) - it('suspends the tunnel immediately while preserving the SOCKS listener', async () => { - const attempts = mockSubscriptionAttempts() - const route = createRoute() - const starting = route.start() - await vi.waitFor(() => expect(attempts).toHaveLength(1)) - ready(attempts[0]!, 7) - const address = await starting - - route.suspend() - expect(attempts[0]!.close).toHaveBeenCalledOnce() - const offline = await connectSocks(address.host, address.port) - await greetSocks(offline) - offline.write(domainConnectRequest('offline.internal', 443)) - expect(Array.from(await readExact(offline, 10))).toEqual([5, 1, 0, 1, 0, 0, 0, 0, 0, 0]) - offline.destroy() - - const reconnecting = route.reconnect() - await vi.waitFor(() => expect(attempts).toHaveLength(2)) - ready(attempts[1]!, 8) - await expect(reconnecting).resolves.toEqual(address) - await route.close() - }) - - it('reattaches with the exact existing v1 payload and capability pair', async () => { - const attempts = mockSubscriptionAttempts() - const route = createRoute({ - subscription: { clientCapabilities: ['existing.optional.v1'] } - }) - const starting = route.start() - await vi.waitFor(() => expect(attempts).toHaveLength(1)) - ready(attempts[0]!, 7) - await starting - attempts[0]!.callbacks.onClose?.() - - const reconnecting = route.reconnect() - await vi.waitFor(() => expect(attempts).toHaveLength(2)) - ready(attempts[1]!, 8) - await reconnecting - - for (const attempt of attempts) { - expect(attempt.method).toBe('network.browserTunnel') - expect(attempt.params).toEqual({ - ...lease, - executionHost: { kind: 'native', runtimeId: 'runtime-a', revision: 1 } - }) - expect(attempt.options).toMatchObject({ - perMessageDeflate: false, - clientCapabilities: [ - 'existing.optional.v1', - 'browser.clientHost.v1', - 'network.browserTunnel.v1' - ] - }) - } - await route.close() - }) - - it('keeps page-command negotiation off the independent tunnel attach', async () => { - const attempts = mockSubscriptionAttempts() - const commandLease = { ...lease, pageCommandProtocolVersion: 1 } - const route = new PairedRuntimeBrowserNetworkRoute({ - connection: pairedRuntimeConnectionFromOffer(pairing), - lease: commandLease, - executionHostRevision: 1 - }) - const starting = route.start() - await vi.waitFor(() => expect(attempts).toHaveLength(1)) - ready(attempts[0]!, 7) - await starting - - expect(attempts[0]!.params).toEqual({ - ...lease, - executionHost: { kind: 'native', runtimeId: 'runtime-a', revision: 1 } - }) - await route.close() - }) - - it('negotiates execution-host routing only for an SSH descriptor', async () => { - const attempts = mockSubscriptionAttempts() - const executionHost = { - kind: 'ssh' as const, - targetId: 'target-a', - providerEpoch: 'provider-epoch-a', - connectionGeneration: 2 - } - const route = createRoute({ executionHost }) - const starting = route.start() - await vi.waitFor(() => expect(attempts).toHaveLength(1)) - ready(attempts[0]!, 7) - await starting - - expect(attempts[0]).toMatchObject({ - method: 'network.browserTunnel', - params: { ...lease, executionHost }, - options: { - clientCapabilities: [ - 'browser.clientHost.v1', - 'network.browserTunnel.v1', - 'network.browserTunnel.executionHosts.v1' - ] - } - }) - await route.close() - }) - it('ignores late callbacks from a superseded transport', async () => { const attempts = mockSubscriptionAttempts() const route = createRoute() @@ -413,69 +213,6 @@ describe('PairedRuntimeBrowserNetworkRoute', () => { await route.close() }) - it('reconnects after a current tunnel protocol failure closes the client', async () => { - const attempts = mockSubscriptionAttempts() - const route = createRoute() - const starting = route.start() - await vi.waitFor(() => expect(attempts).toHaveLength(1)) - ready(attempts[0]!, 7) - const address = await starting - - attempts[0]!.callbacks.onBinary?.(new Uint8Array([1, 2, 3])) - await vi.waitFor(() => expect(attempts[0]!.close).toHaveBeenCalledOnce()) - const reconnecting = route.reconnect() - await vi.waitFor(() => expect(attempts).toHaveLength(2)) - ready(attempts[1]!, 8) - await reconnecting - - const current = await connectSocks(address.host, address.port) - await greetSocks(current) - current.write(domainConnectRequest('recovered.internal', 443)) - await vi.waitFor(() => expect(openFrames(attempts[1]!)).toHaveLength(1)) - attempts[1]!.callbacks.onBinary?.( - encodeBrowserNetworkTunnelFrame({ - opcode: BrowserNetworkTunnelOpcode.Opened, - tunnelGeneration: 8, - streamId: 1, - payload: new Uint8Array() - }) - ) - expect(Array.from(await readExact(current, 10))).toEqual([5, 0, 0, 1, 0, 0, 0, 0, 0, 0]) - - current.destroy() - await route.close() - }) - - it('rebuilds a retained route after its tunnel transport dies unprompted', async () => { - const attempts = mockSubscriptionAttempts() - const route = createRoute() - const starting = route.start() - await vi.waitFor(() => expect(attempts).toHaveLength(1)) - ready(attempts[0]!, 7) - const address = await starting - - attempts[0]!.callbacks.onClose?.() - - await vi.waitFor(() => expect(attempts).toHaveLength(2)) - ready(attempts[1]!, 8) - const recovered = await connectSocks(address.host, address.port) - await greetSocks(recovered) - recovered.write(domainConnectRequest('recovered.internal', 443)) - await vi.waitFor(() => expect(openFrames(attempts[1]!)).toHaveLength(1)) - attempts[1]!.callbacks.onBinary?.( - encodeBrowserNetworkTunnelFrame({ - opcode: BrowserNetworkTunnelOpcode.Opened, - tunnelGeneration: 8, - streamId: 1, - payload: new Uint8Array() - }) - ) - expect(Array.from(await readExact(recovered, 10))).toEqual([5, 0, 0, 1, 0, 0, 0, 0, 0, 0]) - - recovered.destroy() - await route.close() - }) - it('recovers again after a second transport death', async () => { const attempts = mockSubscriptionAttempts() const route = createRoute() @@ -498,46 +235,6 @@ describe('PairedRuntimeBrowserNetworkRoute', () => { await route.close() }) - it('retries a failing rebuild and reports the terminal failure to the route owner', async () => { - const attempts = mockSubscriptionAttempts() - const onUnavailable = vi.fn() - const route = createRoute({ onUnavailable, recoveryGraceMs: 200, recoveryRetryDelayMs: 10 }) - const starting = route.start() - await vi.waitFor(() => expect(attempts).toHaveLength(1)) - ready(attempts[0]!, 7) - await starting - - subscribeRemoteRuntimeRequestMock.mockRejectedValue(new Error('runtime unreachable')) - attempts[0]!.callbacks.onClose?.() - - await vi.waitFor(() => - expect(onUnavailable).toHaveBeenCalledWith( - expect.objectContaining({ message: expect.stringContaining('could not be rebuilt') }) - ) - ) - expect(subscribeRemoteRuntimeRequestMock.mock.calls.length).toBeGreaterThan(2) - await route.close() - }) - - it('abandons transport recovery once the route is suspended', async () => { - const attempts = mockSubscriptionAttempts() - const route = createRoute({ recoveryGraceMs: 5_000 }) - const starting = route.start() - await vi.waitFor(() => expect(attempts).toHaveLength(1)) - ready(attempts[0]!, 7) - await starting - - attempts[0]!.callbacks.onClose?.() - await vi.waitFor(() => expect(attempts).toHaveLength(2)) - attempts[1]!.callbacks.onClose?.() - route.suspend() - - const settled = attempts.length - await new Promise((resolve) => setTimeout(resolve, 250)) - expect(attempts).toHaveLength(settled) - await route.close() - }) - it('rejects a non-increasing replacement generation and remains retryable', async () => { const attempts = mockSubscriptionAttempts() const route = createRoute() diff --git a/src/main/browser/network/execution-routes/ssh-browser-network-execution-route.test.ts b/src/main/browser/network/execution-routes/ssh-browser-network-execution-route.test.ts index a2ba110b761..139a052d21c 100644 --- a/src/main/browser/network/execution-routes/ssh-browser-network-execution-route.test.ts +++ b/src/main/browser/network/execution-routes/ssh-browser-network-execution-route.test.ts @@ -1,10 +1,8 @@ -import { EventEmitter, once } from 'node:events' +import { EventEmitter } from 'node:events' import type { ChildProcess } from 'node:child_process' -import { connect, createServer, type AddressInfo, type Socket } from 'node:net' -import { PassThrough } from 'node:stream' +import { connect, createServer, type Socket } from 'node:net' import { afterEach, describe, expect, it, vi } from 'vitest' import type { SshConnection } from '../../../ssh/connection/ssh-connection' -import type { SshProviderEpoch } from '../../../../shared/ssh-types' import { resolveSshBrowserNetworkExecutionRoute } from './ssh-browser-network-execution-route' import { setNetworkProxySettingsSource } from './browser-network-proxy-settings-source' import type { BrowserNetworkTunnelSocket } from '../tunnels/browser-network-tunnel-stream-state' @@ -48,75 +46,6 @@ afterEach(async () => { }) describe('SSH browser network execution route', () => { - it('rejects an unavailable or stale authority before opening a destination', async () => { - const connection = fakeConnection({ forwardOut: vi.fn() }) - const registerAuthorityAbort = vi.fn() - - await expect( - resolveSshBrowserNetworkExecutionRoute( - { executionHost, runtimeId: 'runtime-a', runtimeRevision: 1 }, - { - connectionManager: { getConnection: () => connection }, - isCurrentAuthority: () => false, - registerAuthorityAbort - } - ) - ).rejects.toThrow('browser_tunnel_execution_host_unavailable') - expect(registerAuthorityAbort).not.toHaveBeenCalled() - }) - - it('passes the exact domain and port to ssh2 forwardOut and fences rotation', async () => { - const channel = new PassThrough() as PassThrough & { close: ReturnType } - channel.close = vi.fn(() => channel.destroy()) - const forwardOut = vi.fn( - ( - _sourceHost: string, - _sourcePort: number, - _host: string, - _port: number, - callback: (error: Error | undefined, channel: PassThrough) => void - ) => callback(undefined, channel) - ) - const connection = fakeConnection({ forwardOut }) - let current = true - let authorityAbort: AbortController | undefined - const removeAuthorityAbort = vi.fn() - const route = await resolveSshBrowserNetworkExecutionRoute( - { executionHost, runtimeId: 'runtime-a', runtimeRevision: 1 }, - { - connectionManager: { getConnection: () => connection }, - isCurrentAuthority: (authority) => - current && - authority.providerEpoch === ('provider-epoch-a' as SshProviderEpoch) && - authority.connectionGeneration === 2, - registerAuthorityAbort: (_authority, controller) => { - authorityAbort = controller - return removeAuthorityAbort - }, - readHostProxyEnvironment: noHostProxy - } - ) - - const socket = route.connect({ host: 'split-horizon.internal', port: 8443 }) - await once(socket as unknown as EventEmitter, 'connect') - expect(forwardOut).toHaveBeenCalledWith( - '127.0.0.1', - 0, - 'split-horizon.internal', - 8443, - expect.any(Function) - ) - - current = false - authorityAbort?.abort() - await route.whenInvalidated - expect(route.isValid()).toBe(false) - await route.close() - await route.close() - expect(removeAuthorityAbort).toHaveBeenCalledOnce() - expect(socket.destroyed).toBe(true) - }) - it('does not adopt a replacement ssh2 client under the captured authority', async () => { const firstForwardOut = vi.fn() const secondForwardOut = vi.fn() @@ -177,83 +106,6 @@ describe('SSH browser network execution route', () => { expect(close).toHaveBeenCalledOnce() }) - it('owns one system dynamic forward for the whole execution route', async () => { - const connection = fakeConnection(null) - const process = new EventEmitter() as EventEmitter & { - exitCode: number | null - signalCode: NodeJS.Signals | null - } - process.exitCode = null - process.signalCode = null - const dispose = vi.fn() - const close = vi.fn(async () => {}) - const startDynamicForward = vi.fn(async () => ({ - localPort: 45678, - process: process as unknown as ChildProcess, - stderrTail: () => '', - dispose, - close - })) - const route = await resolveSshBrowserNetworkExecutionRoute( - { executionHost, runtimeId: 'runtime-a', runtimeRevision: 1 }, - { - connectionManager: { getConnection: () => connection }, - isCurrentAuthority: () => true, - registerAuthorityAbort: () => () => {}, - startDynamicForward, - readHostProxyEnvironment: noHostProxy - } - ) - - expect(startDynamicForward).toHaveBeenCalledOnce() - process.emit('error', new Error('dynamic forward failed')) - await route.whenInvalidated - expect(route.isValid()).toBe(false) - await route.close() - await route.close() - expect(dispose).toHaveBeenCalledOnce() - expect(close).toHaveBeenCalledOnce() - }) - - it('fails a stale system SSH socket loudly instead of closing it silently', async () => { - const listener = createServer() - servers.push(listener) - await new Promise((resolve) => listener.listen(0, '127.0.0.1', resolve)) - const connection = fakeConnection(null) - const forwardProcess = new EventEmitter() as EventEmitter & { - exitCode: number | null - signalCode: NodeJS.Signals | null - } - forwardProcess.exitCode = null - forwardProcess.signalCode = null - const route = await resolveSshBrowserNetworkExecutionRoute( - { executionHost, runtimeId: 'runtime-a', runtimeRevision: 1 }, - { - connectionManager: { getConnection: () => connection }, - isCurrentAuthority: () => true, - registerAuthorityAbort: () => () => {}, - readHostProxyEnvironment: noHostProxy, - startDynamicForward: async () => ({ - localPort: (listener.address() as AddressInfo).port, - process: forwardProcess as unknown as ChildProcess, - stderrTail: () => '', - dispose: () => {}, - close: async () => {} - }) - } - ) - - forwardProcess.exitCode = 0 - const socket = route.connect({ host: 'stale.internal', port: 443 }) - const events: string[] = [] - socket.on('error', (error) => events.push(`error:${error.message}`)) - socket.on('close', () => events.push('close')) - - await vi.waitFor(() => expect(events).toContain('close')) - expect(events[0]).toBe('error:browser_tunnel_execution_host_stale') - await route.close() - }) - it('aborts system SSH startup when route authorization is revoked', async () => { const connection = fakeConnection(null) const controller = new AbortController() diff --git a/src/main/browser/network/execution-routes/wsl-browser-network-execution-route.test.ts b/src/main/browser/network/execution-routes/wsl-browser-network-execution-route.test.ts index a9050389bad..27af4bd4590 100644 --- a/src/main/browser/network/execution-routes/wsl-browser-network-execution-route.test.ts +++ b/src/main/browser/network/execution-routes/wsl-browser-network-execution-route.test.ts @@ -234,17 +234,6 @@ describe('WSL browser network execution route', () => { expect(child.kill).toHaveBeenCalledOnce() }) - it('rejects stale runtime identity before launching WSL', async () => { - const launchRelay = vi.fn() - await expect( - resolveWslBrowserNetworkExecutionRoute( - { executionHost, runtimeId: 'runtime-a', runtimeRevision: 8 }, - { launchRelay } - ) - ).rejects.toThrow('browser_tunnel_execution_host_mismatch') - expect(launchRelay).not.toHaveBeenCalled() - }) - it('invalidates the route when the exact distro process exits', async () => { const child = createRelayProcess(() => new FakeDestination()) const route = await resolveWslBrowserNetworkExecutionRoute( @@ -261,26 +250,6 @@ describe('WSL browser network execution route', () => { await route.close() }) - it('delivers a response larger than the relay receive window', async () => { - const payload = patternedPayload(4 * BROWSER_NETWORK_TUNNEL_INITIAL_WINDOW_BYTES) - const destination = new PacedDestination(payload) - const route = await resolveWslBrowserNetworkExecutionRoute( - { executionHost, runtimeId: 'runtime-a', runtimeRevision: 7 }, - { launchRelay: async () => createRelayProcess(() => destination.accept()) } - ) - - const socket = route.connect({ host: 'large.internal', port: 443 }) - socket.on('error', () => {}) - const received: Buffer[] = [] - socket.on('data', (bytes) => received.push(Buffer.from(bytes))) - - await vi.waitFor(() => expect(Buffer.concat(received).byteLength).toBe(payload.byteLength), { - timeout: 10_000 - }) - expect(Buffer.concat(received).equals(payload)).toBe(true) - await route.close() - }) - it('holds the WSL destination behind a stalled paired-client reader, then completes', async () => { const payload = patternedPayload(8 * BROWSER_NETWORK_TUNNEL_INITIAL_WINDOW_BYTES) const destination = new PacedDestination(payload) diff --git a/src/main/browser/network/tunnels/browser-network-tunnel-client.test.ts b/src/main/browser/network/tunnels/browser-network-tunnel-client.test.ts index d19e71ac5ef..80084db57a6 100644 --- a/src/main/browser/network/tunnels/browser-network-tunnel-client.test.ts +++ b/src/main/browser/network/tunnels/browser-network-tunnel-client.test.ts @@ -10,10 +10,7 @@ import { encodeBrowserNetworkTunnelWindowUpdate, type BrowserNetworkTunnelFrame } from '../../../../shared/browser-network-tunnel-protocol' -import { - BROWSER_NETWORK_TUNNEL_CONNECT_TIMEOUT_MS, - BROWSER_NETWORK_TUNNEL_INITIAL_WINDOW_BYTES -} from './browser-network-tunnel-stream-state' +import { BROWSER_NETWORK_TUNNEL_INITIAL_WINDOW_BYTES } from './browser-network-tunnel-stream-state' import { BrowserNetworkTunnelClient } from './browser-network-tunnel-client' function frame( @@ -130,101 +127,6 @@ describe('BrowserNetworkTunnelClient', () => { client.close() }) - it('destroys every stream when the route closes or rejects a frame', async () => { - const client = new BrowserNetworkTunnelClient({ - tunnelGeneration: 7, - sendBinary: () => true - }) - const firstOpening = client.open({ host: 'one.internal', port: 80 }) - client.handleBinary(frame(BrowserNetworkTunnelOpcode.Opened, new Uint8Array(), 7, 1)) - const first = await firstOpening - first.on('error', () => {}) - const secondOpening = client.open({ host: 'two.internal', port: 80 }) - client.handleBinary(frame(BrowserNetworkTunnelOpcode.Opened, new Uint8Array(), 7, 2)) - const second = await secondOpening - second.on('error', () => {}) - - client.handleBinary(new Uint8Array([1, 2, 3])) - - expect(first.destroyed).toBe(true) - expect(second.destroyed).toBe(true) - await expect(client.open({ host: 'three.internal', port: 80 })).rejects.toThrow( - 'Browser tunnel is closed' - ) - }) - - it('retires a remotely failed stream without echoing an error or closing the route', async () => { - const sent: Uint8Array[] = [] - const client = new BrowserNetworkTunnelClient({ - tunnelGeneration: 7, - sendBinary: (bytes) => { - sent.push(bytes) - return true - } - }) - const opening = client.open({ host: 'refused.internal', port: 443 }) - client.handleBinary(frame(BrowserNetworkTunnelOpcode.Opened)) - const socket = await opening - const socketError = once(socket, 'error') - sent.length = 0 - - client.handleBinary( - frame(BrowserNetworkTunnelOpcode.Error, new TextEncoder().encode('destination_error')) - ) - - await expect(socketError).resolves.toEqual([ - expect.objectContaining({ message: 'Browser tunnel destination failed: destination_error' }) - ]) - expect(sent).toEqual([]) - const nextOpening = client.open({ host: 'healthy.internal', port: 443 }) - expect(decodeBrowserNetworkTunnelFrame(sent[0]!)?.opcode).toBe(BrowserNetworkTunnelOpcode.Open) - client.handleBinary(frame(BrowserNetworkTunnelOpcode.Opened, new Uint8Array(), 7, 2)) - const nextSocket = await nextOpening - nextSocket.on('error', () => {}) - client.close() - }) - - it('rejects a destination failure before opening without an unhandled socket error', async () => { - const client = new BrowserNetworkTunnelClient({ - tunnelGeneration: 7, - sendBinary: () => true - }) - const opening = client.open({ host: 'refused.internal', port: 443 }) - - client.handleBinary( - frame(BrowserNetworkTunnelOpcode.Error, new TextEncoder().encode('destination_error')) - ) - - await expect(opening).rejects.toThrow('Browser tunnel destination failed: destination_error') - // Allow a deferred socket 'error' to surface before the test ends. - await new Promise((resolve) => setImmediate(resolve)) - client.close() - }) - - it('fails a never-connected stream on timeout and notifies the remote', async () => { - vi.useFakeTimers() - let opening: Promise - const sent: Uint8Array[] = [] - try { - const client = new BrowserNetworkTunnelClient({ - tunnelGeneration: 7, - sendBinary: (bytes) => { - sent.push(bytes) - return true - } - }) - opening = client.open({ host: 'unreachable.internal', port: 443 }) - sent.length = 0 - vi.advanceTimersByTime(BROWSER_NETWORK_TUNNEL_CONNECT_TIMEOUT_MS) - } finally { - vi.useRealTimers() - } - - await expect(opening).rejects.toThrow('Browser tunnel destination connect timed out') - expect(decodeBrowserNetworkTunnelFrame(sent[0]!)?.opcode).toBe(BrowserNetworkTunnelOpcode.Error) - await new Promise((resolve) => setImmediate(resolve)) - }) - it('ignores a retired stream frame but rejects a never-allocated stream ID', async () => { const client = new BrowserNetworkTunnelClient({ tunnelGeneration: 7, @@ -285,35 +187,6 @@ describe('BrowserNetworkTunnelClient', () => { ).toEqual([1, 2]) }) - it('withholds destination credit until a readable consumer asks for data', async () => { - const sent: Uint8Array[] = [] - const client = new BrowserNetworkTunnelClient({ - tunnelGeneration: 7, - sendBinary: (bytes) => { - sent.push(bytes) - return true - } - }) - const opening = client.open({ host: 'localhost', port: 8080 }) - client.handleBinary(frame(BrowserNetworkTunnelOpcode.Opened)) - const socket = await opening - socket.on('error', () => {}) - sent.length = 0 - - client.handleBinary(frame(BrowserNetworkTunnelOpcode.Data, new Uint8Array([7, 8, 9]))) - expect(sent).toEqual([]) - - const received = once(socket, 'data') - await expect(received).resolves.toEqual([Buffer.from([7, 8, 9])]) - expect(sent).toEqual([]) - socket.settleRead(3) - await vi.waitFor(() => expect(sent).toHaveLength(1)) - expect(decodeBrowserNetworkTunnelFrame(sent[0]!)?.opcode).toBe( - BrowserNetworkTunnelOpcode.WindowUpdate - ) - client.close() - }) - it('replenishes only bytes settled after the readable consumer takes them', async () => { const sent: Uint8Array[] = [] const client = new BrowserNetworkTunnelClient({ diff --git a/src/main/browser/network/tunnels/browser-network-tunnel-conformance.test.ts b/src/main/browser/network/tunnels/browser-network-tunnel-conformance.test.ts index d2a05e1c8b6..17720dd6e61 100644 --- a/src/main/browser/network/tunnels/browser-network-tunnel-conformance.test.ts +++ b/src/main/browser/network/tunnels/browser-network-tunnel-conformance.test.ts @@ -3,8 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vite import { BrowserNetworkTunnelOpcode, decodeBrowserNetworkTunnelFrame, - decodeBrowserNetworkTunnelWindowUpdate, - encodeBrowserNetworkTunnelFrame + decodeBrowserNetworkTunnelWindowUpdate } from '../../../../shared/browser-network-tunnel-protocol' import { BrowserNetworkTunnelClient } from './browser-network-tunnel-client' import type { BrowserNetworkTunnelDuplex } from './browser-network-tunnel-duplex' @@ -67,42 +66,6 @@ describe('browser network tunnel conformance', () => { await teardown(tunnel) }) - it('ends the client stream when the destination closes without a half-close', async () => { - const tunnel = createConformanceTunnel() - const { duplex, destination } = await openStream(tunnel) - const consumer = consumeDuplex(duplex) - destination.emit('data', new Uint8Array([1, 2, 3])) - await settle() - - destination.emit('close') - await whenClosed(duplex) - - expect(opcodesFor(tunnel, 'sessionToClient', 1).slice(-1)).toEqual(['Close']) - expect(consumer.ended).toBe(true) - expect(consumer.bytes()).toEqual(new Uint8Array([1, 2, 3])) - expect(tunnel.closures).toEqual([]) - expect(tunnel.onSessionClose).not.toHaveBeenCalled() - - await teardown(tunnel) - }) - - it('refuses a stream whose destination connect throws without fencing the tunnel', async () => { - const tunnel = createConformanceTunnel() - const healthy = await openStream(tunnel, 'healthy.internal') - - tunnel.refuseNextConnect = true - await expect(tunnel.client.open({ host: 'refused.internal', port: 443 })).rejects.toThrow( - 'destination_connect_failed' - ) - - expect(tunnel.closures).toEqual([]) - expect(tunnel.onSessionClose).not.toHaveBeenCalled() - expect(healthy.duplex.destroyed).toBe(false) - await expectStreamStillCarriesBytes(healthy) - - await teardown(tunnel) - }) - it('fails only the pending stream when the destination never connects', async () => { const tunnel = createConformanceTunnel() const healthy = await openStream(tunnel, 'healthy.internal') @@ -120,20 +83,6 @@ describe('browser network tunnel conformance', () => { await teardown(tunnel) }) - it('reports the session connect timeout as a stream-scoped error', async () => { - // Use raw frames because the client's earlier timeout would mask the session timeout. - const peer = createRawFramePeerSession() - peer.session.handleBinary(openFrame(1, 'silent.internal', 443)) - - vi.advanceTimersByTime(BROWSER_NETWORK_TUNNEL_CONNECT_TIMEOUT_MS) - - expect(peer.errorsFor(1)).toEqual(['destination_connect_timeout']) - expect(peer.onClose).not.toHaveBeenCalled() - expect(vi.getTimerCount()).toBe(0) - peer.session.close() - expect(peer.aggregate.outstanding).toBe(0) - }) - it('fails only the erroring stream when the destination breaks mid-transfer', async () => { const tunnel = createConformanceTunnel() const healthy = await openStream(tunnel, 'healthy.internal') @@ -249,24 +198,6 @@ describe('browser network tunnel conformance', () => { await teardown(tunnel) }) - it('answers a heartbeat from either peer without disturbing streams', async () => { - const tunnel = createConformanceTunnel() - const healthy = await openStream(tunnel, 'healthy.internal') - const nonce = new Uint8Array([4, 2]) - - tunnel.client.handleBinary(pingFrame(nonce)) - tunnel.session.handleBinary(pingFrame(nonce)) - await settle() - - expect(heartbeatFrames(tunnel, 'clientToSession')).toEqual([['Pong', nonce]]) - expect(heartbeatFrames(tunnel, 'sessionToClient')).toEqual([['Pong', nonce]]) - expect(tunnel.closures).toEqual([]) - expect(tunnel.onSessionClose).not.toHaveBeenCalled() - await expectStreamStillCarriesBytes(healthy) - - await teardown(tunnel) - }) - it('leaves no timer or budget claim behind when both peers tear down mid-transfer', async () => { const tunnel = createConformanceTunnel() const idle = await openStream(tunnel, 'idle.internal') @@ -442,37 +373,6 @@ function createConformanceTunnel(): ConformanceTunnel { return tunnel } -function createRawFramePeerSession() { - const aggregate = createAggregateBudgetProbe() - const frames: RecordedFrame[] = [] - const onClose = vi.fn<() => void>() - const session = new BrowserNetworkTunnelSession({ - tunnelGeneration: TUNNEL_GENERATION, - connect: () => new FakeDestination(), - sendBinary: (bytes) => { - const frame = decodeBrowserNetworkTunnelFrame(bytes) - if (frame) { - frames.push({ direction: 'sessionToClient', ...frame }) - } - return true - }, - onClose, - claimAggregateRetainedBytes: aggregate.claim - }) - return { - session, - onClose, - aggregate, - errorsFor: (streamId: number) => - frames - .filter( - (frame) => - frame.streamId === streamId && frame.opcode === BrowserNetworkTunnelOpcode.Error - ) - .map((frame) => new TextDecoder().decode(frame.payload)) - } -} - function record(tunnel: ConformanceTunnel, direction: FrameDirection, bytes: Uint8Array): void { const frame = decodeBrowserNetworkTunnelFrame(bytes) if (!frame) { @@ -593,44 +493,6 @@ function windowUpdatesFor( .map((frame) => decodeBrowserNetworkTunnelWindowUpdate(frame.payload)!) } -function heartbeatFrames( - tunnel: ConformanceTunnel, - direction: FrameDirection -): [string, Uint8Array][] { - return tunnel.frames - .filter( - (frame) => - frame.direction === direction && - (frame.opcode === BrowserNetworkTunnelOpcode.Ping || - frame.opcode === BrowserNetworkTunnelOpcode.Pong) - ) - .map((frame) => [BrowserNetworkTunnelOpcode[frame.opcode]!, new Uint8Array(frame.payload)]) -} - -function pingFrame(payload: Uint8Array): Uint8Array { - return encodeBrowserNetworkTunnelFrame({ - opcode: BrowserNetworkTunnelOpcode.Ping, - tunnelGeneration: TUNNEL_GENERATION, - streamId: 0, - payload - }) -} - -function openFrame(streamId: number, host: string, port: number): Uint8Array { - const name = new TextEncoder().encode(host) - const payload = new Uint8Array(4 + name.byteLength) - const view = new DataView(payload.buffer) - view.setUint16(0, port, false) - view.setUint16(2, name.byteLength, false) - payload.set(name, 4) - return encodeBrowserNetworkTunnelFrame({ - opcode: BrowserNetworkTunnelOpcode.Open, - tunnelGeneration: TUNNEL_GENERATION, - streamId, - payload - }) -} - function concatBytes(chunks: readonly Uint8Array[]): Uint8Array { return new Uint8Array(Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)))) } diff --git a/src/main/browser/network/tunnels/browser-network-tunnel-proxy.test.ts b/src/main/browser/network/tunnels/browser-network-tunnel-proxy.test.ts index fbcd84266b7..6f84393b920 100644 --- a/src/main/browser/network/tunnels/browser-network-tunnel-proxy.test.ts +++ b/src/main/browser/network/tunnels/browser-network-tunnel-proxy.test.ts @@ -6,10 +6,6 @@ import { resolveNativeBrowserNetworkExecutionRoute } from '../execution-routes/browser-network-execution-route' import { setNetworkProxySettingsSource } from '../execution-routes/browser-network-proxy-settings-source' -import { - matchesProxyBypassRules, - selectBrowserNetworkTunnelProxy -} from './browser-network-tunnel-proxy-selection' import type { BrowserNetworkTunnelSocket } from './browser-network-tunnel-stream-state' const closers: (() => Promise)[] = [] @@ -101,18 +97,6 @@ describe('native browser tunnel upstream proxy (#29154)', () => { ]) }) - it('sends a credential that is not valid percent-encoding as written', async () => { - const proxy = await startConnectProxy(await startEchoServer()) - const socket = connectNativeBrowserNetworkTarget( - { host: 'site.invalid', port: 443 }, - { HTTPS_PROXY: `http://user:p%ss@127.0.0.1:${proxy.port}` } - ) - await expect(roundTrip(socket, 'd')).resolves.toBe('echo:d') - expect(proxy.requests[0]?.authorization).toBe( - `Basic ${Buffer.from('user:p%ss').toString('base64')}` - ) - }) - it("prefers Orca's proxy setting and its bypass list over the environment", async () => { const proxy = await startConnectProxy(await startEchoServer()) setNetworkProxySettingsSource(() => ({ @@ -127,23 +111,6 @@ describe('native browser tunnel upstream proxy (#29154)', () => { expect(proxy.requests.map((request) => request.authority)).toEqual(['site.invalid:80']) }) - it('dials NO_PROXY and loopback destinations directly', async () => { - const echoPort = await startEchoServer() - const proxy = await startConnectProxy(echoPort) - const env = { HTTPS_PROXY: `http://127.0.0.1:${proxy.port}`, NO_PROXY: 'example.test' } - const socket = connectNativeBrowserNetworkTarget({ host: '127.0.0.1', port: echoPort }, env) - await expect(roundTrip(socket, 'b')).resolves.toBe('echo:b') - expect(proxy.requests).toEqual([]) - expect( - selectBrowserNetworkTunnelProxy({ host: 'a.example.test', port: 443 }, null, env) - ).toEqual({ kind: 'direct' }) - for (const host of ['localhost.', 'app.localhost.', '::1', '0:0:0:0:0:0:0:1', '127.1.2.3']) { - expect(selectBrowserNetworkTunnelProxy({ host, port: 3000 }, null, env)).toEqual({ - kind: 'direct' - }) - } - }) - it('reports a proxy refusal as a connect failure instead of falling back to direct', async () => { const proxy = await startConnectProxy(await startEchoServer(), { reject: 403 }) const socket = connectNativeBrowserNetworkTarget( @@ -152,69 +119,4 @@ describe('native browser tunnel upstream proxy (#29154)', () => { ) await expect(roundTrip(socket, 'c')).rejects.toThrow('http_proxy_connect_rejected:403') }) - - it('refuses a proxy kind it cannot speak rather than bypassing it', () => { - expect(() => - connectNativeBrowserNetworkTarget( - { host: 'site.invalid', port: 443 }, - { ALL_PROXY: 'socks4://127.0.0.1:1080' } - ) - ).toThrow('browser_tunnel_proxy_unsupported') - expect( - selectBrowserNetworkTunnelProxy({ host: 'site.invalid', port: 443 }, null, { - ALL_PROXY: 'socks5://127.0.0.1:1080' - }) - ).toMatchObject({ kind: 'socks5' }) - }) - - it('dials directly with no proxy configured', () => { - expect(selectBrowserNetworkTunnelProxy({ host: 'site.invalid', port: 443 }, null, {})).toEqual({ - kind: 'direct' - }) - }) -}) - -describe('proxy bypass rules', () => { - it.each([ - ['*', 'any.host', 443, true, true], - ['', 'intranet', 80, true, true], - ['', 'intranet.corp', 80, false, false], - ['.corp.test', 'a.corp.test', 443, true, true], - ['.corp.test', 'corp.test', 443, true, false], - ['*.corp.test', 'corp.test', 443, true, false], - ['*corp.test', 'corp.test', 443, true, true], - ['corp.test', 'deep.a.corp.test', 443, true, false], - ['corp.test', 'corp.test', 443, true, true], - ['corp.test', 'notcorp.test', 443, false, false], - ['corp.test:8443', 'corp.test', 443, false, false], - ['corp.test:8443', 'corp.test', 8443, true, true], - ['10.0.0.0/8', '10.20.30.40', 443, true, true], - ['10.0.0.0/8', '11.0.0.1', 443, false, false], - ['[fd00::1]', 'fd00::1', 443, true, true], - ['[fd00::1]', 'fd00:0:0:0:0:0:0:1', 443, true, true], - ['fd00:0::1', '[FD00::1]', 443, true, true], - ['https://corp.test', 'corp.test', 443, true, true], - ['https://corp.test', 'corp.test', 80, false, false], - ['ftp://corp.test', 'corp.test', 21, false, false], - ['fc00::/7', 'fd00::123', 443, true, true], - ['fc00::/7', '2001:db8::1', 443, false, false], - ['192.168.*', '192.168.4.20', 443, true, true], - ['api.*.corp.test', 'api.eu.corp.test', 443, true, true], - ['api.*.corp.test', 'web.eu.corp.test', 443, false, false], - ['corp.test', 'corp.test.', 443, true, true], - ['', '169.254.169.254', 80, true, true], - ['<-loopback>', 'localhost', 3000, false, false], - ['<-loopback>;', 'localhost', 3000, true, true], - [';<-loopback>', 'localhost', 3000, false, false], - [';<-loopback>', 'intranet', 80, true, true] - ])('%s against %s:%d (NO_PROXY %s, Chromium %s)', (rule, host, port, noProxy, chromium) => { - expect(matchesProxyBypassRules({ host, port }, rule, 'no-proxy')).toBe(noProxy) - expect(matchesProxyBypassRules({ host, port }, rule, 'chromium')).toBe(chromium) - }) - - it('proxies loopback destinations when the bypass list subtracts them', () => { - const settings = { httpProxyUrl: 'http://proxy.test:8080', httpProxyBypassRules: '<-loopback>' } - const target = { host: '127.0.0.1', port: 3000 } - expect(selectBrowserNetworkTunnelProxy(target, settings, {})).toMatchObject({ kind: 'http' }) - }) }) diff --git a/src/main/browser/network/tunnels/browser-network-tunnel-session.test.ts b/src/main/browser/network/tunnels/browser-network-tunnel-session.test.ts index 533582631d2..b42659442a1 100644 --- a/src/main/browser/network/tunnels/browser-network-tunnel-session.test.ts +++ b/src/main/browser/network/tunnels/browser-network-tunnel-session.test.ts @@ -9,10 +9,7 @@ import { encodeBrowserNetworkTunnelOpen, encodeBrowserNetworkTunnelWindowUpdate } from '../../../../shared/browser-network-tunnel-protocol' -import { - BROWSER_NETWORK_TUNNEL_INITIAL_WINDOW_BYTES, - type BrowserNetworkTunnelSocket -} from './browser-network-tunnel-stream-state' +import type { BrowserNetworkTunnelSocket } from './browser-network-tunnel-stream-state' import { BrowserNetworkTunnelSession } from './browser-network-tunnel-session' class FakeSocket extends EventEmitter implements BrowserNetworkTunnelSocket { @@ -65,39 +62,6 @@ function frame( } describe('BrowserNetworkTunnelSession', () => { - it('opens the exact remote DNS target and grants bounded receive credit', () => { - const socket = new FakeSocket() - const connect = vi.fn(() => socket) - const sent: Uint8Array[] = [] - const session = new BrowserNetworkTunnelSession({ - tunnelGeneration: 7, - connect, - sendBinary: (bytes) => { - sent.push(bytes) - return true - } - }) - - session.handleBinary( - frame( - BrowserNetworkTunnelOpcode.Open, - encodeBrowserNetworkTunnelOpen({ host: 'split-horizon.internal', port: 443 }) - ) - ) - socket.emit('connect') - - expect(connect).toHaveBeenCalledWith({ host: 'split-horizon.internal', port: 443 }) - expect(socket.paused).toBe(true) - expect(sent.map(decodeBrowserNetworkTunnelFrame)).toEqual([ - expect.objectContaining({ opcode: BrowserNetworkTunnelOpcode.Opened, streamId: 3 }), - expect.objectContaining({ opcode: BrowserNetworkTunnelOpcode.WindowUpdate, streamId: 3 }) - ]) - const credit = decodeBrowserNetworkTunnelFrame(sent[1]!) - expect(credit && decodeBrowserNetworkTunnelWindowUpdate(credit.payload)).toBe( - BROWSER_NETWORK_TUNNEL_INITIAL_WINDOW_BYTES - ) - }) - it('replenishes client credit only after the destination write settles', () => { const socket = new FakeSocket() const sent: Uint8Array[] = [] @@ -129,149 +93,6 @@ describe('BrowserNetworkTunnelSession', () => { expect(update && decodeBrowserNetworkTunnelWindowUpdate(update.payload)).toBe(3) }) - it('does not read destination bytes before the client grants credit', () => { - const socket = new FakeSocket() - const sent: Uint8Array[] = [] - const session = new BrowserNetworkTunnelSession({ - tunnelGeneration: 7, - connect: () => socket, - sendBinary: (bytes) => { - sent.push(bytes) - return true - } - }) - session.handleBinary( - frame( - BrowserNetworkTunnelOpcode.Open, - encodeBrowserNetworkTunnelOpen({ host: 'localhost', port: 8080 }) - ) - ) - socket.emit('connect') - sent.length = 0 - - session.handleBinary( - frame(BrowserNetworkTunnelOpcode.WindowUpdate, encodeBrowserNetworkTunnelWindowUpdate(3)) - ) - socket.emit('data', new Uint8Array([4, 5, 6, 7])) - - const first = decodeBrowserNetworkTunnelFrame(sent[0]!) - expect(first?.opcode).toBe(BrowserNetworkTunnelOpcode.Data) - expect(first?.payload).toEqual(new Uint8Array([4, 5, 6])) - expect(socket.paused).toBe(true) - - session.handleBinary( - frame(BrowserNetworkTunnelOpcode.WindowUpdate, encodeBrowserNetworkTunnelWindowUpdate(1)) - ) - const second = decodeBrowserNetworkTunnelFrame(sent[1]!) - expect(second?.payload).toEqual(new Uint8Array([7])) - }) - - it('fences stale generations and closes every stream on disposal', () => { - const socket = new FakeSocket() - const connect = vi.fn(() => socket) - const sent: Uint8Array[] = [] - const session = new BrowserNetworkTunnelSession({ - tunnelGeneration: 7, - connect, - sendBinary: (bytes) => { - sent.push(bytes) - return true - } - }) - - session.handleBinary( - frame( - BrowserNetworkTunnelOpcode.Open, - encodeBrowserNetworkTunnelOpen({ host: 'stale.internal', port: 80 }), - 3, - 6 - ) - ) - expect(connect).not.toHaveBeenCalled() - - session.handleBinary( - frame( - BrowserNetworkTunnelOpcode.Open, - encodeBrowserNetworkTunnelOpen({ host: 'current.internal', port: 80 }) - ) - ) - session.close() - - expect(socket.destroyed).toBe(true) - }) - - it('rejects stream reuse without replacing the original destination', () => { - const firstSocket = new FakeSocket() - const secondSocket = new FakeSocket() - const connect = vi.fn().mockReturnValueOnce(firstSocket).mockReturnValueOnce(secondSocket) - const sent: Uint8Array[] = [] - const session = new BrowserNetworkTunnelSession({ - tunnelGeneration: 7, - connect, - sendBinary: (bytes) => { - sent.push(bytes) - return true - } - }) - const open = (host: string): void => - session.handleBinary( - frame(BrowserNetworkTunnelOpcode.Open, encodeBrowserNetworkTunnelOpen({ host, port: 80 })) - ) - - open('first.internal') - open('second.internal') - - expect(connect).toHaveBeenCalledTimes(1) - expect(firstSocket.destroyed).toBe(true) - expect(secondSocket.destroyed).toBe(false) - const error = decodeBrowserNetworkTunnelFrame(sent.at(-1)!) - expect(error?.opcode).toBe(BrowserNetworkTunnelOpcode.Error) - expect(error ? new TextDecoder().decode(error.payload) : '').toBe('stream_id_reused') - }) - - it('rejects current-generation frames for never-reserved and reused stream IDs', () => { - const socket = new FakeSocket() - const sent: Uint8Array[] = [] - const connect = vi.fn(() => socket) - const session = new BrowserNetworkTunnelSession({ - tunnelGeneration: 7, - connect, - sendBinary: (bytes) => { - sent.push(bytes) - return true - } - }) - const open = frame( - BrowserNetworkTunnelOpcode.Open, - encodeBrowserNetworkTunnelOpen({ host: 'localhost', port: 80 }) - ) - - session.handleBinary(open) - session.handleBinary(frame(BrowserNetworkTunnelOpcode.Close)) - session.handleBinary(open) - - expect(connect).toHaveBeenCalledTimes(1) - expect(socket.destroyed).toBe(true) - expect(decodeBrowserNetworkTunnelFrame(sent.at(-1)!)?.opcode).toBe( - BrowserNetworkTunnelOpcode.Error - ) - - const otherSocket = new FakeSocket() - const unknownSession = new BrowserNetworkTunnelSession({ - tunnelGeneration: 7, - connect: () => otherSocket, - sendBinary: () => true - }) - unknownSession.handleBinary( - frame( - BrowserNetworkTunnelOpcode.Open, - encodeBrowserNetworkTunnelOpen({ host: 'localhost', port: 80 }) - ) - ) - unknownSession.handleBinary(frame(BrowserNetworkTunnelOpcode.Data, new Uint8Array([1]), 4)) - expect(otherSocket.destroyed).toBe(true) - }) - it('keeps concurrent streams alive when late data arrives for a retired stream', () => { const firstSocket = new FakeSocket() const secondSocket = new FakeSocket() @@ -304,88 +125,6 @@ describe('BrowserNetworkTunnelSession', () => { session.close() }) - it('accepts unused out-of-order stream IDs without permitting reuse', () => { - const firstSocket = new FakeSocket() - const secondSocket = new FakeSocket() - const connect = vi.fn().mockReturnValueOnce(firstSocket).mockReturnValueOnce(secondSocket) - const session = new BrowserNetworkTunnelSession({ - tunnelGeneration: 7, - connect, - sendBinary: () => true - }) - const open = (streamId: number): void => - session.handleBinary( - frame( - BrowserNetworkTunnelOpcode.Open, - encodeBrowserNetworkTunnelOpen({ host: 'localhost', port: 80 }), - streamId - ) - ) - - open(5) - session.handleBinary(frame(BrowserNetworkTunnelOpcode.Close, new Uint8Array(), 5)) - open(3) - - expect(connect).toHaveBeenCalledTimes(2) - expect(secondSocket.destroyed).toBe(false) - }) - - it('fences invalid half-close transitions and emits destination EOF once', () => { - const socket = new FakeSocket() - const sent: Uint8Array[] = [] - const session = new BrowserNetworkTunnelSession({ - tunnelGeneration: 7, - connect: () => socket, - sendBinary: (bytes) => { - sent.push(bytes) - return true - } - }) - session.handleBinary( - frame( - BrowserNetworkTunnelOpcode.Open, - encodeBrowserNetworkTunnelOpen({ host: 'localhost', port: 80 }) - ) - ) - socket.emit('connect') - session.handleBinary(frame(BrowserNetworkTunnelOpcode.HalfClose)) - session.handleBinary(frame(BrowserNetworkTunnelOpcode.Data, new Uint8Array([1]))) - expect(socket.ended).toBe(true) - expect(socket.destroyed).toBe(true) - - const responseSocket = new FakeSocket() - const responseFrames: Uint8Array[] = [] - const responseSession = new BrowserNetworkTunnelSession({ - tunnelGeneration: 7, - connect: () => responseSocket, - sendBinary: (bytes) => { - responseFrames.push(bytes) - return true - } - }) - responseSession.handleBinary( - frame( - BrowserNetworkTunnelOpcode.Open, - encodeBrowserNetworkTunnelOpen({ host: 'localhost', port: 80 }) - ) - ) - responseSocket.emit('connect') - responseFrames.length = 0 - responseSession.handleBinary( - frame(BrowserNetworkTunnelOpcode.WindowUpdate, encodeBrowserNetworkTunnelWindowUpdate(1)) - ) - responseSocket.emit('data', new Uint8Array([1])) - responseSocket.emit('end') - responseSession.handleBinary( - frame(BrowserNetworkTunnelOpcode.WindowUpdate, encodeBrowserNetworkTunnelWindowUpdate(1)) - ) - expect( - responseFrames - .map(decodeBrowserNetworkTunnelFrame) - .filter((item) => item?.opcode === BrowserNetworkTunnelOpcode.HalfClose) - ).toHaveLength(1) - }) - it('flushes credited destination bytes before close', () => { const socket = new FakeSocket() const sent: Uint8Array[] = [] @@ -425,120 +164,6 @@ describe('BrowserNetworkTunnelSession', () => { expect(socket.destroyed).toBe(true) }) - it('fails closed on pre-connect credit and synchronous transport failure', () => { - const socket = new FakeSocket() - const session = new BrowserNetworkTunnelSession({ - tunnelGeneration: 7, - connect: () => socket, - sendBinary: () => true - }) - session.handleBinary( - frame( - BrowserNetworkTunnelOpcode.Open, - encodeBrowserNetworkTunnelOpen({ host: 'localhost', port: 80 }) - ) - ) - session.handleBinary( - frame(BrowserNetworkTunnelOpcode.WindowUpdate, encodeBrowserNetworkTunnelWindowUpdate(1)) - ) - expect(socket.destroyed).toBe(true) - - const throwingSocket = new FakeSocket() - const throwingSession = new BrowserNetworkTunnelSession({ - tunnelGeneration: 7, - connect: () => throwingSocket, - sendBinary: () => { - throw new Error('transport closed') - } - }) - throwingSession.handleBinary( - frame( - BrowserNetworkTunnelOpcode.Open, - encodeBrowserNetworkTunnelOpen({ host: 'localhost', port: 80 }) - ) - ) - expect(() => throwingSocket.emit('connect')).not.toThrow() - expect(throwingSocket.destroyed).toBe(true) - }) - - it('closes a stream that exceeds its receive window', () => { - const socket = new FakeSocket() - const sent: Uint8Array[] = [] - const session = new BrowserNetworkTunnelSession({ - tunnelGeneration: 7, - connect: () => socket, - sendBinary: (bytes) => { - sent.push(bytes) - return true - } - }) - session.handleBinary( - frame( - BrowserNetworkTunnelOpcode.Open, - encodeBrowserNetworkTunnelOpen({ host: 'localhost', port: 80 }) - ) - ) - socket.emit('connect') - sent.length = 0 - const chunk = new Uint8Array(64 * 1024) - for (let index = 0; index < 4; index += 1) { - session.handleBinary(frame(BrowserNetworkTunnelOpcode.Data, chunk)) - } - session.handleBinary(frame(BrowserNetworkTunnelOpcode.Data, new Uint8Array([1]))) - - expect(socket.destroyed).toBe(true) - expect(decodeBrowserNetworkTunnelFrame(sent.at(-1)!)?.opcode).toBe( - BrowserNetworkTunnelOpcode.Error - ) - }) - - it('bounds the destination open rate with an injectable monotonic clock', () => { - let now = 1_000 - const sockets: FakeSocket[] = [] - const connect = vi.fn(() => { - const socket = new FakeSocket() - sockets.push(socket) - return socket - }) - const sent: Uint8Array[] = [] - const session = new BrowserNetworkTunnelSession({ - tunnelGeneration: 7, - connect, - sendBinary: (bytes) => { - sent.push(bytes) - return true - }, - now: () => now - }) - const openAndClose = (streamId: number): void => { - const socketCount = sockets.length - session.handleBinary( - frame( - BrowserNetworkTunnelOpcode.Open, - encodeBrowserNetworkTunnelOpen({ host: 'burst.internal', port: 443 }), - streamId - ) - ) - if (sockets.length > socketCount) { - sockets.at(-1)!.emit('connect') - session.handleBinary(frame(BrowserNetworkTunnelOpcode.Close, new Uint8Array(), streamId)) - } - } - - for (let streamId = 1; streamId <= 1024; streamId += 1) { - openAndClose(streamId) - } - openAndClose(1025) - expect(connect).toHaveBeenCalledTimes(1024) - expect(new TextDecoder().decode(decodeBrowserNetworkTunnelFrame(sent.at(-1)!)?.payload)).toBe( - 'open_rate_exceeded' - ) - - now += 10_001 - openAndClose(1026) - expect(connect).toHaveBeenCalledTimes(1025) - }) - it('caps aggregate retained destination bytes and releases exact stream accounting', () => { const sockets: FakeSocket[] = [] const session = new BrowserNetworkTunnelSession({ @@ -620,29 +245,6 @@ describe('BrowserNetworkTunnelSession', () => { expect(sockets.slice(1, 33).some((socket) => socket.destroyed)).toBe(false) }) - it('bounds unsettled destination write chunks independently of byte credit', () => { - const socket = new FakeSocket() - const session = new BrowserNetworkTunnelSession({ - tunnelGeneration: 7, - connect: () => socket, - sendBinary: () => true - }) - session.handleBinary( - frame( - BrowserNetworkTunnelOpcode.Open, - encodeBrowserNetworkTunnelOpen({ host: 'tiny-writes.internal', port: 443 }) - ) - ) - socket.emit('connect') - - for (let index = 0; index < 257; index += 1) { - session.handleBinary(frame(BrowserNetworkTunnelOpcode.Data, new Uint8Array([index & 0xff]))) - } - - expect(socket.writes).toHaveLength(256) - expect(socket.destroyed).toBe(true) - }) - it('releases queued bytes once when an accepted send closes reentrantly', () => { const socket = new FakeSocket() let closeOnData = false diff --git a/src/main/browser/routing/guest-authority/browser-route-guest-popups.test.ts b/src/main/browser/routing/guest-authority/browser-route-guest-popups.test.ts index 456ff47f1b0..d18e0e84c19 100644 --- a/src/main/browser/routing/guest-authority/browser-route-guest-popups.test.ts +++ b/src/main/browser/routing/guest-authority/browser-route-guest-popups.test.ts @@ -12,10 +12,7 @@ import { resolveBrowserRouteGuestPopupOpener } from './browser-route-guest-popup-ownership' import { BrowserRouteWebContentsRegistry } from './browser-route-webcontents-registry' - -const GESTURE_CLICK_AT = 1_700_000_000_000 const partition = `persist:orca-browser-v1-${'a'.repeat(64)}` -const otherPartition = `persist:orca-browser-v1-${'b'.repeat(64)}` const page = { partition, browserPageId: 'page-a', @@ -30,174 +27,7 @@ afterEach(() => { }) describe('route guest OAuth popups', () => { - it('denies window.open without a recognized user gesture', () => { - const { guest } = attachRegisteredGuest() - - expect(guest.openWindow('https://accounts.example.com/oauth')).toEqual({ action: 'deny' }) - expect(popupMocks.openPopupWithOriginBar).not.toHaveBeenCalled() - }) - - it('allows one popup per gesture, bound to the opener partition, and never replays it', () => { - const { guest, popups } = attachRegisteredGuest() - guest.emitInput('mouseDown') - - const allowed = guest.openWindow('https://accounts.example.com/oauth') - - expect(allowed.action).toBe('allow') - expect(allowed.overrideBrowserWindowOptions?.webPreferences?.partition).toBe(partition) - expect(allowed.overrideBrowserWindowOptions?.webPreferences?.sandbox).toBe(true) - const opened = allowed.createWindow?.(popupOptions(createPopupContents())) - expect(opened).not.toBeUndefined() - expect(popups()).toHaveLength(1) - expect(popups()[0]?.loaded).toBe(true) - - expect(guest.openWindow('https://accounts.example.com/oauth')).toEqual({ action: 'deny' }) - }) - // Pin elapsed times independently of the gesture-window constant to catch widening. - it('still honors a gesture 1s after the click', () => { - vi.useFakeTimers({ toFake: ['Date'] }) - vi.setSystemTime(GESTURE_CLICK_AT) - const { guest } = attachRegisteredGuest() - guest.emitInput('mouseDown') - - vi.setSystemTime(GESTURE_CLICK_AT + 1_000) - - expect(guest.openWindow('https://accounts.example.com/oauth').action).toBe('allow') - }) - - it('denies a popup more than 1s after the click that would have authorized it', () => { - vi.useFakeTimers({ toFake: ['Date'] }) - vi.setSystemTime(GESTURE_CLICK_AT) - const { guest } = attachRegisteredGuest() - guest.emitInput('mouseDown') - - vi.setSystemTime(GESTURE_CLICK_AT + 1_001) - - expect(guest.openWindow('https://accounts.example.com/oauth')).toEqual({ action: 'deny' }) - expect(popupMocks.openPopupWithOriginBar).not.toHaveBeenCalled() - }) - - it('rejects clock rollback instead of treating a future input as a current gesture', () => { - vi.useFakeTimers({ toFake: ['Date'] }) - vi.setSystemTime(GESTURE_CLICK_AT) - const { guest } = attachRegisteredGuest() - guest.emitInput('mouseDown') - vi.setSystemTime(GESTURE_CLICK_AT - 1) - expect(guest.openWindow('about:blank')).toEqual({ action: 'deny' }) - }) - - it('stops opening popups at the fourth one even while gestures keep arriving', () => { - const { guest, popups } = attachRegisteredGuest() - - for (let index = 0; index < 4; index += 1) { - guest.emitInput('mouseDown') - const allowed = guest.openWindow(`https://accounts.example.com/oauth/${index}`) - expect(allowed.action).toBe('allow') - allowed.createWindow?.(popupOptions(createPopupContents())) - } - expect(popups()).toHaveLength(4) - - guest.emitInput('mouseDown') - - expect(guest.openWindow('https://accounts.example.com/oauth/overflow')).toEqual({ - action: 'deny' - }) - expect(popups()).toHaveLength(4) - }) - - it('tells the owning page every popup it refused, and stays quiet once the page is gone', () => { - const { guest, blockedPopups } = attachRegisteredGuest() - - guest.openWindow('https://accounts.example.com/oauth') - guest.emitInput('mouseDown') - guest.openWindow('file:///etc/hosts') - - expect(blockedPopups()).toEqual([ - { openerWebContentsId: page.webContentsId, url: 'https://accounts.example.com/oauth' }, - { openerWebContentsId: page.webContentsId, url: 'file:///etc/hosts' } - ]) - - guest.destroy() - guest.openWindow('https://accounts.example.com/oauth') - - expect(blockedPopups()).toHaveLength(2) - }) - - it('applies the WebRTC policy before the popup can load and fails closed when it cannot', () => { - const { guest, popups } = attachRegisteredGuest() - guest.emitInput('mouseDown') - const allowed = guest.openWindow('https://accounts.example.com/oauth') - const contents = createPopupContents() - - allowed.createWindow?.(popupOptions(contents)) - - expect(contents.setWebRTCIPHandlingPolicy).toHaveBeenCalledWith('disable_non_proxied_udp') - expect(popups()[0]?.order).toEqual(['webrtc-policy', 'load']) - - guest.emitInput('mouseDown') - const second = guest.openWindow('https://accounts.example.com/oauth') - second.createWindow?.(popupOptions(createPopupContents({ webRtcPolicyThrows: true }))) - - expect(popups()[1]?.loaded).toBe(false) - expect(popups()[1]?.closed).toBe(true) - }) - - it('fails closed when the popup contents land outside the opener partition', () => { - const { guest, popups } = attachRegisteredGuest() - guest.emitInput('mouseDown') - const allowed = guest.openWindow('https://accounts.example.com/oauth') - - allowed.createWindow?.(popupOptions(createPopupContents({ partition: otherPartition }))) - - expect(popups()[0]?.loaded).toBe(false) - expect(popups()[0]?.closed).toBe(true) - }) - - it('refuses non-web destinations even with a real gesture', () => { - const { guest } = attachRegisteredGuest() - - guest.emitInput('mouseDown') - expect(guest.openWindow('file:///etc/hosts')).toEqual({ action: 'deny' }) - for (const url of [ - 'about:blank?state=x', - 'about:srcdoc', - 'data:text/html,login', - 'javascript:alert(1)' - ]) { - guest.emitInput('mouseDown') - expect(guest.openWindow(url)).toEqual({ action: 'deny' }) - } - expect(popupMocks.openPopupWithOriginBar).not.toHaveBeenCalled() - }) - - it.each(['about:blank', 'about:blank#state'])( - 'admits a gesture-backed blank bootstrap: %s', - (url) => { - const { guest, popups } = attachRegisteredGuest() - expect(guest.openWindow(url)).toEqual({ action: 'deny' }) - guest.emitInput('mouseDown') - const allowed = guest.openWindow(url) - expect(allowed.action).toBe('allow') - allowed.createWindow?.(popupOptions(createPopupContents())) - expect(popups()[0]?.loaded).toBe(true) - expect(guest.openWindow(url)).toEqual({ action: 'deny' }) - } - ) - - it('does not treat the quarantine blank-page exemption as popup authority', () => { - const { registry, guest } = attachGuest() - guest.emitInput('mouseDown') - expect(guest.openWindow('about:blank#state')).toEqual({ action: 'deny' }) - registry.registerGuest(page) - guest.emitInput('mouseDown') - expect(guest.openWindow('about:blank')).toEqual({ action: 'deny' }) - registry.grantNavigation(page) - const claim = registry.claimGuestLifecycle(page)! - registry.revokeNavigation(claim) - guest.emitInput('mouseDown') - expect(guest.openWindow('about:blank')).toEqual({ action: 'deny' }) - }) it('spends a gesture on a denial and rechecks authority before adopting a child', () => { const { registry, guest, popups } = attachRegisteredGuest() @@ -212,21 +42,6 @@ describe('route guest OAuth popups', () => { expect(popups()[0]?.closed).toBe(true) }) - it('holds popups shut until the page is registered and navigation is granted', () => { - const { registry, guest } = attachGuest() - - guest.emitInput('mouseDown') - expect(guest.openWindow('https://accounts.example.com/oauth')).toEqual({ action: 'deny' }) - - registry.registerGuest(page) - guest.emitInput('mouseDown') - expect(guest.openWindow('https://accounts.example.com/oauth')).toEqual({ action: 'deny' }) - - registry.grantNavigation(page) - guest.emitInput('mouseDown') - expect(guest.openWindow('https://accounts.example.com/oauth').action).toBe('allow') - }) - it('closes live popups when the opener page is fenced and reopens them once it is regranted', () => { const { registry, guest, popups } = attachRegisteredGuest() guest.emitInput('mouseDown') @@ -246,43 +61,6 @@ describe('route guest OAuth popups', () => { expect(guest.openWindow('https://accounts.example.com/oauth').action).toBe('allow') }) - it('closes popups and stops accepting new ones when the opener guest is destroyed', () => { - const { guest, popups } = attachRegisteredGuest() - guest.emitInput('mouseDown') - guest - .openWindow('https://accounts.example.com/oauth') - .createWindow?.(popupOptions(createPopupContents())) - - guest.destroy() - - expect(popups()[0]?.closed).toBe(true) - guest.emitInput('mouseDown') - expect(guest.openWindow('https://accounts.example.com/oauth')).toEqual({ action: 'deny' }) - }) - - it('owns popups by their opener page and drops ownership when the popup is destroyed', () => { - const { guest } = attachRegisteredGuest() - guest.emitInput('mouseDown') - const contents = createPopupContents() - guest.openWindow('https://accounts.example.com/oauth').createWindow?.(popupOptions(contents)) - - expect(resolveBrowserRouteGuestPopupOpener(contents.id)).toBe(page.webContentsId) - - contents.emitDestroyed() - - expect(resolveBrowserRouteGuestPopupOpener(contents.id)).toBeNull() - }) - - it('claims no ownership for a popup that failed the partition fence', () => { - const { guest } = attachRegisteredGuest() - guest.emitInput('mouseDown') - const contents = createPopupContents({ partition: otherPartition }) - - guest.openWindow('https://accounts.example.com/oauth').createWindow?.(popupOptions(contents)) - - expect(resolveBrowserRouteGuestPopupOpener(contents.id)).toBeNull() - }) - it('drops popup ownership when the opener guest goes away', () => { const { guest } = attachRegisteredGuest() guest.emitInput('mouseDown') @@ -303,27 +81,6 @@ describe('route guest OAuth popups', () => { expect(resolveBrowserRouteGuestPopupOpener(descendant.id)).toBeNull() }) - it('gives popup descendants the same gesture-gated handler', () => { - const { guest } = attachRegisteredGuest() - guest.emitInput('mouseDown') - const contents = createPopupContents() - guest.openWindow('https://accounts.example.com/oauth').createWindow?.(popupOptions(contents)) - - expect(contents.openWindow('https://accounts.example.com/step-2')).toEqual({ action: 'deny' }) - contents.emitInput('mouseDown') - expect(contents.openWindow('https://accounts.example.com/step-2').action).toBe('allow') - }) - - it('lets a popup page leave when it asks to confirm', () => { - const { guest } = attachRegisteredGuest() - guest.emitInput('mouseDown') - const contents = createPopupContents() - guest.openWindow('https://accounts.example.com/oauth').createWindow?.(popupOptions(contents)) - - // preventDefault() permits navigation for will-prevent-unload. - expect(contents.navigate('https://accounts.example.com/next', 'will-prevent-unload')).toBe(true) - }) - it('guards blank descendants and redirects after the root loses authority', () => { const { registry, guest } = attachRegisteredGuest() guest.emitInput('mouseDown') @@ -339,39 +96,6 @@ describe('route guest OAuth popups', () => { contents.emitInput('mouseDown') expect(contents.openWindow('about:blank')).toEqual({ action: 'deny' }) }) - - it('loads a Cmd/Ctrl+Shift-clicked GET itself but never replays a POST that has no page', () => { - const { guest, blockedPopups } = attachRegisteredGuest() - popupMocks.openPopupWithOriginBar.mockImplementation(() => ({ - contentWebContents: createPopupContents(), - close: vi.fn(), - onClosed: vi.fn() - })) - guest.emitInput('mouseDown') - guest.openWindow('https://accounts.example.com/docs').createWindow?.({}) - expect(popupMocks.openPopupWithOriginBar).toHaveBeenLastCalledWith( - expect.objectContaining({ loadInitialUrl: true }), - 'https://accounts.example.com/docs', - expect.any(Function) - ) - expect(blockedPopups()).toEqual([]) - - guest.emitInput('mouseDown') - guest - .openWindow('https://accounts.example.com/submit', { - contentType: 'application/x-www-form-urlencoded', - data: [] - }) - .createWindow?.({}) - expect(popupMocks.openPopupWithOriginBar).toHaveBeenLastCalledWith( - expect.objectContaining({ loadInitialUrl: false }), - 'https://accounts.example.com/submit', - expect.any(Function) - ) - expect(blockedPopups()).toEqual([ - { openerWebContentsId: page.webContentsId, url: 'https://accounts.example.com/submit' } - ]) - }) }) function popupOptions( diff --git a/src/main/browser/routing/guest-authority/browser-route-webcontents-registry.test.ts b/src/main/browser/routing/guest-authority/browser-route-webcontents-registry.test.ts index 41a8d44a62d..56fd0c9c3ba 100644 --- a/src/main/browser/routing/guest-authority/browser-route-webcontents-registry.test.ts +++ b/src/main/browser/routing/guest-authority/browser-route-webcontents-registry.test.ts @@ -182,103 +182,6 @@ describe('BrowserRouteWebContentsRegistry', () => { ).resolves.toBe(true) }) - it('holds navigation and popups until exact registration and an explicit grant', () => { - const { registry, routeSession } = createHarness() - const guest = createGuest({ session: routeSession }) - - expect(registry.attachGuest(guest.guest)).toBe(true) - const beforeRegistration = navigationEvent() - guest.emit('will-navigate', beforeRegistration, 'https://example.com/') - expect(beforeRegistration.preventDefault).toHaveBeenCalledOnce() - expect(guest.openWindow()).toEqual({ action: 'deny' }) - - expect(registry.registerGuest({ ...page, rendererWebContentsId: 12 })).toBe(false) - expect(registry.grantNavigation(page)).toBe(false) - expect(registry.registerGuest(page)).toBe(true) - - const beforeGrant = navigationEvent() - guest.emit('will-navigate', beforeGrant, 'https://example.com/') - expect(beforeGrant.preventDefault).toHaveBeenCalledOnce() - - expect(registry.grantNavigation(page)).toBe(true) - const afterGrant = navigationEvent() - guest.emit('will-navigate', afterGrant, 'https://example.com/') - expect(afterGrant.preventDefault).not.toHaveBeenCalled() - guest.setUrl('https://example.com/') - const laterNavigation = navigationEvent() - guest.emit('will-navigate', laterNavigation, 'https://example.org/') - expect(laterNavigation.preventDefault).not.toHaveBeenCalled() - expect(guest.openWindow()).toEqual({ action: 'deny' }) - }) - - it('revokes only the exact guest lifecycle claim without destroying it', () => { - const { registry, routeSession } = createHarness() - const guest = createGuest({ session: routeSession }) - registry.attachGuest(guest.guest) - registry.registerGuest(page) - registry.grantNavigation(page) - const claim = requireLifecycleClaim(registry) - - expect( - registry.revokeNavigation({ - ...claim, - registration: { ...page, pageHostGeneration: page.pageHostGeneration + 1 } - }) - ).toBe(false) - const afterStaleFence = navigationEvent() - guest.emit('will-navigate', afterStaleFence, 'https://example.com/') - expect(afterStaleFence.preventDefault).not.toHaveBeenCalled() - - expect(registry.revokeNavigation(claim)).toBe(true) - const afterExactFence = navigationEvent() - guest.emit('will-navigate', afterExactFence, 'https://example.com/') - expect(afterExactFence.preventDefault).toHaveBeenCalledOnce() - expect(guest.guest.close).not.toHaveBeenCalled() - }) - - it('loads only a normalized web URL on the exact registered guest', async () => { - const { registry, routeSession } = createHarness() - const guest = createGuest({ session: routeSession }) - registry.attachGuest(guest.guest) - registry.registerGuest(page) - registry.grantNavigation(page) - const claim = requireLifecycleClaim(registry) - - await expect(registry.navigateGuest(claim, 'example.com/path')).resolves.toBe(true) - expect(guest.guest.loadURL).toHaveBeenCalledWith('https://example.com/path') - await expect( - registry.navigateGuest( - { ...claim, registration: { ...page, pageHostGeneration: 8 } }, - 'https://a.test' - ) - ).resolves.toBe(false) - await expect(registry.navigateGuest(claim, 'file:///etc/passwd')).resolves.toBe(false) - expect(guest.guest.loadURL).toHaveBeenCalledOnce() - }) - - it('retires an exact unregistered quarantined guest without touching a replacement', async () => { - const { registry, routeSession } = createHarness() - const guest = createGuest({ session: routeSession }) - registry.attachGuest(guest.guest) - - expect(registry.claimGuestLifecycle({ ...page, webContentsId: 42 })).toBeNull() - expect(guest.guest.close).not.toHaveBeenCalled() - await expect( - registry.beginGuestRetirement(requireLifecycleClaim(registry)) - ).resolves.toBeUndefined() - expect(guest.guest.close).toHaveBeenCalledOnce() - }) - - it('does not join a different generation already retiring on the same guest', () => { - const { registry, routeSession } = createHarness() - const guest = createGuest({ session: routeSession, closeDestroys: false }) - registry.attachGuest(guest.guest) - registry.registerGuest(page) - - expect(registry.beginGuestRetirement(requireLifecycleClaim(registry))).not.toBeNull() - expect(registry.claimGuestLifecycle({ ...page, pageHostGeneration: 8 })).toBeNull() - }) - it('settles exact guest retirement only after its destroyed acknowledgement', async () => { const { registry, routeSession } = createHarness() const guest = createGuest({ session: routeSession, closeDestroys: false }) @@ -295,36 +198,6 @@ describe('BrowserRouteWebContentsRegistry', () => { await expect(retired).resolves.toBeUndefined() }) - it('rejects a guest whose renderer does not own the prepared authority', () => { - const { registry, routeSession } = createHarness() - const guest = createGuest({ session: routeSession, rendererWebContentsId: 12 }) - const siblingClaim = { ...page, rendererWebContentsId: 12 } - - expect(registry.attachGuest(guest.guest)).toBe(true) - expect(registry.registerGuest(siblingClaim)).toBe(false) - expect(registry.grantNavigation(siblingClaim)).toBe(false) - }) - - it('blocks non-proxied WebRTC before admitting a route guest', () => { - const { registry, routeSession } = createHarness() - const guest = createGuest({ session: routeSession }) - - expect(registry.attachGuest(guest.guest)).toBe(true) - expect(guest.guest.setWebRTCIPHandlingPolicy).toHaveBeenCalledWith('disable_non_proxied_udp') - }) - - it('fails closed when WebRTC route policy cannot be applied', () => { - const { registry, routeSession } = createHarness() - const guest = createGuest({ - session: routeSession, - webRtcPolicyError: new Error('policy unavailable') - }) - - expect(registry.attachGuest(guest.guest)).toBe(false) - expect(guest.guest.close).toHaveBeenCalledOnce() - expect(registry.registerGuest(page)).toBe(false) - }) - it('keeps policy-rejected guests quarantined through delayed or failed close', () => { const { registry, routeSession } = createHarness() for (const rejected of [ @@ -347,68 +220,6 @@ describe('BrowserRouteWebContentsRegistry', () => { } }) - it('allows blank navigation while quarantined but blocks invalid schemes after a grant', () => { - const { registry, routeSession } = createHarness() - const guest = createGuest({ session: routeSession }) - registry.attachGuest(guest.guest) - - const blank = navigationEvent() - guest.emit('will-navigate', blank, 'about:blank') - expect(blank.preventDefault).not.toHaveBeenCalled() - - expect(registry.registerGuest(page)).toBe(true) - expect(registry.grantNavigation(page)).toBe(true) - const localFile = navigationEvent() - guest.emit('will-navigate', localFile, 'file:///etc/passwd') - expect(localFile.preventDefault).toHaveBeenCalledOnce() - }) - - it('destroys invalid route guests but ignores unrelated browser sessions', () => { - const { registry, routeSession } = createHarness() - const unrelated = createGuest({ - session: { marker: 'profile-session' } as unknown as Session - }) - expect(registry.attachGuest(unrelated.guest)).toBe(false) - expect(unrelated.guest.close).not.toHaveBeenCalled() - - for (const invalid of [ - createGuest({ session: routeSession, type: 'window' }), - createGuest({ session: routeSession, url: 'https://example.com/' }), - createGuest({ session: routeSession, rendererWebContentsId: 0 }) - ]) { - expect(registry.attachGuest(invalid.guest)).toBe(false) - expect(invalid.guest.close).toHaveBeenCalledOnce() - } - const alreadyDestroyed = createGuest({ session: routeSession, destroyed: true }) - expect(registry.attachGuest(alreadyDestroyed.guest)).toBe(false) - expect(alreadyDestroyed.guest.close).not.toHaveBeenCalled() - }) - - it('bounds unregistered route guests and destroys excess attachments', () => { - const { registry, routeSession } = createHarness({ maxGuests: 1 }) - const first = createGuest({ session: routeSession }) - const second = createGuest({ id: 42, session: routeSession }) - - expect(registry.attachGuest(first.guest)).toBe(true) - expect(registry.attachGuest(second.guest)).toBe(false) - expect(second.guest.close).toHaveBeenCalledOnce() - }) - - it('keeps delayed or failed admission closure navigation- and popup-denied', () => { - const { registry, routeSession } = createHarness({ maxGuests: 1 }) - registry.attachGuest(createGuest({ session: routeSession }).guest) - for (const rejected of [ - createGuest({ id: 42, session: routeSession, closeDestroys: false }), - createGuest({ id: 43, session: routeSession, closeError: new Error('close failed') }) - ]) { - expect(registry.attachGuest(rejected.guest)).toBe(false) - const navigation = navigationEvent() - rejected.emit('will-navigate', navigation, 'https://example.com/') - expect(navigation.preventDefault).toHaveBeenCalledOnce() - expect(rejected.openWindow()).toEqual({ action: 'deny' }) - } - }) - it('rejects page conflicts and stale destroyed callbacks cannot retire a replacement', () => { const { registry, replaceAuthority, routeSession } = createHarness() const first = createGuest({ session: routeSession }) @@ -429,43 +240,6 @@ describe('BrowserRouteWebContentsRegistry', () => { expect(first.guest.off).toHaveBeenCalledWith('destroyed', expect.any(Function)) }) - it('revokes navigation immediately when the logical page is no longer prepared', () => { - const { registry, routeSession, setPrepared } = createHarness() - const guest = createGuest({ session: routeSession }) - registry.attachGuest(guest.guest) - registry.registerGuest(page) - registry.grantNavigation(page) - setPrepared(false) - - const afterRelease = navigationEvent() - guest.emit('will-redirect', afterRelease, 'https://example.com/', false, true) - expect(afterRelease.preventDefault).toHaveBeenCalledOnce() - }) - - it('quarantines a late attachment after its logical page authority is released', () => { - const { registry, routeSession, setPrepared } = createHarness() - setPrepared(false) - const guest = createGuest({ session: routeSession }) - - expect(registry.attachGuest(guest.guest)).toBe(true) - expect(registry.registerGuest(page)).toBe(false) - const navigation = navigationEvent() - guest.emit('will-navigate', navigation, 'https://example.com/') - expect(navigation.preventDefault).toHaveBeenCalledOnce() - expect(guest.openWindow()).toEqual({ action: 'deny' }) - }) - - it('does not retain mutable registration input as page authority', () => { - const { registry, routeSession } = createHarness() - const guest = createGuest({ session: routeSession }) - const registration = { ...page } - registry.attachGuest(guest.guest) - - expect(registry.registerGuest(registration)).toBe(true) - registration.browserPageId = 'mutated-page' - expect(registry.grantNavigation(page)).toBe(true) - }) - it('does not revive a stale guest when the same logical page tuple is prepared again', () => { const { registry, replaceAuthority, routeSession } = createHarness() const stale = createGuest({ session: routeSession }) @@ -526,110 +300,6 @@ describe('BrowserRouteWebContentsRegistry', () => { expect(duplicateRetired).not.toHaveBeenCalled() }) - it('ignores retirement for a different opaque authority', () => { - const { registry, routeSession } = createHarness() - const guest = createGuest({ session: routeSession }) - registry.attachGuest(guest.guest) - registry.registerGuest(page) - registry.grantNavigation(page) - - expect( - registry.retirePageAuthority({ - partition, - browserPageId: page.browserPageId, - pageHostGeneration: page.pageHostGeneration, - rendererWebContentsId: page.rendererWebContentsId, - pageAuthority: Symbol('wrong-authority'), - onRetired: vi.fn() - }) - ).toBe(true) - expect(guest.guest.close).not.toHaveBeenCalled() - const navigation = navigationEvent() - guest.emit('will-navigate', navigation, 'https://example.com/') - expect(navigation.preventDefault).not.toHaveBeenCalled() - }) - - it('does not settle retirement when destroyed inspection is unavailable', () => { - const { getPageAuthority, registry, routeSession } = createHarness() - const guest = createGuest({ session: routeSession, closeDestroys: false }) - const retired = vi.fn() - registry.attachGuest(guest.guest) - registry.registerGuest(page) - registry.grantNavigation(page) - vi.mocked(guest.guest.isDestroyed).mockImplementation(() => { - throw new Error('inspection unavailable') - }) - - expect( - registry.retirePageAuthority({ - partition, - browserPageId: page.browserPageId, - pageHostGeneration: page.pageHostGeneration, - rendererWebContentsId: page.rendererWebContentsId, - pageAuthority: getPageAuthority(), - onRetired: retired - }) - ).toBe(false) - expect(retired).not.toHaveBeenCalled() - const navigation = navigationEvent() - guest.emit('will-navigate', navigation, 'https://example.com/') - expect(navigation.preventDefault).toHaveBeenCalledOnce() - - guest.destroy() - expect(retired).toHaveBeenCalledOnce() - }) - - it('retires the exact page authority when its guest process exits', () => { - const { registry, routeSession } = createHarness() - const guest = createGuest({ session: routeSession, closeDestroys: false }) - registry.attachGuest(guest.guest) - registry.registerGuest(page) - registry.grantNavigation(page) - - guest.emit('render-process-gone', {}, { reason: 'crashed' }) - - expect(guest.guest.close).toHaveBeenCalledOnce() - expect(registry.grantNavigation(page)).toBe(false) - const navigation = navigationEvent() - guest.emit('will-navigate', navigation, 'https://example.com/') - expect(navigation.preventDefault).toHaveBeenCalledOnce() - }) - - it('reports one exact availability loss for a crashed guest generation', () => { - const { registry, routeSession } = createHarness() - const guest = createGuest({ session: routeSession, closeDestroys: false }) - const unavailable = vi.fn() - registry.watchPageAvailability(page.browserPageId, unavailable) - registry.attachGuest(guest.guest) - registry.registerGuest(page) - registry.grantNavigation(page) - - guest.emit('render-process-gone', {}, { reason: 'crashed' }) - guest.destroy() - - expect(unavailable).toHaveBeenCalledOnce() - expect(unavailable).toHaveBeenCalledWith(page) - }) - - it('does not report explicit page retirement as an availability loss', () => { - const { getPageAuthority, registry, routeSession } = createHarness() - const guest = createGuest({ session: routeSession, closeDestroys: false }) - const unavailable = vi.fn() - registry.watchPageAvailability(page.browserPageId, unavailable) - registry.attachGuest(guest.guest) - registry.registerGuest(page) - registry.grantNavigation(page) - - registry.retirePageAuthority({ - ...page, - pageAuthority: getPageAuthority(), - onRetired: vi.fn() - }) - guest.destroy() - - expect(unavailable).not.toHaveBeenCalled() - }) - it('retires every page owned by a crashed host renderer', () => { const { registry, retirePreparedPagesOwnedByRenderer, routeSession } = createHarness() const first = createGuest({ session: routeSession }) @@ -650,59 +320,6 @@ describe('BrowserRouteWebContentsRegistry', () => { expect(retirePreparedPagesOwnedByRenderer).toHaveBeenCalledWith(page.rendererWebContentsId) }) - it('reports registered pages owned by a crashed host renderer', () => { - const { registry, routeSession } = createHarness() - const guest = createGuest({ session: routeSession, closeDestroys: false }) - const unavailable = vi.fn() - registry.watchPageAvailability(page.browserPageId, unavailable) - registry.attachGuest(guest.guest) - registry.registerGuest(page) - registry.grantNavigation(page) - - registry.retireRenderer(page.rendererWebContentsId) - - expect(unavailable).toHaveBeenCalledOnce() - expect(unavailable).toHaveBeenCalledWith(page) - }) - - it('keeps renderer retirement fail-closed when logical-owner cleanup throws', () => { - const { registry, retirePreparedPagesOwnedByRenderer, routeSession } = createHarness() - const guest = createGuest({ session: routeSession, closeDestroys: false }) - registry.attachGuest(guest.guest) - retirePreparedPagesOwnedByRenderer.mockImplementation(() => { - throw new Error('owner cleanup unavailable') - }) - - expect(() => registry.retireRenderer(page.rendererWebContentsId)).not.toThrow() - expect(guest.guest.close).toHaveBeenCalledOnce() - }) - - it('fails closed when Electron guest inspection races destruction', () => { - const { registry, routeSession } = createHarness() - const guest = createGuest({ session: routeSession }) - vi.mocked(guest.guest.getURL).mockImplementation(() => { - throw new Error('guest destroyed') - }) - - expect(() => registry.attachGuest(guest.guest)).not.toThrow() - expect(guest.guest.close).toHaveBeenCalledOnce() - }) - - it('turns registration-time inspection failures into navigation denial', () => { - const { registry, routeSession } = createHarness() - const guest = createGuest({ session: routeSession }) - registry.attachGuest(guest.guest) - registry.registerGuest(page) - registry.grantNavigation(page) - vi.mocked(guest.guest.getType).mockImplementation(() => { - throw new Error('guest swapped') - }) - - const navigation = navigationEvent() - expect(() => guest.emit('will-navigate', navigation, 'https://example.com/')).not.toThrow() - expect(navigation.preventDefault).toHaveBeenCalledOnce() - }) - it('keeps a destroyed claim distinct from a reused WebContents identity', async () => { const { registry, replaceAuthority, routeSession } = createHarness() const first = createGuest({ session: routeSession }) @@ -727,16 +344,4 @@ describe('BrowserRouteWebContentsRegistry', () => { await expect(registry.beginGuestRetirement(replacementClaim)).resolves.toBeUndefined() expect(replacement.guest.close).toHaveBeenCalledOnce() }) - - it('rejects registration without throwing when blank-document inspection fails', () => { - const { registry, routeSession } = createHarness() - const guest = createGuest({ session: routeSession }) - registry.attachGuest(guest.guest) - vi.mocked(guest.guest.getURL).mockImplementation(() => { - throw new Error('guest unavailable') - }) - - expect(() => registry.registerGuest(page)).not.toThrow() - expect(registry.registerGuest(page)).toBe(false) - }) }) diff --git a/src/main/browser/routing/local-host-partitions/local-ssh-browser-route.test.ts b/src/main/browser/routing/local-host-partitions/local-ssh-browser-route.test.ts index 1e746316f6f..404a4b1acaf 100644 --- a/src/main/browser/routing/local-host-partitions/local-ssh-browser-route.test.ts +++ b/src/main/browser/routing/local-host-partitions/local-ssh-browser-route.test.ts @@ -213,16 +213,6 @@ describe('LocalSshBrowserRoute', () => { accepted.destroy() }) - it('retains one shared route per target', async () => { - const dependencies = { - resolveExecutionRoute: async () => fakeExecutionRoute('route-shared'), - getAuthority: () => ({ providerEpoch: 'epoch-1', connectionGeneration: 1 }) - } - const first = await retainLocalSshBrowserRoute('target-shared', dependencies) - const second = await retainLocalSshBrowserRoute('target-shared', dependencies) - expect(second.port).toBe(first.port) - }) - it('drops the shared route on explicit close so a re-add starts fresh', async () => { const dependencies = { resolveExecutionRoute: async () => fakeExecutionRoute('route-shared'), @@ -236,60 +226,4 @@ describe('LocalSshBrowserRoute', () => { socket.destroy() expect(first.port).not.toBe(0) }) - - it('classifies forwarding probes by the wire reason code, never by scary wording', async () => { - let failure: (Error & { reason?: number }) | null = null - const route = new LocalSshBrowserRoute('target-probe', { - resolveExecutionRoute: async () => { - const executionRoute = fakeExecutionRoute('route-probe') - const originalConnect = executionRoute.connect - executionRoute.connect = (target) => { - if (failure) { - const socket = new FakeTunnelSocket('never-connect') - const error = failure - queueMicrotask(() => { - socket.emit('error', error) - }) - return socket - } - return originalConnect(target) - } - return executionRoute - }, - getAuthority: () => ({ providerEpoch: 'epoch-1', connectionGeneration: 1 }) - }) - routes.push(route) - await route.listen() - - const withReason = (message: string, reason?: number): Error & { reason?: number } => { - const error = new Error(message) as Error & { reason?: number } - if (reason !== undefined) { - error.reason = reason - } - return error - } - - failure = withReason('(SSH) Channel open failure: open failed', 1) - expect(await route.probeForwarding(500)).toBe('forwarding-blocked') - // CONNECT_FAILED means forwarding was permitted even if the destination firewall says "denied". - failure = withReason('(SSH) Channel open failure: connection denied by firewall', 2) - expect(await route.probeForwarding(500)).toBe('ok') - failure = withReason('administratively prohibited: open failed') - expect(await route.probeForwarding(500)).toBe('forwarding-blocked') - failure = withReason('open forbidden: access denied, not allowed') - expect(await route.probeForwarding(500)).toBe('ok') - failure = null - expect(await route.probeForwarding(500)).toBe('ok') - }) - - it('reports ssh-unavailable when the probe cannot resolve a route', async () => { - const route = new LocalSshBrowserRoute('target-down', { - resolveExecutionRoute: async () => { - throw new Error('browser_tunnel_execution_host_unavailable') - }, - getAuthority: () => ({ providerEpoch: 'epoch-1', connectionGeneration: 1 }) - }) - routes.push(route) - expect(await route.probeForwarding(500)).toBe('ssh-unavailable') - }) }) diff --git a/src/main/browser/routing/partitions/browser-route-partition-binding-store.test.ts b/src/main/browser/routing/partitions/browser-route-partition-binding-store.test.ts index daa6e7c7a61..cf72aae9dc2 100644 --- a/src/main/browser/routing/partitions/browser-route-partition-binding-store.test.ts +++ b/src/main/browser/routing/partitions/browser-route-partition-binding-store.test.ts @@ -1,4 +1,4 @@ -import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs' +import { mkdirSync, mkdtempSync, readFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -52,69 +52,6 @@ describe('BrowserRoutePartitionBindingStore', () => { }) }) - it('reads pre-scope v1 metadata as unscoped bindings', () => { - const filePath = createPath() - writeFileSync(filePath, JSON.stringify({ version: 1, bindings: { [partition]: fingerprint } })) - const store = new BrowserRoutePartitionBindingStore({ filePath }) - - expect(store.get(partition)).toBe(fingerprint) - expect(store.listBindings().get(partition)).toEqual({ - fingerprint, - storageScope: null, - lastUsedAt: 0 - }) - }) - - it('removes bindings and rejects a non-hex storage scope', () => { - const filePath = createPath() - const store = new BrowserRoutePartitionBindingStore({ filePath }) - store.set(partition, fingerprint, 'e'.repeat(64)) - - expect(() => store.set(thirdPartition, fingerprint, 'z')).toThrow( - 'browser_route_partition_binding_invalid' - ) - expect(store.remove([partition])).toBe(1) - expect(store.remove([partition])).toBe(0) - expect(new BrowserRoutePartitionBindingStore({ filePath }).get(partition)).toBeNull() - }) - - it('rejects replacement and invalid binding shapes', () => { - const store = new BrowserRoutePartitionBindingStore({ filePath: createPath() }) - store.set(partition, fingerprint, 'e'.repeat(64)) - - expect(() => store.set(partition, 'b'.repeat(64), 'e'.repeat(64))).toThrow( - 'browser_route_partition_binding_conflict' - ) - expect(() => store.set('persist:unowned', fingerprint, 'e'.repeat(64))).toThrow( - 'browser_route_partition_binding_invalid' - ) - expect(() => store.set(partition, 'short', 'e'.repeat(64))).toThrow( - 'browser_route_partition_binding_invalid' - ) - }) - - it('fails closed on corrupt or malformed persisted metadata', () => { - const filePath = createPath() - writeFileSync(filePath, '{broken') - expect(() => new BrowserRoutePartitionBindingStore({ filePath }).get(partition)).toThrow( - 'browser_route_partition_binding_store_invalid' - ) - - writeFileSync(filePath, JSON.stringify({ version: 3, bindings: {} })) - expect(() => new BrowserRoutePartitionBindingStore({ filePath }).get(partition)).toThrow( - 'browser_route_partition_binding_store_invalid' - ) - }) - - it('rejects an oversized binding file before parsing it', () => { - const filePath = createPath() - writeFileSync(filePath, JSON.stringify({ version: 1, bindings: {}, padding: 'x'.repeat(256) })) - - expect(() => - new BrowserRoutePartitionBindingStore({ filePath, maxFileBytes: 128 }).get(partition) - ).toThrow('browser_route_partition_binding_store_invalid') - }) - it('fails closed when Chromium data exists without matching binding metadata', () => { const paths = createStorePaths() mkdirSync(join(paths.partitionDataRoot, partition.slice('persist:'.length)), { @@ -128,46 +65,6 @@ describe('BrowserRoutePartitionBindingStore', () => { ) }) - it('accepts existing Chromium data only with matching durable metadata', () => { - const paths = createStorePaths() - const store = new BrowserRoutePartitionBindingStore(paths) - store.set(partition, fingerprint, 'e'.repeat(64)) - mkdirSync(join(paths.partitionDataRoot, partition.slice('persist:'.length)), { - recursive: true - }) - - expect(store.get(partition)).toBe(fingerprint) - }) - - it('bounds distinct persisted bindings when liveness is unknown', () => { - const store = new BrowserRoutePartitionBindingStore({ filePath: createPath(), maxBindings: 1 }) - store.set(partition, fingerprint, 'e'.repeat(64)) - - expect(() => store.set(otherPartition, 'b'.repeat(64), 'e'.repeat(64))).toThrow( - 'browser_route_partition_binding_capacity' - ) - }) - - it('evicts the least recently used releasable binding instead of failing at capacity', () => { - const store = new BrowserRoutePartitionBindingStore({ - filePath: createPath(), - maxBindings: 2, - isPartitionRetained: () => false - }) - vi.setSystemTime(1_000) - store.set(partition, fingerprint, 'e'.repeat(64)) - vi.setSystemTime(2_000) - store.set(otherPartition, 'b'.repeat(64), 'e'.repeat(64)) - - vi.setSystemTime(3_000) - const evicted = store.set(thirdPartition, 'c'.repeat(64), 'e'.repeat(64)) - - expect(evicted).toEqual([partition]) - expect(store.get(partition)).toBeNull() - expect(store.get(otherPartition)).toBe('b'.repeat(64)) - expect(store.get(thirdPartition)).toBe('c'.repeat(64)) - }) - it('never evicts a retained partition, and fails only when every binding is retained', () => { const filePath = createPath() const retained = new Set() @@ -212,22 +109,6 @@ describe('BrowserRoutePartitionBindingStore', () => { expect(store.get(partition)).toBe(fingerprint) }) - it('resolves an existing partition by fingerprint and re-points it on rebind', () => { - const store = new BrowserRoutePartitionBindingStore({ filePath: createPath() }) - store.set(partition, fingerprint, 'e'.repeat(64)) - - expect(store.findPartitionByFingerprint(fingerprint)).toBe(partition) - expect(store.findPartitionByFingerprint('b'.repeat(64))).toBeNull() - - store.rebind(partition, 'b'.repeat(64), 'e'.repeat(64)) - - expect(store.findPartitionByFingerprint('b'.repeat(64))).toBe(partition) - expect(store.get(partition)).toBe('b'.repeat(64)) - expect(() => store.rebind(otherPartition, 'c'.repeat(64), 'e'.repeat(64))).toThrow( - 'browser_route_partition_binding_invalid' - ) - }) - it('answers to an alias beside the fingerprint older builds read', () => { const store = new BrowserRoutePartitionBindingStore({ filePath: createPath() }) store.set(partition, fingerprint, 'e'.repeat(64)) @@ -242,33 +123,4 @@ describe('BrowserRoutePartitionBindingStore', () => { store.set(partition, fingerprint, 'f'.repeat(64)) expect(store.findPartitionByFingerprint('b'.repeat(64))).toBe(partition) }) - - it('keeps aliases bounded and drops malformed ones without rejecting the file', () => { - const filePath = createPath() - writeFileSync( - filePath, - JSON.stringify({ - version: 2, - bindings: { - [partition]: { - fingerprint, - storageScope: 'e'.repeat(64), - lastUsedAt: 1, - aliasFingerprints: ['not-a-fingerprint', 7, 'b'.repeat(64)] - } - } - }) - ) - const store = new BrowserRoutePartitionBindingStore({ filePath }) - expect(store.listBindings().get(partition)?.aliasFingerprints).toEqual(['b'.repeat(64)]) - - store.rebind(partition, fingerprint, 'e'.repeat(64), 'c'.repeat(64)) - store.rebind(partition, fingerprint, 'e'.repeat(64), 'd'.repeat(64)) - - expect(store.listBindings().get(partition)?.aliasFingerprints).toEqual([ - 'c'.repeat(64), - 'd'.repeat(64) - ]) - expect(store.findPartitionByFingerprint('b'.repeat(64))).toBeNull() - }) }) diff --git a/src/main/browser/routing/sessions/browser-route-session-registry.test.ts b/src/main/browser/routing/sessions/browser-route-session-registry.test.ts index 35e1b955293..47a29691621 100644 --- a/src/main/browser/routing/sessions/browser-route-session-registry.test.ts +++ b/src/main/browser/routing/sessions/browser-route-session-registry.test.ts @@ -136,45 +136,6 @@ function deferred(): { } describe('BrowserRouteSessionRegistry', () => { - it('prepares authority-scoped remote metadata without needing a desktop profile record', async () => { - const { dependencies, registry } = createHarness() - const profileMetadata = { - id: identity.browserProfileId, - label: 'Remote profile', - scope: 'isolated' - } - await prepare(registry, { profileMetadata }) - expect(dependencies.validateProfile).toHaveBeenCalledWith( - identity.browserProfileId, - profileMetadata - ) - expect(dependencies.setupPolicies).toHaveBeenCalledWith( - expect.objectContaining({ - profileMetadata, - browserProfileId: identity.browserProfileId, - partition: 'persist:route-a' - }) - ) - }) - - it.each([ - { id: 'other-profile', label: 'Remote', scope: 'isolated' }, - { - id: identity.browserProfileId, - label: 'Remote', - scope: 'isolated', - partition: 'persist:local-default' - } - ])( - 'rejects inconsistent or arbitrary partition metadata before touching storage', - async (profileMetadata) => { - const { registry, dependencies } = createHarness() - await expect(prepare(registry, { profileMetadata })).rejects.toThrow() - expect(dependencies.getSession).not.toHaveBeenCalled() - expect(dependencies.bindingStore.set).not.toHaveBeenCalled() - } - ) - it('rekeys one prepared authority and makes the old release handle inert', async () => { const { dependencies, registry } = createHarness() const previousHandle = await prepare(registry) @@ -204,126 +165,6 @@ describe('BrowserRouteSessionRegistry', () => { expect(dependencies.clearPolicies).toHaveBeenCalledOnce() }) - it('applies and verifies the exact fail-closed proxy before allowlisting', async () => { - const { dependencies, order, registry, session } = createHarness() - - const handle = await prepare(registry) - - expect(handle.partition).toBe('persist:route-a') - expect(order).toEqual([ - 'validate-profile', - 'persist-binding', - 'get-session', - 'set-proxy', - 'setup-policies', - 'close-connections', - 'resolve-proxy' - ]) - expect(session.setProxy).toHaveBeenCalledWith({ - mode: 'fixed_servers', - proxyRules: 'socks5://127.0.0.1:43123', - proxyBypassRules: '<-loopback>' - }) - expect(session.resolveProxy).toHaveBeenCalledWith('http://browser-route-probe.invalid/') - expect(registry.isAllowedPartition(handle.partition)).toBe(true) - - handle.release() - expect(registry.isAllowedPartition(handle.partition)).toBe(false) - expect(dependencies.clearPolicies).toHaveBeenCalledTimes(1) - }) - - it('indexes only exact live page generations by their Electron session', async () => { - const { registry, session } = createHarness() - const handle = await prepare(registry) - - expect(registry.getPartitionForSession(session)).toBe(handle.partition) - expect( - registry.getPreparedPageAuthority({ - partition: handle.partition, - browserPageId: 'page-a', - pageHostGeneration: 1, - rendererWebContentsId: 11 - }) - ).not.toBeNull() - expect( - registry.getPreparedPageAuthority({ - partition: handle.partition, - browserPageId: 'page-a', - pageHostGeneration: 2, - rendererWebContentsId: 11 - }) - ).toBeNull() - - handle.release() - expect(registry.getPartitionForSession(session)).toBe(handle.partition) - expect( - registry.getPreparedPageAuthority({ - partition: handle.partition, - browserPageId: 'page-a', - pageHostGeneration: 1, - rendererWebContentsId: 11 - }) - ).toBeNull() - }) - - it('binds prepared page authority to the exact owning renderer', async () => { - const { registry } = createHarness() - const handle = await prepare(registry) - const page = { - partition: handle.partition, - browserPageId: 'page-a', - pageHostGeneration: 1 - } - - expect(registry.getPreparedPageAuthority({ ...page, rendererWebContentsId: 11 })).not.toBeNull() - expect(registry.getPreparedPageAuthority({ ...page, rendererWebContentsId: 12 })).toBeNull() - }) - - it('does not move a live page generation to another renderer', async () => { - const { registry } = createHarness() - const owned = await prepare(registry) - - await expect(prepare(registry, { rendererWebContentsId: 12 })).rejects.toThrow( - 'browser_route_partition_page_owner_conflict' - ) - expect( - registry.getPreparedPageAuthority({ - partition: owned.partition, - browserPageId: 'page-a', - pageHostGeneration: 1, - rendererWebContentsId: 11 - }) - ).not.toBeNull() - }) - - it('retires prepared but unregistered pages when their owning renderer exits', async () => { - const { registry } = createHarness() - const owned = await prepare(registry) - await prepare(registry, { - browserPageId: 'page-b', - pageHostGeneration: 2, - rendererWebContentsId: 12 - }) - - expect(registry.retirePreparedPagesOwnedByRenderer(11)).toBe(1) - expect( - registry.getPreparedPageAuthority({ - partition: owned.partition, - browserPageId: 'page-a', - pageHostGeneration: 1, - rendererWebContentsId: 11 - }) - ).toBeNull() - expect( - registry.getPreparedPageAuthority({ - partition: owned.partition, - browserPageId: 'page-b', - pageHostGeneration: 2, - rendererWebContentsId: 12 - }) - ).not.toBeNull() - }) - it('continues owner retirement across partitions when policy cleanup throws', async () => { const { registry } = createHarness({ cleanupError: new Error('cleanup unavailable') }) const first = await prepare(registry) @@ -342,23 +183,6 @@ describe('BrowserRouteSessionRegistry', () => { } }) - it('rejects an in-flight prepare after its renderer owner exits', async () => { - const probeStarted = deferred() - const resolveProxyGate = deferred() - const { dependencies, registry } = createHarness({ - proxyProbeStarted: probeStarted.resolve, - resolveProxyGate: resolveProxyGate.promise - }) - const preparing = prepare(registry) - await probeStarted.promise - - expect(registry.retirePreparedPagesOwnedByRenderer(11)).toBe(0) - resolveProxyGate.resolve() - await expect(preparing).rejects.toThrow('browser_route_partition_renderer_retired') - expect(registry.isAllowedPartition('persist:route-a')).toBe(false) - expect(dependencies.clearPolicies).toHaveBeenCalledOnce() - }) - it('admits a live sibling waiter after fencing a stale pending owner', async () => { const probeStarted = deferred() const resolveProxyGate = deferred() @@ -389,25 +213,6 @@ describe('BrowserRouteSessionRegistry', () => { expect(dependencies.clearPolicies).not.toHaveBeenCalled() }) - it('bounds callers waiting on one pending partition setup', async () => { - const probeStarted = deferred() - const resolveProxyGate = deferred() - const { registry } = createHarness({ - maxPagesPerPartition: 1, - proxyProbeStarted: probeStarted.resolve, - resolveProxyGate: resolveProxyGate.promise - }) - const first = prepare(registry) - await probeStarted.promise - - await expect(prepare(registry, { browserPageId: 'page-b' })).rejects.toThrow( - 'browser_route_partition_pending_capacity' - ) - resolveProxyGate.resolve() - const handle = await first - handle.release() - }) - it('does not resolve a live-partition prepare after owner retirement', async () => { const { registry } = createHarness() const first = await prepare(registry) @@ -422,31 +227,6 @@ describe('BrowserRouteSessionRegistry', () => { expect(registry.isAllowedPartition(first.partition)).toBe(false) }) - it('changes opaque page authority when the same logical tuple is prepared again', async () => { - const { registry } = createHarness() - const first = await prepare(registry) - const firstAuthority = registry.getPreparedPageAuthority({ - partition: first.partition, - browserPageId: 'page-a', - pageHostGeneration: 1, - rendererWebContentsId: 11 - }) - first.release() - - const replacement = await prepare(registry) - const replacementAuthority = registry.getPreparedPageAuthority({ - partition: replacement.partition, - browserPageId: 'page-a', - pageHostGeneration: 1, - rendererWebContentsId: 11 - }) - - expect(firstAuthority).not.toBeNull() - expect(replacementAuthority).not.toBeNull() - expect(replacementAuthority).not.toBe(firstAuthority) - replacement.release() - }) - it('keeps route policy installed until delayed exact-page retirement settles', async () => { const { dependencies, registry, retirements } = createHarness({ maxPagesPerPartition: 1, @@ -493,72 +273,6 @@ describe('BrowserRouteSessionRegistry', () => { expect(dependencies.clearPolicies).toHaveBeenCalledOnce() }) - it('fails closed when exact-page retirement cannot be started', async () => { - const { dependencies, registry } = createHarness({ - retirementError: new Error('retirement unavailable') - }) - const handle = await prepare(registry) - - expect(() => handle.release()).not.toThrow() - expect(registry.isAllowedPartition(handle.partition)).toBe(false) - expect(dependencies.clearPolicies).not.toHaveBeenCalled() - await expect(prepare(registry)).rejects.toThrow('browser_route_partition_page_retiring') - }) - - it('lets an exact lifecycle event begin the same fenced page retirement', async () => { - const { registry, retirements } = createHarness({ retirementSettled: false }) - const handle = await prepare(registry) - const pageAuthority = registry.getPreparedPageAuthority({ - partition: handle.partition, - browserPageId: 'page-a', - pageHostGeneration: 1, - rendererWebContentsId: 11 - }) - - expect( - registry.retirePreparedPage({ - partition: handle.partition, - browserPageId: 'page-a', - pageHostGeneration: 1, - rendererWebContentsId: 11, - pageAuthority: pageAuthority ?? Symbol('missing') - }) - ).toBe(true) - expect(registry.isAllowedPartition(handle.partition)).toBe(false) - expect(retirements).toHaveLength(1) - expect( - registry.retirePreparedPage({ - partition: handle.partition, - browserPageId: 'page-a', - pageHostGeneration: 1, - rendererWebContentsId: 11, - pageAuthority: pageAuthority ?? Symbol('missing') - }) - ).toBe(false) - }) - - it('never allowlists a partition whose proxy resolves direct or elsewhere', async () => { - const { dependencies, registry } = createHarness({ resolvedProxy: 'DIRECT' }) - - await expect(prepare(registry)).rejects.toThrow( - 'browser_route_partition_proxy_verification_failed' - ) - expect(registry.isAllowedPartition('persist:route-a')).toBe(false) - expect(dependencies.clearPolicies).toHaveBeenCalledTimes(1) - }) - - it('clears partially installed policies when async policy setup fails', async () => { - const { dependencies, registry, session } = createHarness({ - setupError: new Error('policy setup failed') - }) - - await expect(prepare(registry)).rejects.toThrow('policy setup failed') - expect(session.setProxy).toHaveBeenCalledTimes(1) - expect(session.closeAllConnections).toHaveBeenCalledTimes(1) - expect(dependencies.clearPolicies).toHaveBeenCalledTimes(1) - expect(registry.isAllowedPartition('persist:route-a')).toBe(false) - }) - it.each([false, true])( 'keeps retries joined until failed policy setup settles proxy cleanup (reject: %s)', async (rejectProxy) => { @@ -597,43 +311,6 @@ describe('BrowserRouteSessionRegistry', () => { } ) - it('rejects a binding collision before opening an Electron session', async () => { - const { bindings, dependencies, registry } = createHarness() - bindings.set('persist:route-a', 'c'.repeat(64)) - - await expect(prepare(registry)).rejects.toThrow('browser_route_partition_binding_conflict') - expect(dependencies.getSession).not.toHaveBeenCalled() - }) - - it('rejects a missing browser profile before consuming durable binding capacity', async () => { - const { dependencies, registry } = createHarness({ - profileError: new Error('browser_route_partition_profile_unavailable') - }) - - await expect(prepare(registry)).rejects.toThrow('browser_route_partition_profile_unavailable') - expect(dependencies.bindingStore.set).not.toHaveBeenCalled() - expect(dependencies.getSession).not.toHaveBeenCalled() - }) - - it('rejects invalid listener and page identities before binding persistence', async () => { - const { dependencies, registry } = createHarness() - - await expect( - prepare(registry, { proxyEndpoint: { host: '0.0.0.0', port: 43123 } }) - ).rejects.toThrow('browser_route_partition_proxy_invalid') - await expect(prepare(registry, { browserPageId: '' })).rejects.toThrow( - 'browser_route_partition_page_invalid' - ) - await expect(prepare(registry, { pageHostGeneration: 0 })).rejects.toThrow( - 'browser_route_partition_page_invalid' - ) - await expect(prepare(registry, { rendererWebContentsId: 0 })).rejects.toThrow( - 'browser_route_partition_page_invalid' - ) - expect(dependencies.bindingStore.set).not.toHaveBeenCalled() - expect(dependencies.getSession).not.toHaveBeenCalled() - }) - it('shares one prepared partition and fences stale page-handle cleanup', async () => { const { dependencies, registry, session } = createHarness() const first = await prepare(registry) @@ -664,27 +341,4 @@ describe('BrowserRouteSessionRegistry', () => { first.release() second.release() }) - - it('bounds distinct live and pending partitions', async () => { - const { registry } = createHarness({ maxLivePartitions: 1 }) - const first = await prepare(registry) - - await expect( - prepare(registry, { - browserPageId: 'page-b', - identity: { ...identity, executionHostIdentity: 'execution-host-b' } - }) - ).rejects.toThrow('browser_route_partition_capacity') - first.release() - }) - - it('bounds distinct page generations retained by one partition', async () => { - const { registry } = createHarness({ maxPagesPerPartition: 1 }) - const first = await prepare(registry) - - await expect(prepare(registry, { browserPageId: 'page-b' })).rejects.toThrow( - 'browser_route_partition_page_capacity' - ) - first.release() - }) }) diff --git a/src/main/browser/sessions/browser-session-partition-policies.test.ts b/src/main/browser/sessions/browser-session-partition-policies.test.ts index bd9a87fe84c..1baf0531735 100644 --- a/src/main/browser/sessions/browser-session-partition-policies.test.ts +++ b/src/main/browser/sessions/browser-session-partition-policies.test.ts @@ -137,66 +137,12 @@ function profileFor(partition: string): BrowserSessionProfile { } } -function fireWillDownload(partition: string): { cancelled: boolean } { - const sess = sessionsByPartition.get(partition) - if (!sess || sess.listeners.length !== 1) { - throw new Error(`expected exactly one will-download listener on ${partition}`) - } - let cancelled = false - sess.listeners[0]({ preventDefault: () => (cancelled = true) }, { id: 'item-1' }, { id: 42 }) - return { cancelled } -} - beforeEach(() => { vi.clearAllMocks() sessionsByPartition.clear() vi.resetModules() }) -describe('partition download policy', () => { - it('routes a download on a partition that did not ask for the deny', async () => { - const install = await loadInstaller() - install(profileFor('persist:browsing-1')) - - expect(fireWillDownload('persist:browsing-1').cancelled).toBe(false) - expect(mocks.handleGuestWillDownload).toHaveBeenCalledWith( - expect.objectContaining({ guestWebContentsId: 42 }) - ) - }) - - it('cancels a download on a partition that asked for the deny, routing nothing', async () => { - const install = await loadInstaller() - install(profileFor('orca-doc-preview'), { downloads: 'deny' }) - - expect(fireWillDownload('orca-doc-preview').cancelled).toBe(true) - expect(mocks.handleGuestWillDownload).not.toHaveBeenCalled() - }) - - it('tells the reader about the refusal, and only on the partition that refused', async () => { - const install = await loadInstaller() - install(profileFor('orca-doc-preview'), { downloads: 'deny' }) - install(profileFor('persist:browsing-1')) - - fireWillDownload('persist:browsing-1') - expect(mocks.noticeDocPreviewDownloadBlocked).not.toHaveBeenCalled() - - fireWillDownload('orca-doc-preview') - expect(mocks.noticeDocPreviewDownloadBlocked).toHaveBeenCalledWith( - expect.objectContaining({ id: 42 }) - ) - }) - - it('keeps each partition on its own decision', async () => { - const install = await loadInstaller() - install(profileFor('orca-doc-preview'), { downloads: 'deny' }) - install(profileFor('persist:browsing-1')) - - expect(fireWillDownload('orca-doc-preview').cancelled).toBe(true) - expect(fireWillDownload('persist:browsing-1').cancelled).toBe(false) - expect(mocks.handleGuestWillDownload).toHaveBeenCalledTimes(1) - }) -}) - describe('partition permission policy', () => { it('denies clipboard reads only to pages an SSH host has driven (#29145)', async () => { mocks.isRemoteCliDrivenGuest.mockImplementation((id?: number) => id === 7) @@ -245,44 +191,6 @@ describe('partition permission policy', () => { expect(checkHandler(guest(8), 'clipboard-read', 'https://a.test')).toBe(true) }) - it('answers non-notification requests without inventorying frames', async () => { - const install = await loadInstaller() - const partition = 'persist:permission-frame-inventory' - await install(profileFor(partition), { sitePermissions: 'session' }) - const { sessionBrowserSitePermissions } = - await import('../site-settings/browser-site-permission-prompt') - const memory = sessionBrowserSitePermissions(partition) - const requestHandler = - sessionsByPartition.get(partition)?.setPermissionRequestHandler.mock.calls[0]?.[0] - if (typeof requestHandler !== 'function') { - throw new Error('Expected the installed permission request handler') - } - const enumerateFrames = vi.fn(() => [{ url: 'about:srcdoc', origin: 'https://other.test' }]) - const tab = { - id: 12, - getURL: () => 'https://a.test/page', - mainFrame: { - get framesInSubtree() { - return enumerateFrames() - } - } - } - - for (const decision of ['allow', 'block'] as const) { - memory.set('https://a.test', ['location', 'clipboard', 'midi'], decision) - for (const permission of ['geolocation', 'clipboard-read', 'midi']) { - const answer = await new Promise((resolve) => { - requestHandler(tab, permission, resolve, { - requestingUrl: 'about:srcdoc', - isMainFrame: false - }) - }) - expect(answer).toBe(decision === 'allow') - } - } - expect(enumerateFrames).not.toHaveBeenCalled() - }) - it("answers a notification from a frame with an inherited origin as the frame's real site", async () => { const install = await loadInstaller() const store = await import('../site-settings/browser-site-permission-store') @@ -499,119 +407,4 @@ describe('partition permission policy', () => { expect.objectContaining({ guestWebContentsId: 11, permission: 'fileSystem' }) ) }) - - it('refuses a blocklisted system folder at once instead of leaving the page waiting', async () => { - const install = await loadInstaller() - install(profileFor('persist:browsing-6')) - install(profileFor('orca-doc-preview'), { permissions: 'deny' }) - const answer = (partition: string) => { - const sess = sessionsByPartition.get(partition) - const listener = sess?.on.mock.calls.find( - ([event]) => event === 'file-system-access-restricted' - )?.[1] - const actions: string[] = [] - listener?.( - {}, - { - origin: 'https://app.test', - isDirectory: true, - path: '/Users/me', - webContents: { id: 12 } - }, - (action: string) => actions.push(action) - ) - return actions - } - - expect(answer('orca-doc-preview')).toEqual(['deny']) - expect(mocks.notifyPermissionDenied).not.toHaveBeenCalled() - expect(answer('persist:browsing-6')).toEqual(['deny']) - expect(mocks.notifyPermissionDenied).toHaveBeenCalledWith({ - guestWebContentsId: 12, - permission: 'fileSystem', - rawUrl: 'https://app.test' - }) - }) - - it('hands a link to another app to the open-app prompt instead of denying it', async () => { - const install = await loadInstaller() - install(profileFor('persist:browsing-4')) - const sess = sessionsByPartition.get('persist:browsing-4') - if (!sess) { - throw new Error('Expected the browsing session') - } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the installer registered exactly this handler on the fake session. - const requestHandler = sess.setPermissionRequestHandler.mock.calls[0]?.[0] as ( - webContents: { id: number; getURL: () => string }, - permission: string, - callback: (allowed: boolean) => void, - details: { externalURL?: string; requestingUrl?: string } - ) => void - const guest = { id: 9, getURL: () => 'https://acme.okta.com/signin' } - const details = { - externalURL: 'com-okta-authenticator:/deviceChallenge', - requestingUrl: 'https://login.acme.okta.com/frame' - } - const decisions: boolean[] = [] - - mocks.requestExternalAppLaunch.mockReturnValueOnce(true) - requestHandler(guest, 'openExternal', (allowed) => decisions.push(allowed), details) - const [launchGuest, launchUrl, requestingUrl] = mocks.requestExternalAppLaunch.mock.calls[0] - expect([launchGuest, launchUrl]).toEqual([guest, 'com-okta-authenticator:/deviceChallenge']) - expect(requestingUrl()).toBe('https://login.acme.okta.com/frame') - expect(mocks.notifyPermissionDenied).not.toHaveBeenCalled() - - mocks.requestExternalAppLaunch.mockReturnValueOnce(false) - requestHandler(guest, 'openExternal', (allowed) => decisions.push(allowed), details) - expect(mocks.notifyPermissionDenied).toHaveBeenCalledWith( - expect.objectContaining({ guestWebContentsId: 9, permission: 'openExternal' }) - ) - // Orca launches approved apps itself, so Chromium's launch remains denied. - expect(decisions).toEqual([false, false]) - }) - - it('cancels a download from a page an SSH host drives (#29145)', async () => { - mocks.isRemoteCliDrivenGuest.mockImplementation((id?: number) => id === 42) - const install = await loadInstaller() - install(profileFor('persist:browsing-3')) - - expect(fireWillDownload('persist:browsing-3')).toEqual({ cancelled: true }) - expect(mocks.handleGuestWillDownload).not.toHaveBeenCalled() - }) - - it('denies every request and check on a strict partition without WebAuthn handlers', async () => { - const install = await loadInstaller() - install(profileFor('orca-doc-preview'), { permissions: 'deny' }) - const sess = sessionsByPartition.get('orca-doc-preview') - if (!sess) { - throw new Error('Expected the preview session') - } - const requestHandler = sess.setPermissionRequestHandler.mock.calls[0]?.[0] as ( - webContents: Electron.WebContents, - permission: string, - callback: (allowed: boolean) => void - ) => void - const checkHandler = sess.setPermissionCheckHandler.mock.calls[0]?.[0] as ( - webContents: Electron.WebContents, - permission: string - ) => boolean - const displayMediaHandler = sess.setDisplayMediaRequestHandler.mock.calls[0]?.[0] as ( - request: Electron.DisplayMediaRequestHandlerHandlerRequest, - callback: (streams: { video?: Electron.WebFrameMain; audio?: 'loopback' }) => void - ) => void - - for (const permission of ['media', 'clipboard-read', 'notifications', 'fullscreen']) { - let decision: boolean | null = null - requestHandler({} as Electron.WebContents, permission, (allowed) => (decision = allowed)) - expect(decision).toBe(false) - expect(checkHandler({} as Electron.WebContents, permission)).toBe(false) - } - expect(mocks.installBrowserWebAuthnAccessHandlers).not.toHaveBeenCalled() - expect(mocks.clearBrowserWebAuthnAccessHandlers).toHaveBeenCalledWith(sess) - let displayMediaDecision: { video?: Electron.WebFrameMain; audio?: 'loopback' } | null = null - displayMediaHandler({} as Electron.DisplayMediaRequestHandlerHandlerRequest, (decision) => { - displayMediaDecision = decision - }) - expect(displayMediaDecision).toEqual({ video: undefined, audio: undefined }) - }) }) diff --git a/src/main/browser/sessions/browser-session-proxy.test.ts b/src/main/browser/sessions/browser-session-proxy.test.ts index 86d16d4ee9d..3ac81726ceb 100644 --- a/src/main/browser/sessions/browser-session-proxy.test.ts +++ b/src/main/browser/sessions/browser-session-proxy.test.ts @@ -33,7 +33,6 @@ import { invalidateBrowserSessionProxyApplication, setBrowserNetworkProxySettingsResolver } from './browser-session-proxy' -import { handleElectronProxyLogin } from '../../network/electron-proxy-credentials' import { applyProxySettingsToSession, releaseProxySessionApplication @@ -68,110 +67,6 @@ describe('browser session proxy', () => { vi.restoreAllMocks() }) - it('pins the configured proxy onto every browser partition', async () => { - await applyBrowserSessionProxies(PROFILES, { - httpProxyUrl: 'socks5://127.0.0.1:1080', - httpProxyBypassRules: '' - }) - - expect(fromPartitionMock).toHaveBeenCalledWith('persist:orca-browser') - expect(fromPartitionMock).toHaveBeenCalledWith('persist:orca-browser-session-iso') - for (const partition of PROFILES.map((p) => p.partition)) { - expect(sessionsByPartition.get(partition)?.setProxy).toHaveBeenCalledWith({ - mode: 'fixed_servers', - proxyRules: 'socks5://127.0.0.1:1080' - }) - } - }) - - it('carries bypass rules through to each partition', async () => { - await applyBrowserSessionProxies(PROFILES, { - httpProxyUrl: 'socks5://127.0.0.1:1080', - httpProxyBypassRules: 'localhost, *.internal' - }) - - expect(sessionsByPartition.get('persist:orca-browser')?.setProxy).toHaveBeenCalledWith({ - mode: 'fixed_servers', - proxyRules: 'socks5://127.0.0.1:1080', - proxyBypassRules: 'localhost;*.internal' - }) - }) - - it('authenticates a credentialed proxy without exposing credentials in partition rules', async () => { - await applyBrowserSessionProxies(PROFILES.slice(0, 1), { - httpProxyUrl: 'http://browser-user:browser-pass@proxy.example:8080', - httpProxyBypassRules: '' - }) - const sess = sessionsByPartition.get('persist:orca-browser') - expect(sess?.setProxy).toHaveBeenCalledWith({ - mode: 'fixed_servers', - proxyRules: 'http://proxy.example:8080' - }) - - const preventDefault = vi.fn() - const callback = vi.fn() - handleElectronProxyLogin( - { preventDefault } as never, - { session: sess } as never, - {} as never, - { - isProxy: true, - scheme: 'basic', - host: 'proxy.example', - port: 8080, - realm: 'proxy' - }, - callback - ) - - expect(preventDefault).toHaveBeenCalledOnce() - expect(callback).toHaveBeenCalledWith('browser-user', 'browser-pass') - }) - - it('keeps sweeping but reports failure so the failed partition stays gated', async () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) - fromPartitionMock.mockImplementationOnce(() => { - throw new Error('partition unavailable') - }) - - await expect( - applyBrowserSessionProxies(PROFILES, { - httpProxyUrl: 'socks5://127.0.0.1:1080', - httpProxyBypassRules: '' - }) - ).rejects.toThrow('persist:orca-browser') - - expect( - sessionsByPartition.get('persist:orca-browser-session-iso')?.setProxy - ).toHaveBeenCalledWith({ mode: 'fixed_servers', proxyRules: 'socks5://127.0.0.1:1080' }) - expect(warn).toHaveBeenCalledWith( - '[proxy] Failed to apply proxy to browser partition', - 'persist:orca-browser' - ) - }) - - it('starts every partition write without waiting for earlier partitions', async () => { - let finishFirstWrite: (() => void) | undefined - const firstSession = fromPartition('persist:orca-browser') - firstSession.setProxy.mockImplementation( - () => new Promise((resolve) => (finishFirstWrite = resolve)) - ) - sessionsByPartition.set('persist:orca-browser', firstSession) - - const sweep = applyBrowserSessionProxies(PROFILES, { - httpProxyUrl: 'socks5://127.0.0.1:1080', - httpProxyBypassRules: '' - }) - - await vi.waitFor(() => - expect( - sessionsByPartition.get('persist:orca-browser-session-iso')?.setProxy - ).toHaveBeenCalledOnce() - ) - finishFirstWrite?.() - await sweep - }) - it('settles an in-flight new partition on the latest swept policy', async () => { let proxyUrl = 'http://old.example:8080' let finishFirstWrite: (() => void) | undefined @@ -218,29 +113,6 @@ describe('browser session proxy', () => { ]) }) - it('reads settings through the injected resolver when none are passed', async () => { - setBrowserNetworkProxySettingsResolver(() => ({ - httpProxyUrl: 'socks5://127.0.0.1:1080', - httpProxyBypassRules: '' - })) - const sess = fromPartitionMock('persist:orca-browser') - - await applyProxyToBrowserSession(sess as never) - - expect(sess.setProxy).toHaveBeenCalledWith({ - mode: 'fixed_servers', - proxyRules: 'socks5://127.0.0.1:1080' - }) - }) - - it('makes no proxy write when no resolver is registered', async () => { - const sess = fromPartitionMock('persist:orca-browser') - - await applyProxyToBrowserSession(sess as never) - - expect(sess.setProxy).not.toHaveBeenCalled() - }) - describe('startup readiness', () => { beforeEach(() => { for (const key of [ @@ -292,24 +164,6 @@ describe('browser session proxy', () => { } }) - it('retries only a failed installation after its bounded attempts are exhausted', async () => { - const failed = fromPartition('persist:orca-browser') - const healthy = fromPartition('persist:orca-browser-session-iso') - for (let attempt = 0; attempt < 3; attempt++) { - failed.resolveProxy.mockRejectedValueOnce(new Error('native proxy failure')) - } - const initialFailure = expect(applyProxySettingsToSession(failed, {})).rejects.toThrow( - 'native proxy failure' - ) - await applyProxySettingsToSession(healthy, {}) - await initialFailure - - await awaitBrowserSessionProxies(PROFILES) - - expect(failed.resolveProxy).toHaveBeenCalledTimes(4) - expect(healthy.resolveProxy).toHaveBeenCalledOnce() - }) - it('awaits every installation and preserves failed-partition warnings and retry budget', async () => { const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) const failed = fromPartition('persist:orca-browser') diff --git a/src/main/browser/sessions/browser-session-registry-identity.persistence.test.ts b/src/main/browser/sessions/browser-session-registry-identity.persistence.test.ts index a19bfe87515..afbb6c3b613 100644 --- a/src/main/browser/sessions/browser-session-registry-identity.persistence.test.ts +++ b/src/main/browser/sessions/browser-session-registry-identity.persistence.test.ts @@ -1,10 +1,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { - CLEAN_USER_AGENT, createFsState, IDENTITY_RECORD_PATH, installModuleMocks, - META_PATH, seedMeta } from './browser-session-registry-persistence-test-fixture' @@ -14,84 +12,6 @@ describe('BrowserSessionRegistry retired identity data', () => { vi.restoreAllMocks() }) - it('ignores legacy persisted UAs, valid or broken, and applies the engine UA', async () => { - const importedPartition = 'persist:orca-browser-session-11111111-1111-4111-8111-111111111111' - const brokenUa = - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/1.158.1 Safari/537.36' - const validUa = 'Mozilla/5.0 Chrome/120.0.0.0 Safari/537.36' - const fsState = createFsState() - seedMeta(fsState, { - defaultSource: { browserFamily: 'arc', importedAt: 1 }, - userAgent: brokenUa, - userAgentByPartition: { - 'persist:orca-browser': brokenUa, - [importedPartition]: validUa - }, - pendingCookieDbPath: null, - pendingCookieImports: {}, - profiles: [ - { - id: '11111111-1111-4111-8111-111111111111', - scope: 'imported', - partition: importedPartition, - label: 'Imported', - source: { browserFamily: 'chrome', importedAt: 1 } - } - ] - }) - - const { sessionFromPartitionMock, installBrowserSessionUserAgentPolicyMock } = - installModuleMocks(fsState) - const { browserSessionRegistry } = await import('./browser-session-registry') - - browserSessionRegistry.initializeBrowserSessionsFromPersistedState() - - const appliedUas = sessionFromPartitionMock.mock.results.flatMap((r) => - r.value.setUserAgent.mock.calls.map((c: unknown[]) => c[0]) - ) - expect(appliedUas).not.toContain(brokenUa) - expect(appliedUas).not.toContain(validUa) - expect(appliedUas.length).toBeGreaterThan(0) - expect(appliedUas.every((ua) => ua === CLEAN_USER_AGENT)).toBe(true) - expect(installBrowserSessionUserAgentPolicyMock).toHaveBeenCalled() - }) - - it('flags the retired per-profile choice without rewriting its persisted bytes', async () => { - const importedPartition = 'persist:orca-browser-session-11111111-1111-4111-8111-111111111111' - const fsState = createFsState() - seedMeta(fsState, { - defaultSource: null, - profiles: [ - { - id: '11111111-1111-4111-8111-111111111111', - scope: 'imported', - partition: importedPartition, - label: 'Imported', - source: { browserFamily: 'comet', importedAt: 1 }, - userAgentMode: 'native' - } - ] - }) - - installModuleMocks(fsState) - const { browserSessionRegistry } = await import('./browser-session-registry') - - browserSessionRegistry.initializeBrowserSessionsFromPersistedState() - - await vi.waitFor(() => - expect(JSON.parse(fsState.files.get(IDENTITY_RECORD_PATH) ?? '{}')).toEqual({ - version: 1, - mode: 'clean', - explicitSelection: false, - migrationNoticePending: true - }) - ) - // Preserve retired choices for rollback. - expect(JSON.parse(fsState.files.get(META_PATH) ?? '{}').profiles[0].userAgentMode).toBe( - 'native' - ) - }) - it('does not re-arm the retired-choice notice on the launch after an explicit choice', async () => { const profileId = '11111111-1111-4111-8111-111111111111' const fsState = createFsState() @@ -128,82 +48,4 @@ describe('BrowserSessionRegistry retired identity data', () => { migrationNoticePending: false }) }) - - it.each([ - { scenario: 'malformed members', malformed: [null, 42, 'broken'], failWrite: false }, - { scenario: 'a read-only notice', malformed: [], failWrite: true } - ])('hydrates the valid profile despite $scenario', async ({ malformed, failWrite }) => { - const profileId = '11111111-1111-4111-8111-111111111111' - const partition = `persist:orca-browser-session-${profileId}` - const fsState = createFsState() - seedMeta(fsState, { - defaultSource: null, - profiles: [ - ...malformed, - { - id: profileId, - scope: 'isolated', - partition, - label: 'Existing', - source: null, - userAgentMode: 'native' - } - ] - }) - installModuleMocks(fsState, new Set(), failWrite) - const errors = vi.spyOn(console, 'error').mockImplementation(() => {}) - const { browserSessionRegistry } = await import('./browser-session-registry') - - expect(() => browserSessionRegistry.initializeBrowserSessionsFromPersistedState()).not.toThrow() - expect(browserSessionRegistry.getProfile(profileId)?.partition).toBe(partition) - if (failWrite) { - await vi.waitFor(() => expect(errors).toHaveBeenCalled()) - expect(errors.mock.calls[0]?.[1]).toMatchObject({ message: 'read-only userData' }) - } - await vi.waitFor(() => expect(fsState.files.has(IDENTITY_RECORD_PATH)).toBe(!failWrite)) - const written = JSON.parse(fsState.files.get(META_PATH) ?? '{}') - expect(written.profiles).toHaveLength(malformed.length + 1) - expect(written.profiles.at(-1).userAgentMode).toBe('native') - }) - - it('hydrates a retired native profile under the process identity', async () => { - const importedPartition = 'persist:orca-browser-session-12121212-1212-4121-8121-121212121212' - const fsState = createFsState() - seedMeta(fsState, { - defaultSource: null, - userAgent: null, - userAgentByPartition: {}, - pendingCookieDbPath: null, - pendingCookieImports: {}, - profiles: [ - { - id: '12121212-1212-4121-8121-121212121212', - scope: 'isolated', - partition: importedPartition, - label: 'Google', - source: null, - userAgentMode: 'native' - } - ] - }) - - const { sessionFromPartitionMock, installBrowserSessionUserAgentPolicyMock } = - installModuleMocks(fsState) - const { browserSessionRegistry } = await import('./browser-session-registry') - - browserSessionRegistry.initializeBrowserSessionsFromPersistedState() - - const importedSessions = sessionFromPartitionMock.mock.results - .filter((_, index) => sessionFromPartitionMock.mock.calls[index]?.[0] === importedPartition) - .map((result) => result.value) - expect(importedSessions.length).toBeGreaterThan(0) - expect( - importedSessions.every((sess) => sess.setUserAgent.mock.calls[0]?.[0] === CLEAN_USER_AGENT) - ).toBe(true) - expect( - installBrowserSessionUserAgentPolicyMock.mock.calls.some( - ([sess]) => sess.partition === importedPartition - ) - ).toBe(true) - }) }) diff --git a/src/main/browser/sessions/browser-session-registry.persistence.test.ts b/src/main/browser/sessions/browser-session-registry.persistence.test.ts deleted file mode 100644 index 5c304020e66..00000000000 --- a/src/main/browser/sessions/browser-session-registry.persistence.test.ts +++ /dev/null @@ -1,518 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { - CLEAN_USER_AGENT, - createFsState, - installModuleMocks, - META_PATH, - seedMeta, - seedSitePermissions -} from './browser-session-registry-persistence-test-fixture' - -describe('BrowserSessionRegistry persistence', () => { - beforeEach(() => { - vi.resetModules() - vi.restoreAllMocks() - }) - - it('expires a legacy pendingCookieDbPath without copying it over the live jar', async () => { - const fsState = createFsState() - seedMeta(fsState, { - defaultSource: null, - userAgent: null, - pendingCookieDbPath: '/staged/legacy', - profiles: [] - }) - fsState.present.add('/staged/legacy') - - installModuleMocks(fsState) - const { browserSessionRegistry } = await import('./browser-session-registry') - - browserSessionRegistry.applyPendingCookieImport() - - const written = JSON.parse(fsState.files.get(META_PATH) ?? '{}') - expect(written.pendingCookieDbPath).toBeNull() - expect(written.pendingCookieImports).toEqual({}) - expect(fsState.present.has('/user-data/Partitions/orca-browser/Cookies')).toBe(false) - }) - - it('keeps the existing Network database when expiring an unmarked legacy image', async () => { - const stagedPath = '/staged/network-import' - const networkPath = '/user-data/Partitions/orca-browser/Network/Cookies' - const legacyPath = '/user-data/Partitions/orca-browser/Cookies' - const fsState = createFsState() - seedMeta(fsState, { - defaultSource: null, - userAgent: null, - pendingCookieDbPath: stagedPath, - profiles: [] - }) - fsState.files.set(stagedPath, 'imported cookies') - fsState.files.set(networkPath, 'old cookies') - fsState.present.add(stagedPath) - fsState.present.add(networkPath) - - installModuleMocks(fsState) - const { browserSessionRegistry } = await import('./browser-session-registry') - - browserSessionRegistry.applyPendingCookieImport() - - expect(fsState.files.get(networkPath)).toBe('old cookies') - expect(fsState.present.has(legacyPath)).toBe(false) - }) - - it('persists new browser session profiles under the active Orca profile directory', async () => { - const fsState = createFsState() - const profileMetaPath = '/user-data/profiles/local-work/browser-session-meta.json' - - installModuleMocks(fsState) - const { browserSessionRegistry } = await import('./browser-session-registry') - - browserSessionRegistry.configureForOrcaProfile({ - orcaProfileId: 'local-work', - profileDirectory: '/user-data/profiles/local-work' - }) - const profile = await browserSessionRegistry.createProfile('isolated', 'Work Browser') - - expect(profile).not.toBeNull() - expect(fsState.files.has(profileMetaPath)).toBe(true) - expect(fsState.files.has(META_PATH)).toBe(false) - expect(JSON.parse(fsState.files.get(profileMetaPath) ?? '{}').profiles[0]).toMatchObject({ - id: profile!.id, - partition: profile!.partition, - label: 'Work Browser' - }) - }) - - it('applies the process identity and request exceptions to new profiles', async () => { - const fsState = createFsState() - const { sessionFromPartitionMock, installBrowserSessionUserAgentPolicyMock } = - installModuleMocks(fsState) - const { browserSessionRegistry } = await import('./browser-session-registry') - - await browserSessionRegistry.createProfile('isolated', 'Default identity') - - const profileSession = sessionFromPartitionMock.mock.results.at(-1)?.value - expect(profileSession.setUserAgent).toHaveBeenCalledWith(CLEAN_USER_AGENT) - expect(installBrowserSessionUserAgentPolicyMock).toHaveBeenCalledWith( - profileSession, - expect.any(Function) - ) - }) - - it('clears only the requested partition and unlinks its staged database files', async () => { - const otherPartition = 'persist:orca-browser-session-bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb' - const fsState = createFsState() - seedMeta(fsState, { - defaultSource: null, - userAgent: null, - userAgentByPartition: {}, - pendingCookieDbPath: '/staged/default', - pendingCookieImports: { - 'persist:orca-browser': '/staged/default', - [otherPartition]: '/staged/other' - }, - profiles: [] - }) - for (const suffix of ['', '-wal', '-shm']) { - fsState.files.set(`/staged/other${suffix}`, 'db') - fsState.present.add(`/staged/other${suffix}`) - fsState.files.set(`/staged/default${suffix}`, 'db') - fsState.present.add(`/staged/default${suffix}`) - } - - installModuleMocks(fsState) - const { browserSessionRegistry } = await import('./browser-session-registry') - - browserSessionRegistry.clearPendingCookieImport(otherPartition) - - const written = JSON.parse(fsState.files.get(META_PATH) ?? '{}') - expect(written.pendingCookieImports).toEqual({ 'persist:orca-browser': '/staged/default' }) - expect(written.pendingCookieDbPath).toBe('/staged/default') - for (const suffix of ['', '-wal', '-shm']) { - expect(fsState.present.has(`/staged/other${suffix}`)).toBe(false) - expect(fsState.present.has(`/staged/default${suffix}`)).toBe(true) - } - }) - - it('drops the legacy pointer when the default partition is the one cleared', async () => { - const otherPartition = 'persist:orca-browser-session-cccccccc-cccc-4ccc-8ccc-cccccccccccc' - const fsState = createFsState() - seedMeta(fsState, { - defaultSource: null, - userAgent: null, - userAgentByPartition: {}, - pendingCookieDbPath: '/staged/default', - pendingCookieImports: { - 'persist:orca-browser': '/staged/default', - [otherPartition]: '/staged/other' - }, - profiles: [] - }) - - installModuleMocks(fsState) - const { browserSessionRegistry } = await import('./browser-session-registry') - - browserSessionRegistry.clearPendingCookieImport('persist:orca-browser') - - const written = JSON.parse(fsState.files.get(META_PATH) ?? '{}') - expect(written.pendingCookieImports).toEqual({ [otherPartition]: '/staged/other' }) - expect(written.pendingCookieDbPath).toBeNull() - }) - - it('is a no-op when the partition has no pending import', async () => { - const fsState = createFsState() - seedMeta(fsState, { - defaultSource: null, - userAgent: null, - userAgentByPartition: {}, - pendingCookieDbPath: '/staged/default', - pendingCookieImports: { 'persist:orca-browser': '/staged/default' }, - profiles: [] - }) - fsState.files.set('/staged/default', 'db') - fsState.present.add('/staged/default') - - installModuleMocks(fsState) - const { browserSessionRegistry } = await import('./browser-session-registry') - const metaBefore = fsState.files.get(META_PATH) - - browserSessionRegistry.clearPendingCookieImport('persist:orca-browser-session-unknown') - - expect(fsState.files.get(META_PATH)).toBe(metaBefore) - expect(fsState.present.has('/staged/default')).toBe(true) - }) - - it('sets up default-partition policies on restore', async () => { - const fsState = createFsState() - seedMeta(fsState, { - defaultSource: null, - userAgent: null, - userAgentByPartition: {}, - pendingCookieDbPath: null, - pendingCookieImports: {}, - profiles: [] - }) - - seedSitePermissions(fsState, { default: { 'https://example.com': { camera: 'allow' } } }) - - const { - sessionFromPartitionMock, - browserManagerHandleGuestWillDownloadMock, - browserManagerNotifyPermissionDeniedMock - } = installModuleMocks(fsState) - const { browserSessionRegistry } = await import('./browser-session-registry') - - browserSessionRegistry.initializeBrowserSessionsFromPersistedState() - - const defaultSessions = sessionFromPartitionMock.mock.results - .filter((_, idx) => sessionFromPartitionMock.mock.calls[idx]?.[0] === 'persist:orca-browser') - .map((r) => r.value) - expect(defaultSessions.length).toBeGreaterThan(0) - const defaultSession = defaultSessions[0] - const requestHandler = defaultSession.setPermissionRequestHandler.mock.calls[0][0] - const checkHandler = defaultSession.setPermissionCheckHandler.mock.calls[0][0] - const guestWc = { id: 401, getURL: vi.fn(() => 'https://example.com/account') } - const permissionCallback = vi.fn() - const decisions = new Map() - const request = ( - permission: string, - details: Partial = {} - ): void => - requestHandler( - guestWc, - permission, - (allowed: boolean) => decisions.set(permission, allowed), - details - ) - - // Only camera has a saved answer; this guest has no prompt window. - request('fullscreen') - request('clipboard-read') - request('clipboard-sanitized-write') - request('notifications') - request('persistent-storage') - request('geolocation') - request('media', { mediaTypes: ['video'] }) - - await vi.waitFor(() => expect(decisions.size).toBe(7)) - expect(Object.fromEntries(decisions)).toEqual({ - fullscreen: true, - 'clipboard-read': false, - 'clipboard-sanitized-write': true, - notifications: false, - 'persistent-storage': true, - geolocation: false, - media: true - }) - expect(browserManagerNotifyPermissionDeniedMock).not.toHaveBeenCalled() - - requestHandler(guestWc, 'mediaKeySystem', permissionCallback, { - requestingUrl: 'https://widget.example.net/embed', - isMainFrame: false - }) - expect(browserManagerNotifyPermissionDeniedMock).toHaveBeenCalledWith({ - guestWebContentsId: 401, - permission: 'mediaKeySystem', - rawUrl: 'https://widget.example.net/embed' - }) - - browserManagerNotifyPermissionDeniedMock.mockClear() - requestHandler(guestWc, 'mediaKeySystem', permissionCallback, { isMainFrame: true }) - requestHandler(guestWc, 'mediaKeySystem', permissionCallback, { - requestingUrl: '', - isMainFrame: false - }) - expect( - browserManagerNotifyPermissionDeniedMock.mock.calls.map(([args]) => args.rawUrl) - ).toEqual(['https://example.com/account', 'https://example.com/account']) - - browserManagerNotifyPermissionDeniedMock.mockClear() - requestHandler(guestWc, 'mediaKeySystem', permissionCallback, { - requestingUrl: 'about:blank', - isMainFrame: false - }) - expect(browserManagerNotifyPermissionDeniedMock).toHaveBeenCalledWith({ - guestWebContentsId: 401, - permission: 'mediaKeySystem', - rawUrl: '' - }) - expect(checkHandler(null, 'fullscreen', '')).toBe(true) - expect(checkHandler(null, 'clipboard-read', 'https://example.com')).toBe(false) - expect(checkHandler(null, 'clipboard-sanitized-write', '')).toBe(true) - expect(checkHandler(null, 'notifications', 'https://example.com')).toBe(false) - expect(checkHandler(null, 'persistent-storage', '')).toBe(true) - expect(checkHandler(null, 'geolocation', 'https://example.com')).toBe(false) - expect(checkHandler(guestWc, 'media', '', { mediaType: 'video' })).toBe(true) - expect(checkHandler(guestWc, 'media', '', { mediaType: 'audio' })).toBe(false) - - requestHandler(guestWc, 'storage-access', permissionCallback, {}) - expect(permissionCallback).toHaveBeenLastCalledWith(true) - expect(checkHandler(null, 'storage-access', '')).toBe(true) - - // Related Website Sets gate top-level storage access; Orca has no such data source. - requestHandler(guestWc, 'top-level-storage-access', permissionCallback, {}) - expect(permissionCallback).toHaveBeenLastCalledWith(false) - expect(checkHandler(null, 'top-level-storage-access', '')).toBe(false) - - expect( - browserManagerNotifyPermissionDeniedMock.mock.calls.map(([args]) => args.permission) - ).toEqual(['mediaKeySystem', 'top-level-storage-access']) - expect(defaultSession.setDisplayMediaRequestHandler).toHaveBeenCalled() - const displayMediaHandler = defaultSession.setDisplayMediaRequestHandler.mock.calls[0][0] - const displayMediaCallback = vi.fn() - displayMediaHandler(null, displayMediaCallback) - expect(displayMediaCallback).toHaveBeenCalledWith({ video: undefined, audio: undefined }) - - const devicePermissionHandler = defaultSession.setDevicePermissionHandler.mock.calls[0][0] - expect( - devicePermissionHandler({ - deviceType: 'hid', - origin: 'https://github.com', - device: { collections: [{ usagePage: 0xf1d0 }] } - }) - ).toBe(true) - expect(checkHandler(null, 'hid', '', { securityOrigin: 'https://github.com' })).toBe(true) - - const selectHidHandler = defaultSession.on.mock.calls.find( - ([eventName]: unknown[]) => eventName === 'select-hid-device' - )?.[1] as ( - event: { preventDefault: () => void }, - details: { - deviceList: { deviceId: string; collections?: { usagePage?: number }[] }[] - frame: { url: string } - }, - callback: (deviceId?: string) => void - ) => void - const hidCallback = vi.fn() - selectHidHandler( - { preventDefault: vi.fn() }, - { - frame: { url: 'https://github.com' }, - deviceList: [ - { deviceId: 'keyboard', collections: [{ usagePage: 1 }] }, - { deviceId: 'security-key', collections: [{ usagePage: 0xf1d0 }] } - ] - }, - hidCallback - ) - expect(hidCallback).toHaveBeenCalledWith('security-key') - - const selectWebAuthnHandler = defaultSession.on.mock.calls.find( - ([eventName]: unknown[]) => eventName === 'select-webauthn-account' - )?.[1] as ( - event: { preventDefault: () => void }, - details: { accounts: { credentialId: string }[] }, - callback: (credentialId?: string | null) => void - ) => void - const webAuthnCallback = vi.fn() - selectWebAuthnHandler( - { preventDefault: vi.fn() }, - { accounts: [{ credentialId: 'credential-1' }] }, - webAuthnCallback - ) - expect(webAuthnCallback).toHaveBeenCalledWith('credential-1') - - const willDownloadHandler = defaultSession.on.mock.calls.find( - ([eventName]: unknown[]) => eventName === 'will-download' - )?.[1] as ( - event: unknown, - item: { getFilename: () => string }, - webContents: { id: number } - ) => void - expect(willDownloadHandler).toBeTypeOf('function') - const item = { getFilename: vi.fn(() => 'report.pdf') } - willDownloadHandler({}, item, { id: 402 }) - expect(browserManagerHandleGuestWillDownloadMock).toHaveBeenCalledWith({ - guestWebContentsId: 402, - item - }) - }) - - it('does not stack default-partition policy handlers on repeated restore', async () => { - const fsState = createFsState() - seedMeta(fsState, { - defaultSource: null, - userAgent: null, - userAgentByPartition: {}, - pendingCookieDbPath: null, - pendingCookieImports: {}, - profiles: [] - }) - - const { sessionFromPartitionMock } = installModuleMocks(fsState) - const { browserSessionRegistry } = await import('./browser-session-registry') - - browserSessionRegistry.initializeBrowserSessionsFromPersistedState() - browserSessionRegistry.initializeBrowserSessionsFromPersistedState() - - const defaultSessions = sessionFromPartitionMock.mock.results - .filter((_, idx) => sessionFromPartitionMock.mock.calls[idx]?.[0] === 'persist:orca-browser') - .map((r) => r.value) - const policySessions = defaultSessions.filter( - (s) => s.setPermissionRequestHandler.mock.calls.length > 0 - ) - expect(policySessions).toHaveLength(1) - expect( - policySessions[0].on.mock.calls.filter( - ([eventName]: unknown[]) => eventName === 'will-download' - ) - ).toHaveLength(1) - expect( - policySessions[0].on.mock.calls.filter( - ([eventName]: unknown[]) => eventName === 'select-hid-device' - ) - ).toHaveLength(1) - expect( - policySessions[0].on.mock.calls.filter( - ([eventName]: unknown[]) => eventName === 'select-webauthn-account' - ) - ).toHaveLength(1) - }) - - it('notifies when default-partition media permission is denied', async () => { - const fsState = createFsState() - seedMeta(fsState, { - defaultSource: null, - userAgent: null, - userAgentByPartition: {}, - pendingCookieDbPath: null, - pendingCookieImports: {}, - profiles: [] - }) - - seedSitePermissions(fsState, { default: { 'https://example.com': { camera: 'allow' } } }) - const { - sessionFromPartitionMock, - browserManagerNotifyPermissionDeniedMock, - requestSystemMediaAccessMock - } = installModuleMocks(fsState) - requestSystemMediaAccessMock.mockResolvedValue(false) - const { browserSessionRegistry } = await import('./browser-session-registry') - - browserSessionRegistry.initializeBrowserSessionsFromPersistedState() - - const defaultSession = sessionFromPartitionMock.mock.results.find( - (_, idx) => sessionFromPartitionMock.mock.calls[idx]?.[0] === 'persist:orca-browser' - )?.value - const requestHandler = defaultSession.setPermissionRequestHandler.mock.calls[0][0] - const guestWc = { id: 403, getURL: vi.fn(() => 'https://example.com/camera') } - const callback = vi.fn() - - requestHandler(guestWc, 'media', callback, { mediaTypes: ['video'] }) - guestWc.getURL.mockReturnValue('https://example.com/after-navigation') - - await vi.waitFor(() => expect(callback).toHaveBeenCalledWith(false)) - expect(browserManagerNotifyPermissionDeniedMock).toHaveBeenCalledWith({ - guestWebContentsId: 403, - permission: 'media', - rawUrl: 'https://example.com/camera' - }) - }) - - it('expires an old unmarked stage and removes unrelated missing entries', async () => { - const importedPartition = 'persist:orca-browser-session-22222222-2222-4222-8222-222222222222' - const fsState = createFsState() - seedMeta(fsState, { - defaultSource: null, - userAgent: null, - userAgentByPartition: {}, - pendingCookieDbPath: null, - pendingCookieImports: { - [importedPartition]: '/staged/imported', - 'persist:orca-browser': '/staged/missing' - }, - profiles: [ - { - id: '22222222-2222-4222-8222-222222222222', - scope: 'imported', - partition: importedPartition, - label: 'Imported', - source: { browserFamily: 'comet', importedAt: 1 } - } - ] - }) - fsState.present.add('/staged/imported') - - installModuleMocks(fsState, new Set(['/staged/imported'])) - const { browserSessionRegistry } = await import('./browser-session-registry') - - browserSessionRegistry.applyPendingCookieImport() - - const written = JSON.parse(fsState.files.get(META_PATH) ?? '{}') - expect(written.pendingCookieImports).toEqual({}) - expect(written.pendingCookieDbPath).toBeNull() - }) - - it('ignores pending cookie imports for invalid persisted profile partitions', async () => { - const invalidPartition = 'persist:../../outside' - const fsState = createFsState() - seedMeta(fsState, { - defaultSource: null, - userAgent: null, - userAgentByPartition: {}, - pendingCookieDbPath: null, - pendingCookieImports: { - [invalidPartition]: '/staged/evil' - }, - profiles: [ - { - id: 'profile-1', - scope: 'imported', - partition: invalidPartition, - label: 'Invalid', - source: null - } - ] - }) - fsState.present.add('/staged/evil') - - installModuleMocks(fsState) - const { browserSessionRegistry } = await import('./browser-session-registry') - - browserSessionRegistry.applyPendingCookieImport() - - const written = JSON.parse(fsState.files.get(META_PATH) ?? '{}') - expect(written.pendingCookieImports).toEqual({}) - expect(fsState.present.has('/outside/Cookies')).toBe(false) - }) -}) diff --git a/src/main/browser/sessions/browser-session-registry.test.ts b/src/main/browser/sessions/browser-session-registry.test.ts index 826c28a4eeb..dab6d3985df 100644 --- a/src/main/browser/sessions/browser-session-registry.test.ts +++ b/src/main/browser/sessions/browser-session-registry.test.ts @@ -1,7 +1,7 @@ -import { mkdtempSync } from 'node:fs' +import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const { sessionFromPartitionMock, @@ -69,20 +69,24 @@ import { listBrowserSitePermissions, setBrowserSitePermissionDecisions } from '../site-settings/browser-site-permission-store' -import { googleAuthUserAgent } from '../identity/browser-google-auth-ua' -import { installBrowserSessionUserAgentPolicy } from '../identity/browser-session-ua' import { setBrowserNetworkProxySettingsResolver } from './browser-session-proxy' import { handleElectronProxyLogin } from '../../network/electron-proxy-credentials' import { applyProxySettingsToSession } from '../../network/proxy-settings' -import { ORCA_BROWSER_PARTITION } from '../../../shared/constants' -import { - DEFAULT_LOCAL_ORCA_PROFILE_ID, - getOrcaProfileBrowserDefaultPartition, - getOrcaProfileBrowserSessionPartition -} from '../../../shared/orca-profiles' +import { DEFAULT_LOCAL_ORCA_PROFILE_ID } from '../../../shared/orca-profiles' describe('BrowserSessionRegistry', () => { + let profileDirectory: string + + afterEach(() => { + rmSync(profileDirectory, { recursive: true, force: true }) + }) + beforeEach(() => { + profileDirectory = mkdtempSync(join(tmpdir(), 'orca-session-registry-')) + browserSessionRegistry.configureForOrcaProfile({ + orcaProfileId: DEFAULT_LOCAL_ORCA_PROFILE_ID, + profileDirectory + }) sessionFromPartitionMock.mockReset() askForMediaAccessMock.mockReset() getMediaAccessStatusMock.mockReset() @@ -108,84 +112,6 @@ describe('BrowserSessionRegistry', () => { }) }) - it('has a default profile on construction', () => { - const defaultProfile = browserSessionRegistry.getDefaultProfile() - expect(defaultProfile.id).toBe('default') - expect(defaultProfile.scope).toBe('default') - expect(defaultProfile.partition).toBe(ORCA_BROWSER_PARTITION) - }) - - it.each([10, 100])( - 'resolves only known actual Sessions with linear profile work at %s', - (population) => { - const directory = mkdtempSync(join(tmpdir(), 'orca-session-media-')) - browserSessionRegistry.configureForOrcaProfile({ - orcaProfileId: DEFAULT_LOCAL_ORCA_PROFILE_ID, - profileDirectory: directory - }) - const policySession = sessionFromPartitionMock() - const sessions = new Map() - sessionFromPartitionMock.mockImplementation((partition: string) => { - const known = sessions.get(partition) - if (!known) { - throw new Error('Lookup attempted to materialize an unknown Session') - } - return known - }) - const profiles = Array.from({ length: population }, (_, index) => { - const id = `00000000-0000-0000-0000-${String(index + 1).padStart(12, '0')}` - const partition = getOrcaProfileBrowserSessionPartition(DEFAULT_LOCAL_ORCA_PROFILE_ID, id) - sessions.set(partition, { ...policySession }) - return { - id, - partition, - scope: 'isolated' as const, - label: `Profile ${index}`, - source: null - } - }) - const defaultProfile = browserSessionRegistry.getDefaultProfile() - sessions.set(defaultProfile.partition, { ...policySession }) - browserSessionRegistry.hydrateFromPersisted(profiles) - sessionFromPartitionMock.mockClear() - expect( - browserSessionRegistry.profileIdForSession(sessions.get(defaultProfile.partition)!) - ).toBe('default') - expect(sessionFromPartitionMock).toHaveBeenCalledTimes(1) - sessionFromPartitionMock.mockClear() - const last = profiles.at(-1)! - expect(browserSessionRegistry.profileIdForSession(sessions.get(last.partition)!)).toBe( - last.id - ) - expect(sessionFromPartitionMock).toHaveBeenCalledTimes(population + 1) - sessionFromPartitionMock.mockClear() - expect(browserSessionRegistry.profileIdForSession({ ...policySession })).toBeNull() - expect(sessionFromPartitionMock).toHaveBeenCalledTimes(population + 1) - browserSessionRegistry.configureForOrcaProfile({ - orcaProfileId: DEFAULT_LOCAL_ORCA_PROFILE_ID, - profileDirectory: directory - }) - } - ) - - it('allows the default partition', () => { - expect(browserSessionRegistry.isAllowedPartition(ORCA_BROWSER_PARTITION)).toBe(true) - }) - - it('rejects unknown partitions', () => { - expect(browserSessionRegistry.isAllowedPartition('persist:evil-partition')).toBe(false) - }) - - it('creates an isolated profile with a unique partition', async () => { - const profile = await browserSessionRegistry.createProfile('isolated', 'Test Isolated') - expect(profile).not.toBeNull() - expect(profile!.scope).toBe('isolated') - expect(profile!.partition).toMatch(/^persist:orca-browser-session-/) - expect(profile!.partition).not.toBe(ORCA_BROWSER_PARTITION) - expect(profile!.label).toBe('Test Isolated') - expect(profile!.source).toBeNull() - }) - it('does not return a runtime profile until its proxy is ready', async () => { let finishWrite: (() => void) | undefined let proxyReady = false @@ -282,79 +208,6 @@ describe('BrowserSessionRegistry', () => { ).rejects.toThrow('retired') }) - it('rejects creating a profile with scope default', async () => { - const profile = await browserSessionRegistry.createProfile('default', 'Sneaky') - expect(profile).toBeNull() - }) - - it('allows created profile partitions', async () => { - const profile = await browserSessionRegistry.createProfile('isolated', 'Allowed') - expect(profile).not.toBeNull() - expect(browserSessionRegistry.isAllowedPartition(profile!.partition)).toBe(true) - }) - - it('creates an imported profile', async () => { - const profile = await browserSessionRegistry.createProfile('imported', 'My Import') - expect(profile).not.toBeNull() - expect(profile!.scope).toBe('imported') - expect(profile!.partition).toMatch(/^persist:orca-browser-session-/) - }) - - it('resolves partition for a known profile', async () => { - const profile = await browserSessionRegistry.createProfile('isolated', 'Resolve Test') - expect(profile).not.toBeNull() - expect(browserSessionRegistry.resolvePartition(profile!.id)).toBe(profile!.partition) - }) - - it('resolves default partition for null/undefined profileId', () => { - expect(browserSessionRegistry.resolvePartition(null)).toBe(ORCA_BROWSER_PARTITION) - expect(browserSessionRegistry.resolvePartition(undefined)).toBe(ORCA_BROWSER_PARTITION) - }) - - it('resolves default partition for unknown profileId', () => { - expect(browserSessionRegistry.resolvePartition('nonexistent')).toBe(ORCA_BROWSER_PARTITION) - }) - - it('strictly resolves known profile partitions without downgrading unknown profiles', async () => { - const profile = await browserSessionRegistry.createProfile('isolated', 'Strict Resolve') - expect(profile).not.toBeNull() - - expect(browserSessionRegistry.resolveKnownPartition(null)).toBe(ORCA_BROWSER_PARTITION) - expect(browserSessionRegistry.resolveKnownPartition(undefined)).toBe(ORCA_BROWSER_PARTITION) - expect(browserSessionRegistry.resolveKnownPartition('default')).toBe(ORCA_BROWSER_PARTITION) - expect(browserSessionRegistry.resolveKnownPartition(profile!.id)).toBe(profile!.partition) - expect(browserSessionRegistry.resolveKnownPartition('missing-profile')).toBeNull() - }) - - it('lists all profiles', async () => { - const before = browserSessionRegistry.listProfiles().length - await browserSessionRegistry.createProfile('isolated', 'List Test') - const after = browserSessionRegistry.listProfiles() - expect(after.length).toBe(before + 1) - }) - - it('updates profile source', async () => { - const profile = await browserSessionRegistry.createProfile('imported', 'Source Test') - expect(profile).not.toBeNull() - const updated = browserSessionRegistry.updateProfileSource(profile!.id, { - browserFamily: 'edge', - importedAt: Date.now() - }) - expect(updated).not.toBeNull() - expect(updated!.source?.browserFamily).toBe('edge') - }) - - it('updates profile source with comet family', async () => { - const profile = await browserSessionRegistry.createProfile('imported', 'Comet Source Test') - expect(profile).not.toBeNull() - const updated = browserSessionRegistry.updateProfileSource(profile!.id, { - browserFamily: 'comet', - importedAt: Date.now() - }) - expect(updated).not.toBeNull() - expect(updated!.source?.browserFamily).toBe('comet') - }) - it('deletes a non-default profile and its remembered site answers', async () => { configureBrowserSitePermissionStore(mkdtempSync(join(tmpdir(), 'orca-site-permissions-'))) const profile = await browserSessionRegistry.createProfile('isolated', 'Delete Test') @@ -368,63 +221,7 @@ describe('BrowserSessionRegistry', () => { expect(listBrowserSitePermissions(profile!.id)).toEqual([]) }) - it('keeps a profile whose saved logins could not be deleted', async () => { - const profile = await browserSessionRegistry.createProfile('isolated', 'Vault Stuck') - forgetBrowserPasswordVaultMock.mockReturnValueOnce(false) - - await expect(browserSessionRegistry.deleteProfile(profile!.id)).resolves.toBe(false) - expect(browserSessionRegistry.getProfile(profile!.id)).not.toBeNull() - }) - - it('retains session security policies when deleting a profile', async () => { - const profile = await browserSessionRegistry.createProfile('isolated', 'Policy Delete Test') - expect(profile).not.toBeNull() - const mockSession = sessionFromPartitionMock.mock.results[0]?.value - const permissionWrites = mockSession.setPermissionRequestHandler.mock.calls.length - const downloadListenerWrites = mockSession.removeListener.mock.calls.length - - await expect(browserSessionRegistry.deleteProfile(profile!.id)).resolves.toBe(true) - - expect(mockSession.setPermissionRequestHandler).toHaveBeenCalledTimes(permissionWrites) - expect(mockSession.removeListener).toHaveBeenCalledTimes(downloadListenerWrites) - expect(removeCertificateRequestGuardMock).not.toHaveBeenCalled() - }) - // Electron Sessions outlive deleted profiles; their header hooks must be retired. - it('retires the user agent policy when deleting a profile', async () => { - const profile = await browserSessionRegistry.createProfile('isolated', 'UA Delete Test') - const mockSession = sessionFromPartitionMock.mock.results[0]?.value - expect(mockSession.webRequest.onBeforeSendHeaders).toHaveBeenCalledWith( - expect.anything(), - expect.any(Function) - ) - - await expect(browserSessionRegistry.deleteProfile(profile!.id)).resolves.toBe(true) - - expect(mockSession.webRequest.onBeforeSendHeaders).toHaveBeenLastCalledWith(null) - }) - - it('keeps the request guard installed while deleted-profile guests remain', async () => { - setBrowserNetworkProxySettingsResolver(() => ({ - httpProxyUrl: 'http://proxy.example:8080', - httpProxyBypassRules: '' - })) - const profile = await browserSessionRegistry.createProfile('isolated', 'Delayed Delete') - const mockSession = sessionFromPartitionMock.mock.results[0]?.value - let finishClose: (() => void) | undefined - mockSession.closeAllConnections.mockImplementationOnce( - () => new Promise((resolve) => (finishClose = resolve)) - ) - removeCertificateRequestGuardMock.mockClear() - - const deletion = browserSessionRegistry.deleteProfile(profile!.id) - await vi.waitFor(() => expect(mockSession.setProxy).toHaveBeenCalledWith({ mode: 'system' })) - - expect(removeCertificateRequestGuardMock).not.toHaveBeenCalled() - finishClose?.() - await expect(deletion).resolves.toBe(true) - expect(removeCertificateRequestGuardMock).not.toHaveBeenCalled() - }) it('creates one managed profile per owner and a fresh one after its owner is removed', async () => { const owner = { kind: 'worktree' as const, id: 'repo-1::/work/feature' } @@ -476,301 +273,4 @@ describe('BrowserSessionRegistry', () => { browserSessionRegistry.onProfilesChanged = null } }) - - it('refuses to delete the default profile', async () => { - const deleted = await browserSessionRegistry.deleteProfile('default') - expect(deleted).toBe(false) - expect(browserSessionRegistry.getDefaultProfile()).not.toBeNull() - }) - - it('hydrates profiles from persisted data', () => { - const fakeProfile = { - id: '00000000-0000-0000-0000-000000000001', - scope: 'imported' as const, - partition: 'persist:orca-browser-session-00000000-0000-0000-0000-000000000001', - label: 'Hydrated', - source: { browserFamily: 'manual' as const, importedAt: 1000 } - } - browserSessionRegistry.hydrateFromPersisted([fakeProfile]) - expect(browserSessionRegistry.getProfile('00000000-0000-0000-0000-000000000001')).not.toBeNull() - expect(browserSessionRegistry.isAllowedPartition(fakeProfile.partition)).toBe(true) - }) - - it('rejects a persisted profile whose partition belongs to a different profile id', () => { - const profileId = '00000000-0000-4000-8000-000000000021' - const claimedPartition = 'persist:orca-browser-session-00000000-0000-4000-8000-000000000022' - - browserSessionRegistry.hydrateFromPersisted([ - { - id: profileId, - scope: 'isolated', - partition: claimedPartition, - label: 'Conflicting identity', - source: null - } - ]) - - expect(browserSessionRegistry.getProfile(profileId)).toBeNull() - expect(browserSessionRegistry.isAllowedPartition(claimedPartition)).toBe(false) - }) - - it('applies and clears existing browser-profile policy on an opaque route partition', async () => { - const partition = - 'persist:orca-browser-v1-1111111111111111222222222222222233333333333333334444444444444444' - setBrowserNetworkProxySettingsResolver(() => ({ - httpProxyUrl: 'http://app-proxy.example:8080', - httpProxyBypassRules: '' - })) - - browserSessionRegistry.setupRoutePartitionPolicies(partition, 'default') - - expect(sessionFromPartitionMock).toHaveBeenCalledWith(partition) - const configuredSession = sessionFromPartitionMock.mock.results[0]?.value - expect(configuredSession.setPermissionRequestHandler).toHaveBeenCalled() - expect(configuredSession.setPermissionCheckHandler).toHaveBeenCalled() - await new Promise((resolve) => setImmediate(resolve)) - expect(configuredSession.setProxy).not.toHaveBeenCalled() - - browserSessionRegistry.clearRoutePartitionPolicies(partition) - const clearedSession = sessionFromPartitionMock.mock.results.at(-1)?.value - expect(clearedSession.setPermissionRequestHandler).toHaveBeenCalledWith(null) - expect(clearedSession.setPermissionCheckHandler).toHaveBeenCalledWith(null) - }) - - it('rejects route partitions for missing browser profiles', () => { - const partition = - 'persist:orca-browser-v1-aaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbccccccccccccccccdddddddddddddddd' - - expect(() => - browserSessionRegistry.setupRoutePartitionPolicies(partition, 'missing-profile') - ).toThrow('browser_route_partition_profile_unavailable') - }) - - it('auto-grants pointer lock for browser partitions', async () => { - await browserSessionRegistry.createProfile('isolated', 'Pointer Lock Test') - const mockSession = sessionFromPartitionMock.mock.results[0]?.value - const requestHandler = mockSession.setPermissionRequestHandler.mock.calls[0][0] - const checkHandler = mockSession.setPermissionCheckHandler.mock.calls[0][0] - const callback = vi.fn() - const guestWc = { id: 7, getURL: vi.fn(() => 'https://example.com/') } - - requestHandler(guestWc, 'pointerLock', callback, {}) - - expect(callback).toHaveBeenCalledWith(true) - expect(checkHandler(null, 'pointerLock', '', {})).toBe(true) - }) - - it('asks macOS for the camera only after the user allowed the site', async () => { - configureBrowserSitePermissionStore(mkdtempSync(join(tmpdir(), 'orca-site-permissions-'))) - const profile = await browserSessionRegistry.createProfile('isolated', 'Media Test') - if (!profile) { - throw new Error('Expected the profile') - } - setBrowserSitePermissionDecisions(profile.id, 'https://example.com', ['camera'], 'allow') - const mockSession = sessionFromPartitionMock.mock.results[0]?.value - const requestHandler = mockSession.setPermissionRequestHandler.mock.calls[0][0] - const checkHandler = mockSession.setPermissionCheckHandler.mock.calls[0][0] - - const allowedSite = vi.fn() - const otherSite = vi.fn() - const allowedGuest = { id: 7, getURL: vi.fn(() => 'https://example.com/') } - const otherGuest = { id: 8, getURL: vi.fn(() => 'https://other.test/') } - requestHandler(allowedGuest, 'media', allowedSite, { mediaTypes: ['video'] }) - requestHandler(otherGuest, 'media', otherSite, { mediaTypes: ['video'] }) - await vi.waitFor(() => expect(allowedSite).toHaveBeenCalledWith(true)) - await vi.waitFor(() => expect(otherSite).toHaveBeenCalledWith(false)) - - expect(checkHandler(allowedGuest, 'media', '', { mediaType: 'video' })).toBe(true) - expect(checkHandler(otherGuest, 'media', '', { mediaType: 'video' })).toBe(false) - expect(checkHandler(null, 'notifications', 'https://example.com', {})).toBe(false) - expect(checkHandler(null, 'persistent-storage', '', {})).toBe(true) - expect(checkHandler(null, 'geolocation', 'https://example.com', {})).toBe(false) - }) - - it('wires WebAuthn device selection for isolated partitions', async () => { - await browserSessionRegistry.createProfile('isolated', 'Security Key Test') - const mockSession = sessionFromPartitionMock.mock.results[0]?.value - const devicePermissionHandler = mockSession.setDevicePermissionHandler.mock.calls[0][0] - const checkHandler = mockSession.setPermissionCheckHandler.mock.calls[0][0] - - expect( - devicePermissionHandler({ - deviceType: 'hid', - origin: 'https://github.com', - device: { collections: [{ usagePage: 0xf1d0 }] } - }) - ).toBe(true) - expect( - devicePermissionHandler({ - deviceType: 'hid', - origin: 'http://[::1]:5173', - device: { collections: [{ usagePage: 0xf1d0 }] } - }) - ).toBe(true) - expect( - devicePermissionHandler({ - deviceType: 'hid', - origin: 'https://github.com', - device: { collections: [{ usagePage: 1 }] } - }) - ).toBe(false) - expect(checkHandler(null, 'hid', '', { securityOrigin: 'https://github.com' })).toBe(true) - - const selectHidHandler = mockSession.on.mock.calls.find( - ([eventName]) => eventName === 'select-hid-device' - )?.[1] - const hidCallback = vi.fn() - selectHidHandler( - { preventDefault: vi.fn() }, - { - frame: { url: 'https://github.com' }, - deviceList: [ - { deviceId: 'keyboard', collections: [{ usagePage: 1 }] }, - { deviceId: 'security-key', collections: [{ usagePage: 0xf1d0 }] } - ] - }, - hidCallback - ) - expect(hidCallback).toHaveBeenCalledWith('security-key') - - const selectWebAuthnHandler = mockSession.on.mock.calls.find( - ([eventName]) => eventName === 'select-webauthn-account' - )?.[1] - const webAuthnCallback = vi.fn() - selectWebAuthnHandler( - { preventDefault: vi.fn() }, - { accounts: [{ credentialId: 'credential-1' }] }, - webAuthnCallback - ) - expect(webAuthnCallback).toHaveBeenCalledWith('credential-1') - }) - - it('uses profile-owned partitions for non-default Orca profiles', async () => { - const orcaProfileId = 'local-work' - browserSessionRegistry.configureForOrcaProfile({ - orcaProfileId, - profileDirectory: '/profiles/local-work' - }) - - expect(browserSessionRegistry.getDefaultProfile().partition).toBe( - getOrcaProfileBrowserDefaultPartition(orcaProfileId) - ) - expect(browserSessionRegistry.isAllowedPartition(ORCA_BROWSER_PARTITION)).toBe(false) - - const profile = await browserSessionRegistry.createProfile('isolated', 'Work Browser') - expect(profile).not.toBeNull() - expect(profile!.partition).toBe( - getOrcaProfileBrowserSessionPartition(orcaProfileId, profile!.id) - ) - - browserSessionRegistry.configureForOrcaProfile({ - orcaProfileId: DEFAULT_LOCAL_ORCA_PROFILE_ID, - profileDirectory: '/profiles/local-default' - }) - }) - - describe('installBrowserSessionUserAgentPolicy', () => { - function install(): (details: unknown, callback: ReturnType) => void { - const onBeforeSendHeaders = vi.fn() - installBrowserSessionUserAgentPolicy( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the hook reads only the mocked webRequest member exercised here. - { webRequest: { onBeforeSendHeaders } } as never, - (request) => - request.currentUserAgent === googleAuthUserAgent() - ? { kind: 'google-auth', userAgent: googleAuthUserAgent() } - : undefined - ) - expect(onBeforeSendHeaders).toHaveBeenCalledWith( - { urls: ['http://*/*', 'https://*/*', 'ws://*/*', 'wss://*/*'] }, - expect.any(Function) - ) - return onBeforeSendHeaders.mock.calls[0][1] - } - - it('leaves ordinary-host identity headers untouched', () => { - const callback = vi.fn() - install()( - { - url: 'https://example.com/', - requestHeaders: { - 'User-Agent': 'Mozilla/5.0 Chrome/150.0.0.0 Safari/537.36', - 'sec-ch-ua': 'browser-owned', - Cookie: 'abc=123' - } - }, - callback - ) - - expect(callback.mock.calls[0][0].requestHeaders).toEqual({ - 'User-Agent': 'Mozilla/5.0 Chrome/150.0.0.0 Safari/537.36', - 'sec-ch-ua': 'browser-owned', - Cookie: 'abc=123' - }) - }) - - it("presents Electron's own UA on Google auth hosts and keeps its client hints", () => { - const callback = vi.fn() - install()( - { - url: 'https://accounts.google.com/v3/signin/identifier', - requestHeaders: { - 'User-Agent': 'Mozilla/5.0 Chrome/150.0.0.0 Safari/537.36', - 'sec-ch-ua': 'browser-owned', - Accept: 'text/html' - } - }, - callback - ) - expect(callback.mock.calls[0][0].requestHeaders).toEqual({ - 'User-Agent': googleAuthUserAgent(), - 'sec-ch-ua': 'browser-owned', - Accept: 'text/html' - }) - }) - - it('drops mobile-preset client hints on Google auth hosts', () => { - const callback = vi.fn() - install()( - { - url: 'https://accounts.google.com/gsi/button', - resourceType: 'subFrame', - requestHeaders: { - 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) CriOS/150.0.0.0', - 'sec-ch-ua': '"Google Chrome";v="150"', - 'sec-ch-ua-mobile': '?1', - 'sec-ch-ua-platform': '"iOS"' - } - }, - callback - ) - expect(callback.mock.calls[0][0].requestHeaders).toEqual({ - 'User-Agent': googleAuthUserAgent() - }) - }) - - it("keeps a request already carrying Electron's UA untouched on Google auth hosts", () => { - const headers = { 'User-Agent': googleAuthUserAgent(), 'sec-ch-ua': 'browser-owned' } - const callback = vi.fn() - install()( - { url: 'https://accounts.google.com/v3/signin/identifier', requestHeaders: { ...headers } }, - callback - ) - expect(callback.mock.calls[0][0].requestHeaders).toEqual(headers) - }) - - it('keeps the session identity on Google app subdomains', () => { - const callback = vi.fn() - install()( - { - url: 'https://myaccount.google.com/', - requestHeaders: { 'User-Agent': 'Chrome/150', 'sec-ch-ua': 'browser-owned' } - }, - callback - ) - expect(callback.mock.calls[0][0].requestHeaders).toEqual({ - 'User-Agent': 'Chrome/150', - 'sec-ch-ua': 'browser-owned' - }) - }) - }) }) diff --git a/src/main/browser/site-settings/browser-site-display-controller.test.ts b/src/main/browser/site-settings/browser-site-display-controller.test.ts index cb222611bb2..5d5501fbebf 100644 --- a/src/main/browser/site-settings/browser-site-display-controller.test.ts +++ b/src/main/browser/site-settings/browser-site-display-controller.test.ts @@ -176,44 +176,6 @@ describe('BrowserSiteDisplayController color scheme', () => { expect(preview.debugger.isAttached()).toBe(false) }) - it.each([10, 100])('sends one media update per eligible guest at population %s', (population) => { - const controller = new BrowserSiteDisplayController() - const ordinary = fakeGuest('https://ordinary.example/') - controller.onGuestRegistered(ordinary, null) - const guests = Array.from({ length: population }, () => - fakeGuest('orca-preview://grant/a.html') - ) - for (const guest of guests) { - controller.onGuestDocumentStart(guest, { - url: guest.url, - profileId: null, - colorScheme: 'dark' - }) - controller.onGuestDocumentStart(guest, { - url: guest.url, - profileId: null, - colorScheme: 'dark' - }) - expect(guest.mediaSent).toEqual(['dark', 'dark']) - expect(guest.debugger.listenerCount('detach')).toBe(1) - guest.mediaSent.length = 0 - } - for (const guest of guests) { - controller.setGuestColorSchemeFallback(guest, 'light') - } - expect(guests.reduce((total, guest) => total + guest.mediaSent.length, 0)).toBe(population) - for (const guest of guests) { - controller.setGuestColorSchemeFallback(guest, 'light') - controller.onGuestRetired(guest.id) - expect(guest.debugger.isAttached()).toBe(false) - expect(guest.debugger.listenerCount('detach')).toBe(0) - controller.setGuestColorSchemeFallback(guest, 'dark') - } - expect(guests.reduce((total, guest) => total + guest.mediaSent.length, 0)).toBe(population) - expect(ordinary.mediaSent).toEqual([]) - expect(ordinary.debugger.isAttached()).toBe(false) - }) - it.each([ { order: 'read-first', profileId: 'work', colorScheme: 'light', saved: 'dark' }, { order: 'registration-first', profileId: 'work', colorScheme: 'light', saved: 'dark' }, @@ -261,48 +223,6 @@ describe('BrowserSiteDisplayController color scheme', () => { expect(guest.debugger.listenerCount('detach')).toBe(0) } ) - - it('sets the site scheme as each page commits and clears it once on leaving', () => { - const controller = new BrowserSiteDisplayController() - const guest = fakeGuest('https://github.com/') - controller.onGuestRegistered(guest, null) - controller.setColorScheme(guest.id, 'dark') - - controller.zoomPage(guest.id, 'in') - commit(controller, guest, 'https://docs.example.com/') - commit(controller, guest, 'https://news.example.com/') - commit(controller, guest, 'https://github.com/x') - - expect(guest.mediaSent).toEqual(['dark', '', 'dark']) - }) - - it('gives a restored tab its scheme when its first load started before it registered', () => { - const controller = new BrowserSiteDisplayController() - const first = fakeGuest('https://github.com/') - controller.onGuestRegistered(first, null) - controller.setColorScheme(first.id, 'light') - - const restored = fakeGuest('') - controller.onGuestRegistered(restored, null) - restored.url = 'https://github.com/restored' - controller.onMainFrameNavigationCommitted(restored.id) - - expect(restored.mediaSent).toEqual(['light']) - }) - - it('attaches again after DevTools detaches the debugger', () => { - const controller = new BrowserSiteDisplayController() - const guest = fakeGuest('https://github.com/') - controller.onGuestRegistered(guest, null) - controller.setColorScheme(guest.id, 'dark') - - guest.debugger.detach() - expect(guest.debugger.isAttached()).toBe(false) - commit(controller, guest, 'https://github.com/next') - - expect(guest.debugger.isAttached()).toBe(true) - expect(guest.mediaSent.at(-1)).toBe('dark') - }) }) vi.mock('electron', () => ({ app: { getPath: () => tmpdir() } })) diff --git a/src/main/browser/site-settings/browser-site-permission-prompt.test.ts b/src/main/browser/site-settings/browser-site-permission-prompt.test.ts index 52e188fdbe3..f7c2c0d1526 100644 --- a/src/main/browser/site-settings/browser-site-permission-prompt.test.ts +++ b/src/main/browser/site-settings/browser-site-permission-prompt.test.ts @@ -126,25 +126,6 @@ describe('browser site permission prompts', () => { vi.useRealTimers() }) - it('asks once for camera and microphone together, then remembers Allow', async () => { - const call = ask(['camera', 'microphone']) - expect(sentRequests(renderer)).toMatchObject([ - { - kind: 'site-permission', - details: { - origin: 'https://meet.example.com', - permissions: ['camera', 'microphone'] - } - } - ]) - await answerLatest('allow') - await expect(call).resolves.toBe(true) - - await expect(ask(['camera'])).resolves.toBe(true) - expect(sentRequests(renderer)).toHaveLength(1) - expect(allowedOnCheck(['camera', 'microphone'])).toBe(true) - }) - it('asks only for what is unanswered, and a remembered Block refuses the whole request', async () => { const camera = ask(['camera']) await answerLatest('allow') @@ -158,25 +139,6 @@ describe('browser site permission prompts', () => { expect(sentRequests(renderer)).toHaveLength(2) }) - it('forgets a dismissed prompt, so the site is asked again later', async () => { - const call = ask(['notifications']) - await answerLatest(null) - await expect(call).resolves.toBe(false) - - expect(listBrowserSitePermissions('default')).toEqual([]) - void ask(['notifications']) - expect(sentRequests(renderer)).toHaveLength(2) - }) - - it('keeps each browser profile to its own answers', async () => { - const call = ask(['clipboard'], { memory: savedBrowserSitePermissions('work') }) - await answerLatest('allow') - await call - - void ask(['clipboard'], { memory: savedBrowserSitePermissions('default') }) - expect(sentRequests(renderer)).toHaveLength(2) - }) - it("keeps a paired server's answers for its session only, where checks still see them", async () => { const pairedSession = sessionBrowserSitePermissions('persist:paired-a') const call = ask(['notifications'], { memory: pairedSession }) @@ -191,15 +153,6 @@ describe('browser site permission prompts', () => { expect(sentRequests(renderer)).toHaveLength(2) }) - it("gives a frame the tab's answers for what the tab delegates", async () => { - const call = ask(['camera']) - await answerLatest('allow') - await call - - await expect(ask(['camera'], { frameOrigin: 'https://video.example.net' })).resolves.toBe(true) - expect(sentRequests(renderer)).toHaveLength(1) - }) - it("gives another site's frame only its own notification answer, and never asks for it", async () => { const tab = ask(['notifications']) await answerLatest('allow') @@ -223,32 +176,6 @@ describe('browser site permission prompts', () => { expect(sentRequests(renderer)).toHaveLength(2) }) - it('asks for a local file, and every local file shares the answer', async () => { - const call = ask(['camera'], { guest: mockGuest('file:///Users/me/lab/permission-lab.html') }) - expect(sentRequests(renderer).at(-1)?.details.origin).toBe('file://') - await answerLatest('allow') - await expect(call).resolves.toBe(true) - - await expect( - ask(['camera'], { guest: mockGuest('file:///Users/me/other/page.html') }) - ).resolves.toBe(true) - expect(sentRequests(renderer)).toHaveLength(1) - }) - - it('refuses an opaque page without asking', async () => { - await expect(ask(['camera'], { guest: mockGuest('data:text/html,

hi

') })).resolves.toBe( - false - ) - expect(sentRequests(renderer)).toEqual([]) - }) - - it('refuses without asking when no window can show the question', async () => { - mocks.resolvePagePromptTarget.mockReturnValue(null) - - await expect(ask(['camera'])).resolves.toBe(false) - expect(sentRequests(renderer)).toEqual([]) - }) - it('remembers answers across a restart', async () => { const directory = mkdtempSync(join(tmpdir(), 'orca-site-permissions-')) configureBrowserSitePermissionStore(directory) diff --git a/src/main/browser/webauthn/browser-webauthn-account-picker.test.ts b/src/main/browser/webauthn/browser-webauthn-account-picker.test.ts index 6c3dfa6366c..9285ca1134d 100644 --- a/src/main/browser/webauthn/browser-webauthn-account-picker.test.ts +++ b/src/main/browser/webauthn/browser-webauthn-account-picker.test.ts @@ -13,11 +13,8 @@ vi.mock('electron', () => ({ vi.mock('../browser-manager', () => ({ browserManager: { getRendererContextForGuest: getRendererContextForGuestMock } })) - import { - BROWSER_WEBAUTHN_ACCOUNT_PICKER_TIMEOUT_MS, cancelAllBrowserWebAuthnAccountRequests, - cancelBrowserWebAuthnAccountRequests, requestBrowserWebAuthnAccount, respondToBrowserWebAuthnAccountRequest } from './browser-webauthn-account-picker' @@ -59,32 +56,6 @@ describe('browser WebAuthn account picker', () => { vi.useRealTimers() }) - it('returns the credential selected by the owning renderer', async () => { - const guest = mockWebContents(41) - const renderer = mockWebContents(42) - fromFrameMock.mockReturnValue(guest) - getRendererContextForGuestMock.mockReturnValue({ browserPageId: 'page-1', renderer }) - - const selection = requestBrowserWebAuthnAccount(accountDetails(), mockSession()) - const request = vi.mocked(renderer.send).mock.calls[0][1] - - expect(request).toMatchObject({ - browserPageId: 'page-1', - relyingPartyId: 'accounts.example.com', - accounts: [ - { credentialId: 'credential-1', displayName: 'Personal', name: 'me@example.com' }, - { credentialId: 'credential-2', displayName: 'Work', name: 'me@work.example' } - ] - }) - expect( - respondToBrowserWebAuthnAccountRequest(renderer, { - requestId: request.requestId, - credentialId: 'credential-2' - }) - ).toBe(true) - await expect(selection).resolves.toBe('credential-2') - }) - it('rejects responses from another renderer or with an unoffered credential', async () => { const guest = mockWebContents(51) const renderer = mockWebContents(52) @@ -112,92 +83,6 @@ describe('browser WebAuthn account picker', () => { await expect(selection).resolves.toBeNull() }) - it('cancels when the tab closes or the prompt times out', async () => { - const guest = mockWebContents(61) - const renderer = mockWebContents(62) - fromFrameMock.mockReturnValue(guest) - getRendererContextForGuestMock.mockReturnValue({ browserPageId: 'page-3', renderer }) - const browserSession = mockSession() - const closedSelection = requestBrowserWebAuthnAccount(accountDetails(), browserSession) - vi.mocked(renderer.send).mockImplementation((channel) => { - if (channel === 'browser:webauthn-account-request-closed') { - throw new Error('renderer destroyed during send') - } - }) - - cancelBrowserWebAuthnAccountRequests('page-3') - await expect(closedSelection).resolves.toBeNull() - - const timedOutSelection = requestBrowserWebAuthnAccount(accountDetails(), browserSession) - await vi.advanceTimersByTimeAsync(BROWSER_WEBAUTHN_ACCOUNT_PICKER_TIMEOUT_MS) - await expect(timedOutSelection).resolves.toBeNull() - }) - - it('cancels when the guest or window is destroyed', async () => { - const guest = mockWebContents(71) - const renderer = mockWebContents(72) - fromFrameMock.mockReturnValue(guest) - getRendererContextForGuestMock.mockReturnValue({ browserPageId: 'page-4', renderer }) - const browserSession = mockSession() - const guestDestroyed = requestBrowserWebAuthnAccount(accountDetails(), browserSession) - guest.emit('destroyed') - await expect(guestDestroyed).resolves.toBeNull() - - const windowDestroyed = requestBrowserWebAuthnAccount(accountDetails(), browserSession) - renderer.emit('destroyed') - await expect(windowDestroyed).resolves.toBeNull() - }) - - it('cancels immediately when the guest or window renderer process exits', async () => { - const guest = mockWebContents(81) - const renderer = mockWebContents(82) - const browserSession = mockSession() - fromFrameMock.mockReturnValue(guest) - getRendererContextForGuestMock.mockReturnValue({ browserPageId: 'page-5', renderer }) - - const guestCrashed = requestBrowserWebAuthnAccount(accountDetails(), browserSession) - guest.emit('render-process-gone') - await expect(guestCrashed).resolves.toBeNull() - - const windowCrashed = requestBrowserWebAuthnAccount(accountDetails(), browserSession) - renderer.emit('render-process-gone') - await expect(windowCrashed).resolves.toBeNull() - }) - - it.each(['guest', 'renderer'] as const)( - 'retires the request when the %s commits a navigation', - async (target) => { - const owners = { guest: mockWebContents(91), renderer: mockWebContents(92) } - fromFrameMock.mockReturnValue(owners.guest) - getRendererContextForGuestMock.mockReturnValue({ - browserPageId: 'page-6', - renderer: owners.renderer - }) - - const selection = requestBrowserWebAuthnAccount(accountDetails(), mockSession()) - const requestId = vi.mocked(owners.renderer.send).mock.calls[0][1].requestId - owners[target].emit('did-navigate') - - await expect(selection).resolves.toBeNull() - expect(owners.renderer.send).toHaveBeenLastCalledWith( - 'browser:webauthn-account-request-closed', - { requestId } - ) - expect( - respondToBrowserWebAuthnAccountRequest(owners.renderer, { - requestId, - credentialId: 'credential-1' - }) - ).toBe(false) - for (const contents of Object.values(owners)) { - for (const event of ['did-navigate', 'render-process-gone', 'destroyed']) { - expect(contents.listenerCount(event)).toBe(0) - } - } - expect(vi.getTimerCount()).toBe(0) - } - ) - it('keeps sibling and successor requests independent of a retired document', async () => { const retiredGuest = mockWebContents(101) const siblingGuest = mockWebContents(102)