mirror of
https://github.com/stablyai/orca.git
synced 2026-11-14 01:47:24 +00:00
test: remove 167 low-value cases and 93 suites (#41057)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -27,7 +27,6 @@ on:
|
||||
- 'src/main/runtime/agent-session/agent-session-account-*.ts'
|
||||
- 'src/main/runtime/agent-session/agent-session-acquisition-*.ts'
|
||||
- 'src/main/runtime/agent-session/agent-session-conversation-*.ts'
|
||||
- 'src/main/runtime/agent-session/agent-session-death-*.ts'
|
||||
- 'src/main/runtime/agent-session/agent-session-launch-*.ts'
|
||||
- 'src/main/runtime/agent-session/agent-session-operation-*.ts'
|
||||
- 'src/main/runtime/agent-session/agent-session-rewind-*.ts'
|
||||
@@ -116,7 +115,6 @@ on:
|
||||
- 'src/main/runtime/agent-session/agent-session-account-*.ts'
|
||||
- 'src/main/runtime/agent-session/agent-session-acquisition-*.ts'
|
||||
- 'src/main/runtime/agent-session/agent-session-conversation-*.ts'
|
||||
- 'src/main/runtime/agent-session/agent-session-death-*.ts'
|
||||
- 'src/main/runtime/agent-session/agent-session-launch-*.ts'
|
||||
- 'src/main/runtime/agent-session/agent-session-operation-*.ts'
|
||||
- 'src/main/runtime/agent-session/agent-session-rewind-*.ts'
|
||||
|
||||
@@ -21,7 +21,6 @@ on:
|
||||
- 'src/main/runtime/session-tabs/session-tabs-phone-layout-moves.test.ts'
|
||||
- 'src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-terminal-writer-acquire.test.ts'
|
||||
- 'src/shared/agent-launch/permissions/tui-agent-permission-choice.test.ts'
|
||||
- 'src/main/runtime/rpc/methods/projects/repo.test.ts'
|
||||
- 'src/renderer/src/components/tab-bar/use-new-tab-agent-menu-host-preferences.test.tsx'
|
||||
- 'src/main/sqlite/sync-database.test.ts'
|
||||
- 'src/main/runtime/orchestration/db/row-column-lists.test.ts'
|
||||
|
||||
@@ -507,7 +507,6 @@
|
||||
3 test-no-wall-clock:src/main/github/pr-refresh-coordinator-active-visible-priority.test.ts
|
||||
8 test-no-wall-clock:src/main/github/pr-refresh-coordinator-alias-coalescing.test.ts
|
||||
5 test-no-wall-clock:src/main/github/pr-refresh-coordinator-completion-order.test.ts
|
||||
1 test-no-wall-clock:src/main/github/pr-refresh-coordinator-error-cadence.test.ts
|
||||
3 test-no-wall-clock:src/main/github/pr-refresh-coordinator-foreground-pacing.test.ts
|
||||
3 test-no-wall-clock:src/main/github/pr-refresh-coordinator-refresh-events.test.ts
|
||||
4 test-no-wall-clock:src/main/github/pr-refresh-coordinator-visibility-policy.test.ts
|
||||
@@ -614,7 +613,6 @@
|
||||
3 test-no-wall-clock:src/main/windows/windows-msys-job.win32.test.ts
|
||||
1 test-no-wall-clock:src/main/windows/windows-process-table-native-addon.win32.test.ts
|
||||
2 test-no-wall-clock:src/main/windows/windows-pty-job.win32.test.ts
|
||||
2 test-no-wall-clock:src/main/workspace-hooks/hook-archive-timeout-observation.test.ts
|
||||
2 test-no-wall-clock:src/main/workspaces/removal/local-worktree-removal-recovery-live.test.ts
|
||||
1 test-no-wall-clock:src/main/workspaces/removal/worktree-removal-close-records.test.ts
|
||||
2 test-no-wall-clock:src/main/wsl/execution/wsl-runner.wsl.test.ts
|
||||
@@ -627,7 +625,6 @@
|
||||
2 test-no-wall-clock:src/renderer/src/components/native-chat/tools/tasks/NativeChatStructuredSessionStatus.test.tsx
|
||||
1 test-no-wall-clock:src/renderer/src/components/sidebar/workspace-card/review/use-workspace-reference-details.test.tsx
|
||||
1 test-no-wall-clock:src/renderer/src/components/sidebar/worktree-list/listing/WorktreeList.lineage-agent-expansion-coupling.test.tsx
|
||||
2 test-no-wall-clock:src/renderer/src/components/task-page/jira/task-page-jira-cache-selectors.test.ts
|
||||
2 test-no-wall-clock:src/renderer/src/components/terminal-pane/pty-connection/emulation/fish-color-scheme-child-stdin.node-pty.test.ts
|
||||
2 test-no-wall-clock:src/renderer/src/components/terminal-pane/snapshots/remote-snapshot-alt-screen-host-chain.test.ts
|
||||
1 test-no-wall-clock:src/renderer/src/components/terminal/initial-terminal-structured-launch.test.tsx
|
||||
@@ -750,7 +747,7 @@
|
||||
1 test-timeout-needs-reason:src/main/windows/windows-msys-job.win32.test.ts
|
||||
2 test-timeout-needs-reason:src/main/windows/windows-pty-job.win32.test.ts
|
||||
5 test-timeout-needs-reason:src/main/worker-thread-child-process/worker-thread-child-process.test.ts
|
||||
2 test-timeout-needs-reason:src/main/workspace-hooks/hook-archive-timeout-observation.test.ts
|
||||
1 test-timeout-needs-reason:src/main/workspace-hooks/hook-archive-timeout-observation.test.ts
|
||||
2 test-timeout-needs-reason:src/main/workspace-hooks/hook-termination-real-process.test.ts
|
||||
2 test-timeout-needs-reason:src/main/workspaces/removal/local-worktree-filesystem-wsl-banner.wsl.test.ts
|
||||
4 test-timeout-needs-reason:src/main/wsl/execution/wsl-runner.wsl.test.ts
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
# The relay retirement (2027-01-15) deletes the folder and edits exactly these files.
|
||||
# Prune: pnpm check:legacy-relay-fence-ratchet --prune
|
||||
src/main/ipc/runtime-environments/orcad-delta-move-handlers.ts
|
||||
src/main/ipc/pty/runtime/kill-legacy-relay-bridged.test.ts
|
||||
src/main/ipc/pty/runtime/kill.ts
|
||||
src/main/ipc/ssh/ssh-connect-decision-cancellation.test.ts
|
||||
src/main/ipc/ssh/ssh-connect-flow.ts
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -831,10 +831,8 @@
|
||||
"src/main/automations/automation-zero-grace-tick-latency.test.ts": 2108,
|
||||
"src/main/automations/dispatch-refusal-wsl-move.test.ts": 577,
|
||||
"src/main/automations/external-automation-manager-cache.test.ts": 70,
|
||||
"src/main/automations/external-automation-owner-guard.test.ts": 332,
|
||||
"src/main/automations/external-automation-probe-scheduler.test.ts": 97,
|
||||
"src/main/automations/external-manager-scoped.test.ts": 391,
|
||||
"src/main/automations/external-manager.test.ts": 388,
|
||||
"src/main/automations/run-terminal-retention.test.ts": 112,
|
||||
"src/main/automations/automation-workspace-create.test.ts": 50,
|
||||
"src/main/automations/hook-review-blocked-run-terminals.test.ts": 4564,
|
||||
@@ -844,7 +842,6 @@
|
||||
"src/main/automations/run-target-resolution.test.ts": 367,
|
||||
"src/main/automations/runtime-chat-run-observer.test.ts": 6812,
|
||||
"src/main/automations/runtime-terminal-run-observer-evidence.test.ts": 230,
|
||||
"src/main/automations/runtime-terminal-run-observer.test.ts": 4026,
|
||||
"src/main/automations/service-precheck.test.ts": 2057,
|
||||
"src/main/automations/service.test.ts": 2970,
|
||||
"src/main/bitbucket/credential-connection.test.ts": 156,
|
||||
@@ -982,7 +979,6 @@
|
||||
"src/main/browser/cookies/browser-cookie-import-partition.electron.test.ts": 2260,
|
||||
"src/main/browser/cookies/browser-cookie-import-snapshot-ownership.test.ts": 1178,
|
||||
"src/main/browser/cookies/browser-cookie-import-validated-partition.electron.test.ts": 2445,
|
||||
"src/main/browser/cookies/browser-cookie-import-write.test.ts": 68,
|
||||
"src/main/browser/cookies/browser-cookie-import.comet.test.ts": 1501,
|
||||
"src/main/browser/cookies/browser-cookie-import.test.ts": 1304,
|
||||
"src/main/browser/cookies/browser-cookie-samesite.electron.test.ts": 1882,
|
||||
@@ -1063,7 +1059,6 @@
|
||||
"src/main/browser/cdp/input/guest-focused-frame-route.test.ts": 78,
|
||||
"src/main/browser/routing/local-host-partitions/local-ssh-browser-partition-identity.test.ts": 479,
|
||||
"src/main/browser/routing/local-host-partitions/local-ssh-browser-profile-retirement.test.ts": 287,
|
||||
"src/main/browser/routing/local-host-partitions/local-ssh-browser-route-target.test.ts": 65,
|
||||
"src/main/browser/routing/local-host-partitions/local-ssh-browser-route.test.ts": 92,
|
||||
"src/main/browser/backends/offscreen-browser-backend-lifecycle.test.ts": 341,
|
||||
"src/main/browser/client-hosts/lifecycle/paired-runtime-browser-client-host-composition.test.ts": 165,
|
||||
@@ -1172,7 +1167,6 @@
|
||||
"src/main/claude/claude-stop-ack-race.test.ts": 1301,
|
||||
"src/main/claude/claude-stream-json-connection-close.test.ts": 621,
|
||||
"src/main/claude/claude-stream-json-connection.test.ts": 8687,
|
||||
"src/main/claude/claude-stream-json-exit-error.test.ts": 170,
|
||||
"src/main/claude/claude-streamed-text-checkpoints.test.ts": 59,
|
||||
"src/main/claude/claude-structured-launch-home-perf.test.ts": 401,
|
||||
"src/main/claude/claude-structured-result-control-overlap.test.ts": 1136,
|
||||
@@ -1916,7 +1910,6 @@
|
||||
"src/main/git/source-control/status-read-failure-real-git.test.ts": 307,
|
||||
"src/main/git/source-control/status-shared-symlinks.test.ts": 746,
|
||||
"src/main/git/source-control/status-submodule.test.ts": 306,
|
||||
"src/main/git/source-control/status-symlink-probe-budget.test.ts": 238,
|
||||
"src/main/git/source-control/status-upstream-ref.test.ts": 52,
|
||||
"src/main/git/source-control/upstream-deferred-fork-remote-real.test.ts": 319,
|
||||
"src/main/git/source-control/upstream.test.ts": 406,
|
||||
@@ -2014,7 +2007,6 @@
|
||||
"src/main/github/pr-refresh-coordinator-active-visible-priority.test.ts": 643,
|
||||
"src/main/github/pr-refresh-coordinator-alias-coalescing.test.ts": 1020,
|
||||
"src/main/github/pr-refresh-coordinator-completion-order.test.ts": 914,
|
||||
"src/main/github/pr-refresh-coordinator-error-cadence.test.ts": 951,
|
||||
"src/main/github/pr-refresh-coordinator-foreground-pacing.test.ts": 650,
|
||||
"src/main/github/pr-refresh-coordinator-refresh-events.test.ts": 617,
|
||||
"src/main/github/pr-refresh-coordinator-visibility-policy.test.ts": 1371,
|
||||
@@ -2157,7 +2149,6 @@
|
||||
"src/main/ipc/filesystem/filesystem-search-file-paths.test.ts": 443,
|
||||
"src/main/ipc/filesystem/filesystem-symlink-directory-entries.test.ts": 56,
|
||||
"src/main/ipc/filesystem/filesystem-write-bytes.test.ts": 147,
|
||||
"src/main/ipc/filesystem/filesystem-write-identical.test.ts": 138,
|
||||
"src/main/ipc/filesystem/filesystem-wsl-stopped-distro-git.test.ts": 1163,
|
||||
"src/main/ipc/filesystem/filesystem-wsl-stopped-distro-reads.test.ts": 1284,
|
||||
"src/main/ipc/filesystem/filesystem.test.ts": 2043,
|
||||
@@ -2238,7 +2229,6 @@
|
||||
"src/main/ipc/pty/ipc/spawn-push-target-materialization-real-git.test.ts": 1316,
|
||||
"src/main/ipc/pty/ipc/write-input-user-input.test.ts": 84,
|
||||
"src/main/ipc/pty/managed-data-account-spawn-environment.test.ts": 1150,
|
||||
"src/main/ipc/pty/pane/join-pane-spawn.test.ts": 1438,
|
||||
"src/main/ipc/pty/pane/pane-owner-replacement.test.ts": 1960,
|
||||
"src/main/ipc/pty/pane/serializer-document-lifetime.test.ts": 77,
|
||||
"src/main/ipc/pty/pane/spawn-registration.test.ts": 935,
|
||||
@@ -2257,7 +2247,6 @@
|
||||
"src/main/ipc/pty/pty-spawn-cwd-fallback.test.ts": 4680,
|
||||
"src/main/ipc/pty/pty-ssh-stop-verdict.test.ts": 3615,
|
||||
"src/main/ipc/pty/pty-write-ipc-validation.test.ts": 5157,
|
||||
"src/main/ipc/pty/runtime/kill-legacy-relay-bridged.test.ts": 3679,
|
||||
"src/main/ipc/pty/runtime/kill-same-id-respawn.test.ts": 3784,
|
||||
"src/main/ipc/pty/runtime/spawn-preflight-wsl-background-hold.test.ts": 1568,
|
||||
"src/main/ipc/filesystem/watcher-process/rebinding-file-watch-noise.test.ts": 317,
|
||||
@@ -2267,7 +2256,6 @@
|
||||
"src/main/ipc/worktrees/removal/registered-worktree-root-stopped-distro.test.ts": 168,
|
||||
"src/main/ipc/worktrees/removal/registered-worktree-root-wsl-runtime.test.ts": 261,
|
||||
"src/main/ipc/worktrees/removal/registered-worktree-roots-scoped-invalidation.test.ts": 141,
|
||||
"src/main/ipc/renderer-lifetime-abort.test.ts": 65,
|
||||
"src/main/ipc/renderer-shutdown-checkpoint-sleep-records.test.ts": 1914,
|
||||
"src/main/ipc/renderer-shutdown-checkpoint.test.ts": 496,
|
||||
"src/main/ipc/renderer-terminal-serializer-readiness.test.ts": 45,
|
||||
@@ -2308,7 +2296,6 @@
|
||||
"src/main/ipc/ssh/ssh-app-shutdown.test.ts": 4455,
|
||||
"src/main/ipc/ssh/ssh-connect-decision-cancellation.test.ts": 6414,
|
||||
"src/main/ipc/ssh/ssh-credential-cancel.test.ts": 4604,
|
||||
"src/main/ipc/ssh/ssh-disconnect-cancellation.test.ts": 6851,
|
||||
"src/main/ipc/ssh/ssh-host-user-disconnect.test.ts": 4734,
|
||||
"src/main/ipc/ssh/ssh-passphrase.test.ts": 135,
|
||||
"src/main/ipc/ssh/ssh.test.ts": 6905,
|
||||
@@ -2438,7 +2425,6 @@
|
||||
"src/main/managed-server/install/orcad-remote-launch-setsid.test.ts": 342,
|
||||
"src/main/managed-server/install/orcad-remote-launch.test.ts": 319,
|
||||
"src/main/managed-server/install/orcad-remote-node-runtime.test.ts": 248,
|
||||
"src/main/managed-server/install/orcad-remote-primitives.test.ts": 337,
|
||||
"src/main/managed-server/install/orcad-remote-readiness-wait.test.ts": 9063,
|
||||
"src/main/managed-server/install/orcad-remote-runtime.test.ts": 488,
|
||||
"src/main/managed-server/install/orcad-remote-shell-commands.integration.test.ts": 9000,
|
||||
@@ -2474,7 +2460,6 @@
|
||||
"src/main/muse/hook-service.test.ts": 145,
|
||||
"src/main/native-chat/agent-model-catalog/agent-model-catalog-discovery-freshness.test.ts": 390,
|
||||
"src/main/native-chat/agent-model-catalog/agent-model-catalog-entry.test.ts": 75,
|
||||
"src/main/native-chat/agent-model-catalog/agent-model-catalog-persistence.test.ts": 115,
|
||||
"src/main/native-chat/agent-model-catalog/agent-model-catalog-probe-runner.test.ts": 4786,
|
||||
"src/main/native-chat/agent-model-catalog/agent-model-catalog-store.test.ts": 611,
|
||||
"src/main/native-chat/agent-model-catalog/agent-model-catalog-unavailable.test.ts": 465,
|
||||
@@ -2693,7 +2678,6 @@
|
||||
"src/main/orcad/orcad-cli-orchestration.integration.test.ts": 214,
|
||||
"src/main/orcad/orcad-forked-child-paths.test.ts": 337,
|
||||
"src/main/orcad/orcad-help.integration.test.ts": 5016,
|
||||
"src/main/orcad/orcad-host-account-launch-env-store.test.ts": 299,
|
||||
"src/main/orcad/orcad-idle-exit-monitor.test.ts": 106,
|
||||
"src/main/orcad/orcad-instance-lock.test.ts": 315,
|
||||
"src/main/orcad/orcad-launcher-isolation.integration.test.ts": 617,
|
||||
@@ -2791,7 +2775,6 @@
|
||||
"src/main/persistence/moving-wsl-projects/wsl-move-quiesce.test.ts": 413,
|
||||
"src/main/persistence/moving-wsl-projects/wsl-move-real-pause.test.ts": 1939,
|
||||
"src/main/persistence/moving-wsl-projects/wsl-move-reset.test.ts": 891,
|
||||
"src/main/persistence/moving-wsl-projects/wsl-move-restore-hints.test.ts": 65,
|
||||
"src/main/persistence/moving-wsl-projects/wsl-move-retention.test.ts": 2285,
|
||||
"src/main/persistence/moving-wsl-projects/wsl-move-rewrite-ledger.test.ts": 641,
|
||||
"src/main/persistence/moving-wsl-projects/wsl-move-scope-guard.test.ts": 639,
|
||||
@@ -3160,7 +3143,6 @@
|
||||
"src/main/ripgrep/bundled-ripgrep-real.test.ts": 242,
|
||||
"src/main/ripgrep/bundled-ripgrep-text-search.test.ts": 707,
|
||||
"src/main/ripgrep/ripgrep-filename-identity-real.test.ts": 274,
|
||||
"src/main/ripgrep/synchronous-spawn-failure.test.ts": 54,
|
||||
"src/main/ripgrep/text-search-unicode-real.test.ts": 332,
|
||||
"src/main/runtime/session-tabs/acknowledged-terminal-tab-retirement-main-close.test.ts": 6833,
|
||||
"src/main/runtime/session-tabs/acknowledged-terminal-tab-retirement.test.ts": 6423,
|
||||
@@ -3176,7 +3158,6 @@
|
||||
"src/main/runtime/agent-session/records/agent-session-store-load-scale.test.ts": 732,
|
||||
"src/main/runtime/agent-session/agent-session-tab-layout-queue.test.ts": 4061,
|
||||
"src/main/runtime/agent-session/agent-session-conversation-clear-commit.test.ts": 957,
|
||||
"src/main/runtime/agent-session/agent-session-death-evidence-persistence.test.ts": 1113,
|
||||
"src/main/runtime/agent-session/agent-session-launch-directory.test.ts": 926,
|
||||
"src/main/runtime/agent-session/agent-session-tab-layout.test.ts": 275,
|
||||
"src/main/runtime/agent-session/agent-session-unsaved-creation-supersession.test.ts": 726,
|
||||
@@ -3440,7 +3421,6 @@
|
||||
"src/main/runtime/orchestration/db/worker-dispatch/worker-dispatch-abandon.test.ts": 662,
|
||||
"src/main/runtime/orchestration/db/schema/dispatch-consumer-generation-migration.test.ts": 640,
|
||||
"src/main/runtime/orchestration/db/dispatch-context/dispatch-failure-idempotency.test.ts": 544,
|
||||
"src/main/runtime/orchestration/federation/synchronization/federation-ack-checkpoints.test.ts": 38,
|
||||
"src/main/runtime/orchestration/federation/synchronization/federation-acknowledgment-integrity.test.ts": 557,
|
||||
"src/main/runtime/orchestration/db/federation/federation-acknowledgment-migration.test.ts": 566,
|
||||
"src/main/runtime/orchestration/federation/synchronization/federation-sync.test.ts": 5264,
|
||||
@@ -3569,13 +3549,11 @@
|
||||
"src/main/runtime/rpc/methods/browser/browser-network-tunnel.test.ts": 1096,
|
||||
"src/main/runtime/rpc/methods/client-state/client-ui-set-collapsed.test.ts": 1091,
|
||||
"src/main/runtime/rpc/methods/clipboard/clipboard.test.ts": 848,
|
||||
"src/main/runtime/rpc/methods/files/file-watch-event-batcher.test.ts": 69,
|
||||
"src/main/runtime/rpc/methods/files/files-path-search.test.ts": 884,
|
||||
"src/main/runtime/rpc/methods/files/files-read-dir-page.test.ts": 1213,
|
||||
"src/main/runtime/rpc/methods/files/files-read-dir-remote-budget.test.ts": 860,
|
||||
"src/main/runtime/rpc/methods/files/files-watch-cancellation.test.ts": 886,
|
||||
"src/main/runtime/rpc/methods/files/files.test.ts": 1018,
|
||||
"src/main/runtime/rpc/methods/source-control/gitlab/gitlab.test.ts": 4209,
|
||||
"src/main/runtime/rpc/methods/integrations/jira/jira-saved-filter-state.test.ts": 937,
|
||||
"src/main/runtime/rpc/methods/integrations/jira/jira.test.ts": 732,
|
||||
"src/main/runtime/rpc/methods/integrations/linear/linear-agent-access.test.ts": 4648,
|
||||
@@ -3621,7 +3599,6 @@
|
||||
"src/main/runtime/rpc/methods/orchestration/messaging/check-worker-consumer-fencing.test.ts": 5876,
|
||||
"src/main/runtime/rpc/methods/orchestration/messaging/check-worker-federated-attachment.test.ts": 4547,
|
||||
"src/main/runtime/rpc/methods/orchestration/messaging/check.test.ts": 4373,
|
||||
"src/main/runtime/rpc/methods/orchestration/messaging/implicit-task-run-routing.test.ts": 4876,
|
||||
"src/main/runtime/rpc/methods/orchestration/messaging/recipient-routing.test.ts": 4675,
|
||||
"src/main/runtime/rpc/methods/orchestration/messaging/run-bound-mailbox-history.test.ts": 4288,
|
||||
"src/main/runtime/rpc/methods/orchestration/messaging/run-bound-mailbox-interleavings.test.ts": 5049,
|
||||
@@ -3660,12 +3637,10 @@
|
||||
"src/main/runtime/rpc/methods/orchestration/worker/worker-terminal-custody-at-creation.test.ts": 5687,
|
||||
"src/main/runtime/rpc/methods/orchestration/worker/workers-new-worktree.test.ts": 6117,
|
||||
"src/main/runtime/rpc/methods/orchestration/worker/workers-recovery.test.ts": 5182,
|
||||
"src/main/runtime/rpc/methods/pairing/pairing.test.ts": 776,
|
||||
"src/main/runtime/rpc/methods/plugins/plugin-dev-emit.test.ts": 3118,
|
||||
"src/main/runtime/rpc/methods/plugins/plugin-dev-request.test.ts": 3725,
|
||||
"src/main/runtime/rpc/methods/plugins/plugins.test.ts": 274,
|
||||
"src/main/runtime/rpc/methods/preflight/preflight-selected-target.test.ts": 5043,
|
||||
"src/main/runtime/rpc/methods/projects/repo.test.ts": 896,
|
||||
"src/main/runtime/rpc/methods/session-tabs/session-tabs-inventory-census-race.test.ts": 4349,
|
||||
"src/main/runtime/rpc/methods/session-tabs/session-tabs-move-validation.test.ts": 4261,
|
||||
"src/main/runtime/rpc/methods/session-tabs/session-tabs-retirement-proof-delta.test.ts": 1726,
|
||||
@@ -3712,7 +3687,6 @@
|
||||
"src/main/runtime/rpc/mobile-socket-wiring.test.ts": 312,
|
||||
"src/main/runtime/rpc/orcad-feature-readiness.test.ts": 40,
|
||||
"src/main/runtime/rpc/orchestration-11745-regression-verification.test.ts": 22874,
|
||||
"src/main/runtime/rpc/orchestration-caller-show.test.ts": 4209,
|
||||
"src/main/runtime/rpc/orchestration-check-wait-session.test.ts": 5096,
|
||||
"src/main/runtime/rpc/orchestration-commit-notify-characterization.test.ts": 5702,
|
||||
"src/main/runtime/rpc/orchestration-contract-fence.test.ts": 6113,
|
||||
@@ -3733,7 +3707,6 @@
|
||||
"src/main/runtime/rpc/orchestration-party-agreement.test.ts": 4581,
|
||||
"src/main/runtime/rpc/orchestration-run-use-invalid-takeover.test.ts": 4575,
|
||||
"src/main/runtime/rpc/orchestration-runtime-update-settlement.test.ts": 5105,
|
||||
"src/main/runtime/rpc/orchestration-session-caller.test.ts": 5717,
|
||||
"src/main/runtime/rpc/orchestration-session-coordinator.test.ts": 4655,
|
||||
"src/main/runtime/rpc/orchestration-session-recipient.test.ts": 5100,
|
||||
"src/main/runtime/rpc/orchestration-session-run-binding.test.ts": 4448,
|
||||
@@ -3742,7 +3715,6 @@
|
||||
"src/main/runtime/rpc/remote-runtime-server-heartbeat-missed-probe-tolerance.test.ts": 59,
|
||||
"src/main/runtime/rpc/remote-runtime-server-heartbeat.test.ts": 60,
|
||||
"src/main/runtime/rpc/repo-add-cli.integration.test.ts": 1603,
|
||||
"src/main/runtime/rpc/rpc-caller-scope.test.ts": 5071,
|
||||
"src/main/runtime/rpc/runtime-client-capabilities.test.ts": 43,
|
||||
"src/main/runtime/rpc/runtime-source-admission.test.ts": 909,
|
||||
"src/main/runtime/rpc/shadowed-port-probe.test.ts": 99,
|
||||
@@ -3756,7 +3728,6 @@
|
||||
"src/main/runtime/rpc/terminal-multiplex-ack-overflow-recovery-history.test.ts": 1892,
|
||||
"src/main/runtime/rpc/terminal-multiplex-ack-overflow-recovery.test.ts": 2369,
|
||||
"src/main/runtime/rpc/terminal-multiplex-desktop-resize-routing.test.ts": 2160,
|
||||
"src/main/runtime/rpc/terminal-multiplex-endpoint-loss-notice.test.ts": 1852,
|
||||
"src/main/runtime/rpc/terminal-multiplex-initial-snapshot-buffering.test.ts": 2118,
|
||||
"src/main/runtime/rpc/terminal-multiplex-input-sequence-replay.test.ts": 5709,
|
||||
"src/main/runtime/rpc/terminal-multiplex-input-write-rejection.test.ts": 2423,
|
||||
@@ -3769,7 +3740,6 @@
|
||||
"src/main/runtime/rpc/terminal-multiplex-snapshot-query-replay.test.ts": 2715,
|
||||
"src/main/runtime/rpc/terminal-multiplex-snapshot-serialization.test.ts": 2224,
|
||||
"src/main/runtime/rpc/terminal-multiplex-subscribe-slot-recovery.test.ts": 1792,
|
||||
"src/main/runtime/rpc/terminal-opencode-send-guard.integration.test.ts": 4512,
|
||||
"src/main/runtime/rpc/terminal-output-batching.test.ts": 2087,
|
||||
"src/main/runtime/rpc/terminal-output-frame-chunks.test.ts": 70,
|
||||
"src/main/runtime/rpc/terminal-passive-resize-input.test.ts": 2656,
|
||||
@@ -3791,7 +3761,6 @@
|
||||
"src/main/runtime/rpc/unpaired-device-auth-throttle.test.ts": 42,
|
||||
"src/main/runtime/rpc/web-session-pairing.test.ts": 402,
|
||||
"src/main/runtime/rpc/workspace-creator-context.test.ts": 40,
|
||||
"src/main/runtime/rpc/worktree-catalog-snapshot.test.ts": 46,
|
||||
"src/main/runtime/rpc/worktree-removal-marker-projection.test.ts": 497,
|
||||
"src/main/runtime/rpc/worktree-rm-cli.integration.test.ts": 6876,
|
||||
"src/main/runtime/rpc/ws-empty-payload-frame-write.test.ts": 117,
|
||||
@@ -3862,7 +3831,6 @@
|
||||
"src/main/runtime/rpc/server/runtime-rpc-websocket-reply-size-limit.test.ts": 4410,
|
||||
"src/main/runtime/files/search/runtime-search-line-fragments.test.ts": 624,
|
||||
"src/main/runtime/files/server-browse/runtime-server-environment-commands.test.ts": 262,
|
||||
"src/main/runtime/rpc/server/runtime-socket-sweep.test.ts": 47,
|
||||
"src/main/runtime/client/runtime-subscription-registry.test.ts": 142,
|
||||
"src/main/runtime/terminal/records/runtime-terminal-exit-contracts.test.ts": 5325,
|
||||
"src/main/runtime/terminal/identity/runtime-terminal-handle-contracts.test.ts": 3983,
|
||||
@@ -3870,7 +3838,6 @@
|
||||
"src/main/runtime/terminal/query/runtime-terminal-inventory-contracts.test.ts": 4427,
|
||||
"src/main/runtime/terminal/adoption/runtime-terminal-orphan-adoption-rollback.test.ts": 1832,
|
||||
"src/main/runtime/terminal/surface/runtime-terminal-retirement-contracts.test.ts": 4505,
|
||||
"src/main/runtime/terminal/title/runtime-terminal-set-pane-title.test.ts": 5216,
|
||||
"src/main/runtime/terminal/input/runtime-terminal-writer.test.ts": 150,
|
||||
"src/main/runtime/construction/runtime-workspace-launch-contracts.test.ts": 4059,
|
||||
"src/main/runtime/workspaces/session-hosts/runtime-workspace-session-controller-twin-repo.test.ts": 298,
|
||||
@@ -3889,11 +3856,9 @@
|
||||
"src/main/runtime/session-tabs/session-tab-close-force.test.ts": 7089,
|
||||
"src/main/runtime/session-tabs/runtime-inventory-publication-authority.test.ts": 5775,
|
||||
"src/main/runtime/session-tabs/headless-mobile-session-absorbed-rows.test.ts": 4223,
|
||||
"src/main/runtime/session-tabs/main-layout-agent-launch-tabs.test.ts": 4233,
|
||||
"src/main/runtime/session-tabs/main-layout-session-tab-move.test.ts": 4233,
|
||||
"src/main/runtime/session-tabs/session-tab-prototype-dispatch.test.ts": 4888,
|
||||
"src/main/runtime/session-tabs/session-tabs-headless-main-change.test.ts": 4320,
|
||||
"src/main/runtime/session-tabs/session-tabs-held-rows-commit.test.ts": 5046,
|
||||
"src/main/runtime/session-tabs/session-tabs-inventory-publication.test.ts": 4166,
|
||||
"src/main/runtime/session-tabs/session-tabs-main-rows-parity.test.ts": 4606,
|
||||
"src/main/runtime/session-tabs/session-tabs-phone-layout-moves.test.ts": 4153,
|
||||
@@ -3929,7 +3894,6 @@
|
||||
"src/main/runtime/agent-session/lifecycle/structured-agent-cli-writers.test.ts": 140,
|
||||
"src/main/runtime/agent-session/lifecycle/structured-agent-session-history-close-race.test.ts": 5020,
|
||||
"src/main/runtime/agent-session/naming/structured-agent-session-provider-naming-shutdown.test.ts": 1987,
|
||||
"src/main/runtime/agent-session/lifecycle/structured-agent-session-tab-authority.test.ts": 11047,
|
||||
"src/main/runtime/agent-session/environment/structured-agent-shell-environment-catalog.test.ts": 720,
|
||||
"src/main/runtime/orchestration/terminal-authority/structured-sessions/structured-chat-coordinator-mail.test.ts": 38220,
|
||||
"src/main/runtime/orchestration/terminal-authority/structured-sessions/structured-chat-dispatch-worker-lifecycle.test.ts": 9312,
|
||||
@@ -4087,7 +4051,6 @@
|
||||
"src/main/ssh/host-keys/ssh-known-hosts.test.ts": 105,
|
||||
"src/main/ssh/external-editors/zed-ssh-url.test.ts": 55,
|
||||
"src/main/ssh/connection/ssh-connect-config.test.ts": 182,
|
||||
"src/main/ssh/connection/ssh-connection-attribution.test.ts": 44,
|
||||
"src/main/ssh/connection/ssh-connection-auth-fallback.test.ts": 512,
|
||||
"src/main/ssh/connection/ssh-connection-channel-error.test.ts": 461,
|
||||
"src/main/ssh/connection/ssh-connection-channel-open.test.ts": 1230,
|
||||
@@ -4310,7 +4273,6 @@
|
||||
"src/main/telemetry/validator-warn-cache.test.ts": 187,
|
||||
"src/main/telemetry/validator.test.ts": 170,
|
||||
"src/main/telemetry/wsl-usage-reporter.test.ts": 407,
|
||||
"src/main/pty/history/terminal-history-async-delete.test.ts": 374,
|
||||
"src/main/pty/history/terminal-history-gc-fs-call-count.test.ts": 157,
|
||||
"src/main/pty/history/terminal-history-gc.test.ts": 824,
|
||||
"src/main/pty/history/terminal-history-tombstone-retry.test.ts": 715,
|
||||
@@ -4518,7 +4480,6 @@
|
||||
"src/main/wsl/execution/wsl-guest-pane-process-script-cost.test.ts": 213,
|
||||
"src/main/wsl/execution/wsl-guest-pane-processes.test.ts": 48,
|
||||
"src/main/wsl/execution/wsl-guest-process-identity.test.ts": 54,
|
||||
"src/main/wsl/host-lifecycle/wsl-host-automation-wake.test.ts": 288,
|
||||
"src/main/wsl/host-lifecycle/wsl-host-kept-for-peers.test.ts": 159,
|
||||
"src/main/wsl/host-lifecycle/wsl-host-pending-removal.test.ts": 955,
|
||||
"src/main/wsl/host-lifecycle/wsl-host-server-stop.test.ts": 407,
|
||||
@@ -4727,12 +4688,10 @@
|
||||
"src/renderer/src/components/browser-pane/host-guest/browser-page-guest-recovery.test.ts": 515,
|
||||
"src/renderer/src/components/browser-pane/host-guest/browser-page-registration-ownership.test.ts": 2197,
|
||||
"src/renderer/src/components/browser-pane/host-guest/browser-page-viewport.test.ts": 242,
|
||||
"src/renderer/src/components/browser-pane/host-guest/browser-page-window-close.test.ts": 54,
|
||||
"src/renderer/src/components/browser-pane/host-guest/browser-system-resume.test.ts": 187,
|
||||
"src/renderer/src/components/browser-pane/host-guest/use-browser-page-keyboard-shortcuts.split.test.tsx": 2098,
|
||||
"src/renderer/src/components/browser-pane/host-guest/use-browser-page-webview-partition.test.ts": 1985,
|
||||
"src/renderer/src/components/browser-pane/host-guest/webview-registry.test.ts": 103,
|
||||
"src/renderer/src/components/browser-pane/navigate/browser-file-editor-open.test.ts": 1853,
|
||||
"src/renderer/src/components/browser-pane/navigate/browser-load-failure-overlay.test.tsx": 831,
|
||||
"src/renderer/src/components/browser-pane/navigate/browser-page-download-activity.test.ts": 44,
|
||||
"src/renderer/src/components/browser-pane/navigate/browser-webview-load-failure.test.ts": 243,
|
||||
@@ -4807,19 +4766,16 @@
|
||||
"src/renderer/src/components/editor/diff/monaco-view/DiffViewer.word-wrap-lifecycle.test.tsx": 2499,
|
||||
"src/renderer/src/components/editor/EditorConflictReviewSurface.live-record.test.tsx": 1193,
|
||||
"src/renderer/src/components/editor/EditorEditFileSurface.monaco-containment.test.tsx": 2666,
|
||||
"src/renderer/src/components/editor/EditorFileLoadErrorView.test.tsx": 660,
|
||||
"src/renderer/src/components/editor/EditorFileLocationMenuItems.test.tsx": 846,
|
||||
"src/renderer/src/components/editor/EditorPanel.custom-editor-flush.test.tsx": 2478,
|
||||
"src/renderer/src/components/editor/EditorPanel.diff-rich-owner.test.tsx": 2823,
|
||||
"src/renderer/src/components/editor/notebook/cells/IpynbCellEditor.external-sync.test.tsx": 1660,
|
||||
"src/renderer/src/components/editor/notebook/kernel/IpynbKernelToolbar.test.tsx": 736,
|
||||
"src/renderer/src/components/editor/notebook/document/IpynbRendering.test.tsx": 2143,
|
||||
"src/renderer/src/components/editor/markdown-preview/surface/MarkdownPreview.link-routing.interaction.test.tsx": 1127,
|
||||
"src/renderer/src/components/editor/MediaViewer.test.tsx": 308,
|
||||
"src/renderer/src/components/editor/PdfFind.test.tsx": 581,
|
||||
"src/renderer/src/components/editor/PluginBytesCustomEditorSurface.test.tsx": 641,
|
||||
"src/renderer/src/components/editor/PluginEditorNeedsDesktopNotice.test.tsx": 2107,
|
||||
"src/renderer/src/components/editor/PluginFileTypeSuggestion.test.tsx": 2062,
|
||||
"src/renderer/src/components/editor/rich-markdown/instance/RichMarkdownEditor.focus-leave.test.tsx": 4927,
|
||||
"src/renderer/src/components/editor/ai-vault-session-log-content.test.tsx": 1899,
|
||||
"src/renderer/src/components/editor/check-details/actions/check-annotation-path.test.ts": 53,
|
||||
@@ -4864,7 +4820,6 @@
|
||||
"src/renderer/src/components/editor/save/bytes/editor-bytes-documents.test.ts": 690,
|
||||
"src/renderer/src/components/editor/save/commands/editor-child-owner-save.test.tsx": 2480,
|
||||
"src/renderer/src/components/editor/external-changes/editor-clean-tab-draft-invariant.test.ts": 1274,
|
||||
"src/renderer/src/components/editor/editor-dropped-file-open.test.ts": 595,
|
||||
"src/renderer/src/components/editor/external-changes/editor-external-watch-path-index.test.ts": 1754,
|
||||
"src/renderer/src/components/editor/save/commands/editor-file-save-utf8-conversion.test.ts": 1915,
|
||||
"src/renderer/src/components/editor/editor-git-discard-other-workspace.test.ts": 1967,
|
||||
@@ -4872,7 +4827,6 @@
|
||||
"src/renderer/src/components/editor/model-lifetime/identity/editor-model-host-isolation.test.ts": 5906,
|
||||
"src/renderer/src/components/editor/model-lifetime/identity/editor-model-uri-labels.test.ts": 1223,
|
||||
"src/renderer/src/components/editor/editor-path-move-inflight.test.ts": 50,
|
||||
"src/renderer/src/components/editor/external-changes/editor-restored-tab-conflict-scan.recheck.test.ts": 1796,
|
||||
"src/renderer/src/components/editor/external-changes/editor-restored-tab-conflict-scan.test.ts": 1809,
|
||||
"src/renderer/src/components/editor/save/queue/editor-save-disk-precondition.races.test.ts": 1290,
|
||||
"src/renderer/src/components/editor/save/queue/editor-save-disk-precondition.test.ts": 1431,
|
||||
@@ -5255,7 +5209,6 @@
|
||||
"src/renderer/src/components/new-workspace/SmartWorkspaceNameField-repo-slug-routing.test.ts": 342,
|
||||
"src/renderer/src/components/new-workspace/smart-workspace-github-direct-link.test.ts": 347,
|
||||
"src/renderer/src/components/new-workspace/smart-workspace-source-results.test.ts": 136,
|
||||
"src/renderer/src/components/new-workspace/use-jira-composer-project.test.tsx": 279,
|
||||
"src/renderer/src/components/new-workspace/use-jira-source-connection.test.tsx": 278,
|
||||
"src/renderer/src/components/new-workspace/use-jira-url-source.test.tsx": 419,
|
||||
"src/renderer/src/components/new-workspace/use-linear-source-connection.test.tsx": 1335,
|
||||
@@ -5282,7 +5235,6 @@
|
||||
"src/renderer/src/components/plugins/view/plugin-view-data-acts.test.tsx": 3002,
|
||||
"src/renderer/src/components/plugins/view/plugin-view-editor-capture.test.tsx": 1934,
|
||||
"src/renderer/src/components/plugins/view/plugin-view-editor-shot-cleanup.test.ts": 59,
|
||||
"src/renderer/src/components/plugins/view/plugin-view-editor-workspace.test.ts": 99,
|
||||
"src/renderer/src/components/plugins/view/plugin-view-panel-settle.test.ts": 206,
|
||||
"src/renderer/src/components/plugins/view/plugin-view-request-handler-workspace.test.ts": 2307,
|
||||
"src/renderer/src/components/plugins/view/plugin-view-request-handler.test.ts": 2849,
|
||||
@@ -5370,7 +5322,6 @@
|
||||
"src/renderer/src/components/right-sidebar/source-control/review/create-pr-intent-operation-target.test.ts": 1735,
|
||||
"src/renderer/src/components/right-sidebar/source-control/review/use-create-pr-intent-jira-title.test.tsx": 1956,
|
||||
"src/renderer/src/components/right-sidebar/source-control/review/use-pull-request-generation.test.tsx": 2720,
|
||||
"src/renderer/src/components/right-sidebar/ssh-port-forward-dialog.ime-enter.test.tsx": 585,
|
||||
"src/renderer/src/components/right-sidebar/ai-vault/search/use-ai-vault-search.test.tsx": 688,
|
||||
"src/renderer/src/components/right-sidebar/checks-panel/refresh/use-checks-panel-terminal-worktree.test.ts": 2116,
|
||||
"src/renderer/src/components/right-sidebar/hosted-review/creation/use-create-jira-review-title.test.tsx": 1979,
|
||||
@@ -5514,7 +5465,6 @@
|
||||
"src/renderer/src/components/sidebar/host-path-browser/remote-file-browser-entry-actions.test.ts": 289,
|
||||
"src/renderer/src/components/sidebar/worktree-list/listing/rendered-sidebar-worktree-order.test.ts": 1873,
|
||||
"src/renderer/src/components/sidebar/project-admission/drop/sidebar-project-drop.test.ts": 227,
|
||||
"src/renderer/src/components/sidebar/project-admission/host-selection/use-add-repo-windows-drive-refusal.test.tsx": 470,
|
||||
"src/renderer/src/components/sidebar/project-admission/host-selection/use-add-repo-wsl-host-paths.test.tsx": 660,
|
||||
"src/renderer/src/components/sidebar/sidebar-feedback/use-feedback-image-drop.test.tsx": 511,
|
||||
"src/renderer/src/components/sidebar/workspace-board/projection/use-workspace-board-task-status-sync.queued.test.ts": 1022,
|
||||
@@ -5589,7 +5539,6 @@
|
||||
"src/renderer/src/components/status-bar/resource-session-kill-confirmation.test.ts": 53,
|
||||
"src/renderer/src/components/status-bar/resource-session-removal-render-budget.test.tsx": 290,
|
||||
"src/renderer/src/components/status-bar/resource-usage-folder-merge.test.ts": 251,
|
||||
"src/renderer/src/components/status-bar/status-bar-pool-countdown.test.tsx": 654,
|
||||
"src/renderer/src/components/status-bar/usage-pool-label.test.ts": 170,
|
||||
"src/renderer/src/components/status-bar/use-owner-usage-rate-limits.test.tsx": 462,
|
||||
"src/renderer/src/components/status-bar/use-resource-memory-snapshot.test.tsx": 321,
|
||||
@@ -5604,10 +5553,8 @@
|
||||
"src/renderer/src/components/tab-bar/cli-chat-resume-run-close.test.ts": 3079,
|
||||
"src/renderer/src/components/tab-bar/group-tab-order.test.ts": 2091,
|
||||
"src/renderer/src/components/tab-bar/host-agent-launch-preferences-source.test.tsx": 3154,
|
||||
"src/renderer/src/components/tab-bar/recent-tab-switching.test.ts": 1795,
|
||||
"src/renderer/src/components/tab-bar/tab-agent-launch-options.test.ts": 438,
|
||||
"src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.test.ts": 206,
|
||||
"src/renderer/src/components/tab-bar/tab-bar-layout-owner-props.test.ts": 174,
|
||||
"src/renderer/src/components/tab-bar/tab-context-menu.test.tsx": 5620,
|
||||
"src/renderer/src/components/tab-bar/tab-create-entry-classifier.test.ts": 510,
|
||||
"src/renderer/src/components/tab-bar/tab-create-entry-local-path.test.ts": 2614,
|
||||
@@ -5640,7 +5587,6 @@
|
||||
"src/renderer/src/components/task-page/jira/task-page-jira-board-detail-integration.test.tsx": 2192,
|
||||
"src/renderer/src/components/task-page/jira/task-page-jira-board-transitions.test.ts": 69,
|
||||
"src/renderer/src/components/task-page/jira/task-page-jira-board-view.test.tsx": 2287,
|
||||
"src/renderer/src/components/task-page/jira/task-page-jira-cache-selectors.test.ts": 54,
|
||||
"src/renderer/src/components/task-page/jira/task-page-jira-favourite-selection.test.tsx": 3326,
|
||||
"src/renderer/src/components/task-page/jira/task-page-jira-grouping.test.ts": 2324,
|
||||
"src/renderer/src/components/task-page/jira/task-page-jira-linked-issues.test.ts": 204,
|
||||
@@ -5839,7 +5785,6 @@
|
||||
"src/renderer/src/components/terminal-pane/layout/terminal-layout-pty-ownership.test.ts": 99,
|
||||
"src/renderer/src/components/terminal-pane/links/terminal-link-click-fallback.test.ts": 985,
|
||||
"src/renderer/src/components/terminal-pane/links/terminal-link-open-beside-source.test.ts": 947,
|
||||
"src/renderer/src/components/terminal-pane/links/http/terminal-link-osc-url-routing.test.ts": 986,
|
||||
"src/renderer/src/components/terminal-pane/links/terminal-link-paired-click-fallback.test.ts": 989,
|
||||
"src/renderer/src/components/terminal-pane/links/gestures/terminal-link-pointer-gesture.test.ts": 550,
|
||||
"src/renderer/src/components/terminal-pane/links/files/terminal-link-provider-batching.test.ts": 1051,
|
||||
@@ -5858,7 +5803,6 @@
|
||||
"src/renderer/src/components/terminal-pane/layout/split/terminal-pane-split-request-routing.test.ts": 2015,
|
||||
"src/renderer/src/components/terminal-pane/layout/detach/terminal-pane-tab-detach-main-commit.test.ts": 2090,
|
||||
"src/renderer/src/components/terminal-pane/layout/detach/terminal-pane-tab-detach-main-move.test.ts": 2147,
|
||||
"src/renderer/src/components/terminal-pane/parking/verdict/terminal-park-verdict-flip-telemetry.test.ts": 123,
|
||||
"src/renderer/src/components/terminal-pane/parking/output-watchers/terminal-parked-tab-watchers.test.ts": 473,
|
||||
"src/renderer/src/components/terminal-pane/parking/output-watchers/terminal-parked-watcher-registry.test.ts": 245,
|
||||
"src/renderer/src/components/terminal-pane/parking/output-watchers/terminal-parked-watcher-sleep-preserved-exit.test.ts": 399,
|
||||
@@ -5900,7 +5844,6 @@
|
||||
"src/renderer/src/components/terminal/terminal-tab-actions-unrouted-close.test.ts": 589,
|
||||
"src/renderer/src/components/terminal/terminal-tab-actions.test.ts": 626,
|
||||
"src/renderer/src/components/terminal/terminal-tab-close-running-confirm.test.ts": 499,
|
||||
"src/renderer/src/components/terminal/terminal-tab-create.test.ts": 367,
|
||||
"src/renderer/src/components/terminal/unsaved-close-queue.test.ts": 42,
|
||||
"src/renderer/src/components/terminal/use-terminal-provider-snapshot-capability.test.tsx": 573,
|
||||
"src/renderer/src/components/terminal/window-close-running-work.test.ts": 97,
|
||||
@@ -5940,7 +5883,6 @@
|
||||
"src/renderer/src/components/worktree-jump/use-worktree-jump-palette-browser-ownership.test.ts": 2096,
|
||||
"src/renderer/src/components/worktree-jump/use-worktree-jump-palette-task-search.test.tsx": 2140,
|
||||
"src/renderer/src/components/worktree-jump/worktree-jump-palette-browser-navigation.test.tsx": 4470,
|
||||
"src/renderer/src/components/wsl-host-stopped/WslHostStoppedNotice.test.tsx": 491,
|
||||
"src/renderer/src/components/wsl-move/WslMoveDialog.test.tsx": 524,
|
||||
"src/renderer/src/components/wsl-move/wsl-move-dialog-store.test.ts": 2011,
|
||||
"src/renderer/src/components/wsl-move/wsl-move-scope-rows.test.ts": 156,
|
||||
@@ -5975,7 +5917,6 @@
|
||||
"src/renderer/src/hooks/ipc-events/direct-ssh-unrestored-forward-sync.test.ts": 95,
|
||||
"src/renderer/src/hooks/ipc-events/mobile-terminal-close-ipc-bridge-reveal.test.ts": 1764,
|
||||
"src/renderer/src/hooks/ipc-events/mobile-terminal-close-ipc-bridge.test.ts": 2200,
|
||||
"src/renderer/src/hooks/ipc-events/orca-profile-auth-ipc-bridge.test.ts": 1908,
|
||||
"src/renderer/src/hooks/ipc-events/os-markdown-file-open-bridge.test.ts": 280,
|
||||
"src/renderer/src/hooks/ipc-events/quit-and-stop-all-sessions-ipc-bridge.test.ts": 398,
|
||||
"src/renderer/src/hooks/ipc-events/runtime-client-ipc-bridge-ownership.test.ts": 2068,
|
||||
@@ -5998,7 +5939,6 @@
|
||||
"src/renderer/src/hooks/runtime-client-events-sync-ownership.test.ts": 59,
|
||||
"src/renderer/src/hooks/runtime-client-events-sync.test.ts": 118,
|
||||
"src/renderer/src/hooks/runtime-project-refresh-scheduler.test.ts": 65,
|
||||
"src/renderer/src/hooks/shortcut-label-cache.test.tsx": 2021,
|
||||
"src/renderer/src/hooks/unpaired-device-auth-notification.test.ts": 41,
|
||||
"src/renderer/src/hooks/use-document-dark-theme.test.tsx": 350,
|
||||
"src/renderer/src/hooks/use-list-multi-selection.test.tsx": 545,
|
||||
@@ -6031,7 +5971,6 @@
|
||||
"src/renderer/src/hooks/useIpcEvents-agent-status-queue-ordering.test.ts": 3680,
|
||||
"src/renderer/src/hooks/useIpcEvents-agent-status-snapshot-replay.test.ts": 4119,
|
||||
"src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts": 3310,
|
||||
"src/renderer/src/hooks/useIpcEvents-browser-navigation.test.ts": 1511,
|
||||
"src/renderer/src/hooks/useIpcEvents-close-routing-session-tabs.test.ts": 5812,
|
||||
"src/renderer/src/hooks/useIpcEvents-new-workspace-shortcut.test.ts": 72,
|
||||
"src/renderer/src/hooks/useIpcEvents-runtime-environment-selectors.test.ts": 530,
|
||||
@@ -6146,7 +6085,6 @@
|
||||
"src/renderer/src/lib/monaco-languages/register-vue.test.ts": 303,
|
||||
"src/renderer/src/lib/monaco-languages/register-zig.test.ts": 4750,
|
||||
"src/renderer/src/lib/monaco-languages/textmate-token-provider.test.ts": 245,
|
||||
"src/renderer/src/lib/monaco-languages/zig-highlighting.test.ts": 7984,
|
||||
"src/renderer/src/lib/agent-session/resume/cli-fork/native-chat-cli-fork-preparation.test.ts": 2773,
|
||||
"src/renderer/src/lib/simulator/launch/open-mobile-emulator-tab.test.ts": 545,
|
||||
"src/renderer/src/lib/pane-manager/pane-close-focus-successor.test.ts": 1123,
|
||||
@@ -6331,10 +6269,8 @@
|
||||
"src/renderer/src/store/plugins/catalog/plugin-panel-retry-timer-cleanup.test.ts": 5216,
|
||||
"src/renderer/src/store/plugins/catalog/plugin-panels.test.ts": 2142,
|
||||
"src/renderer/src/store/projects/project-catalog-null-field-ingest.test.ts": 309,
|
||||
"src/renderer/src/store/diagnostics/react-commit-cascade-write-probe.test.ts": 602,
|
||||
"src/renderer/src/store/repos/dropped-repo-worktree-rows.test.ts": 1738,
|
||||
"src/renderer/src/store/repos/repo-removal.test.ts": 799,
|
||||
"src/renderer/src/store/repos/safe-auto-fork-sync.test.ts": 446,
|
||||
"src/renderer/src/store/repos/wsl-moved-local-session-rows.test.ts": 319,
|
||||
"src/renderer/src/store/repos/wsl-moved-repo-ui-retention.test.ts": 2920,
|
||||
"src/renderer/src/store/slices/runtime-status/views/runtime-status-subscriptions.test.ts": 182,
|
||||
@@ -6398,7 +6334,6 @@
|
||||
"src/renderer/src/store/slices/editor/buffers/editor-buffer-reconcile-identity.test.ts": 1775,
|
||||
"src/renderer/src/store/slices/editor/buffers/editor-buffer-reconcile-joins.test.ts": 1766,
|
||||
"src/renderer/src/store/slices/editor/buffers/editor-buffer-rejoin.test.ts": 1843,
|
||||
"src/renderer/src/store/slices/editor/buffers/editor-buffer-views.test.ts": 2004,
|
||||
"src/renderer/src/store/slices/editor/file-ids/floating-editor-file-id-layout-owner.test.ts": 933,
|
||||
"src/renderer/src/store/slices/worktrees/session/folder-workspace-activation-and-activity.test.ts": 2358,
|
||||
"src/renderer/src/store/slices/diff-comments/folder-workspace-diff-comments.test.ts": 1042,
|
||||
@@ -6481,7 +6416,6 @@
|
||||
"src/renderer/src/store/slices/tabs/tabs-hydration-group-validation.test.ts": 249,
|
||||
"src/renderer/src/store/slices/tabs/tabs-hydration.test.ts": 273,
|
||||
"src/renderer/src/store/slices/tabs/tabs-label-and-pin-state.test.ts": 1921,
|
||||
"src/renderer/src/store/slices/tabs/tabs-model-reconciliation.test.ts": 1800,
|
||||
"src/renderer/src/store/slices/tabs/tabs-open-close-lifecycle.test.ts": 2140,
|
||||
"src/renderer/src/store/slices/tabs/tabs-pane-layout-operations.test.ts": 1652,
|
||||
"src/renderer/src/store/slices/tabs/tabs-unread-and-focus.test.ts": 1801,
|
||||
@@ -6501,7 +6435,6 @@
|
||||
"src/renderer/src/store/slices/ui/ui-modal-slot-dismissal.test.ts": 662,
|
||||
"src/renderer/src/store/slices/ui/ui-slice-surface-actions.test.ts": 1760,
|
||||
"src/renderer/src/store/slices/worktrees/session/workspace-activation-preparation.test.ts": 1932,
|
||||
"src/renderer/src/store/slices/workspace-attention/workspace-unread-characterization.test.ts": 1974,
|
||||
"src/renderer/src/store/slices/workspace-attention/workspace-unread-flag-host-raises.test.ts": 1914,
|
||||
"src/renderer/src/store/slices/workspace-attention/workspace-unread-on-screen-characterization.test.ts": 1780,
|
||||
"src/renderer/src/store/slices/workspace-attention/workspace-unread-screen-moves-characterization.test.ts": 1884,
|
||||
@@ -6559,7 +6492,6 @@
|
||||
"src/renderer/src/store/terminals/terminal-no-op-subscriber.test.ts": 1763,
|
||||
"src/renderer/src/store/terminals/terminal-startup-queue-restart.test.ts": 1895,
|
||||
"src/renderer/src/store/terminals/terminal-tab-close-intent.test.ts": 1945,
|
||||
"src/renderer/src/store/terminals/workspace-terminal-placeholders.test.ts": 302,
|
||||
"src/renderer/src/store/slices/runtime-status/views/use-runtime-status-view.test.tsx": 1979,
|
||||
"src/renderer/src/store/workspace-layout/workspace-layout-cache.test.ts": 63,
|
||||
"src/renderer/src/store/slices/worktrees/listing/worktree-host-collision-index.test.ts": 1875,
|
||||
@@ -7050,7 +6982,6 @@
|
||||
"src/shared/orca-session-address.test.ts": 154,
|
||||
"src/shared/orca-yaml-alias-bounds.test.ts": 425,
|
||||
"src/shared/orca-yaml-bounds.test.ts": 192,
|
||||
"src/shared/orcad-artifacts.test.ts": 45,
|
||||
"src/shared/orcad-build-identity.test.ts": 48,
|
||||
"src/shared/orcad-migration-manifest-contract.test.ts": 323,
|
||||
"src/shared/orcad-migration/orcad-migration-blob-transfer.test.ts": 348,
|
||||
|
||||
@@ -27,7 +27,6 @@ const DESKTOP_SUITE_PREFIXES = [
|
||||
'src/main/runtime/agent-session/agent-session-account-',
|
||||
'src/main/runtime/agent-session/agent-session-acquisition-',
|
||||
'src/main/runtime/agent-session/agent-session-conversation-',
|
||||
'src/main/runtime/agent-session/agent-session-death-',
|
||||
'src/main/runtime/agent-session/agent-session-launch-',
|
||||
'src/main/runtime/agent-session/agent-session-operation-',
|
||||
'src/main/runtime/agent-session/agent-session-rewind-',
|
||||
|
||||
@@ -8,7 +8,6 @@ const contracts = [
|
||||
'src/main/runtime/session-tabs/session-tabs-phone-layout-moves.test.ts',
|
||||
'src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-terminal-writer-acquire.test.ts',
|
||||
'src/shared/agent-launch/permissions/tui-agent-permission-choice.test.ts',
|
||||
'src/main/runtime/rpc/methods/projects/repo.test.ts',
|
||||
'src/renderer/src/components/tab-bar/use-new-tab-agent-menu-host-preferences.test.tsx',
|
||||
'src/main/sqlite/sync-database.test.ts',
|
||||
'src/main/runtime/orchestration/db/row-column-lists.test.ts',
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { resolveDiagnosticsHostId } from './connection-diagnostics-screen-data'
|
||||
|
||||
import type { HostProfile } from '../host-catalog/host-profile'
|
||||
|
||||
const host = (id: string): HostProfile => ({
|
||||
id,
|
||||
name: id,
|
||||
endpoint: 'ws://192.168.1.2:6768',
|
||||
deviceToken: 'token',
|
||||
publicKeyB64: 'key',
|
||||
lastConnected: 0
|
||||
})
|
||||
|
||||
describe('connection diagnostics screen data', () => {
|
||||
it('applies route changes synchronously without discarding a chip choice for the same route', () => {
|
||||
const hosts = [host('a'), host('b')]
|
||||
expect(resolveDiagnosticsHostId(hosts, undefined, null)).toBe('a')
|
||||
expect(resolveDiagnosticsHostId(hosts, 'b', { hostId: 'a', requestedHostId: 'a' })).toBe('b')
|
||||
expect(resolveDiagnosticsHostId(hosts, 'b', { hostId: 'a', requestedHostId: 'b' })).toBe('a')
|
||||
})
|
||||
|
||||
it('does not revive a stale manual choice when routing away and back', () => {
|
||||
const hosts = [host('a'), host('b'), host('c')]
|
||||
const firstRouteA = {}
|
||||
const routeC = {}
|
||||
const secondRouteA = {}
|
||||
const manualSelection = { hostId: 'b', requestedHostId: 'a', routeKey: firstRouteA }
|
||||
|
||||
expect(resolveDiagnosticsHostId(hosts, 'c', manualSelection, routeC)).toBe('c')
|
||||
expect(resolveDiagnosticsHostId(hosts, 'a', manualSelection, secondRouteA)).toBe('a')
|
||||
})
|
||||
})
|
||||
@@ -1,70 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
installChatViewHooks,
|
||||
bannerText,
|
||||
pressSend,
|
||||
render,
|
||||
update
|
||||
} from './MobileNativeChatView.harness.test-fixture'
|
||||
|
||||
vi.mock('react-native', async () => {
|
||||
const { reactNativeStub } = await import('./MobileNativeChatView.render-stubs.test-fixture')
|
||||
return reactNativeStub()
|
||||
})
|
||||
vi.mock('react-native-safe-area-context', () => ({
|
||||
useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 })
|
||||
}))
|
||||
vi.mock('react-native-gesture-handler', async () => {
|
||||
const { gestureHandlerStub } = await import('./MobileNativeChatView.render-stubs.test-fixture')
|
||||
return gestureHandlerStub()
|
||||
})
|
||||
vi.mock('lucide-react-native', () => ({
|
||||
ArrowDown: 'ArrowDown',
|
||||
ChevronsDownUp: 'ChevronsDownUp',
|
||||
ChevronsUpDown: 'ChevronsUpDown',
|
||||
Square: 'Square'
|
||||
}))
|
||||
vi.mock('./transcript/MobileNativeChatMessage', () => ({
|
||||
MobileNativeChatMessage: 'ChatMessage'
|
||||
}))
|
||||
vi.mock('./transcript/MobileNativeChatLiveLine', () => ({
|
||||
MobileNativeChatLiveLine: 'LiveStatus'
|
||||
}))
|
||||
vi.mock('./input/MobileNativeChatAsk', () => ({ MobileNativeChatAsk: 'ChatAsk' }))
|
||||
vi.mock('./input/MobileNativeChatPermission', () => ({
|
||||
MobileNativeChatPermission: 'ChatPermission'
|
||||
}))
|
||||
vi.mock('./input/MobileNativeChatQuestion', () => ({
|
||||
MobileNativeChatQuestion: 'ChatQuestion'
|
||||
}))
|
||||
vi.mock('../../components/ActionSheetModal', () => ({ ActionSheetModal: 'ActionSheetModal' }))
|
||||
vi.mock('./composer/MobileNativeChatComposer', async () => {
|
||||
const { composerStub } = await import('./MobileNativeChatView.render-stubs.test-fixture')
|
||||
return composerStub()
|
||||
})
|
||||
|
||||
describe('MobileNativeChatView: send and read failure banners', () => {
|
||||
installChatViewHooks()
|
||||
|
||||
it('retires the previous banner before sending and preserves the new delivery notice', async () => {
|
||||
const onClearSendError = vi.fn()
|
||||
await render({
|
||||
sendErrorMessage: 'Stop failed',
|
||||
onClearSendError,
|
||||
onSend: async () => {
|
||||
expect(onClearSendError).toHaveBeenCalledOnce()
|
||||
await update({ sendErrorMessage: 'Delivery unconfirmed' })
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
await pressSend()
|
||||
|
||||
expect(onClearSendError).toHaveBeenCalledOnce()
|
||||
expect(bannerText()).toBe('Delivery unconfirmed')
|
||||
})
|
||||
})
|
||||
|
||||
vi.mock('./input/MobileQueuedMessageComposer', () => ({
|
||||
MobileQueuedMessageComposer: 'QueuedMessageComposer'
|
||||
}))
|
||||
@@ -1,189 +0,0 @@
|
||||
import { resolveMobileNativeChatRowProps } from './transcript/use-mobile-native-chat-row-props'
|
||||
import { createElement, type ReactElement } from 'react'
|
||||
import { act, create, type ReactTestInstance, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { afterEach, beforeEach, expect, vi } from 'vitest'
|
||||
import type { NativeChatMessage } from '../../../../src/shared/native-chat/native-chat-types'
|
||||
import { MobileNativeChatView } from './MobileNativeChatView'
|
||||
import { setMobileNativeChatDraftText } from './composer/mobile-native-chat-draft-store'
|
||||
import {
|
||||
scrollToEnd,
|
||||
scrollToOffset,
|
||||
pageScroller
|
||||
} from './MobileNativeChatView.render-stubs.test-fixture'
|
||||
|
||||
vi.mock('../../components/DragReorderList', () => ({ DragReorderList: 'DragReorderList' }))
|
||||
vi.mock('react-native-reanimated', async () => {
|
||||
const { useRef } = await import('react')
|
||||
return {
|
||||
default: { ScrollView: 'ScrollView' },
|
||||
useAnimatedRef: () => useRef(null),
|
||||
useSharedValue: (value: unknown) => useRef({ value }).current
|
||||
}
|
||||
})
|
||||
|
||||
export type Overrides = {
|
||||
messages?: Parameters<typeof MobileNativeChatView>[0]['messages']
|
||||
folded?: Parameters<typeof MobileNativeChatView>[0]['folded']
|
||||
streaming?: string | null
|
||||
sendErrorMessage?: string | null
|
||||
onClearSendError?: () => void
|
||||
inputLockReason?: 'disconnected' | 'waiting' | null
|
||||
composerText?: string
|
||||
onSend?: (text: string) => Promise<boolean>
|
||||
pending?: Parameters<typeof MobileNativeChatView>[0]['pending']
|
||||
structuredActivityUi?: boolean
|
||||
turnIndicator?: Parameters<typeof MobileNativeChatView>[0]['turnIndicator']
|
||||
agentWorking?: boolean
|
||||
canStop?: boolean
|
||||
ask?: Parameters<typeof MobileNativeChatView>[0]['ask']
|
||||
askRequiresTerminal?: boolean
|
||||
collapsedPrompt?: Parameters<typeof MobileNativeChatView>[0]['collapsedPrompt']
|
||||
micActive?: boolean
|
||||
micProcessing?: boolean
|
||||
composerTray?: Parameters<typeof MobileNativeChatView>[0]['composerTray']
|
||||
question?: Parameters<typeof MobileNativeChatView>[0]['question']
|
||||
permission?: Parameters<typeof MobileNativeChatView>[0]['permission']
|
||||
sendSurfaceId?: string
|
||||
keyboardInset?: number
|
||||
hasMore?: boolean
|
||||
olderPages?: Parameters<typeof MobileNativeChatView>[0]['olderPages']
|
||||
onLoadEarlier?: () => void
|
||||
status?: Parameters<typeof MobileNativeChatView>[0]['status']
|
||||
error?: string
|
||||
readFailedFinally?: boolean
|
||||
actionablePromptIds?: readonly string[]
|
||||
}
|
||||
|
||||
export function assistantTurn(id: string, text: string): NativeChatMessage {
|
||||
return { id, role: 'assistant', blocks: [{ type: 'text', text }], timestamp: 0, source: 'hook' }
|
||||
}
|
||||
|
||||
const DRAFT_KEY = 'draft-a'
|
||||
|
||||
function chatViewElement({
|
||||
composerText = '',
|
||||
...overrides
|
||||
}: Overrides): ReturnType<typeof createElement> {
|
||||
setMobileNativeChatDraftText(DRAFT_KEY, composerText)
|
||||
return createElement(MobileNativeChatView, {
|
||||
messages: [],
|
||||
folded: [],
|
||||
status: 'ready',
|
||||
streaming: null,
|
||||
onSend: vi.fn().mockResolvedValue(true),
|
||||
sendSurfaceId: 'tab-a',
|
||||
getSendCompletionGeneration: () => 0,
|
||||
getComposerEditGeneration: () => 0,
|
||||
pending: [],
|
||||
draftKey: DRAFT_KEY,
|
||||
onComposerTextChange: vi.fn(),
|
||||
...overrides
|
||||
})
|
||||
}
|
||||
|
||||
let renderer: ReactTestRenderer | null = null
|
||||
|
||||
export function installChatViewHooks(): void {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
|
||||
setTimeout(() => callback(0), 0)
|
||||
)
|
||||
vi.stubGlobal('cancelAnimationFrame', (handle: ReturnType<typeof setTimeout>) =>
|
||||
clearTimeout(handle)
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
act(() => renderer?.unmount())
|
||||
renderer = null
|
||||
scrollToEnd.mockReset()
|
||||
scrollToOffset.mockReset()
|
||||
Object.assign(pageScroller, { scrollTop: 0, scrollHeight: 0, clientHeight: 500 })
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
}
|
||||
|
||||
export function root(): ReactTestInstance {
|
||||
return renderer!.root
|
||||
}
|
||||
|
||||
// Mocked react-native components are string tags that ReactTestInstance's ElementType omits.
|
||||
export function hostType(node: ReactTestInstance): unknown {
|
||||
return node.type
|
||||
}
|
||||
|
||||
export async function render(overrides: Overrides = {}): Promise<void> {
|
||||
await act(async () => {
|
||||
renderer = create(chatViewElement(overrides))
|
||||
})
|
||||
}
|
||||
|
||||
export async function update(overrides: Overrides = {}): Promise<void> {
|
||||
await act(async () => {
|
||||
renderer?.update(chatViewElement(overrides))
|
||||
})
|
||||
}
|
||||
|
||||
export function listIds(): string[] {
|
||||
return (list().props.data as { id: string }[]).map((row) => row.id)
|
||||
}
|
||||
|
||||
export function list(): ReactTestInstance {
|
||||
return renderer!.root.find((node) => hostType(node) === 'FlatList')
|
||||
}
|
||||
|
||||
export function renderedRow(id: string): ReactElement<Record<string, unknown>> {
|
||||
const listNode = list()
|
||||
const data = listNode.props.data as NativeChatMessage[]
|
||||
const index = data.findIndex((row) => row.id === id)
|
||||
const row = listNode.props.renderItem({ item: data[index], index })
|
||||
const resolved = resolveMobileNativeChatRowProps(index, data[index]!, row.props.lookups)
|
||||
return createElement('Row', { ...row.props, ...resolved })
|
||||
}
|
||||
|
||||
export function banners(): ReactTestInstance[] {
|
||||
return renderer!.root.findAll((node) => node.props.accessibilityRole === 'alert')
|
||||
}
|
||||
|
||||
export function composer(): ReactTestInstance {
|
||||
return renderer!.root.find((node) => hostType(node) === 'Composer')
|
||||
}
|
||||
|
||||
export function bannerText(): string {
|
||||
const [alert, ...rest] = banners()
|
||||
expect(rest).toHaveLength(0)
|
||||
return alert
|
||||
.findAll((node) => hostType(node) === 'Text')
|
||||
.map((node) => node.props.children)
|
||||
.join('')
|
||||
}
|
||||
|
||||
export async function pressSend(): Promise<void> {
|
||||
const composer = renderer!.root.find((node) => hostType(node) === 'Composer')
|
||||
await act(async () => {
|
||||
await composer.props.onPress()
|
||||
})
|
||||
}
|
||||
|
||||
export function scrollEvent(y: number, contentHeight: number) {
|
||||
return {
|
||||
nativeEvent: {
|
||||
contentOffset: { y },
|
||||
contentSize: { height: contentHeight },
|
||||
layoutMeasurement: { height: 500 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function scrollAwayFromTail(): Promise<void> {
|
||||
await act(async () => {
|
||||
list().props.onScrollBeginDrag?.({})
|
||||
list().props.onScroll({
|
||||
nativeEvent: {
|
||||
contentOffset: { y: 200 },
|
||||
contentSize: { height: 1_200 },
|
||||
layoutMeasurement: { height: 500 }
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
const scrollToEnd = vi.hoisted(() => vi.fn())
|
||||
const scrollToOffset = vi.hoisted(() => vi.fn())
|
||||
const platform = vi.hoisted(() => ({ OS: 'ios' }))
|
||||
const pageScroller = vi.hoisted(() => ({
|
||||
scrollTop: 0,
|
||||
scrollHeight: 0,
|
||||
clientHeight: 500,
|
||||
getInnerViewNode: () => ({}),
|
||||
getBoundingClientRect: () => ({ top: 0, bottom: 500 }),
|
||||
getAttribute: () => null,
|
||||
querySelectorAll: () => [],
|
||||
querySelector: () => null,
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {}
|
||||
}))
|
||||
|
||||
export async function reactNativeStub() {
|
||||
const React = await import('react')
|
||||
const { reactNativeMotionStub } = await import('../../test-support/react-native-motion-stub')
|
||||
return {
|
||||
...reactNativeMotionStub(),
|
||||
ActivityIndicator: 'ActivityIndicator',
|
||||
FlatList: React.forwardRef((props, ref) => {
|
||||
const handle = { scrollToEnd, scrollToOffset, getScrollableNode: () => pageScroller }
|
||||
React.useImperativeHandle(ref, () => handle, [])
|
||||
return React.createElement('FlatList', props)
|
||||
}),
|
||||
Platform: platform,
|
||||
Pressable: 'Pressable',
|
||||
ScrollView: React.forwardRef((props, ref) => {
|
||||
const handle = {
|
||||
scrollTo: ({ y, animated }: { y: number; animated: boolean }) =>
|
||||
scrollToOffset({ animated, offset: y }),
|
||||
scrollToEnd,
|
||||
getScrollableNode: () => pageScroller,
|
||||
getInnerViewNode: () => pageScroller.getInnerViewNode()
|
||||
}
|
||||
React.useImperativeHandle(ref, () => handle, [])
|
||||
return React.createElement('ScrollView', props)
|
||||
}),
|
||||
StyleSheet: { create: (styles: unknown) => styles, hairlineWidth: 1 },
|
||||
Text: 'Text',
|
||||
View: 'View'
|
||||
}
|
||||
}
|
||||
|
||||
export function gestureHandlerStub() {
|
||||
const chain = {
|
||||
runOnJS: () => chain,
|
||||
onStart: () => chain,
|
||||
onUpdate: () => chain
|
||||
}
|
||||
return {
|
||||
Gesture: { Simultaneous: () => ({}), Native: () => ({}), Pinch: () => chain },
|
||||
GestureDetector: 'GestureDetector',
|
||||
GestureHandlerRootView: 'GestureHandlerRootView'
|
||||
}
|
||||
}
|
||||
|
||||
export async function composerStub() {
|
||||
const React = await import('react')
|
||||
const { MobileNativeChatStopButton } = await import('./input/MobileNativeChatStopButton')
|
||||
const { useMobileNativeChatDraft } = await import('./composer/mobile-native-chat-draft-store')
|
||||
return {
|
||||
MobileNativeChatDraftComposer: ({
|
||||
draftKey,
|
||||
...props
|
||||
}: {
|
||||
draftKey: string | null
|
||||
onSend: (text: string) => Promise<boolean>
|
||||
disabled?: boolean
|
||||
placeholder?: string
|
||||
stop?: { onStop?: () => void; held: boolean } | null
|
||||
}) =>
|
||||
React.createElement(
|
||||
'Composer',
|
||||
{
|
||||
...props,
|
||||
value: useMobileNativeChatDraft(draftKey),
|
||||
accessibilityLabel: 'Send message',
|
||||
onPress: () => props.onSend('hi')
|
||||
},
|
||||
props.stop ? React.createElement(MobileNativeChatStopButton, props.stop) : null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export { scrollToEnd, scrollToOffset, platform, pageScroller }
|
||||
-65
@@ -1,65 +0,0 @@
|
||||
import { createElement } from 'react'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useMobileNativeChatDrafts } from './use-mobile-native-chat-drafts'
|
||||
import {
|
||||
closeMobileNativeChatTabDraft,
|
||||
useMobileNativeChatDraft
|
||||
} from './mobile-native-chat-draft-store'
|
||||
|
||||
import { loadMobileNativeChatDrafts } from './mobile-native-chat-draft-persistence'
|
||||
|
||||
vi.mock('@react-native-async-storage/async-storage', async () => {
|
||||
const { createInMemoryAsyncStorage } =
|
||||
await import('../../../test-support/in-memory-async-storage')
|
||||
return { default: createInMemoryAsyncStorage() }
|
||||
})
|
||||
|
||||
describe('useMobileNativeChatDrafts with queued cards', () => {
|
||||
let renderer: ReactTestRenderer | null = null
|
||||
let state: (ReturnType<typeof useMobileNativeChatDrafts> & { composerText: string }) | null = null
|
||||
|
||||
afterEach(async () => {
|
||||
act(() => renderer?.unmount())
|
||||
await loadMobileNativeChatDrafts('host', 'worktree')
|
||||
closeMobileNativeChatTabDraft('host\0worktree\0a')
|
||||
await loadMobileNativeChatDrafts('host', 'worktree')
|
||||
renderer = null
|
||||
state = null
|
||||
})
|
||||
|
||||
function Harness(): null {
|
||||
const drafts = useMobileNativeChatDrafts({
|
||||
hostId: 'host',
|
||||
worktreeId: 'worktree',
|
||||
tabId: 'a',
|
||||
sessionId: 'session-a',
|
||||
messages: [],
|
||||
launchDraft: null,
|
||||
transcriptLoading: false,
|
||||
transcriptSettled: true
|
||||
})
|
||||
state = { ...drafts, composerText: useMobileNativeChatDraft(drafts.draftKey) }
|
||||
return null
|
||||
}
|
||||
|
||||
async function render(): Promise<void> {
|
||||
await act(async () => {
|
||||
renderer = create(createElement(Harness))
|
||||
})
|
||||
}
|
||||
|
||||
it("reports whether a queued card's Edit copy landed, so Edit never deletes text it did not keep", async () => {
|
||||
await render()
|
||||
let copied: boolean | undefined
|
||||
act(() => {
|
||||
copied = state?.appendComposerText('')
|
||||
})
|
||||
expect(copied).toBe(false)
|
||||
act(() => {
|
||||
copied = state?.appendComposerText('edited text')
|
||||
})
|
||||
expect(copied).toBe(true)
|
||||
expect(state?.composerText).toBe('edited text')
|
||||
})
|
||||
})
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { expect, it } from 'vitest'
|
||||
import { MobileNativeChatScrollRoots } from './mobile-native-chat-scroll-roots'
|
||||
|
||||
it("forgets a closed tool line's nested command and output positions, not its neighbours'", () => {
|
||||
const at = { top: 30, followsEnd: false }
|
||||
const memory = new Map(
|
||||
['run:o', 'run:o/line:a:command', 'run:o/line:a:output', 'line:a:command']
|
||||
.concat(['run:o/line:ab:command', 'diff:src/line:a.ts'])
|
||||
.map((address) => [address, at] as const)
|
||||
)
|
||||
new MobileNativeChatScrollRoots(memory, 10).forget('line:a')
|
||||
expect([...memory.keys()]).toEqual(['run:o', 'run:o/line:ab:command', 'diff:src/line:a.ts'])
|
||||
})
|
||||
@@ -1,57 +0,0 @@
|
||||
import { createElement } from 'react'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { RpcClient } from '../rpc/rpc-client-contract'
|
||||
import { useHostStatusGates, type HostStatusGates } from './host-status-gates'
|
||||
|
||||
const recordHostAppVersionMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined))
|
||||
|
||||
vi.mock('../../host-descriptors/host-app-version-store', () => ({
|
||||
recordHostAppVersion: (...args: unknown[]) => recordHostAppVersionMock(...args)
|
||||
}))
|
||||
|
||||
const recordDescriptorFromStatusMock = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('../../host-descriptors/host-descriptor-recorder', () => ({
|
||||
recordHostDescriptorFromStatus: (...args: unknown[]) => recordDescriptorFromStatusMock(...args)
|
||||
}))
|
||||
|
||||
describe('useHostStatusGates', () => {
|
||||
beforeEach(() => {
|
||||
recordDescriptorFromStatusMock.mockClear()
|
||||
})
|
||||
|
||||
it('fails closed when the same host reconnects on a replaced client', async () => {
|
||||
const firstClient = {
|
||||
sendRequest: vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
result: { capabilities: ['browser.screencast.v1'], floatingWorkspaceEnabled: true }
|
||||
})
|
||||
} as unknown as RpcClient
|
||||
const secondClient = {
|
||||
sendRequest: vi.fn().mockReturnValue(new Promise(() => {}))
|
||||
} as unknown as RpcClient
|
||||
let gates: HostStatusGates | undefined
|
||||
let renderer: ReactTestRenderer | undefined
|
||||
|
||||
function Probe({ client }: { client: RpcClient }): null {
|
||||
gates = useHostStatusGates({ hostId: 'host-1', client, connState: 'connected' })
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
renderer = create(createElement(Probe, { client: firstClient }))
|
||||
await Promise.resolve()
|
||||
})
|
||||
expect(gates?.hostCapabilities).toEqual(['browser.screencast.v1'])
|
||||
|
||||
await act(async () => {
|
||||
renderer?.update(createElement(Probe, { client: secondClient }))
|
||||
})
|
||||
expect(gates).toMatchObject({ hostCapabilities: [], statusPending: true })
|
||||
} finally {
|
||||
renderer?.unmount()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,40 +0,0 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { gunzipSync } from 'fflate'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { encodeMobileWebBundleRange } from '../../../../src/main/runtime/rpc/methods/mobile-delivery/mobile-web-bundle-range-encoding'
|
||||
import { MOBILE_WEB_BUNDLE_RANGE_BYTES } from '../../../../src/shared/mobile-web-bundle/bundle-rpc-contract'
|
||||
|
||||
/** Script-like and a full range long, so the host's level-6 deflate emits dynamic Huffman blocks. */
|
||||
function scriptRange(): Buffer {
|
||||
const words = ['const ', 'function ', 'return ', 'export ', '=> ', '{', '}', ';\n', 'orca']
|
||||
let text = ''
|
||||
for (let index = 0; text.length < MOBILE_WEB_BUNDLE_RANGE_BYTES; index += 1) {
|
||||
text += words[(index * 7) % words.length] + String(index % 1000)
|
||||
}
|
||||
return Buffer.from(text.slice(0, MOBILE_WEB_BUNDLE_RANGE_BYTES))
|
||||
}
|
||||
|
||||
describe('a host-encoded range decoded by the phone', () => {
|
||||
it('inflates node gzip output with fflate to the same bytes', async () => {
|
||||
const raw = scriptRange()
|
||||
const encoded = await encodeMobileWebBundleRange(raw)
|
||||
|
||||
expect(encoded.encoding).toBe('gzip')
|
||||
const inflated = gunzipSync(new Uint8Array(encoded.bytes), {
|
||||
out: new Uint8Array(raw.byteLength + 1)
|
||||
})
|
||||
expect(Buffer.from(inflated).equals(raw)).toBe(true)
|
||||
})
|
||||
|
||||
it('leaves incompressible bytes as identity, which needs no decoder', async () => {
|
||||
const raw = Buffer.concat(
|
||||
Array.from({ length: 128 }, (_, index) =>
|
||||
createHash('sha256').update(`range-${index}`).digest()
|
||||
)
|
||||
)
|
||||
const encoded = await encodeMobileWebBundleRange(raw)
|
||||
|
||||
expect(encoded.encoding).toBe('identity')
|
||||
expect(encoded.bytes.equals(raw)).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,43 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { OrchestrationRequestEvidence } from './orchestration-compatibility-envelope'
|
||||
|
||||
const CHAT = { ORCA_AGENT_SESSION_ID: '11111111-1111-4111-8111-111111111111' }
|
||||
|
||||
describe('caller evidence on requests to another server', () => {
|
||||
it('sends a chat read there like a shell, with no session id', () => {
|
||||
const evidence = new OrchestrationRequestEvidence(CHAT)
|
||||
expect(
|
||||
evidence.envelopeFor('orchestration.inbox', {
|
||||
remote: true,
|
||||
mutation: false,
|
||||
requestId: undefined
|
||||
}).orchestrationCompatibilityEvidence
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('keeps the session id on writes there, so that server still refuses them', () => {
|
||||
const evidence = new OrchestrationRequestEvidence(CHAT)
|
||||
expect(
|
||||
evidence.envelopeFor('orchestration.taskCreate', {
|
||||
remote: true,
|
||||
mutation: true,
|
||||
requestId: 'request_1'
|
||||
}).orchestrationCompatibilityEvidence
|
||||
).toEqual({ agentSessionId: '11111111-1111-4111-8111-111111111111' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('caller evidence on browser commands', () => {
|
||||
it('names the chat on this host, so its run can show the page it drove', () => {
|
||||
const evidence = new OrchestrationRequestEvidence(CHAT)
|
||||
const request = { mutation: false, requestId: undefined }
|
||||
expect(
|
||||
evidence.envelopeFor('browser.click', { ...request, remote: false })
|
||||
.orchestrationCompatibilityEvidence
|
||||
).toEqual({ agentSessionId: '11111111-1111-4111-8111-111111111111' })
|
||||
expect(
|
||||
evidence.envelopeFor('browser.click', { ...request, remote: true })
|
||||
.orchestrationCompatibilityEvidence
|
||||
).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -1,57 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { AgentWorkspaceTrustDecision } from '../../../shared/agent-launch/permissions/agent-workspace-trust-decision'
|
||||
import type { AgentTrustSpawnFields } from './agent-workspace-trust'
|
||||
import { applyAgentWorkspaceTrustToSpawn } from './agent-workspace-trust-spawn'
|
||||
|
||||
// An SSH launch only forwards the request to the relay, so no local config is written.
|
||||
async function sshTrustRequest(input: {
|
||||
worktreeId: string
|
||||
globalEnabled?: boolean
|
||||
repoDecision?: AgentWorkspaceTrustDecision
|
||||
}): Promise<AgentTrustSpawnFields> {
|
||||
const spawnOptions: AgentTrustSpawnFields = {}
|
||||
await applyAgentWorkspaceTrustToSpawn({
|
||||
launchAgent: 'claude',
|
||||
worktreeId: input.worktreeId,
|
||||
cwd: '/srv/app',
|
||||
store: {
|
||||
getFolderWorkspace: () => ({ folderPath: '/srv/folder' }),
|
||||
getRepo: (repoId) =>
|
||||
repoId === 'repo-1' && input.repoDecision
|
||||
? { agentWorkspaceTrust: input.repoDecision }
|
||||
: undefined
|
||||
},
|
||||
isFreshLaunch: true,
|
||||
settings: { agentWorkspaceTrustEnabled: input.globalEnabled ?? true },
|
||||
env: {},
|
||||
claudeAuth: null,
|
||||
wslDistro: null,
|
||||
connectionId: 'ssh-1',
|
||||
spawnOptions
|
||||
})
|
||||
return spawnOptions
|
||||
}
|
||||
|
||||
describe('agent folder trust follows the repo decision', () => {
|
||||
it('pre-trusts a repo whose decision is pre-trust', async () => {
|
||||
expect(
|
||||
await sshTrustRequest({ worktreeId: 'repo-1::/srv/app', repoDecision: 'pre-trust' })
|
||||
).toEqual({ agentWorkspaceTrust: { workspacePath: '/srv/app' } })
|
||||
})
|
||||
|
||||
it('leaves the folder untrusted when the repo lets agents ask', async () => {
|
||||
expect(
|
||||
await sshTrustRequest({ worktreeId: 'repo-1::/srv/app', repoDecision: 'agent-asks' })
|
||||
).toEqual({})
|
||||
})
|
||||
|
||||
it('never pre-trusts when folder trust is off globally', async () => {
|
||||
expect(
|
||||
await sshTrustRequest({
|
||||
worktreeId: 'repo-1::/srv/app',
|
||||
globalEnabled: false,
|
||||
repoDecision: 'pre-trust'
|
||||
})
|
||||
).toEqual({})
|
||||
})
|
||||
})
|
||||
@@ -4,34 +4,6 @@ import { transcriptMessagesFromDecodedMessage } from './session-transcript-messa
|
||||
import { decodeSessionMessage } from './session-decoded-message'
|
||||
|
||||
const AT = '2026-05-01T10:00:00.000Z'
|
||||
const MESSAGE_LIMIT = 256 * 1024
|
||||
|
||||
it('reads tool-result parts lazily and can project the same decoded result again', () => {
|
||||
const decoded = decodeSessionMessage(
|
||||
'user',
|
||||
[
|
||||
{
|
||||
type: 'tool_result',
|
||||
content: [
|
||||
{ type: 'text', text: 'x'.repeat(MESSAGE_LIMIT) },
|
||||
{ type: 'text', text: 'omitted text' },
|
||||
{
|
||||
type: 'text',
|
||||
get text() {
|
||||
throw new Error('Read beyond the tool-result safety limit')
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
AT
|
||||
)
|
||||
for (let read = 0; read < 2; read++) {
|
||||
expect(transcriptMessagesFromDecodedMessage(decoded)).toEqual([
|
||||
{ role: 'tool', text: 'x'.repeat(MESSAGE_LIMIT), timestamp: AT, textTruncated: true }
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps captured server calls and structured results in full Session History', () => {
|
||||
const messages = advisorCapture.flatMap((frame) =>
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { resolveExternalAutomationScope } from './external-automation-owner-guard'
|
||||
import { AutomationOwnerConflictError } from '../../shared/automations/ownership/automation-owner-conflict'
|
||||
import type { AutomationOwnerRef } from '../../shared/automations/ownership/automation-owner-ref'
|
||||
|
||||
import type { SshTarget } from '../../shared/ssh-types'
|
||||
|
||||
function sshTarget(overrides: Partial<SshTarget> = {}): SshTarget {
|
||||
return {
|
||||
id: 'target-a',
|
||||
label: 'Build box',
|
||||
host: 'build.example',
|
||||
port: 22,
|
||||
username: 'orca',
|
||||
generation: 3,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function registry(targets: SshTarget[]): { getSshTargets: () => SshTarget[] } {
|
||||
return { getSshTargets: () => targets }
|
||||
}
|
||||
|
||||
function desktopSsh(targetId: string, targetGeneration: number): AutomationOwnerRef {
|
||||
return { authority: { kind: 'desktop' }, selector: { kind: 'ssh', targetId, targetGeneration } }
|
||||
}
|
||||
|
||||
describe('resolveExternalAutomationScope', () => {
|
||||
it('fails closed with the host-changed conflict on a stale generation', () => {
|
||||
try {
|
||||
resolveExternalAutomationScope(
|
||||
{ owner: desktopSsh('target-a', 2), provider: 'openclaw' },
|
||||
registry([sshTarget({ generation: 3 })])
|
||||
)
|
||||
expect.unreachable('stale generation must not resolve')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(AutomationOwnerConflictError)
|
||||
expect((error as AutomationOwnerConflictError).code).toBe('automation_owner_changed')
|
||||
}
|
||||
})
|
||||
|
||||
it('fails closed when the registration carries no usable generation', () => {
|
||||
try {
|
||||
resolveExternalAutomationScope(
|
||||
{ owner: desktopSsh('target-a', 1), provider: 'openclaw' },
|
||||
registry([sshTarget({ generation: undefined })])
|
||||
)
|
||||
expect.unreachable('unstamped registration must not resolve')
|
||||
} catch (error) {
|
||||
expect((error as AutomationOwnerConflictError).code).toBe('automation_owner_changed')
|
||||
}
|
||||
})
|
||||
|
||||
it('reports a removed SSH host as a target-removed conflict', () => {
|
||||
try {
|
||||
resolveExternalAutomationScope(
|
||||
{ owner: desktopSsh('gone', 1), provider: 'openclaw' },
|
||||
registry([sshTarget()])
|
||||
)
|
||||
expect.unreachable('missing target must not resolve')
|
||||
} catch (error) {
|
||||
expect((error as AutomationOwnerConflictError).code).toBe('automation_target_removed')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,80 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { runExternalAutomationAction } from './external-manager'
|
||||
|
||||
import type {
|
||||
ExternalAutomationAction,
|
||||
ExternalAutomationProvider
|
||||
} from '../../shared/automations/external/external-automation'
|
||||
import type * as Fs from 'node:fs'
|
||||
|
||||
const runProcessMock = vi.hoisted(() => vi.fn())
|
||||
const existsSyncMock = vi.hoisted(() => vi.fn(() => false))
|
||||
|
||||
vi.mock('@orca/process-host', () => ({ runProcess: runProcessMock }))
|
||||
vi.mock('fs', async () => {
|
||||
const actual = await vi.importActual<typeof Fs>('fs')
|
||||
return {
|
||||
...actual,
|
||||
existsSync: existsSyncMock
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
runProcessMock.mockReset()
|
||||
runProcessMock.mockResolvedValue({
|
||||
code: 0,
|
||||
signal: null,
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
timedOut: false
|
||||
})
|
||||
existsSyncMock.mockReturnValue(false)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe('runExternalAutomationAction', () => {
|
||||
it('rejects job IDs that could be parsed as CLI options', async () => {
|
||||
await expect(
|
||||
runExternalAutomationAction({
|
||||
managerId: 'openclaw:local',
|
||||
provider: 'openclaw',
|
||||
target: { type: 'local' },
|
||||
jobId: '-help',
|
||||
action: 'run'
|
||||
})
|
||||
).rejects.toThrow('Invalid external automation job ID.')
|
||||
|
||||
expect(runProcessMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects an action inherited from the prototype chain', async () => {
|
||||
await expect(
|
||||
runExternalAutomationAction({
|
||||
managerId: 'openclaw:local',
|
||||
provider: 'openclaw',
|
||||
target: { type: 'local' },
|
||||
jobId: 'job-1',
|
||||
action: 'toString' as unknown as ExternalAutomationAction
|
||||
})
|
||||
).rejects.toThrow('Unsupported external automation action.')
|
||||
|
||||
expect(runProcessMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects a provider that has no action table', async () => {
|
||||
await expect(
|
||||
runExternalAutomationAction({
|
||||
managerId: 'openclaw:local',
|
||||
provider: 'shellcat' as ExternalAutomationProvider,
|
||||
target: { type: 'local' },
|
||||
jobId: 'job-1',
|
||||
action: 'run'
|
||||
})
|
||||
).rejects.toThrow('Unsupported external automation action.')
|
||||
|
||||
expect(runProcessMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,54 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
createTranscriptPane,
|
||||
TRANSCRIPT_PANE_PTY_ID
|
||||
} from '../runtime/orca-runtime-tests/agent-transcript-pane-test-harness'
|
||||
import { createRuntimeAutomationRunTerminalObserver } from './runtime-terminal-run-observer'
|
||||
|
||||
describe('createRuntimeAutomationRunTerminalObserver', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('propagates recorded end from the real runtime initial probe without a startup wait', async () => {
|
||||
vi.useRealTimers()
|
||||
const { runtime, handle } = await createTranscriptPane({
|
||||
paneTitle: 'shell',
|
||||
foregroundProcess: 'bash',
|
||||
data: ''
|
||||
})
|
||||
runtime.onPtyExit(TRANSCRIPT_PANE_PTY_ID, 137, 'inc-1', { providerExitObserved: true })
|
||||
vi.useFakeTimers()
|
||||
const observer = createRuntimeAutomationRunTerminalObserver(runtime)
|
||||
await expect(
|
||||
observer.observeCompletion(handle, { signal: new AbortController().signal })
|
||||
).rejects.toThrow('terminal_exited')
|
||||
expect(runtime['terminalWaiters'].forPty(TRANSCRIPT_PANE_PTY_ID)).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('keeps watching a run terminal across a window reload', async () => {
|
||||
const waitForTerminal = vi
|
||||
.fn()
|
||||
// The start probe sees the agent busy.
|
||||
.mockResolvedValueOnce({ satisfied: false })
|
||||
.mockRejectedValueOnce(new Error('runtime_unavailable'))
|
||||
.mockResolvedValueOnce({ satisfied: true })
|
||||
const observer = createRuntimeAutomationRunTerminalObserver({
|
||||
getTerminalHandleForPaneKey: () => 'handle-1',
|
||||
waitForTerminal,
|
||||
readTerminal: async () => ({ tail: ['done'] }),
|
||||
canOpenRunTerminals: () => true
|
||||
})
|
||||
|
||||
const observed = observer.observeCompletion('handle-1', {
|
||||
signal: new AbortController().signal
|
||||
})
|
||||
await vi.advanceTimersByTimeAsync(5_000)
|
||||
|
||||
await expect(observed).resolves.toMatchObject({ status: 'completed' })
|
||||
})
|
||||
})
|
||||
@@ -1,48 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { CookieClearIdentity } from './storage/cookie-store-contracts'
|
||||
import type { SourcePartitionRead } from './sources/browser-cookie-source-partition'
|
||||
import { writeImportedCookies, type SourceCookieToWrite } from './browser-cookie-import-write'
|
||||
|
||||
function sourceCookie(
|
||||
overrides: Partial<SourceCookieToWrite> & { name: string; partition: SourcePartitionRead }
|
||||
): SourceCookieToWrite {
|
||||
return {
|
||||
url: 'https://app.example/',
|
||||
value: 'v',
|
||||
domain: '.app.example',
|
||||
path: '/',
|
||||
secure: true,
|
||||
httpOnly: false,
|
||||
sameSite: 'no_restriction',
|
||||
expirationDate: undefined,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function recordingStore() {
|
||||
const writeCookieIdentity = vi.fn<(identity: CookieClearIdentity) => Promise<void>>(
|
||||
async () => undefined
|
||||
)
|
||||
return { writeCookieIdentity }
|
||||
}
|
||||
|
||||
const silentOptions = { log: () => undefined }
|
||||
|
||||
describe('writeImportedCookies', () => {
|
||||
it('never dispatches an expired cookie, which would delete the existing coordinate', async () => {
|
||||
const store = recordingStore()
|
||||
const result = await writeImportedCookies(
|
||||
store,
|
||||
[
|
||||
sourceCookie({
|
||||
name: 'sid',
|
||||
expirationDate: 1,
|
||||
partition: { status: 'unpartitioned' }
|
||||
})
|
||||
],
|
||||
silentOptions
|
||||
)
|
||||
expect(result).toMatchObject({ importedCount: 0, expired: 1, writeRejected: 0 })
|
||||
expect(store.writeCookieIdentity).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -5,9 +5,8 @@ import type { EncryptionKeyResult } from './chromium-encryption-key-contract'
|
||||
|
||||
const HOST_KEY = '.example.com'
|
||||
const cbcKey = randomBytes(16)
|
||||
const gcmKey = randomBytes(32)
|
||||
|
||||
const cbcKeyResult: EncryptionKeyResult = { mode: 'aes-128-cbc', keysByVersion: { v10: cbcKey } }
|
||||
const gcmKeyResult: EncryptionKeyResult = { mode: 'aes-256-gcm', key: gcmKey }
|
||||
|
||||
function hostKeyHash(hostKey: string): Buffer {
|
||||
return createHash('sha256').update(hostKey).digest()
|
||||
@@ -18,21 +17,9 @@ function encryptCbc(plaintext: Buffer): Buffer {
|
||||
return Buffer.concat([Buffer.from('v10'), cipher.update(plaintext), cipher.final()])
|
||||
}
|
||||
|
||||
function encryptGcm(plaintext: Buffer): Buffer {
|
||||
const nonce = randomBytes(12)
|
||||
const cipher = createCipheriv('aes-256-gcm', gcmKey, nonce)
|
||||
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()])
|
||||
return Buffer.concat([Buffer.from('v10'), nonce, ciphertext, cipher.getAuthTag()])
|
||||
}
|
||||
|
||||
describe('decryptCookieValueRaw host-key hash prefix', () => {
|
||||
it('decrypts an empty value whose plaintext is only the hash', () => {
|
||||
const encrypted = encryptCbc(hostKeyHash(HOST_KEY))
|
||||
expect(decryptCookieValueRaw(encrypted, cbcKeyResult, HOST_KEY)).toEqual(Buffer.alloc(0))
|
||||
})
|
||||
|
||||
it('decrypts an empty value on the Windows AES-GCM path', () => {
|
||||
const encrypted = encryptGcm(hostKeyHash(HOST_KEY))
|
||||
expect(decryptCookieValueRaw(encrypted, gcmKeyResult, HOST_KEY)).toEqual(Buffer.alloc(0))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { requireLocalSshBrowserRouteTarget } from './local-ssh-browser-route'
|
||||
|
||||
const target = { generation: 4 }
|
||||
|
||||
describe('local SSH browser target registration', () => {
|
||||
it('preserves direct SSH callers that do not supply a deployment fence', () => {
|
||||
expect(() => requireLocalSshBrowserRouteTarget(target)).not.toThrow()
|
||||
})
|
||||
|
||||
it.each([3, 5, '4', null])(
|
||||
'refuses a stale or invalid deployment registration: %j',
|
||||
(generation) => {
|
||||
expect(() => requireLocalSshBrowserRouteTarget(target, generation)).toThrow(
|
||||
'browser_local_route_target_stale'
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
it('refuses a removed target instead of borrowing another route', () => {
|
||||
expect(() => requireLocalSshBrowserRouteTarget(undefined, 4)).toThrow(
|
||||
'browser_local_route_target_invalid'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,29 +1,9 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { DescendantSnapshot } from '../pty/process-retirement/pty-descendant-termination'
|
||||
|
||||
import { collectDescendantRows } from '../pty/process-retirement/pty-descendant-termination'
|
||||
import { createClaudeChildTreeReaper } from './claude-agent-sdk-exit-proof'
|
||||
import { mergeClaudeDescendantSnapshots } from './claude-child-tree-snapshot'
|
||||
|
||||
function posixSnapshot(capturedAtMs: number): DescendantSnapshot {
|
||||
return {
|
||||
root: { pid: 100, startedAt: 'Mon Jan 1 00:00:00 2026' },
|
||||
rootPgid: 100,
|
||||
descendants: [{ pid: 200, ppid: 100, pgid: 100, startedAt: 'Mon Jan 1 00:00:01 2026' }],
|
||||
capturedAtMs
|
||||
}
|
||||
}
|
||||
|
||||
describe('Claude child root identity', () => {
|
||||
it('keeps a retained row boundary when a refresh observes no new descendants', () => {
|
||||
const previous = posixSnapshot(1_700_000_000_900)
|
||||
const next = posixSnapshot(1_700_000_002_100)
|
||||
|
||||
expect(mergeClaudeDescendantSnapshots(previous, next)).toEqual({
|
||||
...next,
|
||||
capturedAtMsByPid: { '200': previous.capturedAtMs }
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects mixed old and recycled root rows instead of making the tree killable', async () => {
|
||||
const child = { pid: 100, kill: vi.fn(() => true) }
|
||||
const terminateDescendants = vi.fn(async () => 'exited' as const)
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { expect, it } from 'vitest'
|
||||
import {
|
||||
claudeStartupResultError,
|
||||
claudeStreamJsonExitError
|
||||
} from './claude-stream-json-exit-error'
|
||||
import { providerStartupFailureFact } from '../native-chat/agent-session-wire/startup/structured-agent-session-failure-text'
|
||||
import { claudeStartFailure } from './claude-startup-failure'
|
||||
|
||||
const retryableStructuredAgentSessionStart = (error: unknown): boolean =>
|
||||
claudeStartFailure(error).retry === 'safe'
|
||||
|
||||
const frame = JSON.parse(
|
||||
readFileSync(new URL('./__fixtures__/claude-missing-conversation.jsonl', import.meta.url), 'utf8')
|
||||
)
|
||||
|
||||
it('treats the recorded missing-conversation exit as an explained final refusal', () => {
|
||||
const error = claudeStreamJsonExitError(frame.errors[0], {
|
||||
code: 1,
|
||||
signal: null,
|
||||
processless: false
|
||||
})
|
||||
expect(providerStartupFailureFact(error)).toMatchObject({
|
||||
kind: 'savedConversationMissing',
|
||||
detail: { text: expect.stringContaining(frame.errors[0]), audience: 'log' }
|
||||
})
|
||||
expect(retryableStructuredAgentSessionStart(error)).toBe(false)
|
||||
})
|
||||
|
||||
it('reads the same refusal from the recorded result, and makes other startup results final', () => {
|
||||
const refusal = claudeStartupResultError(frame)
|
||||
expect(providerStartupFailureFact(refusal)).toEqual({
|
||||
kind: 'savedConversationMissing',
|
||||
detail: { text: frame.errors[0], audience: 'log' }
|
||||
})
|
||||
expect(retryableStructuredAgentSessionStart(refusal)).toBe(false)
|
||||
const generic = claudeStartupResultError({ ...frame, errors: ['managed settings failed'] })
|
||||
expect(providerStartupFailureFact(generic)).toMatchObject({
|
||||
kind: 'startFailed',
|
||||
detail: { text: 'managed settings failed', audience: 'log' }
|
||||
})
|
||||
expect(retryableStructuredAgentSessionStart(generic)).toBe(false)
|
||||
})
|
||||
@@ -1,47 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { claudeQuestionItems } from './claude-structured-prompt-items'
|
||||
|
||||
const LAYOUT_A = '┌──────┬────────┐\n│ nav │ main │\n└──────┴────────┘'
|
||||
|
||||
describe('claudeQuestionItems', () => {
|
||||
it('keeps option previews on single-select questions only', () => {
|
||||
const [item] = claudeQuestionItems({
|
||||
sessionId: 'session-1',
|
||||
prompt: {
|
||||
requestId: 'req-1',
|
||||
promptKey: 'req-1',
|
||||
toolUseId: 'toolu-1',
|
||||
toolName: 'AskUserQuestion',
|
||||
kind: 'question',
|
||||
input: {
|
||||
questions: [
|
||||
{
|
||||
question: 'Which layout?',
|
||||
header: 'Layout',
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: 'Sidebar', description: 'Nav on the left', preview: LAYOUT_A },
|
||||
{ label: 'Top bar', description: 'Nav on top', preview: ' \n ' }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: 'Which panes?',
|
||||
header: 'Panes',
|
||||
multiSelect: true,
|
||||
options: [
|
||||
{ label: 'Files', description: 'File tree', preview: 'files' },
|
||||
{ label: 'Git', description: 'Changes', preview: 'git' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
suggestions: [],
|
||||
questionIds: ['q1', 'q2'],
|
||||
settle: vi.fn()
|
||||
}
|
||||
})
|
||||
const questions = item!.body.questions!
|
||||
expect(questions[0]!.options.map((option) => option.preview)).toEqual([LAYOUT_A, undefined])
|
||||
expect(questions[1]!.options.map((option) => option.preview)).toEqual([undefined, undefined])
|
||||
})
|
||||
})
|
||||
+4
-35
@@ -5,12 +5,10 @@ import {
|
||||
getEphemeralVmRecipeResultProjectRoot,
|
||||
isAbsoluteRuntimePath
|
||||
} from '../../shared/ephemeral-vm-recipes'
|
||||
import {
|
||||
ENVIRONMENT_PROVIDER_ERROR_CODES,
|
||||
isEnvironmentProviderError,
|
||||
type EnvironmentCreateSpec,
|
||||
type EnvironmentProviderDriver,
|
||||
type EnvironmentReady
|
||||
import type {
|
||||
EnvironmentCreateSpec,
|
||||
EnvironmentProviderDriver,
|
||||
EnvironmentReady
|
||||
} from './environment-provider-driver'
|
||||
|
||||
export type EnvironmentProviderDriverContractHarness = {
|
||||
@@ -72,34 +70,5 @@ export function runEnvironmentProviderDriverContract(
|
||||
// Idempotent: cleanup retries after a lost response or a restart must not fail.
|
||||
await reopenDriver().destroy(reload(created.handle))
|
||||
})
|
||||
|
||||
it('refuses to bring back a destroyed environment, even after a restart', async () => {
|
||||
const { driver, liveEnvironmentIds, reopenDriver } = await makeHarness()
|
||||
const created = await driver.create(spec('ws-contract-2'))
|
||||
await driver.destroy(created.handle)
|
||||
const error = await reopenDriver()
|
||||
.resume(reload(created.handle))
|
||||
.then(
|
||||
() => null,
|
||||
(caught: unknown) => caught
|
||||
)
|
||||
expect(isEnvironmentProviderError(error)).toBe(true)
|
||||
expect(await liveEnvironmentIds()).not.toContain(created.handle.environmentId)
|
||||
})
|
||||
|
||||
it('rejects a cancelled create with a catalogued error and leaks nothing', async () => {
|
||||
const { driver, liveEnvironmentIds } = await makeHarness()
|
||||
const controller = new AbortController()
|
||||
controller.abort()
|
||||
const error = await driver.create(spec('ws-contract-3'), { signal: controller.signal }).then(
|
||||
() => null,
|
||||
(caught: unknown) => caught
|
||||
)
|
||||
expect(isEnvironmentProviderError(error)).toBe(true)
|
||||
expect(ENVIRONMENT_PROVIDER_ERROR_CODES).toContain(
|
||||
isEnvironmentProviderError(error) ? error.code : null
|
||||
)
|
||||
expect(await liveEnvironmentIds()).toEqual([])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getStatus } from './status-read'
|
||||
|
||||
const { lstat, stream, conflict } = vi.hoisted(() => ({
|
||||
lstat: vi.fn(),
|
||||
stream: vi.fn(),
|
||||
conflict: vi.fn()
|
||||
}))
|
||||
vi.mock('node:fs/promises', () => ({ lstat }))
|
||||
vi.mock('./git-conflict-operation', () => ({ detectConflictOperation: conflict }))
|
||||
vi.mock('../command-runner/git-stream-stdout', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
gitStreamStdout: stream
|
||||
}))
|
||||
|
||||
vi.mock('../command-runner/git-process-env', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
gitOptionalLocksDisabledEnv: () => ({ GIT_OPTIONAL_LOCKS: '0' })
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
conflict.mockResolvedValue(undefined)
|
||||
lstat.mockResolvedValue({ isSymbolicLink: () => true })
|
||||
stream.mockImplementation(async (_args, options) => {
|
||||
options.onStdout('? unrelated.txt\n')
|
||||
return { stoppedEarly: false }
|
||||
})
|
||||
})
|
||||
|
||||
function status(sharedLinkPaths: string[]) {
|
||||
return getStatus('/repo', { sharedLinkPaths, includeLineStats: false })
|
||||
}
|
||||
|
||||
describe('status shared symlink probe budget', () => {
|
||||
it.each([1, 8, 32])(
|
||||
'does no unrelated symlink probes for %i configured paths over 100 refreshes',
|
||||
async (count) => {
|
||||
const paths = Array.from({ length: count }, (_, index) => `shared-${index}`)
|
||||
for (let refresh = 0; refresh < 100; refresh++) {
|
||||
expect((await status(paths)).entries).toEqual([
|
||||
{ path: 'unrelated.txt', status: 'untracked', area: 'untracked' }
|
||||
])
|
||||
}
|
||||
expect(lstat).not.toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
|
||||
it('rechecks matching paths after the filesystem changes and preserves unreadable paths', async () => {
|
||||
const paths = ['unrelated.txt']
|
||||
expect((await status(paths)).entries).toEqual([])
|
||||
lstat.mockResolvedValueOnce({ isSymbolicLink: () => false })
|
||||
expect((await status(paths)).entries).toHaveLength(1)
|
||||
lstat.mockRejectedValueOnce(new Error('EACCES'))
|
||||
expect((await status(paths)).entries).toHaveLength(1)
|
||||
expect(lstat).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
})
|
||||
@@ -1,42 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { coordinatorMocks, moduleMocks } = await vi.hoisted(async () => {
|
||||
const moduleMocks = await import('./pr-refresh-coordinator-test-mocks')
|
||||
return { coordinatorMocks: moduleMocks.createPRRefreshCoordinatorMocks(), moduleMocks }
|
||||
})
|
||||
vi.mock('./pr-refresh-renderer-port', () => moduleMocks.rendererPortModuleMock(coordinatorMocks))
|
||||
vi.mock('./client', () => moduleMocks.clientModuleMock(coordinatorMocks))
|
||||
vi.mock('./github-api-repository', () =>
|
||||
moduleMocks.githubApiRepositoryModuleMock(coordinatorMocks)
|
||||
)
|
||||
vi.mock('./rate-limit', () => moduleMocks.rateLimitModuleMock(coordinatorMocks))
|
||||
|
||||
import { makeCandidate } from './pr-refresh-coordinator-test-harness'
|
||||
|
||||
const { getPRForBranchOutcomeMock } = coordinatorMocks
|
||||
|
||||
describe('failed main refresh pacing', () => {
|
||||
beforeEach(() => {
|
||||
moduleMocks.resetPRRefreshCoordinatorMocks(coordinatorMocks)
|
||||
getPRForBranchOutcomeMock.mockImplementation(async () => ({
|
||||
kind: 'upstream-error',
|
||||
errorType: 'network',
|
||||
message: 'offline',
|
||||
fetchedAt: Date.now()
|
||||
}))
|
||||
})
|
||||
afterEach(() => vi.useRealTimers())
|
||||
|
||||
it('panel foreground exposure cannot bypass a failed lookup deadline', async () => {
|
||||
const { reportVisiblePRRefreshCandidates, enqueuePRRefresh } =
|
||||
await import('./pr-refresh-coordinator')
|
||||
const candidate = makeCandidate({ isSelected: true })
|
||||
reportVisiblePRRefreshCandidates([candidate], 1, 1)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
enqueuePRRefresh(candidate, 'visible', 80, 1)
|
||||
await vi.advanceTimersByTimeAsync(59_999)
|
||||
expect(getPRForBranchOutcomeMock).toHaveBeenCalledTimes(1)
|
||||
await vi.advanceTimersByTimeAsync(1)
|
||||
expect(getPRForBranchOutcomeMock).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
@@ -1,53 +0,0 @@
|
||||
import { chmod, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import { getDiskBaselineSignature } from '../../../shared/file-write/disk-baseline-signature'
|
||||
|
||||
import { registerFilesystemWriteHandlers } from './filesystem-write-handlers'
|
||||
import type { FilesystemHandlerContext } from './filesystem-handler-context'
|
||||
|
||||
const handlers = vi.hoisted(() => new Map<string, (_event: unknown, args: unknown) => unknown>())
|
||||
vi.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
handle: (name: string, handler: (_event: unknown, args: unknown) => unknown) =>
|
||||
handlers.set(name, handler)
|
||||
},
|
||||
shell: {}
|
||||
}))
|
||||
vi.mock('./access/local-file-access-resolution', () => ({
|
||||
resolveLocalWriteRequestPath: async (path: string) => path
|
||||
}))
|
||||
vi.mock('./filesystem-mutations', () => ({ registerFilesystemMutationHandlers: vi.fn() }))
|
||||
vi.mock('../agent-session-attachment-upload-ipc', () => ({
|
||||
registerAgentSessionAttachmentUploadHandlers: vi.fn()
|
||||
}))
|
||||
|
||||
let root = ''
|
||||
beforeEach(async () => {
|
||||
root = await mkdtemp(join(tmpdir(), 'orca-identical-ipc-'))
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: authorization is stubbed, and no other context member is used by this handler.
|
||||
registerFilesystemWriteHandlers({ store: {} } as FilesystemHandlerContext)
|
||||
})
|
||||
afterEach(async () => {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
handlers.clear()
|
||||
})
|
||||
|
||||
it('accepts identical content on a readable, non-writable file through IPC', async () => {
|
||||
const filePath = join(root, 'read-only.txt')
|
||||
await writeFile(filePath, 'edited')
|
||||
await chmod(filePath, 0o444)
|
||||
try {
|
||||
await expect(
|
||||
handlers.get('fs:writeFile')!(null, {
|
||||
filePath,
|
||||
content: 'edited',
|
||||
diskPrecondition: { expectedSignature: getDiskBaselineSignature('original') }
|
||||
})
|
||||
).resolves.toEqual({ ok: true, unchanged: true })
|
||||
expect(await readFile(filePath, 'utf8')).toBe('edited')
|
||||
} finally {
|
||||
await chmod(filePath, 0o644)
|
||||
}
|
||||
})
|
||||
@@ -1,31 +0,0 @@
|
||||
import { afterEach, expect, it } from 'vitest'
|
||||
import { makePaneKey } from '../../../../shared/workspace-layout/stable-pane-id'
|
||||
import { WORKTREE_TERMINALS_SLEEPING_ERROR } from '../../../../shared/workspace-sleep'
|
||||
import { adoptStablePane } from './adopt-stable'
|
||||
import {
|
||||
makePaneSpawnReservationKey,
|
||||
paneSpawnReservationsByOwnerKey,
|
||||
rejectPaneSpawnReservation,
|
||||
reservePaneSpawn
|
||||
} from './spawn-reservation'
|
||||
|
||||
const pane = {
|
||||
worktreeId: 'repo::/w',
|
||||
connectionId: null,
|
||||
tabId: 'tab-1',
|
||||
leafId: '11111111-1111-4111-8111-111111111111'
|
||||
}
|
||||
const ownerKey = makePaneSpawnReservationKey(
|
||||
pane.worktreeId,
|
||||
null,
|
||||
makePaneKey(pane.tabId, pane.leafId)
|
||||
)!
|
||||
|
||||
afterEach(() => paneSpawnReservationsByOwnerKey.clear())
|
||||
|
||||
it('does not hand an automatic refusal to a user adoption of the same pane', async () => {
|
||||
const reservation = reservePaneSpawn(ownerKey)
|
||||
const adopted = adoptStablePane(undefined, undefined, { ...pane, cols: 80, rows: 24 })
|
||||
rejectPaneSpawnReservation(ownerKey, reservation, new Error(WORKTREE_TERMINALS_SLEEPING_ERROR))
|
||||
await expect(adopted).resolves.toBeNull()
|
||||
})
|
||||
@@ -1,60 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { toAppSshPtyId } from '../../../../shared/ssh-pty-id'
|
||||
import type { SshRemotePtyLease } from '../../../../shared/ssh-types'
|
||||
import {
|
||||
closeLegacyRelayPane,
|
||||
installLegacyRelayBridge
|
||||
} from '../../../ssh/legacy-relay/bridge/legacy-relay-bridge-registry'
|
||||
import { stopRendererOwnedPty, type PtyKillIpcDeps } from '../ipc/renderer-kill'
|
||||
|
||||
const TARGET = 'ssh-box'
|
||||
const lease: SshRemotePtyLease = {
|
||||
targetId: TARGET,
|
||||
ptyId: 'pty2:epoch:1',
|
||||
tabId: 'tab-1',
|
||||
state: 'detached',
|
||||
createdAt: 1,
|
||||
updatedAt: 1
|
||||
}
|
||||
|
||||
describe('a tab close of a terminal an older relay still runs', () => {
|
||||
it('hands the stop to the bridge instead of tombstoning the lease the bridge needs', async () => {
|
||||
const leases: SshRemotePtyLease[] = [{ ...lease }]
|
||||
const store = {
|
||||
markSshRemotePtyLease: vi.fn((_target: string, relayPtyId: string) => {
|
||||
for (const row of leases) {
|
||||
if (row.ptyId === relayPtyId) {
|
||||
row.state = 'terminated'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
installLegacyRelayBridge({
|
||||
leases: () => leases,
|
||||
retireLease: vi.fn(),
|
||||
recordStop: vi.fn(),
|
||||
// Disconnected, so the SSH provider is unregistered: the bridge records the stop.
|
||||
connection: () => null,
|
||||
onPanesChanged: () => {},
|
||||
onData: () => {},
|
||||
onReplay: () => {},
|
||||
onExit: () => {},
|
||||
report: () => {}
|
||||
})
|
||||
const ptyId = toAppSshPtyId(TARGET, lease.ptyId)
|
||||
const deps = {
|
||||
store,
|
||||
getLocalPtyProviderStartupPromise: () => undefined,
|
||||
shutdownProviderAndDetectExit: vi.fn(),
|
||||
rememberSyntheticKillExit: vi.fn(),
|
||||
sendPtyExitToRenderer: vi.fn()
|
||||
}
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the bridged branch reads only these deps.
|
||||
await stopRendererOwnedPty(deps as unknown as PtyKillIpcDeps, { id: ptyId })
|
||||
expect(store.markSshRemotePtyLease).not.toHaveBeenCalled()
|
||||
expect(deps.shutdownProviderAndDetectExit).not.toHaveBeenCalled()
|
||||
// The transport's own close that follows still reaches a pending stop, not `not-bridged`.
|
||||
await expect(closeLegacyRelayPane(ptyId)).resolves.toEqual({ status: 'unverifiable' })
|
||||
installLegacyRelayBridge(null)
|
||||
})
|
||||
})
|
||||
@@ -1,32 +0,0 @@
|
||||
import { EventEmitter } from 'node:events'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { abortWhenRendererGone } from './renderer-lifetime-abort'
|
||||
import type { FanoutEventSource } from '../runtime/events/event-listener-fanout'
|
||||
|
||||
function fakeSender(): FanoutEventSource & EventEmitter {
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: EventEmitter implements the once/on/removeListener surface this helper uses, and those three are all it calls; WebContents' overloaded signatures cannot be satisfied structurally.
|
||||
return new EventEmitter() as FanoutEventSource & EventEmitter
|
||||
}
|
||||
|
||||
describe('abortWhenRendererGone', () => {
|
||||
it('aborts once a reload has replaced the document, not on in-app route changes', () => {
|
||||
const sender = fakeSender()
|
||||
const { signal } = abortWhenRendererGone(sender)
|
||||
|
||||
sender.emit('did-start-navigation', {
|
||||
isMainFrame: true,
|
||||
isSameDocument: true,
|
||||
url: 'file:///app#x'
|
||||
})
|
||||
sender.emit('did-navigate-in-page', 'file:///app#x')
|
||||
expect(signal.aborted).toBe(false)
|
||||
|
||||
sender.emit('did-start-navigation', {
|
||||
isMainFrame: true,
|
||||
isSameDocument: false,
|
||||
url: 'file:///app'
|
||||
})
|
||||
sender.emit('did-navigate', 'file:///app', 200, 'OK')
|
||||
expect(signal.aborted).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -51,16 +51,4 @@ describe('addLocalRepoFromPath through a symlink alias', () => {
|
||||
expect(second).toMatchObject({ alreadyExisted: true })
|
||||
expect(repos).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('keeps two different folders as two workspaces', async () => {
|
||||
await mkdir(join(root, 'a'))
|
||||
await mkdir(join(root, 'b'))
|
||||
const repos: Repo[] = []
|
||||
const store = createStore(repos)
|
||||
|
||||
await addLocalRepoFromPath(store, join(root, 'a'), 'folder')
|
||||
await addLocalRepoFromPath(store, join(root, 'b'), 'folder')
|
||||
|
||||
expect(repos).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import { addManagedOrcadEnvironment } from '../../../shared/remote-runtime/host-store/runtime-environment-managed-orcad-store'
|
||||
import { encodePairingOffer } from '../../../shared/remote-runtime/pairing/pairing'
|
||||
import { PAIRING_OFFER_VERSION } from '../../../shared/remote-runtime/pairing/mobile-relay-pairing-offer'
|
||||
import {
|
||||
registerManagedServerActions,
|
||||
type ManagedServerActions
|
||||
} from '../../runtime/hosts/management/managed-server-actions-registry'
|
||||
import { removeRedundantManagedPairing } from './runtime-environment-redundant-managed-removal'
|
||||
|
||||
const forgetRedundant = vi.fn<ManagedServerActions['forgetRedundant']>()
|
||||
let userDataPath: string
|
||||
|
||||
const pairingCode = (port: number, publicKeyB64: string) =>
|
||||
encodePairingOffer({
|
||||
v: PAIRING_OFFER_VERSION,
|
||||
endpoint: `ws://127.0.0.1:${port}/`,
|
||||
deviceToken: `device-${port}`,
|
||||
publicKeyB64
|
||||
})
|
||||
|
||||
function addManaged(id: string, sshTargetId: string, port: number, publicKeyB64: string) {
|
||||
return addManagedOrcadEnvironment(userDataPath, {
|
||||
id,
|
||||
name: id,
|
||||
pairingCode: pairingCode(port, publicKeyB64),
|
||||
orcadDeployment: { sshTargetId, sshTargetGeneration: 1, localPort: port, remotePort: 6_768 }
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
userDataPath = mkdtempSync(join(tmpdir(), 'redundant-managed-removal-'))
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: removal only calls forgetRedundant.
|
||||
registerManagedServerActions({ forgetRedundant } as unknown as ManagedServerActions)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
registerManagedServerActions(null)
|
||||
forgetRedundant.mockReset()
|
||||
rmSync(userDataPath, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('leaves the pairing to the store guard when the other entry manages a different server', () => {
|
||||
const environment = addManaged('managed', 'ssh-1', 46_768, 'public-key')
|
||||
addManaged('other', 'ssh-2', 46_769, 'other-key')
|
||||
expect(removeRedundantManagedPairing(userDataPath, environment)).toBeNull()
|
||||
expect(forgetRedundant).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -1,51 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = await vi.hoisted(async () => {
|
||||
const { createSshIpcMocks } = await import('./ssh-ipc-module-mocks')
|
||||
return createSshIpcMocks()
|
||||
})
|
||||
|
||||
vi.mock('../../ssh/config/ssh-config-host-picker', () => mocks.sshConfigHostPicker)
|
||||
vi.mock('electron', () => mocks.electron)
|
||||
vi.mock('../../ssh/targets/ssh-connection-store', () => mocks.sshConnectionStore)
|
||||
vi.mock('./ssh-host-server-connect', () => mocks.hostServerConnect)
|
||||
vi.mock('../../ssh/connection/ssh-connection-manager', () => mocks.sshConnectionManager)
|
||||
vi.mock('../../ssh/protocol/ssh-channel-multiplexer', () => mocks.sshChannelMultiplexer)
|
||||
vi.mock('../../ssh/port-forwarding/ssh-port-forward', () => mocks.sshPortForward)
|
||||
vi.mock('../ssh/ssh-port-scanner', () => mocks.sshPortScanner)
|
||||
|
||||
import type { SshConnectionState, SshTarget } from '../../../shared/ssh-types'
|
||||
import { createSshIpcHarness } from './ssh-ipc-test-harness'
|
||||
|
||||
const { mockSshStore, mockConnectionManager } = mocks
|
||||
|
||||
describe('SSH IPC handlers', () => {
|
||||
const harness = createSshIpcHarness(mocks)
|
||||
const { handlers } = harness
|
||||
|
||||
beforeEach(harness.reset)
|
||||
|
||||
it('lets a same-turn disconnect invalidate connect before transport admission', async () => {
|
||||
const target: SshTarget = {
|
||||
id: 'ssh-1',
|
||||
label: 'Server',
|
||||
host: 'example.com',
|
||||
port: 22,
|
||||
username: 'deploy'
|
||||
}
|
||||
mockSshStore.getTarget.mockReturnValue(target)
|
||||
mockConnectionManager.connect.mockResolvedValue({})
|
||||
mockConnectionManager.disconnect.mockResolvedValue(undefined)
|
||||
|
||||
const connect = handlers.get('ssh:connect')!(null, {
|
||||
targetId: 'ssh-1'
|
||||
}) as Promise<SshConnectionState>
|
||||
const disconnect = handlers.get('ssh:disconnect')!(null, {
|
||||
targetId: 'ssh-1'
|
||||
}) as Promise<void>
|
||||
|
||||
await disconnect
|
||||
await expect(connect).rejects.toThrow('SSH connection attempt was cancelled')
|
||||
expect(mockConnectionManager.connect).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,54 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { rawRequest } = vi.hoisted(() => ({ rawRequest: vi.fn() }))
|
||||
|
||||
vi.mock('./linear-request-concurrency', () => ({
|
||||
acquire: vi.fn().mockResolvedValue(undefined),
|
||||
release: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('./linear-token-store', () => ({
|
||||
clearToken: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('./client', () => ({
|
||||
getClients: () => [{ workspace: { id: 'workspace-1' }, client: { client: { rawRequest } } }],
|
||||
isAuthError: () => false
|
||||
}))
|
||||
|
||||
import { writeIssueRelation } from './issue-relation-write'
|
||||
|
||||
describe('writeIssueRelation', () => {
|
||||
it('does not create a relation when the duplicate lookup omits pageInfo', async () => {
|
||||
rawRequest.mockImplementation(async (query: string) =>
|
||||
query.includes('issueRelationCreate')
|
||||
? {
|
||||
data: {
|
||||
issueRelationCreate: {
|
||||
success: true,
|
||||
issueRelation: {
|
||||
id: 'rel-new',
|
||||
type: 'blocks',
|
||||
issue: { id: 'issue-1' },
|
||||
relatedIssue: { id: 'issue-2' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
: { data: { issue: { relations: { nodes: [] } } } }
|
||||
)
|
||||
const issue = { id: 'issue-1', identifier: 'LIN-1', title: 'One', url: 'https://x/1' }
|
||||
const relatedIssue = { id: 'issue-2', identifier: 'LIN-2', title: 'Two', url: 'https://x/2' }
|
||||
|
||||
await expect(
|
||||
writeIssueRelation({
|
||||
issue,
|
||||
relatedIssue,
|
||||
relationship: 'blocks',
|
||||
operation: 'add',
|
||||
workspaceId: 'workspace-1'
|
||||
})
|
||||
).rejects.toMatchObject({ code: 'linear_partial' })
|
||||
expect(rawRequest).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -1,38 +0,0 @@
|
||||
import type * as ExecCommandModule from '../../ssh/remote-exec/remote-exec-command'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('../../ssh/remote-exec/remote-exec-command', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof ExecCommandModule>()),
|
||||
execCommand: vi.fn(),
|
||||
isUnconfirmedSshCommandTermination: (error: unknown) =>
|
||||
error instanceof Error && error.message === 'channel lost'
|
||||
}))
|
||||
|
||||
vi.mock('../../ssh/remote-host/ssh-remote-platform-detection', () => ({
|
||||
detectRemoteHostPlatform: vi.fn()
|
||||
}))
|
||||
vi.mock('../host-state/orcad-windows-host-preparation', () => ({
|
||||
prepareWindowsOrcadHost: vi.fn()
|
||||
}))
|
||||
|
||||
import { execCommand } from '../../ssh/remote-exec/remote-exec-command'
|
||||
import { getRemoteHostPlatform } from '../../ssh/remote-host/ssh-remote-platform'
|
||||
import { parseOrcadReadinessOutput } from './orcad-remote-launch'
|
||||
|
||||
const mockExec = vi.mocked(execCommand)
|
||||
getRemoteHostPlatform('linux-x64')
|
||||
getRemoteHostPlatform('win32-x64')
|
||||
Object.create(null)
|
||||
|
||||
beforeEach(() => {
|
||||
mockExec.mockReset()
|
||||
})
|
||||
|
||||
describe('readiness parsing bounds', () => {
|
||||
it('waits on a half-written last line but rejects a finished invalid one', () => {
|
||||
expect(parseOrcadReadinessOutput('{"type":"orca_ser')).toEqual({ state: 'pending' })
|
||||
expect(parseOrcadReadinessOutput('{"type":"orca_ser\n')).toMatchObject({
|
||||
state: 'malformed'
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,52 +0,0 @@
|
||||
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, expect, it } from 'vitest'
|
||||
import { createAgentModelCatalogFilePersistence } from './agent-model-catalog-persistence'
|
||||
import { AgentModelCatalogStore } from './agent-model-catalog-store'
|
||||
|
||||
const directories: string[] = []
|
||||
afterEach(() => {
|
||||
for (const directory of directories.splice(0)) {
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it("imports an older build's catalog file once and keeps later listings", async () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'orca-model-catalog-'))
|
||||
directories.push(directory)
|
||||
const legacyPath = join(directory, 'agent-model-catalog.json')
|
||||
writeFileSync(
|
||||
legacyPath,
|
||||
JSON.stringify({
|
||||
version: 3,
|
||||
entries: [
|
||||
{
|
||||
agent: 'grok',
|
||||
fingerprint: 'fp',
|
||||
discovered: {
|
||||
origin: 'probe',
|
||||
at: 1,
|
||||
models: [{ id: 'A', label: 'A', efforts: [], isDefault: true }]
|
||||
},
|
||||
live: null
|
||||
}
|
||||
]
|
||||
})
|
||||
)
|
||||
const store = new AgentModelCatalogStore()
|
||||
await store.attachPersistence(createAgentModelCatalogFilePersistence(directory))
|
||||
expect(store.get('fp')?.models[0]?.id).toBe('A')
|
||||
expect(existsSync(legacyPath)).toBe(false)
|
||||
|
||||
store.recordSuccess(
|
||||
'fp',
|
||||
'grok',
|
||||
{ origin: 'probe', models: [{ id: 'B', label: 'B', efforts: [], isDefault: true }] },
|
||||
'discovery'
|
||||
)
|
||||
await store.flushPersistence()
|
||||
|
||||
const restarted = await createAgentModelCatalogFilePersistence(directory).load()
|
||||
expect(restarted.map((entry) => entry.models[0]?.id)).toEqual(['B'])
|
||||
})
|
||||
+1
-18
@@ -1,10 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
deriveQueuePauses,
|
||||
journalQueuePauseRestatement,
|
||||
queuePauseHolding,
|
||||
nextSendableQueuedCard
|
||||
} from './queued-message-pause'
|
||||
import { deriveQueuePauses, journalQueuePauseRestatement } from './queued-message-pause'
|
||||
import { createJournalQueuePauseMarks } from './queued-message-pause-marks'
|
||||
|
||||
const card = (messageId: string) => ({
|
||||
@@ -27,18 +22,6 @@ const pauses = (cards = [card('before-one'), card('before-two')], m = marks(), a
|
||||
})
|
||||
|
||||
describe('exact waiting-card membership after context clear', () => {
|
||||
it('holds only recorded waiting IDs and intersects them with cards still waiting', () => {
|
||||
const after = card('after')
|
||||
const current = [card('before-two'), after]
|
||||
const held = pauses(current)
|
||||
expect(held).toEqual([
|
||||
{ reason: 'cleared', since: { epoch: 'epoch', sequence: 5 }, messageIds: ['before-two'] }
|
||||
])
|
||||
expect(queuePauseHolding(held, after)).toBeUndefined()
|
||||
expect(pauses([after])).toEqual([])
|
||||
expect(nextSendableQueuedCard(pauses([after]), [after])).toEqual(after)
|
||||
})
|
||||
|
||||
it('restates an explicit lift when rewind retires the accepted turn', () => {
|
||||
const prior = marks()
|
||||
const cards = [card('before-one'), card('before-two')]
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { OrcaCloudAuthConfig } from './profile-cloud-auth-config'
|
||||
import { createOrcaCloudProfile, OrcaCloudRequestError } from './profile-cloud-client'
|
||||
import type { OrcaCloudSession } from './profile-cloud-session-store'
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe('createOrcaCloudProfile', () => {
|
||||
it('proves the sign-in with its refresh token alongside the access token', async () => {
|
||||
const fetchImpl = vi.fn<typeof globalThis.fetch>(
|
||||
async () => new Response(null, { status: 403 })
|
||||
)
|
||||
vi.stubGlobal('fetch', fetchImpl)
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the call reads only this endpoint.
|
||||
const config = {
|
||||
profileEndpoint: 'https://login.test/v1/desktop/auth/profile'
|
||||
} as OrcaCloudAuthConfig
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the call reads only the two tokens.
|
||||
const session = { accessToken: 'access-1', refreshToken: 'refresh-1' } as OrcaCloudSession
|
||||
|
||||
await expect(
|
||||
createOrcaCloudProfile(config, session, { orgId: 'org-1', name: 'Work' })
|
||||
).rejects.toBeInstanceOf(OrcaCloudRequestError)
|
||||
|
||||
const [url, init] = fetchImpl.mock.calls[0]!
|
||||
expect(url).toBe('https://login.test/v1/desktop/auth/profile')
|
||||
expect(new Headers(init?.headers).get('authorization')).toBe('Bearer access-1')
|
||||
expect(JSON.parse(String(init?.body))).toEqual({
|
||||
orgId: 'org-1',
|
||||
name: 'Work',
|
||||
refreshToken: 'refresh-1'
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,55 +0,0 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
readOrcadHostAccountLaunchEnv,
|
||||
restoreOrcadHostAccountLaunchEnv
|
||||
} from './orcad-host-account-launch-env-store'
|
||||
import { RuntimeAccountController } from '../runtime/agent-session/environment/runtime-account-controller'
|
||||
|
||||
const dirs: string[] = []
|
||||
function userData(): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'orcad-host-accounts-'))
|
||||
dirs.push(dir)
|
||||
return dir
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of dirs.splice(0)) {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
describe('orcad host account launch env store', () => {
|
||||
it('reads nothing from a missing, corrupt or unsafe file', () => {
|
||||
const dir = userData()
|
||||
expect(readOrcadHostAccountLaunchEnv(dir)).toBeNull()
|
||||
writeFileSync(join(dir, 'host-account-launch-env.json'), '{not json')
|
||||
expect(readOrcadHostAccountLaunchEnv(dir)).toBeNull()
|
||||
writeFileSync(
|
||||
join(dir, 'host-account-launch-env.json'),
|
||||
JSON.stringify({ wslDistro: 'Ubuntu', claude: { PATH: '/evil' }, terminal: {} })
|
||||
)
|
||||
expect(readOrcadHostAccountLaunchEnv(dir)).toBeNull()
|
||||
})
|
||||
|
||||
it("holds a fresh WSL host's first agent launch for the desktop's push, and nowhere else", async () => {
|
||||
for (const isWslHost of [true, false]) {
|
||||
const accounts = new RuntimeAccountController()
|
||||
restoreOrcadHostAccountLaunchEnv(
|
||||
{ restoreHostAccountLaunchEnv: accounts.restoreHostLaunchEnv.bind(accounts) },
|
||||
userData(),
|
||||
() => isWslHost
|
||||
)
|
||||
let launched = false
|
||||
void accounts.prepareHostAgentLaunch(null).then(() => {
|
||||
launched = true
|
||||
})
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
expect(launched).toBe(!isWslHost)
|
||||
accounts.setHostLaunchEnv({ wslDistro: 'Ubuntu', claude: {}, terminal: {} })
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,45 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { ColdRestoreDetection } from '../../daemon/history/restoration/terminal-history-cold-restore-info'
|
||||
import { captureWslMoveRestoreHints } from './wsl-move-restore-hints'
|
||||
|
||||
const restored = (
|
||||
cwd: string,
|
||||
hasUnreadableRecovery = false,
|
||||
cwdFromSpawn = false
|
||||
): ColdRestoreDetection => ({
|
||||
status: 'restored',
|
||||
sessionId: 's',
|
||||
hasUnreadableRecovery,
|
||||
...(cwdFromSpawn ? { cwdFromSpawn: true as const } : {}),
|
||||
restoreInfo: {
|
||||
snapshotAnsi: '',
|
||||
scrollbackAnsi: '',
|
||||
rehydrateSequences: '',
|
||||
cwd,
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
modes: {
|
||||
bracketedPaste: false,
|
||||
mouseTracking: false,
|
||||
applicationCursor: false,
|
||||
alternateScreen: false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
describe('captureWslMoveRestoreHints', () => {
|
||||
// Bash on the bridge reports no folder, so an open pane's checkpoint kept its
|
||||
// spawn folder and `cd deep/er` reopened at the project root.
|
||||
it('prefers the folder the shell was in before Sleep over the spawn folder', async () => {
|
||||
const capture = await captureWslMoveRestoreHints(
|
||||
{
|
||||
detectColdRestoreState: async () =>
|
||||
restored('\\\\wsl.localhost\\Ubuntu\\home\\dev\\app', false, true)
|
||||
},
|
||||
[{ paneKey: 't:a', sessionId: 'wt@@a', shellCwd: '/home/dev/app/deep/er' }],
|
||||
'Ubuntu'
|
||||
)
|
||||
expect(capture.hints).toEqual([{ paneKey: 't:a', cwd: '/home/dev/app/deep/er' }])
|
||||
expect(capture.staleCwdPaneKeys).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -1,52 +0,0 @@
|
||||
import { mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { installFakeAppEnvironment } from '../../../../config/scripts/test-runtime/environment/vitest-host-ports-setup'
|
||||
|
||||
let userDataDir: string
|
||||
|
||||
import { hashWorktreeId } from './terminal-history-id'
|
||||
import {
|
||||
deleteWorktreeHistoryDir,
|
||||
flushPendingWorktreeHistoryDeletions
|
||||
} from './terminal-history-deletion'
|
||||
|
||||
describe('deleteWorktreeHistoryDir main-thread safety', () => {
|
||||
beforeEach(() => {
|
||||
userDataDir = mkdtempSync(join(tmpdir(), 'orca-history-async-'))
|
||||
installFakeAppEnvironment({ getPath: () => userDataDir })
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await flushPendingWorktreeHistoryDeletions()
|
||||
rmSync(userDataDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('tombstones without blocking the event loop, then removes via async rm', async () => {
|
||||
const worktreeId = 'repo-1::/path/heavy-wt'
|
||||
const hash = hashWorktreeId(worktreeId)
|
||||
const historyDir = join(userDataDir, 'terminal-history', hash)
|
||||
mkdirSync(historyDir, { recursive: true })
|
||||
for (let i = 0; i < 3_000; i++) {
|
||||
writeFileSync(join(historyDir, `file-${i}.txt`), `payload-${i}`)
|
||||
}
|
||||
|
||||
// Measure the sync delete call; later async-rm timer gaps depend on CI scheduling.
|
||||
const criticalPathStartedAt = performance.now()
|
||||
deleteWorktreeHistoryDir(worktreeId)
|
||||
const criticalPathMs = performance.now() - criticalPathStartedAt
|
||||
|
||||
// AV/shared runners can delay rename; structural assertions below establish tombstoning.
|
||||
expect(criticalPathMs).toBeLessThan(
|
||||
process.env.CI || process.platform === 'win32' ? 1_000 : 100
|
||||
)
|
||||
expect(readdirSync(join(userDataDir, 'terminal-history'))).not.toContain(hash)
|
||||
expect(
|
||||
readdirSync(join(userDataDir, 'terminal-history', '.pending-delete')).length
|
||||
).toBeGreaterThan(0)
|
||||
|
||||
await flushPendingWorktreeHistoryDeletions()
|
||||
expect(readdirSync(join(userDataDir, 'terminal-history', '.pending-delete'))).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
@@ -2,13 +2,6 @@ import { describe, expect, it } from 'vitest'
|
||||
import { redactMiniMaxSecret } from './minimax-request-context'
|
||||
|
||||
describe('redactMiniMaxSecret', () => {
|
||||
it('redacts _token values in header and quoted syntax', () => {
|
||||
const redacted = redactMiniMaxSecret('cookie _token=eyJhABCDEF and _token:"x.y"')
|
||||
expect(redacted).toContain('_token=[REDACTED]')
|
||||
expect(redacted).not.toContain('eyJhABCDEF')
|
||||
expect(redacted).not.toContain('x.y')
|
||||
})
|
||||
|
||||
it('redacts quoted values with whitespace around the colon', () => {
|
||||
const redacted = redactMiniMaxSecret('_token : "spaced.secret"')
|
||||
expect(redacted).not.toContain('spaced.secret')
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { spawn } from 'node:child_process'
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { classifySynchronousRipgrepSpawnFailure } from '../../shared/ripgrep/ripgrep-process-availability'
|
||||
|
||||
const dirs: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of dirs.splice(0)) {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
function fileAsSearchRoot(): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'rg-sync-'))
|
||||
dirs.push(dir)
|
||||
const path = join(dir, 'not-a-directory')
|
||||
writeFileSync(path, '')
|
||||
return path
|
||||
}
|
||||
|
||||
describe('synchronous ripgrep spawn failures', () => {
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
'names the unreachable root instead of surfacing a raw spawn errno',
|
||||
async () => {
|
||||
const root = fileAsSearchRoot()
|
||||
let thrown: unknown
|
||||
try {
|
||||
spawn(process.execPath, ['-e', ''], { cwd: root, stdio: 'ignore' })
|
||||
} catch (error) {
|
||||
thrown = error
|
||||
}
|
||||
|
||||
const verdict = await classifySynchronousRipgrepSpawnFailure(thrown, root)
|
||||
|
||||
expect(verdict.message).toBe(`Search root is not reachable: ${root}`)
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -1,66 +0,0 @@
|
||||
import { closeTestJournalHostDatabases } from '../../native-chat/agent-session-journal/store-lifecycle/journal-host-database-test-support'
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import type { AgentSessionDeathEvidence } from '../../../shared/agent-session/lifecycle/agent-session-death-evidence'
|
||||
import {
|
||||
agentSessionLeaseFixture,
|
||||
agentSessionRecordFixture
|
||||
} from '../../../shared/agent-session/lifecycle/agent-session-record.test-fixture'
|
||||
import type { AgentSessionRecordStore } from './records/agent-session-record-store'
|
||||
import {
|
||||
openTestAgentSessionRecordStore,
|
||||
seedTestAgentSessionRecordStore
|
||||
} from './records/agent-session-record-store-test-harness'
|
||||
|
||||
const SESSION = 'session-alpha-1'
|
||||
const LAST_RENEWED_AT = 30_000
|
||||
|
||||
let directory: string
|
||||
|
||||
async function seed(deathEvidence: AgentSessionDeathEvidence | null): Promise<void> {
|
||||
const lease =
|
||||
deathEvidence === null
|
||||
? agentSessionLeaseFixture()
|
||||
: agentSessionLeaseFixture({
|
||||
ownerProcess: null,
|
||||
reservedSpawnToken: null,
|
||||
claimStatus: 'released',
|
||||
deathEvidence
|
||||
})
|
||||
await seedTestAgentSessionRecordStore(directory, { records: [agentSessionRecordFixture(lease)] })
|
||||
}
|
||||
|
||||
function open(): Promise<AgentSessionRecordStore> {
|
||||
return openTestAgentSessionRecordStore(directory)
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
directory = await mkdtemp(join(tmpdir(), 'orca-death-evidence-'))
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await closeTestJournalHostDatabases()
|
||||
await rm(directory, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe('death evidence on disk', () => {
|
||||
it('records the last renewal before the crash and reads it back after another restart', async () => {
|
||||
await seed(null)
|
||||
const crashed = await open()
|
||||
await crashed.reconcileOnRestart({
|
||||
probe: async () => ({ outcome: 'pid-absent' }),
|
||||
now: 90_000
|
||||
})
|
||||
const evidence = {
|
||||
kind: 'pid-absent',
|
||||
detail: 'recorded pid absent on host',
|
||||
observedAt: 90_000,
|
||||
ownerFence: 7,
|
||||
lastProvenAliveAt: LAST_RENEWED_AT
|
||||
}
|
||||
expect(crashed.getRecord(SESSION)?.lease.deathEvidence).toEqual(evidence)
|
||||
expect((await open()).getRecord(SESSION)?.lease.deathEvidence).toEqual(evidence)
|
||||
})
|
||||
})
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
import { beforeEach, expect, it, vi } from 'vitest'
|
||||
import { folderWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import {
|
||||
root,
|
||||
B,
|
||||
authorityProfile,
|
||||
openAuthorityRuntime
|
||||
} from './structured-agent-session-tab-authority.test-support'
|
||||
import { closeStructuredAgentSessionChild } from '../structured-agent-session-close'
|
||||
|
||||
// CLI capability probing is outside tab authority and must not start a real CLI in this fixture.
|
||||
vi.mock('../../../claude/claude-cli-flag-prewarm', () => ({
|
||||
prewarmClaudeCliFlags: async () => undefined
|
||||
}))
|
||||
|
||||
const layoutOwner = vi.hoisted(() => ({ main: false }))
|
||||
vi.mock('../../../../shared/layout-owner-switch', () => ({
|
||||
get ORCA_LAYOUT_OWNER_IS_MAIN() {
|
||||
return layoutOwner.main
|
||||
}
|
||||
}))
|
||||
beforeEach(() => {
|
||||
layoutOwner.main = false
|
||||
})
|
||||
|
||||
it('captured close retires only its qualified workspace and preserves unrelated mirrors', async () => {
|
||||
const rig = await openAuthorityRuntime(await authorityProfile())
|
||||
await rig.host.setSessionTabVisibility(B, true)
|
||||
rig.runtime.projectStructuredAgentSessionTab({
|
||||
workspaceId: rig.workspace,
|
||||
sessionId: B,
|
||||
agent: 'codex',
|
||||
activate: true
|
||||
})
|
||||
const original = rig.runtime.mobileSessionTabsByWorktree.get(rig.workspace)!
|
||||
const duplicate = rig.profile.createFolderWorkspace({
|
||||
projectGroupId: rig.groupId,
|
||||
folderPath: `${root}/duplicate`,
|
||||
name: 'Duplicate'
|
||||
})
|
||||
const mirror = rig.profile.createFolderWorkspace({
|
||||
projectGroupId: rig.groupId,
|
||||
folderPath: `${root}/mirror`,
|
||||
name: 'Mirror',
|
||||
connectionId: 'foreign'
|
||||
})
|
||||
const duplicateKey = folderWorkspaceKey(duplicate.id),
|
||||
mirrorKey = folderWorkspaceKey(mirror.id)
|
||||
rig.runtime.storeMobileSessionSnapshot(duplicateKey, { ...original, worktree: duplicateKey })
|
||||
const foreign = rig.runtime.storeMobileSessionSnapshot(mirrorKey, {
|
||||
...original,
|
||||
worktree: mirrorKey
|
||||
})
|
||||
await closeStructuredAgentSessionChild(B, { runtime: rig.runtime })
|
||||
expect(rig.runtime.mobileSessionTabsByWorktree.get(rig.workspace)?.tabs).toEqual([])
|
||||
expect(rig.runtime.mobileSessionTabsByWorktree.get(duplicateKey)?.tabs).toEqual(original.tabs)
|
||||
expect(rig.runtime.mobileSessionTabsByWorktree.get(mirrorKey)).toBe(foreign)
|
||||
expect(rig.runtime.retireStructuredAgentSessionTabFromSnapshot(B)).toBe(false)
|
||||
expect(rig.runtime.mobileSessionTabsByWorktree.get(mirrorKey)).toBe(foreign)
|
||||
})
|
||||
@@ -31,7 +31,6 @@ await import('../orca-runtime-tests/pty-title-status.spec')
|
||||
await import('../orca-runtime-tests/terminal-side-effect-facts.spec')
|
||||
await import('../orca-runtime-tests/terminal-side-effect-facts-part-02.spec')
|
||||
await import('../orca-runtime-tests/terminal-side-effect-facts-part-03.spec')
|
||||
await import('../orca-runtime-tests/decorative-title-fact-throttle.spec')
|
||||
await import('../orca-runtime-tests/headless-snapshots.spec')
|
||||
await import('../orca-runtime-tests/headless-snapshots-part-02.spec')
|
||||
await import('../orca-runtime-tests/agent-status-and-waits.spec')
|
||||
@@ -39,7 +38,6 @@ await import('../orca-runtime-tests/agent-status-and-waits-part-02.spec')
|
||||
await import('../orca-runtime-tests/agent-status-and-waits-part-03.spec')
|
||||
await import('../orca-runtime-tests/terminal-creation-and-readiness.spec')
|
||||
await import('../orca-runtime-tests/terminal-creation-and-readiness-part-02.spec')
|
||||
await import('../orca-runtime-tests/terminal-creation-and-readiness-part-03.spec')
|
||||
await import('../orca-runtime-tests/terminal-creation-and-readiness-part-04.spec')
|
||||
await import('../orca-runtime-tests/terminal-creation-and-readiness-part-05.spec')
|
||||
await import('../orca-runtime-tests/terminal-creation-and-readiness-part-06.spec')
|
||||
@@ -103,7 +101,6 @@ await import('../orca-runtime-tests/worktree-setup-and-startup-part-03.spec')
|
||||
await import('../orca-runtime-tests/worktree-setup-and-startup-part-04.spec')
|
||||
await import('../orca-runtime-tests/worktree-setup-single-creator.spec')
|
||||
await import('../orca-runtime-tests/gitlab-and-pr-bases-part-02.spec')
|
||||
await import('../orca-runtime-tests/gitlab-and-pr-bases-part-03.spec')
|
||||
await import('../orca-runtime-tests/worktree-removal-and-reconciliation-part-02.spec')
|
||||
await import('../orca-runtime-tests/worktree-removal-and-reconciliation-part-03.spec')
|
||||
await import('../orca-runtime-tests/worktree-removal-and-reconciliation-part-04.spec')
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { expect, it, vi } from 'vitest'
|
||||
import { createBranchSwitchHost } from './runtime-branch-switch-host'
|
||||
import type { RuntimeStore } from '../../construction/runtime-store-contract'
|
||||
import type { WorktreeMeta } from '../../../../shared/worktree/meta-types'
|
||||
|
||||
vi.mock('../../../persistence/host-qualified-worktree-meta', () => ({
|
||||
worktreeRowTarget: (_store: unknown, worktree: unknown) => worktree
|
||||
}))
|
||||
|
||||
it('keeps automatic naming after commit checkout while preserving branch-link clearing', () => {
|
||||
const meta = { linkedPR: 42, branchNameIsUserChosen: false }
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the switch callback only reads and updates this metadata fixture.
|
||||
const store = {
|
||||
getWorktreeMetaForHost: () => meta,
|
||||
setWorktreeMeta: (_target: unknown, updates: Partial<WorktreeMeta>) =>
|
||||
Object.assign(meta, updates)
|
||||
} as unknown as RuntimeStore
|
||||
const host = createBranchSwitchHost({
|
||||
getStore: () => store,
|
||||
readAgentRows: () => [],
|
||||
readAgentPaneRows: () => [],
|
||||
ptys: () => [],
|
||||
paneOf: () => ({ tabId: null, paneKey: null }),
|
||||
terminalHandlesOf: () => [],
|
||||
hasSyncedTab: () => false,
|
||||
savedTabIds: () => [],
|
||||
probeProcess: async () => null,
|
||||
notifyBranchChanged: () => {},
|
||||
folderWorkspacePaths: () => new Map()
|
||||
})
|
||||
const worktree = { id: 'r::/repo', hostId: 'local' as const, repoId: 'r' }
|
||||
host.recordBranchSwitched(worktree, true)
|
||||
expect(meta).toMatchObject({ linkedPR: null, branchNameIsUserChosen: false })
|
||||
host.recordBranchSwitched(worktree)
|
||||
expect(meta.branchNameIsUserChosen).toBe(true)
|
||||
})
|
||||
@@ -1,54 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { TerminalSideEffectBatch } from '../../../shared/terminal/output/terminal-side-effect-facts'
|
||||
import { syncSinglePty } from './orca-runtime-test-fixtures.spec'
|
||||
import { createSideEffectRuntime } from './orca-runtime-test-scenario-builders.spec'
|
||||
const SPINNER_INTERVAL_MS = 80
|
||||
const EPOCH = 1_700_000_000_000
|
||||
|
||||
type TitleFact = { kind: 'title'; normalizedTitle: string; rawTitle: string }
|
||||
|
||||
function titleFacts(batches: TerminalSideEffectBatch[]): TitleFact[] {
|
||||
return batches.flatMap((batch) =>
|
||||
batch.facts.filter((fact): fact is TitleFact => fact.kind === 'title')
|
||||
)
|
||||
}
|
||||
|
||||
describe('decorative title fact throttle', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers({ toFake: ['Date'] })
|
||||
vi.setSystemTime(new Date(EPOCH))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('propagates a changed working label immediately even while the spinner rotates', () => {
|
||||
// Only spinner glyphs are decorative; Grok/Pi label churn changes content.
|
||||
const { runtime, batches } = createSideEffectRuntime()
|
||||
syncSinglePty(runtime)
|
||||
|
||||
runtime.ingestSyntheticTitleFrame('pty-1', '\x1b]0;⠋ Claude Code\x07')
|
||||
vi.setSystemTime(new Date(EPOCH + SPINNER_INTERVAL_MS))
|
||||
runtime.ingestSyntheticTitleFrame('pty-1', '\x1b]0;⠙ Reviewing diff — Claude Code\x07')
|
||||
|
||||
expect(titleFacts(batches).map((fact) => fact.rawTitle)).toEqual([
|
||||
'⠋ Claude Code',
|
||||
'⠙ Reviewing diff — Claude Code'
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps main-side tracked title state current for every suppressed frame', () => {
|
||||
// Mobile/remote read tracked records; suppressed facts must not freeze those snapshots.
|
||||
const { runtime } = createSideEffectRuntime()
|
||||
syncSinglePty(runtime)
|
||||
|
||||
runtime.ingestSyntheticTitleFrame('pty-1', '\x1b]0;⠋ Claude Code\x07')
|
||||
vi.setSystemTime(new Date(EPOCH + SPINNER_INTERVAL_MS))
|
||||
runtime.ingestSyntheticTitleFrame('pty-1', '\x1b]0;⠙ Claude Code\x07')
|
||||
|
||||
expect(runtime.getTerminalSideEffectSnapshot('pty-1')?.facts).toEqual([
|
||||
{ kind: 'title', normalizedTitle: '⠙ Claude Code', rawTitle: '⠙ Claude Code' }
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -1,41 +0,0 @@
|
||||
import * as gitExecFile from '../../git/command-runner/git-exec-file'
|
||||
import { SshHostNotServedError } from '../../providers/ssh-host-not-served-error'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import './runtime-test-module-registration.spec'
|
||||
import { OrcaRuntimeService } from '../orca-runtime'
|
||||
import { getGlabKnownHostsMock } from '../orca-runtime-test-mocks/setup.spec'
|
||||
import { TEST_REPO_ID, store } from './orca-runtime-test-fixtures.spec'
|
||||
|
||||
describe('OrcaRuntimeService', () => {
|
||||
it('refuses SSH GitLab MR bases instead of resolving them on this machine', async () => {
|
||||
const remoteRepo = {
|
||||
id: TEST_REPO_ID,
|
||||
path: '/remote/repo',
|
||||
displayName: 'repo',
|
||||
badgeColor: 'blue',
|
||||
addedAt: 1,
|
||||
connectionId: 'ssh-1',
|
||||
issueSourcePreference: 'origin' as const
|
||||
}
|
||||
const runtimeStore = {
|
||||
...store,
|
||||
getRepos: () => [remoteRepo],
|
||||
getRepo: (id: string) => (id === remoteRepo.id ? remoteRepo : undefined)
|
||||
}
|
||||
const gitSpy = vi.spyOn(gitExecFile, 'gitExecFileAsync')
|
||||
const runtime = new OrcaRuntimeService(runtimeStore as never)
|
||||
|
||||
await expect(
|
||||
runtime.resolveManagedMrBase({
|
||||
repoSelector: 'id:repo-1',
|
||||
mrIid: 77,
|
||||
sourceBranch: 'feature',
|
||||
targetBranch: 'main',
|
||||
isCrossRepository: true
|
||||
})
|
||||
).rejects.toThrow(SshHostNotServedError)
|
||||
expect(getGlabKnownHostsMock).not.toHaveBeenCalled()
|
||||
expect(gitSpy).not.toHaveBeenCalled()
|
||||
gitSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
@@ -1,57 +0,0 @@
|
||||
import 'node:fs/promises'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import '../../preflight/agent-command-detection'
|
||||
import './runtime-test-module-registration.spec'
|
||||
import '../../../shared/constants'
|
||||
import { OrcaRuntimeService } from '../orca-runtime'
|
||||
import '../../../shared/agent-launch/startup/setup-agent-sequencing'
|
||||
import { join } from 'node:path'
|
||||
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { tmpdir } from 'node:os'
|
||||
import {
|
||||
TEST_FOLDER_WORKSPACE_ID,
|
||||
TEST_FOLDER_WORKSPACE_KEY,
|
||||
createFolderWorkspaceRuntimeStore,
|
||||
makeFolderProjectGroup,
|
||||
makeFolderWorkspace
|
||||
} from './orca-runtime-test-fixtures.spec'
|
||||
import '../terminal/launch/spawned-pty-env-fixtures'
|
||||
|
||||
describe('OrcaRuntimeService', () => {
|
||||
it('rejects folder workspace terminal creation when the backing path is missing', async () => {
|
||||
const missingPath = join(tmpdir(), `orca-missing-folder-workspace-${randomUUID()}`)
|
||||
const spawn = vi.fn().mockResolvedValue({ id: 'pty-folder' })
|
||||
const folderWorkspace = makeFolderWorkspace({ folderPath: missingPath })
|
||||
const projectGroup = makeFolderProjectGroup({ parentPath: missingPath })
|
||||
const runtime = new OrcaRuntimeService(
|
||||
createFolderWorkspaceRuntimeStore(folderWorkspace, projectGroup) as never
|
||||
)
|
||||
runtime.setPtyController({
|
||||
spawn,
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null
|
||||
})
|
||||
|
||||
await expect(runtime.createTerminal(TEST_FOLDER_WORKSPACE_KEY)).rejects.toThrow(
|
||||
'folder_workspace_path_missing'
|
||||
)
|
||||
expect(spawn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects folder workspace folderPath updates when the new path is missing', async () => {
|
||||
const missingPath = join(tmpdir(), `orca-missing-folder-update-${randomUUID()}`)
|
||||
const folderWorkspace = makeFolderWorkspace()
|
||||
const runtimeStore = {
|
||||
...createFolderWorkspaceRuntimeStore(folderWorkspace),
|
||||
updateFolderWorkspace: vi.fn()
|
||||
}
|
||||
const runtime = new OrcaRuntimeService(runtimeStore as never)
|
||||
|
||||
await expect(
|
||||
runtime.updateFolderWorkspace(TEST_FOLDER_WORKSPACE_ID, { folderPath: missingPath })
|
||||
).rejects.toThrow('folder_workspace_path_missing')
|
||||
expect(runtimeStore.updateFolderWorkspace).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { OrcaRuntimeService } from '../../../orca-runtime'
|
||||
import {
|
||||
acquireFederationAckLease,
|
||||
clearFederationAckCheckpoints,
|
||||
getFederationAckedThrough,
|
||||
recordFederationAckCheckpoint,
|
||||
type FederationAckIdentity
|
||||
} from '../../legacy-remote-home/federation-ack-checkpoints'
|
||||
|
||||
describe('federation acknowledgment checkpoints', () => {
|
||||
it('matches checkpoints only to their exact remote identity and never moves backward', () => {
|
||||
const runtime = {} as OrcaRuntimeService
|
||||
const identity: FederationAckIdentity = {
|
||||
environmentId: 'environment_windows',
|
||||
peerFingerprint: 'windows_peer_fingerprint',
|
||||
remoteRuntimeEpoch: 'remote_epoch_1'
|
||||
}
|
||||
const lease = acquireFederationAckLease(runtime, 'dispatch_remote')
|
||||
recordFederationAckCheckpoint(runtime, lease, {
|
||||
...identity,
|
||||
throughSequence: 2
|
||||
})
|
||||
|
||||
recordFederationAckCheckpoint(runtime, lease, {
|
||||
...identity,
|
||||
throughSequence: 3
|
||||
})
|
||||
recordFederationAckCheckpoint(runtime, lease, {
|
||||
...identity,
|
||||
throughSequence: 2
|
||||
})
|
||||
|
||||
expect(getFederationAckedThrough(lease, identity)).toBe(3)
|
||||
expect(
|
||||
getFederationAckedThrough(lease, { ...identity, remoteRuntimeEpoch: 'remote_epoch_2' })
|
||||
).toBe(0)
|
||||
expect(
|
||||
getFederationAckedThrough(lease, { ...identity, peerFingerprint: 'replacement_peer' })
|
||||
).toBe(0)
|
||||
expect(getFederationAckedThrough(lease, { ...identity, environmentId: 'replacement' })).toBe(0)
|
||||
})
|
||||
|
||||
it('fences delayed writes after runtime reset', () => {
|
||||
const runtime = {} as OrcaRuntimeService
|
||||
const identity: FederationAckIdentity = {
|
||||
environmentId: 'environment_windows',
|
||||
peerFingerprint: 'windows_peer_fingerprint',
|
||||
remoteRuntimeEpoch: 'remote_epoch_1'
|
||||
}
|
||||
const staleRuntimeLease = acquireFederationAckLease(runtime, 'dispatch_remote')
|
||||
clearFederationAckCheckpoints(runtime)
|
||||
recordFederationAckCheckpoint(runtime, staleRuntimeLease, {
|
||||
...identity,
|
||||
throughSequence: 2
|
||||
})
|
||||
expect(
|
||||
getFederationAckedThrough(acquireFederationAckLease(runtime, 'dispatch_remote'), identity)
|
||||
).toBe(0)
|
||||
})
|
||||
})
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { ORCHESTRATION_FEDERATION_RELEASE_ARCHIVE_RUNTIME_CAPABILITY } from '../../../../../shared/orchestration/orchestration-runtime-capabilities'
|
||||
import { createIdleSyncHarness } from './federation-sync-test-harness'
|
||||
import { armHomeAutoReleaseForOlderHost } from '../../legacy-remote-home/federation-sync-capability'
|
||||
|
||||
function releasableTerminalWorker() {
|
||||
const harness = createIdleSyncHarness()
|
||||
harness.setRemoteCapabilities([ORCHESTRATION_FEDERATION_RELEASE_ARCHIVE_RUNTIME_CAPABILITY])
|
||||
const db = harness.runtime.getOrchestrationDb()
|
||||
const federated = db.getFederatedDispatch('dispatch_remote')!
|
||||
db.getFederatedDispatch = () => ({
|
||||
...federated,
|
||||
remote_terminal_handle: 'term_remote_worker',
|
||||
home_auto_release_state: null
|
||||
})
|
||||
const worker = db.getWorkerDispatch('dispatch_remote')!
|
||||
db.getWorkerDispatch = () => ({ ...worker, state: 'succeeded', stage: 'reported' })
|
||||
db.workerAutoReleaseEnabled = () => true
|
||||
return {
|
||||
runtime: harness.runtime,
|
||||
remoteCall: harness.remoteCall,
|
||||
dispatchId: federated.dispatch_id,
|
||||
armedDeadlines: () => db.claimDueFederatedHomeAutoReleases(Date.UTC(2100, 0, 1))
|
||||
}
|
||||
}
|
||||
|
||||
describe('armHomeAutoReleaseForOlderHost', () => {
|
||||
it('does not arm from a sync the relay stopped while it probed the host', async () => {
|
||||
const { runtime, remoteCall, dispatchId, armedDeadlines } = releasableTerminalWorker()
|
||||
const answer = remoteCall.getMockImplementation()!
|
||||
let noteProbe!: () => void
|
||||
let releaseProbe!: () => void
|
||||
const probing = new Promise<void>((resolve) => (noteProbe = resolve))
|
||||
const released = new Promise<void>((resolve) => (releaseProbe = resolve))
|
||||
remoteCall.mockImplementation(async (...args) => {
|
||||
if (args[1] === 'status.get') {
|
||||
noteProbe()
|
||||
await released
|
||||
}
|
||||
return answer(...args)
|
||||
})
|
||||
let current = true
|
||||
|
||||
const arming = armHomeAutoReleaseForOlderHost(runtime, dispatchId, 1, () => current)
|
||||
await probing
|
||||
current = false
|
||||
releaseProbe()
|
||||
await arming
|
||||
|
||||
expect(armedDeadlines()).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -1,62 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { OrcaRuntimeService } from '../orca-runtime'
|
||||
import { defineStreamingMethod, type RpcResponse } from './core'
|
||||
import { RpcDispatcher } from './dispatcher'
|
||||
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: These test methods only read the runtime ID; optional runtime hooks are absent.
|
||||
const runtime = { getRuntimeId: () => 'test-runtime' } as OrcaRuntimeService
|
||||
|
||||
describe('object and JSON stream transports', () => {
|
||||
it('preserves ordered stream results and errors across structured cloning and JSON', async () => {
|
||||
const dispatcher = new RpcDispatcher({
|
||||
runtime,
|
||||
methods: [
|
||||
defineStreamingMethod({
|
||||
name: 'test.stream',
|
||||
permission: 'workspace',
|
||||
params: null,
|
||||
handler: async (_params, _ctx, emit) => {
|
||||
emit({ type: 'text', text: 'hello\n"😀' })
|
||||
emit({ type: 'end' })
|
||||
throw new Error('boom')
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
const request = { id: 'sub-1', authToken: 'token', method: 'test.stream' }
|
||||
const objects: RpcResponse[] = []
|
||||
const json: string[] = []
|
||||
await dispatcher.dispatchStreamingObjects(request, (response) =>
|
||||
objects.push(structuredClone(response))
|
||||
)
|
||||
await dispatcher.dispatchStreaming(request, (response) => json.push(response))
|
||||
|
||||
expect(objects).toEqual([
|
||||
{
|
||||
id: 'sub-1',
|
||||
ok: true,
|
||||
result: { type: 'text', text: 'hello\n"😀' },
|
||||
_meta: { runtimeId: 'test-runtime' },
|
||||
streaming: true
|
||||
},
|
||||
{
|
||||
id: 'sub-1',
|
||||
ok: true,
|
||||
result: { type: 'end' },
|
||||
_meta: { runtimeId: 'test-runtime' },
|
||||
streaming: true
|
||||
},
|
||||
{
|
||||
id: 'sub-1',
|
||||
ok: false,
|
||||
error: { code: 'runtime_error', message: 'boom' },
|
||||
_meta: { runtimeId: 'test-runtime' }
|
||||
}
|
||||
])
|
||||
expect(json).toEqual([
|
||||
'{"id":"sub-1","ok":true,"result":{"type":"text","text":"hello\\n\\"😀"},"_meta":{"runtimeId":"test-runtime"},"streaming":true}',
|
||||
'{"id":"sub-1","ok":true,"result":{"type":"end"},"_meta":{"runtimeId":"test-runtime"},"streaming":true}',
|
||||
'{"id":"sub-1","ok":false,"error":{"code":"runtime_error","message":"boom"},"_meta":{"runtimeId":"test-runtime"}}'
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -158,7 +158,7 @@ function hostWithWindow(
|
||||
events,
|
||||
published,
|
||||
selectCreatedMobileSessionTabForClient: vi.fn(() => true),
|
||||
// Stand in for main's layout write so both builds share fakes; main-layout-agent-launch-tabs.test.ts covers the real write.
|
||||
// Stand in for main's layout write so both builds share fakes.
|
||||
publishAgentLaunchTabInMainLayout: vi.fn(
|
||||
(request: Omit<AgentLaunchTabPublishRequest, 'requestId'>) =>
|
||||
runtime.publishAgentLaunchTab(request)
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { FsChangeEvent } from '../../../../../shared/file-watch/filesystem-change-event'
|
||||
import '../../../../../shared/file-watch/filesystem-watch-batch-window'
|
||||
import { createFileWatchEventBatcher } from './file-watch-event-batcher'
|
||||
|
||||
describe('createFileWatchEventBatcher', () => {
|
||||
it('collapses very large watcher bursts without spreading them onto the stack', () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const emit = vi.fn()
|
||||
const batcher = createFileWatchEventBatcher('worktree-1', emit)
|
||||
const event: FsChangeEvent = { kind: 'update', absolutePath: '/repo/file.ts' }
|
||||
const burst = Array.from({ length: 200_000 }, () => event)
|
||||
|
||||
expect(() => batcher.push(burst)).not.toThrow()
|
||||
batcher.flush()
|
||||
|
||||
expect(emit).toHaveBeenCalledWith({
|
||||
type: 'changed',
|
||||
worktree: 'worktree-1',
|
||||
events: [{ kind: 'overflow', absolutePath: '/repo/file.ts' }]
|
||||
})
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
})
|
||||
+1
-106
@@ -60,38 +60,6 @@ export function registerFederatedAutoReleaseScenarios(harness: AutoReleaseScenar
|
||||
return dispatchId
|
||||
}
|
||||
|
||||
async function releaseDueOnHost(): Promise<void> {
|
||||
vi.setSystemTime(Date.now() + GRACE_MS)
|
||||
await getWorkerTerminalAutoReleaseScheduler(harness.workerRuntime()).releaseDue()
|
||||
}
|
||||
|
||||
async function listFromHome(dispatchId: string) {
|
||||
const listed = await harness.dispatchHome({
|
||||
id: 'rpc_remote_list',
|
||||
authToken: 'coordinator-token',
|
||||
method: 'orchestration.workerList',
|
||||
params: { includeRemote: true }
|
||||
})
|
||||
if (!listed.ok) {
|
||||
throw new Error('worker-list failed')
|
||||
}
|
||||
const result: unknown = listed.result
|
||||
const workers =
|
||||
typeof result === 'object' &&
|
||||
result !== null &&
|
||||
'workers' in result &&
|
||||
Array.isArray(result.workers)
|
||||
? result.workers
|
||||
: []
|
||||
return workers.find(
|
||||
(worker: unknown) =>
|
||||
typeof worker === 'object' &&
|
||||
worker !== null &&
|
||||
'dispatchId' in worker &&
|
||||
worker.dispatchId === dispatchId
|
||||
)?.projection
|
||||
}
|
||||
|
||||
function retainFromHome(dispatchId: string) {
|
||||
return harness.dispatchHome({
|
||||
id: 'rpc_remote_retain',
|
||||
@@ -103,54 +71,6 @@ export function registerFederatedAutoReleaseScenarios(harness: AutoReleaseScenar
|
||||
})
|
||||
}
|
||||
|
||||
it('converges the home through worker-show after a host-side release', async () => {
|
||||
const dispatchId = await settleRemotely()
|
||||
await releaseDueOnHost()
|
||||
|
||||
await harness.dispatchHome({
|
||||
id: 'rpc_remote_show_after_release',
|
||||
authToken: 'coordinator-token',
|
||||
method: 'orchestration.workerShow',
|
||||
params: { dispatch: dispatchId }
|
||||
})
|
||||
|
||||
expect(harness.homeDb().getWorkerDispatch(dispatchId)).toMatchObject({
|
||||
stage: 'released',
|
||||
agent_terminal_handle: null
|
||||
})
|
||||
})
|
||||
|
||||
it('forwards worker-retain to the host so its deadline never fires', async () => {
|
||||
const dispatchId = await settleRemotely()
|
||||
|
||||
await expect(retainFromHome(dispatchId)).resolves.toMatchObject({
|
||||
ok: true,
|
||||
result: { state: 'retained', reason: 'user_requested' }
|
||||
})
|
||||
await releaseDueOnHost()
|
||||
|
||||
expect(harness.remoteCalls()).toContain('orchestration.federationRetain')
|
||||
expect(harness.workerRuntime().closeTerminal).not.toHaveBeenCalled()
|
||||
expect(harness.workerDb().getWorkerTerminalResourceByOwner(dispatchId)).toMatchObject({
|
||||
release_state: 'retained',
|
||||
auto_release_at: null
|
||||
})
|
||||
})
|
||||
|
||||
it('holds a host close while its agent works again after worker_done', async () => {
|
||||
const dispatchId = await settleRemotely()
|
||||
vi.spyOn(harness.workerRuntime(), 'isWorkerAgentInTurn').mockReturnValue(true)
|
||||
|
||||
await releaseDueOnHost()
|
||||
|
||||
expect(harness.workerRuntime().closeTerminal).not.toHaveBeenCalled()
|
||||
expect(harness.workerDb().getWorkerTerminalResourceByOwner(dispatchId)).toMatchObject({
|
||||
ownership_state: 'owned',
|
||||
release_state: 'not_requested',
|
||||
auto_release_at: null
|
||||
})
|
||||
})
|
||||
|
||||
function holdHostCapture() {
|
||||
const read = {
|
||||
handle: 'term_windows_worker',
|
||||
@@ -184,30 +104,5 @@ export function registerFederatedAutoReleaseScenarios(harness: AutoReleaseScenar
|
||||
expect(harness.workerDb().getWorkerTerminalArchive(dispatchId)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('keeps manual release when the home predates automatic release', async () => {
|
||||
harness.setHomeOmitsAutoRelease(true)
|
||||
const dispatchId = await settleRemotely()
|
||||
|
||||
await releaseDueOnHost()
|
||||
|
||||
expect(harness.workerDb().getRemoteDispatchAttachment(dispatchId)?.state).toBe('succeeded')
|
||||
expect(harness.workerDb().getWorkerTerminalResourceByOwner(dispatchId)).toMatchObject({
|
||||
release_state: 'not_requested',
|
||||
auto_release_at: null
|
||||
})
|
||||
expect(harness.workerRuntime().closeTerminal).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
registerHomeAutoReleaseScenarios({ harness, settleRemotely, listFromHome, retainFromHome })
|
||||
|
||||
it('keeps worker-retain home-only for a host that predates automatic release', async () => {
|
||||
harness.setWorkerAdvertisesAutoRelease(false)
|
||||
const dispatchId = await harness.startRemoteWorker()
|
||||
|
||||
await expect(retainFromHome(dispatchId)).resolves.toMatchObject({
|
||||
ok: true,
|
||||
result: { state: 'retained', reason: 'no_owned_resource' }
|
||||
})
|
||||
expect(harness.remoteCalls()).not.toContain('orchestration.federationRetain')
|
||||
})
|
||||
registerHomeAutoReleaseScenarios({ harness, settleRemotely })
|
||||
}
|
||||
|
||||
-2
@@ -8,8 +8,6 @@ import { GRACE_MS } from './federation-auto-release-grace.test-support'
|
||||
export function registerHomeAutoReleaseScenarios(args: {
|
||||
harness: AutoReleaseScenarioHarness
|
||||
settleRemotely: () => Promise<string>
|
||||
listFromHome: (dispatchId: string) => Promise<unknown>
|
||||
retainFromHome: (dispatchId: string) => Promise<unknown>
|
||||
}): void {
|
||||
const { harness } = args
|
||||
|
||||
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createRootDispatch } from '../../../../orchestration/db/root-dispatch-test-fixture'
|
||||
import { createOrchestrationRpcHarness } from '../rpc-test-harness'
|
||||
import { mapRuntimeError } from '../../../errors'
|
||||
|
||||
describe('implicit task-scoped message routing', () => {
|
||||
const harness = createOrchestrationRpcHarness()
|
||||
afterEach(() => harness.cleanup())
|
||||
|
||||
function setup(sender: 'coordinator' | 'worker' = 'coordinator') {
|
||||
const state = harness.setup()
|
||||
const ownTask = state.db.createTask({ spec: 'Sender work' })
|
||||
const otherRun = state.db.createRun({
|
||||
objective: 'Other Run',
|
||||
coordinatorHandle: 'term_other',
|
||||
coordinatorPaneKey: 'tab_other:leaf_other'
|
||||
})
|
||||
const otherTask = state.db.createTask({ runId: otherRun.id, spec: 'Other Run work' })
|
||||
const from = sender === 'worker' ? 'term_worker' : 'term_coord'
|
||||
let dispatchId: string | undefined
|
||||
if (sender === 'worker') {
|
||||
const paneKey = 'tab_worker:leaf_worker'
|
||||
dispatchId = createRootDispatch(state.db, ownTask.id, from, paneKey).id
|
||||
vi.mocked(state.runtime.getTerminalPaneKey).mockImplementation((handle) =>
|
||||
handle === from ? paneKey : null
|
||||
)
|
||||
}
|
||||
const send = (params: Record<string, unknown> = {}) =>
|
||||
harness.call(
|
||||
'orchestration.send',
|
||||
{ from, type: 'status', subject: 'Task progress', ...params },
|
||||
state.ctx
|
||||
)
|
||||
return { ...state, ownTask, otherTask, otherRun, from, dispatchId, send }
|
||||
}
|
||||
|
||||
it.each(['coordinator', 'worker'] as const)(
|
||||
'refuses a %s message naming another Run task before writing or waking mail',
|
||||
async (sender) => {
|
||||
const { db, runtime, otherTask, otherRun, activeRunId, send } = setup(sender)
|
||||
const notify = vi.spyOn(runtime, 'notifyMessageArrived')
|
||||
|
||||
const error = await send({ payload: JSON.stringify({ taskId: otherTask.id }) }).catch(
|
||||
(caught: unknown) => caught
|
||||
)
|
||||
expect(mapRuntimeError('req', { runtimeId: 'runtime' }, error).error).toMatchObject({
|
||||
code: 'task_run_mismatch',
|
||||
message: expect.stringContaining(
|
||||
`Task ${otherTask.id} belongs to Run ${otherRun.id}, not the inferred Run ${activeRunId}.`
|
||||
)
|
||||
})
|
||||
|
||||
expect(db.getInbox(100)).toEqual([])
|
||||
expect(notify).not.toHaveBeenCalled()
|
||||
expect(db.getTask(otherTask.id)?.status).toBe('ready')
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -1,54 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { OrcaRuntimeService } from '../../../orca-runtime'
|
||||
import { RpcDispatcher } from '../../dispatcher'
|
||||
import { PAIRING_METHODS } from './pairing'
|
||||
|
||||
function dispatchPairing(
|
||||
method: string,
|
||||
params: unknown,
|
||||
pairing: NonNullable<Parameters<RpcDispatcher['dispatchStreaming']>[2]>['pairing']
|
||||
): Promise<Record<string, unknown>> {
|
||||
return new Promise((resolve) => {
|
||||
const runtime = { getRuntimeId: () => 'test-runtime' }
|
||||
const dispatcher = new RpcDispatcher({
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Pairing handlers use the supplied pairing context; the envelope reads only getRuntimeId.
|
||||
runtime: runtime as OrcaRuntimeService,
|
||||
methods: PAIRING_METHODS
|
||||
})
|
||||
void dispatcher.dispatchStreaming(
|
||||
{ id: 'request-1', authToken: '', method, params },
|
||||
(response) => resolve(JSON.parse(response) as Record<string, unknown>),
|
||||
{ pairing }
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
describe('pairing RPC methods', () => {
|
||||
it('rejects caller-selected identity and authorization metadata', async () => {
|
||||
const pairing = { getEndpoints: vi.fn(), provisionRelay: vi.fn() }
|
||||
|
||||
for (const injected of [
|
||||
{ relayDeviceId: 'attacker-device' },
|
||||
{ authorization: { mode: 'relay-basis', basisConnId: 'attacker-basis' } },
|
||||
{ directAuthId: 'attacker-direct' },
|
||||
{ acceptedCredentialVersion: 99 }
|
||||
]) {
|
||||
await expect(
|
||||
dispatchPairing(
|
||||
'pairing.provisionRelay',
|
||||
{ reqId: 'install-1', newResumeTokenHash: 'A'.repeat(43), ...injected },
|
||||
pairing
|
||||
)
|
||||
).resolves.toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
|
||||
}
|
||||
await expect(
|
||||
dispatchPairing(
|
||||
'pairing.getEndpoints',
|
||||
{ installReqId: 'status-1', basisConnId: 'injected' },
|
||||
pairing
|
||||
)
|
||||
).resolves.toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
|
||||
expect(pairing.provisionRelay).not.toHaveBeenCalled()
|
||||
expect(pairing.getEndpoints).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,46 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { z } from 'zod'
|
||||
import type { Repo } from '../../../../../shared/repo-types'
|
||||
import '../../dispatcher'
|
||||
|
||||
import './repo'
|
||||
import { makeRequest, repoDispatcher } from './repo-rpc.test-fixture'
|
||||
import '../../../../../shared/protocol-version'
|
||||
|
||||
import '../../../../../shared/worktree-runtime-capabilities'
|
||||
import '../../../../../shared/remote-runtime/remote-runtime-client-capabilities'
|
||||
import '../../../../../shared/repo-search-limits'
|
||||
import '../../../../../shared/ssh-target-management'
|
||||
|
||||
describe('repo RPC methods', () => {
|
||||
it('answers repo.list snapshot reads unchanged while the repos stay the same', async () => {
|
||||
let repos: Repo[] = [
|
||||
{ id: 'repo-1', path: '/r', displayName: 'api', badgeColor: '#fff', addedAt: 1 }
|
||||
]
|
||||
const dispatcher = repoDispatcher({
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
listVisibleRepos: () => repos
|
||||
})
|
||||
const snapshotIdOf = (reply: unknown) =>
|
||||
z.object({ snapshotId: z.string() }).parse(reply).snapshotId
|
||||
const result = async (params?: unknown) => {
|
||||
const response = await dispatcher.dispatch(makeRequest('repo.list', params))
|
||||
return response.ok ? response.result : null
|
||||
}
|
||||
|
||||
const legacy = await result()
|
||||
const first = await result({ afterSnapshotId: null })
|
||||
const repeat = await result({ afterSnapshotId: snapshotIdOf(first) })
|
||||
repos = [{ id: 'repo-1', path: '/r', displayName: 'api-2', badgeColor: '#fff', addedAt: 1 }]
|
||||
const changed = await result({ afterSnapshotId: snapshotIdOf(first) })
|
||||
|
||||
expect(legacy).toEqual({ repos: [expect.objectContaining({ displayName: 'api' })] })
|
||||
expect(first).toEqual({
|
||||
repos: [expect.objectContaining({ displayName: 'api' })],
|
||||
snapshotId: snapshotIdOf(first)
|
||||
})
|
||||
expect(repeat).toEqual({ unchanged: true, snapshotId: snapshotIdOf(first) })
|
||||
expect(changed).toMatchObject({ repos: [expect.objectContaining({ displayName: 'api-2' })] })
|
||||
expect(snapshotIdOf(changed)).not.toBe(snapshotIdOf(first))
|
||||
})
|
||||
})
|
||||
@@ -1,41 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { RpcDispatcher } from '../../../dispatcher'
|
||||
import type { RpcRequest } from '../../../core'
|
||||
import '../../../../orca-runtime'
|
||||
import type { OrcaRuntimeService } from '../../../../orca-runtime'
|
||||
|
||||
import { GITLAB_METHODS } from './gitlab'
|
||||
import '../../../../../../shared/rpc-contract/gitlab-params'
|
||||
|
||||
function makeRequest(method: string, params?: unknown): RpcRequest {
|
||||
return { id: 'req-1', authToken: 'tok', method, params }
|
||||
}
|
||||
|
||||
describe('gitlab RPC methods', () => {
|
||||
// Bound excerpts before serialization: WS/relay frames over 1 MiB close the connection (#7732).
|
||||
it('bounds the job trace before it crosses the transport when logExcerpt is set', async () => {
|
||||
const noisyTrace = [
|
||||
'section_start:1699000000:build\r\u001b[0K$ pnpm build',
|
||||
...Array.from({ length: 400 }, (_, index) => `line ${index}`),
|
||||
'ERROR: Job failed: exit code 1'
|
||||
].join('\n')
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
getGitLabRepoJobTrace: vi.fn().mockResolvedValue({ ok: true, trace: noisyTrace })
|
||||
} as unknown as OrcaRuntimeService
|
||||
const dispatcher = new RpcDispatcher({ runtime, methods: GITLAB_METHODS })
|
||||
|
||||
const raw = await dispatcher.dispatch(
|
||||
makeRequest('gitlab.jobTrace', { repo: 'id:repo-1', jobId: 99 })
|
||||
)
|
||||
const excerpt = await dispatcher.dispatch(
|
||||
makeRequest('gitlab.jobTrace', { repo: 'id:repo-1', jobId: 99, logExcerpt: true })
|
||||
)
|
||||
|
||||
expect((raw as { result: { trace: string } }).result.trace).toBe(noisyTrace)
|
||||
const bounded = (excerpt as { result: { trace: string } }).result.trace
|
||||
expect(bounded).toContain('ERROR: Job failed: exit code 1')
|
||||
expect(bounded).not.toContain('section_start')
|
||||
expect(bounded).not.toContain('line 0\n')
|
||||
})
|
||||
})
|
||||
@@ -1,60 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import '../../../shared/orchestration/orchestration-session-caller-codes'
|
||||
|
||||
import {
|
||||
ADDRESS_X,
|
||||
createSessionCallerHarness,
|
||||
orchestrationRequest,
|
||||
resultOf,
|
||||
SESSION_X,
|
||||
SESSION_Y,
|
||||
sessionRecord,
|
||||
type SessionCallerHarness
|
||||
} from './orchestration-session-caller-test-fixture'
|
||||
|
||||
const hostRef = vi.hoisted((): { current: unknown } => ({ current: null }))
|
||||
vi.mock('../../native-chat/agent-session-wire/host/structured-agent-session-registry', () => ({
|
||||
getStructuredAgentSessionHost: () => hostRef.current
|
||||
}))
|
||||
|
||||
describe('orchestration.callerShow: a session learns its Orca session ID from the host', () => {
|
||||
let h: SessionCallerHarness
|
||||
|
||||
beforeEach(() => {
|
||||
h = createSessionCallerHarness(hostRef)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
h.close()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
function callerShow(options: Parameters<typeof orchestrationRequest>[2]) {
|
||||
return orchestrationRequest('orchestration.callerShow', {}, options)
|
||||
}
|
||||
|
||||
it('gives the menu the Orca session ID each session of a cleared chat acts as', async () => {
|
||||
const cleared = sessionRecord(SESSION_X)
|
||||
h.records.set(SESSION_X, {
|
||||
...cleared,
|
||||
conversationCommand: {
|
||||
command: 'clear',
|
||||
state: 'completed',
|
||||
replacementSessionId: SESSION_Y,
|
||||
operationId: 'op',
|
||||
callerKey: 'caller',
|
||||
phase: 'committed'
|
||||
}
|
||||
})
|
||||
|
||||
for (const sessionId of [SESSION_X, SESSION_Y]) {
|
||||
const shown = resultOf(
|
||||
await h.dispatch(orchestrationRequest('orchestration.sessionAddress', { sessionId }))
|
||||
)
|
||||
const acting = resultOf(await h.dispatch(callerShow({ sessionId })))
|
||||
// One derivation: the conversation's root, which the successor copies and acts as too.
|
||||
expect(shown.orcaSessionId).toBe(ADDRESS_X)
|
||||
expect(acting.caller).toMatchObject({ orcaSessionId: shown.orcaSessionId })
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,74 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import 'zod'
|
||||
|
||||
import './core'
|
||||
import './methods/orchestration/orchestration'
|
||||
|
||||
import {
|
||||
ADDRESS_X,
|
||||
createSessionCallerHarness,
|
||||
orchestrationRequest,
|
||||
idOf,
|
||||
resultOf,
|
||||
SESSION_X,
|
||||
SESSION_Y,
|
||||
sessionRecord,
|
||||
type SessionCallerHarness
|
||||
} from './orchestration-session-caller-test-fixture'
|
||||
import './orchestration-session-caller'
|
||||
import '../orchestration/addressing/orchestration-party'
|
||||
|
||||
const hostRef = vi.hoisted((): { current: unknown } => ({ current: null }))
|
||||
vi.mock('../../native-chat/agent-session-wire/host/structured-agent-session-registry', () => ({
|
||||
getStructuredAgentSessionHost: () => hostRef.current
|
||||
}))
|
||||
|
||||
describe('orchestration session callers at the dispatch entry', () => {
|
||||
let h: SessionCallerHarness
|
||||
|
||||
beforeEach(() => {
|
||||
h = createSessionCallerHarness(hostRef)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
h.close()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('a declared caller must name the session', () => {
|
||||
it('accepts a /clear successor naming the address it had before the clear, and no other', async () => {
|
||||
// Y continued X after a /clear, so X's address is Y's: the lineage root names the chat.
|
||||
h.records.set(SESSION_X, {
|
||||
...sessionRecord(SESSION_X),
|
||||
conversationCommand: {
|
||||
command: 'clear',
|
||||
state: 'completed',
|
||||
replacementSessionId: SESSION_Y,
|
||||
operationId: 'op',
|
||||
callerKey: 'caller',
|
||||
phase: 'committed'
|
||||
}
|
||||
})
|
||||
const run = resultOf(
|
||||
await h.dispatch(
|
||||
orchestrationRequest(
|
||||
'orchestration.runCreate',
|
||||
{ objective: 'o', from: ADDRESS_X },
|
||||
{ sessionId: SESSION_Y }
|
||||
)
|
||||
)
|
||||
).run
|
||||
expect(h.db.getRunRaw(idOf(run))?.coordinator_orca_session_id).toBe(SESSION_X)
|
||||
|
||||
const stranger = 'orca_session_id:0b5e2d7c-9a41-4c3e-8f62-7d1a3e5b9c08'
|
||||
const refused = await h.dispatch(
|
||||
orchestrationRequest(
|
||||
'orchestration.runCreate',
|
||||
{ objective: 'o', from: stranger },
|
||||
{ sessionId: SESSION_Y }
|
||||
)
|
||||
)
|
||||
expect(refused).toMatchObject({ ok: false, error: { code: 'consumer_fenced' } })
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,62 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { OrcaRuntimeService } from '../orca-runtime'
|
||||
import type { RpcRequest, RpcResponse } from './core'
|
||||
import { RpcDispatcher } from './dispatcher'
|
||||
import { ALL_RPC_METHODS } from './methods/rpc-method-registry'
|
||||
import type { RpcCallerScope } from './rpc-caller-scope'
|
||||
|
||||
const PAIRED: RpcCallerScope = { kind: 'runtime-paired', grants: [] }
|
||||
|
||||
function request(method: string, params?: unknown): RpcRequest {
|
||||
return { id: `req-${method}`, authToken: 'unused', method, params }
|
||||
}
|
||||
|
||||
async function dispatchAs(
|
||||
scope: RpcCallerScope | undefined,
|
||||
method: string,
|
||||
params?: unknown,
|
||||
runtime = new OrcaRuntimeService()
|
||||
): Promise<RpcResponse> {
|
||||
return new RpcDispatcher({ runtime, methods: ALL_RPC_METHODS }).dispatch(
|
||||
request(method, params),
|
||||
scope ? { callerScope: scope } : undefined
|
||||
)
|
||||
}
|
||||
|
||||
function errorCode(response: RpcResponse): string | null {
|
||||
return response.ok ? null : response.error.code
|
||||
}
|
||||
|
||||
describe('paired runtime clients', () => {
|
||||
it('cannot drive the desktop without a pairing-time grant', async () => {
|
||||
const response = await dispatchAs(PAIRED, 'computer.click', { app: 'Finder' })
|
||||
expect(errorCode(response)).toBe('forbidden')
|
||||
})
|
||||
|
||||
it.each(['pairing.provisionRelay', 'pairing.getEndpoints', 'notifications.registerPush'])(
|
||||
'are refused pairing and push administration: %s',
|
||||
async (method) => {
|
||||
const response = await dispatchAs(PAIRED, method, {})
|
||||
expect(errorCode(response)).toBe('forbidden')
|
||||
}
|
||||
)
|
||||
|
||||
it('learn an unregistered method is missing rather than forbidden', async () => {
|
||||
const response = await dispatchAs(PAIRED, 'computer.methodFromTheFuture', {})
|
||||
expect(errorCode(response)).toBe('method_not_found')
|
||||
})
|
||||
})
|
||||
|
||||
describe('a pinned in-process bridge dispatcher', () => {
|
||||
it('ignores a wider per-call scope', async () => {
|
||||
const dispatcher = new RpcDispatcher({
|
||||
runtime: new OrcaRuntimeService(),
|
||||
methods: ALL_RPC_METHODS,
|
||||
callerScope: PAIRED
|
||||
})
|
||||
const response = await dispatcher.dispatch(request('computer.click', { app: 'Finder' }), {
|
||||
callerScope: { kind: 'owner' }
|
||||
})
|
||||
expect(errorCode(response)).toBe('forbidden')
|
||||
})
|
||||
})
|
||||
@@ -1,55 +0,0 @@
|
||||
import { existsSync, mkdtempSync, writeFileSync } from 'node:fs'
|
||||
|
||||
import { tmpdir } from 'node:os'
|
||||
|
||||
import { join } from 'node:path'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
createRuntimeTransportMetadata,
|
||||
sweepOrphanedRuntimeSockets
|
||||
} from './runtime-rpc-socket-metadata'
|
||||
|
||||
describe('sweepOrphanedRuntimeSockets', () => {
|
||||
// Use an always-live non-runner pid (POSIX init) to distinguish own-pid skip, live sibling retention and dead sweep.
|
||||
const SYNTHETIC_OWN_PID = 1
|
||||
const KNOWN_DEAD_PID = 99999999
|
||||
|
||||
it.runIf(process.platform !== 'win32')(
|
||||
'sweeps dead-pid sockets while retaining own, alive, and non-matching entries',
|
||||
() => {
|
||||
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-sweep-'))
|
||||
|
||||
const ownPidSocket = join(userDataPath, `o-${SYNTHETIC_OWN_PID}-aaaa.sock`)
|
||||
const aliveSocket = join(userDataPath, `o-${process.pid}-bbbb.sock`)
|
||||
const deadSocket = join(userDataPath, `o-${KNOWN_DEAD_PID}-cccc.sock`)
|
||||
const unrelatedFile = join(userDataPath, 'foo.sock')
|
||||
|
||||
writeFileSync(ownPidSocket, '')
|
||||
writeFileSync(aliveSocket, '')
|
||||
writeFileSync(deadSocket, '')
|
||||
writeFileSync(unrelatedFile, '')
|
||||
|
||||
sweepOrphanedRuntimeSockets(userDataPath, SYNTHETIC_OWN_PID)
|
||||
|
||||
expect(existsSync(ownPidSocket)).toBe(true)
|
||||
expect(existsSync(aliveSocket)).toBe(true)
|
||||
expect(existsSync(deadSocket)).toBe(false)
|
||||
expect(existsSync(unrelatedFile)).toBe(true)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('createRuntimeTransportMetadata', () => {
|
||||
it.runIf(process.platform !== 'win32')(
|
||||
'keeps the socket path within the macOS sun_path limit for a long userData path',
|
||||
() => {
|
||||
const userDataPath = `/Users/someone/${'x'.repeat(130)}/userdata`
|
||||
const { endpoint } = createRuntimeTransportMetadata(userDataPath, 12345, 'darwin', 'rt')
|
||||
// macOS listen() returns EINVAL past 103 bytes (sun_path is 104 including NUL).
|
||||
expect(Buffer.byteLength(endpoint)).toBeLessThanOrEqual(103)
|
||||
expect(endpoint).toMatch(/^\/tmp\/\.orca-daemon-\d+\/[0-9a-f]{12}\/o-12345-rt\.sock$/)
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -1,67 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
TerminalStreamOpcode,
|
||||
decodeTerminalStreamFrame,
|
||||
encodeTerminalStreamFrame
|
||||
} from '../../../shared/terminal/stream/terminal-stream-protocol'
|
||||
import { publishPtyEndpointLoss } from '../terminal/pty-endpoint-loss-signal'
|
||||
import {
|
||||
WRITE_UNAVAILABLE_OPCODE,
|
||||
sendDesktopMultiplexSubscribe,
|
||||
startDesktopMultiplexSubscribe
|
||||
} from './terminal-multiplex-test-harness'
|
||||
|
||||
async function subscribe(capabilities: Record<string, 1>) {
|
||||
const harness = startDesktopMultiplexSubscribe()
|
||||
await vi.waitFor(() => expect(harness.handlers.has(0)).toBe(true))
|
||||
sendDesktopMultiplexSubscribe(harness.handlers, capabilities)
|
||||
await vi.waitFor(() =>
|
||||
expect(
|
||||
harness.messages.some((message) => JSON.parse(message).result?.type === 'subscribed')
|
||||
).toBe(true)
|
||||
)
|
||||
harness.binaryFrames.splice(0)
|
||||
return harness
|
||||
}
|
||||
|
||||
const sawWriteUnavailable = (frames: Uint8Array[]): boolean =>
|
||||
frames.some((frame) => frame[2] === WRITE_UNAVAILABLE_OPCODE)
|
||||
|
||||
describe('terminal multiplex daemon endpoint loss notice', () => {
|
||||
it('tells a pane its daemon endpoint dropped before it types anything', async () => {
|
||||
const harness = await subscribe({ ackOutput: 1, writeUnavailable: 1, endpointLossNotice: 1 })
|
||||
|
||||
publishPtyEndpointLoss('pty-1')
|
||||
|
||||
// Push endpoint death immediately; waiting for the next refused keystroke leaves the pane frozen and loses input.
|
||||
expect(sawWriteUnavailable(harness.binaryFrames)).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps the unprompted frame from a client that reads it as refused input', async () => {
|
||||
const harness = await subscribe({ ackOutput: 1, writeUnavailable: 1 })
|
||||
|
||||
publishPtyEndpointLoss('pty-1')
|
||||
|
||||
expect(sawWriteUnavailable(harness.binaryFrames)).toBe(false)
|
||||
})
|
||||
|
||||
it('stops notifying once the stream unsubscribes', async () => {
|
||||
const harness = await subscribe({ ackOutput: 1, writeUnavailable: 1, endpointLossNotice: 1 })
|
||||
harness.handlers.get(7)?.(
|
||||
decodeTerminalStreamFrame(
|
||||
encodeTerminalStreamFrame({
|
||||
opcode: TerminalStreamOpcode.Unsubscribe,
|
||||
streamId: 7,
|
||||
seq: 1,
|
||||
payload: new Uint8Array()
|
||||
})
|
||||
)!
|
||||
)
|
||||
await vi.waitFor(() => expect(harness.handlers.has(7)).toBe(false))
|
||||
harness.binaryFrames.splice(0)
|
||||
|
||||
publishPtyEndpointLoss('pty-1')
|
||||
|
||||
expect(sawWriteUnavailable(harness.binaryFrames)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,71 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { OrcaRuntimeService } from '../orca-runtime'
|
||||
import { RpcDispatcher } from './dispatcher'
|
||||
import type { RpcRequest } from './core'
|
||||
import { TERMINAL_METHODS } from './methods/terminal/terminal'
|
||||
|
||||
function request(method: string, params: unknown): RpcRequest {
|
||||
return { id: 'request-1', authToken: 'test-token', method, params }
|
||||
}
|
||||
|
||||
describe('OpenCode guarded terminal send', () => {
|
||||
afterEach(() => vi.useRealTimers())
|
||||
|
||||
it('refuses a marker title left on a shell without writing notes', async () => {
|
||||
vi.useFakeTimers()
|
||||
const write = vi.fn(() => true)
|
||||
const runtime = new OrcaRuntimeService()
|
||||
runtime.setPtyController({
|
||||
write,
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => 'zsh'
|
||||
})
|
||||
runtime.attachWindow(1)
|
||||
runtime.syncWindowGraph(1, {
|
||||
tabs: [
|
||||
{
|
||||
tabId: 'tab-1',
|
||||
worktreeId: 'repo-1::/tmp/worktree',
|
||||
title: 'OC | zsh',
|
||||
activeLeafId: 'pane-1',
|
||||
layout: null
|
||||
}
|
||||
],
|
||||
leaves: [
|
||||
{
|
||||
tabId: 'tab-1',
|
||||
worktreeId: 'repo-1::/tmp/worktree',
|
||||
leafId: 'pane-1',
|
||||
paneRuntimeId: 1,
|
||||
ptyId: 'pty-1',
|
||||
paneTitle: 'OC | zsh'
|
||||
}
|
||||
]
|
||||
})
|
||||
const [terminal] = (await runtime.listTerminals()).terminals
|
||||
const dispatcher = new RpcDispatcher({ runtime, methods: TERMINAL_METHODS })
|
||||
|
||||
await expect(
|
||||
dispatcher.dispatch(request('terminal.agentStatus', { terminal: terminal.handle }))
|
||||
).resolves.toMatchObject({
|
||||
ok: true,
|
||||
result: { agentStatus: { isRunningAgent: false, status: null } }
|
||||
})
|
||||
|
||||
const send = dispatcher.dispatch(
|
||||
request('terminal.send', {
|
||||
terminal: terminal.handle,
|
||||
text: '$(touch should-not-run)',
|
||||
requireAgentStatus: 'sendable',
|
||||
client: { id: 'desktop-1', type: 'desktop' }
|
||||
})
|
||||
)
|
||||
await vi.advanceTimersByTimeAsync(1_500)
|
||||
|
||||
await expect(send).resolves.toMatchObject({
|
||||
ok: true,
|
||||
result: { send: { accepted: false, bytesWritten: 0, refusedReason: 'no-agent' } }
|
||||
})
|
||||
expect(write).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,35 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { RuntimeWorktreePsResult } from '../../../shared/runtime-worktree-contracts'
|
||||
import { resolveWorktreeCatalogSnapshot } from './worktree-catalog-snapshot'
|
||||
|
||||
function result(totalCount: number): RuntimeWorktreePsResult {
|
||||
return { worktrees: [], totalCount, truncated: false }
|
||||
}
|
||||
|
||||
describe('resolveWorktreeCatalogSnapshot', () => {
|
||||
it('returns unchanged only when the caller owns the exact current snapshot', () => {
|
||||
const first = resolveWorktreeCatalogSnapshot(result(1), { afterSnapshotId: null })
|
||||
expect(first).toMatchObject({ totalCount: 1 })
|
||||
|
||||
const snapshotId = first.snapshotId
|
||||
expect(resolveWorktreeCatalogSnapshot(result(1), { afterSnapshotId: snapshotId })).toEqual({
|
||||
unchanged: true,
|
||||
snapshotId
|
||||
})
|
||||
expect(resolveWorktreeCatalogSnapshot(result(1), { afterSnapshotId: 'unknown' })).toEqual({
|
||||
...result(1),
|
||||
snapshotId
|
||||
})
|
||||
})
|
||||
|
||||
it('derives ids from content alone, so memo state cannot change the answer', () => {
|
||||
// Identical catalogs must yield identical ids whether the memo is warm or was just
|
||||
// displaced — that is what makes the memo droppable and restarts safe.
|
||||
const warm = resolveWorktreeCatalogSnapshot(result(1), { afterSnapshotId: null }).snapshotId
|
||||
resolveWorktreeCatalogSnapshot(result(99), { afterSnapshotId: null })
|
||||
|
||||
expect(resolveWorktreeCatalogSnapshot(result(1), { afterSnapshotId: null }).snapshotId).toBe(
|
||||
warm
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,62 +0,0 @@
|
||||
// Layout-owner build, no window: an agent launch's early tab in a workspace main owns is written by
|
||||
// main, a retry finds it, and a withdrawn launch pane is taken back by main.
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
WORKTREE_ID,
|
||||
setupMainOwnedWorkspace
|
||||
} from '../terminal/layout/main-owned-workspace-runtime-fixture'
|
||||
|
||||
import { publishAgentLaunchTabEarly } from '../rpc/methods/agent-launch/agent-launch-tab-publication'
|
||||
|
||||
import { resetAgentLaunchPanesForTests } from '../../agent-launch/agent-launch-pane-attachment'
|
||||
|
||||
vi.mock('../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
|
||||
|
||||
const NEW_TAB = '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
|
||||
const NEW_LEAF = '3f2504e0-4f89-41d3-9a0c-0305e82c3301'
|
||||
|
||||
afterEach(() => resetAgentLaunchPanesForTests())
|
||||
|
||||
function launchHost() {
|
||||
const fixture = setupMainOwnedWorkspace()
|
||||
Object.assign(fixture.runtime, {
|
||||
showTerminalWorkspaceLaunchScope: async () => ({ id: WORKTREE_ID, path: '/workspace' })
|
||||
})
|
||||
const context = { runtime: fixture.runtime }
|
||||
const launch = () =>
|
||||
publishAgentLaunchTabEarly(
|
||||
{
|
||||
agent: 'claude',
|
||||
target: { kind: 'existing', worktree: `id:${WORKTREE_ID}` },
|
||||
paneKey: `${NEW_TAB}:${NEW_LEAF}`,
|
||||
surface: 'terminal'
|
||||
},
|
||||
context
|
||||
)
|
||||
const tabIds = () => fixture.session().tabsByWorktree[WORKTREE_ID]?.map((tab) => tab.id) ?? []
|
||||
return { fixture, launch, tabIds }
|
||||
}
|
||||
|
||||
describe("an agent launch's early tab in a workspace main owns, with no window", () => {
|
||||
it('takes the tab back when nothing will run in it', async () => {
|
||||
const { launch, tabIds } = launchHost()
|
||||
|
||||
const early = await launch()
|
||||
// Never admitted: the launch made this tab, and nothing will ever run in it.
|
||||
early?.finish('agent_launch_refused')
|
||||
await vi.waitFor(() => expect(tabIds()).not.toContain(NEW_TAB))
|
||||
})
|
||||
|
||||
it('keeps a tab a retry found, since that launch made none', async () => {
|
||||
const { launch, tabIds } = launchHost()
|
||||
|
||||
await launch()
|
||||
resetAgentLaunchPanesForTests()
|
||||
const retry = await launch()
|
||||
retry?.finish('agent_launch_refused')
|
||||
await Promise.resolve()
|
||||
|
||||
expect(tabIds()).toContain(NEW_TAB)
|
||||
})
|
||||
})
|
||||
@@ -1,62 +0,0 @@
|
||||
// A windowless server's held session.tabs rows across main's layout commits: what a commit keeps,
|
||||
// what it pushes, and which writes the row writer may skip.
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { HEADLESS_RUNTIME_WINDOW_ID } from '../../../shared/runtime-session-contracts'
|
||||
import {
|
||||
HEADLESS_LEAF_ID,
|
||||
TEST_WORKTREE_ID
|
||||
} from '../orca-runtime-tests/orca-runtime-test-fixtures.spec'
|
||||
import {
|
||||
SURFACE_TAB_ID,
|
||||
surfaceRuntime
|
||||
} from '../terminal/surface/terminal-surface-replies.test-fixture'
|
||||
import './session-tabs-layout-moves.test-fixture'
|
||||
|
||||
vi.mock('../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
|
||||
|
||||
type Runtime = ReturnType<typeof surfaceRuntime>['runtime']
|
||||
|
||||
const held = (runtime: Runtime) => runtime.mobileSessionTabsByWorktree.get(TEST_WORKTREE_ID)
|
||||
|
||||
async function windowlessRows(runtime: Runtime): Promise<void> {
|
||||
runtime.syncWindowGraph(HEADLESS_RUNTIME_WINDOW_ID, { tabs: [], leaves: [] })
|
||||
await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)
|
||||
await vi.runAllTimersAsync()
|
||||
}
|
||||
|
||||
describe('held session.tabs rows across layout commits (switch on)', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('a pane main rebound takes the new terminal once it connects, with no commit', async () => {
|
||||
const { runtime, bindPane } = surfaceRuntime()
|
||||
await windowlessRows(runtime)
|
||||
const rowId = `${SURFACE_TAB_ID}::${HEADLESS_LEAF_ID}`
|
||||
const ptyOf = () => {
|
||||
const row = held(runtime)?.tabs.find((tab) => tab.id === rowId)
|
||||
return row?.type === 'terminal' ? row.ptyId : undefined
|
||||
}
|
||||
|
||||
await bindPane({
|
||||
worktreeId: TEST_WORKTREE_ID,
|
||||
tabId: SURFACE_TAB_ID,
|
||||
leafId: HEADLESS_LEAF_ID,
|
||||
ptyId: 'pty-new'
|
||||
})
|
||||
await vi.runAllTimersAsync()
|
||||
// Not running yet: the running terminal stands.
|
||||
expect(ptyOf()).toBe('pty-a')
|
||||
|
||||
runtime.registerPty('pty-new', TEST_WORKTREE_ID, null, {
|
||||
tabId: SURFACE_TAB_ID,
|
||||
leafId: HEADLESS_LEAF_ID
|
||||
})
|
||||
await vi.runAllTimersAsync()
|
||||
|
||||
expect(ptyOf()).toBe('pty-new')
|
||||
})
|
||||
})
|
||||
@@ -1,53 +0,0 @@
|
||||
import '../../orca-runtime-tests/orca-runtime-test-lifecycle.spec'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
createRuntime,
|
||||
syncSinglePty,
|
||||
TEST_WORKTREE_ID
|
||||
} from '../../orca-runtime-tests/orca-runtime-test-fixtures.spec'
|
||||
import { electronMocks } from '../../orca-runtime-test-mocks/setup.spec'
|
||||
import { createMobileCreateTestNotifier } from '../../orca-runtime-tests/orca-runtime-test-scenario-builders.spec'
|
||||
|
||||
async function createPaneTitleRuntime() {
|
||||
electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false })
|
||||
const runtime = createRuntime()
|
||||
syncSinglePty(runtime)
|
||||
const setPaneTitle = vi.fn(() => true)
|
||||
const renameTerminal = vi.fn()
|
||||
const notifier = { ...createMobileCreateTestNotifier(vi.fn()), setPaneTitle, renameTerminal }
|
||||
runtime.setNotifier(notifier)
|
||||
const handle = (await runtime.listTerminals()).terminals[0].handle
|
||||
return { runtime, handle, setPaneTitle, renameTerminal, notifier }
|
||||
}
|
||||
|
||||
describe('pane-scoped terminal title', () => {
|
||||
it('rejects stale handles after a leaf is replaced, even if its numeric id is reused', async () => {
|
||||
const { runtime, handle, setPaneTitle } = await createPaneTitleRuntime()
|
||||
runtime.syncWindowGraph(1, {
|
||||
tabs: [
|
||||
{
|
||||
tabId: 'tab-1',
|
||||
worktreeId: TEST_WORKTREE_ID,
|
||||
title: 'Codex',
|
||||
activeLeafId: 'replacement',
|
||||
layout: null
|
||||
}
|
||||
],
|
||||
leaves: [
|
||||
{
|
||||
tabId: 'tab-1',
|
||||
worktreeId: TEST_WORKTREE_ID,
|
||||
leafId: 'replacement',
|
||||
paneRuntimeId: 1,
|
||||
ptyId: 'pty-2',
|
||||
paneTitle: null
|
||||
}
|
||||
]
|
||||
})
|
||||
await expect(runtime.setPaneTitle(handle, 'WRONG')).rejects.toThrow('terminal_handle_stale')
|
||||
await expect(runtime.setPaneTitle('unknown-handle', 'WRONG')).rejects.toThrow(
|
||||
'terminal_handle_stale'
|
||||
)
|
||||
expect(setPaneTitle).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,53 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { SshConnection } from './ssh-connection'
|
||||
import {
|
||||
adoptSshConnection,
|
||||
isSshConnectionSolelyOwnedBy,
|
||||
recordSshConnectionOpened,
|
||||
runAttributedToSshOwner
|
||||
} from './ssh-connection-attribution'
|
||||
import { unusedSshConnection } from '../../managed-server/testing/managed-host-transport.test-fixture'
|
||||
|
||||
const transport = (): SshConnection => unusedSshConnection()
|
||||
|
||||
describe('SSH transport attribution', () => {
|
||||
it('lets a cancelled attempt own only the transport its own work opened', async () => {
|
||||
const attempt = Symbol('a')
|
||||
const opened = transport()
|
||||
await runAttributedToSshOwner(attempt, async () => {
|
||||
await Promise.resolve()
|
||||
recordSshConnectionOpened(opened)
|
||||
})
|
||||
const unrelated = transport()
|
||||
recordSshConnectionOpened(unrelated)
|
||||
expect(isSshConnectionSolelyOwnedBy(opened, attempt)).toBe(true)
|
||||
expect(isSshConnectionSolelyOwnedBy(unrelated, attempt)).toBe(false)
|
||||
})
|
||||
|
||||
it('gives the transport up once a completed replacement connect adopts it', async () => {
|
||||
const stale = Symbol('stale')
|
||||
const replacement = Symbol('replacement')
|
||||
const opened = transport()
|
||||
await runAttributedToSshOwner(stale, async () => recordSshConnectionOpened(opened))
|
||||
adoptSshConnection(opened, replacement)
|
||||
expect(isSshConnectionSolelyOwnedBy(opened, stale)).toBe(false)
|
||||
})
|
||||
|
||||
it('gives the transport up once a managed tunnel outside any attempt relies on it', async () => {
|
||||
const stale = Symbol('stale')
|
||||
const opened = transport()
|
||||
await runAttributedToSshOwner(stale, async () => recordSshConnectionOpened(opened))
|
||||
adoptSshConnection(opened)
|
||||
expect(isSshConnectionSolelyOwnedBy(opened, stale)).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps the transport the attempt’s own tunnel adopted inside its decision', async () => {
|
||||
const attempt = Symbol('a')
|
||||
const opened = transport()
|
||||
await runAttributedToSshOwner(attempt, async () => {
|
||||
recordSshConnectionOpened(opened)
|
||||
adoptSshConnection(opened)
|
||||
})
|
||||
expect(isSshConnectionSolelyOwnedBy(opened, attempt)).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,70 +0,0 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { getSetupTrustContent } from '../../shared/orca-hook-setup-config'
|
||||
import '../../renderer/src/runtime/repo-hooks/orca-hook-trust'
|
||||
import '../runtime/repositories/runtime-repository-hooks-commands'
|
||||
import { loadHooks } from './hooks'
|
||||
import { hashOrcaHookTrustContent } from './created-worktree-setup-trust'
|
||||
|
||||
// The prompt hashes the primary checkout's orca.yaml; the host hashes the created worktree's.
|
||||
// Same committed file => same hash, whatever the checkout did to line endings or a BOM.
|
||||
const SETUP_YAML = [
|
||||
'base: &base',
|
||||
' title: Dev server',
|
||||
'scripts:',
|
||||
' setup: |',
|
||||
' pnpm install ',
|
||||
' \techo "tabbed"',
|
||||
'',
|
||||
' echo "ünïcødé ✓"',
|
||||
'defaultTabs:',
|
||||
' - <<: *base',
|
||||
' command: >',
|
||||
' pnpm dev',
|
||||
' --port 3000',
|
||||
' - title: Notes',
|
||||
' - command: "printf \'%s\\n\' quoted"',
|
||||
'issueCommand: gh issue view',
|
||||
'scripts_unrelated: ignored',
|
||||
''
|
||||
].join('\n')
|
||||
|
||||
const dirs: string[] = []
|
||||
function checkout(content: string): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'orca-setup-parity-'))
|
||||
dirs.push(dir)
|
||||
writeFileSync(join(dir, 'orca.yaml'), content)
|
||||
return dir
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
dirs.length = 0
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
for (const dir of dirs) {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
describe('approved setup hash parity between the trust prompt and the host', () => {
|
||||
it('pins the digest so a format change, which would invalidate saved automation approvals, is deliberate', () => {
|
||||
expect(getSetupTrustContent(loadHooks(checkout(SETUP_YAML)))).toBe(
|
||||
[
|
||||
'pnpm install \n\techo "tabbed"\n\necho "ünïcødé ✓"',
|
||||
'# defaultTabs[1] Dev server\npnpm dev --port 3000',
|
||||
// The double-quoted "\n" escape is a real newline in YAML, on both sides.
|
||||
"# defaultTabs[3]\nprintf '%s\n' quoted"
|
||||
].join('\n\n')
|
||||
)
|
||||
expect(hashOrcaHookTrustContent(getSetupTrustContent(loadHooks(checkout(SETUP_YAML))))).toBe(
|
||||
PINNED_SETUP_HASH
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const PINNED_SETUP_HASH = '112aac76dbc70758498c2b87a2dc42e77f7ce366a5b611749ec38b6757681593'
|
||||
@@ -30,19 +30,4 @@ describe.skipIf(process.platform === 'win32')('archive hook deadline', () => {
|
||||
expect(result.exitCode).toBeUndefined()
|
||||
expect(result.output).toContain('timed out')
|
||||
}, 20_000)
|
||||
|
||||
it('settles at the deadline even when the hook refuses to die', async () => {
|
||||
const started = Date.now()
|
||||
const result = await runArchive("trap '' TERM; sleep 30")
|
||||
expect(result.success).toBe(false)
|
||||
expect(Date.now() - started).toBeLessThan(10_000)
|
||||
}, 20_000)
|
||||
|
||||
it('passes a hook that finishes inside its deadline', async () => {
|
||||
await expect(runArchive('echo archived')).resolves.toMatchObject({ success: true })
|
||||
})
|
||||
|
||||
it('reports an observed non-zero exit as the exit it is', async () => {
|
||||
await expect(runArchive('exit 23')).resolves.toMatchObject({ success: false, exitCode: 23 })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import '../../../shared/automations/scheduling/automation-schedule-occurrences'
|
||||
import type { WslHostAutomationDue } from './wsl-host-automation-wake'
|
||||
import {
|
||||
readWslHostAutomationWakes,
|
||||
writeWslHostAutomationWakes
|
||||
} from './wsl-host-automation-wake-store'
|
||||
|
||||
const HOUR = 60 * 60 * 1000
|
||||
const HOURLY = '0 * * * *'
|
||||
|
||||
/** By default the host was last seen ready an hour before the run, so the run was missed. */
|
||||
function due(
|
||||
nextRunAt: number,
|
||||
graceHours = 12,
|
||||
lastSeenReadyAt = nextRunAt - HOUR
|
||||
): WslHostAutomationDue {
|
||||
return {
|
||||
nextRunAt,
|
||||
wakeUntil: nextRunAt + graceHours * HOUR,
|
||||
schedules: [{ rrule: HOURLY, dtstart: 0, missedRunGraceMinutes: graceHours * 60 }],
|
||||
lastSeenReadyAt
|
||||
}
|
||||
}
|
||||
|
||||
describe('persisted wake times', () => {
|
||||
let dir: string | null = null
|
||||
afterEach(() => {
|
||||
if (dir) {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('round-trips and drops malformed entries', () => {
|
||||
dir = mkdtempSync(join(tmpdir(), 'orca-wsl-wake-'))
|
||||
writeWslHostAutomationWakes(dir, { a: due(1) })
|
||||
expect(readWslHostAutomationWakes(dir)).toEqual({ a: due(1) })
|
||||
writeFileSync(
|
||||
join(dir, 'wsl-host-automation-wakes.json'),
|
||||
JSON.stringify({ b: { nextRunAt: 1 } })
|
||||
)
|
||||
expect(readWslHostAutomationWakes(dir)).toEqual({})
|
||||
expect(readWslHostAutomationWakes(join(dir, 'missing'))).toEqual({})
|
||||
})
|
||||
})
|
||||
@@ -1,62 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { dispatchMock, closeBrowserPageMock, storeState } = vi.hoisted(() => {
|
||||
const value: Record<string, unknown> = {}
|
||||
return { dispatchMock: vi.fn(), closeBrowserPageMock: vi.fn(), storeState: { value } }
|
||||
})
|
||||
|
||||
vi.mock('@/store', () => ({ useAppStore: { getState: () => storeState.value } }))
|
||||
vi.mock('@/lib/workspace/tabs/workspace-tab-commands', () => ({
|
||||
dispatchWorkspaceTabCommand: dispatchMock
|
||||
}))
|
||||
import { handleGuestWindowCloseMessage } from './browser-page-window-close'
|
||||
import { BROWSER_GUEST_WINDOW_CLOSE_CHANNEL } from '../../../../../shared/browser-guest-window-close'
|
||||
|
||||
function seedTab(
|
||||
pageId: string,
|
||||
isPinned = false,
|
||||
siblingPageIds: string[] = [],
|
||||
browserAnnotationsByPageId: Record<string, unknown[]> = {}
|
||||
): void {
|
||||
storeState.value = {
|
||||
closeBrowserPage: closeBrowserPageMock,
|
||||
browserAnnotationsByPageId,
|
||||
browserTabsByWorktree: { 'wt-1': [{ id: 'ws-1' }] },
|
||||
browserPagesByWorkspace: { 'ws-1': [{ id: pageId }, ...siblingPageIds.map((id) => ({ id }))] },
|
||||
unifiedTabsByWorktree: {
|
||||
'wt-1': [{ id: 'tab-1', contentType: 'browser', entityId: 'ws-1', isPinned }]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeFrom(pageId: string, history: { back: boolean; forward: boolean }): void {
|
||||
const webview = { canGoBack: () => history.back, canGoForward: () => history.forward }
|
||||
handleGuestWindowCloseMessage(pageId, webview, { channel: BROWSER_GUEST_WINDOW_CLOSE_CHANNEL })
|
||||
}
|
||||
|
||||
describe('window.close() from a browser page', () => {
|
||||
beforeEach(() => {
|
||||
dispatchMock.mockReset()
|
||||
closeBrowserPageMock.mockReset()
|
||||
})
|
||||
|
||||
it('never closes a pinned tab', () => {
|
||||
seedTab('page-d', true)
|
||||
closeFrom('page-d', { back: false, forward: false })
|
||||
expect(dispatchMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('never discards unsent notes', () => {
|
||||
seedTab('page-n', false, [], { 'page-n': [{ id: 'note-1' }] })
|
||||
closeFrom('page-n', { back: false, forward: false })
|
||||
expect(dispatchMock).not.toHaveBeenCalled()
|
||||
expect(closeBrowserPageMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('closes only the calling page when its Orca tab holds other pages', () => {
|
||||
seedTab('page-e', false, ['page-sibling'])
|
||||
closeFrom('page-e', { back: false, forward: false })
|
||||
expect(closeBrowserPageMock).toHaveBeenCalledWith('page-e')
|
||||
expect(dispatchMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,47 +0,0 @@
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import type * as ConnectionOwnerModule from '@/lib/workspace/ownership/connection-owner-resolution'
|
||||
|
||||
// A workspace this computer owns.
|
||||
vi.mock('@/lib/workspace/ownership/connection-owner-resolution', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof ConnectionOwnerModule>()),
|
||||
getConnectionIdFromState: () => null,
|
||||
getConnectionIdForFileFromState: () => null
|
||||
}))
|
||||
vi.mock('@/lib/files/access/user-opened-local-path', () => ({
|
||||
statUserOpenedPath: vi.fn().mockResolvedValue({ isDirectory: false, escapesWorktree: false })
|
||||
}))
|
||||
|
||||
import { useAppStore } from '@/store'
|
||||
import { editorTabFileAccess } from '@/lib/files/access/local-file-access'
|
||||
import { statUserOpenedPath } from '@/lib/files/access/user-opened-local-path'
|
||||
import { makeWorktree } from '@/store/slices/worktrees/worktree.test-fixture'
|
||||
|
||||
import { openBrowserFileSource } from './browser-file-editor-open'
|
||||
|
||||
const initialState = useAppStore.getInitialState()
|
||||
|
||||
afterEach(() => {
|
||||
useAppStore.setState(initialState, true)
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('keeps user-file access when a previewed symlink escapes the workspace', async () => {
|
||||
const filePath = '/repo/docs-link/report.html'
|
||||
const openFile = vi.fn()
|
||||
useAppStore.setState({
|
||||
openFile,
|
||||
worktreesByRepo: {
|
||||
repo: [makeWorktree({ id: 'wt-1', repoId: 'repo', path: '/repo', hostId: 'local' })]
|
||||
}
|
||||
})
|
||||
vi.mocked(statUserOpenedPath).mockResolvedValueOnce({
|
||||
isDirectory: false,
|
||||
escapesWorktree: true
|
||||
})
|
||||
|
||||
await openBrowserFileSource('wt-1', filePath)
|
||||
|
||||
const opened = openFile.mock.calls[0][0]
|
||||
expect(opened).toMatchObject({ filePath, relativePath: filePath, mode: 'edit' })
|
||||
expect(editorTabFileAccess(useAppStore.getState(), opened)).toEqual({ kind: 'user-file' })
|
||||
})
|
||||
@@ -1,58 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { SpeechModelManifest, SpeechModelState } from '../../../../shared/speech-types'
|
||||
import { findSpeechModelUpdateTarget } from './speech-model-update-target'
|
||||
|
||||
function model(id: string, extra: Partial<SpeechModelManifest> = {}): SpeechModelManifest {
|
||||
return {
|
||||
id,
|
||||
label: id,
|
||||
type: 'transducer',
|
||||
provider: 'local',
|
||||
language: 'en',
|
||||
languages: ['en'],
|
||||
sampleRate: 16000,
|
||||
streaming: false,
|
||||
...extra
|
||||
}
|
||||
}
|
||||
|
||||
const catalog = [
|
||||
model('parakeet'),
|
||||
model('nemotron'),
|
||||
model('zipformer-ko', { replacedBy: 'nemotron' }),
|
||||
model('gpt', { provider: 'openai' })
|
||||
]
|
||||
|
||||
function target(sttModel: string, states: SpeechModelState[], enabled = true) {
|
||||
const found = findSpeechModelUpdateTarget({ enabled, sttModel }, catalog, states)
|
||||
return found && { model: found.model.id, retired: found.retired?.id }
|
||||
}
|
||||
|
||||
describe('findSpeechModelUpdateTarget', () => {
|
||||
it('offers the update when only the old files of the selected model are on disk', () => {
|
||||
const states: SpeechModelState[] = [
|
||||
{ id: 'parakeet', status: 'not-downloaded', needsUpdate: true }
|
||||
]
|
||||
expect(target('parakeet', states)).toEqual({ model: 'parakeet', retired: undefined })
|
||||
expect(target('parakeet', states, false)).toBeNull()
|
||||
})
|
||||
|
||||
it('offers the replacement when the retired model it replaced is still downloaded', () => {
|
||||
const states: SpeechModelState[] = [
|
||||
{ id: 'nemotron', status: 'not-downloaded' },
|
||||
{ id: 'zipformer-ko', status: 'ready' }
|
||||
]
|
||||
expect(target('nemotron', states)).toEqual({ model: 'nemotron', retired: 'zipformer-ko' })
|
||||
expect(
|
||||
target('nemotron', [
|
||||
{ id: 'nemotron', status: 'ready' },
|
||||
{ id: 'zipformer-ko', status: 'ready' }
|
||||
])
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('stays hidden for cloud models and for people who never downloaded one', () => {
|
||||
expect(target('gpt', [{ id: 'zipformer-ko', status: 'ready' }])).toBeNull()
|
||||
expect(target('nemotron', [{ id: 'nemotron', status: 'not-downloaded' }])).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,54 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act, cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { StoppedWslHost } from '../wsl-host-stopped/use-stopped-wsl-host'
|
||||
import { EditorFileLoadErrorView } from './EditorFileLoadErrorView'
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const stopped: { current: StoppedWslHost | null } = { current: null }
|
||||
return {
|
||||
stopped,
|
||||
start: vi.fn<(environmentId: string) => Promise<boolean>>()
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../wsl-host-stopped/use-stopped-wsl-host', () => ({
|
||||
useStoppedWslHost: (worktreeId: string | undefined) =>
|
||||
worktreeId === 'wt-wsl' ? mocks.stopped.current : null
|
||||
}))
|
||||
vi.mock('@/runtime/hosts/wsl/wsl-host-user-start', () => ({
|
||||
startWslHostAndReloadProjects: mocks.start
|
||||
}))
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
mocks.stopped.current = null
|
||||
mocks.start.mockReset()
|
||||
})
|
||||
|
||||
describe('EditorFileLoadErrorView on a stopped WSL host', () => {
|
||||
it('offers Start instead of the read error, and reloads without a wake once started', async () => {
|
||||
mocks.stopped.current = { environmentId: 'wsl:ubuntu', distro: 'Ubuntu' }
|
||||
mocks.start.mockResolvedValue(true)
|
||||
const onRetry = vi.fn()
|
||||
const onHostStarted = vi.fn()
|
||||
const view = render(
|
||||
<EditorFileLoadErrorView
|
||||
message="The server isn't answering"
|
||||
worktreeId="wt-wsl"
|
||||
onRetry={onRetry}
|
||||
onHostStarted={onHostStarted}
|
||||
/>
|
||||
)
|
||||
expect(view.queryByText('Unable to load file')).toBeNull()
|
||||
expect(view.getByText('Ubuntu is stopped')).toBeTruthy()
|
||||
expect(mocks.start).not.toHaveBeenCalled()
|
||||
|
||||
await act(async () => fireEvent.click(view.getByRole('button', { name: 'Start' })))
|
||||
expect(mocks.start).toHaveBeenCalledWith('wsl:ubuntu')
|
||||
// Retry asks to wake the distro; after Start the reload must not.
|
||||
expect(onHostStarted).toHaveBeenCalledTimes(1)
|
||||
expect(onRetry).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,56 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { cleanup, fireEvent, render, waitFor } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import { getDefaultSettings } from '../../../../shared/constants'
|
||||
import { useAppStore } from '@/store'
|
||||
import { usePluginPreviewRequestStore } from '@/store/plugins/navigation/plugin-preview-request'
|
||||
import { PluginFileTypeSuggestion } from './PluginFileTypeSuggestion'
|
||||
|
||||
const suggestForFileType = vi.fn()
|
||||
const updateSettings = vi.fn().mockResolvedValue(undefined)
|
||||
const openSettingsTarget = vi.fn()
|
||||
const openSettingsPage = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
suggestForFileType.mockResolvedValue({
|
||||
marketplaceSourceId: 'official',
|
||||
pluginKey: 'stablyai.orca-typst',
|
||||
fileType: 'typ',
|
||||
name: 'Typst'
|
||||
})
|
||||
vi.stubGlobal('api', { plugins: { suggestForFileType } })
|
||||
useAppStore.setState({
|
||||
settings: { ...getDefaultSettings('/tmp'), pluginSystemEnabled: true },
|
||||
updateSettings,
|
||||
openSettingsTarget,
|
||||
openSettingsPage
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.unstubAllGlobals()
|
||||
vi.clearAllMocks()
|
||||
usePluginPreviewRequestStore.setState({ request: null })
|
||||
})
|
||||
|
||||
it('keeps "Not now" scoped to the file type when the same editor switches files', async () => {
|
||||
suggestForFileType.mockImplementation(async (fileType: string) => ({
|
||||
marketplaceSourceId: 'official',
|
||||
pluginKey: `samples.${fileType}`,
|
||||
fileType,
|
||||
name: `Plugin ${fileType}`
|
||||
}))
|
||||
const view = render(<PluginFileTypeSuggestion filePath="a.foo" />)
|
||||
fireEvent.click(await view.findByRole('button', { name: 'Not now' }))
|
||||
expect(view.queryByRole('status')).toBeNull()
|
||||
|
||||
view.rerender(<PluginFileTypeSuggestion filePath="a.bar" />)
|
||||
expect(await view.findByText('A plugin can open .bar files: Plugin bar')).toBeTruthy()
|
||||
|
||||
fireEvent.click(view.getByRole('button', { name: 'Not now' }))
|
||||
suggestForFileType.mockClear()
|
||||
view.rerender(<PluginFileTypeSuggestion filePath="a.foo" />)
|
||||
await waitFor(() => expect(suggestForFileType).not.toHaveBeenCalled())
|
||||
expect(view.queryByRole('status')).toBeNull()
|
||||
})
|
||||
@@ -1,37 +0,0 @@
|
||||
import { describe, expect, it, afterEach, vi } from 'vitest'
|
||||
|
||||
import { getRuntimeTargetForWorktreeOwner } from '@/lib/files/access/file-owner-runtime-target'
|
||||
|
||||
describe('shouldUploadRemoteEditorFileDrop', () => {
|
||||
it('uses the worktree owner runtime instead of the focused runtime', () => {
|
||||
expect(
|
||||
getRuntimeTargetForWorktreeOwner(
|
||||
{
|
||||
settings: { activeRuntimeEnvironmentId: 'focused-runtime' },
|
||||
repos: [{ id: 'repo-1', connectionId: null, executionHostId: 'runtime:owner-runtime' }],
|
||||
worktreesByRepo: { 'repo-1': [{ id: 'wt-1', repoId: 'repo-1' }] }
|
||||
},
|
||||
'wt-1'
|
||||
)
|
||||
).toEqual({ kind: 'environment', environmentId: 'owner-runtime' })
|
||||
})
|
||||
})
|
||||
|
||||
const { rejectAppStoreAccess } = vi.hoisted(() => ({
|
||||
rejectAppStoreAccess: vi.fn(() => {
|
||||
throw new Error('Pure helper tests must not access the app store')
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: Object.assign(rejectAppStoreAccess, {
|
||||
getState: rejectAppStoreAccess,
|
||||
getInitialState: rejectAppStoreAccess,
|
||||
setState: rejectAppStoreAccess,
|
||||
subscribe: rejectAppStoreAccess
|
||||
})
|
||||
}))
|
||||
|
||||
afterEach(() => {
|
||||
expect(rejectAppStoreAccess).not.toHaveBeenCalled()
|
||||
})
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
readRuntimeFileContent: vi.fn(),
|
||||
runtimePathsExist: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/runtime/files/reads/runtime-file-read-client', () => ({
|
||||
readRuntimeFileContent: mocks.readRuntimeFileContent
|
||||
}))
|
||||
vi.mock('@/runtime/files/metadata/runtime-path-existence-batch', () => ({
|
||||
runtimePathsExist: mocks.runtimePathsExist
|
||||
}))
|
||||
vi.mock('@/lib/workspace/ownership/connection-context', () => ({
|
||||
getConnectionIdForFile: () => undefined
|
||||
}))
|
||||
vi.mock('@/lib/files/access/file-owner-runtime-target', () => {
|
||||
const owner = { kind: 'environment', environmentId: 'env-1' }
|
||||
return {
|
||||
getRuntimeTargetForFileOwner: () => owner,
|
||||
requireRuntimeTargetForFileOwner: () => owner
|
||||
}
|
||||
})
|
||||
|
||||
import { createTestStore } from '@/store/testing/app-store-test-fixture'
|
||||
import { attachRestoredTabConflictScan } from './editor-restored-tab-conflict-scan'
|
||||
import { getDiskBaselineSignature } from '../../../../../shared/file-write/disk-baseline-signature'
|
||||
|
||||
const FILE = '/repo/a.ts'
|
||||
let store: ReturnType<typeof createTestStore>
|
||||
let detach: () => void
|
||||
|
||||
// A server's file is named by its owned id, not its path.
|
||||
let fileId: string
|
||||
const tab = () => store.getState().openFiles.find((file) => file.id === fileId)!
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.clearAllMocks()
|
||||
store = createTestStore()
|
||||
fileId = store.getState().openFile({
|
||||
filePath: FILE,
|
||||
relativePath: 'a.ts',
|
||||
worktreeId: 'wt-1',
|
||||
runtimeEnvironmentId: 'env-1',
|
||||
language: 'typescript',
|
||||
mode: 'edit'
|
||||
})
|
||||
store.getState().setEditorDraft(fileId, 'unsaved')
|
||||
store.getState().markFileDirty(fileId, true)
|
||||
store.getState().setLastKnownDiskSignature(fileId, getDiskBaselineSignature('v1'))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
detach()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('marks a dirty runtime-owned tab deleted when its host says the file is gone', async () => {
|
||||
mocks.readRuntimeFileContent.mockRejectedValue(new Error('read failed'))
|
||||
mocks.runtimePathsExist.mockResolvedValue([{ exists: false }])
|
||||
detach = attachRestoredTabConflictScan(store)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(mocks.runtimePathsExist).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ worktreePath: '/repo' }),
|
||||
[FILE],
|
||||
undefined,
|
||||
{ codedAbsenceOnly: true }
|
||||
)
|
||||
expect(tab().externalMutation).toBe('deleted')
|
||||
})
|
||||
+1
-18
@@ -1,11 +1,7 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import './markdown-preview-search'
|
||||
import { findTextMatchRanges } from '../../../../lib/text-search/text-match-ranges'
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('case-insensitive Markdown search offsets', () => {
|
||||
it.each([
|
||||
{ character: 'İ', first: 9, second: 18 },
|
||||
@@ -18,17 +14,4 @@ describe('case-insensitive Markdown search offsets', () => {
|
||||
{ start: second, end: second + 6 }
|
||||
])
|
||||
})
|
||||
|
||||
it('preserves whole code-point ranges for lone surrogate queries', () => {
|
||||
const text = 'plain😊plain'
|
||||
expect(findTextMatchRanges(text, '\ud83d')).toEqual([{ start: 5, end: 7 }])
|
||||
expect(findTextMatchRanges(text, '\ude0a')).toEqual([{ start: 5, end: 7 }])
|
||||
})
|
||||
|
||||
it('keeps character-wise Greek sigma casing in a Unicode document', () => {
|
||||
expect(findTextMatchRanges('AΣ Aς Aσ', 'aσ')).toEqual([
|
||||
{ start: 0, end: 2 },
|
||||
{ start: 6, end: 8 }
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
-17
@@ -3,27 +3,10 @@ import './markdown-preview-search'
|
||||
import { findTextMatchRanges } from '../../../../lib/text-search/text-match-ranges'
|
||||
|
||||
describe('whole-word search around combining marks', () => {
|
||||
it('does not match a Devanagari word inside a longer word ending in a vowel sign', () => {
|
||||
expect(findTextMatchRanges('किताबें किताब', 'किताब', { wholeWord: true })).toEqual([
|
||||
{ start: 8, end: 13 }
|
||||
])
|
||||
})
|
||||
|
||||
it('finds a complete decomposed accented word and preserves source code-unit offsets', () => {
|
||||
const text = 'café cafe caféine'
|
||||
expect(findTextMatchRanges(text, 'café', { wholeWord: true })).toEqual([{ start: 0, end: 5 }])
|
||||
expect(findTextMatchRanges(text, 'cafe', { wholeWord: true })).toEqual([{ start: 6, end: 10 }])
|
||||
expect(findTextMatchRanges(text, 'cafe')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['⚠️Warning here', 'Warning'],
|
||||
['✔️Done', 'Done'],
|
||||
['1️⃣Step', 'Step']
|
||||
])('matches a whole word right after an emoji presentation sequence in %s', (text, query) => {
|
||||
const start = text.indexOf(query)
|
||||
expect(findTextMatchRanges(text, query, { wholeWord: true })).toEqual([
|
||||
{ start, end: start + query.length }
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
-8
@@ -3,14 +3,6 @@ import './markdown-preview-search'
|
||||
import { findTextMatchRanges } from '../../../../lib/text-search/text-match-ranges'
|
||||
|
||||
describe('findTextMatchRanges', () => {
|
||||
it('folds uppercase Greek queries the same way as the indexed text', () => {
|
||||
expect(findTextMatchRanges('ΟΣ οσ ος', 'ΟΣ')).toEqual([
|
||||
{ start: 0, end: 2 },
|
||||
{ start: 3, end: 5 }
|
||||
])
|
||||
expect(findTextMatchRanges('ΟΣ οσ', 'ΟΣ', { matchCase: true })).toEqual([{ start: 0, end: 2 }])
|
||||
})
|
||||
|
||||
it('checks whole-word boundaries around astral letters by code point', () => {
|
||||
const text = '𐐀cat cat𐐀 cat'
|
||||
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { IpynbCellOutputs } from '../outputs/IpynbCellOutputs'
|
||||
|
||||
import type { IpynbCell, IpynbOutput } from './ipynb-parse'
|
||||
|
||||
vi.mock('@/i18n/i18n', () => ({
|
||||
i18n: { language: 'en' },
|
||||
translate: (_key: string, fallback: string) => fallback
|
||||
}))
|
||||
vi.mock('@/hooks/use-document-dark-theme', () => ({ useDocumentDarkTheme: () => true }))
|
||||
vi.mock('@/lib/editor/monaco/monaco-setup', () => ({ monaco: {} }))
|
||||
vi.mock('../../MonacoCodeExcerpt', () => ({
|
||||
useMonacoColorizedLines: () => []
|
||||
}))
|
||||
|
||||
function cell(kind: IpynbCell['kind'], source: string, outputs: IpynbOutput[] = []): IpynbCell {
|
||||
return { id: 'cell-1', kind, language: 'python', source, executionCount: 1, outputs }
|
||||
}
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('notebook outputs', () => {
|
||||
it('sandboxes HTML output behind a no-network CSP and strips scripts', () => {
|
||||
render(
|
||||
<IpynbCellOutputs
|
||||
cell={cell('code', 'html', [
|
||||
{
|
||||
kind: 'display',
|
||||
outputType: 'display_data',
|
||||
executionCount: null,
|
||||
items: [{ mime: 'text/html', value: '<script>alert(1)</script><b>safe</b>' }]
|
||||
}
|
||||
])}
|
||||
/>
|
||||
)
|
||||
const frame = screen.getByTitle('Notebook HTML output')
|
||||
const source = frame.getAttribute('srcdoc') ?? ''
|
||||
expect(frame.getAttribute('sandbox')).toBe('')
|
||||
expect(source).toContain("default-src 'none'; img-src data:")
|
||||
expect(source).not.toContain('<script')
|
||||
expect(source).toContain('<b>safe</b>')
|
||||
})
|
||||
})
|
||||
-32
@@ -4,38 +4,6 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
import { createClaudeModelSwitchConfirmationObserver } from './claude-model-switch-confirmation'
|
||||
|
||||
describe('Claude model switch confirmation detection', () => {
|
||||
it.each([
|
||||
[
|
||||
'quoted model name',
|
||||
'Claude Opus 5.5',
|
||||
'Set model to `Opus 5.5` and saved as your default for new sessions'
|
||||
],
|
||||
[
|
||||
'matching requested context variant',
|
||||
'Claude Opus 5.5 (1M context)',
|
||||
'Set model to `Opus 5.5 (1M context)` and saved as your default for new sessions'
|
||||
],
|
||||
['raw model id fallback', 'claude-opus-5-5', 'Set model to Opus 5.5'],
|
||||
['alias picker row', 'Opus alias', 'Set model to Opus 5.5']
|
||||
])('confirms a picker label with %s', async (_name, label, output) => {
|
||||
const dataObserver = { current: (_data: string): void => {} }
|
||||
const observer = createClaudeModelSwitchConfirmationObserver({
|
||||
ptyId: 'pty-1',
|
||||
expectedModelLabel: label,
|
||||
subscribeToData: (watcher) => {
|
||||
dataObserver.current = watcher
|
||||
return () => {}
|
||||
}
|
||||
})
|
||||
|
||||
await observer.ready
|
||||
observer.arm()
|
||||
observer.startDetection()
|
||||
dataObserver.current(output)
|
||||
|
||||
await expect(observer.result).resolves.toBe('applied')
|
||||
})
|
||||
|
||||
it('reports matching success output split across PTY chunks', async () => {
|
||||
const dataObserver = { current: (_data: string): void => {} }
|
||||
const unsubscribe = vi.fn(() => {})
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { act, cleanup, renderHook } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import { normalizeTaskSourceContext } from '../../../../shared/work-items/task-source-context'
|
||||
import { useJiraComposerProject } from './use-jira-composer-project'
|
||||
vi.mock('../use-jira-project-inventory', () => ({
|
||||
useJiraProjectInventory: () => ({ projects: [], connected: true, error: null, loading: false })
|
||||
}))
|
||||
const project = { id: '10', key: 'ALP', name: 'Alpha', siteId: 'site-a' }
|
||||
const other = { ...project, id: '20', key: 'BRV', name: 'Bravo' }
|
||||
const repo: Repo = {
|
||||
id: 'repo',
|
||||
path: '/repo',
|
||||
displayName: 'Alpha',
|
||||
badgeColor: '',
|
||||
addedAt: 0,
|
||||
defaultJiraProject: project
|
||||
}
|
||||
const source = normalizeTaskSourceContext({ provider: 'jira', projectId: 'repo', hostId: 'local' })
|
||||
afterEach(cleanup)
|
||||
describe('Jira composer project defaults', () => {
|
||||
it('resets All when switching between same-ID local and SSH repositories', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ current }) => useJiraComposerProject(current, source, true),
|
||||
{ initialProps: { current: repo } }
|
||||
)
|
||||
act(() => result.current.setJiraProject(null))
|
||||
rerender({ current: { ...repo, executionHostId: 'ssh:target', defaultJiraProject: other } })
|
||||
expect(result.current.jiraProject).toEqual(other)
|
||||
})
|
||||
it('resets All when switching between distinct same-host repository locations', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ current }) => useJiraComposerProject(current, source, true),
|
||||
{ initialProps: { current: repo } }
|
||||
)
|
||||
act(() => result.current.setJiraProject(null))
|
||||
rerender({ current: { ...repo, path: '/other-location', defaultJiraProject: other } })
|
||||
expect(result.current.jiraProject).toEqual(other)
|
||||
})
|
||||
})
|
||||
@@ -1,64 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@/lib/workspace/activation/worktree-activation', () => ({
|
||||
activateAndRevealWorkspace: vi.fn()
|
||||
}))
|
||||
vi.mock('@/store', () => ({ useAppStore: { getState: vi.fn() } }))
|
||||
|
||||
import { findWorkspaceShowingFile, folderWorkspaceRows } from './plugin-view-editor-workspace'
|
||||
|
||||
const worktree = (id: string, path: string, extra: Record<string, unknown> = {}) => ({
|
||||
id,
|
||||
path,
|
||||
isArchived: false,
|
||||
...extra
|
||||
})
|
||||
|
||||
describe('the workspace that shows a file', () => {
|
||||
it('matches the real path when the given one goes through a symlink, preferring the deepest', () => {
|
||||
const repos = [
|
||||
worktree('r1::/private/tmp', '/private/tmp'),
|
||||
worktree('r2::/private/tmp/sketch', '/private/tmp/sketch')
|
||||
]
|
||||
expect(
|
||||
findWorkspaceShowingFile(
|
||||
repos,
|
||||
['/tmp/sketch/sample.sketch', '/private/tmp/sketch/sample.sketch'],
|
||||
null
|
||||
)
|
||||
).toEqual({
|
||||
worktreeId: 'r2::/private/tmp/sketch',
|
||||
relativePath: 'sample.sketch',
|
||||
filePath: '/private/tmp/sketch/sample.sketch'
|
||||
})
|
||||
})
|
||||
|
||||
it('skips remote and archived workspaces, and finds none outside every workspace', () => {
|
||||
const repos = [
|
||||
worktree('r1::/w', '/w', { hostId: 'ssh:box' }),
|
||||
worktree('r1::/w2', '/w', { isArchived: true })
|
||||
]
|
||||
expect(findWorkspaceShowingFile(repos, ['/w/a.sketch'], null)).toBeNull()
|
||||
expect(findWorkspaceShowingFile([], ['/w/a.sketch'], null)).toBeNull()
|
||||
})
|
||||
|
||||
it('prefers the active workspace among equally deep ones', () => {
|
||||
const repos = [worktree('a', '/w'), worktree('b', '/w')]
|
||||
expect(findWorkspaceShowingFile(repos, ['/w/a.sketch'], 'b')?.worktreeId).toBe('b')
|
||||
})
|
||||
|
||||
it('finds a local folder workspace by its tab key, never a remote one', () => {
|
||||
const folder = (id: string, folderPath: string, connectionId: string | null) => ({
|
||||
id,
|
||||
folderPath,
|
||||
connectionId,
|
||||
isArchived: false
|
||||
})
|
||||
const rows = folderWorkspaceRows([folder('f1', '/notes', null), folder('f2', '/notes', 'box')])
|
||||
expect(findWorkspaceShowingFile(rows, ['/notes/a.sketch'], null)).toEqual({
|
||||
worktreeId: 'folder:f1',
|
||||
relativePath: 'a.sketch',
|
||||
filePath: '/notes/a.sketch'
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,65 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { endComposition } from '@/lib/text-input/ime/ime-composition-event.test-support'
|
||||
import { SshPortForwardDialog } from './ssh-port-forward-dialog'
|
||||
|
||||
const addPortForward = vi.fn(() => Promise.resolve())
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
addPortForward.mockClear()
|
||||
Object.defineProperty(window, 'api', {
|
||||
configurable: true,
|
||||
value: { ssh: { addPortForward } }
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
// happy-dom has no implicit submission, so a keydown left unprevented submits the form here.
|
||||
function pressEnterWithImplicitSubmit(field: HTMLElement): boolean {
|
||||
const notPrevented = fireEvent.keyDown(field, { key: 'Enter', keyCode: 13 })
|
||||
if (notPrevented) {
|
||||
fireEvent.submit(field.closest('form')!)
|
||||
}
|
||||
return notPrevented
|
||||
}
|
||||
|
||||
describe('SshPortForwardDialog IME confirmation', () => {
|
||||
it.each([
|
||||
{ field: 'Remote Port', value: '8080', sent: { remotePort: 8080 } },
|
||||
{ field: 'Local Port', value: '18080', sent: { localPort: 18080 } },
|
||||
{ field: 'Remote Host', value: '開発機', sent: { remoteHost: '開発機' } },
|
||||
{ field: 'Label (optional)', value: '開発', sent: { label: '開発' } }
|
||||
])('keeps the confirm redispatch in $field from submitting', async ({ field, value, sent }) => {
|
||||
render(
|
||||
<SshPortForwardDialog
|
||||
state={{ mode: 'add', defaults: { remotePort: 3000, targetId: 'target-1' } }}
|
||||
activeConnectionId="target-1"
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
)
|
||||
const input = screen.getByLabelText(field)
|
||||
|
||||
fireEvent.compositionStart(input)
|
||||
fireEvent.keyDown(input, { key: 'Process', keyCode: 229, isComposing: true })
|
||||
fireEvent.change(input, { target: { value } })
|
||||
endComposition(input, value)
|
||||
fireEvent.keyUp(input, { key: 'Process', keyCode: 229 })
|
||||
|
||||
await act(async () => {
|
||||
expect(pressEnterWithImplicitSubmit(input)).toBe(false)
|
||||
})
|
||||
expect(addPortForward).not.toHaveBeenCalled()
|
||||
|
||||
await act(async () => {
|
||||
expect(pressEnterWithImplicitSubmit(input)).toBe(true)
|
||||
})
|
||||
expect(addPortForward).toHaveBeenCalledOnce()
|
||||
expect(addPortForward).toHaveBeenCalledWith(expect.objectContaining(sent))
|
||||
})
|
||||
})
|
||||
@@ -15,22 +15,6 @@ describe('CommentMarkdown unknown HTML as text', () => {
|
||||
expect(markup).not.toContain('<script>')
|
||||
})
|
||||
|
||||
it('still sanitizes unsafe attributes and URLs on allowed HTML', () => {
|
||||
const markup = renderChat('<a href="javascript:alert(1)" onclick="alert(2)">link</a>')
|
||||
|
||||
expect(markup).toContain('>link</a>')
|
||||
expect(markup).not.toContain('javascript:')
|
||||
expect(markup).not.toContain('onclick')
|
||||
})
|
||||
|
||||
it('keeps disallowed elements inert even when they have executable attributes', () => {
|
||||
const markup = renderChat('<iframe srcdoc="<script>alert(1)</script>"></iframe>')
|
||||
|
||||
expect(markup).toContain('<iframe')
|
||||
expect(markup).not.toContain('<iframe')
|
||||
expect(markup).not.toContain('<script')
|
||||
})
|
||||
|
||||
it('keeps an unknown tag inert inside a mixed HTML block', () => {
|
||||
const markup = renderChat('<div>\n<iframe srcdoc="<script>alert(1)</script>"></iframe>\n</div>')
|
||||
|
||||
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { act, cleanup, fireEvent, render, renderHook, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { ExecutionHostId } from '../../../../../../shared/execution-host'
|
||||
|
||||
const client = vi.hoisted(() => ({ windowsDesktop: true }))
|
||||
vi.mock('@/lib/files/os-open/windows-path-access', () => ({
|
||||
isWindowsDesktopClient: () => client.windowsDesktop
|
||||
}))
|
||||
|
||||
import { useAddRepoWindowsDriveRefusal } from './use-add-repo-windows-drive-refusal'
|
||||
|
||||
const WSL_HOST: ExecutionHostId = 'runtime:wsl%3AUbuntu'
|
||||
const REFUSAL = { kind: 'wsl_host_windows_drive_path', windowsPath: 'C:\\src\\app' } as const
|
||||
|
||||
function setup(selectable = true) {
|
||||
const selectHost = vi.fn(async () => selectable)
|
||||
const addLocalPath = vi.fn(async () => {})
|
||||
const createStep = { setStep: vi.fn(), setCreateParent: vi.fn() }
|
||||
const view = renderHook(
|
||||
({ selectedHostId }: { selectedHostId: ExecutionHostId }) =>
|
||||
useAddRepoWindowsDriveRefusal({
|
||||
isOpen: true,
|
||||
selectedHostId,
|
||||
selectHost,
|
||||
addLocalPath,
|
||||
createStep
|
||||
}),
|
||||
{ initialProps: { selectedHostId: WSL_HOST } }
|
||||
)
|
||||
const clickAddOnThisComputer = () => {
|
||||
const { unmount } = render(<>{view.result.current.notice}</>)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Add on this computer' }))
|
||||
unmount()
|
||||
}
|
||||
return { view, selectHost, addLocalPath, createStep, clickAddOnThisComputer }
|
||||
}
|
||||
|
||||
describe('useAddRepoWindowsDriveRefusal', () => {
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
client.windowsDesktop = true
|
||||
})
|
||||
|
||||
it('drops the add when this computer could not be selected, so a later switch adds nothing', async () => {
|
||||
const { view, addLocalPath, clickAddOnThisComputer } = setup(false)
|
||||
act(() => view.result.current.showRefusal(REFUSAL))
|
||||
clickAddOnThisComputer()
|
||||
await act(async () => {})
|
||||
view.rerender({ selectedHostId: 'local' })
|
||||
expect(addLocalPath).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,64 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act, cleanup, render } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { ProviderRateLimits } from '../../../../shared/rate-limit-types'
|
||||
import { ProviderSegment } from './StatusBarProviderSegment'
|
||||
|
||||
vi.mock('@/i18n/i18n', () => ({
|
||||
i18n: { language: 'en' },
|
||||
translate: (_key: string, fallback: string, values: Record<string, string> = {}) =>
|
||||
Object.entries(values).reduce(
|
||||
(text, [key, value]) => text.replace(`{{${key}}}`, value),
|
||||
fallback
|
||||
)
|
||||
}))
|
||||
vi.mock('@/components/agent/icons/AgentIcon', () => ({
|
||||
AgentIcon: () => null
|
||||
}))
|
||||
|
||||
const START = Date.UTC(2026, 9, 8, 12)
|
||||
const MINUTE = 60_000
|
||||
|
||||
function limits(usedPercent = 90): ProviderRateLimits {
|
||||
return {
|
||||
provider: 'antigravity',
|
||||
session: null,
|
||||
weekly: null,
|
||||
buckets: [
|
||||
{
|
||||
name: 'Gemini Models · 5h Limit Remaining',
|
||||
group: 'Gemini Models',
|
||||
windowName: '5h Limit Remaining',
|
||||
usedPercent,
|
||||
windowMinutes: 300,
|
||||
resetsAt: START + 90 * MINUTE + 30_000,
|
||||
resetDescription: null
|
||||
}
|
||||
],
|
||||
status: 'ok',
|
||||
error: null,
|
||||
updatedAt: START
|
||||
}
|
||||
}
|
||||
|
||||
describe('status-bar pool countdown', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(START)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('starts the clock when an idle segment receives an urgent pool', () => {
|
||||
const { container, rerender } = render(
|
||||
<ProviderSegment p={null} compact={false} display="used" />
|
||||
)
|
||||
rerender(<ProviderSegment p={limits()} compact={false} display="used" />)
|
||||
act(() => vi.advanceTimersByTime(30_001))
|
||||
expect(container.textContent).toContain('1h 29m')
|
||||
})
|
||||
})
|
||||
@@ -1,80 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createTestStore } from '@/store/testing/app-store-test-fixture'
|
||||
import type { AppState } from '../../store/types'
|
||||
import type { Tab } from '../../../../shared/tab-types'
|
||||
import { buildRecentTabSwitcherModel } from './recent-tab-switching'
|
||||
|
||||
const WT = 'wt-1'
|
||||
const GROUP = 'group-1'
|
||||
|
||||
function tab(id: string, entityId: string, label: string): Tab {
|
||||
return {
|
||||
id,
|
||||
entityId,
|
||||
groupId: GROUP,
|
||||
worktreeId: WT,
|
||||
contentType: 'editor',
|
||||
label,
|
||||
customLabel: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 1
|
||||
}
|
||||
}
|
||||
|
||||
function stateWithTabs(
|
||||
tabOrder: string[],
|
||||
recentTabIds: string[],
|
||||
activeTabId: string,
|
||||
hydratingTabIds: string[] = []
|
||||
): AppState {
|
||||
// Include only actual group tabs; extra tabs absent from tabOrder would be appended by strip reconciliation.
|
||||
const tabs = [
|
||||
tab('tab-a', 'file-a', 'A'),
|
||||
tab('tab-b', 'file-b', 'B'),
|
||||
tab('tab-c', 'file-c', 'C')
|
||||
].filter((entry) => tabOrder.includes(entry.id) || hydratingTabIds.includes(entry.id))
|
||||
return {
|
||||
...createTestStore().getState(),
|
||||
activeBrowserTabId: null,
|
||||
activeFileId: tabs.find((entry) => entry.id === activeTabId)?.entityId ?? null,
|
||||
activeGroupIdByWorktree: { [WT]: GROUP },
|
||||
activeTabId: null,
|
||||
activeTabType: 'editor',
|
||||
browserTabsByWorktree: {},
|
||||
groupsByWorktree: {
|
||||
[WT]: [{ id: GROUP, worktreeId: WT, activeTabId, tabOrder, recentTabIds }]
|
||||
},
|
||||
openFiles: tabs.map((entry) => ({
|
||||
id: entry.entityId,
|
||||
worktreeId: WT
|
||||
})) as AppState['openFiles'],
|
||||
tabBarOrderByWorktree: {},
|
||||
tabsByWorktree: {},
|
||||
unifiedTabsByWorktree: { [WT]: tabs }
|
||||
}
|
||||
}
|
||||
|
||||
describe('buildRecentTabSwitcherModel', () => {
|
||||
it('orders tabs by MRU with the active tab first', () => {
|
||||
const model = buildRecentTabSwitcherModel(
|
||||
stateWithTabs(['tab-a', 'tab-b', 'tab-c'], ['tab-a', 'tab-c', 'tab-b'], 'tab-b'),
|
||||
WT,
|
||||
'mru'
|
||||
)
|
||||
|
||||
expect(model?.items.map((item) => item.label)).toEqual(['B', 'C', 'A'])
|
||||
expect(model?.activeIndex).toBe(0)
|
||||
})
|
||||
|
||||
it('includes a unified tab that has not yet entered persisted group order', () => {
|
||||
const model = buildRecentTabSwitcherModel(
|
||||
stateWithTabs(['tab-a'], [], 'tab-a', ['tab-b']),
|
||||
WT,
|
||||
'sequential'
|
||||
)
|
||||
|
||||
expect(model?.items.map((item) => item.label)).toEqual(['A', 'B'])
|
||||
expect(model?.activeIndex).toBe(0)
|
||||
})
|
||||
})
|
||||
@@ -1,67 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { withLayoutOwnerTabProps } from './tab-bar-layout-owner-props'
|
||||
|
||||
const mocks = vi.hoisted(
|
||||
(): {
|
||||
callRuntimeRpc: ReturnType<typeof vi.fn>
|
||||
ownerEnvironmentId: string | null
|
||||
recordFeatureInteraction: ReturnType<typeof vi.fn>
|
||||
} => ({ callRuntimeRpc: vi.fn(), ownerEnvironmentId: null, recordFeatureInteraction: vi.fn() })
|
||||
)
|
||||
|
||||
vi.mock('@/runtime/rpc/calls/runtime-rpc-call', () => ({
|
||||
callRuntimeRpc: mocks.callRuntimeRpc
|
||||
}))
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: {
|
||||
getState: () => ({
|
||||
unifiedTabsByWorktree: {},
|
||||
recordFeatureInteraction: mocks.recordFeatureInteraction
|
||||
})
|
||||
}
|
||||
}))
|
||||
vi.mock('@/lib/workspace/ownership/resolve-owner', () => ({
|
||||
runtimeEnvironmentIdUnlessTwin: () => mocks.ownerEnvironmentId
|
||||
}))
|
||||
|
||||
const pinnedItem = { unifiedTabId: 'tab-editor', isPinned: true }
|
||||
|
||||
function windowActions() {
|
||||
return {
|
||||
togglePinned: vi.fn<(item: typeof pinnedItem) => void>(),
|
||||
makePreviewFilePermanent: vi.fn<(fileId: string, tabId?: string) => void>()
|
||||
}
|
||||
}
|
||||
|
||||
function wrap(today: ReturnType<typeof windowActions>) {
|
||||
return withLayoutOwnerTabProps(today, () => 'wt-1')
|
||||
}
|
||||
|
||||
describe('withLayoutOwnerTabProps', () => {
|
||||
beforeEach(() => {
|
||||
mocks.callRuntimeRpc.mockReset()
|
||||
mocks.ownerEnvironmentId = null
|
||||
mocks.recordFeatureInteraction.mockReset()
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
it('leaves the window as it is when main refuses', async () => {
|
||||
mocks.callRuntimeRpc.mockResolvedValue({ ok: false, code: 'invalid_params' })
|
||||
const today = windowActions()
|
||||
|
||||
wrap(today).togglePinned(pinnedItem)
|
||||
|
||||
await vi.waitFor(() => expect(console.warn).toHaveBeenCalled())
|
||||
expect(today.togglePinned).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('acts as today at once for a paired server’s workspace', () => {
|
||||
mocks.ownerEnvironmentId = 'env-1'
|
||||
const today = windowActions()
|
||||
|
||||
wrap(today).togglePinned(pinnedItem)
|
||||
|
||||
expect(mocks.callRuntimeRpc).not.toHaveBeenCalled()
|
||||
expect(today.togglePinned).toHaveBeenCalledWith(pinnedItem)
|
||||
})
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user