diff --git a/config/flaky-test-pattern-baseline.txt b/config/flaky-test-pattern-baseline.txt index 343285a7ec4..e5529f42df0 100644 --- a/config/flaky-test-pattern-baseline.txt +++ b/config/flaky-test-pattern-baseline.txt @@ -584,7 +584,6 @@ 3 test-no-wall-clock:src/main/github/pr-refresh-coordinator-visible-follow-up.test.ts 4 test-no-wall-clock:src/main/github/pr-refresh-queue-growth-bound.test.ts 2 test-no-wall-clock:src/main/grok/grok-hook-stdin-no-eof.test.ts -1 test-no-wall-clock:src/main/ipc/ai-vault/ai-vault.test.ts 3 test-no-wall-clock:src/main/ipc/runtime-environments/runtime-environment-request-connections.test.ts 1 test-no-wall-clock:src/main/ipc/terminal-render-desync-evidence.test.ts 5 test-no-wall-clock:src/main/ipc/worktrees-orphan-directory-cleanup.test.ts @@ -806,7 +805,6 @@ 1 test-timeout-needs-reason:src/main/codex/structured/launch/codex-structured-real-cli-command-env.test.ts 1 test-timeout-needs-reason:src/main/codex/structured/launch/codex-structured-real-cli-permissions.test.ts 1 test-timeout-needs-reason:src/main/codex/structured/launch/codex-structured-real-cli-visuals.test.ts -1 test-timeout-needs-reason:src/main/codex/trust/toml/codex-trust-config-concurrent-launch.test.ts 1 test-timeout-needs-reason:src/main/cursor/hook-service.test.ts 1 test-timeout-needs-reason:src/main/daemon/executable-host/linux/testing/daemon-appimage-copy-host.electron.test.ts 2 test-timeout-needs-reason:src/main/daemon/health/testing/issue-6814-daemon-failure-classification.test.ts diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 6798bfd4b6a..06f2ab1a7db 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -4527,7 +4527,7 @@ "providers": ["remote-runtime"], "coveredPlatforms": ["macos"], "coveredProviders": [], - "coverageNotes": "Audit1973: Delete the browser RPC forwarding/schema/scalar suite and its prospective selector. Preserve all27 unrelated dated receipts; no actual browser operation alarm is replaced. Whole original changed dated receipts are retired, never narrowed or transferred. This metadata review claims no fresh execution, equivalent replacement, provider/PTY, live remote/mobile, rendered, cross-platform, soak or measured-budget result. Historical prior account (preserved verbatim): Audit1925 (sources1922/1923): The complete terminal-file-link-actions suite is removed. Lose supplied local/remote menu-row inventories, browser label/order/download/reveal callback forwarding, platform-copy and source-group pinning examples. No OS reveal/download, real remote preview or browser navigation was exercised; the other real document-preview sources are not equivalent replacements. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1899 replays sealed Audit1895: workspace-doc-history.test.ts is entirely removed from its current command and testFiles. Supplied recency/dedupe/cap/title/identity examples and the10000-row getter-read budget alarm are intentionally lost. No current assertionRef or active receipt selects that file. Unrelated active receipts and all prior gap literals remain verbatim; no replacement history/index-work coverage or new browser/paired-preview gate execution is claimed. Historical coverage account before Audit1899: Audit1853 reduces LinkActionPopover.test.tsx and archives its complete 2026-09-06 mixed-selector receipt. The lost copy/open-state and Settings callback-order/omission alarms are accepted without equivalent replacement coverage. No fresh execution of this gate is credited. Audit1709 removes low-value test definitions and retires affected assertion or execution-receipt claims without equivalent replacement coverage. Per-source reviews bound retained behavior and injected/native/remote limits. Unchanged assertions and receipts keep their original scope; gates lacking current active evidence are downgraded. Historical scope before this audit: Audit1687 removes low-value test projections, mock wiring, private-state assertions, inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained public behavior and refusal boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1630 removes low-value Git, GitHub/GitLab and IPC projections, argument inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained cancellation, ownership, authorization, filesystem and SQLite boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: 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.", + "coverageNotes": "Audit2043: Remove the registration-wait, webview-security, preview-authorization, guest-lifecycle and grant-IPC unit suites and the sibling-directory assertion reference. Runtime and browser capability consumers also lose removed imported examples. Remaining document-policy, file-reader and grant-registry assertions do not replace those alarms. Preserve the 27 independent dated receipts at their original unchanged selectors; reduced prospective unit selectors have no fresh result or equivalent replacement claim. Historical prior account (preserved verbatim): Audit1973: Delete the browser RPC forwarding/schema/scalar suite and its prospective selector. Preserve all27 unrelated dated receipts; no actual browser operation alarm is replaced. Whole original changed dated receipts are retired, never narrowed or transferred. This metadata review claims no fresh execution, equivalent replacement, provider/PTY, live remote/mobile, rendered, cross-platform, soak or measured-budget result. Historical prior account (preserved verbatim): Audit1925 (sources1922/1923): The complete terminal-file-link-actions suite is removed. Lose supplied local/remote menu-row inventories, browser label/order/download/reveal callback forwarding, platform-copy and source-group pinning examples. No OS reveal/download, real remote preview or browser navigation was exercised; the other real document-preview sources are not equivalent replacements. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1899 replays sealed Audit1895: workspace-doc-history.test.ts is entirely removed from its current command and testFiles. Supplied recency/dedupe/cap/title/identity examples and the10000-row getter-read budget alarm are intentionally lost. No current assertionRef or active receipt selects that file. Unrelated active receipts and all prior gap literals remain verbatim; no replacement history/index-work coverage or new browser/paired-preview gate execution is claimed. Historical coverage account before Audit1899: Audit1853 reduces LinkActionPopover.test.tsx and archives its complete 2026-09-06 mixed-selector receipt. The lost copy/open-state and Settings callback-order/omission alarms are accepted without equivalent replacement coverage. No fresh execution of this gate is credited. Audit1709 removes low-value test definitions and retires affected assertion or execution-receipt claims without equivalent replacement coverage. Per-source reviews bound retained behavior and injected/native/remote limits. Unchanged assertions and receipts keep their original scope; gates lacking current active evidence are downgraded. Historical scope before this audit: Audit1687 removes low-value test projections, mock wiring, private-state assertions, inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained public behavior and refusal boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1630 removes low-value Git, GitHub/GitLab and IPC projections, argument inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained cancellation, ownership, authorization, filesystem and SQLite boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: 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", @@ -4538,23 +4538,23 @@ "https://github.com/stablyai/orca/pull/13876" ], "invariant": "Opening one HTML preview from a paired client renders the workspace document in exactly one client-local browser tab, located by that document and served over the orca-preview scheme. The client gains exactly that one browser workspace and it is the document one — blank where a URL page carries a URL, named by the document, with the chip naming the file — while the host gains no browser page at all, neither in its own page registry nor in the tab snapshot its clients publish into. The preview occupies its own split without taking focus from the source editor; an explicit click activates it, and closing it removes only the preview. Following a document from a file link is the other half of that switch and does move the reader to it, tab group included, whether the preview is new or already open, because opening a file is a request to look at it. A document tab quit with the client comes back as the same row on a grant the relaunched client mints afresh. A preview is named by the browser page it is open in, not by a namespace of its own, and the page registry has two halves: a workspace-document guest is registered in its own map and is absent from the browsing one entirely. That absence is the fence. Page, session and profile management, agent tab enumeration and command targeting, download routing and certificate attribution all read the browsing map directly, in more places than a per-channel guard could be remembered in, so none of them can name a document page and none of them carries a guard. Browser tools the reader drives (element grab, hover describe, selection capture, the annotation viewport bridge) are the one operation that legitimately spans the halves, and they go through the single authority that reads both, keyed by the page and its hosting renderer. The halves are disjoint in both directions: browsing registration refuses a page the document half already holds, and minting a grant refuses a page the browsing half already holds, so one id can never name a surface in both. The headless backend acts on that refusal by destroying the window it had already opened rather than leaving a policy-less page behind an id nothing can drive, keeping nothing under that id for its own shutdown to hand back. Registration refuses on the same terms when the guest it was asked about is already gone. The exit door is guarded in both its halves: a preview withdraws by revoking its grant and never through the unregister channel, so a page the document half holds arriving there is refused before either the registration teardown or the grab-state disposal beside it, which would otherwise drop the intent an in-flight preview grab compares by identity and leave that grab answering ok without ever arming its guest. A bridge request whose guest does not resolve is refused without tearing down the page it named, so a misaddressed request cannot cancel a healthy page's in-flight downloads and grabs. The annotation viewport bridge resolves its guest when its serialized op actually runs rather than when the request arrived, so a cross-process navigation while it waited cannot leave the bridge installed in a retired guest while the reader looks at a new one. State main keys by a preview's page is disposed when that page's grant is revoked, which is the only signal a preview's surface is gone. A tool asking for a page whose guest has not attached yet waits for that registration and arms when it arrives, rather than answering not-ready at the reader; that wait resolves only the request already naming this page, never the worktree-wide or any-tab waits the CLI and agents use to ask for a browser tab to drive. Handing the previewed document to the reader's own machine routes on the owners its grant was minted against — the file's own connection owner and the worktree's own runtime owner, neither read from the tab's stored fields. Only a document proven to live on this machine reaches the client OS; one with a resolved remote owner is downloaded first; and one whose owner cannot be resolved at all, workspace root included, is refused with a message naming that, because the download route would otherwise read the same absolute path on the client and hand back a same-named local file under the remote document's name. A runtime-owned path that falls outside its worktree root is refused by that route itself and surfaces as a failure toast rather than a download. Nothing the document does writes a file to this machine either: the preview partition denies downloads outright instead of routing them through the browser download flow, which has no page to attribute a preview's bytes to and would otherwise reserve a name in this desktop's Downloads folder and write them there unprompted. That refusal is visible to the reader and invisible to the document: the preview's shell carries a fixed sentence saying downloads are off, published at most once per preview per interval so a document asking in a loop cannot fill Orca's chrome, while the page itself gets back exactly what it got before, which is nothing. The sentence names no file, because the document chooses the name it offers; and a refusal never takes the document away the way an entry document's own failure does, whatever it names. A preview is a browser tab, not an editor tab in a preview mode: it is named the way a browser tab is named — by the document it shows when that document declares a title, and by the file it shows when it does not — while the chip goes on naming the file and the host whatever the document calls itself. A title is refused on the same terms the url is: a document that declares none has Chromium report the grant URL as its title, and that title is stored, mirrored onto the tab and written to disk, so anything carrying the scheme falls back to the file instead. It is created by the preview action as a page located by its document, it carries the workspace-relative path copy the editor's path header owned, and closing it revokes the grant that made the document readable while a URL tab closing beside it revokes nothing. Chrome persisted by builds that made previews editor tabs is dropped on restore rather than coming back naming a surface no restore can produce, and the ordinary editor tab for the same document is left alone. A document tab is held back at the mobile publish boundary — no client holds its grant, and the wire has no tab kind for it — while an ordinary browser tab beside it still publishes. It is held back from the group projection that publishes tab order, recency and group activity as well as from the tab list itself, so no published group names a tab the phone is never sent. A browser page can be located by a workspace document instead of a URL, and the document is the whole of its stored identity. The grant and the orca-preview URL that document is served over are minted when the page mounts and replaced by a hard reload, so neither is ever written to the page's url, mirrored onto its tab, persisted or published: such a page's url is the blank URL from creation through restore, including when a session written elsewhere carries a grant URL in, and what the session carries is the worktree and path a restored page mints afresh against today's owners. Every door onto a page's url holds that line — creation, the title update, and the navigation commit alike — so a report about a document page cannot give it a URL it never had, and the title fallback and the loading affordance follow the url each door actually wrote. The mirror carries the document too, so a tab entry cannot go on naming a document its active page has left. Every guest in the app is policy-attached through one door: a workspace document takes a restricted profile there rather than a separate installer beside it, so the attachment bookkeeping that door owns — what registration refuses, and what teardown frees — covers a preview on the same terms as a browsing page, and a preview takes none of the browsing machinery that door installs. That authority answers from the moment the embedder hands the guest over rather than only after a later navigation: the guest binds to the grant it is already showing, so the tools reach the document the reader opened and not just one they navigated to. A read the host reports as truncated or over-cap is refused rather than served partially, and a document outside the paired worktree is refused with a message naming that boundary instead of a bare read failure. The rendered document reaches nothing off-machine on its own: every served response carries a self-only content security policy, the preview session cancels any request that is not in-document, subframes cannot navigate outside the grant, a guest no document has yet bound to a grant may not navigate at all, the guest gathers no ICE candidates, and an SSH path that canonicalizes outside the grant root is refused before it is read. The one route out is a link the reader presses: a trusted click on an anchor, reported by the preview's own preload from a guest still bound to a live grant, leaves through the configured browser destination and captured source route — and only after the reader confirms the exact destination URL, so a document cannot spend a single stray press exfiltrating what it can read into a link it authored. The preview hands its guest that focus itself whenever it is the surface the reader is in — a browsing page gets it from the chrome around it, and a preview has no chrome to get it from — and it does so only then, so a preview mounted behind a terminal or an editor never takes the keyboard from what the reader is actually in. It offers again when the window itself takes focus back and nothing in the embedder has claimed that focus, because another app coming to the front lands focus on the embedder rather than the guest and the route out would otherwise stay shut until something remounted the pane — while the same window focus also arrives when the reader presses a tab, that being the guest's own blur returning, and taking focus back from there would fight the reader for their own click. Nothing else does. A navigation or popup the document starts by itself is swallowed whatever else is happening, including immediately after a genuine press elsewhere in the document, so a page that can read its grant cannot hand it to a browser tab; a middle click opens nothing; and a fragment link is answered inside the document. A preview attach carries the preview preload and no renderer-supplied one, and no other attach path can acquire it. A subresource the workspace will not send degrades the document to a notice naming that file, never to a failure panel over a page that rendered. A grant outlives neither the tab that owns it nor the renderer document that minted it, and only the trusted renderer can mint or revoke one. For the browser creations this gate still owns, owner-pinned creation returns the canonical host page identity before navigation readiness; delayed navigation cannot turn a created page into an unidentifiable failure or a duplicate retry. Capability rejection before host mutation must preserve the original error, issue no RPC, surface a failure toast, and remove only a caller-declared newly-created empty split. Post-create reconciliation failure requires exact rollback; ambiguous rollback rejects without local fallback.", - "oracle": "Audit1973: Delete the browser RPC forwarding/schema/scalar suite and its prospective selector. Preserve all27 unrelated dated receipts; no actual browser operation alarm is replaced. Whole original changed dated receipts are retired, never narrowed or transferred. This metadata review claims no fresh execution, equivalent replacement, provider/PTY, live remote/mobile, rendered, cross-platform, soak or measured-budget result. Historical prior account (preserved verbatim): Audit1925 (sources1922/1923): The complete terminal-file-link-actions suite is removed. Lose supplied local/remote menu-row inventories, browser label/order/download/reveal callback forwarding, platform-copy and source-group pinning examples. No OS reveal/download, real remote preview or browser navigation was exercised; the other real document-preview sources are not equivalent replacements. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1709 retires changed-source assertion and execution scope without equivalent replacement coverage. Existing unchanged-selector receipts remain scoped to their original sources. Historical prior-source oracle: Audit1687 retires changed-source assertion and execution scope without equivalent replacement coverage. Existing unchanged-selector receipts remain scoped to their original sources. Historical prior-source oracle: Audit1421 narrows renderer unit coverage as recorded in coverageNotes. Prior oracle descriptions are historical for the changed suite. The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): Retained unchanged-selector receipts are the only current execution evidence. Reduced-source assertions await validation. Historical prior oracle: The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): In paired Electron, write an HTML fixture that declares its own title on the host, invoke the Explorer preview action on the client, and require the document text to be readable out of the orca-preview guest before judging any absence. With that presence established, require the client to hold exactly one browser workspace more than its baseline and that workspace to be the document one: page and tab url blank, the document path mirrored onto both, the tab named by the document's title, the chip naming the file, no editor row of the retired preview species anywhere, and the guest URL carrying the orca-preview scheme. Ask the host through its own page registry as well as through the tab snapshot, and in the same run open an ordinary URL browser tab from the same client and require that one to arrive in both — the presence precondition without which “the host gained nothing” is satisfied just as well by an oracle that cannot see browser pages at all. Require the preview to sit in a group other than the source editor's while the active group and tab remain the source editor's. Then click away to the terminal, click the preview tab, require it to reactivate and still render, close it with its own X, and require the document tab to be gone while the host still holds only the URL tab and the source group, source editor and terminal survive. Quit the client with a document tab open and relaunch it on the same profile: require the same workspace row to come back, blank and named by the document, rendering the document again over a grant URL that differs from the one that was quit, with no preview-scheme or document-named page anywhere in what the host holds. Prove the halves are live by flipping one product property at a time and requiring the run to fail: publish document workspaces to the host like ordinary ones, and stop mirroring the document onto the workspace row. Have the fixture document attempt its own egress on every load — an unattended window.open and location.href to an off-machine URL, plus an inline ICE gathering probe — and require the same baseline counts and a candidate count of zero, so the document's own attempts are measured rather than assumed. Then, as a separate phase after the close oracle has already run, bring the client window to the front, press the document's heading with a real mouse event, and require the document to report that the same press drove it to attempt a second window.open and location.href while both browser counts stay at that phase's baseline and nothing routes — the case a recent-input gate cannot distinguish from the press's own effect. Only then press the target=_blank link with a real mouse event and require both a recorded routing call that returned success and a browser count above that baseline, with the preview tab still open. Drive the preload's click policy as a unit oracle over a real document: a dispatched click, a trusted press on an external anchor, an anchor reached through what it wraps, an SVG animated href, a sibling preview link, fragment and percent-encoded fragment targets, a bare hash, and a middle click. Create a browser page located by a workspace document, handing creation a live grant URL, and require its stored url, its mirrored tab url and the written session payload all to be blank with no orca-preview string anywhere in what was written, while an ordinary page created the same way keeps the URL it was given and asks for the address bar the document page never does. Parse the written page and tab through the session schema and require the document to survive both halves. Hydrate them back and require the document page to return blank and still named — including when its page row was salvaged away and only the tab's own copy remains, and when a foreign session carried a grant URL into both rows. Drive the mirror across a page switch out of the document and back, and across a repair in which the document is the only mirrored field that differs. Name a document page from its document and require the tab to take that name, name it with an empty title and require the file, name it with a live grant URL and require the file again with no preview scheme anywhere in the written session, and require an ordinary blank browser tab beside it to still be called New Tab. Dispatch a title update out of a rendered preview's own guest and require it to reach the page state while the identity chip still reads the document's workspace-relative path. Attach a browsing guest and a workspace-document guest through the same method in one run and require the browsing one to take clicked-link routing, popup handling and auth-identity detach tracking while the document guest takes none of them, stays inside the grant it is showing, denies every window it asks for, and is dropped from the page-keyed document registry by the same teardown that frees its id for a later attach. Register a browsing guest and attach a workspace-document guest in one run against the real manager, require the one door to answer each page with the guest of its own half, and require the document page to be absent from the browsing map and from its enumeration. Drive both browsing registration entry points with a page the document half already holds and require them to register nothing, and drive the mint channel with a page the browsing half already holds and require it to refuse; and drive the offscreen one with a guest that is missing and with one already destroyed, requiring the same refusal. Arm a grab on a live preview target, drive the unregister channel at that same target in the window before the queued operation runs, and require the grab to reach the guest anyway — then drive the same sequence for an ordinary browser page and require its grab state to be disposed after all. Hold one viewport-bridge op open, queue a second behind it, swap the page's guest while that second op waits, and require the injection to land in the guest the page has then. Revoke a grant after a tool has run against its page and require that page's grab state to be cancelled and disposed. Ask a tool for a document page whose guest has not attached, require the request to park in the registration wait, attach the guest, and require the same request to arm on it; require a page nothing ever renders to answer not-ready once that wait elapses. With a document open, ask a tool for a browsing page id and require it to be answered by the browsing half or not at all, with the same channel reaching the document guest under the page it really renders. Drive open-externally for a document whose per-file owner is remote while the workspace-scoped owner is unresolved, for a runtime-owned worktree whose preview tab carries no runtime id of its own, and for a worktree that resolves no runtime owner while the tab still carries one, requiring the download route in each; and for an owner that cannot be resolved at all, and for an unknown workspace root while nothing names another host, requiring a refusal that neither opens nor downloads. Drive the headless backend with a page the document half already holds and require it to reject, destroy the window, and unregister nothing — then shut the backend down and require it still to have unregistered nothing. Navigate a bound preview guest at a second grant through both latch events and require it to stay on the grant it bound to. Mount a preview while a renderer drag is already in flight and require its guest to be click-through at the moment it is appended, not a turn later. Render the editor panel shell in each remaining tab mode and require the path header exactly where the surface does not already name itself. Drive the preview action and require a browser tab located by the document rather than an editor tab, require a second open of the same document to activate the tab it is already in, and require closing that tab to revoke its grant while a URL tab closed beside it revokes none. Publish a worktree holding a document tab and a URL tab and require only the URL tab to reach the mobile snapshot. Install the shared partition policies for a preview partition and for an ordinary browsing partition in the same run, fire each one's own will-download listener, and require the preview's to cancel while the browsing one still reaches the download router — then require the preview protocol installer to be what asks for that deny. In the same run, require the cancelled download to raise a reader-facing notice and the routed one to raise none. require the guest registry to name the bound grant for a live preview guest and nothing for a contents that is not one, has committed no document, or is gone. Drive the shell with a refusal and require one fixed sentence, still one row after three more refusals, standing beside an asset failure rather than being counted with it, gone behind the failure panel, and ignored when it names another preview's grant. Drive the main-side report gate directly for a sender that is no preview guest, a guest with no bound or a revoked grant, a genuine press Electron's webview focus flag misreports as unfocused, and non-web URLs; drive the reader-facing confirmation for accept, cancel, and a confirmed tab the browser refuses; and drive will-attach-webview in both preload directions. Run the per-owner reader, grant-containment, scheme-admission, guest-policy, and plan-routing contracts as unit oracles, including a host-reported truncation, an over-cap binary, and an out-of-worktree paired path. Drive the reader-facing component with the payloads the reader can actually produce — the entry document fails only as truncated or unreadable, a subresource additionally as a refused format — and require the asset case to leave the guest mounted. Drive the closed-tab cleanup hook, the window installer, and the grant IPC handlers directly, requiring the grant to be released when the preview tab closes, cleared at window creation and on a cross-document main-frame navigation, and refused to any sender that is not the trusted renderer. For the browser creations this gate still owns, run the unchanged contract oracle for direct create and side-preview callers with absent status, unknown capabilities, and a mixed-version host, requiring the original unsupported error or visible toast, zero RPCs, and no retained new split; hold a real navigation response beyond the 15-second client deadline after host creation and require the first RPC to return the exact host inventory page ID, one host page, and no retry; repeat reconciliation faults against headless serve and retain the separate exact-page reconciliation rollback oracle.", + "oracle": "Audit2043: Remove the registration-wait, webview-security, preview-authorization, guest-lifecycle and grant-IPC unit suites and the sibling-directory assertion reference. Runtime and browser capability consumers also lose removed imported examples. Remaining document-policy, file-reader and grant-registry assertions do not replace those alarms. Preserve the 27 independent dated receipts at their original unchanged selectors; reduced prospective unit selectors have no fresh result or equivalent replacement claim. Historical prior account (preserved verbatim): Audit1973: Delete the browser RPC forwarding/schema/scalar suite and its prospective selector. Preserve all27 unrelated dated receipts; no actual browser operation alarm is replaced. Whole original changed dated receipts are retired, never narrowed or transferred. This metadata review claims no fresh execution, equivalent replacement, provider/PTY, live remote/mobile, rendered, cross-platform, soak or measured-budget result. Historical prior account (preserved verbatim): Audit1925 (sources1922/1923): The complete terminal-file-link-actions suite is removed. Lose supplied local/remote menu-row inventories, browser label/order/download/reveal callback forwarding, platform-copy and source-group pinning examples. No OS reveal/download, real remote preview or browser navigation was exercised; the other real document-preview sources are not equivalent replacements. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1709 retires changed-source assertion and execution scope without equivalent replacement coverage. Existing unchanged-selector receipts remain scoped to their original sources. Historical prior-source oracle: Audit1687 retires changed-source assertion and execution scope without equivalent replacement coverage. Existing unchanged-selector receipts remain scoped to their original sources. Historical prior-source oracle: Audit1421 narrows renderer unit coverage as recorded in coverageNotes. Prior oracle descriptions are historical for the changed suite. The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): Retained unchanged-selector receipts are the only current execution evidence. Reduced-source assertions await validation. Historical prior oracle: The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): In paired Electron, write an HTML fixture that declares its own title on the host, invoke the Explorer preview action on the client, and require the document text to be readable out of the orca-preview guest before judging any absence. With that presence established, require the client to hold exactly one browser workspace more than its baseline and that workspace to be the document one: page and tab url blank, the document path mirrored onto both, the tab named by the document's title, the chip naming the file, no editor row of the retired preview species anywhere, and the guest URL carrying the orca-preview scheme. Ask the host through its own page registry as well as through the tab snapshot, and in the same run open an ordinary URL browser tab from the same client and require that one to arrive in both — the presence precondition without which “the host gained nothing” is satisfied just as well by an oracle that cannot see browser pages at all. Require the preview to sit in a group other than the source editor's while the active group and tab remain the source editor's. Then click away to the terminal, click the preview tab, require it to reactivate and still render, close it with its own X, and require the document tab to be gone while the host still holds only the URL tab and the source group, source editor and terminal survive. Quit the client with a document tab open and relaunch it on the same profile: require the same workspace row to come back, blank and named by the document, rendering the document again over a grant URL that differs from the one that was quit, with no preview-scheme or document-named page anywhere in what the host holds. Prove the halves are live by flipping one product property at a time and requiring the run to fail: publish document workspaces to the host like ordinary ones, and stop mirroring the document onto the workspace row. Have the fixture document attempt its own egress on every load — an unattended window.open and location.href to an off-machine URL, plus an inline ICE gathering probe — and require the same baseline counts and a candidate count of zero, so the document's own attempts are measured rather than assumed. Then, as a separate phase after the close oracle has already run, bring the client window to the front, press the document's heading with a real mouse event, and require the document to report that the same press drove it to attempt a second window.open and location.href while both browser counts stay at that phase's baseline and nothing routes — the case a recent-input gate cannot distinguish from the press's own effect. Only then press the target=_blank link with a real mouse event and require both a recorded routing call that returned success and a browser count above that baseline, with the preview tab still open. Drive the preload's click policy as a unit oracle over a real document: a dispatched click, a trusted press on an external anchor, an anchor reached through what it wraps, an SVG animated href, a sibling preview link, fragment and percent-encoded fragment targets, a bare hash, and a middle click. Create a browser page located by a workspace document, handing creation a live grant URL, and require its stored url, its mirrored tab url and the written session payload all to be blank with no orca-preview string anywhere in what was written, while an ordinary page created the same way keeps the URL it was given and asks for the address bar the document page never does. Parse the written page and tab through the session schema and require the document to survive both halves. Hydrate them back and require the document page to return blank and still named — including when its page row was salvaged away and only the tab's own copy remains, and when a foreign session carried a grant URL into both rows. Drive the mirror across a page switch out of the document and back, and across a repair in which the document is the only mirrored field that differs. Name a document page from its document and require the tab to take that name, name it with an empty title and require the file, name it with a live grant URL and require the file again with no preview scheme anywhere in the written session, and require an ordinary blank browser tab beside it to still be called New Tab. Dispatch a title update out of a rendered preview's own guest and require it to reach the page state while the identity chip still reads the document's workspace-relative path. Attach a browsing guest and a workspace-document guest through the same method in one run and require the browsing one to take clicked-link routing, popup handling and auth-identity detach tracking while the document guest takes none of them, stays inside the grant it is showing, denies every window it asks for, and is dropped from the page-keyed document registry by the same teardown that frees its id for a later attach. Register a browsing guest and attach a workspace-document guest in one run against the real manager, require the one door to answer each page with the guest of its own half, and require the document page to be absent from the browsing map and from its enumeration. Drive both browsing registration entry points with a page the document half already holds and require them to register nothing, and drive the mint channel with a page the browsing half already holds and require it to refuse; and drive the offscreen one with a guest that is missing and with one already destroyed, requiring the same refusal. Arm a grab on a live preview target, drive the unregister channel at that same target in the window before the queued operation runs, and require the grab to reach the guest anyway — then drive the same sequence for an ordinary browser page and require its grab state to be disposed after all. Hold one viewport-bridge op open, queue a second behind it, swap the page's guest while that second op waits, and require the injection to land in the guest the page has then. Revoke a grant after a tool has run against its page and require that page's grab state to be cancelled and disposed. Ask a tool for a document page whose guest has not attached, require the request to park in the registration wait, attach the guest, and require the same request to arm on it; require a page nothing ever renders to answer not-ready once that wait elapses. With a document open, ask a tool for a browsing page id and require it to be answered by the browsing half or not at all, with the same channel reaching the document guest under the page it really renders. Drive open-externally for a document whose per-file owner is remote while the workspace-scoped owner is unresolved, for a runtime-owned worktree whose preview tab carries no runtime id of its own, and for a worktree that resolves no runtime owner while the tab still carries one, requiring the download route in each; and for an owner that cannot be resolved at all, and for an unknown workspace root while nothing names another host, requiring a refusal that neither opens nor downloads. Drive the headless backend with a page the document half already holds and require it to reject, destroy the window, and unregister nothing — then shut the backend down and require it still to have unregistered nothing. Navigate a bound preview guest at a second grant through both latch events and require it to stay on the grant it bound to. Mount a preview while a renderer drag is already in flight and require its guest to be click-through at the moment it is appended, not a turn later. Render the editor panel shell in each remaining tab mode and require the path header exactly where the surface does not already name itself. Drive the preview action and require a browser tab located by the document rather than an editor tab, require a second open of the same document to activate the tab it is already in, and require closing that tab to revoke its grant while a URL tab closed beside it revokes none. Publish a worktree holding a document tab and a URL tab and require only the URL tab to reach the mobile snapshot. Install the shared partition policies for a preview partition and for an ordinary browsing partition in the same run, fire each one's own will-download listener, and require the preview's to cancel while the browsing one still reaches the download router — then require the preview protocol installer to be what asks for that deny. In the same run, require the cancelled download to raise a reader-facing notice and the routed one to raise none. require the guest registry to name the bound grant for a live preview guest and nothing for a contents that is not one, has committed no document, or is gone. Drive the shell with a refusal and require one fixed sentence, still one row after three more refusals, standing beside an asset failure rather than being counted with it, gone behind the failure panel, and ignored when it names another preview's grant. Drive the main-side report gate directly for a sender that is no preview guest, a guest with no bound or a revoked grant, a genuine press Electron's webview focus flag misreports as unfocused, and non-web URLs; drive the reader-facing confirmation for accept, cancel, and a confirmed tab the browser refuses; and drive will-attach-webview in both preload directions. Run the per-owner reader, grant-containment, scheme-admission, guest-policy, and plan-routing contracts as unit oracles, including a host-reported truncation, an over-cap binary, and an out-of-worktree paired path. Drive the reader-facing component with the payloads the reader can actually produce — the entry document fails only as truncated or unreadable, a subresource additionally as a refused format — and require the asset case to leave the guest mounted. Drive the closed-tab cleanup hook, the window installer, and the grant IPC handlers directly, requiring the grant to be released when the preview tab closes, cleared at window creation and on a cross-document main-frame navigation, and refused to any sender that is not the trusted renderer. For the browser creations this gate still owns, run the unchanged contract oracle for direct create and side-preview callers with absent status, unknown capabilities, and a mixed-version host, requiring the original unsupported error or visible toast, zero RPCs, and no retained new split; hold a real navigation response beyond the 15-second client deadline after host creation and require the first RPC to return the exact host inventory page ID, one host page, and no retry; repeat reconciliation faults against headless serve and retain the separate exact-page reconciliation rollback oracle.", "commands": [ "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/browser/client-pages/orca-runtime-browser.test.ts src/renderer/src/runtime/web-session-tabs-sync-focus-intent.test.ts src/renderer/src/runtime/web-session-tabs-sync-browser-tabs.test.ts", - "pnpm exec vitest run --config config/vitest.config.ts src/main/browser/document-preview/doc-preview-grant-registry.test.ts src/main/browser/document-preview/doc-preview-file-reader.test.ts src/main/browser/document-preview/doc-preview-guest-policy.test.ts src/main/browser/document-preview/doc-preview-failure-notice.test.ts src/shared/doc-preview-scheme.test.ts src/renderer/src/lib/doc-preview-grants.test.ts src/main/window/main-window-webview-security.test.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.failure-message.test.tsx src/renderer/src/components/browser-pane/workspace-doc/doc-preview-external-link-confirmation.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/browser/document-preview/doc-preview-grant-registry.test.ts src/main/browser/document-preview/doc-preview-file-reader.test.ts src/main/browser/document-preview/doc-preview-guest-policy.test.ts src/main/browser/document-preview/doc-preview-failure-notice.test.ts src/shared/doc-preview-scheme.test.ts src/renderer/src/lib/doc-preview-grants.test.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.failure-message.test.tsx src/renderer/src/components/browser-pane/workspace-doc/doc-preview-external-link-confirmation.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/main/browser/sessions/browser-session-partition-policies.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/main/browser/sessions/browser-session-partition-policies.test.ts src/main/browser/document-preview/doc-preview-guest-policy.test.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.failure-message.test.tsx", - "pnpm exec vitest run --config config/vitest.config.ts src/main/window/main-window-webview-security.test.ts src/main/browser/document-preview/doc-preview-guest-policy.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/browser/document-preview/doc-preview-guest-policy.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/browser/pages/browser-workspace-doc-location.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/browser/pages/browser-workspace-doc-location.test.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx src/shared/doc-preview-scheme.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/browser/pages/browser-workspace-doc-location.test.ts src/renderer/src/runtime/sync-runtime-graph-editor-diff-tabs.test.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx", "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.failure-message.test.tsx", - "pnpm exec vitest run --config config/vitest.config.ts src/preload/doc-preview-link-interception.test.ts src/main/browser/document-preview/doc-preview-guest-policy.test.ts src/main/window/main-window-webview-security.test.ts src/main/ipc/doc-preview-grant-ipc.test.ts", - "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/browser/browser-preview-tool-authorization.test.ts src/main/browser/document-preview/doc-preview-guest-policy.test.ts src/main/ipc/browser/browser.test.ts", - "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/browser/browser-preview-tool-authorization.test.ts src/main/browser/guest-lifecycle/manager/browser-manager-annotation-bridge.test.ts src/main/browser/guest-lifecycle/manager/browser-manager-guest-lifecycle.test.ts src/shared/doc-preview-scheme.test.ts src/renderer/src/components/browser-pane/workspace-doc/doc-preview-document-actions.test.ts src/renderer/src/components/browser-pane/workspace-doc/use-doc-preview-guest-tools.test.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx", - "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/browser/browser-preview-tool-authorization.test.ts src/main/browser/guest-lifecycle/manager/browser-manager-annotation-bridge.test.ts src/main/browser/guest-lifecycle/manager/browser-manager-guest-lifecycle.test.ts src/main/browser/backends/offscreen-browser-backend-lifecycle.test.ts src/main/browser/document-preview/doc-preview-guest-policy.test.ts src/shared/doc-preview-scheme.test.ts src/renderer/src/components/browser-pane/workspace-doc/doc-preview-document-actions.test.ts src/renderer/src/components/browser-pane/workspace-doc/use-doc-preview-guest-tools.test.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx", - "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/doc-preview-grant-ipc.test.ts src/renderer/src/components/link-actions/LinkActionPopover.test.tsx src/renderer/src/runtime/sync-runtime-graph-editor-diff-tabs.test.ts", - "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/browser/browser-preview-tool-authorization.test.ts src/main/ipc/browser/browser-tab-registration-wait.test.ts src/main/ipc/doc-preview-grant-ipc.test.ts src/main/browser/guest-lifecycle/manager/browser-manager-guest-lifecycle.test.ts src/main/browser/guest-lifecycle/manager/browser-manager-annotation-bridge.test.ts src/main/browser/backends/offscreen-browser-backend-lifecycle.test.ts src/main/browser/document-preview/doc-preview-guest-policy.test.ts src/shared/doc-preview-scheme.test.ts src/renderer/src/components/browser-pane/workspace-doc/use-doc-preview-guest-tools.test.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx", + "pnpm exec vitest run --config config/vitest.config.ts src/preload/doc-preview-link-interception.test.ts src/main/browser/document-preview/doc-preview-guest-policy.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/browser/document-preview/doc-preview-guest-policy.test.ts src/main/ipc/browser/browser.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/browser/guest-lifecycle/manager/browser-manager-annotation-bridge.test.ts src/shared/doc-preview-scheme.test.ts src/renderer/src/components/browser-pane/workspace-doc/doc-preview-document-actions.test.ts src/renderer/src/components/browser-pane/workspace-doc/use-doc-preview-guest-tools.test.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx", + "pnpm exec vitest run --config config/vitest.config.ts src/main/browser/guest-lifecycle/manager/browser-manager-annotation-bridge.test.ts src/main/browser/backends/offscreen-browser-backend-lifecycle.test.ts src/main/browser/document-preview/doc-preview-guest-policy.test.ts src/shared/doc-preview-scheme.test.ts src/renderer/src/components/browser-pane/workspace-doc/doc-preview-document-actions.test.ts src/renderer/src/components/browser-pane/workspace-doc/use-doc-preview-guest-tools.test.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx", + "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/link-actions/LinkActionPopover.test.tsx src/renderer/src/runtime/sync-runtime-graph-editor-diff-tabs.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/browser/guest-lifecycle/manager/browser-manager-annotation-bridge.test.ts src/main/browser/backends/offscreen-browser-backend-lifecycle.test.ts src/main/browser/document-preview/doc-preview-guest-policy.test.ts src/shared/doc-preview-scheme.test.ts src/renderer/src/components/browser-pane/workspace-doc/use-doc-preview-guest-tools.test.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx", "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/browser/pages/browser-page-conversion.test.ts src/renderer/src/runtime/sync-runtime-graph-conversion-publish.test.ts src/renderer/src/lib/workspace-doc-address-input.test.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx", "SKIP_BUILD=1 ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-preview-address-bar-convergence.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --reporter=list", "SKIP_BUILD=1 ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-preview-local-render.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --reporter=line", @@ -4575,14 +4575,10 @@ "src/main/browser/document-preview/doc-preview-file-reader.test.ts", "src/main/browser/sessions/browser-session-partition-policies.test.ts", "src/main/browser/document-preview/doc-preview-guest-policy.test.ts", - "src/main/ipc/browser/browser-tab-registration-wait.test.ts", "src/main/browser/document-preview/doc-preview-failure-notice.test.ts", "src/renderer/src/store/slices/browser/pages/browser-workspace-doc-location.test.ts", "src/preload/doc-preview-link-interception.test.ts", - "src/main/window/main-window-webview-security.test.ts", - "src/main/ipc/browser/browser-preview-tool-authorization.test.ts", "src/main/browser/guest-lifecycle/manager/browser-manager-annotation-bridge.test.ts", - "src/main/browser/guest-lifecycle/manager/browser-manager-guest-lifecycle.test.ts", "src/main/browser/backends/offscreen-browser-backend-lifecycle.test.ts", "src/renderer/src/components/browser-pane/workspace-doc/doc-preview-document-actions.test.ts", "src/renderer/src/components/browser-pane/workspace-doc/use-doc-preview-guest-tools.test.ts", @@ -4591,7 +4587,6 @@ "src/renderer/src/lib/doc-preview-grants.test.ts", "src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.failure-message.test.tsx", "src/renderer/src/components/browser-pane/workspace-doc/doc-preview-external-link-confirmation.test.ts", - "src/main/ipc/doc-preview-grant-ipc.test.ts", "src/renderer/src/components/link-actions/LinkActionPopover.test.tsx", "src/renderer/src/runtime/sync-runtime-graph-editor-diff-tabs.test.ts", "src/renderer/src/store/slices/browser/pages/browser-page-conversion.test.ts", @@ -4646,7 +4641,6 @@ "file": "src/main/browser/document-preview/doc-preview-grant-registry.test.ts", "assertions": [ "keeps silent authority over a document directory strictly inside the workspace, and only there", - "authorizes only the requested sibling directory", "uses Windows semantics when authorizing a sibling directory", "keeps an approved SSH directory inside the canonical workspace boundary" ] @@ -4688,15 +4682,6 @@ "an SVG link is resolved against the document, so its relative, rooted and fragment forms behave exactly like the identical HTML anchor" ] }, - { - "file": "src/main/window/main-window-webview-security.test.ts", - "assertions": [ - "an orca-preview src is admitted only on the preview partition with a live grant", - "preview guests receive their own policy instead of the browser guest policies", - "a preview attach is pinned to the preview preload over any renderer-supplied value, and a browsing attach never receives it", - "grants a renderer can no longer release are cleared at window creation and on reload, and survive an in-document navigation" - ] - }, { "file": "src/shared/doc-preview-scheme.test.ts", "assertions": [ @@ -4704,13 +4689,6 @@ "undecodable percent escapes are refused" ] }, - { - "file": "src/main/ipc/doc-preview-grant-ipc.test.ts", - "assertions": [ - "a sender that is not the trusted renderer mints no grant and revokes none", - "a reported link click reaches the guest policy with its own sender, and a non-string report reaches nothing" - ] - }, { "file": "src/renderer/src/store/slices/browser/pages/browser-workspace-doc-location.test.ts", "assertions": [ @@ -5096,7 +5074,11 @@ "Audit1925 (sources1922/1923): The complete terminal-file-link-actions suite is removed. Lose supplied local/remote menu-row inventories, browser label/order/download/reveal callback forwarding, platform-copy and source-group pinning examples. No OS reveal/download, real remote preview or browser navigation was exercised; the other real document-preview sources are not equivalent replacements. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Exact retired authored alarms: 'offers Orca and system-default actions for a local file'; 'offers a local HTML file the link-routing browsers, primary first'; 'labels workspace switching and omits an impossible remote alternate'; 'offers the same rows for a remote previewable file, downloading before the OS opens it'; 'keeps row parity between local and remote previewable files'; 'drops the remote download row for a path that announces itself as a directory'; 'offers Reveal in Finder for a local file link and reveals it through the shared path'; 'selects a folder link in its parent instead of opening it'; 'uses the platform file manager name'; 'omits the row for a workspace-root link, which has its own Open in Finder row'; 'omits the row for a file owned by an SSH or runtime host'; 'omits the row when the pane shell runs on %s'; 'keeps the row for a local link while a server is focused, and omits it for a runtime-owned link'; 'reveals nothing until the row is clicked'; 'pins the source group with groups $groupIds'; 'omits the row for an unknown source or a workspace switch'.", "Audit1973: Delete the browser RPC forwarding/schema/scalar suite and its prospective selector. Preserve all27 unrelated dated receipts; no actual browser operation alarm is replaced. Whole original changed dated receipts are retired, never narrowed or transferred. This metadata review claims no fresh execution, equivalent replacement, provider/PTY, live remote/mobile, rendered, cross-platform, soak or measured-budget result.", "Audit1973 historical original commands object, preserved verbatim; retired objects are not active execution evidence or replacement coverage:\n\"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/browser/client-pages/orca-runtime-browser.test.ts src/main/runtime/rpc/methods/browser/browser.test.ts src/renderer/src/runtime/web-session-tabs-sync-focus-intent.test.ts src/renderer/src/runtime/web-session-tabs-sync-browser-tabs.test.ts\"", - "Audit1973 historical original testFiles object, preserved verbatim; retired objects are not active execution evidence or replacement coverage:\n\"src/main/runtime/rpc/methods/browser/browser.test.ts\"" + "Audit1973 historical original testFiles object, preserved verbatim; retired objects are not active execution evidence or replacement coverage:\n\"src/main/runtime/rpc/methods/browser/browser.test.ts\"", + "Audit2043: Remove the registration-wait, webview-security, preview-authorization, guest-lifecycle and grant-IPC unit suites and the sibling-directory assertion reference. Runtime and browser capability consumers also lose removed imported examples. Remaining document-policy, file-reader and grant-registry assertions do not replace those alarms. Preserve the 27 independent dated receipts at their original unchanged selectors; reduced prospective unit selectors have no fresh result or equivalent replacement claim.", + "Audit2043 historical original assertionRefs object, preserved verbatim; retired objects are not active evidence:\n{\n \"file\": \"src/main/browser/document-preview/doc-preview-grant-registry.test.ts\",\n \"assertions\": [\n \"keeps silent authority over a document directory strictly inside the workspace, and only there\",\n \"authorizes only the requested sibling directory\",\n \"uses Windows semantics when authorizing a sibling directory\",\n \"keeps an approved SSH directory inside the canonical workspace boundary\"\n ]\n }", + "Audit2043 historical original assertionRefs object, preserved verbatim; retired objects are not active evidence:\n{\n \"file\": \"src/main/window/main-window-webview-security.test.ts\",\n \"assertions\": [\n \"an orca-preview src is admitted only on the preview partition with a live grant\",\n \"preview guests receive their own policy instead of the browser guest policies\",\n \"a preview attach is pinned to the preview preload over any renderer-supplied value, and a browsing attach never receives it\",\n \"grants a renderer can no longer release are cleared at window creation and on reload, and survive an in-document navigation\"\n ]\n }", + "Audit2043 historical original assertionRefs object, preserved verbatim; retired objects are not active evidence:\n{\n \"file\": \"src/main/ipc/doc-preview-grant-ipc.test.ts\",\n \"assertions\": [\n \"a sender that is not the trusted renderer mints no grant and revokes none\",\n \"a reported link click reaches the guest policy with its own sender, and a non-string report reaches nothing\"\n ]\n }" ], "demotionRule": "Demote if a paired HTML preview creates a browser workspace on either side, renders from anything other than the orca-preview scheme, serves a read the host reported as truncated or over-cap, serves a document without the self-only content security policy, lets the preview session carry an off-machine request, reads an SSH path that canonicalizes outside its grant root, steals focus from the source editor when it opens, removes unrelated editor or terminal state on close, hides a document that rendered because one subresource was refused, leaves a grant readable after the tab or renderer that owns it is gone, mints one for a sender that is not the trusted renderer, opens a browser tab for a navigation the document started with no one at the keyboard, or lets the guest gather an ICE candidate; or if a paired browser creation this gate still owns produces duplicate client or host identities or loses its requested split." }, @@ -14818,7 +14800,7 @@ "providers": ["local", "remote-runtime", "ssh", "wsl"], "coveredPlatforms": [], "coveredProviders": [], - "coverageNotes": "Audit1556 removes the whole mocked helper session lifecycle suite and reduces offscreen lifecycle and tab-routing suites. Mock helper argv/route/concurrency, private active state, explicit unregister ordering, standalone renderer-destroy notification and debugger-detach alarms are retired without equivalent replacement. Remaining offscreen cases prove same-ID replacement preservation and shutdown joining pending retirement against injected windows/manager/helper dependencies. The retained tab-routing case proves snapshot-reference provenance against injected helper replies. Reduced reliability commands have no fresh cohort/native receipt; this batch ran its ten retained authored cases plus two unchanged imported allowlist cases only. Historical pre-audit scope: The owner-boundary contract injects an isolated BrowserManager, offscreen WebContents, and AgentBrowserBridge. It proves explicit close removes the page from command routing before awaiting exact named-session retirement, runs retirement on unexpected renderer destruction, preserves unrelated pages, joins pending creation/retirement during shutdown, rejects command admission after terminal cleanup begins, retries Electron quit after a vetoed signal, preserves Windows shared-console graceful Ctrl-C, reserves the full renderer-acknowledgement and committed-teardown budgets before supervisor force-kill, bounds process-swap retirement to five seconds, and caps cleanup concurrency at four. A built-CLI macOS headless serve run used an isolated profile, folder workspace, socket directory, and port: exact PPID-1 helper/session identity disappeared within five seconds after page close and after Ctrl-C runtime shutdown, while an unrelated page survived close and reconnect. Linux cgroup and paired/SSH journeys remain gaps. Audit1379 changes src/main/browser/cdp/snapshots/snapshot-engine-iframe-sessions.test.ts (3 authored registrations removed; 0 retained); src/main/browser/client-hosts/file-transfers/browser-client-download-routing.test.ts (3 authored registrations removed; 4 retained). Intentional losses: Loses ref/string/Map.get-count projections; no real browser operation. Intentionally removes 3 authored registrations: 'cancels a client-hosted download whose owner cannot be resolved'; 'leaves ordinary browser guests on their desktop Downloads path'; 'stops routing to a composition once it starts closing'. Retained focused cases are not claimed as equivalent replacement for these losses. No equivalent replacement coverage is claimed. Existing exact primary-selector receipts and primary claims remain scoped to their unchanged sources; the broad optional selector has no new execution result. Audit1383: src/main/browser/guest-lifecycle/browser-guest-devtools-dock.test.ts reduced. Lose capture failure baseline and destroyed-host callback control. Fake Electron view models native lifecycle and custody; no native rendering/visible focus claimed. 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. Audit1390: src/main/browser/agent-browser/agent-browser-action-effects.test.ts reduced. No standalone baseline snapshot call-count alarm. src/main/browser/routing/partitions/browser-route-partition-stability.test.ts removed. Standalone assembled partition stability/separation/binding-count alarms across runtime/SSH/WSL/environment identity removed. No real browser cookie/session continuity or replacement equivalence is claimed. src/main/browser/element-grab/browser-grab-payload.test.ts reduced. Standalone annotation clamp/list-count/default preservation characterization removed. src/main/browser/cdp/input/guest-focused-frame-route.test.ts reduced. Attach-call count, default widget fallback and claimed detach-forgetting alarm removed; no actual Chromium frame/input delivery claimed. 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. Audit1396: src/main/browser/network/egress/browser-route-webrtc-egress.electron.test.ts removed. Deletes entire one-registration native Electron baseline/protected UDP probe, including fixture shell and packet observer. No equivalent actual network egress proof is claimed; actual Orca policy owner reviewed separately. src/main/browser/cdp/screencast/browser-screencast-live-frame-allocation.test.ts reduced. Retires captured PNG/JPEG twelve-frame digest/metadata/ack replay bundled with zero copied-byte instrumentation; surviving retry ownership witness is not claimed equivalent format or sequence coverage. src/main/browser/extensions/context-menus/browser-extension-context-menu-model.test.ts reduced. Toolbar-context constant membership, wrapper submenu structure and click-data property projection no longer independently asserted. 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. Audit1400: src/main/browser/cookies/sources/browser-cookie-source-partition.test.ts reduced. Lose standalone true identity projections, default-schema/unpartitioned examples and repeated raw flag/object type rejection alarms; retained cases are not claimed equivalent coverage. src/main/browser/routing/partitions/browser-route-partition-storage-runtime.test.ts reduced. Lose callback error propagation/no-clear alarm; file-backed pending queue and production lifecycle remain exercised. src/main/browser/client-hosts/file-transfers/browser-client-download-relay.test.ts reduced. Lose standalone successful destination projection and first/final offset expectations; no replacement delivery proof claimed. src/main/browser/cdp/screencast/browser-screencast-guest-view-limit.test.ts reduced. Lose the direct 320x240 floor example; modeled CDP frames do not prove native compositor pixels. src/main/browser/identity/browser-identity-mode-store.test.ts reduced. Lose basic last-assignment-wins example; no asynchronous write race is exercised. 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. Audit1401: src/main/browser/client-hosts/file-transfers/browser-client-file-channel-negotiation.test.ts removed. Loses file-channel negotiation refusal at the schema parse boundary; no replacement asserted. 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. src/main/browser/client-hosts/file-transfers/browser-client-upload-command.test.ts removed. Loses original upload-error priority when cleanup also fails and staging-release alarm. src/main/browser/extensions/installation/browser-extension-install-store.test.ts reduced. Loses install-directory path escape refusal alarm from the unpinned exception case. src/main/browser/routing/local-host-partitions/wsl-host-browser-jar-identity.test.ts removed. Loses WSL deploy/rename/reimport cookie-jar identity helper alarms without actual page/storage attachment. src/main/browser/client-hosts/lifecycle/paired-runtime-browser-client-host-identity-wiring.test.ts removed. Loses profile-stable versus profile-isolated host identity composition alarms. 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/extensions/native-messaging/browser-extension-native-host-process-backpressure.test.ts reduced. Loses synchronous host/global queue refusal, frame encoding and UTF8/body bounds at fake stdin, largest-stalled-host eviction/accounting, capacity completion and shutdown claim release alarms. The retained final-reply case does not establish outgoing pressure behavior. src/main/browser/cdp/input/browser-element-functions.test.ts removed. Loses independently fixed disabled/read-only/element-kind guard messages, number sanitization rollback, spinbutton targeting, focus hijack/replacement rejection, composed input event order and upload/selection behavior in happy-dom. No equivalent browser coverage is claimed. src/main/browser/extensions/permissions/browser-extension-settings-prompt-lifetime.test.ts reduced. Loses accepted-install continuation through renderer navigation while loading. The kept cancelled/retry path does not validate that exact ownership window. src/main/browser/cookies/client-route/browser-client-route-cookie-import.test.ts reduced. Loses no-route delegation/picker, failed-import source badge, compatible runtime-id success and manual/import destination argument wiring alarms. Retained refusal results do not prove cookie import or stored source updates. 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. The existing primary-selector receipt and its claims remain scoped to their unchanged sources; the broad optional command has no new execution result.", + "coverageNotes": "Audit2043: Remove the imported allowlist inventory examples described in the historical account below. This gate already has no active commands, testFiles, assertionRefs or evidenceRuns and protection remains none. No replacement coverage or fresh execution is claimed. Historical prior account (preserved verbatim): Audit1556 removes the whole mocked helper session lifecycle suite and reduces offscreen lifecycle and tab-routing suites. Mock helper argv/route/concurrency, private active state, explicit unregister ordering, standalone renderer-destroy notification and debugger-detach alarms are retired without equivalent replacement. Remaining offscreen cases prove same-ID replacement preservation and shutdown joining pending retirement against injected windows/manager/helper dependencies. The retained tab-routing case proves snapshot-reference provenance against injected helper replies. Reduced reliability commands have no fresh cohort/native receipt; this batch ran its ten retained authored cases plus two unchanged imported allowlist cases only. Historical pre-audit scope: The owner-boundary contract injects an isolated BrowserManager, offscreen WebContents, and AgentBrowserBridge. It proves explicit close removes the page from command routing before awaiting exact named-session retirement, runs retirement on unexpected renderer destruction, preserves unrelated pages, joins pending creation/retirement during shutdown, rejects command admission after terminal cleanup begins, retries Electron quit after a vetoed signal, preserves Windows shared-console graceful Ctrl-C, reserves the full renderer-acknowledgement and committed-teardown budgets before supervisor force-kill, bounds process-swap retirement to five seconds, and caps cleanup concurrency at four. A built-CLI macOS headless serve run used an isolated profile, folder workspace, socket directory, and port: exact PPID-1 helper/session identity disappeared within five seconds after page close and after Ctrl-C runtime shutdown, while an unrelated page survived close and reconnect. Linux cgroup and paired/SSH journeys remain gaps. Audit1379 changes src/main/browser/cdp/snapshots/snapshot-engine-iframe-sessions.test.ts (3 authored registrations removed; 0 retained); src/main/browser/client-hosts/file-transfers/browser-client-download-routing.test.ts (3 authored registrations removed; 4 retained). Intentional losses: Loses ref/string/Map.get-count projections; no real browser operation. Intentionally removes 3 authored registrations: 'cancels a client-hosted download whose owner cannot be resolved'; 'leaves ordinary browser guests on their desktop Downloads path'; 'stops routing to a composition once it starts closing'. Retained focused cases are not claimed as equivalent replacement for these losses. No equivalent replacement coverage is claimed. Existing exact primary-selector receipts and primary claims remain scoped to their unchanged sources; the broad optional selector has no new execution result. Audit1383: src/main/browser/guest-lifecycle/browser-guest-devtools-dock.test.ts reduced. Lose capture failure baseline and destroyed-host callback control. Fake Electron view models native lifecycle and custody; no native rendering/visible focus claimed. 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. Audit1390: src/main/browser/agent-browser/agent-browser-action-effects.test.ts reduced. No standalone baseline snapshot call-count alarm. src/main/browser/routing/partitions/browser-route-partition-stability.test.ts removed. Standalone assembled partition stability/separation/binding-count alarms across runtime/SSH/WSL/environment identity removed. No real browser cookie/session continuity or replacement equivalence is claimed. src/main/browser/element-grab/browser-grab-payload.test.ts reduced. Standalone annotation clamp/list-count/default preservation characterization removed. src/main/browser/cdp/input/guest-focused-frame-route.test.ts reduced. Attach-call count, default widget fallback and claimed detach-forgetting alarm removed; no actual Chromium frame/input delivery claimed. 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. Audit1396: src/main/browser/network/egress/browser-route-webrtc-egress.electron.test.ts removed. Deletes entire one-registration native Electron baseline/protected UDP probe, including fixture shell and packet observer. No equivalent actual network egress proof is claimed; actual Orca policy owner reviewed separately. src/main/browser/cdp/screencast/browser-screencast-live-frame-allocation.test.ts reduced. Retires captured PNG/JPEG twelve-frame digest/metadata/ack replay bundled with zero copied-byte instrumentation; surviving retry ownership witness is not claimed equivalent format or sequence coverage. src/main/browser/extensions/context-menus/browser-extension-context-menu-model.test.ts reduced. Toolbar-context constant membership, wrapper submenu structure and click-data property projection no longer independently asserted. 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. Audit1400: src/main/browser/cookies/sources/browser-cookie-source-partition.test.ts reduced. Lose standalone true identity projections, default-schema/unpartitioned examples and repeated raw flag/object type rejection alarms; retained cases are not claimed equivalent coverage. src/main/browser/routing/partitions/browser-route-partition-storage-runtime.test.ts reduced. Lose callback error propagation/no-clear alarm; file-backed pending queue and production lifecycle remain exercised. src/main/browser/client-hosts/file-transfers/browser-client-download-relay.test.ts reduced. Lose standalone successful destination projection and first/final offset expectations; no replacement delivery proof claimed. src/main/browser/cdp/screencast/browser-screencast-guest-view-limit.test.ts reduced. Lose the direct 320x240 floor example; modeled CDP frames do not prove native compositor pixels. src/main/browser/identity/browser-identity-mode-store.test.ts reduced. Lose basic last-assignment-wins example; no asynchronous write race is exercised. 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. Audit1401: src/main/browser/client-hosts/file-transfers/browser-client-file-channel-negotiation.test.ts removed. Loses file-channel negotiation refusal at the schema parse boundary; no replacement asserted. 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. src/main/browser/client-hosts/file-transfers/browser-client-upload-command.test.ts removed. Loses original upload-error priority when cleanup also fails and staging-release alarm. src/main/browser/extensions/installation/browser-extension-install-store.test.ts reduced. Loses install-directory path escape refusal alarm from the unpinned exception case. src/main/browser/routing/local-host-partitions/wsl-host-browser-jar-identity.test.ts removed. Loses WSL deploy/rename/reimport cookie-jar identity helper alarms without actual page/storage attachment. src/main/browser/client-hosts/lifecycle/paired-runtime-browser-client-host-identity-wiring.test.ts removed. Loses profile-stable versus profile-isolated host identity composition alarms. 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/extensions/native-messaging/browser-extension-native-host-process-backpressure.test.ts reduced. Loses synchronous host/global queue refusal, frame encoding and UTF8/body bounds at fake stdin, largest-stalled-host eviction/accounting, capacity completion and shutdown claim release alarms. The retained final-reply case does not establish outgoing pressure behavior. src/main/browser/cdp/input/browser-element-functions.test.ts removed. Loses independently fixed disabled/read-only/element-kind guard messages, number sanitization rollback, spinbutton targeting, focus hijack/replacement rejection, composed input event order and upload/selection behavior in happy-dom. No equivalent browser coverage is claimed. src/main/browser/extensions/permissions/browser-extension-settings-prompt-lifetime.test.ts reduced. Loses accepted-install continuation through renderer navigation while loading. The kept cancelled/retry path does not validate that exact ownership window. src/main/browser/cookies/client-route/browser-client-route-cookie-import.test.ts reduced. Loses no-route delegation/picker, failed-import source badge, compatible runtime-id success and manual/import destination argument wiring alarms. Retained refusal results do not prove cookie import or stored source updates. 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. The existing primary-selector receipt and its claims remain scoped to their unchanged sources; the broad optional command has no new execution result.", "motivatingLinks": [ "https://linear.app/stably/issue/STA-5400", "https://github.com/stablyai/orca/issues/16367" @@ -14866,7 +14848,8 @@ "Audit1396: src/main/browser/network/egress/browser-route-webrtc-egress.electron.test.ts removed. Deletes entire one-registration native Electron baseline/protected UDP probe, including fixture shell and packet observer. No equivalent actual network egress proof is claimed; actual Orca policy owner reviewed separately. src/main/browser/cdp/screencast/browser-screencast-live-frame-allocation.test.ts reduced. Retires captured PNG/JPEG twelve-frame digest/metadata/ack replay bundled with zero copied-byte instrumentation; surviving retry ownership witness is not claimed equivalent format or sequence coverage. src/main/browser/extensions/context-menus/browser-extension-context-menu-model.test.ts reduced. Toolbar-context constant membership, wrapper submenu structure and click-data property projection no longer independently asserted. No equivalent replacement coverage is claimed. Existing exact primary-selector receipts remain unchanged.", "Audit1400: src/main/browser/cookies/sources/browser-cookie-source-partition.test.ts reduced. Lose standalone true identity projections, default-schema/unpartitioned examples and repeated raw flag/object type rejection alarms; retained cases are not claimed equivalent coverage. src/main/browser/routing/partitions/browser-route-partition-storage-runtime.test.ts reduced. Lose callback error propagation/no-clear alarm; file-backed pending queue and production lifecycle remain exercised. src/main/browser/client-hosts/file-transfers/browser-client-download-relay.test.ts reduced. Lose standalone successful destination projection and first/final offset expectations; no replacement delivery proof claimed. src/main/browser/cdp/screencast/browser-screencast-guest-view-limit.test.ts reduced. Lose the direct 320x240 floor example; modeled CDP frames do not prove native compositor pixels. src/main/browser/identity/browser-identity-mode-store.test.ts reduced. Lose basic last-assignment-wins example; no asynchronous write race is exercised. No equivalent replacement coverage is claimed. Existing exact primary-selector receipts remain unchanged.", "Audit1401: src/main/browser/client-hosts/file-transfers/browser-client-file-channel-negotiation.test.ts removed. Loses file-channel negotiation refusal at the schema parse boundary; no replacement asserted. 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. src/main/browser/client-hosts/file-transfers/browser-client-upload-command.test.ts removed. Loses original upload-error priority when cleanup also fails and staging-release alarm. src/main/browser/extensions/installation/browser-extension-install-store.test.ts reduced. Loses install-directory path escape refusal alarm from the unpinned exception case. src/main/browser/routing/local-host-partitions/wsl-host-browser-jar-identity.test.ts removed. Loses WSL deploy/rename/reimport cookie-jar identity helper alarms without actual page/storage attachment. src/main/browser/client-hosts/lifecycle/paired-runtime-browser-client-host-identity-wiring.test.ts removed. Loses profile-stable versus profile-isolated host identity composition alarms. No equivalent replacement coverage is claimed. Existing exact primary-selector receipts remain unchanged.", - "Audit1412: src/main/browser/extensions/native-messaging/browser-extension-native-host-process-backpressure.test.ts reduced. Loses synchronous host/global queue refusal, frame encoding and UTF8/body bounds at fake stdin, largest-stalled-host eviction/accounting, capacity completion and shutdown claim release alarms. The retained final-reply case does not establish outgoing pressure behavior. src/main/browser/cdp/input/browser-element-functions.test.ts removed. Loses independently fixed disabled/read-only/element-kind guard messages, number sanitization rollback, spinbutton targeting, focus hijack/replacement rejection, composed input event order and upload/selection behavior in happy-dom. No equivalent browser coverage is claimed. src/main/browser/extensions/permissions/browser-extension-settings-prompt-lifetime.test.ts reduced. Loses accepted-install continuation through renderer navigation while loading. The kept cancelled/retry path does not validate that exact ownership window. src/main/browser/cookies/client-route/browser-client-route-cookie-import.test.ts reduced. Loses no-route delegation/picker, failed-import source badge, compatible runtime-id success and manual/import destination argument wiring alarms. Retained refusal results do not prove cookie import or stored source updates. 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. Existing exact primary-selector receipts remain unchanged." + "Audit1412: src/main/browser/extensions/native-messaging/browser-extension-native-host-process-backpressure.test.ts reduced. Loses synchronous host/global queue refusal, frame encoding and UTF8/body bounds at fake stdin, largest-stalled-host eviction/accounting, capacity completion and shutdown claim release alarms. The retained final-reply case does not establish outgoing pressure behavior. src/main/browser/cdp/input/browser-element-functions.test.ts removed. Loses independently fixed disabled/read-only/element-kind guard messages, number sanitization rollback, spinbutton targeting, focus hijack/replacement rejection, composed input event order and upload/selection behavior in happy-dom. No equivalent browser coverage is claimed. src/main/browser/extensions/permissions/browser-extension-settings-prompt-lifetime.test.ts reduced. Loses accepted-install continuation through renderer navigation while loading. The kept cancelled/retry path does not validate that exact ownership window. src/main/browser/cookies/client-route/browser-client-route-cookie-import.test.ts reduced. Loses no-route delegation/picker, failed-import source badge, compatible runtime-id success and manual/import destination argument wiring alarms. Retained refusal results do not prove cookie import or stored source updates. 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. Existing exact primary-selector receipts remain unchanged.", + "Audit2043: Remove the imported allowlist inventory examples described in the historical account below. This gate already has no active commands, testFiles, assertionRefs or evidenceRuns and protection remains none. No replacement coverage or fresh execution is claimed." ], "demotionRule": "Demote if a closed or destroyed page leaves its exact named helper session, if quit can exit before helper retirement settles, if unrelated live pages are retired, or if cleanup regresses to a global process-name kill or timer-only sweep." }, diff --git a/config/scripts/ci/sharding/ci-shard-timings.json b/config/scripts/ci/sharding/ci-shard-timings.json index fd916db57c3..02cc66d15f0 100644 --- a/config/scripts/ci/sharding/ci-shard-timings.json +++ b/config/scripts/ci/sharding/ci-shard-timings.json @@ -952,7 +952,6 @@ "src/main/browser/client-pages/lifecycle/browser-client-page-reconciliation-adapters.test.ts": 252, "src/main/browser/client-pages/renderer/browser-client-page-renderer-bridge.test.ts": 163, "src/main/browser/client-pages/lifecycle/browser-client-page-upload-routing.test.ts": 303, - "src/main/browser/cookies/client-route/browser-client-route-cookie-import-partition-parity.test.ts": 374, "src/main/browser/cookies/client-route/browser-client-route-cookie-import.test.ts": 101, "src/main/browser/client-hosts/file-transfers/browser-client-upload-buffer-retention.test.ts": 736, "src/main/browser/client-hosts/file-transfers/browser-client-upload-staging.test.ts": 140, @@ -979,7 +978,6 @@ "src/main/browser/guest-lifecycle/manager/browser-manager-grab-capture.test.ts": 516, "src/main/browser/guest-lifecycle/manager/browser-manager-grab-mode.test.ts": 567, "src/main/browser/guest-lifecycle/manager/browser-manager-grab-selection.test.ts": 584, - "src/main/browser/guest-lifecycle/manager/browser-manager-guest-lifecycle.test.ts": 554, "src/main/browser/guest-lifecycle/manager/browser-manager-guest-unload.test.ts": 534, "src/main/browser/guest-lifecycle/manager/browser-manager-guest-visibility.test.ts": 543, "src/main/browser/guest-lifecycle/manager/browser-manager-load-failure-replay.test.ts": 549, @@ -1066,7 +1064,6 @@ "src/main/browser/cookies/browser-cookie-import-write.test.ts": 68, "src/main/browser/cookies/browser-cookie-import.comet.test.ts": 1501, "src/main/browser/cookies/browser-cookie-import.test.ts": 1304, - "src/main/browser/cookies/browser-cookie-public-suffix-scope.test.ts": 92, "src/main/browser/cookies/browser-cookie-samesite.electron.test.ts": 1882, "src/main/browser/cookies/sources/browser-cookie-decryption.test.ts": 74, "src/main/browser/cookies/sources/browser-cookie-import-undecryptable.test.ts": 541, @@ -1481,7 +1478,6 @@ "src/main/codex/configuration/shared-server/codex-shared-server-probe.test.ts": 181, "src/main/codex/configuration/homes/codex-stale-pane-accounts.test.ts": 146, "src/main/codex/history/index/codex-state-db-backfill-recovery-supervision.test.ts": 4660, - "src/main/codex/history/index/codex-state-db-backfill-recovery.test.ts": 444, "src/main/codex/history/index/codex-state-db.test.ts": 127, "src/main/codex/structured/acquisition/codex-structured-acquisition-exit-proof.test.ts": 580, "src/main/codex/structured/launch/codex-structured-child-environment.test.ts": 887, @@ -1535,7 +1531,6 @@ "src/main/codex/structured/launch/codex-structured-visuals.test.ts": 783, "src/main/codex/structured/background-work/codex-subagent-interrupts.test.ts": 587, "src/main/codex/structured/threads/codex-thread-catch-up-rewind.test.ts": 1479, - "src/main/codex/trust/toml/codex-trust-config-concurrent-launch.test.ts": 254, "src/main/codex/trust/toml/codex-trust-config-mutation-queue.test.ts": 51, "src/main/codex/structured/journal/codex-turn-lifecycle-completion.test.ts": 374, "src/main/codex/structured/journal/codex-turn-lifecycle-history.test.ts": 374, @@ -1572,7 +1567,6 @@ "src/main/codex/trust/hooks/sta-4737-unreadable-is-not-absent.test.ts": 182, "src/main/codex/configuration/homes/sta-4823-shared-state-survives-a-failed-read.test.ts": 242, "src/main/codex/hooks/installation/windows-hook-command.test.ts": 129, - "src/main/codex/hooks/installation/windows-hook-upgrade.test.ts": 312, "src/main/codex/history/bridges/wsl-codex-session-bridge-script.test.ts": 190, "src/main/codex/history/bridges/wsl-codex-session-bridge.test.ts": 346, "src/main/command-code/hook-service.test.ts": 271, @@ -1894,7 +1888,6 @@ "src/main/foreign-sqlite-readers/readers/cursor-profile.test.ts": 403, "src/main/foreign-sqlite-readers/readers/opencode-binder-sessions.test.ts": 227, "src/main/foreign-sqlite-readers/readers/opencode-prompt-failure.test.ts": 346, - "src/main/gemini/hook-service.test.ts": 108, "src/main/ghostty/index.test.ts": 118, "src/main/ghostty/theme-import.test.ts": 100, "src/main/git/base-ref-search-selector.test.ts": 257, @@ -2202,13 +2195,10 @@ "src/main/ipc/ai-vault/ai-vault-search-merged-cursor.test.ts": 103, "src/main/ipc/ai-vault/ai-vault-search.test.ts": 1424, "src/main/ipc/ai-vault/ai-vault-unresolved-owner.test.ts": 1436, - "src/main/ipc/ai-vault/ai-vault.test.ts": 1453, "src/main/ipc/app.test.ts": 319, "src/main/ipc/automations-external-scope.test.ts": 359, "src/main/ipc/browser/browser-client-page-metadata-ipc.test.ts": 143, - "src/main/ipc/browser/browser-preview-tool-authorization.test.ts": 1105, "src/main/ipc/browser/browser-session-profile-ipc.test.ts": 699, - "src/main/ipc/browser/browser-tab-registration-wait.test.ts": 234, "src/main/ipc/browser/browser.test.ts": 1209, "src/main/ipc/codex-config-sync.test.ts": 101, "src/main/ipc/command-path/command-path-directory-worker.test.ts": 640, @@ -2217,7 +2207,6 @@ "src/main/ipc/created-worktree-root-prune.test.ts": 494, "src/main/ipc/developer-permissions.test.ts": 147, "src/main/ipc/diagnostics.test.ts": 124, - "src/main/ipc/doc-preview-grant-ipc.test.ts": 58, "src/main/ipc/dropped-path-resolution.test.ts": 92, "src/main/ipc/emulator-frame-stream-socket-lifetime.test.ts": 166, "src/main/ipc/emulator-renderer-lifetime.test.ts": 116, @@ -4726,7 +4715,6 @@ "src/main/window/macos-tahoe-release.test.ts": 48, "src/main/window/main-window-state-lifecycle.test.ts": 60, "src/main/window/main-window-updater-deferred-setup.test.ts": 215, - "src/main/window/main-window-webview-security.test.ts": 296, "src/main/window/native-chat-paste-files.test.ts": 666, "src/main/window/native-chat-pasted-text-file.test.ts": 94, "src/main/window/privileged-window-navigation.test.ts": 246, diff --git a/config/scripts/workflow/source-routing/pr-package-test-paths.mjs b/config/scripts/workflow/source-routing/pr-package-test-paths.mjs index 4da6a7c1094..07f9dc018a7 100644 --- a/config/scripts/workflow/source-routing/pr-package-test-paths.mjs +++ b/config/scripts/workflow/source-routing/pr-package-test-paths.mjs @@ -184,7 +184,6 @@ export const WINDOWS_BOUNDARY_TESTS = [ 'src/main/agent-hooks/windows-powershell-hook-launcher.test.ts', 'src/main/claude/claude-status-line-windows.test.ts', 'src/main/codex/hooks/installation/windows-hook-command.test.ts', - 'src/main/codex/hooks/installation/windows-hook-upgrade.test.ts', 'src/main/codex/hooks/installation/hook-service-managed-install.test.ts', 'src/main/windows/windows-pty-job.win32.test.ts', 'src/main/windows/windows-msys-job.win32.test.ts', diff --git a/mobile/src/session/use-mobile-session-close-actions.test.tsx b/mobile/src/session/use-mobile-session-close-actions.test.tsx index 91066b342f2..45552d4bbc5 100644 --- a/mobile/src/session/use-mobile-session-close-actions.test.tsx +++ b/mobile/src/session/use-mobile-session-close-actions.test.tsx @@ -20,7 +20,6 @@ import { resetMobileNativeChatDraftStoreForTests, setMobileNativeChatDraftText } from './native-chat/composer/mobile-native-chat-draft-store' -import './native-chat/mobile-native-chat-scope-key' import { resolveActiveSessionTab } from './active-session-tab' const initial: Terminal[] = ['a', 'b', 'c'].map((handle) => ({ diff --git a/src/main/browser/cdp/commands/cdp-bridge-integration.test.ts b/src/main/browser/cdp/commands/cdp-bridge-integration.test.ts index df3032b658b..f62703c8d7f 100644 --- a/src/main/browser/cdp/commands/cdp-bridge-integration.test.ts +++ b/src/main/browser/cdp/commands/cdp-bridge-integration.test.ts @@ -1,6 +1,5 @@ import type * as WorktreeScanCacheModule from '../../../git/worktrees/catalog/worktree-scan-cache' import type * as WorktreeListingModule from '../../../git/worktrees/catalog/worktree-listing' -import '../../../runtime/desktop-browser-bridge/desktop-browser-bridge-allowlist.spec' import { mkdtempSync } from 'node:fs' import { RuntimeBrowserCommands } from '../../../runtime/browser/commands/browser-commands' import { setRuntimeBrowserCommandsFactory } from '../../../runtime/browser/installation/runtime-browser-commands-factory' diff --git a/src/main/browser/cookies/browser-cookie-public-suffix-scope.test.ts b/src/main/browser/cookies/browser-cookie-public-suffix-scope.test.ts deleted file mode 100644 index ee8e1d6afde..00000000000 --- a/src/main/browser/cookies/browser-cookie-public-suffix-scope.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { normalizeCookieImportDomain } from './browser-cookie-import-policy' - -// Suffix-list changes can widen removal scopes and destroy unrelated sessions. -describe('registrable family across public-suffix sections', () => { - it('rejects a bare suffix as an import domain but keeps real hosts', () => { - expect(normalizeCookieImportDomain('co.uk')).toBeNull() - expect(normalizeCookieImportDomain('api.br')).toBeNull() - expect(normalizeCookieImportDomain('.example.com')).toBe('example.com') - expect(normalizeCookieImportDomain('foo.github.io')).toBe('foo.github.io') - }) -}) diff --git a/src/main/browser/cookies/client-route/browser-client-route-cookie-import-partition-parity.test.ts b/src/main/browser/cookies/client-route/browser-client-route-cookie-import-partition-parity.test.ts deleted file mode 100644 index 6ae69f700c6..00000000000 --- a/src/main/browser/cookies/client-route/browser-client-route-cookie-import-partition-parity.test.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import type { BrowserHostLeaseAuthority } from '../../../../shared/browser/client-hosting/browser-client-host-protocol' -import type { BrowserNetworkExecutionHost } from '../../../../shared/browser/client-hosting/browser-network-execution-host' - -const { - bindingStore, - detectInstalledBrowsersMock, - getProfileMock, - getRouteIdentityMock, - importCookiesFromBrowserMock, - requireRouteBrowserProfileMock, - updateProfileSourceMock -} = vi.hoisted(() => ({ - bindingStore: { - get: vi.fn(() => null as string | null), - set: vi.fn(() => [] as readonly string[]), - touch: vi.fn(), - findPartitionByFingerprint: vi.fn(() => null as string | null), - rebind: vi.fn() - }, - detectInstalledBrowsersMock: vi.fn(), - getProfileMock: vi.fn(), - getRouteIdentityMock: vi.fn(), - importCookiesFromBrowserMock: vi.fn(), - requireRouteBrowserProfileMock: vi.fn(), - updateProfileSourceMock: vi.fn() -})) - -vi.mock('../browser-cookie-import', () => ({ - detectInstalledBrowsers: detectInstalledBrowsersMock, - importCookiesFromBrowser: importCookiesFromBrowserMock, - selectBrowserProfile: vi.fn() -})) -vi.mock('../../routing/partitions/browser-route-partition-binding-runtime', () => ({ - currentBrowserRoutePartitionBindingStore: () => bindingStore -})) -vi.mock('../../sessions/browser-session-registry', () => ({ - browserSessionRegistry: { - getProfile: getProfileMock, - requireRouteBrowserProfile: requireRouteBrowserProfileMock, - updateProfileSource: updateProfileSourceMock - } -})) -vi.mock('../../client-hosts/lifecycle/paired-runtime-browser-client-host-runtime', () => ({ - getPairedRuntimeBrowserClientRouteIdentity: getRouteIdentityMock -})) - -import { importCookiesIntoClientRoutePartition } from './browser-client-route-cookie-import' -import { BrowserClientNetworkRouteRegistry } from '../../network/client-routes/browser-client-network-route-registry' -import { - browserAuthorityExecutionHostStorageIdentity, - legacyBrowserNativeExecutionHostStorageIdentity -} from '../../routing/identity/browser-execution-host-storage-identity' -import { createBrowserRoutePartitionBindingStoreFake } from '../../routing/partitions/browser-route-partition-binding-store-fake' -import { browserNetworkExecutionHostKey } from '../../network/execution-routes/browser-network-execution-route' -import { BrowserRouteSessionRegistry } from '../../routing/sessions/browser-route-session-registry' -import type { BrowserRouteElectronSession } from '../../routing/sessions/browser-route-session-policy' - -const orcaProfileId = 'orca-profile-a' -const browserProfileId = 'default' -const authorityConnectionIdentity = 'paired-runtime:authority-a' -const authorityRuntimeId = 'runtime-a' -const storageScope = 'e'.repeat(64) - -const authority: BrowserHostLeaseAuthority = { - authorityRuntimeId, - authorityEpoch: 'epoch-a', - browserHostClientId: 'client-a', - browserHostGeneration: 1 -} - -async function pagePartition(host: BrowserNetworkExecutionHost): Promise { - const routes = new BrowserClientNetworkRouteRegistry({ - // Native and WSL routes require their own runtime authority. - authority: - host.kind === 'ssh' ? authority : { ...authority, authorityRuntimeId: host.runtimeId }, - authorityStorageKey: storageScope, - createRoute: () => ({ - start: vi.fn(async () => ({ host: '127.0.0.1', port: 43123 })), - reconnect: vi.fn(async () => ({ host: '127.0.0.1', port: 43123 })), - suspend: vi.fn(), - close: vi.fn(async () => {}) - }) - }) - const route = await routes.retain( - browserNetworkExecutionHostKey(host), - new AbortController().signal - ) - const session: BrowserRouteElectronSession = { - setProxy: vi.fn(async () => {}), - closeAllConnections: vi.fn(async () => {}), - resolveProxy: vi.fn(async () => 'SOCKS5 127.0.0.1:43123') - } - // Exercise the default partition derivation used by real pages. - const sessions = new BrowserRouteSessionRegistry({ - validateProfile: vi.fn(), - getSession: () => session, - setupPolicies: vi.fn(), - clearPolicies: vi.fn(), - retirePageAuthority: vi.fn(() => true), - bindingStore: createBrowserRoutePartitionBindingStoreFake() - }) - const handle = await sessions.preparePage({ - identity: { - orcaProfileId, - browserProfileId, - authorityConnectionIdentity, - executionHostIdentity: route.executionHostIdentity - }, - storageScope, - browserPageId: 'page-a', - pageHostGeneration: 1, - rendererWebContentsId: 11, - proxyEndpoint: route.proxyEndpoint - }) - const partition = handle.partition - handle.release() - await route.release() - await routes.close() - return partition -} - -async function importPartition(): Promise { - bindingStore.get.mockReturnValue(null) - getProfileMock.mockReturnValue({ id: browserProfileId, partition: 'persist:legacy-server' }) - detectInstalledBrowsersMock.mockReturnValue([ - { family: 'chrome', label: 'Chrome', profiles: [], selectedProfile: 'Default' } - ]) - importCookiesFromBrowserMock.mockResolvedValue({ ok: true, summary: { importedCookies: 1 } }) - getRouteIdentityMock.mockReturnValue({ - orcaProfileId, - authorityConnectionIdentity, - // Settings imports target the server machine, using the host startup identity. - executionHostIdentity: browserAuthorityExecutionHostStorageIdentity(storageScope), - pairingAuthorityConnectionIdentity: 'paired-runtime:pairing-authority-a', - legacyAuthorityConnectionIdentity: 'paired-runtime:legacy-authority-a', - legacyExecutionHostIdentity: - legacyBrowserNativeExecutionHostStorageIdentity(authorityRuntimeId), - storageScope - }) - - const result = await importCookiesIntoClientRoutePartition({ - environmentId: 'env-a', - browserProfileId, - browserFamily: 'chrome' - }) - - expect(result).toMatchObject({ ok: true }) - return importCookiesFromBrowserMock.mock.calls.at(-1)?.[1] as string -} - -describe('cookie-import target vs. client-hosted page partition', () => { - it('imports into the same partition a page on the server’s own machine uses', async () => { - const imported = await importPartition() - - expect(imported).toBe( - await pagePartition({ kind: 'native', runtimeId: authorityRuntimeId, revision: 1 }) - ) - }) - - // Settings imports exclude nested SSH and WSL workspace partitions. - it('does not reach pages hosted on a nested SSH target or WSL distro', async () => { - const imported = await importPartition() - - expect(imported).not.toBe( - await pagePartition({ - kind: 'ssh', - targetId: 'ssh-1700000000-aaa111', - providerEpoch: 'epoch-1', - connectionGeneration: 1 - }) - ) - expect(imported).not.toBe( - await pagePartition({ - kind: 'wsl', - runtimeId: authorityRuntimeId, - revision: 1, - distro: 'Ubuntu' - }) - ) - }) -}) diff --git a/src/main/browser/cookies/client-route/browser-client-route-cookie-import.test.ts b/src/main/browser/cookies/client-route/browser-client-route-cookie-import.test.ts index 3ec023ad6a9..d925afa7b4a 100644 --- a/src/main/browser/cookies/client-route/browser-client-route-cookie-import.test.ts +++ b/src/main/browser/cookies/client-route/browser-client-route-cookie-import.test.ts @@ -102,13 +102,6 @@ describe('importCookiesIntoClientRoutePartition', () => { expect(importCookiesFromBrowserMock).not.toHaveBeenCalled() }) - it('rejects a traversing browser profile name before touching the filesystem', async () => { - expect( - await importCookiesIntoClientRoutePartition({ ...request, browserProfile: '../../etc' }) - ).toMatchObject({ ok: false, reason: 'Invalid browser profile name.' }) - expect(detectInstalledBrowsersMock).not.toHaveBeenCalled() - }) - // A replacement during import must not report success for an abandoned partition. it('fails the import when the server is re-paired mid-import', async () => { getRouteIdentityMock.mockReturnValueOnce(routeIdentity).mockReturnValue(repairedRouteIdentity) @@ -122,25 +115,4 @@ describe('importCookiesIntoClientRoutePartition', () => { expect(importCookiesFromBrowserMock).toHaveBeenCalledOnce() expect(recordClientRouteImportSourceMock).not.toHaveBeenCalled() }) - - it('fails the import when the client host is retired mid-import', async () => { - getRouteIdentityMock.mockReturnValueOnce(routeIdentity).mockReturnValue(null) - - const result = await importCookiesIntoClientRoutePartition(request) - - expect(result).toMatchObject({ - ok: false, - reason: 'The connection to this server ended during the import. Reconnect and try again.' - }) - expect(recordClientRouteImportSourceMock).not.toHaveBeenCalled() - }) - - it('rejects an unknown browser session profile', async () => { - getProfileMock.mockReturnValue(null) - - expect(await importCookiesIntoClientRoutePartition(request)).toMatchObject({ - ok: false, - reason: 'Session profile not found.' - }) - }) }) diff --git a/src/main/browser/cookies/sources/browser-cookie-decryption.test.ts b/src/main/browser/cookies/sources/browser-cookie-decryption.test.ts index 33ed500cc51..5c19a557d95 100644 --- a/src/main/browser/cookies/sources/browser-cookie-decryption.test.ts +++ b/src/main/browser/cookies/sources/browser-cookie-decryption.test.ts @@ -35,18 +35,4 @@ describe('decryptCookieValueRaw host-key hash prefix', () => { const encrypted = encryptGcm(hostKeyHash(HOST_KEY)) expect(decryptCookieValueRaw(encrypted, gcmKeyResult, HOST_KEY)).toEqual(Buffer.alloc(0)) }) - - it('keeps a pre-schema-24 value that has no prefix', () => { - const encrypted = encryptCbc(Buffer.from('plain-legacy-value')) - expect(decryptCookieValueRaw(encrypted, cbcKeyResult, HOST_KEY)).toEqual( - Buffer.from('plain-legacy-value') - ) - }) - - it('still strips a binary prefix that is not this host key hash', () => { - const plaintext = Buffer.concat([hostKeyHash('.other.com'), Buffer.from('value')]) - expect(decryptCookieValueRaw(encryptCbc(plaintext), cbcKeyResult, HOST_KEY)).toEqual( - Buffer.from('value') - ) - }) }) diff --git a/src/main/browser/document-preview/doc-preview-grant-registry.test.ts b/src/main/browser/document-preview/doc-preview-grant-registry.test.ts index 6fbf0479953..095e0c538e7 100644 --- a/src/main/browser/document-preview/doc-preview-grant-registry.test.ts +++ b/src/main/browser/document-preview/doc-preview-grant-registry.test.ts @@ -38,29 +38,6 @@ describe('resolveDocPreviewTargetPath', () => { expect(resolveDocPreviewTargetPath(atWorkspaceRoot, 'docs/styles.css')).toBeNull() }) - it('authorizes only the requested sibling directory', () => { - const grant = mintDocPreviewGrant({ - owner: sshOwner, - requestBase: '/srv/repo', - root: '/srv/repo/docs', - entryRelativePath: 'docs/report.html', - browserPageId: 'page-1' - }) - - expect(resolveDocPreviewTargetPath(grant, 'docs/report.html')).toBe( - '/srv/repo/docs/report.html' - ) - expect(resolveDocPreviewTargetPath(grant, 'assets/chart.js')).toBeNull() - expect(resolveDocPreviewTargetPath(grant, '.env')).toBeNull() - - expect(authorizeDocPreviewDirectory(grant.id, 'assets/chart.js')).toBe(true) - expect(resolveDocPreviewTargetPath(grant, 'assets/chart.js')).toBe('/srv/repo/assets/chart.js') - expect(resolveDocPreviewTargetPath(grant, 'assets/theme.css')).toBe( - '/srv/repo/assets/theme.css' - ) - expect(resolveDocPreviewTargetPath(grant, '.env')).toBeNull() - }) - it('uses Windows semantics when authorizing a sibling directory', () => { const grant = mintDocPreviewGrant({ owner: sshOwner, diff --git a/src/main/browser/document-preview/doc-preview-terminal-file-reader.test.ts b/src/main/browser/document-preview/doc-preview-terminal-file-reader.test.ts index 04d9eba6c94..9810a15d107 100644 --- a/src/main/browser/document-preview/doc-preview-terminal-file-reader.test.ts +++ b/src/main/browser/document-preview/doc-preview-terminal-file-reader.test.ts @@ -98,26 +98,6 @@ describe('outside-workspace HTML preview', () => { }) }) - // A macOS host spells /tmp as /private/tmp and returns a case-insensitive volume's stored name. - it('reads paths the host spells differently once canonicalized', async () => { - const page = grant() - authorizeDocPreviewDirectory(page.id, 'assets/styles.css') - host( - { - '/tmp/reports': { canonical: '/private/tmp/reports' }, - '/tmp/reports/report.html': { canonical: '/private/tmp/reports/report.html' }, - '/tmp/reports/assets': { canonical: '/private/tmp/reports/assets' }, - '/tmp/reports/assets/styles.css': { canonical: '/private/tmp/reports/assets/Styles.css' } - }, - { ok: true, result: { content: 'h1{}', isBinary: false } } - ) - - await expect(readDocPreviewFile(page, 'report.html')).resolves.toMatchObject({ ok: true }) - await expect(readDocPreviewFile(page, 'assets/styles.css')).resolves.toMatchObject({ - ok: true - }) - }) - it('refuses symlink redirection outside an approved asset directory', async () => { const page = grant() authorizeDocPreviewDirectory(page.id, 'assets/data.json') diff --git a/src/main/browser/element-grab/browser-grab-payload.test.ts b/src/main/browser/element-grab/browser-grab-payload.test.ts index 6cb5750b125..e08471d719b 100644 --- a/src/main/browser/element-grab/browser-grab-payload.test.ts +++ b/src/main/browser/element-grab/browser-grab-payload.test.ts @@ -67,19 +67,6 @@ function makeRawPayload(overrides: Record = {}) { } describe('clampGrabPayload', () => { - it('redacts secret-bearing cssClasses outside the attribute path', () => { - const payload = clampGrabPayload( - makeRawPayload({ - target: { - ...makeRawPayload().target, - cssClasses: 'primary access_token=secret' - } - }) - ) - - expect(payload?.target.cssClasses).toBe('[redacted]') - }) - it('redacts secret-bearing browser annotation metadata fields', () => { const payload = clampGrabPayload( makeRawPayload({ @@ -161,60 +148,4 @@ describe('clampGrabPayload', () => { revokeDocPreviewGrant(grant.id) expect(clampGrabPayload(raw, guestUrl)?.page.sanitizedUrl).toBe('') }) - - it('keeps safe and ARIA attributes while stripping unsafe names', () => { - const payload = clampGrabPayload( - makeRawPayload({ - target: { - ...makeRawPayload().target, - attributes: { - ID: 'checkout', - class: 'source-code stateful auth token', - role: 'button', - type: 'button', - 'aria-label': 'Submit', - 'aria-labelledby': 'checkout-label', - 'aria-hidden': 'false', - onclick: 'alert(1)', - style: 'display:none', - 'data-secret': 'hidden', - 'notaria-label': 'unexpected' - } - } - }) - ) - - expect(payload?.target.attributes).toEqual({ - id: 'checkout', - class: 'source-code stateful auth token', - role: 'button', - type: 'button', - 'aria-label': 'Submit', - 'aria-labelledby': 'checkout-label', - 'aria-hidden': 'false' - }) - }) - - it.each([ - 'access_token=credential', - 'API_KEY=credential', - 'password=credential', - 'secret=credential', - 'session_id=credential', - 'csrf=credential' - ])('redacts %s in allowed attribute values', (value) => { - const payload = clampGrabPayload( - makeRawPayload({ - target: { - ...makeRawPayload().target, - attributes: { title: value, 'aria-label': value } - } - }) - ) - - expect(payload?.target.attributes).toEqual({ - title: '[redacted]', - 'aria-label': '[redacted]' - }) - }) }) diff --git a/src/main/browser/guest-lifecycle/manager/browser-manager-guest-lifecycle.test.ts b/src/main/browser/guest-lifecycle/manager/browser-manager-guest-lifecycle.test.ts deleted file mode 100644 index 7982a41855b..00000000000 --- a/src/main/browser/guest-lifecycle/manager/browser-manager-guest-lifecycle.test.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { EventEmitter } from 'node:events' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -const browserMocks = vi.hoisted(() => ({ - appGetPathMock: vi.fn(() => '/downloads'), - shellOpenExternalMock: vi.fn(), - browserWindowFromWebContentsMock: vi.fn(), - menuBuildFromTemplateMock: vi.fn(), - guestOffMock: vi.fn(), - guestOnMock: vi.fn(), - guestSetBackgroundThrottlingMock: vi.fn(), - guestSetWindowOpenHandlerMock: vi.fn(), - guestOpenDevToolsMock: vi.fn(), - webContentsFromIdMock: vi.fn(), - screenGetCursorScreenPointMock: vi.fn(() => ({ x: 0, y: 0 })), - openPopupWithOriginBarMock: vi.fn() -})) - -vi.mock('electron', () => ({ - WebContentsView: class { - onDestroyed: (() => void) | null = null - webContents = { - id: 9001, - isDestroyed: () => false, - close: () => this.onDestroyed?.(), - once: (_event: string, listener: () => void) => { - this.onDestroyed = listener - }, - focus: vi.fn() - } - setVisible(): void {} - }, - app: { - getPath: browserMocks.appGetPathMock - }, - BrowserWindow: { - fromWebContents: browserMocks.browserWindowFromWebContentsMock - }, - clipboard: { writeText: vi.fn() }, - shell: { openExternal: browserMocks.shellOpenExternalMock }, - Menu: { - buildFromTemplate: browserMocks.menuBuildFromTemplateMock - }, - screen: { - getCursorScreenPoint: browserMocks.screenGetCursorScreenPointMock - }, - webContents: { - fromId: browserMocks.webContentsFromIdMock - } -})) - -vi.mock('../popups/popup-origin-bar-window', () => ({ - openPopupWithOriginBar: browserMocks.openPopupWithOriginBarMock -})) - -import { browserManager } from '../../browser-manager' -import { - rendererWebContentsId, - resetBrowserManagerMocks, - resetBrowserManagerState -} from './browser-manager-test-harness' -import { revokeAllDocPreviewGrants } from '../../document-preview/doc-preview-grant-registry' - -const { - guestSetBackgroundThrottlingMock, - guestSetWindowOpenHandlerMock, - guestOpenDevToolsMock, - webContentsFromIdMock -} = browserMocks - -describe('browserManager', () => { - beforeEach(() => { - resetBrowserManagerMocks(browserMocks) - resetBrowserManagerState() - revokeAllDocPreviewGrants() - }) - - afterEach(() => { - vi.useRealTimers() - }) - - // The headless backend destroys its window when registration returns false. - - it("publishes an offscreen page's top-level address changes, not its subframes'", () => { - const stateChanged = vi.fn() - browserManager.setBrowserGuestStateChangedListener(stateChanged) - let address = 'https://example.com/' - const guest = Object.assign(new EventEmitter(), { - id: 515, - isDestroyed: vi.fn(() => false), - getType: vi.fn(() => 'window'), - setBackgroundThrottling: guestSetBackgroundThrottlingMock, - isCurrentlyAudible: vi.fn(() => false), - setWindowOpenHandler: guestSetWindowOpenHandlerMock, - getURL: vi.fn(() => address) - }) - webContentsFromIdMock.mockReturnValue(guest) - browserManager.registerOffscreenGuest({ - browserPageId: 'offscreen-page', - worktreeId: 'remote-worktree', - webContentsId: guest.id - }) - const navigateInPage = (url: string): void => { - address = url - guest.emit('did-navigate-in-page', {}, url, true, 1, 1) - } - - guest.emit('did-navigate-in-page', {}, 'https://frame.example/#x', false, 1, 2) - expect(stateChanged).not.toHaveBeenCalled() - - navigateInPage('https://example.com/#top') - expect(stateChanged).toHaveBeenCalledTimes(1) - expect(stateChanged).toHaveBeenLastCalledWith('remote-worktree') - - // replaceState without a new address. - navigateInPage('https://example.com/#top') - expect(stateChanged).toHaveBeenCalledTimes(1) - - address = 'https://example.com/next' - guest.emit('did-navigate', {}, address, 200, 'OK') - expect(stateChanged).toHaveBeenCalledTimes(2) - - // A full navigation also counts as the address to compare with. - navigateInPage('https://example.com/next') - expect(stateChanged).toHaveBeenCalledTimes(2) - - browserManager.unregisterGuest('offscreen-page') - expect(guest.listenerCount('did-navigate-in-page')).toBe(0) - }) - - it('retires stale guest mappings when a page re-registers after a process swap', () => { - const rendererSendMock = vi.fn() - const oldGuestOnMock = vi.fn() - const oldGuestOffMock = vi.fn() - const newGuestOnMock = vi.fn() - const newGuestOffMock = vi.fn() - const oldGuest = { - id: 501, - isDestroyed: vi.fn(() => false), - getType: vi.fn(() => 'webview'), - isCurrentlyAudible: vi.fn(() => false), - send: vi.fn(), - setBackgroundThrottling: guestSetBackgroundThrottlingMock, - setWindowOpenHandler: guestSetWindowOpenHandlerMock, - on: oldGuestOnMock, - off: oldGuestOffMock, - openDevTools: guestOpenDevToolsMock, - getURL: vi.fn(() => 'https://old.example') - } - const newGuest = { - id: 502, - isDestroyed: vi.fn(() => false), - getType: vi.fn(() => 'webview'), - isCurrentlyAudible: vi.fn(() => false), - send: vi.fn(), - setBackgroundThrottling: guestSetBackgroundThrottlingMock, - setWindowOpenHandler: guestSetWindowOpenHandlerMock, - on: newGuestOnMock, - off: newGuestOffMock, - openDevTools: guestOpenDevToolsMock, - getURL: vi.fn(() => 'https://new.example') - } - - webContentsFromIdMock.mockImplementation((id: number) => { - if (id === oldGuest.id) { - return oldGuest - } - if (id === newGuest.id) { - return newGuest - } - if (id === rendererWebContentsId) { - return { isDestroyed: vi.fn(() => false), send: rendererSendMock } - } - return null - }) - - browserManager.attachGuestPolicies(oldGuest as never) - browserManager.registerGuest({ - browserPageId: 'browser-1', - webContentsId: oldGuest.id, - rendererWebContentsId - }) - - browserManager.attachGuestPolicies(newGuest as never) - browserManager.registerGuest({ - browserPageId: 'browser-1', - webContentsId: newGuest.id, - rendererWebContentsId - }) - // Registration also reports audio state; this test is about load-failure routing. - rendererSendMock.mockClear() - - const oldDidFailLoadHandler = oldGuestOnMock.mock.calls.find( - ([event]) => event === 'did-fail-load' - )?.[1] as - | (( - event: unknown, - errorCode: number, - errorDescription: string, - validatedUrl: string, - isMainFrame: boolean - ) => void) - | undefined - const newDidFailLoadHandler = newGuestOnMock.mock.calls.find( - ([event]) => event === 'did-fail-load' - )?.[1] as - | (( - event: unknown, - errorCode: number, - errorDescription: string, - validatedUrl: string, - isMainFrame: boolean - ) => void) - | undefined - - oldDidFailLoadHandler?.(null, -105, 'Old guest failed', 'https://old.example', true) - expect(rendererSendMock).not.toHaveBeenCalled() - - newDidFailLoadHandler?.(null, -106, 'New guest failed', 'https://new.example', true) - expect(rendererSendMock).toHaveBeenCalledWith('browser:guest-load-failed', { - browserPageId: 'browser-1', - loadError: { - code: -106, - description: 'New guest failed', - validatedUrl: 'https://new.example' - } - }) - expect(oldGuestOffMock).toHaveBeenCalled() - expect(browserManager.getGuestWebContentsId('browser-1')).toBe(newGuest.id) - expect(browserManager.getTabIdForWebContentsId(oldGuest.id)).toBeNull() - expect(browserManager.getTabIdForWebContentsId(newGuest.id)).toBe('browser-1') - - browserManager.unregisterGuest('browser-1') - expect(browserManager.getTabIdForWebContentsId(newGuest.id)).toBeNull() - }) - - // Browsing guests track override invalidation without attaching a debugger. - - // The renderer re-applies a tab's mute on each registration report and clears the - // speaker on the unregister report, so neither may be skipped. -}) diff --git a/src/main/codex/history/index/codex-state-db-backfill-recovery.test.ts b/src/main/codex/history/index/codex-state-db-backfill-recovery.test.ts deleted file mode 100644 index d31091e3e8a..00000000000 --- a/src/main/codex/history/index/codex-state-db-backfill-recovery.test.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { EventEmitter } from 'node:events' -import { link, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, describe, expect, it, vi } from 'vitest' -import * as ownerIdentity from '../../../agent-hooks/managed-hook-owner-identity' -import { - _internals, - resolveCodexBackfillSupervisorLockRoot, - runCodexStateDbBackfillRecovery, - startCodexStateDbBackfillRecoveryInBackground, - withCodexBackfillSupervisorLock -} from './codex-state-db-backfill-recovery' -import type { CodexStateDbBackfillStatus } from './codex-state-db' - -const temporaryRoots: string[] = [] -const originalPlatform = process.platform - -function createFakeChild(): EventEmitter & { - stdin: { end: ReturnType } - exitCode: number | null - signalCode: NodeJS.Signals | null - kill: ReturnType -} { - return Object.assign(new EventEmitter(), { - stdin: { end: vi.fn() }, - exitCode: null, - signalCode: null, - kill: vi.fn(() => true) - }) -} - -async function createTemporaryRoot(): Promise { - const root = await mkdtemp(join(tmpdir(), 'orca-codex-backfill-recovery-')) - temporaryRoots.push(root) - return root -} - -afterEach(async () => { - _internals.resetForTests() - Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform }) - vi.restoreAllMocks() - vi.unstubAllEnvs() - await Promise.all( - temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })) - ) -}) - -describe('Codex state DB backfill recovery', () => { - it('does not start a supervisor for an unreadable index', async () => { - const home = await createTemporaryRoot() - await writeFile(join(home, 'state_5.sqlite'), 'not a sqlite database') - const run = vi.fn() - const withLock = vi.fn() - - await expect( - startCodexStateDbBackfillRecoveryInBackground(home, { - run, - withLock: withLock as never - }) - ).resolves.toBeNull() - expect(withLock).not.toHaveBeenCalled() - expect(run).not.toHaveBeenCalled() - }) - - it('bounds permanent claimant exits just beyond the fast-failure window', async () => { - const controller = new AbortController() - const children = new Map, number>() - const timerDurations: number[] = [] - let now = 0 - let exitCount = 0 - let pendingTimers = 0 - let maxPendingTimers = 0 - let maxLiveChildren = 0 - let safetyFuseTripped = false - const terminate = vi.fn(async () => {}) - const spawnProcess = vi.fn(() => { - const child = createFakeChild() - children.set(child, now + 10_001) - maxLiveChildren = Math.max(maxLiveChildren, children.size) - return child - }) - const sleep = vi.fn(async (ms: number) => { - timerDurations.push(ms) - pendingTimers += 1 - maxPendingTimers = Math.max(maxPendingTimers, pendingTimers) - const wakeAt = now + ms - for (const [child, exitAt] of children) { - if (exitAt <= wakeAt) { - now = exitAt - children.delete(child) - exitCount += 1 - child.emit('exit', 1, null) - } - } - now = wakeAt - pendingTimers -= 1 - if (ms === 2_000 && spawnProcess.mock.calls.length === 6) { - safetyFuseTripped = true - controller.abort() - throw Object.assign(new Error('reproduction safety fuse'), { name: 'AbortError' }) - } - await Promise.resolve() - }) - - const summary = await runCodexStateDbBackfillRecovery('/managed-home', controller.signal, { - spawnProcess: spawnProcess as never, - readStatus: vi.fn((): CodexStateDbBackfillStatus => ({ - kind: 'incomplete', - stateDbPath: '/state.sqlite', - status: 'running' - })), - terminate, - sleep, - now: () => now - }) - - expect({ - summary, - spawnCount: spawnProcess.mock.calls.length, - exitCount, - timerCount: timerDurations.length, - pollCount: timerDurations.filter((ms) => ms === 5_000).length, - backoffCount: timerDurations.filter((ms) => ms === 2_000).length, - liveChildren: children.size, - pendingTimers, - maxLiveChildren, - maxPendingTimers, - terminateCount: terminate.mock.calls.length, - safetyFuseTripped - }).toEqual({ - summary: { outcome: 'gave-up', spawnCount: 5 }, - spawnCount: 5, - exitCount: 5, - timerCount: 19, - pollCount: 15, - backoffCount: 4, - liveChildren: 0, - pendingTimers: 0, - maxLiveChildren: 1, - maxPendingTimers: 1, - terminateCount: 0, - safetyFuseTripped: false - }) - }) -}) - -describe.skipIf(process.platform === 'win32')('Codex backfill supervisor owner lock', () => { - it('recovers a dead owner whose PID was reused with a different start identity', async () => { - const userData = await createTemporaryRoot() - vi.stubEnv('ORCA_USER_DATA_PATH', userData) - const home = join(userData, 'managed-home') - const lockRoot = resolveCodexBackfillSupervisorLockRoot(home) - const lockParent = join(lockRoot, '.orca') - const token = '00000000-0000-4000-8000-000000000000' - const ownerPath = join(lockParent, `managed-hook-install.owner-${token}.json`) - const lockPath = join(lockParent, 'managed-hook-install.lock') - await mkdir(lockParent, { recursive: true }) - await writeFile( - ownerPath, - JSON.stringify({ - token, - pid: process.pid, - hostIdentity: await ownerIdentity.readManagedHookHostIdentity(), - processIdentity: 'stale-process-start-time' - }) - ) - await link(ownerPath, lockPath) - - await expect( - withCodexBackfillSupervisorLock(home, undefined, async () => 'recovered') - ).resolves.toBe('recovered') - }) - - it('does not interfere with a live supervisor from another Orca instance', async () => { - const userData = await createTemporaryRoot() - vi.stubEnv('ORCA_USER_DATA_PATH', userData) - const home = join(userData, 'managed-home') - let releaseFirst!: () => void - const first = withCodexBackfillSupervisorLock( - home, - undefined, - async () => await new Promise((resolve) => (releaseFirst = resolve)) - ) - await vi.waitFor(async () => { - await expect( - import('node:fs/promises').then(({ readFile }) => - readFile( - join( - resolveCodexBackfillSupervisorLockRoot(home), - '.orca', - 'managed-hook-install.lock' - ), - 'utf8' - ) - ) - ).resolves.toContain('processIdentity') - }) - const controller = new AbortController() - const secondRun = vi.fn(async () => {}) - setTimeout(() => controller.abort(), 40) - - await expect( - withCodexBackfillSupervisorLock(home, controller.signal, secondRun) - ).rejects.toMatchObject({ name: 'AbortError' }) - expect(secondRun).not.toHaveBeenCalled() - releaseFirst() - await first - }) -}) diff --git a/src/main/codex/history/resume/codex-legacy-session-resume.test.ts b/src/main/codex/history/resume/codex-legacy-session-resume.test.ts index a18dbb8fb14..f3935fd8a4f 100644 --- a/src/main/codex/history/resume/codex-legacy-session-resume.test.ts +++ b/src/main/codex/history/resume/codex-legacy-session-resume.test.ts @@ -1,23 +1,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { - existsSync, - linkSync, - mkdtempSync, - mkdirSync, - readFileSync, - rmSync, - statSync, - writeFileSync -} from 'node:fs' +import { mkdtempSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' -import { basename, dirname, join } from 'node:path' +import { basename, join } from 'node:path' import type * as NodeFsPromises from 'node:fs/promises' -import { - codexRolloutHardlinkIdentity, - dedupeCodexRolloutFileAliases -} from '../../../ai-vault/scanners/codex/codex-session-root-dedup' +import '../../../ai-vault/scanners/codex/codex-session-root-dedup' import { prepareLegacySharedCodexSessionResume } from './codex-legacy-session-resume' -import { ManagedCodexHomeTemporarilyUnavailableError } from '../../../codex-accounts/host-codex-managed-home-ownership' +import '../../../codex-accounts/host-codex-managed-home-ownership' const lstatFaults = vi.hoisted(() => ({ path: null as string | null, @@ -89,21 +77,6 @@ describe('prepareLegacySharedCodexSessionResume', () => { expect(statSync(targetPath).ino).toBe(statSync(rolloutPath).ino) }) - it('coalesces concurrent resumes of the same legacy rollout', async () => { - const [first, second] = await Promise.all([prepare(), prepare()]) - - expect(first.useRealCodexHome).toBe(true) - expect(second.useRealCodexHome).toBe(true) - expect(readFileSync(targetRolloutPath(), 'utf-8')).toContain('session_meta') - }) - - it('fails clearly and retryably instead of approving a missing rollout', async () => { - rmSync(rolloutPath) - - await expect(prepare()).rejects.toThrow(/Retry resume/) - expect(existsSync(targetRolloutPath())).toBe(false) - }) - it('fails closed when a different rollout already owns the target path', async () => { mkdirSync(join(targetRolloutPath(), '..'), { recursive: true }) writeFileSync(targetRolloutPath(), '{"different":true}\n', 'utf-8') @@ -137,125 +110,3 @@ describe('prepareLegacySharedCodexSessionResume', () => { return join(systemHome, 'sessions', '2026', '07', '20', basename(rolloutPath)) } }) - -// Vault dedup's smallest bridge alias may name a peer account instead of the selected one. -describe('per-account resume repin', () => { - let root: string - // Make the selected account lose the path tie-break. - let peerHome: string - let selectedHome: string - let recordedRolloutPath: string - let bridgedRolloutPath: string - - const rolloutRelativePath = join( - 'sessions', - '2026', - '07', - '20', - 'rollout-2026-07-20T10-00-00-abcdef00-1234-4321-9999-cafecafecafe.jsonl' - ) - - beforeEach(() => { - root = mkdtempSync(join(tmpdir(), 'orca-codex-account-repin-')) - peerHome = join(root, 'codex-accounts', '11111111-aaaa-4aaa-8aaa-111111111111', 'home') - selectedHome = join(root, 'codex-accounts', '99999999-bbbb-4bbb-8bbb-999999999999', 'home') - recordedRolloutPath = join(selectedHome, rolloutRelativePath) - bridgedRolloutPath = join(peerHome, rolloutRelativePath) - mkdirSync(dirname(recordedRolloutPath), { recursive: true }) - writeFileSync(recordedRolloutPath, '{"type":"session_meta"}\n', 'utf-8') - mkdirSync(dirname(bridgedRolloutPath), { recursive: true }) - linkSync(recordedRolloutPath, bridgedRolloutPath) - }) - - afterEach(() => { - rmSync(root, { recursive: true, force: true }) - }) - - it.each([['recorded rollout first'], ['bridged alias first']])( - 'repins the surviving vault row to the selected account, discovered %s', - async (order) => { - const recorded = rolloutCandidate(recordedRolloutPath, selectedHome) - const bridged = rolloutCandidate(bridgedRolloutPath, peerHome) - const survivors = dedupeCodexRolloutFileAliases( - order === 'recorded rollout first' ? [recorded, bridged] : [bridged, recorded], - rolloutCandidateAccessors - ) - - expect(survivors).toEqual([bridged]) - - const result = await prepareLegacySharedCodexSessionResume( - { - agent: 'codex', - filePath: bridged.filePath, - codexHome: peerHome, - executionHostId: 'local' - }, - repinOptions() - ) - - expect(result).toEqual({ useRealCodexHome: false, substituteCodexHome: selectedHome }) - } - ) - - it('declines while the bridge has not yet linked the rollout into the selected home', async () => { - const unbridgedSelectedHome = join( - root, - 'codex-accounts', - 'ffffffff-cccc-4ccc-8ccc-ffffffffffff', - 'home' - ) - mkdirSync(unbridgedSelectedHome, { recursive: true }) - - const result = await prepareLegacySharedCodexSessionResume( - { - agent: 'codex', - filePath: bridgedRolloutPath, - codexHome: peerHome, - executionHostId: 'local' - }, - { ...repinOptions(), getSelectedHostAccountCodexHomePath: () => unbridgedSelectedHome } - ) - - expect(result).toEqual({ useRealCodexHome: false }) - }) - - it('refuses when the selected rollout is temporarily unreadable', async () => { - lstatFaults.path = recordedRolloutPath - - await expect( - prepareLegacySharedCodexSessionResume( - { - agent: 'codex', - filePath: bridgedRolloutPath, - codexHome: peerHome, - executionHostId: 'local' - }, - repinOptions() - ) - ).rejects.toBeInstanceOf(ManagedCodexHomeTemporarilyUnavailableError) - }) - - function repinOptions() { - return { - isHostSystemDefaultRealHomeSelected: () => false, - getSelectedHostAccountCodexHomePath: () => selectedHome - } - } - - function rolloutCandidate(filePath: string, codexHome: string) { - const stat = statSync(filePath) - return { - filePath, - codexHome, - file: { dev: stat.dev, ino: stat.ino, nlink: stat.nlink } - } - } -}) - -const rolloutCandidateAccessors = { - isCodex: () => true, - getFilePath: (candidate: { filePath: string }) => candidate.filePath, - getCodexHome: (candidate: { codexHome: string }) => candidate.codexHome, - getHardlinkIdentity: (candidate: { file: { dev: number; ino: number; nlink: number } }) => - codexRolloutHardlinkIdentity(candidate.file) -} diff --git a/src/main/codex/hooks/installation/windows-hook-upgrade.test.ts b/src/main/codex/hooks/installation/windows-hook-upgrade.test.ts deleted file mode 100644 index 03ab9c2214d..00000000000 --- a/src/main/codex/hooks/installation/windows-hook-upgrade.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' -import { join } from 'node:path' -import type * as Os from 'node:os' -import { setupCodexHookHomes } from './hook-service-test-harness' - -const { getPathMock, homedirMock } = vi.hoisted(() => ({ - getPathMock: vi.fn<(name: string) => string>(), - homedirMock: vi.fn<() => string>() -})) -vi.mock('electron', () => ({ app: { getPath: getPathMock } })) -vi.mock('os', async (importOriginal) => ({ - ...(await importOriginal()), - homedir: homedirMock -})) - -import './hook-service' -import { CodexHookService } from './codex-hook-service-implementation' -import { CODEX_EVENTS, CODEX_EVENT_LABEL } from '../definitions/codex-hook-definition' -import { buildCodexHookCommand as getManagedCommand } from '../definitions/codex-hook-command-form' -import '../../../agent-hooks/installer-utils' -import { readHooksJson } from '../../../agent-hooks/hooks-json-read' -import { wrapWindowsHookCommand } from '../../../agent-hooks/windows-hook-command' -import { upsertHookTrustEntries } from '../../trust/toml/config-toml-trust' -import { - computeCodexTrustedHash as computeTrustedHash, - getExplicitHomeCodexHookSourcePath as getCodexExplicitHomeHookSourcePath -} from '../../trust/identity/codex-trust-identity' - -const homes = setupCodexHookHomes(homedirMock, getPathMock) - -describe.skipIf(process.platform !== 'win32')('Unicode Windows hook upgrade', () => { - it('replaces all encoded commands and trust hashes while preserving user hooks on reinstall', async () => { - const home = join(homes.tmpHome, '测试 用户') - mkdirSync(home) - homedirMock.mockReturnValue(home) - const runtimeHome = join(homes.userDataDir, 'codex-runtime-home', 'home') - const configPath = join(runtimeHome, 'hooks.json') - const tomlPath = join(runtimeHome, 'config.toml') - const scriptPath = join(home, '.orca', 'agent-hooks', 'codex-hook.cmd') - const oldCommand = wrapWindowsHookCommand(scriptPath) - const userHome = join(home, '.codex') - mkdirSync(userHome) - const userConfig = JSON.stringify({ - hooks: { Stop: [{ hooks: [{ type: 'command', command: 'user-hook' }] }] } - }) - writeFileSync(join(userHome, 'hooks.json'), userConfig) - mkdirSync(runtimeHome, { recursive: true }) - writeFileSync( - configPath, - JSON.stringify({ - hooks: Object.fromEntries( - CODEX_EVENTS.map((event) => [ - event, - [{ hooks: [{ type: 'command', command: oldCommand, timeout: 10 }] }] - ]) - ) - }) - ) - upsertHookTrustEntries( - tomlPath, - CODEX_EVENTS.map((event) => ({ - sourcePath: getCodexExplicitHomeHookSourcePath(configPath), - eventLabel: CODEX_EVENT_LABEL[event], - groupIndex: 0, - handlerIndex: 0, - command: oldCommand, - timeoutSec: 10 - })) - ) - const service = new CodexHookService() - expect(service.getStatus(runtimeHome).state).not.toBe('installed') - for (let pass = 0; pass < 2; pass++) { - expect((await service.install()).state).toBe('installed') - expect(service.getStatus(runtimeHome).state).toBe('installed') - const hooks = readHooksJson(configPath)?.hooks - const trust = readFileSync(tomlPath, 'utf8') - for (const event of CODEX_EVENTS) { - const commands = hooks?.[event]?.flatMap((group) => group.hooks ?? []) ?? [] - expect( - commands.filter((hook) => hook.command === getManagedCommand(scriptPath)) - ).toHaveLength(1) - expect(commands.some((hook) => hook.command === oldCommand)).toBe(false) - const entry = { - sourcePath: getCodexExplicitHomeHookSourcePath(configPath), - eventLabel: CODEX_EVENT_LABEL[event], - groupIndex: 0, - handlerIndex: 0, - command: getManagedCommand(scriptPath), - timeoutSec: 10 - } - expect(trust).toContain(computeTrustedHash(entry)) - expect(trust).not.toContain(computeTrustedHash({ ...entry, command: oldCommand })) - } - expect( - hooks?.Stop?.some((group) => group.hooks?.some((hook) => hook.command === 'user-hook')) - ).toBe(true) - expect(readFileSync(join(userHome, 'hooks.json'), 'utf8')).toBe(userConfig) - } - }) -}) diff --git a/src/main/codex/trust/toml/codex-trust-config-concurrent-launch.test.ts b/src/main/codex/trust/toml/codex-trust-config-concurrent-launch.test.ts deleted file mode 100644 index fb0b9c96f0e..00000000000 --- a/src/main/codex/trust/toml/codex-trust-config-concurrent-launch.test.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { - CodexHookTrustGrantRequest, - CodexHookTrustGrantSessionResult -} from '../../app-server/codex-app-server-client' -import type { CodexManagedTrustGrantPlan } from '../grants/codex-managed-trust-grant-plan' -import type { CodexTrustEntry } from './config-toml-trust' - -// Keep the guest identity probe unstamped without requiring wsl.exe. -vi.mock('@orca/process-host', () => ({ - runProcess: vi.fn(async () => ({ - code: 1, - signal: null, - timedOut: false, - stdout: '', - stderr: '' - })) -})) - -const testState = { - fakeHomeDir: '', - userDataDir: '', - previousUserDataPath: undefined as string | undefined -} - -vi.mock(import('node:os'), async (importOriginal) => { - const actual = await importOriginal() - return { ...actual, homedir: () => testState.fakeHomeDir } -}) - -const { CodexAppServerUnsupportedError } = await import('../../app-server/codex-app-server-session') -const { codexAppServerCapabilityCache } = - await import('../../app-server/codex-app-server-capability-cache') -const { _internals, grantManagedCodexHookTrust } = await import('../grants/codex-hook-trust-grant') -const { markCodexProjectTrusted } = await import('../../../agent-launch/trust/agent-trust-presets') -const { getLocalCodexTrustConfigFiles } = await import('../../configuration/homes/codex-home-paths') -const { setCodexTrustGrantTelemetry } = await import('../grants/codex-trust-grant-telemetry') -const [ - { readHookTrustEntries, upsertHookTrustEntries }, - { - computeCodexTrustKey: computeTrustKey, - computeCodexTrustedHash: computeTrustedHash, - normalizeCodexHookTrustLookupKey: normalizeHookTrustKeyForLookup - } -] = await Promise.all([import('./config-toml-trust'), import('../identity/codex-trust-identity')]) - -let runtimeHomeDir: string - -beforeEach(() => { - testState.fakeHomeDir = mkdtempSync(join(tmpdir(), 'orca-concurrent-home-')) - testState.userDataDir = mkdtempSync(join(tmpdir(), 'orca-concurrent-userdata-')) - testState.previousUserDataPath = process.env.ORCA_USER_DATA_PATH - process.env.ORCA_USER_DATA_PATH = testState.userDataDir - runtimeHomeDir = join(testState.userDataDir, 'codex-runtime-home', 'home') - mkdirSync(runtimeHomeDir, { recursive: true }) - writeFileSync(join(runtimeHomeDir, 'hooks.json'), '{"hooks":{}}\n', 'utf-8') - mkdirSync(join(testState.fakeHomeDir, '.codex'), { recursive: true }) - codexAppServerCapabilityCache.clear() - _internals.resetDiagnostics() -}) - -afterEach(() => { - _internals.setGrantSessionRunner(null) - setCodexTrustGrantTelemetry(() => {}) - codexAppServerCapabilityCache.clear() - if (testState.previousUserDataPath === undefined) { - delete process.env.ORCA_USER_DATA_PATH - } else { - process.env.ORCA_USER_DATA_PATH = testState.previousUserDataPath - } - delete process.env.ORCA_DISABLE_CODEX_TRUST_RPC - rmSync(testState.fakeHomeDir, { recursive: true, force: true }) - rmSync(testState.userDataDir, { recursive: true, force: true }) -}) - -const MANAGED_COMMAND = "/bin/sh '/tmp/orca/codex-hook.sh'" - -function managedEntry(eventLabel: CodexTrustEntry['eventLabel']): CodexTrustEntry { - return { - sourcePath: join(runtimeHomeDir, 'hooks.json'), - eventLabel, - groupIndex: 0, - handlerIndex: 0, - command: MANAGED_COMMAND, - timeoutSec: 10 - } -} - -function buildPlan( - entries: CodexTrustEntry[], - overrides: Partial = {} -): CodexManagedTrustGrantPlan { - return { - runtimeHomePath: runtimeHomeDir, - tomlPath: join(runtimeHomeDir, 'config.toml'), - managedCommand: MANAGED_COMMAND, - managedEntries: entries, - host: { - kind: 'wsl', - distro: 'Ubuntu', - linuxRuntimeHome: overrides.runtimeHomePath ?? runtimeHomeDir - }, - ...overrides - } -} - -const tick = (): Promise => new Promise((resolve) => setTimeout(resolve, 0)) - -/** Write across an await to model concurrent app-server mutations. */ -function writingSessionRunner(args: { - tomlPath: string - entries: CodexTrustEntry[] - hashPrefix: string - gate?: Promise - outcome?: 'granted' | 'verify-failed' -}) { - return async ( - _request: CodexHookTrustGrantRequest - ): Promise => { - const granted = args.entries.map((entry) => { - const key = computeTrustKey(entry) - return { - key, - normalizedKey: normalizeHookTrustKeyForLookup(key), - trustedHash: `${args.hashPrefix}${entry.eventLabel}` - } - }) - await tick() - upsertHookTrustEntries( - args.tomlPath, - args.entries.map((entry, index) => ({ ...entry, trustedHash: granted[index].trustedHash })) - ) - if (args.gate) { - await args.gate - } - if (args.outcome === 'verify-failed') { - return { - outcome: 'verify-failed', - reason: 'listed hash mismatch', - reasonClass: 'post-grant-mismatch' - } - } - return { outcome: 'granted', wroteTrust: true, entries: granted } - } -} - -describe('two Codex pane launches against one config.toml', () => { - it("leaves a concurrent grant's records in place when a sibling grant fails", async () => { - // Warm the capability cache so both grant sessions can overlap. - codexAppServerCapabilityCache.rememberSupported('wsl:Ubuntu') - const tomlPath = join(runtimeHomeDir, 'config.toml') - const entries = [managedEntry('session_start')] - let call = 0 - let releaseFirst!: () => void - const firstGate = new Promise((resolve) => { - releaseFirst = resolve - }) - - _internals.setGrantSessionRunner(async (request) => { - call += 1 - const isFirst = call === 1 - return writingSessionRunner({ - tomlPath, - entries, - hashPrefix: isFirst ? 'sha256:doomed-' : 'sha256:survivor-', - gate: isFirst ? firstGate : undefined, - outcome: isFirst ? 'verify-failed' : 'granted' - })(request) - }) - - const doomed = grantManagedCodexHookTrust(buildPlan(entries)) - const survivor = grantManagedCodexHookTrust(buildPlan(entries)) - await tick() - await tick() - releaseFirst() - - expect(await doomed).toMatchObject({ lane: 'fallback', reason: 'verify-failed' }) - expect(await survivor).toMatchObject({ lane: 'rpc' }) - const trust = readHookTrustEntries(tomlPath) - const key = normalizeHookTrustKeyForLookup(computeTrustKey(entries[0])) - expect(trust.get(key)?.trustedHash).toBe('sha256:survivor-session_start') - }) - - it('writes project trust while a grant session is still running, and keeps it', async () => { - codexAppServerCapabilityCache.rememberSupported('wsl:Ubuntu') - const tomlPath = join(runtimeHomeDir, 'config.toml') - const entries = [managedEntry('session_start')] - const workspace = mkdtempSync(join(tmpdir(), 'orca-concurrent-ws-')) - let releaseSession!: () => void - const sessionGate = new Promise((resolve) => { - releaseSession = resolve - }) - - _internals.setGrantSessionRunner( - writingSessionRunner({ - tomlPath, - entries, - hashPrefix: 'sha256:doomed-', - gate: sessionGate, - outcome: 'verify-failed' - }) - ) - - try { - const grant = grantManagedCodexHookTrust(buildPlan(entries)) - await tick() - await tick() - await markCodexProjectTrusted(workspace, getLocalCodexTrustConfigFiles(testState.fakeHomeDir)) - expect(readFileSync(tomlPath, 'utf-8')).toContain('trust_level = "trusted"') - - releaseSession() - expect(await grant).toMatchObject({ lane: 'fallback', reason: 'verify-failed' }) - expect(readFileSync(tomlPath, 'utf-8')).toContain('trust_level = "trusted"') - } finally { - rmSync(workspace, { recursive: true, force: true }) - } - }) -}) - -describe('concurrent capability probes against a cold host', () => { - it('leaves the waiter config.toml untouched when the shared probe reports unsupported', async () => { - const secondHome = join(testState.userDataDir, 'second-runtime-home') - mkdirSync(secondHome, { recursive: true }) - writeFileSync(join(secondHome, 'hooks.json'), '{"hooks":{}}\n', 'utf-8') - const waiterToml = join(secondHome, 'config.toml') - const waiterEntry = { - ...managedEntry('session_start'), - sourcePath: join(secondHome, 'hooks.json') - } - upsertHookTrustEntries(waiterToml, [ - { ...waiterEntry, trustedHash: computeTrustedHash(waiterEntry) } - ]) - const before = readFileSync(waiterToml, 'utf-8') - - let releaseProbe!: () => void - const probeGate = new Promise((resolve) => { - releaseProbe = resolve - }) - _internals.setGrantSessionRunner(async () => { - await probeGate - throw new CodexAppServerUnsupportedError('hooks/grantTrust: method not found') - }) - - const first = grantManagedCodexHookTrust(buildPlan([managedEntry('session_start')])) - const waiter = grantManagedCodexHookTrust( - buildPlan([waiterEntry], { runtimeHomePath: secondHome, tomlPath: waiterToml }) - ) - await tick() - releaseProbe() - await first - expect(await waiter).toMatchObject({ lane: 'fallback', reason: 'unsupported-cached' }) - expect(readFileSync(waiterToml, 'utf-8')).toBe(before) - }) -}) - -describe('reentrancy under concurrency', () => { - it('completes a grant nested inside an installer that already holds both lanes', async () => { - const { runExclusivelyForCodexTrustConfig } = - await import('./codex-trust-config-mutation-queue') - const entries = [managedEntry('session_start')] - const tomlPath = join(runtimeHomeDir, 'config.toml') - const systemToml = join(testState.fakeHomeDir, '.codex', 'config.toml') - _internals.setGrantSessionRunner( - writingSessionRunner({ tomlPath, entries, hashPrefix: 'sha256:nested-' }) - ) - const workspace = mkdtempSync(join(tmpdir(), 'orca-nested-ws-')) - try { - const outcome = await runExclusivelyForCodexTrustConfig(tomlPath, () => - runExclusivelyForCodexTrustConfig(systemToml, async () => { - await markCodexProjectTrusted( - workspace, - getLocalCodexTrustConfigFiles(testState.fakeHomeDir) - ) - return grantManagedCodexHookTrust(buildPlan(entries)) - }) - ) - expect(outcome).toMatchObject({ lane: 'rpc' }) - expect(readFileSync(tomlPath, 'utf-8')).toContain('trust_level = "trusted"') - } finally { - rmSync(workspace, { recursive: true, force: true }) - } - }, 5000) -}) diff --git a/src/main/codex/trust/toml/config-toml-trust-loadability.test.ts b/src/main/codex/trust/toml/config-toml-trust-loadability.test.ts index 5560b81c8a1..6d65c82ff22 100644 --- a/src/main/codex/trust/toml/config-toml-trust-loadability.test.ts +++ b/src/main/codex/trust/toml/config-toml-trust-loadability.test.ts @@ -49,13 +49,12 @@ vi.mock('../../../agent-hooks/installer-utils-remote', async (importOriginal) => import { isCodexConfigTomlRefusedError, moveHookTrustEntries, - readHookTrustEntries, upsertHookTrustEntries, type CodexTrustEntry } from './config-toml-trust' import { computeCodexTrustKey as computeTrustKey } from '../identity/codex-trust-identity' import { escapeTomlBasicString as escapeTomlString } from '../../configuration/toml/config-toml-syntax' -import { installCodexHooksRemote } from '../../hooks/installation/codex-hook-remote-install' +import '../../hooks/installation/codex-hook-remote-install' let dir: string let tomlPath: string @@ -128,43 +127,4 @@ describe('hook approval writes never break a config.toml Codex can load', () => expect(readFileSync(tomlPath, 'utf-8')).toBe(original) }) - - it('the SSH installer reports the refusal and leaves the remote config.toml untouched', async () => { - const remoteToml = '/home/u/.codex/config.toml' - const original = 'model = "m"\nhooks = { state = {} }\n' - mocks.remoteFiles.set(remoteToml, original) - mocks.remoteFiles.set('/home/u/.codex/hooks.json', '{"hooks":{}}') - - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mocked remote helpers never touch the SFTP handle. - const status = await installCodexHooksRemote({} as SFTPWrapper, '/home/u') - - expect(status).toMatchObject({ - state: 'error', - detail: expect.stringContaining('defines hook approvals in a form Orca cannot add to') - }) - expect(mocks.remoteFiles.get(remoteToml)).toBe(original) - }) -}) - -describe('reading approvals Codex or the user wrote without a table header', () => { - it('reads an approval written as dotted keys', () => { - writeFileSync(tomlPath, `hooks.state."${tomlKey(stopEntry())}".trusted_hash = "sha256:user"\n`) - - expect(readHookTrustEntries(tomlPath).get(computeTrustKey(stopEntry()))).toEqual({ - trustedHash: 'sha256:user', - enabled: undefined - }) - }) - - it('reads an approval written as an inline table', () => { - writeFileSync( - tomlPath, - `[hooks]\nstate = { "${tomlKey(stopEntry())}" = { trusted_hash = "sha256:user", enabled = false } }\n` - ) - - expect(readHookTrustEntries(tomlPath).get(computeTrustKey(stopEntry()))).toEqual({ - trustedHash: 'sha256:user', - enabled: false - }) - }) }) diff --git a/src/main/gemini/hook-service.test.ts b/src/main/gemini/hook-service.test.ts deleted file mode 100644 index 1d17655c631..00000000000 --- a/src/main/gemini/hook-service.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' -import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import type * as osModule from 'node:os' - -const { getPathMock, homedirMock } = vi.hoisted(() => ({ - getPathMock: vi.fn<(name: string) => string>(), - homedirMock: vi.fn<() => string>() -})) - -vi.mock('electron', () => ({ - app: { - getPath: getPathMock - } -})) - -vi.mock('os', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - homedir: homedirMock - } -}) - -import { GeminiHookService } from './hook-service' -import { wrapWindowsBatchHookCommand } from '../agent-hooks/windows-direct-cmd-hook-command' - -describe('GeminiHookService', () => { - let homeDir: string - let userDataDir: string - - beforeAll(() => { - homeDir = mkdtempSync(join(tmpdir(), 'orca-gemini-home-')) - userDataDir = mkdtempSync(join(tmpdir(), 'orca-gemini-userdata-')) - homedirMock.mockReturnValue(homeDir) - getPathMock.mockImplementation((name: string) => { - if (name === 'userData') { - return userDataDir - } - throw new Error(`unexpected getPath(${name})`) - }) - }) - - afterAll(() => { - rmSync(homeDir, { recursive: true, force: true }) - rmSync(userDataDir, { recursive: true, force: true }) - }) - - it('preserves user-authored PreToolUse hooks while sweeping stale managed Gemini hooks', () => { - const managedHookFileName = process.platform === 'win32' ? 'gemini-hook.cmd' : 'gemini-hook.sh' - const staleManagedHookPath = - process.platform === 'win32' - ? `C:\\Users\\ramzi\\.orca\\agent-hooks\\${managedHookFileName}` - : `/Users/ramzi/.orca/agent-hooks/${managedHookFileName}` - const staleManagedCommand = - process.platform === 'win32' - ? staleManagedHookPath - : `if [ -x '${staleManagedHookPath}' ]; then /bin/sh '${staleManagedHookPath}'; fi` - const configDir = join(homeDir, '.gemini') - mkdirSync(configDir, { recursive: true }) - writeFileSync( - join(configDir, 'settings.json'), - JSON.stringify( - { - hooks: { - PreToolUse: [ - { - hooks: [{ type: 'command', command: staleManagedCommand }] - }, - { - hooks: [{ type: 'command', command: 'echo user-authored' }] - } - ] - } - }, - null, - 2 - ) - ) - - const status = new GeminiHookService().install() - const config = JSON.parse(readFileSync(join(configDir, 'settings.json'), 'utf8')) - const preToolCommands = config.hooks.PreToolUse.flatMap( - (definition: { hooks?: { command: string }[] }) => - (definition.hooks ?? []).map((hook) => hook.command) - ) - - expect(status.state).toBe('installed') - expect(preToolCommands).toEqual(['echo user-authored']) - expect(config.hooks.BeforeTool[0].hooks[0].command).toMatch( - process.platform === 'win32' - ? wrapWindowsBatchHookCommand(join(homeDir, '.orca', 'agent-hooks', managedHookFileName)) - : new RegExp(managedHookFileName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) - ) - }) -}) diff --git a/src/main/ipc/ai-vault/ai-vault.test.ts b/src/main/ipc/ai-vault/ai-vault.test.ts deleted file mode 100644 index 0906702fd15..00000000000 --- a/src/main/ipc/ai-vault/ai-vault.test.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { homedir } from 'node:os' -import { join, sep } from 'node:path' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { - AiVaultListResult, - AiVaultSession -} from '../../../shared/ai-vault/contracts/ai-vault-types' -import type * as CachedSessionListModule from '../../ai-vault/cached-session-list' -import type * as SessionParseCacheModule from '../../ai-vault/parse-cache/session-parse-cache-store' - -const mocks = vi.hoisted(() => ({ - scanLocalSessions: vi.fn(), - resolveAiVaultSessionTitlesInService: vi.fn(), - listSubagentsInService: vi.fn(), - scanRuntimeAiVaultSessions: vi.fn(), - getAiVaultWslHomeDirs: vi.fn(), - ipcHandle: vi.fn(), - trashItem: vi.fn(), - deleteAiVaultSessionFile: vi.fn(), - invalidateAiVaultSessionListCache: vi.fn(), - invalidateSessionParseCacheEntry: vi.fn() -})) - -vi.mock('electron', () => ({ - app: { on: vi.fn() }, - ipcMain: { handle: mocks.ipcHandle }, - shell: { trashItem: mocks.trashItem } -})) - -vi.mock('../../ai-vault/scan-service/session-scanner-service-spawn', async (importOriginal) => ({ - ...(await importOriginal()), - scanAiVaultSessionsInService: mocks.scanLocalSessions, - resolveAiVaultSessionTitlesInService: mocks.resolveAiVaultSessionTitlesInService, - listAiVaultSubagentSessionsInService: mocks.listSubagentsInService -})) - -vi.mock('../../ai-vault/session-delete', () => ({ - deleteAiVaultSessionFile: mocks.deleteAiVaultSessionFile -})) - -// Mock only invalidation so routing and cache tests retain the real list implementation. -vi.mock('../../ai-vault/cached-session-list', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - invalidateAiVaultSessionListCache: mocks.invalidateAiVaultSessionListCache - } -}) - -vi.mock('../../ai-vault/parse-cache/session-parse-cache-store', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - invalidateSessionParseCacheEntry: mocks.invalidateSessionParseCacheEntry - } -}) - -vi.mock('../../wsl/discovery/distro-catalog', () => ({ - listRunningWslDistrosAsync: vi.fn().mockResolvedValue([]), - listRunningWslHomeDirsAsync: vi.fn().mockResolvedValue([]), - hasCachedWslDistros: vi.fn(() => false) -})) - -const { resolveOmpSessionsDir } = await import('../../ai-vault/scanners/omp/omp-session-root') -const { _internals, registerAiVaultHandlers } = await import('./ai-vault') -const { deleteAiVaultSession: deleteAiVaultSessionWithDeps } = await import('./ai-vault-delete') -await import('../../ai-vault/host-session-delete') - -beforeEach(() => { - vi.clearAllMocks() - _internals.resetAiVaultCacheForTests() - mocks.scanLocalSessions.mockResolvedValue(result([session('local', 'local-session')])) - mocks.resolveAiVaultSessionTitlesInService.mockResolvedValue({ titles: [] }) - mocks.listSubagentsInService.mockResolvedValue({ sessions: [], issues: [] }) - mocks.scanRuntimeAiVaultSessions.mockResolvedValue( - result([session('runtime:remote-server', 'runtime-session')]) - ) -}) - -describe('prepareSessionResume IPC', () => { - it('preserves SSH session homes without reading their paths locally', async () => { - const prepareSessionResume = vi.fn() - const prepareRuntimeSessionResume = vi.fn() - registerAiVaultHandlers({ prepareSessionResume, prepareRuntimeSessionResume }) - - await expect( - getPrepareSessionResumeHandler()( - {}, - { - agent: 'codex', - filePath: '/managed/sessions/2026/07/20/rollout-a.jsonl', - codexHome: '/managed', - executionHostId: 'ssh:dev-box' - } - ) - ).resolves.toEqual({ useRealCodexHome: false }) - expect(prepareSessionResume).not.toHaveBeenCalled() - expect(prepareRuntimeSessionResume).not.toHaveBeenCalled() - }) -}) - -function getPrepareSessionResumeHandler(): ( - event: unknown, - args: unknown -) => Promise<{ useRealCodexHome: boolean }> { - const registration = mocks.ipcHandle.mock.calls.find( - ([channel]) => channel === 'aiVault:prepareSessionResume' - ) - if (!registration) { - throw new Error('aiVault:prepareSessionResume was not registered') - } - return registration[1] -} - -describe('listAiVaultSubagentSessions gating', () => { - const claudeRoot = join(homedir(), '.claude', 'projects') - - it('rejects a dot-segment traversal out of the Claude projects root', async () => { - // Built with sep (not join) so the `..` segments survive into the arg. - const traversal = [claudeRoot, '..', '..', '..', 'etc', 'passwd.jsonl'].join(sep) - - const result = await _internals.listAiVaultSubagentSessions({ - agent: 'claude', - parentFilePath: traversal, - executionHostId: 'local' - }) - - expect(result).toEqual({ sessions: [], issues: [] }) - expect(mocks.listSubagentsInService).not.toHaveBeenCalled() - }) - - it('rejects an OMP path that only sits inside another agent root', async () => { - // Each agent's allowlist is its own root: a Claude path must not be - // readable through the OMP branch (or vice versa). - const crossAgent = await _internals.listAiVaultSubagentSessions({ - agent: 'omp', - parentFilePath: join(claudeRoot, 'proj', 'sess.jsonl'), - executionHostId: 'local' - }) - const traversal = await _internals.listAiVaultSubagentSessions({ - agent: 'omp', - // Built with sep (not join) so the `..` segments survive into the arg. - parentFilePath: [resolveOmpSessionsDir(), '..', '..', '..', 'etc', 'passwd.jsonl'].join(sep), - executionHostId: 'local' - }) - - expect(crossAgent).toEqual({ sessions: [], issues: [] }) - expect(traversal).toEqual({ sessions: [], issues: [] }) - expect(mocks.listSubagentsInService).not.toHaveBeenCalled() - }) -}) - -describe('deleteAiVaultSession', () => { - const args = { - agent: 'gemini' as const, - sessionId: 'session-1', - filePath: '/home/ada/.gemini/tmp/sess.json', - executionHostId: 'local' as const - } - - it("routes a server-hosted session to its owning host and drops that host's cached list", async () => { - const deleteRuntimeAiVaultSession = vi.fn().mockResolvedValue({ outcome: 'deleted' }) - const invalidateMultiHostListCache = vi.fn() - - const result = await deleteAiVaultSessionWithDeps( - { ...args, executionHostId: 'runtime:gpu%20box' }, - { invalidateMultiHostListCache, deleteRuntimeSession: deleteRuntimeAiVaultSession } - ) - - expect(result).toEqual({ outcome: 'deleted' }) - expect(deleteRuntimeAiVaultSession).toHaveBeenCalledWith( - 'gpu box', - expect.objectContaining({ agent: 'gemini', filePath: args.filePath }) - ) - expect(invalidateMultiHostListCache).toHaveBeenCalledTimes(1) - // This computer never touches a path that belongs to another host. - expect(mocks.deleteAiVaultSessionFile).not.toHaveBeenCalled() - }) - - it('never routes the unresolved-owner sentinel to a host', async () => { - const deleteRuntimeAiVaultSession = vi.fn() - mocks.deleteAiVaultSessionFile.mockResolvedValue({ - outcome: 'rejected', - agent: 'gemini', - reason: 'non-local-host' - }) - - await deleteAiVaultSessionWithDeps( - { ...args, executionHostId: 'runtime:unresolved-owner' }, - { - invalidateMultiHostListCache: vi.fn(), - deleteRuntimeSession: deleteRuntimeAiVaultSession - } - ) - - expect(deleteRuntimeAiVaultSession).not.toHaveBeenCalled() - }) -}) - -function result(sessions: AiVaultSession[]): AiVaultListResult { - return { sessions, issues: [], scannedAt: new Date().toISOString() } -} - -function session( - executionHostId: AiVaultSession['executionHostId'], - sessionId: string -): AiVaultSession { - return { - id: `${executionHostId}:codex:${sessionId}:/tmp/${sessionId}.jsonl`, - executionHostId, - agent: 'codex', - sessionId, - title: sessionId, - cwd: '/repo', - branch: null, - model: null, - filePath: `/tmp/${sessionId}.jsonl`, - codexHome: null, - createdAt: null, - updatedAt: - sessionId === 'runtime-session' - ? '2026-07-04T03:00:00.000Z' - : sessionId === 'remote-session' - ? '2026-07-04T02:00:00.000Z' - : '2026-07-04T01:00:00.000Z', - modifiedAt: '2026-07-04T00:00:00.000Z', - messageCount: 1, - totalTokens: 0, - previewMessages: [], - queuedMessageCount: 0, - subagentTranscriptCount: 0, - resumeCommand: `codex resume ${sessionId}`, - subagent: null - } -} diff --git a/src/main/ipc/browser/browser-preview-tool-authorization.test.ts b/src/main/ipc/browser/browser-preview-tool-authorization.test.ts deleted file mode 100644 index 50246df0cd7..00000000000 --- a/src/main/ipc/browser/browser-preview-tool-authorization.test.ts +++ /dev/null @@ -1,371 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type * as DocPreviewGuestPolicyModule from '../../browser/document-preview/doc-preview-guest-policy' -import type * as TabRegistrationWaitModule from './browser-tab-registration-wait' - -const { - handleMock, - removeHandlerMock, - getAuthorizedGuestMock, - setGrabModeMock, - awaitGrabSelectionMock, - cancelGrabOpMock, - captureSelectionScreenshotMock, - extractHoverPayloadMock, - setAnnotationViewportBridgeMock, - openDevToolsMock, - setViewportOverrideMock, - previewAuthoritySpy, - registrationWaitSpy -} = vi.hoisted(() => ({ - handleMock: vi.fn(), - removeHandlerMock: vi.fn(), - getAuthorizedGuestMock: vi.fn(), - setGrabModeMock: vi.fn().mockResolvedValue(true), - awaitGrabSelectionMock: vi.fn().mockResolvedValue({ opId: 'op', kind: 'cancelled' }), - cancelGrabOpMock: vi.fn(), - captureSelectionScreenshotMock: vi.fn().mockResolvedValue(null), - extractHoverPayloadMock: vi.fn().mockResolvedValue(null), - setAnnotationViewportBridgeMock: vi.fn().mockResolvedValue(true), - openDevToolsMock: vi.fn().mockResolvedValue(true), - setViewportOverrideMock: vi.fn().mockResolvedValue(true), - previewAuthoritySpy: vi.fn(), - registrationWaitSpy: vi.fn() -})) - -vi.mock('./browser-passwords-ipc', () => ({ registerBrowserPasswordsHandlers: vi.fn() })) -vi.mock('electron', () => ({ - BrowserWindow: { fromWebContents: vi.fn() }, - ipcMain: { removeHandler: removeHandlerMock, handle: handleMock }, - webContents: { fromId: vi.fn(() => ({ isDestroyed: () => false })) } -})) - -vi.mock('../../browser/browser-manager', () => ({ - browserCertificateTrustController: { proceed: vi.fn(() => ({ ok: true })) }, - browserManager: { - registerGuest: vi.fn(() => true), - attachGuestPolicies: vi.fn(), - unregisterGuest: vi.fn(), - getGuestWebContentsId: vi.fn(), - getWebContentsIdByTabId: vi.fn(() => new Map()), - getWorktreeIdForTab: vi.fn(), - getAuthorizedGuest: getAuthorizedGuestMock, - setGrabMode: setGrabModeMock, - awaitGrabSelection: awaitGrabSelectionMock, - cancelGrabOp: cancelGrabOpMock, - captureSelectionScreenshot: captureSelectionScreenshotMock, - extractHoverPayload: extractHoverPayloadMock, - setAnnotationViewportBridge: setAnnotationViewportBridgeMock, - openDevTools: openDevToolsMock, - setViewportOverride: setViewportOverrideMock, - cancelDownload: vi.fn() - } -})) - -// Spy on the real policy so registry selection remains observable. -vi.mock('../../browser/document-preview/doc-preview-guest-policy', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - getWorkspaceDocPageGuest: (browserPageId: string, senderWebContentsId: number) => { - previewAuthoritySpy(browserPageId, senderWebContentsId) - return actual.getWorkspaceDocPageGuest(browserPageId, senderWebContentsId) - } - } -}) - -// Keep registration waits real; observe only which targets enter them. -vi.mock('./browser-tab-registration-wait', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - waitForNextTabRegistration: (...args: Parameters) => { - registrationWaitSpy(args[0]) - return actual.waitForNextTabRegistration(...args) - } - } -}) - -import { registerBrowserHandlers } from './browser' -import { setTrustedBrowserRendererWebContentsId } from './browser-renderer-trust' -import { browserManager } from '../../browser/browser-manager' -import { - getWorkspaceDocPageGuest, - installDocPreviewGuestPolicy -} from '../../browser/document-preview/doc-preview-guest-policy' -import { - mintDocPreviewGrant, - revokeAllDocPreviewGrants -} from '../../browser/document-preview/doc-preview-grant-registry' -import { buildDocPreviewUrl } from '../../../shared/doc-preview-scheme' - -const HOST_RENDERER_ID = 91 -const OTHER_RENDERER_ID = 92 -// Match browser-grab-ipc's registration timeout. -const GRAB_REGISTRATION_WAIT_MS = 1_000 - -/** Exhaustively classify invoke channels as guest tools or browser/session/profile management. */ -const TOOL_CHANNELS = [ - 'browser:setGrabMode', - 'browser:awaitGrabSelection', - 'browser:cancelGrab', - 'browser:captureSelectionScreenshot', - 'browser:extractHoverPayload', - 'browser:setAnnotationViewportBridge' -] - -type Handler = (event: { sender: Electron.WebContents }, args: unknown) => unknown - -function registeredHandlers(): Map { - const handlers = new Map() - for (const [channel, handler] of handleMock.mock.calls as [string, Handler][]) { - handlers.set(channel, handler) - } - return handlers -} - -function trustedSender(id: number): { sender: Electron.WebContents } { - return { - sender: { - id, - isDestroyed: () => false, - getType: () => 'window', - getURL: () => 'file:///renderer/index.html' - } as unknown as Electron.WebContents - } -} - -let nextDocPageOrdinal = 0 - -function grantForNewDocPage(): { id: string; browserPageId: string } { - nextDocPageOrdinal += 1 - const browserPageId = `doc-page-${nextDocPageOrdinal}` - const grant = mintDocPreviewGrant({ - owner: { kind: 'ssh', connectionId: 'ssh-1' }, - root: '/home/alice/docs', - entryRelativePath: 'index.html', - browserPageId - }) - return { id: grant.id, browserPageId } -} - -type PreviewGuestContents = { - isDestroyed: () => boolean - getURL: () => string -} - -function renderPreviewForGrant( - grant: { id: string; browserPageId: string }, - hostId: number = HOST_RENDERER_ID -): { - grantId: string - browserPageId: string - contents: PreviewGuestContents - markContentsDestroyed: () => void -} { - const browserPageId = grant.browserPageId - const handlers: Record void> = {} - const register = (event: string, handler: (...args: never[]) => void): void => { - handlers[event] = handler - } - // Policy attaches mid-load, when the guest already reports its document URL. - const documentUrl = buildDocPreviewUrl(grant.id, 'index.html') - let contentsDestroyed = false - const guest = { - isFocused: () => true, - isDestroyed: () => contentsDestroyed, - getURL: () => documentUrl, - on: vi.fn(register), - once: vi.fn(register), - setWindowOpenHandler: vi.fn(), - setWebRTCIPHandlingPolicy: vi.fn() - } - installDocPreviewGuestPolicy(guest as never, { id: hostId, send: vi.fn() }) - handlers['did-start-navigation']?.({ url: documentUrl, isMainFrame: true } as never) - return { - grantId: grant.id, - browserPageId, - contents: guest, - // Contents can be destroyed before Chromium delivers the destroyed event. - markContentsDestroyed: () => { - contentsDestroyed = true - } - } -} - -function liveRenderedPreview( - hostId: number = HOST_RENDERER_ID -): ReturnType { - return renderPreviewForGrant(grantForNewDocPage(), hostId) -} - -/** Minimal well-formed args per channel, so a refusal is authorization and not shape validation. */ -function toolArgs(channel: string, browserPageId: string): Record { - switch (channel) { - case 'browser:setGrabMode': - return { browserPageId, enabled: true } - case 'browser:awaitGrabSelection': - return { browserPageId, opId: 'op-1' } - case 'browser:captureSelectionScreenshot': - return { browserPageId, rect: { x: 0, y: 0, width: 10, height: 10 } } - case 'browser:setAnnotationViewportBridge': - return { - browserPageId, - enabled: true, - emitViewport: true, - markers: [], - token: 'annotation-bridge-token-1' - } - default: - return { browserPageId } - } -} - -// Unwrap viewport resolvers as well as direct guest arguments. -function resolvesToGuest(argument: unknown, guest: PreviewGuestContents): boolean { - return argument === guest || (typeof argument === 'function' && argument() === guest) -} - -const GUEST_RECEIVING_MOCKS = [ - setGrabModeMock, - awaitGrabSelectionMock, - captureSelectionScreenshotMock, - extractHoverPayloadMock, - setAnnotationViewportBridgeMock -] - -beforeEach(() => { - vi.stubEnv('ELECTRON_RENDERER_URL', '') - setTrustedBrowserRendererWebContentsId(HOST_RENDERER_ID) - // Revoke before clearing mocks so disposal calls belong to the previous test. - revokeAllDocPreviewGrants() - vi.clearAllMocks() - // This suite checks channel authorization; real manager registry splitting is covered separately. - getAuthorizedGuestMock.mockImplementation((browserPageId: string, senderWebContentsId: number) => - getWorkspaceDocPageGuest(browserPageId, senderWebContentsId) - ) - setGrabModeMock.mockResolvedValue(true) - awaitGrabSelectionMock.mockResolvedValue({ opId: 'op-1', kind: 'cancelled' }) - captureSelectionScreenshotMock.mockResolvedValue(null) - extractHoverPayloadMock.mockResolvedValue(null) - setAnnotationViewportBridgeMock.mockResolvedValue(true) - registerBrowserHandlers({ getSettings: vi.fn() }) - // Fake timers settle registration waits for guests that never attach. - vi.useFakeTimers() -}) - -afterEach(() => { - vi.useRealTimers() -}) - -async function settle(pending: Promise | T | undefined): Promise { - await vi.advanceTimersByTimeAsync(GRAB_REGISTRATION_WAIT_MS) - return pending -} - -describe('doc preview tool authorization', () => { - it.each(TOOL_CHANNELS)('drives the preview guest from %s', async (channel) => { - const preview = liveRenderedPreview() - const handler = registeredHandlers().get(channel) - - await handler?.(trustedSender(HOST_RENDERER_ID), toolArgs(channel, preview.browserPageId)) - - // Assert the actual WebContents receiving the tool operation. - const receivedGuest = GUEST_RECEIVING_MOCKS.some((mock) => - mock.mock.calls.some((args) => args.some((arg) => resolvesToGuest(arg, preview.contents))) - ) - expect(receivedGuest || cancelGrabOpMock.mock.calls.length > 0).toBe(true) - }) - - // Browser/session management must never consult the document registry. - - it.each(TOOL_CHANNELS)( - 'refuses %s from a renderer that does not host the preview', - async (channel) => { - const preview = liveRenderedPreview() - const handler = registeredHandlers().get(channel) - - await settle( - handler?.(trustedSender(OTHER_RENDERER_ID), toolArgs(channel, preview.browserPageId)) - ) - - for (const mock of [...GUEST_RECEIVING_MOCKS, cancelGrabOpMock]) { - expect(mock).not.toHaveBeenCalled() - } - } - ) - - // A live grant and trusted sender are insufficient once contents are destroyed. - it.each(TOOL_CHANNELS)('refuses %s once the preview contents are destroyed', async (channel) => { - const preview = liveRenderedPreview() - preview.markContentsDestroyed() - const handler = registeredHandlers().get(channel) - - await settle( - handler?.(trustedSender(HOST_RENDERER_ID), toolArgs(channel, preview.browserPageId)) - ) - - for (const mock of [...GUEST_RECEIVING_MOCKS, cancelGrabOpMock]) { - expect(mock).not.toHaveBeenCalled() - } - }) - - // Document tools wait for their own attaching guest instead of returning not-ready immediately. - it('waits for a document page whose guest has not attached yet, and arms when it does', async () => { - const grant = grantForNewDocPage() - const handler = registeredHandlers().get('browser:setGrabMode') - - const pending = handler?.(trustedSender(HOST_RENDERER_ID), { - browserPageId: grant.browserPageId, - enabled: true - }) - await vi.advanceTimersByTimeAsync(0) - expect(registrationWaitSpy).toHaveBeenCalledWith(grant.browserPageId) - - const preview = renderPreviewForGrant(grant) - - await expect(pending).resolves.toEqual({ ok: true }) - expect(setGrabModeMock).toHaveBeenCalledWith(grant.browserPageId, true, preview.contents) - }) - - // Unrendered documents must eventually answer not-ready. - - // Revocation, including re-minting, is how previews withdraw and release retained tool state. - - // unregisterGuest must not drop preview intent and silently settle its grab without arming. - it('leaves an in-flight preview grab armed when unregisterGuest names its target', async () => { - const preview = liveRenderedPreview() - const handlers = registeredHandlers() - const pending = handlers.get('browser:setGrabMode')?.(trustedSender(HOST_RENDERER_ID), { - browserPageId: preview.browserPageId, - enabled: true - }) - - // Intent exists before the queued operation runs; test disposal in that gap. - const unregistered = handlers.get('browser:unregisterGuest')?.( - trustedSender(HOST_RENDERER_ID), - { browserPageId: preview.browserPageId } - ) - - // A dropped intent also returns ok; only the driven guest proves the grab armed. - await expect(pending).resolves.toEqual({ ok: true }) - expect(setGrabModeMock).toHaveBeenCalledWith(preview.browserPageId, true, preview.contents) - expect(unregistered).toBe(false) - expect(vi.mocked(browserManager.unregisterGuest)).not.toHaveBeenCalled() - }) - - // The same guard must still allow browser tabs to dispose their grabs. - - // An open document must not answer for a browsing page ID. - it('never hands a browsing page id the guest of a document that is open', async () => { - const preview = liveRenderedPreview() - const handler = registeredHandlers().get('browser:extractHoverPayload') - - await handler?.(trustedSender(HOST_RENDERER_ID), { browserPageId: 'browser-page-1' }) - - expect(getAuthorizedGuestMock).toHaveBeenCalledWith('browser-page-1', HOST_RENDERER_ID) - expect(previewAuthoritySpy).toHaveBeenCalledWith('browser-page-1', HOST_RENDERER_ID) - expect(extractHoverPayloadMock).not.toHaveBeenCalled() - await handler?.(trustedSender(HOST_RENDERER_ID), { browserPageId: preview.browserPageId }) - expect(extractHoverPayloadMock).toHaveBeenCalledWith(preview.browserPageId, preview.contents) - }) -}) diff --git a/src/main/ipc/browser/browser-tab-registration-wait.test.ts b/src/main/ipc/browser/browser-tab-registration-wait.test.ts deleted file mode 100644 index e434c6c0b28..00000000000 --- a/src/main/ipc/browser/browser-tab-registration-wait.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -const mocks = vi.hoisted(() => ({ - getWebContentsIdByTabId: vi.fn(() => new Map()), - getWorktreeIdForTab: vi.fn(() => undefined as string | undefined), - getGuestWebContentsId: vi.fn(() => null as number | null), - webContentsFromId: vi.fn(() => null as unknown) -})) - -vi.mock('electron', () => ({ webContents: { fromId: mocks.webContentsFromId } })) -vi.mock('../../browser/browser-manager', () => ({ - browserManager: { - getWebContentsIdByTabId: mocks.getWebContentsIdByTabId, - getWorktreeIdForTab: mocks.getWorktreeIdForTab, - getGuestWebContentsId: mocks.getGuestWebContentsId - } -})) - -import { - waitForAnyTabRegistration, - waitForNextTabRegistration, - waitForWorktreeTabRegistration -} from './browser-tab-registration-wait' -import { installDocPreviewGuestPolicy } from '../../browser/document-preview/doc-preview-guest-policy' -import { - mintDocPreviewGrant, - revokeAllDocPreviewGrants -} from '../../browser/document-preview/doc-preview-grant-registry' -import { buildDocPreviewUrl } from '../../../shared/doc-preview-scheme' - -const WAIT_MS = 1_000 - -function attachDocumentGuest(browserPageId: string): void { - const grant = mintDocPreviewGrant({ - owner: { kind: 'ssh', connectionId: 'ssh-1' }, - root: '/home/alice/docs', - entryRelativePath: 'index.html', - browserPageId - }) - const guest = { - isFocused: () => false, - isDestroyed: () => false, - getURL: () => buildDocPreviewUrl(grant.id, grant.entryRelativePath), - on: vi.fn(), - once: vi.fn(), - setWindowOpenHandler: vi.fn(), - setWebRTCIPHandlingPolicy: vi.fn() - } - installDocPreviewGuestPolicy(guest as never, { id: 91, send: vi.fn() }) -} - -function track(pending: Promise): () => 'pending' | 'resolved' | 'rejected' { - let state: 'pending' | 'resolved' | 'rejected' = 'pending' - pending.then( - () => { - state = 'resolved' - }, - () => { - state = 'rejected' - } - ) - return () => state -} - -beforeEach(() => { - revokeAllDocPreviewGrants() - vi.clearAllMocks() - mocks.getWebContentsIdByTabId.mockReturnValue(new Map()) - mocks.getGuestWebContentsId.mockReturnValue(null) - vi.useFakeTimers() -}) - -afterEach(() => { - vi.useRealTimers() -}) - -describe('a workspace document registering', () => { - it('settles a wait that already names its page', async () => { - const settled = track(waitForNextTabRegistration('doc-page-1', WAIT_MS)) - - attachDocumentGuest('doc-page-1') - await vi.advanceTimersByTimeAsync(0) - - expect(settled()).toBe('resolved') - }) - - // CLI/agent any-tab waits need drivable browser tabs; documents satisfy only their own page wait. - it('leaves the waits that ask for any browser tab still waiting', async () => { - const worktreeWait = track(waitForWorktreeTabRegistration('wt-1', WAIT_MS)) - const anyWait = track(waitForAnyTabRegistration(WAIT_MS)) - - attachDocumentGuest('doc-page-2') - await vi.advanceTimersByTimeAsync(0) - - expect(worktreeWait()).toBe('pending') - expect(anyWait()).toBe('pending') - - // Verify unsatisfied waits actually time out. - await vi.advanceTimersByTimeAsync(WAIT_MS) - expect(worktreeWait()).toBe('rejected') - expect(anyWait()).toBe('rejected') - }) -}) diff --git a/src/main/ipc/doc-preview-grant-ipc.test.ts b/src/main/ipc/doc-preview-grant-ipc.test.ts deleted file mode 100644 index 76c9627accb..00000000000 --- a/src/main/ipc/doc-preview-grant-ipc.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' - -const mocks = vi.hoisted(() => ({ - handlers: new Map unknown>(), - listeners: new Map unknown>(), - isTrustedBrowserRenderer: vi.fn(), - reportDocPreviewLinkClick: vi.fn(), - getGuestWebContentsId: vi.fn() -})) - -vi.mock('electron', () => ({ - ipcMain: { - handle: (channel: string, handler: (event: unknown, ...args: unknown[]) => unknown) => { - mocks.handlers.set(channel, handler) - }, - on: (channel: string, listener: (event: unknown, ...args: unknown[]) => unknown) => { - mocks.listeners.set(channel, listener) - } - } -})) -vi.mock('./browser/browser-renderer-trust', () => ({ - isTrustedBrowserRenderer: mocks.isTrustedBrowserRenderer -})) -vi.mock('../browser/document-preview/doc-preview-guest-policy', () => ({ - reportDocPreviewLinkClick: mocks.reportDocPreviewLinkClick -})) -vi.mock('../browser/browser-manager', () => ({ - browserManager: { getGuestWebContentsId: mocks.getGuestWebContentsId } -})) - -import { - registerDocPreviewGrantHandlers, - type DocPreviewGrantRequest -} from './doc-preview-grant-ipc' -import { - getDocPreviewGrant, - revokeAllDocPreviewGrants -} from '../browser/document-preview/doc-preview-grant-registry' -import { - DOC_PREVIEW_AUTHORIZE_DIRECTORY_CHANNEL, - DOC_PREVIEW_MINT_GRANT_CHANNEL, - DOC_PREVIEW_REVOKE_GRANT_CHANNEL -} from '../../shared/doc-preview-scheme' - -const REQUEST: DocPreviewGrantRequest = { - owner: { kind: 'ssh', connectionId: 'ssh-1' }, - requestBase: '/home/alice/docs', - root: '/home/alice/docs', - entryRelativePath: 'index.html', - browserPageId: 'doc-page-1' -} - -const sender = { id: 7 } - -function mint(request: DocPreviewGrantRequest = REQUEST): { grantId: string; url: string } { - const handler = mocks.handlers.get(DOC_PREVIEW_MINT_GRANT_CHANNEL) - if (!handler) { - throw new Error('mint handler not registered') - } - return handler({ sender }, request) as { grantId: string; url: string } -} - -function revoke(grantId: string): boolean { - const handler = mocks.handlers.get(DOC_PREVIEW_REVOKE_GRANT_CHANNEL) - if (!handler) { - throw new Error('revoke handler not registered') - } - return handler({ sender }, grantId) as boolean -} - -function authorize(grantId: unknown, relativePath: unknown): boolean { - const handler = mocks.handlers.get(DOC_PREVIEW_AUTHORIZE_DIRECTORY_CHANNEL) - if (!handler) { - throw new Error('authorize handler not registered') - } - return handler({ sender }, grantId, relativePath) as boolean -} - -beforeEach(() => { - mocks.handlers.clear() - mocks.listeners.clear() - vi.clearAllMocks() - revokeAllDocPreviewGrants() - mocks.isTrustedBrowserRenderer.mockReturnValue(true) - mocks.getGuestWebContentsId.mockReturnValue(null) - registerDocPreviewGrantHandlers() -}) - -describe('document preview grant handlers', () => { - // Keep browser/document registries disjoint when minting the document identity. - it('refuses to make a page that already hosts a browsing guest into a document page', () => { - mocks.getGuestWebContentsId.mockReturnValue(42) - - expect(() => mint()).toThrow(/browsing page/) - expect(mocks.getGuestWebContentsId).toHaveBeenCalledWith('doc-page-1') - }) - - // Untrusted senders must receive no filesystem-read grant. - it('mints nothing for a sender that is not the trusted renderer', () => { - mocks.isTrustedBrowserRenderer.mockReturnValue(false) - - expect(() => mint()).toThrow(/Untrusted/) - }) - - it('refuses to revoke on behalf of a sender that is not the trusted renderer', () => { - const result = mint() - mocks.isTrustedBrowserRenderer.mockReturnValue(false) - - expect(revoke(result.grantId)).toBe(false) - expect(getDocPreviewGrant(result.grantId)).not.toBeNull() - }) - - it('lets only the trusted renderer authorize a valid requested directory', () => { - const result = mint({ - ...REQUEST, - requestBase: '/home/alice', - root: '/home/alice/docs', - entryRelativePath: 'docs/index.html' - }) - - expect(authorize(result.grantId, 'assets/app.js')).toBe(true) - expect(getDocPreviewGrant(result.grantId)?.authorizedRoots).toEqual(['/home/alice/assets']) - - mocks.isTrustedBrowserRenderer.mockReturnValue(false) - expect(authorize(result.grantId, 'secrets/token.txt')).toBe(false) - expect(getDocPreviewGrant(result.grantId)?.authorizedRoots).toEqual(['/home/alice/assets']) - }) - - it('refuses malformed directory authorization arguments', () => { - const result = mint() - - expect(authorize(result.grantId, '../secret.txt')).toBe(false) - expect(authorize(result.grantId, null)).toBe(false) - expect(authorize(null, 'assets/app.js')).toBe(false) - expect(authorize('0'.repeat(32), 'assets/app.js')).toBe(false) - }) -}) diff --git a/src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-start-lifecycle-perf.test.ts b/src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-start-lifecycle-perf.test.ts deleted file mode 100644 index 5acb7a7b3e7..00000000000 --- a/src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-start-lifecycle-perf.test.ts +++ /dev/null @@ -1,453 +0,0 @@ -import { getEventListeners } from 'node:events' -import { mkdtemp, rm } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { PassThrough } from 'node:stream' -import { afterEach, beforeEach, expect, it, vi } from 'vitest' -import { createAcpAgentConnection } from '../../../../acp/acp-agent-connection' -import { openAcpAdapterRig } from '../../../../acp/acp-structured-adapter.test-support' -import { readAntigravitySignInLine } from '../../../../antigravity/antigravity-sign-in-url' -import { findInErrorChain } from '../../../../provider-process/provider-error-chain' -import { createFakePipedChild } from '../../../../../shared/__fixtures__/fake-spawned-child' -import { closeProviderTimelineRigs } from '../../../agent-session-timeline/provider-timeline-assembler-test-support' -import { computeAgentSessionPayloadFingerprint } from '../../../../../shared/agent-session/lifecycle/agent-session-mutation-envelope' -import { AgentSessionStartFailure } from '../../startup/structured-agent-session-start-failure' -import { CodexAppServerExitError } from '../../../../codex/app-server/codex-app-server-exit-error' -import { CodexAppServerRequestError } from '../../../../codex/app-server/codex-app-server-request-error' -import { - adapterFor, - fakeCodex, - THREAD_ID -} from '../../../../codex/structured/session-lifecycle/codex-structured-session-adapter-fixture' -import { openTestAgentSessionRecordStore } from '../../../../runtime/agent-session/records/agent-session-record-store-test-harness' -import { - closeTestJournalHostDatabases, - openTestJournalHostDatabase -} from '../../../agent-session-journal/store-lifecycle/journal-host-database-test-support' -import { - claudeAndCodexAgents, - claudeAndCodexRouter -} from '../../providers/testing/structured-agent-session-adapter-router-test-support' -import type * as ExitObservation from '../../termination/structured-agent-session-exit-observation' -import { StructuredAgentSessionHost } from '../structured-agent-session-host' -import { - HOST_TEST_NOW as NOW, - HOST_TEST_SESSION as SESSION, - hostTestAttachParams, - hostTestOperationId, - resetHostTestOperationIds -} from './structured-agent-session-host-test-data' -import { createStructuredAgentSessionLogger } from '../structured-agent-session-logger' -import { restTestSend } from './structured-agent-session-rest-test-rig' -import type * as StartFailure from '../../startup/structured-agent-session-start-failure' - -// Every adapter's startup receipts and every acquisition's root-exit receipt, to count what they -// still hold after each start, whichever adapter or sink created them. -const created = vi.hoisted(() => ({ - endings: new Array(), - rootExits: new Array() -})) -const retainedReceipts = () => - created.endings.reduce((sum, endings) => sum + endings['endings'].size, 0) -vi.mock('../../startup/structured-agent-session-start-failure', async (original) => { - const actual = await original() - class CountedEndings extends actual.AgentSessionStartEndings { - constructor() { - super() - created.endings.push(this) - } - } - return { ...actual, AgentSessionStartEndings: CountedEndings } -}) -vi.mock('../../termination/structured-agent-session-exit-observation', async (original) => { - const actual = await original() - class CountedRootExit extends actual.StructuredAgentSessionRootExitReceipt { - constructor() { - super() - created.rootExits.push(this) - } - } - return { ...actual, StructuredAgentSessionRootExitReceipt: CountedRootExit } -}) - -const CALLER = { callerKey: 'client-1' } -const CEILING_MS = 20_000 -type Cycle = 'ready' | 'final' | 'retried' | 'expired' | 'stopped' -const CYCLES: readonly Cycle[] = ['ready', 'final', 'retried', 'expired', 'stopped'] -const SCRIPTS: Record = { - ready: ['ok'], - final: ['refuse'], - retried: ['hang', 'ok'], - expired: ['hang'], - stopped: ['hang'] -} - -let root: string -beforeEach(async () => { - vi.useFakeTimers({ - toFake: ['Date', 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'] - }) - root = await mkdtemp(join(tmpdir(), 'orca-start-lifecycle-')) - resetHostTestOperationIds() - created.endings.length = 0 - created.rootExits.length = 0 -}) -afterEach(async () => { - try { - await closeTestJournalHostDatabases() - await rm(root, { recursive: true, force: true }) - } finally { - vi.useRealTimers() - vi.restoreAllMocks() - } -}) - -/** Runs `count` rounds of each start outcome through the production host, router and Codex adapter. */ -async function runStarts(count: number) { - // Each thread/start or thread/resume takes the next scripted answer. - const answers: ('ok' | 'refuse' | 'hang')[] = [] - const threadAnswer = () => { - const answer = answers.shift() - if (answer === 'refuse') { - throw new CodexAppServerRequestError('thread/start', -32600, 'invalid', 'invalid') - } - return answer === 'hang' - ? new Promise(() => {}) - : { thread: { id: THREAD_ID }, model: 'gpt-live', reasoningEffort: 'medium' } - } - const codex = fakeCodex({ 'thread/start': threadAnswer, 'thread/resume': threadAnswer }) - let host: StructuredAgentSessionHost | undefined - const store = await openTestAgentSessionRecordStore(join(root, `starts-${count}`)) - const adapter = adapterFor(codex, {}, [], { - // Once a thread exists, each new child resumes it. - resolveLaunch: async () => ({ - command: 'codex', - args: ['app-server'], - cwd: '/work/repo', - codexHome: null, - resumeThreadId: store.getRecord(SESSION)?.providerHandleChain.length ? THREAD_ID : null - }), - onEvent: (event) => { - if (event.type === 'ended' && 'cause' in event) { - void host?.handleAdapterEvent(event) - } - } - }) - const other = adapterFor(fakeCodex()) - const router = claudeAndCodexRouter({ claude: other, codex: adapter }, async () => { - await adapter.closeAll() - await other.closeAll() - }) - const dispatch = vi.fn(async () => ({ - state: 'accepted' as const, - providerIdentity: { - provider: 'codex' as const, - threadId: THREAD_ID, - turnId: `turn-${dispatch.mock.calls.length}`, - ordinal: dispatch.mock.calls.length - } - })) - // Turn work is not under test; every start-path call reaches the production router. - Object.assign(router, { dispatch }) - host = new StructuredAgentSessionHost({ - agents: claudeAndCodexAgents({ claude: other, codex: adapter }), - logger: createStructuredAgentSessionLogger(), - store, - adapter: router, - journalDatabase: openTestJournalHostDatabase(join(root, `starts-${count}`)), - claimKeyId: 'key-1', - mintSpawnToken: () => `spawn-${codex.connections.length}`, - startupLimits: { ceilingMs: CEILING_MS }, - idleSweep: { intervalMs: 60 * 60_000, idleMs: () => 60 * 60_000 }, - now: () => NOW - }) - const { runtimeState } = host.collaboratorsForTests() - const spies = { - opens: vi.spyOn(router, 'open'), - // The router reaches every registered adapter by exact generation. - codexEndingReads: vi.spyOn(adapter, 'startupEnding'), - otherEndingReads: vi.spyOn(other, 'startupEnding'), - codexRetirements: vi.spyOn(adapter, 'retireStartup'), - otherRetirements: vi.spyOn(other, 'retireStartup'), - timersScheduled: vi.spyOn(globalThis, 'setTimeout') - } - // Abort handles the host still holds, and the listeners on their signals. - const abortSignals = () => - [...runtimeState.acquireAborts['inFlight'].values()].flatMap((waits) => - [...waits].map(({ controller }) => controller.signal) - ) - const abortListeners = () => - abortSignals().reduce((sum, signal) => sum + getEventListeners(signal, 'abort').length, 0) - const held = () => ({ - timers: vi.getTimerCount(), - abortListeners: abortListeners(), - abortHandles: abortSignals().length, - trackedStarts: runtimeState.startupAttempts['open'].size, - owners: runtimeState.startupAttempts['owners'].size, - flights: runtimeState.startFlights['flying'].size, - bufferedExits: runtimeState.startFlights['exits'].size, - receipts: retainedReceipts(), - rootExitListeners: created.rootExits.reduce((sum, exit) => sum + exit['listeners'].size, 0), - liveChildren: codex.connections.filter((connection) => !connection.closed).length, - // Host work still tracked or queued after its caller returned. - trackedTasks: host!['tasks']['attaching'].size, - queuedTasks: host!['tasks']['chains'].size - }) - const eventually = (assertion: () => unknown) => - vi.waitFor(assertion, { timeout: 10_000, interval: 0 }) - - const submission = async (id: string) => - (await host!.journalSnapshot(SESSION)).submissions.find((row) => row.clientMessageId === id) - const failureRows = async () => - (await host!.journalSnapshot(SESSION)).items.filter( - (item) => item.body.kind === 'status' && item.body.tone === 'error' - ).length - const phase = () => host!.collaboratorsForTests().sessions.get(SESSION)?.child?.phase - const exitChild = (connection = codex.connections.at(-1)!) => { - connection.closed = true - const exit = new CodexAppServerExitError('codex app-server connection ended') - // A duplicate terminal report must change nothing. - connection.handlers.onExit?.(exit, { expected: false }) - connection.handlers.onExit?.(exit, { expected: false }) - } - - // Work the starts did, counted from a ready chat. - const work = async (): Promise> => ({ - ...Object.fromEntries( - Object.entries(spies).map(([field, spy]) => [field, spy.mock.calls.length]) - ), - spawns: codex.connections.length, - failureRows: await failureRows() - }) - - answers.push('ok') - expect(await host.attach(CALLER, hostTestAttachParams(null))).toMatchObject({ ok: true }) - await eventually(() => expect(phase()).toBe('ready')) - const begun = await work() - const after: Record[]> = { - ready: [], - final: [], - retried: [], - expired: [], - stopped: [] - } - let rows = 0 - // What a Stopped start holds while its child's cleanup is still running. - const stopping: ReturnType[] = [] - // What one start holds while its child is still answering. - const starting: ReturnType[] = [] - for (let round = 0; round < count; round++) { - for (const cycle of CYCLES) { - // The chat's ready child dies, so the next message starts a new one as the script says. - if (phase() === 'ready') { - exitChild() - await eventually(() => expect(phase()).toBeUndefined()) - } - await host.flushStreamedEvents(SESSION) - rows = await failureRows() - const spawned = codex.connections.length - answers.push(...SCRIPTS[cycle]) - const send = restTestSend(cycle, store.getRecord(SESSION)!.lease.runtimeFence) - expect(await host.send(CALLER, send), `${cycle} ${round}`).toMatchObject({ ok: true }) - if (cycle === 'retried') { - await eventually(() => expect(answers).toEqual(['ok'])) - starting.push(held()) - exitChild() - } - if (cycle === 'expired') { - await eventually(() => expect(answers).toEqual([])) - await vi.advanceTimersByTimeAsync(CEILING_MS) - } - if (cycle === 'stopped') { - await eventually(() => expect(answers).toEqual([])) - // The user stops the start while its open waits; the child's cleanup is held, then ends. - const connection = codex.connections.at(-1)! - const close = connection.close - const cleanup = Promise.withResolvers() - connection.close = async () => { - await cleanup.promise - return close() - } - const fence = store.getRecord(SESSION)!.lease.runtimeFence - const stop = { - sessionId: SESSION, - clientOperationId: hostTestOperationId(), - expectedRuntimeFence: fence, - payloadFingerprint: computeAgentSessionPayloadFingerprint({ - method: 'agentSession.cancel', - sessionId: SESSION, - fields: {} - }) - } - const stopped = host.cancel(CALLER, { envelope: stop }) - await vi.advanceTimersByTimeAsync(100) - stopping.push(held()) - cleanup.resolve() - expect(await stopped).toMatchObject({ ok: true }) - } - // The start's owner decides it: the message runs on a ready child, or one row says why not. - const started = cycle === 'ready' || cycle === 'retried' - await eventually(async () => { - await vi.advanceTimersByTimeAsync(100) - expect(runtimeState.startFlights.has(SESSION)).toBe(false) - expect(phase(), `${cycle} ${round}`).toBe(started ? 'ready' : undefined) - expect((await submission(send.envelope.clientOperationId))?.dispatchState).toBe( - started ? 'accepted' : 'rejected' - ) - expect(await failureRows()).toBe(rows + (cycle === 'final' || cycle === 'expired' ? 1 : 0)) - }) - expect(codex.connections).toHaveLength(spawned + (cycle === 'retried' ? 2 : 1)) - await host.flushStreamedEvents(SESSION) - await vi.advanceTimersByTimeAsync(2_000) - after[cycle].push(held()) - } - } - const done = await work() - await host.close(SESSION, 'evict') - await host.flushAllStreamedEvents() - await router.closeAll() - vi.restoreAllMocks() - const counts = Object.fromEntries( - Object.entries(done).map(([field, count]) => [field, count - begun[field]]) - ) - return { after, starting, stopping, counts } -} - -it('returns every start resource to baseline and does constant work per start, at N and 10N', async () => { - const n = await runStarts(1) - const tenN = await runStarts(10) - for (const cycle of CYCLES) { - const [first] = n.after[cycle] - // Nothing a finished start owned survives it: no owner, abort handle, tracked attempt, flight, - // buffered exit, adapter receipt, root-exit listener or tracked task, and no exited child left - // running. - expect(first, cycle).toMatchObject({ - abortHandles: 0, - trackedStarts: 0, - owners: 0, - flights: 0, - bufferedExits: 0, - receipts: 0, - rootExitListeners: 0, - liveChildren: cycle === 'ready' || cycle === 'retried' ? 1 : 0, - trackedTasks: 0, - queuedTasks: 0 - }) - // Timers and abort listeners held after each start do not grow with the starts before it. - for (const held of tenN.after[cycle]) { - expect(held, cycle).toEqual(first) - } - } - // While a start waits on its child it holds a fixed set, however many starts came before it: - // one deadline timer, the flight's and the owner's abort handles, one tracked attempt, owner, - // receipt and root-exit subscription. - expect(n.starting[0]).toMatchObject({ - timers: n.after.ready[0].timers + 1, - abortHandles: 2, - trackedStarts: 1, - owners: 1, - flights: 1, - receipts: 1, - rootExitListeners: 1 - }) - for (const held of tenN.starting) { - expect(held).toEqual(n.starting[0]) - } - // A Stop releases the start's owner at once; while its child's cleanup is held, that cleanup is - // the one task still tracked and queued, however many Stops came before it. - expect(n.stopping[0]).toMatchObject({ - owners: 0, - flights: 0, - abortHandles: 0, - receipts: 0, - trackedTasks: 1, - queuedTasks: 1, - liveChildren: 1 - }) - for (const held of tenN.stopping) { - expect(held).toEqual(n.stopping[0]) - } - // Per round of five starts: six spawns and opens (the retried start opens twice) and a failure - // row for the final and the expired start; a Stopped start reads no ending. Router reads and - // retirements reach the registered adapters a fixed number of times per open, whatever the - // starts before it. - expect(n.counts).toMatchObject({ - spawns: 6, - opens: 6, - codexEndingReads: 4, - otherEndingReads: 4, - codexRetirements: 6, - otherRetirements: 6, - failureRows: 2 - }) - for (const [field, count] of Object.entries(n.counts)) { - expect(tenN.counts[field], field).toBe(count * 10) - } -}) - -/** Starts an ACP agent that prints a sign-in line Orca cannot read, `count` times. */ -async function refuseAcpStarts(count: number) { - const connections: ReturnType[] = [] - const refusals = new Map, Error>() - const rig = await openAcpAdapterRig({ - deps: { - connect: (launch, options) => { - const child = Object.assign(createFakePipedChild(), { - pid: 9_999_999, - stdout: new PassThrough(), - stdin: new PassThrough(), - stderr: new PassThrough(), - kill: () => true - }) - // Orca's cleanup ends stdin; the agent then exits. - child.stdin.once('finish', () => child.emit('exit', 0, null)) - const connection: ReturnType = createAcpAgentConnection( - launch, - { - ...options, - onClose: (error) => { - if (!refusals.has(connection)) { - refusals.set(connection, error) - } - options.onClose?.(error) - }, - signIn: { read: readAntigravitySignInLine } - }, - () => child - ) - connections.push(connection) - child.stdout.write('Open the following link to authenticate the ACP server: not-a-url\n') - return connection - } - } - }) - const retained: number[] = [] - try { - for (let index = 0; index < count; index++) { - const error = await rig.acquire({ waitForStart: false }).catch((caught: unknown) => caught) - const connection = connections[index] - // The verdict is final and carries this connection's own first refusal, not a later close. - const verdict = findInErrorChain(error, (cause) => - cause instanceof AgentSessionStartFailure ? cause : undefined - ) - expect(verdict?.retry).toBe('never') - expect(verdict?.cause).toBe(refusals.get(connection)) - expect(connection.lostBeforeExit).toBe(refusals.get(connection)) - expect(connection.exited).toBe(true) - retained.push(retainedReceipts()) - } - return { spawns: connections.length, retained } - } finally { - await rig.adapter.closeAll() - await closeProviderTimelineRigs() - } -} - -it('ACP: each refused start keeps its own cause and leaves no receipt or child, at N and 10N', async () => { - vi.useRealTimers() - for (const count of [10, 100]) { - const { spawns, retained } = await refuseAcpStarts(count) - expect(spawns).toBe(count) - expect(retained).toEqual(Array(count).fill(0)) - } -}) diff --git a/src/main/runtime/desktop-browser-bridge/desktop-browser-bridge-allowlist.spec.ts b/src/main/runtime/desktop-browser-bridge/desktop-browser-bridge-allowlist.spec.ts deleted file mode 100644 index a3cd1eaafa5..00000000000 --- a/src/main/runtime/desktop-browser-bridge/desktop-browser-bridge-allowlist.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { DESKTOP_BROWSER_BRIDGE_METHODS } from '../../../shared/desktop-browser-bridge-protocol' -import { ALL_RPC_METHODS } from '../rpc/methods/rpc-method-registry' - -describe('desktop browser bridge allowlist', () => { - const registered = new Map( - ALL_RPC_METHODS.map((method) => [method.name, method]) - ) - - it('names only real one-shot browser methods in the workspace tier', () => { - for (const name of DESKTOP_BROWSER_BRIDGE_METHODS) { - const method = registered.get(name) - expect(method, name).toBeDefined() - expect(name.startsWith('browser.'), name).toBe(true) - expect(method?.permission, name).toBe('workspace') - expect('stream' in (method ?? {}), name).toBe(false) - } - }) - - it('leaves out every browser method that touches desktop files, secrets or browser identity', () => { - const excluded = [...registered.keys()].filter( - (name) => name.startsWith('browser.') && !DESKTOP_BROWSER_BRIDGE_METHODS.has(name) - ) - expect(excluded).toEqual( - expect.arrayContaining([ - 'browser.upload', - 'browser.download', - 'browser.pdf', - 'browser.exec', - 'browser.cookie.get', - 'browser.cookie.set', - 'browser.cookie.delete', - 'browser.network', - 'browser.clipboardRead', - 'browser.clipboardWrite', - 'browser.storage.local.get', - 'browser.profileImportFromBrowser', - 'browser.profileCreate', - 'browser.profileDelete', - 'browser.tabSetProfile', - 'browser.identity.set', - 'browser.setCredentials', - 'browser.setHeaders', - 'browser.intercept.enable', - 'browser.certificate.proceed', - 'browser.screencast', - 'browser.openUrl', - 'browser.clientHost.attach' - ]) - ) - }) -}) diff --git a/src/main/runtime/mobile-subscribe-integration.spec.ts b/src/main/runtime/mobile-subscribe-integration.spec.ts index 6e585b54ea7..38720ec3a53 100644 --- a/src/main/runtime/mobile-subscribe-integration.spec.ts +++ b/src/main/runtime/mobile-subscribe-integration.spec.ts @@ -174,113 +174,6 @@ describe('mobile subscribe integration', () => { } }) - it('serializeMainTerminalBuffer returns an empty snapshot for an empty headless buffer', async () => { - const { runtime } = createRuntime() - type HeadlessStateForTest = { - emulator: { - isAlternateScreen: boolean - getSnapshot: (opts: { scrollbackRows?: number }) => { - rehydrateSequences: string - snapshotAnsi: string - cols: number - rows: number - } - } - outputSequence: number - writeChain: Promise - ownership: { settle: () => Promise; owner: undefined } - } - const runtimePrivate = runtime as unknown as { - headlessTerminals: Map - } - runtimePrivate.headlessTerminals.set('pty-empty', { - emulator: { - isAlternateScreen: false, - getSnapshot: () => ({ rehydrateSequences: '', snapshotAnsi: '', cols: 90, rows: 30 }) - }, - outputSequence: 17, - writeChain: Promise.resolve(), - ownership: { settle: async () => {}, owner: undefined } - }) - - await expect(runtime.serializeMainTerminalBuffer('pty-empty')).resolves.toEqual({ - data: '', - cols: 90, - rows: 30, - seq: 17, - source: 'headless', - // Preserve destructive scrollback clear on non-alt-screen restore. - alternateScreen: false - }) - await expect(runtime.serializeTerminalBuffer('pty-empty')).resolves.toBeNull() - }) - - it('handleMobileSubscribe resizes PTY to phone dims', async () => { - const { runtime, ptySizes, resizes, notifications } = createRuntime() - - const result = await runtime.handleMobileSubscribe('pty-1', 'client-a', { - cols: 45, - rows: 20 - }) - - expect(result).toBe(true) - expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 }) - expect(resizes).toEqual([{ ptyId: 'pty-1', cols: 45, rows: 20 }]) - expect(notifications).toEqual([{ ptyId: 'pty-1', mode: 'mobile-fit', cols: 45, rows: 20 }]) - expect(runtime.isMobileSubscriberActive('pty-1')).toBe(true) - }) - - it('handleMobileSubscribe skips resize when mode is desktop', async () => { - const { runtime, ptySizes, resizes } = createRuntime() - runtime.setMobileDisplayMode('pty-1', 'desktop') - - const result = await runtime.handleMobileSubscribe('pty-1', 'client-a', { - cols: 45, - rows: 20 - }) - - expect(result).toBe(false) - expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 }) - expect(resizes).toEqual([]) - }) - - it('handleMobileSubscribe skips resize when no viewport provided', async () => { - const { runtime, ptySizes, resizes } = createRuntime() - - const result = await runtime.handleMobileSubscribe('pty-1', 'client-a') - - expect(result).toBe(false) - expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 }) - expect(resizes).toEqual([]) - }) - - it('handleMobileUnsubscribe restores PTY after debounce in auto mode', async () => { - const { runtime, ptySizes } = createRuntime() - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 }) - - runtime.handleMobileUnsubscribe('pty-1', 'client-a') - expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 }) - - await vi.advanceTimersByTimeAsync(LEGACY_RESTORE_MS) - expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 }) - }) - - it('preserves previousDims across re-subscribes to same terminal', async () => { - const { runtime, ptySizes } = createRuntime() - - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 }) - - // Re-subscribe sees phone-fitted PTY dims but must retain the original desktop baseline. - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - - runtime.handleMobileUnsubscribe('pty-1', 'client-a') - await vi.advanceTimersByTimeAsync(LEGACY_RESTORE_MS) - - expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 }) - }) - it('preserves held-override baseline across resubscribe-after-indefinite-hold', async () => { // Indefinite-hold re-subscribe must inherit the retained desktop baseline for later Restore. settingsState.mobileAutoRestoreFitMs = null @@ -300,184 +193,7 @@ describe('mobile subscribe integration', () => { expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 }) }) - it('clamps viewport to valid range', async () => { - const { runtime, ptySizes } = createRuntime() - - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 10, rows: 3 }) - expect(ptySizes.get('pty-1')).toEqual({ cols: 20, rows: 8 }) - }) - - describe('display mode', () => { - it('defaults to auto', () => { - const { runtime } = createRuntime() - expect(runtime.getMobileDisplayMode('pty-1')).toBe('auto') - }) - - it('set/get round-trip', () => { - const { runtime } = createRuntime() - runtime.setMobileDisplayMode('pty-1', 'desktop') - expect(runtime.getMobileDisplayMode('pty-1')).toBe('desktop') - - runtime.setMobileDisplayMode('pty-1', 'auto') - expect(runtime.getMobileDisplayMode('pty-1')).toBe('auto') - }) - }) - - describe('applyMobileDisplayMode', () => { - it('desktop mode restores PTY when currently phone-fitted', async () => { - const { runtime, ptySizes } = createRuntime() - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 }) - - const resizeEvents: unknown[] = [] - runtime.subscribeToTerminalResize('pty-1', (event) => resizeEvents.push(event)) - - runtime.setMobileDisplayMode('pty-1', 'desktop') - await runtime.applyMobileDisplayMode('pty-1') - - expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 }) - expect(resizeEvents).toHaveLength(1) - expect(resizeEvents[0]).toMatchObject({ - cols: 150, - rows: 40, - displayMode: 'desktop', - reason: 'apply-layout' - }) - }) - - it('auto mode re-fits PTY when subscriber exists and not phone-fitted', async () => { - const { runtime, ptySizes } = createRuntime() - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - - runtime.setMobileDisplayMode('pty-1', 'desktop') - await runtime.applyMobileDisplayMode('pty-1') - expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 }) - - const resizeEvents: unknown[] = [] - runtime.subscribeToTerminalResize('pty-1', (event) => resizeEvents.push(event)) - - runtime.setMobileDisplayMode('pty-1', 'auto') - await runtime.applyMobileDisplayMode('pty-1') - - expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 }) - expect(resizeEvents).toHaveLength(1) - expect(resizeEvents[0]).toMatchObject({ - displayMode: 'phone', - reason: 'apply-layout' - }) - }) - }) - - describe('cleanup paths', () => { - it('onClientDisconnected restores all PTYs immediately (no debounce)', async () => { - const { runtime, ptySizes } = createRuntime() - - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - await runtime.handleMobileSubscribe('pty-2', 'client-a', { cols: 45, rows: 20 }) - - runtime.onClientDisconnected('client-a') - // Disconnect queues work; flush microtasks and zero-delay timers. - await vi.advanceTimersByTimeAsync(0) - - expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 }) - expect(ptySizes.get('pty-2')).toEqual({ cols: 120, rows: 35 }) - expect(runtime.isMobileSubscriberActive('pty-1')).toBe(false) - expect(runtime.isMobileSubscriberActive('pty-2')).toBe(false) - }) - - it('onClientDisconnected cancels pending restore timers', async () => { - const { runtime, ptySizes } = createRuntime() - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - runtime.handleMobileUnsubscribe('pty-1', 'client-a') - - runtime.onClientDisconnected('client-a') - - await vi.advanceTimersByTimeAsync(LEGACY_RESTORE_MS) - expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 }) - }) - - it('onPtyExit cleans up mobileSubscribers and pending timers', async () => { - const { runtime } = createRuntime() - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - runtime.handleMobileUnsubscribe('pty-1', 'client-a') - - runtime.onPtyExit('pty-1', 0) - expect(runtime.isMobileSubscriberActive('pty-1')).toBe(false) - expect(runtime.getMobileDisplayMode('pty-1')).toBe('auto') - - await vi.advanceTimersByTimeAsync(LEGACY_RESTORE_MS) - }) - - it('onPtyExit does not cancel timers for other PTYs', async () => { - const { runtime, ptySizes } = createRuntime() - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - runtime.handleMobileUnsubscribe('pty-1', 'client-a') - - runtime.onPtyExit('pty-2', 0) - - await vi.advanceTimersByTimeAsync(LEGACY_RESTORE_MS) - expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 }) - }) - }) - - describe('resize listener system', () => { - it('subscribe/unsubscribe lifecycle', async () => { - const { runtime } = createRuntime() - const events: unknown[] = [] - const unsubscribe = runtime.subscribeToTerminalResize('pty-1', (e) => events.push(e)) - - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - runtime.setMobileDisplayMode('pty-1', 'desktop') - await runtime.applyMobileDisplayMode('pty-1') - - expect(events.length).toBeGreaterThan(0) - - const countBefore = events.length - unsubscribe() - - runtime.setMobileDisplayMode('pty-1', 'auto') - await runtime.applyMobileDisplayMode('pty-1') - expect(events.length).toBe(countBefore) - }) - }) - describe('onExternalPtyResize', () => { - it('updates previousCols when desktop renderer resizes PTY after desktop restore', async () => { - const { runtime, ptySizes } = createRuntime() - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - - runtime.setMobileDisplayMode('pty-1', 'desktop') - await runtime.applyMobileDisplayMode('pty-1') - expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 }) - - // Wait out the 500ms phone-to-desktop resize cascade suppression before reporting renderer geometry. - await vi.advanceTimersByTimeAsync(500) - - runtime.onExternalPtyResize('pty-1', 105, 40) - - runtime.setMobileDisplayMode('pty-1', 'auto') - await runtime.applyMobileDisplayMode('pty-1') - expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 }) - - runtime.setMobileDisplayMode('pty-1', 'desktop') - await runtime.applyMobileDisplayMode('pty-1') - expect(ptySizes.get('pty-1')).toEqual({ cols: 105, rows: 40 }) - }) - - it('uses lastRendererSize for previousCols on first subscribe', async () => { - const { runtime, ptySizes } = createRuntime() - - ptySizes.set('pty-1', { cols: 214, rows: 72 }) - runtime.onExternalPtyResize('pty-1', 105, 40) - - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 }) - - runtime.setMobileDisplayMode('pty-1', 'desktop') - await runtime.applyMobileDisplayMode('pty-1') - expect(ptySizes.get('pty-1')).toEqual({ cols: 105, rows: 40 }) - }) - it('ignores reports while a mobile-fit override is in place', async () => { // Phone-fit echoes must not poison the desktop restore baseline; see docs/mobile-fit-hold.md. const { runtime, ptySizes } = createRuntime() @@ -490,28 +206,6 @@ describe('mobile subscribe integration', () => { expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 }) }) - it('still updates baseline once the override is cleared (real desktop resize)', async () => { - // Once the override clears, legitimate renderer reports must update the baseline. - const { runtime, ptySizes } = createRuntime() - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - - runtime.setMobileDisplayMode('pty-1', 'desktop') - await runtime.applyMobileDisplayMode('pty-1') - expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 }) - - await vi.advanceTimersByTimeAsync(500) - - runtime.onExternalPtyResize('pty-1', 130, 35) - - runtime.setMobileDisplayMode('pty-1', 'auto') - await runtime.applyMobileDisplayMode('pty-1') - expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 }) - - runtime.setMobileDisplayMode('pty-1', 'desktop') - await runtime.applyMobileDisplayMode('pty-1') - expect(ptySizes.get('pty-1')).toEqual({ cols: 130, rows: 35 }) - }) - it('updates baseline mid-fit when desktop reports dims that differ from the override', async () => { // A newly visible desktop tab can report real geometry during phone fit; retain it for restore. const { runtime, ptySizes } = createRuntime() @@ -528,45 +222,6 @@ describe('mobile subscribe integration', () => { }) describe('recordRendererGeometry (pty:reportGeometry IPC)', () => { - it('refreshes subscriber baseline while a mobile-fit override is active', async () => { - // Never-active terminals need measurement-only geometry to replace spawn-default restore sizes. - const { runtime, ptySizes } = createRuntime() - ptySizes.set('pty-1', { cols: 80, rows: 24 }) - runtime.onExternalPtyResize('pty-1', 80, 24) - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - - runtime.recordRendererGeometry('pty-1', 214, 72) - - runtime.setMobileDisplayMode('pty-1', 'desktop') - await runtime.applyMobileDisplayMode('pty-1') - expect(ptySizes.get('pty-1')).toEqual({ cols: 214, rows: 72 }) - }) - - it('restores reported geometry captured before the first mobile subscribe', async () => { - const { runtime, ptySizes, resizes } = createRuntime() - runtime.recordRendererGeometry('pty-1', 180, 50) - expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 }) - expect(resizes).toEqual([]) - - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - expect(await runtime.reclaimTerminalForDesktop('pty-1')).toBe(true) - expect(ptySizes.get('pty-1')).toEqual({ cols: 180, rows: 50 }) - }) - - it('ignores non-positive dims when capturing restore geometry', async () => { - const { runtime, ptySizes, resizes } = createRuntime() - runtime.recordRendererGeometry('pty-1', 180, 50) - runtime.recordRendererGeometry('pty-1', 0, 0) - runtime.recordRendererGeometry('pty-1', -5, 10) - runtime.recordRendererGeometry('pty-1', 10, -5) - expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 }) - expect(resizes).toEqual([]) - - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - expect(await runtime.reclaimTerminalForDesktop('pty-1')).toBe(true) - expect(ptySizes.get('pty-1')).toEqual({ cols: 180, rows: 50 }) - }) - it('bypasses the cascade-suppress window (it is measurement-only)', async () => { const { runtime, ptySizes, resizes } = createRuntime() await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) @@ -589,78 +244,8 @@ describe('mobile subscribe integration', () => { }) }) - describe('backward compatibility', () => { - it('old resizeForClient still works alongside new system', async () => { - const { runtime, ptySizes } = createRuntime() - - const fitResult = await runtime.resizeForClient('pty-1', 'mobile-fit', 'client-old', 45, 20) - expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 }) - expect(fitResult.mode).toBe('mobile-fit') - - const restoreResult = await runtime.resizeForClient('pty-1', 'restore', 'client-old') - expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 }) - expect(restoreResult.mode).toBe('desktop-fit') - }) - }) - // See docs/mobile-fit-hold.md. describe('mobileAutoRestoreFitMs (fit hold)', () => { - it('null (indefinite) keeps PTY at phone dims after last unsubscribe', async () => { - settingsState.mobileAutoRestoreFitMs = null - const { runtime, ptySizes } = createRuntime() - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - runtime.handleMobileUnsubscribe('pty-1', 'client-a') - - await vi.advanceTimersByTimeAsync(60_000) - expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 }) - }) - - it('finite ms restores after the configured delay', async () => { - settingsState.mobileAutoRestoreFitMs = 60_000 - const { runtime, ptySizes } = createRuntime() - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - runtime.handleMobileUnsubscribe('pty-1', 'client-a') - - await vi.advanceTimersByTimeAsync(30_000) - expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 }) - - await vi.advanceTimersByTimeAsync(30_000) - expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 }) - }) - - it('re-subscribe before timer fires cancels pending restore', async () => { - settingsState.mobileAutoRestoreFitMs = 60_000 - const { runtime, ptySizes } = createRuntime() - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - runtime.handleMobileUnsubscribe('pty-1', 'client-a') - - await vi.advanceTimersByTimeAsync(30_000) - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - - await vi.advanceTimersByTimeAsync(120_000) - expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 }) - }) - - it('reclaimTerminalForDesktop with active subscriber keeps desktop mode until the phone acts', async () => { - // #28487: reconnect/resume re-subscribe stays passive; only typing takes the floor. - const { runtime, ptySizes } = createRuntime() - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 }) - - const ok = await runtime.reclaimTerminalForDesktop('pty-1') - expect(ok).toBe(true) - expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 }) - expect(runtime.getMobileDisplayMode('pty-1')).toBe('desktop') - - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 }) - expect(runtime.getDriver('pty-1').kind).not.toBe('mobile') - - await runtime.mobileTookFloor('pty-1', 'client-a') - expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 }) - expect(runtime.getMobileDisplayMode('pty-1')).toBe('auto') - }) - it('reclaim with WS disconnect between cycles — held branch restores correctly each time', async () => { // Repeated held-override reclaim must restore desktop geometry, never the current phone fit. settingsState.mobileAutoRestoreFitMs = null @@ -690,176 +275,5 @@ describe('mobile subscribe integration', () => { expect(runtime.getTerminalFitOverride('pty-1'), `iter ${i}: override cleared`).toBeNull() } }) - - it('reclaim → re-subscribe → reclaim cycle works repeatedly', async () => { - const { runtime, ptySizes } = createRuntime() - - for (let i = 0; i < 3; i++) { - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - await runtime.mobileTookFloor('pty-1', 'client-a') - expect(ptySizes.get('pty-1'), `iter ${i}: phone-fit`).toEqual({ cols: 45, rows: 20 }) - - const ok = await runtime.reclaimTerminalForDesktop('pty-1') - expect(ok, `iter ${i}: reclaim ok`).toBe(true) - expect(ptySizes.get('pty-1'), `iter ${i}: reclaimed to desktop`).toEqual({ - cols: 150, - rows: 40 - }) - expect(runtime.getTerminalFitOverride('pty-1'), `iter ${i}: override cleared`).toBeNull() - } - }) - - it('null (indefinite) keeps PTY at phone dims when WS connection closes (onClientDisconnected)', async () => { - // WS close bypasses handleMobileUnsubscribe but must still honor indefinite hold. - settingsState.mobileAutoRestoreFitMs = null - const { runtime, ptySizes } = createRuntime() - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - - runtime.onClientDisconnected('client-a') - await vi.advanceTimersByTimeAsync(0) - await vi.advanceTimersByTimeAsync(60_000) - - expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 }) - expect(runtime.getTerminalFitOverride('pty-1')).not.toBeNull() - }) - - it('reclaimTerminalForDesktop returns held PTY to desktop dims (no subscriber)', async () => { - settingsState.mobileAutoRestoreFitMs = null - const { runtime, ptySizes } = createRuntime() - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - runtime.handleMobileUnsubscribe('pty-1', 'client-a') - - await runtime.reclaimTerminalForDesktop('pty-1') - expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 }) - - await vi.advanceTimersByTimeAsync(250) - expect(runtime.getDriver('pty-1')).toEqual({ kind: 'desktop' }) - }) - - it('reclaim cancels the soft-leave timer in the remote-layout branch', async () => { - const { runtime } = createRuntime() - await runtime.updateRemoteDesktopViewer('pty-1', 'sub-a', 'viewer-a', 120, 32) - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - runtime.handleMobileUnsubscribe('pty-1', 'client-a') - - expect(await runtime.reclaimTerminalForDesktop('pty-1')).toBe(true) - await vi.advanceTimersByTimeAsync(250) - expect(runtime.getDriver('pty-1')).toEqual({ kind: 'desktop' }) - }) - - it('reclaim cancels pending restore timers on the active-subscriber branch', async () => { - const { runtime } = createRuntime() - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - runtime.handleMobileUnsubscribe('pty-1', 'client-a') - await runtime.handleMobileSubscribe('pty-1', 'client-b', { cols: 40, rows: 18 }) - - const pendingRestore = runtime.restoreTimers - pendingRestore.set('pty-1', { timer: setTimeout(() => {}, 60_000), clientId: 'client-b' }) - const pendingSoft = runtime.softLeavers - expect(pendingSoft.has('pty-1')).toBe(true) - await runtime.reclaimTerminalForDesktop('pty-1') - expect(pendingRestore.has('pty-1')).toBe(false) - expect(pendingSoft.has('pty-1')).toBe(false) - }) - - it('reclaim cancels pending restore timers on the orphan-driver branch', async () => { - const { runtime } = createRuntime() - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - runtime.handleMobileUnsubscribe('pty-1', 'client-a') - runtime.fitOverrides.delete('pty-1') - - const pendingRestore = runtime.restoreTimers - const pendingSoft = runtime.softLeavers - await runtime.reclaimTerminalForDesktop('pty-1') - expect(pendingRestore.has('pty-1')).toBe(false) - expect(pendingSoft.has('pty-1')).toBe(false) - }) - - it('reclaim cancels pending restore timers when no driver lock remains', async () => { - const { runtime } = createRuntime() - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - runtime.handleMobileUnsubscribe('pty-1', 'client-a') - runtime.fitOverrides.delete('pty-1') - runtime.drivers.set('pty-1', { kind: 'idle' }) - - const pendingRestore = runtime.restoreTimers - const pendingSoft = runtime.softLeavers - expect(await runtime.reclaimTerminalForDesktop('pty-1')).toBe(false) - expect(pendingRestore.has('pty-1')).toBe(false) - expect(pendingSoft.has('pty-1')).toBe(false) - }) - - it('reclaim revokes soft-leave grace admission for mobile input', async () => { - const { runtime } = createRuntime() - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - runtime.handleMobileUnsubscribe('pty-1', 'client-a') - - const claim = runtime.beginMobileInputFloor('pty-1', 'client-a') - expect(claim).not.toBeNull() - claim?.rollback() - - await runtime.reclaimTerminalForDesktop('pty-1') - expect(runtime.getDriver('pty-1')).toEqual({ kind: 'desktop' }) - expect(runtime.beginMobileInputFloor('pty-1', 'client-a')).toBeNull() - }) - - it('reclaimTerminalForDesktop prefers fresh desktop geometry for a held PTY', async () => { - settingsState.mobileAutoRestoreFitMs = null - const { runtime, ptySizes } = createRuntime() - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - runtime.handleMobileUnsubscribe('pty-1', 'client-a') - - await vi.advanceTimersByTimeAsync(60_000) - expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 }) - - runtime.recordRendererGeometry('pty-1', 180, 50) - - const ok = await runtime.reclaimTerminalForDesktop('pty-1') - expect(ok).toBe(true) - expect(ptySizes.get('pty-1')).toEqual({ cols: 180, rows: 50 }) - }) - - it('setMobileAutoRestoreFitMs(null) clears all pending timers', async () => { - settingsState.mobileAutoRestoreFitMs = 60_000 - const { runtime, ptySizes } = createRuntime() - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - runtime.handleMobileUnsubscribe('pty-1', 'client-a') - - runtime.setMobileAutoRestoreFitMs(null) - await vi.advanceTimersByTimeAsync(120_000) - expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 }) - }) - - it('setMobileAutoRestoreFitMs to a finite value does not retroactively schedule', async () => { - settingsState.mobileAutoRestoreFitMs = null - const { runtime, ptySizes } = createRuntime() - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - runtime.handleMobileUnsubscribe('pty-1', 'client-a') - - // Finite preferences apply on the next unsubscribe, never retroactively to an existing hold. - runtime.setMobileAutoRestoreFitMs(60_000) - await vi.advanceTimersByTimeAsync(120_000) - expect(ptySizes.get('pty-1')).toEqual({ cols: 45, rows: 20 }) - }) - - it('setMobileAutoRestoreFitMs clamps below MIN to MIN', () => { - const { runtime } = createRuntime() - const result = runtime.setMobileAutoRestoreFitMs(100) - expect(result).toBe(5_000) - }) - - it('setMobileAutoRestoreFitMs clamps above MAX to MAX', () => { - const { runtime } = createRuntime() - const result = runtime.setMobileAutoRestoreFitMs(99 * 60 * 60 * 1000) - expect(result).toBe(60 * 60 * 1000) - }) - - it('reclaimTerminalForDesktop on already-restored PTY returns false', async () => { - const { runtime } = createRuntime() - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - runtime.handleMobileUnsubscribe('pty-1', 'client-a') - await vi.advanceTimersByTimeAsync(LEGACY_RESTORE_MS) - expect(await runtime.reclaimTerminalForDesktop('pty-1')).toBe(false) - }) }) }) diff --git a/src/main/runtime/orca-runtime-automations.spec.ts b/src/main/runtime/orca-runtime-automations.spec.ts index 4b333e148a2..2c0a88d0b0c 100644 --- a/src/main/runtime/orca-runtime-automations.spec.ts +++ b/src/main/runtime/orca-runtime-automations.spec.ts @@ -1,4 +1,3 @@ -import type { RuntimeAutomationController } from './runtime-automation-controller' import { describe, expect, it, vi } from 'vitest' import { OrcaRuntimeService } from './orca-runtime' import type { Automation } from '../../shared/automations-types' @@ -73,38 +72,6 @@ export function registerOrcaRuntimeAutomationsTests(): void { } satisfies Automation describe('OrcaRuntimeService automation methods', () => { - it('creates repo-scoped automations through the shared store', async () => { - const store = makeStore() - const runtime = new OrcaRuntimeService(store as never) - - const automation = await runtime.createAutomation({ - name: 'Daily review', - prompt: 'Review changes', - precheck: { command: 'test -f ready', timeoutSeconds: 30 }, - agentId: 'codex', - repo: 'repo-1', - workspaceMode: 'new_per_run', - setupDecision: 'skip', - rrule: 'FREQ=DAILY;BYHOUR=9;BYMINUTE=0', - dtstart: 1 - }) - - expect(store.createAutomation).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'Daily review', - prompt: 'Review changes', - precheck: { command: 'test -f ready', timeoutSeconds: 30 }, - agentId: 'codex', - projectId: 'repo-1', - workspaceMode: 'new_per_run', - workspaceId: null, - setupDecision: 'skip' - }), - { destination: undefined, resolvedRepo: repo } - ) - expect(automation.id).toBe('auto-1') - }) - it('rejects a run context that names a different repo path', async () => { const store = makeStore() const runtime = new OrcaRuntimeService(store as never) @@ -132,38 +99,6 @@ export function registerOrcaRuntimeAutomationsTests(): void { expect(store.createAutomation).not.toHaveBeenCalled() }) - it('rejects a mismatched run context when the workspace is the machine selector', async () => { - const store = makeStore() - const runtime = new OrcaRuntimeService(store as never) - vi.spyOn(runtime, 'showManagedWorktree').mockResolvedValue({ - id: 'repo-1::/tmp/orca', - repoId: 'repo-1', - path: '/tmp/orca' - } as never) - - await expect( - runtime.createAutomation({ - name: 'Workspace review', - prompt: 'Review changes', - agentId: 'codex', - workspace: 'id:repo-1::/tmp/orca', - runContext: { - kind: 'workspace-run', - projectId: 'project-1', - hostId: 'local', - projectHostSetupId: 'setup-1', - repoId: 'repo-1', - path: '/other/repo' - }, - workspaceMode: 'existing', - setupDecision: 'skip', - rrule: 'FREQ=DAILY;BYHOUR=9;BYMINUTE=0', - dtstart: 1 - }) - ).rejects.toThrow('Automation project does not match its run context.') - expect(store.createAutomation).not.toHaveBeenCalled() - }) - it('rejects a run-context-only update that names a different repo path', async () => { const store = makeStore([existingAutomation]) const runtime = new OrcaRuntimeService(store as never) @@ -182,169 +117,5 @@ export function registerOrcaRuntimeAutomationsTests(): void { ).rejects.toThrow('Automation project does not match its run context.') expect(store.updateAutomation).not.toHaveBeenCalled() }) - - it('updates and deletes existing automations through the shared store', async () => { - const store = makeStore([existingAutomation]) - const runtime = new OrcaRuntimeService(store as never) - - const updated = await runtime.updateAutomation('auto-1', { enabled: false }) - const removed = await runtime.deleteAutomation('auto-1') - - expect(store.updateAutomation).toHaveBeenCalledWith( - 'auto-1', - { enabled: false }, - { resolvedRepo: undefined } - ) - expect(updated).toMatchObject({ - prompt: existingAutomation.prompt, - baseBranch: existingAutomation.baseBranch, - workspaceMode: existingAutomation.workspaceMode, - enabled: false - }) - expect(store.deleteAutomation).toHaveBeenCalledWith('auto-1', undefined) - expect(removed).toEqual({ removed: true, id: 'auto-1' }) - }) - - it('preserves explicit nullable fields in sparse automation updates', async () => { - const store = makeStore([existingAutomation]) - const runtime = new OrcaRuntimeService(store as never) - - await runtime.updateAutomation('auto-1', { baseBranch: null }) - - expect(store.updateAutomation).toHaveBeenCalledWith( - 'auto-1', - { baseBranch: null }, - { resolvedRepo: undefined } - ) - }) - - it('passes setup decision updates through the shared store', async () => { - const store = makeStore([existingAutomation]) - const runtime = new OrcaRuntimeService(store as never) - - await runtime.updateAutomation('auto-1', { setupDecision: 'run' }) - - expect(store.updateAutomation).toHaveBeenCalledWith( - 'auto-1', - { setupDecision: 'run' }, - { resolvedRepo: undefined } - ) - }) - - it('passes session reuse updates for existing-workspace automations', async () => { - const existing = { - ...existingAutomation, - workspaceMode: 'existing', - workspaceId: 'repo-1::/tmp/orca', - baseBranch: null - } satisfies Automation - const store = makeStore([existing]) - const runtime = new OrcaRuntimeService(store as never) - - await runtime.updateAutomation('auto-1', { reuseSession: true }) - - expect(store.updateAutomation).toHaveBeenCalledWith( - 'auto-1', - { reuseSession: true }, - { resolvedRepo: undefined } - ) - }) - - it('clears session reuse when retargeting to new-per-run', async () => { - const existing = { - ...existingAutomation, - workspaceMode: 'existing', - workspaceId: 'repo-1::/tmp/orca', - baseBranch: null, - reuseSession: true - } satisfies Automation - const store = makeStore([existing]) - const runtime = new OrcaRuntimeService(store as never) - - await runtime.updateAutomation('auto-1', { - repo: 'repo-1', - workspaceMode: 'new_per_run' - }) - - expect(store.updateAutomation).toHaveBeenCalledWith( - 'auto-1', - expect.objectContaining({ - projectId: 'repo-1', - workspaceMode: 'new_per_run', - workspaceId: null, - reuseSession: false - }), - { resolvedRepo: repo } - ) - }) - - it('rejects session reuse for new-per-run automations', async () => { - const store = makeStore() - const runtime = new OrcaRuntimeService(store as never) - - await expect( - runtime.createAutomation({ - name: 'Fresh', - prompt: 'Run checks', - agentId: 'codex', - repo: 'repo-1', - workspaceMode: 'new_per_run', - reuseSession: true, - rrule: 'FREQ=DAILY;BYHOUR=9;BYMINUTE=0', - dtstart: 1 - }) - ).rejects.toThrow('Session reuse requires an existing workspace target.') - expect(store.createAutomation).not.toHaveBeenCalled() - }) - - it('rejects repo-only updates for existing-workspace automations', async () => { - const existing = { - ...existingAutomation, - workspaceMode: 'existing', - workspaceId: 'repo-1::/tmp/orca-worktree', - baseBranch: null - } satisfies Automation - const store = makeStore([existing]) - const runtime = new OrcaRuntimeService(store as never) - - await expect(runtime.updateAutomation('auto-1', { repo: 'repo-2' })).rejects.toThrow( - 'Repo updates for existing-workspace automation require workspaceMode new_per_run.' - ) - expect(store.updateAutomation).not.toHaveBeenCalled() - }) - }) - - it('keeps generic automation reads on the current owner before invoking the reader', () => { - const runtime = new OrcaRuntimeService(null) - const owner = runtime['automation'] - const result = { rows: ['run-1'] } - const events: string[] = [] - const read = vi.fn(() => { - events.push('read') - return result - }) - const priority = vi.spyOn(owner, 'withExternalProbePriority').mockImplementation(function ( - this: RuntimeAutomationController, - operation: () => T - ): T { - expect(this).toBe(owner) - expect(arguments.length).toBe(1) - events.push('priority') - return operation() - }) - Object.defineProperty(runtime, 'automation', { - get() { - events.push('owner') - return owner - } - }) - try { - expect(runtime['readAutomationRuns'](undefined, undefined, read)).toBe(result) - expect(events).toEqual(['owner', 'priority', 'read']) - expect(priority).toHaveBeenCalledOnce() - expect(read).toHaveBeenCalledOnce() - } finally { - priority.mockRestore() - } }) } diff --git a/src/main/runtime/orca-runtime-tests/agent-status-and-waits-part-02.spec.ts b/src/main/runtime/orca-runtime-tests/agent-status-and-waits-part-02.spec.ts index a7f7dad4711..a1097c6ed27 100644 --- a/src/main/runtime/orca-runtime-tests/agent-status-and-waits-part-02.spec.ts +++ b/src/main/runtime/orca-runtime-tests/agent-status-and-waits-part-02.spec.ts @@ -1,475 +1,12 @@ import { describe, expect, it, vi } from 'vitest' import './runtime-test-module-registration.spec' -import { OrcaRuntimeService } from '../orca-runtime' -import { inspectPtyProviderProcess } from '../../providers/pty-process-inspection' -import { makePaneKey } from '../../../shared/workspace-layout/stable-pane-id' -import type { IPtyProvider } from '../../providers/pty-provider-contract' -import { - TEST_WORKTREE_ID, - createExplicitAgentStatusHarness, - deferred, - store -} from './orca-runtime-test-fixtures.spec' +import '../orca-runtime' +import '../../providers/pty-process-inspection' +import '../../../shared/workspace-layout/stable-pane-id' + +import { createExplicitAgentStatusHarness, deferred } from './orca-runtime-test-fixtures.spec' describe('OrcaRuntimeService', () => { - it('prefers newer explicit working state over older explicit permission state', async () => { - const leafId = '11111111-1111-4111-8111-111111111111' - const paneKey = makePaneKey('tab-1', leafId) - const now = Date.now() - const runtime = new OrcaRuntimeService(store, undefined, { - getAgentStatusSnapshot: () => [ - { - paneKey, - state: 'waiting', - prompt: '', - agentType: 'codex', - connectionId: null, - receivedAt: now - 1000, - stateStartedAt: now - 1000, - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID - }, - { - paneKey, - state: 'working', - prompt: '', - agentType: 'codex', - connectionId: null, - receivedAt: now, - stateStartedAt: now, - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID - } - ] - }) - runtime.setPtyController({ - spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), - write: () => true, - kill: () => true, - getForegroundProcess: async () => null - }) - runtime.attachWindow(1) - runtime.syncWindowGraph(1, { - tabs: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - title: 'repo terminal', - activeLeafId: leafId, - layout: null - } - ], - leaves: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - leafId, - paneRuntimeId: 1, - ptyId: 'pty-1' - } - ] - }) - - const [terminal] = (await runtime.listTerminals()).terminals - - await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ - handle: terminal.handle, - isRunningAgent: true, - status: 'working' - }) - }) - - it('prefers fresh explicit working state over a stale permission title', async () => { - const leafId = '11111111-1111-4111-8111-111111111111' - const paneKey = makePaneKey('tab-1', leafId) - const runtime = new OrcaRuntimeService(store, undefined, { - getAgentStatusSnapshot: () => [ - { - paneKey, - state: 'working', - prompt: '', - agentType: 'codex', - connectionId: null, - receivedAt: Date.now(), - stateStartedAt: Date.now(), - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID - } - ] - }) - runtime.setPtyController({ - spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), - write: () => true, - kill: () => true, - getForegroundProcess: async () => null - }) - runtime.attachWindow(1) - runtime.syncWindowGraph(1, { - tabs: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - title: 'Codex - action required', - activeLeafId: leafId, - layout: null - } - ], - leaves: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - leafId, - paneRuntimeId: 1, - ptyId: 'pty-1' - } - ] - }) - - const [terminal] = (await runtime.listTerminals()).terminals - - await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ - handle: terminal.handle, - isRunningAgent: true, - status: 'working' - }) - }) - - it('reports permission from a live title over fresh explicit working state', async () => { - const leafId = '11111111-1111-4111-8111-111111111111' - const paneKey = makePaneKey('tab-1', leafId) - const runtime = new OrcaRuntimeService(store, undefined, { - getAgentStatusSnapshot: () => [ - { - paneKey, - state: 'working', - prompt: '', - agentType: 'codex', - connectionId: null, - receivedAt: Date.now(), - stateStartedAt: Date.now(), - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID - } - ] - }) - runtime.setPtyController({ - spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), - write: () => true, - kill: () => true, - getForegroundProcess: async () => null - }) - runtime.attachWindow(1) - runtime.syncWindowGraph(1, { - tabs: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - title: 'repo terminal', - activeLeafId: leafId, - layout: null - } - ], - leaves: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - leafId, - paneRuntimeId: 1, - ptyId: 'pty-1', - paneTitle: 'Codex - action required' - } - ] - }) - - const [terminal] = (await runtime.listTerminals()).terminals - - await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ - handle: terminal.handle, - isRunningAgent: true, - status: 'permission' - }) - }) - - it('does not let fresh explicit hook state authorize a current shell terminal', async () => { - const leafId = '11111111-1111-4111-8111-111111111111' - const paneKey = makePaneKey('tab-1', leafId) - const runtime = new OrcaRuntimeService(store, undefined, { - getAgentStatusSnapshot: () => [ - { - paneKey, - state: 'working', - prompt: '', - agentType: 'codex', - connectionId: null, - receivedAt: Date.now(), - stateStartedAt: Date.now(), - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID - } - ] - }) - runtime.setPtyController({ - spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), - write: () => true, - kill: () => true, - getForegroundProcess: async () => null - }) - runtime.attachWindow(1) - runtime.syncWindowGraph(1, { - tabs: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - title: 'zsh', - activeLeafId: leafId, - layout: null - } - ], - leaves: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - leafId, - paneRuntimeId: 1, - ptyId: 'pty-1' - } - ] - }) - - const [terminal] = (await runtime.listTerminals()).terminals - - await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ - handle: terminal.handle, - isRunningAgent: false, - status: null - }) - }) - - it('does not let fresh explicit hook state authorize a shell foreground process', async () => { - const leafId = '11111111-1111-4111-8111-111111111111' - const paneKey = makePaneKey('tab-1', leafId) - const runtime = new OrcaRuntimeService(store, undefined, { - getAgentStatusSnapshot: () => [ - { - paneKey, - state: 'working', - prompt: '', - agentType: 'codex', - connectionId: null, - receivedAt: Date.now(), - stateStartedAt: Date.now(), - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID - } - ] - }) - runtime.setPtyController({ - spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), - write: () => true, - kill: () => true, - getForegroundProcess: async () => 'zsh' - }) - runtime.attachWindow(1) - runtime.syncWindowGraph(1, { - tabs: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - title: 'repo terminal', - activeLeafId: leafId, - layout: null - } - ], - leaves: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - leafId, - paneRuntimeId: 1, - ptyId: 'pty-1' - } - ] - }) - - const [terminal] = (await runtime.listTerminals()).terminals - - await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ - handle: terminal.handle, - isRunningAgent: false, - status: null - }) - }) - - it('uses strong provider confirmation to authorize fresh hook state over a shell foreground', async () => { - const getForegroundProcess = vi.fn(async () => 'powershell.exe') - const confirmForegroundProcess = vi.fn(async () => 'claude') - const { runtime, handle } = await createExplicitAgentStatusHarness({ - getForegroundProcess, - confirmForegroundProcess - }) - - await expect(runtime.getTerminalAgentStatus(handle)).resolves.toEqual({ - handle, - isRunningAgent: true, - status: 'working' - }) - expect(getForegroundProcess).toHaveBeenCalledOnce() - expect(getForegroundProcess).toHaveBeenCalledWith('pty-1') - expect(confirmForegroundProcess).toHaveBeenCalledOnce() - expect(confirmForegroundProcess).toHaveBeenCalledWith('pty-1') - }) - - it('preserves provider failure during completion-sensitive process inspection', async () => { - const failure = new Error('daemon unavailable') - const providerInspectProcess = vi.fn().mockRejectedValue(failure) - const provider = { inspectProcess: providerInspectProcess } as unknown as IPtyProvider - const inspectProcess = vi.fn((ptyId: string) => inspectPtyProviderProcess(provider, ptyId)) - const getForegroundProcess = vi.fn(async () => null) - const { runtime, handle } = await createExplicitAgentStatusHarness({ - getForegroundProcess, - inspectProcess - }) - - await expect(runtime.inspectTerminalProcess(handle)).rejects.toBe(failure) - expect(inspectProcess).toHaveBeenCalledExactlyOnceWith('pty-1') - expect(providerInspectProcess).toHaveBeenCalledExactlyOnceWith('pty-1') - expect(getForegroundProcess).not.toHaveBeenCalled() - }) - - it('preserves provider unavailable results during process inspection', async () => { - const inspection = { - foregroundProcess: null, - hasChildProcesses: true, - unavailable: true as const - } - const inspectProcess = vi.fn(async () => inspection) - const getForegroundProcess = vi.fn(async () => null) - const { runtime, handle } = await createExplicitAgentStatusHarness({ - getForegroundProcess, - inspectProcess - }) - - await expect(runtime.inspectTerminalProcess(handle)).resolves.toEqual(inspection) - expect(inspectProcess).toHaveBeenCalledExactlyOnceWith('pty-1') - expect(getForegroundProcess).not.toHaveBeenCalled() - }) - - it('calls foreground confirmation with its controller receiver', async () => { - const getForegroundProcess = vi.fn(async () => 'powershell.exe') - const confirmForegroundProcess = vi.fn(async function (this: { - getForegroundProcess: typeof getForegroundProcess - }) { - return this.getForegroundProcess === getForegroundProcess ? 'codex' : null - }) - const { runtime, handle } = await createExplicitAgentStatusHarness({ - getForegroundProcess, - confirmForegroundProcess - }) - - await expect(runtime.getTerminalAgentStatus(handle)).resolves.toMatchObject({ - isRunningAgent: true, - status: 'working' - }) - expect(confirmForegroundProcess).toHaveBeenCalledOnce() - }) - - it.each([ - ['shell', async () => 'pwsh.exe'], - ['non-agent', async () => 'vim'], - ['unavailable', async () => null], - [ - 'failure', - async () => { - throw new Error('provider unavailable') - } - ] - ])('fails closed when shell-conflict confirmation returns %s', async (_case, confirm) => { - const confirmForegroundProcess = vi.fn(confirm) - const { runtime, handle } = await createExplicitAgentStatusHarness({ - getForegroundProcess: async () => 'zsh', - confirmForegroundProcess - }) - - await expect(runtime.getTerminalAgentStatus(handle)).resolves.toEqual({ - handle, - isRunningAgent: false, - status: null - }) - expect(confirmForegroundProcess).toHaveBeenCalledOnce() - }) - - it('fails closed on a shell conflict when the controller cannot confirm it', async () => { - const { runtime, handle } = await createExplicitAgentStatusHarness({ - getForegroundProcess: async () => 'zsh' - }) - - await expect(runtime.getTerminalAgentStatus(handle)).resolves.toEqual({ - handle, - isRunningAgent: false, - status: null - }) - }) - - it('skips strong confirmation when ordinary foreground evidence recognizes an agent', async () => { - const getForegroundProcess = vi.fn(async () => 'codex') - const confirmForegroundProcess = vi.fn(async () => 'codex') - const { runtime, handle } = await createExplicitAgentStatusHarness({ - getForegroundProcess, - confirmForegroundProcess - }) - - await expect(runtime.getTerminalAgentStatus(handle)).resolves.toMatchObject({ - isRunningAgent: true, - status: 'working' - }) - expect(getForegroundProcess).toHaveBeenCalledOnce() - expect(confirmForegroundProcess).not.toHaveBeenCalled() - }) - - it('skips both foreground reads when current title evidence blocks explicit hook state', async () => { - const getForegroundProcess = vi.fn(async () => 'zsh') - const confirmForegroundProcess = vi.fn(async () => 'codex') - const { runtime, handle } = await createExplicitAgentStatusHarness({ - getForegroundProcess, - confirmForegroundProcess, - title: 'zsh' - }) - - await expect(runtime.getTerminalAgentStatus(handle)).resolves.toMatchObject({ - isRunningAgent: false, - status: null - }) - expect(getForegroundProcess).not.toHaveBeenCalled() - expect(confirmForegroundProcess).not.toHaveBeenCalled() - }) - - it('skips foreground reads for permission title and blocked wait evidence', async () => { - for (const blocked of ['title', 'wait'] as const) { - const getForegroundProcess = vi.fn(async () => 'zsh') - const confirmForegroundProcess = vi.fn(async () => 'codex') - const { runtime, handle } = await createExplicitAgentStatusHarness({ - getForegroundProcess, - confirmForegroundProcess - }) - runtime.onPtyData( - 'pty-1', - blocked === 'title' - ? '\x1b]0;Codex waiting for permission\x07' - : 'Hooks need review. Press enter to confirm\n', - Date.now() + 1000 - ) - getForegroundProcess.mockClear() - confirmForegroundProcess.mockClear() - - await expect(runtime.getTerminalAgentStatus(handle)).resolves.toMatchObject({ - isRunningAgent: true, - status: 'permission' - }) - expect(getForegroundProcess).not.toHaveBeenCalled() - expect(confirmForegroundProcess).not.toHaveBeenCalled() - } - }) - it('rejects foreground evidence when the handle rebinds during the ordinary read', async () => { const foreground = deferred() const getForegroundProcess = vi.fn(() => foreground.promise) @@ -531,99 +68,4 @@ describe('OrcaRuntimeService', () => { await expect(status).rejects.toThrow('terminal_exited') }) - - it('reports permission from a title-derived action-required agent state', async () => { - const runtime = new OrcaRuntimeService(store) - runtime.setPtyController({ - spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), - write: () => true, - kill: () => true, - getForegroundProcess: async () => null - }) - const leafId = '11111111-1111-4111-8111-111111111111' - runtime.attachWindow(1) - runtime.syncWindowGraph(1, { - tabs: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - title: 'Codex waiting for permission', - activeLeafId: leafId, - layout: null - } - ], - leaves: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - leafId, - paneRuntimeId: 1, - ptyId: 'pty-1' - } - ] - }) - - const [terminal] = (await runtime.listTerminals()).terminals - - await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ - handle: terminal.handle, - isRunningAgent: true, - status: 'permission' - }) - }) - - it('maps fresh explicit done hook state to idle for send readiness', async () => { - const leafId = '11111111-1111-4111-8111-111111111111' - const paneKey = makePaneKey('tab-1', leafId) - const runtime = new OrcaRuntimeService(store, undefined, { - getAgentStatusSnapshot: () => [ - { - paneKey, - state: 'done', - prompt: '', - agentType: 'codex', - connectionId: null, - receivedAt: Date.now(), - stateStartedAt: Date.now(), - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID - } - ] - }) - runtime.setPtyController({ - spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), - write: () => true, - kill: () => true, - getForegroundProcess: async () => null - }) - runtime.attachWindow(1) - runtime.syncWindowGraph(1, { - tabs: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - title: 'repo terminal', - activeLeafId: leafId, - layout: null - } - ], - leaves: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - leafId, - paneRuntimeId: 1, - ptyId: 'pty-1' - } - ] - }) - - const [terminal] = (await runtime.listTerminals()).terminals - - await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ - handle: terminal.handle, - isRunningAgent: true, - status: 'idle' - }) - }) }) diff --git a/src/main/runtime/orca-runtime-tests/agent-status-and-waits.spec.ts b/src/main/runtime/orca-runtime-tests/agent-status-and-waits.spec.ts index 5e761f5200a..fa8cb417cdf 100644 --- a/src/main/runtime/orca-runtime-tests/agent-status-and-waits.spec.ts +++ b/src/main/runtime/orca-runtime-tests/agent-status-and-waits.spec.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest' import { committingSpawn } from './bind-first-pane.test-fixture' import './runtime-test-module-registration.spec' import { OrcaRuntimeService } from '../orca-runtime' -import { makePaneKey } from '../../../shared/workspace-layout/stable-pane-id' +import '../../../shared/workspace-layout/stable-pane-id' import type { RuntimeTerminalAgentStatusEvent } from '../terminal/output/terminal-agent-status-events' import { TEST_WORKTREE_ID, @@ -107,116 +107,6 @@ describe('OrcaRuntimeService', () => { ) }) - it('reads bounded terminal output and writes through the PTY controller', async () => { - const writes: string[] = [] - const runtime = new OrcaRuntimeService(store) - runtime.setPtyController({ - write: (_ptyId, data) => { - writes.push(data) - return true - }, - kill: () => true, - getForegroundProcess: async () => null - }) - - runtime.attachWindow(1) - runtime.syncWindowGraph(1, { - tabs: [ - { - tabId: 'tab-1', - worktreeId: 'repo-1::/tmp/worktree-a', - title: 'Claude', - activeLeafId: 'pane:1', - layout: null - } - ], - leaves: [ - { - tabId: 'tab-1', - worktreeId: 'repo-1::/tmp/worktree-a', - leafId: 'pane:1', - paneRuntimeId: 1, - ptyId: 'pty-1' - } - ] - }) - runtime.onPtyData('pty-1', '\u001b[32mhello\u001b[0m\nworld\n', 123) - - const [terminal] = (await runtime.listTerminals()).terminals - const read = await runtime.readTerminal(terminal.handle) - expect(read).toMatchObject({ - handle: terminal.handle, - status: 'running', - tail: ['hello', 'world'], - truncated: false, - nextCursor: expect.any(String) - }) - - vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'], shouldClearNativeTimers: true }) - try { - const [send] = await Promise.all([ - runtime.sendTerminal( - terminal.handle, - { - text: 'continue', - enter: true - }, - { inputKind: 'driving' } - ), - vi.runAllTimersAsync() - ]) - expect(send).toMatchObject({ - handle: terminal.handle, - accepted: true - }) - expect(writes).toEqual(['continue', '\r']) - } finally { - vi.clearAllTimers() - vi.useRealTimers() - } - }) - - it('reports permission from blocked terminal wait text', async () => { - const runtime = new OrcaRuntimeService(store) - runtime.setPtyController({ - spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), - write: () => true, - kill: () => true, - getForegroundProcess: async () => null - }) - const leafId = '11111111-1111-4111-8111-111111111111' - runtime.attachWindow(1) - runtime.syncWindowGraph(1, { - tabs: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - title: 'repo terminal', - activeLeafId: leafId, - layout: null - } - ], - leaves: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - leafId, - paneRuntimeId: 1, - ptyId: 'pty-1' - } - ] - }) - runtime.onPtyData('pty-1', 'Hooks need review. Press enter to confirm\n', 123) - - const [terminal] = (await runtime.listTerminals()).terminals - - await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ - handle: terminal.handle, - isRunningAgent: true, - status: 'permission' - }) - }) - it('keeps blocked prompt text authoritative over an OpenCode marker', async () => { const runtime = new OrcaRuntimeService(store) runtime.setPtyController({ @@ -267,370 +157,4 @@ describe('OrcaRuntimeService', () => { blockedReason: 'agent-interactive-prompt' }) }) - - it('reports permission from blocked wait text over title-only working state', async () => { - const runtime = new OrcaRuntimeService(store) - runtime.setPtyController({ - spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), - write: () => true, - kill: () => true, - getForegroundProcess: async () => null - }) - const leafId = '11111111-1111-4111-8111-111111111111' - runtime.attachWindow(1) - runtime.syncWindowGraph(1, { - tabs: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - title: 'Codex working', - activeLeafId: leafId, - layout: null - } - ], - leaves: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - leafId, - paneRuntimeId: 1, - ptyId: 'pty-1' - } - ] - }) - runtime.onPtyData('pty-1', 'Hooks need review. Press enter to confirm\n', 123) - - const [terminal] = (await runtime.listTerminals()).terminals - - await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ - handle: terminal.handle, - isRunningAgent: true, - status: 'permission' - }) - }) - - it('lets a live non-permission title supersede stale blocked wait text', async () => { - const runtime = new OrcaRuntimeService(store) - runtime.setPtyController({ - spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), - write: () => true, - kill: () => true, - getForegroundProcess: async () => null - }) - const leafId = '11111111-1111-4111-8111-111111111111' - runtime.attachWindow(1) - runtime.syncWindowGraph(1, { - tabs: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - title: 'repo terminal', - activeLeafId: leafId, - layout: null - } - ], - leaves: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - leafId, - paneRuntimeId: 1, - ptyId: 'pty-1' - } - ] - }) - runtime.onPtyData('pty-1', 'Hooks need review. Press enter to confirm\n', 123) - runtime.onPtyData('pty-1', '\x1b]0;Codex working\x07', 124) - - const [terminal] = (await runtime.listTerminals()).terminals - - await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ - handle: terminal.handle, - isRunningAgent: true, - status: 'working' - }) - }) - - it('maps fresh explicit waiting hook state to permission over a working title', async () => { - const leafId = '11111111-1111-4111-8111-111111111111' - const paneKey = makePaneKey('tab-1', leafId) - const runtime = new OrcaRuntimeService(store, undefined, { - getAgentStatusSnapshot: () => [ - { - paneKey, - state: 'waiting', - prompt: '', - agentType: 'codex', - connectionId: null, - receivedAt: Date.now(), - stateStartedAt: Date.now(), - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID - } - ] - }) - runtime.setPtyController({ - spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), - write: () => true, - kill: () => true, - getForegroundProcess: async () => null - }) - runtime.attachWindow(1) - runtime.syncWindowGraph(1, { - tabs: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - title: 'Codex working', - activeLeafId: leafId, - layout: null - } - ], - leaves: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - leafId, - paneRuntimeId: 1, - ptyId: 'pty-1' - } - ] - }) - - const [terminal] = (await runtime.listTerminals()).terminals - - await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ - handle: terminal.handle, - isRunningAgent: true, - status: 'permission' - }) - }) - - // A restored permission wait is the exception; agent-prompt-restored-permission.test.ts covers it (#35962). - it('does not treat a restored-unconfirmed hook row as live terminal status', async () => { - const leafId = '11111111-1111-4111-8111-111111111111' - const paneKey = makePaneKey('tab-1', leafId) - const runtime = new OrcaRuntimeService(store, undefined, { - getAgentStatusSnapshot: () => [ - { - paneKey, - state: 'working', - prompt: '', - agentType: 'codex', - connectionId: null, - receivedAt: Date.now(), - stateStartedAt: Date.now(), - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - restoredUnconfirmed: true - } - ] - }) - runtime.setPtyController({ - spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), - write: () => true, - kill: () => true, - getForegroundProcess: async () => null - }) - runtime.attachWindow(1) - runtime.syncWindowGraph(1, { - tabs: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - title: 'repo terminal', - activeLeafId: leafId, - layout: null - } - ], - leaves: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - leafId, - paneRuntimeId: 1, - ptyId: 'pty-1' - } - ] - }) - - const [terminal] = (await runtime.listTerminals()).terminals - - await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ - handle: terminal.handle, - isRunningAgent: false, - status: null - }) - }) - - it('does not let stale wait text override a fresh explicit working state', async () => { - const leafId = '11111111-1111-4111-8111-111111111111' - const paneKey = makePaneKey('tab-1', leafId) - const runtime = new OrcaRuntimeService(store, undefined, { - getAgentStatusSnapshot: () => [ - { - paneKey, - state: 'working', - prompt: '', - agentType: 'codex', - connectionId: null, - receivedAt: Date.now(), - stateStartedAt: Date.now(), - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID - } - ] - }) - runtime.setPtyController({ - spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), - write: () => true, - kill: () => true, - getForegroundProcess: async () => null - }) - runtime.attachWindow(1) - runtime.syncWindowGraph(1, { - tabs: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - title: 'Codex working', - activeLeafId: leafId, - layout: null - } - ], - leaves: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - leafId, - paneRuntimeId: 1, - ptyId: 'pty-1' - } - ] - }) - runtime.onPtyData('pty-1', 'Hooks need review. Press enter to confirm\n', 123) - - const [terminal] = (await runtime.listTerminals()).terminals - - await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ - handle: terminal.handle, - isRunningAgent: true, - status: 'working' - }) - }) - - it('reports permission when blocked wait text is newer than explicit working state', async () => { - const leafId = '11111111-1111-4111-8111-111111111111' - const paneKey = makePaneKey('tab-1', leafId) - const now = Date.now() - const runtime = new OrcaRuntimeService(store, undefined, { - getAgentStatusSnapshot: () => [ - { - paneKey, - state: 'working', - prompt: '', - agentType: 'codex', - connectionId: null, - receivedAt: now - 1000, - stateStartedAt: now - 1000, - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID - } - ] - }) - runtime.setPtyController({ - spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), - write: () => true, - kill: () => true, - getForegroundProcess: async () => null - }) - runtime.attachWindow(1) - runtime.syncWindowGraph(1, { - tabs: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - title: 'Codex working', - activeLeafId: leafId, - layout: null - } - ], - leaves: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - leafId, - paneRuntimeId: 1, - ptyId: 'pty-1' - } - ] - }) - runtime.onPtyData('pty-1', 'Hooks need review. Press enter to confirm\n', now) - - const [terminal] = (await runtime.listTerminals()).terminals - - await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ - handle: terminal.handle, - isRunningAgent: true, - status: 'permission' - }) - }) - - it('timestamps blocked wait text when the prompt arrives across PTY chunks', async () => { - const leafId = '11111111-1111-4111-8111-111111111111' - const paneKey = makePaneKey('tab-1', leafId) - const now = Date.now() - const runtime = new OrcaRuntimeService(store, undefined, { - getAgentStatusSnapshot: () => [ - { - paneKey, - state: 'working', - prompt: '', - agentType: 'codex', - connectionId: null, - receivedAt: now, - stateStartedAt: now, - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID - } - ] - }) - runtime.setPtyController({ - spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), - write: () => true, - kill: () => true, - getForegroundProcess: async () => null - }) - runtime.attachWindow(1) - runtime.syncWindowGraph(1, { - tabs: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - title: 'Codex working', - activeLeafId: leafId, - layout: null - } - ], - leaves: [ - { - tabId: 'tab-1', - worktreeId: TEST_WORKTREE_ID, - leafId, - paneRuntimeId: 1, - ptyId: 'pty-1' - } - ] - }) - runtime.onPtyData('pty-1', 'Hooks need review. ', now + 1000) - runtime.onPtyData('pty-1', 'Press enter to confirm\n', now + 1001) - - const [terminal] = (await runtime.listTerminals()).terminals - - await expect(runtime.getTerminalAgentStatus(terminal.handle)).resolves.toEqual({ - handle: terminal.handle, - isRunningAgent: true, - status: 'permission' - }) - }) }) diff --git a/src/main/runtime/orca-runtime-tests/browser-capabilities-part-02.spec.ts b/src/main/runtime/orca-runtime-tests/browser-capabilities-part-02.spec.ts deleted file mode 100644 index e53ac280548..00000000000 --- a/src/main/runtime/orca-runtime-tests/browser-capabilities-part-02.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import './runtime-test-module-registration.spec' -import { OrcaRuntimeService } from '../orca-runtime' -import { - TEST_REPO_ID, - TEST_WORKTREE_ID, - TEST_WORKTREE_PATH, - store -} from './orca-runtime-test-fixtures.spec' - -describe('OrcaRuntimeService', () => { - it('rejects a qualified removal when only another host has persisted ownership', async () => { - const runtimeStore = { - ...store, - getWorktreeMeta: () => ({ ...store.getWorktreeMeta(TEST_WORKTREE_ID), hostId: 'local' }) - } - const runtime = new OrcaRuntimeService(runtimeStore as never) - const internals = runtime as unknown as { - listResolvedWorktrees: () => Promise - resolveWorktreeRemovalTarget: ( - worktreeSelector: string, - requiredHostId?: string - ) => Promise - } - internals.listResolvedWorktrees = vi.fn().mockResolvedValue([ - { - id: TEST_WORKTREE_ID, - repoId: TEST_REPO_ID, - path: TEST_WORKTREE_PATH, - hostId: 'local' - } - ]) - - await expect( - internals.resolveWorktreeRemovalTarget(TEST_WORKTREE_ID, 'runtime:env-b') - ).rejects.toThrow('selector_not_found') - expect(internals.listResolvedWorktrees).not.toHaveBeenCalled() - }) -}) diff --git a/src/main/runtime/orca-runtime-tests/browser-capabilities.spec.ts b/src/main/runtime/orca-runtime-tests/browser-capabilities.spec.ts index f33792b26ea..31dc7d65eb7 100644 --- a/src/main/runtime/orca-runtime-tests/browser-capabilities.spec.ts +++ b/src/main/runtime/orca-runtime-tests/browser-capabilities.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest' import './runtime-test-module-registration.spec' import { OrcaRuntimeService } from '../orca-runtime' -import { deleteWorktreeHistoryDirMock } from '../orca-runtime-test-mocks/setup.spec' +import '../orca-runtime-test-mocks/setup.spec' import { getBrowserHostLeaseRegistry } from '../browser-host/leases/browser-host-lease-registry-instance' import { getRuntimeBrowserPageRegistry } from '../browser/client-pages/runtime-browser-page-registry' import { @@ -14,7 +14,7 @@ import { attachClientBrowserHost, publishClientHostedPage } from './orca-runtime-test-scenario-builders.spec' -import { hostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity' +import '../../../shared/worktree/host-qualified-identity' describe('OrcaRuntimeService', () => { it('preserves a live client page across headed renderer graph updates and prunes it after retirement', async () => { @@ -164,71 +164,4 @@ describe('OrcaRuntimeService', () => { placement ) }) - - it('preserves bare-id runtime state when removing a different qualified owner', () => { - const removeWorktreeMeta = vi.fn() - const runtimeStore = { - ...store, - getWorktreeMeta: () => ({ ...store.getWorktreeMeta(TEST_WORKTREE_ID), hostId: 'local' }), - removeWorktreeMeta - } - const runtime = new OrcaRuntimeService(runtimeStore as never) - const internals = runtime as unknown as { - mobileSessionTabsByWorktree: Map - removeWorktreeMetadataAndHistory: ( - runtimeStore: typeof store, - worktreeId: string, - hostId: string - ) => void - } - const localSession = { tabs: [{ id: 'local-tab' }] } - internals.mobileSessionTabsByWorktree.set(TEST_WORKTREE_ID, localSession) - - internals.removeWorktreeMetadataAndHistory( - runtimeStore as typeof store, - TEST_WORKTREE_ID, - 'runtime:env-b' - ) - - expect(removeWorktreeMeta).toHaveBeenCalledWith( - hostWorktreeTarget({ id: TEST_WORKTREE_ID, hostId: 'runtime:env-b' }) - ) - expect(internals.mobileSessionTabsByWorktree.get(TEST_WORKTREE_ID)).toBe(localSession) - }) - - it('preserves bare-id runtime state when removed-host metadata masks another owner', () => { - const localRepo = store.getRepo(TEST_REPO_ID)! - const remoteRepo = { ...localRepo, connectionId: 'ssh-1' } - const removeWorktreeMeta = vi.fn() - const runtimeStore = { - ...store, - getRepos: () => [localRepo, remoteRepo], - getWorktreeMeta: () => ({ ...store.getWorktreeMeta(TEST_WORKTREE_ID), hostId: 'ssh:ssh-1' }), - removeWorktreeMeta - } - const runtime = new OrcaRuntimeService(runtimeStore as never) - const internals = runtime as unknown as { - mobileSessionTabsByWorktree: Map - removeWorktreeMetadataAndHistory: ( - runtimeStore: typeof store, - worktreeId: string, - hostId: string - ) => void - } - const survivingSession = { tabs: [{ id: 'same-id-local-tab' }] } - internals.mobileSessionTabsByWorktree.set(TEST_WORKTREE_ID, survivingSession) - deleteWorktreeHistoryDirMock.mockClear() - - internals.removeWorktreeMetadataAndHistory( - runtimeStore as typeof store, - TEST_WORKTREE_ID, - 'ssh:ssh-1' - ) - - expect(removeWorktreeMeta).toHaveBeenCalledWith( - hostWorktreeTarget({ id: TEST_WORKTREE_ID, hostId: 'ssh:ssh-1' }) - ) - expect(internals.mobileSessionTabsByWorktree.get(TEST_WORKTREE_ID)).toBe(survivingSession) - expect(deleteWorktreeHistoryDirMock).not.toHaveBeenCalled() - }) }) diff --git a/src/main/runtime/orca-runtime-tests/conpty-stream-read.spec.ts b/src/main/runtime/orca-runtime-tests/conpty-stream-read.spec.ts index e627e38f2e0..200623e0120 100644 --- a/src/main/runtime/orca-runtime-tests/conpty-stream-read.spec.ts +++ b/src/main/runtime/orca-runtime-tests/conpty-stream-read.spec.ts @@ -63,16 +63,4 @@ describe('OrcaRuntimeService ConPTY stream reads', () => { clearNativeWindowsConptyPty(ptyId) } }) - - it('keeps accumulated-stream reads for terminals that are not ConPTY', async () => { - const ptyId = 'pty-posix-prediction' - const runtime = startConptyRuntime(ptyId) - const [terminal] = (await runtime.listTerminals()).terminals - runtime.onPtyData(ptyId, 'one\r\ntwo\rTWO\r\n', 100) - - const read = await runtime.readTerminal(terminal.handle) - - expect(read.tail).toEqual(['one', 'TWO']) - expect(read.source).toBe('stream') - }) }) diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index 0ab3586f971..1d6cfe34cd5 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -17,7 +17,6 @@ await import('./orca-runtime-tests/dedupe-worktree-create.spec') await import('./orca-runtime-tests/paired-settings.spec') await import('./orca-runtime-tests/runtime-availability.spec') await import('./orca-runtime-tests/browser-capabilities.spec') -await import('./orca-runtime-tests/browser-capabilities-part-02.spec') await import('./orca-runtime-tests/managed-server-capability.spec') await import('./orca-runtime-tests/window-authority.spec') await import('./orca-runtime-tests/terminal-handles.spec') diff --git a/src/main/runtime/worktree-removal-agent-trust-cleanup.test.ts b/src/main/runtime/worktree-removal-agent-trust-cleanup.test.ts index 9e4e4732ee8..9a9883d30ab 100644 --- a/src/main/runtime/worktree-removal-agent-trust-cleanup.test.ts +++ b/src/main/runtime/worktree-removal-agent-trust-cleanup.test.ts @@ -49,12 +49,6 @@ describe('worktree removal drops the workspace trust Orca wrote', () => { } }) - it('removes the recorded entry before the purge resolves', async () => { - await purgeWithMeta()(WORKTREE_ID) - - expect(readFileSync(configPath, 'utf-8')).not.toContain(`[projects."${PROJECT}"]`) - }) - it('removes only the Antigravity trustedWorkspaces entry Orca appended', async () => { const agyHome = join(userDataDir, 'agy-home') const settingsPath = join(agyHome, '.gemini', 'antigravity-cli', 'settings.json') @@ -73,19 +67,6 @@ describe('worktree removal drops the workspace trust Orca wrote', () => { }) }) - it('removes a WSL guest entry recorded under the host workspace path', async () => { - const agyHome = join(userDataDir, 'agy-home') - const settingsPath = join(agyHome, '.gemini', 'antigravity-cli', 'settings.json') - markAntigravityWorkspaceTrusted(PROJECT, agyHome, '/home/u/feature-r1') - expect(JSON.parse(readFileSync(settingsPath, 'utf-8')).trustedWorkspaces).toEqual([ - '/home/u/feature-r1' - ]) - - await purgeWithMeta()(WORKTREE_ID) - - expect(JSON.parse(readFileSync(settingsPath, 'utf-8')).trustedWorkspaces).toEqual([]) - }) - it('keeps an Antigravity entry that was trusted before Orca saw the path', async () => { const agyHome = join(userDataDir, 'agy-home') const settingsPath = join(agyHome, '.gemini', 'antigravity-cli', 'settings.json') @@ -99,18 +80,4 @@ describe('worktree removal drops the workspace trust Orca wrote', () => { trustedWorkspaces: [PROJECT] }) }) - - it('keeps the folder trust when one folder-workspace session is removed', async () => { - const session = `${WORKTREE_ID}::workspace:5e8cc6b2-1d3f-4a5b-9c2d-7f6e5a4b3c2d` - await purgeWithMeta()(session, undefined, false) - await purgeWithMeta()(session) - - expect(readFileSync(configPath, 'utf-8')).toContain(`[projects."${PROJECT}"]`) - }) - - it('keeps the entry while another host still owns the same path-derived id', async () => { - await purgeWithMeta({ hostId: 'ssh:host-b' })(WORKTREE_ID, 'ssh:host-a') - - expect(readFileSync(configPath, 'utf-8')).toContain(`[projects."${PROJECT}"]`) - }) }) diff --git a/src/main/source-control/hosted-review-azure-devops.integration.test.ts b/src/main/source-control/hosted-review-azure-devops.integration.test.ts index 94ade46b143..75c34420603 100644 --- a/src/main/source-control/hosted-review-azure-devops.integration.test.ts +++ b/src/main/source-control/hosted-review-azure-devops.integration.test.ts @@ -149,117 +149,4 @@ describe('Azure DevOps hosted review integration', () => { } } ) - - it('prefers an active Azure Repos PR over a newer abandoned PR for the same branch', async () => { - const seen: SeenRequest[] = [] - const server = createServer((req: IncomingMessage, res: ServerResponse) => { - const url = new URL(req.url ?? '/', `http://${req.headers.host ?? '127.0.0.1'}`) - seen.push({ - pathname: url.pathname, - search: url.search, - authorization: req.headers.authorization - }) - - if (url.pathname === '/acme/Project/_apis/git/repositories/repo') { - sendJson(res, { - id: 'repo-guid', - webUrl: 'https://dev.azure.com/acme/Project/_git/repo' - }) - return - } - - if (url.pathname === '/acme/Project/_apis/git/repositories/repo-guid/pullRequests') { - sendJson(res, { - value: [ - { - pullRequestId: 40, - title: 'Abandoned branch', - status: 'abandoned', - creationDate: '2026-05-10T00:00:00Z', - closedDate: '2026-05-20T00:00:00Z', - mergeStatus: 'conflicts', - lastMergeSourceCommit: { commitId: 'old123' } - }, - { - pullRequestId: 41, - title: 'Active branch', - status: 'active', - creationDate: '2026-05-01T00:00:00Z', - mergeStatus: 'succeeded', - lastMergeSourceCommit: { commitId: 'active123' } - } - ] - }) - return - } - - if ( - url.pathname === '/acme/Project/_apis/git/repositories/repo-guid/pullRequests/40/statuses' - ) { - sendJson(res, { value: [{ state: 'failed' }] }) - return - } - - if ( - url.pathname === '/acme/Project/_apis/git/repositories/repo-guid/pullRequests/41/statuses' - ) { - sendJson(res, { value: [{ state: 'succeeded' }] }) - return - } - - res.writeHead(404, { 'Content-Type': 'application/json' }) - res.end(JSON.stringify({ message: 'not found' })) - }) - await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)) - - const repoPath = await mkdtemp(join(tmpdir(), 'orca-azure-review-active-')) - try { - const address = server.address() - if (!address || typeof address === 'string') { - throw new Error('expected TCP server address') - } - process.env.ORCA_AZURE_DEVOPS_API_BASE_URL = `http://127.0.0.1:${address.port}/acme/Project` - - await execFileAsync('git', ['init'], { cwd: repoPath }) - await execFileAsync( - 'git', - ['remote', 'add', 'origin', 'https://dev.azure.com/acme/Project/_git/repo'], - { cwd: repoPath } - ) - - await expect( - getHostedReviewForBranch({ - executionHostId: 'local', - repoPath, - branch: 'refs/heads/feature/azure' - }) - ).resolves.toMatchObject({ - provider: 'azure-devops', - number: 41, - title: 'Active branch', - state: 'open', - status: 'success', - checksSummary: { - state: 'success', - total: 1, - passed: 1, - failed: 0, - cancelled: 0, - pending: 0, - neutral: 0 - }, - mergeable: 'MERGEABLE', - headSha: 'active123' - }) - - expect(seen.map((request) => request.pathname)).toContain( - '/acme/Project/_apis/git/repositories/repo-guid/pullRequests/41/statuses' - ) - } finally { - await rm(repoPath, { recursive: true, force: true }) - await new Promise((resolve, reject) => { - server.close((error) => (error ? reject(error) : resolve())) - }) - } - }) }) diff --git a/src/main/window/main-window-webview-security.test.ts b/src/main/window/main-window-webview-security.test.ts deleted file mode 100644 index 1687622b8ad..00000000000 --- a/src/main/window/main-window-webview-security.test.ts +++ /dev/null @@ -1,451 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { ORCA_BROWSER_GUEST_WEB_PREFERENCES } from '../../shared/browser-guest-web-preferences' - -vi.mock('../host/native-chat-visual-guest-theme', () => ({ - installNativeChatVisualGuestTheme: vi.fn() -})) - -const mocks = vi.hoisted(() => ({ - attachGuestPolicies: vi.fn(), - installNavigationPolicy: vi.fn(), - isAllowedPartition: vi.fn(), - attachRouteGuest: vi.fn(), - installJavaScriptDialogs: vi.fn(), - registerPluginGuard: vi.fn(), - admitPluginFrame: vi.fn(), - installPluginFramePolicies: vi.fn() -})) - -vi.mock('../browser/browser-manager', () => ({ - browserManager: { attachGuestPolicies: mocks.attachGuestPolicies } -})) -vi.mock('../browser/sessions/browser-session-registry', () => ({ - browserSessionRegistry: { isAllowedPartition: mocks.isAllowedPartition } -})) -vi.mock('./host-frame-navigation-guard', () => ({ - registerHostFrameNavigationGuard: mocks.registerPluginGuard -})) -vi.mock('./privileged-window-navigation', () => ({ - installPrivilegedWindowNavigationPolicy: mocks.installNavigationPolicy -})) -vi.mock('../browser/routing/sessions/browser-route-session-runtime', () => ({ - browserRouteSessionRegistry: { isAllowedPartition: () => false }, - browserRouteWebContentsRegistry: { attachGuest: mocks.attachRouteGuest } -})) -vi.mock('../browser/routing/local-host-partitions/local-ssh-browser-partitions', () => ({ - isLocalSshBrowserPartition: () => false, - enforceLocalSshWebRtcPolicyForGuest: vi.fn() -})) -vi.mock('../browser/extensions/profile-sessions/browser-extension-session-loader', () => ({ - getBrowserExtensionSessionForProfile: () => null -})) -vi.mock('../browser/guest-lifecycle/prompts/browser-javascript-dialogs', () => ({ - installBrowserJavaScriptDialogs: mocks.installJavaScriptDialogs -})) -vi.mock(import('../plugins/panels/plugin-frame-guest-registry'), async (importOriginal) => ({ - PLUGIN_FRAME_GUEST_RENDERER_ARGUMENTS: (await importOriginal()) - .PLUGIN_FRAME_GUEST_RENDERER_ARGUMENTS, - admitPluginFrameAttach: mocks.admitPluginFrame, - isPluginFramePartition: (partition: string) => partition.startsWith('persist:orca-plugin-'), - attachPluginFrameGuest: (guest: { session?: unknown }, host: unknown) => { - if (guest.session !== 'plugin-session') { - return false - } - mocks.installPluginFramePolicies(guest, host) - return true - } -})) -vi.mock('../browser-passwords/browser-password-guest-service', () => ({ - attachBrowserPasswordGuest: vi.fn() -})) -vi.mock('../browser/document-preview/doc-preview-protocol', () => ({ - isDocPreviewSession: (candidate: unknown) => candidate === 'doc-preview-session' -})) - -import { installMainWindowWebviewSecurity } from './main-window-webview-security' -import { - getDocPreviewGrant, - mintDocPreviewGrant, - revokeAllDocPreviewGrants -} from '../browser/document-preview/doc-preview-grant-registry' -import { - publishDocPreviewFailure, - setDocPreviewFailureSink -} from '../browser/document-preview/doc-preview-failure-notice' -import { buildDocPreviewUrl, DOC_PREVIEW_PARTITION } from '../../shared/doc-preview-scheme' - -function installOnFakeWindow(): { - handlers: Record void> - webContents: { on: ReturnType; setWebRTCIPHandlingPolicy: ReturnType } -} { - const handlers: Record void> = {} - const webContents = { - on: vi.fn((event: string, handler: (...args: never[]) => void) => { - handlers[event] = handler - }), - setWebRTCIPHandlingPolicy: vi.fn() - } - installMainWindowWebviewSecurity({ webContents } as never) - return { handlers, webContents } -} - -function mintPreviewGrant(): ReturnType { - return mintDocPreviewGrant({ - owner: { kind: 'ssh', connectionId: 'ssh-1' }, - root: '/home/alice/docs', - entryRelativePath: 'index.html', - browserPageId: 'page-1' - }) -} - -describe('main window webview security', () => { - beforeEach(() => { - vi.clearAllMocks() - revokeAllDocPreviewGrants() - }) - - it('fails closed before applying hardened guest preferences', () => { - const { handlers, webContents } = installOnFakeWindow() - mocks.isAllowedPartition.mockReturnValue(false) - const preventDefault = vi.fn() - - handlers['will-attach-webview']?.( - { preventDefault } as never, - { partition: 'persist:untrusted', preload: 'attacker.js' } as never, - { src: 'https://example.com', preload: 'attacker.js' } as never - ) - - expect(preventDefault).toHaveBeenCalledOnce() - expect(mocks.installNavigationPolicy).toHaveBeenCalledWith(webContents) - expect(mocks.registerPluginGuard).toHaveBeenCalledWith(webContents) - expect(webContents.setWebRTCIPHandlingPolicy).toHaveBeenCalledWith('disable_non_proxied_udp') - }) - - it('removes renderer preload input and restores every hardened preference', () => { - const { handlers } = installOnFakeWindow() - mocks.isAllowedPartition.mockReturnValue(true) - const params = { src: 'https://example.com', preload: 'attacker.js' } - const preferences: Record = { - partition: 'persist:orca-browser', - preload: 'attacker.js', - preloadURL: 'attacker.js', - sandbox: false - } - - handlers['will-attach-webview']?.( - { preventDefault: vi.fn() } as never, - preferences as never, - params as never - ) - - expect(params).not.toHaveProperty('preload') - expect(preferences).toMatchObject({ - ...ORCA_BROWSER_GUEST_WEB_PREFERENCES, - partition: 'persist:orca-browser', - contextIsolation: true, - nodeIntegration: false, - nodeIntegrationInSubFrames: true, - sandbox: true, - webSecurity: true - }) - expect(preferences).not.toHaveProperty('preloadURL') - expect(String(preferences.preload)).toMatch(/browser-guest-preload\.js$/) - }) - - it('runs preloads in subframes of every local profile tab, so extensions switched on later reach it', () => { - const { handlers } = installOnFakeWindow() - mocks.isAllowedPartition.mockReturnValue(true) - const preferences: Record = { - partition: 'persist:orca-browser-work', - nodeIntegrationInSubFrames: false - } - handlers['will-attach-webview']?.( - { preventDefault: vi.fn() } as never, - preferences as never, - { src: 'https://example.com' } as never - ) - - expect(preferences.nodeIntegrationInSubFrames).toBe(true) - }) -}) - -describe('orca-preview scheme admission', () => { - beforeEach(() => { - vi.clearAllMocks() - revokeAllDocPreviewGrants() - }) - - it('admits a preview URL only on the preview partition and only with a live grant', () => { - // Install first, as the window does: installation itself clears the registry. - const { handlers } = installOnFakeWindow() - const grant = mintPreviewGrant() - mocks.isAllowedPartition.mockReturnValue(false) - const preventDefault = vi.fn() - const preferences: Record = { - partition: DOC_PREVIEW_PARTITION, - preload: 'attacker.js', - sandbox: false - } - - handlers['will-attach-webview']?.( - { preventDefault } as never, - preferences as never, - { src: buildDocPreviewUrl(grant.id, 'index.html'), preload: 'attacker.js' } as never - ) - - expect(preventDefault).not.toHaveBeenCalled() - expect(preferences).toMatchObject({ - partition: DOC_PREVIEW_PARTITION, - contextIsolation: true, - nodeIntegration: false, - sandbox: true, - webSecurity: true - }) - expect(preferences).not.toHaveProperty('preloadURL') - }) - - // Only preview guests get the pinned link preload; browsing guests must never acquire it and renderer values must never replace it. - it('pins the preview preload onto a preview attach, replacing whatever the renderer asked for', () => { - const { handlers } = installOnFakeWindow() - const grant = mintPreviewGrant() - mocks.isAllowedPartition.mockReturnValue(false) - const params = { src: buildDocPreviewUrl(grant.id, 'index.html'), preload: 'attacker.js' } - const preferences: Record = { - partition: DOC_PREVIEW_PARTITION, - preload: 'attacker.js' - } - - handlers['will-attach-webview']?.( - { preventDefault: vi.fn() } as never, - preferences as never, - params as never - ) - - expect(params).not.toHaveProperty('preload') - expect(String(preferences.preload)).toMatch(/doc-preview-link-preload\.js$/) - }) - - it('keeps the preview preload off a browsing attach', () => { - const { handlers } = installOnFakeWindow() - mocks.isAllowedPartition.mockReturnValue(true) - const preferences: Record = { partition: 'persist:orca-browser' } - - handlers['will-attach-webview']?.( - { preventDefault: vi.fn() } as never, - preferences as never, - { src: 'https://example.com' } as never - ) - - expect(String(preferences.preload)).toMatch(/browser-guest-preload\.js$/) - }) - - it('denies a preview URL whose grant is unknown or revoked', () => { - const { handlers } = installOnFakeWindow() - mocks.isAllowedPartition.mockReturnValue(false) - const preventDefault = vi.fn() - - handlers['will-attach-webview']?.( - { preventDefault } as never, - { partition: DOC_PREVIEW_PARTITION } as never, - { src: `orca-preview://${'0'.repeat(32)}/index.html` } as never - ) - - expect(preventDefault).toHaveBeenCalledOnce() - }) - - it('denies a preview URL smuggled onto a browsing partition', () => { - const { handlers } = installOnFakeWindow() - const grant = mintPreviewGrant() - // Even an allowlisted browsing partition must not load the preview scheme. - mocks.isAllowedPartition.mockReturnValue(true) - const preventDefault = vi.fn() - - handlers['will-attach-webview']?.( - { preventDefault } as never, - { partition: 'persist:orca-browser' } as never, - { src: buildDocPreviewUrl(grant.id, 'index.html') } as never - ) - - expect(preventDefault).toHaveBeenCalledOnce() - }) - - it('denies a web URL on the preview partition', () => { - const { handlers } = installOnFakeWindow() - mocks.isAllowedPartition.mockReturnValue(false) - const preventDefault = vi.fn() - - handlers['will-attach-webview']?.( - { preventDefault } as never, - { partition: DOC_PREVIEW_PARTITION } as never, - { src: 'https://example.com' } as never - ) - - expect(preventDefault).toHaveBeenCalledOnce() - }) - - // The granting renderer is the only valid click sink; verify this window hosts the preview, not merely its profile. - it('attaches a preview guest under the workspace-doc profile, hosted by this window', () => { - const { handlers, webContents } = installOnFakeWindow() - - handlers['did-attach-webview']?.({} as never, { session: 'doc-preview-session' } as never) - - expect(mocks.attachGuestPolicies).toHaveBeenCalledWith( - { session: 'doc-preview-session' }, - null, - { - profile: 'workspace-doc', - host: webContents - } - ) - expect(mocks.attachRouteGuest).not.toHaveBeenCalled() - expect(mocks.installJavaScriptDialogs).not.toHaveBeenCalled() - }) - - it('keeps browser guests on the browsing profile and its route registration', () => { - const { handlers } = installOnFakeWindow() - - handlers['did-attach-webview']?.({} as never, { session: 'browser-session' } as never) - - expect(mocks.attachGuestPolicies).toHaveBeenCalledOnce() - expect(mocks.attachGuestPolicies.mock.calls[0]?.[2]).toBeUndefined() - expect(mocks.attachRouteGuest).toHaveBeenCalledOnce() - expect(mocks.installJavaScriptDialogs).toHaveBeenCalledOnce() - }) - - // WebContents teardown leaves no preview readers; clear grants and the dead failure sink. - it('drops the failure sink and every grant when the window contents are destroyed', () => { - const { handlers } = installOnFakeWindow() - const grant = mintPreviewGrant() - const send = vi.fn() - setDocPreviewFailureSink({ send }) - expect(getDocPreviewGrant(grant.id)).not.toBeNull() - - handlers['destroyed']?.() - publishDocPreviewFailure({ - grantId: grant.id, - relativePath: 'index.html', - reason: 'unreadable' - }) - - expect(getDocPreviewGrant(grant.id)).toBeNull() - expect(send).not.toHaveBeenCalled() - }) - - // A renderer owns grant-to-preview bookkeeping; replacing it must revoke otherwise unreleasable read authority. - it('clears grants a previous renderer left behind when the window is created', () => { - const stranded = mintPreviewGrant() - - installOnFakeWindow() - - expect(getDocPreviewGrant(stranded.id)).toBeNull() - }) - - it('clears grants the renderer forgot across a reload', () => { - const { handlers } = installOnFakeWindow() - const grant = mintPreviewGrant() - - handlers['did-start-navigation']?.({ isMainFrame: true, isSameDocument: false } as never) - - expect(getDocPreviewGrant(grant.id)).toBeNull() - }) - - it('keeps grants across an in-document or subframe navigation, which keeps the renderer', () => { - const { handlers } = installOnFakeWindow() - const grant = mintPreviewGrant() - - handlers['did-start-navigation']?.({ isMainFrame: true, isSameDocument: true } as never) - handlers['did-start-navigation']?.({ isMainFrame: false, isSameDocument: false } as never) - - expect(getDocPreviewGrant(grant.id)).not.toBeNull() - }) -}) - -describe('plugin frame guests', () => { - const partition = 'persist:orca-plugin-0123456789abcdef' - const src = 'orca-plugin-frame://0123456789abcdef/panel.html' - - beforeEach(() => vi.clearAllMocks()) - - it('refuses a plugin partition main did not prepare for that origin', () => { - const { handlers } = installOnFakeWindow() - mocks.admitPluginFrame.mockReturnValue(false) - const preventDefault = vi.fn() - const preferences: Record = { partition, preload: 'attacker.js' } - - handlers['will-attach-webview']?.( - { preventDefault } as never, - preferences as never, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the handler reads only params.src. - { src } as never - ) - - expect(mocks.admitPluginFrame).toHaveBeenCalledWith(partition, src) - expect(preventDefault).toHaveBeenCalledOnce() - expect(preferences.preload).toBe('attacker.js') - }) - - it('pins the guest preload and every secure preference, whatever the markup asked', () => { - const { handlers } = installOnFakeWindow() - mocks.admitPluginFrame.mockReturnValue(true) - const params: Record = { src, preload: 'attacker.js', allowpopups: 'true' } - const preferences: Record = { - partition, - preload: 'attacker.js', - preloadURL: 'attacker.js', - additionalArguments: ['--x'], - sandbox: false, - contextIsolation: false, - nodeIntegration: true, - webSecurity: false, - webviewTag: true, - disablePopups: false, - experimentalFeatures: true, - enableWebSQL: true, - zoomFactor: 1.25 - } - - handlers['will-attach-webview']?.( - { preventDefault: vi.fn() } as never, - preferences as never, - params as never - ) - - expect(params).not.toHaveProperty('preload') - expect(params).not.toHaveProperty('allowpopups') - expect(preferences).not.toHaveProperty('preloadURL') - // Anything main did not pin is dropped; zoom still follows the host window. - expect(preferences).not.toHaveProperty('experimentalFeatures') - expect(preferences).not.toHaveProperty('enableWebSQL') - expect(preferences.zoomFactor).toBe(1.25) - // Markup's arguments are replaced by main's renderer pins, never merged. - expect(preferences.additionalArguments).toEqual([ - '--blink-settings=dnsPrefetchingEnabled=false' - ]) - expect(String(preferences.preload)).toMatch(/plugin-frame-guest-preload\.js$/) - expect(preferences).toMatchObject({ - partition, - sandbox: true, - contextIsolation: true, - nodeIntegration: false, - nodeIntegrationInSubFrames: false, - webSecurity: true, - allowRunningInsecureContent: false, - webviewTag: false, - disablePopups: true, - disableDialogs: true - }) - expect(mocks.isAllowedPartition).not.toHaveBeenCalled() - }) - - it('gives attached plugin guests their own policy, never the browser guest one', () => { - const { handlers } = installOnFakeWindow() - const guest = { session: 'plugin-session' } - - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the handler reads only guest.session. - handlers['did-attach-webview']?.({} as never, guest as never) - - expect(mocks.installPluginFramePolicies).toHaveBeenCalledWith(guest, expect.anything()) - expect(mocks.attachGuestPolicies).not.toHaveBeenCalled() - expect(mocks.installJavaScriptDialogs).not.toHaveBeenCalled() - }) -})