From e1d4314a461f7c7cc58b197b217b65e4bd96f034 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:47:00 -0800 Subject: [PATCH] refactor(window): organize clipboard, lifecycle, security, and relay owners (#41634) * refactor(window): group clipboard, lifecycle, security, and relay owners * refactor(window): keep Electron paste handling with desktop clipboard --- .github/workflows/macos-updater-tests.yml | 4 +- config/flaky-test-pattern-baseline.txt | 6 +- config/reliability-gates.jsonc | 19 +- .../vitest-node-runtime-files.mjs | 4 +- .../source-routing/pr-e2e-source-routing.mjs | 2 +- .../pr-plugin-e2e-source-routes.mjs | 2 +- config/tsconfig.tc.web.json | 6 +- .../backend-desktop-capabilities.ts | 2 +- .../backend-supervisor.test.ts | 2 +- ...wser-extension-toolbar-electron-fixture.ts | 2 +- .../browser-extension-electron-fixture.ts | 2 +- .../extensions/api/notifications.test.ts | 2 +- .../browser/extensions/api/notifications.ts | 2 +- ...nsion-native-messaging-electron-fixture.ts | 2 +- ...er-extension-lifecycle-electron-fixture.ts | 2 +- ...-extension-persistence-electron-fixture.ts | 2 +- .../browser-extension-window-model.test.ts | 2 +- .../browser-extension-window-model.ts | 2 +- ...r-find-frame-traversal-electron-fixture.ts | 2 +- .../input/browser-guest-context-menu.ts | 2 +- .../browser-manager-guest-visibility.test.ts | 90 +--- .../manager/browser-manager-visibility.ts | 2 +- ...r-link-materialization-electron-fixture.ts | 2 +- .../page-activity/browser-agent-activity.ts | 2 +- ...er-popup-compatibility-electron-fixture.ts | 2 +- .../browser-popup-native-window.ts | 2 +- .../presentation/popup-origin-bar-window.ts | 2 +- .../popup-origin-chrome-electron-fixture.ts | 2 +- .../owned-temp-staging-root.ts | 2 +- .../host/electron-runtime-desktop-surface.ts | 2 +- src/main/index.ts | 12 +- src/main/ipc/app.ts | 2 +- src/main/ipc/dashboard-popout.ts | 4 +- src/main/ipc/memory.ts | 2 +- src/main/ipc/native-notification-delivery.ts | 4 +- src/main/ipc/notifications.ts | 2 +- src/main/ipc/pty/ipc/leaf-move.ts | 2 +- src/main/ipc/pty/ipc/session-listing.ts | 2 +- .../register-core-handlers.ts | 8 +- .../workspace-space/workspace-space-window.ts | 4 +- .../worktree-base-directory-poller.ts | 2 +- src/main/ipc/wsl/wsl-move-driver-wiring.ts | 2 +- .../agent-session-context-snapshots.ts | 2 +- .../native-chat-paste-files.test.ts | 8 +- .../paste-files}/native-chat-paste-files.ts | 4 +- src/main/orcad/electron-sidecar-profile.ts | 2 +- .../plugin-frame-guest-policies.ts | 2 +- .../history}/history-gc-worktree-ids.test.ts | 25 -- .../history}/history-gc-worktree-ids.ts | 6 +- ...-page-browser-commands-host-test-double.ts | 2 +- .../browser/screencast/browser-screencasts.ts | 2 +- .../rpc/methods/clipboard/clipboard.test.ts | 2 +- .../rpc/methods/clipboard/clipboard.ts | 2 +- .../rpc/server/runtime-rpc-startup-failure.ts | 2 +- .../close/close-mobile-session-tab.ts | 2 +- .../mobile-terminal-chat-fork-create.ts | 2 +- .../launch/mobile-terminal-renderer-create.ts | 2 +- .../startup/main-process-preflight-failure.ts | 2 +- .../startup/main-process-ready-foundation.ts | 2 +- .../main-window/main-process-i18n-menu.ts | 2 +- ...actions-quit-and-stop-all-sessions.test.ts | 12 +- .../main-window/main-window-actions.ts | 13 +- .../main-window/main-window-controller.ts | 8 +- .../profile-state-recovery-preflight.test.ts | 2 +- .../profile-state-recovery-preflight.ts | 2 +- .../profile-state-write-failure.ts | 4 +- .../main-process-runtime-launch.ts | 4 +- ...main-window-updater-deferred-setup.test.ts | 12 +- .../lifecycle}/main-window-updater.ts | 30 +- .../updater-package-recovery-ipc.test.ts | 37 +- .../{ => activation}/focus-existing-window.ts | 0 .../foreground-activation-policy.test.ts | 38 +- .../foreground-activation-policy.ts | 0 .../{ => activation}/macos-app-activation.ts | 0 .../{ => activation}/macos-desktop-handoff.ts | 0 .../window/attach-main-window-services.ts | 10 +- ...ard-agent-session-attachment-paste.test.ts | 14 +- .../clipboard-copied-file-paths.test.ts | 0 .../clipboard-copied-file-paths.ts | 0 .../clipboard-dashboard-popout-access.test.ts | 8 +- .../{ => clipboard}/clipboard-file-copy.ts | 2 +- .../clipboard-image-png-encode.ts | 2 +- .../{ => clipboard}/clipboard-image-source.ts | 2 +- .../clipboard-image-temp-file.ts | 12 +- .../clipboard-image-thumbnail.ts | 2 +- .../{ => clipboard}/clipboard-ipc-handlers.ts | 26 +- .../clipboard-ipc-sender-trust.ts | 4 +- .../clipboard-macos-image-paste.test.ts | 18 +- .../{ => clipboard}/clipboard-native-image.ts | 4 +- .../clipboard-plain-text-read.test.ts | 0 .../clipboard-plain-text-read.ts | 0 .../clipboard-publisher-executable.test.ts | 0 .../clipboard-publisher-executable.ts | 0 .../clipboard-remote-file-copy.ts | 0 .../clipboard-remote-file-staging-fs.test.ts | 0 .../clipboard-remote-file-staging.test.ts | 0 .../clipboard-remote-file-staging.ts | 4 +- .../clipboard-runtime-image-upload.ts | 4 +- .../clipboard-text-publisher.test.ts | 105 +---- .../clipboard-text-publisher.ts | 10 +- .../clipboard-text-write-verify.ts | 0 .../clipboard-windows-image-file.test.ts | 2 +- .../clipboard-windows-image-file.ts | 2 +- .../native-chat-pasted-text-file.test.ts | 6 +- .../native-chat-pasted-text-file.ts | 8 +- ...reateMainWindow-close-confirmation.test.ts | 365 ----------------- ...MainWindow-low-commit-oom-recovery.test.ts | 183 --------- ...teMainWindow-markdown-editor-focus.test.ts | 385 ------------------ ...MainWindow-renderer-crash-recovery.test.ts | 383 ----------------- .../window/createMainWindow-test-harness.ts | 193 --------- .../createMainWindow-user-quit-close.test.ts | 133 ------ src/main/window/createMainWindow.ts | 34 +- src/main/window/dashboard-popout-window.ts | 23 +- .../macos-tahoe-release.ts | 0 .../editable-context-menu-labels.ts | 2 +- .../editable-context-menu-popup.ts | 0 .../{ => editing}/editable-context-menu.ts | 6 +- .../{ => editing}/mac-app-paste-input.test.ts | 0 .../{ => editing}/mac-app-paste-input.ts | 5 +- .../checked-drag-temp-source.ts | 8 +- .../darwin-user-temp-dir.test.ts | 0 .../{ => file-drop}/darwin-user-temp-dir.ts | 0 .../window/{ => file-drop}/drag-temp-path.ts | 0 .../dragged-temp-file-copy.test.ts | 4 +- .../{ => file-drop}/dragged-temp-file-copy.ts | 6 +- .../dropped-path-preparation-ipc.test.ts | 0 .../dropped-path-preparation-ipc.ts | 6 +- .../dropped-path-preparation.test.ts | 60 +-- .../dropped-path-preparation.ts | 8 +- src/main/window/macos-app-activation.test.ts | 38 -- src/main/window/macos-desktop-handoff.test.ts | 150 ------- .../main-window-state-lifecycle.test.ts | 150 ------- .../{ => main-window}/main-window-activity.ts | 0 .../main-window-close-lifecycle.ts | 10 +- .../main-window-contracts.ts | 8 +- .../main-window-focus-lifecycle.ts | 24 +- .../main-window-shortcut-actions.ts | 2 +- .../main-window-shortcut-routing.ts | 18 +- .../main-window-state-lifecycle.test.ts | 68 ++++ .../main-window-state-lifecycle.ts | 6 +- .../main-window-visual-lifecycle.ts | 2 +- .../user-quit-window-close.ts | 0 .../window-close-decision.ts | 0 .../window-bounds-validation.ts | 0 .../window-material-options.ts | 2 +- .../window-material-suppression.ts | 2 +- .../privileged-window-navigation.test.ts | 94 ----- .../renderer-document-navigation.test.ts | 131 ------ .../renderer-recovery-reload-watchdog.test.ts | 136 ------- .../main-window-load-error-code.ts | 0 .../renderer-launch-failure-backoff.ts | 0 .../renderer-launch-failure-probe.test.ts | 45 +- .../renderer-launch-failure-probe.ts | 4 +- .../renderer-liveness-at-death.test.ts | 16 +- .../renderer-liveness-at-death.ts | 4 +- .../renderer-recovery-prompt.ts | 4 +- .../renderer-recovery-reload-watchdog.ts | 9 +- .../agent-launch-tab-publish-relay.ts | 2 +- .../agent-pane-activity-relay.ts | 2 +- .../project-move-pause.test.ts | 76 +--- .../project-move-pause.ts | 6 +- .../renderer-reply-request.ts | 4 +- ...ntime-renderer-notification-sender.test.ts | 16 - .../runtime-renderer-notification-sender.ts | 0 .../session-tab-close-request-relay.test.ts | 60 --- .../session-tab-close-request-relay.ts | 6 +- .../terminal-pane-move-commit-fence.ts | 2 +- .../terminal-pane-move-request-relay.test.ts | 2 +- .../terminal-pane-move-request-relay.ts | 6 +- .../terminal-pane-reveal-relay.ts | 4 +- .../terminal-tab-close-request-relay.test.ts | 0 .../terminal-tab-close-request-relay.ts | 4 +- ...e-window-lifecycle-terminal-reveal.test.ts | 4 +- src/main/window/runtime-window-lifecycle.ts | 18 +- .../host-frame-navigation-guard.test.ts | 26 +- .../host-frame-navigation-guard.ts | 4 +- .../host-frame-permission-policy.test.ts | 0 .../host-frame-permission-policy.ts | 0 .../main-window-webview-security.ts | 36 +- .../privileged-window-navigation.ts | 2 +- .../renderer-document-navigation.ts | 0 ...r-publication-throttle-electron-fixture.ts | 4 +- .../hidden-window-frame-pump.ts | 2 +- .../main-window-visibility.ts | 0 ...erer-publication-throttle.electron.test.ts | 7 +- .../renderer-publication-throttle.ts | 0 .../resource-view-admission.ts | 4 +- .../wait-for-window-to-show.ts | 0 src/main/window/workspace-space-window.ts | 7 +- .../benchmarks/spinner-rendering/main.ts | 2 +- 190 files changed, 469 insertions(+), 3265 deletions(-) rename src/main/{window => filesystem}/owned-temp-staging-root.ts (97%) rename src/main/{window => native-chat/paste-files}/native-chat-paste-files.test.ts (94%) rename src/main/{window => native-chat/paste-files}/native-chat-paste-files.ts (95%) rename src/main/{window => pty/history}/history-gc-worktree-ids.test.ts (58%) rename src/main/{window => pty/history}/history-gc-worktree-ids.ts (78%) rename src/main/{window => updater/lifecycle}/main-window-updater-deferred-setup.test.ts (93%) rename src/main/{window => updater/lifecycle}/main-window-updater.ts (88%) rename src/main/{window => updater/lifecycle}/updater-package-recovery-ipc.test.ts (81%) rename src/main/window/{ => activation}/focus-existing-window.ts (100%) rename src/main/window/{ => activation}/foreground-activation-policy.test.ts (55%) rename src/main/window/{ => activation}/foreground-activation-policy.ts (100%) rename src/main/window/{ => activation}/macos-app-activation.ts (100%) rename src/main/window/{ => activation}/macos-desktop-handoff.ts (100%) rename src/main/window/{ => clipboard}/clipboard-agent-session-attachment-paste.test.ts (87%) rename src/main/window/{ => clipboard}/clipboard-copied-file-paths.test.ts (100%) rename src/main/window/{ => clipboard}/clipboard-copied-file-paths.ts (100%) rename src/main/window/{ => clipboard}/clipboard-dashboard-popout-access.test.ts (94%) rename src/main/window/{ => clipboard}/clipboard-file-copy.ts (98%) rename src/main/window/{ => clipboard}/clipboard-image-png-encode.ts (84%) rename src/main/window/{ => clipboard}/clipboard-image-source.ts (94%) rename src/main/window/{ => clipboard}/clipboard-image-temp-file.ts (84%) rename src/main/window/{ => clipboard}/clipboard-image-thumbnail.ts (96%) rename src/main/window/{ => clipboard}/clipboard-ipc-handlers.ts (91%) rename src/main/window/{ => clipboard}/clipboard-ipc-sender-trust.ts (86%) rename src/main/window/{ => clipboard}/clipboard-macos-image-paste.test.ts (86%) rename src/main/window/{ => clipboard}/clipboard-native-image.ts (84%) rename src/main/window/{ => clipboard}/clipboard-plain-text-read.test.ts (100%) rename src/main/window/{ => clipboard}/clipboard-plain-text-read.ts (100%) rename src/main/window/{ => clipboard}/clipboard-publisher-executable.test.ts (100%) rename src/main/window/{ => clipboard}/clipboard-publisher-executable.ts (100%) rename src/main/window/{ => clipboard}/clipboard-remote-file-copy.ts (100%) rename src/main/window/{ => clipboard}/clipboard-remote-file-staging-fs.test.ts (100%) rename src/main/window/{ => clipboard}/clipboard-remote-file-staging.test.ts (100%) rename src/main/window/{ => clipboard}/clipboard-remote-file-staging.ts (97%) rename src/main/window/{ => clipboard}/clipboard-runtime-image-upload.ts (96%) rename src/main/window/{ => clipboard}/clipboard-text-publisher.test.ts (56%) rename src/main/window/{ => clipboard}/clipboard-text-publisher.ts (93%) rename src/main/window/{ => clipboard}/clipboard-text-write-verify.ts (100%) rename src/main/window/{ => clipboard}/clipboard-windows-image-file.test.ts (97%) rename src/main/window/{ => clipboard}/clipboard-windows-image-file.ts (99%) rename src/main/window/{ => clipboard}/native-chat-pasted-text-file.test.ts (79%) rename src/main/window/{ => clipboard}/native-chat-pasted-text-file.ts (79%) delete mode 100644 src/main/window/createMainWindow-close-confirmation.test.ts delete mode 100644 src/main/window/createMainWindow-low-commit-oom-recovery.test.ts delete mode 100644 src/main/window/createMainWindow-markdown-editor-focus.test.ts delete mode 100644 src/main/window/createMainWindow-renderer-crash-recovery.test.ts delete mode 100644 src/main/window/createMainWindow-test-harness.ts delete mode 100644 src/main/window/createMainWindow-user-quit-close.test.ts rename src/main/window/{ => desktop-integration}/macos-tahoe-release.ts (100%) rename src/main/window/{ => editing}/editable-context-menu-labels.ts (97%) rename src/main/window/{ => editing}/editable-context-menu-popup.ts (100%) rename src/main/window/{ => editing}/editable-context-menu.ts (97%) rename src/main/window/{ => editing}/mac-app-paste-input.test.ts (100%) rename src/main/window/{ => editing}/mac-app-paste-input.ts (79%) rename src/main/window/{ => file-drop}/checked-drag-temp-source.ts (94%) rename src/main/window/{ => file-drop}/darwin-user-temp-dir.test.ts (100%) rename src/main/window/{ => file-drop}/darwin-user-temp-dir.ts (100%) rename src/main/window/{ => file-drop}/drag-temp-path.ts (100%) rename src/main/window/{ => file-drop}/dragged-temp-file-copy.test.ts (98%) rename src/main/window/{ => file-drop}/dragged-temp-file-copy.ts (97%) rename src/main/window/{ => file-drop}/dropped-path-preparation-ipc.test.ts (100%) rename src/main/window/{ => file-drop}/dropped-path-preparation-ipc.ts (93%) rename src/main/window/{ => file-drop}/dropped-path-preparation.test.ts (85%) rename src/main/window/{ => file-drop}/dropped-path-preparation.ts (95%) delete mode 100644 src/main/window/macos-app-activation.test.ts delete mode 100644 src/main/window/macos-desktop-handoff.test.ts delete mode 100644 src/main/window/main-window-state-lifecycle.test.ts rename src/main/window/{ => main-window}/main-window-activity.ts (100%) rename src/main/window/{ => main-window}/main-window-close-lifecycle.ts (96%) rename src/main/window/{ => main-window}/main-window-contracts.ts (89%) rename src/main/window/{ => main-window}/main-window-focus-lifecycle.ts (92%) rename src/main/window/{ => main-window}/main-window-shortcut-actions.ts (96%) rename src/main/window/{ => main-window}/main-window-shortcut-routing.ts (94%) create mode 100644 src/main/window/main-window/main-window-state-lifecycle.test.ts rename src/main/window/{ => main-window}/main-window-state-lifecycle.ts (96%) rename src/main/window/{ => main-window}/main-window-visual-lifecycle.ts (97%) rename src/main/window/{ => main-window}/user-quit-window-close.ts (100%) rename src/main/window/{ => main-window}/window-close-decision.ts (100%) rename src/main/window/{ => presentation}/window-bounds-validation.ts (100%) rename src/main/window/{ => presentation}/window-material-options.ts (95%) rename src/main/window/{ => presentation}/window-material-suppression.ts (91%) delete mode 100644 src/main/window/privileged-window-navigation.test.ts delete mode 100644 src/main/window/renderer-document-navigation.test.ts delete mode 100644 src/main/window/renderer-recovery-reload-watchdog.test.ts rename src/main/window/{ => renderer-recovery}/main-window-load-error-code.ts (100%) rename src/main/window/{ => renderer-recovery}/renderer-launch-failure-backoff.ts (100%) rename src/main/window/{ => renderer-recovery}/renderer-launch-failure-probe.test.ts (56%) rename src/main/window/{ => renderer-recovery}/renderer-launch-failure-probe.ts (92%) rename src/main/window/{ => renderer-recovery}/renderer-liveness-at-death.test.ts (87%) rename src/main/window/{ => renderer-recovery}/renderer-liveness-at-death.ts (96%) rename src/main/window/{ => renderer-recovery}/renderer-recovery-prompt.ts (97%) rename src/main/window/{ => renderer-recovery}/renderer-recovery-reload-watchdog.ts (97%) rename src/main/window/{ => renderer-relays}/agent-launch-tab-publish-relay.ts (96%) rename src/main/window/{ => renderer-relays}/agent-pane-activity-relay.ts (93%) rename src/main/window/{ => renderer-relays}/project-move-pause.test.ts (58%) rename src/main/window/{ => renderer-relays}/project-move-pause.ts (96%) rename src/main/window/{ => renderer-relays}/renderer-reply-request.ts (93%) rename src/main/window/{ => renderer-relays}/runtime-renderer-notification-sender.test.ts (83%) rename src/main/window/{ => renderer-relays}/runtime-renderer-notification-sender.ts (100%) rename src/main/window/{ => renderer-relays}/session-tab-close-request-relay.test.ts (58%) rename src/main/window/{ => renderer-relays}/session-tab-close-request-relay.ts (90%) rename src/main/window/{ => renderer-relays}/terminal-pane-move-commit-fence.ts (94%) rename src/main/window/{ => renderer-relays}/terminal-pane-move-request-relay.test.ts (95%) rename src/main/window/{ => renderer-relays}/terminal-pane-move-request-relay.ts (94%) rename src/main/window/{ => renderer-relays}/terminal-pane-reveal-relay.ts (87%) rename src/main/window/{ => renderer-relays}/terminal-tab-close-request-relay.test.ts (100%) rename src/main/window/{ => renderer-relays}/terminal-tab-close-request-relay.ts (93%) rename src/main/window/{ => security}/host-frame-navigation-guard.test.ts (68%) rename src/main/window/{ => security}/host-frame-navigation-guard.ts (93%) rename src/main/window/{ => security}/host-frame-permission-policy.test.ts (100%) rename src/main/window/{ => security}/host-frame-permission-policy.ts (100%) rename src/main/window/{ => security}/main-window-webview-security.ts (85%) rename src/main/window/{ => security}/privileged-window-navigation.ts (92%) rename src/main/window/{ => security}/renderer-document-navigation.ts (100%) rename src/main/window/{ => testing}/renderer-publication-throttle-electron-fixture.ts (93%) rename src/main/window/{ => visibility}/hidden-window-frame-pump.ts (88%) rename src/main/window/{ => visibility}/main-window-visibility.ts (100%) rename src/main/window/{ => visibility}/renderer-publication-throttle.electron.test.ts (89%) rename src/main/window/{ => visibility}/renderer-publication-throttle.ts (100%) rename src/main/window/{ => visibility}/resource-view-admission.ts (74%) rename src/main/window/{ => visibility}/wait-for-window-to-show.ts (100%) diff --git a/.github/workflows/macos-updater-tests.yml b/.github/workflows/macos-updater-tests.yml index 9a3e04afc4a..6956a23e03d 100644 --- a/.github/workflows/macos-updater-tests.yml +++ b/.github/workflows/macos-updater-tests.yml @@ -12,7 +12,7 @@ on: - 'config/scripts/macos-shipit-scheduling*' - 'config/electron-builder.config.cjs' - 'src/main/startup/quit/main-process-quit*' - - 'src/main/window/main-window-state-lifecycle*' + - 'src/main/window/main-window/main-window-state-lifecycle*' - 'src/main/window/dashboard-popout-window*' - 'src/packages/process-host/**' - 'src/main/runtime/agent-session/lifecycle/structured-agent-cli-writers*' @@ -56,7 +56,7 @@ jobs: src/main/updater/updater.headless-serve-install.test.ts src/main/updater/mac-install/updater-mac-quit-guard.test.ts src/main/startup/quit/main-process-quit-update-veto.test.ts - src/main/window/main-window-state-lifecycle.test.ts + src/main/window/main-window/main-window-state-lifecycle.test.ts - name: Prove login-backed terminal writer census on the disposable runner env: ORCA_MACOS_LOGIN_CENSUS_CI: '1' diff --git a/config/flaky-test-pattern-baseline.txt b/config/flaky-test-pattern-baseline.txt index 44fc5be3a3e..8d55d06937e 100644 --- a/config/flaky-test-pattern-baseline.txt +++ b/config/flaky-test-pattern-baseline.txt @@ -20,7 +20,7 @@ 1 test-no-in-test-import-actual:src/main/speech/openai-api-key-store.test.ts 5 test-no-in-test-import-actual:src/main/ssh/config/ssh-config-loader-regression.test.ts 2 test-no-in-test-import-actual:src/main/ssh/connection/ssh-connection-auth-fallback.test.ts -1 test-no-in-test-import-actual:src/main/window/dropped-path-preparation.test.ts +1 test-no-in-test-import-actual:src/main/window/file-drop/dropped-path-preparation.test.ts 2 test-no-in-test-import-actual:src/main/workspace-hooks/hooks-runner.test.ts 1 test-no-in-test-import-actual:src/main/workspaces/removal/local-worktree-removal-recovery.test.ts 1 test-no-in-test-import-actual:src/renderer/src/components/terminal-pane/pty-connection/notifications/pty-connection-hook-idle-arbitration.test.ts @@ -201,7 +201,7 @@ 2 test-no-real-time-wait:src/main/startup/gpu-recovery/gpu-lifecycle-install-dir-acl-guard.test.ts 3 test-no-real-time-wait:config/scripts/development/launch/run-electron-vite-dev.test.ts 1 test-no-real-time-wait:src/main/updater/lifecycle/serve-supervisor-install-park.test.ts -1 test-no-real-time-wait:src/main/window/main-window-updater-deferred-setup.test.ts +1 test-no-real-time-wait:src/main/updater/lifecycle/main-window-updater-deferred-setup.test.ts 3 test-no-real-time-wait:src/main/windows/windows-host-job.win32.test.ts 1 test-no-real-time-wait:src/main/windows/windows-process-tree-command-line-patch.test.ts 2 test-no-real-time-wait:src/main/windows/windows-pty-job.win32.test.ts @@ -630,7 +630,7 @@ 1 test-timeout-needs-reason:src/main/sqlite/sync-database-concurrent-backup.test.ts 2 test-timeout-needs-reason:src/main/ssh/connection/ssh-connection-sftp-wire.test.ts 2 test-timeout-needs-reason:src/main/ssh/system-transport/forwarding/system-ssh-forward-process-real-openssh.test.ts -1 test-timeout-needs-reason:src/main/window/renderer-publication-throttle.electron.test.ts +1 test-timeout-needs-reason:src/main/window/visibility/renderer-publication-throttle.electron.test.ts 1 test-timeout-needs-reason:src/main/windows/windows-administrator-restart.win32.test.ts 1 test-timeout-needs-reason:src/main/windows/windows-host-job.win32.test.ts 1 test-timeout-needs-reason:src/main/windows/windows-msys-job.win32.test.ts diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 2a073b788f6..1ee9c681616 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -3782,7 +3782,7 @@ "providers": ["local", "daemon", "ssh", "paired-runtime"], "coveredPlatforms": ["macos"], "coveredProviders": ["local", "daemon", "paired-runtime"], - "coverageNotes": "Audit3994 retires two authored callbacks imported by the shared construction test driver: split OSC 9999 carry and rendererless status ownership, plus OpenCode permission text taking priority in status and tui-idle refusal. Remaining imports and selectors keep their scope. No equivalent replacement or fresh gate execution is claimed. Audit3417 composes root-adopted3415v2/independent3416v2: intentionally removes117authored/173potential expanded registrations from12semantic sources, deleting4whole conventional suites and3registration modules and retaining4authored/5expanded cases, without equivalent replacement or fresh execution. The whole main-window attach-services suite is removed: runtime/window attach, notifier wiring, reload/teardown and permission-handler mock orchestration alarms are intentionally lost. Prune its exact deleted command selectors, testFiles entries and notifier assertionRef where present; complete prior selectors/ref are archived. Unrelated dated receipts remain exact and retain their original scope; no fresh reduced-selector, native-window or remote execution is claimed. Audit3403 composes root-adopted3393/3394,3400/3401 and3398/3399: intentionally retires167 authored registrations/196 potential expanded cases across93 whole suites without equivalent replacement or fresh execution. Retained65 registrations/79 potential cases include one conditional missing-input failure sentinel; potential counts are not executions. Historical notes and complete dated receipts retain their exact original scopes. The retained runtime registration driver loses three complete imported fragments: changed-working-label title facts and suppressed-frame tracked snapshots; refusing SSH GitLab MR-base lookup before local Git/glab calls; and missing folder-workspace path creation/update refusal before spawn/store mutation. These alarms are intentionally lost. Other imported fragments and separately rooted dated receipts are unchanged; they are not equivalent replacement coverage. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2090 (sources2088/2089): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2086 (sources2082/2083): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime-tests/terminal-handles-part-02.spec.ts: recovers a moved SSH pane through the tab it now sits in, not the one its lease froze; reports a plain expired orphan; does not report a pane whose only recent lease was superseded; does not report a pane whose only recent lease had its relay id recycled; picks the eligible orphan over a superseded predecessor that matches first; collects the same tabs the per-tab reader reports, in one sweep of the leases; reports no candidate when no lease names any of the worktree tabs. src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts: starts unavailable with no authoritative window; reports the configured Windows terminal shell on status; reports floating workspace availability from settings on status; polls floating tabs with targeted PTY liveness and no repo/provider inventory; hydrates persisted tabs when the store cannot report repos; advertises browser screencast only when a renderer window is available; routes mobile Codex reset consumption through the account mutation coordinator; maps a definite pre-provider rejection into an authoritative current snapshot. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit1640 removes low-value IPC, SSH, workspace and transcript-watcher tests without equivalent replacement coverage. Per-source reviews record retained public refusal, cancellation, execution-host and real-filesystem outcomes with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Deterministic lifecycle, IPC, window-notification, and runtime ownership tests cover generation replacement, stale callbacks, same-frame old-document publication, blocked external navigation, cancelled renderer navigation, concurrent provisional starts, process loss, timeout cleanup, headless fallback, and competing windows. Isolated macOS Electron validation covers cancelled navigation, forced renderer recovery, and headless serve promotion with live daemon-backed PTY identity and I/O. Linux, Windows, WSL, SSH promotion, and mixed-version desktop/server live runs remain uncollected; the renderer generation stays local to Electron IPC and does not change the remote wire. The renderer scheduling fixture now retains in-flight coalescing only, and payload partitioning retains publication-failure and main-dropped-worktree retry. Immediate publication, stale SSH cleanup, reconnect resync and isolated partition membership alarms were removed; other lifecycle evidence does not establish equivalent coverage. 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. Audit1400: src/renderer/src/runtime/sync-runtime-graph-scheduling.test.ts removed. Removes whole coalescing call-count suite; no newest-state delivery or replacement claim. No equivalent replacement coverage is claimed. Existing exact unaffected receipts remain unchanged and apply only to their unchanged selectors. Changed optional selectors have no new execution result. 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. Existing exact unaffected receipts remain unchanged and apply only to their unchanged selectors. Changed optional selectors have no new execution result.", + "coverageNotes": "Audit3994 retires two authored callbacks imported by the shared construction test driver: split OSC 9999 carry and rendererless status ownership, plus OpenCode permission text taking priority in status and tui-idle refusal. Remaining imports and selectors keep their scope. No equivalent replacement or fresh gate execution is claimed. Audit3417 composes root-adopted3415v2/independent3416v2: intentionally removes117authored/173potential expanded registrations from12semantic sources, deleting4whole conventional suites and3registration modules and retaining4authored/5expanded cases, without equivalent replacement or fresh execution. The whole main-window attach-services suite is removed: runtime/window attach, notifier wiring, reload/teardown and permission-handler mock orchestration alarms are intentionally lost. Prune its exact deleted command selectors, testFiles entries and notifier assertionRef where present; complete prior selectors/ref are archived. Unrelated dated receipts remain exact and retain their original scope; no fresh reduced-selector, native-window or remote execution is claimed. Audit3403 composes root-adopted3393/3394,3400/3401 and3398/3399: intentionally retires167 authored registrations/196 potential expanded cases across93 whole suites without equivalent replacement or fresh execution. Retained65 registrations/79 potential cases include one conditional missing-input failure sentinel; potential counts are not executions. Historical notes and complete dated receipts retain their exact original scopes. The retained runtime registration driver loses three complete imported fragments: changed-working-label title facts and suppressed-frame tracked snapshots; refusing SSH GitLab MR-base lookup before local Git/glab calls; and missing folder-workspace path creation/update refusal before spawn/store mutation. These alarms are intentionally lost. Other imported fragments and separately rooted dated receipts are unchanged; they are not equivalent replacement coverage. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2090 (sources2088/2089): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2086 (sources2082/2083): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime-tests/terminal-handles-part-02.spec.ts: recovers a moved SSH pane through the tab it now sits in, not the one its lease froze; reports a plain expired orphan; does not report a pane whose only recent lease was superseded; does not report a pane whose only recent lease had its relay id recycled; picks the eligible orphan over a superseded predecessor that matches first; collects the same tabs the per-tab reader reports, in one sweep of the leases; reports no candidate when no lease names any of the worktree tabs. src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts: starts unavailable with no authoritative window; reports the configured Windows terminal shell on status; reports floating workspace availability from settings on status; polls floating tabs with targeted PTY liveness and no repo/provider inventory; hydrates persisted tabs when the store cannot report repos; advertises browser screencast only when a renderer window is available; routes mobile Codex reset consumption through the account mutation coordinator; maps a definite pre-provider rejection into an authoritative current snapshot. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit1640 removes low-value IPC, SSH, workspace and transcript-watcher tests without equivalent replacement coverage. Per-source reviews record retained public refusal, cancellation, execution-host and real-filesystem outcomes with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Deterministic lifecycle, IPC, window-notification, and runtime ownership tests cover generation replacement, stale callbacks, same-frame old-document publication, blocked external navigation, cancelled renderer navigation, concurrent provisional starts, process loss, timeout cleanup, headless fallback, and competing windows. Isolated macOS Electron validation covers cancelled navigation, forced renderer recovery, and headless serve promotion with live daemon-backed PTY identity and I/O. Linux, Windows, WSL, SSH promotion, and mixed-version desktop/server live runs remain uncollected; the renderer generation stays local to Electron IPC and does not change the remote wire. The renderer scheduling fixture now retains in-flight coalescing only, and payload partitioning retains publication-failure and main-dropped-worktree retry. Immediate publication, stale SSH cleanup, reconnect resync and isolated partition membership alarms were removed; other lifecycle evidence does not establish equivalent coverage. 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. Audit1400: src/renderer/src/runtime/sync-runtime-graph-scheduling.test.ts removed. Removes whole coalescing call-count suite; no newest-state delivery or replacement claim. No equivalent replacement coverage is claimed. Existing exact unaffected receipts remain unchanged and apply only to their unchanged selectors. Changed optional selectors have no new execution result. 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. Existing exact unaffected receipts remain unchanged and apply only to their unchanged selectors. Changed optional selectors have no new execution result.\nWindow folder cleanup retires the mock-call-only navigation fence suite and the per-generation warning-count case. Retained sender results and existing rendered checks keep their own original scope; no equivalent replacement or fresh execution is claimed here.", "motivatingLinks": [ "https://github.com/stablyai/orca/issues/14066", "https://linear.app/stably/issue/STA-4016", @@ -3791,7 +3791,7 @@ "invariant": "Every genuine renderer graph reload revision settles exactly once, while a blocked external navigation that preserves the current document never starts a revision. A cancelled renderer-document navigation restores authority only when that same renderer survived; cancellation after an earlier document committed remains fenced until publication or timeout. Replacing a reload cancels only the prior revision; its timer and callbacks cannot settle a newer revision. A desktop reload succeeds only from a publication carrying a new renderer-document generation, while a failed headless promotion may recover only from its originating window. Timeout, renderer loss, and close reach a terminal graph state, and successful desktop publication permanently retires headless sentinel fallback until a new headless runtime is established.", "oracle": "Audit3832 deliberately retires76 authored/79 finite cases:75 runtime compatibility cases/78 finite cases across14 fragments, plus one localization extractor default-flag case. Three whole runtime fragments are deleted;27 authored/30 finite cases remain across the reviewed sources. This gate retains only its own selected scope; no equivalent replacement or fresh runtime/native/remote evidence is claimed. Runtime component A: Audit3828 composes3822 with independent3824: deliberately retires49 authored/52 finite cases from seven compatibility fragments, leaving15 authored/18 finite cases and deleting two entire fragments. Accepted losses: positive folder-worker recovery/materialization and reveal deadline; persisted-key normalization and legacy split/group/focus projection; fixed stale CAS refusal; occupied/alias/restored/preallocated terminal-handle variants; fixed OpenCode/Cursor/Claude/Codex/Antigravity classification, readiness, title and management examples; direct private-map browser authority projections; browser-focus and OMP title projection; already-aborted claim cleanup. Additional3824 loss: src/main/runtime/orca-runtime-tests/terminal-handles-and-agent-status-part-02.spec.ts :: does not poll a wrapper foreground for a speculative CLI prompt send :: Speculative retryForegroundWrappers:false check must return false on a wrapper after one foreground read, without polling the supplied later codex response. These real bounded alarms are deliberately retired for low marginal value. Retained callbacks are unchanged and provide only their own bounded scope, not equivalent replacement. Historical receipts do not validate reduced callbacks. No fresh runtime/native/SSH/WSL/phone/browser evidence or full transitive closure is claimed. Runtime component B: Audit3834 deliberately retires26 authored/26 finite cases from seven fresh compatibility fragments, leaving10/10 and deleting the one-case saved-binding reattachment fragment. Original position58 is excluded: its existing substantive3578 KEEP7 review remains historical, with no fresh credit. Accepted losses: fixed hook/provider-session identities and stale metadata refusal; decorative/pre-title foreground ordering variants; saved-binding surface reattachment; saved-row exit ghost removal and silent activation selection; unsupported headless wake; positive pending materialization/group projections; per-caller navigation selection; positive SSH hydration/read partition; laptop mobile publication/buffer and background/pending/status projections; abnormal/unregistered SSH grace, normal-exit refusal, persisted SSH hydration, positive serve reattachment, pending-agent launch arguments and direct seeded running-launch admission guard. These are real bounded alarms deliberately retired for low marginal value. Retained cases preserve their own distinct deferred foreground coalescing, usable handle continuity, stale inventory refresh, concurrent browser snapshot preservation, renderer graph fencing, caller dedup isolation, SSH partition writes, runtime split lifetime, independent relay-ready publication, lease admission/state/expiry. The direct running-launch no-spawn guard alarm is deliberately retired; no remaining owning-launch refusal coverage is claimed. Historical runs do not validate reduced callbacks. No equivalent replacement, runtime/native/SSH/WSL/phone/browser result or full transitive closure is claimed. Additional extractor loss: config/scripts/localization/source-extraction/audit-localization-native-dialogs.test.mjs :: does not change the existing renderer scan by default :: Default options must omit the five native-only fields in this supplied snippet. No equivalent replacement is claimed. Historical prior account (preserved verbatim): Audit3814 deliberately retires60 authored/60 finite cases:53runtime compatibility cases,4native XCTest methods and3Python clipboard methods, including the entire StableElementIndexes suite. Clipboard cuts retire the one-over-cap sample, incoming-offer destruction trace and sequential20-copy replay; actual wire, FD/pipe, startup and Xvfb/GTK boundaries remain without equivalent replacement claims. Accepted losses include narrow terminal-send payload/provider projections, legacy and direct no-op/error permutations, timing/work-count examples, private state/title/activity projections and fixture-driven readiness examples, individually enumerated below. These include real bounded production alarms consciously retired for marginal value; retained failed writes, duplicate submission, stale capture, transcript/ring and allocation fences are not equivalent replacements. No fresh native/remote/browser execution or complete transitive closure is claimed. Earlier dated receipts and raw selectors remain historical accounts of prior callback sets. Audit3806 deliberately retires40 authored/40 finite runtime compatibility cases, including the terminal-side-effect-facts-part03 headless-snapshots-part02 and agent-status-and-waits-part03 fragments, plus20 scalar Swift precondition sites. Lost runtime alarms include status/title/attention projection examples, simple renderer/headless frame restoration and status waiting outcomes detailed in the accepted-loss ledger. Swift cuts retire duplicate generation-map/schema/navigation/bridge and notification-wrapper assertions; no standalone native program is deleted. These include real bounded production alarms consciously retired for marginal value. Retained races, pending writes/reflow, namespace and waiter ownership are not equivalent replacements. No fresh native/remote/browser execution or full transitive closure is claimed. Earlier dated receipts and raw selectors remain historical accounts of prior callback sets. Audit3798 deliberately retires21 authored/24 finite compatibility cases, including the entire SSH-worktree-lifecycle and repository-project-operations fragments. Accepted losses include navigation-only settings acknowledgement, browser session cleanup and host preservation projections, window positive/policy-state examples, terminal recovery/reload and no-incarnation remint examples, terminal listing compatibility/malformed IDs, directory browse/sort/Windows path smoke, SSH create refusal/manual lineage, recursive parent-folder creation and fork/origin/avatar/icon projections. Real bounded production alarms are consciously retired for marginal value; retained authority races, cleanup/clone ownership, pending waiters, nested-folder and scan invalidation cases are not equivalent replacements. No fresh native/remote/browser execution or full transitive closure is claimed. Earlier dated receipts and raw selectors remain historical accounts of prior callback sets. Audit3417 composes root-adopted3415v2/independent3416v2: intentionally removes117authored/173potential expanded registrations from12semantic sources, deleting4whole conventional suites and3registration modules and retaining4authored/5expanded cases, without equivalent replacement or fresh execution. The whole main-window attach-services suite is removed: runtime/window attach, notifier wiring, reload/teardown and permission-handler mock orchestration alarms are intentionally lost. Prune its exact deleted command selectors, testFiles entries and notifier assertionRef where present; complete prior selectors/ref are archived. Unrelated dated receipts remain exact and retain their original scope; no fresh reduced-selector, native-window or remote execution is claimed. Audit3403 composes root-adopted3393/3394,3400/3401 and3398/3399: intentionally retires167 authored registrations/196 potential expanded cases across93 whole suites without equivalent replacement or fresh execution. Retained65 registrations/79 potential cases include one conditional missing-input failure sentinel; potential counts are not executions. Historical notes and complete dated receipts retain their exact original scopes. The retained runtime registration driver loses three complete imported fragments: changed-working-label title facts and suppressed-frame tracked snapshots; refusing SSH GitLab MR-base lookup before local Git/glab calls; and missing folder-workspace path creation/update refusal before spawn/store mutation. These alarms are intentionally lost. Other imported fragments and separately rooted dated receipts are unchanged; they are not equivalent replacement coverage. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2090 (sources2088/2089): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2086 (sources2082/2083): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime-tests/terminal-handles-part-02.spec.ts: recovers a moved SSH pane through the tab it now sits in, not the one its lease froze; reports a plain expired orphan; does not report a pane whose only recent lease was superseded; does not report a pane whose only recent lease had its relay id recycled; picks the eligible orphan over a superseded predecessor that matches first; collects the same tabs the per-tab reader reports, in one sweep of the leases; reports no candidate when no lease names any of the worktree tabs. src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts: starts unavailable with no authoritative window; reports the configured Windows terminal shell on status; reports floating workspace availability from settings on status; polls floating tabs with targeted PTY liveness and no repo/provider inventory; hydrates persisted tabs when the store cannot report repos; advertises browser screencast only when a renderer window is available; routes mobile Codex reset consumption through the account mutation coordinator; maps a definite pre-provider rejection into an authoritative current snapshot. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: Start a blocked external main-frame navigation and require the current renderer graph and notification path to remain authoritative. Start a same-renderer navigation, cancel it before commit, and require the renderer canary and ready authority to survive; coalesce overlapping provisional starts into one fence, and keep an earlier committed reload fenced if a later navigation is cancelled. Drive two genuine reloads half a timeout apart and require the first deadline to leave the second revision reloading while the second deadline alone makes it unavailable. During the second revision publish from the prior renderer document through the still-current main frame and require rejection without graph mutation or timeout cancellation. Restore headless authority after failed promotion, publish from a competing window, and require rejection while the sentinel remains authoritative. Then force a real renderer crash and separately promote an isolated headless serve owner; require runtime graph readiness, stable runtime/main/daemon/PTY identities, recovered visible terminal output, and post-recovery input/output.", "commands": [ - "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/session-graph/runtime-graph-reload-lifecycle.test.ts src/main/window/runtime-renderer-notification-sender.test.ts src/main/window/renderer-document-navigation.test.ts src/main/ipc/runtime.test.ts src/main/runtime/construction/orca-runtime.test.ts src/renderer/src/runtime/sync-runtime-graph/sync-runtime-graph-payload-partition.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/session-graph/runtime-graph-reload-lifecycle.test.ts src/main/window/renderer-relays/runtime-renderer-notification-sender.test.ts src/main/ipc/runtime.test.ts src/main/runtime/construction/orca-runtime.test.ts src/renderer/src/runtime/sync-runtime-graph/sync-runtime-graph-payload-partition.test.ts", "pnpm exec electron-vite build --mode e2e", "SKIP_BUILD=1 pnpm exec playwright test tests/e2e/renderer-blocked-navigation-graph-authority.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1", "SKIP_BUILD=1 pnpm exec playwright test e2e/renderer-crash-recovery-terminal-input.spec.ts tests/e2e/renderer-crash-recovery-terminal-input.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1", @@ -3799,8 +3799,7 @@ ], "testFiles": [ "src/main/runtime/session-graph/runtime-graph-reload-lifecycle.test.ts", - "src/main/window/runtime-renderer-notification-sender.test.ts", - "src/main/window/renderer-document-navigation.test.ts", + "src/main/window/renderer-relays/runtime-renderer-notification-sender.test.ts", "src/main/ipc/runtime.test.ts", "src/main/runtime/construction/orca-runtime.test.ts", "src/renderer/src/runtime/sync-runtime-graph/sync-runtime-graph-payload-partition.test.ts", @@ -3818,20 +3817,12 @@ ] }, { - "file": "src/main/window/runtime-renderer-notification-sender.test.ts", + "file": "src/main/window/renderer-relays/runtime-renderer-notification-sender.test.ts", "assertions": [ - "renderer failure is contained once per load generation", + "disposed-frame failures are contained and send admission remains closed until a valid renderer document returns", "late events cannot revive a closed sender" ] }, - { - "file": "src/main/window/renderer-document-navigation.test.ts", - "assertions": [ - "blocked external navigation does not start a renderer generation", - "only the packaged renderer file or development origin starts a renderer generation", - "concurrent provisional starts share one reload fence and cancel only after every attempt fails" - ] - }, { "file": "tests/e2e/renderer-blocked-navigation-graph-authority.spec.ts", "assertions": [ diff --git a/config/scripts/test-runtime/sqlite-admission/vitest-node-runtime-files.mjs b/config/scripts/test-runtime/sqlite-admission/vitest-node-runtime-files.mjs index 155ed1f6ec5..8b639caf0d7 100644 --- a/config/scripts/test-runtime/sqlite-admission/vitest-node-runtime-files.mjs +++ b/config/scripts/test-runtime/sqlite-admission/vitest-node-runtime-files.mjs @@ -113,13 +113,13 @@ const NODE_RUNTIME_INCLUDE = [ 'src/shared/workspace-session/workspace-session-salvage.test.ts', 'src/main/git/repositories/discovery/repo-detection.test.ts', 'src/renderer/src/components/browser-pane/annotate/browser-grab-toast-owner-cleanup.test.tsx', - 'src/main/window/native-chat-paste-files.test.ts', + 'src/main/native-chat/paste-files/native-chat-paste-files.test.ts', 'src/main/computer/desktop-script/runtime-host/desktop-script-serve-channel.test.ts', 'src/main/providers/local-pty/local-pty-child-process-verdict.test.ts', 'src/main/providers/working-directory-validation.test.ts', 'src/main/pty/process-retirement/posix-pty-process-groups.integration.test.ts', 'src/shared/ephemeral-vm-recipe-output-capture.test.ts', - 'src/main/window/main-window-updater-deferred-setup.test.ts', + 'src/main/updater/lifecycle/main-window-updater-deferred-setup.test.ts', 'src/renderer/src/components/terminal-pane/host-state/terminal-pane-host-state-memo.test.ts', 'src/main/native-chat/agent-session-journal/**/*.test.ts', 'src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-terminal-writer-acquire.test.ts', diff --git a/config/scripts/workflow/source-routing/pr-e2e-source-routing.mjs b/config/scripts/workflow/source-routing/pr-e2e-source-routing.mjs index a62f0c5e205..185f388ef22 100644 --- a/config/scripts/workflow/source-routing/pr-e2e-source-routing.mjs +++ b/config/scripts/workflow/source-routing/pr-e2e-source-routing.mjs @@ -151,7 +151,7 @@ export const PR_E2E_SOURCE_ROUTES = [ specs: ['tests/e2e/file-explorer-copy-file-windows.spec.ts'], matches: (file) => isProductSource(file) && - /^src\/(?:main\/window\/clipboard-(?:file-copy|ipc-handlers)|renderer\/src\/components\/right-sidebar\/file-explorer\/panel\/file-explorer-row-file-transfer)\.ts$/.test( + /^src\/(?:main\/window\/clipboard\/clipboard-(?:file-copy|ipc-handlers)|renderer\/src\/components\/right-sidebar\/file-explorer\/panel\/file-explorer-row-file-transfer)\.ts$/.test( file ) }, diff --git a/config/scripts/workflow/source-routing/pr-plugin-e2e-source-routes.mjs b/config/scripts/workflow/source-routing/pr-plugin-e2e-source-routes.mjs index 5ff0c8aa204..b7314a73fe9 100644 --- a/config/scripts/workflow/source-routing/pr-plugin-e2e-source-routes.mjs +++ b/config/scripts/workflow/source-routing/pr-plugin-e2e-source-routes.mjs @@ -123,7 +123,7 @@ const SPECIFIC_PLUGIN_ROUTES = [ 'src\\/main\\/plugins\\/panels\\/(?:frame-serving\\/|guest-isolation\\/|desktop-actions\\/)?plugin-frame-', 'src\\/shared\\/plugins\\/frames\\/plugin-(?:frame-guest|panel-shell)', 'src\\/preload\\/plugin-frame-guest', - 'src\\/main\\/window\\/(?:main-window-webview-security|attach-main-window-services|host-frame-permission-policy)', + 'src\\/main\\/window\\/(?:security/(?:main-window-webview-security|host-frame-permission-policy)|attach-main-window-services)', 'src\\/main\\/ipc\\/(?:plugins|plugin-frame-entry-ipc)\\.ts$', 'src\\/main\\/startup\\/plugin-startup\\/main-process-plugins', 'config\\/build-plugins\\/main-build-entry-points', diff --git a/config/tsconfig.tc.web.json b/config/tsconfig.tc.web.json index dcda9919545..82e91423858 100644 --- a/config/tsconfig.tc.web.json +++ b/config/tsconfig.tc.web.json @@ -92,9 +92,9 @@ "../src/main/startup/serve-launch/serve-mode-argv.ts", "../src/main/startup/profile-startup/single-instance-lock.ts", "../src/main/startup/diagnostics/startup-diagnostics.ts", - "../src/main/window/focus-existing-window.ts", - "../src/main/window/foreground-activation-policy.ts", - "../src/main/window/macos-app-activation.ts" + "../src/main/window/activation/focus-existing-window.ts", + "../src/main/window/activation/foreground-activation-policy.ts", + "../src/main/window/activation/macos-app-activation.ts" ], "compilerOptions": { "paths": { diff --git a/src/main/backend-desktop/backend-desktop-capabilities.ts b/src/main/backend-desktop/backend-desktop-capabilities.ts index 0d8bef80826..22080046761 100644 --- a/src/main/backend-desktop/backend-desktop-capabilities.ts +++ b/src/main/backend-desktop/backend-desktop-capabilities.ts @@ -7,7 +7,7 @@ import { loadKnownUsageWorktreesByRepo } from '../usage/worktree-attribution/usa import { isTelemetryEnabled, track } from '../telemetry/client' import type { UsageWorktreeSource } from '../usage/worktree-attribution/usage-worktree-metadata' import { currentWorkerEntryLayout } from '../worker-threads/worker-thread-entry-path' -import { isRendererDocumentNavigation } from '../window/renderer-document-navigation' +import { isRendererDocumentNavigation } from '../window/security/renderer-document-navigation' export function trustsBackendRenderer(contents: WebContents): boolean { if ( diff --git a/src/main/backend-desktop/backend-supervisor.test.ts b/src/main/backend-desktop/backend-supervisor.test.ts index fa5a1e0c8e4..3b60b188e2c 100644 --- a/src/main/backend-desktop/backend-supervisor.test.ts +++ b/src/main/backend-desktop/backend-supervisor.test.ts @@ -37,7 +37,7 @@ vi.mock('../worker-threads/worker-thread-entry-path', () => ({ currentWorkerEntryLayout: () => ({ moduleDir: '/' }), resolveWorkerThreadEntryPath: () => 'backend-entry.js' })) -vi.mock('../window/renderer-document-navigation', () => ({ +vi.mock('../window/security/renderer-document-navigation', () => ({ isRendererDocumentNavigation: () => true })) vi.mock('../usage/worktree-attribution/usage-worktree-metadata', () => ({ diff --git a/src/main/browser/extensions/actions/browser-extension-toolbar-electron-fixture.ts b/src/main/browser/extensions/actions/browser-extension-toolbar-electron-fixture.ts index 2457e919576..fa42fd3652c 100644 --- a/src/main/browser/extensions/actions/browser-extension-toolbar-electron-fixture.ts +++ b/src/main/browser/extensions/actions/browser-extension-toolbar-electron-fixture.ts @@ -2,7 +2,7 @@ import { app, BaseWindow, BrowserWindow, protocol, ipcMain, session } from 'elec import { renameSync, writeFileSync } from 'node:fs' import '../../../../shared/constants' import { ORCA_BROWSER_PARTITION } from '../../../../shared/browser/browser-session-partition' -import { applyBackgroundActivationPolicy } from '../../../window/foreground-activation-policy' +import { applyBackgroundActivationPolicy } from '../../../window/activation/foreground-activation-policy' import { browserManager } from '../../guest-lifecycle/manager/browser-manager' import { browserSessionRegistry } from '../../sessions/profiles/browser-session-registry' import { initializeBrowserExtensionSupport } from '../browser-extension-startup' diff --git a/src/main/browser/extensions/api-runtime/browser-extension-electron-fixture.ts b/src/main/browser/extensions/api-runtime/browser-extension-electron-fixture.ts index 36c5c85afaa..08667ea3933 100644 --- a/src/main/browser/extensions/api-runtime/browser-extension-electron-fixture.ts +++ b/src/main/browser/extensions/api-runtime/browser-extension-electron-fixture.ts @@ -3,7 +3,7 @@ import { renameSync, writeFileSync } from 'node:fs' import { createServer } from 'node:http' import '../../../../shared/constants' import { ORCA_BROWSER_PARTITION } from '../../../../shared/browser/browser-session-partition' -import { applyBackgroundActivationPolicy } from '../../../window/foreground-activation-policy' +import { applyBackgroundActivationPolicy } from '../../../window/activation/foreground-activation-policy' import { browserManager } from '../../guest-lifecycle/manager/browser-manager' import { browserSessionRegistry } from '../../sessions/profiles/browser-session-registry' import { initializeBrowserExtensionSupport } from '../browser-extension-startup' diff --git a/src/main/browser/extensions/api/notifications.test.ts b/src/main/browser/extensions/api/notifications.test.ts index 4443dcd4b8b..cb04d6850c4 100644 --- a/src/main/browser/extensions/api/notifications.test.ts +++ b/src/main/browser/extensions/api/notifications.test.ts @@ -27,7 +27,7 @@ vi.mock('electron', async () => { nativeImage: {} } }) -vi.mock('../../../window/foreground-activation-policy', () => ({ +vi.mock('../../../window/activation/foreground-activation-policy', () => ({ isBackgroundLaunch: () => mocks.background })) vi.mock('../api-runtime/browser-extension-api-host', () => ({ emitBrowserExtensionEvent: vi.fn() })) diff --git a/src/main/browser/extensions/api/notifications.ts b/src/main/browser/extensions/api/notifications.ts index e397022f9a5..0cfcdbf09c2 100644 --- a/src/main/browser/extensions/api/notifications.ts +++ b/src/main/browser/extensions/api/notifications.ts @@ -5,7 +5,7 @@ import { getChromeExtensionOrigin, parseChromeExtensionUrl } from '../../../../shared/browser/extensions/browser-extension-url' -import { isBackgroundLaunch } from '../../../window/foreground-activation-policy' +import { isBackgroundLaunch } from '../../../window/activation/foreground-activation-policy' import { emitBrowserExtensionEvent } from '../api-runtime/browser-extension-api-host' import { resolveInsideExtensionDirectory } from '../installation/browser-extension-manifest' import { diff --git a/src/main/browser/extensions/native-messaging/browser-extension-native-messaging-electron-fixture.ts b/src/main/browser/extensions/native-messaging/browser-extension-native-messaging-electron-fixture.ts index 74b1ccbcfe3..95b89a663ad 100644 --- a/src/main/browser/extensions/native-messaging/browser-extension-native-messaging-electron-fixture.ts +++ b/src/main/browser/extensions/native-messaging/browser-extension-native-messaging-electron-fixture.ts @@ -3,7 +3,7 @@ import { existsSync, mkdirSync, renameSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import '../../../../shared/constants' import { ORCA_BROWSER_PARTITION } from '../../../../shared/browser/browser-session-partition' -import { applyBackgroundActivationPolicy } from '../../../window/foreground-activation-policy' +import { applyBackgroundActivationPolicy } from '../../../window/activation/foreground-activation-policy' import { browserSessionRegistry } from '../../sessions/profiles/browser-session-registry' import { initializeBrowserExtensionSupport } from '../browser-extension-startup' import { BROWSER_EXTENSION_API_CUSTOM_SCHEME } from '../api-runtime/browser-extension-api-scheme' diff --git a/src/main/browser/extensions/profile-sessions/browser-extension-lifecycle-electron-fixture.ts b/src/main/browser/extensions/profile-sessions/browser-extension-lifecycle-electron-fixture.ts index 211b8fa78fe..ae899b46811 100644 --- a/src/main/browser/extensions/profile-sessions/browser-extension-lifecycle-electron-fixture.ts +++ b/src/main/browser/extensions/profile-sessions/browser-extension-lifecycle-electron-fixture.ts @@ -2,7 +2,7 @@ import { app, BaseWindow, protocol, session } from 'electron' import { renameSync, writeFileSync } from 'node:fs' import '../../../../shared/constants' import { ORCA_BROWSER_PARTITION } from '../../../../shared/browser/browser-session-partition' -import { applyBackgroundActivationPolicy } from '../../../window/foreground-activation-policy' +import { applyBackgroundActivationPolicy } from '../../../window/activation/foreground-activation-policy' import { browserSessionRegistry } from '../../sessions/profiles/browser-session-registry' import { initializeBrowserExtensionSupport } from '../browser-extension-startup' import { BROWSER_EXTENSION_API_CUSTOM_SCHEME } from '../api-runtime/browser-extension-api-scheme' diff --git a/src/main/browser/extensions/profile-storage/browser-extension-persistence-electron-fixture.ts b/src/main/browser/extensions/profile-storage/browser-extension-persistence-electron-fixture.ts index 35ca7f99861..ef5aaf52ada 100644 --- a/src/main/browser/extensions/profile-storage/browser-extension-persistence-electron-fixture.ts +++ b/src/main/browser/extensions/profile-storage/browser-extension-persistence-electron-fixture.ts @@ -3,7 +3,7 @@ import { renameSync, writeFileSync } from 'node:fs' import { createServer } from 'node:http' import '../../../../shared/constants' import { ORCA_BROWSER_PARTITION } from '../../../../shared/browser/browser-session-partition' -import { applyBackgroundActivationPolicy } from '../../../window/foreground-activation-policy' +import { applyBackgroundActivationPolicy } from '../../../window/activation/foreground-activation-policy' import { browserManager } from '../../guest-lifecycle/manager/browser-manager' import { browserSessionRegistry } from '../../sessions/profiles/browser-session-registry' import { initializeBrowserExtensionSupport } from '../browser-extension-startup' diff --git a/src/main/browser/extensions/tabs-and-windows/browser-extension-window-model.test.ts b/src/main/browser/extensions/tabs-and-windows/browser-extension-window-model.test.ts index a28abda9bca..283644fdd9d 100644 --- a/src/main/browser/extensions/tabs-and-windows/browser-extension-window-model.test.ts +++ b/src/main/browser/extensions/tabs-and-windows/browser-extension-window-model.test.ts @@ -26,7 +26,7 @@ vi.mock('electron', async () => { } return { BrowserWindow: FakeBrowserWindow } }) -vi.mock('../../../window/foreground-activation-policy', () => ({ +vi.mock('../../../window/activation/foreground-activation-policy', () => ({ isBackgroundLaunch: () => true, showWindowWithoutStealingFocus: () => {} })) diff --git a/src/main/browser/extensions/tabs-and-windows/browser-extension-window-model.ts b/src/main/browser/extensions/tabs-and-windows/browser-extension-window-model.ts index 9f9dd27a543..e023258148b 100644 --- a/src/main/browser/extensions/tabs-and-windows/browser-extension-window-model.ts +++ b/src/main/browser/extensions/tabs-and-windows/browser-extension-window-model.ts @@ -5,7 +5,7 @@ import { isSameExtensionNavigation } from '../../../../shared/browser/extensions import { isBackgroundLaunch, showWindowWithoutStealingFocus -} from '../../../window/foreground-activation-policy' +} from '../../../window/activation/foreground-activation-policy' import { emitBrowserExtensionEvent } from '../api-runtime/browser-extension-api-host' import { watchBrowserMainWindowReadiness } from '../../guest-lifecycle/browser-main-window-readiness' diff --git a/src/main/browser/guest-lifecycle/find/browser-find-frame-traversal-electron-fixture.ts b/src/main/browser/guest-lifecycle/find/browser-find-frame-traversal-electron-fixture.ts index dc1f8dd766e..26f01b325e1 100644 --- a/src/main/browser/guest-lifecycle/find/browser-find-frame-traversal-electron-fixture.ts +++ b/src/main/browser/guest-lifecycle/find/browser-find-frame-traversal-electron-fixture.ts @@ -1,5 +1,5 @@ import { app, BrowserWindow } from 'electron' -import { applyBackgroundActivationPolicy } from '../../../window/foreground-activation-policy' +import { applyBackgroundActivationPolicy } from '../../../window/activation/foreground-activation-policy' if (process.env.ORCA_BACKGROUND_LAUNCH !== '1') { throw new Error('Find traversal fixture requires background launch') diff --git a/src/main/browser/guest-lifecycle/input/browser-guest-context-menu.ts b/src/main/browser/guest-lifecycle/input/browser-guest-context-menu.ts index 4ea7a66c295..4a980940d9b 100644 --- a/src/main/browser/guest-lifecycle/input/browser-guest-context-menu.ts +++ b/src/main/browser/guest-lifecycle/input/browser-guest-context-menu.ts @@ -5,7 +5,7 @@ import { redactKagiSessionToken } from '../../../../shared/browser-url' import type { BrowserContextMenuRequestedEvent } from '../../../../shared/browser/guest-lifecycle/browser-guest-events' -import { readContextMenuSpelling } from '../../../window/editable-context-menu' +import { readContextMenuSpelling } from '../../../window/editing/editable-context-menu' import { readGuestNavigationState } from '../navigation/browser-guest-navigation-state' import type { ResolveRenderer } from '../browser-guest-renderer-target' // oxlint-disable-next-line import/no-cycle -- browser subsystem singleton hub (browserManager); breaking it needs DI and test-mock updates, left for a human diff --git a/src/main/browser/guest-lifecycle/manager/browser-manager-guest-visibility.test.ts b/src/main/browser/guest-lifecycle/manager/browser-manager-guest-visibility.test.ts index 5be8dc68670..92b4feb1092 100644 --- a/src/main/browser/guest-lifecycle/manager/browser-manager-guest-visibility.test.ts +++ b/src/main/browser/guest-lifecycle/manager/browser-manager-guest-visibility.test.ts @@ -40,7 +40,7 @@ vi.mock('../popups/presentation/popup-origin-bar-window', () => ({ })) import { browserManager } from './browser-manager' -import { rendererPublicationThrottle } from '../../../window/renderer-publication-throttle' + import { guestNavigationMethods, rendererWebContentsId, @@ -60,28 +60,6 @@ describe('browserManager', () => { vi.useRealTimers() }) - it('shares guest leases with a concurrent stream even without a desktop renderer', () => { - vi.useFakeTimers() - const guest = { - id: 1708, - isDestroyed: vi.fn(() => false), - setBackgroundThrottling: vi.fn(), - capturePage: vi.fn(async () => null) - } - webContentsFromIdMock.mockReturnValue(guest) - const releaseStream = rendererPublicationThrottle.acquire(guest) - const releaseCapture = browserManager.holdPaintForCapture(guest.id) - - releaseStream() - vi.runAllTimers() - expect(guest.setBackgroundThrottling.mock.calls).toEqual([[false]]) - - releaseCapture() - releaseCapture() - vi.runAllTimers() - expect(guest.setBackgroundThrottling.mock.calls).toEqual([[false], [true]]) - }) - it.each(['acquire'])('cleans capture ownership after renderer %s fails', (failure) => { vi.useFakeTimers() const guest = { @@ -139,70 +117,4 @@ describe('browserManager', () => { vi.runAllTimers() expect(guest.setBackgroundThrottling.mock.calls).toEqual([[false], [true], [false], [true]]) }) - - it('does not let a destroyed guest and renderer release a replacement page hold', () => { - vi.useFakeTimers() - const makeGuest = (id: number) => ({ - ...guestNavigationMethods(), - id, - isDestroyed: vi.fn(() => false), - isCurrentlyAudible: vi.fn(() => false), - getType: vi.fn(() => 'webview'), - setBackgroundThrottling: vi.fn(), - capturePage: vi.fn(async () => null), - setWindowOpenHandler: vi.fn(), - on: vi.fn(), - off: vi.fn(), - openDevTools: vi.fn() - }) - const makeRenderer = (id: number) => ({ - id, - isDestroyed: vi.fn(() => false), - send: vi.fn(), - setBackgroundThrottling: vi.fn(), - capturePage: vi.fn(async () => null) - }) - const oldGuest = makeGuest(1709) - const newGuest = makeGuest(1710) - const oldRenderer = makeRenderer(rendererWebContentsId) - const newRenderer = makeRenderer(rendererWebContentsId + 1) - webContentsFromIdMock.mockImplementation((id: number) => - [oldGuest, newGuest, oldRenderer, newRenderer].find((contents) => contents.id === id) - ) - const register = (guest: ReturnType, rendererId: number) => { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: this guest provides the policy and registration members exercised here. - browserManager.attachGuestPolicies(guest as never) - browserManager.registerGuest({ - browserPageId: 'page-replaced', - webContentsId: guest.id, - rendererWebContentsId: rendererId - }) - } - register(oldGuest, oldRenderer.id) - const releaseOld = browserManager.holdPaintForCapture(oldGuest.id) - oldGuest.isDestroyed.mockReturnValue(true) - oldRenderer.isDestroyed.mockReturnValue(true) - register(newGuest, newRenderer.id) - newRenderer.send.mockClear() - const releaseNew = browserManager.holdPaintForCapture(newGuest.id) - - releaseOld() - vi.runAllTimers() - expect(newRenderer.send.mock.calls).toEqual([ - ['browser:capturePaintHold', { browserPageId: 'page-replaced', held: true }] - ]) - expect(oldGuest.setBackgroundThrottling.mock.calls).toEqual([[false]]) - expect(oldRenderer.setBackgroundThrottling.mock.calls).toEqual([[false]]) - expect(newGuest.setBackgroundThrottling.mock.calls).toEqual([[false]]) - expect(newRenderer.setBackgroundThrottling.mock.calls).toEqual([[false]]) - - releaseNew() - vi.runAllTimers() - expect(newRenderer.send.mock.calls.at(-1)).toEqual([ - 'browser:capturePaintHold', - { browserPageId: 'page-replaced', held: false } - ]) - expect(newGuest.setBackgroundThrottling.mock.calls).toEqual([[false], [true]]) - expect(newRenderer.setBackgroundThrottling.mock.calls).toEqual([[false], [true]]) - }) }) diff --git a/src/main/browser/guest-lifecycle/manager/browser-manager-visibility.ts b/src/main/browser/guest-lifecycle/manager/browser-manager-visibility.ts index fb004fab1ce..89bb3d77c93 100644 --- a/src/main/browser/guest-lifecycle/manager/browser-manager-visibility.ts +++ b/src/main/browser/guest-lifecycle/manager/browser-manager-visibility.ts @@ -1,5 +1,5 @@ import { webContents } from 'electron' -import { rendererPublicationThrottle } from '../../../window/renderer-publication-throttle' +import { rendererPublicationThrottle } from '../../../window/visibility/renderer-publication-throttle' import { BrowserManagerState } from './browser-manager-state' import { focusGuardUnavailableError, diff --git a/src/main/browser/guest-lifecycle/navigation/browser-link-materialization-electron-fixture.ts b/src/main/browser/guest-lifecycle/navigation/browser-link-materialization-electron-fixture.ts index 78fc3d6f9ac..4c0af0e1229 100644 --- a/src/main/browser/guest-lifecycle/navigation/browser-link-materialization-electron-fixture.ts +++ b/src/main/browser/guest-lifecycle/navigation/browser-link-materialization-electron-fixture.ts @@ -1,6 +1,6 @@ import { app, BrowserWindow, ipcMain, session, webContents } from 'electron' import { join } from 'node:path' -import { applyBackgroundActivationPolicy } from '../../../window/foreground-activation-policy' +import { applyBackgroundActivationPolicy } from '../../../window/activation/foreground-activation-policy' if (process.env.ORCA_BACKGROUND_LAUNCH !== '1') { throw new Error('Popup identity probes require a background launch') diff --git a/src/main/browser/guest-lifecycle/page-activity/browser-agent-activity.ts b/src/main/browser/guest-lifecycle/page-activity/browser-agent-activity.ts index d53adfbe81e..ca3bbe1a79a 100644 --- a/src/main/browser/guest-lifecycle/page-activity/browser-agent-activity.ts +++ b/src/main/browser/guest-lifecycle/page-activity/browser-agent-activity.ts @@ -1,5 +1,5 @@ import { BrowserWindow, ipcMain, type WebContents } from 'electron' -import { isResourceWindowVisible } from '../../../window/resource-view-admission' +import { isResourceWindowVisible } from '../../../window/visibility/resource-view-admission' import { sendGuestCdpCommand } from '../../cdp/debugger/guest-cdp-command' import { isAgentRpcCaller } from '../../../runtime/rpc/callers/rpc-caller-identity' import { diff --git a/src/main/browser/guest-lifecycle/popups/compatibility/browser-popup-compatibility-electron-fixture.ts b/src/main/browser/guest-lifecycle/popups/compatibility/browser-popup-compatibility-electron-fixture.ts index bfd564a762f..e44319d6ee7 100644 --- a/src/main/browser/guest-lifecycle/popups/compatibility/browser-popup-compatibility-electron-fixture.ts +++ b/src/main/browser/guest-lifecycle/popups/compatibility/browser-popup-compatibility-electron-fixture.ts @@ -9,7 +9,7 @@ import { } from 'electron' import { writeFileSync } from 'node:fs' import { renameFileWithWindowsRetry } from '../../../../../shared/windows-retry-file-operations' -import { applyBackgroundActivationPolicy } from '../../../../window/foreground-activation-policy' +import { applyBackgroundActivationPolicy } from '../../../../window/activation/foreground-activation-policy' import { BrowserPopupCompatibilityManager } from './browser-popup-compatibility-manager-fixture' import { BrowserRouteWebContentsRegistry } from '../../../routing/guest-authority/browser-route-webcontents-registry' import { initializeBrowserProcessUserAgent } from '../../../identity/browser-process-user-agent' diff --git a/src/main/browser/guest-lifecycle/popups/presentation/browser-popup-native-window.ts b/src/main/browser/guest-lifecycle/popups/presentation/browser-popup-native-window.ts index f8b19f879ff..c52c67c8e3a 100644 --- a/src/main/browser/guest-lifecycle/popups/presentation/browser-popup-native-window.ts +++ b/src/main/browser/guest-lifecycle/popups/presentation/browser-popup-native-window.ts @@ -3,7 +3,7 @@ import type { BrowserWindow, WebContents } from 'electron' import { isBackgroundLaunch, showWhenAppActive -} from '../../../../window/foreground-activation-policy' +} from '../../../../window/activation/foreground-activation-policy' import type { PopupChildWindowOptions, PopupOriginBarWindow } from './browser-popup-window-contract' import { isNativeBrowserPopupWindow, diff --git a/src/main/browser/guest-lifecycle/popups/presentation/popup-origin-bar-window.ts b/src/main/browser/guest-lifecycle/popups/presentation/popup-origin-bar-window.ts index e5a90a46658..1e947aaf060 100644 --- a/src/main/browser/guest-lifecycle/popups/presentation/popup-origin-bar-window.ts +++ b/src/main/browser/guest-lifecycle/popups/presentation/popup-origin-bar-window.ts @@ -3,7 +3,7 @@ import { isBackgroundLaunch, revealWindowsAwaitingAppActive, showWhenAppActive -} from '../../../../window/foreground-activation-policy' +} from '../../../../window/activation/foreground-activation-policy' import { discardBrowserPopupContents, prepareNativeBrowserPopup diff --git a/src/main/browser/guest-lifecycle/popups/presentation/popup-origin-chrome-electron-fixture.ts b/src/main/browser/guest-lifecycle/popups/presentation/popup-origin-chrome-electron-fixture.ts index 68cff07d9fa..13037781fd1 100644 --- a/src/main/browser/guest-lifecycle/popups/presentation/popup-origin-chrome-electron-fixture.ts +++ b/src/main/browser/guest-lifecycle/popups/presentation/popup-origin-chrome-electron-fixture.ts @@ -3,7 +3,7 @@ import { mkdirSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { openPopupWithOriginBar } from './popup-origin-bar-window' import { prepareNativeBrowserPopup } from './browser-popup-native-window' -import { applyBackgroundActivationPolicy } from '../../../../window/foreground-activation-policy' +import { applyBackgroundActivationPolicy } from '../../../../window/activation/foreground-activation-policy' if (process.env.ORCA_BACKGROUND_LAUNCH !== '1') { throw new Error('Popup identity probes require a background launch') diff --git a/src/main/window/owned-temp-staging-root.ts b/src/main/filesystem/owned-temp-staging-root.ts similarity index 97% rename from src/main/window/owned-temp-staging-root.ts rename to src/main/filesystem/owned-temp-staging-root.ts index 1f582aa38bc..d95d6c113a4 100644 --- a/src/main/window/owned-temp-staging-root.ts +++ b/src/main/filesystem/owned-temp-staging-root.ts @@ -1,6 +1,6 @@ import type { Dir } from 'node:fs' import { isSafeOwnedDirectory } from '../../shared/owned-private-directory' -export { isSafeOwnedDirectory } from '../../shared/owned-private-directory' + import { lstat, mkdir, opendir } from 'node:fs/promises' import { dirname, join, resolve } from 'node:path' import { removeTree } from '../../shared/windows-transient-lock-removal' diff --git a/src/main/host/electron-runtime-desktop-surface.ts b/src/main/host/electron-runtime-desktop-surface.ts index 532c1270cb6..410a94a3ce5 100644 --- a/src/main/host/electron-runtime-desktop-surface.ts +++ b/src/main/host/electron-runtime-desktop-surface.ts @@ -1,4 +1,4 @@ -import { isResourceWindowVisible } from '../window/resource-view-admission' +import { isResourceWindowVisible } from '../window/visibility/resource-view-admission' import { BrowserWindow, ipcMain, Notification, powerMonitor } from 'electron' import { readDesktopAwayState } from '../notifications/desktop-away-state' import type { RuntimeDesktopSurface } from '../runtime/construction/runtime-desktop-surface' diff --git a/src/main/index.ts b/src/main/index.ts index 12aebe9cf84..50254b4c1b8 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,8 +1,14 @@ import { app, clipboard, dialog, type BrowserWindow } from 'electron' import { isRoutedOrcaDeepLink } from './startup/open-routing/orca-deep-link-url' -import { createMacAppActivationHandler } from './window/macos-app-activation' -import { isBackgroundLaunch, isOrcadSidecarLaunch } from './window/foreground-activation-policy' -import { createDesktopHandoff, desktopHandoffArguments } from './window/macos-desktop-handoff' +import { createMacAppActivationHandler } from './window/activation/macos-app-activation' +import { + isBackgroundLaunch, + isOrcadSidecarLaunch +} from './window/activation/foreground-activation-policy' +import { + createDesktopHandoff, + desktopHandoffArguments +} from './window/activation/macos-desktop-handoff' import { focusExistingWindow as focusExistingWindowAction, setMainWindowOpener diff --git a/src/main/ipc/app.ts b/src/main/ipc/app.ts index 6f52e5c6028..48cc7d326fc 100644 --- a/src/main/ipc/app.ts +++ b/src/main/ipc/app.ts @@ -9,7 +9,7 @@ import type { AppIdentity } from '../../shared/app-identity' import type { MarkdownDocument } from '../../shared/file-listing/file-document' import type { FloatingTerminalCwdRequest } from '../../shared/ui-chrome-types' import { relaunchApp, runBeforeRelaunchCleanup } from '../startup/quit/app-relaunch' -import { setWindowMaterialSuppressed } from '../window/window-material-suppression' +import { setWindowMaterialSuppressed } from '../window/presentation/window-material-suppression' import { isTrustedUIRenderer } from './ui' import { prepareWindowsAdministratorRestart } from '../windows/windows-administrator-restart' import { quitProcess } from '../startup/quit/process-quit-request' diff --git a/src/main/ipc/dashboard-popout.ts b/src/main/ipc/dashboard-popout.ts index 715b6abf7fb..28a0acd02d4 100644 --- a/src/main/ipc/dashboard-popout.ts +++ b/src/main/ipc/dashboard-popout.ts @@ -9,8 +9,8 @@ import { isDashboardPopoutRenderer, onDashboardPopoutOpenChanged } from '../window/dashboard-popout-window' -import { safelyRevealWindow } from '../window/focus-existing-window' -import { isBackgroundLaunch } from '../window/foreground-activation-policy' +import { safelyRevealWindow } from '../window/activation/focus-existing-window' +import { isBackgroundLaunch } from '../window/activation/foreground-activation-policy' import { getTrustedUIRendererWindow, isTrustedUIRenderer, sendToTrustedUIRenderer } from './ui' import { admitDashboardSnapshot, diff --git a/src/main/ipc/memory.ts b/src/main/ipc/memory.ts index e9de80a1eae..8bf6348d93a 100644 --- a/src/main/ipc/memory.ts +++ b/src/main/ipc/memory.ts @@ -6,7 +6,7 @@ import { collectMemorySnapshot } from '../memory/collector' import { assertResourceViewVisible, isResourceWindowVisible -} from '../window/resource-view-admission' +} from '../window/visibility/resource-view-admission' import type { ResourceViewAdmission } from '../../shared/resource-view-admission' export function registerMemoryHandlers(store: Store): void { diff --git a/src/main/ipc/native-notification-delivery.ts b/src/main/ipc/native-notification-delivery.ts index 99d86e4a4d9..11f9c11a3d4 100644 --- a/src/main/ipc/native-notification-delivery.ts +++ b/src/main/ipc/native-notification-delivery.ts @@ -4,8 +4,8 @@ import type { NotificationDispatchResult, NotificationSettings } from '../../shared/notification-settings-types' -import { safelyRevealWindow } from '../window/focus-existing-window' -import { isBackgroundLaunch } from '../window/foreground-activation-policy' +import { safelyRevealWindow } from '../window/activation/focus-existing-window' +import { isBackgroundLaunch } from '../window/activation/foreground-activation-policy' import { getRepoIdFromWorktreeId } from '../../shared/worktree/id' import { parsePaneKey } from '../../shared/workspace-layout/stable-pane-id' import type { buildNotificationOptions } from './notification-options' diff --git a/src/main/ipc/notifications.ts b/src/main/ipc/notifications.ts index c8dc097a415..759923097ed 100644 --- a/src/main/ipc/notifications.ts +++ b/src/main/ipc/notifications.ts @@ -14,7 +14,7 @@ import { isNotificationAttentionAcknowledgement } from '../../shared/notificatio import type { OrcaRuntimeService } from '../runtime/orca-runtime' import { readNotificationAuthorizationStatus } from './notification-authorization-status' import { setTrayAttention } from '../tray/system-tray' -import { isMainWindowVisible } from '../window/main-window-visibility' +import { isMainWindowVisible } from '../window/visibility/main-window-visibility' import { activeNotificationsById } from './native-notification-lifecycle' import { deliverNativeNotification } from './native-notification-delivery' import { removeReadMacosNotifications } from './macos-notification-center' diff --git a/src/main/ipc/pty/ipc/leaf-move.ts b/src/main/ipc/pty/ipc/leaf-move.ts index 978b50f64d7..7cee39f9e93 100644 --- a/src/main/ipc/pty/ipc/leaf-move.ts +++ b/src/main/ipc/pty/ipc/leaf-move.ts @@ -2,7 +2,7 @@ import { getPtyIpc } from '../pty-host-bindings' import type { Store } from '../../../persistence/loading-store/store' import type { OrcaRuntimeService } from '../../../runtime/orca-runtime' import { rekeyMovedPaneOwners } from '../../../runtime/terminal/identity/moved-pane-owner-rekey' -import { commitRelayedPaneMove } from '../../../window/terminal-pane-move-commit-fence' +import { commitRelayedPaneMove } from '../../../window/renderer-relays/terminal-pane-move-commit-fence' import { isTerminalLeafMoveRequest, type TerminalLeafMoveRequest, diff --git a/src/main/ipc/pty/ipc/session-listing.ts b/src/main/ipc/pty/ipc/session-listing.ts index 83a099fa3d9..b823244388b 100644 --- a/src/main/ipc/pty/ipc/session-listing.ts +++ b/src/main/ipc/pty/ipc/session-listing.ts @@ -1,5 +1,5 @@ import type { ResourceViewAdmission } from '../../../../shared/resource-view-admission' -import { assertResourceViewVisible } from '../../../window/resource-view-admission' +import { assertResourceViewVisible } from '../../../window/visibility/resource-view-admission' import { getPtyIpc } from '../pty-host-bindings' import { PtyProcessListAdmission, diff --git a/src/main/ipc/register-core-handlers/register-core-handlers.ts b/src/main/ipc/register-core-handlers/register-core-handlers.ts index f100d92b59f..e4f0915ff0c 100644 --- a/src/main/ipc/register-core-handlers/register-core-handlers.ts +++ b/src/main/ipc/register-core-handlers/register-core-handlers.ts @@ -78,11 +78,9 @@ import { registerMiniMaxCredentialsHandlers } from '../minimax-credentials' import { registerZcodePlanCredentialsHandlers } from '../zcode-plan-credentials' import { registerGrokAccountHandlers } from '../grok-accounts' import { registerCursorAccountHandlers } from '../cursor-accounts' -import { registerUpdaterHandlers } from '../../window/attach-main-window-services' -import { - registerClipboardHandlers, - setTrustedClipboardRendererWebContentsId -} from '../../window/clipboard-ipc-handlers' +import { registerUpdaterHandlers } from '../../updater/lifecycle/main-window-updater' +import { registerClipboardHandlers } from '../../window/clipboard/clipboard-ipc-handlers' +import { setTrustedClipboardRendererWebContentsId } from '../../window/clipboard/clipboard-ipc-sender-trust' import { isDashboardPopoutRenderer } from '../../window/dashboard-popout-window' import type { RateLimitService } from '../../rate-limits/service/rate-limit-service' import type { CodexAccountService } from '../../codex-accounts/service' diff --git a/src/main/ipc/workspace-space/workspace-space-window.ts b/src/main/ipc/workspace-space/workspace-space-window.ts index 1c5443a91be..21f99e41cf6 100644 --- a/src/main/ipc/workspace-space/workspace-space-window.ts +++ b/src/main/ipc/workspace-space/workspace-space-window.ts @@ -5,8 +5,8 @@ import { getWorkspaceSpaceWindow, isWorkspaceSpaceWindowRenderer } from '../../window/workspace-space-window' -import { safelyRevealWindow } from '../../window/focus-existing-window' -import { isBackgroundLaunch } from '../../window/foreground-activation-policy' +import { safelyRevealWindow } from '../../window/activation/focus-existing-window' +import { isBackgroundLaunch } from '../../window/activation/foreground-activation-policy' import { normalizeExecutionHostId } from '../../../shared/execution-host' import { isWorkspaceSpaceChange } from '../../../shared/workspace-disk-space/workspace-space-change' import type { Store } from '../../persistence/loading-store/store' diff --git a/src/main/ipc/worktrees/creation/worktree-base-directory-poller.ts b/src/main/ipc/worktrees/creation/worktree-base-directory-poller.ts index 9ecf57a8f77..147d4cb4cbe 100644 --- a/src/main/ipc/worktrees/creation/worktree-base-directory-poller.ts +++ b/src/main/ipc/worktrees/creation/worktree-base-directory-poller.ts @@ -1,7 +1,7 @@ import { isMainWindowVisible, onMainWindowBecameVisible -} from '../../../window/main-window-visibility' +} from '../../../window/visibility/main-window-visibility' import type { WorktreeBaseRepoWatchConfig, WorktreeBaseWatchTarget diff --git a/src/main/ipc/wsl/wsl-move-driver-wiring.ts b/src/main/ipc/wsl/wsl-move-driver-wiring.ts index 5ed63a7bb5c..c587298beda 100644 --- a/src/main/ipc/wsl/wsl-move-driver-wiring.ts +++ b/src/main/ipc/wsl/wsl-move-driver-wiring.ts @@ -78,7 +78,7 @@ import { listRemoteOrcadRepositories } from '../../managed-server/migration/link import { stageRemoteOrcadMigrationBlobChunk } from '../../managed-server/migration/transfers/orcad-migration-chunk-client' import { listRemoteOrcadFolderCatalog } from '../../managed-server/migration/linking/orcad-server-folder-workspaces' import { makeTerminalScrollbackSnapshotRef } from '../../pty/scrollback/terminal-scrollback-snapshots' -import { pauseWorkspacesForProjectMove } from '../../window/project-move-pause' +import { pauseWorkspacesForProjectMove } from '../../window/renderer-relays/project-move-pause' import { getWslHostService } from '../../wsl/host-lifecycle/wsl-host-service-instance' import { readWslGuestProcessTable } from '../../wsl/execution/wsl-guest-process-table' import { isWslDistroRegistered } from '../../wsl/discovery/wsl-distro-registry-id' diff --git a/src/main/native-chat/agent-session-context-snapshots.ts b/src/main/native-chat/agent-session-context-snapshots.ts index 33693cffe1f..6fef5293ecb 100644 --- a/src/main/native-chat/agent-session-context-snapshots.ts +++ b/src/main/native-chat/agent-session-context-snapshots.ts @@ -7,7 +7,7 @@ import { renameFileWithWindowsRetryAsync } from '../../shared/windows-retry-file import { ensureOwnedTempStagingRoot, getOwnedTempStagingRoot -} from '../window/owned-temp-staging-root' +} from '../filesystem/owned-temp-staging-root' const claims = new Map() const publications = new Map>() diff --git a/src/main/window/native-chat-paste-files.test.ts b/src/main/native-chat/paste-files/native-chat-paste-files.test.ts similarity index 94% rename from src/main/window/native-chat-paste-files.test.ts rename to src/main/native-chat/paste-files/native-chat-paste-files.test.ts index 77498786878..22f09399e3f 100644 --- a/src/main/window/native-chat-paste-files.test.ts +++ b/src/main/native-chat/paste-files/native-chat-paste-files.test.ts @@ -2,11 +2,11 @@ import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSyn import { tmpdir } from 'node:os' import path from 'node:path' import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { installFakeAppEnvironment } from '../../../config/scripts/test-runtime/environment/vitest-host-ports-setup' +import { installFakeAppEnvironment } from '../../../../config/scripts/test-runtime/environment/vitest-host-ports-setup' -import type { Store } from '../persistence/loading-store/store' -import { resolveLocalFileRequestPath } from '../ipc/filesystem/access/local-file-access-resolution' -import { readLocalFileContent } from '../ipc/filesystem/filesystem-file-content-inspection' +import type { Store } from '../../persistence/loading-store/store' +import { resolveLocalFileRequestPath } from '../../ipc/filesystem/access/local-file-access-resolution' +import { readLocalFileContent } from '../../ipc/filesystem/filesystem-file-content-inspection' import { restoreNativeChatPastes } from './native-chat-paste-files' // A store with no projects: nothing but an access kind decides what a read may reach. diff --git a/src/main/window/native-chat-paste-files.ts b/src/main/native-chat/paste-files/native-chat-paste-files.ts similarity index 95% rename from src/main/window/native-chat-paste-files.ts rename to src/main/native-chat/paste-files/native-chat-paste-files.ts index fbe384593f7..4f224ba322c 100644 --- a/src/main/window/native-chat-paste-files.ts +++ b/src/main/native-chat/paste-files/native-chat-paste-files.ts @@ -2,8 +2,8 @@ import { lstat, realpath, stat } from 'node:fs/promises' import path from 'node:path' -import { getAppEnvironment } from '../../shared/app-environment' -import { NATIVE_CHAT_PASTE_FOLDER } from '../../shared/native-chat/input/native-chat-paste-folder' +import { getAppEnvironment } from '../../../shared/app-environment' +import { NATIVE_CHAT_PASTE_FOLDER } from '../../../shared/native-chat/input/native-chat-paste-folder' const MAX_RESTORED_PASTES = 256 diff --git a/src/main/orcad/electron-sidecar-profile.ts b/src/main/orcad/electron-sidecar-profile.ts index bc12b6f3291..234d1d55dd1 100644 --- a/src/main/orcad/electron-sidecar-profile.ts +++ b/src/main/orcad/electron-sidecar-profile.ts @@ -6,7 +6,7 @@ import { z } from 'zod' import { isSafeOwnedDirectory } from '../../shared/owned-private-directory' import { isProcessAlive } from '../../shared/serve-bundle-coordination' import { removeTree } from '../../shared/windows-transient-lock-removal' -import { isDirectChild } from '../window/owned-temp-staging-root' +import { isDirectChild } from '../filesystem/owned-temp-staging-root' import { readRuntimeMetadataAsync } from '../runtime/rpc/server/runtime-metadata' import { sendOrcadSidecarRequest } from './orcad-sidecar-runtime-client' diff --git a/src/main/plugins/panels/guest-isolation/plugin-frame-guest-policies.ts b/src/main/plugins/panels/guest-isolation/plugin-frame-guest-policies.ts index dc179bd4e16..777ef91c478 100644 --- a/src/main/plugins/panels/guest-isolation/plugin-frame-guest-policies.ts +++ b/src/main/plugins/panels/guest-isolation/plugin-frame-guest-policies.ts @@ -2,7 +2,7 @@ import { BrowserWindow, Menu, webContents as electronWebContents } from 'electro import type { Session, WebContents } from 'electron' import type { KeybindingOverrides } from '../../../../shared/keybindings' import { setupGuestShortcutForwarding } from '../../../browser/guest-lifecycle/input/shortcuts/browser-guest-shortcut-forwarding' -import { buildEditableContextMenuTemplate } from '../../../window/editable-context-menu' +import { buildEditableContextMenuTemplate } from '../../../window/editing/editable-context-menu' import { isAllowedPluginFrameNavigation } from './plugin-frame-request-policy' /** `did-attach-webview`: per-guest fences the session cannot express, plus Orca's own input. */ diff --git a/src/main/window/history-gc-worktree-ids.test.ts b/src/main/pty/history/history-gc-worktree-ids.test.ts similarity index 58% rename from src/main/window/history-gc-worktree-ids.test.ts rename to src/main/pty/history/history-gc-worktree-ids.test.ts index 4e3278bced8..0604b456f0a 100644 --- a/src/main/window/history-gc-worktree-ids.test.ts +++ b/src/main/pty/history/history-gc-worktree-ids.test.ts @@ -1,6 +1,5 @@ /** An incomplete GC live set deletes owned history; regressions here caused the #14863 revert (#14975). */ import { describe, expect, it } from 'vitest' -import { folderWorkspaceKey } from '../../shared/workspace-scope' import { getKnownWorktreeIdsForHistoryGc } from './history-gc-worktree-ids' const noOtherProfiles = () => ({ ids: new Set(), unreadableProfiles: 0 }) @@ -19,32 +18,8 @@ describe('getKnownWorktreeIdsForHistoryGc', () => { }) // Folder PTYs mint history under folder:; omitting their separate store collection falsely marks live history orphaned. - it('includes folder workspaces under their PTY workspace key', () => { - const live = getKnownWorktreeIdsForHistoryGc( - store(['repo-1::/path/wt'], ['fw-1', 'fw-2']), - noOtherProfiles - ) - - expect(live.has(folderWorkspaceKey('fw-1'))).toBe(true) - expect(live.has(folderWorkspaceKey('fw-2'))).toBe(true) - expect(live.size).toBe(3) - }) - - it('is empty only when there is genuinely nothing live', () => { - expect(getKnownWorktreeIdsForHistoryGc(store([], []), noOtherProfiles).size).toBe(0) - }) // History is not profile-scoped, so other profiles must remain in the live set. - it('includes worktrees owned by other profiles', () => { - const live = getKnownWorktreeIdsForHistoryGc(store(['repo-1::/b'], ['fw-b']), () => ({ - ids: new Set(['repo-1::/a', folderWorkspaceKey('fw-a')]), - unreadableProfiles: 0 - })) - - expect(live).toEqual( - new Set(['repo-1::/b', folderWorkspaceKey('fw-b'), 'repo-1::/a', folderWorkspaceKey('fw-a')]) - ) - }) // Unreadable profiles make ownership unknown; empty means prune nothing, while partial IDs would delete live history. it('reports nothing live when a profile could not be read', () => { diff --git a/src/main/window/history-gc-worktree-ids.ts b/src/main/pty/history/history-gc-worktree-ids.ts similarity index 78% rename from src/main/window/history-gc-worktree-ids.ts rename to src/main/pty/history/history-gc-worktree-ids.ts index 577c4287d4a..89b99f5a624 100644 --- a/src/main/window/history-gc-worktree-ids.ts +++ b/src/main/pty/history/history-gc-worktree-ids.ts @@ -1,6 +1,6 @@ -import type { Store } from '../persistence/loading-store/store' -import { folderWorkspaceKey } from '../../shared/workspace-scope' -import { getOtherProfileWorktreeIdsForHistoryGc } from '../orca-profiles/catalog/other-profile-workspace-catalog' +import type { Store } from '../../persistence/loading-store/store' +import { folderWorkspaceKey } from '../../../shared/workspace-scope' +import { getOtherProfileWorktreeIdsForHistoryGc } from '../../orca-profiles/catalog/other-profile-workspace-catalog' /** Include git and folder: PTY workspace keys; omitting folder workspaces makes their live shell history appear orphaned. */ export function getKnownWorktreeIdsForHistoryGc( diff --git a/src/main/runtime/browser/client-pages/single-page-browser-commands-host-test-double.ts b/src/main/runtime/browser/client-pages/single-page-browser-commands-host-test-double.ts index 8e7dc7dba3c..30c55779828 100644 --- a/src/main/runtime/browser/client-pages/single-page-browser-commands-host-test-double.ts +++ b/src/main/runtime/browser/client-pages/single-page-browser-commands-host-test-double.ts @@ -2,7 +2,7 @@ import { vi } from 'vitest' import type { AgentBrowserBridge } from '../../../browser/agent-browser/agent-browser-bridge' import type { RuntimeBrowserCommandHost } from '../commands/browser-command-contracts' import { RuntimeBrowserPageRegistry } from './runtime-browser-page-registry' -import type { RendererPublicationThrottleTarget } from '../../../window/renderer-publication-throttle' +import type { RendererPublicationThrottleTarget } from '../../../window/visibility/renderer-publication-throttle' // The part of the authoritative window a browser stream touches: its renderer's throttle lease target. export type AuthoritativeWindowDouble = { diff --git a/src/main/runtime/browser/screencast/browser-screencasts.ts b/src/main/runtime/browser/screencast/browser-screencasts.ts index 6ccf2e96361..14410b8d4a6 100644 --- a/src/main/runtime/browser/screencast/browser-screencasts.ts +++ b/src/main/runtime/browser/screencast/browser-screencasts.ts @@ -3,7 +3,7 @@ import { BrowserError } from '../../../browser/browser-error' import { randomUUID } from 'node:crypto' import { startBrowserScreencast } from '../../../browser/cdp/screencast/browser-screencast-stream' import { browserManager } from '../../../browser/guest-lifecycle/manager/browser-manager' -import { rendererPublicationThrottle } from '../../../window/renderer-publication-throttle' +import { rendererPublicationThrottle } from '../../../window/visibility/renderer-publication-throttle' import { mintBrowserScreencastSubscriptionId } from './browser-screencast-subscription-id' import { cancelScreencastSubscriberFrameRetry, diff --git a/src/main/runtime/rpc/methods/clipboard/clipboard.test.ts b/src/main/runtime/rpc/methods/clipboard/clipboard.test.ts index 19814d9ccdd..c529821a302 100644 --- a/src/main/runtime/rpc/methods/clipboard/clipboard.test.ts +++ b/src/main/runtime/rpc/methods/clipboard/clipboard.test.ts @@ -8,7 +8,7 @@ const { saveClipboardImageBufferAsTempFile } = vi.hoisted(() => ({ saveClipboardImageBufferAsTempFile: vi.fn() })) -vi.mock('../../../../window/clipboard-image-temp-file', () => ({ +vi.mock('../../../../window/clipboard/clipboard-image-temp-file', () => ({ saveClipboardImageBufferAsTempFile })) import '../../../../../shared/rpc-contract/clipboard-params' diff --git a/src/main/runtime/rpc/methods/clipboard/clipboard.ts b/src/main/runtime/rpc/methods/clipboard/clipboard.ts index b195766554c..af18a7b0e6b 100644 --- a/src/main/runtime/rpc/methods/clipboard/clipboard.ts +++ b/src/main/runtime/rpc/methods/clipboard/clipboard.ts @@ -1,5 +1,5 @@ import { defineMethod, type RpcContext } from '../../core' -import { saveClipboardImageBufferAsTempFile } from '../../../../window/clipboard-image-temp-file' +import { saveClipboardImageBufferAsTempFile } from '../../../../window/clipboard/clipboard-image-temp-file' import { randomUUID } from 'node:crypto' import { recordMobileClipboardImagePath } from '../../clipboard/mobile-clipboard-image-provenance' import { diff --git a/src/main/runtime/rpc/server/runtime-rpc-startup-failure.ts b/src/main/runtime/rpc/server/runtime-rpc-startup-failure.ts index e2a81fa115d..4f079f9b7fb 100644 --- a/src/main/runtime/rpc/server/runtime-rpc-startup-failure.ts +++ b/src/main/runtime/rpc/server/runtime-rpc-startup-failure.ts @@ -3,7 +3,7 @@ import { dialog, type BrowserWindow, type MessageBoxOptions } from 'electron' import type { RuntimeRpcStartErrorClass } from '../../../../shared/telemetry-daemon-event-schemas' import { translateMain } from '../../../i18n/main-i18n' import { track } from '../../../telemetry/client' -import { waitForWindowToShow } from '../../../window/wait-for-window-to-show' +import { waitForWindowToShow } from '../../../window/visibility/wait-for-window-to-show' const MAX_VISIBLE_CAUSE_LENGTH = 500 diff --git a/src/main/runtime/session-lifecycle/close/close-mobile-session-tab.ts b/src/main/runtime/session-lifecycle/close/close-mobile-session-tab.ts index 9b5123c115e..961fb4eda4d 100644 --- a/src/main/runtime/session-lifecycle/close/close-mobile-session-tab.ts +++ b/src/main/runtime/session-lifecycle/close/close-mobile-session-tab.ts @@ -14,7 +14,7 @@ import { } from './mobile-session-tab-close-outcome' import { getRuntimeBrowserPageRegistry } from '../../browser/client-pages/runtime-browser-page-registry' import type { RuntimeEdgeCommandSurface } from '../../construction/runtime-edge-command-controller' -import { rendererPublicationThrottle } from '../../../window/renderer-publication-throttle' +import { rendererPublicationThrottle } from '../../../window/visibility/renderer-publication-throttle' import { retireHeadlessMobileSessionEditorTab } from '../../session-tabs/headless-editor-tabs' import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host' import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch' diff --git a/src/main/runtime/terminal/launch/mobile-terminal-chat-fork-create.ts b/src/main/runtime/terminal/launch/mobile-terminal-chat-fork-create.ts index f51e4090d00..63a4efaff68 100644 --- a/src/main/runtime/terminal/launch/mobile-terminal-chat-fork-create.ts +++ b/src/main/runtime/terminal/launch/mobile-terminal-chat-fork-create.ts @@ -14,7 +14,7 @@ import { isWslUncPath } from '../../../../shared/wsl-paths' import { isTuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent-config' import { applyRemoteViewerLaunchEnv } from './remote-viewer-launch-env' import { deterministicAgentSessionUuid } from './runtime-agent-launch-resolution' -import { rendererPublicationThrottle } from '../../../window/renderer-publication-throttle' +import { rendererPublicationThrottle } from '../../../window/visibility/renderer-publication-throttle' import { PROJECT_RUNTIME_REPAIR_REQUIRED_CODE } from '../../../../shared/project-runtime-repair-message' import { OrchestrationError } from '../../orchestration/orchestration-error' import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host' diff --git a/src/main/runtime/terminal/launch/mobile-terminal-renderer-create.ts b/src/main/runtime/terminal/launch/mobile-terminal-renderer-create.ts index f79c0bc11ee..81c59d9b381 100644 --- a/src/main/runtime/terminal/launch/mobile-terminal-renderer-create.ts +++ b/src/main/runtime/terminal/launch/mobile-terminal-renderer-create.ts @@ -12,7 +12,7 @@ import { MOBILE_TERMINAL_SURFACE_TIMEOUT_MS } from '../view/terminal-surface-deadlines' import { isClientDisconnectedError } from '../../client/runtime-client-disconnect' -import { rendererPublicationThrottle } from '../../../window/renderer-publication-throttle' +import { rendererPublicationThrottle } from '../../../window/visibility/renderer-publication-throttle' import { deterministicAgentSessionUuid } from './runtime-agent-launch-resolution' import { assertMobileTerminalShellSupported } from './mobile-terminal-shell-refusal' import { anchorTabOfCreate, mainPlacedAfter } from './mobile-terminal-create-anchor' diff --git a/src/main/startup/main-process-preflight-failure.ts b/src/main/startup/main-process-preflight-failure.ts index b6a07c5cc8d..66462e23346 100644 --- a/src/main/startup/main-process-preflight-failure.ts +++ b/src/main/startup/main-process-preflight-failure.ts @@ -1,7 +1,7 @@ import { app, dialog } from 'electron' import { prepareEarlyMainUiLanguage, translateMain } from '../i18n/main-i18n' import { formatProfileStateStartupFailure } from '../persistence/profile-state/startup/profile-state-startup-failure' -import { isBackgroundLaunch } from '../window/foreground-activation-policy' +import { isBackgroundLaunch } from '../window/activation/foreground-activation-policy' import { mainProcessState as state } from './main-process-state' import { SINGLE_INSTANCE_ALREADY_RUNNING_EXIT_CODE } from '../../shared/single-instance-exit-code' import { acquireDesktopProfileInstanceLock } from './profile-startup/desktop-profile-instance-lock' diff --git a/src/main/startup/main-process-ready-foundation.ts b/src/main/startup/main-process-ready-foundation.ts index f4af5a93b94..354f4df927b 100644 --- a/src/main/startup/main-process-ready-foundation.ts +++ b/src/main/startup/main-process-ready-foundation.ts @@ -1,5 +1,5 @@ import { app, session } from 'electron' -import { applyBackgroundActivationPolicy } from '../window/foreground-activation-policy' +import { applyBackgroundActivationPolicy } from '../window/activation/foreground-activation-policy' import { applyElectronProxySettings } from '../network/proxy-settings' import { installElectronProxyRequestGuard } from '../network/electron-proxy-request-guard' import { handleElectronProxyLogin } from '../network/electron-proxy-credentials' diff --git a/src/main/startup/main-window/main-process-i18n-menu.ts b/src/main/startup/main-window/main-process-i18n-menu.ts index aa6c193184f..8478c84e938 100644 --- a/src/main/startup/main-window/main-process-i18n-menu.ts +++ b/src/main/startup/main-window/main-process-i18n-menu.ts @@ -17,7 +17,7 @@ import { sendOpenFeatureTour, sendOpenSetupGuide } from './main-window-actions' -import { ensureAutoUpdaterConfigured } from '../../window/main-window-updater' +import { ensureAutoUpdaterConfigured } from '../../updater/lifecycle/main-window-updater' import { logStartupMilestone } from '../diagnostics/startup-diagnostics' import { browserManager } from '../../browser/guest-lifecycle/manager/browser-manager' diff --git a/src/main/startup/main-window/main-window-actions-quit-and-stop-all-sessions.test.ts b/src/main/startup/main-window/main-window-actions-quit-and-stop-all-sessions.test.ts index bbce9150377..138e953b20d 100644 --- a/src/main/startup/main-window/main-window-actions-quit-and-stop-all-sessions.test.ts +++ b/src/main/startup/main-window/main-window-actions-quit-and-stop-all-sessions.test.ts @@ -40,10 +40,10 @@ vi.mock('../../tray/system-tray', () => ({ createSystemTray: vi.fn(), setMacMenuBarIconVisible: vi.fn() })) -vi.mock('../../window/main-window-updater', () => ({ +vi.mock('../../updater/lifecycle/main-window-updater', () => ({ ensureAutoUpdaterConfigured: vi.fn() })) -vi.mock('../../window/focus-existing-window', () => ({ +vi.mock('../../window/activation/focus-existing-window', () => ({ focusExistingMainWindow: vi.fn(), safelyRevealWindow: vi.fn() })) @@ -53,12 +53,14 @@ vi.mock('../windows-install-permissions/windows-install-dir-acl-recovery', () => describeInstallDirAclPoison: vi.fn(), isBlockingInstallDirAclRepairInFlight: vi.fn() })) -vi.mock('../../window/renderer-recovery-prompt', () => ({ presentRendererRecoveryPrompt: vi.fn() })) -vi.mock('../../window/renderer-launch-failure-probe', () => ({ +vi.mock('../../window/renderer-recovery/renderer-recovery-prompt', () => ({ + presentRendererRecoveryPrompt: vi.fn() +})) +vi.mock('../../window/renderer-recovery/renderer-launch-failure-probe', () => ({ probeRendererLaunchCapacity: vi.fn() })) -import { safelyRevealWindow } from '../../window/focus-existing-window' +import { safelyRevealWindow } from '../../window/activation/focus-existing-window' import { consumeQuitAndStopAllSessionsRequest, quitAfterRendererStoppedAllSessions, diff --git a/src/main/startup/main-window/main-window-actions.ts b/src/main/startup/main-window/main-window-actions.ts index 33c046c7907..988d5e75d0a 100644 --- a/src/main/startup/main-window/main-window-actions.ts +++ b/src/main/startup/main-window/main-window-actions.ts @@ -20,13 +20,16 @@ import { setMacMenuBarIconVisible, type SystemTrayOptions } from '../../tray/system-tray' -import { ensureAutoUpdaterConfigured } from '../../window/main-window-updater' -import { focusExistingMainWindow, safelyRevealWindow } from '../../window/focus-existing-window' +import { ensureAutoUpdaterConfigured } from '../../updater/lifecycle/main-window-updater' +import { + focusExistingMainWindow, + safelyRevealWindow +} from '../../window/activation/focus-existing-window' import { mainProcessState as state } from '../main-process-state' import { quitTeardownStartGate } from '../quit/quit-teardown-start-gate' import { getDashboardPopoutWindow } from '../../window/dashboard-popout-window' import { getWorkspaceSpaceWindow } from '../../window/workspace-space-window' -import { markUserQuitWindowClose } from '../../window/user-quit-window-close' +import { markUserQuitWindowClose } from '../../window/main-window/user-quit-window-close' import { loadMainWindow } from '../../window/createMainWindow' import { describeInstallDirAclPoison, @@ -35,8 +38,8 @@ import { import { presentRendererRecoveryPrompt, type RendererRecoveryPromptFailure -} from '../../window/renderer-recovery-prompt' -import { probeRendererLaunchCapacity } from '../../window/renderer-launch-failure-probe' +} from '../../window/renderer-recovery/renderer-recovery-prompt' +import { probeRendererLaunchCapacity } from '../../window/renderer-recovery/renderer-launch-failure-probe' import { confirmQuitAndStopAllSessions, explainStopAllSessionsUnavailable diff --git a/src/main/startup/main-window/main-window-controller.ts b/src/main/startup/main-window/main-window-controller.ts index 9deb6e93a71..1e3e4031df3 100644 --- a/src/main/startup/main-window/main-window-controller.ts +++ b/src/main/startup/main-window/main-window-controller.ts @@ -15,7 +15,7 @@ import { startWindowsInstallDirAclRepairIfPoisoned } from '../windows-install-permissions/windows-install-dir-acl-recovery' import { logStartupMilestone } from '../diagnostics/startup-diagnostics' -import { notifyMainWindowBecameVisible } from '../../window/main-window-visibility' +import { notifyMainWindowBecameVisible } from '../../window/visibility/main-window-visibility' import { setTrayAttention } from '../../tray/system-tray' import { createSystemTrayDeferred, showRendererRecoveryPrompt } from './main-window-actions' import { attachMainWindowCoreServices } from './main-window-core-services' @@ -37,14 +37,14 @@ import { stopSyntheticTitleSpinnerTimer } from './synthetic-title-runtime' import { requireMainWindowServices } from './main-window-service-readiness' -import { recordRendererLaunchFailureProbe } from '../../window/renderer-launch-failure-probe' +import { recordRendererLaunchFailureProbe } from '../../window/renderer-recovery/renderer-launch-failure-probe' import { trackBrowserExtensionMainWindow } from '../../browser/extensions/tabs-and-windows/browser-extension-window-model' -import { isResourceWindowVisible } from '../../window/resource-view-admission' +import { isResourceWindowVisible } from '../../window/visibility/resource-view-admission' import { applyBackgroundActivationPolicy, applyServeDesktopWindowActivationPolicy -} from '../../window/foreground-activation-policy' +} from '../../window/activation/foreground-activation-policy' const TRAY_CREATE_FALLBACK_MS = 12_000 const AGENT_STATE_CRASH_BREADCRUMB_MIN_INTERVAL_MS = 30_000 diff --git a/src/main/startup/profile-startup/profile-state-recovery-preflight.test.ts b/src/main/startup/profile-startup/profile-state-recovery-preflight.test.ts index 01206c9cb3f..0c0cd88d412 100644 --- a/src/main/startup/profile-startup/profile-state-recovery-preflight.test.ts +++ b/src/main/startup/profile-startup/profile-state-recovery-preflight.test.ts @@ -51,7 +51,7 @@ vi.mock('electron', () => ({ app: mocks, dialog: { showMessageBox: mocks.showMessageBox } })) -vi.mock('../../window/foreground-activation-policy', () => ({ +vi.mock('../../window/activation/foreground-activation-policy', () => ({ applyBackgroundActivationPolicy: mocks.background })) vi.mock('node:fs', async (original) => { diff --git a/src/main/startup/profile-startup/profile-state-recovery-preflight.ts b/src/main/startup/profile-startup/profile-state-recovery-preflight.ts index 0ffa5094164..2f57764585f 100644 --- a/src/main/startup/profile-startup/profile-state-recovery-preflight.ts +++ b/src/main/startup/profile-startup/profile-state-recovery-preflight.ts @@ -14,7 +14,7 @@ import { import { prepareEarlyMainUiLanguage, translateMain } from '../../i18n/main-i18n' import { acquireProfileStateMaintenance } from '../../persistence/profile-state/access/profile-state-access' import { rollbackProfileState } from '../../persistence/profile-state/recovery/profile-state-recovery-command' -import { applyBackgroundActivationPolicy } from '../../window/foreground-activation-policy' +import { applyBackgroundActivationPolicy } from '../../window/activation/foreground-activation-policy' import { acquireSingleInstanceLock } from './single-instance-lock' /** The process owning both locks performs recovery before Electron can initialize a runtime. */ diff --git a/src/main/startup/profile-startup/profile-state-write-failure.ts b/src/main/startup/profile-startup/profile-state-write-failure.ts index 7c941725cf9..cf1a3de02ae 100644 --- a/src/main/startup/profile-startup/profile-state-write-failure.ts +++ b/src/main/startup/profile-startup/profile-state-write-failure.ts @@ -2,8 +2,8 @@ import { app, dialog, type BrowserWindow, type MessageBoxOptions } from 'electro import { translateMain } from '../../i18n/main-i18n' import { profileStateWriterFailureOutcome } from '../../persistence/profile-state/writer/profile-state-writer-errors' import { recordProfileStateWriteFailureReport } from '../../persistence/profile-state/writer/profile-state-writer-diagnostics' -import { isBackgroundLaunch } from '../../window/foreground-activation-policy' -import { waitForWindowToShow } from '../../window/wait-for-window-to-show' +import { isBackgroundLaunch } from '../../window/activation/foreground-activation-policy' +import { waitForWindowToShow } from '../../window/visibility/wait-for-window-to-show' import { mainProcessState } from '../main-process-state' let presenting = false diff --git a/src/main/startup/runtime-startup/main-process-runtime-launch.ts b/src/main/startup/runtime-startup/main-process-runtime-launch.ts index 1e30cbf9522..d1894ac7e42 100644 --- a/src/main/startup/runtime-startup/main-process-runtime-launch.ts +++ b/src/main/startup/runtime-startup/main-process-runtime-launch.ts @@ -7,7 +7,7 @@ import { } from '../../persistence/loading-store/user-data-path' import { OrcaRuntimeRpcServer } from '../../runtime/rpc/server/runtime-rpc' import { resolveServePinnedBindHost } from '../serve-launch/serve-pinned-bind-host' -import { isOrcadSidecarLaunch } from '../../window/foreground-activation-policy' +import { isOrcadSidecarLaunch } from '../../window/activation/foreground-activation-policy' import { registerMobileHandlers } from '../../ipc/mobile' import { createRuntimeEnvironmentMobileRelayHosts } from '../../ipc/runtime-environments/runtime-environment-mobile-relay-hosts' import { getLocalPtyProvider } from '../../ipc/pty/provider/registry' @@ -49,7 +49,7 @@ import { } from '../cloud-startup/main-process-connector-startup' import { mainProcessState as state } from '../main-process-state' import { logStartupMilestone } from '../diagnostics/startup-diagnostics' -import { setupHeadlessServeAutoUpdater } from '../../window/main-window-updater' +import { setupHeadlessServeAutoUpdater } from '../../updater/lifecycle/main-window-updater' import { preserveStateBeforeUpdateRestart } from '../../updater/update-restart-preservation' import { resolveUpdateInstallMode } from '../../updater/updater-service' import { scheduleAgentLaunchRecordWarmup } from '../agent-accounts/agent-launch-record-warmup' diff --git a/src/main/window/main-window-updater-deferred-setup.test.ts b/src/main/updater/lifecycle/main-window-updater-deferred-setup.test.ts similarity index 93% rename from src/main/window/main-window-updater-deferred-setup.test.ts rename to src/main/updater/lifecycle/main-window-updater-deferred-setup.test.ts index 4fe0463fb9b..37ba6575344 100644 --- a/src/main/window/main-window-updater-deferred-setup.test.ts +++ b/src/main/updater/lifecycle/main-window-updater-deferred-setup.test.ts @@ -1,8 +1,8 @@ import { EventEmitter } from 'node:events' import type { BrowserWindow } from 'electron' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { Store } from '../persistence/loading-store/store' -import type { UpdaterSetupOptions } from '../updater/lifecycle/updater-setup' +import type { Store } from '../../persistence/loading-store/store' +import type { UpdaterSetupOptions } from './updater-setup' const { setupMock, milestoneMock, getMainWindowMock } = vi.hoisted(() => ({ getMainWindowMock: vi.fn<() => BrowserWindow | null>(() => null), @@ -11,15 +11,15 @@ const { setupMock, milestoneMock, getMainWindowMock } = vi.hoisted(() => ({ })) vi.mock('electron', () => ({ app: {}, ipcMain: {} })) -vi.mock('../updater/updater-service', () => ({ setupAutoUpdater: setupMock })) -vi.mock('../updater/lifecycle/electron-updater-proxy-session', () => ({ +vi.mock('../updater-service', () => ({ setupAutoUpdater: setupMock })) +vi.mock('./electron-updater-proxy-session', () => ({ applyProxyToElectronUpdaterSession: vi.fn() })) -vi.mock('../ipc/ui', () => ({ +vi.mock('../../ipc/ui', () => ({ isTrustedUIRenderer: () => true, getTrustedUIRendererWindow: getMainWindowMock })) -vi.mock('../startup/diagnostics/startup-diagnostics', () => ({ +vi.mock('../../startup/diagnostics/startup-diagnostics', () => ({ logStartupMilestone: milestoneMock })) diff --git a/src/main/window/main-window-updater.ts b/src/main/updater/lifecycle/main-window-updater.ts similarity index 88% rename from src/main/window/main-window-updater.ts rename to src/main/updater/lifecycle/main-window-updater.ts index 1498793ff96..c634b515e72 100644 --- a/src/main/window/main-window-updater.ts +++ b/src/main/updater/lifecycle/main-window-updater.ts @@ -1,14 +1,17 @@ import { app, ipcMain } from 'electron' import type { BrowserWindow, IpcMainInvokeEvent } from 'electron' -import type { ReleaseBuildListResult, UpdateCheckOptions } from '../../shared/update-status-types' -import { RELEASE_CHANNELS, type ReleaseChannel } from '../../shared/release-channel' -import { getTrustedUIRendererWindow, isTrustedUIRenderer } from '../ipc/ui' -import type { Store } from '../persistence/loading-store/store' -import { logStartupMilestone } from '../startup/diagnostics/startup-diagnostics' -import { applyProxyToElectronUpdaterSession } from '../updater/lifecycle/electron-updater-proxy-session' -import { updaterErrorMessage } from '../updater/lifecycle/updater-coded-error' -import { updaterCodedMessage } from '../../shared/updater-failure-copy' -import { normalizeUpdateDelayDays } from '../../shared/update-delay' +import type { + ReleaseBuildListResult, + UpdateCheckOptions +} from '../../../shared/update-status-types' +import { RELEASE_CHANNELS, type ReleaseChannel } from '../../../shared/release-channel' +import { getTrustedUIRendererWindow, isTrustedUIRenderer } from '../../ipc/ui' +import type { Store } from '../../persistence/loading-store/store' +import { logStartupMilestone } from '../../startup/diagnostics/startup-diagnostics' +import { applyProxyToElectronUpdaterSession } from './electron-updater-proxy-session' +import { updaterErrorMessage } from './updater-coded-error' +import { updaterCodedMessage } from '../../../shared/updater-failure-copy' +import { normalizeUpdateDelayDays } from '../../../shared/update-delay' import { checkForUpdatesFromMenu, dismissAvailableUpdate, @@ -20,12 +23,9 @@ import { quitAndInstall, setupAutoUpdater, showLinuxPackage -} from '../updater/updater-service' -import type { - PreQuitCleanupFailureMode, - UpdateInstallMode -} from '../updater/lifecycle/updater-state' -import type { UpdaterSetupOptions } from '../updater/lifecycle/updater-setup' +} from '../updater-service' +import type { PreQuitCleanupFailureMode, UpdateInstallMode } from './updater-state' +import type { UpdaterSetupOptions } from './updater-setup' const UPDATER_SETUP_FALLBACK_MS = 15_000 diff --git a/src/main/window/updater-package-recovery-ipc.test.ts b/src/main/updater/lifecycle/updater-package-recovery-ipc.test.ts similarity index 81% rename from src/main/window/updater-package-recovery-ipc.test.ts rename to src/main/updater/lifecycle/updater-package-recovery-ipc.test.ts index 60c1ab212c9..30adc0f477f 100644 --- a/src/main/window/updater-package-recovery-ipc.test.ts +++ b/src/main/updater/lifecycle/updater-package-recovery-ipc.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import type { IpcMainInvokeEvent, WebContents } from 'electron' -import type { Store } from '../persistence/loading-store/store' -import type * as IpcUiModule from '../ipc/ui' +import type { Store } from '../../persistence/loading-store/store' +import type * as IpcUiModule from '../../ipc/ui' const { handleMock, @@ -31,27 +31,27 @@ vi.mock('electron', () => ({ powerMonitor: { on: vi.fn(), off: vi.fn() } })) -vi.mock('../ipc/repos', () => ({ registerRepoHandlers: vi.fn() })) -vi.mock('../ipc/worktrees', () => ({ registerWorktreeHandlers: vi.fn() })) -vi.mock('../ipc/worktrees/listing/worktree-change-invalidators', () => ({ +vi.mock('../../ipc/repos', () => ({ registerRepoHandlers: vi.fn() })) +vi.mock('../../ipc/worktrees', () => ({ registerWorktreeHandlers: vi.fn() })) +vi.mock('../../ipc/worktrees/listing/worktree-change-invalidators', () => ({ runWorktreeChangeInvalidators: vi.fn() })) -vi.mock('../ipc/pty/provider/registry', () => ({ getLocalPtyProvider: vi.fn() })) -vi.mock('../ipc/pty/register-handlers', () => ({ registerPtyHandlers: vi.fn() })) -vi.mock('../memory/hydrate-local-pty-registry', () => ({ +vi.mock('../../ipc/pty/provider/registry', () => ({ getLocalPtyProvider: vi.fn() })) +vi.mock('../../ipc/pty/register-handlers', () => ({ registerPtyHandlers: vi.fn() })) +vi.mock('../../memory/hydrate-local-pty-registry', () => ({ hydrateLocalPtyRegistryAtBoot: vi.fn() })) -vi.mock('../browser/guest-lifecycle/manager/browser-manager', () => ({ +vi.mock('../../browser/guest-lifecycle/manager/browser-manager', () => ({ browserManager: { unregisterAll: vi.fn() } })) -vi.mock('../macos-permissions/macos-tcc-prompt-notice', () => ({ +vi.mock('../../macos-permissions/macos-tcc-prompt-notice', () => ({ acknowledgePendingTccPromptNotice: vi.fn(), consumePendingTccPromptNotice: vi.fn(), dismissTccPromptNotice: vi.fn(), releasePendingTccPromptNotice: vi.fn() })) -vi.mock('../updater/updater-service', () => ({ +vi.mock('../updater-service', () => ({ checkForUpdates: vi.fn(), checkForUpdatesFromMenu: vi.fn(), downloadUpdate: vi.fn(), @@ -65,7 +65,7 @@ vi.mock('../updater/updater-service', () => ({ })) // Keep the seam mock for wiring proof but delegate to the real predicate for sender-authorization branches. -vi.mock('../ipc/ui', async (importOriginal) => ({ +vi.mock('../../ipc/ui', async (importOriginal) => ({ ...(await importOriginal()), isTrustedUIRenderer: (sender: unknown) => isTrustedUIRendererMock(sender) })) @@ -120,7 +120,7 @@ function expectBothChannelsRejected(sender: Partial): void { describe('updater linux package recovery IPC handlers', () => { beforeAll(async () => { // Read the real predicate using the same trusted-ID state these tests set. - actualUi = await vi.importActual('../ipc/ui') + actualUi = await vi.importActual('../../ipc/ui') }) beforeEach(() => { @@ -141,17 +141,6 @@ describe('updater linux package recovery IPC handlers', () => { vi.unstubAllEnvs() }) - it('serves the current main UI renderer', async () => { - const event = senderEvent(webContents({})) - - await expect(getHandler(RECOVERY_CHANNELS[0])(event)).resolves.toEqual({ - ok: true, - command: "sudo apt install -- ''", - packageFileName: 'p' - }) - await expect(getHandler(RECOVERY_CHANNELS[1])(event)).resolves.toBeUndefined() - }) - // Exercise distinct real authorization branches, not relabelled mock verdicts. it.each([ ['a guest webview', webContents({ getType: () => 'webview' })], diff --git a/src/main/window/focus-existing-window.ts b/src/main/window/activation/focus-existing-window.ts similarity index 100% rename from src/main/window/focus-existing-window.ts rename to src/main/window/activation/focus-existing-window.ts diff --git a/src/main/window/foreground-activation-policy.test.ts b/src/main/window/activation/foreground-activation-policy.test.ts similarity index 55% rename from src/main/window/foreground-activation-policy.test.ts rename to src/main/window/activation/foreground-activation-policy.test.ts index aa37676f455..c89d058302e 100644 --- a/src/main/window/foreground-activation-policy.test.ts +++ b/src/main/window/activation/foreground-activation-policy.test.ts @@ -1,43 +1,9 @@ -import type { BrowserWindow } from 'electron' import { describe, expect, it, vi } from 'vitest' vi.mock('electron', () => ({ app: { dock: { hide: vi.fn() }, setActivationPolicy: vi.fn() } })) -const { - applyBackgroundActivationPolicy, - applyServeDesktopWindowActivationPolicy, - showWindowWithoutStealingFocus -} = await import('./foreground-activation-policy') - -function makeWindow(destroyed = false): BrowserWindow & { - show: ReturnType - showInactive: ReturnType -} { - return { - isDestroyed: () => destroyed, - show: vi.fn(() => {}), - showInactive: vi.fn(() => {}) - } as unknown as BrowserWindow & { - show: ReturnType - showInactive: ReturnType - } -} - -describe('showWindowWithoutStealingFocus', () => { - it('never reveals an explicitly background window', () => { - const window = makeWindow() - showWindowWithoutStealingFocus(window, { ORCA_BACKGROUND_LAUNCH: '1' }) - expect(window.showInactive).not.toHaveBeenCalled() - expect(window.show).not.toHaveBeenCalled() - }) - - it('still reveals explicitly headful E2E without activation', () => { - const window = makeWindow() - showWindowWithoutStealingFocus(window, { ORCA_E2E_HEADFUL: '1' }) - expect(window.showInactive).toHaveBeenCalledOnce() - expect(window.show).not.toHaveBeenCalled() - }) -}) +const { applyBackgroundActivationPolicy, applyServeDesktopWindowActivationPolicy } = + await import('./foreground-activation-policy') describe('applyBackgroundActivationPolicy', () => { function makeApp() { diff --git a/src/main/window/foreground-activation-policy.ts b/src/main/window/activation/foreground-activation-policy.ts similarity index 100% rename from src/main/window/foreground-activation-policy.ts rename to src/main/window/activation/foreground-activation-policy.ts diff --git a/src/main/window/macos-app-activation.ts b/src/main/window/activation/macos-app-activation.ts similarity index 100% rename from src/main/window/macos-app-activation.ts rename to src/main/window/activation/macos-app-activation.ts diff --git a/src/main/window/macos-desktop-handoff.ts b/src/main/window/activation/macos-desktop-handoff.ts similarity index 100% rename from src/main/window/macos-desktop-handoff.ts rename to src/main/window/activation/macos-desktop-handoff.ts diff --git a/src/main/window/attach-main-window-services.ts b/src/main/window/attach-main-window-services.ts index af4bbf2911a..e76388c6a9e 100644 --- a/src/main/window/attach-main-window-services.ts +++ b/src/main/window/attach-main-window-services.ts @@ -36,23 +36,21 @@ import { openCodeHookService, openCode2HookService } from '../opencode/status-ho import { listLiveDaemonPtyIds } from '../daemon/providers/lifecycle/daemon-provider-state' import { hydrateLocalPtyRegistryAtBoot } from '../memory/hydrate-local-pty-registry' import type { ClaudeRuntimeAuthPreparation } from '../claude-accounts/runtime-auth/runtime-auth-types' -import { getKnownWorktreeIdsForHistoryGc } from './history-gc-worktree-ids' +import { getKnownWorktreeIdsForHistoryGc } from '../pty/history/history-gc-worktree-ids' import type { ClaudeAccountSelectionTarget } from '../claude-accounts/selection/runtime-selection' import { scheduleWorktreeBaseDirectoryWatcherSync, setWorktreeBaseDirectoryWatcherSyncContext } from '../ipc/worktrees/creation/worktree-base-directory-watcher' import { startFolderRepoGitUpgradeWatch } from '../ipc/folder-repo-git-upgrade' -import { scheduleMainWindowAutoUpdaterSetup } from './main-window-updater' +import { scheduleMainWindowAutoUpdaterSetup } from '../updater/lifecycle/main-window-updater' import { registerRuntimeWindowLifecycle } from './runtime-window-lifecycle' import { ensureStructuredAgentSessionHostUnlessRefused } from '../runtime/agent-session/structured-agent-session-host-refusal' import { getAgentSessionAttachmentStore } from '../native-chat/agent-session-attachments/agent-session-attachment-store-registry' -import { registerDroppedPathPreparation } from './dropped-path-preparation-ipc' -import { isPluginFramePermissionRequest } from './host-frame-permission-policy' +import { registerDroppedPathPreparation } from './file-drop/dropped-path-preparation-ipc' +import { isPluginFramePermissionRequest } from './security/host-frame-permission-policy' import { installPluginDesktopFrameServices } from '../plugins/panels/desktop-actions/plugin-frame-desktop-services' -export { ensureAutoUpdaterConfigured, registerUpdaterHandlers } from './main-window-updater' - let appReloadHandlerTokenCounter = 0 let activeAppReloadHandlerToken: number | null = null let tccPromptHandlerTokenCounter = 0 diff --git a/src/main/window/clipboard-agent-session-attachment-paste.test.ts b/src/main/window/clipboard/clipboard-agent-session-attachment-paste.test.ts similarity index 87% rename from src/main/window/clipboard-agent-session-attachment-paste.test.ts rename to src/main/window/clipboard/clipboard-agent-session-attachment-paste.test.ts index ababc677469..9931c6994fd 100644 --- a/src/main/window/clipboard-agent-session-attachment-paste.test.ts +++ b/src/main/window/clipboard/clipboard-agent-session-attachment-paste.test.ts @@ -3,7 +3,7 @@ import { mkdtemp, readFile, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { AgentSessionAttachmentStore } from '../native-chat/agent-session-attachments/agent-session-attachment-store' +import { AgentSessionAttachmentStore } from '../../native-chat/agent-session-attachments/agent-session-attachment-store' const mocks = vi.hoisted(() => ({ handle: vi.fn(), @@ -21,22 +21,20 @@ vi.mock('electron', () => ({ ipcMain: { removeHandler: vi.fn(), handle: mocks.handle }, nativeImage: { createFromBuffer: vi.fn() } })) -vi.mock('../ipc/runtime-environments/runtime-environment-transport-routing', () => ({ +vi.mock('../../ipc/runtime-environments/runtime-environment-transport-routing', () => ({ callRuntimeEnvironment: mocks.callRuntimeEnvironment })) -vi.mock('../../shared/remote-runtime/host-store/runtime-environment-store', () => ({ +vi.mock('../../../shared/remote-runtime/host-store/runtime-environment-store', () => ({ resolveEnvironment: (_userDataPath: string, selector: string) => ({ id: selector }) })) vi.mock('./clipboard-remote-file-copy', () => ({ cleanupExpiredRemoteClipboardFiles: vi.fn(async () => {}), scheduleLegacyRemoteClipboardFileCleanup: vi.fn() })) -vi.mock('./dashboard-popout-window', () => ({ isDashboardPopoutRenderer: () => false })) +vi.mock('../dashboard-popout-window', () => ({ isDashboardPopoutRenderer: () => false })) -import { - registerClipboardHandlers, - setTrustedClipboardRendererWebContentsId -} from './clipboard-ipc-handlers' +import { registerClipboardHandlers } from './clipboard-ipc-handlers' +import { setTrustedClipboardRendererWebContentsId } from './clipboard-ipc-sender-trust' function saveHandler(): (...args: unknown[]) => Promise { const entry = mocks.handle.mock.calls.find( diff --git a/src/main/window/clipboard-copied-file-paths.test.ts b/src/main/window/clipboard/clipboard-copied-file-paths.test.ts similarity index 100% rename from src/main/window/clipboard-copied-file-paths.test.ts rename to src/main/window/clipboard/clipboard-copied-file-paths.test.ts diff --git a/src/main/window/clipboard-copied-file-paths.ts b/src/main/window/clipboard/clipboard-copied-file-paths.ts similarity index 100% rename from src/main/window/clipboard-copied-file-paths.ts rename to src/main/window/clipboard/clipboard-copied-file-paths.ts diff --git a/src/main/window/clipboard-dashboard-popout-access.test.ts b/src/main/window/clipboard/clipboard-dashboard-popout-access.test.ts similarity index 94% rename from src/main/window/clipboard-dashboard-popout-access.test.ts rename to src/main/window/clipboard/clipboard-dashboard-popout-access.test.ts index 889afa780e0..62ef2923e39 100644 --- a/src/main/window/clipboard-dashboard-popout-access.test.ts +++ b/src/main/window/clipboard/clipboard-dashboard-popout-access.test.ts @@ -36,16 +36,14 @@ vi.mock('electron', () => ({ nativeImage: { createFromBuffer: vi.fn() } })) -vi.mock('./dashboard-popout-window', () => ({ isDashboardPopoutRenderer })) +vi.mock('../dashboard-popout-window', () => ({ isDashboardPopoutRenderer })) vi.mock('./clipboard-remote-file-copy', () => ({ cleanupExpiredRemoteClipboardFiles: vi.fn(async () => undefined), scheduleLegacyRemoteClipboardFileCleanup: vi.fn() })) -import { - registerClipboardHandlers, - setTrustedClipboardRendererWebContentsId -} from './clipboard-ipc-handlers' +import { registerClipboardHandlers } from './clipboard-ipc-handlers' +import { setTrustedClipboardRendererWebContentsId } from './clipboard-ipc-sender-trust' const popoutEvent = { sender: { diff --git a/src/main/window/clipboard-file-copy.ts b/src/main/window/clipboard/clipboard-file-copy.ts similarity index 98% rename from src/main/window/clipboard-file-copy.ts rename to src/main/window/clipboard/clipboard-file-copy.ts index 43886735ac2..c2cc2044eeb 100644 --- a/src/main/window/clipboard-file-copy.ts +++ b/src/main/window/clipboard/clipboard-file-copy.ts @@ -2,7 +2,7 @@ import { isAbsolute } from 'node:path' import { pathToFileURL } from 'node:url' import type { ProcessResult, ProcessSpec } from '@orca/process-host/process-spec' import { windowsPowerShellPath } from '@orca/process-host/windows-system-binary' -import { withTimeout } from '../../shared/promise-timeout-fallback' +import { withTimeout } from '../../../shared/promise-timeout-fallback' export type ClipboardFileResult = { ok: boolean; reason?: string } diff --git a/src/main/window/clipboard-image-png-encode.ts b/src/main/window/clipboard/clipboard-image-png-encode.ts similarity index 84% rename from src/main/window/clipboard-image-png-encode.ts rename to src/main/window/clipboard/clipboard-image-png-encode.ts index a40df8f3641..701ebd1a0a7 100644 --- a/src/main/window/clipboard-image-png-encode.ts +++ b/src/main/window/clipboard/clipboard-image-png-encode.ts @@ -1,5 +1,5 @@ import type { NativeImage } from 'electron' -import { encodeClipboardImageWithinLimit } from '../../shared/clipboard-image' +import { encodeClipboardImageWithinLimit } from '../../../shared/clipboard-image' export function encodeClipboardNativeImagePng( image: Pick diff --git a/src/main/window/clipboard-image-source.ts b/src/main/window/clipboard/clipboard-image-source.ts similarity index 94% rename from src/main/window/clipboard-image-source.ts rename to src/main/window/clipboard/clipboard-image-source.ts index 6bcb4e168ee..1f64770008f 100644 --- a/src/main/window/clipboard-image-source.ts +++ b/src/main/window/clipboard/clipboard-image-source.ts @@ -1,4 +1,4 @@ -import { clipboardFormatsIncludeImage } from '../../shared/clipboard-image' +import { clipboardFormatsIncludeImage } from '../../../shared/clipboard-image' import { readWindowsCopiedImageFilePath, type WindowsClipboardFileFormats diff --git a/src/main/window/clipboard-image-temp-file.ts b/src/main/window/clipboard/clipboard-image-temp-file.ts similarity index 84% rename from src/main/window/clipboard-image-temp-file.ts rename to src/main/window/clipboard/clipboard-image-temp-file.ts index b1ea02406b8..ba443aebdbb 100644 --- a/src/main/window/clipboard-image-temp-file.ts +++ b/src/main/window/clipboard/clipboard-image-temp-file.ts @@ -2,16 +2,16 @@ import fs from 'node:fs/promises' import path from 'node:path' import { randomUUID } from 'node:crypto' -import { SshHostNotServedError } from '../providers/ssh-host-not-served-error' -import { getAppEnvironment } from '../../shared/app-environment' -import { assertClipboardImageByteLengthWithinLimit } from '../../shared/clipboard-image' +import { SshHostNotServedError } from '../../providers/ssh-host-not-served-error' +import { getAppEnvironment } from '../../../shared/app-environment' +import { assertClipboardImageByteLengthWithinLimit } from '../../../shared/clipboard-image' import { agentSessionPastedImageName, sanitizeAgentSessionAttachmentName, type AgentSessionAttachmentClipboardTarget -} from '../../shared/agent-session/agent-session-attachments' -import { detectImageFileExtension } from '../../shared/image-file-signature' -import { nativeChatPasteFolder } from './native-chat-paste-files' +} from '../../../shared/agent-session/agent-session-attachments' +import { detectImageFileExtension } from '../../../shared/image-file-signature' +import { nativeChatPasteFolder } from '../../native-chat/paste-files/native-chat-paste-files' export type SaveClipboardImageAsTempFileArgs = { connectionId?: string | null diff --git a/src/main/window/clipboard-image-thumbnail.ts b/src/main/window/clipboard/clipboard-image-thumbnail.ts similarity index 96% rename from src/main/window/clipboard-image-thumbnail.ts rename to src/main/window/clipboard/clipboard-image-thumbnail.ts index 3d9e9633eea..9976597f0ca 100644 --- a/src/main/window/clipboard-image-thumbnail.ts +++ b/src/main/window/clipboard/clipboard-image-thumbnail.ts @@ -2,7 +2,7 @@ import { clipboardImageThumbnailSize, type ClipboardImageDimensions, type ClipboardImageThumbnail -} from '../../shared/clipboard-image' +} from '../../../shared/clipboard-image' /** Minimal NativeImage surface for tests without an Electron runtime. */ export type ClipboardImageLike = { diff --git a/src/main/window/clipboard-ipc-handlers.ts b/src/main/window/clipboard/clipboard-ipc-handlers.ts similarity index 91% rename from src/main/window/clipboard-ipc-handlers.ts rename to src/main/window/clipboard/clipboard-ipc-handlers.ts index 4ab7419c76c..0696415f903 100644 --- a/src/main/window/clipboard-ipc-handlers.ts +++ b/src/main/window/clipboard/clipboard-ipc-handlers.ts @@ -2,10 +2,10 @@ import { app, clipboard, ipcMain, nativeImage } from 'electron' import { randomUUID } from 'node:crypto' import { open, stat } from 'node:fs/promises' import { runProcess } from '@orca/process-host' -import type { Store } from '../persistence/loading-store/store' -import { PATH_ACCESS_DENIED_MESSAGE } from '../ipc/filesystem/access/filesystem-auth' -import { resolveDesktopAuthorizedPath } from '../ipc/filesystem/access/local-file-access-resolution' -import { isENOENT } from '../ipc/filesystem/access/filesystem-path-containment' +import type { Store } from '../../persistence/loading-store/store' +import { PATH_ACCESS_DENIED_MESSAGE } from '../../ipc/filesystem/access/filesystem-auth' +import { resolveDesktopAuthorizedPath } from '../../ipc/filesystem/access/local-file-access-resolution' +import { isENOENT } from '../../ipc/filesystem/access/filesystem-path-containment' import { assertTrustedClipboardSender, assertTrustedClipboardTextSender @@ -14,8 +14,8 @@ import { assertClipboardTextWriteWithinLimitWithYield, assertClipboardTextWithinLimitWithYield, type ReadClipboardTextOptions -} from '../../shared/clipboard-text' -import { clipboardHtmlMayHaveLinks } from '../../shared/clipboard-link-html' +} from '../../../shared/clipboard-text' +import { clipboardHtmlMayHaveLinks } from '../../../shared/clipboard-link-html' import { saveClipboardImageBufferAsTempFile, type SaveClipboardImageAsTempFileArgs @@ -25,19 +25,19 @@ import { assertClipboardImageByteLengthWithinLimit, assertClipboardImageDimensionsWithinLimit, type ClipboardImageThumbnail -} from '../../shared/clipboard-image' +} from '../../../shared/clipboard-image' import { writeFileToClipboard, type ClipboardFileDeps, type ClipboardFileResult } from './clipboard-file-copy' -import { SshHostNotServedError } from '../providers/ssh-host-not-served-error' +import { SshHostNotServedError } from '../../providers/ssh-host-not-served-error' import { cleanupExpiredRemoteClipboardFiles, scheduleLegacyRemoteClipboardFileCleanup } from './clipboard-remote-file-copy' import { saveClipboardImageBufferInRuntime } from './clipboard-runtime-image-upload' -import { uploadPasteToAgentSessionAttachments } from '../ipc/agent-session-attachment-upload' +import { uploadPasteToAgentSessionAttachments } from '../../ipc/agent-session-attachment-upload' import { readWindowsClipboardImageFileAsPng } from './clipboard-windows-image-file' import { readClipboardImageSource } from './clipboard-image-source' import { readClipboardCopiedFilePaths } from './clipboard-copied-file-paths' @@ -50,12 +50,10 @@ import { publishClipboardOperation, publishClipboardText } from './clipboard-text-publisher' -import { restoreNativeChatPastes } from './native-chat-paste-files' +import { restoreNativeChatPastes } from '../../native-chat/paste-files/native-chat-paste-files' import { savePastedTextAsFile, type SavePastedTextAsFileArgs } from './native-chat-pasted-text-file' -import { agentSessionPastedImageName } from '../../shared/agent-session/agent-session-attachments' -import type { AgentSessionAttachmentStore } from '../native-chat/agent-session-attachments/agent-session-attachment-store' - -export { setTrustedClipboardRendererWebContentsId } from './clipboard-ipc-sender-trust' +import { agentSessionPastedImageName } from '../../../shared/agent-session/agent-session-attachments' +import type { AgentSessionAttachmentStore } from '../../native-chat/agent-session-attachments/agent-session-attachment-store' type ClipboardWriteFileRequest = { filePath: string diff --git a/src/main/window/clipboard-ipc-sender-trust.ts b/src/main/window/clipboard/clipboard-ipc-sender-trust.ts similarity index 86% rename from src/main/window/clipboard-ipc-sender-trust.ts rename to src/main/window/clipboard/clipboard-ipc-sender-trust.ts index 8961d5147de..7febecd0dff 100644 --- a/src/main/window/clipboard-ipc-sender-trust.ts +++ b/src/main/window/clipboard/clipboard-ipc-sender-trust.ts @@ -1,6 +1,6 @@ import type { IpcMainInvokeEvent, WebContents } from 'electron' -import { isTrustedMainRendererSender } from '../ipc/main-renderer-sender-trust' -import { isDashboardPopoutRenderer } from './dashboard-popout-window' +import { isTrustedMainRendererSender } from '../../ipc/main-renderer-sender-trust' +import { isDashboardPopoutRenderer } from '../dashboard-popout-window' let trustedClipboardRendererWebContentsId: number | null = null diff --git a/src/main/window/clipboard-macos-image-paste.test.ts b/src/main/window/clipboard/clipboard-macos-image-paste.test.ts similarity index 86% rename from src/main/window/clipboard-macos-image-paste.test.ts rename to src/main/window/clipboard/clipboard-macos-image-paste.test.ts index 66c5f3b103a..3b3e9bd95b6 100644 --- a/src/main/window/clipboard-macos-image-paste.test.ts +++ b/src/main/window/clipboard/clipboard-macos-image-paste.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type * as FsPromises from 'node:fs/promises' -import { installFakeAppEnvironment } from '../../../config/scripts/test-runtime/environment/vitest-host-ports-setup' -import '../../shared/clipboard-image' +import { installFakeAppEnvironment } from '../../../../config/scripts/test-runtime/environment/vitest-host-ports-setup' +import '../../../shared/clipboard-image' const mocks = vi.hoisted(() => ({ handlers: new Map unknown>(), @@ -32,22 +32,20 @@ vi.mock('node:fs/promises', async (importOriginal) => { const fs = await importOriginal() return { ...fs, default: { ...fs, writeFile: mocks.writeFile } } }) -vi.mock('../persistence/loading-store/store', () => ({ Store: vi.fn() })) +vi.mock('../../persistence/loading-store/store', () => ({ Store: vi.fn() })) vi.mock('./clipboard-remote-file-copy', () => ({ cleanupExpiredRemoteClipboardFiles: vi.fn(), scheduleLegacyRemoteClipboardFileCleanup: vi.fn() })) -vi.mock('./native-chat-paste-files', () => ({ +vi.mock('../../native-chat/paste-files/native-chat-paste-files', () => ({ restoreNativeChatPastes: vi.fn(), nativeChatPasteFolder: () => '/tmp/native-chat-pastes' })) -vi.mock('./dashboard-popout-window', () => ({ isDashboardPopoutRenderer: () => false })) +vi.mock('../dashboard-popout-window', () => ({ isDashboardPopoutRenderer: () => false })) -import { Store } from '../persistence/loading-store/store' -import { - registerClipboardHandlers, - setTrustedClipboardRendererWebContentsId -} from './clipboard-ipc-handlers' +import { Store } from '../../persistence/loading-store/store' +import { registerClipboardHandlers } from './clipboard-ipc-handlers' +import { setTrustedClipboardRendererWebContentsId } from './clipboard-ipc-sender-trust' const event = { sender: { diff --git a/src/main/window/clipboard-native-image.ts b/src/main/window/clipboard/clipboard-native-image.ts similarity index 84% rename from src/main/window/clipboard-native-image.ts rename to src/main/window/clipboard/clipboard-native-image.ts index b93b8273478..1028333734c 100644 --- a/src/main/window/clipboard-native-image.ts +++ b/src/main/window/clipboard/clipboard-native-image.ts @@ -2,8 +2,8 @@ import { clipboard, nativeImage, type NativeImage } from 'electron' import { assertClipboardImageByteLengthWithinLimit, assertClipboardImageDimensionsWithinLimit -} from '../../shared/clipboard-image' -import { readRasterImageDimensions } from '../../shared/file-preview/raster-image-dimensions' +} from '../../../shared/clipboard-image' +import { readRasterImageDimensions } from '../../../shared/file-preview/raster-image-dimensions' export function readClipboardNativeImage(): NativeImage { const image = clipboard.readImage() diff --git a/src/main/window/clipboard-plain-text-read.test.ts b/src/main/window/clipboard/clipboard-plain-text-read.test.ts similarity index 100% rename from src/main/window/clipboard-plain-text-read.test.ts rename to src/main/window/clipboard/clipboard-plain-text-read.test.ts diff --git a/src/main/window/clipboard-plain-text-read.ts b/src/main/window/clipboard/clipboard-plain-text-read.ts similarity index 100% rename from src/main/window/clipboard-plain-text-read.ts rename to src/main/window/clipboard/clipboard-plain-text-read.ts diff --git a/src/main/window/clipboard-publisher-executable.test.ts b/src/main/window/clipboard/clipboard-publisher-executable.test.ts similarity index 100% rename from src/main/window/clipboard-publisher-executable.test.ts rename to src/main/window/clipboard/clipboard-publisher-executable.test.ts diff --git a/src/main/window/clipboard-publisher-executable.ts b/src/main/window/clipboard/clipboard-publisher-executable.ts similarity index 100% rename from src/main/window/clipboard-publisher-executable.ts rename to src/main/window/clipboard/clipboard-publisher-executable.ts diff --git a/src/main/window/clipboard-remote-file-copy.ts b/src/main/window/clipboard/clipboard-remote-file-copy.ts similarity index 100% rename from src/main/window/clipboard-remote-file-copy.ts rename to src/main/window/clipboard/clipboard-remote-file-copy.ts diff --git a/src/main/window/clipboard-remote-file-staging-fs.test.ts b/src/main/window/clipboard/clipboard-remote-file-staging-fs.test.ts similarity index 100% rename from src/main/window/clipboard-remote-file-staging-fs.test.ts rename to src/main/window/clipboard/clipboard-remote-file-staging-fs.test.ts diff --git a/src/main/window/clipboard-remote-file-staging.test.ts b/src/main/window/clipboard/clipboard-remote-file-staging.test.ts similarity index 100% rename from src/main/window/clipboard-remote-file-staging.test.ts rename to src/main/window/clipboard/clipboard-remote-file-staging.test.ts diff --git a/src/main/window/clipboard-remote-file-staging.ts b/src/main/window/clipboard/clipboard-remote-file-staging.ts similarity index 97% rename from src/main/window/clipboard-remote-file-staging.ts rename to src/main/window/clipboard/clipboard-remote-file-staging.ts index d7043d22869..114f5f8272a 100644 --- a/src/main/window/clipboard-remote-file-staging.ts +++ b/src/main/window/clipboard/clipboard-remote-file-staging.ts @@ -5,10 +5,10 @@ import { getOwnedTempStagingRoot, isDirectChild, isMissingPathError, - isSafeOwnedDirectory, removeOwnedDirectory, sweepExpiredOwnedDirectories -} from './owned-temp-staging-root' +} from '../../filesystem/owned-temp-staging-root' +import { isSafeOwnedDirectory } from '../../../shared/owned-private-directory' const REMOTE_CLIPBOARD_STAGING_ROOT_NAME = 'orca-clipboard-files' const REMOTE_CLIPBOARD_LEGACY_PREFIX = 'orca-clipboard-file-' diff --git a/src/main/window/clipboard-runtime-image-upload.ts b/src/main/window/clipboard/clipboard-runtime-image-upload.ts similarity index 96% rename from src/main/window/clipboard-runtime-image-upload.ts rename to src/main/window/clipboard/clipboard-runtime-image-upload.ts index 6b1f293503f..950baab483b 100644 --- a/src/main/window/clipboard-runtime-image-upload.ts +++ b/src/main/window/clipboard/clipboard-runtime-image-upload.ts @@ -1,5 +1,5 @@ -import { assertClipboardImageByteLengthWithinLimit } from '../../shared/clipboard-image' -import { callRuntimeEnvironment } from '../ipc/runtime-environments/runtime-environment-transport-routing' +import { assertClipboardImageByteLengthWithinLimit } from '../../../shared/clipboard-image' +import { callRuntimeEnvironment } from '../../ipc/runtime-environments/runtime-environment-transport-routing' const CLIPBOARD_IMAGE_UPLOAD_CHUNK_BASE64_CHARS = 512 * 1024 const CLIPBOARD_IMAGE_SINGLE_FRAME_FALLBACK_BASE64_CHARS = 256 * 1024 diff --git a/src/main/window/clipboard-text-publisher.test.ts b/src/main/window/clipboard/clipboard-text-publisher.test.ts similarity index 56% rename from src/main/window/clipboard-text-publisher.test.ts rename to src/main/window/clipboard/clipboard-text-publisher.test.ts index 3f585fec2d8..64c7c3fabfe 100644 --- a/src/main/window/clipboard-text-publisher.test.ts +++ b/src/main/window/clipboard/clipboard-text-publisher.test.ts @@ -2,10 +2,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { ProcessResult, ProcessSpec } from '@orca/process-host/process-spec' import { assertClipboardTextWriteWithinLimitWithYield, - CLIPBOARD_TEXT_MEASURE_YIELD_CODE_UNITS, CLIPBOARD_TEXT_WRITE_MAX_BYTES -} from '../../shared/clipboard-text' -import { CLIPBOARD_IMAGE_MAX_BASE64_CHARS } from '../../shared/clipboard-image' +} from '../../../shared/clipboard-text' +import { CLIPBOARD_IMAGE_MAX_BASE64_CHARS } from '../../../shared/clipboard-image' import { createClipboardPublisher } from './clipboard-text-publisher' vi.mock('electron', () => ({ app: {}, clipboard: {} })) @@ -34,38 +33,6 @@ describe('terminal clipboard writes', () => { run.mockResolvedValue(ok) }) - it('publishes different and duplicate text without asking Electron to own the selection', async () => { - const copy = writer() - const text = '遥か\r\nline two\n' - await copy(text) - await copy(text) - expect(run).toHaveBeenCalledTimes(2) - expect(run).toHaveBeenCalledWith({ - program: '/bundle/bin/orca-wayland-clipboard', - input: text, - timeoutMs: 2000, - stdio: ['pipe', 'ignore', 'ignore'] - }) - expect(write).not.toHaveBeenCalled() - expect(clearCache).toHaveBeenCalledTimes(2) - }) - - it('only discards a rejected Chromium source after the new owner is acknowledged', async () => { - let release: (value: typeof ok) => void = () => undefined - run.mockImplementationOnce( - () => - new Promise((resolve) => { - release = resolve - }) - ) - const pending = writer()('copy') - await vi.waitFor(() => expect(run).toHaveBeenCalledTimes(1)) - expect(clearCache).not.toHaveBeenCalled() - release(ok) - await pending - expect(clearCache).toHaveBeenCalledTimes(1) - }) - it.each([ ['timeout', { ...ok, code: null, timedOut: true }], ['helper failure', { ...ok, code: 70 }] @@ -76,20 +43,6 @@ describe('terminal clipboard writes', () => { expect(clearCache).not.toHaveBeenCalled() }) - it('preserves the existing clipboard when the bundled helper cannot start', async () => { - run.mockRejectedValueOnce(new Error('ENOENT')) - await expect(writer()('copy')).rejects.toThrow('ENOENT') - expect(write).not.toHaveBeenCalled() - expect(clearCache).not.toHaveBeenCalled() - }) - - it('retains verified Electron writes outside native Wayland', async () => { - await writer(false)('copy') - expect(write).toHaveBeenCalledWith('copy', 'terminal') - expect(run).not.toHaveBeenCalled() - expect(clearCache).not.toHaveBeenCalled() - }) - it('rejects oversized text before either clipboard backend is invoked', async () => { await expect(writer()('x'.repeat(CLIPBOARD_TEXT_WRITE_MAX_BYTES + 1))).rejects.toThrow( 'Clipboard text is too large' @@ -98,60 +51,6 @@ describe('terminal clipboard writes', () => { expect(write).not.toHaveBeenCalled() }) - it.each([false, true])( - 'keeps a newer copy after yielding validation (Wayland=%s)', - async (wayland) => { - vi.useFakeTimers() - try { - const copy = writer(wayland) - const large = 'x'.repeat(CLIPBOARD_TEXT_MEASURE_YIELD_CODE_UNITS * 2) - const first = copy(large, 'terminal') - const second = copy('newer', 'ordinary') - - await vi.runAllTimersAsync() - await Promise.all([first, second]) - - if (wayland) { - expect(run.mock.calls.map(([spec]) => spec.input === large)).toEqual([true, false]) - expect(run.mock.calls[1]?.[0].input).toBe('newer') - expect(write).not.toHaveBeenCalled() - } else { - expect(write.mock.calls.map(([text, kind]) => [text === large, kind])).toEqual([ - [true, 'terminal'], - [false, 'ordinary'] - ]) - expect(write.mock.calls[1]?.[0]).toBe('newer') - expect(run).not.toHaveBeenCalled() - } - } finally { - vi.useRealTimers() - } - } - ) - - it('preserves request order across yielding validation and resumes after failure', async () => { - let release: (text: string) => void = () => undefined - const validate = vi.fn(async (text: string) => text) - validate.mockImplementationOnce( - () => - new Promise((resolve) => { - release = resolve - }) - ) - run.mockRejectedValueOnce(new Error('helper failed')) - const copy = writer(true, validate) - const first = copy('first') - const firstRejected = expect(first).rejects.toThrow('helper failed') - const second = copy('second') - await vi.waitFor(() => expect(validate).toHaveBeenCalledTimes(1)) - expect(run).not.toHaveBeenCalled() - release('first') - await firstRejected - await second - expect(run.mock.calls.map(([spec]) => spec.input)).toEqual(['first', 'second']) - expect(clearCache).toHaveBeenCalledTimes(1) - }) - it.each([ { label: 'count', count: 32, bytes: 1 }, { label: 'retained bytes', count: 1, bytes: CLIPBOARD_IMAGE_MAX_BASE64_CHARS * 2 } diff --git a/src/main/window/clipboard-text-publisher.ts b/src/main/window/clipboard/clipboard-text-publisher.ts similarity index 93% rename from src/main/window/clipboard-text-publisher.ts rename to src/main/window/clipboard/clipboard-text-publisher.ts index 022338b6fed..0a66ffb3098 100644 --- a/src/main/window/clipboard-text-publisher.ts +++ b/src/main/window/clipboard/clipboard-text-publisher.ts @@ -5,13 +5,13 @@ import { assertClipboardTextWriteWithinLimitWithYield, CLIPBOARD_TEXT_WRITE_MAX_BYTES, CLIPBOARD_TEXT_WRITE_TOO_LARGE_ERROR -} from '../../shared/clipboard-text' -import { CLIPBOARD_IMAGE_MAX_BASE64_CHARS } from '../../shared/clipboard-image' -import { isLinuxWaylandSession } from '../../shared/linux-wayland-session' +} from '../../../shared/clipboard-text' +import { CLIPBOARD_IMAGE_MAX_BASE64_CHARS } from '../../../shared/clipboard-image' +import { isLinuxWaylandSession } from '../../../shared/linux-wayland-session' import { writeClipboardTextAndVerify } from './clipboard-text-write-verify' import { stageClipboardPublisher } from './clipboard-publisher-executable' -import { envWithoutAppImageRuntime } from '../pty/shell-startup/appimage-terminal-env' -import { isLegacyAppImageRuntimeIdentity } from '../updater/linux-package/linux-update-package-type' +import { envWithoutAppImageRuntime } from '../../pty/shell-startup/appimage-terminal-env' +import { isLegacyAppImageRuntimeIdentity } from '../../updater/linux-package/linux-update-package-type' const WAYLAND_CLIPBOARD_WRITE_FAILED_ERROR = 'Wayland clipboard write failed' const MAX_PENDING_CLIPBOARD_WRITES = 32 diff --git a/src/main/window/clipboard-text-write-verify.ts b/src/main/window/clipboard/clipboard-text-write-verify.ts similarity index 100% rename from src/main/window/clipboard-text-write-verify.ts rename to src/main/window/clipboard/clipboard-text-write-verify.ts diff --git a/src/main/window/clipboard-windows-image-file.test.ts b/src/main/window/clipboard/clipboard-windows-image-file.test.ts similarity index 97% rename from src/main/window/clipboard-windows-image-file.test.ts rename to src/main/window/clipboard/clipboard-windows-image-file.test.ts index c9657718077..d80d9f45d99 100644 --- a/src/main/window/clipboard-windows-image-file.test.ts +++ b/src/main/window/clipboard/clipboard-windows-image-file.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import { CLIPBOARD_IMAGE_MAX_PIXELS } from '../../shared/clipboard-image' +import { CLIPBOARD_IMAGE_MAX_PIXELS } from '../../../shared/clipboard-image' import { readWindowsClipboardImageFileAsPng } from './clipboard-windows-image-file' function fileNameW(filePath: string): Buffer { diff --git a/src/main/window/clipboard-windows-image-file.ts b/src/main/window/clipboard/clipboard-windows-image-file.ts similarity index 99% rename from src/main/window/clipboard-windows-image-file.ts rename to src/main/window/clipboard/clipboard-windows-image-file.ts index 814f262fe33..c6f8483a91c 100644 --- a/src/main/window/clipboard-windows-image-file.ts +++ b/src/main/window/clipboard/clipboard-windows-image-file.ts @@ -4,7 +4,7 @@ import type { NativeImage } from 'electron' import { assertClipboardImageByteLengthWithinLimit, assertClipboardImageDimensionsWithinLimit -} from '../../shared/clipboard-image' +} from '../../../shared/clipboard-image' import { encodeClipboardNativeImagePng } from './clipboard-image-png-encode' type ClipboardImageFileHandle = Pick diff --git a/src/main/window/native-chat-pasted-text-file.test.ts b/src/main/window/clipboard/native-chat-pasted-text-file.test.ts similarity index 79% rename from src/main/window/native-chat-pasted-text-file.test.ts rename to src/main/window/clipboard/native-chat-pasted-text-file.test.ts index 3770e9f8259..e70c9dacaa8 100644 --- a/src/main/window/native-chat-pasted-text-file.test.ts +++ b/src/main/window/clipboard/native-chat-pasted-text-file.test.ts @@ -2,17 +2,17 @@ import { mkdtempSync, readFileSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import path from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { installFakeAppEnvironment } from '../../../config/scripts/test-runtime/environment/vitest-host-ports-setup' +import { installFakeAppEnvironment } from '../../../../config/scripts/test-runtime/environment/vitest-host-ports-setup' const mocks = vi.hoisted(() => ({ upload: vi.fn() })) vi.mock('electron', () => ({ app: { getPath: () => '/user-data' } })) -vi.mock('../ipc/agent-session-attachment-upload', () => ({ +vi.mock('../../ipc/agent-session-attachment-upload', () => ({ uploadPasteToAgentSessionAttachments: mocks.upload })) import { savePastedTextAsFile } from './native-chat-pasted-text-file' -import { AgentSessionAttachmentStore } from '../native-chat/agent-session-attachments/agent-session-attachment-store' +import { AgentSessionAttachmentStore } from '../../native-chat/agent-session-attachments/agent-session-attachment-store' describe('a long paste saved as a file', () => { let root: string diff --git a/src/main/window/native-chat-pasted-text-file.ts b/src/main/window/clipboard/native-chat-pasted-text-file.ts similarity index 79% rename from src/main/window/native-chat-pasted-text-file.ts rename to src/main/window/clipboard/native-chat-pasted-text-file.ts index 406b88e1f78..81fa1571d0c 100644 --- a/src/main/window/native-chat-pasted-text-file.ts +++ b/src/main/window/clipboard/native-chat-pasted-text-file.ts @@ -4,10 +4,10 @@ import { app } from 'electron' import { agentSessionPastedTextName, type AgentSessionAttachmentClipboardTarget -} from '../../shared/agent-session/agent-session-attachments' -import { assertClipboardTextWithinLimit } from '../../shared/clipboard-text' -import { uploadPasteToAgentSessionAttachments } from '../ipc/agent-session-attachment-upload' -import type { AgentSessionAttachmentStore } from '../native-chat/agent-session-attachments/agent-session-attachment-store' +} from '../../../shared/agent-session/agent-session-attachments' +import { assertClipboardTextWithinLimit } from '../../../shared/clipboard-text' +import { uploadPasteToAgentSessionAttachments } from '../../ipc/agent-session-attachment-upload' +import type { AgentSessionAttachmentStore } from '../../native-chat/agent-session-attachments/agent-session-attachment-store' export type SavePastedTextAsFileArgs = { text: string diff --git a/src/main/window/createMainWindow-close-confirmation.test.ts b/src/main/window/createMainWindow-close-confirmation.test.ts deleted file mode 100644 index ad620060a01..00000000000 --- a/src/main/window/createMainWindow-close-confirmation.test.ts +++ /dev/null @@ -1,365 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' - -vi.mock('electron', async () => - (await import('./createMainWindow-test-harness')).electronModuleMock() -) -vi.mock('./macos-tahoe-release', async () => - (await import('./createMainWindow-test-harness')).macosTahoeReleaseMock() -) -vi.mock('./desktop-integration/app-icon', async () => - (await import('./createMainWindow-test-harness')).appIconMock() -) -vi.mock('../browser/guest-lifecycle/manager/browser-manager', async () => - (await import('./createMainWindow-test-harness')).browserManagerMock() -) -vi.mock('../browser/routing/sessions/browser-route-session-runtime', async () => { - const harness = await import('./createMainWindow-test-harness') - return { - browserRouteSessionRegistry: { isAllowedPartition: harness.routePartitionAllowedMock }, - browserRouteWebContentsRegistry: { - attachGuest: harness.attachRouteGuestMock, - retireRenderer: harness.retireRouteRendererMock - } - } -}) -vi.mock('../browser/client-pages/renderer/browser-client-page-renderer-runtime', async () => { - const harness = await import('./createMainWindow-test-harness') - return { - attachBrowserClientPageRenderer: harness.attachClientPageRendererMock, - retireBrowserClientPageRenderer: harness.retireClientPageRendererMock - } -}) - -import { createMainWindow, WINDOW_QUIT_RENDERER_ACK_TIMEOUT_MS } from './createMainWindow' -import { ipcMain } from 'electron' -import { resetExpectedTeardownStateForTest } from '../crash-reporting/process-exits/expected-teardown-state' -import { - attachClientPageRendererMock, - browserWindowMock, - createRendererRecoveryWindowHarness, - resetMainWindowMocks, - retireClientPageRendererMock, - retireRouteRendererMock -} from './createMainWindow-test-harness' - -describe('createMainWindow', () => { - beforeEach(() => { - resetMainWindowMocks() - resetExpectedTeardownStateForTest() - vi.useRealTimers() - }) - - it('sends the next close as a plain close once its renderer cancels the newest quit request', () => { - const ipcHandlers: Record void> = {} - vi.mocked(ipcMain.on).mockImplementation((channel, handler) => { - ipcHandlers[channel] = handler as (...args: any[]) => void - return ipcMain - }) - const { browserWindowInstance, windowHandlers } = createRendererRecoveryWindowHarness() - let isQuitting = true - createMainWindow(null, { - getIsQuitting: () => isQuitting, - onQuitAborted: () => { - isQuitting = false - } - }) - const cancel = (senderId: number): void => - ipcHandlers['window:close-cancelled']?.({ sender: { id: senderId } }) - const retryClose = (): unknown => { - windowHandlers.close({ preventDefault: vi.fn() } as never) - return browserWindowInstance.webContents.send.mock.lastCall - } - - expect(retryClose()).toEqual(['window:close-requested', { isQuitting: true, requestId: 1 }]) - // Why: a cancel sent before the renderer saw this quit belongs to an older prompt. - cancel(143) - ipcHandlers['window:close-request-received']?.({ sender: { id: 143 } }, 1) - cancel(99) - expect(retryClose()).toEqual(['window:close-requested', { isQuitting: true, requestId: 2 }]) - ipcHandlers['window:close-request-received']?.({ sender: { id: 143 } }, 2) - cancel(143) - expect(retryClose()).toEqual(['window:close-requested', { isQuitting: false, requestId: 3 }]) - }) - - it('allows close after the renderer process is gone', () => { - const windowHandlers: Record void> = {} - const webContents = { - id: 71, - on: vi.fn((event, handler) => { - windowHandlers[event] = handler - }), - setZoomLevel: vi.fn(), - setBackgroundThrottling: vi.fn(), - setWebRTCIPHandlingPolicy: vi.fn(), - invalidate: vi.fn(), - setWindowOpenHandler: vi.fn(), - send: vi.fn(), - isCrashed: vi.fn(() => false) - } - const browserWindowInstance = { - webContents, - on: vi.fn((event, handler) => { - windowHandlers[event] = handler - }), - isDestroyed: vi.fn(() => false), - isMaximized: vi.fn(() => true), - isFullScreen: vi.fn(() => false), - getSize: vi.fn(() => [1200, 800]), - setSize: vi.fn(), - maximize: vi.fn(), - show: vi.fn(), - loadFile: vi.fn(() => Promise.resolve()), - loadURL: vi.fn(() => Promise.resolve()) - } - const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) - browserWindowMock.mockImplementation(function () { - return browserWindowInstance - }) - - createMainWindow(null, { getIsQuitting: () => true }) - - windowHandlers['render-process-gone']?.( - {} as never, - { - reason: 'crashed', - exitCode: 5 - } as never - ) - expect(retireRouteRendererMock).toHaveBeenCalledWith(71) - const preventDefault = vi.fn() - windowHandlers.close({ preventDefault } as never) - - expect(preventDefault).not.toHaveBeenCalled() - expect(webContents.send).not.toHaveBeenCalledWith( - 'window:close-requested', - expect.objectContaining({ isQuitting: true }) - ) - - consoleError.mockRestore() - }) - - it('does not persist pending bounds after bypassing close for a gone renderer', () => { - vi.useFakeTimers() - - const windowHandlers: Record void> = {} - const webContents = { - on: vi.fn((event, handler) => { - windowHandlers[event] = handler - }), - setZoomLevel: vi.fn(), - setBackgroundThrottling: vi.fn(), - setWebRTCIPHandlingPolicy: vi.fn(), - invalidate: vi.fn(), - setWindowOpenHandler: vi.fn(), - send: vi.fn(), - isCrashed: vi.fn(() => false) - } - const browserWindowInstance = { - webContents, - on: vi.fn((event, handler) => { - windowHandlers[event] = handler - }), - isDestroyed: vi.fn(() => false), - isMaximized: vi.fn(() => false), - isFullScreen: vi.fn(() => false), - getSize: vi.fn(() => [1200, 800]), - getBounds: vi.fn(() => ({ x: 10, y: 20, width: 1000, height: 700 })), - setSize: vi.fn(), - maximize: vi.fn(), - show: vi.fn(), - loadFile: vi.fn(() => Promise.resolve()), - loadURL: vi.fn(() => Promise.resolve()) - } - const updateUI = vi.fn() - const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) - browserWindowMock.mockImplementation(function () { - return browserWindowInstance - }) - - createMainWindow({ - getUI: () => ({}), - getSettings: () => ({ windowBackgroundBlur: false }), - updateUI - } as never) - - windowHandlers.resize() - windowHandlers['render-process-gone']?.( - {} as never, - { - reason: 'crashed', - exitCode: 5 - } as never - ) - const preventDefault = vi.fn() - windowHandlers.close({ preventDefault } as never) - vi.advanceTimersByTime(500) - - expect(preventDefault).not.toHaveBeenCalled() - expect(updateUI).not.toHaveBeenCalled() - - consoleError.mockRestore() - }) - - it('resumes close confirmation after a renderer process reloads', () => { - const windowHandlers: Record void> = {} - const webContents = { - on: vi.fn((event, handler) => { - windowHandlers[event] = handler - }), - setZoomLevel: vi.fn(), - setBackgroundThrottling: vi.fn(), - setWebRTCIPHandlingPolicy: vi.fn(), - invalidate: vi.fn(), - setWindowOpenHandler: vi.fn(), - send: vi.fn(), - isCrashed: vi.fn(() => false) - } - const browserWindowInstance = { - webContents, - on: vi.fn((event, handler) => { - windowHandlers[event] = handler - }), - isDestroyed: vi.fn(() => false), - isMaximized: vi.fn(() => true), - isFullScreen: vi.fn(() => false), - getSize: vi.fn(() => [1200, 800]), - setSize: vi.fn(), - maximize: vi.fn(), - show: vi.fn(), - loadFile: vi.fn(() => Promise.resolve()), - loadURL: vi.fn(() => Promise.resolve()) - } - const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) - browserWindowMock.mockImplementation(function () { - return browserWindowInstance - }) - - createMainWindow(null, { getIsQuitting: () => true }) - - windowHandlers['render-process-gone']?.( - {} as never, - { - reason: 'crashed', - exitCode: 5 - } as never - ) - windowHandlers['did-finish-load']?.() - const preventDefault = vi.fn() - windowHandlers.close({ preventDefault } as never) - - expect(preventDefault).toHaveBeenCalledTimes(1) - expect(retireClientPageRendererMock).toHaveBeenCalledWith(webContents) - expect(attachClientPageRendererMock).toHaveBeenCalledWith(webContents) - expect(webContents.send).toHaveBeenCalledWith('window:close-requested', { - isQuitting: true, - requestId: expect.any(Number) - }) - - consoleError.mockRestore() - }) - - it('destroys an already-unresponsive renderer after an app-wide quit deadline', async () => { - vi.useFakeTimers() - const windowHandlers: Record void> = {} - const webContents = { - id: 42, - on: vi.fn((event, handler) => { - windowHandlers[event] = handler - }), - setZoomLevel: vi.fn(), - setBackgroundThrottling: vi.fn(), - setWebRTCIPHandlingPolicy: vi.fn(), - invalidate: vi.fn(), - setWindowOpenHandler: vi.fn(), - send: vi.fn(), - isCrashed: vi.fn(() => false) - } - const destroy = vi.fn() - browserWindowMock.mockImplementation(function () { - return { - webContents, - on: vi.fn((event, handler) => { - windowHandlers[event] = handler - }), - isDestroyed: vi.fn(() => false), - isMaximized: vi.fn(() => true), - isFullScreen: vi.fn(() => false), - getSize: vi.fn(() => [1200, 800]), - setSize: vi.fn(), - maximize: vi.fn(), - show: vi.fn(), - destroy, - loadFile: vi.fn(() => Promise.resolve()), - loadURL: vi.fn(() => Promise.resolve()) - } - }) - createMainWindow(null, { getIsQuitting: () => true }) - - windowHandlers.close({ preventDefault: vi.fn() } as never) - await vi.advanceTimersByTimeAsync(WINDOW_QUIT_RENDERER_ACK_TIMEOUT_MS - 1) - expect(destroy).not.toHaveBeenCalled() - await vi.advanceTimersByTimeAsync(1) - - expect(destroy).toHaveBeenCalledOnce() - }) - - it('keeps the renderer-owned close flow after the quit request is acknowledged', async () => { - vi.useFakeTimers() - const windowHandlers: Record void> = {} - const ipcHandlers: Record void> = {} - vi.mocked(ipcMain.on).mockImplementation((channel, handler) => { - ipcHandlers[channel] = handler as (...args: any[]) => void - return ipcMain - }) - const webContents = { - id: 42, - on: vi.fn((event, handler) => { - windowHandlers[event] = handler - }), - setZoomLevel: vi.fn(), - setBackgroundThrottling: vi.fn(), - setWebRTCIPHandlingPolicy: vi.fn(), - invalidate: vi.fn(), - setWindowOpenHandler: vi.fn(), - send: vi.fn(), - isCrashed: vi.fn(() => false) - } - const destroy = vi.fn() - browserWindowMock.mockImplementation(function () { - return { - webContents, - on: vi.fn((event, handler) => { - windowHandlers[event] = handler - }), - isDestroyed: vi.fn(() => false), - isMaximized: vi.fn(() => true), - isFullScreen: vi.fn(() => false), - getSize: vi.fn(() => [1200, 800]), - setSize: vi.fn(), - maximize: vi.fn(), - show: vi.fn(), - destroy, - loadFile: vi.fn(() => Promise.resolve()), - loadURL: vi.fn(() => Promise.resolve()) - } - }) - createMainWindow(null, { getIsQuitting: () => true }) - - windowHandlers.close({ preventDefault: vi.fn() } as never) - windowHandlers.close({ preventDefault: vi.fn() } as never) - const closeRequests = vi - .mocked(webContents.send) - .mock.calls.filter(([channel]) => channel === 'window:close-requested') - .map(([, request]) => request as { requestId: number }) - expect(closeRequests).toHaveLength(2) - const [staleRequest, currentRequest] = closeRequests - ipcHandlers['window:close-request-received']?.({ sender: { id: 99 } }, currentRequest.requestId) - ipcHandlers['window:close-request-received']?.({ sender: { id: 42 } }, staleRequest.requestId) - await vi.advanceTimersByTimeAsync(WINDOW_QUIT_RENDERER_ACK_TIMEOUT_MS - 1) - expect(destroy).not.toHaveBeenCalled() - ipcHandlers['window:close-request-received']?.({ sender: { id: 42 } }, currentRequest.requestId) - await vi.advanceTimersByTimeAsync(1) - - expect(destroy).not.toHaveBeenCalled() - }) -}) diff --git a/src/main/window/createMainWindow-low-commit-oom-recovery.test.ts b/src/main/window/createMainWindow-low-commit-oom-recovery.test.ts deleted file mode 100644 index 7ee68f4178a..00000000000 --- a/src/main/window/createMainWindow-low-commit-oom-recovery.test.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -vi.mock('electron', async () => - (await import('./createMainWindow-test-harness')).electronModuleMock() -) -vi.mock('./macos-tahoe-release', async () => - (await import('./createMainWindow-test-harness')).macosTahoeReleaseMock() -) -vi.mock('./desktop-integration/app-icon', async () => - (await import('./createMainWindow-test-harness')).appIconMock() -) -vi.mock('../browser/guest-lifecycle/manager/browser-manager', async () => - (await import('./createMainWindow-test-harness')).browserManagerMock() -) -vi.mock('../browser/client-pages/renderer/browser-client-page-renderer-runtime', async () => { - const harness = await import('./createMainWindow-test-harness') - return { - attachBrowserClientPageRenderer: harness.attachClientPageRendererMock, - retireBrowserClientPageRenderer: harness.retireClientPageRendererMock - } -}) - -import { createMainWindow } from './createMainWindow' -import { resetExpectedTeardownStateForTest } from '../crash-reporting/process-exits/expected-teardown-state' -import { - resetPreGoneSystemMemorySamplingForTest, - samplePreGoneSystemMemory -} from '../crash-reporting/memory/pre-gone-host-memory' -import { setSystemMemoryInfoReaderForTest } from '../crash-reporting/memory/system-memory-details' -import { setSwapVolumeFreeSpaceReaderForTest } from '../crash-reporting/memory/swap-volume-free-space' -import { - createRendererRecoveryWindowHarness, - resetMainWindowMocks, - withPlatform -} from './createMainWindow-test-harness' - -// Launch 13084 (Scan-31 1790683596/1790684449/1790684587): the second reload OOMed 3.458 s after the first. -const OOM_AT = [ - '2026-09-29T12:06:19.869Z', - '2026-09-29T12:20:37.207Z', - '2026-09-29T12:20:40.665Z', - '2026-09-29T12:22:59.975Z' -].map((iso) => Date.parse(iso)) -// Commit at the two OOMs of launch 22912 (Scan-30 1790622432/1790622459): 744 MB, then 60 MB of a 130 GB limit. -const HEALTHY_FIRST_OOM_COMMIT_MB = 744 -const EXHAUSTED_COMMIT_MB = 60 -const RECOVERED_COMMIT_MB = 2_029 - -const OOM: Electron.RenderProcessGoneDetails = { reason: 'oom', exitCode: -536870904 } - -function hostWithAvailableCommit(swapFreeMB: number): void { - setSystemMemoryInfoReaderForTest(() => ({ - total: 32_000 * 1024, - free: 976 * 1024, - swapTotal: 130_000 * 1024, - swapFree: swapFreeMB * 1024 - })) - void samplePreGoneSystemMemory(Date.now()) -} - -async function runOomSequence( - platform: NodeJS.Platform, - commitAtEachOomMB: readonly (number | null)[], - goneTimeCommitMB?: number -): Promise<{ reloads: number; onRendererRecoveryExhausted: ReturnType }> { - const onRendererRecoveryExhausted = vi.fn() - const { browserWindowInstance, windowHandlers } = createRendererRecoveryWindowHarness() - createMainWindow(null, { onRendererRecoveryExhausted }) - for (const [index, goneAt] of OOM_AT.entries()) { - const commitMB = commitAtEachOomMB[index] - // null: no sampler tick since the previous OOM. - if (commitMB !== null) { - vi.setSystemTime(goneAt - 2_000) - withPlatform(platform, () => hostWithAvailableCommit(commitMB)) - } - vi.setSystemTime(goneAt) - if (goneTimeCommitMB !== undefined) { - // Only the host changes; no sampler tick commits it. - setSystemMemoryInfoReaderForTest(() => ({ swapFree: goneTimeCommitMB * 1024 })) - } - withPlatform(platform, () => windowHandlers['render-process-gone']?.({}, OOM)) - await vi.advanceTimersByTimeAsync(250) - } - return { - reloads: browserWindowInstance.loadFile.mock.calls.length - 1, - onRendererRecoveryExhausted - } -} - -describe('Windows renderer OOM recovery under exhausted commit', () => { - beforeEach(() => { - resetMainWindowMocks() - resetExpectedTeardownStateForTest() - resetPreGoneSystemMemorySamplingForTest() - setSwapVolumeFreeSpaceReaderForTest(async () => undefined) - vi.useFakeTimers() - vi.spyOn(console, 'error').mockImplementation(() => {}) - }) - - afterEach(() => { - setSystemMemoryInfoReaderForTest(null) - setSwapVolumeFreeSpaceReaderForTest(null) - resetPreGoneSystemMemorySamplingForTest() - vi.useRealTimers() - vi.restoreAllMocks() - }) - - it('stops reloading into a repeat OOM and tells the user how much commit is left', async () => { - const { reloads, onRendererRecoveryExhausted } = await runOomSequence('win32', [ - HEALTHY_FIRST_OOM_COMMIT_MB, - EXHAUSTED_COMMIT_MB, - EXHAUSTED_COMMIT_MB, - EXHAUSTED_COMMIT_MB - ]) - expect(reloads).toBe(2) - expect(onRendererRecoveryExhausted).toHaveBeenCalledOnce() - expect(onRendererRecoveryExhausted).toHaveBeenCalledWith( - expect.objectContaining({ - details: OOM, - cause: 'low-commit', - lowCommit: { - availableCommitMB: EXHAUSTED_COMMIT_MB, - sincePreviousOomMs: 3_458, - commitReading: 'pre-gone' - } - }) - ) - }) - - // Reproduce the 3.458s OOM gap with no intervening 10s sampler tick. - it('reads commit at gone time when no sampler tick landed since the previous OOM', async () => { - const { reloads, onRendererRecoveryExhausted } = await runOomSequence('win32', [ - HEALTHY_FIRST_OOM_COMMIT_MB, - EXHAUSTED_COMMIT_MB, - null, - EXHAUSTED_COMMIT_MB - ]) - expect(reloads).toBe(2) - expect(onRendererRecoveryExhausted).toHaveBeenCalledOnce() - expect(onRendererRecoveryExhausted).toHaveBeenCalledWith( - expect.objectContaining({ - cause: 'low-commit', - lowCommit: { - availableCommitMB: EXHAUSTED_COMMIT_MB, - sincePreviousOomMs: 3_458, - commitReading: 'gone-time' - } - }) - ) - }) - - it('keeps reloading when the gone-time read shows commit recovered and no tick landed', async () => { - const { reloads, onRendererRecoveryExhausted } = await runOomSequence( - 'win32', - [HEALTHY_FIRST_OOM_COMMIT_MB, EXHAUSTED_COMMIT_MB, null, null], - RECOVERED_COMMIT_MB - ) - expect(reloads).toBe(4) - expect(onRendererRecoveryExhausted).not.toHaveBeenCalled() - }) - - it('keeps auto-reloading repeat OOMs once commit has recovered', async () => { - const { reloads, onRendererRecoveryExhausted } = await runOomSequence('win32', [ - HEALTHY_FIRST_OOM_COMMIT_MB, - RECOVERED_COMMIT_MB, - RECOVERED_COMMIT_MB, - RECOVERED_COMMIT_MB - ]) - expect(reloads).toBe(4) - expect(onRendererRecoveryExhausted).not.toHaveBeenCalled() - }) - - it.each(['darwin', 'linux'] as const)('is a no-op on %s', async (platform) => { - const { reloads, onRendererRecoveryExhausted } = await runOomSequence(platform, [ - EXHAUSTED_COMMIT_MB, - EXHAUSTED_COMMIT_MB, - EXHAUSTED_COMMIT_MB, - EXHAUSTED_COMMIT_MB - ]) - expect(reloads).toBe(4) - expect(onRendererRecoveryExhausted).not.toHaveBeenCalled() - }) -}) diff --git a/src/main/window/createMainWindow-markdown-editor-focus.test.ts b/src/main/window/createMainWindow-markdown-editor-focus.test.ts deleted file mode 100644 index a49f4e214b9..00000000000 --- a/src/main/window/createMainWindow-markdown-editor-focus.test.ts +++ /dev/null @@ -1,385 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' - -vi.mock('electron', async () => - (await import('./createMainWindow-test-harness')).electronModuleMock() -) -vi.mock('./macos-tahoe-release', async () => - (await import('./createMainWindow-test-harness')).macosTahoeReleaseMock() -) -vi.mock('./desktop-integration/app-icon', async () => - (await import('./createMainWindow-test-harness')).appIconMock() -) -vi.mock('../browser/guest-lifecycle/manager/browser-manager', async () => - (await import('./createMainWindow-test-harness')).browserManagerMock() -) - -import { createMainWindow } from './createMainWindow' -import { ipcMain } from 'electron' -import { resetExpectedTeardownStateForTest } from '../crash-reporting/process-exits/expected-teardown-state' -import { - browserWindowMock, - buildFromTemplateMock, - menuPopupMock, - resetMainWindowMocks -} from './createMainWindow-test-harness' - -function openMainWindowForContextMenu() { - const windowHandlers: Record void> = {} - const webContents = { - on: vi.fn((event, handler) => { - windowHandlers[event] = handler - }), - setZoomLevel: vi.fn(), - setBackgroundThrottling: vi.fn(), - setWebRTCIPHandlingPolicy: vi.fn(), - invalidate: vi.fn(), - setWindowOpenHandler: vi.fn(), - send: vi.fn(), - isDevToolsOpened: vi.fn(), - openDevTools: vi.fn(), - closeDevTools: vi.fn(), - replaceMisspelling: vi.fn(), - session: { addWordToSpellCheckerDictionary: vi.fn() }, - getURL: vi.fn(() => 'file:///opt/orca/renderer/index.html'), - isLoadingMainFrame: vi.fn(() => true) - } - const browserWindowInstance = { - webContents, - on: vi.fn(), - isDestroyed: vi.fn(() => false), - isMaximized: vi.fn(() => true), - isFullScreen: vi.fn(() => false), - getSize: vi.fn(() => [1200, 800]), - setSize: vi.fn(), - maximize: vi.fn(), - show: vi.fn(), - loadFile: vi.fn(() => Promise.resolve()), - loadURL: vi.fn(() => Promise.resolve()) - } - browserWindowMock.mockImplementation(function () { - return browserWindowInstance - }) - - createMainWindow(null) - return { webContents, windowHandlers, browserWindowInstance } -} - -describe('createMainWindow', () => { - beforeEach(() => { - resetMainWindowMocks() - resetExpectedTeardownStateForTest() - vi.useRealTimers() - }) - - it('intercepts Cmd+B for sidebar when the markdown editor is not focused', () => { - const windowHandlers: Record void> = {} - const webContents = { - on: vi.fn((event, handler) => { - windowHandlers[event] = handler - }), - setZoomLevel: vi.fn(), - setBackgroundThrottling: vi.fn(), - setWebRTCIPHandlingPolicy: vi.fn(), - invalidate: vi.fn(), - setWindowOpenHandler: vi.fn(), - send: vi.fn(), - isDevToolsOpened: vi.fn(), - openDevTools: vi.fn(), - closeDevTools: vi.fn() - } - const browserWindowInstance = { - webContents, - on: vi.fn(), - isDestroyed: vi.fn(() => false), - isMaximized: vi.fn(() => true), - isFullScreen: vi.fn(() => false), - getSize: vi.fn(() => [1200, 800]), - setSize: vi.fn(), - maximize: vi.fn(), - show: vi.fn(), - loadFile: vi.fn(() => Promise.resolve()), - loadURL: vi.fn(() => Promise.resolve()) - } - browserWindowMock.mockImplementation(function () { - return browserWindowInstance - }) - - createMainWindow(null) - - const preventDefault = vi.fn() - const isDarwin = process.platform === 'darwin' - windowHandlers['before-input-event']( - { preventDefault } as never, - { - type: 'keyDown', - code: 'KeyB', - key: 'b', - meta: isDarwin, - control: !isDarwin, - alt: false, - shift: false - } as never - ) - - expect(preventDefault).toHaveBeenCalledTimes(1) - expect(webContents.send).toHaveBeenCalledWith('ui:toggleLeftSidebar') - }) - - it('leaves bold and logical link shortcuts to the focused markdown editor', () => { - const windowHandlers: Record void> = {} - const webContents = { - on: vi.fn((event, handler) => { - windowHandlers[event] = handler - }), - setZoomLevel: vi.fn(), - setBackgroundThrottling: vi.fn(), - setWebRTCIPHandlingPolicy: vi.fn(), - invalidate: vi.fn(), - setWindowOpenHandler: vi.fn(), - send: vi.fn(), - isDevToolsOpened: vi.fn(), - openDevTools: vi.fn(), - closeDevTools: vi.fn() - } - const browserWindowInstance = { - webContents, - on: vi.fn(), - isDestroyed: vi.fn(() => false), - isMaximized: vi.fn(() => true), - isFullScreen: vi.fn(() => false), - getSize: vi.fn(() => [1200, 800]), - setSize: vi.fn(), - maximize: vi.fn(), - show: vi.fn(), - loadFile: vi.fn(() => Promise.resolve()), - loadURL: vi.fn(() => Promise.resolve()) - } - browserWindowMock.mockImplementation(function () { - return browserWindowInstance - }) - - createMainWindow(null) - - const setFocusedListener = vi - .mocked(ipcMain.on) - .mock.calls.find(([channel]) => channel === 'ui:setMarkdownEditorFocused')?.[1] - expect(setFocusedListener).toBeTypeOf('function') - setFocusedListener?.({ sender: webContents } as never, true) - - const event = new Event('before-input-event', { cancelable: true }) - const isDarwin = process.platform === 'darwin' - windowHandlers['before-input-event'](event, { - type: 'keyDown', - code: 'KeyB', - key: 'b', - meta: isDarwin, - control: !isDarwin, - alt: false, - shift: false - } as never) - - windowHandlers['before-input-event'](event, { - type: 'keyDown', - code: 'KeyV', - key: 'k', - meta: isDarwin, - control: !isDarwin, - alt: false, - shift: false - }) - expect(event.defaultPrevented).toBe(false) - }) - - it('lets the shortcut recorder capture app shortcuts before main interception', () => { - const windowHandlers: Record void> = {} - const webContents = { - on: vi.fn((event, handler) => { - windowHandlers[event] = handler - }), - setZoomLevel: vi.fn(), - setBackgroundThrottling: vi.fn(), - setWebRTCIPHandlingPolicy: vi.fn(), - invalidate: vi.fn(), - setWindowOpenHandler: vi.fn(), - send: vi.fn(), - isDevToolsOpened: vi.fn(), - openDevTools: vi.fn(), - closeDevTools: vi.fn() - } - const browserWindowInstance = { - webContents, - on: vi.fn(), - isDestroyed: vi.fn(() => false), - isMaximized: vi.fn(() => true), - isFullScreen: vi.fn(() => false), - getSize: vi.fn(() => [1200, 800]), - setSize: vi.fn(), - maximize: vi.fn(), - show: vi.fn(), - loadFile: vi.fn(() => Promise.resolve()), - loadURL: vi.fn(() => Promise.resolve()) - } - browserWindowMock.mockImplementation(function () { - return browserWindowInstance - }) - - createMainWindow(null) - - const setFocusedListener = vi - .mocked(ipcMain.on) - .mock.calls.find(([channel]) => channel === 'ui:setShortcutRecorderFocused')?.[1] - expect(setFocusedListener).toBeTypeOf('function') - setFocusedListener?.({ sender: webContents } as never, true) - - const preventDefault = vi.fn() - const isDarwin = process.platform === 'darwin' - windowHandlers['before-input-event']( - { preventDefault } as never, - { - type: 'keyDown', - code: 'KeyB', - key: 'b', - meta: isDarwin, - control: !isDarwin, - alt: false, - shift: false - } as never - ) - - expect(preventDefault).not.toHaveBeenCalled() - expect(webContents.send).not.toHaveBeenCalledWith('ui:toggleLeftSidebar') - }) - - it('skips Cmd+B interception when floating terminal input is focused', () => { - const windowHandlers: Record void> = {} - const webContents = { - on: vi.fn((event, handler) => { - windowHandlers[event] = handler - }), - setZoomLevel: vi.fn(), - setBackgroundThrottling: vi.fn(), - setWebRTCIPHandlingPolicy: vi.fn(), - invalidate: vi.fn(), - setWindowOpenHandler: vi.fn(), - send: vi.fn(), - isDevToolsOpened: vi.fn(), - openDevTools: vi.fn(), - closeDevTools: vi.fn() - } - const browserWindowInstance = { - webContents, - on: vi.fn(), - isDestroyed: vi.fn(() => false), - isMaximized: vi.fn(() => true), - isFullScreen: vi.fn(() => false), - getSize: vi.fn(() => [1200, 800]), - setSize: vi.fn(), - maximize: vi.fn(), - show: vi.fn(), - loadFile: vi.fn(() => Promise.resolve()), - loadURL: vi.fn(() => Promise.resolve()) - } - browserWindowMock.mockImplementation(function () { - return browserWindowInstance - }) - - createMainWindow(null) - - const setFocusedListener = vi - .mocked(ipcMain.on) - .mock.calls.find(([channel]) => channel === 'ui:setFloatingFocus')?.[1] - expect(setFocusedListener).toBeTypeOf('function') - setFocusedListener?.( - { sender: webContents } as never, - { panelFocused: true, terminalFocused: true } as never - ) - - const preventDefault = vi.fn() - const isDarwin = process.platform === 'darwin' - windowHandlers['before-input-event']( - { preventDefault } as never, - { - type: 'keyDown', - code: 'KeyB', - key: 'b', - meta: isDarwin, - control: !isDarwin, - alt: false, - shift: false - } as never - ) - - expect(preventDefault).not.toHaveBeenCalled() - expect(webContents.send).not.toHaveBeenCalledWith('ui:toggleLeftSidebar') - - webContents.send.mockClear() - const newWorkspacePreventDefault = vi.fn() - windowHandlers['before-input-event']( - { preventDefault: newWorkspacePreventDefault } as never, - { - type: 'keyDown', - code: 'KeyN', - key: 'n', - meta: isDarwin, - control: !isDarwin, - alt: false, - shift: false - } as never - ) - - expect(newWorkspacePreventDefault).toHaveBeenCalledTimes(1) - expect(webContents.send).toHaveBeenCalledWith('ui:openNewWorkspace') - }) - - it('retires the edit menu with its document but not on same-document navigation', () => { - const { webContents } = openMainWindowForContextMenu() - const emit = (event: string, ...args: unknown[]): void => { - for (const [name, handler] of webContents.on.mock.calls) { - if (name === event) { - handler(...args) - } - } - } - const openMenu = () => { - const closePopup = vi.fn() - buildFromTemplateMock.mockImplementationOnce(() => ({ - popup: menuPopupMock, - closePopup - })) - emit( - 'context-menu', - {}, - { - x: 1, - y: 2, - isEditable: true, - formControlType: 'input', - dictionarySuggestions: [], - misspelledWord: '' - } - ) - const template = buildFromTemplateMock.mock.calls.at(-1)?.[0] - const paste = Array.isArray(template) - ? template.find((item) => item.label === 'Paste') - : undefined - expect(paste?.click).toBeTypeOf('function') - return { closePopup, paste } - } - const nextDocument = 'file:///opt/orca/renderer/index.html?reload' - - const first = openMenu() - emit('did-start-navigation', {}, `${nextDocument}#hash`, true, true) - expect(first.closePopup).not.toHaveBeenCalled() - emit('did-start-navigation', {}, nextDocument, false, true) - expect(first.closePopup).toHaveBeenCalledTimes(1) - - // The old document still takes right-clicks until the pending reload commits. - const pending = openMenu() - emit('did-frame-navigate', {}, nextDocument, 200, 'OK', false) - expect(pending.closePopup).not.toHaveBeenCalled() - emit('did-frame-navigate', {}, nextDocument, 200, 'OK', true) - expect(pending.closePopup).toHaveBeenCalledTimes(1) - pending.paste.click() - expect(webContents.send).not.toHaveBeenCalledWith('ui:editableContextPaste', expect.anything()) - }) -}) diff --git a/src/main/window/createMainWindow-renderer-crash-recovery.test.ts b/src/main/window/createMainWindow-renderer-crash-recovery.test.ts deleted file mode 100644 index abf52e7510f..00000000000 --- a/src/main/window/createMainWindow-renderer-crash-recovery.test.ts +++ /dev/null @@ -1,383 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' - -vi.mock('electron', async () => - (await import('./createMainWindow-test-harness')).electronModuleMock() -) -vi.mock('./macos-tahoe-release', async () => - (await import('./createMainWindow-test-harness')).macosTahoeReleaseMock() -) -vi.mock('./desktop-integration/app-icon', async () => - (await import('./createMainWindow-test-harness')).appIconMock() -) -vi.mock('../browser/guest-lifecycle/manager/browser-manager', async () => - (await import('./createMainWindow-test-harness')).browserManagerMock() -) -vi.mock('../browser/client-pages/renderer/browser-client-page-renderer-runtime', async () => { - const harness = await import('./createMainWindow-test-harness') - return { - attachBrowserClientPageRenderer: harness.attachClientPageRendererMock, - retireBrowserClientPageRenderer: harness.retireClientPageRendererMock - } -}) - -import { createMainWindow } from './createMainWindow' -import { ipcMain } from 'electron' -import { shouldRecoverRendererAfterProcessGone } from '../crash-reporting/process-exits/process-gone-classification' -import { - resetExpectedTeardownStateForTest, - resolveExpectedTeardownScope -} from '../crash-reporting/process-exits/expected-teardown-state' -import { - attachClientPageRendererMock, - browserWindowMock, - createRendererRecoveryWindowHarness, - resetMainWindowMocks, - retireClientPageRendererMock, - withPlatform -} from './createMainWindow-test-harness' - -describe('createMainWindow', () => { - beforeEach(() => { - resetMainWindowMocks() - resetExpectedTeardownStateForTest() - vi.useRealTimers() - }) - - it('does not read destroyed webContents during closed cleanup', () => { - const windowHandlers: Record void> = {} - const webContents = { - on: vi.fn(), - setZoomLevel: vi.fn(), - setBackgroundThrottling: vi.fn(), - setWebRTCIPHandlingPolicy: vi.fn(), - invalidate: vi.fn(), - setWindowOpenHandler: vi.fn(), - send: vi.fn(), - isDevToolsOpened: vi.fn(), - openDevTools: vi.fn(), - closeDevTools: vi.fn() - } - let webContentsDestroyed = false - const browserWindowInstance = { - get webContents() { - if (webContentsDestroyed) { - throw new Error('Object has been destroyed') - } - return webContents - }, - on: vi.fn((event, handler) => { - windowHandlers[event] = handler - }), - isDestroyed: vi.fn(() => false), - isMaximized: vi.fn(() => true), - isFullScreen: vi.fn(() => false), - getSize: vi.fn(() => [1200, 800]), - setSize: vi.fn(), - maximize: vi.fn(), - show: vi.fn(), - loadFile: vi.fn(() => Promise.resolve()), - loadURL: vi.fn(() => Promise.resolve()) - } - browserWindowMock.mockImplementation(function () { - return browserWindowInstance - }) - - createMainWindow(null) - - webContentsDestroyed = true - - // Updater teardown may destroy WebContents before closed cleanup; throwing would prevent Squirrel relaunch. - expect(() => windowHandlers.closed?.()).not.toThrow() - }) - - it('retires renderer generations and resets focus on crash, navigation, and destroy', async () => { - const windowHandlers: Record void> = {} - const webContents = { - getURL: vi.fn(() => 'file:///opt/orca/renderer/index.html'), - isDestroyed: vi.fn(() => false), - isLoadingMainFrame: vi.fn(() => false), - mainFrame: {}, - on: vi.fn((event, handler) => { - // Retain multiple lifecycle listeners like EventEmitter. - const previous = windowHandlers[event] - windowHandlers[event] = previous - ? (...args: unknown[]) => { - previous(...args) - handler(...args) - } - : handler - }), - setZoomLevel: vi.fn(), - setBackgroundThrottling: vi.fn(), - setWebRTCIPHandlingPolicy: vi.fn(), - invalidate: vi.fn(), - setWindowOpenHandler: vi.fn(), - send: vi.fn(), - isDevToolsOpened: vi.fn(), - openDevTools: vi.fn(), - closeDevTools: vi.fn() - } - const browserWindowInstance = { - webContents, - on: vi.fn(), - isDestroyed: vi.fn(() => false), - isMaximized: vi.fn(() => true), - isFullScreen: vi.fn(() => false), - getSize: vi.fn(() => [1200, 800]), - setSize: vi.fn(), - maximize: vi.fn(), - show: vi.fn(), - loadFile: vi.fn(() => Promise.resolve()), - loadURL: vi.fn(() => Promise.resolve()) - } - browserWindowMock.mockImplementation(function () { - return browserWindowInstance - }) - - createMainWindow(null) - windowHandlers['did-finish-load']?.() - expect(attachClientPageRendererMock).toHaveBeenCalledWith(webContents) - - const setFocusedListener = vi - .mocked(ipcMain.on) - .mock.calls.find(([channel]) => channel === 'ui:setMarkdownEditorFocused')?.[1] - const isDarwin = process.platform === 'darwin' - - const cmdBInput = { - type: 'keyDown', - code: 'KeyB', - key: 'b', - meta: isDarwin, - control: !isDarwin, - alt: false, - shift: false - } as never - - const assertInterceptsAfterReset = (): void => { - webContents.send.mockClear() - const preventDefault = vi.fn() - windowHandlers['before-input-event']({ preventDefault } as never, cmdBInput) - expect(preventDefault).toHaveBeenCalledTimes(1) - expect(webContents.send).toHaveBeenCalledWith('ui:toggleLeftSidebar') - } - - retireClientPageRendererMock.mockClear() - setFocusedListener?.({ sender: webContents } as never, true) - windowHandlers['render-process-gone']?.() - expect(retireClientPageRendererMock).toHaveBeenCalledWith(webContents) - assertInterceptsAfterReset() - windowHandlers['did-finish-load']?.() - - retireClientPageRendererMock.mockClear() - setFocusedListener?.({ sender: webContents } as never, true) - windowHandlers['did-start-navigation']?.({} as never, 'https://example.com/', false, true) - expect(retireClientPageRendererMock).not.toHaveBeenCalled() - - windowHandlers['did-start-navigation']?.( - {} as never, - 'file:///opt/orca/renderer/index.html?reload=1', - false, - true - ) - expect(retireClientPageRendererMock).toHaveBeenCalledWith(webContents) - assertInterceptsAfterReset() - - attachClientPageRendererMock.mockClear() - windowHandlers['did-fail-provisional-load']?.( - {} as never, - -3, - 'aborted', - 'file:///opt/orca/renderer/index.html?reload=1', - true - ) - await Promise.resolve() - expect(attachClientPageRendererMock).toHaveBeenCalledWith(webContents) - - retireClientPageRendererMock.mockClear() - setFocusedListener?.({ sender: webContents } as never, true) - windowHandlers['did-start-navigation']?.({} as never, '#same-document', true, true) - expect(retireClientPageRendererMock).not.toHaveBeenCalled() - windowHandlers['did-start-navigation']?.({} as never, 'https://example.com/', false, false) - expect(retireClientPageRendererMock).not.toHaveBeenCalled() - webContents.send.mockClear() - const subframePreventDefault = vi.fn() - windowHandlers['before-input-event']( - { preventDefault: subframePreventDefault } as never, - cmdBInput - ) - expect(subframePreventDefault).not.toHaveBeenCalled() - expect(webContents.send).not.toHaveBeenCalledWith('ui:toggleLeftSidebar') - - retireClientPageRendererMock.mockClear() - setFocusedListener?.({ sender: webContents } as never, true) - windowHandlers['destroyed']?.() - expect(retireClientPageRendererMock).toHaveBeenCalledWith(webContents) - assertInterceptsAfterReset() - }) - - it('still preserves PTYs and reloads after Windows session-end', () => { - vi.useFakeTimers() - - const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) - const { browserWindowInstance, windowHandlers } = createRendererRecoveryWindowHarness() - const onBeforeRecoveryReload = vi.fn() - const onRendererRecoveryExhausted = vi.fn() - - withPlatform('win32', () => { - createMainWindow(null, { - onBeforeRecoveryReload, - onRendererRecoveryExhausted, - shouldRecoverRenderer: (details) => - shouldRecoverRendererAfterProcessGone({ - reason: details.reason, - expectedTeardown: resolveExpectedTeardownScope({ - isQuitting: false, - isQuittingForUpdate: false, - isExpectedRendererReload: false, - includeSystemSessionEnd: false - }) - }) - }) - }) - windowHandlers['session-end']?.({} as never) - windowHandlers['render-process-gone']?.( - {} as never, - { reason: 'killed', exitCode: 1 } as Electron.RenderProcessGoneDetails - ) - vi.advanceTimersByTime(250) - - expect(onBeforeRecoveryReload).toHaveBeenCalledWith(143, 'automatic') - expect(browserWindowInstance.loadFile).toHaveBeenCalledTimes(2) - // Deliberate logoff reloads must never raise recovery dialogs during teardown. - vi.runAllTimers() - expect(onRendererRecoveryExhausted).not.toHaveBeenCalled() - consoleError.mockRestore() - }) - - it('rechecks the renderer recovery predicate before reloading', () => { - vi.useFakeTimers() - - const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) - const { browserWindowInstance, windowHandlers } = createRendererRecoveryWindowHarness() - let shouldRecover = true - - createMainWindow(null, { shouldRecoverRenderer: () => shouldRecover }) - - windowHandlers['render-process-gone']?.( - {} as never, - { - reason: 'crashed', - exitCode: 5 - } as Electron.RenderProcessGoneDetails - ) - shouldRecover = false - vi.advanceTimersByTime(250) - - expect(browserWindowInstance.loadFile).toHaveBeenCalledTimes(1) - expect(browserWindowInstance.loadURL).not.toHaveBeenCalled() - - consoleError.mockRestore() - }) - - it('coalesces repeated renderer losses into one recovery reload', () => { - vi.useFakeTimers() - - const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) - const { browserWindowInstance, windowHandlers } = createRendererRecoveryWindowHarness() - - createMainWindow(null) - - const details = { - reason: 'crashed', - exitCode: 5 - } as Electron.RenderProcessGoneDetails - windowHandlers['render-process-gone']?.({} as never, details) - windowHandlers['render-process-gone']?.({} as never, details) - vi.advanceTimersByTime(250) - - expect(browserWindowInstance.loadFile).toHaveBeenCalledTimes(2) - expect(browserWindowInstance.loadURL).not.toHaveBeenCalled() - - consoleError.mockRestore() - }) - - it('does not reload after a clean renderer exit', () => { - vi.useFakeTimers() - - const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) - const { browserWindowInstance, windowHandlers } = createRendererRecoveryWindowHarness() - - createMainWindow(null) - - windowHandlers['render-process-gone']?.( - {} as never, - { - reason: 'clean-exit', - exitCode: 0 - } as Electron.RenderProcessGoneDetails - ) - vi.advanceTimersByTime(250) - - expect(browserWindowInstance.loadFile).toHaveBeenCalledTimes(1) - expect(browserWindowInstance.loadURL).not.toHaveBeenCalled() - - consoleError.mockRestore() - }) - - it('cancels renderer recovery when the crashed window is closing', () => { - vi.useFakeTimers() - - const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) - const { browserWindowInstance, windowHandlers } = createRendererRecoveryWindowHarness() - - createMainWindow(null) - - windowHandlers['render-process-gone']?.( - {} as never, - { - reason: 'crashed', - exitCode: 5 - } as Electron.RenderProcessGoneDetails - ) - windowHandlers.close({ preventDefault: vi.fn() } as never) - vi.advanceTimersByTime(250) - - expect(browserWindowInstance.loadFile).toHaveBeenCalledTimes(1) - expect(browserWindowInstance.loadURL).not.toHaveBeenCalled() - - consoleError.mockRestore() - }) - - it('serves a manual retry while the breaker stays open for the next crash', () => { - vi.useFakeTimers() - - const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) - const onRendererRecoveryExhausted = vi.fn() - const { browserWindowInstance, windowHandlers } = createRendererRecoveryWindowHarness() - - createMainWindow(null, { onRendererRecoveryExhausted }) - - const details = { reason: 'crashed', exitCode: 5 } as Electron.RenderProcessGoneDetails - const driveCrashCycle = (): void => { - windowHandlers['render-process-gone']?.({} as never, details) - vi.advanceTimersByTime(250) - } - driveCrashCycle() - driveCrashCycle() - driveCrashCycle() - driveCrashCycle() - expect(onRendererRecoveryExhausted).toHaveBeenCalledTimes(1) - expect(browserWindowInstance.loadFile).toHaveBeenCalledTimes(4) - - // Manual watched retry bypasses the automatic-recovery breaker. - onRendererRecoveryExhausted.mock.calls[0]?.[0].retry() - expect(browserWindowInstance.loadFile).toHaveBeenCalledTimes(5) - - // Manual retry must not reset the poisoned machine's automatic-reload breaker. - driveCrashCycle() - expect(browserWindowInstance.loadFile).toHaveBeenCalledTimes(5) - expect(onRendererRecoveryExhausted).toHaveBeenCalledTimes(2) - - consoleError.mockRestore() - }) -}) diff --git a/src/main/window/createMainWindow-test-harness.ts b/src/main/window/createMainWindow-test-harness.ts deleted file mode 100644 index f99b477f67c..00000000000 --- a/src/main/window/createMainWindow-test-harness.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { vi } from 'vitest' -import type { Mock } from 'vitest' - -/** Loose spy signature: these stand in for electron APIs the suites only assert calls on. */ -export type MainWindowSpy = Mock<(...args: unknown[]) => unknown> - -export const menuPopupMock: MainWindowSpy = vi.fn() -export const notificationShowMock: MainWindowSpy = vi.fn() -/** Constructor spy: suites read the options object back off `mock.calls`. */ -export const browserWindowMock: Mock< - (options: Electron.BrowserWindowConstructorOptions) => unknown -> = vi.fn() -export const openExternalMock: MainWindowSpy = vi.fn() -export const attachGuestPoliciesMock: MainWindowSpy = vi.fn() -export const attachRouteGuestMock: MainWindowSpy = vi.fn(() => false) -export const retireRouteRendererMock: MainWindowSpy = vi.fn() -export const attachClientPageRendererMock: MainWindowSpy = vi.fn() -export const retireClientPageRendererMock: MainWindowSpy = vi.fn() -export const buildFromTemplateMock: Mock< - (...args: unknown[]) => { popup: MainWindowSpy; closePopup: MainWindowSpy } -> = vi.fn(() => ({ popup: menuPopupMock, closePopup: vi.fn() })) -export const notificationMock: Mock<(...args: unknown[]) => { show: MainWindowSpy }> = vi.fn( - function () { - return { show: notificationShowMock } - } -) -export const powerMonitorOnMock: MainWindowSpy = vi.fn() -export const powerMonitorRemoveListenerMock: MainWindowSpy = vi.fn() -export const routePartitionAllowedMock: Mock<(partition: string) => boolean> = vi.fn(() => false) -export const appMock: { isPackaged: boolean; on: MainWindowSpy; removeListener: MainWindowSpy } = { - isPackaged: true, - on: vi.fn(), - removeListener: vi.fn() -} -export const macosTahoeMock = { value: false } - -type IpcMainMock = { - on: MainWindowSpy - removeListener: MainWindowSpy - handle: MainWindowSpy - removeHandler: MainWindowSpy -} - -const ipcMainMock: IpcMainMock = { - on: vi.fn(), - removeListener: vi.fn(), - handle: vi.fn(), - removeHandler: vi.fn() -} - -export type ElectronModuleMock = { - app: typeof appMock - BrowserWindow: typeof browserWindowMock - ipcMain: IpcMainMock - Menu: { buildFromTemplate: typeof buildFromTemplateMock } - Notification: typeof notificationMock - nativeTheme: { shouldUseDarkColors: boolean } - powerMonitor: { on: MainWindowSpy; removeListener: MainWindowSpy } - screen: { - getPrimaryDisplay: () => { workAreaSize: { width: number; height: number } } - getDisplayMatching: () => { scaleFactor: number } - } - shell: { openExternal: MainWindowSpy } -} - -export type BrowserManagerModuleMock = { - browserManager: { - attachGuestPolicies: MainWindowSpy - setDictationShortcutForwardingPredicate: MainWindowSpy - } -} - -// vi.mock factories are hoisted per suite; builders reuse the Electron surface without duplicating it. -export function electronModuleMock(): ElectronModuleMock { - return { - app: appMock, - BrowserWindow: browserWindowMock, - ipcMain: ipcMainMock, - Menu: { buildFromTemplate: buildFromTemplateMock }, - Notification: notificationMock, - nativeTheme: { shouldUseDarkColors: false }, - powerMonitor: { on: powerMonitorOnMock, removeListener: powerMonitorRemoveListenerMock }, - screen: { - getPrimaryDisplay: () => ({ workAreaSize: { width: 1440, height: 900 } }), - getDisplayMatching: () => ({ scaleFactor: 2 }) - }, - shell: { openExternal: openExternalMock } - } -} - -export function macosTahoeReleaseMock() { - return { - isMacosTahoeOrNewer: vi.fn(() => macosTahoeMock.value) - } -} - -export function appIconMock() { - return { - getAppIconPath: vi.fn(() => 'icon') - } -} - -export function browserManagerMock(): BrowserManagerModuleMock { - return { - browserManager: { - attachGuestPolicies: attachGuestPoliciesMock, - setDictationShortcutForwardingPredicate: vi.fn() - } - } -} - -export function resetMainWindowMocks(): void { - browserWindowMock.mockReset() - openExternalMock.mockReset() - attachGuestPoliciesMock.mockReset() - attachRouteGuestMock.mockReset() - attachRouteGuestMock.mockReturnValue(false) - retireRouteRendererMock.mockReset() - attachClientPageRendererMock.mockReset() - retireClientPageRendererMock.mockReset() - buildFromTemplateMock.mockClear() - menuPopupMock.mockClear() - notificationMock.mockClear() - notificationShowMock.mockClear() - powerMonitorOnMock.mockReset() - powerMonitorRemoveListenerMock.mockReset() - routePartitionAllowedMock.mockReset() - routePartitionAllowedMock.mockReturnValue(false) - appMock.isPackaged = true - macosTahoeMock.value = false - ipcMainMock.on.mockReset() - ipcMainMock.removeListener.mockReset() - ipcMainMock.handle.mockReset() - ipcMainMock.removeHandler.mockReset() -} - -export function withPlatform(platform: NodeJS.Platform, run: () => T): T { - const original = process.platform - Object.defineProperty(process, 'platform', { configurable: true, value: platform }) - try { - return run() - } finally { - Object.defineProperty(process, 'platform', { configurable: true, value: original }) - } -} - -export type RendererRecoveryWindowHarness = { - browserWindowInstance: { - loadFile: Mock<() => Promise> - loadURL: Mock<() => Promise> - webContents: { send: Mock } - } - windowHandlers: Record void> -} - -/** A main window whose webContents events land in `windowHandlers` for suites that drive renderer recovery. */ -export function createRendererRecoveryWindowHarness(): RendererRecoveryWindowHarness { - const windowHandlers: RendererRecoveryWindowHarness['windowHandlers'] = {} - const webContents = { - id: 143, - getURL: vi.fn(() => 'file:///opt/orca/renderer/index.html'), - isDestroyed: vi.fn(() => false), - on: vi.fn((event: string, handler: (...args: unknown[]) => void) => { - windowHandlers[event] = handler - }), - setZoomLevel: vi.fn(), - setBackgroundThrottling: vi.fn(), - setWebRTCIPHandlingPolicy: vi.fn(), - invalidate: vi.fn(), - setWindowOpenHandler: vi.fn(), - send: vi.fn() - } - const browserWindowInstance = { - webContents, - on: vi.fn((event: string, handler: (...args: unknown[]) => void) => { - windowHandlers[event] = handler - }), - isDestroyed: vi.fn(() => false), - isMaximized: vi.fn(() => true), - isFullScreen: vi.fn(() => false), - getSize: vi.fn(() => [1200, 800]), - setSize: vi.fn(), - maximize: vi.fn(), - show: vi.fn(), - loadFile: vi.fn(() => Promise.resolve()), - loadURL: vi.fn(() => Promise.resolve()) - } - browserWindowMock.mockImplementation(function () { - return browserWindowInstance - }) - - return { browserWindowInstance, windowHandlers } -} diff --git a/src/main/window/createMainWindow-user-quit-close.test.ts b/src/main/window/createMainWindow-user-quit-close.test.ts deleted file mode 100644 index 8726de70d0c..00000000000 --- a/src/main/window/createMainWindow-user-quit-close.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -vi.mock('electron', async () => - (await import('./createMainWindow-test-harness')).electronModuleMock() -) -vi.mock('./macos-tahoe-release', async () => - (await import('./createMainWindow-test-harness')).macosTahoeReleaseMock() -) -vi.mock('./desktop-integration/app-icon', async () => - (await import('./createMainWindow-test-harness')).appIconMock() -) -vi.mock('../browser/guest-lifecycle/manager/browser-manager', async () => - (await import('./createMainWindow-test-harness')).browserManagerMock() -) - -import { ipcMain } from 'electron' -import { createMainWindow, WINDOW_QUIT_RENDERER_ACK_TIMEOUT_MS } from './createMainWindow' -import { resetExpectedTeardownStateForTest } from '../crash-reporting/process-exits/expected-teardown-state' -import { browserWindowMock, resetMainWindowMocks } from './createMainWindow-test-harness' -import { markUserQuitWindowClose } from './user-quit-window-close' - -type Handler = (...args: any[]) => void - -function setupWindow() { - const windowHandlers: Record = {} - const ipcHandlers: Record = {} - vi.mocked(ipcMain.on).mockImplementation((channel, handler) => { - ipcHandlers[channel] = handler - return ipcMain - }) - const webContents = { - id: 42, - on: vi.fn((event: string, handler: Handler) => { - windowHandlers[event] = handler - }), - setZoomLevel: vi.fn(), - setBackgroundThrottling: vi.fn(), - setWebRTCIPHandlingPolicy: vi.fn(), - invalidate: vi.fn(), - setWindowOpenHandler: vi.fn(), - send: vi.fn(), - isCrashed: vi.fn(() => false) - } - const instance = { - webContents, - on: vi.fn((event: string, handler: Handler) => { - windowHandlers[event] = handler - }), - isDestroyed: vi.fn(() => false), - isMaximized: vi.fn(() => false), - isFullScreen: vi.fn(() => false), - isMinimized: vi.fn(() => false), - getSize: vi.fn(() => [1200, 800]), - setSize: vi.fn(), - maximize: vi.fn(), - show: vi.fn(), - hide: vi.fn(), - destroy: vi.fn(), - loadFile: vi.fn(() => Promise.resolve()), - loadURL: vi.fn(() => Promise.resolve()) - } - browserWindowMock.mockImplementation(function () { - return instance - }) - return { windowHandlers, ipcHandlers, webContents, instance } -} - -function lastCloseRequestId(send: ReturnType): number { - const requests = send.mock.calls.filter(([channel]) => channel === 'window:close-requested') - const request: unknown = requests.at(-1)?.[1] - return typeof request === 'object' && request !== null && 'requestId' in request - ? Number(request.requestId) - : -1 -} - -describe('serve user-Quit window close', () => { - const originalPlatform = process.platform - - beforeEach(() => { - resetMainWindowMocks() - resetExpectedTeardownStateForTest() - vi.useFakeTimers() - }) - - afterEach(() => { - Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true }) - vi.useRealTimers() - }) - - it('destroys a frozen renderer after the quit ack deadline without quitting the process', async () => { - const { windowHandlers, webContents, instance } = setupWindow() - createMainWindow(null, { getIsQuitting: () => false }) - - markUserQuitWindowClose(instance) - windowHandlers.close({ preventDefault: vi.fn() }) - - expect(webContents.send).toHaveBeenCalledWith('window:close-requested', { - isQuitting: false, - requestId: expect.any(Number) - }) - await vi.advanceTimersByTimeAsync(WINDOW_QUIT_RENDERER_ACK_TIMEOUT_MS) - expect(instance.destroy).toHaveBeenCalledOnce() - }) - - it('leaves a responsive renderer free to veto the close', async () => { - const { windowHandlers, ipcHandlers, webContents, instance } = setupWindow() - createMainWindow(null, { getIsQuitting: () => false }) - - markUserQuitWindowClose(instance) - windowHandlers.close({ preventDefault: vi.fn() }) - ipcHandlers['window:close-request-received']?.( - { sender: { id: 42 } }, - lastCloseRequestId(webContents.send) - ) - await vi.advanceTimersByTimeAsync(WINDOW_QUIT_RENDERER_ACK_TIMEOUT_MS) - - expect(instance.destroy).not.toHaveBeenCalled() - }) - - it('keeps a plain window close without a deadline', async () => { - const { windowHandlers, instance } = setupWindow() - createMainWindow(null, { getIsQuitting: () => false }) - - markUserQuitWindowClose(instance) - windowHandlers.close({ preventDefault: vi.fn() }) - windowHandlers['will-prevent-unload']?.() - windowHandlers.close({ preventDefault: vi.fn() }) - // The first attempt's deadline is cleared by the veto; the second is a plain close. - await vi.advanceTimersByTimeAsync(WINDOW_QUIT_RENDERER_ACK_TIMEOUT_MS * 2) - - expect(instance.destroy).not.toHaveBeenCalled() - }) -}) diff --git a/src/main/window/createMainWindow.ts b/src/main/window/createMainWindow.ts index 1e12a11b8a6..d7a1ef426ac 100644 --- a/src/main/window/createMainWindow.ts +++ b/src/main/window/createMainWindow.ts @@ -8,8 +8,8 @@ import { browserManager } from '../browser/guest-lifecycle/manager/browser-manag import { getBrowserClientHostId } from '../browser/client-hosts/lifecycle/browser-client-host-id' import { formatBrowserClientHostIdArgument } from '../../shared/browser/client-hosting/browser-client-host-id-argument' import { formatWindowMaterialArgument, resolveWindowMaterial } from '../../shared/window-material' -import { windowMaterialOptions } from './window-material-options' -import { rememberWindowMaterial } from './window-material-suppression' +import { windowMaterialOptions } from './presentation/window-material-options' +import { rememberWindowMaterial } from './presentation/window-material-suppression' import { markSystemSessionEnding } from '../crash-reporting/process-exits/expected-teardown-state' import { recordDurableCrashBreadcrumb } from '../crash-reporting/breadcrumbs/durable-crash-breadcrumb' import { clearTrustedUIRendererWebContentsId, setTrustedUIRendererWebContentsId } from '../ipc/ui' @@ -17,17 +17,17 @@ import { flushUnsavedCloudSessions } from '../orca-profiles/cloud-session/profil import type { Store } from '../persistence/loading-store/store' import { closeDashboardPopout } from './dashboard-popout-window' import { closeWorkspaceSpaceWindow } from './workspace-space-window' -import { - installMainWindowCloseLifecycle, - WINDOW_QUIT_RENDERER_ACK_TIMEOUT_MS -} from './main-window-close-lifecycle' -import type { CreateMainWindowOptions, MainWindowLoadObserver } from './main-window-contracts' -import { mainWindowLoadErrorCode } from './main-window-load-error-code' -import { installMainWindowFocusLifecycle } from './main-window-focus-lifecycle' -import { installMainWindowShortcutRouting } from './main-window-shortcut-routing' -import { installMainWindowStateLifecycle } from './main-window-state-lifecycle' -import { installMainWindowActivityRelay } from './main-window-activity' -import { stopHiddenWindowFramePumpOnShow } from './hidden-window-frame-pump' +import { installMainWindowCloseLifecycle } from './main-window/main-window-close-lifecycle' +import type { + CreateMainWindowOptions, + MainWindowLoadObserver +} from './main-window/main-window-contracts' +import { mainWindowLoadErrorCode } from './renderer-recovery/main-window-load-error-code' +import { installMainWindowFocusLifecycle } from './main-window/main-window-focus-lifecycle' +import { installMainWindowShortcutRouting } from './main-window/main-window-shortcut-routing' +import { installMainWindowStateLifecycle } from './main-window/main-window-state-lifecycle' +import { installMainWindowActivityRelay } from './main-window/main-window-activity' +import { stopHiddenWindowFramePumpOnShow } from './visibility/hidden-window-frame-pump' import { wslHostsWindowArguments } from '../wsl/host-lifecycle/wsl-host-gate' import { formatHiddenWindowFramePumpArgument, @@ -41,14 +41,12 @@ import { TITLEBAR_CSS_CENTER, TRAFFIC_LIGHT_RADIUS, TRAFFIC_LIGHT_X -} from './main-window-visual-lifecycle' -import { installMainWindowWebviewSecurity } from './main-window-webview-security' -import { rectHasVisibleAreaOnAnyDisplay } from './window-bounds-validation' +} from './main-window/main-window-visual-lifecycle' +import { installMainWindowWebviewSecurity } from './security/main-window-webview-security' +import { rectHasVisibleAreaOnAnyDisplay } from './presentation/window-bounds-validation' import { installWindowsPathRegistryChangeListener } from '../pty/environment/windows-path-registry-change' import { recordAgentSessionRuntimeEnd } from '../runtime/agent-session/records/agent-session-runtime-end-record' -export { WINDOW_QUIT_RENDERER_ACK_TIMEOUT_MS } - export function loadMainWindow(mainWindow: BrowserWindow, observer?: MainWindowLoadObserver): void { installRendererHangStackPolicy( mainWindow.webContents, diff --git a/src/main/window/dashboard-popout-window.ts b/src/main/window/dashboard-popout-window.ts index 2da942050a3..441b38458f0 100644 --- a/src/main/window/dashboard-popout-window.ts +++ b/src/main/window/dashboard-popout-window.ts @@ -2,20 +2,23 @@ import { mainI18n, translateMain } from '../i18n/main-i18n' import { app, BrowserWindow, nativeTheme, type WebContents, type Event } from 'electron' import { join } from 'node:path' import type { Store } from '../persistence/loading-store/store' -import { isBackgroundLaunch, showWindowWithoutStealingFocus } from './foreground-activation-policy' -import { rectHasVisibleAreaOnAnyDisplay } from './window-bounds-validation' +import { + isBackgroundLaunch, + showWindowWithoutStealingFocus +} from './activation/foreground-activation-policy' +import { rectHasVisibleAreaOnAnyDisplay } from './presentation/window-bounds-validation' import { sendToTrustedUIRenderer } from '../ipc/ui' -import { installPrivilegedWindowNavigationPolicy } from './privileged-window-navigation' +import { installPrivilegedWindowNavigationPolicy } from './security/privileged-window-navigation' import { stepUIZoomLevel, type UIZoomDirection } from '../../shared/ui-zoom-level' import { nativeZoomCommandMatchesKeybindings } from '../../shared/window-shortcut-policy' -import { - keybindingMatchesAction, - type KeybindingActionId, - type KeybindingInput, - type KeybindingOverrides -} from '../../shared/keybindings' +import { keybindingMatchesAction } from '../../shared/keybindings/matching' +import type { + KeybindingActionId, + KeybindingInput, + KeybindingOverrides +} from '../../shared/keybindings/types' import { wslHostsWindowArguments } from '../wsl/host-lifecycle/wsl-host-gate' -import { isMacAppPasteInput } from './mac-app-paste-input' +import { isMacAppPasteInput } from './editing/mac-app-paste-input' const MIN_WIDTH = 480 const MIN_HEIGHT = 360 diff --git a/src/main/window/macos-tahoe-release.ts b/src/main/window/desktop-integration/macos-tahoe-release.ts similarity index 100% rename from src/main/window/macos-tahoe-release.ts rename to src/main/window/desktop-integration/macos-tahoe-release.ts diff --git a/src/main/window/editable-context-menu-labels.ts b/src/main/window/editing/editable-context-menu-labels.ts similarity index 97% rename from src/main/window/editable-context-menu-labels.ts rename to src/main/window/editing/editable-context-menu-labels.ts index ec7b9c580cd..8dbc18939c1 100644 --- a/src/main/window/editable-context-menu-labels.ts +++ b/src/main/window/editing/editable-context-menu-labels.ts @@ -1,4 +1,4 @@ -import { translateMain } from '../i18n/main-i18n' +import { translateMain } from '../../i18n/main-i18n' /** Labels resolve at menu-build time so they follow the current UI language. */ export function editableContextMenuLabels() { diff --git a/src/main/window/editable-context-menu-popup.ts b/src/main/window/editing/editable-context-menu-popup.ts similarity index 100% rename from src/main/window/editable-context-menu-popup.ts rename to src/main/window/editing/editable-context-menu-popup.ts diff --git a/src/main/window/editable-context-menu.ts b/src/main/window/editing/editable-context-menu.ts similarity index 97% rename from src/main/window/editable-context-menu.ts rename to src/main/window/editing/editable-context-menu.ts index 3322028eec1..4949975c440 100644 --- a/src/main/window/editable-context-menu.ts +++ b/src/main/window/editing/editable-context-menu.ts @@ -3,9 +3,9 @@ import { type RichMarkdownContextMenuCommand, type RichMarkdownContextMenuCommandPayload, type RichMarkdownContextMenuTableTarget -} from '../../shared/rich-markdown-context-menu' -import type { ContextMenuSpelling } from '../../shared/browser/guest-lifecycle/browser-guest-events' -import { translateMain } from '../i18n/main-i18n' +} from '../../../shared/rich-markdown-context-menu' +import type { ContextMenuSpelling } from '../../../shared/browser/guest-lifecycle/browser-guest-events' +import { translateMain } from '../../i18n/main-i18n' import { editableContextMenuLabels } from './editable-context-menu-labels' type EditableContextMenuWebContents = Pick< diff --git a/src/main/window/mac-app-paste-input.test.ts b/src/main/window/editing/mac-app-paste-input.test.ts similarity index 100% rename from src/main/window/mac-app-paste-input.test.ts rename to src/main/window/editing/mac-app-paste-input.test.ts diff --git a/src/main/window/mac-app-paste-input.ts b/src/main/window/editing/mac-app-paste-input.ts similarity index 79% rename from src/main/window/mac-app-paste-input.ts rename to src/main/window/editing/mac-app-paste-input.ts index 2b5c91eef02..0459f5c64af 100644 --- a/src/main/window/mac-app-paste-input.ts +++ b/src/main/window/editing/mac-app-paste-input.ts @@ -1,4 +1,7 @@ -import { logicalKeyTokenFromInput, physicalCodeKeyTokenFromInput } from '../../shared/keybindings' +import { + logicalKeyTokenFromInput, + physicalCodeKeyTokenFromInput +} from '../../../shared/keybindings/input' export function isMacAppPasteInput(input: Electron.Input): boolean { return ( diff --git a/src/main/window/checked-drag-temp-source.ts b/src/main/window/file-drop/checked-drag-temp-source.ts similarity index 94% rename from src/main/window/checked-drag-temp-source.ts rename to src/main/window/file-drop/checked-drag-temp-source.ts index 79a0a0f049b..1f831eb060e 100644 --- a/src/main/window/checked-drag-temp-source.ts +++ b/src/main/window/file-drop/checked-drag-temp-source.ts @@ -2,15 +2,15 @@ import { createWriteStream, type Stats } from 'node:fs' import { lstat, mkdtemp, rm, writeFile, type FileHandle } from 'node:fs/promises' import { basename, join } from 'node:path' import { pipeline } from 'node:stream/promises' -import { AGENT_SESSION_ATTACHMENT_MAX_BYTES } from '../../shared/agent-session/agent-session-attachments' -import type { NativeFileDropCopyFailureReason } from '../../shared/file-import/native-file-drop' -import { readNodeFileHandleWithinLimit } from '../../shared/file-content/node-bounded-file-reader' +import { AGENT_SESSION_ATTACHMENT_MAX_BYTES } from '../../../shared/agent-session/agent-session-attachments' +import type { NativeFileDropCopyFailureReason } from '../../../shared/file-import/native-file-drop' +import { readNodeFileHandleWithinLimit } from '../../../shared/file-content/node-bounded-file-reader' import type { DragTempCopyBatch, DragTempCopyEnvironment, DragTempCopyItemResult } from './dragged-temp-file-copy' -import { ensureOwnedTempStagingRoot } from './owned-temp-staging-root' +import { ensureOwnedTempStagingRoot } from '../../filesystem/owned-temp-staging-root' const COPY_DIR_PREFIX = 'orca-drop-' const COPY_HIGH_WATER_MARK = 1024 * 1024 diff --git a/src/main/window/darwin-user-temp-dir.test.ts b/src/main/window/file-drop/darwin-user-temp-dir.test.ts similarity index 100% rename from src/main/window/darwin-user-temp-dir.test.ts rename to src/main/window/file-drop/darwin-user-temp-dir.test.ts diff --git a/src/main/window/darwin-user-temp-dir.ts b/src/main/window/file-drop/darwin-user-temp-dir.ts similarity index 100% rename from src/main/window/darwin-user-temp-dir.ts rename to src/main/window/file-drop/darwin-user-temp-dir.ts diff --git a/src/main/window/drag-temp-path.ts b/src/main/window/file-drop/drag-temp-path.ts similarity index 100% rename from src/main/window/drag-temp-path.ts rename to src/main/window/file-drop/drag-temp-path.ts diff --git a/src/main/window/dragged-temp-file-copy.test.ts b/src/main/window/file-drop/dragged-temp-file-copy.test.ts similarity index 98% rename from src/main/window/dragged-temp-file-copy.test.ts rename to src/main/window/file-drop/dragged-temp-file-copy.test.ts index ba806ce338c..868504568b9 100644 --- a/src/main/window/dragged-temp-file-copy.test.ts +++ b/src/main/window/file-drop/dragged-temp-file-copy.test.ts @@ -15,7 +15,7 @@ import { tmpdir } from 'node:os' import { basename, dirname, join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { runProcess } from '@orca/process-host' -import type * as RuntimeImportLimits from '../ipc/runtime-import-limits' +import type * as RuntimeImportLimits from '../../ipc/runtime-import-limits' const fsFaults: { beforeCopyWrite: (() => void) | null @@ -36,7 +36,7 @@ vi.mock('node:fs/promises', async (importOriginal) => { } }) -vi.mock('../ipc/runtime-import-limits', async (importOriginal) => ({ +vi.mock('../../ipc/runtime-import-limits', async (importOriginal) => ({ ...(await importOriginal()), REMOTE_IMPORT_MAX_FILE_BYTES: 10, REMOTE_IMPORT_MAX_TOTAL_BYTES: 16 diff --git a/src/main/window/dragged-temp-file-copy.ts b/src/main/window/file-drop/dragged-temp-file-copy.ts similarity index 97% rename from src/main/window/dragged-temp-file-copy.ts rename to src/main/window/file-drop/dragged-temp-file-copy.ts index c21519fb377..65f117ef4fe 100644 --- a/src/main/window/dragged-temp-file-copy.ts +++ b/src/main/window/file-drop/dragged-temp-file-copy.ts @@ -1,19 +1,19 @@ import { constants, type Stats } from 'node:fs' import { lstat, open, realpath, rm } from 'node:fs/promises' import { basename, resolve } from 'node:path' -import type { NativeFileDropCopyFailureReason } from '../../shared/file-import/native-file-drop' +import type { NativeFileDropCopyFailureReason } from '../../../shared/file-import/native-file-drop' import { formatByteCeiling, REMOTE_IMPORT_MAX_FILE_BYTES, REMOTE_IMPORT_MAX_TOTAL_BYTES -} from '../ipc/runtime-import-limits' +} from '../../ipc/runtime-import-limits' import { hasDragTempMarker, isPathWithin, mayNeedDragTempCopy, relativeSegments } from './drag-temp-path' -import { getOwnedTempStagingRoot } from './owned-temp-staging-root' +import { getOwnedTempStagingRoot } from '../../filesystem/owned-temp-staging-root' import { classifyFailure, copyCheckedDragTempSource, diff --git a/src/main/window/dropped-path-preparation-ipc.test.ts b/src/main/window/file-drop/dropped-path-preparation-ipc.test.ts similarity index 100% rename from src/main/window/dropped-path-preparation-ipc.test.ts rename to src/main/window/file-drop/dropped-path-preparation-ipc.test.ts diff --git a/src/main/window/dropped-path-preparation-ipc.ts b/src/main/window/file-drop/dropped-path-preparation-ipc.ts similarity index 93% rename from src/main/window/dropped-path-preparation-ipc.ts rename to src/main/window/file-drop/dropped-path-preparation-ipc.ts index 92e5c558c45..fe02210f706 100644 --- a/src/main/window/dropped-path-preparation-ipc.ts +++ b/src/main/window/file-drop/dropped-path-preparation-ipc.ts @@ -2,12 +2,12 @@ import { app, ipcMain, type BrowserWindow } from 'electron' import { createRejectedNativeFileDropPayload, validateNativeFileDropPaths -} from '../../shared/file-import/native-file-drop' +} from '../../../shared/file-import/native-file-drop' import type { PrepareDroppedPathsRequest, PreparedDroppedPaths -} from '../../shared/file-import/native-file-drop-preparation' -import { abortWhenRendererGone } from '../ipc/renderer-lifetime-abort' +} from '../../../shared/file-import/native-file-drop-preparation' +import { abortWhenRendererGone } from '../../ipc/renderer-lifetime-abort' import { getDragTempCopyRoot } from './dragged-temp-file-copy' import { createDroppedPathPreparationQueue, diff --git a/src/main/window/dropped-path-preparation.test.ts b/src/main/window/file-drop/dropped-path-preparation.test.ts similarity index 85% rename from src/main/window/dropped-path-preparation.test.ts rename to src/main/window/file-drop/dropped-path-preparation.test.ts index 50a3264ebb7..8e4ea2f8cc9 100644 --- a/src/main/window/dropped-path-preparation.test.ts +++ b/src/main/window/file-drop/dropped-path-preparation.test.ts @@ -11,8 +11,8 @@ import { writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' -import { AgentSessionAttachmentStore } from '../native-chat/agent-session-attachments/agent-session-attachment-store' -import { AGENT_SESSION_ATTACHMENT_MAX_BYTES } from '../../shared/agent-session/agent-session-attachments' +import { AgentSessionAttachmentStore } from '../../native-chat/agent-session-attachments/agent-session-attachment-store' +import { AGENT_SESSION_ATTACHMENT_MAX_BYTES } from '../../../shared/agent-session/agent-session-attachments' import { runProcess } from '@orca/process-host' import { dirname, join } from 'node:path' import { beforeEach, describe, expect, it, onTestFinished, vi } from 'vitest' @@ -278,32 +278,6 @@ describe('dropped path preparation through the shared copy queue', () => { expect(handles.every((handle) => handle.fd === -1)).toBe(true) }) - it.each(['agent', 'main-reader'] as const)( - 'withholds uncopied originals only from agents: %s', - async (consumer) => { - materialize.mockResolvedValue([ - { sourcePath: ORDINARY, status: 'imported', destPath: ORDINARY }, - { sourcePath: TEMP, status: 'uncopied', reason: 'storage-full' } - ]) - const { enqueue } = queue() - const result = await enqueue({ paths: [ORDINARY, TEMP], consumer }) - expect(result.paths).toEqual(consumer === 'agent' ? [ORDINARY] : [ORDINARY, TEMP]) - expect(result.failures).toEqual( - consumer === 'agent' - ? [ - { - byteLength: 0, - pathCount: 1, - reason: 'temp-copy-failed', - target: 'rejected', - commonReason: 'storage-full' - } - ] - : [] - ) - } - ) - it('shares serialization and the pending cap across replacement-window queues', async () => { materialize.mockReturnValue(new Promise(() => undefined)) const lane = createDragTempCopyLane() @@ -334,36 +308,6 @@ describe('dropped path preparation through the shared copy queue', () => { await pending }) - it('aborts running and queued requests on reload and releases their listeners', async () => { - materialize.mockReturnValue(new Promise(() => undefined)) - const { enqueue, controller, dispose } = queue() - const running = enqueue({ paths: [TEMP], consumer: 'agent' }) - const queued = enqueue({ paths: [TEMP], consumer: 'agent' }) - const assertions = [ - expect(running).rejects.toThrow('reloaded'), - expect(queued).rejects.toThrow('reloaded') - ] - await settle() - controller.abort(new Error('reloaded')) - await Promise.all(assertions) - await settle() - expect(materialize).toHaveBeenCalledTimes(1) - expect(dispose).toHaveBeenCalledTimes(2) - }) - - it('reports timeout, keeps ordinary paths, and serves the next copy', async () => { - materialize - .mockReturnValueOnce(new Promise(() => undefined)) - .mockResolvedValueOnce([{ sourcePath: TEMP, status: 'imported', destPath: COPY }]) - const { enqueue } = queue(undefined, 5) - const first = enqueue({ paths: [ORDINARY, TEMP], consumer: 'agent' }) - const next = enqueue({ paths: [TEMP], consumer: 'agent' }) - await expect(first).resolves.toMatchObject({ - paths: [ORDINARY], - failures: [{ commonReason: 'timed-out' }] - }) - await expect(next).resolves.toEqual({ paths: [COPY], failures: [] }) - }) it('passes drag-temp-looking paths unchanged on Linux and Windows', () => { for (const platform of ['linux', 'win32'] as const) { const enqueue = createDroppedPathPreparationQueue({ diff --git a/src/main/window/dropped-path-preparation.ts b/src/main/window/file-drop/dropped-path-preparation.ts similarity index 95% rename from src/main/window/dropped-path-preparation.ts rename to src/main/window/file-drop/dropped-path-preparation.ts index 2e98806c196..ad6588a3b0b 100644 --- a/src/main/window/dropped-path-preparation.ts +++ b/src/main/window/file-drop/dropped-path-preparation.ts @@ -1,11 +1,11 @@ -import { AGENT_SESSION_ATTACHMENT_MAX_BYTES } from '../../shared/agent-session/agent-session-attachments' +import { AGENT_SESSION_ATTACHMENT_MAX_BYTES } from '../../../shared/agent-session/agent-session-attachments' import { basename } from 'node:path' -import type { AgentSessionAttachmentStore } from '../native-chat/agent-session-attachments/agent-session-attachment-store' -import type { NativeFileDropCopyFailureReason } from '../../shared/file-import/native-file-drop' +import type { AgentSessionAttachmentStore } from '../../native-chat/agent-session-attachments/agent-session-attachment-store' +import type { NativeFileDropCopyFailureReason } from '../../../shared/file-import/native-file-drop' import type { PrepareDroppedPathsRequest, PreparedDroppedPaths -} from '../../shared/file-import/native-file-drop-preparation' +} from '../../../shared/file-import/native-file-drop-preparation' import { DragTempCopyBatch, closeDragTempSource, diff --git a/src/main/window/macos-app-activation.test.ts b/src/main/window/macos-app-activation.test.ts deleted file mode 100644 index e4670422cb4..00000000000 --- a/src/main/window/macos-app-activation.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { BrowserWindow } from 'electron' -import { describe, expect, it, vi } from 'vitest' -import { createMacAppActivationHandler } from './macos-app-activation' - -function makeWindow(destroyed = false): BrowserWindow { - return { - isDestroyed: vi.fn(() => destroyed) - } as unknown as BrowserWindow -} - -describe('createMacAppActivationHandler', () => { - it('leaves an existing window to native macOS activation', () => { - const requestActivation = vi.fn() - const handler = createMacAppActivationHandler({ - getWindow: () => makeWindow(), - requestActivation - }) - - handler() - - expect(requestActivation).not.toHaveBeenCalled() - }) - - it.each([null, makeWindow(true)])( - 'requests desktop activation for a missing or destroyed window', - (window) => { - const requestActivation = vi.fn() - const handler = createMacAppActivationHandler({ - getWindow: () => window, - requestActivation - }) - - handler() - - expect(requestActivation).toHaveBeenCalledTimes(1) - } - ) -}) diff --git a/src/main/window/macos-desktop-handoff.test.ts b/src/main/window/macos-desktop-handoff.test.ts deleted file mode 100644 index 1ad987ec0a9..00000000000 --- a/src/main/window/macos-desktop-handoff.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { EventEmitter } from 'node:events' -import { afterEach, describe, expect, it, vi } from 'vitest' - -const { spawnProcessMock } = vi.hoisted(() => ({ spawnProcessMock: vi.fn() })) - -vi.mock('electron', () => ({ - app: { - getPath: () => '/Applications/Orca.app/Contents/MacOS/Orca', - dock: { hide: vi.fn() }, - setActivationPolicy: vi.fn() - } -})) -vi.mock('@orca/process-host', () => ({ spawnProcess: spawnProcessMock })) - -const { createDesktopHandoff, handOffToDesktopApp } = await import('./macos-desktop-handoff') - -function makeChild(): EventEmitter & { unref: ReturnType } { - return Object.assign(new EventEmitter(), { unref: vi.fn() }) -} - -afterEach(() => { - spawnProcessMock.mockReset() - vi.restoreAllMocks() -}) - -describe('handOffToDesktopApp', () => { - it('opens a new instance of the installed bundle with a Dock launch environment', () => { - const child = makeChild() - spawnProcessMock.mockReturnValue(child) - - handOffToDesktopApp([], { - platform: 'darwin', - env: {}, - reapplyActivationPolicy: vi.fn() - }) - - expect(spawnProcessMock).toHaveBeenCalledWith({ - program: '/usr/bin/open', - args: ['-n', '/Applications/Orca.app'], - env: {}, - detached: true, - stdio: 'ignore' - }) - expect(child.unref).toHaveBeenCalledOnce() - }) - - it('passes buffered links and documents to the new instance', () => { - spawnProcessMock.mockReturnValue(makeChild()) - - handOffToDesktopApp(['orca://skills/share/abc', '/Users/me/notes.md'], { - platform: 'darwin', - env: {}, - executablePath: '/Users/me/Applications/Orca.app/Contents/MacOS/Orca', - reapplyActivationPolicy: vi.fn() - }) - - expect(spawnProcessMock.mock.calls[0][0].args).toEqual([ - '-n', - '/Users/me/Applications/Orca.app', - '--args', - 'orca://skills/share/abc', - '/Users/me/notes.md' - ]) - }) - - it('drops back out of the Dock after launching the desktop app', () => { - spawnProcessMock.mockReturnValue(makeChild()) - const reapplyActivationPolicy = vi.fn() - - handOffToDesktopApp([], { - platform: 'darwin', - env: {}, - reapplyActivationPolicy - }) - - expect(reapplyActivationPolicy).toHaveBeenCalledOnce() - expect(reapplyActivationPolicy.mock.invocationCallOrder[0]).toBeGreaterThan( - spawnProcessMock.mock.invocationCallOrder[0] - ) - }) - - it('logs a failed launch instead of taking main down', () => { - const child = makeChild() - spawnProcessMock.mockReturnValue(child) - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) - - handOffToDesktopApp([], { - platform: 'darwin', - env: {}, - reapplyActivationPolicy: vi.fn() - }) - child.emit('error', new Error('spawn /usr/bin/open ENOENT')) - - expect(warn).toHaveBeenCalledOnce() - }) - - it.each([{ ORCA_BACKGROUND_LAUNCH: '1' }, { ORCA_E2E_USER_DATA_DIR: '/tmp/e2e' }])( - 'never launches the real desktop app from an automated sidecar (%o)', - (env) => { - const reapplyActivationPolicy = vi.fn() - - handOffToDesktopApp([], { - platform: 'darwin', - env, - reapplyActivationPolicy - }) - - expect(spawnProcessMock).not.toHaveBeenCalled() - expect(reapplyActivationPolicy).toHaveBeenCalledOnce() - } - ) -}) - -describe('createDesktopHandoff', () => { - const nextTurn = (): Promise => new Promise((resolve) => setImmediate(resolve)) - - it('hands a multi-file open over in one launch carrying every file', async () => { - const pending: string[] = [] - const handOff = vi.fn() - const requestHandoff = createDesktopHandoff({ - drainArguments: () => pending.splice(0), - handOff - }) - - for (const file of ['/Users/me/a.md', '/Users/me/b.md', '/Users/me/c.csv']) { - pending.push(file) - requestHandoff() - } - expect(handOff).not.toHaveBeenCalled() - await nextTurn() - - expect(handOff).toHaveBeenCalledOnce() - expect(handOff).toHaveBeenCalledWith(['/Users/me/a.md', '/Users/me/b.md', '/Users/me/c.csv']) - }) - - it('launches again for a later open', async () => { - const handOff = vi.fn() - const requestHandoff = createDesktopHandoff({ - drainArguments: () => [], - handOff - }) - - requestHandoff() - await nextTurn() - requestHandoff() - await nextTurn() - - expect(handOff).toHaveBeenCalledTimes(2) - }) -}) diff --git a/src/main/window/main-window-state-lifecycle.test.ts b/src/main/window/main-window-state-lifecycle.test.ts deleted file mode 100644 index 5cc44785853..00000000000 --- a/src/main/window/main-window-state-lifecycle.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { EventEmitter } from 'node:events' -import { afterEach, beforeEach, expect, it, vi } from 'vitest' - -const { appMock, launchPolicy } = await vi.hoisted(async () => { - const { EventEmitter } = await import('node:events') - return { appMock: new EventEmitter(), launchPolicy: { background: false, windowless: true } } -}) -vi.mock('electron', () => ({ app: appMock })) -vi.mock('./foreground-activation-policy', () => ({ - isBackgroundLaunch: () => launchPolicy.background, - isWindowlessLaunch: () => launchPolicy.windowless, - showWindowWithoutStealingFocus: vi.fn() -})) -vi.mock('./main-window-visual-lifecycle', () => ({ - MIN_WIDTH: 480, - MIN_HEIGHT: 360, - syncTrafficLightPosition: vi.fn() -})) - -import { installMainWindowStateLifecycle } from './main-window-state-lifecycle' - -beforeEach(() => { - vi.useFakeTimers() - appMock.removeAllListeners() - launchPolicy.background = false - launchPolicy.windowless = true -}) -afterEach(() => vi.useRealTimers()) - -it('continues saving bounds after an updater quit veto and freezes them on allowed quit', async () => { - const mainWindow = Object.assign(new EventEmitter(), { - webContents: Object.assign(new EventEmitter(), { - send: vi.fn(), - setZoomLevel: vi.fn() - }), - isDestroyed: () => false, - isFullScreen: () => false, - isMaximized: () => false, - getBounds: () => ({ x: 0, y: 0, width: 1200, height: 800 }) - }) - const updateUI = vi.fn() - const lifecycle = installMainWindowStateLifecycle({ - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This fixture implements the window members read by the bounds lifecycle. - mainWindow: mainWindow as never, - revealOnDidFinishLoad: false, - savedMaximized: false, - savedFullScreen: false, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Bounds persistence reads only updateUI from this store fixture. - store: { updateUI } as never - }) - appMock.emit('before-quit', { defaultPrevented: true }) - expect(lifecycle.isWindowClosing()).toBe(false) - mainWindow.emit('resize') - await vi.advanceTimersByTimeAsync(500) - expect(updateUI).toHaveBeenCalledWith({ - windowMaximized: false, - windowBounds: { x: 0, y: 0, width: 1200, height: 800 } - }) - - updateUI.mockClear() - appMock.emit('before-quit', { defaultPrevented: false }) - expect(lifecycle.isWindowClosing()).toBe(true) - mainWindow.emit('resize') - await vi.advanceTimersByTimeAsync(500) - expect(updateUI).not.toHaveBeenCalled() - lifecycle.clearInitialRevealFallbackTimer() - lifecycle.dispose() -}) - -function installFullScreenFixture(): { - lifecycle: ReturnType - mainWindow: EventEmitter & { setFullScreen: (flag: boolean) => void } - windowState: { maximized: boolean; fullScreen: boolean; maximizedUnderFullScreen: boolean } - persisted: Record -} { - launchPolicy.windowless = false - const windowState = { maximized: false, fullScreen: false, maximizedUnderFullScreen: false } - const mainWindow = Object.assign(new EventEmitter(), { - webContents: Object.assign(new EventEmitter(), { send: vi.fn(), setZoomLevel: vi.fn() }), - isDestroyed: () => false, - isFullScreen: () => windowState.fullScreen, - getBounds: () => ({ x: 0, y: 0, width: 2560, height: 1440 }), - maximize: () => { - windowState.maximized = true - }, - setFullScreen: (flag: boolean) => { - if (flag) { - windowState.maximizedUnderFullScreen = windowState.maximized - } - windowState.fullScreen = flag - } - }) - const persisted: Record = {} - const lifecycle = installMainWindowStateLifecycle({ - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This fixture implements the window members read by reveal and fullscreen persistence. - mainWindow: mainWindow as never, - revealOnDidFinishLoad: false, - savedMaximized: true, - savedFullScreen: true, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Fullscreen persistence reads only updateUI from this store fixture. - store: { - updateUI: (patch: Record) => Object.assign(persisted, patch) - } as never - }) - return { lifecycle, mainWindow, windowState, persisted } -} - -it('restores native fullscreen over the maximized frame and keeps that frame while fullscreen', () => { - const { lifecycle, mainWindow, windowState, persisted } = installFullScreenFixture() - const leaveFullScreen = (): void => { - mainWindow.setFullScreen(false) - mainWindow.emit('leave-full-screen') - } - mainWindow.emit('ready-to-show') - expect(windowState).toEqual({ maximized: true, fullScreen: true, maximizedUnderFullScreen: true }) - // macOS emits unmaximize mid-transition, after the fullscreen style mask is set. - mainWindow.emit('unmaximize') - mainWindow.emit('enter-full-screen') - expect(persisted).toEqual({ windowFullScreen: true }) - - leaveFullScreen() - expect(persisted).toEqual({ windowFullScreen: false }) - mainWindow.emit('unmaximize') - expect(persisted).toEqual({ - windowFullScreen: false, - windowMaximized: false, - windowBounds: { x: 0, y: 0, width: 2560, height: 1440 } - }) - - mainWindow.setFullScreen(true) - mainWindow.emit('enter-full-screen') - appMock.emit('before-quit', { defaultPrevented: false }) - leaveFullScreen() - expect(persisted).toMatchObject({ windowFullScreen: true }) - lifecycle.clearInitialRevealFallbackTimer() - lifecycle.dispose() -}) - -it('does not restore native fullscreen for background launches', () => { - launchPolicy.background = true - const { lifecycle, mainWindow, windowState } = installFullScreenFixture() - mainWindow.emit('ready-to-show') - expect(windowState).toEqual({ - maximized: true, - fullScreen: false, - maximizedUnderFullScreen: false - }) - lifecycle.clearInitialRevealFallbackTimer() - lifecycle.dispose() -}) diff --git a/src/main/window/main-window-activity.ts b/src/main/window/main-window/main-window-activity.ts similarity index 100% rename from src/main/window/main-window-activity.ts rename to src/main/window/main-window/main-window-activity.ts diff --git a/src/main/window/main-window-close-lifecycle.ts b/src/main/window/main-window/main-window-close-lifecycle.ts similarity index 96% rename from src/main/window/main-window-close-lifecycle.ts rename to src/main/window/main-window/main-window-close-lifecycle.ts index dd6ba13f6cc..2275643160a 100644 --- a/src/main/window/main-window-close-lifecycle.ts +++ b/src/main/window/main-window/main-window-close-lifecycle.ts @@ -1,7 +1,7 @@ import { ipcMain, Menu, Notification, type BrowserWindow } from 'electron' -import { QUIT_RENDERER_ACK_TIMEOUT_MS } from '../../shared/quit-teardown-deadline' -import { translateMain } from '../i18n/main-i18n' -import type { Store } from '../persistence/loading-store/store' +import { QUIT_RENDERER_ACK_TIMEOUT_MS } from '../../../shared/quit-teardown-deadline' +import { translateMain } from '../../i18n/main-i18n' +import type { Store } from '../../persistence/loading-store/store' import { resolveWindowCloseAction } from './window-close-decision' import type { CreateMainWindowOptions } from './main-window-contracts' import type { MainWindowFocusLifecycle } from './main-window-focus-lifecycle' @@ -9,8 +9,6 @@ import type { MainWindowStateLifecycle } from './main-window-state-lifecycle' import { syncTrafficLightPosition } from './main-window-visual-lifecycle' import { consumeUserQuitWindowClose } from './user-quit-window-close' -export const WINDOW_QUIT_RENDERER_ACK_TIMEOUT_MS = QUIT_RENDERER_ACK_TIMEOUT_MS - export function installMainWindowCloseLifecycle(args: { focus: MainWindowFocusLifecycle mainWindow: BrowserWindow @@ -50,7 +48,7 @@ export function installMainWindowCloseLifecycle(args: { console.warn('[window] Renderer did not acknowledge quit; destroying unresponsive window') state.freezeBoundsOnQuit() mainWindow.destroy() - }, WINDOW_QUIT_RENDERER_ACK_TIMEOUT_MS) + }, QUIT_RENDERER_ACK_TIMEOUT_MS) quitRendererAckTimer.unref?.() } let lastAcknowledgedRequestId = 0 diff --git a/src/main/window/main-window-contracts.ts b/src/main/window/main-window/main-window-contracts.ts similarity index 89% rename from src/main/window/main-window-contracts.ts rename to src/main/window/main-window/main-window-contracts.ts index 91937c3b16b..0d84dd910c7 100644 --- a/src/main/window/main-window-contracts.ts +++ b/src/main/window/main-window/main-window-contracts.ts @@ -1,11 +1,11 @@ -import type { KeybindingOverrides } from '../../shared/keybindings' -import type { LowCommitOomVerdict } from '../crash-reporting/recovery/low-commit-oom-recovery-gate' +import type { KeybindingOverrides } from '../../../shared/keybindings/types' +import type { LowCommitOomVerdict } from '../../crash-reporting/recovery/low-commit-oom-recovery-gate' import type { RecoveryExhaustionCause, RecoveryReloadMilestone, RecoveryReloadTrigger -} from './renderer-recovery-reload-watchdog' -import type { RendererLivenessAtDeath } from './renderer-liveness-at-death' +} from '../renderer-recovery/renderer-recovery-reload-watchdog' +import type { RendererLivenessAtDeath } from '../renderer-recovery/renderer-liveness-at-death' /** Per-load outcome from Electron's load promise, which is scoped to that one load unlike `did-finish-load`. */ export type MainWindowLoadObserver = { diff --git a/src/main/window/main-window-focus-lifecycle.ts b/src/main/window/main-window/main-window-focus-lifecycle.ts similarity index 92% rename from src/main/window/main-window-focus-lifecycle.ts rename to src/main/window/main-window/main-window-focus-lifecycle.ts index ba9c1380998..c14f19d2b31 100644 --- a/src/main/window/main-window-focus-lifecycle.ts +++ b/src/main/window/main-window/main-window-focus-lifecycle.ts @@ -1,32 +1,32 @@ import { ipcMain, type BrowserWindow } from 'electron' -import { isCrashReportReason } from '../../shared/crash-reporting' +import { isCrashReportReason } from '../../../shared/crash-reporting' import { plainTextContextMenuTargetChannel, richMarkdownContextMenuTargetChannel, type RichMarkdownContextMenuTableTarget -} from '../../shared/rich-markdown-context-menu' +} from '../../../shared/rich-markdown-context-menu' import { DEFAULT_RENDERER_RECOVERY_MAX_RECOVERIES, DEFAULT_RENDERER_RECOVERY_WINDOW_MS, RendererRecoveryCircuitBreaker -} from '../crash-reporting/recovery/renderer-recovery-circuit-breaker' -import { createLowCommitOomRecoveryGate } from '../crash-reporting/recovery/low-commit-oom-recovery-gate' +} from '../../crash-reporting/recovery/renderer-recovery-circuit-breaker' +import { createLowCommitOomRecoveryGate } from '../../crash-reporting/recovery/low-commit-oom-recovery-gate' import { buildEditableContextMenuTemplate, matchingRichMarkdownContextMenuTableTarget, parseRichMarkdownContextMenuTableTarget -} from './editable-context-menu' -import { createEditableContextMenuPopup } from './editable-context-menu-popup' +} from '../editing/editable-context-menu' +import { createEditableContextMenuPopup } from '../editing/editable-context-menu-popup' import type { CreateMainWindowOptions, MainWindowLoadObserver } from './main-window-contracts' -import { browserRouteWebContentsRegistry } from '../browser/routing/sessions/browser-route-session-runtime' +import { browserRouteWebContentsRegistry } from '../../browser/routing/sessions/browser-route-session-runtime' import { attachBrowserClientPageRenderer, retireBrowserClientPageRenderer -} from '../browser/client-pages/renderer/browser-client-page-renderer-runtime' -import { registerRendererDocumentNavigation } from './renderer-document-navigation' -import { createRendererRecoveryReloadWatchdog } from './renderer-recovery-reload-watchdog' -import { createRendererLaunchFailureBackoff } from './renderer-launch-failure-backoff' -import { trackRendererLivenessAtDeath } from './renderer-liveness-at-death' +} from '../../browser/client-pages/renderer/browser-client-page-renderer-runtime' +import { registerRendererDocumentNavigation } from '../security/renderer-document-navigation' +import { createRendererRecoveryReloadWatchdog } from '../renderer-recovery/renderer-recovery-reload-watchdog' +import { createRendererLaunchFailureBackoff } from '../renderer-recovery/renderer-launch-failure-backoff' +import { trackRendererLivenessAtDeath } from '../renderer-recovery/renderer-liveness-at-death' export type MainWindowFocusLifecycle = { dispose: () => void diff --git a/src/main/window/main-window-shortcut-actions.ts b/src/main/window/main-window/main-window-shortcut-actions.ts similarity index 96% rename from src/main/window/main-window-shortcut-actions.ts rename to src/main/window/main-window/main-window-shortcut-actions.ts index 03acfaa28ab..80663d860f2 100644 --- a/src/main/window/main-window-shortcut-actions.ts +++ b/src/main/window/main-window/main-window-shortcut-actions.ts @@ -1,5 +1,5 @@ import type { BrowserWindow } from 'electron' -import type { WindowShortcutAction } from '../../shared/window-shortcut-policy' +import type { WindowShortcutAction } from '../../../shared/window-shortcut-policy' export function sendResolvedWindowShortcutAction( mainWindow: BrowserWindow, diff --git a/src/main/window/main-window-shortcut-routing.ts b/src/main/window/main-window/main-window-shortcut-routing.ts similarity index 94% rename from src/main/window/main-window-shortcut-routing.ts rename to src/main/window/main-window/main-window-shortcut-routing.ts index ce6f15827eb..32333f87e8a 100644 --- a/src/main/window/main-window-shortcut-routing.ts +++ b/src/main/window/main-window/main-window-shortcut-routing.ts @@ -3,11 +3,9 @@ import type { BrowserWindow } from 'electron' import { ModifierDoubleTapDetector, toModifierDoubleTapEvent -} from '../../shared/modifier-double-tap-detector' -import { - normalizeTerminalShortcutPolicy, - type KeybindingMatchOptions -} from '../../shared/keybindings' +} from '../../../shared/modifier-double-tap-detector' +import { normalizeTerminalShortcutPolicy } from '../../../shared/keybindings/effective' +import type { KeybindingMatchOptions } from '../../../shared/keybindings/types' import { getWindowShortcutActionId, matchesRecentTabSwitcherChord, @@ -15,18 +13,18 @@ import { resolveWindowShortcutAction, windowShortcutActionCapturesTerminal, type WindowShortcutAction -} from '../../shared/window-shortcut-policy' -import type { Store } from '../persistence/loading-store/store' +} from '../../../shared/window-shortcut-policy' +import type { Store } from '../../persistence/loading-store/store' import type { CreateMainWindowOptions } from './main-window-contracts' import type { MainWindowFocusLifecycle } from './main-window-focus-lifecycle' import { sendResolvedWindowShortcutAction } from './main-window-shortcut-actions' -import { isMacAppPasteInput } from './mac-app-paste-input' -import { dictationShortcutMode } from '../../shared/dictation-shortcut-mode' +import { isMacAppPasteInput } from '../editing/mac-app-paste-input' +import { dictationShortcutMode } from '../../../shared/dictation-shortcut-mode' import { nativeChatPromptClaimsShortcut, textInputOwnsComposition, type TextShortcutInput -} from '../../shared/text-input-shortcut-policy' +} from '../../../shared/text-input-shortcut-policy' export function installMainWindowShortcutRouting(args: { focus: MainWindowFocusLifecycle diff --git a/src/main/window/main-window/main-window-state-lifecycle.test.ts b/src/main/window/main-window/main-window-state-lifecycle.test.ts new file mode 100644 index 00000000000..e10da85f2c8 --- /dev/null +++ b/src/main/window/main-window/main-window-state-lifecycle.test.ts @@ -0,0 +1,68 @@ +import { EventEmitter } from 'node:events' +import { afterEach, beforeEach, expect, it, vi } from 'vitest' + +const { appMock, launchPolicy } = await vi.hoisted(async () => { + const { EventEmitter } = await import('node:events') + return { appMock: new EventEmitter(), launchPolicy: { background: false, windowless: true } } +}) +vi.mock('electron', () => ({ app: appMock })) +vi.mock('../activation/foreground-activation-policy', () => ({ + isBackgroundLaunch: () => launchPolicy.background, + isWindowlessLaunch: () => launchPolicy.windowless, + showWindowWithoutStealingFocus: vi.fn() +})) +vi.mock('./main-window-visual-lifecycle', () => ({ + MIN_WIDTH: 480, + MIN_HEIGHT: 360, + syncTrafficLightPosition: vi.fn() +})) + +import { installMainWindowStateLifecycle } from './main-window-state-lifecycle' + +beforeEach(() => { + vi.useFakeTimers() + appMock.removeAllListeners() + launchPolicy.background = false + launchPolicy.windowless = true +}) +afterEach(() => vi.useRealTimers()) + +it('continues saving bounds after an updater quit veto and freezes them on allowed quit', async () => { + const mainWindow = Object.assign(new EventEmitter(), { + webContents: Object.assign(new EventEmitter(), { + send: vi.fn(), + setZoomLevel: vi.fn() + }), + isDestroyed: () => false, + isFullScreen: () => false, + isMaximized: () => false, + getBounds: () => ({ x: 0, y: 0, width: 1200, height: 800 }) + }) + const updateUI = vi.fn() + const lifecycle = installMainWindowStateLifecycle({ + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This fixture implements the window members read by the bounds lifecycle. + mainWindow: mainWindow as never, + revealOnDidFinishLoad: false, + savedMaximized: false, + savedFullScreen: false, + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Bounds persistence reads only updateUI from this store fixture. + store: { updateUI } as never + }) + appMock.emit('before-quit', { defaultPrevented: true }) + expect(lifecycle.isWindowClosing()).toBe(false) + mainWindow.emit('resize') + await vi.advanceTimersByTimeAsync(500) + expect(updateUI).toHaveBeenCalledWith({ + windowMaximized: false, + windowBounds: { x: 0, y: 0, width: 1200, height: 800 } + }) + + updateUI.mockClear() + appMock.emit('before-quit', { defaultPrevented: false }) + expect(lifecycle.isWindowClosing()).toBe(true) + mainWindow.emit('resize') + await vi.advanceTimersByTimeAsync(500) + expect(updateUI).not.toHaveBeenCalled() + lifecycle.clearInitialRevealFallbackTimer() + lifecycle.dispose() +}) diff --git a/src/main/window/main-window-state-lifecycle.ts b/src/main/window/main-window/main-window-state-lifecycle.ts similarity index 96% rename from src/main/window/main-window-state-lifecycle.ts rename to src/main/window/main-window/main-window-state-lifecycle.ts index d772aa78b5b..7e6db78021d 100644 --- a/src/main/window/main-window-state-lifecycle.ts +++ b/src/main/window/main-window/main-window-state-lifecycle.ts @@ -1,11 +1,11 @@ import { app, type BrowserWindow, type Event } from 'electron' -import type { Store } from '../persistence/loading-store/store' -import { uiZoomFactorFromLevel } from '../../shared/ui-zoom-level' +import type { Store } from '../../persistence/loading-store/store' +import { uiZoomFactorFromLevel } from '../../../shared/ui-zoom-level' import { isBackgroundLaunch, isWindowlessLaunch, showWindowWithoutStealingFocus -} from './foreground-activation-policy' +} from '../activation/foreground-activation-policy' import { MIN_HEIGHT, MIN_WIDTH, syncTrafficLightPosition } from './main-window-visual-lifecycle' export type MainWindowStateLifecycle = { diff --git a/src/main/window/main-window-visual-lifecycle.ts b/src/main/window/main-window/main-window-visual-lifecycle.ts similarity index 97% rename from src/main/window/main-window-visual-lifecycle.ts rename to src/main/window/main-window/main-window-visual-lifecycle.ts index a79ebdda1ca..8b5ee92bb21 100644 --- a/src/main/window/main-window-visual-lifecycle.ts +++ b/src/main/window/main-window/main-window-visual-lifecycle.ts @@ -1,5 +1,5 @@ import { ipcMain, type BrowserWindow } from 'electron' -import { isMacosTahoeOrNewer } from './macos-tahoe-release' +import { isMacosTahoeOrNewer } from '../desktop-integration/macos-tahoe-release' const activeRepaintJiggles = new WeakSet() export function forceRepaint(window: BrowserWindow): void { diff --git a/src/main/window/user-quit-window-close.ts b/src/main/window/main-window/user-quit-window-close.ts similarity index 100% rename from src/main/window/user-quit-window-close.ts rename to src/main/window/main-window/user-quit-window-close.ts diff --git a/src/main/window/window-close-decision.ts b/src/main/window/main-window/window-close-decision.ts similarity index 100% rename from src/main/window/window-close-decision.ts rename to src/main/window/main-window/window-close-decision.ts diff --git a/src/main/window/window-bounds-validation.ts b/src/main/window/presentation/window-bounds-validation.ts similarity index 100% rename from src/main/window/window-bounds-validation.ts rename to src/main/window/presentation/window-bounds-validation.ts diff --git a/src/main/window/window-material-options.ts b/src/main/window/presentation/window-material-options.ts similarity index 95% rename from src/main/window/window-material-options.ts rename to src/main/window/presentation/window-material-options.ts index f5856b6c231..9bf2d84454c 100644 --- a/src/main/window/window-material-options.ts +++ b/src/main/window/presentation/window-material-options.ts @@ -1,5 +1,5 @@ import type { BrowserWindowConstructorOptions } from 'electron' -import type { WindowMaterial } from '../../shared/window-material' +import type { WindowMaterial } from '../../../shared/window-material' type WindowMaterialOptions = Pick< BrowserWindowConstructorOptions, diff --git a/src/main/window/window-material-suppression.ts b/src/main/window/presentation/window-material-suppression.ts similarity index 91% rename from src/main/window/window-material-suppression.ts rename to src/main/window/presentation/window-material-suppression.ts index 16b2f4ab821..f0a9cc5d397 100644 --- a/src/main/window/window-material-suppression.ts +++ b/src/main/window/presentation/window-material-suppression.ts @@ -1,5 +1,5 @@ import type { BrowserWindow } from 'electron' -import type { WindowMaterial } from '../../shared/window-material' +import type { WindowMaterial } from '../../../shared/window-material' import { MAC_BLUR_VIBRANCY } from './window-material-options' const createdMaterials = new WeakMap() diff --git a/src/main/window/privileged-window-navigation.test.ts b/src/main/window/privileged-window-navigation.test.ts deleted file mode 100644 index a1e706bb699..00000000000 --- a/src/main/window/privileged-window-navigation.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' - -const openExternal = vi.fn() -vi.mock('electron', () => ({ - shell: { openExternal: (url: string) => openExternal(url) } -})) - -import { installPrivilegedWindowNavigationPolicy } from './privileged-window-navigation' - -describe('privileged window navigation policy', () => { - function createFixture(currentUrl: string) { - const handlers = new Map void>() - const contents = { - getURL: () => currentUrl, - setWindowOpenHandler: vi.fn(), - on: vi.fn((event: string, handler: (...args: unknown[]) => void) => { - handlers.set(event, handler) - }) - } - installPrivilegedWindowNavigationPolicy(contents as never) - return { - willNavigate(url: string) { - const event = { preventDefault: vi.fn() } - const handler = handlers.get('will-navigate') - // Assert handler presence or missing navigation guards pass allow-cases vacuously. - if (!handler) { - throw new Error('no will-navigate handler was registered') - } - handler(event, url) - return event - } - } - } - - beforeEach(() => { - openExternal.mockClear() - }) - - it('lets the packaged renderer document reload itself', () => { - const appUrl = - 'file:///Applications/Orca.app/Contents/Resources/app.asar/out/renderer/index.html' - const event = createFixture(appUrl).willNavigate(appUrl) - - expect(event.preventDefault).not.toHaveBeenCalled() - expect(openExternal).not.toHaveBeenCalled() - }) - - it('still blocks and hands off an external http target', () => { - const appUrl = - 'file:///Applications/Orca.app/Contents/Resources/app.asar/out/renderer/index.html' - const event = createFixture(appUrl).willNavigate('https://example.com/') - - expect(event.preventDefault).toHaveBeenCalled() - expect(openExternal).toHaveBeenCalledWith('https://example.com/') - }) - - it('lets the dev renderer origin navigate but blocks foreign documents', () => { - const fixture = createFixture('http://localhost:5173/') - - expect( - fixture.willNavigate('http://localhost:5173/index.html').preventDefault - ).not.toHaveBeenCalled() - expect(fixture.willNavigate('https://example.com/').preventDefault).toHaveBeenCalled() - expect( - fixture.willNavigate('blob:http://localhost:5173/attacker-document').preventDefault - ).toHaveBeenCalled() - }) - - // The exact packaged file host/path defines privilege; foreign hosts and copied paths under other schemes must fail. - it('blocks a foreign file host and a data: URL that reuse the renderer path', () => { - const appUrl = - 'file:///Applications/Orca.app/Contents/Resources/app.asar/out/renderer/index.html' - const fixture = createFixture(appUrl) - - expect( - fixture.willNavigate( - 'file://evil.example/Applications/Orca.app/Contents/Resources/app.asar/out/renderer/index.html' - ).preventDefault - ).toHaveBeenCalled() - expect( - fixture.willNavigate('data:text/html,').preventDefault - ).toHaveBeenCalled() - expect(openExternal).not.toHaveBeenCalled() - }) - - it('still blocks navigation to an unrelated local file', () => { - const appUrl = - 'file:///Applications/Orca.app/Contents/Resources/app.asar/out/renderer/index.html' - const event = createFixture(appUrl).willNavigate('file:///Users/someone/.ssh/id_rsa') - - expect(event.preventDefault).toHaveBeenCalled() - expect(openExternal).not.toHaveBeenCalled() - }) -}) diff --git a/src/main/window/renderer-document-navigation.test.ts b/src/main/window/renderer-document-navigation.test.ts deleted file mode 100644 index 8c5a9d5eb2a..00000000000 --- a/src/main/window/renderer-document-navigation.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { registerRendererDocumentNavigation } from './renderer-document-navigation' - -describe('renderer document navigation', () => { - function createFixture(currentUrl: string, onStarted = vi.fn(() => vi.fn())) { - const handlers = new Map void>() - let loadingMainFrame = false - const on = vi.fn((event: string, handler: (...args: unknown[]) => void) => { - handlers.set(event, handler) - }) - registerRendererDocumentNavigation( - { getURL: () => currentUrl, isLoadingMainFrame: () => loadingMainFrame, on } as never, - onStarted - ) - return { - navigate: handlers.get('did-start-navigation'), - failProvisionalLoad: handlers.get('did-fail-provisional-load'), - willNavigate: handlers.get('will-navigate'), - commitNavigation: handlers.get('did-frame-navigate'), - stopLoading: handlers.get('did-stop-loading'), - onStarted, - setLoadingMainFrame(value: boolean) { - loadingMainFrame = value - } - } - } - - it('shares one reload fence across concurrent provisional navigations', async () => { - const cancel = vi.fn() - const onStarted = vi.fn(() => cancel) - const fixture = createFixture('http://localhost:5173/', onStarted) - - fixture.navigate?.({}, 'http://localhost:5173/reload-a', false, true) - fixture.navigate?.({}, 'http://localhost:5173/reload-b', false, true) - - expect(onStarted).toHaveBeenCalledOnce() - fixture.setLoadingMainFrame(true) - fixture.failProvisionalLoad?.({}, -3, 'aborted', 'http://localhost:5173/reload-a', true, 1, 1) - await Promise.resolve() - expect(cancel).not.toHaveBeenCalled() - fixture.setLoadingMainFrame(false) - fixture.failProvisionalLoad?.({}, -3, 'aborted', 'http://localhost:5173/reload-b', true, 1, 1) - await Promise.resolve() - expect(cancel).toHaveBeenCalledOnce() - }) - - it('restores an idle surviving document after loading stops', async () => { - const cancel = vi.fn() - const fixture = createFixture( - 'http://localhost:5173/', - vi.fn(() => cancel) - ) - - fixture.navigate?.({}, 'http://localhost:5173/reload', false, true) - fixture.setLoadingMainFrame(true) - fixture.failProvisionalLoad?.({}, -3, 'aborted', 'http://localhost:5173/reload', true, 1, 1) - await Promise.resolve() - expect(cancel).not.toHaveBeenCalled() - - fixture.setLoadingMainFrame(false) - fixture.stopLoading?.() - expect(cancel).toHaveBeenCalledOnce() - }) - - it('restores after a concurrent failure and blocked replacement both settle', async () => { - const cancel = vi.fn() - const fixture = createFixture( - 'http://localhost:5173/', - vi.fn(() => cancel) - ) - const event = { defaultPrevented: false } - - fixture.navigate?.({}, 'http://localhost:5173/reload-a', false, true) - fixture.navigate?.({}, 'http://localhost:5173/reload-b', false, true) - fixture.setLoadingMainFrame(true) - fixture.failProvisionalLoad?.({}, -3, 'aborted', 'http://localhost:5173/reload-a', true) - await Promise.resolve() - expect(cancel).not.toHaveBeenCalled() - - fixture.willNavigate?.(event, 'http://localhost:5173/reload-b', false, true) - event.defaultPrevented = true - fixture.setLoadingMainFrame(false) - await Promise.resolve() - - expect(cancel).toHaveBeenCalledOnce() - }) - - it('does not let a stale same-URL failure cancel a replacement navigation', async () => { - const cancels: ReturnType[] = [] - const fixture = createFixture( - 'http://localhost:5173/', - vi.fn(() => { - const cancel = vi.fn() - cancels.push(cancel) - return cancel - }) - ) - const url = 'http://localhost:5173/reload' - - fixture.navigate?.({}, url, false, true) - fixture.commitNavigation?.({}, url, -1, '', true, 1, 1) - fixture.navigate?.({}, url, false, true) - fixture.setLoadingMainFrame(true) - fixture.failProvisionalLoad?.({}, -3, 'aborted', url, true, 1, 1) - await Promise.resolve() - - expect(cancels).toHaveLength(2) - expect(cancels[1]).not.toHaveBeenCalled() - - fixture.setLoadingMainFrame(false) - fixture.failProvisionalLoad?.({}, -3, 'aborted', url, true, 1, 1) - await Promise.resolve() - expect(cancels[1]).toHaveBeenCalledOnce() - }) - - it('cancels when a later will-navigate listener blocks the navigation', async () => { - const cancel = vi.fn() - const fixture = createFixture( - 'http://localhost:5173/', - vi.fn(() => cancel) - ) - const event = { defaultPrevented: false } - - fixture.navigate?.({}, 'http://localhost:5173/reload', false, true) - fixture.willNavigate?.(event, 'http://localhost:5173/reload', false, true, 1, 1) - event.defaultPrevented = true - await Promise.resolve() - - expect(cancel).toHaveBeenCalledOnce() - }) -}) diff --git a/src/main/window/renderer-recovery-reload-watchdog.test.ts b/src/main/window/renderer-recovery-reload-watchdog.test.ts deleted file mode 100644 index 9148ab5a326..00000000000 --- a/src/main/window/renderer-recovery-reload-watchdog.test.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { EventEmitter } from 'node:events' -import type { BrowserWindow } from 'electron' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { MainWindowLoadObserver } from './main-window-contracts' -import { - createRendererRecoveryReloadWatchdog, - RENDERER_RECOVERY_LOAD_TIMEOUT_MS -} from './renderer-recovery-reload-watchdog' - -vi.mock('electron', () => ({ app: { isPackaged: true } })) - -function createHarness() { - const webContents = Object.assign(new EventEmitter(), { id: 143 }) - const mainWindow = { webContents, isDestroyed: () => false } as unknown as BrowserWindow - const loads: MainWindowLoadObserver[] = [] - const onRecoveryReloadOutcome = vi.fn() - const onRendererRecoveryExhausted = vi.fn() - const watchdog = createRendererRecoveryReloadWatchdog({ - mainWindow, - rendererWebContentsId: webContents.id, - isRecoveryPending: () => false, - isWindowClosing: () => false, - reloadMainWindow: (observer) => loads.push(observer), - opts: { onRecoveryReloadOutcome, onRendererRecoveryExhausted } - }) - const abortLatestLoad = () => loads.at(-1)?.onError?.(new Error('ERR_ABORTED (-3)')) - watchdog.issue({ reason: 'crashed', exitCode: 5 }, 1) - return { - watchdog, - webContents, - loads, - abortLatestLoad, - onRecoveryReloadOutcome, - onRendererRecoveryExhausted - } -} - -describe('superseding recovery navigations', () => { - beforeEach(() => vi.useFakeTimers()) - afterEach(() => vi.useRealTimers()) - - it('removes listeners and the pending stall timer during teardown', () => { - const { watchdog, webContents, loads, onRecoveryReloadOutcome } = createHarness() - expect(webContents.eventNames().sort()).toEqual(['did-fail-load', 'did-navigate', 'dom-ready']) - expect(vi.getTimerCount()).toBe(1) - watchdog.clear() - expect(webContents.eventNames()).toEqual([]) - expect(vi.getTimerCount()).toBe(0) - loads[0]?.onLoaded?.() - loads[0]?.onError?.(new Error('ERR_FILE_NOT_FOUND')) - watchdog.notifySystemResume() - expect(onRecoveryReloadOutcome).not.toHaveBeenCalled() - expect(vi.getTimerCount()).toBe(0) - }) - - it('does not mistake a replacement error page for recovery', () => { - const { - watchdog, - webContents, - abortLatestLoad, - onRecoveryReloadOutcome, - onRendererRecoveryExhausted - } = createHarness() - abortLatestLoad() - webContents.emit('did-navigate') - webContents.emit('did-fail-load', {}, -6, 'ERR_FILE_NOT_FOUND', 'file:///missing', true) - watchdog.notifyDocumentLoaded() - - expect(onRecoveryReloadOutcome).not.toHaveBeenCalledWith( - expect.objectContaining({ status: 'loaded' }) - ) - expect(onRendererRecoveryExhausted).toHaveBeenCalledOnce() - expect(onRecoveryReloadOutcome).toHaveBeenCalledWith( - expect.objectContaining({ status: 'failed', errorCode: 'ERR_FILE_NOT_FOUND' }) - ) - watchdog.clear() - }) - - it('ignores subframe failures and aborted replacement navigations', () => { - const { watchdog, webContents, abortLatestLoad, onRecoveryReloadOutcome } = createHarness() - abortLatestLoad() - webContents.emit('did-fail-load', {}, -6, 'ERR_FILE_NOT_FOUND', 'file:///missing', false) - webContents.emit('did-fail-load', {}, -3, 'ERR_ABORTED', 'file:///previous', true) - watchdog.notifyDocumentLoaded() - expect(onRecoveryReloadOutcome).toHaveBeenCalledWith( - expect.objectContaining({ status: 'loaded', superseded: true }) - ) - watchdog.clear() - }) - - it('keeps Reload available if the replacement fails beneath an existing prompt', () => { - const { - watchdog, - webContents, - loads, - abortLatestLoad, - onRecoveryReloadOutcome, - onRendererRecoveryExhausted - } = createHarness() - abortLatestLoad() - webContents.emit('did-navigate') - vi.advanceTimersByTime(RENDERER_RECOVERY_LOAD_TIMEOUT_MS * 2) - expect(onRendererRecoveryExhausted).toHaveBeenCalledOnce() - webContents.emit('did-fail-load', {}, -6, 'ERR_FILE_NOT_FOUND', 'file:///missing', true) - watchdog.notifyDocumentLoaded() - expect(onRecoveryReloadOutcome).not.toHaveBeenCalledWith( - expect.objectContaining({ status: 'loaded' }) - ) - onRendererRecoveryExhausted.mock.calls[0]?.[0].retry() - expect(loads).toHaveLength(2) - expect(onRendererRecoveryExhausted).toHaveBeenCalledOnce() - watchdog.clear() - }) - - it('recognizes a successful replacement started after the stall prompt', () => { - const { - watchdog, - loads, - abortLatestLoad, - onRecoveryReloadOutcome, - onRendererRecoveryExhausted - } = createHarness() - vi.advanceTimersByTime(RENDERER_RECOVERY_LOAD_TIMEOUT_MS * 2) - expect(onRendererRecoveryExhausted).toHaveBeenCalledOnce() - abortLatestLoad() - watchdog.notifyDocumentLoaded() - expect(onRecoveryReloadOutcome).toHaveBeenCalledWith( - expect.objectContaining({ status: 'loaded', superseded: true, afterPrompt: true }) - ) - onRendererRecoveryExhausted.mock.calls[0]?.[0].retry() - expect(loads).toHaveLength(2) - vi.advanceTimersByTime(RENDERER_RECOVERY_LOAD_TIMEOUT_MS * 2) - expect(onRendererRecoveryExhausted).toHaveBeenCalledOnce() - watchdog.clear() - }) -}) diff --git a/src/main/window/main-window-load-error-code.ts b/src/main/window/renderer-recovery/main-window-load-error-code.ts similarity index 100% rename from src/main/window/main-window-load-error-code.ts rename to src/main/window/renderer-recovery/main-window-load-error-code.ts diff --git a/src/main/window/renderer-launch-failure-backoff.ts b/src/main/window/renderer-recovery/renderer-launch-failure-backoff.ts similarity index 100% rename from src/main/window/renderer-launch-failure-backoff.ts rename to src/main/window/renderer-recovery/renderer-launch-failure-backoff.ts diff --git a/src/main/window/renderer-launch-failure-probe.test.ts b/src/main/window/renderer-recovery/renderer-launch-failure-probe.test.ts similarity index 56% rename from src/main/window/renderer-launch-failure-probe.test.ts rename to src/main/window/renderer-recovery/renderer-launch-failure-probe.test.ts index f762eac4d86..18253cdfd15 100644 --- a/src/main/window/renderer-launch-failure-probe.test.ts +++ b/src/main/window/renderer-recovery/renderer-launch-failure-probe.test.ts @@ -5,11 +5,19 @@ const { runProcessMock, recordDurableCrashBreadcrumbMock } = vi.hoisted(() => ({ recordDurableCrashBreadcrumbMock: vi.fn() })) vi.mock('@orca/process-host', () => ({ runProcess: runProcessMock })) -vi.mock('../crash-reporting/breadcrumbs/durable-crash-breadcrumb', () => ({ +vi.mock('../../crash-reporting/breadcrumbs/durable-crash-breadcrumb', () => ({ recordDurableCrashBreadcrumb: recordDurableCrashBreadcrumbMock })) -import { withPlatform } from './createMainWindow-test-harness' +function withPlatform(platform: NodeJS.Platform, run: () => T): T { + const original = process.platform + Object.defineProperty(process, 'platform', { configurable: true, value: platform }) + try { + return run() + } finally { + Object.defineProperty(process, 'platform', { configurable: true, value: original }) + } +} import { probeRendererLaunchCapacity, @@ -50,17 +58,6 @@ describe('probeRendererLaunchCapacity', () => { expect(runProcessMock).not.toHaveBeenCalled() }) - it('records a skipped Windows probe without spawning a child', async () => { - await expect( - withPlatform('win32', () => recordRendererLaunchFailureProbe({ exitCode: 18 }, 100_000)) - ).resolves.toBe('skipped') - expect(runProcessMock).not.toHaveBeenCalled() - expect(recordDurableCrashBreadcrumbMock).toHaveBeenCalledWith('renderer_launch_failed_probe', { - spawnError: 'skipped', - exitCode: 18 - }) - }) - it('never rejects when the breadcrumb write throws', async () => { runProcessMock.mockRejectedValue(spawnError('EAGAIN')) recordDurableCrashBreadcrumbMock.mockImplementation(() => { @@ -68,26 +65,4 @@ describe('probeRendererLaunchCapacity', () => { }) await expect(recordPosixFailure(90_000)).resolves.toBe('EAGAIN') }) - - it('records the refused spawn as a durable breadcrumb', async () => { - runProcessMock.mockRejectedValue(spawnError('EAGAIN')) - await expect(recordPosixFailure(10_000)).resolves.toBe('EAGAIN') - expect(recordDurableCrashBreadcrumbMock).toHaveBeenCalledWith('renderer_launch_failed_probe', { - spawnError: 'EAGAIN', - exitCode: 1003 - }) - }) - - it('probes once for the duplicate render-process-gone of one failed launch', async () => { - runProcessMock.mockRejectedValue(spawnError('EAGAIN')) - await recordPosixFailure(50_000) - await recordPosixFailure(50_010) - expect(runProcessMock).toHaveBeenCalledOnce() - expect(recordDurableCrashBreadcrumbMock).toHaveBeenCalledOnce() - - // The next backoff retry fails again: a fresh probe and breadcrumb. - await recordPosixFailure(50_250) - expect(runProcessMock).toHaveBeenCalledTimes(2) - expect(recordDurableCrashBreadcrumbMock).toHaveBeenCalledTimes(2) - }) }) diff --git a/src/main/window/renderer-launch-failure-probe.ts b/src/main/window/renderer-recovery/renderer-launch-failure-probe.ts similarity index 92% rename from src/main/window/renderer-launch-failure-probe.ts rename to src/main/window/renderer-recovery/renderer-launch-failure-probe.ts index 9127b53da17..43ae8dec045 100644 --- a/src/main/window/renderer-launch-failure-probe.ts +++ b/src/main/window/renderer-recovery/renderer-launch-failure-probe.ts @@ -1,6 +1,6 @@ import { runProcess } from '@orca/process-host' -import { recordDurableCrashBreadcrumb } from '../crash-reporting/breadcrumbs/durable-crash-breadcrumb' -import { errnoCodeOrUnknown } from '../../shared/errno-code' +import { recordDurableCrashBreadcrumb } from '../../crash-reporting/breadcrumbs/durable-crash-breadcrumb' +import { errnoCodeOrUnknown } from '../../../shared/errno-code' /** 'ok' when a throwaway child could start, the spawn errno (EAGAIN = per-user process limit), or 'skipped' on Windows. */ export type RendererLaunchProbeResult = string diff --git a/src/main/window/renderer-liveness-at-death.test.ts b/src/main/window/renderer-recovery/renderer-liveness-at-death.test.ts similarity index 87% rename from src/main/window/renderer-liveness-at-death.test.ts rename to src/main/window/renderer-recovery/renderer-liveness-at-death.test.ts index 58d2ed252a2..90d4d0b74b0 100644 --- a/src/main/window/renderer-liveness-at-death.test.ts +++ b/src/main/window/renderer-recovery/renderer-liveness-at-death.test.ts @@ -2,7 +2,7 @@ import { EventEmitter } from 'node:events' import { beforeEach, describe, expect, it, vi } from 'vitest' const recordCoalescedDurableCrashBreadcrumb = vi.fn() -vi.mock('../crash-reporting/breadcrumbs/durable-crash-breadcrumb', () => ({ +vi.mock('../../crash-reporting/breadcrumbs/durable-crash-breadcrumb', () => ({ recordCoalescedDurableCrashBreadcrumb: (...args: unknown[]) => recordCoalescedDurableCrashBreadcrumb(...args) })) @@ -97,18 +97,4 @@ describe('trackRendererLivenessAtDeath', () => { expect(state).toMatchObject({ rendererDocumentLoaded: true, rendererDocumentAgeMs: 5_000 }) expect(state.rendererDocumentLoadFailed).toBeUndefined() }) - - it('does not log the replacement renderer as the dead one recovering', () => { - const { window, contents } = createWindow() - const tracker = trackRendererLivenessAtDeath(window, () => now) - contents.emit('did-finish-load') - window.emit('unresponsive') - now += 20_000 - expect(tracker.takeAtRendererGone().rendererUnresponsiveForMs).toBe(20_000) - recordCoalescedDurableCrashBreadcrumb.mockClear() - - window.emit('responsive') - - expect(recordCoalescedDurableCrashBreadcrumb).not.toHaveBeenCalled() - }) }) diff --git a/src/main/window/renderer-liveness-at-death.ts b/src/main/window/renderer-recovery/renderer-liveness-at-death.ts similarity index 96% rename from src/main/window/renderer-liveness-at-death.ts rename to src/main/window/renderer-recovery/renderer-liveness-at-death.ts index b4cc6336711..2c30f95a463 100644 --- a/src/main/window/renderer-liveness-at-death.ts +++ b/src/main/window/renderer-recovery/renderer-liveness-at-death.ts @@ -1,5 +1,5 @@ import type { BrowserWindow } from 'electron' -import { recordCoalescedDurableCrashBreadcrumb } from '../crash-reporting/breadcrumbs/durable-crash-breadcrumb' +import { recordCoalescedDurableCrashBreadcrumb } from '../../crash-reporting/breadcrumbs/durable-crash-breadcrumb' /** Windows killed/exit1 cannot identify Task Manager, EDR or Orca as the cause. Retain prior hang/load state to distinguish users terminating a frozen app from unexplained external kills. */ export type RendererLivenessAtDeath = { @@ -45,7 +45,7 @@ export function trackRendererLivenessAtDeath( const onUnresponsive = (): void => { if (unresponsiveSince === null) { const epoch = ++captureEpoch - void import('../hang-watchdog/renderer-hang-capture') + void import('../../hang-watchdog/renderer-hang-capture') .then((collector) => { if (epoch === captureEpoch && unresponsiveSince !== null && !window.isDestroyed()) { capture = collector.startRendererHangCapture(window) diff --git a/src/main/window/renderer-recovery-prompt.ts b/src/main/window/renderer-recovery/renderer-recovery-prompt.ts similarity index 97% rename from src/main/window/renderer-recovery-prompt.ts rename to src/main/window/renderer-recovery/renderer-recovery-prompt.ts index 5f7ebabf9c2..17e12b73280 100644 --- a/src/main/window/renderer-recovery-prompt.ts +++ b/src/main/window/renderer-recovery/renderer-recovery-prompt.ts @@ -1,6 +1,6 @@ import type { MessageBoxOptions, MessageBoxReturnValue } from 'electron' -import { translateMain } from '../i18n/main-i18n' -import type { InstallDirAclPoisonDiagnosis } from '../startup/windows-install-permissions/windows-install-dir-acl-recovery' +import { translateMain } from '../../i18n/main-i18n' +import type { InstallDirAclPoisonDiagnosis } from '../../startup/windows-install-permissions/windows-install-dir-acl-recovery' import type { RecoveryExhaustionCause } from './renderer-recovery-reload-watchdog' import type { RendererLaunchProbeResult } from './renderer-launch-failure-probe' diff --git a/src/main/window/renderer-recovery-reload-watchdog.ts b/src/main/window/renderer-recovery/renderer-recovery-reload-watchdog.ts similarity index 97% rename from src/main/window/renderer-recovery-reload-watchdog.ts rename to src/main/window/renderer-recovery/renderer-recovery-reload-watchdog.ts index e86d18fae6d..872dd815fc2 100644 --- a/src/main/window/renderer-recovery-reload-watchdog.ts +++ b/src/main/window/renderer-recovery/renderer-recovery-reload-watchdog.ts @@ -1,8 +1,11 @@ import { app, type BrowserWindow } from 'electron' -import { isSystemSessionEnding } from '../crash-reporting/process-exits/expected-teardown-state' -import type { LowCommitOomVerdict } from '../crash-reporting/recovery/low-commit-oom-recovery-gate' -import type { CreateMainWindowOptions, MainWindowLoadObserver } from './main-window-contracts' +import { isSystemSessionEnding } from '../../crash-reporting/process-exits/expected-teardown-state' +import type { LowCommitOomVerdict } from '../../crash-reporting/recovery/low-commit-oom-recovery-gate' +import type { + CreateMainWindowOptions, + MainWindowLoadObserver +} from '../main-window/main-window-contracts' import { mainWindowLoadErrorCode } from './main-window-load-error-code' // Field recoveries took up to 30.4s; allow 45s before retrying a load with no document. diff --git a/src/main/window/agent-launch-tab-publish-relay.ts b/src/main/window/renderer-relays/agent-launch-tab-publish-relay.ts similarity index 96% rename from src/main/window/agent-launch-tab-publish-relay.ts rename to src/main/window/renderer-relays/agent-launch-tab-publish-relay.ts index 753b712c273..547e02b7d80 100644 --- a/src/main/window/agent-launch-tab-publish-relay.ts +++ b/src/main/window/renderer-relays/agent-launch-tab-publish-relay.ts @@ -6,7 +6,7 @@ import type { AgentLaunchTabPublished, AgentLaunchTabPublishReply, AgentLaunchTabPublishRequest -} from '../../shared/agent-launch/contracts/agent-launch-tab-publication' +} from '../../../shared/agent-launch/contracts/agent-launch-tab-publication' const AGENT_LAUNCH_TAB_PUBLISH_TIMEOUT_MS = 10_000 diff --git a/src/main/window/agent-pane-activity-relay.ts b/src/main/window/renderer-relays/agent-pane-activity-relay.ts similarity index 93% rename from src/main/window/agent-pane-activity-relay.ts rename to src/main/window/renderer-relays/agent-pane-activity-relay.ts index a8c33e8231d..92c445e1d87 100644 --- a/src/main/window/agent-pane-activity-relay.ts +++ b/src/main/window/renderer-relays/agent-pane-activity-relay.ts @@ -4,7 +4,7 @@ import type { AgentListPublication, AgentPaneActivityChange, AgentPaneActivityReply -} from '../../shared/agent-status/presentation/agent-list-status' +} from '../../../shared/agent-status/presentation/agent-list-status' import { requestRendererReply } from './renderer-reply-request' // Under the client's call timeout, so the client hears a refusal rather than losing the reply. diff --git a/src/main/window/project-move-pause.test.ts b/src/main/window/renderer-relays/project-move-pause.test.ts similarity index 58% rename from src/main/window/project-move-pause.test.ts rename to src/main/window/renderer-relays/project-move-pause.test.ts index 6f4ab7941f7..0c6407f792d 100644 --- a/src/main/window/project-move-pause.test.ts +++ b/src/main/window/renderer-relays/project-move-pause.test.ts @@ -1,8 +1,8 @@ import { EventEmitter } from 'node:events' import { ipcMain as ipc, type BrowserWindow } from 'electron' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { ProjectMovePauseRequest } from '../../shared/project-move-pause' -import type { WorkspaceSleepOutcome, WorkspaceSleepTarget } from '../../shared/workspace-sleep' +import type { ProjectMovePauseRequest } from '../../../shared/project-move-pause' +import type { WorkspaceSleepOutcome, WorkspaceSleepTarget } from '../../../shared/workspace-sleep' type PauseResponseFixture = { requestId?: string; protocol?: number } & ( | { phase: 'prepare'; targets: WorkspaceSleepTarget[] } @@ -64,31 +64,6 @@ describe('main-owned project move pause', () => { vi.useRealTimers() }) - it('waits for an explicit renderer persistence receipt and preserves all stop receipts', async () => { - const f = fixture() - const paused = pauseWorkspacesForProjectMove(f.mainWindow, f, ['wt', 'wt']) - f.prepare() - await vi.advanceTimersByTimeAsync(0) - expect(f.sleepWorkspaces).toHaveBeenCalledWith({ - targets: [ - { worktreeId: 'wt', terminalHost: { kind: 'self' }, chatHosts: [{ kind: 'self' }] } - ], - markChatsAsleep: false, - projectMovePauseId: expect.any(String) - }) - let returned = false - void paused.then(() => { - returned = true - }) - expect(f.request().phase).toBe('persist') - expect(returned).toBe(false) - f.respond({ phase: 'persist' }) - await expect(paused).resolves.toEqual(f.outcomes) - expect(f.request().phase).toBe('release') - expect(ipc.listenerCount('ui:projectMovePauseResponse')).toBe(0) - expect(f.webContents.eventNames()).toEqual([]) - }) - it('shares one lifecycle listener across overlapping pauses and releases it after', async () => { const f = fixture() const paused = Array.from({ length: 12 }, () => @@ -118,20 +93,6 @@ describe('main-owned project move pause', () => { expect(ipc.listenerCount('ui:projectMovePauseResponse')).toBe(0) }) - it('ignores another window, stale request and unsupported protocol', async () => { - const f = fixture() - const paused = pauseWorkspacesForProjectMove(f.mainWindow, f, ['wt']) - f.respond({ phase: 'prepare', targets: [] }, {}) - f.respond({ phase: 'prepare', targets: [], requestId: 'stale' }) - f.respond({ phase: 'prepare', targets: [], protocol: 0 }) - await vi.advanceTimersByTimeAsync(0) - expect(f.sleepWorkspaces).not.toHaveBeenCalled() - f.prepare() - await vi.advanceTimersByTimeAsync(0) - f.respond({ phase: 'persist' }) - await paused - }) - it('rejects reload during the host stop instead of accepting a new renderer persistence reply', async () => { const f = fixture() const stop = Promise.withResolvers() @@ -144,37 +105,4 @@ describe('main-owned project move pause', () => { await expect(paused).rejects.toThrow('renderer_unavailable') expect(f.webContents.send.mock.calls.map(([, value]) => value.phase)).toEqual(['prepare']) }) - - it('propagates failed durability without returning success to the exporter', async () => { - const f = fixture() - const paused = pauseWorkspacesForProjectMove(f.mainWindow, f, ['wt']) - f.prepare() - await vi.advanceTimersByTimeAsync(0) - f.respond({ phase: 'error', error: 'disk full' }) - await expect(paused).rejects.toMatchObject({ message: 'disk full', outcomes: f.outcomes }) - expect(f.request().phase).toBe('release') - }) - - it('retains partial stop failures and successful sibling receipts when renderer rollback fails', async () => { - const f = fixture() - f.outcomes[0].failure = 'terminal_liveness_unavailable' - f.outcomes.push({ - worktreeId: 'sibling', - chatFailed: false, - receipts: [{ executionHostId: 'runtime:env', stoppedSessionIds: ['remote-chat'] }] - }) - const paused = pauseWorkspacesForProjectMove(f.mainWindow, f, ['wt', 'sibling']) - f.respond({ - phase: 'prepare', - targets: ['wt', 'sibling'].map((worktreeId) => ({ - worktreeId, - terminalHost: { kind: 'self' }, - chatHosts: [{ kind: 'self' }] - })) - }) - await vi.advanceTimersByTimeAsync(0) - f.respond({ phase: 'error', error: 'host_terminal_stop_failed' }) - await expect(paused).rejects.toMatchObject({ outcomes: f.outcomes }) - expect(f.outcomes[1]?.receipts[0]?.stoppedSessionIds).toEqual(['remote-chat']) - }) }) diff --git a/src/main/window/project-move-pause.ts b/src/main/window/renderer-relays/project-move-pause.ts similarity index 96% rename from src/main/window/project-move-pause.ts rename to src/main/window/renderer-relays/project-move-pause.ts index 76121bdcf7d..81c3551bc2c 100644 --- a/src/main/window/project-move-pause.ts +++ b/src/main/window/renderer-relays/project-move-pause.ts @@ -5,9 +5,9 @@ import { projectMovePauseResponseSchema, type ProjectMovePauseRequest, type ProjectMovePauseResponse -} from '../../shared/project-move-pause' -import type { WorkspaceSleepOutcome, WorkspaceSleepRequest } from '../../shared/workspace-sleep' -import { addFanoutListener, addFanoutListeners } from '../runtime/events/event-listener-fanout' +} from '../../../shared/project-move-pause' +import type { WorkspaceSleepOutcome, WorkspaceSleepRequest } from '../../../shared/workspace-sleep' +import { addFanoutListener, addFanoutListeners } from '../../runtime/events/event-listener-fanout' type SleepRuntime = { sleepWorkspaces: (request: WorkspaceSleepRequest) => Promise diff --git a/src/main/window/renderer-reply-request.ts b/src/main/window/renderer-relays/renderer-reply-request.ts similarity index 93% rename from src/main/window/renderer-reply-request.ts rename to src/main/window/renderer-relays/renderer-reply-request.ts index 80582d8b885..ec2ae0b57f9 100644 --- a/src/main/window/renderer-reply-request.ts +++ b/src/main/window/renderer-relays/renderer-reply-request.ts @@ -2,8 +2,8 @@ import { randomUUID } from 'node:crypto' import { ipcMain, type BrowserWindow } from 'electron' -import { addFanoutListener } from '../runtime/events/event-listener-fanout' -import { abortWhenRendererGone } from '../ipc/renderer-lifetime-abort' +import { addFanoutListener } from '../../runtime/events/event-listener-fanout' +import { abortWhenRendererGone } from '../../ipc/renderer-lifetime-abort' /** Sends one request to the window and resolves with the reply its own renderer sends; rejects when it goes away or times out. */ export function requestRendererReply( diff --git a/src/main/window/runtime-renderer-notification-sender.test.ts b/src/main/window/renderer-relays/runtime-renderer-notification-sender.test.ts similarity index 83% rename from src/main/window/runtime-renderer-notification-sender.test.ts rename to src/main/window/renderer-relays/runtime-renderer-notification-sender.test.ts index d482bb404fb..5ae840abb4d 100644 --- a/src/main/window/runtime-renderer-notification-sender.test.ts +++ b/src/main/window/renderer-relays/runtime-renderer-notification-sender.test.ts @@ -76,22 +76,6 @@ describe('runtime renderer notification sender', () => { expect(missingWebContents.onFailure).not.toHaveBeenCalled() }) - it('reports renderer process loss once per load generation', () => { - const fixture = createSender() - - fixture.sender.onRendererProcessGone() - fixture.sender.onRendererProcessGone() - fixture.sender.onMainFrameReloadStarted() - fixture.sender.onMainFrameLoadFinished() - fixture.sender.onRendererProcessGone() - fixture.sender.onRendererProcessGone() - - expect(fixture.warn).toHaveBeenCalledTimes(2) - expect(fixture.onFailure).toHaveBeenCalledTimes(2) - expect(fixture.onFailure).toHaveBeenNthCalledWith(1, 'renderer-process-gone') - expect(fixture.onFailure).toHaveBeenNthCalledWith(2, 'renderer-process-gone') - }) - it('keeps close terminal when renderer lifecycle events arrive late', () => { const fixture = createSender() diff --git a/src/main/window/runtime-renderer-notification-sender.ts b/src/main/window/renderer-relays/runtime-renderer-notification-sender.ts similarity index 100% rename from src/main/window/runtime-renderer-notification-sender.ts rename to src/main/window/renderer-relays/runtime-renderer-notification-sender.ts diff --git a/src/main/window/session-tab-close-request-relay.test.ts b/src/main/window/renderer-relays/session-tab-close-request-relay.test.ts similarity index 58% rename from src/main/window/session-tab-close-request-relay.test.ts rename to src/main/window/renderer-relays/session-tab-close-request-relay.test.ts index 11b7a921d01..62c6a9e939a 100644 --- a/src/main/window/session-tab-close-request-relay.test.ts +++ b/src/main/window/renderer-relays/session-tab-close-request-relay.test.ts @@ -106,64 +106,4 @@ describe('requestSessionTabCloseFromRenderer', () => { await rejection expect(ipcEmitter.listenerCount('ui:sessionTabCloseResponse')).toBe(0) }) - - it('shares one lifecycle listener across overlapping close requests', async () => { - const { requestSessionTabCloseFromRenderer } = await import('./session-tab-close-request-relay') - const { mainWindow, webContents } = createMainWindow() - const pending = Array.from({ length: 15 }, (_, index) => - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the EventEmitter double implements the BrowserWindow members the relay uses. - requestSessionTabCloseFromRenderer(mainWindow as never, `tab-${index}`, 'wt-1') - ) - - expect(ipcEmitter.listenerCount('ui:sessionTabCloseResponse')).toBe(1) - expect(mainWindow.listenerCount('closed')).toBe(1) - for (const event of ['destroyed', 'render-process-gone', 'did-start-loading']) { - expect(webContents.listenerCount(event)).toBe(1) - } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the relay sends only session tab close requests on this double. - const requests = webContents.send.mock.calls as [string, { requestId: string }][] - for (const [, request] of requests) { - ipcEmitter.emit( - 'ui:sessionTabCloseResponse', - { sender: webContents }, - { requestId: request.requestId } - ) - } - - await Promise.all(pending) - expect(mainWindow.eventNames()).toEqual([]) - expect(webContents.eventNames()).toEqual([]) - expect(ipcEmitter.listenerCount('ui:sessionTabCloseResponse')).toBe(0) - }) - - it.each([ - ['window', 'closed'], - ['webContents', 'destroyed'], - ['webContents', 'did-start-loading'] - ] as const)( - 'rejects both overlapping requests on one %s %s and returns to baseline listeners', - async (target, eventName) => { - const { requestSessionTabCloseFromRenderer } = - await import('./session-tab-close-request-relay') - const { mainWindow, webContents } = createMainWindow() - const baseline = { - window: mainWindow.eventNames(), - webContents: webContents.eventNames(), - ipc: ipcEmitter.listenerCount('ui:sessionTabCloseResponse') - } - const pending = ['tab-1', 'tab-2'].map((tabId) => - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the EventEmitter double implements the BrowserWindow members the relay uses. - requestSessionTabCloseFromRenderer(mainWindow as never, tabId, 'wt-1') - ) - - ;(target === 'window' ? mainWindow : webContents).emit(eventName) - - await Promise.all( - pending.map((request) => expect(request).rejects.toThrow('renderer_unavailable')) - ) - expect(mainWindow.eventNames()).toEqual(baseline.window) - expect(webContents.eventNames()).toEqual(baseline.webContents) - expect(ipcEmitter.listenerCount('ui:sessionTabCloseResponse')).toBe(baseline.ipc) - } - ) }) diff --git a/src/main/window/session-tab-close-request-relay.ts b/src/main/window/renderer-relays/session-tab-close-request-relay.ts similarity index 90% rename from src/main/window/session-tab-close-request-relay.ts rename to src/main/window/renderer-relays/session-tab-close-request-relay.ts index e9564342bdc..7ee2fe87939 100644 --- a/src/main/window/session-tab-close-request-relay.ts +++ b/src/main/window/renderer-relays/session-tab-close-request-relay.ts @@ -5,9 +5,9 @@ import type { BrowserWindow } from 'electron' import type { SessionTabCloseRequest, SessionTabCloseResponse -} from '../../shared/session-tab-close' -import { SESSION_TAB_CLOSE_TIMEOUT_ERROR } from '../../shared/session-tab-close' -import { addFanoutListener, addFanoutListeners } from '../runtime/events/event-listener-fanout' +} from '../../../shared/session-tab-close' +import { SESSION_TAB_CLOSE_TIMEOUT_ERROR } from '../../../shared/session-tab-close' +import { addFanoutListener, addFanoutListeners } from '../../runtime/events/event-listener-fanout' const SESSION_TAB_CLOSE_CONFIRMATION_MS = 5 * 60_000 const SESSION_TAB_CLOSE_RESPONSE_GRACE_MS = 5_000 diff --git a/src/main/window/terminal-pane-move-commit-fence.ts b/src/main/window/renderer-relays/terminal-pane-move-commit-fence.ts similarity index 94% rename from src/main/window/terminal-pane-move-commit-fence.ts rename to src/main/window/renderer-relays/terminal-pane-move-commit-fence.ts index ced98706ad8..ed1d9990595 100644 --- a/src/main/window/terminal-pane-move-commit-fence.ts +++ b/src/main/window/renderer-relays/terminal-pane-move-commit-fence.ts @@ -1,4 +1,4 @@ -import type { TerminalLeafMoveResult } from '../../shared/workspace-layout/terminal-topology/terminal-leaf-move' +import type { TerminalLeafMoveResult } from '../../../shared/workspace-layout/terminal-topology/terminal-leaf-move' // Why its own module: the plain-Node runtime commits leaf moves too, so this must stay Electron-free. diff --git a/src/main/window/terminal-pane-move-request-relay.test.ts b/src/main/window/renderer-relays/terminal-pane-move-request-relay.test.ts similarity index 95% rename from src/main/window/terminal-pane-move-request-relay.test.ts rename to src/main/window/renderer-relays/terminal-pane-move-request-relay.test.ts index fdbe0c5fb6c..f8c6ef346ca 100644 --- a/src/main/window/terminal-pane-move-request-relay.test.ts +++ b/src/main/window/renderer-relays/terminal-pane-move-request-relay.test.ts @@ -1,6 +1,6 @@ import { EventEmitter } from 'node:events' import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { TerminalLeafMoveResult } from '../../shared/workspace-layout/terminal-topology/terminal-leaf-move' +import type { TerminalLeafMoveResult } from '../../../shared/workspace-layout/terminal-topology/terminal-leaf-move' const ipcEmitter = new EventEmitter() const ipcMainMock = { diff --git a/src/main/window/terminal-pane-move-request-relay.ts b/src/main/window/renderer-relays/terminal-pane-move-request-relay.ts similarity index 94% rename from src/main/window/terminal-pane-move-request-relay.ts rename to src/main/window/renderer-relays/terminal-pane-move-request-relay.ts index f7242f2832c..869c08fea56 100644 --- a/src/main/window/terminal-pane-move-request-relay.ts +++ b/src/main/window/renderer-relays/terminal-pane-move-request-relay.ts @@ -6,9 +6,9 @@ import type { TerminalLeafMoveResult, TerminalPaneMoveToNewTabReply, TerminalPaneMoveToNewTabRequest -} from '../../shared/workspace-layout/terminal-topology/terminal-leaf-move' -import { addFanoutListener } from '../runtime/events/event-listener-fanout' -import { abortWhenRendererGone } from '../ipc/renderer-lifetime-abort' +} from '../../../shared/workspace-layout/terminal-topology/terminal-leaf-move' +import { addFanoutListener } from '../../runtime/events/event-listener-fanout' +import { abortWhenRendererGone } from '../../ipc/renderer-lifetime-abort' import { holdRelayedPaneMove, type RelayedPaneMove } from './terminal-pane-move-commit-fence' // Why: under the client's 15s call timeout, so the client hears a refusal rather than losing the reply. diff --git a/src/main/window/terminal-pane-reveal-relay.ts b/src/main/window/renderer-relays/terminal-pane-reveal-relay.ts similarity index 87% rename from src/main/window/terminal-pane-reveal-relay.ts rename to src/main/window/renderer-relays/terminal-pane-reveal-relay.ts index 3c652fc04f6..4185c7dbda4 100644 --- a/src/main/window/terminal-pane-reveal-relay.ts +++ b/src/main/window/renderer-relays/terminal-pane-reveal-relay.ts @@ -3,8 +3,8 @@ import { app, type BrowserWindow } from 'electron' import type { TerminalPaneRevealReply, TerminalPaneRevealTarget -} from '../../shared/workspace-layout/terminal-surfaces/terminal-pane-reveal' -import { focusExistingMainWindow } from './focus-existing-window' +} from '../../../shared/workspace-layout/terminal-surfaces/terminal-pane-reveal' +import { focusExistingMainWindow } from '../activation/focus-existing-window' import { requestRendererReply } from './renderer-reply-request' const TERMINAL_PANE_REVEAL_TIMEOUT_MS = 10_000 diff --git a/src/main/window/terminal-tab-close-request-relay.test.ts b/src/main/window/renderer-relays/terminal-tab-close-request-relay.test.ts similarity index 100% rename from src/main/window/terminal-tab-close-request-relay.test.ts rename to src/main/window/renderer-relays/terminal-tab-close-request-relay.test.ts diff --git a/src/main/window/terminal-tab-close-request-relay.ts b/src/main/window/renderer-relays/terminal-tab-close-request-relay.ts similarity index 93% rename from src/main/window/terminal-tab-close-request-relay.ts rename to src/main/window/renderer-relays/terminal-tab-close-request-relay.ts index 5afd1db16c5..8af32bac09e 100644 --- a/src/main/window/terminal-tab-close-request-relay.ts +++ b/src/main/window/renderer-relays/terminal-tab-close-request-relay.ts @@ -5,8 +5,8 @@ import { ipcMain, type BrowserWindow } from 'electron' import type { TerminalTabCloseRequest, TerminalTabCloseResponse -} from '../../shared/workspace-layout/terminal-surfaces/terminal-tab-close' -import { addFanoutListener, addFanoutListeners } from '../runtime/events/event-listener-fanout' +} from '../../../shared/workspace-layout/terminal-surfaces/terminal-tab-close' +import { addFanoutListener, addFanoutListeners } from '../../runtime/events/event-listener-fanout' const TERMINAL_TAB_CLOSE_TIMEOUT_MS = 20_000 diff --git a/src/main/window/runtime-window-lifecycle-terminal-reveal.test.ts b/src/main/window/runtime-window-lifecycle-terminal-reveal.test.ts index 6ef20dd9e07..c3ace70cae0 100644 --- a/src/main/window/runtime-window-lifecycle-terminal-reveal.test.ts +++ b/src/main/window/runtime-window-lifecycle-terminal-reveal.test.ts @@ -11,7 +11,9 @@ vi.mock('electron', () => ({ ipcMain })) vi.mock('../ipc/worktrees/listing/worktree-change-invalidators', () => ({ runWorktreeChangeInvalidators: vi.fn() })) -vi.mock('./renderer-document-navigation', () => ({ registerRendererDocumentNavigation: vi.fn() })) +vi.mock('./security/renderer-document-navigation', () => ({ + registerRendererDocumentNavigation: vi.fn() +})) import { registerRuntimeWindowLifecycle } from './runtime-window-lifecycle' diff --git a/src/main/window/runtime-window-lifecycle.ts b/src/main/window/runtime-window-lifecycle.ts index 00ca5b917f1..c82c7d14eec 100644 --- a/src/main/window/runtime-window-lifecycle.ts +++ b/src/main/window/runtime-window-lifecycle.ts @@ -4,18 +4,18 @@ import { ipcMain, type BrowserWindow } from 'electron' import type { CreateWorktreeResult } from '../../shared/worktree/create-types' import type { WorktreeStartupLaunch } from '../../shared/worktree/launch-types' -import type { RuntimeMobileSessionTabMove } from '../../shared/runtime-types' +import type { RuntimeMobileSessionTabMove } from '../../shared/runtime-session-contracts' import type { TerminalTabCreateReply } from '../../shared/workspace-layout/terminal-surfaces/terminal-reveal-identity' import { runWorktreeChangeInvalidators } from '../ipc/worktrees/listing/worktree-change-invalidators' -import { requestAgentLaunchTabPublishFromRenderer } from './agent-launch-tab-publish-relay' -import { requestAgentPaneActivityFromRenderer } from './agent-pane-activity-relay' +import { requestAgentLaunchTabPublishFromRenderer } from './renderer-relays/agent-launch-tab-publish-relay' +import { requestAgentPaneActivityFromRenderer } from './renderer-relays/agent-pane-activity-relay' import type { OrcaRuntimeService } from '../runtime/orca-runtime' -import { registerRendererDocumentNavigation } from './renderer-document-navigation' -import { createRuntimeRendererNotificationSender } from './runtime-renderer-notification-sender' -import { requestSessionTabCloseFromRenderer } from './session-tab-close-request-relay' -import { requestTerminalPaneMoveFromRenderer } from './terminal-pane-move-request-relay' -import { revealTerminalPaneInWindow } from './terminal-pane-reveal-relay' -import { requestTerminalTabCloseFromRenderer } from './terminal-tab-close-request-relay' +import { registerRendererDocumentNavigation } from './security/renderer-document-navigation' +import { createRuntimeRendererNotificationSender } from './renderer-relays/runtime-renderer-notification-sender' +import { requestSessionTabCloseFromRenderer } from './renderer-relays/session-tab-close-request-relay' +import { requestTerminalPaneMoveFromRenderer } from './renderer-relays/terminal-pane-move-request-relay' +import { revealTerminalPaneInWindow } from './renderer-relays/terminal-pane-reveal-relay' +import { requestTerminalTabCloseFromRenderer } from './renderer-relays/terminal-tab-close-request-relay' import { ORCA_SIDEBAR_INPUTS_IS_MAIN } from '../../shared/sidebar-inputs-switch' import type { AgentListPublication } from '../../shared/agent-status/presentation/agent-list-status' import { addFanoutListener } from '../runtime/events/event-listener-fanout' diff --git a/src/main/window/host-frame-navigation-guard.test.ts b/src/main/window/security/host-frame-navigation-guard.test.ts similarity index 68% rename from src/main/window/host-frame-navigation-guard.test.ts rename to src/main/window/security/host-frame-navigation-guard.test.ts index 2cd3bea64bb..9d48e3aa39b 100644 --- a/src/main/window/host-frame-navigation-guard.test.ts +++ b/src/main/window/security/host-frame-navigation-guard.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { PLUGIN_PANEL_FRAME_NAME_PREFIX } from '../../shared/plugins/frames/plugin-frame-bridge' -import { NATIVE_CHAT_VISUAL_FRAME_NAME_PREFIX } from '../../shared/native-chat/native-chat-visual-shell' +import { PLUGIN_PANEL_FRAME_NAME_PREFIX } from '../../../shared/plugins/frames/plugin-frame-bridge' +import { NATIVE_CHAT_VISUAL_FRAME_NAME_PREFIX } from '../../../shared/native-chat/native-chat-visual-shell' import { HostFrameNavigationRegistry, hostFrameKindForName } from './host-frame-navigation-guard' function frame(input: { id: number; name?: string; url?: string }) { @@ -55,26 +55,4 @@ describe('HostFrameNavigationRegistry', () => { const child = frame({ id: 4 }) expect(registry.shouldBlock(child, visual, 'https://example.com')).toBe(true) }) - - it('does not inspect unrelated guarded frames during registration or navigation', () => { - const registry = new HostFrameNavigationRegistry() - let livenessChecks = 0 - const panels = Array.from({ length: 100 }, (_, id) => ({ - ...frame({ id, name: `${PLUGIN_PANEL_FRAME_NAME_PREFIX}${id}` }), - isDestroyed: () => { - livenessChecks++ - return false - } - })) - for (const panel of panels) { - registry.register(panel) - } - const ordinary = frame({ id: 101 }) - registry.register(ordinary) - expect(registry.shouldBlock(ordinary, null, 'https://example.com')).toBe(false) - expect(registry.shouldBlock(panels[0], null, 'about:srcdoc')).toBe(false) - expect(registry.shouldBlock(panels[0], panels[0], 'https://example.com')).toBe(true) - expect(registry.shouldBlock(ordinary, panels[1], 'https://example.com')).toBe(true) - expect(livenessChecks).toBeLessThanOrEqual(4) - }) }) diff --git a/src/main/window/host-frame-navigation-guard.ts b/src/main/window/security/host-frame-navigation-guard.ts similarity index 93% rename from src/main/window/host-frame-navigation-guard.ts rename to src/main/window/security/host-frame-navigation-guard.ts index 47a516eb3ec..a938260eea2 100644 --- a/src/main/window/host-frame-navigation-guard.ts +++ b/src/main/window/security/host-frame-navigation-guard.ts @@ -1,6 +1,6 @@ import type { WebContents, WebFrameMain } from 'electron' -import { NATIVE_CHAT_VISUAL_FRAME_NAME_PREFIX } from '../../shared/native-chat/native-chat-visual-shell' -import { PLUGIN_PANEL_FRAME_NAME_PREFIX } from '../../shared/plugins/frames/plugin-frame-bridge' +import { NATIVE_CHAT_VISUAL_FRAME_NAME_PREFIX } from '../../../shared/native-chat/native-chat-visual-shell' +import { PLUGIN_PANEL_FRAME_NAME_PREFIX } from '../../../shared/plugins/frames/plugin-frame-bridge' type NavigationFrame = Pick diff --git a/src/main/window/host-frame-permission-policy.test.ts b/src/main/window/security/host-frame-permission-policy.test.ts similarity index 100% rename from src/main/window/host-frame-permission-policy.test.ts rename to src/main/window/security/host-frame-permission-policy.test.ts diff --git a/src/main/window/host-frame-permission-policy.ts b/src/main/window/security/host-frame-permission-policy.ts similarity index 100% rename from src/main/window/host-frame-permission-policy.ts rename to src/main/window/security/host-frame-permission-policy.ts diff --git a/src/main/window/main-window-webview-security.ts b/src/main/window/security/main-window-webview-security.ts similarity index 85% rename from src/main/window/main-window-webview-security.ts rename to src/main/window/security/main-window-webview-security.ts index d403d87e974..315da43ee10 100644 --- a/src/main/window/main-window-webview-security.ts +++ b/src/main/window/security/main-window-webview-security.ts @@ -1,42 +1,42 @@ import type { BrowserWindow } from 'electron' import { join } from 'node:path' -import { ORCA_BROWSER_GUEST_WEB_PREFERENCES } from '../../shared/browser/guest-lifecycle/browser-guest-web-preferences' -import { normalizeBrowserNavigationUrl } from '../../shared/browser-url' -import { browserManager } from '../browser/guest-lifecycle/manager/browser-manager' -import { browserSessionRegistry } from '../browser/sessions/profiles/browser-session-registry' +import { ORCA_BROWSER_GUEST_WEB_PREFERENCES } from '../../../shared/browser/guest-lifecycle/browser-guest-web-preferences' +import { normalizeBrowserNavigationUrl } from '../../../shared/browser-url' +import { browserManager } from '../../browser/guest-lifecycle/manager/browser-manager' +import { browserSessionRegistry } from '../../browser/sessions/profiles/browser-session-registry' import { enforceLocalSshWebRtcPolicyForGuest, isLocalSshBrowserPartition -} from '../browser/routing/local-host-partitions/local-ssh-browser-partitions' +} from '../../browser/routing/local-host-partitions/local-ssh-browser-partitions' import { browserRouteSessionRegistry, browserRouteWebContentsRegistry -} from '../browser/routing/sessions/browser-route-session-runtime' -import '../../shared/constants' -import { ORCA_BROWSER_BLANK_URL } from '../../shared/browser/browser-blank-page' +} from '../../browser/routing/sessions/browser-route-session-runtime' +import '../../../shared/constants' +import { ORCA_BROWSER_BLANK_URL } from '../../../shared/browser/browser-blank-page' import { DOC_PREVIEW_PARTITION, parseDocPreviewUrl -} from '../../shared/document-preview/doc-preview-scheme' -import { setDocPreviewFailureSink } from '../browser/document-preview/doc-preview-failure-notice' +} from '../../../shared/document-preview/doc-preview-scheme' +import { setDocPreviewFailureSink } from '../../browser/document-preview/doc-preview-failure-notice' import { getDocPreviewGrant, revokeAllDocPreviewGrants -} from '../browser/document-preview/doc-preview-grant-registry' -import { isDocPreviewSession } from '../browser/document-preview/doc-preview-protocol' +} from '../../browser/document-preview/doc-preview-grant-registry' +import { isDocPreviewSession } from '../../browser/document-preview/doc-preview-protocol' import { registerHostFrameNavigationGuard } from './host-frame-navigation-guard' import { installPrivilegedWindowNavigationPolicy } from './privileged-window-navigation' -import { parseChromeExtensionUrl } from '../../shared/browser/extensions/browser-extension-url' -import { getBrowserExtensionSessionForProfile } from '../browser/extensions/profile-sessions/browser-extension-session-loader' -import { installBrowserJavaScriptDialogs } from '../browser/guest-lifecycle/prompts/browser-javascript-dialogs' -import { installNativeChatVisualGuestTheme } from '../host/native-chat-visual-guest-theme' +import { parseChromeExtensionUrl } from '../../../shared/browser/extensions/browser-extension-url' +import { getBrowserExtensionSessionForProfile } from '../../browser/extensions/profile-sessions/browser-extension-session-loader' +import { installBrowserJavaScriptDialogs } from '../../browser/guest-lifecycle/prompts/browser-javascript-dialogs' +import { installNativeChatVisualGuestTheme } from '../../host/native-chat-visual-guest-theme' import { admitPluginFrameAttach, attachPluginFrameGuest, isPluginFramePartition, PLUGIN_FRAME_GUEST_RENDERER_ARGUMENTS -} from '../plugins/panels/guest-isolation/plugin-frame-guest-registry' -import { attachBrowserPasswordGuest } from '../browser-passwords/browser-password-guest-service' +} from '../../plugins/panels/guest-isolation/plugin-frame-guest-registry' +import { attachBrowserPasswordGuest } from '../../browser-passwords/browser-password-guest-service' /** orca-preview is outside normal browser URL admission; require the doc partition and a main-minted user-preview grant. */ function isAdmissibleDocPreviewAttach(partition: string, src: string): boolean { diff --git a/src/main/window/privileged-window-navigation.ts b/src/main/window/security/privileged-window-navigation.ts similarity index 92% rename from src/main/window/privileged-window-navigation.ts rename to src/main/window/security/privileged-window-navigation.ts index fd50766d06c..c9d4fa1db93 100644 --- a/src/main/window/privileged-window-navigation.ts +++ b/src/main/window/security/privileged-window-navigation.ts @@ -1,5 +1,5 @@ import { shell, type WebContents } from 'electron' -import { normalizeExternalBrowserUrl } from '../../shared/browser-url' +import { normalizeExternalBrowserUrl } from '../../../shared/browser-url' import { isRendererDocumentNavigation } from './renderer-document-navigation' /** Keep remote documents from inheriting an Orca window's privileged preload. */ diff --git a/src/main/window/renderer-document-navigation.ts b/src/main/window/security/renderer-document-navigation.ts similarity index 100% rename from src/main/window/renderer-document-navigation.ts rename to src/main/window/security/renderer-document-navigation.ts diff --git a/src/main/window/renderer-publication-throttle-electron-fixture.ts b/src/main/window/testing/renderer-publication-throttle-electron-fixture.ts similarity index 93% rename from src/main/window/renderer-publication-throttle-electron-fixture.ts rename to src/main/window/testing/renderer-publication-throttle-electron-fixture.ts index 3a417f1dc9f..ef9c2eb52e0 100644 --- a/src/main/window/renderer-publication-throttle-electron-fixture.ts +++ b/src/main/window/testing/renderer-publication-throttle-electron-fixture.ts @@ -1,6 +1,6 @@ import { app, BrowserWindow } from 'electron' -import { startBrowserScreencast } from '../browser/cdp/screencast/browser-screencast-stream' -import { RendererPublicationThrottle } from './renderer-publication-throttle' +import { startBrowserScreencast } from '../../browser/cdp/screencast/browser-screencast-stream' +import { RendererPublicationThrottle } from '../visibility/renderer-publication-throttle' async function run(): Promise { // Serve must survive with no page open; keep Electron alive after last-window close. diff --git a/src/main/window/hidden-window-frame-pump.ts b/src/main/window/visibility/hidden-window-frame-pump.ts similarity index 88% rename from src/main/window/hidden-window-frame-pump.ts rename to src/main/window/visibility/hidden-window-frame-pump.ts index 70ab990920a..1f7fb1a7b44 100644 --- a/src/main/window/hidden-window-frame-pump.ts +++ b/src/main/window/visibility/hidden-window-frame-pump.ts @@ -1,4 +1,4 @@ -import { HIDDEN_WINDOW_FRAME_PUMP_STOP_CHANNEL } from '../../shared/hidden-window-frame-pump' +import { HIDDEN_WINDOW_FRAME_PUMP_STOP_CHANNEL } from '../../../shared/hidden-window-frame-pump' type FramePumpWindow = { on(event: 'show', listener: () => void): unknown diff --git a/src/main/window/main-window-visibility.ts b/src/main/window/visibility/main-window-visibility.ts similarity index 100% rename from src/main/window/main-window-visibility.ts rename to src/main/window/visibility/main-window-visibility.ts diff --git a/src/main/window/renderer-publication-throttle.electron.test.ts b/src/main/window/visibility/renderer-publication-throttle.electron.test.ts similarity index 89% rename from src/main/window/renderer-publication-throttle.electron.test.ts rename to src/main/window/visibility/renderer-publication-throttle.electron.test.ts index a01dfceb678..90211732c24 100644 --- a/src/main/window/renderer-publication-throttle.electron.test.ts +++ b/src/main/window/visibility/renderer-publication-throttle.electron.test.ts @@ -5,7 +5,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { describe, expect, it } from 'vitest' import { build } from 'vite' -import { resolveElectronProbeLaunch } from '../browser/electron-fixtures/electron-probe-display-launch' +import { resolveElectronProbeLaunch } from '../../browser/electron-fixtures/electron-probe-display-launch' describe('publication lease release during Electron guest destruction', () => { it('closes streamed guests without terminating the host', async () => { @@ -17,7 +17,10 @@ describe('publication lease release during Electron guest destruction', () => { build: { outDir: root, lib: { - entry: join(import.meta.dirname, 'renderer-publication-throttle-electron-fixture.ts'), + entry: join( + import.meta.dirname, + '../testing/renderer-publication-throttle-electron-fixture.ts' + ), formats: ['cjs'], fileName: () => 'main.cjs' }, diff --git a/src/main/window/renderer-publication-throttle.ts b/src/main/window/visibility/renderer-publication-throttle.ts similarity index 100% rename from src/main/window/renderer-publication-throttle.ts rename to src/main/window/visibility/renderer-publication-throttle.ts diff --git a/src/main/window/resource-view-admission.ts b/src/main/window/visibility/resource-view-admission.ts similarity index 74% rename from src/main/window/resource-view-admission.ts rename to src/main/window/visibility/resource-view-admission.ts index 8bec07bd3eb..4cc6fb030a1 100644 --- a/src/main/window/resource-view-admission.ts +++ b/src/main/window/visibility/resource-view-admission.ts @@ -1,6 +1,6 @@ import type { BrowserWindow, IpcMainInvokeEvent } from 'electron' -import { getRuntimeDesktopSurface } from '../runtime/construction/runtime-desktop-surface' -import type { ResourceViewAdmission } from '../../shared/resource-view-admission' +import { getRuntimeDesktopSurface } from '../../runtime/construction/runtime-desktop-surface' +import type { ResourceViewAdmission } from '../../../shared/resource-view-admission' export function isResourceWindowVisible(window: BrowserWindow | null): boolean { return !!window && !window.isDestroyed() && window.isVisible() && !window.isMinimized() diff --git a/src/main/window/wait-for-window-to-show.ts b/src/main/window/visibility/wait-for-window-to-show.ts similarity index 100% rename from src/main/window/wait-for-window-to-show.ts rename to src/main/window/visibility/wait-for-window-to-show.ts diff --git a/src/main/window/workspace-space-window.ts b/src/main/window/workspace-space-window.ts index 08bbfedc31e..da4deba0e58 100644 --- a/src/main/window/workspace-space-window.ts +++ b/src/main/window/workspace-space-window.ts @@ -1,8 +1,11 @@ import { mainI18n, translateMain } from '../i18n/main-i18n' import { app, BrowserWindow, nativeTheme, type WebContents } from 'electron' import { join } from 'node:path' -import { isBackgroundLaunch, showWindowWithoutStealingFocus } from './foreground-activation-policy' -import { installPrivilegedWindowNavigationPolicy } from './privileged-window-navigation' +import { + isBackgroundLaunch, + showWindowWithoutStealingFocus +} from './activation/foreground-activation-policy' +import { installPrivilegedWindowNavigationPolicy } from './security/privileged-window-navigation' import { sendToTrustedUIRenderer } from '../ipc/ui' import type { Store } from '../persistence/loading-store/store' import { stepUIZoomLevel, type UIZoomDirection } from '../../shared/ui-zoom-level' diff --git a/tests/tools/benchmarks/spinner-rendering/main.ts b/tests/tools/benchmarks/spinner-rendering/main.ts index 8b107868e80..4dcc48eb68d 100644 --- a/tests/tools/benchmarks/spinner-rendering/main.ts +++ b/tests/tools/benchmarks/spinner-rendering/main.ts @@ -1,6 +1,6 @@ import { app, BrowserWindow } from 'electron' import path from 'node:path' -import { applyBackgroundActivationPolicy } from '../../../../src/main/window/foreground-activation-policy' +import { applyBackgroundActivationPolicy } from '../../../../src/main/window/activation/foreground-activation-policy' if (process.env.ORCA_BACKGROUND_LAUNCH !== '1') { throw new Error('Spinner measurements require ORCA_BACKGROUND_LAUNCH=1')