mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
test: remove 39 low-value suites from audit batch 849 (#36491)
* test: remove 39 low-value suites from audit batch 849 * test: remove obsolete shell-contract trigger for deleted suite --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -831,7 +831,6 @@ jobs:
|
||||
src/main/daemon/shell-ready.test.ts \
|
||||
src/main/daemon/node-pty-fd-leak.test.ts \
|
||||
src/main/providers/local-pty-shell-ready-zsh-launch-environment.test.ts \
|
||||
src/main/providers/__tests__/shell-ready-framework-example.test.ts \
|
||||
src/main/pty/codex-shell-launch-preflight.test.ts \
|
||||
src/main/pty/codex-shell-no-daemon.test.ts \
|
||||
src/main/pty/omp-shell-wrapper-alias-safety.test.ts \
|
||||
|
||||
@@ -1118,7 +1118,6 @@
|
||||
5 test-no-wall-clock:src/renderer/src/components/status-bar/inline-usage-bars.test.tsx
|
||||
4 test-no-wall-clock:src/renderer/src/components/status-bar/provider-segment-balance.test.tsx
|
||||
2 test-no-wall-clock:src/renderer/src/components/status-bar/status-bar-antigravity-segment.test.tsx
|
||||
1 test-no-wall-clock:src/renderer/src/components/status-bar/status-bar-cursor-segment.test.tsx
|
||||
3 test-no-wall-clock:src/renderer/src/components/status-bar/status-bar-provider-menu-focus.test.tsx
|
||||
1 test-no-wall-clock:src/renderer/src/components/status-bar/workspace-space-worktree-row.test.tsx
|
||||
1 test-no-wall-clock:src/renderer/src/components/tab-bar/TabBarCreateEntry.history.test.tsx
|
||||
|
||||
@@ -1431,7 +1431,6 @@
|
||||
"src/main/claude-accounts/claude-profile-router.test.ts": 864,
|
||||
"src/main/claude-accounts/claude-profile-setup-worker.test.ts": 331,
|
||||
"src/main/claude-accounts/claude-profile-setup.test.ts": 243,
|
||||
"src/main/claude-accounts/claude-profile-shell-composition.test.ts": 70,
|
||||
"src/main/claude-accounts/claude-profile-wsl-router.test.ts": 821,
|
||||
"src/main/claude-accounts/claude-sign-in-browser.test.ts": 288,
|
||||
"src/main/claude-accounts/claude-structured-auth-policy.test.ts": 57,
|
||||
@@ -1760,7 +1759,6 @@
|
||||
"src/main/codex/codex-config-mirror-mcp-ownership.test.ts": 114,
|
||||
"src/main/codex/codex-config-mirror.test.ts": 247,
|
||||
"src/main/codex/codex-config-settings-removal.test.ts": 36,
|
||||
"src/main/codex/codex-configured-launch-defaults.test.ts": 62,
|
||||
"src/main/codex/codex-daemon-socket-path-guard.test.ts": 101,
|
||||
"src/main/codex/codex-default-mode-subagents.test.ts": 574,
|
||||
"src/main/codex/codex-goal-journal-rows.test.ts": 74,
|
||||
@@ -2499,7 +2497,6 @@
|
||||
"src/main/git/status-upstream-negative-cache.test.ts": 839,
|
||||
"src/main/git/status-upstream-probe-churn.test.ts": 147,
|
||||
"src/main/git/status-upstream-ref.test.ts": 42,
|
||||
"src/main/git/status-wsl-pathspecs.test.ts": 161,
|
||||
"src/main/git/status.test.ts": 184,
|
||||
"src/main/git/upstream-deferred-fork-remote-real.test.ts": 195,
|
||||
"src/main/git/upstream.test.ts": 371,
|
||||
@@ -2518,7 +2515,6 @@
|
||||
"src/main/git/worktree-checkout-progress-real-git.test.ts": 631,
|
||||
"src/main/git/worktree-common-dir-comparison.test.ts": 178,
|
||||
"src/main/git/worktree-configured-paths-concurrency.test.ts": 67,
|
||||
"src/main/git/worktree-create-admission-tier.test.ts": 249,
|
||||
"src/main/git/worktree-create-git-executor-real-git.test.ts": 540,
|
||||
"src/main/git/worktree-create-preparation-real-git.test.ts": 2248,
|
||||
"src/main/git/worktree-create-preparation-real-wsl.test.ts": 209,
|
||||
@@ -2685,7 +2681,6 @@
|
||||
"src/main/gitlab/client-work-items.test.ts": 539,
|
||||
"src/main/gitlab/client.test.ts": 395,
|
||||
"src/main/gitlab/gitlab-known-host-backoff.test.ts": 349,
|
||||
"src/main/gitlab/gitlab-known-host-probe-wsl-fallback.test.ts": 457,
|
||||
"src/main/gitlab/gitlab-known-host-probe.test.ts": 420,
|
||||
"src/main/gitlab/gitlab-known-host-retirement.test.ts": 420,
|
||||
"src/main/gitlab/gl-utils.test.ts": 437,
|
||||
@@ -2945,7 +2940,6 @@
|
||||
"src/main/ipc/preflight-wsl-command.test.ts": 48,
|
||||
"src/main/ipc/pty-attach-view-joins-main-start.test.ts": 3883,
|
||||
"src/main/ipc/pty-claude-agent-view-readiness.test.ts": 3531,
|
||||
"src/main/ipc/pty-painted-cold-restore-ack-layout-owner.test.ts": 2928,
|
||||
"src/main/ipc/pty-spawn-saved-resume-admission.test.ts": 6908,
|
||||
"src/main/ipc/pty/daemon-late-exit.test.ts": 6475,
|
||||
"src/main/ipc/pty/delivery/attached-pty-size.test.ts": 68,
|
||||
@@ -3279,7 +3273,6 @@
|
||||
"src/main/ipc/worktrees/listing/detected-worktree-scan-hygiene-gate.test.ts": 79,
|
||||
"src/main/ipc/worktrees/listing/detected-worktree-scan-listing-distro.test.ts": 677,
|
||||
"src/main/ipc/worktrees/listing/detected-worktree-scan-superseded.test.ts": 46,
|
||||
"src/main/ipc/worktrees/listing/register-sparse-checkout-cache-invalidation.test.ts": 122,
|
||||
"src/main/ipc/wsl-guest-windows-drive-path.test.ts": 58,
|
||||
"src/main/ipc/wsl-host-gate-off.test.ts": 3774,
|
||||
"src/main/ipc/wsl-move-handlers.test.ts": 399,
|
||||
@@ -3775,7 +3768,6 @@
|
||||
"src/main/native-chat/native-chat-visuals-delivery.test.ts": 653,
|
||||
"src/main/native-chat/native-chat-visuals-sweep.test.ts": 150,
|
||||
"src/main/native-chat/session-file-resolver-claude-roots.test.ts": 157,
|
||||
"src/main/native-chat/session-file-resolver-codex-roots.test.ts": 164,
|
||||
"src/main/native-chat/session-file-resolver-wsl-scan-gate.test.ts": 138,
|
||||
"src/main/native-chat/session-file-resolver-wsl.test.ts": 107,
|
||||
"src/main/native-chat/session-file-resolver.test.ts": 150,
|
||||
@@ -5237,7 +5229,6 @@
|
||||
"src/main/runtime/rpc/methods/ai-vault-structured-ownership.test.ts": 1351,
|
||||
"src/main/runtime/rpc/methods/ai-vault.test.ts": 2715,
|
||||
"src/main/runtime/rpc/methods/artifacts.test.ts": 241,
|
||||
"src/main/runtime/rpc/methods/automatic-work.test.ts": 2248,
|
||||
"src/main/runtime/rpc/methods/automation-scoped-list-methods.test.ts": 526,
|
||||
"src/main/runtime/rpc/methods/automations.test.ts": 527,
|
||||
"src/main/runtime/rpc/methods/browser-client-host-attach-adoption.test.ts": 621,
|
||||
@@ -5719,7 +5710,6 @@
|
||||
"src/main/runtime/runtime-worktree-default-tabs-layout-owner.test.ts": 36,
|
||||
"src/main/runtime/runtime-worktree-ps-summaries.test.ts": 83,
|
||||
"src/main/runtime/runtime-worktree-ps-unverifiable-terminals.test.ts": 35,
|
||||
"src/main/runtime/runtime-worktree-removal-process-cleanup.test.ts": 3713,
|
||||
"src/main/runtime/runtime-worktree-selection.test.ts": 36,
|
||||
"src/main/runtime/runtime-worktree-startup-readiness.test.ts": 215,
|
||||
"src/main/runtime/runtime-worktree-structured-agent-rows-liveness.test.ts": 762,
|
||||
@@ -5760,7 +5750,6 @@
|
||||
"src/main/runtime/sidebar-inputs/orca-runtime-sort-stamps.test.ts": 4086,
|
||||
"src/main/runtime/sidebar-inputs/orca-runtime-terminal-use.test.ts": 2341,
|
||||
"src/main/runtime/sidebar-inputs/orca-runtime-workspace-activity-facts.test.ts": 3455,
|
||||
"src/main/runtime/sidebar-inputs/orca-runtime-workspace-review-facts.test.ts": 3885,
|
||||
"src/main/runtime/sidebar-inputs/orca-runtime-workspace-unread.test.ts": 3997,
|
||||
"src/main/runtime/sidebar-inputs/paired-server-rows-producer.test.ts": 265,
|
||||
"src/main/runtime/sidebar-inputs/repo-icon-data-urls.test.ts": 191,
|
||||
@@ -7283,7 +7272,6 @@
|
||||
"src/renderer/src/components/editor/combined-diff-on-demand-load.test.ts": 94,
|
||||
"src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree-collapsed-work.test.tsx": 742,
|
||||
"src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree-render.test.tsx": 737,
|
||||
"src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree-row-drag.test.tsx": 305,
|
||||
"src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree-windowing.test.tsx": 577,
|
||||
"src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree.test.ts": 77,
|
||||
"src/renderer/src/components/editor/combined-diff/browse-files/use-combined-diff-file-tree-resize.test.tsx": 248,
|
||||
@@ -8151,7 +8139,6 @@
|
||||
"src/renderer/src/components/right-sidebar/AiVaultSessionSubagents.test.tsx": 1926,
|
||||
"src/renderer/src/components/right-sidebar/AiVaultSessionVirtualList.keyboard.test.tsx": 2036,
|
||||
"src/renderer/src/components/right-sidebar/AiVaultSessionVirtualList.measurement.test.tsx": 2554,
|
||||
"src/renderer/src/components/right-sidebar/AiVaultShowMoreSessionsRow.test.tsx": 369,
|
||||
"src/renderer/src/components/right-sidebar/AiVaultVirtualRow.test.tsx": 2253,
|
||||
"src/renderer/src/components/right-sidebar/ChecksPanel.updated-at-metadata.test.tsx": 243,
|
||||
"src/renderer/src/components/right-sidebar/CommitArea.chevron-spinner.test.tsx": 1564,
|
||||
@@ -8376,7 +8363,6 @@
|
||||
"src/renderer/src/components/right-sidebar/source-control/review/use-review-push-target.test.tsx": 255,
|
||||
"src/renderer/src/components/right-sidebar/source-control/sync/git-history-format.test.ts": 47,
|
||||
"src/renderer/src/components/right-sidebar/source-control/sync/git-history-graph-svg.test.tsx": 102,
|
||||
"src/renderer/src/components/right-sidebar/source-control/sync/use-status-refresh.local-ignore.test.tsx": 272,
|
||||
"src/renderer/src/components/right-sidebar/use-ai-vault-search-focus-request.test.tsx": 1566,
|
||||
"src/renderer/src/components/right-sidebar/use-ai-vault-search.test.tsx": 485,
|
||||
"src/renderer/src/components/right-sidebar/use-checks-panel-terminal-worktree.test.ts": 1574,
|
||||
@@ -8512,7 +8498,6 @@
|
||||
"src/renderer/src/components/settings/SessionHistorySettingsPane.test.tsx": 903,
|
||||
"src/renderer/src/components/settings/SettingsConstants.test.ts": 97,
|
||||
"src/renderer/src/components/settings/SettingsFormControls.font-autocomplete.test.tsx": 789,
|
||||
"src/renderer/src/components/settings/SettingsFormControls.number-field.test.tsx": 753,
|
||||
"src/renderer/src/components/settings/SettingsFormControls.segmented-control.test.tsx": 548,
|
||||
"src/renderer/src/components/settings/SettingsSidebar.test.tsx": 2169,
|
||||
"src/renderer/src/components/settings/ShareSkillsSettingsPane.test.tsx": 601,
|
||||
@@ -8557,7 +8542,6 @@
|
||||
"src/renderer/src/components/settings/browser-session-host-selection.test.ts": 43,
|
||||
"src/renderer/src/components/settings/bundled-skill-setup-result-copy.test.ts": 304,
|
||||
"src/renderer/src/components/settings/chat-naming-persistence.test.tsx": 3654,
|
||||
"src/renderer/src/components/settings/chat-settings-unsaved-guard.test.tsx": 1560,
|
||||
"src/renderer/src/components/settings/cli-install-failure.test.ts": 34,
|
||||
"src/renderer/src/components/settings/codex-config-sync-warning.test.ts": 34,
|
||||
"src/renderer/src/components/settings/codex-session-source-home-control.test.tsx": 154,
|
||||
@@ -8665,7 +8649,6 @@
|
||||
"src/renderer/src/components/sidebar/NewExternalWorktreesInboxLine.test.tsx": 1735,
|
||||
"src/renderer/src/components/sidebar/NonGitFolderDialog.test.tsx": 1097,
|
||||
"src/renderer/src/components/sidebar/NoticeHostGlyph.test.tsx": 2211,
|
||||
"src/renderer/src/components/sidebar/OrcaYamlTrustDialog.test.tsx": 54,
|
||||
"src/renderer/src/components/sidebar/PendingWorktreeRow.test.tsx": 1397,
|
||||
"src/renderer/src/components/sidebar/ProjectAddedDialog.test.tsx": 187,
|
||||
"src/renderer/src/components/sidebar/ProjectGroupDeleteDialog.test.tsx": 2252,
|
||||
@@ -9017,7 +9000,6 @@
|
||||
"src/renderer/src/components/sparse/SparsePresetChooser.test.tsx": 510,
|
||||
"src/renderer/src/components/sparse/sparse-directory-browse.test.ts": 45,
|
||||
"src/renderer/src/components/sparse/sparse-directory-entry-input.test.ts": 107,
|
||||
"src/renderer/src/components/standalone-terminal/open-standalone-terminal.test.ts": 1321,
|
||||
"src/renderer/src/components/star-nag/StarNagAgentValueMomentObserver.test.tsx": 1534,
|
||||
"src/renderer/src/components/star-nag/StarNagToastHost.test.tsx": 344,
|
||||
"src/renderer/src/components/stats/CodexUsagePane.test.tsx": 753,
|
||||
@@ -9070,7 +9052,6 @@
|
||||
"src/renderer/src/components/status-bar/ssh-status-menu-rows.test.ts": 48,
|
||||
"src/renderer/src/components/status-bar/status-bar-antigravity-segment.test.tsx": 968,
|
||||
"src/renderer/src/components/status-bar/status-bar-copy-localization.test.tsx": 885,
|
||||
"src/renderer/src/components/status-bar/status-bar-cursor-segment.test.tsx": 1503,
|
||||
"src/renderer/src/components/status-bar/status-bar-density.test.ts": 57,
|
||||
"src/renderer/src/components/status-bar/status-bar-pool-countdown.test.tsx": 389,
|
||||
"src/renderer/src/components/status-bar/status-bar-provider-menu-focus.test.tsx": 941,
|
||||
@@ -9230,11 +9211,9 @@
|
||||
"src/renderer/src/components/task-page-jira-status-catalog.test.ts": 60,
|
||||
"src/renderer/src/components/task-page/jira/task-page-jira-status-order-session.test.tsx": 2111,
|
||||
"src/renderer/src/components/task-page/jira/task-page-jira-text-fallback-notice.test.tsx": 375,
|
||||
"src/renderer/src/components/task-page/jira/task-page-jira-workspace-actions.test.tsx": 1971,
|
||||
"src/renderer/src/components/task-page/task-page-list-chrome-visibility.test.ts": 33,
|
||||
"src/renderer/src/components/task-page/task-page-mutation-page-allocation.test.ts": 128,
|
||||
"src/renderer/src/components/task-page/task-page-new-issue-draft.test.ts": 76,
|
||||
"src/renderer/src/components/task-page/task-page-repo-source-divergence.test.ts": 1043,
|
||||
"src/renderer/src/components/task-page/task-page-task-source-host-availability.test.ts": 401,
|
||||
"src/renderer/src/components/task-page/task-page-work-item-pagination.test.ts": 56,
|
||||
"src/renderer/src/components/task-page/github/rows-twin-host-routing.test.tsx": 1521,
|
||||
@@ -9700,7 +9679,6 @@
|
||||
"src/renderer/src/components/terminal-pane/terminal-option-punctuation.test.ts": 65,
|
||||
"src/renderer/src/components/terminal-pane/terminal-output-visibility.test.ts": 39,
|
||||
"src/renderer/src/components/terminal-pane/terminal-pane-attention-subscriptions.test.ts": 42,
|
||||
"src/renderer/src/components/terminal-pane/terminal-pane-bootstrap-split-in-main.test.ts": 1119,
|
||||
"src/renderer/src/components/terminal-pane/terminal-pane-close-identity.test.ts": 26,
|
||||
"src/renderer/src/components/terminal-pane/terminal-pane-detach-layout-owner.test.ts": 1521,
|
||||
"src/renderer/src/components/terminal-pane/terminal-pane-element-file-drop.test.tsx": 1853,
|
||||
@@ -9830,7 +9808,6 @@
|
||||
"src/renderer/src/components/terminal-search-decoration-leak.test.ts": 1946,
|
||||
"src/renderer/src/components/terminal-search-long-wrapped-line.test.ts": 3017,
|
||||
"src/renderer/src/components/terminal-search-safe-find.test.ts": 48,
|
||||
"src/renderer/src/components/terminal-split-workspace-surfaces-startup-hold.test.tsx": 1949,
|
||||
"src/renderer/src/components/terminal-startup-hold-placeholder.test.tsx": 559,
|
||||
"src/renderer/src/components/terminal-workspace-keydown.test.ts": 646,
|
||||
"src/renderer/src/components/terminal-workspace-surface-ids.test.tsx": 1948,
|
||||
@@ -9886,7 +9863,6 @@
|
||||
"src/renderer/src/components/use-terminal-editor-close-foundation.window-close.test.tsx": 1766,
|
||||
"src/renderer/src/components/use-terminal-editor-close-queue.reveal.test.tsx": 581,
|
||||
"src/renderer/src/components/use-terminal-parking-pass.test.tsx": 1759,
|
||||
"src/renderer/src/components/use-terminal-window-lifecycle.lazy-ref.test.tsx": 170,
|
||||
"src/renderer/src/components/use-terminal-workspace-foundation.runtime-status.test.tsx": 1503,
|
||||
"src/renderer/src/components/use-worktree-jump-palette-browser-ownership.test.ts": 1477,
|
||||
"src/renderer/src/components/use-worktree-jump-palette-task-search.test.tsx": 1933,
|
||||
@@ -9905,7 +9881,6 @@
|
||||
"src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row-data.test.ts": 279,
|
||||
"src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row.test.tsx": 2141,
|
||||
"src/renderer/src/components/workspace-cleanup/workspace-cleanup-confirm-remove.test.tsx": 658,
|
||||
"src/renderer/src/components/workspace-cleanup/workspace-cleanup-confirm-stop-agents.test.tsx": 343,
|
||||
"src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-controls.test.tsx": 601,
|
||||
"src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-filter.test.ts": 107,
|
||||
"src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-sort.test.ts": 127,
|
||||
@@ -9941,7 +9916,6 @@
|
||||
"src/renderer/src/hooks/client-workspace-view-report.test.ts": 42,
|
||||
"src/renderer/src/hooks/composer-branch-selection.test.ts": 50,
|
||||
"src/renderer/src/hooks/composer-drop-result.test.ts": 25,
|
||||
"src/renderer/src/hooks/composer-native-file-drop.test.ts": 62,
|
||||
"src/renderer/src/hooks/composer-state/attachment-drop-failure.test.tsx": 547,
|
||||
"src/renderer/src/hooks/composer-state/attachment-drop-lifetime.test.tsx": 741,
|
||||
"src/renderer/src/hooks/composer-state/branch-start-point-actions.test.ts": 231,
|
||||
@@ -10619,7 +10593,6 @@
|
||||
"src/renderer/src/lib/simulator-tab-palette-activation.test.ts": 1304,
|
||||
"src/renderer/src/lib/skill-freshness-display-status.test.ts": 109,
|
||||
"src/renderer/src/lib/sleeping-agent-pane-ownership.test.ts": 45,
|
||||
"src/renderer/src/lib/sleeping-agent-session-launch-layout-owner.test.ts": 536,
|
||||
"src/renderer/src/lib/sleeping-agent-session-launch-windows-quoting.test.ts": 533,
|
||||
"src/renderer/src/lib/smart-github-submit.test.ts": 75,
|
||||
"src/renderer/src/lib/source-control-agent-action-launch-check.test.ts": 1325,
|
||||
@@ -11162,7 +11135,6 @@
|
||||
"src/renderer/src/store/plugin-panels.test.ts": 122,
|
||||
"src/renderer/src/store/project-host-setup-selector.test.ts": 47,
|
||||
"src/renderer/src/store/projects/project-catalog-null-field-ingest.test.ts": 143,
|
||||
"src/renderer/src/store/projects/project-wsl-filesystem-boundary-advisory.test.ts": 72,
|
||||
"src/renderer/src/store/react-commit-cascade-write-probe.test.ts": 169,
|
||||
"src/renderer/src/store/repos/converted-ssh-browser-pages.test.ts": 489,
|
||||
"src/renderer/src/store/repos/converted-ssh-host-rows.test.ts": 33,
|
||||
@@ -11354,7 +11326,6 @@
|
||||
"src/renderer/src/store/slices/project-removal-owner-host.test.ts": 1455,
|
||||
"src/renderer/src/store/slices/project-removal-runtime-terminal-order.test.ts": 1801,
|
||||
"src/renderer/src/store/slices/pull-request-generation-jira-title.test.ts": 76,
|
||||
"src/renderer/src/store/slices/purge-stale-runtime-host-groups.test.ts": 1390,
|
||||
"src/renderer/src/store/slices/purge-stale-runtime-host-ownership.test.ts": 1529,
|
||||
"src/renderer/src/store/slices/purge-stale-runtime-host-state.test.ts": 1678,
|
||||
"src/renderer/src/store/slices/readopted-ssh-worktree-rows.test.ts": 47,
|
||||
@@ -11399,7 +11370,6 @@
|
||||
"src/renderer/src/store/slices/runtime-status-catalog-identity.test.ts": 286,
|
||||
"src/renderer/src/store/slices/runtime-status-first-publication.test.ts": 329,
|
||||
"src/renderer/src/store/slices/runtime-status-reconnect-connection-generation.test.ts": 1219,
|
||||
"src/renderer/src/store/slices/runtime-status-restored-browser-host-attach.test.ts": 376,
|
||||
"src/renderer/src/store/slices/runtime-status-skill-cache-eviction.test.ts": 269,
|
||||
"src/renderer/src/store/slices/runtime-status-snapshot.test.ts": 378,
|
||||
"src/renderer/src/store/slices/runtime-status.test.ts": 576,
|
||||
@@ -11459,7 +11429,6 @@
|
||||
"src/renderer/src/store/slices/tabs-unread-and-focus.test.ts": 1556,
|
||||
"src/renderer/src/store/slices/tabs/hydrated-workspace-reconciliation-batch.test.ts": 1774,
|
||||
"src/renderer/src/store/slices/tabs/layout-owner-shared-path-gates.test.ts": 2018,
|
||||
"src/renderer/src/store/slices/tabs/preview-tab-promotion.test.ts": 33,
|
||||
"src/renderer/src/store/slices/tabs/structured-chat-close-owner.test.ts": 1418,
|
||||
"src/renderer/src/store/slices/tabs/tab-selection-contract.test.ts": 237,
|
||||
"src/renderer/src/store/slices/tabs/tabs-layout-owner-groups-order.test.ts": 1456,
|
||||
|
||||
@@ -29,7 +29,6 @@ export const UNIT_EXCLUDE = [
|
||||
'src/main/daemon/shell-ready.test.ts',
|
||||
'src/main/daemon/node-pty-fd-leak.test.ts',
|
||||
'src/main/providers/local-pty-shell-ready-zsh-launch-environment.test.ts',
|
||||
'src/main/providers/__tests__/shell-ready-framework-example.test.ts',
|
||||
'src/main/pty/omp-shell-wrapper-alias-safety.test.ts',
|
||||
'src/main/pty/omp-shell-wrapper.node-pty.test.ts',
|
||||
'src/main/shell-startup-feature-channel.test.ts',
|
||||
|
||||
@@ -6,7 +6,6 @@ export const SHELL_PREFIXES = [
|
||||
'src/main/daemon/daemon-shell-ready-wrapper',
|
||||
'src/main/daemon/node-pty-fd-leak',
|
||||
'src/main/providers/local-pty-shell-ready',
|
||||
'src/main/providers/__tests__/shell-ready-framework-example',
|
||||
'src/main/pty/',
|
||||
'src/main/shell-templates',
|
||||
'src/main/shell-startup-',
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
buildBrowserExtensionBackgroundWrapper,
|
||||
withBrowserExtensionApiConnectSource
|
||||
} from './browser-extension-background-wrapper'
|
||||
|
||||
const base = { manifest_version: 3, name: 'Vault', version: '1.0' }
|
||||
|
||||
describe('background wrapper', () => {
|
||||
it('loads the shim, then the classic worker, through importScripts', () => {
|
||||
const wrapper = buildBrowserExtensionBackgroundWrapper({
|
||||
...base,
|
||||
background: { service_worker: 'background.js' }
|
||||
})
|
||||
expect(wrapper?.wrapperPath).toBe('orca-background.js')
|
||||
expect(wrapper?.wrapperSource).toContain(
|
||||
'importScripts("/orca-extension-api.js", "/background.js");'
|
||||
)
|
||||
expect(wrapper?.manifest.background).toEqual({ service_worker: 'orca-background.js' })
|
||||
})
|
||||
|
||||
it('imports the shim, then the module worker, from beside the original', () => {
|
||||
const wrapper = buildBrowserExtensionBackgroundWrapper({
|
||||
...base,
|
||||
background: { service_worker: './build/service worker.js', type: 'module' }
|
||||
})
|
||||
expect(wrapper?.wrapperPath).toBe('build/orca-background.js')
|
||||
expect(wrapper?.wrapperSource).toContain(
|
||||
'import "/orca-extension-api.js";\nimport "/build/service%20worker.js";'
|
||||
)
|
||||
expect(wrapper?.manifest.background).toEqual({
|
||||
service_worker: 'build/orca-background.js',
|
||||
type: 'module'
|
||||
})
|
||||
})
|
||||
|
||||
it('drops the Firefox background.scripts key that Chromium warns about in MV3', () => {
|
||||
const wrapper = buildBrowserExtensionBackgroundWrapper({
|
||||
...base,
|
||||
background: {
|
||||
service_worker: 'assets/background.js',
|
||||
type: 'module',
|
||||
scripts: ['assets/background.js']
|
||||
}
|
||||
})
|
||||
expect(wrapper?.manifest.background).toEqual({
|
||||
service_worker: 'assets/orca-background.js',
|
||||
type: 'module'
|
||||
})
|
||||
})
|
||||
|
||||
it('leaves extensions without a background worker alone', () => {
|
||||
expect(buildBrowserExtensionBackgroundWrapper(base)).toBeNull()
|
||||
expect(buildBrowserExtensionBackgroundWrapper({ ...base, background: {} })).toBeNull()
|
||||
})
|
||||
|
||||
it('rejects Manifest V2 and workers outside the folder', () => {
|
||||
expect(() =>
|
||||
buildBrowserExtensionBackgroundWrapper({
|
||||
...base,
|
||||
manifest_version: 2,
|
||||
background: { scripts: ['bg.js'] }
|
||||
})
|
||||
).toThrow('Manifest V3')
|
||||
expect(() =>
|
||||
buildBrowserExtensionBackgroundWrapper({
|
||||
...base,
|
||||
background: { service_worker: '../outside.js' }
|
||||
})
|
||||
).toThrow('inside the extension folder')
|
||||
})
|
||||
|
||||
it('lets a restrictive extension-pages policy reach the API', () => {
|
||||
const wrapper = buildBrowserExtensionBackgroundWrapper({
|
||||
...base,
|
||||
background: { service_worker: 'bg.js' },
|
||||
content_security_policy: {
|
||||
extension_pages: "script-src 'self'; connect-src https://vault.example",
|
||||
sandbox: 'sandbox allow-scripts'
|
||||
}
|
||||
})
|
||||
expect(wrapper?.manifest.content_security_policy).toEqual({
|
||||
extension_pages:
|
||||
"script-src 'self'; connect-src https://vault.example 'self' orca-extension-api:",
|
||||
sandbox: 'sandbox allow-scripts'
|
||||
})
|
||||
expect(withBrowserExtensionApiConnectSource("default-src 'none'; script-src 'self'")).toBe(
|
||||
"default-src 'none'; script-src 'self'; connect-src 'self' orca-extension-api:"
|
||||
)
|
||||
expect(withBrowserExtensionApiConnectSource("script-src 'self'")).toBe("script-src 'self'")
|
||||
})
|
||||
})
|
||||
@@ -1,83 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { InstalledBrowserExtension } from '../../../shared/browser-extension-types'
|
||||
|
||||
const state = vi.hoisted(() => {
|
||||
const contents: unknown[] = []
|
||||
return { contents, enabled: new Map<string, string[]>() }
|
||||
})
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
webContents: { getAllWebContents: () => state.contents }
|
||||
}))
|
||||
vi.mock('./browser-extension-api-host', () => ({
|
||||
getBrowserExtensionProfileIdForSession: (session: { profileId: string }) => session.profileId
|
||||
}))
|
||||
vi.mock('../browser-session-registry', () => ({
|
||||
browserSessionRegistry: {
|
||||
getEnabledExtensionIds: (profileId: string) => state.enabled.get(profileId) ?? []
|
||||
}
|
||||
}))
|
||||
|
||||
const { notifyChromeWebStoreInstalled, notifyChromeWebStoreUninstalled } =
|
||||
await import('./browser-extension-webstore-events')
|
||||
|
||||
const extension: InstalledBrowserExtension = {
|
||||
id: 'a'.repeat(32),
|
||||
name: 'Example',
|
||||
version: '1',
|
||||
source: 'webstore',
|
||||
path: '/extensions/example',
|
||||
iconPath: null,
|
||||
permissions: [],
|
||||
hostPermissions: [],
|
||||
optionsPage: null
|
||||
}
|
||||
|
||||
function storePage(profileId: string, url = 'https://chromewebstore.google.com/') {
|
||||
const send = vi.fn()
|
||||
const contents = {
|
||||
isDestroyed: () => false,
|
||||
getType: () => 'webview',
|
||||
session: { profileId },
|
||||
mainFrame: { url, send }
|
||||
}
|
||||
state.contents.push(contents)
|
||||
return send
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
state.contents = []
|
||||
state.enabled.clear()
|
||||
})
|
||||
|
||||
describe('Chrome Web Store profile events', () => {
|
||||
it('reports an installation only to profiles where the extension is enabled', () => {
|
||||
const enabledPage = storePage('personal')
|
||||
const disabledPage = storePage('work')
|
||||
const unrelatedPage = storePage('personal', 'https://example.com/')
|
||||
state.enabled.set('personal', [extension.id])
|
||||
|
||||
notifyChromeWebStoreInstalled(extension)
|
||||
|
||||
expect(enabledPage).toHaveBeenCalledWith(expect.any(String), {
|
||||
event: 'onInstalled',
|
||||
args: [expect.objectContaining({ id: extension.id, enabled: true })]
|
||||
})
|
||||
expect(disabledPage).not.toHaveBeenCalled()
|
||||
expect(unrelatedPage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reports a global uninstall to store pages in every profile', () => {
|
||||
const personal = storePage('personal')
|
||||
const work = storePage('work')
|
||||
|
||||
notifyChromeWebStoreUninstalled(extension.id)
|
||||
|
||||
for (const send of [personal, work]) {
|
||||
expect(send).toHaveBeenCalledWith(expect.any(String), {
|
||||
event: 'onUninstalled',
|
||||
args: [extension.id]
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,94 +0,0 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { chmodSync, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, expect, it } from 'vitest'
|
||||
const FISH =
|
||||
['/opt/homebrew/bin/fish', '/usr/local/bin/fish', '/usr/bin/fish'].find(existsSync) ??
|
||||
'/usr/bin/fish'
|
||||
const roots: string[] = []
|
||||
afterEach(() => {
|
||||
roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true }))
|
||||
})
|
||||
function fishInit({ args }: { args: string[] | null }): string {
|
||||
const init = args?.[2]
|
||||
if (!init) {
|
||||
throw new Error('fish launch was not wrapped')
|
||||
}
|
||||
return init
|
||||
}
|
||||
async function composed() {
|
||||
const local = await import('../providers/local-pty-shell-ready')
|
||||
const daemon = await import('../daemon/shell-ready')
|
||||
const powershell = await import('../powershell-osc133-bootstrap')
|
||||
const localBash = await import('../providers/local-pty-shell-ready-bash-rcfile')
|
||||
const daemonBash = await import('../daemon/daemon-bash-shell-ready-rcfile')
|
||||
const zsh = await import('../zsh-startup-wrapper-builder')
|
||||
const localZsh = await import('../providers/local-pty-shell-ready-wrapper-fileset')
|
||||
const daemonZsh = await import('../daemon/daemon-zsh-shell-ready-wrapper-spec')
|
||||
return {
|
||||
zsh: [
|
||||
zsh.buildZshStartupHook(localZsh.getLocalZshWrapperSpec()),
|
||||
zsh.buildZshStartupHook(daemonZsh.getDaemonZshWrapperSpec())
|
||||
],
|
||||
bash: [
|
||||
localBash.getBashShellReadyRcfileContent(),
|
||||
daemonBash.getDaemonBashShellReadyRcfileContent()
|
||||
],
|
||||
fish: [
|
||||
fishInit(local.getShellLaunchConfig(FISH, ['ready'])),
|
||||
fishInit(daemon.getShellLaunchConfig(FISH, ['ready']))
|
||||
],
|
||||
powershell: powershell.getPowerShellOsc133Bootstrap()
|
||||
}
|
||||
}
|
||||
|
||||
it('gives every composed shell startup and the plain fish vendor snippet the claude function', async () => {
|
||||
const on = await composed()
|
||||
for (const text of [...on.bash, ...on.zsh]) {
|
||||
expect(text).toContain('ORCA_CLAUDE_INJECTED_CONFIG_DIR')
|
||||
}
|
||||
const { getFishVendorConfSnippet } = await import('../fish-xdg-data-dirs-handoff')
|
||||
const { getFishClaudeShellFunction } = await import('../../shared/claude-shell-function')
|
||||
expect(getFishClaudeShellFunction()).toContain('function claude')
|
||||
expect(getFishVendorConfSnippet()).toContain(getFishClaudeShellFunction())
|
||||
})
|
||||
|
||||
it('starts the PowerShell claude function on its own line after the codex fragment', async () => {
|
||||
const on = await composed()
|
||||
expect(on.powershell).toContain(
|
||||
'Remove-Variable orcaCodexCommand -ErrorAction SilentlyContinue\n$orcaClaudeCommand'
|
||||
)
|
||||
})
|
||||
|
||||
it.skipIf(!existsSync(FISH))(
|
||||
'defines both the ready hook and the claude function from the composed fish init',
|
||||
async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'claude-fish-compose-'))
|
||||
roots.push(root)
|
||||
mkdirSync(join(root, 'bin'))
|
||||
writeFileSync(join(root, 'bin', 'claude'), '#!/bin/sh\nexit 0\n')
|
||||
chmodSync(join(root, 'bin', 'claude'), 0o700)
|
||||
const on = await composed()
|
||||
for (const init of on.fish) {
|
||||
const result = spawnSync(
|
||||
'/usr/bin/env',
|
||||
[
|
||||
'-i',
|
||||
`HOME=${root}`,
|
||||
`PATH=${join(root, 'bin')}:/usr/bin:/bin`,
|
||||
`ORCA_CLAUDE_PROFILE_POINTER=${join(root, 'selected')}`,
|
||||
FISH,
|
||||
'--no-config',
|
||||
'-c',
|
||||
`test (command -s claude) = '${join(root, 'bin', 'claude')}'; or exit 97\n${init}\nfunctions -q claude; and echo CLAUDE-FN\nfunctions -q __orca_shell_ready_marker; and echo READY-HOOK`
|
||||
],
|
||||
{ encoding: 'utf8' }
|
||||
)
|
||||
expect(result.status).not.toBe(97)
|
||||
expect(result.stderr).toBe('')
|
||||
expect(result.stdout).toContain('CLAUDE-FN')
|
||||
expect(result.stdout).toContain('READY-HOOK')
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -1,99 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { CodexAppServerConnection } from './app-server/codex-app-server-connection-types'
|
||||
import { fetchCodexModelCatalogListing } from './codex-structured-model-catalog'
|
||||
|
||||
function modelRow(id: string, isDefault = false): Record<string, unknown> {
|
||||
return {
|
||||
model: id,
|
||||
displayName: id.toUpperCase(),
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: [
|
||||
{ reasoningEffort: 'medium' },
|
||||
{ reasoningEffort: 'high' },
|
||||
{ reasoningEffort: 'xhigh' }
|
||||
],
|
||||
defaultReasoningEffort: 'medium',
|
||||
isDefault
|
||||
}
|
||||
}
|
||||
|
||||
function connection(
|
||||
configRead: { answer: unknown } | { error: Error }
|
||||
): Pick<CodexAppServerConnection, 'request'> & { request: ReturnType<typeof vi.fn> } {
|
||||
return {
|
||||
request: vi.fn(async (method: string) => {
|
||||
if (method === 'model/list') {
|
||||
return { data: [modelRow('gpt-6-astra', true), modelRow('gpt-5.5')], nextCursor: null }
|
||||
}
|
||||
if (method === 'config/read') {
|
||||
if ('error' in configRead) {
|
||||
throw configRead.error
|
||||
}
|
||||
return configRead.answer
|
||||
}
|
||||
throw new Error(`unexpected ${method}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function defaults(models: { id: string; isDefault: boolean; defaultEffort?: string }[]) {
|
||||
return models.map(({ id, isDefault, defaultEffort }) => ({ id, isDefault, defaultEffort }))
|
||||
}
|
||||
|
||||
describe('Codex listing default follows the configured launch model', () => {
|
||||
it('marks the configured model and effort as what a launch runs', async () => {
|
||||
const listing = await fetchCodexModelCatalogListing({
|
||||
connection: connection({
|
||||
answer: { config: { model: 'gpt-5.5', model_reasoning_effort: 'xhigh' } }
|
||||
})
|
||||
})
|
||||
expect(defaults(listing.models)).toEqual([
|
||||
{ id: 'gpt-6-astra', isDefault: false, defaultEffort: 'medium' },
|
||||
{ id: 'gpt-5.5', isDefault: true, defaultEffort: 'xhigh' }
|
||||
])
|
||||
})
|
||||
|
||||
it('applies a configured effort to the listed default when no model is configured', async () => {
|
||||
const listing = await fetchCodexModelCatalogListing({
|
||||
connection: connection({
|
||||
answer: { config: { model: null, model_reasoning_effort: 'high' } }
|
||||
})
|
||||
})
|
||||
expect(defaults(listing.models)).toEqual([
|
||||
{ id: 'gpt-6-astra', isDefault: true, defaultEffort: 'high' },
|
||||
{ id: 'gpt-5.5', isDefault: false, defaultEffort: 'medium' }
|
||||
])
|
||||
})
|
||||
|
||||
it('names no default when the configured model is one the listing does not offer', async () => {
|
||||
const listing = await fetchCodexModelCatalogListing({
|
||||
connection: connection({
|
||||
answer: { config: { model: 'gpt-oss:20b', model_reasoning_effort: 'high' } }
|
||||
})
|
||||
})
|
||||
expect(defaults(listing.models)).toEqual([
|
||||
{ id: 'gpt-6-astra', isDefault: false, defaultEffort: 'medium' },
|
||||
{ id: 'gpt-5.5', isDefault: false, defaultEffort: 'medium' }
|
||||
])
|
||||
})
|
||||
|
||||
it('ignores a configured effort the listed default cannot run', async () => {
|
||||
const listing = await fetchCodexModelCatalogListing({
|
||||
connection: connection({ answer: { config: { model: null, model_reasoning_effort: 'max' } } })
|
||||
})
|
||||
expect(defaults(listing.models)).toEqual([
|
||||
{ id: 'gpt-6-astra', isDefault: true, defaultEffort: 'medium' },
|
||||
{ id: 'gpt-5.5', isDefault: false, defaultEffort: 'medium' }
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps the listing as Codex sent it when config/read is unavailable', async () => {
|
||||
const rpc = connection({ error: new Error('Method not found: config/read') })
|
||||
const listing = await fetchCodexModelCatalogListing({ connection: rpc })
|
||||
expect(rpc.request.mock.calls.map(([method]) => method)).toEqual(['model/list', 'config/read'])
|
||||
expect(defaults(listing.models)).toEqual([
|
||||
{ id: 'gpt-6-astra', isDefault: true, defaultEffort: 'medium' },
|
||||
{ id: 'gpt-5.5', isDefault: false, defaultEffort: 'medium' }
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -1,91 +0,0 @@
|
||||
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { gitExecFileAsyncMock } = vi.hoisted(() => ({
|
||||
gitExecFileAsyncMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('./runner', () => ({
|
||||
gitExecFileAsync: gitExecFileAsyncMock,
|
||||
gitExecFileAsyncBuffer: vi.fn(),
|
||||
gitOptionalLocksDisabledEnv: (env: NodeJS.ProcessEnv = process.env) => env,
|
||||
gitStreamStdout: vi.fn()
|
||||
}))
|
||||
|
||||
import {
|
||||
bulkDiscardChanges,
|
||||
bulkStageFiles,
|
||||
bulkUnstageFiles,
|
||||
discardChanges,
|
||||
stageFile,
|
||||
unstageFile
|
||||
} from './status'
|
||||
|
||||
const worktreePath = path.resolve('repo')
|
||||
const windowsRelativePath = 'tests\\breakgit'
|
||||
const wslOptions = { wslDistro: 'Ubuntu' }
|
||||
|
||||
describe('WSL git pathspecs', () => {
|
||||
beforeEach(() => {
|
||||
gitExecFileAsyncMock.mockReset()
|
||||
gitExecFileAsyncMock.mockResolvedValue({ stdout: 'tests/breakgit\0', stderr: '' })
|
||||
})
|
||||
|
||||
it('uses POSIX separators for tracked file mutations executed inside WSL', async () => {
|
||||
await stageFile(worktreePath, windowsRelativePath, wslOptions)
|
||||
await unstageFile(worktreePath, windowsRelativePath, wslOptions)
|
||||
await bulkStageFiles(worktreePath, [windowsRelativePath], wslOptions)
|
||||
await bulkUnstageFiles(worktreePath, [windowsRelativePath], wslOptions)
|
||||
await discardChanges(worktreePath, windowsRelativePath, wslOptions)
|
||||
await bulkDiscardChanges(worktreePath, [windowsRelativePath], wslOptions)
|
||||
|
||||
const pathspecs = gitExecFileAsyncMock.mock.calls.flatMap(([args]) =>
|
||||
(args as string[]).filter((arg) => arg.startsWith(':(literal)'))
|
||||
)
|
||||
expect(pathspecs).toEqual(Array(5).fill(':(literal)tests/breakgit'))
|
||||
const inputs = gitExecFileAsyncMock.mock.calls
|
||||
.map(([, options]) => options.stdin)
|
||||
.filter((input) => input !== undefined)
|
||||
expect(inputs).toEqual(Array(3).fill(':(literal)tests/breakgit\0'))
|
||||
})
|
||||
|
||||
it('uses POSIX separators when discarding untracked files inside WSL', async () => {
|
||||
const repo = await mkdtemp(path.join(tmpdir(), 'orca-wsl-pathspec-'))
|
||||
const targetPath = path.resolve(repo, windowsRelativePath)
|
||||
await mkdir(path.dirname(targetPath), { recursive: true })
|
||||
await writeFile(targetPath, 'untracked')
|
||||
|
||||
try {
|
||||
gitExecFileAsyncMock
|
||||
.mockRejectedValueOnce(new Error('not tracked'))
|
||||
.mockResolvedValue({ stdout: '', stderr: '' })
|
||||
await discardChanges(repo, windowsRelativePath, wslOptions)
|
||||
|
||||
expect(gitExecFileAsyncMock.mock.calls.map(([args]) => args)).toEqual([
|
||||
['ls-files', '--error-unmatch', '--', ':(literal)tests/breakgit'],
|
||||
['clean', '-ffdx', '--', ':(literal)tests/breakgit']
|
||||
])
|
||||
|
||||
gitExecFileAsyncMock.mockReset()
|
||||
gitExecFileAsyncMock.mockResolvedValue({ stdout: '', stderr: '' })
|
||||
await bulkDiscardChanges(repo, [windowsRelativePath], wslOptions)
|
||||
|
||||
expect(gitExecFileAsyncMock.mock.calls.map(([args]) => args)).toEqual([
|
||||
['ls-files', '-z', '--', ':(literal)tests/breakgit'],
|
||||
['clean', '-ffdx', '--', ':(literal)tests/breakgit']
|
||||
])
|
||||
} finally {
|
||||
await rm(repo, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves backslashes for host Git where they can be literal filename characters', async () => {
|
||||
await stageFile(worktreePath, windowsRelativePath)
|
||||
|
||||
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['add', '--', ':(literal)tests\\breakgit'], {
|
||||
cwd: worktreePath
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,83 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type * as WorktreePreparationLock from './worktree-preparation-lock'
|
||||
|
||||
type GitExec = (
|
||||
args: string[],
|
||||
options: Record<string, unknown>
|
||||
) => Promise<{ stdout: string; stderr: string }>
|
||||
|
||||
const gitExecFileAsyncMock = vi.hoisted(() => vi.fn<GitExec>())
|
||||
|
||||
vi.mock('./runner', () => ({ gitExecFileAsync: gitExecFileAsyncMock }))
|
||||
vi.mock('./worktree-preparation-lock', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof WorktreePreparationLock>()),
|
||||
verifyWorktreePreparationLock: vi.fn(async () => '/owned-lock'),
|
||||
verifyWorktreePreparationLockAtPath: vi.fn(),
|
||||
unlockWorktreePreparationAtPath: vi.fn()
|
||||
}))
|
||||
|
||||
import { addWorktree } from './worktree-add'
|
||||
import { listWorktreesSharedStrict } from './worktree-scan-cache'
|
||||
import { finalizePreparedWorktree } from './worktree-create-preparation'
|
||||
|
||||
const HEAD = 'a'.repeat(40)
|
||||
|
||||
function optionsForCommand(match: string): Record<string, unknown>[] {
|
||||
return gitExecFileAsyncMock.mock.calls
|
||||
.filter((call) => call[0].join(' ').includes(match))
|
||||
.map((call) => call[1])
|
||||
}
|
||||
|
||||
describe('worktree create admission tier', () => {
|
||||
beforeEach(() => {
|
||||
gitExecFileAsyncMock.mockReset().mockResolvedValue({ stdout: HEAD, stderr: '' })
|
||||
})
|
||||
|
||||
it('runs the create add at the tier the caller asked for', async () => {
|
||||
await addWorktree('/repo', '/repo-wt', 'feature', 'main', false, false, {
|
||||
admissionTier: 'interactive'
|
||||
})
|
||||
|
||||
const addOptions = optionsForCommand('worktree add')
|
||||
expect(addOptions).toHaveLength(1)
|
||||
expect(addOptions[0]).toMatchObject({
|
||||
cwd: '/repo',
|
||||
admissionTier: 'interactive'
|
||||
})
|
||||
})
|
||||
|
||||
it('runs the post-add listing at the tier the caller asked for', async () => {
|
||||
await listWorktreesSharedStrict('/repo', { admissionTier: 'interactive' })
|
||||
|
||||
const listOptions = optionsForCommand('worktree list')
|
||||
expect(listOptions.length).toBeGreaterThan(0)
|
||||
for (const options of listOptions) {
|
||||
expect(options).toMatchObject({ admissionTier: 'interactive' })
|
||||
}
|
||||
})
|
||||
|
||||
it('runs the prepared-checkout finalize at the tier the caller asked for', async () => {
|
||||
await finalizePreparedWorktree(
|
||||
'/repo',
|
||||
'/prepared',
|
||||
'/repo-wt',
|
||||
'feature',
|
||||
'main',
|
||||
false,
|
||||
{ admissionTier: 'interactive' },
|
||||
'owner'
|
||||
)
|
||||
|
||||
for (const match of ['worktree move', 'checkout --no-track']) {
|
||||
const options = optionsForCommand(match)
|
||||
expect(options, match).toHaveLength(1)
|
||||
expect(options[0], match).toMatchObject({ admissionTier: 'interactive' })
|
||||
}
|
||||
})
|
||||
|
||||
it('leaves a command with no tier at the scheduler default', async () => {
|
||||
await addWorktree('/repo', '/repo-wt', 'feature', 'main')
|
||||
|
||||
expect(optionsForCommand('worktree add')[0]).not.toHaveProperty('admissionTier')
|
||||
})
|
||||
})
|
||||
@@ -1,86 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type * as BackgroundDistroGate from '../wsl-background-distro-gate'
|
||||
import { fakeSpawnDispatch } from '../../shared/__fixtures__/fake-spawned-child'
|
||||
import type * as WslModule from '../wsl'
|
||||
|
||||
const { execFileSyncMock, spawnMock, getDefaultWslDistroMock } = vi.hoisted(() => ({
|
||||
execFileSyncMock: vi.fn(),
|
||||
spawnMock: vi.fn(),
|
||||
getDefaultWslDistroMock: vi.fn()
|
||||
}))
|
||||
|
||||
// The default-distro fallback asks whether the distro runs before it may start it (E9).
|
||||
vi.mock('../wsl-background-distro-gate', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof BackgroundDistroGate>()),
|
||||
readWslDistroRunningState: vi.fn(async () => 'running')
|
||||
}))
|
||||
|
||||
vi.mock('child_process', () => ({
|
||||
execFile: vi.fn(),
|
||||
execFileSync: execFileSyncMock,
|
||||
spawn: spawnMock
|
||||
}))
|
||||
|
||||
vi.mock('../wsl', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof WslModule>()),
|
||||
getDefaultWslDistro: getDefaultWslDistroMock
|
||||
}))
|
||||
|
||||
import { setDefaultWslDistroOverride } from '../git/runner'
|
||||
import { _resetKnownHostsCache, getGlabKnownHosts } from './gitlab-known-host-probe'
|
||||
|
||||
describe('glab known-hosts probe on Windows', () => {
|
||||
const originalPlatform = process.platform
|
||||
|
||||
const hostGlabMissingWslLoggedIn = (): void => {
|
||||
spawnMock.mockImplementation(
|
||||
fakeSpawnDispatch((program) =>
|
||||
program === 'wsl.exe'
|
||||
? { stdout: 'Logged in to gitlab.wsl.test as user' }
|
||||
: { spawnError: Object.assign(new Error('spawn glab ENOENT'), { code: 'ENOENT' }) }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
spawnMock.mockReset()
|
||||
getDefaultWslDistroMock.mockReset()
|
||||
getDefaultWslDistroMock.mockReturnValue('Ubuntu')
|
||||
setDefaultWslDistroOverride(null)
|
||||
_resetKnownHostsCache()
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
setDefaultWslDistroOverride(null)
|
||||
_resetKnownHostsCache()
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
|
||||
})
|
||||
|
||||
it('does not wake the default WSL distro for the native execution key', async () => {
|
||||
hostGlabMissingWslLoggedIn()
|
||||
|
||||
await expect(getGlabKnownHosts()).resolves.toEqual(['gitlab.com'])
|
||||
|
||||
expect(spawnMock).toHaveBeenCalledTimes(1)
|
||||
expect(spawnMock).toHaveBeenCalledWith(
|
||||
'glab',
|
||||
['auth', 'status'],
|
||||
expect.objectContaining({ cwd: undefined })
|
||||
)
|
||||
})
|
||||
|
||||
// Connection-keyed auth and API calls both use local glab, including its default-distro fallback.
|
||||
it('keeps the default-distro fallback for a connection execution key', async () => {
|
||||
hostGlabMissingWslLoggedIn()
|
||||
|
||||
await expect(getGlabKnownHosts('conn-1')).resolves.toEqual(['gitlab.com', 'gitlab.wsl.test'])
|
||||
|
||||
expect(spawnMock).toHaveBeenCalledWith(
|
||||
'wsl.exe',
|
||||
['-d', 'Ubuntu', '--exec', 'bash', '-c', "'glab' 'auth' 'status'"],
|
||||
// A concrete safe cwd avoids inheriting a deletable worktree's UNC directory during Windows spawn.
|
||||
expect.objectContaining({ cwd: expect.any(String) })
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,86 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { onMock } from './pty/pty-ipc-mock-registry'
|
||||
import { setupPtyIpcSuite } from './pty/pty-ipc-test-harness'
|
||||
import { registerPtyHandlers } from './pty/register-handlers'
|
||||
import { setLocalPtyProvider } from './pty/provider/registry'
|
||||
vi.mock('electron', () => import('./pty/pty-ipc-mock-registry').then((m) => m.electronModuleMock()))
|
||||
vi.mock('fs', () => import('./pty/pty-ipc-mock-registry').then((m) => m.fsModuleMock()))
|
||||
vi.mock('node-pty', () => import('./pty/pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock()))
|
||||
vi.mock('node:child_process', async (importOriginal) =>
|
||||
(await import('./pty/pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal())
|
||||
)
|
||||
vi.mock('../opencode/hook-service', () =>
|
||||
import('./pty/pty-ipc-mock-registry').then((m) => m.openCodeHookServiceModuleMock())
|
||||
)
|
||||
vi.mock('../mimo/hook-service', () =>
|
||||
import('./pty/pty-ipc-mock-registry').then((m) => m.mimoHookServiceModuleMock())
|
||||
)
|
||||
vi.mock('../agent-hooks/server', () =>
|
||||
import('./pty/pty-ipc-mock-registry').then((m) => m.agentHookServerModuleMock())
|
||||
)
|
||||
vi.mock('../pi/titlebar-extension-service', () =>
|
||||
import('./pty/pty-ipc-mock-registry').then((m) => m.piTitlebarExtensionModuleMock())
|
||||
)
|
||||
vi.mock('../pwsh', () => import('./pty/pty-ipc-mock-registry').then((m) => m.pwshModuleMock()))
|
||||
vi.mock('../wsl', async (importOriginal) =>
|
||||
(await import('./pty/pty-ipc-mock-registry')).wslModuleMock(await importOriginal())
|
||||
)
|
||||
vi.mock('../telemetry/client', () =>
|
||||
import('./pty/pty-ipc-mock-registry').then((m) => m.telemetryClientModuleMock())
|
||||
)
|
||||
vi.mock('../telemetry/classify-error', () =>
|
||||
import('./pty/pty-ipc-mock-registry').then((m) => m.classifyErrorModuleMock())
|
||||
)
|
||||
vi.mock('../cli/linux-terminal-orca-cli-shim', () =>
|
||||
import('./pty/pty-ipc-mock-registry').then((m) => m.linuxCliShimModuleMock())
|
||||
)
|
||||
vi.mock('../memory/pty-registry', () =>
|
||||
import('./pty/pty-ipc-mock-registry').then((m) => m.ptyRegistryModuleMock())
|
||||
)
|
||||
vi.mock('../agent-hooks/migration-unsupported-pty-state', () =>
|
||||
import('./pty/pty-ipc-mock-registry').then((m) => m.migrationUnsupportedPtyModuleMock())
|
||||
)
|
||||
vi.mock('../codex/codex-pane-account-registry', () =>
|
||||
import('./pty/pty-ipc-mock-registry').then((m) => m.codexPaneAccountRegistryModuleMock())
|
||||
)
|
||||
vi.mock('../codex/codex-state-db-backfill-recovery', () =>
|
||||
import('./pty/pty-ipc-mock-registry').then((m) => m.codexBackfillRecoveryModuleMock())
|
||||
)
|
||||
vi.mock('../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
|
||||
|
||||
const PANE = { worktreeId: 'repo::/wt', tabId: 'tab-1', leafId: 'leaf-1' }
|
||||
|
||||
describe('a painted cold-restore ack in the layout-owner build', () => {
|
||||
const { mainWindow } = setupPtyIpcSuite()
|
||||
|
||||
function ackWith(boundPtyId: string, args: unknown): ReturnType<typeof vi.fn> {
|
||||
const ackColdRestore = vi.fn()
|
||||
const isRestoredPaneBoundTo = (_pane: unknown, ptyId: string): boolean => ptyId === boundPtyId
|
||||
// Only the binding read matters here; every other runtime hook the handlers install is inert.
|
||||
const runtime = new Proxy(
|
||||
{},
|
||||
{ get: (_target, key) => (key === 'isRestoredPaneBoundTo' ? isRestoredPaneBoundTo : vi.fn()) }
|
||||
)
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the Proxy answers every runtime hook the handlers install.
|
||||
registerPtyHandlers(mainWindow, runtime as never)
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the ack listener calls only ackColdRestore.
|
||||
setLocalPtyProvider({ ackColdRestore } as never)
|
||||
const call = onMock.mock.calls.findLast((entry: unknown[]) => entry[0] === 'pty:ackColdRestore')
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: ipcMain.on registered this channel with an (event, args) listener.
|
||||
const listener = call?.[1] as (event: unknown, args: unknown) => void
|
||||
listener(null, args)
|
||||
return ackColdRestore
|
||||
}
|
||||
|
||||
it('releases the terminal service copy for the pane’s current terminal', () => {
|
||||
expect(ackWith('pty-1', { id: 'pty-1', pane: PANE })).toHaveBeenCalledWith('pty-1')
|
||||
})
|
||||
|
||||
it('keeps it when a stale view acks a terminal the pane no longer binds', () => {
|
||||
expect(ackWith('pty-new', { id: 'pty-old', pane: PANE })).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps it for an ack that names no pane', () => {
|
||||
expect(ackWith('pty-1', { id: 'pty-1' })).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,85 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import type { Store } from '../../../persistence/loading-store/store'
|
||||
import { runWorktreeChangeInvalidators } from '../../worktree-change-invalidators'
|
||||
import { registerSparseCheckoutCacheInvalidation } from './register-sparse-checkout-cache-invalidation'
|
||||
|
||||
const {
|
||||
clearSparseCheckoutStateCacheMock,
|
||||
clearSparseCheckoutStateCacheForRepoMock,
|
||||
onSparseCheckoutStateChangedMock,
|
||||
notifyWorktreesChangedMock
|
||||
} = vi.hoisted(() => ({
|
||||
clearSparseCheckoutStateCacheMock: vi.fn(),
|
||||
clearSparseCheckoutStateCacheForRepoMock: vi.fn(),
|
||||
onSparseCheckoutStateChangedMock: vi.fn(),
|
||||
notifyWorktreesChangedMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../../../git/worktree-sparse-checkout-cache', () => ({
|
||||
clearSparseCheckoutStateCache: clearSparseCheckoutStateCacheMock,
|
||||
clearSparseCheckoutStateCacheForRepo: clearSparseCheckoutStateCacheForRepoMock,
|
||||
onSparseCheckoutStateChanged: onSparseCheckoutStateChangedMock
|
||||
}))
|
||||
|
||||
vi.mock('../../worktree-remote', () => ({
|
||||
notifyWorktreesChanged: notifyWorktreesChangedMock
|
||||
}))
|
||||
|
||||
function makeStore(repos: Repo[]): Store {
|
||||
return {
|
||||
getRepo: (id: string) => repos.find((repo) => repo.id === id),
|
||||
getRepos: () => repos
|
||||
} as unknown as Store
|
||||
}
|
||||
|
||||
const mainWindow = {} as never
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('registerSparseCheckoutCacheInvalidation', () => {
|
||||
it('clears only the resolved repo`s cache when the invalidator registry fires', () => {
|
||||
const store = makeStore([{ id: 'repo-1', path: '/repo-1' } as Repo])
|
||||
const dispose = registerSparseCheckoutCacheInvalidation(mainWindow, store)
|
||||
try {
|
||||
runWorktreeChangeInvalidators('repo-1')
|
||||
expect(clearSparseCheckoutStateCacheForRepoMock).toHaveBeenCalledWith('/repo-1')
|
||||
expect(clearSparseCheckoutStateCacheMock).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('falls back to a full clear when the repo cannot be resolved', () => {
|
||||
const store = makeStore([])
|
||||
const dispose = registerSparseCheckoutCacheInvalidation(mainWindow, store)
|
||||
try {
|
||||
runWorktreeChangeInvalidators('unknown-repo')
|
||||
expect(clearSparseCheckoutStateCacheMock).toHaveBeenCalledTimes(1)
|
||||
expect(clearSparseCheckoutStateCacheForRepoMock).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('forwards a background stale-while-revalidate flip to the shared worktrees-changed notification', () => {
|
||||
const store = makeStore([{ id: 'repo-1', path: '/repo-1' } as Repo])
|
||||
const dispose = registerSparseCheckoutCacheInvalidation(mainWindow, store)
|
||||
try {
|
||||
const listener = onSparseCheckoutStateChangedMock.mock.calls.at(-1)?.[0]
|
||||
listener?.('/repo-1', '/repo-1/wt-a', true)
|
||||
expect(notifyWorktreesChangedMock).toHaveBeenCalledWith(mainWindow, 'repo-1')
|
||||
} finally {
|
||||
dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('drops the change listener on disposal so a stale window/store stops receiving flips', () => {
|
||||
const store = makeStore([{ id: 'repo-1', path: '/repo-1' } as Repo])
|
||||
const dispose = registerSparseCheckoutCacheInvalidation(mainWindow, store)
|
||||
dispose()
|
||||
expect(onSparseCheckoutStateChangedMock).toHaveBeenLastCalledWith(undefined)
|
||||
})
|
||||
})
|
||||
@@ -1,97 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const order = vi.hoisted(() => ({ calls: new Array<string>(), serving: true }))
|
||||
vi.mock('electron', () => ({
|
||||
BrowserWindow: { getAllWindows: () => [] },
|
||||
ipcMain: { removeHandler: vi.fn(), handle: vi.fn() }
|
||||
}))
|
||||
vi.mock('../wsl/wsl-host-gate', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
wslHostsServing: () => order.serving
|
||||
}))
|
||||
vi.mock('../../shared/runtime-environment-wsl-host-store', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
reconcileWslHostBridgePorts: () => {
|
||||
order.calls.push('reconcile')
|
||||
return []
|
||||
}
|
||||
}))
|
||||
vi.mock('../wsl/wsl-host-connector', () => ({ createWslHostConnector: vi.fn() }))
|
||||
vi.mock('../wsl/wsl-host-e2e-fake-connector', () => ({
|
||||
readFakeWslHostsConfig: () => ({ serverFiles: 'present' }),
|
||||
createFakeWslHostConnector: () => {
|
||||
order.calls.push('connector')
|
||||
return {
|
||||
connect: async () => {
|
||||
order.calls.push('connect')
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
vi.mock('../wsl/wsl-orphan-holder-release', () => ({
|
||||
releaseUnwantedWslHolders: async () => {},
|
||||
wslHolderKeepPolicy: () => () => true
|
||||
}))
|
||||
vi.mock('./wsl-host-automation-wake-wiring', () => ({ createWslHostAutomationWake: () => null }))
|
||||
vi.mock('../wsl/wsl-host-service', () => ({
|
||||
WslHostService: class {
|
||||
connector: { connect: (distro: string) => Promise<void> } | null = null
|
||||
attach(connector: { connect: (distro: string) => Promise<void> }): void {
|
||||
this.connector = connector
|
||||
}
|
||||
handleConnectionChange(): void {}
|
||||
isKnownDistro(): boolean {
|
||||
return true
|
||||
}
|
||||
hasUserVisibleWork(): boolean {
|
||||
return false
|
||||
}
|
||||
// Startup's first bridge work: adopting holders, then any reconnect.
|
||||
async start(): Promise<void> {
|
||||
order.calls.push('start')
|
||||
await this.connector?.connect('Ubuntu')
|
||||
}
|
||||
}
|
||||
}))
|
||||
const instance = vi.hoisted((): { service: unknown } => ({ service: null }))
|
||||
vi.mock('../wsl/wsl-host-service-instance', () => ({
|
||||
getWslHostService: () => instance.service,
|
||||
setWslHostService: (service: unknown) => {
|
||||
instance.service = service
|
||||
},
|
||||
// The phone's gate is built beside the service; startup order is all this suite checks.
|
||||
setWslHostPhoneGate: () => {}
|
||||
}))
|
||||
|
||||
import { registerWslHostHandlers } from './wsl-host-handlers'
|
||||
|
||||
function register(): void {
|
||||
registerWslHostHandlers({
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: startup reads settings only through onSettingsChanged.
|
||||
store: { getSettings: () => ({}), onSettingsChanged: vi.fn() } as never,
|
||||
getUserDataPath: () => '/user-data',
|
||||
invalidateTransport: vi.fn()
|
||||
})
|
||||
}
|
||||
|
||||
// A crash between a pairing save's two writes hides the host unless its record is realigned first.
|
||||
describe('WSL host startup: the bridge-port reconcile', () => {
|
||||
afterEach(() => {
|
||||
order.calls = []
|
||||
order.serving = true
|
||||
instance.service = null
|
||||
})
|
||||
|
||||
it('realigns saved records before the connector exists or any bridge connects', async () => {
|
||||
register()
|
||||
await vi.waitFor(() => expect(order.calls).toContain('connect'))
|
||||
expect(order.calls).toEqual(['reconcile', 'connector', 'start', 'connect'])
|
||||
})
|
||||
|
||||
it('reads and writes no record while WSL hosts are not serving', () => {
|
||||
order.serving = false
|
||||
register()
|
||||
expect(order.calls).toEqual([])
|
||||
expect(instance.service).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,91 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type * as WslModule from '../wsl'
|
||||
import type * as WslRunningPathFilterModule from '../wsl-running-path-filter'
|
||||
|
||||
const MANAGED_HOME = '/tmp/orca-user-data/codex-runtime-home/home'
|
||||
|
||||
// Polling may read paths only; the launch-time getter mkdirSyncs and would block every tick.
|
||||
vi.mock('../codex/codex-home-paths', () => ({
|
||||
getOrcaManagedCodexHomePath: vi.fn(() => MANAGED_HOME),
|
||||
resolveOrcaManagedCodexHomePath: vi.fn(() => MANAGED_HOME)
|
||||
}))
|
||||
|
||||
// Disable WSL fallback so the host-roots test behaves identically on every platform.
|
||||
vi.mock('../wsl', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof WslModule>()),
|
||||
listRunningWslHomeDirsAsync: vi.fn(async () => [])
|
||||
}))
|
||||
vi.mock('../wsl-running-path-filter', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof WslRunningPathFilterModule>()),
|
||||
filterPathsToRunningWslDistrosAsync: vi.fn(async (paths: readonly string[]) => [...paths])
|
||||
}))
|
||||
|
||||
const scanned = vi.hoisted(() => ({ dirs: [] as string[] }))
|
||||
vi.mock('../ai-vault/session-scanner-discovery', () => ({
|
||||
walkSessionFiles: async (dir: string) => {
|
||||
scanned.dirs.push(dir)
|
||||
return []
|
||||
}
|
||||
}))
|
||||
|
||||
import { join } from 'node:path'
|
||||
import { getOrcaManagedCodexHomePath } from '../codex/codex-home-paths'
|
||||
import {
|
||||
configureHostReadableTranscriptPathSources,
|
||||
resetHostReadableTranscriptPathCacheForTests
|
||||
} from './host-readable-transcript-path'
|
||||
import { resolveSessionFilePath } from './session-file-resolver'
|
||||
|
||||
beforeEach(() => {
|
||||
resetHostReadableTranscriptPathCacheForTests()
|
||||
vi.mocked(getOrcaManagedCodexHomePath).mockClear()
|
||||
scanned.dirs = []
|
||||
})
|
||||
|
||||
describe('codex sessions roots', () => {
|
||||
it('scans the managed home without materializing it', async () => {
|
||||
await resolveSessionFilePath('codex', 'sess-1')
|
||||
|
||||
expect(scanned.dirs).toContain(join(MANAGED_HOME, 'sessions'))
|
||||
expect(vi.mocked(getOrcaManagedCodexHomePath)).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('scans per-account homes after the default roots, leaving WSL shares to their tier', async () => {
|
||||
const accountHome = '/home/u/.local/share/orca/codex-accounts/a1/home'
|
||||
configureHostReadableTranscriptPathSources({
|
||||
getAdditionalCodexHomePaths: () => [accountHome, '\\\\wsl.localhost\\Ubuntu\\home\\u\\.codex']
|
||||
})
|
||||
|
||||
await resolveSessionFilePath('codex', 'sess-1')
|
||||
|
||||
const accountDir = join(accountHome, 'sessions')
|
||||
expect(scanned.dirs).toContain(accountDir)
|
||||
expect(scanned.dirs.indexOf(accountDir)).toBeGreaterThan(
|
||||
scanned.dirs.indexOf(join(MANAGED_HOME, 'sessions'))
|
||||
)
|
||||
expect(scanned.dirs.some((dir) => dir.startsWith('\\\\'))).toBe(false)
|
||||
})
|
||||
|
||||
it('leaves per-account homes out when the caller names the roots', async () => {
|
||||
const getAdditionalCodexHomePaths = vi.fn(() => ['/home/u/acct/home'])
|
||||
configureHostReadableTranscriptPathSources({ getAdditionalCodexHomePaths })
|
||||
|
||||
await resolveSessionFilePath('codex', 'sess-1', { codexSessionsDirs: ['/pinned/sessions'] })
|
||||
|
||||
expect(scanned.dirs).toEqual(['/pinned/sessions'])
|
||||
expect(getAdditionalCodexHomePaths).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('still answers when the account homes cannot be listed', async () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
configureHostReadableTranscriptPathSources({
|
||||
getAdditionalCodexHomePaths: () => {
|
||||
throw new Error('EACCES')
|
||||
}
|
||||
})
|
||||
|
||||
await expect(resolveSessionFilePath('codex', 'sess-1')).resolves.toBe(null)
|
||||
expect(scanned.dirs).toContain(join(MANAGED_HOME, 'sessions'))
|
||||
warn.mockRestore()
|
||||
})
|
||||
})
|
||||
@@ -1,87 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { shellScriptTest } from './shell-ready-framework/shell-script-test'
|
||||
|
||||
const { getUserDataPathMock } = vi.hoisted(() => ({
|
||||
getUserDataPathMock: vi.fn<() => string>()
|
||||
}))
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
app: {
|
||||
getPath: (name: string) => {
|
||||
if (name === 'userData') {
|
||||
return getUserDataPathMock()
|
||||
}
|
||||
throw new Error(`unexpected app.getPath(${name})`)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
const hasZsh = process.platform !== 'win32' && spawnSync('which', ['zsh']).status === 0
|
||||
const describeIfZsh = hasZsh ? describe : describe.skip
|
||||
|
||||
describeIfZsh('shell-script-literal framework example', () => {
|
||||
let userDataPath: string
|
||||
|
||||
beforeEach(() => {
|
||||
userDataPath = mkdtempSync(join(tmpdir(), 'shell-test-userdata-'))
|
||||
getUserDataPathMock.mockReturnValue(userDataPath)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(userDataPath, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('discovers ZDOTDIR when .zshenv sources another file', async () => {
|
||||
const { stdout } = await shellScriptTest(
|
||||
`
|
||||
# Setup multi-file config
|
||||
mkdir -p ~/.config/zsh
|
||||
cat > ~/.config/zsh/env <<'EOF'
|
||||
export ZDOTDIR="$HOME/.config/zsh"
|
||||
EOF
|
||||
|
||||
cat > ~/.zshenv <<'EOF'
|
||||
source "$HOME/.config/zsh/env"
|
||||
EOF
|
||||
|
||||
# Run: check discovered ZDOTDIR
|
||||
zsh -c 'env | grep -E "^(ORCA_|ZDOTDIR|HOME)=" | sort'
|
||||
`,
|
||||
{ userDataPath }
|
||||
)
|
||||
|
||||
expect(stdout).toMatchInlineSnapshot(`
|
||||
"HOME=<HOME>
|
||||
ZDOTDIR=<HOME>/.config/zsh
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
it('handles conditional ZDOTDIR based on SSH_CONNECTION', async () => {
|
||||
const { stdout } = await shellScriptTest(
|
||||
`
|
||||
mkdir -p ~/.config/zsh-local ~/.config/zsh-remote
|
||||
cat > ~/.zshenv <<'EOF'
|
||||
if [[ -n "$SSH_CONNECTION" ]]; then
|
||||
export ZDOTDIR="$HOME/.config/zsh-remote"
|
||||
else
|
||||
export ZDOTDIR="$HOME/.config/zsh-local"
|
||||
fi
|
||||
EOF
|
||||
|
||||
# Run with SSH_CONNECTION set
|
||||
SSH_CONNECTION='192.168.1.100 52100 192.168.1.1 22' zsh -c 'env | grep ZDOTDIR | sort'
|
||||
`,
|
||||
{ userDataPath }
|
||||
)
|
||||
|
||||
expect(stdout).toMatchInlineSnapshot(`
|
||||
"ZDOTDIR=<HOME>/.config/zsh-remote
|
||||
"
|
||||
`)
|
||||
})
|
||||
})
|
||||
@@ -1,82 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { OrcaRuntimeService } from '../../orca-runtime'
|
||||
import { denyRpcMethodForCaller } from '../rpc-caller-scope'
|
||||
import { eraseRpcMethods } from '../core'
|
||||
import { AUTOMATIC_WORK_METHODS } from './automatic-work'
|
||||
import type { RpcContext } from '../core'
|
||||
import type { RuntimePtyController } from '../../runtime-pty-controller-contract'
|
||||
|
||||
function context(runtime: OrcaRuntimeService): RpcContext {
|
||||
return { runtime }
|
||||
}
|
||||
|
||||
function method(name: string) {
|
||||
const found = eraseRpcMethods(AUTOMATIC_WORK_METHODS).find((candidate) => candidate.name === name)
|
||||
if (!found) {
|
||||
throw new Error(`Missing method ${name}`)
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
function runtimeWithController(
|
||||
controller: Pick<RuntimePtyController, 'getAutomaticWorkStatus' | 'resumeAutomaticWork'>
|
||||
): OrcaRuntimeService {
|
||||
const runtime = new OrcaRuntimeService(null)
|
||||
runtime.setPtyController({
|
||||
spawn: vi.fn(),
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null,
|
||||
...controller
|
||||
})
|
||||
return runtime
|
||||
}
|
||||
|
||||
describe('runtime.automaticWork RPC methods', () => {
|
||||
it('resumes through the local terminal owner and reports what it cleared', async () => {
|
||||
const cleared = { kind: 'paused' as const, reason: 'unclean_host_death' as const, since: 5 }
|
||||
const resumeAutomaticWork = vi.fn(async () => ({
|
||||
admission: 'enforce' as const,
|
||||
mode: { kind: 'normal' as const },
|
||||
cleared
|
||||
}))
|
||||
const runtime = runtimeWithController({ resumeAutomaticWork })
|
||||
|
||||
await expect(
|
||||
method('runtime.automaticWork.resume').handler(undefined, context(runtime))
|
||||
).resolves.toEqual({
|
||||
automaticWork: { admission: 'enforce', mode: { kind: 'normal' }, cleared }
|
||||
})
|
||||
expect(resumeAutomaticWork).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('answers null when the local owner has no gate', async () => {
|
||||
const runtime = runtimeWithController({})
|
||||
await expect(
|
||||
method('runtime.automaticWork.status').handler(undefined, context(runtime))
|
||||
).resolves.toEqual({ automaticWork: null })
|
||||
await expect(
|
||||
method('runtime.automaticWork.resume').handler(undefined, context(runtime))
|
||||
).resolves.toEqual({ automaticWork: null })
|
||||
})
|
||||
|
||||
it('says when the running terminal host predates resume instead of claiming no gate', async () => {
|
||||
const runtime = runtimeWithController({
|
||||
resumeAutomaticWork: async () => 'terminal-host-predates-resume' as const
|
||||
})
|
||||
await expect(
|
||||
method('runtime.automaticWork.resume').handler(undefined, context(runtime))
|
||||
).resolves.toEqual({ automaticWork: null, terminalHostPredatesResume: true })
|
||||
})
|
||||
|
||||
it('lets a paired desktop resume but never a phone', () => {
|
||||
const resume = method('runtime.automaticWork.resume')
|
||||
expect(resume.permission).toBe('host-admin')
|
||||
expect(
|
||||
denyRpcMethodForCaller({ kind: 'runtime-paired', grants: [] }, resume.name, resume.permission)
|
||||
).toBeNull()
|
||||
expect(
|
||||
denyRpcMethodForCaller({ kind: 'mobile' }, resume.name, resume.permission)
|
||||
).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,87 +0,0 @@
|
||||
/** Runtime removals must clear the same per-id process state and persisted scan snapshots as desktop IPC removals. */
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
unregisterWorktree: vi.fn(),
|
||||
pruneCleanupScan: vi.fn(async () => {}),
|
||||
pruneSpaceAnalysis: vi.fn(async () => {})
|
||||
}))
|
||||
|
||||
vi.mock('../localhost-worktree-label-proxy', () => ({
|
||||
localhostWorktreeLabelProxy: { unregisterWorktree: mocks.unregisterWorktree }
|
||||
}))
|
||||
vi.mock('../workspace-cleanup-scan-snapshot', () => ({
|
||||
pruneWorkspaceCleanupScanSnapshot: mocks.pruneCleanupScan
|
||||
}))
|
||||
vi.mock('../workspace-space-analysis-snapshot', () => ({
|
||||
pruneWorkspaceSpaceAnalysisSnapshot: mocks.pruneSpaceAnalysis
|
||||
}))
|
||||
|
||||
import type { Repo } from '../../shared/repo-types'
|
||||
import { OrcaRuntimeService } from './orca-runtime'
|
||||
|
||||
const WT = 'repo-1::/tmp/worktree-a'
|
||||
|
||||
function repo(overrides: Partial<Repo> = {}): Repo {
|
||||
return {
|
||||
id: 'repo-1',
|
||||
path: '/tmp/repo',
|
||||
displayName: 'repo',
|
||||
badgeColor: 'blue',
|
||||
addedAt: 1,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function createRuntime(repos: Repo[]) {
|
||||
const store = {
|
||||
getRepo: () => repos[0],
|
||||
getRepos: () => repos,
|
||||
addRepo: () => {},
|
||||
updateRepo: () => undefined,
|
||||
getAllWorktreeMeta: () => ({}),
|
||||
getWorktreeMeta: () => undefined,
|
||||
removeWorktreeMeta: () => {},
|
||||
getGitHubCache: () => ({ pr: {}, issue: {} }),
|
||||
setWorktreeMeta: () => undefined,
|
||||
getRetiredWorktreeNameRegistry: () => ({ exhaustedTiers: 0, names: [] }),
|
||||
addRetiredWorktreeName: () => {},
|
||||
mergeRetiredWorktreeNames: () => false,
|
||||
getProfileStorageDirectory: () => '/tmp/profile',
|
||||
getSettings: () => ({
|
||||
workspaceDir: '/tmp/workspaces',
|
||||
nestWorkspaces: false,
|
||||
refreshLocalBaseRefOnWorktreeCreate: false,
|
||||
branchPrefix: 'none',
|
||||
branchPrefixCustom: ''
|
||||
})
|
||||
}
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a partial store covering every method this removal path reads.
|
||||
const runtimeStore = store as never
|
||||
return { runtime: new OrcaRuntimeService(runtimeStore), store: runtimeStore }
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('runtime worktree removal cleanup', () => {
|
||||
it('drops label routes and persisted scan snapshots', () => {
|
||||
const { runtime, store } = createRuntime([repo()])
|
||||
|
||||
runtime.removeWorktreeMetadataAndHistory(store, WT, 'local')
|
||||
|
||||
expect(mocks.unregisterWorktree).toHaveBeenCalledWith(WT)
|
||||
expect(mocks.pruneCleanupScan).toHaveBeenCalledWith('/tmp/profile', WT, 'local')
|
||||
expect(mocks.pruneSpaceAnalysis).toHaveBeenCalledWith('/tmp/profile', WT, 'local')
|
||||
})
|
||||
|
||||
it('keeps a surviving same-id twin its process state but prunes only this host snapshot', () => {
|
||||
const { runtime, store } = createRuntime([repo(), repo({ connectionId: 'box' })])
|
||||
|
||||
runtime.removeWorktreeMetadataAndHistory(store, WT, 'local')
|
||||
|
||||
expect(mocks.unregisterWorktree).not.toHaveBeenCalled()
|
||||
expect(mocks.pruneCleanupScan).toHaveBeenCalledWith('/tmp/profile', WT, 'local')
|
||||
})
|
||||
})
|
||||
@@ -1,87 +0,0 @@
|
||||
// Runs on plain Node, as orcad does: facts come from the host's persisted PR cache.
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { OrcaRuntimeService } from '../orca-runtime'
|
||||
import type { PRInfo } from '../../../shared/github/pull-request-types'
|
||||
import type { RuntimeStore } from '../runtime-store-contract'
|
||||
import type { ResolvedWorktreeSnapshot } from '../runtime-resolved-worktree-cache'
|
||||
import type { ResolvedWorktree } from '../runtime-worktree-path-identity'
|
||||
|
||||
vi.mock('../../../shared/sidebar-inputs-switch', () => ({ ORCA_SIDEBAR_INPUTS_IS_MAIN: true }))
|
||||
|
||||
function withSidebarFacts(runtime: OrcaRuntimeService) {
|
||||
const facts = runtime.sidebarFacts
|
||||
if (!facts) {
|
||||
throw new Error('built without the sidebar facts layer')
|
||||
}
|
||||
return facts
|
||||
}
|
||||
|
||||
const repo = { id: 'repo-1', path: '/r', displayName: 'r', badgeColor: 'blue', addedAt: 1 }
|
||||
|
||||
function worktree(id: string, branch: string): ResolvedWorktree {
|
||||
return {
|
||||
id,
|
||||
repoId: repo.id,
|
||||
path: id,
|
||||
branch,
|
||||
head: '',
|
||||
isBare: false,
|
||||
isMainWorktree: false,
|
||||
displayName: id,
|
||||
comment: '',
|
||||
linkedIssue: null,
|
||||
linkedPR: null,
|
||||
linkedLinearIssue: null,
|
||||
linkedGitLabMR: null,
|
||||
linkedGitLabIssue: null,
|
||||
isArchived: false,
|
||||
isUnread: false,
|
||||
isPinned: false,
|
||||
sortOrder: 0,
|
||||
lastActivityAt: 0,
|
||||
parentWorktreeId: null,
|
||||
childWorktreeIds: [],
|
||||
lineage: null,
|
||||
git: { path: id, head: '', branch, isBare: false, isMainWorktree: false }
|
||||
}
|
||||
}
|
||||
|
||||
describe('getWorkspaceReviewFacts', () => {
|
||||
it('keys the persisted PR cache by workspace row', async () => {
|
||||
const merged: PRInfo = {
|
||||
number: 3,
|
||||
title: 'Ship it',
|
||||
state: 'merged',
|
||||
url: 'https://github.com/o/r/pull/3',
|
||||
checksStatus: 'success',
|
||||
updatedAt: '2026-10-17T00:00:00Z',
|
||||
mergeable: 'UNKNOWN'
|
||||
}
|
||||
const pr = { 'repo-1::a': { data: merged, fetchedAt: 1 } }
|
||||
const store: Pick<RuntimeStore, 'getRepos' | 'getGitHubCache'> = {
|
||||
getRepos: () => [repo],
|
||||
getGitHubCache: () => ({ pr, issue: {} })
|
||||
}
|
||||
class Probe extends OrcaRuntimeService {
|
||||
protected override async listResolvedWorktreeSnapshot(): Promise<ResolvedWorktreeSnapshot> {
|
||||
return {
|
||||
worktrees: [worktree('repo-1::/r/a', 'refs/heads/a'), worktree('repo-1::/r/b', 'b')],
|
||||
platformByRepoId: new Map()
|
||||
}
|
||||
}
|
||||
}
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the read touches only repos and the GitHub cache.
|
||||
const runtime = withSidebarFacts(new Probe(store as RuntimeStore))
|
||||
await expect(runtime.getWorkspaceReviewFacts()).resolves.toEqual({
|
||||
facts: { '|repo-1::/r/a': { provider: 'github', reviewState: 'merged' } }
|
||||
})
|
||||
})
|
||||
|
||||
it('publishes nothing without a store', async () => {
|
||||
await expect(
|
||||
withSidebarFacts(new OrcaRuntimeService()).getWorkspaceReviewFacts()
|
||||
).resolves.toEqual({
|
||||
facts: {}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,84 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { loadWindowsSecurity, setIsolatedSpawner } = vi.hoisted(() => ({
|
||||
loadWindowsSecurity: vi.fn(),
|
||||
setIsolatedSpawner: vi.fn()
|
||||
}))
|
||||
vi.mock('../../shared/windows-security-addon', () => ({ loadWindowsSecurity }))
|
||||
vi.mock('@orca/process-host/isolated-detached-spawn', () => ({ setIsolatedSpawner }))
|
||||
|
||||
import { installWindowsIsolatedSpawner } from './windows-isolated-spawner'
|
||||
|
||||
function addonWith(hasNativeExport: (name: string) => boolean) {
|
||||
const spawnDetachedIsolated = vi.fn()
|
||||
return {
|
||||
spawnDetachedIsolated,
|
||||
security: { userSid: 'S-1-5-18', addon: { hasNativeExport, spawnDetachedIsolated } }
|
||||
}
|
||||
}
|
||||
|
||||
describe('installWindowsIsolatedSpawner', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
loadWindowsSecurity.mockReset()
|
||||
setIsolatedSpawner.mockReset()
|
||||
})
|
||||
|
||||
it('is a no-op off Windows', () => {
|
||||
installWindowsIsolatedSpawner('darwin')
|
||||
|
||||
expect(loadWindowsSecurity).not.toHaveBeenCalled()
|
||||
expect(setIsolatedSpawner).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('installs the native spawner when the binary exports it', () => {
|
||||
const { security, spawnDetachedIsolated } = addonWith(() => true)
|
||||
loadWindowsSecurity.mockReturnValue(security)
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
installWindowsIsolatedSpawner('win32')
|
||||
|
||||
expect(setIsolatedSpawner).toHaveBeenCalledWith(spawnDetachedIsolated)
|
||||
expect(warn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('still installs a pre-breakaway spawner for its handle isolation, and logs it', () => {
|
||||
const { security, spawnDetachedIsolated } = addonWith(
|
||||
(name) => name === 'spawnDetachedIsolated'
|
||||
)
|
||||
loadWindowsSecurity.mockReturnValue(security)
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
installWindowsIsolatedSpawner('win32')
|
||||
|
||||
expect(setIsolatedSpawner).toHaveBeenCalledWith(spawnDetachedIsolated)
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
'[startup] detached spawns stay in our jobs; the addon build predates breakaway'
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps libuv and logs when the binary is stale', () => {
|
||||
loadWindowsSecurity.mockReturnValue(addonWith(() => false).security)
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
installWindowsIsolatedSpawner('win32')
|
||||
|
||||
expect(setIsolatedSpawner).not.toHaveBeenCalled()
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
'[startup] detached spawns inherit handles; the addon build is stale'
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps libuv and logs when the addon is unavailable or throws', () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
loadWindowsSecurity.mockReturnValueOnce({ error: 'missing binary' })
|
||||
loadWindowsSecurity.mockImplementationOnce(() => {
|
||||
throw new Error('boom')
|
||||
})
|
||||
|
||||
expect(() => installWindowsIsolatedSpawner('win32')).not.toThrow()
|
||||
expect(() => installWindowsIsolatedSpawner('win32')).not.toThrow()
|
||||
expect(setIsolatedSpawner).not.toHaveBeenCalled()
|
||||
expect(warn).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
@@ -1,96 +0,0 @@
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import { UNVERIFIABLE_ORCAD_TERMINAL_CENSUS } from '../../shared/orcad-terminal-census'
|
||||
import { encodePairingOffer } from '../../shared/pairing'
|
||||
import { createFakeWslHostConnector } from './wsl-host-e2e-fake-connector'
|
||||
|
||||
const pairingCode = encodePairingOffer({
|
||||
v: 2,
|
||||
endpoint: 'ws://127.0.0.1:42000',
|
||||
deviceToken: 'fake-token',
|
||||
publicKeyB64: Buffer.alloc(32, 7).toString('base64')
|
||||
})
|
||||
const runtime = {
|
||||
call: async () => ({}),
|
||||
census: async () => UNVERIFIABLE_ORCAD_TERMINAL_CENSUS
|
||||
}
|
||||
|
||||
let dir: string
|
||||
let distrosFile: string
|
||||
|
||||
function writeState(state: 'running' | 'stopped'): void {
|
||||
writeFileSync(distrosFile, JSON.stringify([{ name: 'Ubuntu', state, version: 2 }]))
|
||||
}
|
||||
|
||||
function connector(onChange = vi.fn(), stopSilently = false) {
|
||||
const fake = createFakeWslHostConnector(
|
||||
{ distros: [], distrosFile, pairingCode, stopSilently },
|
||||
onChange,
|
||||
{ isKnownDistro: () => true, runtime }
|
||||
)
|
||||
return { fake, onChange }
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
dir = mkdtempSync(join(tmpdir(), 'orca-fake-wsl-'))
|
||||
distrosFile = join(dir, 'distros.json')
|
||||
writeState('running')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('drops the bridge of a distro the test stops and refuses a no-boot reconnect', async () => {
|
||||
const { fake, onChange } = connector()
|
||||
await fake.connect('Ubuntu')
|
||||
expect(await fake.reachability('Ubuntu', 'wsl:ubuntu')).toBe('available')
|
||||
writeState('stopped')
|
||||
expect(await fake.reachability('Ubuntu', 'wsl:ubuntu')).toBe('stopped')
|
||||
expect(fake.get('Ubuntu')).toBeNull()
|
||||
// Matches production liveness, which reports the stop rather than a plain close.
|
||||
expect(onChange).toHaveBeenLastCalledWith('Ubuntu', null, {
|
||||
state: 'distro-stopped',
|
||||
terminals: 'exited',
|
||||
recovering: false
|
||||
})
|
||||
await expect(fake.connect('Ubuntu', { noBoot: true })).rejects.toThrow('Ubuntu is not running.')
|
||||
})
|
||||
|
||||
it('leaves a silent stop for a runtime call to report, as when liveness saw only a quick reboot', async () => {
|
||||
const { fake, onChange } = connector(vi.fn(), true)
|
||||
await fake.connect('Ubuntu')
|
||||
writeState('stopped')
|
||||
expect(await fake.reachability('Ubuntu', 'wsl:ubuntu')).toBe('stopped')
|
||||
expect(onChange).toHaveBeenLastCalledWith('Ubuntu', null, { state: 'closed' })
|
||||
})
|
||||
|
||||
it('still reports a plain close for an explicit disconnect of a running distro', async () => {
|
||||
const { fake, onChange } = connector()
|
||||
await fake.connect('Ubuntu')
|
||||
await fake.disconnect('Ubuntu')
|
||||
expect(onChange).toHaveBeenLastCalledWith('Ubuntu', null, { state: 'closed' })
|
||||
})
|
||||
|
||||
it('boots a stopped distro on a plain connect', async () => {
|
||||
const { fake } = connector()
|
||||
writeState('stopped')
|
||||
await fake.connect('Ubuntu')
|
||||
expect(JSON.parse(readFileSync(distrosFile, 'utf8'))).toEqual([
|
||||
{ name: 'Ubuntu', state: 'running', version: 2 }
|
||||
])
|
||||
expect(await fake.reachability('Ubuntu', 'wsl:ubuntu')).toBe('available')
|
||||
})
|
||||
|
||||
it('keeps a fixed listing always running', async () => {
|
||||
const fake = createFakeWslHostConnector(
|
||||
{ distros: [{ name: 'Legacy', state: 'stopped', version: 1 }], pairingCode },
|
||||
vi.fn(),
|
||||
{ isKnownDistro: () => true, runtime }
|
||||
)
|
||||
expect(await fake.reachability('Legacy', 'wsl:legacy')).toBe('unreachable')
|
||||
await fake.connect('Legacy', { noBoot: true })
|
||||
expect(await fake.reachability('Legacy', 'wsl:legacy')).toBe('available')
|
||||
})
|
||||
-86
@@ -1,86 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { ExecutionHostId } from '../../../../../../shared/execution-host'
|
||||
|
||||
const testState: { executionHostId: ExecutionHostId } = vi.hoisted(() => ({
|
||||
executionHostId: 'local'
|
||||
}))
|
||||
|
||||
vi.mock('@/store', () => ({ useAppStore: { getState: () => ({}) } }))
|
||||
vi.mock('@/lib/worktree-runtime-owner', () => ({
|
||||
getKnownExecutionHostIdForWorktree: () => testState.executionHostId
|
||||
}))
|
||||
|
||||
const { CombinedDiffFileTreeRow } = await import('./combined-diff-file-tree-row')
|
||||
const { readWorkspaceFileDragSource } = await import('@/lib/workspace-file-drag')
|
||||
|
||||
globalThis.IS_REACT_ACT_ENVIRONMENT = true
|
||||
|
||||
const roots: Root[] = []
|
||||
afterEach(() => {
|
||||
roots.splice(0).forEach((root) => act(() => root.unmount()))
|
||||
document.body.replaceChildren()
|
||||
testState.executionHostId = 'local'
|
||||
})
|
||||
|
||||
function renderRow(sourceWorkspaceId?: string): HTMLDivElement {
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
const root = createRoot(container)
|
||||
roots.push(root)
|
||||
act(() => {
|
||||
root.render(
|
||||
<CombinedDiffFileTreeRow
|
||||
node={{
|
||||
type: 'directory',
|
||||
key: 'dir::unstaged::src',
|
||||
path: 'src',
|
||||
name: 'src',
|
||||
depth: 0,
|
||||
area: 'unstaged',
|
||||
fileCount: 1,
|
||||
children: []
|
||||
}}
|
||||
mode="uncommitted"
|
||||
worktreePath="/repo/worktree"
|
||||
sourceWorkspaceId={sourceWorkspaceId}
|
||||
activeSectionKey={null}
|
||||
sectionIndexByKey={new Map()}
|
||||
isCollapsed={false}
|
||||
onToggleDirectory={() => {}}
|
||||
onNavigate={() => {}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
return container
|
||||
}
|
||||
|
||||
function dragRow(container: HTMLDivElement): DataTransfer {
|
||||
const transfer = new DataTransfer()
|
||||
const event = new Event('dragstart', { bubbles: true, cancelable: true })
|
||||
Object.defineProperty(event, 'dataTransfer', { value: transfer })
|
||||
act(() => {
|
||||
container.querySelector('[draggable="true"]')?.dispatchEvent(event)
|
||||
})
|
||||
return transfer
|
||||
}
|
||||
|
||||
describe('combined diff rows stamp their drag source', () => {
|
||||
// Entry lists are snapshots; drag paths must use the workspace’s live owner, matching source-control rows.
|
||||
it('stamps the live owner of the workspace the diff belongs to', () => {
|
||||
testState.executionHostId = 'runtime:env-1'
|
||||
expect(readWorkspaceFileDragSource(dragRow(renderRow('wt-1')))).toEqual({
|
||||
executionHostId: 'runtime:env-1',
|
||||
workspaceId: 'wt-1'
|
||||
})
|
||||
})
|
||||
|
||||
it('leaves the drag unstamped when the owner or the workspace is unknown', () => {
|
||||
expect(readWorkspaceFileDragSource(dragRow(renderRow(undefined)))).toBeNull()
|
||||
testState.executionHostId = 'runtime:unresolved-owner'
|
||||
expect(readWorkspaceFileDragSource(dragRow(renderRow('wt-1')))).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,85 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { AiVaultShowMoreSessionsRow } from './AiVaultShowMoreSessionsRow'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
it('steps the history depth up by one page once the scan filled it', async () => {
|
||||
const onSessionLimitChange = vi.fn()
|
||||
render(
|
||||
<AiVaultShowMoreSessionsRow
|
||||
loaded={500}
|
||||
loadedSessionLimit={500}
|
||||
loading={false}
|
||||
sessionLimit={500}
|
||||
onSessionLimitChange={onSessionLimitChange}
|
||||
/>
|
||||
)
|
||||
await userEvent.setup().click(screen.getByRole('button', { name: 'Show more sessions' }))
|
||||
expect(onSessionLimitChange).toHaveBeenCalledWith(750)
|
||||
})
|
||||
|
||||
it('stays put in a loading state while the deeper rescan runs', () => {
|
||||
render(
|
||||
<AiVaultShowMoreSessionsRow
|
||||
loaded={250}
|
||||
loadedSessionLimit={250}
|
||||
loading
|
||||
sessionLimit={500}
|
||||
onSessionLimitChange={vi.fn()}
|
||||
/>
|
||||
)
|
||||
const button = screen.getByRole('button', { name: 'Loading more sessions…' })
|
||||
expect(button.hasAttribute('disabled')).toBe(true)
|
||||
})
|
||||
|
||||
// Depth-minus-page is zero at the default, falsely claiming every foreground rescan loads deeper history.
|
||||
it('stays hidden during a foreground rescan the scan had room for', () => {
|
||||
render(
|
||||
<AiVaultShowMoreSessionsRow
|
||||
loaded={40}
|
||||
loadedSessionLimit={250}
|
||||
loading
|
||||
sessionLimit={250}
|
||||
onSessionLimitChange={vi.fn()}
|
||||
/>
|
||||
)
|
||||
expect(screen.queryByRole('button')).toBeNull()
|
||||
})
|
||||
|
||||
it('stays hidden while the scan has room or is already unlimited', () => {
|
||||
render(
|
||||
<AiVaultShowMoreSessionsRow
|
||||
loaded={12}
|
||||
loadedSessionLimit={250}
|
||||
loading={false}
|
||||
sessionLimit={250}
|
||||
onSessionLimitChange={vi.fn()}
|
||||
/>
|
||||
)
|
||||
render(
|
||||
<AiVaultShowMoreSessionsRow
|
||||
loaded={5000}
|
||||
loadedSessionLimit="unlimited"
|
||||
loading={false}
|
||||
sessionLimit="unlimited"
|
||||
onSessionLimitChange={vi.fn()}
|
||||
/>
|
||||
)
|
||||
expect(screen.queryByRole('button')).toBeNull()
|
||||
})
|
||||
|
||||
it('stays hidden until the first scan reports the depth it ran at', () => {
|
||||
render(
|
||||
<AiVaultShowMoreSessionsRow
|
||||
loaded={0}
|
||||
loadedSessionLimit={null}
|
||||
loading
|
||||
sessionLimit={250}
|
||||
onSessionLimitChange={vi.fn()}
|
||||
/>
|
||||
)
|
||||
expect(screen.queryByRole('button')).toBeNull()
|
||||
})
|
||||
-86
@@ -1,86 +0,0 @@
|
||||
/** @vitest-environment happy-dom */
|
||||
import { act } from 'react'
|
||||
import { runtimeTargetForOwnerEnvironment } from '@/runtime/runtime-client-target'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import { clearHugeRepoWarningDismissalsForTests } from '@/lib/source-control-huge-repo-warning-dismissals'
|
||||
import { useSourceControlStatusRefresh } from './use-status-refresh'
|
||||
|
||||
globalThis.IS_REACT_ACT_ENVIRONMENT = true
|
||||
|
||||
const actions = vi.hoisted(() => ({
|
||||
setGitStatus: vi.fn(),
|
||||
updateWorktreeGitIdentity: vi.fn(),
|
||||
setUpstreamStatus: vi.fn(),
|
||||
fetchUpstreamStatus: vi.fn()
|
||||
}))
|
||||
const warning = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: (select: (state: typeof actions) => unknown) => select(actions)
|
||||
}))
|
||||
vi.mock('sonner', () => ({ toast: { warning } }))
|
||||
vi.mock('@/lib/connection-context', () => ({ getConnectionId: () => null }))
|
||||
vi.mock('../../git-status-refresh', () => ({ refreshGitStatusForWorktree: vi.fn() }))
|
||||
vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, fallback: string) => fallback }))
|
||||
|
||||
const findHugeFoldersToIgnore = vi.fn()
|
||||
const appendGitignore = vi.fn()
|
||||
let root: Root | null = null
|
||||
let container: HTMLDivElement | null = null
|
||||
|
||||
beforeEach(() => {
|
||||
findHugeFoldersToIgnore.mockReset().mockResolvedValue(['dist'])
|
||||
appendGitignore.mockReset().mockResolvedValue(true)
|
||||
warning.mockReset()
|
||||
vi.stubGlobal('window', {
|
||||
...window,
|
||||
api: { git: { findHugeFoldersToIgnore, appendGitignore } }
|
||||
})
|
||||
container = document.createElement('div')
|
||||
root = createRoot(container)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root?.unmount())
|
||||
root = null
|
||||
container = null
|
||||
vi.unstubAllGlobals()
|
||||
clearHugeRepoWarningDismissalsForTests()
|
||||
})
|
||||
|
||||
async function renderOwner(
|
||||
runtimeEnvironmentId: string | null,
|
||||
connectionId: string | null = null
|
||||
) {
|
||||
function Probe() {
|
||||
useSourceControlStatusRefresh({
|
||||
activeRepoTarget: runtimeTargetForOwnerEnvironment(runtimeEnvironmentId),
|
||||
activeWorktreeId: 'repo::/same/path',
|
||||
worktreePath: '/same/path',
|
||||
isFolder: false,
|
||||
repositoryHuge: { limit: 1000 },
|
||||
activeConnectionId: connectionId
|
||||
})
|
||||
return null
|
||||
}
|
||||
await act(async () => root?.render(<Probe />))
|
||||
}
|
||||
|
||||
it('does not probe or offer a desktop ignore action for a managed owner without a legacy SSH ID', async () => {
|
||||
await renderOwner('managed-owner')
|
||||
expect(findHugeFoldersToIgnore).not.toHaveBeenCalled()
|
||||
expect(warning).not.toHaveBeenCalled()
|
||||
expect(appendGitignore).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps the existing local suggestion', async () => {
|
||||
await renderOwner(null)
|
||||
expect(findHugeFoldersToIgnore).toHaveBeenCalledExactlyOnceWith({ worktreePath: '/same/path' })
|
||||
expect(warning).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('keeps direct SSH targets out of the desktop helper', async () => {
|
||||
await renderOwner(null, 'ssh-owner')
|
||||
expect(findHugeFoldersToIgnore).not.toHaveBeenCalled()
|
||||
expect(warning).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -1,98 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { getDefaultSettings } from '../../../../shared/constants'
|
||||
import { AppearanceThemeSlotSelect } from './AppearanceThemeControl'
|
||||
|
||||
const themes = vi.hoisted((): { app: unknown[]; terminal: unknown[] } => ({
|
||||
app: [],
|
||||
terminal: []
|
||||
}))
|
||||
vi.mock('@/store/plugin-themes', () => ({
|
||||
usePluginThemes: () => themes.app,
|
||||
usePluginTerminalThemes: () => themes.terminal
|
||||
}))
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
themes.app = []
|
||||
themes.terminal = []
|
||||
})
|
||||
|
||||
const dusk = {
|
||||
id: 'plugin:samples.dusk/dusk',
|
||||
pluginKey: 'samples.dusk',
|
||||
revision: 'r1',
|
||||
pluginName: 'Dusk',
|
||||
label: 'Dusk',
|
||||
base: 'dark',
|
||||
tokens: { '--background': '#1b1430', '--foreground': '#e8e0ff' }
|
||||
}
|
||||
|
||||
it('lists only themes of its mode, grouped by plugin, and offers the plugin terminal colors', async () => {
|
||||
themes.app = [
|
||||
dusk,
|
||||
{ ...dusk, id: 'plugin:samples.dusk/dawn', label: 'Dawn', base: 'light' },
|
||||
{
|
||||
...dusk,
|
||||
id: 'plugin:acme.night/night',
|
||||
pluginKey: 'acme.night',
|
||||
pluginName: 'Acme',
|
||||
label: 'Night'
|
||||
}
|
||||
]
|
||||
themes.terminal = [
|
||||
{
|
||||
...dusk,
|
||||
mode: 'dark',
|
||||
terminal: { background: '#1b1430', foreground: '#e8e0ff', red: '#f00' }
|
||||
}
|
||||
]
|
||||
const updateSettings = vi.fn()
|
||||
render(
|
||||
<AppearanceThemeSlotSelect
|
||||
base="dark"
|
||||
label="Dark theme"
|
||||
settings={{
|
||||
...getDefaultSettings('/tmp'),
|
||||
pluginAppThemeDark: 'plugin:samples.dusk/dusk',
|
||||
terminalColorOverrides: { red: '#123456' }
|
||||
}}
|
||||
updateSettings={updateSettings}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByRole('combobox', { name: 'Dark theme' }).textContent).toContain('Dusk')
|
||||
screen.getByRole('button', { name: 'Use its terminal colors' }).click()
|
||||
expect(updateSettings).toHaveBeenCalledWith({
|
||||
terminalThemeDark: 'plugin:samples.dusk/dusk',
|
||||
terminalPluginPalettes: [
|
||||
{
|
||||
id: 'plugin:samples.dusk/dusk',
|
||||
colors: { background: '#1b1430', foreground: '#e8e0ff', red: '#f00' }
|
||||
}
|
||||
]
|
||||
})
|
||||
// Overrides would otherwise mask the palette the user just chose.
|
||||
expect(updateSettings.mock.calls[0]?.[0]).toHaveProperty('terminalColorOverrides', undefined)
|
||||
screen.getByRole('combobox', { name: 'Dark theme' }).click()
|
||||
expect(await screen.findByText('Acme')).toBeDefined()
|
||||
expect(screen.queryByRole('option', { name: 'Dawn' })).toBeNull()
|
||||
screen.getByRole('option', { name: 'Night' }).click()
|
||||
expect(updateSettings).toHaveBeenLastCalledWith({ pluginAppThemeDark: 'plugin:acme.night/night' })
|
||||
})
|
||||
|
||||
it('returns a slot to the built-in colors', async () => {
|
||||
themes.app = [dusk]
|
||||
const updateSettings = vi.fn()
|
||||
render(
|
||||
<AppearanceThemeSlotSelect
|
||||
base="dark"
|
||||
label="Dark theme"
|
||||
settings={{ ...getDefaultSettings('/tmp'), pluginAppThemeDark: 'plugin:samples.dusk/dusk' }}
|
||||
updateSettings={updateSettings}
|
||||
/>
|
||||
)
|
||||
screen.getByRole('combobox', { name: 'Dark theme' }).click()
|
||||
;(await screen.findByRole('option', { name: 'Built-in' })).click()
|
||||
expect(updateSettings).toHaveBeenCalledWith({ pluginAppThemeDark: null })
|
||||
})
|
||||
@@ -1,85 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { NumberField } from './SettingsFormControls'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
// Optional settings need a clear path back to automatic behavior (#10754).
|
||||
describe('NumberField clearable fields', () => {
|
||||
it('renders the placeholder and commits nothing while the value is unset', () => {
|
||||
render(
|
||||
<NumberField
|
||||
label="Minimum Contrast Ratio"
|
||||
description=""
|
||||
value={undefined}
|
||||
min={1}
|
||||
max={21}
|
||||
placeholder="Auto"
|
||||
onChange={vi.fn()}
|
||||
onClear={vi.fn()}
|
||||
/>
|
||||
)
|
||||
|
||||
const input = screen.getByLabelText('Minimum Contrast Ratio') as HTMLInputElement
|
||||
expect(input.value).toBe('')
|
||||
expect(input.getAttribute('placeholder')).toBe('Auto')
|
||||
})
|
||||
|
||||
it('clears the setting when the field is emptied', () => {
|
||||
const onChange = vi.fn()
|
||||
const onClear = vi.fn()
|
||||
render(
|
||||
<NumberField
|
||||
label="Minimum Contrast Ratio"
|
||||
description=""
|
||||
value={1}
|
||||
min={1}
|
||||
max={21}
|
||||
placeholder="Auto"
|
||||
onChange={onChange}
|
||||
onClear={onClear}
|
||||
/>
|
||||
)
|
||||
|
||||
const input = screen.getByLabelText('Minimum Contrast Ratio')
|
||||
fireEvent.change(input, { target: { value: '' } })
|
||||
fireEvent.blur(input)
|
||||
|
||||
expect(onClear).toHaveBeenCalledTimes(1)
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('still snaps back to the current value when the field is not clearable', () => {
|
||||
const onChange = vi.fn()
|
||||
render(<NumberField label="Font Size" description="" value={14} min={6} onChange={onChange} />)
|
||||
|
||||
const input = screen.getByLabelText('Font Size') as HTMLInputElement
|
||||
fireEvent.change(input, { target: { value: '' } })
|
||||
fireEvent.blur(input)
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
expect(input.value).toBe('14')
|
||||
})
|
||||
|
||||
it('clamps a committed value into the min/max window', () => {
|
||||
const onChange = vi.fn()
|
||||
render(
|
||||
<NumberField
|
||||
label="Minimum Contrast Ratio"
|
||||
description=""
|
||||
value={undefined}
|
||||
min={1}
|
||||
max={21}
|
||||
onChange={onChange}
|
||||
onClear={vi.fn()}
|
||||
/>
|
||||
)
|
||||
|
||||
const input = screen.getByLabelText('Minimum Contrast Ratio')
|
||||
fireEvent.change(input, { target: { value: '99' } })
|
||||
fireEvent.blur(input)
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(21)
|
||||
})
|
||||
})
|
||||
@@ -1,84 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { act, cleanup, renderHook } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getDefaultSettings } from '../../../../shared/constants'
|
||||
import {
|
||||
useSettingsInteractionController,
|
||||
type SettingsInteractionModel
|
||||
} from './use-settings-interaction-controller'
|
||||
import { useSettingsNavigationActions } from './settings-view-model'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
function createModel(shouldDiscard = false, dirty = true) {
|
||||
return {
|
||||
closeSettingsPage: vi.fn(),
|
||||
confirm: vi.fn(async () => shouldDiscard),
|
||||
hasUnsavedBranchPromptChanges: false,
|
||||
hasUnsavedCommitPromptChanges: false,
|
||||
hasUnsavedChatPromptChanges: dirty,
|
||||
highlightedSettingsTargetId: null,
|
||||
setFontSuggestions: vi.fn(),
|
||||
setHasUnsavedBranchPromptChanges: vi.fn(),
|
||||
setHasUnsavedCommitPromptChanges: vi.fn(),
|
||||
setHasUnsavedChatPromptChanges: vi.fn(),
|
||||
setHighlightedSettingsTargetId: vi.fn(),
|
||||
setSettingsSearchQuery: vi.fn(),
|
||||
setSourceControlAiPromptDiscardSignal: vi.fn(),
|
||||
settings: getDefaultSettings('/tmp'),
|
||||
updateSettingsOrThrow: vi.fn(async () => {}),
|
||||
activeSectionId: 'chat',
|
||||
setActiveSectionId: vi.fn(),
|
||||
setPendingNavRequestTick: vi.fn(),
|
||||
settingsSearchQuery: ''
|
||||
} satisfies SettingsInteractionModel & Parameters<typeof useSettingsNavigationActions>[0]
|
||||
}
|
||||
|
||||
function renderController(model: ReturnType<typeof createModel>) {
|
||||
return renderHook(() => {
|
||||
const interactions = useSettingsInteractionController(model)
|
||||
const navigation = useSettingsNavigationActions(model, interactions)
|
||||
return { ...interactions, ...navigation }
|
||||
})
|
||||
}
|
||||
|
||||
describe('Chat settings unsaved guard', () => {
|
||||
it('asks before closing with a dirty chat recipe and keeps the page open when cancelled', async () => {
|
||||
const model = createModel()
|
||||
const { result } = renderController(model)
|
||||
await act(async () => result.current.closeSettingsPageWithPromptGuard())
|
||||
expect(model.confirm).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ title: 'Discard unsaved AI settings?' })
|
||||
)
|
||||
expect(model.closeSettingsPage).not.toHaveBeenCalled()
|
||||
expect(model.setHasUnsavedChatPromptChanges).not.toHaveBeenCalled()
|
||||
expect(model.setSourceControlAiPromptDiscardSignal).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('clears the chat draft through the existing discard signal before closing', async () => {
|
||||
const model = createModel(true)
|
||||
const { result } = renderController(model)
|
||||
await act(async () => result.current.closeSettingsPageWithPromptGuard())
|
||||
expect(model.confirm).toHaveBeenCalledOnce()
|
||||
expect(model.setSourceControlAiPromptDiscardSignal).toHaveBeenCalledOnce()
|
||||
expect(model.setHasUnsavedChatPromptChanges).toHaveBeenCalledWith(false)
|
||||
expect(model.closeSettingsPage).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('guards settings navigation with the same dirty chat recipe', async () => {
|
||||
const model = createModel()
|
||||
const { result } = renderController(model)
|
||||
await act(async () => result.current.scrollToSection('git'))
|
||||
expect(model.confirm).toHaveBeenCalledOnce()
|
||||
expect(model.setActiveSectionId).not.toHaveBeenCalled()
|
||||
expect(model.setSourceControlAiPromptDiscardSignal).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('closes without asking when the recipe is clean', async () => {
|
||||
const model = createModel(false, false)
|
||||
const { result } = renderController(model)
|
||||
await act(async () => result.current.closeSettingsPageWithPromptGuard())
|
||||
expect(model.confirm).not.toHaveBeenCalled()
|
||||
expect(model.closeSettingsPage).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
@@ -1,92 +0,0 @@
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import type { ButtonHTMLAttributes, ReactNode } from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
state: {
|
||||
activeModal: 'confirm-orca-yaml-hooks' as string | null,
|
||||
modalData: {} as Record<string, unknown>,
|
||||
closeModal: vi.fn(),
|
||||
markOrcaHookScriptConfirmed: vi.fn(),
|
||||
markOrcaHookRepoAlwaysTrusted: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: Object.assign(
|
||||
(selector: (state: typeof mocks.state) => unknown) => selector(mocks.state),
|
||||
{
|
||||
getState: () => mocks.state
|
||||
}
|
||||
)
|
||||
}))
|
||||
|
||||
vi.mock('@/components/ui/dialog', () => ({
|
||||
Dialog: ({ open, children }: { open: boolean; children: ReactNode }) =>
|
||||
open ? <div>{children}</div> : null,
|
||||
DialogContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
DialogDescription: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
DialogFooter: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
DialogHeader: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
DialogTitle: ({ children }: { children: ReactNode }) => <div>{children}</div>
|
||||
}))
|
||||
|
||||
vi.mock('@/components/ui/button', () => ({
|
||||
Button: ({
|
||||
children,
|
||||
...props
|
||||
}: ButtonHTMLAttributes<HTMLButtonElement> & { children: ReactNode }) => (
|
||||
<button {...props}>{children}</button>
|
||||
)
|
||||
}))
|
||||
|
||||
function decodeHtml(html: string): string {
|
||||
return html
|
||||
.replace(/'/g, "'")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
}
|
||||
|
||||
describe('OrcaYamlTrustDialog', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.state.activeModal = 'confirm-orca-yaml-hooks'
|
||||
mocks.state.modalData = {
|
||||
repoId: 'repo-1',
|
||||
repoName: 'orca',
|
||||
scriptKind: 'setup',
|
||||
scriptContent: 'node config/scripts/run-internal-dev-setup.mjs\npnpm install',
|
||||
contentHash: 'hash-1',
|
||||
previouslyApproved: false
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps spaces around orca.yaml and the repo name in the first-run copy', async () => {
|
||||
const { default: OrcaYamlTrustDialog } = await import('./OrcaYamlTrustDialog')
|
||||
const text = decodeHtml(renderToStaticMarkup(<OrcaYamlTrustDialog />)).replace(/<[^>]+>/g, '')
|
||||
|
||||
expect(text).toContain("This repository's orca.yaml runs on your machine")
|
||||
expect(text).toContain('Only run if you trust orca.')
|
||||
expect(text).toContain('Always trust orca.yaml in orca')
|
||||
expect(text).not.toContain("repository'sorca.yaml")
|
||||
expect(text).not.toContain('trustorca')
|
||||
expect(text).not.toContain('trustorca.yaml')
|
||||
expect(text).not.toContain('inorca')
|
||||
})
|
||||
|
||||
it('keeps spaces around orca.yaml when the script changed since last approval', async () => {
|
||||
mocks.state.modalData = {
|
||||
...mocks.state.modalData,
|
||||
previouslyApproved: true
|
||||
}
|
||||
const { default: OrcaYamlTrustDialog } = await import('./OrcaYamlTrustDialog')
|
||||
const text = decodeHtml(renderToStaticMarkup(<OrcaYamlTrustDialog />)).replace(/<[^>]+>/g, '')
|
||||
|
||||
expect(text).toContain('orca.yaml changed since you last approved')
|
||||
expect(text).toContain('Always trust orca.yaml in orca')
|
||||
expect(text).not.toContain('Always trustorca.yaml')
|
||||
expect(text).not.toContain('inorca')
|
||||
})
|
||||
})
|
||||
@@ -1,92 +0,0 @@
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Tab } from '../../../../shared/tab-types'
|
||||
import type { WslMovedProject } from '../../../../shared/wsl-move/wsl-moved-project'
|
||||
|
||||
type MockState = {
|
||||
wslMovedProjects: WslMovedProject[]
|
||||
worktreesByRepo: Record<string, { id: string }[]>
|
||||
folderWorkspaces: { id: string }[]
|
||||
unifiedTabsByWorktree: Record<string, Partial<Tab>[]>
|
||||
activeWorktreeId: string | null
|
||||
activeGroupIdByWorktree: Record<string, string>
|
||||
groupsByWorktree: Record<string, { id: string; activeTabId: string | null }[]>
|
||||
}
|
||||
|
||||
let mockState: MockState
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: (selector: (state: MockState) => unknown) => selector(mockState)
|
||||
}))
|
||||
vi.mock('@/lib/wsl-moved-chat-activation', () => ({ activateWslMovedWorkspace: vi.fn() }))
|
||||
vi.mock('@/lib/wsl-moved-project-use-here', () => ({ bringWslMovedProjectBack: vi.fn() }))
|
||||
vi.mock('@/components/confirmation-dialog-context', () => ({
|
||||
useConfirmationDialog: () => vi.fn()
|
||||
}))
|
||||
|
||||
const { default: WslMovedProjectsSection } = await import('./WslMovedProjectsSection')
|
||||
const { TooltipProvider } = await import('@/components/ui/tooltip')
|
||||
|
||||
// The app shell provides the tooltip context the sidebar renders in.
|
||||
const render = (): string =>
|
||||
renderToStaticMarkup(
|
||||
<TooltipProvider>
|
||||
<WslMovedProjectsSection />
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
const MOVED = 'repo1::\\\\wsl.localhost\\Ubuntu\\home\\dev\\repo1'
|
||||
const chat = (id: string, label: string): Partial<Tab> => ({
|
||||
id,
|
||||
entityId: id,
|
||||
worktreeId: MOVED,
|
||||
contentType: 'agent-session',
|
||||
label,
|
||||
customLabel: null
|
||||
})
|
||||
|
||||
describe('WslMovedProjectsSection', () => {
|
||||
beforeEach(() => {
|
||||
mockState = {
|
||||
wslMovedProjects: [
|
||||
{ kind: 'repo', id: 'repo1', displayName: 'Repo 1', path: MOVED, distro: 'Ubuntu' }
|
||||
],
|
||||
worktreesByRepo: {},
|
||||
folderWorkspaces: [],
|
||||
unifiedTabsByWorktree: {
|
||||
[MOVED]: [chat('agent-session:a', 'Fix the parser'), chat('agent-session:b', 'Plan')]
|
||||
},
|
||||
activeWorktreeId: MOVED,
|
||||
activeGroupIdByWorktree: { [MOVED]: 'group-1' },
|
||||
groupsByWorktree: { [MOVED]: [{ id: 'group-1', activeTabId: 'agent-session:b' }] }
|
||||
}
|
||||
})
|
||||
|
||||
it('lists the moved project read-only, labeled with its host, with only its chats', () => {
|
||||
const markup = render()
|
||||
expect(markup).toContain('Repo 1')
|
||||
expect(markup).toContain('Moved to Ubuntu host')
|
||||
expect(markup).toContain('Fix the parser')
|
||||
// One button per chat plus "use here again": no new terminal, new chat, rename or menu.
|
||||
expect(markup.match(/<button/g)).toHaveLength(3)
|
||||
expect(markup.match(/data-testid="wsl-moved-chat"/g)).toHaveLength(2)
|
||||
expect(markup.match(/data-testid="wsl-moved-use-here"/g)).toHaveLength(1)
|
||||
expect(markup).toContain('aria-label="Use on this computer again"')
|
||||
expect(markup).toContain('data-current="true"')
|
||||
})
|
||||
|
||||
it('still lists a moved project without chats here, so it can be used here again', () => {
|
||||
mockState.unifiedTabsByWorktree = {
|
||||
[MOVED]: [{ ...chat('t', 'Shell'), contentType: 'terminal' }]
|
||||
}
|
||||
const markup = render()
|
||||
expect(markup).toContain('Moved to Ubuntu host')
|
||||
expect(markup.match(/data-testid="wsl-moved-chat"/g)).toBeNull()
|
||||
expect(markup.match(/data-testid="wsl-moved-use-here"/g)).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('renders nothing when no project is moved', () => {
|
||||
mockState.wslMovedProjects = []
|
||||
expect(render()).toBe('')
|
||||
})
|
||||
})
|
||||
@@ -1,86 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { TerminalKittyKeyboardModeTracker } from '../../../../shared/terminal-kitty-keyboard-mode-tracker'
|
||||
import { openStandaloneTerminal } from './open-standalone-terminal'
|
||||
|
||||
let emitData: (data: string) => void = () => {}
|
||||
let signalUserInput: () => void = () => {}
|
||||
|
||||
vi.mock('@xterm/xterm', () => ({
|
||||
Terminal: class {
|
||||
open(): void {}
|
||||
dispose(): void {}
|
||||
write(_data: string, done?: () => void): void {
|
||||
done?.()
|
||||
}
|
||||
onData(listener: (data: string) => void): { dispose: () => void } {
|
||||
emitData = listener
|
||||
return { dispose: () => {} }
|
||||
}
|
||||
}
|
||||
}))
|
||||
vi.mock('@xterm/xterm/css/xterm.css', () => ({}))
|
||||
vi.mock('@/components/terminal-pane/terminal-user-input-signal', () => ({
|
||||
subscribeToTerminalUserInput: (_terminal: unknown, listener: () => void) => {
|
||||
signalUserInput = listener
|
||||
return { dispose: () => {} }
|
||||
}
|
||||
}))
|
||||
vi.mock('@/components/terminal-pane/terminal-native-copy-gutter', () => ({
|
||||
installTerminalNativeCopyGutterTrim: () => ({ dispose: () => {} })
|
||||
}))
|
||||
vi.mock('../dashboard-popout/preview-terminal-compatibility', () => ({
|
||||
installPreviewTerminalCompatibility: () => () => {}
|
||||
}))
|
||||
vi.mock('../dashboard-popout/preview-terminal-ime-bridge', () => ({
|
||||
installPreviewImeBridge: () => ({ claimKeyEvent: () => false, dispose: () => {} })
|
||||
}))
|
||||
vi.mock('../dashboard-popout/preview-terminal-key-handler', () => ({
|
||||
installPreviewTerminalKeyHandler: () => () => {}
|
||||
}))
|
||||
vi.mock('../dashboard-popout/preview-terminal-app-menu-clipboard', () => ({
|
||||
installPreviewTerminalAppMenuClipboard: () => () => {}
|
||||
}))
|
||||
vi.mock('../dashboard-popout/preview-terminal-right-click-paste', () => ({
|
||||
installPreviewTerminalRightClickPaste: () => () => {}
|
||||
}))
|
||||
|
||||
function open(answersQueries: boolean): ReturnType<typeof vi.fn> {
|
||||
const sendInput = vi.fn(() => true)
|
||||
openStandaloneTerminal({
|
||||
container: document.createElement('div'),
|
||||
options: {},
|
||||
kittyKeyboardModes: new TerminalKittyKeyboardModeTracker(),
|
||||
sessionKey: 's',
|
||||
sendInput,
|
||||
answersQueries,
|
||||
getSettings: () => null,
|
||||
getMacOptionAsAlt: () => 'false',
|
||||
getTerminalInput: () => null
|
||||
})
|
||||
return sendInput
|
||||
}
|
||||
|
||||
const CURSOR_POSITION_REPLY = '\u001b[5;1R'
|
||||
|
||||
describe('standalone terminal input', () => {
|
||||
it('forwards the user’s typing either way', () => {
|
||||
const sendInput = open(false)
|
||||
signalUserInput()
|
||||
emitData('ls\r')
|
||||
expect(sendInput).toHaveBeenCalledWith('ls\r')
|
||||
})
|
||||
|
||||
it('leaves query replies to the real pane when it only mirrors one', () => {
|
||||
const sendInput = open(false)
|
||||
emitData(CURSOR_POSITION_REPLY)
|
||||
expect(sendInput).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('answers queries when it is the session’s only emulator', () => {
|
||||
const sendInput = open(true)
|
||||
emitData(CURSOR_POSITION_REPLY)
|
||||
expect(sendInput).toHaveBeenCalledWith(CURSOR_POSITION_REPLY)
|
||||
})
|
||||
})
|
||||
@@ -1,92 +0,0 @@
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
CURSOR_MODELS_BUCKET_NAME,
|
||||
CURSOR_OTHER_MODELS_BUCKET_NAME
|
||||
} from '../../../../shared/cursor-usage-buckets'
|
||||
import type { ProviderRateLimits, RateLimitWindow } from '../../../../shared/rate-limit-types'
|
||||
|
||||
vi.mock('@/i18n/i18n', () => ({
|
||||
i18n: { language: 'en' },
|
||||
translate: (_key: string, fallback: string, values?: Record<string, string>) => {
|
||||
let result = fallback
|
||||
for (const [key, value] of Object.entries(values ?? {})) {
|
||||
result = result.replace(`{{${key}}}`, value)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/agent-catalog', () => ({
|
||||
AgentIcon: () => null
|
||||
}))
|
||||
|
||||
vi.mock('../../store', () => ({
|
||||
useAppStore: (selector: (state: { usagePercentageDisplay: 'used' | 'remaining' }) => unknown) =>
|
||||
selector({ usagePercentageDisplay: 'used' })
|
||||
}))
|
||||
|
||||
function windowOf(usedPercent: number): RateLimitWindow {
|
||||
return { usedPercent, windowMinutes: 43_200, resetsAt: null, resetDescription: null }
|
||||
}
|
||||
|
||||
function cursorLimits(overrides: Partial<ProviderRateLimits> = {}): ProviderRateLimits {
|
||||
return {
|
||||
provider: 'cursor',
|
||||
session: null,
|
||||
weekly: null,
|
||||
monthly: windowOf(92),
|
||||
buckets: [
|
||||
{ name: CURSOR_MODELS_BUCKET_NAME, ...windowOf(45) },
|
||||
{ name: CURSOR_OTHER_MODELS_BUCKET_NAME, ...windowOf(50) }
|
||||
],
|
||||
updatedAt: Date.now(),
|
||||
error: null,
|
||||
status: 'ok',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('Cursor status-bar segment', () => {
|
||||
it('renders both plan pools instead of filtering them out as unknown buckets', async () => {
|
||||
// Cursor’s whole-meter pools must bypass Gemini’s experimental-model allowlist.
|
||||
const { ProviderSegment } = await import('./StatusBar')
|
||||
const markup = renderToStaticMarkup(
|
||||
<ProviderSegment p={cursorLimits()} compact={false} display="used" mode="verbose" />
|
||||
)
|
||||
expect(markup).toContain(CURSOR_MODELS_BUCKET_NAME)
|
||||
expect(markup).toContain(CURSOR_OTHER_MODELS_BUCKET_NAME)
|
||||
expect(markup).toContain('45%')
|
||||
expect(markup).toContain('50%')
|
||||
})
|
||||
|
||||
it('falls back to the plan total when no bucket is renderable', async () => {
|
||||
const { ProviderSegment } = await import('./StatusBar')
|
||||
const markup = renderToStaticMarkup(
|
||||
<ProviderSegment
|
||||
p={cursorLimits({ buckets: [{ name: 'Unknown pool', ...windowOf(70) }] })}
|
||||
compact={false}
|
||||
display="used"
|
||||
mode="verbose"
|
||||
/>
|
||||
)
|
||||
expect(markup).not.toContain('Unknown pool')
|
||||
expect(markup).toContain('92%')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getWindowSections for Cursor', () => {
|
||||
it('keeps the plan total beside the pools', async () => {
|
||||
// Cursor’s monthly plan total can be nearer the cap than individual buckets.
|
||||
const { getWindowSections } = await import('./tooltip')
|
||||
const sections = getWindowSections(cursorLimits())
|
||||
expect(sections.find((section) => section.label === 'Plan')?.window?.usedPercent).toBe(92)
|
||||
expect(sections.map((section) => section.label)).toContain(CURSOR_MODELS_BUCKET_NAME)
|
||||
})
|
||||
|
||||
it('leaves other providers unchanged when they report no monthly window', async () => {
|
||||
const { getWindowSections } = await import('./tooltip')
|
||||
const sections = getWindowSections(cursorLimits({ monthly: null, provider: 'gemini' }))
|
||||
expect(sections.some((section) => section.label === 'Plan')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,85 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import { TaskPageJiraIssueList } from '../../task-page-jira-issue-list'
|
||||
import { defaultJiraListColumnIds, visibleJiraListColumns } from '../../jira-list-columns'
|
||||
import type { JiraIssue } from '../../../../../shared/jira/jira-types'
|
||||
import { makeWorktree } from '@/store/slices/worktrees-slice-test-fixtures'
|
||||
|
||||
const issue: JiraIssue = {
|
||||
id: '1',
|
||||
key: 'APP-1',
|
||||
siteId: 'site-a',
|
||||
title: 'Fix login',
|
||||
url: 'https://acme.atlassian.net/browse/APP-1',
|
||||
project: { id: '1', key: 'APP', name: 'App' },
|
||||
issueType: { id: '1', name: 'Bug' },
|
||||
status: {
|
||||
id: '1',
|
||||
name: 'In Progress',
|
||||
categoryKey: 'indeterminate',
|
||||
categoryName: 'In Progress'
|
||||
},
|
||||
labels: [],
|
||||
createdAt: '2026-10-01',
|
||||
updatedAt: '2026-10-01'
|
||||
}
|
||||
const first = makeWorktree({ id: 'first', repoId: 'api', displayName: 'API fix' })
|
||||
const second = makeWorktree({ id: 'second', repoId: 'web', displayName: 'UI fix' })
|
||||
// The row receives a scoped lookup so the UI cannot accidentally match bare keys.
|
||||
const onOpenWorkspace = vi.fn()
|
||||
const onOpenIssue = vi.fn()
|
||||
const onStartWorkspace = vi.fn()
|
||||
function renderList(workspaces = [first, second]) {
|
||||
const props = {
|
||||
columns: visibleJiraListColumns(defaultJiraListColumnIds()),
|
||||
issues: [issue],
|
||||
selectedIssue: null,
|
||||
showSiteContext: false,
|
||||
statusOrder: null,
|
||||
formatUpdatedAt: () => 'Today',
|
||||
getStatusTone: () => '',
|
||||
onOpenIssue,
|
||||
onStartWorkspace,
|
||||
getIssueWorkspaces: () => workspaces,
|
||||
onOpenWorkspace
|
||||
}
|
||||
return render(
|
||||
<TooltipProvider>
|
||||
<TaskPageJiraIssueList {...props} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
describe('Jira linked workspace actions', () => {
|
||||
it('shows all linked workspaces and opens the selected one', async () => {
|
||||
renderList()
|
||||
const user = userEvent.setup()
|
||||
await user.click(screen.getByRole('button', { name: '2 workspaces linked to APP-1' }))
|
||||
await user.click(await screen.findByRole('menuitem', { name: /UI fix/ }))
|
||||
expect(onOpenWorkspace).toHaveBeenCalledWith(second)
|
||||
})
|
||||
it('offers existing and new workspaces when exactly one is linked', async () => {
|
||||
renderList([first])
|
||||
const user = userEvent.setup()
|
||||
await user.click(screen.getByRole('button', { name: '1 workspace linked to APP-1' }))
|
||||
expect(onOpenWorkspace).not.toHaveBeenCalled()
|
||||
expect(screen.getByRole('menuitem', { name: /API fix/ })).toBeInTheDocument()
|
||||
await user.click(screen.getByRole('menuitem', { name: 'Start new workspace' }))
|
||||
expect(onStartWorkspace).toHaveBeenCalledWith(issue)
|
||||
expect(onOpenIssue).not.toHaveBeenCalled()
|
||||
await user.click(screen.getByRole('button', { name: '1 workspace linked to APP-1' }))
|
||||
await user.click(screen.getByRole('menuitem', { name: /API fix/ }))
|
||||
expect(onOpenWorkspace).toHaveBeenCalledWith(first)
|
||||
})
|
||||
it('keeps the new-workspace action for an unlinked issue', () => {
|
||||
renderList([])
|
||||
expect(screen.getByRole('button', { name: 'Start workspace from APP-1' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -1,92 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { TaskPageRepoSourceState } from '@/components/task-page/task-page-cache-selectors'
|
||||
import { hasDivergentSources, hasUpstreamCandidateDivergence } from './task-page-draft-storage'
|
||||
|
||||
function state(
|
||||
sources: Partial<NonNullable<TaskPageRepoSourceState['sources']>>
|
||||
): TaskPageRepoSourceState {
|
||||
return {
|
||||
repoId: 'repo-1',
|
||||
repoPath: '/tmp/repo',
|
||||
hostId: 'local',
|
||||
sourceKey: 'repo-1',
|
||||
sources: {
|
||||
issues: null,
|
||||
prs: null,
|
||||
originCandidate: null,
|
||||
upstreamCandidate: null,
|
||||
...sources
|
||||
},
|
||||
error: null
|
||||
}
|
||||
}
|
||||
|
||||
describe('hasDivergentSources', () => {
|
||||
it('is true when issues and PRs point at different owner/repos', () => {
|
||||
expect(
|
||||
hasDivergentSources(
|
||||
state({
|
||||
issues: { owner: 'acme', repo: 'fork', host: 'github.com' },
|
||||
prs: { owner: 'acme', repo: 'orca', host: 'github.com' }
|
||||
})
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('is false when the PR source is missing', () => {
|
||||
expect(
|
||||
hasDivergentSources(
|
||||
state({
|
||||
issues: { owner: 'acme', repo: 'fork', host: 'github.com' },
|
||||
prs: null
|
||||
})
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('is false when issues and PRs share an owner/repo', () => {
|
||||
expect(
|
||||
hasDivergentSources(
|
||||
state({
|
||||
issues: { owner: 'acme', repo: 'orca', host: 'github.com' },
|
||||
prs: { owner: 'acme', repo: 'orca', host: 'github.com' }
|
||||
})
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasUpstreamCandidateDivergence', () => {
|
||||
it('is true when origin and upstream candidates differ', () => {
|
||||
expect(
|
||||
hasUpstreamCandidateDivergence(
|
||||
state({
|
||||
originCandidate: { owner: 'me', repo: 'fork', host: 'github.com' },
|
||||
upstreamCandidate: { owner: 'acme', repo: 'orca', host: 'github.com' }
|
||||
})
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('is false when the upstream candidate is missing', () => {
|
||||
expect(
|
||||
hasUpstreamCandidateDivergence(
|
||||
state({
|
||||
originCandidate: { owner: 'me', repo: 'fork', host: 'github.com' },
|
||||
upstreamCandidate: null
|
||||
})
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('is false when origin and upstream candidates match', () => {
|
||||
expect(
|
||||
hasUpstreamCandidateDivergence(
|
||||
state({
|
||||
originCandidate: { owner: 'acme', repo: 'orca', host: 'github.com' },
|
||||
upstreamCandidate: { owner: 'acme', repo: 'orca', host: 'github.com' }
|
||||
})
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
-86
@@ -1,86 +0,0 @@
|
||||
import { beforeEach, expect, it, vi } from 'vitest'
|
||||
import type { PaneManager } from '@/lib/pane-manager/pane-manager'
|
||||
import type { PtyConnectionDeps } from './pty-connection-types'
|
||||
import type { splitPaneInMain } from './terminal-pane-split-in-main'
|
||||
import { runBootstrapSplitInMain } from './terminal-pane-bootstrap-split-in-main'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
splitPaneInMain: vi.fn<typeof splitPaneInMain>(),
|
||||
runTerminalPaneBootstrapSplits: vi.fn()
|
||||
}))
|
||||
vi.mock('./terminal-pane-split-in-main', async (importOriginal) => ({
|
||||
...(await importOriginal<object>()),
|
||||
splitPaneInMain: mocks.splitPaneInMain
|
||||
}))
|
||||
vi.mock('./terminal-pane-mount-bootstrap', () => ({
|
||||
runTerminalPaneBootstrapSplits: mocks.runTerminalPaneBootstrapSplits
|
||||
}))
|
||||
|
||||
const SOURCE = '11111111-1111-4111-8111-111111111111'
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the startup split reads only the target pane.
|
||||
const manager = {
|
||||
getActivePane: () => ({ id: 1, leafId: SOURCE, terminal: { cols: 120, rows: 30 } }),
|
||||
getPanes: () => [{ id: 1, leafId: SOURCE, terminal: { cols: 120, rows: 30 } }]
|
||||
} as unknown as PaneManager
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: passed through to today's split only.
|
||||
const ptyDeps = {} as PtyConnectionDeps
|
||||
const issue = { command: 'issue runner', env: { ISSUE: '42' } }
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.splitPaneInMain.mockReset()
|
||||
mocks.runTerminalPaneBootstrapSplits.mockReset()
|
||||
})
|
||||
|
||||
function run(): void {
|
||||
runBootstrapSplitInMain({
|
||||
manager,
|
||||
worktreeId: 'wt-1',
|
||||
tabId: 'tab-1',
|
||||
ptyDeps,
|
||||
issueCommandSplit: issue,
|
||||
isActive: true
|
||||
})
|
||||
}
|
||||
|
||||
it('asks main to split the tab and start its startup command at half the source pane, keeping focus there', () => {
|
||||
mocks.splitPaneInMain.mockReturnValue(true)
|
||||
|
||||
run()
|
||||
|
||||
expect(mocks.splitPaneInMain).toHaveBeenCalledWith(
|
||||
{
|
||||
worktreeId: 'wt-1',
|
||||
tabId: 'tab-1',
|
||||
sourceLeafId: SOURCE,
|
||||
direction: 'vertical',
|
||||
initialSize: { cols: 60, rows: 30 },
|
||||
keepSourceFocused: true,
|
||||
command: 'issue runner',
|
||||
env: { ISSUE: '42' }
|
||||
},
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(mocks.runTerminalPaneBootstrapSplits).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('splits as today where main does not own the tab', () => {
|
||||
mocks.splitPaneInMain.mockReturnValue(false)
|
||||
|
||||
run()
|
||||
|
||||
expect(mocks.runTerminalPaneBootstrapSplits).toHaveBeenCalledWith({
|
||||
manager,
|
||||
ptyDeps,
|
||||
issueCommandSplit: issue,
|
||||
isActive: true
|
||||
})
|
||||
})
|
||||
|
||||
it('splits as today when main answers that a paired server holds the tab', () => {
|
||||
mocks.splitPaneInMain.mockReturnValue(true)
|
||||
|
||||
run()
|
||||
mocks.splitPaneInMain.mock.calls[0]![1]()
|
||||
|
||||
expect(mocks.runTerminalPaneBootstrapSplits).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
@@ -1,86 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { act } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { TerminalSplitWorkspaceSurfaces } from './TerminalSplitWorkspaceSurfaces'
|
||||
import type { StartupTerminalTabHold } from './terminal/startup-terminal-tab-hold'
|
||||
import type { TerminalController } from './use-terminal-controller'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
surfaceProps: new Map<string, ReadonlySet<string> | null>()
|
||||
}))
|
||||
vi.mock('./TerminalWorktreeSplitSurface', () => ({
|
||||
WorktreeSplitSurface: (props: {
|
||||
worktreeId: string
|
||||
activationDeferredMountTabIds: ReadonlySet<string> | null
|
||||
}) => {
|
||||
mocks.surfaceProps.set(props.worktreeId, props.activationDeferredMountTabIds)
|
||||
return null
|
||||
}
|
||||
}))
|
||||
vi.mock('./browser-pane/host-guest/browser-guest-paint-retention', () => ({
|
||||
useAnyBrowserGuestNeedsPaint: () => false
|
||||
}))
|
||||
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
|
||||
|
||||
const HELD_WORKTREE_ID = 'repo::/held'
|
||||
const DEFERRED_WORKTREE_ID = 'repo::/deferred'
|
||||
const OTHER_WORKTREE_ID = 'repo::/other'
|
||||
const WORKTREE_IDS = [HELD_WORKTREE_ID, DEFERRED_WORKTREE_ID, OTHER_WORKTREE_ID]
|
||||
|
||||
let root: Root | undefined
|
||||
afterEach(async () => {
|
||||
await act(async () => root?.unmount())
|
||||
root = undefined
|
||||
mocks.surfaceProps.clear()
|
||||
})
|
||||
|
||||
async function renderSurfaces(
|
||||
hold: StartupTerminalTabHold | null,
|
||||
deferred: Map<string, ReadonlySet<string>>
|
||||
): Promise<void> {
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: TerminalSplitWorkspaceSurfaces reads only the fields listed here; the rest of the controller is machinery this render never touches.
|
||||
const controller = {
|
||||
activationDeferredMountTabIdsByWorktreeRef: { current: deferred },
|
||||
activeGroupIdByWorktree: {},
|
||||
activeView: 'terminal',
|
||||
activityTerminalPortals: [],
|
||||
anyMountedWorktreeHasLayout: true,
|
||||
backgroundMountTabIdsByWorktreeRef: { current: new Map() },
|
||||
effectiveActiveLayout: { type: 'leaf', groupId: 'group-1' },
|
||||
effectiveParkedTerminalWorktreeIds: new Set(),
|
||||
forceParkedTerminalWorktreeIds: new Set(),
|
||||
getEffectiveLayoutForWorktree: () => ({ type: 'leaf', groupId: 'group-1' }),
|
||||
measurableBackgroundWorktreeIdsRef: { current: new Set() },
|
||||
mountedWorktreeIdsRef: { current: new Set(WORKTREE_IDS) },
|
||||
renderedActiveWorktreeId: HELD_WORKTREE_ID,
|
||||
startupTerminalTabHold: hold,
|
||||
workspaceSurfaces: WORKTREE_IDS.map((id) => ({ id, path: `/${id}` }))
|
||||
} as unknown as TerminalController
|
||||
const container = document.createElement('div')
|
||||
root = createRoot(container)
|
||||
await act(async () => root?.render(<TerminalSplitWorkspaceSurfaces controller={controller} />))
|
||||
}
|
||||
|
||||
describe('split workspace surfaces under a startup terminal hold', () => {
|
||||
it('hands each surface its parked-equivalent tabs: deferral set, else held tabs, else none', async () => {
|
||||
const hold: StartupTerminalTabHold = {
|
||||
worktreeId: HELD_WORKTREE_ID,
|
||||
heldTabIds: new Set(['held-tab'])
|
||||
}
|
||||
const deferredTabIds = new Set(['deferred-tab'])
|
||||
|
||||
await renderSurfaces(hold, new Map([[DEFERRED_WORKTREE_ID, deferredTabIds]]))
|
||||
|
||||
// Held tabs render no pane, so the surface must hand them to parked watchers.
|
||||
expect(mocks.surfaceProps.get(HELD_WORKTREE_ID)).toBe(hold.heldTabIds)
|
||||
expect(mocks.surfaceProps.get(DEFERRED_WORKTREE_ID)).toBe(deferredTabIds)
|
||||
expect(mocks.surfaceProps.get(OTHER_WORKTREE_ID)).toBeNull()
|
||||
})
|
||||
|
||||
it('passes no held tabs once the hold is released', async () => {
|
||||
await renderSurfaces(null, new Map())
|
||||
|
||||
expect(mocks.surfaceProps.get(HELD_WORKTREE_ID)).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,82 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
/** Collect browser webview IDs once per mount; eager useRef arguments previously walked every page/tab on every render. */
|
||||
import { useState } from 'react'
|
||||
import { act, cleanup, render } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const collectBrowserWebviewIdsCalls = vi.hoisted(() => ({ count: 0 }))
|
||||
|
||||
vi.mock('../store', () => {
|
||||
const state = {
|
||||
browserTabsByWorktree: {},
|
||||
browserPagesByWorkspace: {},
|
||||
openFiles: []
|
||||
}
|
||||
const useAppStore = Object.assign(() => undefined, {
|
||||
getState: () => state,
|
||||
subscribe: () => () => {}
|
||||
})
|
||||
return { useAppStore }
|
||||
})
|
||||
|
||||
vi.mock('../store/slices/browser-webview-cleanup', () => ({
|
||||
collectBrowserWebviewIds: (...args: unknown[]) => {
|
||||
collectBrowserWebviewIdsCalls.count += 1
|
||||
void args
|
||||
return new Set<string>()
|
||||
},
|
||||
destroyWebviewsRemovedFromStore: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/updater-beforeunload', () => ({
|
||||
isIntentionalAppRestartInProgress: () => false
|
||||
}))
|
||||
vi.mock('@/lib/shutdown-checkpoint-guard', () => ({
|
||||
preventUnloadAndScheduleShutdownCheckpointReset: vi.fn()
|
||||
}))
|
||||
vi.mock('./window-close-request-coordinator', () => ({
|
||||
setWindowCloseRequestHandler: vi.fn()
|
||||
}))
|
||||
|
||||
const { useTerminalWindowLifecycle } = await import('./use-terminal-window-lifecycle')
|
||||
|
||||
const controller = {
|
||||
activeBrowserTabId: null,
|
||||
activeTabType: 'terminal',
|
||||
activeWorktreeBrowserTabIdsKey: '',
|
||||
proceedToNativeWindowClose: () => {},
|
||||
queueEditorCloseRequests: () => {},
|
||||
renderedActiveWorktreeId: null,
|
||||
setActiveBrowserTab: () => {},
|
||||
setActiveTabType: () => {},
|
||||
windowCloseAfterDirtyRef: { current: false }
|
||||
} as unknown as Parameters<typeof useTerminalWindowLifecycle>[0]
|
||||
|
||||
let bumpRender: (() => void) | null = null
|
||||
|
||||
function Host(): null {
|
||||
const [, setTick] = useState(0)
|
||||
bumpRender = () => setTick((tick) => tick + 1)
|
||||
useTerminalWindowLifecycle(controller)
|
||||
return null
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
collectBrowserWebviewIdsCalls.count = 0
|
||||
bumpRender = null
|
||||
})
|
||||
|
||||
describe('useTerminalWindowLifecycle browser-webview id seed', () => {
|
||||
it('collects the id set once per mount, not once per render', () => {
|
||||
render(<Host />)
|
||||
expect(collectBrowserWebviewIdsCalls.count).toBe(1)
|
||||
|
||||
for (let i = 0; i < 20; i += 1) {
|
||||
act(() => bumpRender?.())
|
||||
}
|
||||
|
||||
expect(collectBrowserWebviewIdsCalls.count).toBe(1)
|
||||
})
|
||||
})
|
||||
-91
@@ -1,91 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { act, type ReactNode } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { makeFacetCandidate } from './workspace-cleanup-facet.test.fixture'
|
||||
import { WorkspaceCleanupConfirmStopAgents } from './workspace-cleanup-confirm-stop-agents'
|
||||
import { getWorkspaceCleanupCandidateAccessibleName } from './workspace-cleanup-host-label'
|
||||
|
||||
vi.mock('@/components/ui/dialog', () => ({
|
||||
DialogDescription: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
DialogFooter: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
DialogHeader: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
DialogTitle: ({ children }: { children: ReactNode }) => <div>{children}</div>
|
||||
}))
|
||||
|
||||
vi.mock('@/components/ui/scroll-area', () => ({
|
||||
ScrollArea: ({ children }: { children: ReactNode }) => <div>{children}</div>
|
||||
}))
|
||||
|
||||
vi.mock('@/components/ui/tooltip', () => ({
|
||||
Tooltip: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||
TooltipContent: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||
TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children}</>
|
||||
}))
|
||||
|
||||
let container: HTMLDivElement
|
||||
let root: Root
|
||||
|
||||
describe('WorkspaceCleanupConfirmStopAgents', () => {
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
root = createRoot(container)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount())
|
||||
document.body.replaceChildren()
|
||||
})
|
||||
|
||||
it('names each workspace whose agent will be stopped and wires both buttons', () => {
|
||||
const candidates = [
|
||||
makeFacetCandidate({ worktreeId: 'repo-1::/b', displayName: 'b' }),
|
||||
makeFacetCandidate({ worktreeId: 'repo-1::/c', displayName: 'c' })
|
||||
]
|
||||
const onBack = vi.fn()
|
||||
const onConfirm = vi.fn()
|
||||
act(() =>
|
||||
root.render(
|
||||
<WorkspaceCleanupConfirmStopAgents
|
||||
candidates={candidates}
|
||||
onBack={onBack}
|
||||
onConfirm={onConfirm}
|
||||
/>
|
||||
)
|
||||
)
|
||||
|
||||
expect(container.textContent).toContain('Stop these agents?')
|
||||
expect(container.textContent).toContain("Deleting will stop these agents' current work.")
|
||||
for (const candidate of candidates) {
|
||||
expect(
|
||||
container.querySelector(
|
||||
`[aria-label="${getWorkspaceCleanupCandidateAccessibleName(candidate)}"]`
|
||||
)
|
||||
).not.toBeNull()
|
||||
}
|
||||
const buttons = [...container.querySelectorAll('button')]
|
||||
act(() => buttons.find((button) => button.textContent === 'Back')?.click())
|
||||
act(() => buttons.find((button) => button.textContent === 'Stop agents and delete')?.click())
|
||||
expect(onBack).toHaveBeenCalledTimes(1)
|
||||
expect(onConfirm).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('reuses the close-terminal wording for a single agent', () => {
|
||||
act(() =>
|
||||
root.render(
|
||||
<WorkspaceCleanupConfirmStopAgents
|
||||
candidates={[makeFacetCandidate({ worktreeId: 'repo-1::/b', displayName: 'b' })]}
|
||||
onBack={vi.fn()}
|
||||
onConfirm={vi.fn()}
|
||||
/>
|
||||
)
|
||||
)
|
||||
|
||||
expect(container.textContent).toContain('Stop this agent?')
|
||||
expect(container.textContent).toContain("Deleting will stop the agent's current work.")
|
||||
expect([...container.querySelectorAll('button')].map((button) => button.textContent)).toContain(
|
||||
'Stop agent and delete'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,82 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { applyComposerNativeFileDrop } from './composer-native-file-drop'
|
||||
|
||||
function createDropArgs() {
|
||||
return {
|
||||
paths: ['/local/a.txt'],
|
||||
isCurrentOwner: vi.fn(() => true),
|
||||
uploadPaths: vi.fn(),
|
||||
applyLocalPaths: vi.fn().mockResolvedValue(undefined),
|
||||
addAttachments: vi.fn(),
|
||||
insertFolderPaths: vi.fn(),
|
||||
onError: vi.fn()
|
||||
}
|
||||
}
|
||||
|
||||
describe('applyComposerNativeFileDrop', () => {
|
||||
it('reports upload owner failures without falling through to client-local paths', async () => {
|
||||
const args = createDropArgs()
|
||||
const error = new Error('Attachment upload host changed; retry the upload.')
|
||||
args.uploadPaths.mockRejectedValue(error)
|
||||
|
||||
await applyComposerNativeFileDrop(args)
|
||||
|
||||
expect(args.onError).toHaveBeenCalledWith(error)
|
||||
expect(args.applyLocalPaths).not.toHaveBeenCalled()
|
||||
expect(args.addAttachments).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not surface a failure after the composer loses drop ownership', async () => {
|
||||
const args = createDropArgs()
|
||||
args.uploadPaths.mockRejectedValue(new Error('stale owner'))
|
||||
args.isCurrentOwner.mockReturnValue(false)
|
||||
|
||||
await applyComposerNativeFileDrop(args)
|
||||
|
||||
expect(args.onError).not.toHaveBeenCalled()
|
||||
expect(args.applyLocalPaths).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('applies local paths when no remote upload route is needed', async () => {
|
||||
const args = createDropArgs()
|
||||
args.uploadPaths.mockResolvedValue(null)
|
||||
|
||||
await applyComposerNativeFileDrop(args)
|
||||
|
||||
expect(args.applyLocalPaths).toHaveBeenCalledWith(args.paths, args.isCurrentOwner)
|
||||
expect(args.addAttachments).not.toHaveBeenCalled()
|
||||
expect(args.insertFolderPaths).not.toHaveBeenCalled()
|
||||
expect(args.onError).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('applies uploaded files and folders only while the composer still owns the drop', async () => {
|
||||
const args = createDropArgs()
|
||||
args.uploadPaths.mockResolvedValue({
|
||||
filePaths: ['/remote/a.txt'],
|
||||
folderPaths: ['/remote/folder']
|
||||
})
|
||||
|
||||
await applyComposerNativeFileDrop(args)
|
||||
|
||||
expect(args.addAttachments).toHaveBeenCalledWith(['/remote/a.txt'])
|
||||
expect(args.insertFolderPaths).toHaveBeenCalledWith(['/remote/folder'])
|
||||
expect(args.applyLocalPaths).not.toHaveBeenCalled()
|
||||
expect(args.onError).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('discards uploaded paths after the composer loses drop ownership', async () => {
|
||||
const args = createDropArgs()
|
||||
args.uploadPaths.mockResolvedValue({
|
||||
filePaths: ['/remote/a.txt'],
|
||||
folderPaths: ['/remote/folder']
|
||||
})
|
||||
args.isCurrentOwner.mockReturnValue(false)
|
||||
|
||||
await applyComposerNativeFileDrop(args)
|
||||
|
||||
expect(args.addAttachments).not.toHaveBeenCalled()
|
||||
expect(args.insertFolderPaths).not.toHaveBeenCalled()
|
||||
expect(args.applyLocalPaths).not.toHaveBeenCalled()
|
||||
expect(args.onError).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,86 +0,0 @@
|
||||
// Layout-owner build: the sidebar's resume of a sleeping agent opens a new tab whose start main's
|
||||
// create cannot carry yet (L252), so the window spawns it and its pane seeds the "session
|
||||
// restored" banner from the tab's startup, as with the switch off.
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { SleepingAgentSessionRecord } from '../../../shared/agent-session-resume'
|
||||
import { startupNeedsWindowSpawn } from '@/store/terminals/window-tab-start-in-main'
|
||||
import { seedStartupSessionRestoredBanner } from '@/components/terminal-pane/session-restored-banner-pane-state'
|
||||
import type { AppState } from '@/store/types'
|
||||
|
||||
type TabStartup = AppState['pendingStartupByTabId'][string]
|
||||
|
||||
const mockCreateTab = vi.fn((..._args: unknown[]) => ({ id: 'tab-1' }))
|
||||
|
||||
vi.mock('../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: {
|
||||
getState: () => ({
|
||||
settings: { agentCmdOverrides: {}, agentDefaultArgs: {}, agentDefaultEnv: {} },
|
||||
repos: [{ id: 'repo-1', connectionId: null, path: '/repo' }],
|
||||
getKnownWorktreeById: () => ({ id: 'wt-1', repoId: 'repo-1', path: '/repo/wt' }),
|
||||
sshConnectionStates: new Map(),
|
||||
sshStateByEnvironment: new Map(),
|
||||
runtimeStatusByEnvironmentId: new Map(),
|
||||
agentStatusByPaneKey: {},
|
||||
tabsByWorktree: { 'wt-1': [{ id: 'tab-1' }] },
|
||||
openFiles: [],
|
||||
browserTabsByWorktree: {},
|
||||
tabBarOrderByWorktree: {},
|
||||
createTab: mockCreateTab,
|
||||
claimAutomaticAgentResume: vi.fn(),
|
||||
clearSleepingAgentSession: vi.fn(),
|
||||
setActiveTabType: vi.fn(),
|
||||
setTabBarOrder: vi.fn()
|
||||
})
|
||||
}
|
||||
}))
|
||||
vi.mock('@/lib/new-workspace', () => ({ CLIENT_PLATFORM: 'darwin' }))
|
||||
vi.mock('sonner', () => ({ toast: { message: vi.fn(), error: vi.fn() } }))
|
||||
vi.mock('@/lib/telemetry', () => ({
|
||||
track: vi.fn(),
|
||||
tuiAgentToAgentKind: (agent: string) => agent
|
||||
}))
|
||||
vi.mock('@/components/tab-bar/reconcile-order', () => ({
|
||||
reconcileTabOrder: vi.fn((_stored, termIds: string[]) => [...termIds])
|
||||
}))
|
||||
|
||||
const record: SleepingAgentSessionRecord = {
|
||||
paneKey: 'tab-0:leaf-0',
|
||||
tabId: 'tab-0',
|
||||
worktreeId: 'wt-1',
|
||||
agent: 'claude',
|
||||
providerSession: { key: 'session_id', id: 'claude-session-1' },
|
||||
prompt: 'finish the task',
|
||||
state: 'done',
|
||||
origin: 'worktree-sleep',
|
||||
capturedAt: 1,
|
||||
updatedAt: 1
|
||||
}
|
||||
|
||||
async function sidebarResumeStartup(): Promise<TabStartup | undefined> {
|
||||
const { launchSleepingAgentSession } = await import('./sleeping-agent-session-launch')
|
||||
expect(launchSleepingAgentSession(record)).toBe(true)
|
||||
const options: unknown = mockCreateTab.mock.calls.at(-1)?.[3]
|
||||
return typeof options === 'object' && options !== null && 'pendingStartup' in options
|
||||
? // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: createTab's fourth argument carries the tab's pending startup.
|
||||
(options.pendingStartup as TabStartup)
|
||||
: undefined
|
||||
}
|
||||
|
||||
describe("the sidebar's resume of a sleeping agent (layout-owner build)", () => {
|
||||
beforeEach(() => {
|
||||
mockCreateTab.mockClear()
|
||||
})
|
||||
|
||||
it('keeps the window spawn, whose pane shows the session restored banner', async () => {
|
||||
const startup = await sidebarResumeStartup()
|
||||
|
||||
expect(startup).toMatchObject({ resumeProviderSession: record.providerSession })
|
||||
// Main's create cannot carry this start yet, so the window spawns it (L252).
|
||||
expect(startupNeedsWindowSpawn(startup)).toBe(true)
|
||||
const onShow = vi.fn()
|
||||
seedStartupSessionRestoredBanner(startup, 7, onShow)
|
||||
expect(onShow).toHaveBeenCalledExactlyOnceWith(7)
|
||||
})
|
||||
})
|
||||
@@ -1,95 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { GlobalSettings } from '../../../../shared/global-settings-types'
|
||||
import type { Project } from '../../../../shared/project-types'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
|
||||
const { warningMock } = vi.hoisted(() => ({ warningMock: vi.fn() }))
|
||||
|
||||
vi.mock('sonner', () => ({ toast: { warning: warningMock } }))
|
||||
vi.mock('@/i18n/i18n', () => ({
|
||||
translate: (_key: string, fallback: string, vars?: Record<string, string>) =>
|
||||
fallback.replace(/\{\{(\w+)\}\}/g, (_match, name: string) => vars?.[name] ?? '')
|
||||
}))
|
||||
|
||||
import { warnIfProjectCrossesWslFilesystemBoundary } from './project-wsl-filesystem-boundary-advisory'
|
||||
|
||||
const WSL_DEFAULT = {
|
||||
localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu-24.04' }
|
||||
} as unknown as GlobalSettings
|
||||
|
||||
function makeRepo(overrides: Partial<Repo> = {}): Repo {
|
||||
return { id: 'repo-1', path: 'C:\\Users\\alice\\orca', ...overrides } as Repo
|
||||
}
|
||||
|
||||
function makeProject(overrides: Partial<Project> = {}): Project {
|
||||
return { id: 'project-1', sourceRepoIds: ['repo-1'], ...overrides } as Project
|
||||
}
|
||||
|
||||
describe('warnIfProjectCrossesWslFilesystemBoundary', () => {
|
||||
beforeEach(() => {
|
||||
warningMock.mockReset()
|
||||
})
|
||||
|
||||
it('warns and names the distro when a Windows-drive project runs WSL git', () => {
|
||||
warnIfProjectCrossesWslFilesystemBoundary(makeRepo(), [makeProject()], WSL_DEFAULT)
|
||||
|
||||
expect(warningMock).toHaveBeenCalledTimes(1)
|
||||
expect(warningMock.mock.calls[0]?.[1]?.description).toContain('Ubuntu-24.04')
|
||||
})
|
||||
|
||||
// The project override is what actually routes git, so it must beat the global default here too.
|
||||
it('respects a project override that pins the project back to the Windows host', () => {
|
||||
warnIfProjectCrossesWslFilesystemBoundary(
|
||||
makeRepo(),
|
||||
[makeProject({ localWindowsRuntimePreference: { kind: 'windows-host' } })],
|
||||
WSL_DEFAULT
|
||||
)
|
||||
|
||||
expect(warningMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('warns from a project override even when the global default is the Windows host', () => {
|
||||
warnIfProjectCrossesWslFilesystemBoundary(
|
||||
makeRepo(),
|
||||
[makeProject({ localWindowsRuntimePreference: { kind: 'wsl', distro: 'Debian' } })],
|
||||
{ localWindowsRuntimeDefault: { kind: 'windows-host' } } as unknown as GlobalSettings
|
||||
)
|
||||
|
||||
expect(warningMock.mock.calls[0]?.[1]?.description).toContain('Debian')
|
||||
})
|
||||
|
||||
// Windows runtime preference is local-only; Windows-shaped SSH paths still belong to another filesystem.
|
||||
it('stays silent for a repo on a remote execution host', () => {
|
||||
warnIfProjectCrossesWslFilesystemBoundary(
|
||||
makeRepo({ connectionId: 'ssh-1' }),
|
||||
[makeProject()],
|
||||
WSL_DEFAULT
|
||||
)
|
||||
|
||||
expect(warningMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('stays silent when the WSL default has no distro to repair to', () => {
|
||||
warnIfProjectCrossesWslFilesystemBoundary(makeRepo(), [makeProject()], {
|
||||
localWindowsRuntimeDefault: { kind: 'wsl', distro: null }
|
||||
} as unknown as GlobalSettings)
|
||||
|
||||
expect(warningMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('never throws when the advisory itself fails', () => {
|
||||
const repo = makeRepo()
|
||||
Object.defineProperty(repo, 'path', {
|
||||
get: () => {
|
||||
throw new Error('unreadable path')
|
||||
}
|
||||
})
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
expect(() =>
|
||||
warnIfProjectCrossesWslFilesystemBoundary(repo, [makeProject()], WSL_DEFAULT)
|
||||
).not.toThrow()
|
||||
expect(warningMock).not.toHaveBeenCalled()
|
||||
warnSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
@@ -1,82 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { toRuntimeExecutionHostId, type ExecutionHostId } from '../../../../shared/execution-host'
|
||||
import type { FolderWorkspace } from '../../../../shared/folder-workspace-types'
|
||||
import type { ProjectGroup } from '../../../../shared/project-group-types'
|
||||
|
||||
vi.mock('sonner', () => ({
|
||||
toast: { info: vi.fn(), success: vi.fn(), error: vi.fn(), warning: vi.fn() }
|
||||
}))
|
||||
vi.mock('@/components/terminal-pane/pty-dispatcher', () => ({
|
||||
restorePtyDataHandlersAfterFailedShutdown: vi.fn(),
|
||||
unregisterPtyDataHandlers: vi.fn()
|
||||
}))
|
||||
|
||||
// @ts-expect-error -- minimal window.api stub for the store under test
|
||||
globalThis.window = { api: {} }
|
||||
|
||||
const { createTestStore } = await import('./store-test-helpers')
|
||||
|
||||
function group(id: string, executionHostId: string): ProjectGroup {
|
||||
return {
|
||||
id,
|
||||
name: 'root/repo',
|
||||
parentPath: '/srv',
|
||||
executionHostId,
|
||||
parentGroupId: null,
|
||||
createdFrom: 'manual',
|
||||
tabOrder: 0,
|
||||
isCollapsed: false,
|
||||
color: null,
|
||||
createdAt: 1,
|
||||
updatedAt: 1
|
||||
}
|
||||
}
|
||||
|
||||
function folder(id: string, executionHostId: ExecutionHostId): FolderWorkspace {
|
||||
return {
|
||||
id,
|
||||
projectGroupId: 'group',
|
||||
name: 'folder',
|
||||
folderPath: '/srv/folder',
|
||||
executionHostId,
|
||||
linkedTask: null,
|
||||
comment: '',
|
||||
isArchived: false,
|
||||
isUnread: false,
|
||||
isPinned: false,
|
||||
sortOrder: 0,
|
||||
lastActivityAt: 1,
|
||||
createdAt: 1,
|
||||
updatedAt: 1
|
||||
}
|
||||
}
|
||||
|
||||
describe('purging a removed runtime’s project groups', () => {
|
||||
it('drops the removed server’s groups and folder rows, and keeps every other host’s', () => {
|
||||
const store = createTestStore()
|
||||
const kept = [group('local-group', 'local'), group('other', toRuntimeExecutionHostId('env-b'))]
|
||||
store.setState({
|
||||
projectGroups: [...kept, group('stopped', toRuntimeExecutionHostId('env-a'))],
|
||||
folderWorkspaces: [
|
||||
folder('local-folder', 'local'),
|
||||
folder('stopped-folder', toRuntimeExecutionHostId('env-a'))
|
||||
]
|
||||
})
|
||||
|
||||
store.getState().purgeStaleRuntimeHostState(['env-a'])
|
||||
|
||||
expect(store.getState().projectGroups.map((entry) => entry.id)).toEqual([
|
||||
'local-group',
|
||||
'other'
|
||||
])
|
||||
expect(store.getState().folderWorkspaces.map((entry) => entry.id)).toEqual(['local-folder'])
|
||||
})
|
||||
|
||||
it('leaves the state untouched when no group belongs to the removed server', () => {
|
||||
const store = createTestStore()
|
||||
const projectGroups = [group('local-group', 'local')]
|
||||
store.setState({ projectGroups, folderWorkspaces: [] })
|
||||
store.getState().purgeStaleRuntimeHostState(['env-a'])
|
||||
expect(store.getState().projectGroups).toBe(projectGroups)
|
||||
})
|
||||
})
|
||||
@@ -1,86 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { create } from 'zustand'
|
||||
import { createCompatibleRuntimeStatusResponse } from '../../runtime/runtime-compatibility-test-fixture'
|
||||
import { resetRestoredBrowserClientHostAttachForTests } from '@/runtime/restored-client-hosted-browser-host-attach'
|
||||
import { replayClientHostedBrowserCloseIntents } from '@/runtime/client-hosted-browser-close-intent-replay'
|
||||
import {
|
||||
clearRuntimeEnvironmentConnectionGenerationsForTests,
|
||||
createRuntimeStatusSlice,
|
||||
type RuntimeStatusSlice
|
||||
} from './runtime-status'
|
||||
import { stubRuntimeEnvironmentBridge } from '../../runtime/runtime-environment-bridge-fixture'
|
||||
|
||||
vi.mock('sonner', () => ({
|
||||
toast: { warning: vi.fn(), dismiss: vi.fn() }
|
||||
}))
|
||||
|
||||
vi.mock('@/runtime/client-hosted-browser-close-intent-replay', () => ({
|
||||
replayClientHostedBrowserCloseIntents: vi.fn(async () => {})
|
||||
}))
|
||||
|
||||
const prepareBrowserClientHostPlacement = vi.fn(async (_args: { selector: string }) => ({
|
||||
kind: 'server' as const
|
||||
}))
|
||||
|
||||
function stubApi(getStatus: ReturnType<typeof vi.fn>): void {
|
||||
stubRuntimeEnvironmentBridge({
|
||||
getStatus,
|
||||
list: vi.fn(),
|
||||
prepareBrowserClientHostPlacement
|
||||
})
|
||||
}
|
||||
|
||||
/** A status slice whose state also carries the browser handles the attach step reads. */
|
||||
function storeWithRestoredHandles(clientHosted: boolean) {
|
||||
return create<RuntimeStatusSlice & { remoteBrowserPageHandlesByPageId: unknown }>()((...a) => ({
|
||||
...createRuntimeStatusSlice(...(a as unknown as Parameters<typeof createRuntimeStatusSlice>)),
|
||||
remoteBrowserPageHandlesByPageId: {
|
||||
'page-1': {
|
||||
environmentId: 'env-a',
|
||||
remotePageId: 'page-1',
|
||||
restoredFromSession: true,
|
||||
...(clientHosted ? { restoredClientHosted: true } : {})
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
describe('restored client-hosted browser host attach on reachability', () => {
|
||||
beforeEach(() => {
|
||||
clearRuntimeEnvironmentConnectionGenerationsForTests()
|
||||
resetRestoredBrowserClientHostAttachForTests()
|
||||
prepareBrowserClientHostPlacement.mockClear()
|
||||
vi.mocked(replayClientHostedBrowserCloseIntents).mockClear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('runs both recovery follow-ups after a successful refresh', async () => {
|
||||
stubApi(vi.fn().mockResolvedValue(createCompatibleRuntimeStatusResponse('runtime-a')))
|
||||
|
||||
await storeWithRestoredHandles(true).getState().refreshRuntimeEnvironmentStatus('env-a')
|
||||
|
||||
expect(prepareBrowserClientHostPlacement).toHaveBeenCalledWith({
|
||||
selector: 'env-a',
|
||||
preference: 'auto'
|
||||
})
|
||||
expect(replayClientHostedBrowserCloseIntents).toHaveBeenCalledWith('env-a', expect.anything())
|
||||
})
|
||||
|
||||
it('runs no recovery follow-ups when the environment is unreachable', async () => {
|
||||
stubApi(vi.fn().mockRejectedValue(new Error('unreachable')))
|
||||
await storeWithRestoredHandles(true).getState().refreshRuntimeEnvironmentStatus('env-a')
|
||||
expect(prepareBrowserClientHostPlacement).not.toHaveBeenCalled()
|
||||
expect(replayClientHostedBrowserCloseIntents).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('starts no browser client host for restored pages the server hosts', async () => {
|
||||
stubApi(vi.fn().mockResolvedValue(createCompatibleRuntimeStatusResponse('runtime-a')))
|
||||
|
||||
await storeWithRestoredHandles(false).getState().refreshRuntimeEnvironmentStatus('env-a')
|
||||
|
||||
expect(prepareBrowserClientHostPlacement).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,86 +0,0 @@
|
||||
// host_not_owned routes twin-workspace promotion to the window copy, matching window-owned preview behavior.
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Tab } from '../../../../../shared/tab-types'
|
||||
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../../shared/layout-owner-switch'
|
||||
import type { AppState } from '../../types'
|
||||
import type { LayoutCommandResult } from '../../../../../shared/workspace-layout/workspace-layout-command-types'
|
||||
|
||||
type Answer = 'asToday' | 'apply' | 'refused'
|
||||
const answer = vi.hoisted((): { value: Answer } => ({ value: 'asToday' }))
|
||||
|
||||
vi.mock('@/lib/resolve-owner', () => ({ runtimeEnvironmentIdUnlessTwin: () => null }))
|
||||
vi.mock('@/runtime/layout-apply-send', () => ({
|
||||
sendLayoutApply: (
|
||||
_state: unknown,
|
||||
_worktreeId: string,
|
||||
_command: unknown,
|
||||
handlers: {
|
||||
apply: (result: LayoutCommandResult) => void
|
||||
asToday: () => void
|
||||
refused?: () => void
|
||||
}
|
||||
) => {
|
||||
if (answer.value === 'asToday') {
|
||||
handlers.asToday()
|
||||
} else if (answer.value === 'apply') {
|
||||
handlers.apply({})
|
||||
} else {
|
||||
handlers.refused?.()
|
||||
}
|
||||
return true
|
||||
}
|
||||
}))
|
||||
|
||||
const { promotePreviewTabsThroughLayoutOwner } = await import('./preview-tab-promotion')
|
||||
|
||||
const WT = 'repo::/twin'
|
||||
|
||||
function previewTab(id: string): Tab {
|
||||
return {
|
||||
id,
|
||||
entityId: `/twin/${id}.ts`,
|
||||
groupId: 'g1',
|
||||
worktreeId: WT,
|
||||
contentType: 'editor',
|
||||
label: `${id}.ts`,
|
||||
customLabel: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 1,
|
||||
isPreview: true
|
||||
}
|
||||
}
|
||||
|
||||
function promote(id: string): Tab {
|
||||
let tabs: AppState['unifiedTabsByWorktree'] = { [WT]: [previewTab(id)] }
|
||||
const set = (
|
||||
update: (
|
||||
state: Pick<AppState, 'unifiedTabsByWorktree'>
|
||||
) => Partial<Pick<AppState, 'unifiedTabsByWorktree'>>
|
||||
): void => {
|
||||
tabs = update({ unifiedTabsByWorktree: tabs }).unifiedTabsByWorktree ?? tabs
|
||||
}
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mocked sender and owner lookup read nothing from the state.
|
||||
promotePreviewTabsThroughLayoutOwner({} as AppState, set, [{ id, worktreeId: WT }])
|
||||
return tabs[WT]![0]!
|
||||
}
|
||||
|
||||
describe.runIf(ORCA_LAYOUT_OWNER_IS_MAIN)(
|
||||
'preview promotion of a twin tab, layout-owner build',
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
answer.value = 'asToday'
|
||||
})
|
||||
|
||||
it('ends the preview here when main routes the tab to a paired server', () => {
|
||||
expect(promote('routed').isPreview).toBeUndefined()
|
||||
})
|
||||
|
||||
it("leaves the flag to main's frame when main promotes it, and keeps it on a refusal", () => {
|
||||
answer.value = 'apply'
|
||||
expect(promote('applied').isPreview).toBe(true)
|
||||
answer.value = 'refused'
|
||||
expect(promote('refused').isPreview).toBe(true)
|
||||
})
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user