mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 09:27:29 +00:00
Remove 129 low-value test cases and 45 whole suites (#38233)
* Remove low-value renderer and main tests 129 registrations removed; 45 whole suites deleted. * Clean deleted-suite CI reference and retained type import --------- Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
@@ -629,9 +629,7 @@ jobs:
|
||||
export GIT_EXEC_PATH="$HOME/.cache/orca-git-compat/git-$ORCA_GIT_COMPAT_VERSION"
|
||||
fi
|
||||
pnpm exec vitest run --config config/vitest.config.ts \
|
||||
src/shared/git/compatibility/git-binary-compatibility.test.ts \
|
||||
src/main/git/worktrees/creation/worktree-safety-real-git.test.ts \
|
||||
src/main/git/worktrees/removal/worktree-rebase-update-refs-real-git.test.ts
|
||||
src/shared/git/compatibility/git-binary-compatibility.test.ts
|
||||
|
||||
# Run pinned real Codex: thread/read must repair an unindexed rollout’s threads row; stub-only units cannot detect upstream removal of this behavior.
|
||||
codex_index_heal_contract:
|
||||
|
||||
@@ -548,7 +548,6 @@
|
||||
1 test-no-wall-clock:src/main/skills/skill-placement-reconciliation.test.ts
|
||||
1 test-no-wall-clock:src/main/skills/skill-placement-transaction.test.ts
|
||||
1 test-no-wall-clock:src/main/ssh/legacy-relay/legacy-relay-bridge-runtime-exit.test.ts
|
||||
1 test-no-wall-clock:src/main/ssh/ssh-agent-hook-interrupt-reconciliation.test.ts
|
||||
1 test-no-wall-clock:src/main/star-nag/service-force-show-races.test.ts
|
||||
1 test-no-wall-clock:src/main/startup/login-shell-environment.test.ts
|
||||
1 test-no-wall-clock:src/main/window/desktop-integration/macos-press-and-hold-default.defaults-domain.test.ts
|
||||
@@ -1009,7 +1008,6 @@
|
||||
2 test-no-wall-clock:src/main/git/command-runner/git-command-resolution-windows-checkout.test.ts
|
||||
2 test-no-wall-clock:src/main/git/refs/maintenance/repo-ref-maintenance-real-git.test.ts
|
||||
2 test-no-wall-clock:src/main/git/runner-gh-account-binding.test.ts
|
||||
2 test-no-wall-clock:src/main/git/source-control/settled-diff-cache-bounds.test.ts
|
||||
2 test-no-wall-clock:src/main/git/worktrees/catalog/worktree-created-description-real-git.test.ts
|
||||
2 test-no-wall-clock:src/main/github/client-pr-fallback-number.test.ts
|
||||
2 test-no-wall-clock:src/main/github/pr-refresh-coordinator-error-cadence.test.ts
|
||||
@@ -1245,7 +1243,6 @@
|
||||
3 test-timeout-needs-reason:src/main/daemon/daemon-shutdown-final-checkpoint-caller-deadline.test.ts
|
||||
3 test-timeout-needs-reason:src/main/daemon/node-pty-windows-input-error.win32.test.ts
|
||||
3 test-timeout-needs-reason:src/main/native-chat/agent-model-catalog/agent-model-catalog-probe-runner.test.ts
|
||||
1 test-timeout-needs-reason:src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts
|
||||
3 test-timeout-needs-reason:src/main/runtime/orchestration-cli-subprocess.test.ts
|
||||
3 test-timeout-needs-reason:src/main/runtime/terminal-interactive-wait-visibility.test.ts
|
||||
3 test-timeout-needs-reason:src/main/runtime/tui-idle-name-only-real-pty.integration.test.ts
|
||||
|
||||
+111
-259
File diff suppressed because one or more lines are too long
@@ -1089,7 +1089,6 @@
|
||||
"src/main/ai-vault/scanners/opencode/reader-transport/session-scanner-opencode-sqlite-process.test.ts": 3815,
|
||||
"src/main/ai-vault/scanners/opencode/sqlite/session-scanner-opencode-sqlite-scan-scope.test.ts": 54,
|
||||
"src/main/ai-vault/scanners/opencode/reader-transport/session-scanner-opencode-sqlite-worker-client.test.ts": 181,
|
||||
"src/main/ai-vault/scanners/opencode/reader-transport/session-scanner-opencode-sqlite-worker-spawn.test.ts": 180,
|
||||
"src/main/ai-vault/scanners/opencode/sqlite/session-scanner-opencode-sqlite.test.ts": 410,
|
||||
"src/main/ai-vault/scanners/opencode/reader-transport/session-scanner-opencode-wsl-routing.test.ts": 200,
|
||||
"src/main/ai-vault/scanners/opencode2/session-scanner-opencode2-message.test.ts": 138,
|
||||
@@ -1200,7 +1199,6 @@
|
||||
"src/main/automations/runtime-terminal-run-observer-evidence.test.ts": 230,
|
||||
"src/main/automations/runtime-terminal-run-observer.test.ts": 4026,
|
||||
"src/main/automations/schedule-drift-report.test.ts": 83,
|
||||
"src/main/automations/service-lifecycle.test.ts": 1952,
|
||||
"src/main/automations/service-precheck.test.ts": 2057,
|
||||
"src/main/automations/service.test.ts": 2970,
|
||||
"src/main/azure-devops/client.test.ts": 411,
|
||||
@@ -1235,7 +1233,6 @@
|
||||
"src/main/browser/certificate-trust/browser-certificate-trust-controller.test.ts": 496,
|
||||
"src/main/browser/client-hosts/file-transfers/browser-client-download-relay.test.ts": 124,
|
||||
"src/main/browser/client-hosts/file-transfers/browser-client-download-routing.test.ts": 124,
|
||||
"src/main/browser/client-hosts/file-transfers/browser-client-file-channel-negotiation.test.ts": 116,
|
||||
"src/main/browser/client-hosts/file-transfers/browser-client-file-channel-reconnect.test.ts": 556,
|
||||
"src/main/browser/client-hosts/lifecycle/browser-client-host-authority-replacement.test.ts": 47,
|
||||
"src/main/browser/client-hosts/commands/browser-client-host-command-dispatcher.test.ts": 156,
|
||||
@@ -1260,7 +1257,6 @@
|
||||
"src/main/browser/cookies/client-route/browser-client-route-cookie-import-partition-parity.test.ts": 374,
|
||||
"src/main/browser/cookies/client-route/browser-client-route-cookie-import.test.ts": 101,
|
||||
"src/main/browser/client-hosts/file-transfers/browser-client-upload-buffer-retention.test.ts": 736,
|
||||
"src/main/browser/client-hosts/file-transfers/browser-client-upload-command.test.ts": 115,
|
||||
"src/main/browser/client-hosts/file-transfers/browser-client-upload-staging.test.ts": 140,
|
||||
"src/main/browser/client-hosts/file-transfers/browser-client-upload-transfer.test.ts": 93,
|
||||
"src/main/browser/cdp/input/browser-element-functions.test.ts": 228,
|
||||
@@ -1471,7 +1467,6 @@
|
||||
"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,
|
||||
"src/main/browser/client-hosts/lifecycle/paired-runtime-browser-client-host-identity-wiring.test.ts": 1368,
|
||||
"src/main/browser/client-hosts/lifecycle/paired-runtime-browser-client-host-reconnect.test.ts": 568,
|
||||
"src/main/browser/client-hosts/lifecycle/paired-runtime-browser-client-host-registry.test.ts": 53,
|
||||
"src/main/browser/client-hosts/lifecycle/paired-runtime-browser-client-host-route-identity.test.ts": 1404,
|
||||
@@ -1490,7 +1485,6 @@
|
||||
"src/main/browser/network/tunnels/system-ssh-socks-client-socket.test.ts": 53,
|
||||
"src/main/browser/network/execution-routes/wsl-browser-network-execution-route.test.ts": 678,
|
||||
"src/main/browser/network/execution-routes/wsl-browser-network-relay-launch.test.ts": 146,
|
||||
"src/main/browser/routing/local-host-partitions/wsl-host-browser-jar-identity.test.ts": 806,
|
||||
"src/main/browser/routing/local-host-partitions/wsl-host-browser-route-partition.test.ts": 753,
|
||||
"src/main/claude-accounts/claude-account-folder.test.ts": 153,
|
||||
"src/main/claude-accounts/claude-account-service.test.ts": 1207,
|
||||
@@ -1755,7 +1749,6 @@
|
||||
"src/main/codex-accounts/runtime-home-windows-profile-ownership.test.ts": 359,
|
||||
"src/main/codex-accounts/runtime-home-wsl-managed-accounts.test.ts": 1199,
|
||||
"src/main/codex-accounts/runtime-home-wsl-session-bridge.test.ts": 1018,
|
||||
"src/main/codex-accounts/runtime-home-wsl-sibling-bridge.test.ts": 545,
|
||||
"src/main/codex-accounts/runtime-home-wsl-system-default.test.ts": 758,
|
||||
"src/main/codex-accounts/service-abandoned-login-supersede.test.ts": 780,
|
||||
"src/main/codex-accounts/service-account-add-login.test.ts": 757,
|
||||
@@ -2175,7 +2168,6 @@
|
||||
"src/main/daemon/daemon-session-owner-resolution.test.ts": 114,
|
||||
"src/main/daemon/daemon-session-scrollback-window.test.ts": 554,
|
||||
"src/main/daemon/daemon-severed-restart.test.ts": 629,
|
||||
"src/main/daemon/daemon-shared-owner-incarnation-retention.test.ts": 1025,
|
||||
"src/main/daemon/daemon-shutdown-final-checkpoint-caller-deadline.test.ts": 2543,
|
||||
"src/main/daemon/daemon-socket-endpoint-path.test.ts": 529,
|
||||
"src/main/daemon/daemon-spawner.test.ts": 708,
|
||||
@@ -2408,7 +2400,6 @@
|
||||
"src/main/git/command-runner/git-admission-output-parity.test.ts": 271,
|
||||
"src/main/git/command-runner/git-admission-storm-measurement.test.ts": 264,
|
||||
"src/main/git/command-runner/git-command-resolution-windows-checkout.test.ts": 133,
|
||||
"src/main/git/command-runner/git-command-timeout-behavior.test.ts": 2006,
|
||||
"src/main/git/command-runner/git-exec-admission-lifetime.test.ts": 718,
|
||||
"src/main/git/command-runner/git-operation-executor.test.ts": 59,
|
||||
"src/main/git/command-runner/git-spawn-wsl-admission.test.ts": 153,
|
||||
@@ -2479,7 +2470,6 @@
|
||||
"src/main/git/runner-wsl-network-ssh-policy.test.ts": 260,
|
||||
"src/main/git/runner.test.ts": 284,
|
||||
"src/main/git/saved-clone-target.test.ts": 890,
|
||||
"src/main/git/source-control/settled-diff-cache-bounds.test.ts": 71,
|
||||
"src/main/git/source-control/blob-absence-real-git.test.ts": 629,
|
||||
"src/main/git/source-control/complete-merge-real-git.test.ts": 958,
|
||||
"src/main/git/source-control/diff-textconv-real-git.test.ts": 520,
|
||||
@@ -2525,7 +2515,6 @@
|
||||
"src/main/git/worktrees/creation/worktree-base-refresh-lock-real-git.test.ts": 1588,
|
||||
"src/main/git/worktrees/creation/worktree-checkout-progress-real-git.test.ts": 769,
|
||||
"src/main/git/worktrees/creation/worktree-configured-paths-concurrency.test.ts": 80,
|
||||
"src/main/git/worktrees/creation/worktree-create-git-executor-real-git.test.ts": 528,
|
||||
"src/main/git/worktrees/creation/worktree-create-preparation-real-git.test.ts": 1858,
|
||||
"src/main/git/worktrees/creation/worktree-create-preparation-real-wsl.test.ts": 382,
|
||||
"src/main/git/worktrees/catalog/worktree-created-description-real-git.test.ts": 1119,
|
||||
@@ -2554,10 +2543,8 @@
|
||||
"src/main/git/worktrees/creation/worktree-preparation-tip-refresh-real-git.test.ts": 844,
|
||||
"src/main/git/worktrees/creation/worktree-preparation-tip-refresh.test.ts": 113,
|
||||
"src/main/git/worktrees/removal/worktree-prune-guard.test.ts": 159,
|
||||
"src/main/git/worktrees/removal/worktree-rebase-update-refs-real-git.test.ts": 284,
|
||||
"src/main/git/worktrees/removal/worktree-removal-real-git.test.ts": 3365,
|
||||
"src/main/git/worktrees/removal/worktree-remove-branch-deletion.test.ts": 353,
|
||||
"src/main/git/worktrees/creation/worktree-safety-real-git.test.ts": 549,
|
||||
"src/main/git/worktrees/catalog/worktree-scan-cache-annotation-reuse.test.ts": 361,
|
||||
"src/main/git/worktrees/catalog/worktree-separate-git-dir.test.ts": 689,
|
||||
"src/main/git/worktrees/content-paths/worktree-shared-directories.test.ts": 1045,
|
||||
@@ -2770,7 +2757,6 @@
|
||||
"src/main/ipc/created-worktree-root-prune.test.ts": 494,
|
||||
"src/main/ipc/dashboard-payload-validation.test.ts": 188,
|
||||
"src/main/ipc/dashboard-popout.test.ts": 124,
|
||||
"src/main/ipc/browser/desktop-browser-bridge-wiring.test.ts": 1211,
|
||||
"src/main/ipc/developer-permissions.test.ts": 147,
|
||||
"src/main/ipc/diagnostics.test.ts": 124,
|
||||
"src/main/ipc/doc-preview-grant-ipc.test.ts": 58,
|
||||
@@ -2869,7 +2855,6 @@
|
||||
"src/main/ipc/folder-repo-git-upgrade-lifetime.test.ts": 123,
|
||||
"src/main/ipc/folder-repo-git-upgrade.test.ts": 2302,
|
||||
"src/main/ipc/worktrees/git/git-status-upstream-ref-watch-request.test.ts": 405,
|
||||
"src/main/ipc/source-control/github-pr-refresh-document-lifetime.test.ts": 120,
|
||||
"src/main/ipc/source-control/github-pr-refresh-routing.test.ts": 715,
|
||||
"src/main/ipc/source-control/github-repo-access-guards.test.ts": 805,
|
||||
"src/main/ipc/source-control/github-repo-routing-twin-host.test.ts": 124,
|
||||
@@ -2958,12 +2943,10 @@
|
||||
"src/main/ipc/pty/host-env/spawn-cwd-hung-folder.test.ts": 650,
|
||||
"src/main/ipc/pty/host-env/spawn-cwd-wsl-move.test.ts": 817,
|
||||
"src/main/ipc/pty/ipc/claude-old-terminal.test.ts": 47,
|
||||
"src/main/ipc/pty/ipc/closed-pane-spawn-refusal.test.ts": 242,
|
||||
"src/main/ipc/pty/ipc/spawn-commit-run-facts.test.ts": 1661,
|
||||
"src/main/ipc/pty/ipc/spawn-options-codex-isolation.test.ts": 1115,
|
||||
"src/main/ipc/pty/ipc/spawn-push-target-materialization-real-git.test.ts": 1316,
|
||||
"src/main/ipc/pty/ipc/spawn-reattach-size-cache.test.ts": 1459,
|
||||
"src/main/ipc/pty/ipc/terminal-view-attributes-ipc.test.ts": 516,
|
||||
"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/opencode-legacy-spawn-deletions.test.ts": 1214,
|
||||
@@ -3010,7 +2993,6 @@
|
||||
"src/main/ipc/pty/pty-serializer-settlement-mapping.test.ts": 4245,
|
||||
"src/main/ipc/pty/pty-session-liveness-and-ownership.test.ts": 3423,
|
||||
"src/main/ipc/pty/pty-spawn-codex-home-unavailable.test.ts": 4208,
|
||||
"src/main/ipc/pty/pty-spawn-cwd-fallback-hung-folder.test.ts": 3949,
|
||||
"src/main/ipc/pty/pty-spawn-cwd-fallback.test.ts": 4680,
|
||||
"src/main/ipc/pty/pty-spawn-env-agent-overlays.test.ts": 3981,
|
||||
"src/main/ipc/pty/pty-spawn-env-codex-home-routing.test.ts": 6383,
|
||||
@@ -3317,7 +3299,6 @@
|
||||
"src/main/workspaces/removal/local-worktree-filesystem-wsl-banner.wsl.test.ts": 84,
|
||||
"src/main/workspaces/removal/local-worktree-filesystem.test.ts": 4284,
|
||||
"src/main/workspaces/catalog/local-worktree-metadata-prune-gate.test.ts": 41,
|
||||
"src/main/workspaces/catalog/local-worktree-path-presence.test.ts": 52,
|
||||
"src/main/workspaces/removal/local-worktree-removal-recovery-live.test.ts": 465,
|
||||
"src/main/workspaces/removal/local-worktree-removal-recovery.test.ts": 351,
|
||||
"src/main/ports/workspace-labels/localhost-worktree-label-proxy.test.ts": 120,
|
||||
@@ -3421,9 +3402,7 @@
|
||||
"src/main/memory/collector.test.ts": 925,
|
||||
"src/main/memory/host-malloc-tunables.test.ts": 41,
|
||||
"src/main/memory/host-memory.test.ts": 80,
|
||||
"src/main/memory/hydrate-local-pty-registry-folder-workspaces.test.ts": 535,
|
||||
"src/main/memory/hydrate-local-pty-registry.test.ts": 1970,
|
||||
"src/main/memory/memory-snapshot-buckets.test.ts": 113,
|
||||
"src/main/memory/verified-local-folder-workspaces.test.ts": 74,
|
||||
"src/main/menu/register-app-menu.test.ts": 280,
|
||||
"src/main/mimo/hook-service.test.ts": 115,
|
||||
@@ -3526,7 +3505,6 @@
|
||||
"src/main/native-chat/agent-session-wire/agent-session-history-page.test.ts": 1049,
|
||||
"src/main/native-chat/agent-session-wire/agent-session-item-wire-form.test.ts": 2246,
|
||||
"src/main/native-chat/agent-session-wire/agent-session-stop-note-paging.test.ts": 1337,
|
||||
"src/main/native-chat/agent-session-wire/agent-session-stop-note-refresh.test.ts": 213,
|
||||
"src/main/native-chat/agent-session-wire/at-rest-command-demand.test.ts": 43,
|
||||
"src/main/native-chat/agent-session-wire/hook-failure-provider-frame.test.ts": 234,
|
||||
"src/main/native-chat/agent-session-wire/hook-outcome-rows.test.ts": 2040,
|
||||
@@ -3565,7 +3543,6 @@
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-codex-applied-inputs.test.ts": 2594,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-codex-held-goal.test.ts": 2081,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-codex-input-host.test.ts": 19298,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-codex-version-recovery.test.ts": 2227,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-command-publication.test.ts": 1350,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-command-readers.test.ts": 567,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-conversations.test.ts": 2096,
|
||||
@@ -3615,7 +3592,6 @@
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-omp-menu-persistence.test.ts": 1063,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-operation-settlement.test.ts": 1852,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-operation-start-refusal.test.ts": 1210,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-options-deadline.test.ts": 1036,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-options-read-scaling.test.ts": 1006,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-options-read.accepted-inputs.test.ts": 1063,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-options-read.test.ts": 1405,
|
||||
@@ -3650,7 +3626,6 @@
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-received-output.test.ts": 2740,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-receiving-cancellation.test.ts": 5834,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-receiving-catalog.test.ts": 5757,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-recovered-queue-idle.test.ts": 1857,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-recovered-turn-clock.test.ts": 1539,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-recovery-exits.test.ts": 2112,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-recovery-read.test.ts": 918,
|
||||
@@ -3665,7 +3640,6 @@
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-restart-cap.test.ts": 731,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-restart-child-work.test.ts": 1898,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-restart-context-clear.test.ts": 538,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-restart-continuation-wait.test.ts": 191,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-restart-continuation.test.ts": 533,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-restart-failure-filing.test.ts": 2350,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-restart-offer-after-reattach.test.ts": 1903,
|
||||
@@ -3739,7 +3713,6 @@
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-unexpected-exit.test.ts": 430,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-unnamed-stop-races.test.ts": 2959,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-unopened-send-withdrawal.test.ts": 156,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-unopened-tab.test.ts": 1885,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-unrun-send-hold.test.ts": 2343,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-unsent-send-hold.test.ts": 6694,
|
||||
"src/main/native-chat/agent-session-wire/structured-agent-session-view-start-after-failed-start.test.ts": 5757,
|
||||
@@ -3877,7 +3850,6 @@
|
||||
"src/main/opencode/opencode-acp-connection.test.ts": 1205,
|
||||
"src/main/opencode/opencode-cli-version.test.ts": 54,
|
||||
"src/main/opencode/opencode-client-sweep.test.ts": 124,
|
||||
"src/main/opencode/opencode-credential-backend.test.ts": 84,
|
||||
"src/main/opencode/opencode-go-api-key-store.test.ts": 128,
|
||||
"src/main/opencode/opencode-launch-model-config.test.ts": 87,
|
||||
"src/main/opencode/opencode-launch-model-context-cleanup.test.ts": 1204,
|
||||
@@ -4077,7 +4049,6 @@
|
||||
"src/main/persistence/loading-store/secret-sentinel-substitution.test.ts": 90,
|
||||
"src/main/persistence/loading-store/state-write-round-trip.test.ts": 1983,
|
||||
"src/main/persistence/loading-store/store-host-partition-before-unload.test.ts": 1670,
|
||||
"src/main/persistence/loading-store/store-prune-gate-signals.test.ts": 1787,
|
||||
"src/main/persistence/loading-store/store-runtime-authored-session-writes.test.ts": 1757,
|
||||
"src/main/persistence/loading-store/store-workspace-session-host-removal.test.ts": 1738,
|
||||
"src/main/persistence/loading-store/terminal-close-async-durability.test.ts": 5719,
|
||||
@@ -4568,7 +4539,6 @@
|
||||
"src/main/providers/pty-default-cwd.test.ts": 54,
|
||||
"src/main/providers/pty-process-inspection.test.ts": 45,
|
||||
"src/main/providers/pty-process-list-admission.test.ts": 138,
|
||||
"src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts": 2018,
|
||||
"src/main/providers/stable-foreground-process.test.ts": 77,
|
||||
"src/main/providers/startup-line-typed-length.live-shell.test.ts": 316,
|
||||
"src/main/providers/terminal-path-existence-batch.integration.test.ts": 337,
|
||||
@@ -4707,7 +4677,6 @@
|
||||
"src/main/runtime/agent-session-record-store-damaged.test.ts": 1557,
|
||||
"src/main/runtime/agent-session-saved-roster.test.ts": 3035,
|
||||
"src/main/runtime/agent-session-store-load-scale.test.ts": 732,
|
||||
"src/main/runtime/agent-session-tab-layout-command.test.ts": 279,
|
||||
"src/main/runtime/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,
|
||||
@@ -4860,7 +4829,6 @@
|
||||
"src/main/runtime/device-registry-own-quarantine.test.ts": 123,
|
||||
"src/main/runtime/device-registry-startup-offer.test.ts": 102,
|
||||
"src/main/runtime/dsh-launch-prompt-resubmit.test.ts": 6527,
|
||||
"src/main/runtime/dsh-readiness-transcript.test.ts": 71,
|
||||
"src/main/runtime/e2ee-keypair.test.ts": 77,
|
||||
"src/main/runtime/editor-view-layout-apply.test.ts": 6870,
|
||||
"src/main/runtime/editor-view-partition-changes.test.ts": 5531,
|
||||
@@ -5236,7 +5204,6 @@
|
||||
"src/main/runtime/renderer-browser-session-reconciliation.test.ts": 3882,
|
||||
"src/main/runtime/renderer-owned-tab-properties.test.ts": 15027,
|
||||
"src/main/runtime/repo-icon-fork-backfill.test.ts": 4159,
|
||||
"src/main/runtime/repo-worktree-admin-fingerprint.test.ts": 668,
|
||||
"src/main/runtime/repo-worktree-row-resolution.test.ts": 664,
|
||||
"src/main/runtime/restored-editor-move-restart.test.ts": 5390,
|
||||
"src/main/runtime/restored-pane-hook-store-resume.test.ts": 4423,
|
||||
@@ -5408,7 +5375,6 @@
|
||||
"src/main/runtime/rpc/methods/orchestration/worker/worker-run-dispatch-contracts.test.ts": 5136,
|
||||
"src/main/runtime/rpc/methods/orchestration/worker/worker-start-budgets.test.ts": 45,
|
||||
"src/main/runtime/rpc/methods/orchestration/worker/worker-start-launch-executor-parity.test.ts": 1137,
|
||||
"src/main/runtime/rpc/methods/orchestration/worker/worker-start-prompt-budget.test.ts": 100,
|
||||
"src/main/runtime/rpc/methods/orchestration/worker/worker-start-prompt-contract.test.ts": 6279,
|
||||
"src/main/runtime/rpc/methods/orchestration/worker/worker-stop-exit-race.test.ts": 4508,
|
||||
"src/main/runtime/rpc/methods/orchestration/worker/worker-stop-liveness-verdict.test.ts": 5279,
|
||||
@@ -5667,7 +5633,6 @@
|
||||
"src/main/runtime/runtime-provisioned-root-adoption.test.ts": 537,
|
||||
"src/main/runtime/runtime-reference-agents.test.ts": 207,
|
||||
"src/main/runtime/runtime-reference-find.test.ts": 245,
|
||||
"src/main/runtime/runtime-reference-integration.test.ts": 4136,
|
||||
"src/main/runtime/runtime-remote-fetch-ref-maintenance.test.ts": 419,
|
||||
"src/main/runtime/runtime-repo-update-worktree-refresh.test.ts": 4375,
|
||||
"src/main/runtime/runtime-repository-clone-host-dedupe.test.ts": 874,
|
||||
@@ -5890,13 +5855,11 @@
|
||||
"src/main/runtime/terminal/identity/main-pane-record-arrival.test.ts": 5024,
|
||||
"src/main/runtime/terminal/identity/terminal-identity-placement-schedules.test.ts": 4255,
|
||||
"src/main/runtime/terminal/identity/terminal-identity-schedules.test.ts": 4334,
|
||||
"src/main/runtime/terminal/identity/terminal-identity-source.test.ts": 3963,
|
||||
"src/main/runtime/terminal/input/agent-prompt-permission-hold.test.ts": 48,
|
||||
"src/main/runtime/terminal/input/terminal-send-plugin-hook.test.ts": 169,
|
||||
"src/main/runtime/terminal/launch/mobile-terminal-chat-fork.test.ts": 3900,
|
||||
"src/main/runtime/terminal/launch/terminal-split-layout-owner.test.ts": 4503,
|
||||
"src/main/runtime/terminal/launch/terminal-split-retry.test.ts": 4038,
|
||||
"src/main/runtime/terminal/launch/window-tab-start-layout-owner.test.ts": 4124,
|
||||
"src/main/runtime/terminal/launch/window-tab-start-switch-off.test.ts": 4475,
|
||||
"src/main/runtime/terminal/layout/agent-generated-tab-titles.test.ts": 5572,
|
||||
"src/main/runtime/terminal/lifecycle/main-pane-liveness.test.ts": 4871,
|
||||
@@ -5956,7 +5919,6 @@
|
||||
"src/main/runtime/worktree-teardown-daemon-inventory.test.ts": 463,
|
||||
"src/main/runtime/worktree-teardown-unstopped-pty.test.ts": 419,
|
||||
"src/main/runtime/worktree-teardown.test.ts": 862,
|
||||
"src/main/runtime/worktree-terminal-mutation-lock.test.ts": 55,
|
||||
"src/main/runtime/worktree-terminal-sleep-host-twin.test.ts": 4345,
|
||||
"src/main/runtime/wsl-distro-loss-exit-retention.test.ts": 5519,
|
||||
"src/main/runtime/wsl-guest-drive-mountinfo.test.ts": 38,
|
||||
@@ -6011,7 +5973,6 @@
|
||||
"src/main/skills/skill-delete/wsl-enumeration-protocol.test.ts": 52,
|
||||
"src/main/skills/skill-discovery-concurrency.test.ts": 222,
|
||||
"src/main/skills/skill-discovery-target.test.ts": 124,
|
||||
"src/main/skills/skill-discovery-wsl-alias-sharing.test.ts": 165,
|
||||
"src/main/skills/skill-discovery-wsl-bash-filter.test.ts": 2105,
|
||||
"src/main/skills/skill-discovery-wsl-script-roundtrip.test.ts": 136,
|
||||
"src/main/skills/skill-discovery-wsl.test.ts": 104,
|
||||
@@ -6185,7 +6146,6 @@
|
||||
"src/main/ssh/orcad-migration-delta-move.test.ts": 3812,
|
||||
"src/main/ssh/orcad-migration-delta-snapshot.test.ts": 2611,
|
||||
"src/main/ssh/orcad-migration-manifest-export.test.ts": 613,
|
||||
"src/main/ssh/orcad-migration-path-rewrite.test.ts": 175,
|
||||
"src/main/ssh/orcad-migration-repository-link.test.ts": 4281,
|
||||
"src/main/ssh/orcad-migration-snapshot-coordinator.test.ts": 309,
|
||||
"src/main/ssh/orcad-migration-snapshot-resume.test.ts": 1878,
|
||||
@@ -6221,7 +6181,6 @@
|
||||
"src/main/ssh/sftp-stream-late-error.test.ts": 209,
|
||||
"src/main/ssh/sftp-upload-pipeline.test.ts": 480,
|
||||
"src/main/ssh/sftp-upload.test.ts": 533,
|
||||
"src/main/ssh/ssh-agent-hook-interrupt-reconciliation.test.ts": 610,
|
||||
"src/main/ssh/ssh-agent-identity-filter.test.ts": 54,
|
||||
"src/main/ssh/ssh-channel-multiplexer-backpressure.test.ts": 102,
|
||||
"src/main/ssh/ssh-channel-multiplexer-saturation-wedge.test.ts": 60,
|
||||
|
||||
@@ -49,7 +49,6 @@ export const WINDOWS_BOUNDARY_TESTS = [
|
||||
'src/main/browser/client-pages/renderer/browser-client-page-renderer-lifecycle.electron.test.ts',
|
||||
'src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts',
|
||||
'src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts',
|
||||
'src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts',
|
||||
'src/shared/__tests__/process-host/windows-command-line.win32.test.ts',
|
||||
'src/shared/__tests__/process-host/windows-cmd-shim-resolution.test.ts',
|
||||
'src/shared/__tests__/process-host/windows-cmd-shim-resolution.win32.test.ts',
|
||||
@@ -95,7 +94,6 @@ export const WINDOWS_BOUNDARY_TESTS = [
|
||||
'src/main/startup/windows-app-data-path.test.ts',
|
||||
'src/main/startup/windows-std-handle-inheritance.win32.test.ts',
|
||||
'src/main/startup/windows-isolated-spawner.win32.test.ts',
|
||||
'src/main/runtime/repo-worktree-admin-fingerprint.test.ts',
|
||||
'src/main/runtime/worktree-scan-admin-fingerprint-gate.test.ts',
|
||||
'src/shared/secure-file-fsync-flags.test.ts',
|
||||
'src/shared/secure-path-windows-acl.win32.test.ts',
|
||||
|
||||
@@ -5,10 +5,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { AiVaultScanWork } from '../../shared/ai-vault/scanning/ai-vault-scan-progress'
|
||||
import {
|
||||
AI_VAULT_SCAN_PROGRESS_INTERVAL_MS,
|
||||
noteAiVaultScanFileFound,
|
||||
aiVaultScanWork,
|
||||
openAiVaultScanSource,
|
||||
noteAiVaultScanWorkChanged,
|
||||
trackAiVaultScanProgress
|
||||
} from './ai-vault-scan-progress'
|
||||
import { scanAiVaultSessions } from './session-scanner'
|
||||
@@ -30,37 +26,7 @@ async function tempRoot(): Promise<string> {
|
||||
return root
|
||||
}
|
||||
|
||||
function codexFile(index: number): { path: string; dev: number; ino: number } {
|
||||
return { path: `/sessions/${index}.jsonl`, dev: 1, ino: index + 1 }
|
||||
}
|
||||
|
||||
describe('scan progress reporting', () => {
|
||||
it('reports at most once per interval, with the latest tally trailing', () => {
|
||||
vi.useFakeTimers()
|
||||
const signal = new AbortController().signal
|
||||
const files: number[] = []
|
||||
const stop = trackAiVaultScanProgress(
|
||||
signal,
|
||||
(work) => files.push(work.agents[0]?.files ?? 0),
|
||||
() => Date.now()
|
||||
)
|
||||
|
||||
noteAiVaultScanFileFound(signal, 'codex', codexFile(0))
|
||||
noteAiVaultScanWorkChanged(signal)
|
||||
for (let index = 1; index < 16; index++) {
|
||||
noteAiVaultScanFileFound(signal, 'codex', codexFile(index))
|
||||
noteAiVaultScanWorkChanged(signal)
|
||||
}
|
||||
expect(files).toEqual([1])
|
||||
vi.advanceTimersByTime(AI_VAULT_SCAN_PROGRESS_INTERVAL_MS)
|
||||
expect(files).toEqual([1, 16])
|
||||
|
||||
stop()
|
||||
noteAiVaultScanWorkChanged(signal)
|
||||
vi.advanceTimersByTime(AI_VAULT_SCAN_PROGRESS_INTERVAL_MS)
|
||||
expect(files).toEqual([1, 16])
|
||||
})
|
||||
|
||||
it('reports discovery and reading for its own scan, never for an unrelated one', async () => {
|
||||
const root = await tempRoot()
|
||||
const roots = isolatedScanRoots(root)
|
||||
@@ -104,19 +70,3 @@ describe('scan progress reporting', () => {
|
||||
expect(reports.at(-1)).toMatchObject({ stage: 'reading', read: { done: 3, total: 3 } })
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps source ownership and final facts independent of immediate and trailing sink faults', () => {
|
||||
vi.useFakeTimers()
|
||||
const signal = new AbortController().signal
|
||||
const sink = vi.fn(() => {
|
||||
throw new Error('Synthetic reporting sink fault')
|
||||
})
|
||||
const stop = trackAiVaultScanProgress(signal, sink, () => Date.now())
|
||||
const close = openAiVaultScanSource(signal, 'claude')
|
||||
expect(aiVaultScanWork(signal)?.agents[0]?.walking).toBe(true)
|
||||
close()
|
||||
expect(() => vi.advanceTimersByTime(AI_VAULT_SCAN_PROGRESS_INTERVAL_MS)).not.toThrow()
|
||||
expect(sink).toHaveBeenCalledTimes(2)
|
||||
expect(aiVaultScanWork(signal)?.agents).toEqual([])
|
||||
stop()
|
||||
})
|
||||
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
import { expect, it, vi } from 'vitest'
|
||||
import type * as OpenCodeWslClient from './session-scanner-opencode-wsl-client'
|
||||
import type { OpenCodeSqliteWorkerClient } from './session-scanner-opencode-sqlite-worker-client'
|
||||
import type { AiVaultScanIssue } from '../../../../../shared/ai-vault/contracts/ai-vault-types'
|
||||
import { listOpenCodeSqliteSessionsViaWorker } from './session-scanner-opencode-sqlite-worker-spawn'
|
||||
|
||||
const mocks = vi.hoisted(() => ({ list: vi.fn<OpenCodeSqliteWorkerClient['list']>() }))
|
||||
vi.mock('./session-scanner-opencode-wsl-client', async (importOriginal) => {
|
||||
const { parseWslUncPath } = await import('../../../../../shared/wsl-paths')
|
||||
return {
|
||||
...(await importOriginal<typeof OpenCodeWslClient>()),
|
||||
openCodeWslPath: parseWslUncPath,
|
||||
openCodeWslClient: async () => ({ list: mocks.list })
|
||||
}
|
||||
})
|
||||
|
||||
it('routes scoped SQLite reads only to the owning distro and reports a scoped-only failure', async () => {
|
||||
const ubuntu = String.raw`\\wsl.localhost\Ubuntu\home\ada\opencode.db`
|
||||
const debian = String.raw`\\wsl.localhost\Debian\home\ada\opencode.db`
|
||||
const issues: AiVaultScanIssue[] = []
|
||||
mocks.list.mockImplementation(async (args) => {
|
||||
if (args.scopePaths?.length) {
|
||||
expect(args.scopePaths).toEqual(['/home/ada/repo'])
|
||||
args.issues.push({
|
||||
agent: 'opencode',
|
||||
kind: 'scope',
|
||||
path: args.dbPaths[0],
|
||||
message: 'Scoped read timed out'
|
||||
})
|
||||
}
|
||||
return []
|
||||
})
|
||||
await listOpenCodeSqliteSessionsViaWorker({ dbPaths: [ubuntu], limit: 1, issues })
|
||||
await listOpenCodeSqliteSessionsViaWorker({
|
||||
dbPaths: [ubuntu, debian],
|
||||
limit: 1,
|
||||
issues,
|
||||
scopePaths: [String.raw`\\wsl.localhost\Ubuntu\home\ada\repo`]
|
||||
})
|
||||
expect(mocks.list).toHaveBeenCalledTimes(2)
|
||||
expect(issues).toEqual([
|
||||
{ agent: 'opencode', kind: 'scope', path: ubuntu, message: 'Scoped read timed out' }
|
||||
])
|
||||
})
|
||||
+1
-27
@@ -1,10 +1,9 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Dirent } from 'node:fs'
|
||||
|
||||
import type * as NodeFsPromisesModule from 'node:fs/promises'
|
||||
import type { AiVaultScanIssue } from '../../../../shared/ai-vault/contracts/ai-vault-types'
|
||||
|
||||
const WSL_HOME = '\\\\wsl.localhost\\Ubuntu\\home\\ada'
|
||||
const WSL_DATA_DIR = `${WSL_HOME}/.local/share/opencode`
|
||||
|
||||
const mocks = vi.hoisted(() => ({ readdir: vi.fn(), stat: vi.fn() }))
|
||||
|
||||
@@ -58,31 +57,6 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
describe('OpenCode source discovery with a stalled WSL data directory', () => {
|
||||
it('reports the stalled home as an issue and still resolves every discovery', async () => {
|
||||
mocks.readdir.mockImplementation((dir: string) =>
|
||||
dir === WSL_DATA_DIR ? stalls() : Promise.resolve([] as Dirent[])
|
||||
)
|
||||
const issues: AiVaultScanIssue[] = []
|
||||
const discoveries = Promise.all(
|
||||
opencodeDiscoveries(
|
||||
{ opencodeStorageDir: '/home/ada/.local/share/opencode/storage' },
|
||||
[WSL_HOME],
|
||||
10,
|
||||
issues
|
||||
)
|
||||
)
|
||||
// The storage scan queues before the data-dir timeout marks the route stuck; both reach their deadlines.
|
||||
await vi.advanceTimersByTimeAsync(WSL_TRANSCRIPT_FS_SCAN_TIMEOUT_MS * 2 + 2)
|
||||
|
||||
// A scan issue distinguishes an unreachable source from an uninstalled agent.
|
||||
const resolved = await discoveries
|
||||
expect(resolved).toHaveLength(4)
|
||||
expect(mocks.readdir.mock.calls.filter(([path]) => path === WSL_DATA_DIR)).toHaveLength(1)
|
||||
expect(resolved.every((discovery) => discovery.files.length === 0)).toBe(true)
|
||||
expect(issues.some((issue) => issue.path === WSL_DATA_DIR)).toBe(true)
|
||||
expect(issues.every((issue) => issue.agent === 'opencode')).toBe(true)
|
||||
})
|
||||
|
||||
it('reports a refused primary data directory instead of scanning clean', async () => {
|
||||
const previousXdg = process.env.XDG_DATA_HOME
|
||||
const previousDb = process.env.OPENCODE_DB
|
||||
|
||||
@@ -10,7 +10,7 @@ import { copyStructuredChatConversation } from './structured-chat-conversation-c
|
||||
import { installCopyingChat as installChat } from './structured-session-ownership.test-support'
|
||||
|
||||
const OMP_ID = '019fd611-2b3c-7d4e-9f50-6b7c8d9e0f1a'
|
||||
const OPENCODE_ID = 'ses_2f9aB3cD4eF5gH6iJ7kL8mN9oP'
|
||||
|
||||
const OMP_FILE = `/sessions/${OMP_ID}.jsonl`
|
||||
|
||||
const acp = (agent: 'opencode' | 'omp', nativeId: string): AgentSessionProviderHandle => ({
|
||||
@@ -34,31 +34,6 @@ const fork = (
|
||||
afterEach(() => setStructuredAgentSessionHost(null))
|
||||
|
||||
describe('copying a chat conversation for Resume in New CLI', () => {
|
||||
it.each([
|
||||
{ agent: 'omp' as const, handle: acp('omp', OMP_ID), sessionId: OMP_ID },
|
||||
{ agent: 'opencode2' as const, handle: acp('opencode', OPENCODE_ID), sessionId: OPENCODE_ID }
|
||||
])(
|
||||
"has the chat's host copy a $agent row's conversation",
|
||||
async ({ agent, handle, sessionId }) => {
|
||||
const copy = vi.fn(async () => ({
|
||||
sessionId: 'copy-1',
|
||||
filePath: '/sessions/copy-1.jsonl'
|
||||
}))
|
||||
installChat(handle, copy)
|
||||
|
||||
await expect(
|
||||
copyStructuredChatConversation(fork({ agent, sessionId, filePath: `/x/${sessionId}` }))
|
||||
).resolves.toEqual({
|
||||
useRealCodexHome: false,
|
||||
forkedSession: {
|
||||
sessionId: 'copy-1',
|
||||
filePath: '/sessions/copy-1.jsonl',
|
||||
agent: handle.agent
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['source', 'owned'])('rejects a %s fork id on the execution host', async (returned) => {
|
||||
const lease = agentSessionLeaseFixture()
|
||||
const fixture = agentSessionRecordFixture(lease)
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { it, expect, vi } from 'vitest'
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { createStore, closeTestStores, testState } from '../persistence/persistence-test-harness'
|
||||
import { buildProfileStateCutoverFixture } from '../persistence/profile-state-cutover-fixture'
|
||||
import { AutomationRunLaunchError } from './headless-dispatch'
|
||||
import { AutomationService } from './service'
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
app: { getPath: () => testState.dir },
|
||||
safeStorage: {
|
||||
isEncryptionAvailable: () => true,
|
||||
encryptString: (plaintext: string) => Buffer.from(`encrypted:${plaintext}`, 'utf-8'),
|
||||
decryptString: (ciphertext: Buffer) => ciphertext.toString('utf-8').slice('encrypted:'.length)
|
||||
}
|
||||
}))
|
||||
|
||||
it.each(['chat', 'terminal'] as const)(
|
||||
'durably saves an unsuccessful published %s target without a resend',
|
||||
async (kind) => {
|
||||
testState.dir = mkdtempSync(join(tmpdir(), 'orca-34332-failed-target-'))
|
||||
const store = await createStore()
|
||||
try {
|
||||
store.addRepo({
|
||||
...buildProfileStateCutoverFixture('/repo').repos[0],
|
||||
id: 'r1',
|
||||
path: '/repo'
|
||||
})
|
||||
const automation = store.createAutomation({
|
||||
name: 'Failed delivery',
|
||||
prompt: 'check',
|
||||
agentId: 'claude',
|
||||
launchSetupPatch: { surface: 'chat' },
|
||||
projectId: 'r1',
|
||||
workspaceMode: 'existing',
|
||||
workspaceId: 'r1::/repo',
|
||||
timezone: 'UTC',
|
||||
rrule: 'FREQ=DAILY',
|
||||
dtstart: 1
|
||||
})
|
||||
const published = {
|
||||
workspaceId: 'actually-created',
|
||||
sessionKind: kind,
|
||||
chatSessionId: kind === 'chat' ? 'claude_actual' : null,
|
||||
terminalSessionId: kind === 'terminal' ? 'actual-tab' : null,
|
||||
terminalPaneKey:
|
||||
kind === 'terminal' ? 'actual-tab:11111111-1111-4111-8111-111111111111' : null,
|
||||
terminalPtyId: kind === 'terminal' ? 'actual-pty' : null
|
||||
}
|
||||
const dispatch = vi.fn(async () => {
|
||||
throw new AutomationRunLaunchError(
|
||||
'Claude could not confirm delivery. Open the run.',
|
||||
published
|
||||
)
|
||||
})
|
||||
const service = new AutomationService(store, { chatDispatcher: dispatch })
|
||||
service.setWebContents({ isDestroyed: () => false, send: vi.fn() })
|
||||
service.setRendererReady()
|
||||
const failed = await service.runNow(automation.id)
|
||||
expect(failed).toMatchObject({ ...published, status: 'dispatch_failed' })
|
||||
expect(store.listAutomationRuns()[0]).toMatchObject({
|
||||
...published,
|
||||
status: 'dispatch_failed'
|
||||
})
|
||||
expect(dispatch).toHaveBeenCalledOnce()
|
||||
service.stop()
|
||||
} finally {
|
||||
await closeTestStores()
|
||||
rmSync(testState.dir, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
)
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { BrowserClientHostAttachParams } from '../../../../shared/browser/client-hosting/browser-client-host-protocol'
|
||||
|
||||
describe('browser file channel negotiation', () => {
|
||||
it('rejects an attach that asks for the file channel without command negotiation', () => {
|
||||
expect(
|
||||
BrowserClientHostAttachParams.safeParse({
|
||||
authorityRuntimeId: 'runtime-1',
|
||||
browserHostClientId: 'host-1',
|
||||
hostCapabilities: ['webview'],
|
||||
fileChannelProtocolVersion: 1
|
||||
}).success
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,91 +0,0 @@
|
||||
import { mkdtemp, realpath, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import type { BrowserClientHostCommandEvent } from '../../../../shared/browser/client-hosting/browser-client-host-protocol'
|
||||
import { BrowserClientFileChannelTransport } from './browser-client-file-channel-transport'
|
||||
import { executeBrowserClientUploadCommand } from './browser-client-upload-command'
|
||||
import { BrowserClientUploadStaging } from './browser-client-upload-staging'
|
||||
|
||||
let stagingRoot = ''
|
||||
|
||||
beforeEach(async () => {
|
||||
stagingRoot = await realpath(await mkdtemp(path.join(tmpdir(), 'orca-upload-command-')))
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(stagingRoot, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
function uploadEvent(files: unknown): BrowserClientHostCommandEvent {
|
||||
return {
|
||||
type: 'command',
|
||||
pageCommandProtocolVersion: 1,
|
||||
authorityRuntimeId: 'runtime-1',
|
||||
authorityEpoch: 'epoch-1',
|
||||
browserHostClientId: 'host-1',
|
||||
browserHostGeneration: 2,
|
||||
browserPageId: 'page-1',
|
||||
pageHostGeneration: 3,
|
||||
commandSequence: 1,
|
||||
commandId: 'command-1',
|
||||
command: { type: 'automation', method: 'browser.upload', params: { element: '#f', files } }
|
||||
} as BrowserClientHostCommandEvent
|
||||
}
|
||||
|
||||
function transportReturning(
|
||||
responses: Map<string, { contentBase64: string; bytesRead: number; eof: boolean }[]>
|
||||
): { transport: BrowserClientFileChannelTransport; calls: unknown[] } {
|
||||
const calls: unknown[] = []
|
||||
const transport = new BrowserClientFileChannelTransport()
|
||||
transport.bind({
|
||||
fileChannelNegotiated: true,
|
||||
fileChannelAvailability: 'negotiated' as const,
|
||||
sendFileChannelRequest: async (_method, params) => {
|
||||
calls.push(params)
|
||||
const requested = (params as { workspaceRelativePath: string }).workspaceRelativePath
|
||||
const queue = responses.get(requested)
|
||||
if (!queue?.length) {
|
||||
return { ok: false, error: { code: 'not_found', message: 'missing' }, _meta: {} } as never
|
||||
}
|
||||
const next = queue.shift()
|
||||
return {
|
||||
ok: true,
|
||||
result: { ...next, totalBytes: next?.bytesRead ?? 0 },
|
||||
_meta: {}
|
||||
} as never
|
||||
}
|
||||
})
|
||||
return { transport, calls }
|
||||
}
|
||||
|
||||
describe('executeBrowserClientUploadCommand', () => {
|
||||
it('reports the upload failure, not the cleanup failure, when both fail', async () => {
|
||||
const staging = new BrowserClientUploadStaging(stagingRoot, {
|
||||
mkdir: async () => {},
|
||||
writeFile: async () => {},
|
||||
removeDirectorySync: () => {},
|
||||
removeDirectory: async () => {
|
||||
throw new Error('EBUSY: resource busy or locked')
|
||||
}
|
||||
})
|
||||
const { transport } = transportReturning(
|
||||
new Map([['a.txt', [{ contentBase64: '', bytesRead: 0, eof: true }]]])
|
||||
)
|
||||
|
||||
await expect(
|
||||
executeBrowserClientUploadCommand({
|
||||
event: uploadEvent(['a.txt']),
|
||||
params: { element: '#f', files: ['a.txt'] },
|
||||
fileChannel: transport,
|
||||
staging,
|
||||
run: async () => {
|
||||
throw new Error('element not found')
|
||||
}
|
||||
})
|
||||
).rejects.toThrow('element not found')
|
||||
// A later page release needs the retained directory record to retry cleanup.
|
||||
expect(staging.activeStagingCount()).toBe(1)
|
||||
})
|
||||
})
|
||||
-124
@@ -1,124 +0,0 @@
|
||||
import { mkdtempSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { KnownRuntimeEnvironment } from '../../../../shared/remote-runtime/host-catalog/runtime-environments'
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
app: { getPath: () => tmpdir() },
|
||||
ipcMain: { on: vi.fn(), removeListener: vi.fn() }
|
||||
}))
|
||||
|
||||
type CreateHost = (
|
||||
next: { pairing: unknown; authorityRuntimeId: string },
|
||||
callbacks: Record<string, unknown>
|
||||
) => unknown
|
||||
|
||||
const createHostCallbacks: CreateHost[] = []
|
||||
const hostOptions: { browserHostClientId: string }[] = []
|
||||
|
||||
vi.mock('./paired-runtime-browser-client-host', () => ({
|
||||
PairedRuntimeBrowserClientHost: class {
|
||||
constructor(options: { browserHostClientId: string }) {
|
||||
hostOptions.push(options)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('../file-transfers/browser-client-file-channel-transport', () => ({
|
||||
BrowserClientFileChannelTransport: class {
|
||||
bind(): void {}
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('./paired-runtime-browser-client-host-composition', () => ({
|
||||
PairedRuntimeBrowserClientHostComposition: class {
|
||||
constructor(options: { createHost: CreateHost }) {
|
||||
createHostCallbacks.push(options.createHost)
|
||||
}
|
||||
|
||||
start(): Promise<unknown> {
|
||||
return Promise.resolve({ authority: 'lease-a' })
|
||||
}
|
||||
|
||||
close(): Promise<boolean> {
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
|
||||
whenClosed(): Promise<void> {
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
function pairedEnvironment(id: string): KnownRuntimeEnvironment {
|
||||
return {
|
||||
id,
|
||||
name: `Environment ${id}`,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
lastUsedAt: null,
|
||||
runtimeId: null,
|
||||
preferredEndpointId: 'endpoint-a',
|
||||
endpoints: [
|
||||
{
|
||||
id: 'endpoint-a',
|
||||
endpoint: 'ws://127.0.0.1:9999',
|
||||
deviceToken: 'token-a',
|
||||
publicKeyB64: 'key-a'
|
||||
}
|
||||
]
|
||||
} as KnownRuntimeEnvironment
|
||||
}
|
||||
|
||||
async function launch(profileDirectory: string): Promise<void> {
|
||||
vi.resetModules()
|
||||
createHostCallbacks.length = 0
|
||||
hostOptions.length = 0
|
||||
const { initializeBrowserClientHostId } = await import('./browser-client-host-id')
|
||||
initializeBrowserClientHostId(profileDirectory)
|
||||
}
|
||||
|
||||
async function hostingIdentity(): Promise<string> {
|
||||
const runtime = await import('./paired-runtime-browser-client-host-runtime')
|
||||
runtime.configurePairedRuntimeBrowserClientHostsForOrcaProfile({ orcaProfileId: 'profile-a' })
|
||||
await runtime.startPairedRuntimeBrowserClientHost({
|
||||
environment: pairedEnvironment('environment-a'),
|
||||
authorityRuntimeId: 'runtime-a'
|
||||
})
|
||||
const createHost = createHostCallbacks[0]
|
||||
if (!createHost) {
|
||||
throw new Error('client host composition never asked for a host')
|
||||
}
|
||||
createHost({ pairing: {}, authorityRuntimeId: 'runtime-a' }, {})
|
||||
const options = hostOptions[0]
|
||||
if (!options) {
|
||||
throw new Error('client host was never constructed')
|
||||
}
|
||||
return options.browserHostClientId
|
||||
}
|
||||
|
||||
async function hostingIdentityForLaunch(profileDirectory: string): Promise<string> {
|
||||
await launch(profileDirectory)
|
||||
return await hostingIdentity()
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
describe('client host hosting identity wiring', () => {
|
||||
it('attaches under the same identity after the desktop relaunches', async () => {
|
||||
const profileDirectory = mkdtempSync(join(tmpdir(), 'orca-host-wiring-'))
|
||||
|
||||
expect(await hostingIdentityForLaunch(profileDirectory)).toBe(
|
||||
await hostingIdentityForLaunch(profileDirectory)
|
||||
)
|
||||
})
|
||||
|
||||
it('attaches under a different identity for a different Orca profile', async () => {
|
||||
expect(
|
||||
await hostingIdentityForLaunch(mkdtempSync(join(tmpdir(), 'orca-host-wiring-')))
|
||||
).not.toBe(await hostingIdentityForLaunch(mkdtempSync(join(tmpdir(), 'orca-host-wiring-'))))
|
||||
})
|
||||
})
|
||||
@@ -9,26 +9,6 @@ describe('browser client page inventory', () => {
|
||||
expect(prepareBrowserClientPageInventoryForAttach([page, page])).toBeUndefined()
|
||||
})
|
||||
|
||||
it('uses codepoint order to break equal URL-compaction ties across input order', () => {
|
||||
const pageIds = [
|
||||
'ä-page',
|
||||
'z-page',
|
||||
...Array.from({ length: 254 }, (_, index) => `page-${index.toString().padStart(3, '0')}`)
|
||||
]
|
||||
const inventory = pageIds.map(inventoryPage)
|
||||
const forward = prepareBrowserClientPageInventoryForAttach(inventory)
|
||||
const reversed = prepareBrowserClientPageInventoryForAttach(inventory.toReversed())
|
||||
if (!forward || !reversed) {
|
||||
throw new Error('expected encodable inventory')
|
||||
}
|
||||
const omitted = omittedPageIds(forward)
|
||||
const expected = [...pageIds].sort(compareCodepoints).slice(0, omitted.length)
|
||||
|
||||
expect(omitted.length).toBeGreaterThan(0)
|
||||
expect(omitted).toEqual(expected)
|
||||
expect(omittedPageIds(reversed)).toEqual(expected)
|
||||
})
|
||||
|
||||
it('declines the optional snapshot instead of dropping an unencodable page', () => {
|
||||
expect(
|
||||
prepareBrowserClientPageInventoryForAttach([
|
||||
@@ -53,14 +33,3 @@ function inventoryPage(browserPageId: string): BrowserClientHostedPageInventory
|
||||
currentUrl: `https://remote.internal/${'x'.repeat(4096)}`
|
||||
}
|
||||
}
|
||||
|
||||
function omittedPageIds(pages: readonly BrowserClientHostedPageInventory[]): string[] {
|
||||
return pages
|
||||
.filter((page) => page.currentUrl === undefined)
|
||||
.map((page) => page.browserPageId)
|
||||
.sort(compareCodepoints)
|
||||
}
|
||||
|
||||
function compareCodepoints(left: string, right: string): number {
|
||||
return left < right ? -1 : left > right ? 1 : 0
|
||||
}
|
||||
|
||||
@@ -72,12 +72,6 @@ describe('BrowserExtensionInstallStore', () => {
|
||||
store.save(record({ id: STORE_ID }))
|
||||
expect(store.get(STORE_ID)?.installTime).toBeGreaterThanOrEqual(updated ?? 0)
|
||||
})
|
||||
|
||||
it('refuses install directories that could escape the root', () => {
|
||||
const store = new BrowserExtensionInstallStore(root)
|
||||
expect(() => store.installDirectory('../evil', '1.0')).toThrow()
|
||||
expect(() => store.installDirectory(STORE_ID, '../../1.0')).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('readBrowserExtensionManifest', () => {
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
/**
|
||||
* R45: which in-app browser jar a WSL host's pages get, pinned by what can change about the host.
|
||||
* The jar follows the environment (named by the distro) and orcad's key; the distro's registry id
|
||||
* is deliberately not part of it (#29610).
|
||||
*/
|
||||
import { mkdtempSync, realpathSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { KnownRuntimeEnvironment } from '../../../../shared/remote-runtime/host-catalog/runtime-environments'
|
||||
import { defaultWslOrcadEnvironmentId } from '../../../wsl/orcad/wsl-orcad-deploy'
|
||||
|
||||
const bindings = vi.hoisted(() => ({ filePath: '' }))
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
app: { getPath: () => tmpdir() },
|
||||
ipcMain: { on: vi.fn(), removeListener: vi.fn() }
|
||||
}))
|
||||
vi.mock('../partitions/browser-route-partition-binding-runtime', async () => {
|
||||
const { BrowserRoutePartitionBindingStore } =
|
||||
await import('../partitions/browser-route-partition-binding-store')
|
||||
return {
|
||||
currentBrowserRoutePartitionBindingStore: () =>
|
||||
new BrowserRoutePartitionBindingStore({ filePath: bindings.filePath })
|
||||
}
|
||||
})
|
||||
vi.mock('../partitions/browser-route-partition-storage-dependencies', () => ({
|
||||
releaseEvictedBrowserRoutePartitionStorage: vi.fn(async () => {})
|
||||
}))
|
||||
|
||||
import {
|
||||
bindWslHostBrowserRoutePartition,
|
||||
wslHostBrowserRouteStorage
|
||||
} from './wsl-host-browser-route-partition'
|
||||
|
||||
const ORCA_PROFILE = 'profile-a'
|
||||
|
||||
function wslHost(
|
||||
distro: string,
|
||||
hostKey: string,
|
||||
pairing = { revision: 1, device: 'device-a' }
|
||||
): KnownRuntimeEnvironment {
|
||||
return {
|
||||
id: defaultWslOrcadEnvironmentId(distro),
|
||||
name: distro,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
lastUsedAt: null,
|
||||
runtimeId: null,
|
||||
pairingRevision: pairing.revision,
|
||||
pairedDeviceId: pairing.device,
|
||||
orcadDeployment: { kind: 'wsl', distro, localPort: 9999, remotePort: 7777 },
|
||||
preferredEndpointId: 'endpoint-a',
|
||||
endpoints: [
|
||||
{
|
||||
id: 'endpoint-a',
|
||||
kind: 'websocket',
|
||||
label: distro,
|
||||
endpoint: 'ws://127.0.0.1:9999',
|
||||
deviceToken: `token-${pairing.device}`,
|
||||
publicKeyB64: hostKey
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function jar(environment: KnownRuntimeEnvironment): string {
|
||||
const result = wslHostBrowserRouteStorage(ORCA_PROFILE, environment)
|
||||
if (result.kind !== 'stable') {
|
||||
throw new Error(`expected a stable partition, got ${result.reason}`)
|
||||
}
|
||||
return bindWslHostBrowserRoutePartition(result.storage, 'default')
|
||||
}
|
||||
|
||||
let directory: string
|
||||
|
||||
beforeEach(() => {
|
||||
directory = realpathSync(mkdtempSync(join(realpathSync(tmpdir()), 'wsl-host-jar-')))
|
||||
bindings.filePath = join(directory, 'bindings.json')
|
||||
})
|
||||
afterEach(() => {
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe('WSL host browser jar identity', () => {
|
||||
it('starts a new jar when the distro runs a newly deployed orcad (a new key)', () => {
|
||||
const before = jar(wslHost('Ubuntu', 'orcad-key-a'))
|
||||
|
||||
expect(jar(wslHost('Ubuntu', 'orcad-key-b'))).not.toBe(before)
|
||||
// The old jar is still the old key's; nothing of it moved to the new one.
|
||||
expect(jar(wslHost('Ubuntu', 'orcad-key-a'))).toBe(before)
|
||||
})
|
||||
|
||||
it('starts a new jar for a renamed distro, even one that kept its orcad key', () => {
|
||||
const before = jar(wslHost('Ubuntu', 'orcad-key-a'))
|
||||
|
||||
expect(jar(wslHost('Ubuntu-work', 'orcad-key-a'))).not.toBe(before)
|
||||
})
|
||||
|
||||
// Accepted (R45, Low): the same name and key is the same host to this desktop.
|
||||
it('keeps the jar for a distro re-imported under its old name with its old key', () => {
|
||||
const before = jar(wslHost('Ubuntu', 'orcad-key-a'))
|
||||
|
||||
expect(jar(wslHost('Ubuntu', 'orcad-key-a', { revision: 7, device: 'device-b' }))).toBe(before)
|
||||
})
|
||||
})
|
||||
@@ -1,116 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { join } from 'node:path'
|
||||
import { createSettings } from './runtime-home-settings-test-fixtures'
|
||||
import {
|
||||
createStore,
|
||||
setupRuntimeHomeTest,
|
||||
teardownRuntimeHomeTest,
|
||||
testState
|
||||
} from './runtime-home-service-test-harness'
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
app: {
|
||||
getPath: () => testState.userDataDir
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock(import('node:os'), async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
return {
|
||||
...actual,
|
||||
homedir: () => testState.fakeHomeDir
|
||||
}
|
||||
})
|
||||
|
||||
const bridge = vi.hoisted(() => ({
|
||||
start: vi.fn<(target: Record<string, string>) => Promise<void>>(() => Promise.resolve())
|
||||
}))
|
||||
|
||||
vi.mock('../codex/history/bridges/wsl-codex-session-bridge', () => ({
|
||||
startWslCodexSessionBridgeInBackground: bridge.start
|
||||
}))
|
||||
|
||||
let wslHome = ''
|
||||
vi.mock('../wsl/discovery/distro-catalog', () => ({
|
||||
getDefaultWslDistro: () => 'Ubuntu',
|
||||
getWslHome: () => wslHome
|
||||
}))
|
||||
|
||||
function accountsRoot(): string {
|
||||
return join(wslHome, '.local', 'share', 'orca', 'codex-accounts')
|
||||
}
|
||||
|
||||
function wslAccount(id: string, distro: string) {
|
||||
return {
|
||||
id,
|
||||
email: `${id}@example.com`,
|
||||
managedHomePath: join(accountsRoot(), id, 'home'),
|
||||
managedHomeRuntime: 'wsl' as const,
|
||||
wslDistro: distro,
|
||||
wslLinuxHomePath: join(accountsRoot(), id, 'home'),
|
||||
providerAccountId: null,
|
||||
workspaceLabel: null,
|
||||
workspaceAccountId: null,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
lastAuthenticatedAt: 1
|
||||
}
|
||||
}
|
||||
|
||||
async function createService(selectedUbuntuAccount: string | null) {
|
||||
const store = createStore(
|
||||
createSettings({
|
||||
codexManagedAccounts: [
|
||||
wslAccount('a', 'Ubuntu'),
|
||||
wslAccount('b', 'Ubuntu'),
|
||||
wslAccount('other-distro', 'Debian')
|
||||
],
|
||||
activeCodexManagedAccountIdsByRuntime: {
|
||||
host: null,
|
||||
wsl: { Ubuntu: selectedUbuntuAccount }
|
||||
}
|
||||
})
|
||||
)
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the harness store implements the getSettings/updateSettings surface this service reads.
|
||||
return new CodexRuntimeHomeService(store as never)
|
||||
}
|
||||
|
||||
describe('WSL sibling account session bridging', () => {
|
||||
let originalPlatform: PropertyDescriptor | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
setupRuntimeHomeTest()
|
||||
originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
|
||||
wslHome = join(testState.userDataDir, 'ubuntu-home')
|
||||
bridge.start.mockReset()
|
||||
bridge.start.mockImplementation(() => Promise.resolve())
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (originalPlatform) {
|
||||
Object.defineProperty(process, 'platform', originalPlatform)
|
||||
}
|
||||
teardownRuntimeHomeTest()
|
||||
})
|
||||
|
||||
it('lets a concurrent launch join the sibling loop already bridging that home', async () => {
|
||||
let releaseFirst: () => void = () => {}
|
||||
bridge.start.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
releaseFirst = resolve
|
||||
})
|
||||
)
|
||||
const service = await createService('b')
|
||||
|
||||
service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })
|
||||
service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })
|
||||
await vi.waitFor(() => expect(bridge.start).toHaveBeenCalledTimes(1))
|
||||
releaseFirst()
|
||||
await vi.waitFor(() => expect(bridge.start).toHaveBeenCalledTimes(2))
|
||||
await Promise.resolve()
|
||||
expect(bridge.start).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
@@ -1,99 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { DaemonPtyAdapter } from './daemon-pty-adapter'
|
||||
import { DegradedDaemonPtyProvider } from './degraded-daemon-pty-provider'
|
||||
import { LocalPtyProvider } from '../providers/local-pty/local-pty-provider'
|
||||
import type { PtyProcessInfo } from '../providers/pty-provider-contract'
|
||||
import { TerminalSessionOwnerUnverifiedError } from './daemon-errors'
|
||||
|
||||
const cleanups: (() => void)[] = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const cleanup of cleanups.splice(0)) {
|
||||
cleanup()
|
||||
}
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
function adapterFixture(label: string, pid: number) {
|
||||
const adapter = new DaemonPtyAdapter({
|
||||
socketPath: join(tmpdir(), `unused-${label}.sock`),
|
||||
tokenPath: join(tmpdir(), `unused-${label}.token`)
|
||||
})
|
||||
const client = adapter['client']
|
||||
vi.spyOn(client, 'ensureConnected').mockResolvedValue()
|
||||
vi.spyOn(client, 'ensureConnectedWithin').mockResolvedValue()
|
||||
const identity = vi.spyOn(client, 'getDaemonIdentity')
|
||||
const request = vi.spyOn(client, 'request').mockResolvedValue({ sessions: [] })
|
||||
const spawn = vi.spyOn(adapter, 'spawn').mockImplementation(async (opts) => ({
|
||||
id: opts.sessionId ?? 'unexpected-fresh-spawn',
|
||||
incarnationId: opts.expectedIncarnationId,
|
||||
isReattach: true
|
||||
}))
|
||||
return {
|
||||
adapter,
|
||||
request,
|
||||
spawn,
|
||||
setProcesses(processes: PtyProcessInfo[]) {
|
||||
request.mockResolvedValue({
|
||||
sessions: processes.map((process) => ({
|
||||
sessionId: process.id,
|
||||
incarnationId: process.incarnationId,
|
||||
cwd: process.cwd,
|
||||
isAlive: true
|
||||
}))
|
||||
})
|
||||
},
|
||||
async publishIdentity(generation: number) {
|
||||
identity.mockReturnValue({
|
||||
pid,
|
||||
startedAtMs: generation + 1,
|
||||
launchNonce: label + generation
|
||||
})
|
||||
await adapter.establishLifecycleLease()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fixture() {
|
||||
const current = adapterFixture('current', 999_999_997)
|
||||
const legacy = adapterFixture('legacy', 999_999_998)
|
||||
const fallback = new LocalPtyProvider()
|
||||
vi.spyOn(fallback, 'listProcesses').mockResolvedValue([])
|
||||
const provider = new DegradedDaemonPtyProvider({
|
||||
current: current.adapter,
|
||||
legacy: [legacy.adapter],
|
||||
fallback
|
||||
})
|
||||
cleanups.push(() => provider.dispose())
|
||||
await current.publishIdentity(0)
|
||||
await legacy.publishIdentity(0)
|
||||
const recovery = provider['ownerRecovery']
|
||||
return { current, legacy, provider, recovery }
|
||||
}
|
||||
|
||||
function processInfo(id: string, incarnationId: string): PtyProcessInfo {
|
||||
return { id, incarnationId, cwd: '', title: 'shell' }
|
||||
}
|
||||
|
||||
describe('shared daemon owner incarnation retirement', () => {
|
||||
it('retains authoritative incarnation mismatch refusal after another daemon changes', async () => {
|
||||
const { current, legacy, provider } = await fixture()
|
||||
legacy.setProcesses([processInfo('live', 'current-incarnation')])
|
||||
await provider.discoverDaemonSessions()
|
||||
await current.publishIdentity(1)
|
||||
await expect(
|
||||
provider.spawn({
|
||||
sessionId: 'live',
|
||||
attachOnly: true,
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
expectedIncarnationId: 'retired-incarnation',
|
||||
expectedIncarnationIsAuthoritative: true
|
||||
})
|
||||
).rejects.toBeInstanceOf(TerminalSessionOwnerUnverifiedError)
|
||||
expect(current.spawn).not.toHaveBeenCalled()
|
||||
expect(legacy.spawn).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -46,39 +46,6 @@ describe('tracked gh capture lifetime', () => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it.each([false, true])(
|
||||
'keeps grace settlement separate from termination (abort: %s)',
|
||||
async (abort) => {
|
||||
const value = child(1234)
|
||||
spawnMock.mockReturnValue(value)
|
||||
const owner = createGhLogicalLifetime()
|
||||
const controller = new AbortController()
|
||||
const result = ghExecFileAsync(
|
||||
['api', 'user'],
|
||||
{ timeout: 10, ...(abort ? { signal: controller.signal } : {}) },
|
||||
owner.hooks
|
||||
)
|
||||
const rejection = expect(result).rejects.toThrow()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
if (abort) {
|
||||
controller.abort('private-reason')
|
||||
}
|
||||
await vi.advanceTimersByTimeAsync(12_010)
|
||||
await rejection
|
||||
owner.seal()
|
||||
let terminated = false
|
||||
void owner.terminated.then(() => {
|
||||
terminated = true
|
||||
})
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(terminated).toBe(false)
|
||||
expect(spawnMock).toHaveBeenCalledOnce()
|
||||
value.emit('close', null, 'SIGKILL')
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(terminated).toBe(true)
|
||||
}
|
||||
)
|
||||
|
||||
it('waits for a PID error close before retry and ignores its stale callback', async () => {
|
||||
const values = [child(11), child(12)]
|
||||
spawnMock.mockReturnValueOnce(values[0]).mockReturnValueOnce(values[1])
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import { chmod, mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { gitExecFileAsync, gitExecFileAsyncBuffer } from './git-exec-file'
|
||||
import { GitCommandTimeoutError } from '../../../shared/git/commands/git-command-timeout'
|
||||
import { gitStreamStdout } from './git-stream-stdout'
|
||||
|
||||
const tempRoots: string[] = []
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })))
|
||||
})
|
||||
|
||||
async function createTimedGitFixture(): Promise<{
|
||||
cwd: string
|
||||
env: NodeJS.ProcessEnv
|
||||
}> {
|
||||
const root = await mkdtemp(path.join(tmpdir(), 'orca-git-timeout-'))
|
||||
tempRoots.push(root)
|
||||
const binDir = path.join(root, 'bin')
|
||||
const cwd = path.join(root, 'repo')
|
||||
await mkdir(binDir)
|
||||
await mkdir(cwd)
|
||||
const script = path.join(binDir, 'git')
|
||||
await writeFile(
|
||||
script,
|
||||
`#!/usr/bin/env node
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
async function run() {
|
||||
if (process.env.ORCA_STUB_PROGRESSIVE === '1') {
|
||||
const remaining = Math.max(0, Number(process.env.ORCA_STUB_EXIT_AT_MS) - Date.now())
|
||||
const step = remaining / 3
|
||||
await sleep(step)
|
||||
process.stdout.write('one')
|
||||
await sleep(step)
|
||||
process.stdout.write('two')
|
||||
await sleep(step)
|
||||
process.stdout.write('three')
|
||||
return
|
||||
}
|
||||
await sleep(Number(process.env.ORCA_STUB_SLEEP_MS))
|
||||
process.stdout.write('done')
|
||||
}
|
||||
void run()
|
||||
`
|
||||
)
|
||||
await chmod(script, 0o755)
|
||||
return {
|
||||
cwd,
|
||||
env: { ...process.env, PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ''}` }
|
||||
}
|
||||
}
|
||||
|
||||
describe.skipIf(process.platform === 'win32')('git read timeout behavior', () => {
|
||||
it('rejects a silent read at 1.1 times the deadline with a typed error', async () => {
|
||||
const fixture = await createTimedGitFixture()
|
||||
await expect(
|
||||
gitExecFileAsync(['status', '--porcelain=v2'], {
|
||||
...fixture,
|
||||
env: { ...fixture.env, ORCA_STUB_SLEEP_MS: '110' },
|
||||
timeoutMsForTest: 100
|
||||
})
|
||||
).rejects.toBeInstanceOf(GitCommandTimeoutError)
|
||||
})
|
||||
|
||||
it('times out a silent streaming read with the same typed error', async () => {
|
||||
const fixture = await createTimedGitFixture()
|
||||
await expect(
|
||||
gitStreamStdout(['status'], {
|
||||
...fixture,
|
||||
env: { ...fixture.env, ORCA_STUB_SLEEP_MS: '110' },
|
||||
timeoutMsForTest: 100,
|
||||
onStdout: () => {}
|
||||
})
|
||||
).rejects.toBeInstanceOf(GitCommandTimeoutError)
|
||||
})
|
||||
|
||||
it('applies the read default to binary blob reads', async () => {
|
||||
const fixture = await createTimedGitFixture()
|
||||
await expect(
|
||||
gitExecFileAsyncBuffer(['show', 'HEAD:file.bin'], {
|
||||
...fixture,
|
||||
env: { ...fixture.env, ORCA_STUB_SLEEP_MS: '110' },
|
||||
timeoutMsForTest: 100
|
||||
})
|
||||
).rejects.toBeInstanceOf(GitCommandTimeoutError)
|
||||
})
|
||||
|
||||
it.each([['fetch'], ['checkout'], ['unrecognized-command']])(
|
||||
'does not default-timeout the fail-safe %s class',
|
||||
async (subcommand) => {
|
||||
const fixture = await createTimedGitFixture()
|
||||
await expect(
|
||||
gitExecFileAsync([subcommand], {
|
||||
...fixture,
|
||||
env: { ...fixture.env, ORCA_STUB_SLEEP_MS: '110' },
|
||||
timeoutMsForTest: 100
|
||||
})
|
||||
).resolves.toMatchObject({ stdout: 'done' })
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -1,11 +1,7 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { resolveGitAdmissionTier } from './git-operation-executor'
|
||||
import {
|
||||
acquireGitAdmission,
|
||||
_resetGitAdmissionForTests,
|
||||
_gitAdmissionSnapshotForTests
|
||||
} from './git-subprocess-admission'
|
||||
import { GitAdmissionScheduler } from '../../../shared/git/admission/git-admission-scheduler'
|
||||
import { _resetGitAdmissionForTests } from './git-subprocess-admission'
|
||||
|
||||
import {
|
||||
worktreeCreateGit,
|
||||
worktreePreparationGit
|
||||
@@ -34,44 +30,4 @@ describe('Git operation execution policy', () => {
|
||||
await create
|
||||
expect(resolveGitAdmissionTier()).toBe('status')
|
||||
})
|
||||
|
||||
it.each([false, true])(
|
||||
'expires inherited policy after completion (failure: %s)',
|
||||
async (fail) => {
|
||||
const finishDetached = Promise.withResolvers<void>()
|
||||
let detached: Promise<string> | undefined
|
||||
const create = worktreeCreateGit.run(async () => {
|
||||
detached = finishDetached.promise.then(() => resolveGitAdmissionTier())
|
||||
if (fail) {
|
||||
throw new Error('create failed')
|
||||
}
|
||||
})
|
||||
await (fail ? expect(create).rejects.toThrow('create failed') : create)
|
||||
finishDetached.resolve()
|
||||
await expect(detached).resolves.toBe('status')
|
||||
}
|
||||
)
|
||||
|
||||
it('admits nested commands without priority options while preparation work stays queued', async () => {
|
||||
_resetGitAdmissionForTests(new GitAdmissionScheduler({ generalCap: 1, generalHeadroom: 1 }))
|
||||
const blocker = await acquireGitAdmission({ args: ['status'], cwd: '/repo' })
|
||||
const preparation = worktreePreparationGit.run(() =>
|
||||
acquireGitAdmission({ args: ['status'], cwd: '/repo' })
|
||||
)
|
||||
try {
|
||||
await worktreeCreateGit.run(async () => {
|
||||
const grant = await acquireGitAdmission({ args: ['rev-parse', 'HEAD'], cwd: '/repo' })
|
||||
expect(_gitAdmissionSnapshotForTests()).toMatchObject({
|
||||
queued: 1,
|
||||
budgets: { general: { baseUsed: 1, headroomUsed: 1 } }
|
||||
})
|
||||
grant.release()
|
||||
})
|
||||
} finally {
|
||||
blocker.release()
|
||||
const grant = await preparation
|
||||
grant.release()
|
||||
}
|
||||
expect(_gitAdmissionSnapshotForTests().queued).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { GitDiffResult } from '../../../shared/git/diff/git-diff-compare-types'
|
||||
import { MAX_SETTLED_DIFF_CACHE_RESULT_CHARACTERS, SettledDiffCache } from './settled-diff-cache'
|
||||
import type { WorktreeDiffStamp } from './worktree-diff-stamp'
|
||||
|
||||
function settledStamp(value: string): WorktreeDiffStamp {
|
||||
// Old enough that the racy-write margin is satisfied.
|
||||
return { value, newestMtimeMs: Date.now() - 60_000, capturedAtMs: Date.now() }
|
||||
}
|
||||
|
||||
function diffOfSize(characters: number): GitDiffResult {
|
||||
return {
|
||||
kind: 'text',
|
||||
originalContent: '',
|
||||
modifiedContent: 'x'.repeat(characters),
|
||||
originalIsBinary: false,
|
||||
modifiedIsBinary: false
|
||||
}
|
||||
}
|
||||
|
||||
describe('SettledDiffCache bounds', () => {
|
||||
it('declines a single result larger than the per-entry cap', () => {
|
||||
const cache = new SettledDiffCache()
|
||||
|
||||
cache.set(
|
||||
'huge',
|
||||
settledStamp('huge-stamp'),
|
||||
diffOfSize(MAX_SETTLED_DIFF_CACHE_RESULT_CHARACTERS + 1),
|
||||
cache.beginRead()
|
||||
)
|
||||
|
||||
expect(cache.stats().entries).toBe(0)
|
||||
expect(cache.stats().retainedCharacters).toBe(0)
|
||||
})
|
||||
|
||||
it('releases retained characters when a key is overwritten', () => {
|
||||
const cache = new SettledDiffCache()
|
||||
cache.set('key', settledStamp('first'), diffOfSize(1_000), cache.beginRead())
|
||||
cache.set('key', settledStamp('second'), diffOfSize(10), cache.beginRead())
|
||||
|
||||
expect(cache.stats().entries).toBe(1)
|
||||
expect(cache.stats().retainedCharacters).toBe(10)
|
||||
expect(cache.get('key', settledStamp('first'))).toBeUndefined()
|
||||
expect(cache.get('key', settledStamp('second'))).toBeDefined()
|
||||
})
|
||||
|
||||
it('drops everything and refuses in-flight stores after a clear', () => {
|
||||
const cache = new SettledDiffCache()
|
||||
const readGeneration = cache.beginRead()
|
||||
cache.clear()
|
||||
cache.set('key', settledStamp('stamp'), diffOfSize(1), readGeneration)
|
||||
|
||||
expect(cache.stats().entries).toBe(0)
|
||||
expect(cache.stats().invalidatedDuringRead).toBe(1)
|
||||
})
|
||||
})
|
||||
@@ -1,108 +0,0 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, expect, it } from 'vitest'
|
||||
import { gitExecFileAsync } from '../../command-runner/git-exec-file'
|
||||
import { addWorktree } from './worktree-add'
|
||||
import { listWorktrees } from '../catalog/worktree-scan-cache'
|
||||
import { finalizePreparedWorktree } from './worktree-create-preparation'
|
||||
import { worktreeCreateGit } from './worktree-create-git-executor'
|
||||
import {
|
||||
_resetPreparationPoolForTests,
|
||||
listPreparations,
|
||||
startPreparation,
|
||||
takePreparation
|
||||
} from '../../../workspaces/creation/preparation/worktree-create-preparation-pool'
|
||||
import {
|
||||
acquireGitAdmission,
|
||||
_resetGitAdmissionForTests
|
||||
} from '../../command-runner/git-subprocess-admission'
|
||||
import { GitAdmissionScheduler } from '../../../../shared/git/admission/git-admission-scheduler'
|
||||
import type { GitAdmissionEvent } from '../../../../shared/git/admission/git-admission-state'
|
||||
|
||||
const roots: string[] = []
|
||||
afterEach(async () => {
|
||||
_resetGitAdmissionForTests()
|
||||
await _resetPreparationPoolForTests()
|
||||
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })))
|
||||
})
|
||||
|
||||
it('creates cold and prepared worktrees with real Git while status capacity is occupied', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'orca-create-policy-'))
|
||||
roots.push(root)
|
||||
const repo = join(root, 'repo')
|
||||
await gitExecFileAsync(['init', '--quiet', repo], { cwd: root })
|
||||
await gitExecFileAsync(['symbolic-ref', 'HEAD', 'refs/heads/main'], { cwd: repo })
|
||||
await writeFile(join(repo, 'file.txt'), 'workspace content\n')
|
||||
await gitExecFileAsync(['add', '.'], { cwd: repo })
|
||||
await gitExecFileAsync(
|
||||
['-c', 'user.name=Test', '-c', 'user.email=test@example.com', 'commit', '-qm', 'fixture'],
|
||||
{ cwd: repo }
|
||||
)
|
||||
|
||||
const events: GitAdmissionEvent[] = []
|
||||
_resetGitAdmissionForTests(
|
||||
new GitAdmissionScheduler({
|
||||
generalCap: 1,
|
||||
generalHeadroom: 1,
|
||||
onAdmissionEvent: (event) => {
|
||||
if (event.phase === 'grant') {
|
||||
events.push(event)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
await worktreeCreateGit.run(() =>
|
||||
startPreparation({
|
||||
repoPath: repo,
|
||||
workspaceRoot: root,
|
||||
baseBranch: 'main',
|
||||
canonicalBase: 'refs/heads/main',
|
||||
options: {}
|
||||
})
|
||||
)
|
||||
expect(events.length).toBeGreaterThan(0)
|
||||
expect(events.every((event) => event.tier === 'status')).toBe(true)
|
||||
const [prepared] = listPreparations()
|
||||
expect(prepared).toBeDefined()
|
||||
takePreparation(prepared)
|
||||
|
||||
const blocker = await acquireGitAdmission({ args: ['status'], cwd: repo })
|
||||
events.length = 0
|
||||
try {
|
||||
await worktreeCreateGit.run(async () => {
|
||||
await gitExecFileAsync(['fetch', repo, 'main'], {
|
||||
cwd: repo,
|
||||
useConfiguredSshCommandForNetwork: true,
|
||||
env: { ...process.env, GIT_SSH_COMMAND: '' }
|
||||
})
|
||||
await addWorktree(repo, join(root, 'cold'), 'cold', 'main')
|
||||
await finalizePreparedWorktree(
|
||||
repo,
|
||||
prepared.preparedPath,
|
||||
join(root, 'warm'),
|
||||
'warm',
|
||||
'main',
|
||||
false,
|
||||
{},
|
||||
prepared.lockReason
|
||||
)
|
||||
expect(await listWorktrees(repo)).toHaveLength(3)
|
||||
})
|
||||
expect(
|
||||
events.filter((event) => event.args[0] === 'config').map((event) => event.args)
|
||||
).toContainEqual(['config', '--null', '--get-regexp', '^(core\\.sshcommand|ssh\\.variant)$'])
|
||||
expect(events.some((event) => event.args.includes('fetch'))).toBe(true)
|
||||
expect(events.every((event) => event.tier === 'interactive')).toBe(true)
|
||||
expect(
|
||||
events
|
||||
.filter((event) => event.admissionClass === 'general')
|
||||
.every((event) => event.slotKind === 'headroom')
|
||||
).toBe(true)
|
||||
for (const name of ['cold', 'warm']) {
|
||||
expect(await readFile(join(root, name, 'file.txt'), 'utf8')).toBe('workspace content\n')
|
||||
}
|
||||
} finally {
|
||||
blocker.release()
|
||||
}
|
||||
})
|
||||
@@ -1,107 +0,0 @@
|
||||
import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { runner } = vi.hoisted(() => ({ runner: vi.fn() }))
|
||||
vi.mock('../../command-runner/git-exec-file', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
gitExecFileAsync: runner
|
||||
}))
|
||||
|
||||
vi.mock('../../command-runner/git-exec-file-sync', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
gitExecFileSync: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../../command-runner/wsl-path-translation', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
translateWslOutputPaths: (value: string) => value
|
||||
}))
|
||||
vi.mock('../../source-control/git-read-cache-invalidation', () => ({
|
||||
runWithGitReadCacheInvalidation: (run: () => unknown) => run()
|
||||
}))
|
||||
|
||||
import { getBranchConflictKind, getBranchConflictKindViaExec } from '../../repo-branch-conflict'
|
||||
import { clearGitCapabilityStateForTests } from '../../command-runner/git-capability-state'
|
||||
import { GitBinaryTestFixture } from '../../../../shared/git/compatibility/git-binary.test-fixture'
|
||||
|
||||
const expectedVersion = process.env.ORCA_GIT_COMPAT_VERSION
|
||||
let fixture: GitBinaryTestFixture
|
||||
let root = ''
|
||||
let repo = ''
|
||||
|
||||
async function git(args: string[], cwd = repo): Promise<{ stdout: string; stderr: string }> {
|
||||
return fixture.exec(args, cwd)
|
||||
}
|
||||
|
||||
async function initializeRepo(cwd: string): Promise<void> {
|
||||
await mkdir(cwd, { recursive: true })
|
||||
await git(['init', '-q', '-b', 'main'], cwd).catch(() => git(['init', '-q'], cwd))
|
||||
await git(['config', 'user.name', 'Worktree Safety'], cwd)
|
||||
await git(['config', 'user.email', 'safety@example.invalid'], cwd)
|
||||
await git(['config', 'commit.gpgSign', 'false'], cwd)
|
||||
await git(['config', 'core.hooksPath', '.git/no-hooks'], cwd)
|
||||
await git(['config', 'gc.auto', '0'], cwd)
|
||||
await writeFile(join(cwd, 'seed'), 'seed\n')
|
||||
await git(['add', 'seed'], cwd)
|
||||
await git(['commit', '-qm', 'seed'], cwd)
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
root = await realpath(await mkdtemp(join(tmpdir(), 'orca-worktree-safety-')))
|
||||
fixture = new GitBinaryTestFixture(root)
|
||||
await fixture.start()
|
||||
const { stdout } = await fixture.exec(['--version'])
|
||||
expect(stdout).toContain(expectedVersion ? `git version ${expectedVersion}` : 'git version ')
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
try {
|
||||
await fixture.stop()
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
repo = await mkdtemp(join(root, 'repo-'))
|
||||
await initializeRepo(repo)
|
||||
clearGitCapabilityStateForTests()
|
||||
runner.mockImplementation((args: string[], options: { cwd?: string }) => git(args, options.cwd))
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(repo, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe('worktree safety with the real Git binary', () => {
|
||||
it.each([
|
||||
['feature', 'Feature'],
|
||||
['Ä', 'ä'],
|
||||
['K', 'K'],
|
||||
['ß', 'ẞ']
|
||||
])(
|
||||
'protects packed %s from the alias %s on native and SSH hosts',
|
||||
async (existing, candidate) => {
|
||||
await git(['branch', existing])
|
||||
await git(['pack-refs', '--all'])
|
||||
const before = (await git(['rev-parse', `refs/heads/${existing}`])).stdout
|
||||
await writeFile(join(repo, 'seed'), 'advanced base\n')
|
||||
await git(['commit', '-qam', 'advance'])
|
||||
for (const setting of ['true', 'false', 'unset']) {
|
||||
await git(
|
||||
setting === 'unset'
|
||||
? ['config', '--unset', 'core.ignoreCase']
|
||||
: ['config', 'core.ignoreCase', setting]
|
||||
)
|
||||
await expect(getBranchConflictKind(repo, candidate)).resolves.toBe('local')
|
||||
await expect(getBranchConflictKindViaExec((args) => git(args), candidate)).resolves.toBe(
|
||||
'local'
|
||||
)
|
||||
expect((await git(['rev-parse', `refs/heads/${existing}`])).stdout).toBe(before)
|
||||
}
|
||||
},
|
||||
90_000
|
||||
)
|
||||
})
|
||||
@@ -1,109 +0,0 @@
|
||||
import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { GitBinaryTestFixture } from '../../../../shared/git/compatibility/git-binary.test-fixture'
|
||||
|
||||
vi.mock('../../command-runner/git-exec-file', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
gitExecFileAsync: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../../command-runner/git-exec-file-sync', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
gitExecFileSync: vi.fn()
|
||||
}))
|
||||
vi.mock('../../refs/maintenance/local-repo-ref-maintenance', () => ({
|
||||
withRepoRefMaintenancePaused: (_reason: string, run: () => unknown) => run()
|
||||
}))
|
||||
|
||||
import { forceDeleteLocalBranch } from './worktree-branch-removal'
|
||||
|
||||
let fixture: GitBinaryTestFixture
|
||||
let root = ''
|
||||
let repo = ''
|
||||
let checkout = ''
|
||||
|
||||
async function git(args: string[], cwd = repo): Promise<{ stdout: string; stderr: string }> {
|
||||
return fixture.exec(args, cwd, { GIT_CONFIG_GLOBAL: join(root, 'empty-config') })
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
root = await realpath(await mkdtemp(join(tmpdir(), 'orca-rebase-reservation-')))
|
||||
fixture = new GitBinaryTestFixture(root)
|
||||
await fixture.start()
|
||||
repo = join(root, 'repo')
|
||||
checkout = join(root, 'checkout')
|
||||
await mkdir(repo)
|
||||
await writeFile(join(root, 'empty-config'), '')
|
||||
await git(['init', '-q'])
|
||||
await git(['symbolic-ref', 'HEAD', 'refs/heads/main'])
|
||||
for (const [key, value] of [
|
||||
['user.name', 'Reservation Safety'],
|
||||
['user.email', 'safety@example.invalid'],
|
||||
['commit.gpgSign', 'false'],
|
||||
['core.hooksPath', '.git/no-hooks'],
|
||||
['gc.auto', '0']
|
||||
]) {
|
||||
await git(['config', key, value])
|
||||
}
|
||||
await writeFile(join(repo, 'seed'), 'seed\n')
|
||||
await git(['add', 'seed'])
|
||||
await git(['commit', '-qm', 'seed'])
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await fixture.stop()
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
describe('rebase branch reservations with real Git', () => {
|
||||
it.for(['native'] as const)(
|
||||
'%s retains auxiliary branches reserved by a paused real rebase --update-refs',
|
||||
{ timeout: 120_000 },
|
||||
async (_host, context) => {
|
||||
await git(['worktree', 'add', '-q', '-b', 'feature', checkout])
|
||||
await writeFile(join(checkout, 'first'), 'first\n')
|
||||
await git(['add', 'first'], checkout)
|
||||
await git(['commit', '-qm', 'first'], checkout)
|
||||
await git(['branch', 'auxiliary'], checkout)
|
||||
const expected = (await git(['rev-parse', 'refs/heads/auxiliary'])).stdout.trim()
|
||||
await writeFile(join(checkout, 'second'), 'second\n')
|
||||
await git(['add', 'second'], checkout)
|
||||
await git(['commit', '-qm', 'second'], checkout)
|
||||
await writeFile(join(repo, 'base'), 'advanced main\n')
|
||||
await git(['add', 'base'])
|
||||
await git(['commit', '-qm', 'advance main'])
|
||||
try {
|
||||
await git(['rebase', '--update-refs', '--exec', 'false', 'main'], checkout)
|
||||
throw new Error('Rebase did not pause')
|
||||
} catch (error) {
|
||||
if (error instanceof Error && /unknown option[^\n]*update-refs/.test(error.message)) {
|
||||
context.skip()
|
||||
}
|
||||
if (!(error instanceof Error) || !/execution failed/.test(error.message)) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
const gitDir = (await git(['rev-parse', '--absolute-git-dir'], checkout)).stdout.trim()
|
||||
expect(await readFile(join(gitDir, 'rebase-merge', 'update-refs'), 'utf8')).toContain(
|
||||
'refs/heads/auxiliary\n'
|
||||
)
|
||||
await expect(git(['branch', '-D', 'auxiliary'])).rejects.toThrow(
|
||||
/checked out|used by worktree/
|
||||
)
|
||||
const cleanupGit = vi.fn((args: string[], cwd: string) => git(args, cwd))
|
||||
const remove = () => forceDeleteLocalBranch(repo, 'auxiliary', expected, cleanupGit)
|
||||
await expect(remove()).rejects.toThrow('checked out in another worktree')
|
||||
expect(cleanupGit.mock.calls.map(([args]) => args)).toEqual([
|
||||
['worktree', 'list', '--porcelain']
|
||||
])
|
||||
expect((await git(['rev-parse', 'refs/heads/auxiliary'])).stdout.trim()).toBe(expected)
|
||||
await git(['rebase', '--abort'], checkout)
|
||||
await expect(remove()).resolves.toBeUndefined()
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -1,96 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isMap, parseDocument } from 'yaml'
|
||||
|
||||
import { enablePlugin, updateConfigContent } from './hermes-config-yaml'
|
||||
|
||||
function expectInsertion(source: string, expected: unknown, eol: string): string {
|
||||
const original = parseDocument(source)
|
||||
expect(original.errors).toEqual([])
|
||||
expect(original.warnings).toEqual([])
|
||||
const plugins = original.get('plugins', true)
|
||||
const parent = isMap(plugins) ? plugins : original.contents
|
||||
if (!isMap(parent) || !parent.range) {
|
||||
throw new Error('Fixture must contain a ranged mapping')
|
||||
}
|
||||
const position = parent.range[1] - 1
|
||||
const result = updateConfigContent(source, enablePlugin)
|
||||
expect(result.detail).toBeUndefined()
|
||||
const output = result.content ?? ''
|
||||
const document = parseDocument(output)
|
||||
expect(document.errors).toEqual([])
|
||||
expect(document.warnings).toEqual([])
|
||||
expect(document.toJS()).toEqual(expected)
|
||||
expect(output.startsWith(source.slice(0, position))).toBe(true)
|
||||
expect(output.endsWith(source.slice(position))).toBe(true)
|
||||
expect(updateConfigContent(output, enablePlugin)).toEqual({ content: output })
|
||||
if (eol === '\r\n') {
|
||||
expect(output.replaceAll(eol, '')).not.toContain('\n')
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
describe.each(['\n', '\r\n'])('Hermes flow insertion with tab separation (%j)', (eol) => {
|
||||
it.each([
|
||||
{
|
||||
name: 'minimal tab',
|
||||
source: 'plugins: {custom: keep # note\n\t}',
|
||||
padding: '\n '
|
||||
},
|
||||
{
|
||||
name: 'deeper escaped quoted key',
|
||||
source: ' "plu\\u0067ins": {custom: keep # note\n \t}',
|
||||
padding: '\n '
|
||||
},
|
||||
{
|
||||
name: 'mixed spaces and tabs below required indentation',
|
||||
source: ' plugins: {custom: keep # note\n \t \t }',
|
||||
padding: '\n '
|
||||
},
|
||||
{
|
||||
name: 'whitespace-only closing line',
|
||||
source: 'plugins: {custom: keep # note\n\t \t }',
|
||||
padding: '\n '
|
||||
},
|
||||
{
|
||||
name: 'sufficient spaces before a tab',
|
||||
source: 'plugins: {custom: keep # note\n \t}',
|
||||
padding: ''
|
||||
},
|
||||
{
|
||||
name: 'deeper sufficient spaces before mixed separation',
|
||||
source: ' plugins: {custom: keep # note\n \t \t}',
|
||||
padding: ''
|
||||
},
|
||||
{
|
||||
name: 'root flow with zero required indentation',
|
||||
source: ' {plugins: {custom: keep # note\n\t}}',
|
||||
padding: ''
|
||||
},
|
||||
{
|
||||
name: 'tab after content on the same line',
|
||||
source: 'plugins: {custom: keep\t}',
|
||||
padding: ''
|
||||
},
|
||||
{
|
||||
name: 'explicit plugins key',
|
||||
source: '? "plugins"\n: {custom: keep # note\n\t}',
|
||||
padding: '\n '
|
||||
},
|
||||
{
|
||||
name: 'deeper split explicit key',
|
||||
source: ' ?\n "plugins"\n : {custom: keep # note\n \t}',
|
||||
padding: '\n '
|
||||
}
|
||||
])('preserves source around $name', ({ source: input, padding }) => {
|
||||
const source = input.replaceAll('\n', eol)
|
||||
const output = expectInsertion(
|
||||
source,
|
||||
{ plugins: { custom: 'keep', enabled: ['orca-status'] } },
|
||||
eol
|
||||
)
|
||||
const insertion = `${padding}, enabled: [ orca-status ]`.replaceAll('\n', eol)
|
||||
expect(output).toBe(source.replace('}', `${insertion}}`))
|
||||
})
|
||||
|
||||
it('rejects an unrelated value change after repairing tab-separated insertion', () => {
|
||||
const source = 'model: "001"\nplugins: {custom: keep # note\n\t}'.replaceAll('\n', eol)
|
||||
expect(
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import type * as RuntimeEnvironmentConnectivityHandlersModule from '../runtime-environments/runtime-environment-connectivity-handlers'
|
||||
import type * as RuntimeEnvironmentManualDisconnectModule from '../runtime-environments/runtime-environment-manual-disconnect'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { GlobalSettings } from '../../../shared/global-settings-types'
|
||||
import type { SshConnectionState, SshTarget } from '../../../shared/ssh-types'
|
||||
|
||||
const { targets, settings } = vi.hoisted(() => {
|
||||
const targets: SshTarget[] = []
|
||||
const settings: { current: Partial<GlobalSettings> } = { current: {} }
|
||||
return { targets, settings }
|
||||
})
|
||||
|
||||
vi.mock('electron', () => ({ webContents: { fromId: () => null } }))
|
||||
vi.mock('../../browser/browser-manager', () => ({ browserManager: {} }))
|
||||
vi.mock('../../runtime/rpc/methods/index', () => ({ ALL_RPC_METHODS: [] }))
|
||||
vi.mock('../ssh/ssh-ipc-context', () => ({
|
||||
persistedStore: { getSettings: () => settings.current }
|
||||
}))
|
||||
vi.mock('../../managed-server/lifecycle/orcad-managed-tunnel', () => ({
|
||||
ensureOrcadManagedTunnel: vi.fn()
|
||||
}))
|
||||
vi.mock('../../ssh/targets/ssh-target-registry', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
getSshTargetRegistryStore: () => ({ listTargets: () => targets })
|
||||
}))
|
||||
vi.mock('../../../shared/managed-orcad-ssh-owner', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
getManagedOrcadFenceEnvironmentId: (target: SshTarget) => `env-${target.id}`
|
||||
}))
|
||||
vi.mock('../runtime-environments/runtime-environment-connectivity-handlers', () => ({
|
||||
isRuntimeEnvironmentManuallyDisconnected: () => false
|
||||
}))
|
||||
|
||||
vi.mock('../runtime-environments/runtime-environment-manual-disconnect', async (importOriginal) => {
|
||||
const facadeMocks = await vi.importMock<typeof RuntimeEnvironmentConnectivityHandlersModule>(
|
||||
'../runtime-environments/runtime-environment-connectivity-handlers'
|
||||
)
|
||||
return {
|
||||
...(await importOriginal<typeof RuntimeEnvironmentManualDisconnectModule>()),
|
||||
isRuntimeEnvironmentManuallyDisconnected: facadeMocks.isRuntimeEnvironmentManuallyDisconnected
|
||||
}
|
||||
})
|
||||
vi.mock(
|
||||
'../../../shared/remote-runtime/host-store/runtime-environment-store',
|
||||
async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
resolveEnvironment: (_userDataPath: string, environmentId: string) => ({ environmentId })
|
||||
})
|
||||
)
|
||||
vi.mock(
|
||||
'../../../shared/remote-runtime/host-catalog/runtime-environments',
|
||||
async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
getOrcadSshDeployment: ({ environmentId }: { environmentId: string }) => ({
|
||||
sshTargetId: environmentId.slice('env-'.length)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
import { listDesktopBrowserBridgeCandidates } from './desktop-browser-bridge-wiring'
|
||||
|
||||
const host = (id: string, extra: Partial<SshTarget> = {}): SshTarget => ({
|
||||
id,
|
||||
label: id,
|
||||
host: `${id}.example.com`,
|
||||
port: 22,
|
||||
username: 'me',
|
||||
...extra
|
||||
})
|
||||
const connected = {
|
||||
getState: (targetId: string): SshConnectionState => ({
|
||||
targetId,
|
||||
status: 'connected',
|
||||
error: null,
|
||||
reconnectAttempt: 0
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
targets.length = 0
|
||||
settings.current = {}
|
||||
})
|
||||
|
||||
describe('which managed hosts may attach the desktop browser bridge', () => {
|
||||
it('attaches a host with no saved switch or an explicit true, never an explicit false', () => {
|
||||
targets.push(
|
||||
host('absent'),
|
||||
host('on', { allowRemoteCliControl: true }),
|
||||
host('off', { allowRemoteCliControl: false })
|
||||
)
|
||||
|
||||
const candidates = listDesktopBrowserBridgeCandidates('/user-data', connected)
|
||||
|
||||
expect(candidates.map((candidate) => candidate.targetId)).toEqual(['absent', 'on'])
|
||||
})
|
||||
|
||||
it('holds back a host whose pages browse from this device', () => {
|
||||
targets.push(host('routed'), host('opted-out'))
|
||||
settings.current = { browserSshWorkspaceRoutingDisabledTargetIds: ['opted-out'] }
|
||||
expect(
|
||||
listDesktopBrowserBridgeCandidates('/user-data', connected).map((c) => c.targetId)
|
||||
).toEqual(['routed'])
|
||||
settings.current = { browserSshWorkspaceRoutingEnabled: false }
|
||||
expect(listDesktopBrowserBridgeCandidates('/user-data', connected)).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -1,69 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getDefaultWorkspaceSession } from '../../../../shared/constants'
|
||||
import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types'
|
||||
import type { Store } from '../../../persistence/loading-store/store'
|
||||
import {
|
||||
_resetClosedPaneFencesForTests,
|
||||
fencedIfLayoutOwner,
|
||||
partitionsFence
|
||||
} from '../../../persistence/terminal-topology/closed-pane-binding-fence'
|
||||
import { refuseClosedPaneSpawn } from './closed-pane-spawn-refusal'
|
||||
|
||||
vi.mock('../../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
|
||||
|
||||
function session(leafIds: string[]): WorkspaceSessionState {
|
||||
const [first, second] = leafIds
|
||||
return {
|
||||
...getDefaultWorkspaceSession(),
|
||||
terminalLayoutsByTabId: {
|
||||
t: {
|
||||
root: second
|
||||
? {
|
||||
type: 'split',
|
||||
direction: 'vertical',
|
||||
first: { type: 'leaf', leafId: first! },
|
||||
second: { type: 'leaf', leafId: second }
|
||||
}
|
||||
: { type: 'leaf', leafId: first! },
|
||||
activeLeafId: first!,
|
||||
expandedLeafId: null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function storeHolding(current: { value: WorkspaceSessionState }): Store {
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: refuseClosedPaneSpawn reads only these two Store members.
|
||||
return {
|
||||
getWorkspaceSessionHostIds: () => ['local'],
|
||||
getWorkspaceSession: () => current.value
|
||||
} as unknown as Store
|
||||
}
|
||||
|
||||
describe('a spawn for a pane main closed while it waited', () => {
|
||||
afterEach(() => _resetClosedPaneFencesForTests())
|
||||
|
||||
it('starts nothing once main took the pane back', () => {
|
||||
const current = { value: session(['a', 'b']) }
|
||||
fencedIfLayoutOwner(
|
||||
() => partitionsFence(['local'], () => current.value, true),
|
||||
() => ((current.value = session(['a'])), { value: null })
|
||||
)()
|
||||
|
||||
expect(() => refuseClosedPaneSpawn(storeHolding(current), 't', 'b')).toThrow(
|
||||
'terminal_pane_closed'
|
||||
)
|
||||
})
|
||||
|
||||
it('starts a pane main never closed, or one it holds again', () => {
|
||||
const current = { value: session(['a', 'b']) }
|
||||
expect(() => refuseClosedPaneSpawn(storeHolding(current), 't', 'b')).not.toThrow()
|
||||
|
||||
fencedIfLayoutOwner(
|
||||
() => partitionsFence(['local'], () => current.value, true),
|
||||
() => ((current.value = session(['a'])), { value: null })
|
||||
)()
|
||||
current.value = session(['a', 'b'])
|
||||
expect(() => refuseClosedPaneSpawn(storeHolding(current), 't', 'b')).not.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -1,113 +0,0 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { getDefaultSettings } from '../../../../shared/constants'
|
||||
import type { TerminalOscColorQueryReplyColors } from '../../../../shared/terminal/protocol/queries/terminal-osc-color-reply'
|
||||
import { LocalPtyProvider } from '../../../providers/local-pty/local-pty-provider'
|
||||
import {
|
||||
_resetTerminalViewAttributesForTest,
|
||||
getTerminalViewerColors,
|
||||
setPairedViewerColors,
|
||||
setTerminalViewAttributes,
|
||||
setTerminalViewerColorsListener
|
||||
} from '../../../runtime/terminal-view-attribute-store'
|
||||
|
||||
import type { TerminalViewAttributes } from '../../../../shared/terminal/protocol/queries/terminal-view-attributes'
|
||||
import {
|
||||
_resetColorQueryReplyColorsForTest,
|
||||
getLocalPtyProvider,
|
||||
setLocalPtyProvider
|
||||
} from '../provider/registry'
|
||||
import { installTerminalViewAttributesIpc } from './terminal-view-attributes-ipc'
|
||||
|
||||
class RecordingProvider extends LocalPtyProvider {
|
||||
readonly pushes: TerminalOscColorQueryReplyColors[] = []
|
||||
|
||||
override setColorQueryReplyColors(colors: TerminalOscColorQueryReplyColors): void {
|
||||
this.pushes.push(colors)
|
||||
}
|
||||
}
|
||||
|
||||
const DESKTOP_ATTRIBUTES: TerminalViewAttributes = {
|
||||
foreground: [0, 0, 0],
|
||||
background: [0x12, 0x34, 0x56],
|
||||
cursor: [0, 0, 0],
|
||||
ansi: Array.from({ length: 256 }, (): [number, number, number] => [0, 0, 0]),
|
||||
colorSchemeMode: 'light',
|
||||
cursorStyle: 'block',
|
||||
cursorBlink: false
|
||||
}
|
||||
const DESKTOP = { foreground: '#000000', background: '#123456' }
|
||||
const CLIENT = { foreground: '#2e3434', background: '#ffffff' }
|
||||
|
||||
describe('seeding PTY owner colours from saved settings', () => {
|
||||
const originalLocal = getLocalPtyProvider()
|
||||
const settings = getDefaultSettings('/tmp')
|
||||
|
||||
afterEach(() => {
|
||||
_resetTerminalViewAttributesForTest()
|
||||
_resetColorQueryReplyColorsForTest()
|
||||
setLocalPtyProvider(originalLocal)
|
||||
})
|
||||
|
||||
it('seeds a host with no display from its saved dark theme', () => {
|
||||
const owner = new RecordingProvider()
|
||||
setLocalPtyProvider(owner)
|
||||
|
||||
installTerminalViewAttributesIpc({ getSettings: () => ({ ...settings, theme: 'dark' }) })
|
||||
|
||||
expect(owner.pushes).toEqual([{ foreground: '#ffffff', background: '#282c34' }])
|
||||
})
|
||||
|
||||
it('publishes colours a renderer already pushed instead of the saved theme', () => {
|
||||
setTerminalViewAttributes(DESKTOP_ATTRIBUTES)
|
||||
const owner = new RecordingProvider()
|
||||
setLocalPtyProvider(owner)
|
||||
|
||||
installTerminalViewAttributesIpc({ getSettings: () => settings })
|
||||
|
||||
expect(owner.pushes).toEqual([DESKTOP])
|
||||
})
|
||||
})
|
||||
|
||||
describe('one viewer colour value for every PTY owner', () => {
|
||||
const originalLocal = getLocalPtyProvider()
|
||||
const settings = getDefaultSettings('/tmp')
|
||||
const SEED = { foreground: '#ffffff', background: '#282c34' }
|
||||
|
||||
afterEach(() => {
|
||||
_resetTerminalViewAttributesForTest()
|
||||
_resetColorQueryReplyColorsForTest()
|
||||
setLocalPtyProvider(originalLocal)
|
||||
})
|
||||
|
||||
function install(): RecordingProvider {
|
||||
const owner = new RecordingProvider()
|
||||
setLocalPtyProvider(owner)
|
||||
installTerminalViewAttributesIpc({ getSettings: () => ({ ...settings, theme: 'dark' }) })
|
||||
return owner
|
||||
}
|
||||
|
||||
it("switches to this host's window theme once its renderer pushes", () => {
|
||||
const owner = install()
|
||||
setPairedViewerColors(CLIENT)
|
||||
|
||||
setTerminalViewAttributes(DESKTOP_ATTRIBUTES)
|
||||
setPairedViewerColors({ foreground: '#111111', background: '#eeeeee' })
|
||||
|
||||
expect(getTerminalViewerColors()).toEqual(DESKTOP)
|
||||
expect(owner.pushes).toEqual([SEED, CLIENT, DESKTOP])
|
||||
})
|
||||
|
||||
it('notifies once per change of the answered colours', () => {
|
||||
install()
|
||||
const published: unknown[] = []
|
||||
setTerminalViewerColorsListener((colors) => published.push(colors))
|
||||
|
||||
setPairedViewerColors(CLIENT)
|
||||
setPairedViewerColors({ ...CLIENT })
|
||||
setTerminalViewAttributes(DESKTOP_ATTRIBUTES)
|
||||
setTerminalViewAttributes({ ...DESKTOP_ATTRIBUTES, cursorBlink: true })
|
||||
setPairedViewerColors({ foreground: '#111111', background: '#eeeeee' })
|
||||
|
||||
expect(published).toEqual([CLIENT, DESKTOP])
|
||||
})
|
||||
})
|
||||
@@ -3,12 +3,7 @@ import { TerminalRunFactsRegister } from '../../../runtime/terminal-run-facts'
|
||||
import { ptyOwnership } from '../provider/ownership-state'
|
||||
import { createPtyWriteInput } from './write-input'
|
||||
|
||||
import {
|
||||
WRITE_ACCEPTED,
|
||||
writeRefused,
|
||||
writeUnverifiable,
|
||||
type WriteSettlement
|
||||
} from '../../../../shared/terminal/pty/lifecycle/pty-write-settlement'
|
||||
import type { WriteSettlement } from '../../../../shared/terminal/pty/lifecycle/pty-write-settlement'
|
||||
|
||||
const PTY_ID = 'pty-user-input'
|
||||
|
||||
@@ -52,41 +47,6 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('renderer PTY writes: input kind', () => {
|
||||
it.each(['local', 'ssh:ssh-connection'] as const)(
|
||||
'observes a settled write only after acceptance on the execution host (%s)',
|
||||
async (owner) => {
|
||||
ptyOwnership.set(PTY_ID, owner)
|
||||
const observe = vi.fn()
|
||||
const input = createWriteInput(new TerminalRunFactsRegister(), observe)
|
||||
let finish: (settlement: WriteSettlement) => void = () => {}
|
||||
provider.writeWithSettlement.mockReturnValueOnce(
|
||||
new Promise((resolve) => {
|
||||
finish = resolve
|
||||
})
|
||||
)
|
||||
const args = {
|
||||
id: PTY_ID,
|
||||
data: '\x1b[27u',
|
||||
inputKind: 'driving' as const,
|
||||
requireWriteSettlement: true as const
|
||||
}
|
||||
const pending = input.writePtyInputAccepted(args)
|
||||
expect(observe).not.toHaveBeenCalled()
|
||||
finish(WRITE_ACCEPTED)
|
||||
expect(await pending).toBe(true)
|
||||
expect(observe).toHaveBeenCalledTimes(owner === 'local' ? 1 : 0)
|
||||
observe.mockClear()
|
||||
provider.writeWithSettlement.mockReturnValueOnce(writeRefused('endpoint_disconnected'))
|
||||
expect(await input.writePtyInputAccepted(args)).toBe(false)
|
||||
provider.writeWithSettlement.mockResolvedValueOnce(
|
||||
writeUnverifiable('transport_settlement_lost', true)
|
||||
)
|
||||
await expect(input.writePtyInputAccepted(args)).rejects.toThrow('acknowledgment unavailable')
|
||||
expect(observe).not.toHaveBeenCalled()
|
||||
expect(provider.write).not.toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['writePtyInput', 'writePtyInputAccepted'] as const)(
|
||||
'%s records driving input before the provider write',
|
||||
async (writer) => {
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import type * as FsPromises from 'node:fs/promises'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { spawnMock } from './pty-ipc-mock-registry'
|
||||
|
||||
import { setupPtyIpcSuite } from './pty-ipc-test-harness'
|
||||
import { registerPtyHandlers } from './register-handlers'
|
||||
|
||||
type DirectoryStats = { isDirectory(): boolean }
|
||||
|
||||
const statMock = vi.hoisted(() => vi.fn<(path: string) => Promise<DirectoryStats>>())
|
||||
|
||||
vi.mock('node:fs/promises', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof FsPromises>()),
|
||||
stat: statMock
|
||||
}))
|
||||
vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock()))
|
||||
vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock()))
|
||||
vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock()))
|
||||
vi.mock('node:child_process', async (importOriginal) =>
|
||||
(await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal())
|
||||
)
|
||||
vi.mock('../../opencode/hook-service', () =>
|
||||
import('./pty-ipc-mock-registry').then((m) => m.openCodeHookServiceModuleMock())
|
||||
)
|
||||
vi.mock('../../mimo/hook-service', () =>
|
||||
import('./pty-ipc-mock-registry').then((m) => m.mimoHookServiceModuleMock())
|
||||
)
|
||||
vi.mock('../../agent-hooks/server', () =>
|
||||
import('./pty-ipc-mock-registry').then((m) => m.agentHookServerModuleMock())
|
||||
)
|
||||
vi.mock('../../pi/titlebar-extension-service', () =>
|
||||
import('./pty-ipc-mock-registry').then((m) => m.piTitlebarExtensionModuleMock())
|
||||
)
|
||||
vi.mock('../../pty/shell-startup/pwsh', () =>
|
||||
import('./pty-ipc-mock-registry').then((m) => m.pwshModuleMock())
|
||||
)
|
||||
vi.mock('../../wsl/filesystem/unc-path-access', async (importOriginal) =>
|
||||
(await import('./pty-ipc-mock-registry')).wslModuleMock(await importOriginal())
|
||||
)
|
||||
vi.mock('../../telemetry/client', () =>
|
||||
import('./pty-ipc-mock-registry').then((m) => m.telemetryClientModuleMock())
|
||||
)
|
||||
vi.mock('../../telemetry/classify-error', () =>
|
||||
import('./pty-ipc-mock-registry').then((m) => m.classifyErrorModuleMock())
|
||||
)
|
||||
vi.mock('../../cli/linux-terminal-orca-cli-shim', () =>
|
||||
import('./pty-ipc-mock-registry').then((m) => m.linuxCliShimModuleMock())
|
||||
)
|
||||
vi.mock('../../memory/pty-registry', () =>
|
||||
import('./pty-ipc-mock-registry').then((m) => m.ptyRegistryModuleMock())
|
||||
)
|
||||
vi.mock('../../agent-hooks/migration-unsupported-pty-state', () =>
|
||||
import('./pty-ipc-mock-registry').then((m) => m.migrationUnsupportedPtyModuleMock())
|
||||
)
|
||||
vi.mock('../../codex/configuration/homes/codex-pane-account-registry', () =>
|
||||
import('./pty-ipc-mock-registry').then((m) => m.codexPaneAccountRegistryModuleMock())
|
||||
)
|
||||
vi.mock('../../codex/history/index/codex-state-db-backfill-recovery', () =>
|
||||
import('./pty-ipc-mock-registry').then((m) => m.codexBackfillRecoveryModuleMock())
|
||||
)
|
||||
|
||||
const WORKTREE = '/Volumes/nas/repo'
|
||||
const directoryStats = { isDirectory: () => true }
|
||||
|
||||
function spawnWithWorktreeFallback(cwd: string): Promise<unknown> {
|
||||
return Promise.resolve(
|
||||
handlers.get('pty:spawn')!(null, {
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
cwd,
|
||||
cwdFallback: 'worktree',
|
||||
worktreeId: `repo-1::${WORKTREE}`
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const { handlers, mainWindow } = setupPtyIpcSuite()
|
||||
|
||||
beforeEach(() => {
|
||||
statMock.mockReset()
|
||||
// Provider validation uses sync stat; folder-workspace validation uses async stat.
|
||||
statMock.mockImplementation(async () => directoryStats)
|
||||
})
|
||||
|
||||
describe('pty:spawn with the worktree-folder fallback on a mount that stopped answering', () => {
|
||||
it('never asks the worktree folder once the saved folder answers', async () => {
|
||||
const outsideCwd = '/Users/alice/scratch'
|
||||
statMock.mockImplementation(async (path) =>
|
||||
path === outsideCwd ? directoryStats : new Promise<DirectoryStats>(() => {})
|
||||
)
|
||||
registerPtyHandlers(mainWindow as never)
|
||||
|
||||
await spawnWithWorktreeFallback(outsideCwd)
|
||||
|
||||
expect(statMock.mock.calls.map(([path]) => path)).toEqual([outsideCwd])
|
||||
const [, , options] = spawnMock.mock.calls.at(-1) as [string, string[], { cwd: string }]
|
||||
expect(options.cwd).toBe(outsideCwd)
|
||||
})
|
||||
})
|
||||
@@ -67,25 +67,4 @@ describe('runtime environment removal storage clearing', () => {
|
||||
finishTeardown()
|
||||
await vi.waitFor(() => expect(clearStorageMock).toHaveBeenCalledWith('environment-a'))
|
||||
})
|
||||
|
||||
it('retries a partition the first pass refused as live', async () => {
|
||||
clearStorageMock
|
||||
.mockResolvedValueOnce({ clearedPartitions: [], livePartitions: ['persist:one'] })
|
||||
.mockResolvedValueOnce({ clearedPartitions: ['persist:one'], livePartitions: [] })
|
||||
registerRuntimeEnvironmentConnectivityHandlers({
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: remove reads only settings and the session-host prune.
|
||||
store: {
|
||||
getSettings: () => ({}),
|
||||
removeWorkspaceSessionHost: vi.fn(),
|
||||
setRemoteLayoutSnapshot: vi.fn(),
|
||||
setHostRepoCatalogSnapshot: vi.fn()
|
||||
} as never,
|
||||
getUserDataPath: () => '/tmp/orca-user-data',
|
||||
invalidateTransport: () => Promise.resolve()
|
||||
})
|
||||
|
||||
removeHandler()(null, { selector: 'environment-a' })
|
||||
|
||||
await vi.waitFor(() => expect(clearStorageMock).toHaveBeenCalledTimes(2), { timeout: 2_000 })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import type * as ClientModule from '../../github/client'
|
||||
import type * as GetPrForBranchModule from '../../github/client/lookup/get-pr-for-branch'
|
||||
import { EventEmitter } from 'node:events'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { PRRefreshOutcome } from '../../../shared/github/pull-request-refresh-types'
|
||||
|
||||
const { handlers, lookup, contents } = vi.hoisted(() => ({
|
||||
handlers: new Map<string, (...args: unknown[]) => unknown>(),
|
||||
lookup: vi.fn(),
|
||||
contents: new Map<number, EventEmitter & { id: number; isDestroyed: () => boolean }>()
|
||||
}))
|
||||
vi.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
handle: (channel: string, handler: (...args: unknown[]) => unknown) =>
|
||||
handlers.set(channel, handler)
|
||||
},
|
||||
webContents: { fromId: (id: number) => contents.get(id) }
|
||||
}))
|
||||
vi.mock('../../github/client', () => ({ getPRForBranch: vi.fn(), getPRForBranchOutcome: lookup }))
|
||||
|
||||
vi.mock('../../github/client/lookup/get-pr-for-branch', async (importOriginal) => {
|
||||
const facadeMocks = await vi.importMock<typeof ClientModule>('../../github/client')
|
||||
return {
|
||||
...(await importOriginal<typeof GetPrForBranchModule>()),
|
||||
getPRForBranch: facadeMocks.getPRForBranch
|
||||
}
|
||||
})
|
||||
vi.mock('../../github/pr-refresh-rate-limit-gate', () => ({
|
||||
prRefreshRateLimitPausedUntil: async () => null
|
||||
}))
|
||||
vi.mock('../../wsl/filesystem/wsl-unc-access-gate', () => ({
|
||||
wslDistroForPath: () => null,
|
||||
blockedWslDistroForPath: async () => null
|
||||
}))
|
||||
vi.mock('../../crash-reporting/crash-breadcrumb-store', () => ({
|
||||
recordCoalescedCrashBreadcrumb: () => {}
|
||||
}))
|
||||
vi.mock('../ui', () => ({ sendToTrustedUIRenderer: () => {} }))
|
||||
vi.mock('./github-repo-routing', () => ({
|
||||
validateAutomaticPRRefreshCandidate: (candidate: unknown) => ({ kind: 'ok', candidate }),
|
||||
isPRRefreshCandidateRegistered: () => true
|
||||
}))
|
||||
|
||||
import { makeCandidate, makePR } from '../../github/pr-refresh-coordinator-test-harness'
|
||||
|
||||
function sender(id: number) {
|
||||
return Object.assign(new EventEmitter(), { id, isDestroyed: () => false })
|
||||
}
|
||||
function found(): PRRefreshOutcome {
|
||||
return { kind: 'found', pr: makePR({ checksStatus: 'pending' }), fetchedAt: Date.now() }
|
||||
}
|
||||
async function setup() {
|
||||
const { registerGitHubPRRefreshHandlers } = await import('./github-pr-refresh-handlers')
|
||||
const coordinator = await import('../../github/pr-refresh-coordinator')
|
||||
const owner = sender(17)
|
||||
const other = sender(18)
|
||||
contents.set(owner.id, owner)
|
||||
contents.set(other.id, other)
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Visible refresh registration reads only this store fixture's repo list.
|
||||
const store = { getRepos: () => [] } as never
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Every PR is already counted, so the observer reads only this stats method.
|
||||
const stats = { hasCountedPR: () => true } as never
|
||||
registerGitHubPRRefreshHandlers(store, stats)
|
||||
const report = (target = owner) =>
|
||||
handlers.get('gh:reportVisiblePRRefreshCandidates')!(
|
||||
{ sender: target },
|
||||
{ candidates: [makeCandidate()], generation: Date.now() }
|
||||
)
|
||||
return { owner, other, report, coordinator }
|
||||
}
|
||||
|
||||
describe('automatic PR refresh document ownership', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(1_700_000_000_000)
|
||||
handlers.clear()
|
||||
contents.clear()
|
||||
lookup.mockReset().mockImplementation(async () => found())
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.clearAllTimers()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('does not requeue a late lookup after document retirement', async () => {
|
||||
const pending = Promise.withResolvers<PRRefreshOutcome>()
|
||||
lookup.mockImplementationOnce(() => pending.promise)
|
||||
const { owner, report, coordinator } = await setup()
|
||||
report()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
owner.emit('render-process-gone')
|
||||
pending.resolve(found())
|
||||
await vi.advanceTimersByTimeAsync(360_000)
|
||||
expect(lookup).toHaveBeenCalledTimes(1)
|
||||
expect(coordinator._getVisiblePRRefreshWindowCountForTests()).toBe(0)
|
||||
expect(coordinator._getPRRefreshQueueSizeForTests()).toBe(0)
|
||||
})
|
||||
})
|
||||
@@ -1,68 +0,0 @@
|
||||
import { beforeEach, expect, it, vi } from 'vitest'
|
||||
import type { FolderWorkspace } from '../../shared/folder-workspaces/folder-workspace-types'
|
||||
import type { SessionInfo } from '../daemon/types'
|
||||
import type { Store } from '../persistence/loading-store/store'
|
||||
|
||||
const getDaemonProviderMock = vi.fn()
|
||||
vi.mock('../daemon/daemon-provider-state', () => ({
|
||||
getDaemonProvider: () => getDaemonProviderMock()
|
||||
}))
|
||||
vi.mock('../workspaces/execution-host/project-runtime-git-options', () => ({
|
||||
getLocalProjectWorktreeGitOptions: () => ({})
|
||||
}))
|
||||
const listLocalRepoWorktreesStrictMock = vi.fn()
|
||||
vi.mock('../workspaces/catalog/repo-worktrees', () => ({
|
||||
listLocalRepoWorktreesStrict: (...args: unknown[]) => listLocalRepoWorktreesStrictMock(...args)
|
||||
}))
|
||||
|
||||
function makeStore(folderWorkspaces: FolderWorkspace[]): Store {
|
||||
const store: Partial<Store> = {
|
||||
getRepos: () => [],
|
||||
getFolderWorkspaces: () => folderWorkspaces,
|
||||
getProjectGroups: () => [],
|
||||
getAllWorktreeMeta: () => ({}),
|
||||
getAllWorktreeMetaForHost: () => ({})
|
||||
}
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: hydration reads only the store members stubbed here.
|
||||
return store as Store
|
||||
}
|
||||
|
||||
// Reset both memoized hydration and the module-scoped registry.
|
||||
async function loadFresh() {
|
||||
vi.resetModules()
|
||||
const hydrateMod = await import('./hydrate-local-pty-registry')
|
||||
const registryMod = await import('./pty-registry')
|
||||
return {
|
||||
hydrate: hydrateMod.hydrateLocalPtyRegistryAtBoot,
|
||||
listRegisteredPtys: registryMod.listRegisteredPtys
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
getDaemonProviderMock.mockReset()
|
||||
listLocalRepoWorktreesStrictMock.mockReset()
|
||||
})
|
||||
|
||||
it.each(['deleted', 'remote', 'ssh'] as const)(
|
||||
'rechecks folder ownership after inventory when the catalog becomes %s',
|
||||
async (change) => {
|
||||
const { hydrate, listRegisteredPtys } = await loadFresh()
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: ownership reads only id and executionHostId.
|
||||
const folders = [{ id: 'folder-1', executionHostId: 'local' } as FolderWorkspace]
|
||||
getDaemonProviderMock.mockReturnValue({
|
||||
listSessions: vi.fn().mockImplementation(async (): Promise<Partial<SessionInfo>[]> => {
|
||||
if (change === 'deleted') {
|
||||
folders.splice(0)
|
||||
} else {
|
||||
folders[0].executionHostId = change === 'ssh' ? 'ssh:target-1' : 'runtime:environment-1'
|
||||
}
|
||||
return [{ sessionId: 'folder:folder-1@@cafebabe', pid: 4242 }]
|
||||
})
|
||||
})
|
||||
|
||||
await hydrate(makeStore(folders))
|
||||
|
||||
expect(listRegisteredPtys()).toEqual([])
|
||||
expect(listLocalRepoWorktreesStrictMock).not.toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
@@ -1,71 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { FolderWorkspace } from '../../shared/folder-workspaces/folder-workspace-types'
|
||||
import type { MemorySnapshotStore } from './collector'
|
||||
import { resolveWorktreeMemoryNames } from './memory-snapshot-buckets'
|
||||
|
||||
const folder: FolderWorkspace = {
|
||||
id: 'notes-folder',
|
||||
projectGroupId: 'documentation',
|
||||
name: 'Release notes',
|
||||
folderPath: '/notes',
|
||||
linkedTask: null,
|
||||
comment: '',
|
||||
isArchived: false,
|
||||
isUnread: false,
|
||||
isPinned: false,
|
||||
sortOrder: 0,
|
||||
lastActivityAt: 0,
|
||||
createdAt: 0,
|
||||
updatedAt: 0
|
||||
}
|
||||
const group = { id: 'documentation', name: 'Documentation' }
|
||||
|
||||
function makeStore(): MemorySnapshotStore &
|
||||
Required<Pick<MemorySnapshotStore, 'getFolderWorkspace' | 'getProjectGroups'>> {
|
||||
return {
|
||||
getRepo: vi.fn(),
|
||||
getWorktreeMeta: vi.fn(),
|
||||
getFolderWorkspace: vi.fn((id) => (id === folder.id ? folder : undefined)),
|
||||
getProjectGroups: vi.fn(() => [group])
|
||||
}
|
||||
}
|
||||
|
||||
describe('memory snapshot workspace names', () => {
|
||||
it('resolves a folder workspace and its project group from persisted metadata', () => {
|
||||
const store = makeStore()
|
||||
expect(resolveWorktreeMemoryNames('folder:notes-folder', store)).toEqual({
|
||||
worktreeName: 'Release notes',
|
||||
repoId: 'folder-workspace:documentation',
|
||||
repoName: 'Documentation'
|
||||
})
|
||||
expect(store.getRepo).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reads updated folder and project-group names on the next snapshot', () => {
|
||||
const store = makeStore()
|
||||
resolveWorktreeMemoryNames('folder:notes-folder', store)
|
||||
vi.mocked(store.getFolderWorkspace).mockReturnValue({ ...folder, name: 'Changelog' })
|
||||
vi.mocked(store.getProjectGroups).mockReturnValue([{ ...group, name: 'Docs' }])
|
||||
expect(resolveWorktreeMemoryNames('folder:notes-folder', store)).toMatchObject({
|
||||
worktreeName: 'Changelog',
|
||||
repoName: 'Docs'
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps a readable folder label if its project-group metadata is missing', () => {
|
||||
const store = makeStore()
|
||||
vi.mocked(store.getProjectGroups).mockReturnValue([])
|
||||
expect(resolveWorktreeMemoryNames('folder:notes-folder', store)).toMatchObject({
|
||||
worktreeName: 'Release notes',
|
||||
repoName: 'Release notes'
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps unknown folder ids identifiable without assigning them to a different folder', () => {
|
||||
expect(resolveWorktreeMemoryNames('folder:missing', makeStore())).toEqual({
|
||||
worktreeName: 'folder:missing',
|
||||
repoId: 'folder:missing',
|
||||
repoName: 'folder:missing'
|
||||
})
|
||||
})
|
||||
})
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
import type { createFixture } from './journal-atomic-settlement-contract-test-fixture'
|
||||
import { expect, it } from 'vitest'
|
||||
import {
|
||||
AGENT_JOURNAL_THREAD_SCOPE,
|
||||
type AgentJournalSnapshot
|
||||
} from '../../../shared/agent-session/journal/agent-session-journal-types'
|
||||
import { settleObservedExitForTest } from '../agent-session-wire/structured-agent-session-observed-exit.test-fixture'
|
||||
import { openTestJournalHostDatabase } from './journal-host-database-test-support'
|
||||
import { MAX_JOURNAL_LIFECYCLE_BATCH_BYTES } from './journal-row-schema'
|
||||
|
||||
export function registerJournalAtomicSettlementOversizedTests(
|
||||
fixture: ReturnType<typeof createFixture>
|
||||
): void {
|
||||
it.each(['startup', 'stop'] as const)(
|
||||
'rolls back the %s submission verdict with its oversized item',
|
||||
async (mode) => {
|
||||
await fixture.journal.appendSubmission({
|
||||
clientMessageId: 'unanswered',
|
||||
payloadFingerprint: 'unanswered',
|
||||
fence: 7,
|
||||
handoverRecorded: true,
|
||||
body: { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'next' }] }
|
||||
})
|
||||
await fixture.journal.resolveDispatch({
|
||||
clientMessageId: 'unanswered',
|
||||
state: 'pending',
|
||||
fence: 7,
|
||||
turnScope: AGENT_JOURNAL_THREAD_SCOPE
|
||||
})
|
||||
if (mode === 'stop') {
|
||||
await fixture.journal.appendStopEvent({ reason: 'user-stop' }, 7)
|
||||
}
|
||||
await fixture.journal.appendItem(
|
||||
{ provider: 'orca', clientMessageId: 'oversized-call' },
|
||||
{
|
||||
kind: 'tool-call',
|
||||
name: 'Write',
|
||||
input: 'x'.repeat(MAX_JOURNAL_LIFECYCLE_BATCH_BYTES),
|
||||
state: 'running'
|
||||
},
|
||||
{ fence: 7, turnScope: AGENT_JOURNAL_THREAD_SCOPE }
|
||||
)
|
||||
const input = {
|
||||
journal: fixture.journal,
|
||||
sessionId: fixture.SESSION,
|
||||
fence: 7,
|
||||
settlementId: `${mode}:7`,
|
||||
verdict: { state: 'interrupted' as const, completedAt: 1_000 },
|
||||
pendingSubmissionReason: 'owner exited',
|
||||
showUnexpectedExitOutcome: mode === 'startup',
|
||||
...(mode === 'startup' ? { exitedDuringStartup: { generation: 'start-7' } } : {})
|
||||
}
|
||||
const database = openTestJournalHostDatabase(fixture.root)
|
||||
const before = fixture.journal.snapshot()
|
||||
const diskBefore = fixture.rows()
|
||||
const published: AgentJournalSnapshot[] = []
|
||||
fixture.journal.observeCommits(() => published.push(fixture.journal.snapshot()))
|
||||
database.db.exec(`CREATE TEMP TRIGGER reject_submission_settlement_item
|
||||
BEFORE INSERT ON journal_rows
|
||||
WHEN NEW.session_id = '${fixture.SESSION}' AND json_extract(NEW.row_json, '$.recovered') = 1
|
||||
AND json_extract(NEW.row_json, '$.kind') = 'item'
|
||||
BEGIN SELECT RAISE(ABORT, 'item rejected after submission'); END`)
|
||||
expect(await settleObservedExitForTest(input)).toMatchObject({ ok: false })
|
||||
expect(fixture.journal.snapshot()).toEqual(before)
|
||||
expect(fixture.rows()).toEqual(diskBefore)
|
||||
expect(published).toEqual([])
|
||||
database.db.exec('DROP TRIGGER reject_submission_settlement_item')
|
||||
expect(await settleObservedExitForTest(input)).toMatchObject({ ok: true })
|
||||
expect(fixture.journal.submission('unanswered')).toMatchObject({
|
||||
dispatchState: mode === 'stop' ? 'unknown' : 'rejected',
|
||||
recovered: true,
|
||||
...(mode === 'stop' ? {} : { rejection: { kind: 'providerStartFailed' } })
|
||||
})
|
||||
const committed = fixture.rows().slice(diskBefore.length)
|
||||
expect(committed.filter((row) => row.kind === 'dispatch')).toHaveLength(1)
|
||||
expect(published).toEqual([fixture.journal.snapshot()])
|
||||
expect(await settleObservedExitForTest(input)).toMatchObject({ ok: true })
|
||||
expect(fixture.rows().slice(diskBefore.length)).toEqual(committed)
|
||||
expect(published).toHaveLength(1)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import { createFixture } from './journal-atomic-settlement-contract-test-fixture'
|
||||
import { registerJournalAtomicSettlementRollbackTests } from './journal-atomic-settlement-rollback.spec'
|
||||
import { registerJournalAtomicSettlementOversizedTests } from './journal-atomic-settlement-oversized.spec'
|
||||
|
||||
export function registerJournalAtomicTerminalSettlementTests(): void {
|
||||
const fixture = createFixture()
|
||||
registerJournalAtomicSettlementRollbackTests(fixture)
|
||||
registerJournalAtomicSettlementOversizedTests(fixture)
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
// Rows frames must restate current work or clients interpret them as legacy-host frames.
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { agentJournalItemKey } from '../../../shared/agent-session/journal/agent-session-journal-item-key'
|
||||
import type { AgentJournalRenderItem } from '../../../shared/agent-session/journal/agent-session-journal-types'
|
||||
import type { AgentSessionSubscribeEvent } from '../../../shared/agent-session/agent-session-wire'
|
||||
import type { AgentSessionJournal } from '../agent-session-journal/journal-store'
|
||||
import { refreshDerivedStopNotes } from './agent-session-stop-note-refresh'
|
||||
import { structuredAgentSessionStopNoteIdentity } from './structured-agent-session-command-turn'
|
||||
import { StructuredAgentSessionCurrentWork } from './structured-agent-session-current-work'
|
||||
import type { Subscriber } from './structured-agent-session-subscribers'
|
||||
|
||||
const staleTurn: AgentJournalRenderItem = {
|
||||
itemId: 'turn-old',
|
||||
revision: 1,
|
||||
sequence: 1,
|
||||
observedAt: 1,
|
||||
body: { kind: 'turn', turnId: 't-old', state: 'running', startedAt: 1 }
|
||||
}
|
||||
const stalePrompt: AgentJournalRenderItem = {
|
||||
itemId: 'prompt-old',
|
||||
revision: 1,
|
||||
sequence: 2,
|
||||
observedAt: 2,
|
||||
body: {
|
||||
kind: 'approval',
|
||||
title: 'Run ls',
|
||||
detail: null,
|
||||
options: [{ id: 'allow', label: 'Allow' }],
|
||||
resolution: { state: 'pending', selectedOptionId: null, resolvedBy: null, resolvedAt: null }
|
||||
}
|
||||
}
|
||||
const noteId = agentJournalItemKey(structuredAgentSessionStopNoteIdentity('t-old'))
|
||||
const unconfirmed = {
|
||||
kind: 'status' as const,
|
||||
text: 'x',
|
||||
failure: { kind: 'cancelUnconfirmed' as const }
|
||||
}
|
||||
const projectedNote: AgentJournalRenderItem = {
|
||||
itemId: noteId,
|
||||
revision: 2,
|
||||
sequence: 3,
|
||||
observedAt: 3,
|
||||
body: { kind: 'status', text: 'Stopped.' }
|
||||
}
|
||||
const items = [staleTurn, stalePrompt, projectedNote]
|
||||
|
||||
describe('the stop-note refresh frame', () => {
|
||||
it("restates the host's current work", () => {
|
||||
const emitted: AgentSessionSubscribeEvent[] = []
|
||||
const journal = {
|
||||
visitItems: (visit: (itemId: string, sequence: number, body: unknown) => void) =>
|
||||
visit(noteId, 3, unconfirmed),
|
||||
itemsRead: () => ({
|
||||
sessionId: 's',
|
||||
cursor: { epoch: 'e', sequence: 3 },
|
||||
items,
|
||||
submissions: []
|
||||
}),
|
||||
itemBody: (itemId: string) => (itemId === noteId ? unconfirmed : null)
|
||||
}
|
||||
const work = new StructuredAgentSessionCurrentWork(
|
||||
{
|
||||
cursor: () => ({ epoch: 'e', sequence: 3 }),
|
||||
item: (id) => items.find((item) => item.itemId === id) ?? null,
|
||||
runningTurn: () => ({ item: staleTurn, turnId: 't-old' }),
|
||||
itemFence: () => 1,
|
||||
visitItems: (visit) => {
|
||||
for (const item of items) {
|
||||
visit(item.itemId, item.sequence, item.body)
|
||||
}
|
||||
},
|
||||
submissions: () => [],
|
||||
inputOrder: { recoverableSubmissions: () => [] },
|
||||
wroteBeforeOpen: () => false
|
||||
},
|
||||
2
|
||||
)
|
||||
const subscriber = { sessionId: 's', cursor: { epoch: 'e', sequence: 3 }, fence: 2 }
|
||||
refreshDerivedStopNotes(
|
||||
{ emit: (_subscriber, event) => emitted.push(event), isActive: () => true },
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the refresh reads only the subscriber's session, cursor and fence.
|
||||
subscriber as unknown as Subscriber,
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the refresh reads only visitItems, snapshot and itemBody, all supplied.
|
||||
journal as unknown as AgentSessionJournal,
|
||||
10,
|
||||
work
|
||||
)
|
||||
|
||||
expect(emitted).toHaveLength(1)
|
||||
const frame = emitted[0]
|
||||
if (frame?.type !== 'batch') {
|
||||
throw new Error('expected a batch')
|
||||
}
|
||||
expect(frame.batch.items.length).toBeGreaterThan(0)
|
||||
expect(frame).toMatchObject({ working: false, actionablePromptIds: [], latestTurn: null })
|
||||
})
|
||||
})
|
||||
-96
@@ -1,96 +0,0 @@
|
||||
import { afterEach, beforeEach, expect, it } from 'vitest'
|
||||
import { AgentCliInstallationError } from '../agent-cli-installation-error'
|
||||
import { isStructuredAgentSessionStartFailureRow } from '../../../shared/agent-session/failures/structured-agent-session-start-failure-row-key'
|
||||
import { projectQueuedMessageCards } from '../../../shared/agent-session/view/structured-agent-session-queued-cards'
|
||||
import { HOST_TEST_SESSION, hostTestMessage } from './structured-agent-session-host-test-data'
|
||||
import {
|
||||
createQueuedMessageTestRig,
|
||||
eventually,
|
||||
type QueuedMessageTestRig
|
||||
} from './structured-agent-session-queued-message-rig.test-fixture'
|
||||
|
||||
let rig: QueuedMessageTestRig
|
||||
beforeEach(async () => {
|
||||
rig = await createQueuedMessageTestRig({
|
||||
restartable: true,
|
||||
idleSweep: { idleMs: () => 0, intervalMs: 60 * 60 * 1000 }
|
||||
})
|
||||
})
|
||||
afterEach(() => rig.dispose())
|
||||
|
||||
async function retainedCards() {
|
||||
const result = await rig.host.history({ sessionId: HOST_TEST_SESSION, direction: 'tail' })
|
||||
if (!result.ok) {
|
||||
throw new Error('History refused')
|
||||
}
|
||||
const { page } = result
|
||||
const cards = projectQueuedMessageCards(page.queuedMessages, page.submissions, {
|
||||
queuePaused: Boolean(page.queuePause)
|
||||
})
|
||||
return { page, cards }
|
||||
}
|
||||
|
||||
async function retainedDraft() {
|
||||
const working = await rig.workingSend()
|
||||
const queued = await rig.send('retained instruction', 'queue-if-active').result
|
||||
if (!queued.ok || !('queued' in queued.value)) {
|
||||
throw new Error('Expected a queued draft')
|
||||
}
|
||||
await rig.stop()
|
||||
await rig.settleAccepted(working, 'earlier-turn')
|
||||
await rig.host.collaboratorsForTests().lifetime.idleSweep.tick()
|
||||
rig.starts.mockImplementationOnce(() => {
|
||||
throw new AgentCliInstallationError({
|
||||
agent: 'codex',
|
||||
installedVersion: '0.135.0',
|
||||
minimumVersion: '0.136.0'
|
||||
})
|
||||
})
|
||||
return queued.value.queued.messageId
|
||||
}
|
||||
|
||||
it.each(['returned', 'held'] as const)(
|
||||
'uses per-message Send after an existing conversation restart is version-refused with a %s draft and no pause',
|
||||
async (kind) => {
|
||||
let id = await retainedDraft()
|
||||
expect(await rig.resume()).toMatchObject({ ok: true, value: { resumed: true } })
|
||||
await eventually(async () => expect((await rig.handoff(id))?.dispatchState).toBe('rejected'))
|
||||
const rejected = await rig.handoffId(id)
|
||||
if (kind === 'held') {
|
||||
await rig.deleteQueued(id)
|
||||
id = 'held-draft'
|
||||
const journal = rig.host.collaboratorsForTests().sessions.get(HOST_TEST_SESSION)?.journal
|
||||
if (!journal) {
|
||||
throw new Error('Missing journal')
|
||||
}
|
||||
await journal.queuedMessages.insert({
|
||||
messageId: id,
|
||||
body: hostTestMessage('retained instruction'),
|
||||
holdReason: 'send_failed'
|
||||
})
|
||||
}
|
||||
const { page, cards } = await retainedCards()
|
||||
expect(page.queuePause ?? null).toBeNull()
|
||||
expect(cards).toMatchObject([{ messageId: id, hold: kind === 'held' ? 'paused' : 'returned' }])
|
||||
expect(
|
||||
page.items.filter((item) => isStructuredAgentSessionStartFailureRow(item.itemId))
|
||||
).toMatchObject([
|
||||
{
|
||||
body: {
|
||||
text: 'Codex 0.135.0 is too old for chats. Update to 0.136.0 or newer.',
|
||||
tone: 'error',
|
||||
failure: { refusal: { details: { codexInstallation: { installedVersion: '0.135.0' } } } }
|
||||
}
|
||||
}
|
||||
])
|
||||
expect(await rig.resume()).toMatchObject({ ok: true, value: { resumed: false } })
|
||||
const attempts = rig.starts.mock.calls.length
|
||||
expect((await rig.submission(rejected))?.handedOverAt).toBeUndefined()
|
||||
expect(rig.starts).toHaveBeenCalledTimes(attempts)
|
||||
expect(await rig.sendNow(id)).toMatchObject({ ok: true })
|
||||
await eventually(async () => expect((await rig.handoff(id))?.handedOverAt).toBeDefined())
|
||||
expect(await rig.handoffId(id)).not.toBe(rejected)
|
||||
expect(rig.starts).toHaveBeenCalledTimes(attempts + 1)
|
||||
expect((await rig.submission(rejected))?.dispatchState).toBe('rejected')
|
||||
}
|
||||
)
|
||||
-100
@@ -1,100 +0,0 @@
|
||||
import { agentSessionConversationView } from '../../../shared/agent-session/lifecycle/agent-session-conversation-view'
|
||||
import { it, expect, vi } from 'vitest'
|
||||
import { createClaudeModelCatalogProbe } from '../../claude/claude-model-catalog-probe'
|
||||
import { createAgentModelCatalogService } from '../agent-model-catalog/agent-model-catalog-service'
|
||||
import { AgentModelCatalogStore } from '../agent-model-catalog/agent-model-catalog-store'
|
||||
import { agentSessionRecordFixture } from '../agent-session-record-test-fixture'
|
||||
import { claudeAndCodexAgents } from './structured-agent-session-adapter-router-test-support'
|
||||
import type { StructuredAgentSessionMutationContext } from './structured-agent-session-host-mutations'
|
||||
import { readStructuredAgentSessionOptions } from './structured-agent-session-options-read'
|
||||
|
||||
const SESSION = 'session-1'
|
||||
function restingJournal() {
|
||||
return {
|
||||
threadGoal: () => null,
|
||||
visitItems: () => {},
|
||||
inputOrder: { hasInputs: () => false },
|
||||
statusRead: () => ({ items: [], submissions: [] }),
|
||||
contextUsage: () => null,
|
||||
context: { floor: () => null }
|
||||
}
|
||||
}
|
||||
|
||||
it('keeps one legacy deadline across an optional join and current resting read', async () => {
|
||||
vi.useFakeTimers()
|
||||
const args = Promise.withResolvers<string[]>()
|
||||
const resolving = vi.fn(() => args.promise)
|
||||
const environment = vi.fn(async () => ({}))
|
||||
const record = agentSessionRecordFixture({
|
||||
sessionId: SESSION,
|
||||
provider: 'claude',
|
||||
permissionMode: 'auto',
|
||||
accountHome: { variable: 'CLAUDE_CONFIG_DIR', path: '/fixture/account' }
|
||||
})
|
||||
const catalog = createAgentModelCatalogService({
|
||||
store: new AgentModelCatalogStore(),
|
||||
getConversation: () => {
|
||||
const held = record
|
||||
return held ? agentSessionConversationView(held) : undefined
|
||||
},
|
||||
getRecord: () => record,
|
||||
drivesRecord: () => true,
|
||||
resolveAccountHome: async () => record.accountHome,
|
||||
recordWorkspacePath: async () => '/fixture/workspace',
|
||||
probes: {
|
||||
claude: createClaudeModelCatalogProbe({
|
||||
resolveLaunchArgs: resolving,
|
||||
resolveInheritedEnv: environment,
|
||||
resolveCommand: () => ({ command: '/fixture/claude', prefixArgs: [] }),
|
||||
resolveAuthPolicy: () => ({ account: 'system' })
|
||||
})
|
||||
}
|
||||
})
|
||||
const completion = Promise.withResolvers<void>()
|
||||
const optionsReady = vi.fn(() => completion.promise)
|
||||
let child: { phase: 'ready'; fence: number } | null = { phase: 'ready', fence: 1 }
|
||||
const resting = { child: null, params: { provider: 'claude' }, journal: restingJournal() }
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the full read only uses these context members.
|
||||
const context = {
|
||||
deps: {
|
||||
adapter: { readOptions: async () => ({ models: [], current: {} }) },
|
||||
agents: claudeAndCodexAgents(),
|
||||
store: {
|
||||
getConversation: () => {
|
||||
const held = record
|
||||
return held ? agentSessionConversationView(held) : undefined
|
||||
},
|
||||
getRecord: () => record
|
||||
},
|
||||
modelCatalog: catalog
|
||||
},
|
||||
deliveryOrder: { optionsReady },
|
||||
serialize: (_id: string, task: () => Promise<unknown>) => task(),
|
||||
openConversation: async () => ({ ...resting, child }),
|
||||
conversation: async () => resting
|
||||
} as unknown as StructuredAgentSessionMutationContext
|
||||
try {
|
||||
const answer = readStructuredAgentSessionOptions(context, SESSION, {
|
||||
readsWithoutModel: false
|
||||
})
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(resolving).not.toHaveBeenCalled()
|
||||
await vi.advanceTimersByTimeAsync(10_000)
|
||||
child = null
|
||||
completion.resolve()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(optionsReady).toHaveBeenCalledExactlyOnceWith(SESSION)
|
||||
expect(resolving).toHaveBeenCalledOnce()
|
||||
await vi.advanceTimersByTimeAsync(20_001)
|
||||
await expect(answer).resolves.toMatchObject({ current: { permissionMode: 'auto' } })
|
||||
expect(resolving).toHaveBeenCalledOnce()
|
||||
args.resolve([])
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(environment).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
completion.resolve()
|
||||
args.resolve([])
|
||||
catalog.stop()
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { projectStructuredAgentSessionMessages } from '../../../shared/agent-session/view/structured-agent-session-message-projection'
|
||||
import { agentJournalSubmissionKey } from '../../../shared/agent-session/journal/agent-session-journal-item-key'
|
||||
import {
|
||||
createQueuedMessageTestRig,
|
||||
eventually
|
||||
} from './structured-agent-session-queued-message-rig.test-fixture'
|
||||
import {
|
||||
HOST_TEST_NOW,
|
||||
HOST_TEST_SESSION as SESSION
|
||||
} from './structured-agent-session-host-test-data'
|
||||
import { STRUCTURED_AGENT_SESSION_IDLE_MS as IDLE_MS } from './structured-agent-session-idle-sweep'
|
||||
|
||||
it('keeps a recovered handoff consumed through natural idle unload and a history reopen', async () => {
|
||||
let now = HOST_TEST_NOW
|
||||
const rig = await createQueuedMessageTestRig({
|
||||
restartable: true,
|
||||
now: () => now,
|
||||
idleSweep: { idleMs: () => IDLE_MS, intervalMs: 2_000_000_000 }
|
||||
})
|
||||
try {
|
||||
const first = await rig.workingSend()
|
||||
await rig.send('handed-over card', 'queue-if-active').result
|
||||
await rig.send('still waiting', 'queue-if-active').result
|
||||
const cards = await rig.drafts()
|
||||
const consumedId = cards[0]!.messageId
|
||||
const waitingId = cards[1]!.messageId
|
||||
await rig.settleAccepted(first, 'initial')
|
||||
await eventually(async () =>
|
||||
expect((await rig.handoff(consumedId))?.handedOverAt).toBeDefined()
|
||||
)
|
||||
const handoffId = await rig.handoffId(consumedId)
|
||||
const dispatches = rig.dispatch.mock.calls.length
|
||||
// Simulate process loss without a graceful close settling the handed-over row.
|
||||
rig.host.collaboratorsForTests().lifetime.idleSweep.dispose()
|
||||
rig.crashRestartHostProcess()
|
||||
const read = async () => {
|
||||
const result = await rig.host.history({ sessionId: SESSION, direction: 'tail' })
|
||||
if (!result.ok) {
|
||||
throw new Error('history refused')
|
||||
}
|
||||
return result.page
|
||||
}
|
||||
const recovered = await read()
|
||||
expect(recovered.submissions.find((row) => row.clientMessageId === handoffId)).toMatchObject({
|
||||
queuedMessageId: consumedId,
|
||||
dispatchState: 'unknown',
|
||||
recovered: true
|
||||
})
|
||||
expect(recovered.queuedMessages?.map((card) => card.messageId)).toEqual([waitingId])
|
||||
const messages = () =>
|
||||
read().then((page) => projectStructuredAgentSessionMessages(page.items, [], page.submissions))
|
||||
const bubble = (await messages()).filter(
|
||||
(message) => message.id === agentJournalSubmissionKey(handoffId)
|
||||
)
|
||||
expect(bubble).toHaveLength(1)
|
||||
expect(bubble[0]?.unsent).toBeUndefined()
|
||||
expect(bubble[0]?.queued).toBeUndefined()
|
||||
now += IDLE_MS + 1
|
||||
await rig.host.collaboratorsForTests().lifetime.idleSweep.tick()
|
||||
expect(rig.host.collaboratorsForTests().sessions.has(SESSION)).toBe(true)
|
||||
expect(await rig.deleteQueued(waitingId)).toMatchObject({ ok: true })
|
||||
now += IDLE_MS + 1
|
||||
await rig.host.collaboratorsForTests().lifetime.idleSweep.tick()
|
||||
expect(rig.host.collaboratorsForTests().sessions.has(SESSION)).toBe(false)
|
||||
const reopened = await read()
|
||||
expect(reopened.queuedMessages ?? []).toEqual([])
|
||||
expect(
|
||||
(await messages()).filter((message) => message.id === agentJournalSubmissionKey(handoffId))
|
||||
).toEqual(bubble)
|
||||
expect(rig.dispatch).toHaveBeenCalledTimes(dispatches)
|
||||
} finally {
|
||||
await rig.dispose()
|
||||
}
|
||||
})
|
||||
+2
-45
@@ -1,6 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest'
|
||||
import { AGENT_SESSION_MAX_NEW_OPERATION_AGE_MS } from '../../../shared/agent-session/agent-session-host-authority'
|
||||
import type { AgentSessionJournal } from '../agent-session-journal/journal-store'
|
||||
|
||||
import { openTestJournalHostDatabase } from '../agent-session-journal/journal-host-database-test-support'
|
||||
import type { StructuredAgentSessionAdapter } from './structured-agent-session-adapter'
|
||||
import type { StructuredAgentSessionHost } from './structured-agent-session-host'
|
||||
@@ -10,11 +10,7 @@ import {
|
||||
envelope,
|
||||
hostTestState
|
||||
} from './structured-agent-session-host-test-harness'
|
||||
import {
|
||||
HOST_TEST_NOW as NOW,
|
||||
HOST_TEST_SESSION as SESSION,
|
||||
hostTestMessage
|
||||
} from './structured-agent-session-host-test-data'
|
||||
import { HOST_TEST_NOW as NOW, hostTestMessage } from './structured-agent-session-host-test-data'
|
||||
|
||||
let root: string
|
||||
let host: StructuredAgentSessionHost
|
||||
@@ -26,12 +22,6 @@ beforeEach(() => {
|
||||
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
|
||||
function hostJournal(): AgentSessionJournal {
|
||||
return (
|
||||
host as unknown as { sessions: Map<string, { journal: AgentSessionJournal }> }
|
||||
).sessions.get(SESSION)!.journal
|
||||
}
|
||||
|
||||
function sendParams(text: string, clientOperationId?: string) {
|
||||
const body = hostTestMessage(text)
|
||||
return {
|
||||
@@ -72,39 +62,6 @@ describe('send without ledger policy', () => {
|
||||
})
|
||||
|
||||
describe('a /clear in flight', () => {
|
||||
it('answers a resend from the attempt queued ahead of it, delivering once', async () => {
|
||||
await attach()
|
||||
const journal = hostJournal()
|
||||
const append = journal.appendSubmission.bind(journal)
|
||||
const held = Promise.withResolvers<void>()
|
||||
// Hold the earlier send so clear arrives before the queued retry runs.
|
||||
vi.spyOn(journal, 'appendSubmission').mockImplementationOnce(async (...args) => {
|
||||
await held.promise
|
||||
return append(...args)
|
||||
})
|
||||
const earlier = host.send(CALLER, sendParams('holds the queue'))
|
||||
const params = sendParams('queued ahead of the clear')
|
||||
const id = params.envelope.clientOperationId
|
||||
const attempt = host.send(CALLER, params)
|
||||
await vi.waitFor(() => expect(journal.appendSubmission).toHaveBeenCalledTimes(1))
|
||||
|
||||
const clearing = clear()
|
||||
const resent = host.send(CALLER, params)
|
||||
held.resolve()
|
||||
const [first, second] = await Promise.all([attempt, resent, earlier, clearing])
|
||||
|
||||
expect(first).toMatchObject({ ok: true, replayed: false })
|
||||
expect(second).toMatchObject({
|
||||
ok: true,
|
||||
replayed: true,
|
||||
value: { submission: { clientMessageId: id } }
|
||||
})
|
||||
expect(journal.submissions().filter((entry) => entry.clientMessageId === id)).toHaveLength(1)
|
||||
expect(
|
||||
dispatch.mock.calls.filter(([input]) => input.clientMessageId === id).length
|
||||
).toBeLessThanOrEqual(1)
|
||||
})
|
||||
|
||||
it('refuses an old id only for the clear in flight', async () => {
|
||||
await attach()
|
||||
const clearing = clear()
|
||||
|
||||
-110
@@ -1,110 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { AgentJournalSubmission } from '../../../shared/agent-session/journal/agent-session-journal-types'
|
||||
import { structuredAgentSessionRestartResumeSurfaces } from './structured-agent-session-restart-resume-wiring'
|
||||
|
||||
import { StructuredAgentSessionSendSettlement } from './structured-agent-session-send-settlement'
|
||||
|
||||
const SESSION = 'session-1'
|
||||
const MESSAGE = 'continuation-1'
|
||||
|
||||
let submission: AgentJournalSubmission
|
||||
const journal = {
|
||||
submission: (id: string): AgentJournalSubmission | undefined =>
|
||||
id === MESSAGE ? submission : undefined,
|
||||
cursor: () => ({ epoch: 'epoch-1', sequence: 1 }),
|
||||
activeTurnId: () => null
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
submission = {
|
||||
clientMessageId: MESSAGE,
|
||||
fence: 2,
|
||||
payloadFingerprint: 'fingerprint',
|
||||
dispatchState: 'pending',
|
||||
providerItemId: null,
|
||||
reason: null,
|
||||
submittedAt: 1,
|
||||
resolvedAt: null,
|
||||
handoverRecorded: true
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe('the restart continuation waits for its delivery (W18)', () => {
|
||||
it('reaches accepted after a 40 s cold start, with nothing filed as unconfirmed', async () => {
|
||||
const settlement = new StructuredAgentSessionSendSettlement(() => journal)
|
||||
const surfaces = structuredAgentSessionRestartResumeSurfaces(
|
||||
{
|
||||
revealSession: async () => ({ readable: true }),
|
||||
send: async () => {
|
||||
throw new Error('not used')
|
||||
},
|
||||
waitForSendSettlement: settlement.wait
|
||||
},
|
||||
() => 0
|
||||
)
|
||||
|
||||
const verdict = surfaces.awaitSendSettlement(SESSION, MESSAGE)
|
||||
await vi.advanceTimersByTimeAsync(40_000)
|
||||
submission = { ...submission, dispatchState: 'accepted', providerItemId: 'item-1' }
|
||||
settlement.publish(SESSION, journal)
|
||||
|
||||
await expect(verdict).resolves.toMatchObject({
|
||||
value: { submission: { dispatchState: 'accepted' } }
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('a restart batch holds a chat until its continuation is handed over', () => {
|
||||
function surfacesOver(settlement: StructuredAgentSessionSendSettlement) {
|
||||
return structuredAgentSessionRestartResumeSurfaces(
|
||||
{
|
||||
revealSession: async () => ({ readable: true }),
|
||||
send: async () => {
|
||||
throw new Error('not used')
|
||||
},
|
||||
waitForSendSettlement: settlement.wait
|
||||
},
|
||||
() => 0
|
||||
)
|
||||
}
|
||||
|
||||
it('keeps waiting through a slow start and resolves once the agent takes the message', async () => {
|
||||
const settlement = new StructuredAgentSessionSendSettlement(() => journal)
|
||||
let released = false
|
||||
const handedOver = surfacesOver(settlement)
|
||||
.awaitSendHandedOver(SESSION, MESSAGE)
|
||||
.then((result) => {
|
||||
released = true
|
||||
return result
|
||||
})
|
||||
await vi.advanceTimersByTimeAsync(5 * 60_000)
|
||||
expect(released).toBe(false)
|
||||
submission = { ...submission, handedOverAt: 2 }
|
||||
settlement.publish(SESSION, journal)
|
||||
await expect(handedOver).resolves.toMatchObject({
|
||||
value: { submission: { dispatchState: 'pending', handedOverAt: 2 } }
|
||||
})
|
||||
})
|
||||
|
||||
it('resolves once the start fails and the message is rejected', async () => {
|
||||
const settlement = new StructuredAgentSessionSendSettlement(() => journal)
|
||||
const handedOver = surfacesOver(settlement).awaitSendHandedOver(SESSION, MESSAGE)
|
||||
submission = { ...submission, dispatchState: 'rejected', reason: 'Codex could not start.' }
|
||||
settlement.publish(SESSION, journal)
|
||||
await expect(handedOver).resolves.toMatchObject({
|
||||
value: { submission: { dispatchState: 'rejected' } }
|
||||
})
|
||||
})
|
||||
|
||||
it('resolves undefined when the session closes first', async () => {
|
||||
const settlement = new StructuredAgentSessionSendSettlement(() => journal)
|
||||
const handedOver = surfacesOver(settlement).awaitSendHandedOver(SESSION, MESSAGE)
|
||||
settlement.closeSession(SESSION)
|
||||
await expect(handedOver).resolves.toBeUndefined()
|
||||
})
|
||||
})
|
||||
-116
@@ -1,116 +0,0 @@
|
||||
import { cp, rm } from 'node:fs/promises'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import { openTestAgentSessionRecordStore } from '../../runtime/agent-session/records/agent-session-record-store-test-harness'
|
||||
import { JOURNAL_OUTCOME_MARKER_VERSION } from '../agent-session-journal/journal-outcome-format'
|
||||
import type Database from '../../sqlite/sync-database'
|
||||
import {
|
||||
closeTestJournalHostDatabases,
|
||||
liveTestJournalRows,
|
||||
openTestJournalHostDatabase,
|
||||
updateTestJournalRowJson
|
||||
} from '../agent-session-journal/journal-host-database-test-support'
|
||||
import { StructuredAgentSessionHost } from './structured-agent-session-host'
|
||||
import {
|
||||
adapter,
|
||||
attach,
|
||||
CALLER,
|
||||
envelope,
|
||||
hostTestState,
|
||||
replaceHostTestState
|
||||
} from './structured-agent-session-host-test-harness'
|
||||
import {
|
||||
HOST_TEST_NOW as NOW,
|
||||
HOST_TEST_SESSION as SESSION,
|
||||
hostTestMessage
|
||||
} from './structured-agent-session-host-test-data'
|
||||
import { createStructuredAgentSessionLogger } from './structured-agent-session-logger'
|
||||
import { NO_STRUCTURED_AGENTS } from './structured-agent-session-adapter-router-test-support'
|
||||
|
||||
const relaunchedRoots: string[] = []
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks()
|
||||
await closeTestJournalHostDatabases()
|
||||
await Promise.all(
|
||||
relaunchedRoots.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))
|
||||
)
|
||||
})
|
||||
|
||||
async function relaunchWith(
|
||||
edit: (db: Database.Database) => void
|
||||
): Promise<StructuredAgentSessionHost> {
|
||||
const before = hostTestState()
|
||||
await attach()
|
||||
await before.host.flushStreamedEvents(SESSION)
|
||||
await before.store.renewLeases([])
|
||||
const relaunched = `${before.root}-relaunched`
|
||||
relaunchedRoots.push(relaunched)
|
||||
// Simulate process death without copying its live lock.
|
||||
await cp(before.root, relaunched, {
|
||||
recursive: true,
|
||||
filter: (source) => !source.includes('.lock')
|
||||
})
|
||||
edit(openTestJournalHostDatabase(relaunched).db)
|
||||
const store = await openTestAgentSessionRecordStore(relaunched, {
|
||||
legacySessionTabIds: [SESSION]
|
||||
})
|
||||
const host = new StructuredAgentSessionHost({
|
||||
agents: NO_STRUCTURED_AGENTS,
|
||||
logger: createStructuredAgentSessionLogger(),
|
||||
store,
|
||||
adapter: adapter(),
|
||||
journalDatabase: openTestJournalHostDatabase(relaunched),
|
||||
claimKeyId: 'key-1',
|
||||
mintSpawnToken: () => 'spawn-next',
|
||||
now: () => NOW
|
||||
})
|
||||
replaceHostTestState({ store, host })
|
||||
return host
|
||||
}
|
||||
|
||||
const damaged = (db: Database.Database) => updateTestJournalRowJson(db, SESSION, 1, '}{')
|
||||
const savedByNewerOrca = (db: Database.Database) => {
|
||||
const epochRow = liveTestJournalRows(db, SESSION).find((row) => row.seq === 1)!
|
||||
const newer = { ...JSON.parse(epochRow.rowJson), v: JOURNAL_OUTCOME_MARKER_VERSION + 1 }
|
||||
updateTestJournalRowJson(db, SESSION, 1, JSON.stringify(newer))
|
||||
}
|
||||
|
||||
it.each([
|
||||
['damaged', damaged, 'journalCorrupt'],
|
||||
['saved by a newer Orca', savedByNewerOrca, 'journalWrittenByNewerOrca']
|
||||
] as const)(
|
||||
'lists a restored chat that could not be opened (%s), and its read and send are refused with why',
|
||||
async (_why, edit, reason) => {
|
||||
const host = await relaunchWith(edit)
|
||||
const workspaceId = hostTestState().store.getRecord(SESSION)?.location.workspaceId
|
||||
const refusal = { code: 'agent_session_journal_unreadable', details: { reason } }
|
||||
|
||||
await host.seedSessionTabs([SESSION])
|
||||
|
||||
expect(host.hasSession(SESSION)).toBe(false)
|
||||
expect(host.listSessionTabs()).toEqual([
|
||||
{ sessionId: SESSION, workspaceId, agent: expect.any(String) }
|
||||
])
|
||||
for (let read = 0; read < 3; read += 1) {
|
||||
await expect(host.history({ sessionId: SESSION, direction: 'tail' })).rejects.toMatchObject({
|
||||
refusal
|
||||
})
|
||||
}
|
||||
const readFailureLogs = vi
|
||||
.mocked(console.warn)
|
||||
.mock.calls.filter(([line]) => String(line).includes('open-for-read'))
|
||||
expect(readFailureLogs).toHaveLength(1)
|
||||
const body = hostTestMessage('sent to a chat that cannot load')
|
||||
await expect(
|
||||
host.send(CALLER, { envelope: envelope('agentSession.send', { body }), body })
|
||||
).resolves.toMatchObject({ ok: false, refusal })
|
||||
|
||||
await host.setSessionTabVisibility(SESSION, false)
|
||||
expect(host.listSessionTabs()).toEqual([])
|
||||
await host.flushAllStreamedEvents()
|
||||
}
|
||||
)
|
||||
@@ -61,12 +61,6 @@ async function lookup(cwd = '/repo/orca', homeDir = join(root, 'home'), temp = j
|
||||
}
|
||||
|
||||
describe('OMP known-workspace transcript lookup', () => {
|
||||
it('does not search unrelated workspaces when the known cwd has no transcript', async () => {
|
||||
await record('--unrelated-project--')
|
||||
expect(await lookup()).toBeNull()
|
||||
expect(io.open).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('finds the reverted SHA256 bucket used by OMP 17.2.5–17.2.8', async () => {
|
||||
const windows = process.platform === 'win32'
|
||||
const digest = windows
|
||||
@@ -88,28 +82,4 @@ describe('OMP known-workspace transcript lookup', () => {
|
||||
expect(await lookup(alias, home)).toBe(expected)
|
||||
}
|
||||
)
|
||||
|
||||
it('never descends into session artifacts even when a child carries the wanted ID', async () => {
|
||||
const dir = join(sessionsDir, '--repo-orca--')
|
||||
const artifact = FILE.slice(0, -'.jsonl'.length)
|
||||
await mkdir(join(dir, artifact), { recursive: true })
|
||||
await writeFile(join(dir, artifact, FILE), '{}\n')
|
||||
expect(await lookup()).toBeNull()
|
||||
expect(io.open.mock.calls.map(([path]) => path)).toEqual([dir])
|
||||
})
|
||||
|
||||
it.skipIf(process.platform === 'win32')('rejects symlinked groups and files', async () => {
|
||||
const outside = join(root, 'outside')
|
||||
await mkdir(outside, { recursive: true })
|
||||
await mkdir(sessionsDir, { recursive: true })
|
||||
await writeFile(join(outside, FILE), '{}\n')
|
||||
const group = join(sessionsDir, '--repo-orca--')
|
||||
await symlink(outside, group)
|
||||
expect(await lookup()).toBeNull()
|
||||
expect(io.open).not.toHaveBeenCalled()
|
||||
await rm(group)
|
||||
await mkdir(group)
|
||||
await symlink(join(outside, FILE), join(group, FILE))
|
||||
expect(await lookup()).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { runProcess } from '@orca/process-host'
|
||||
import { resolveCommandOnLocalPath } from '../ipc/command-path/command-path-resolver'
|
||||
import {
|
||||
detectOpenCodeCredentialBackend,
|
||||
resetOpenCodeCredentialBackendProbes
|
||||
} from './opencode-credential-backend'
|
||||
|
||||
const files = vi.hoisted(() => ({ realpath: vi.fn(), stat: vi.fn() }))
|
||||
|
||||
vi.mock('@orca/process-host', () => ({ runProcess: vi.fn() }))
|
||||
vi.mock('../ipc/command-path/command-path-resolver', () => ({ resolveCommandOnLocalPath: vi.fn() }))
|
||||
vi.mock('node:fs/promises', () => files)
|
||||
|
||||
describe('OpenCode credential execution backend', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
resetOpenCodeCredentialBackendProbes()
|
||||
files.realpath.mockImplementation(async (path: string) => path)
|
||||
files.stat.mockResolvedValue({ dev: 1, ino: 2, size: 3, mtimeMs: 4, ctimeMs: 5 })
|
||||
vi.mocked(resolveCommandOnLocalPath).mockResolvedValue('/task/bin/opencode')
|
||||
vi.mocked(runProcess).mockResolvedValue({
|
||||
code: 0,
|
||||
signal: null,
|
||||
stdout: '1.18.30\n',
|
||||
stderr: '',
|
||||
timedOut: false
|
||||
})
|
||||
})
|
||||
|
||||
it('reprobes when the resolved binary is replaced', async () => {
|
||||
expect(await detectOpenCodeCredentialBackend()).toBe('v1')
|
||||
files.stat.mockResolvedValue({ dev: 1, ino: 6, size: 3, mtimeMs: 7, ctimeMs: 8 })
|
||||
vi.mocked(runProcess).mockResolvedValue({
|
||||
code: 0,
|
||||
signal: null,
|
||||
stdout: 'opencode v2.0.16',
|
||||
stderr: '',
|
||||
timedOut: false
|
||||
})
|
||||
|
||||
expect(await detectOpenCodeCredentialBackend()).toBe('v2')
|
||||
expect(runProcess).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
@@ -5,10 +5,8 @@ import { afterEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
activationFenceExists,
|
||||
createOrcadIdleProbes,
|
||||
resolveOrcadManagedIdleExit,
|
||||
type OrcadManagedIdleExitPorts
|
||||
} from './orcad-managed-idle-exit'
|
||||
import { ORCAD_E2E_IDLE_TIMEOUT_ENV } from '../../shared/orcad-idle-exit'
|
||||
|
||||
const config = { timeoutMs: 1_000, activationRoot: '/home/u/.orca-remote/.fence' }
|
||||
|
||||
@@ -34,41 +32,7 @@ async function verdicts(ports: OrcadManagedIdleExitPorts): Promise<Record<string
|
||||
return Object.fromEntries(entries)
|
||||
}
|
||||
|
||||
describe('resolveOrcadManagedIdleExit', () => {
|
||||
it('stays off for a server the user started or paired', () => {
|
||||
expect(resolveOrcadManagedIdleExit({})).toBeNull()
|
||||
expect(resolveOrcadManagedIdleExit({ [ORCAD_E2E_IDLE_TIMEOUT_ENV]: '50' })).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('createOrcadIdleProbes', () => {
|
||||
it.each<[string, Partial<OrcadManagedIdleExitPorts>, string]>([
|
||||
[
|
||||
'an open client socket',
|
||||
{
|
||||
readClientActivity: () => ({ openConnections: 1, requestsInFlight: 0, lastRequestAt: 0 })
|
||||
},
|
||||
'clients'
|
||||
],
|
||||
[
|
||||
'a request still running',
|
||||
{
|
||||
readClientActivity: () => ({ openConnections: 0, requestsInFlight: 1, lastRequestAt: 0 })
|
||||
},
|
||||
'clients'
|
||||
],
|
||||
['an in-process terminal', { listTerminals: async () => [{ id: 'pty-1' }] }, 'terminals'],
|
||||
['a live daemon session', { countDaemonSessions: async () => 2 }, 'terminals'],
|
||||
['a working agent', { agentStates: () => [{ state: 'working' }] }, 'agents'],
|
||||
['a loaded chat provider', { hasChatProviders: () => true }, 'chat-providers'],
|
||||
['a staged migration', { hasStagedMigration: () => true }, 'migration'],
|
||||
['an enabled or running automation', { automationsBusy: () => true }, 'automations'],
|
||||
['an Orca-started emulator', { emulatorsBusy: async () => true }, 'emulators'],
|
||||
['a held activation fence', { activationFenceExists: async () => true }, 'activation']
|
||||
])('reads %s as busy', async (_label, override, probe) => {
|
||||
expect((await verdicts({ ...idlePorts(), ...override }))[probe]).toBe('busy')
|
||||
})
|
||||
|
||||
it('treats a daemon that did not answer as unverifiable, not as no terminals', async () => {
|
||||
const result = await verdicts({ ...idlePorts(), countDaemonSessions: async () => null })
|
||||
expect(result.terminals).toBe('unverifiable')
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import { hostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity'
|
||||
import { closeTestStores, createSqliteTestStore } from '../persistence-test-harness'
|
||||
// Drive real Store writers so disconnected prune-gate call sites cannot pass unnoticed.
|
||||
import { mkdtempSync, realpathSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { WorkspaceSessionState } from '../../../shared/workspace-session/workspace-session-state-types'
|
||||
import { removeWorktreeMetaRow } from '../host-qualified-worktree-meta'
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
app: {
|
||||
getPath: () => tmpdir(),
|
||||
getName: () => 'orca-test',
|
||||
getVersion: () => '0.0.0-test',
|
||||
isPackaged: false,
|
||||
on: () => {},
|
||||
whenReady: () => Promise.resolve()
|
||||
},
|
||||
safeStorage: {
|
||||
isEncryptionAvailable: () => false,
|
||||
encryptString: (value: string) => Buffer.from(value),
|
||||
decryptString: (value: Buffer) => value.toString()
|
||||
},
|
||||
ipcMain: { on: () => {}, handle: () => {} },
|
||||
BrowserWindow: { getAllWindows: () => [] }
|
||||
}))
|
||||
|
||||
const { Store } = await import('./store')
|
||||
const {
|
||||
__resetLocalWorktreeMetadataPruneGateForTests,
|
||||
isLocalWorktreeMetadataPruneDue,
|
||||
markLocalWorktreeMetadataPruneStarted
|
||||
} = await import('../../workspaces/catalog/local-worktree-metadata-prune-gate')
|
||||
|
||||
const REPO_ID = 'repo-1'
|
||||
const WORKTREE_ID = `${REPO_ID}::/tmp/worktree-a`
|
||||
|
||||
const stores: InstanceType<typeof Store>[] = []
|
||||
|
||||
beforeEach(() => {
|
||||
__resetLocalWorktreeMetadataPruneGateForTests()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// Freeze debounced writes before their temporary directories disappear.
|
||||
for (const store of stores.splice(0)) {
|
||||
store.freezeWrites()
|
||||
}
|
||||
await closeTestStores()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
function createStore(): InstanceType<typeof Store> {
|
||||
const dir = realpathSync(mkdtempSync(join(tmpdir(), 'orca-store-prune-gate-')))
|
||||
const store = createSqliteTestStore(Store, { dataFile: join(dir, 'orca-data.json') })
|
||||
stores.push(store)
|
||||
return store
|
||||
}
|
||||
|
||||
function parkGate(): void {
|
||||
markLocalWorktreeMetadataPruneStarted(REPO_ID)
|
||||
expect(isLocalWorktreeMetadataPruneDue(REPO_ID)).toBe(false)
|
||||
}
|
||||
|
||||
describe('store signals to the worktree metadata prune gate', () => {
|
||||
it('re-arms the gate when a session releases its claim on a worktree', () => {
|
||||
const store = createStore()
|
||||
store.setWorkspaceSession({
|
||||
activeRepoId: REPO_ID,
|
||||
activeWorktreeId: WORKTREE_ID,
|
||||
activeTabId: 'tab-1',
|
||||
tabsByWorktree: { [WORKTREE_ID]: [{ id: 'tab-1', worktreeId: WORKTREE_ID }] },
|
||||
terminalLayoutsByTabId: {}
|
||||
} as unknown as WorkspaceSessionState)
|
||||
parkGate()
|
||||
|
||||
store.removeWorkspaceSessionStateForWorktree(
|
||||
hostWorktreeTarget({ id: WORKTREE_ID, hostId: 'local' })
|
||||
)
|
||||
|
||||
expect(isLocalWorktreeMetadataPruneDue(REPO_ID)).toBe(true)
|
||||
})
|
||||
|
||||
it('re-arms the gate when a metadata row is removed', () => {
|
||||
const store = createStore()
|
||||
store.setWorktreeMeta(hostWorktreeTarget({ id: WORKTREE_ID, hostId: 'local' }), {
|
||||
displayName: 'a',
|
||||
hostId: 'local'
|
||||
})
|
||||
parkGate()
|
||||
|
||||
removeWorktreeMetaRow(store, WORKTREE_ID, undefined)
|
||||
|
||||
expect(isLocalWorktreeMetadataPruneDue(REPO_ID)).toBe(true)
|
||||
})
|
||||
|
||||
it('leaves the gate parked for writes that only add or update a claim', () => {
|
||||
const store = createStore()
|
||||
parkGate()
|
||||
|
||||
store.setWorktreeMeta(hostWorktreeTarget({ id: WORKTREE_ID, hostId: 'local' }), {
|
||||
displayName: 'a',
|
||||
hostId: 'local'
|
||||
})
|
||||
store.setWorktreeMeta(hostWorktreeTarget({ id: WORKTREE_ID, hostId: 'local' }), {
|
||||
displayName: 'b',
|
||||
hostId: 'local'
|
||||
})
|
||||
|
||||
// Ordinary scan writes must not rearm pruning and recreate the scan storm.
|
||||
expect(isLocalWorktreeMetadataPruneDue(REPO_ID)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -3,10 +3,7 @@ import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type * as FsPromises from 'node:fs/promises'
|
||||
import {
|
||||
renamePluginFileWithWindowsRetry,
|
||||
writePluginFileAtomically
|
||||
} from './plugin-atomic-file-write'
|
||||
import { writePluginFileAtomically } from './plugin-atomic-file-write'
|
||||
|
||||
// Windows AV/indexer locks cannot be provoked on CI, so queue the errno codes instead.
|
||||
const locks = vi.hoisted(() => ({ codes: [] as string[] }))
|
||||
@@ -89,38 +86,3 @@ describe('writePluginFileAtomically', () => {
|
||||
expect(await readFile(target, 'utf8')).toMatch(/^value-\d$/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('renamePluginFileWithWindowsRetry', () => {
|
||||
describe('on Windows', () => {
|
||||
let dir: string
|
||||
let restorePlatform: () => void
|
||||
|
||||
beforeEach(async () => {
|
||||
dir = await mkdtemp(join(tmpdir(), 'orca-plugin-rename-win-'))
|
||||
restorePlatform = withPlatform('win32')
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
restorePlatform()
|
||||
locks.codes = []
|
||||
await rm(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it.each(['EPERM', 'EACCES', 'EBUSY'])('retries past a transient %s lock', async (code) => {
|
||||
await writePluginFileAtomically(join(dir, 'from'), 'payload')
|
||||
locks.codes = [code, code]
|
||||
await renamePluginFileWithWindowsRetry(join(dir, 'from'), join(dir, 'to'))
|
||||
expect(await readFile(join(dir, 'to'), 'utf8')).toBe('payload')
|
||||
expect(locks.codes).toEqual([])
|
||||
})
|
||||
|
||||
it('rethrows a non-retryable code without retrying', async () => {
|
||||
await writePluginFileAtomically(join(dir, 'from'), 'payload')
|
||||
locks.codes = ['ENOSPC', 'ENOSPC']
|
||||
await expect(
|
||||
renamePluginFileWithWindowsRetry(join(dir, 'from'), join(dir, 'to'))
|
||||
).rejects.toMatchObject({ code: 'ENOSPC' })
|
||||
expect(locks.codes).toEqual(['ENOSPC'])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { resolvePluginModuleSpecifier } from './plugin-frame-module-resolution'
|
||||
import { createPluginSourceModuleCompiler } from './plugin-frame-module-transform'
|
||||
import { transpilePluginSource } from './plugin-source-transpiler'
|
||||
|
||||
// The real transform, in process; the worker hop is covered by plugin-source-transpile-client.
|
||||
const inProcess = () =>
|
||||
createPluginSourceModuleCompiler({
|
||||
transpile: async (path, source) => transpilePluginSource(path, source)
|
||||
})
|
||||
|
||||
const FILES = new Set([
|
||||
'src/panel.tsx',
|
||||
@@ -18,7 +10,6 @@ const FILES = new Set([
|
||||
'src/my file.ts'
|
||||
])
|
||||
const exists = async (path: string) => FILES.has(path)
|
||||
const encode = (text: string) => new TextEncoder().encode(text)
|
||||
|
||||
describe('resolvePluginModuleSpecifier', () => {
|
||||
it('refuses bare packages, escapes and missing files', async () => {
|
||||
@@ -36,31 +27,3 @@ describe('resolvePluginModuleSpecifier', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('createPluginSourceModuleCompiler', () => {
|
||||
it('strips types, compiles JSX for orca and rewrites relative imports, keeping lines', async () => {
|
||||
const compile = inProcess()
|
||||
const source = [
|
||||
"import { Stack, type Atom } from 'orca'",
|
||||
"import { add } from './util'",
|
||||
'type Props = { n: number }',
|
||||
'export default function Panel({ n }: Props) {',
|
||||
' return <Stack gap="md">{add(n, 1)}</Stack>',
|
||||
'}',
|
||||
"export const later = () => import('./list')"
|
||||
].join('\n')
|
||||
const code = await compile({ relativePath: 'src/panel.tsx', bytes: encode(source), exists })
|
||||
expect(code).toContain('from "orca/jsx-runtime"')
|
||||
expect(code).toContain("from './util.ts'")
|
||||
expect(code).toContain('import("./list/index.tsx")')
|
||||
expect(code).not.toContain('Props')
|
||||
expect(code).not.toContain('Atom')
|
||||
// Line-preserving: a guest error's line number is the author's line number.
|
||||
const lines = code.split('\n')
|
||||
expect(lines[4]).toContain('_jsx(Stack')
|
||||
const mapLine = lines.at(-2) ?? ''
|
||||
expect(mapLine).toMatch(/^\/\/# sourceMappingURL=data:application\/json/)
|
||||
const map = JSON.parse(Buffer.from(mapLine.split('base64,')[1], 'base64').toString('utf8'))
|
||||
expect(map).toMatchObject({ sources: ['panel.tsx'], sourcesContent: [source] })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -67,23 +67,3 @@ describe('worker panel requests', () => {
|
||||
await pending
|
||||
})
|
||||
})
|
||||
|
||||
describe('workspace.changed in workers', () => {
|
||||
it('replays the current context to a handler as it subscribes', async () => {
|
||||
let orca!: PluginWorkerOrcaApi
|
||||
const { send, runtime } = await activate((api) => (orca = api))
|
||||
const handler = vi.fn()
|
||||
const disposed = vi.fn()
|
||||
orca.events.on('workspace.changed', handler)
|
||||
orca.events.on('workspace.changed', disposed).dispose()
|
||||
const reads = send.mock.calls
|
||||
.map(([message]) => message)
|
||||
.filter((message) => message.method === 'workspace.readContext')
|
||||
expect(reads).toHaveLength(2)
|
||||
for (const read of reads) {
|
||||
await runtime.handleMessage({ orca: 1, kind: 'res', id: read.id, result: { branch: 'main' } })
|
||||
}
|
||||
await vi.waitFor(() => expect(handler).toHaveBeenCalledWith({ context: { branch: 'main' } }))
|
||||
expect(disposed).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
/** #15192: replay ConPTY bytes/resizes into the host emulator and assert buffer cells, since valid repaint bytes can still duplicate glyphs.
|
||||
* Fixture/control assertions also run on POSIX; only bundled-vs-system ConPTY comparison requires Windows. */
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { Terminal } from '@xterm/headless'
|
||||
import { HeadlessEmulator } from '../daemon/headless-emulator'
|
||||
import { readWrappedLineGlyphs } from '../daemon/__fixtures__/terminal-wide-cell-grid'
|
||||
|
||||
const KOREAN_LINE = '안녕하세요 오르카 테스트입니다. 결론부터 말씀드리면 시각적 피로도'
|
||||
const LATIN_LINE = 'roadmap/complete-overhaul-backlog-history.md (1.75) R-08)'
|
||||
const LINE_REPEATS = 8
|
||||
const COLS = 40
|
||||
const ROWS = 12
|
||||
|
||||
const glyphsOf = (line: string): string => line.replace(/\s+/g, '')
|
||||
const KOREAN_GLYPHS = glyphsOf(KOREAN_LINE)
|
||||
const LATIN_GLYPHS = glyphsOf(LATIN_LINE)
|
||||
|
||||
type Event = { data: string } | { resizeTo: number }
|
||||
|
||||
type RunOptions = {
|
||||
useConptyDll: boolean
|
||||
/** Milliseconds after spawn at which to resize, paired with the target width. */
|
||||
resizes?: { atMs: number; cols: number }[]
|
||||
/** How long the child stays alive after its last write, so a resize can trigger a repaint. */
|
||||
holdMs: number
|
||||
}
|
||||
|
||||
// Node writes deterministic UTF-8; cmd.exe output depends on the machine codepage.
|
||||
function childScript(holdMs: number): string {
|
||||
return [
|
||||
`const ko=${JSON.stringify(KOREAN_LINE)};`,
|
||||
`const la=${JSON.stringify(LATIN_LINE)};`,
|
||||
`let i=0;`,
|
||||
`const t=setInterval(()=>{process.stdout.write(ko+"\\r\\n"+la+"\\r\\n");`,
|
||||
`if(++i>=${LINE_REPEATS}){clearInterval(t);setTimeout(()=>process.exit(0),${holdMs});}},25);`
|
||||
].join('')
|
||||
}
|
||||
|
||||
async function recordConpty(options: RunOptions): Promise<Event[]> {
|
||||
const nodePty = await import('node-pty')
|
||||
const proc = nodePty.spawn(process.execPath, ['-e', childScript(options.holdMs)], {
|
||||
name: 'xterm-256color',
|
||||
cols: COLS,
|
||||
rows: ROWS,
|
||||
cwd: process.cwd(),
|
||||
env: process.env as Record<string, string>,
|
||||
...(options.useConptyDll ? { useConptyDll: true } : {})
|
||||
})
|
||||
|
||||
// Preserve resize/output order for faithful Session.resize replay.
|
||||
const events: Event[] = []
|
||||
proc.onData((chunk) => {
|
||||
events.push({ data: chunk })
|
||||
})
|
||||
const timers = (options.resizes ?? []).map((resize) =>
|
||||
setTimeout(() => {
|
||||
events.push({ resizeTo: resize.cols })
|
||||
try {
|
||||
proc.resize(resize.cols, ROWS)
|
||||
} catch {
|
||||
/* the child may already have exited */
|
||||
}
|
||||
}, resize.atMs)
|
||||
)
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
proc.onExit(() => resolve())
|
||||
})
|
||||
for (const timer of timers) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
function replayIntoEmulator(events: Event[]): string[] {
|
||||
const emulator = new HeadlessEmulator({ cols: COLS, rows: ROWS })
|
||||
for (const event of events) {
|
||||
if ('resizeTo' in event) {
|
||||
// Session.resize sizes the emulator before the pty; keep that order.
|
||||
emulator.resize(event.resizeTo, ROWS)
|
||||
} else {
|
||||
emulator.writeSync(event.data)
|
||||
}
|
||||
}
|
||||
const terminal = (emulator as unknown as { terminal: Terminal }).terminal
|
||||
const lines = readWrappedLineGlyphs(terminal).filter((line) => line.length > 0)
|
||||
emulator.dispose()
|
||||
return lines
|
||||
}
|
||||
|
||||
/** Adjacent identical wide glyphs. Neither fixture contains any, so a hit is duplication. */
|
||||
|
||||
function expectFixtureLinesOnly(lines: string[]): void {
|
||||
expect(lines.length).toBeGreaterThan(0)
|
||||
expect(lines.filter((line) => line === KOREAN_GLYPHS)).toHaveLength(LINE_REPEATS)
|
||||
// Latin is the control for script-specific corruption.
|
||||
expect(lines.filter((line) => line === LATIN_GLYPHS)).toHaveLength(LINE_REPEATS)
|
||||
expect(lines.filter((line) => line !== KOREAN_GLYPHS && line !== LATIN_GLYPHS)).toEqual([])
|
||||
}
|
||||
|
||||
describe('pty repaint fidelity in the terminal buffer (#15192)', () => {
|
||||
it('survives the resize the reporter used as a workaround, after output settles', async () => {
|
||||
// Resize a quiet child so only ConPTY repaint can change the settled text.
|
||||
const events = await recordConpty({
|
||||
useConptyDll: true,
|
||||
holdMs: 900,
|
||||
resizes: [
|
||||
{ atMs: 450, cols: 31 },
|
||||
{ atMs: 650, cols: 47 }
|
||||
]
|
||||
})
|
||||
expectFixtureLinesOnly(replayIntoEmulator(events))
|
||||
}, 60_000)
|
||||
})
|
||||
@@ -1,129 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { Tab } from '../../shared/tab-types'
|
||||
import type { WorkspaceSessionState } from '../../shared/workspace-session/workspace-session-state-types'
|
||||
import { chatTabLayoutCommand, loadChatTabLayout } from './agent-session/agent-session-tab-layout'
|
||||
|
||||
const WT = 'repo-1::/tmp/worktree-a'
|
||||
|
||||
function chatTab(sessionId: string, id: string): Tab {
|
||||
return {
|
||||
id,
|
||||
entityId: sessionId,
|
||||
groupId: 'group-1',
|
||||
worktreeId: WT,
|
||||
contentType: 'agent-session',
|
||||
label: 'Chat',
|
||||
customLabel: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 1
|
||||
}
|
||||
}
|
||||
|
||||
function session(tabs: Tab[], activeGroupId?: string): WorkspaceSessionState {
|
||||
return {
|
||||
activeRepoId: 'repo-1',
|
||||
activeWorktreeId: WT,
|
||||
activeTabId: null,
|
||||
tabsByWorktree: {},
|
||||
terminalLayoutsByTabId: {},
|
||||
unifiedTabs: { [WT]: tabs },
|
||||
tabGroups: {
|
||||
[WT]: [{ id: 'group-1', worktreeId: WT, activeTabId: null, tabOrder: tabs.map((t) => t.id) }]
|
||||
},
|
||||
...(activeGroupId ? { activeGroupIdByWorktree: { [WT]: activeGroupId } } : {})
|
||||
}
|
||||
}
|
||||
|
||||
function commandOn(
|
||||
stored: WorkspaceSessionState,
|
||||
change: Parameters<typeof chatTabLayoutCommand>[3]
|
||||
) {
|
||||
const { loaded, activeGroupId } = loadChatTabLayout('local', stored, WT)
|
||||
return chatTabLayoutCommand(loaded.layout.workspaces[WT], activeGroupId, WT, change)
|
||||
}
|
||||
|
||||
const open = (sessionId: string) => ({
|
||||
open: { sessionId, agent: 'codex' as const, tabId: `agent-session:${sessionId}` }
|
||||
})
|
||||
|
||||
describe("a host's chat tab change as main's layout command", () => {
|
||||
it('names no group the workspace no longer holds, nor one with no layout loaded', () => {
|
||||
expect(commandOn(session([chatTab('a', 'saved-tab')], 'gone'), open('b'))).toEqual(
|
||||
expect.not.objectContaining({ groupId: expect.anything() })
|
||||
)
|
||||
expect(chatTabLayoutCommand(undefined, 'group-1', WT, open('b'))).toEqual(
|
||||
expect.not.objectContaining({ groupId: expect.anything() })
|
||||
)
|
||||
})
|
||||
|
||||
describe("a paired client's chosen place (L270)", () => {
|
||||
function twoGroups(): WorkspaceSessionState {
|
||||
const tabs = [
|
||||
chatTab('a', 'tab-a'),
|
||||
chatTab('c', 'tab-c'),
|
||||
{ ...chatTab('d', 'tab-d'), groupId: 'group-2' }
|
||||
]
|
||||
return {
|
||||
...session(tabs, 'group-1'),
|
||||
tabGroups: {
|
||||
[WT]: [
|
||||
{ id: 'group-1', worktreeId: WT, activeTabId: null, tabOrder: ['tab-a', 'tab-c'] },
|
||||
{ id: 'group-2', worktreeId: WT, activeTabId: null, tabOrder: ['tab-d'] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
const placed = (placement: { groupId?: string; afterTabId?: string }) => ({
|
||||
open: { ...open('b').open, placement }
|
||||
})
|
||||
|
||||
it('opens the chat in the named group, after the named tab', () => {
|
||||
expect(commandOn(twoGroups(), placed({ groupId: 'group-1', afterTabId: 'tab-a' }))).toEqual({
|
||||
type: 'openAgentSessionTab',
|
||||
workspace: WT,
|
||||
tabId: 'agent-session:b',
|
||||
sessionId: 'b',
|
||||
agent: 'codex',
|
||||
groupId: 'group-1',
|
||||
afterTabId: 'tab-a'
|
||||
})
|
||||
})
|
||||
|
||||
it('prefers the named group to the remembered active one', () => {
|
||||
expect(commandOn(twoGroups(), placed({ groupId: 'group-2' }))).toMatchObject({
|
||||
groupId: 'group-2'
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to the active group for a group the workspace no longer holds', () => {
|
||||
expect(
|
||||
commandOn(twoGroups(), placed({ groupId: 'gone', afterTabId: 'tab-a' }))
|
||||
).toMatchObject({
|
||||
groupId: 'group-1',
|
||||
afterTabId: 'tab-a'
|
||||
})
|
||||
})
|
||||
|
||||
it('drops an anchor tab that is not in the chosen group', () => {
|
||||
expect(commandOn(twoGroups(), placed({ groupId: 'group-2', afterTabId: 'tab-a' }))).toEqual(
|
||||
expect.not.objectContaining({ afterTabId: expect.anything() })
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('opens nothing for a chat a tab already shows', () => {
|
||||
expect(commandOn(session([chatTab('a', 'saved-tab')], 'group-1'), open('a'))).toBeNull()
|
||||
})
|
||||
|
||||
it('closes the tab that shows the chat, under its saved id, forced', () => {
|
||||
expect(
|
||||
commandOn(session([chatTab('a', 'saved-tab')]), { close: 'a', tabId: 'saved-tab' })
|
||||
).toEqual({
|
||||
type: 'closeTabs',
|
||||
workspace: WT,
|
||||
tabIds: ['saved-tab'],
|
||||
force: true
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -23,19 +23,6 @@ describe('browser execution host key resolution', () => {
|
||||
).toMatchObject({ kind: 'native', runtimeId: runtime.getRuntimeId() })
|
||||
})
|
||||
|
||||
it('holds a resolvable workspace whose execution host cannot be minted yet', async () => {
|
||||
const runtime = createRuntime()
|
||||
Object.defineProperty(runtime, 'resolveBrowserNetworkExecutionHostForWorktree', {
|
||||
value: async () => {
|
||||
throw new Error('Host provider is not ready')
|
||||
}
|
||||
})
|
||||
|
||||
await expect(runtime.resolveBrowserExecutionHostKeyForWorkspace(WORKSPACE_ID)).resolves.toEqual(
|
||||
{ status: 'unavailable' }
|
||||
)
|
||||
})
|
||||
|
||||
it('settles a workspace that no longer resolves', async () => {
|
||||
const runtime = createRuntime()
|
||||
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createDraftPasteReadyScanner } from '../../shared/draft-paste-ready-scanner'
|
||||
import {
|
||||
getAgentLabel,
|
||||
isGeminiTerminalTitle,
|
||||
resolveExplicitTerminalTitleAgentType
|
||||
} from '../../shared/agent-status/title/terminal-title-agent-type'
|
||||
|
||||
// Raw dsh-tui 0.10.2 / @deepseek-ai/dsh 0.1.5-rc.1 capture; agent-pty-transcript-capture.md.
|
||||
const TRANSCRIPT = readFileSync(join(__dirname, '__fixtures__', 'dsh-tui-ready-no-key.txt'), 'utf8')
|
||||
|
||||
const ESC = String.fromCharCode(27)
|
||||
const BEL = String.fromCharCode(7)
|
||||
|
||||
function readFirstOscTitle(data: string): string {
|
||||
const open = data.indexOf(`${ESC}]0;`)
|
||||
const bodyStart = open === -1 ? -1 : open + 4
|
||||
const terminator = bodyStart === -1 ? -1 : data.indexOf(BEL, bodyStart)
|
||||
if (terminator === -1) {
|
||||
throw new Error('the captured transcript carries no BEL-terminated OSC 0 title')
|
||||
}
|
||||
return data.slice(bodyStart, terminator)
|
||||
}
|
||||
|
||||
describe('DSH-TUI readiness from captured terminal bytes', () => {
|
||||
it('fires the composer-ready signal well before the transcript ends', () => {
|
||||
const scanner = createDraftPasteReadyScanner('dsh-composer-prompt')
|
||||
let readyAt = -1
|
||||
for (let offset = 0; offset < TRANSCRIPT.length; offset += 1024) {
|
||||
const chunk = TRANSCRIPT.slice(offset, offset + 1024)
|
||||
if (scanner.observe(chunk).ready && readyAt === -1) {
|
||||
readyAt = offset + chunk.length
|
||||
}
|
||||
}
|
||||
// Composer arrives near byte 5910; do not wait for the remaining ~70KB whale animation.
|
||||
expect(readyAt).toBeGreaterThan(0)
|
||||
expect(readyAt).toBeLessThan(8192)
|
||||
})
|
||||
|
||||
it('arms the quiet-window fallback from DECSET 2004 as well', () => {
|
||||
const scanner = createDraftPasteReadyScanner('dsh-composer-prompt')
|
||||
expect(scanner.observe(TRANSCRIPT.slice(0, 40)).armQuietTimer).toBe(true)
|
||||
})
|
||||
|
||||
it('identifies the pane from DSH’s own title, not Gemini’s', () => {
|
||||
const title = readFirstOscTitle(TRANSCRIPT)
|
||||
expect(title).toContain('✦')
|
||||
expect(title).toContain('\u{1F40B}')
|
||||
expect(isGeminiTerminalTitle(title)).toBe(false)
|
||||
expect(getAgentLabel(title)).toBe('DeepSeek Harness')
|
||||
expect(resolveExplicitTerminalTitleAgentType(title)).toBe('dsh')
|
||||
})
|
||||
|
||||
it('keeps a working DSH title out of Claude’s braille-spinner lane', () => {
|
||||
// DSH working braille overlaps Claude's spinner glyphs.
|
||||
for (const frame of ['⠂', '⠐']) {
|
||||
const working = `${frame} \u{1F40B} fix the flaky test`
|
||||
expect(getAgentLabel(working)).toBe('DeepSeek Harness')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,43 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { OrcaRuntimeService } from '../orca-runtime-test-mocks.spec'
|
||||
import {
|
||||
HEADLESS_LEAF_ID,
|
||||
HEADLESS_SECOND_LEAF_ID,
|
||||
TEST_WORKTREE_ID,
|
||||
makeHeadlessTerminalLayout,
|
||||
makeRuntimeStoreWithWorkspaceSession,
|
||||
makeWorkspaceSessionWithHeadlessTerminal
|
||||
} from '../orca-runtime-test-fixtures.spec'
|
||||
import { planDuplicatePtyPaneRepair } from '../../persistence/terminal-topology/duplicate-pty-pane-repair'
|
||||
import type { WorkspaceSessionState } from '../../../shared/workspace-session/workspace-session-state-types'
|
||||
|
||||
// Windowless listing reads stored layout; duplicate PTY bindings need durable repair to avoid duplicate tabs.
|
||||
const duplicate = makeWorkspaceSessionWithHeadlessTerminal({
|
||||
terminalLayoutsByTabId: {
|
||||
'host-tab': makeHeadlessTerminalLayout({
|
||||
[HEADLESS_LEAF_ID]: 'persisted-pty',
|
||||
[HEADLESS_SECOND_LEAF_ID]: 'persisted-pty'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
async function terminalRows(session: WorkspaceSessionState): Promise<number> {
|
||||
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(session)
|
||||
const runtime = new OrcaRuntimeService(runtimeStore)
|
||||
runtime.setPtyController({
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null,
|
||||
listProcesses: async () => []
|
||||
})
|
||||
const snapshots = await runtime.listAllMobileSessionTabs()
|
||||
return (snapshots.find((snapshot) => snapshot.worktree === TEST_WORKTREE_ID)?.tabs ?? []).filter(
|
||||
(tab) => tab.type === 'terminal'
|
||||
).length
|
||||
}
|
||||
|
||||
describe('windowless mobile list of a terminal stored in two panes', () => {
|
||||
it('lists one row once main has repaired the stored tree', async () => {
|
||||
await expect(terminalRows(planDuplicatePtyPaneRepair(duplicate)!.session)).resolves.toBe(1)
|
||||
})
|
||||
})
|
||||
@@ -1,100 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { OrcaRuntimeService } from '../orca-runtime-test-mocks.spec'
|
||||
import type { WorkspaceSessionState } from '../../../shared/workspace-session/workspace-session-state-types'
|
||||
import {
|
||||
HEADLESS_LEAF_ID,
|
||||
HEADLESS_SECOND_LEAF_ID,
|
||||
TEST_WORKTREE_ID,
|
||||
makeHeadlessTerminalLayout,
|
||||
makeRuntimeStoreWithWorkspaceSession,
|
||||
makeWorkspaceSessionWithHeadlessTerminal
|
||||
} from '../orca-runtime-test-fixtures.spec'
|
||||
|
||||
// Model a moved/restored tab with an agent first pane, shell sibling and no live terminals; launchAgent applies tab-wide.
|
||||
function splitAgentTabSession(withRecord: boolean): WorkspaceSessionState {
|
||||
const base = makeWorkspaceSessionWithHeadlessTerminal()
|
||||
const tab = base.tabsByWorktree[TEST_WORKTREE_ID]?.[0]
|
||||
return {
|
||||
...base,
|
||||
tabsByWorktree: {
|
||||
[TEST_WORKTREE_ID]: tab ? [{ ...tab, ptyId: null, launchAgent: 'codex' }] : []
|
||||
},
|
||||
terminalLayoutsByTabId: {
|
||||
'host-tab': makeHeadlessTerminalLayout({
|
||||
[HEADLESS_LEAF_ID]: undefined,
|
||||
[HEADLESS_SECOND_LEAF_ID]: undefined
|
||||
})
|
||||
},
|
||||
sleepingAgentSessionsByPaneKey: withRecord
|
||||
? {
|
||||
[`host-tab:${HEADLESS_LEAF_ID}`]: {
|
||||
paneKey: `host-tab:${HEADLESS_LEAF_ID}`,
|
||||
tabId: 'host-tab',
|
||||
worktreeId: TEST_WORKTREE_ID,
|
||||
agent: 'codex',
|
||||
providerSession: { key: 'session_id', id: 'codex-session-1' },
|
||||
prompt: 'do the thing',
|
||||
state: 'done',
|
||||
capturedAt: 1,
|
||||
updatedAt: 1,
|
||||
origin: 'worktree-sleep'
|
||||
}
|
||||
}
|
||||
: {}
|
||||
}
|
||||
}
|
||||
|
||||
function startRuntime(session: WorkspaceSessionState): {
|
||||
runtime: OrcaRuntimeService
|
||||
spawn: ReturnType<typeof vi.fn>
|
||||
} {
|
||||
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(session)
|
||||
const spawn = vi.fn().mockResolvedValue({ id: 'started-pty' })
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture store implements the runtime store methods these tests reach.
|
||||
const runtime = new OrcaRuntimeService(runtimeStore as never)
|
||||
runtime.setPtyController({
|
||||
spawn,
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null,
|
||||
listProcesses: async () => []
|
||||
})
|
||||
runtime.syncWindowGraph(0, { tabs: [], leaves: [] })
|
||||
return { runtime, spawn }
|
||||
}
|
||||
|
||||
function spawnCommand(spawn: ReturnType<typeof vi.fn>): string {
|
||||
const args: unknown = spawn.mock.calls[0]?.[0]
|
||||
return typeof args === 'object' && args !== null && 'command' in args
|
||||
? String(args.command ?? '')
|
||||
: ''
|
||||
}
|
||||
|
||||
const activate = (runtime: OrcaRuntimeService, leafId: string) =>
|
||||
runtime.activateMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab', leafId, {
|
||||
notifyClients: false,
|
||||
navigation: 'caller',
|
||||
intent: 'user'
|
||||
})
|
||||
|
||||
describe('OrcaRuntimeService', () => {
|
||||
describe('a split agent tab starting with no live terminal', () => {
|
||||
it('starts the split sibling as a plain shell, not as a fresh agent', async () => {
|
||||
const { runtime, spawn } = startRuntime(splitAgentTabSession(true))
|
||||
|
||||
await activate(runtime, HEADLESS_SECOND_LEAF_ID)
|
||||
|
||||
expect(spawn).toHaveBeenCalledOnce()
|
||||
expect(spawnCommand(spawn)).not.toContain('codex')
|
||||
expect(spawn.mock.calls[0]?.[0]).not.toMatchObject({ launchAgent: 'codex' })
|
||||
})
|
||||
|
||||
it('still resumes the agent in the pane that recorded it', async () => {
|
||||
const { runtime, spawn } = startRuntime(splitAgentTabSession(true))
|
||||
|
||||
await activate(runtime, HEADLESS_LEAF_ID)
|
||||
|
||||
expect(spawnCommand(spawn)).toContain('codex-session-1')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,111 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { getDefaultWorkspaceSession } from '../../../shared/constants'
|
||||
import { WorktreeCreate } from '../../../shared/rpc-contract/worktree-create-params'
|
||||
import {
|
||||
buildRuntimeWorktreeCreateParams,
|
||||
type WorktreeCreateRequest
|
||||
} from '../../../renderer/src/store/slices/worktrees/create/worktree-create-payload'
|
||||
import { buildManagedWorktreeCreateArgs } from '../rpc/methods/worktree-create-args'
|
||||
import {
|
||||
OrcaRuntimeService,
|
||||
computeWorktreePathMock,
|
||||
createSetupRunnerScript,
|
||||
ensurePathWithinWorkspaceMock,
|
||||
getDefaultTabsLaunch,
|
||||
getEffectiveHooks,
|
||||
listWorktrees
|
||||
} from '../orca-runtime-test-mocks.spec'
|
||||
import {
|
||||
makeRuntimeStoreWithWorkspaceSession,
|
||||
spawnedPtyEnv,
|
||||
store
|
||||
} from '../orca-runtime-test-fixtures.spec'
|
||||
|
||||
const WORKTREE_PATH = '/tmp/workspaces/native-chat-setup'
|
||||
const RUNNER = '/tmp/repo/.git/orca/setup-runner.sh'
|
||||
const SETUP_ENV = { ORCA_WORKTREE_PATH: WORKTREE_PATH, SETUP_INPUT: 'preserved' }
|
||||
|
||||
async function createFromRendererPayload(options: {
|
||||
agent?: WorktreeCreateRequest['createdWithAgent']
|
||||
mode?: 'new-tab' | 'split-horizontal' | 'split-vertical'
|
||||
activate?: boolean
|
||||
runHooks?: boolean
|
||||
withSetup?: boolean
|
||||
defaultTabs?: boolean
|
||||
startup?: boolean
|
||||
}) {
|
||||
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(getDefaultWorkspaceSession())
|
||||
const runtime = new OrcaRuntimeService({
|
||||
...runtimeStore,
|
||||
getSettings: () => ({
|
||||
...store.getSettings(),
|
||||
...(options.mode ? { setupScriptLaunchMode: options.mode } : {})
|
||||
})
|
||||
})
|
||||
const spawn = vi.fn(async (_args: { command?: string; env?: Record<string, string> }) => ({
|
||||
id: `pty-native-chat-${spawn.mock.calls.length}`
|
||||
}))
|
||||
runtime.setPtyController({
|
||||
spawn,
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null
|
||||
})
|
||||
computeWorktreePathMock.mockReturnValue(WORKTREE_PATH)
|
||||
ensurePathWithinWorkspaceMock.mockReturnValue(WORKTREE_PATH)
|
||||
vi.mocked(listWorktrees).mockResolvedValue([
|
||||
{
|
||||
path: WORKTREE_PATH,
|
||||
head: 'def',
|
||||
branch: 'native-chat-setup',
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
}
|
||||
])
|
||||
vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: { setup: 'pnpm install' } })
|
||||
vi.mocked(createSetupRunnerScript).mockResolvedValue({
|
||||
runnerScriptPath: RUNNER,
|
||||
envVars: SETUP_ENV
|
||||
})
|
||||
if (options.defaultTabs) {
|
||||
vi.mocked(getDefaultTabsLaunch).mockReturnValue({
|
||||
runCommands: true,
|
||||
tabs: [{ title: 'Dev', command: 'pnpm dev' }]
|
||||
})
|
||||
}
|
||||
const params = WorktreeCreate.parse({
|
||||
...buildRuntimeWorktreeCreateParams(
|
||||
{
|
||||
repoId: 'repo-1',
|
||||
name: 'native-chat-setup',
|
||||
setupDecision: options.withSetup === false ? 'skip' : 'run',
|
||||
...(options.agent ? { createdWithAgent: options.agent } : {}),
|
||||
...(options.startup ? { startup: { command: 'codex' } } : {})
|
||||
},
|
||||
{ name: 'native-chat-setup' }
|
||||
),
|
||||
...(options.activate ? { activate: true } : {}),
|
||||
...(options.runHooks ? { runHooks: true } : {})
|
||||
})
|
||||
const args = buildManagedWorktreeCreateArgs(params, {}, { clientKind: 'runtime' })
|
||||
expect(args.createdWithAgent).toBe(options.agent)
|
||||
expect(args.startup?.command).toBe(options.startup ? 'codex' : undefined)
|
||||
expect(args.activate).toBe(options.activate === true || options.startup === true)
|
||||
expect(args.runHooks).toBe(options.runHooks === true)
|
||||
const result = await runtime.createManagedWorktree({ ...args, awaitTerminalProvisioning: true })
|
||||
return { result, spawn }
|
||||
}
|
||||
|
||||
describe('native-chat setup through renderer payload, RPC and execution host', () => {
|
||||
it.each(['split-horizontal', 'split-vertical'] as const)(
|
||||
'gives the requested startup and Setup separate tabs despite retired location %s',
|
||||
async (mode) => {
|
||||
const { spawn } = await createFromRendererPayload({ agent: 'codex', mode, startup: true })
|
||||
expect(spawn.mock.calls.map(([args]) => args.command)).toEqual(['codex', `bash ${RUNNER}`])
|
||||
const startupTabId = spawnedPtyEnv(spawn.mock.calls[0][0]).ORCA_TAB_ID
|
||||
expect(startupTabId).toEqual(expect.any(String))
|
||||
expect(startupTabId).not.toBe('')
|
||||
expect(startupTabId).not.toBe(spawnedPtyEnv(spawn.mock.calls[1][0]).ORCA_TAB_ID)
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -91,9 +91,7 @@ await import('./orca-runtime-tests/mobile-session-tabs-part-13.spec')
|
||||
await import('./orca-runtime-tests/mobile-session-tabs-part-14.spec')
|
||||
await import('./orca-runtime-tests/mobile-session-tabs-cold-serve-hydrate.spec')
|
||||
await import('./orca-runtime-tests/mobile-session-tabs-empty-headless-snapshot.spec')
|
||||
await import('./orca-runtime-tests/mobile-session-tabs-duplicate-pty-repair.spec')
|
||||
await import('./orca-runtime-tests/mobile-session-tabs-headless-chat-rebuild.spec')
|
||||
await import('./orca-runtime-tests/mobile-session-tabs-split-agent-tab.spec')
|
||||
await import('./orca-runtime-tests/mobile-session-tabs-windowless-sleep-resume.spec')
|
||||
await import('./orca-runtime-tests/mobile-session-tabs-targeted-agent-resume.spec')
|
||||
await import('./orca-runtime-tests/mobile-session-tabs-headless-wake-report.spec')
|
||||
@@ -122,7 +120,6 @@ await import('./orca-runtime-tests/worktree-setup-and-startup-part-02.spec')
|
||||
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/worktree-native-chat-setup.spec')
|
||||
await import('./orca-runtime-tests/gitlab-and-pr-bases.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')
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
// Use real Git to verify admin fingerprints, including Windows path/CRLF/move/lock semantics that Linux mocks cannot cover.
|
||||
import { execFile } from 'node:child_process'
|
||||
import { mkdir, mkdtemp, realpath, writeFile } from 'node:fs/promises'
|
||||
import { removeTree } from '../../shared/windows-transient-lock-removal'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { promisify } from 'node:util'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { readRepoWorktreeAdminFingerprint } from './repo-worktree-admin-fingerprint'
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
|
||||
let scratchDir = ''
|
||||
let repoPath = ''
|
||||
let worktreePath = ''
|
||||
|
||||
async function git(args: string[], cwd: string): Promise<string> {
|
||||
const { stdout } = await execFileAsync('git', args, { cwd })
|
||||
return stdout
|
||||
}
|
||||
|
||||
async function fingerprint(path = repoPath): Promise<string | null> {
|
||||
return await readRepoWorktreeAdminFingerprint(path)
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
// realpath: macOS hands out /var/... temp paths while Git records /private/var/... in `gitdir`.
|
||||
scratchDir = await realpath(await mkdtemp(join(tmpdir(), 'orca-worktree-fingerprint-')))
|
||||
repoPath = join(scratchDir, 'repo')
|
||||
worktreePath = join(scratchDir, 'trees', 'feature')
|
||||
await mkdir(repoPath, { recursive: true })
|
||||
await mkdir(join(scratchDir, 'trees'), { recursive: true })
|
||||
await git(['init', '-q'], repoPath)
|
||||
await git(['config', 'user.email', 'fingerprint@example.invalid'], repoPath)
|
||||
await git(['config', 'user.name', 'Fingerprint'], repoPath)
|
||||
await writeFile(join(repoPath, 'seed.txt'), 'seed\n')
|
||||
await git(['add', '-A'], repoPath)
|
||||
await git(['commit', '-qm', 'seed'], repoPath)
|
||||
await git(['worktree', 'add', '-q', worktreePath, '-b', 'feature'], repoPath)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await removeTree(scratchDir)
|
||||
})
|
||||
|
||||
describe('readRepoWorktreeAdminFingerprint', () => {
|
||||
it('changes when a worktree directory is deleted outside Git', async () => {
|
||||
// The admin dir is untouched by `rm -rf`, but the row's `prunable` flag flips.
|
||||
const before = await fingerprint()
|
||||
await removeTree(worktreePath)
|
||||
expect(await fingerprint()).not.toBe(before)
|
||||
})
|
||||
|
||||
it('changes when an existing worktree lock is replaced', async () => {
|
||||
await git(
|
||||
['worktree', 'lock', '--reason', 'orca-create-preparation:v1:12345:lease', worktreePath],
|
||||
repoPath
|
||||
)
|
||||
const before = await fingerprint()
|
||||
const gitDir = (await git(['rev-parse', '--absolute-git-dir'], worktreePath)).trim()
|
||||
await writeFile(join(gitDir, 'locked'), 'different user-owned lock\n')
|
||||
expect(await fingerprint()).not.toBe(before)
|
||||
})
|
||||
|
||||
it('changes when a linked worktree records a commit', async () => {
|
||||
// Committing rewrites refs/heads/feature and leaves HEAD alone, but moves the reported HEAD oid.
|
||||
const before = await fingerprint()
|
||||
await writeFile(join(worktreePath, 'work.txt'), 'work\n')
|
||||
await git(['add', '-A'], worktreePath)
|
||||
await git(['commit', '-qm', 'work'], worktreePath)
|
||||
expect(await fingerprint()).not.toBe(before)
|
||||
})
|
||||
|
||||
it('still tracks a tip whose loose ref has been packed away', async () => {
|
||||
await git(['pack-refs', '--all'], repoPath)
|
||||
const before = await fingerprint()
|
||||
await writeFile(join(worktreePath, 'packed.txt'), 'packed\n')
|
||||
await git(['add', '-A'], worktreePath)
|
||||
await git(['commit', '-qm', 'packed'], worktreePath)
|
||||
expect(await fingerprint()).not.toBe(before)
|
||||
})
|
||||
|
||||
it('reads a bare repo through its own gitdir', async () => {
|
||||
const barePath = join(scratchDir, 'bare.git')
|
||||
await git(['clone', '-q', '--bare', repoPath, barePath], scratchDir)
|
||||
const before = await fingerprint(barePath)
|
||||
expect(before).not.toBeNull()
|
||||
await git(
|
||||
['worktree', 'add', '-q', join(scratchDir, 'trees', 'from-bare'), '-b', 'bare-tree'],
|
||||
barePath
|
||||
)
|
||||
expect(await fingerprint(barePath)).not.toBe(before)
|
||||
})
|
||||
})
|
||||
@@ -1,14 +1,10 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import nacl from 'tweetnacl'
|
||||
import {
|
||||
generateKeyPair,
|
||||
deriveSharedKey,
|
||||
encrypt,
|
||||
decrypt,
|
||||
encryptBytes,
|
||||
decryptBytes,
|
||||
MAX_E2EE_ENCRYPTED_BASE64_CHARACTERS,
|
||||
publicKeyFromBase64
|
||||
decryptBytes
|
||||
} from '../../../shared/remote-runtime/e2ee/node/e2ee-crypto'
|
||||
import { MOBILE_E2EE_LEGACY_FIXTURE } from '../../../shared/remote-runtime/e2ee/mobile-e2ee-legacy-fixtures'
|
||||
|
||||
@@ -28,78 +24,8 @@ describe('e2ee-crypto', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('decrypt returns null with wrong key', () => {
|
||||
const server = generateKeyPair()
|
||||
const client = generateKeyPair()
|
||||
const attacker = generateKeyPair()
|
||||
|
||||
const clientShared = deriveSharedKey(client.secretKey, server.publicKey)
|
||||
const attackerShared = deriveSharedKey(attacker.secretKey, server.publicKey)
|
||||
|
||||
const encrypted = encrypt('secret data', clientShared)
|
||||
expect(decrypt(encrypted, attackerShared)).toBeNull()
|
||||
})
|
||||
|
||||
it('each encryption produces unique ciphertext (random nonce)', () => {
|
||||
const server = generateKeyPair()
|
||||
const client = generateKeyPair()
|
||||
const shared = deriveSharedKey(client.secretKey, server.publicKey)
|
||||
|
||||
const message = 'same message'
|
||||
const a = encrypt(message, shared)
|
||||
const b = encrypt(message, shared)
|
||||
|
||||
expect(a).not.toBe(b)
|
||||
})
|
||||
|
||||
it('handles unicode content', () => {
|
||||
const server = generateKeyPair()
|
||||
const client = generateKeyPair()
|
||||
const shared = deriveSharedKey(client.secretKey, server.publicKey)
|
||||
|
||||
const message = '日本語テスト 🎉 émojis'
|
||||
const encrypted = encrypt(message, shared)
|
||||
expect(decrypt(encrypted, shared)).toBe(message)
|
||||
})
|
||||
|
||||
it('decrypt returns null for truncated data', () => {
|
||||
const shared = deriveSharedKey(generateKeyPair().secretKey, generateKeyPair().publicKey)
|
||||
expect(decrypt('dG9vc2hvcnQ=', shared)).toBeNull()
|
||||
})
|
||||
|
||||
it('rejects oversized encoded inputs before base64 decoding', () => {
|
||||
const shared = deriveSharedKey(generateKeyPair().secretKey, generateKeyPair().publicKey)
|
||||
const decode = vi.spyOn(Buffer, 'from')
|
||||
|
||||
expect(() => publicKeyFromBase64('A'.repeat(45))).toThrow('encoded value is too large')
|
||||
expect(decrypt('A'.repeat(MAX_E2EE_ENCRYPTED_BASE64_CHARACTERS + 1), shared)).toBeNull()
|
||||
expect(decode).not.toHaveBeenCalled()
|
||||
decode.mockRestore()
|
||||
})
|
||||
|
||||
it('decrypt returns null for tampered data', () => {
|
||||
const server = generateKeyPair()
|
||||
const client = generateKeyPair()
|
||||
const shared = deriveSharedKey(client.secretKey, server.publicKey)
|
||||
|
||||
const encrypted = encrypt('hello', shared)
|
||||
const bytes = Buffer.from(encrypted, 'base64')
|
||||
bytes[bytes.length - 1] ^= 0xff
|
||||
const tampered = bytes.toString('base64')
|
||||
|
||||
expect(decrypt(tampered, shared)).toBeNull()
|
||||
})
|
||||
|
||||
it('encrypt/decrypt round-trips raw bytes for binary terminal frames', () => {
|
||||
const server = generateKeyPair()
|
||||
const client = generateKeyPair()
|
||||
const serverShared = deriveSharedKey(server.secretKey, client.publicKey)
|
||||
const clientShared = deriveSharedKey(client.secretKey, server.publicKey)
|
||||
const payload = new Uint8Array([0, 1, 2, 127, 128, 255])
|
||||
|
||||
const encrypted = encryptBytes(payload, clientShared)
|
||||
const decrypted = decryptBytes(encrypted, serverShared)
|
||||
|
||||
expect(decrypted).toEqual(payload)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getTerminalPasteIngestMs } from '../../../../../../shared/agent-launch/startup/agent-prompt-injection'
|
||||
import { ORCHESTRATION_WORKER_START_CLIENT_GRACE_MS } from '../../../../../../shared/orchestration-timing-budgets'
|
||||
import {
|
||||
isWorkerStartTaskSpecTooLarge,
|
||||
ORCHESTRATION_WORKER_START_PROMPT_MAX_BYTES,
|
||||
ORCHESTRATION_WORKER_START_TASK_SPEC_MAX_BYTES
|
||||
} from '../../../../../../shared/orchestration-worker-start-prompt-budget'
|
||||
import { getTerminalInputByteLength } from '../../../../../../shared/terminal/input/terminal-input'
|
||||
import { buildDispatchPreamble } from '../../../../orchestration/preamble'
|
||||
|
||||
describe('worker-start prompt budget', () => {
|
||||
it('refuses an 8 MiB Task spec whose fake-Windows ingest outlives RPC grace', async () => {
|
||||
const spec = 'x'.repeat(8 * 1024 * 1024)
|
||||
const prompt = buildDispatchPreamble({
|
||||
taskId: 'task_test',
|
||||
dispatchId: 'ctx_test',
|
||||
taskSpec: spec,
|
||||
coordinatorHandle: 'term_coordinator',
|
||||
workerHandle: 'term_worker'
|
||||
})
|
||||
|
||||
expect(getTerminalPasteIngestMs('win32', getTerminalInputByteLength(prompt))).toBeGreaterThan(
|
||||
ORCHESTRATION_WORKER_START_CLIENT_GRACE_MS
|
||||
)
|
||||
await expect(isWorkerStartTaskSpecTooLarge(spec)).resolves.toBe(true)
|
||||
})
|
||||
|
||||
it('keeps a maximum legal composition under the derived full-prompt ceiling', () => {
|
||||
const prompt = buildDispatchPreamble({
|
||||
taskId: `task_${'a'.repeat(32)}`,
|
||||
dispatchId: `ctx_${'b'.repeat(32)}`,
|
||||
taskSpec: 'x'.repeat(ORCHESTRATION_WORKER_START_TASK_SPEC_MAX_BYTES),
|
||||
coordinatorHandle: `term_${'d'.repeat(256)}`,
|
||||
workerHandle: `term_${'e'.repeat(256)}`,
|
||||
canDispatchSubWorkers: true
|
||||
})
|
||||
|
||||
expect(getTerminalInputByteLength(prompt)).toBeLessThanOrEqual(
|
||||
ORCHESTRATION_WORKER_START_PROMPT_MAX_BYTES
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,37 +1,10 @@
|
||||
import { gunzipSync } from 'node:zlib'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
COMPRESSED_FRAMES_CLIENT_CAPABILITY,
|
||||
readCompressedFrame
|
||||
} from '../../../shared/remote-runtime/e2ee/paired-e2ee-compressed-frame'
|
||||
import { COMPRESSED_FRAMES_CLIENT_CAPABILITY } from '../../../shared/remote-runtime/e2ee/paired-e2ee-compressed-frame'
|
||||
import { compressMobileE2EEV2Item } from './mobile-e2ee-v2-compressed-frame'
|
||||
|
||||
const capable = [COMPRESSED_FRAMES_CLIENT_CAPABILITY]
|
||||
|
||||
function inflate(item: ReturnType<typeof compressMobileE2EEV2Item>) {
|
||||
const frame = item.kind === 'binary' ? readCompressedFrame(item.plaintext) : null
|
||||
return frame && { payloadKind: frame.payloadKind, bytes: gunzipSync(frame.gzipped) }
|
||||
}
|
||||
|
||||
describe('compressMobileE2EEV2Item', () => {
|
||||
it('sends large text to a capable phone as a compressed frame that inflates to the same text', () => {
|
||||
const plaintext = JSON.stringify({ rows: Array.from({ length: 200 }, (_, i) => `ü-row-${i}`) })
|
||||
|
||||
const inflated = inflate(compressMobileE2EEV2Item({ kind: 'text', plaintext }, capable))
|
||||
|
||||
expect(inflated?.payloadKind).toBe('text')
|
||||
expect(inflated?.bytes.toString('utf8')).toBe(plaintext)
|
||||
})
|
||||
|
||||
it('compresses binary only when its sender marks it compressible', () => {
|
||||
const terminal = { kind: 'binary' as const, plaintext: new Uint8Array(4096).fill(0x74) }
|
||||
|
||||
expect(compressMobileE2EEV2Item(terminal, capable)).toBe(terminal)
|
||||
const inflated = inflate(compressMobileE2EEV2Item(terminal, capable, { compressible: true }))
|
||||
expect(inflated?.payloadKind).toBe('binary')
|
||||
expect(new Uint8Array(inflated!.bytes)).toEqual(terminal.plaintext)
|
||||
})
|
||||
|
||||
it('leaves frames alone for phones without the capability and for small frames', () => {
|
||||
const large = { kind: 'text' as const, plaintext: 'x'.repeat(4096) }
|
||||
const small = { kind: 'text' as const, plaintext: '{"id":"1"}' }
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
OrcaRuntimeService,
|
||||
listWorktrees,
|
||||
listWorktreesStrict,
|
||||
listWorktreesSharedStrict
|
||||
} from './orca-runtime-test-mocks.spec'
|
||||
import { store, TEST_WORKTREE_ID } from './orca-runtime-test-fixtures.spec'
|
||||
import { parseWorkspaceReferenceUrl } from '../../shared/workspace-attachments/workspace-reference-identity'
|
||||
|
||||
vi.mock('./agent-session/records/agent-session-record-store-slot', () => ({
|
||||
peekOpenedAgentSessionRecordStore: () => null,
|
||||
openAgentSessionRecordStoreOnce: async () => ({
|
||||
store: { listConversations: () => [], getConversation: () => null }
|
||||
})
|
||||
}))
|
||||
|
||||
const linear = 'https://linear.app/acme/issue/STA-1234'
|
||||
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
|
||||
function setup(Runtime: typeof OrcaRuntimeService = OrcaRuntimeService) {
|
||||
const base = store.getAllWorktreeMeta()[TEST_WORKTREE_ID]
|
||||
const linkedItems = [parseWorkspaceReferenceUrl(linear)]
|
||||
const runtimeStore = {
|
||||
...store,
|
||||
getAllWorktreeMeta: () => ({
|
||||
[TEST_WORKTREE_ID]: { ...base, linkedItems },
|
||||
'repo-2::/elsewhere': { ...base, displayName: 'other', linkedItems }
|
||||
}),
|
||||
getRepos: () => [
|
||||
...store.getRepos(),
|
||||
{ id: 'repo-2', path: '/elsewhere', displayName: 'other', badgeColor: '', addedAt: 0 }
|
||||
]
|
||||
}
|
||||
vi.mocked(listWorktrees).mockClear()
|
||||
vi.mocked(listWorktreesStrict).mockClear()
|
||||
vi.mocked(listWorktreesSharedStrict).mockClear()
|
||||
return new Runtime(runtimeStore)
|
||||
}
|
||||
|
||||
describe('reference find runtime integration', () => {
|
||||
it('searches cold saved metadata across repos without a Git scan', async () => {
|
||||
const runtime = setup()
|
||||
const result = await runtime.findWorkspaceReferences({ query: 'STA-1234' })
|
||||
expect(result.matches.map(({ workspace }) => workspace.id)).toEqual([
|
||||
TEST_WORKTREE_ID,
|
||||
'repo-2::/elsewhere'
|
||||
])
|
||||
expect(listWorktrees).not.toHaveBeenCalled()
|
||||
expect(listWorktreesStrict).not.toHaveBeenCalled()
|
||||
expect(listWorktreesSharedStrict).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('filters explicit repo and workspace selectors without populating the Git cache', async () => {
|
||||
const runtime = setup()
|
||||
expect(
|
||||
(await runtime.findWorkspaceReferences({ query: linear, repo: 'id:repo-2' })).matches.map(
|
||||
({ workspace }) => workspace.id
|
||||
)
|
||||
).toEqual(['repo-2::/elsewhere'])
|
||||
expect(
|
||||
(await runtime.findWorkspaceReferences({ query: linear, worktree: 'name:foo' })).matches.map(
|
||||
({ workspace }) => workspace.id
|
||||
)
|
||||
).toEqual([TEST_WORKTREE_ID])
|
||||
expect(listWorktrees).not.toHaveBeenCalled()
|
||||
expect(listWorktreesStrict).not.toHaveBeenCalled()
|
||||
expect(listWorktreesSharedStrict).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('warms the worktree cache when a branch selector misses cold metadata', async () => {
|
||||
const discovered = {
|
||||
path: '/tmp/worktree-a',
|
||||
head: 'abc',
|
||||
branch: 'refs/heads/feat',
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
}
|
||||
class ScannedRuntime extends OrcaRuntimeService {
|
||||
protected override async listRepoWorktreesForResolution(repo: { id: string }) {
|
||||
return { ok: true as const, worktrees: repo.id === 'repo-2' ? [] : [discovered] }
|
||||
}
|
||||
}
|
||||
const runtime = setup(ScannedRuntime)
|
||||
const result = await runtime.findWorkspaceReferences({ query: linear, worktree: 'branch:feat' })
|
||||
expect(result.matches.map(({ workspace }) => workspace.id)).toEqual([TEST_WORKTREE_ID])
|
||||
})
|
||||
})
|
||||
@@ -76,14 +76,6 @@ export function registerRuntimeRemoveProjectHostScopeTests(): void {
|
||||
}
|
||||
|
||||
describe('repo.rm with the same repo id on two execution hosts', () => {
|
||||
it('removes only the resolved row when a path selector picks the SSH host copy', async () => {
|
||||
const { runtime, repos } = createRuntime()
|
||||
|
||||
await runtime.removeProject('path:/remote/dup')
|
||||
|
||||
expect(repos.map((repo) => repo.path)).toEqual(['/laptop/dup'])
|
||||
})
|
||||
|
||||
it('refuses a bare duplicated id rather than guessing a host', async () => {
|
||||
const { runtime, repos, removeProjectForHost } = createRuntime()
|
||||
|
||||
@@ -118,24 +110,4 @@ export function registerRuntimeRemoveProjectHostScopeTests(): void {
|
||||
expect(repos.map((repo) => repo.path)).toEqual(['/laptop/dup'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('projectGroup.moveProject with the same repo id on two execution hosts', () => {
|
||||
it('moves the SSH row a paired client names by its connection', async () => {
|
||||
const { runtime, repos } = createRuntime()
|
||||
|
||||
await runtime.moveProjectToGroup('dup', 'group-1', undefined, 'ssh-1')
|
||||
|
||||
expect(repos.map((repo) => repo.projectGroupId)).toEqual([undefined, 'group-1'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('repo.update with the same repo id on two execution hosts', () => {
|
||||
it('writes the SSH row a path selector picked, not its local sibling', async () => {
|
||||
const { runtime, repos } = createRuntime()
|
||||
|
||||
await runtime.updateRepo('path:/remote/dup', { displayName: 'Renamed Remote' })
|
||||
|
||||
expect(repos.map((repo) => repo.displayName)).toEqual(['Dup Local', 'Renamed Remote'])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,59 +1,5 @@
|
||||
import { expect, it, vi } from 'vitest'
|
||||
import { createTranscriptPane, TRANSCRIPT_PANE_PTY_ID } from './agent-transcript-pane-test-harness'
|
||||
import { registerTerminalPtyExitWaiterTests } from './terminal-pty-exit-waiter.spec'
|
||||
import { registerClaudeAgentTeamsPtyExitLeakTests } from './claude-agent-teams-pty-exit-leak.spec'
|
||||
|
||||
registerTerminalPtyExitWaiterTests()
|
||||
registerClaudeAgentTeamsPtyExitLeakTests()
|
||||
|
||||
it('reports an already-exited terminal from the idle wait command', async () => {
|
||||
const originalExitCode = process.exitCode
|
||||
const { runtime, handle } = await createTranscriptPane({
|
||||
paneTitle: 'shell',
|
||||
foregroundProcess: 'bash',
|
||||
data: ''
|
||||
})
|
||||
runtime.onPtyExit(TRANSCRIPT_PANE_PTY_ID, 137, 'inc-1', { providerExitObserved: true })
|
||||
const callMock = vi.fn(
|
||||
async (method: string, params: { terminal: string; for: 'tui-idle'; timeoutMs: number }) => {
|
||||
expect(method).toBe('terminal.wait')
|
||||
return runtime.waitForTerminal(params.terminal, {
|
||||
condition: params.for,
|
||||
timeoutMs: params.timeoutMs
|
||||
})
|
||||
}
|
||||
)
|
||||
vi.doMock('../../cli/runtime-client', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
RuntimeClient: class {
|
||||
call = callMock
|
||||
}
|
||||
}))
|
||||
try {
|
||||
process.exitCode = undefined
|
||||
vi.stubEnv('ORCA_ENVIRONMENT', '')
|
||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
const cliPath = '../../cli/index'
|
||||
const { main }: { main(argv: string[], cwd: string): Promise<void> } = await import(cliPath)
|
||||
|
||||
await main(
|
||||
['terminal', 'wait', '--terminal', handle, '--for', 'tui-idle', '--timeout-ms', '25'],
|
||||
'/tmp/repo'
|
||||
)
|
||||
|
||||
expect(callMock).toHaveBeenCalledExactlyOnceWith(
|
||||
'terminal.wait',
|
||||
{ terminal: handle, for: 'tui-idle', timeoutMs: 25 },
|
||||
{ timeoutMs: 5025 }
|
||||
)
|
||||
expect(process.exitCode).toBe(1)
|
||||
expect(errorSpy.mock.calls.flat().join('\n')).toContain('terminal_exited')
|
||||
expect(runtime['terminalWaiters'].forPty(TRANSCRIPT_PANE_PTY_ID)).toHaveLength(0)
|
||||
} finally {
|
||||
process.exitCode = originalExitCode
|
||||
vi.doUnmock('../../cli/runtime-client')
|
||||
vi.unstubAllEnvs()
|
||||
vi.restoreAllMocks()
|
||||
vi.resetModules()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import { expect, it, vi } from 'vitest'
|
||||
import { identityFixture, LEAF, PANE } from '../query/terminal-identity.test-fixture'
|
||||
import {
|
||||
makeRuntimeStoreWithWorkspaceSession,
|
||||
makeWorkspaceSessionWithHeadlessTerminal,
|
||||
TEST_WORKTREE_ID
|
||||
} from '../../orca-runtime-test-fixtures.spec'
|
||||
import { recordPtySurface, forgetPtySurface } from '../../pty-recorded-surface-topology'
|
||||
import type { TerminalLeafMoveResult } from '../../../../shared/workspace-layout/terminal-leaf-move'
|
||||
|
||||
it('retires before dependent callbacks, keeps connected output idle and preserves partial writes when a callback fails', () => {
|
||||
const f = identityFixture()
|
||||
const pty = f.owner.recordPtyWorktree('pty-owned', TEST_WORKTREE_ID)
|
||||
const changes: (string | null | undefined)[] = []
|
||||
const stop = f.owner.crewChanges.subscribe(() => changes.push(pty.paneKey))
|
||||
recordPtySurface(f.owner.source, pty, 'tab-original', PANE, 3)
|
||||
expect(changes).toEqual([PANE])
|
||||
recordPtySurface(f.owner.source, pty, 'tab-original', PANE, 2)
|
||||
expect(changes).toEqual([PANE])
|
||||
f.owner.source.replaceLeaves(f.owner.leaves)
|
||||
f.owner.source.bindPty('pty-owned', 'term_owned')
|
||||
const before = changes.length
|
||||
f.owner.source.bindPty('pty-owned', 'term_owned')
|
||||
for (let i = 0; i < 30; i++) {
|
||||
f.runtime.onPtyData('pty-owned', 'output', 500 + i)
|
||||
}
|
||||
expect(changes).toHaveLength(before)
|
||||
const fail = f.owner.crewChanges.subscribe(() => {
|
||||
throw new Error('consumer_failure')
|
||||
})
|
||||
expect(() => forgetPtySurface(f.owner.source, pty)).not.toThrow()
|
||||
expect(pty.paneKey).toBeNull()
|
||||
expect(changes.at(-1)).toBeNull()
|
||||
fail()
|
||||
expect(() => {
|
||||
recordPtySurface(f.owner.source, pty, 'tab-original', PANE, 4)
|
||||
throw new Error('later_operation_failure')
|
||||
}).toThrow('later_operation_failure')
|
||||
expect(changes.at(-1)).toBe(PANE)
|
||||
stop()
|
||||
})
|
||||
|
||||
it('wakes for preallocation before any leaf or incarnation and for a real pane move only after its awaited commit', async () => {
|
||||
const harness = makeRuntimeStoreWithWorkspaceSession(makeWorkspaceSessionWithHeadlessTerminal())
|
||||
const f = identityFixture(undefined, harness.runtimeStore)
|
||||
const wake = vi.fn()
|
||||
const stop = f.owner.crewChanges.subscribe(wake)
|
||||
const old = f.runtime.createTerminalIdentityReadContext()
|
||||
expect(f.owner.getLivePtyForHandle('absent', old)).toBeNull()
|
||||
const handle = f.runtime.preAllocateHandleForPty('pty-owned')
|
||||
expect(wake).toHaveBeenCalledTimes(1)
|
||||
expect(f.owner.handleByPtyId.get('pty-owned')).toBe(handle)
|
||||
expect(f.owner.leaves.size).toBe(0)
|
||||
expect(f.owner.handleByPtyIncarnation.size).toBe(0)
|
||||
f.runtime.onPtySpawned('pty-owned')
|
||||
const pty = f.owner.recordPtyWorktree('pty-owned', TEST_WORKTREE_ID, { paneKey: PANE })
|
||||
let finish: ((result: TerminalLeafMoveResult) => void) | undefined
|
||||
const commit = vi.fn(
|
||||
() =>
|
||||
new Promise<TerminalLeafMoveResult>((resolve) => {
|
||||
finish = resolve
|
||||
})
|
||||
)
|
||||
const move = f.runtime.moveMobileSessionLeafToNewTab(
|
||||
`id:${TEST_WORKTREE_ID}`,
|
||||
{
|
||||
sourceTabId: 'tab-original',
|
||||
targetTabId: 'tab-moved',
|
||||
leafId: LEAF
|
||||
},
|
||||
commit
|
||||
)
|
||||
expect(commit).toHaveBeenCalledOnce()
|
||||
const before = wake.mock.calls.length
|
||||
expect(pty.paneKey).toBe(PANE)
|
||||
const warmed = f.runtime.createTerminalIdentityReadContext()
|
||||
f.owner.getPtyRecordForPaneKey(`tab-moved:${LEAF}`, warmed)
|
||||
if (!finish) {
|
||||
throw new Error('Expected the production move to await commit')
|
||||
}
|
||||
finish({ status: 'moved', ptyId: 'pty-owned' })
|
||||
await expect(move).resolves.toEqual({ status: 'moved', ptyId: 'pty-owned' })
|
||||
expect(pty.paneKey).toBe(`tab-moved:${LEAF}`)
|
||||
expect(wake.mock.calls.length).toBeGreaterThan(before)
|
||||
expect(
|
||||
f.owner.getPtyRecordForPaneKey(
|
||||
`tab-moved:${LEAF}`,
|
||||
f.runtime.createTerminalIdentityReadContext()
|
||||
)
|
||||
).toBe(f.owner.getPtyRecordForPaneKey(`tab-moved:${LEAF}`))
|
||||
stop()
|
||||
})
|
||||
@@ -1,25 +1,8 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { PluginHookName } from '../../../../shared/plugins/contributions/plugin-hook-contribution'
|
||||
import type { PluginHookChainOutcome } from '../../../../shared/plugins/workers/plugin-hook-chain'
|
||||
|
||||
import { setPluginHookRunner } from '../../../plugins/hooks/plugin-hook-dispatch'
|
||||
import {
|
||||
runTerminalSendPluginHook,
|
||||
terminalHookOrderKind,
|
||||
TerminalPluginHookOrder
|
||||
} from './terminal-send-plugin-hook'
|
||||
import { terminalHookOrderKind, TerminalPluginHookOrder } from './terminal-send-plugin-hook'
|
||||
|
||||
function installRunner(
|
||||
decide: (payload: unknown) => PluginHookChainOutcome | Promise<PluginHookChainOutcome>
|
||||
) {
|
||||
const run = vi.fn(async (_event: PluginHookName, payload: unknown) => decide(payload))
|
||||
setPluginHookRunner({ wants: (event) => event === 'terminal.send', run })
|
||||
return run
|
||||
}
|
||||
|
||||
const rewrite = (changes: Record<string, unknown>) => (payload: unknown) => ({
|
||||
kind: 'continue' as const,
|
||||
payload: { ...Object(payload), ...changes }
|
||||
})
|
||||
const allowed = vi.fn(async () => undefined)
|
||||
|
||||
afterEach(() => {
|
||||
@@ -27,21 +10,6 @@ afterEach(() => {
|
||||
allowed.mockClear()
|
||||
})
|
||||
|
||||
describe('terminal.send hook site', () => {
|
||||
it('refuses a rewrite the caller’s own checks would refuse, and skips them when unchanged', async () => {
|
||||
installRunner(rewrite({ text: 'claude --resume owned-by-chat' }))
|
||||
const refuse = vi.fn(async () => {
|
||||
throw new Error('legacy writer refused')
|
||||
})
|
||||
await expect(runTerminalSendPluginHook('t', { text: 'ls' }, 'driving', refuse)).rejects.toThrow(
|
||||
'legacy writer refused'
|
||||
)
|
||||
installRunner(rewrite({}))
|
||||
await runTerminalSendPluginHook('t', { text: 'ls' }, 'driving', refuse)
|
||||
expect(refuse).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
describe('TerminalPluginHookOrder', () => {
|
||||
const pass = async <A>(value: A) => value
|
||||
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { collectLeafIds } from '../../../../shared/workspace-layout/terminal-pane-layout-tree'
|
||||
import { toRuntimeExecutionHostId } from '../../../../shared/execution-host'
|
||||
import { setupLayoutOwnerRuntime, WORKTREE_ID } from '../../orca-runtime-layout-owner-start-fixture'
|
||||
|
||||
vi.mock('../../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
|
||||
|
||||
const TAB_ID = 'window-tab'
|
||||
const LEAF_ID = '33333333-3333-4333-8333-333333333333'
|
||||
|
||||
async function windowTab(runtime: ReturnType<typeof setupLayoutOwnerRuntime>['runtime']) {
|
||||
// The window drew the tab and recorded it in main under its own ids, as createTab does.
|
||||
const reply = await runtime.applyLayoutCommand({
|
||||
type: 'createTerminalTab',
|
||||
workspace: WORKTREE_ID,
|
||||
tabId: TAB_ID,
|
||||
leafId: LEAF_ID
|
||||
})
|
||||
expect(reply.ok).toBe(true)
|
||||
}
|
||||
|
||||
describe("the window's new tab, started by main in a layout-owner build", () => {
|
||||
it('starts the first pane in the tab the window wrote, in its folder, with nothing revealed', async () => {
|
||||
const { runtime, spawn, session } = setupLayoutOwnerRuntime()
|
||||
await windowTab(runtime)
|
||||
const statuses: unknown[] = []
|
||||
spawn.mockImplementationOnce(async (args) => {
|
||||
statuses.push(runtime.startStatusOf(`${args.tabId}:${args.leafId}`))
|
||||
return { id: 'pty-window' }
|
||||
})
|
||||
|
||||
const created = await runtime.startWindowTabTerminal(
|
||||
WORKTREE_ID,
|
||||
{ tabId: TAB_ID, leafId: LEAF_ID },
|
||||
{ cwd: '/workspace/sub', command: 'make dev' }
|
||||
)
|
||||
|
||||
expect(spawn).toHaveBeenCalledTimes(1)
|
||||
expect(spawn.mock.calls[0]![0]).toMatchObject({
|
||||
tabId: TAB_ID,
|
||||
leafId: LEAF_ID,
|
||||
cwd: '/workspace/sub',
|
||||
resolvedWorkspace: expect.objectContaining({
|
||||
id: WORKTREE_ID,
|
||||
path: '/workspace',
|
||||
connectionId: null
|
||||
}),
|
||||
command: 'make dev'
|
||||
})
|
||||
// Publish starting status so views cannot spawn duplicates; clear it after binding.
|
||||
expect(statuses).toEqual([{ status: 'starting' }])
|
||||
expect(runtime.startStatusOf(`${TAB_ID}:${LEAF_ID}`)).toBeNull()
|
||||
expect(created).toMatchObject({ tabId: TAB_ID, ptyId: 'pty-window', surface: 'background' })
|
||||
expect(session().tabsByWorktree[WORKTREE_ID]?.map((tab) => tab.id)).toEqual([TAB_ID])
|
||||
expect(collectLeafIds(session().terminalLayoutsByTabId[TAB_ID]?.root)).toEqual([LEAF_ID])
|
||||
})
|
||||
|
||||
it('keeps the pane showing why when its start fails, as a start that timed out does', async () => {
|
||||
const { runtime, spawn, session } = setupLayoutOwnerRuntime()
|
||||
await windowTab(runtime)
|
||||
spawn.mockRejectedValueOnce(new Error('Request timed out: createOrAttach'))
|
||||
|
||||
await expect(
|
||||
runtime.startWindowTabTerminal(WORKTREE_ID, { tabId: TAB_ID, leafId: LEAF_ID }, {})
|
||||
).rejects.toThrow('Request timed out: createOrAttach')
|
||||
|
||||
expect(session().tabsByWorktree[WORKTREE_ID]?.map((tab) => tab.id)).toEqual([TAB_ID])
|
||||
expect(runtime.startStatusOf(`${TAB_ID}:${LEAF_ID}`)).toEqual({
|
||||
status: 'failed',
|
||||
reason: 'Request timed out: createOrAttach'
|
||||
})
|
||||
})
|
||||
|
||||
it("refuses a paired server's tab, starting nothing, so the window starts it as today", async () => {
|
||||
const { runtime, spawn } = setupLayoutOwnerRuntime()
|
||||
await windowTab(runtime)
|
||||
Object.assign(runtime, {
|
||||
getWorkspaceSessionHostIdsForTab: () => [toRuntimeExecutionHostId('env-1')]
|
||||
})
|
||||
|
||||
await expect(
|
||||
runtime.startWindowTabTerminal(WORKTREE_ID, { tabId: TAB_ID, leafId: LEAF_ID }, {})
|
||||
).rejects.toThrow('host_not_owned')
|
||||
expect(spawn).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,4 @@
|
||||
import type * as WorktreeListingModule from '../git/worktrees/catalog/worktree-listing'
|
||||
// Check scan-cache gating here; real-Git fingerprint behavior lives in repo-worktree-admin-fingerprint.test.ts. Windows CI also exercises repo-path handling.
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const electronMocks = vi.hoisted(() => {
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
WORKTREE_TERMINAL_SLEEP_TIMEOUT_ERROR,
|
||||
WorktreeTerminalMutationLock
|
||||
} from './worktree-terminal-mutation-lock'
|
||||
|
||||
const KEY = 'repo::/tmp/worktree'
|
||||
|
||||
describe('WorktreeTerminalMutationLock', () => {
|
||||
it('hands the turn onward when a queued sleep abandons ahead of a spawn', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const lock = new WorktreeTerminalMutationLock()
|
||||
const releaseSpawn = await lock.acquire(KEY, 'shared')
|
||||
const sleep = lock.acquire(KEY, 'exclusive', Date.now() + 1_000)
|
||||
const rejection = expect(sleep).rejects.toThrow(WORKTREE_TERMINAL_SLEEP_TIMEOUT_ERROR)
|
||||
let laterSpawnAcquired = false
|
||||
const laterSpawn = lock.acquire(KEY, 'shared').then((release) => {
|
||||
laterSpawnAcquired = true
|
||||
return release
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1_001)
|
||||
await rejection
|
||||
// The spawn was queued behind the sleep; its abandonment must release it.
|
||||
const releaseLater = await laterSpawn
|
||||
expect(laterSpawnAcquired).toBe(true)
|
||||
releaseSpawn()
|
||||
releaseLater()
|
||||
expect(lock.trackedKeyCount).toBe(0)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,78 +0,0 @@
|
||||
import { beforeEach, expect, it, vi } from 'vitest'
|
||||
const io = vi.hoisted(() => ({ run: vi.fn(), plugins: vi.fn(async () => []) }))
|
||||
vi.mock('../wsl/execution/wsl-runner', () => ({ runWslProcess: io.run }))
|
||||
vi.mock('./claude-plugin-skill-sources-wsl', () => ({
|
||||
discoverClaudePluginSkillSourcesInWsl: io.plugins
|
||||
}))
|
||||
vi.mock('./discovery', () => ({ clearSkillRootScanCache: vi.fn(), discoverSkills: vi.fn() }))
|
||||
import { clearSkillDiscoveryCaches, discoverSkillsOnTarget } from './skill-discovery-target'
|
||||
|
||||
const target = { kind: 'wsl' as const, distro: 'Ubuntu', homeDir: '/home/test', cwd: '/repo' }
|
||||
const record = (...fields: string[]) => `${fields.join('\0')}\0`
|
||||
const encoded = Buffer.from('---\nname: shared-frontmatter\ndescription: Fixture\n---\n').toString(
|
||||
'base64'
|
||||
)
|
||||
const common = '/opt/physical/SKILL.md'
|
||||
const rows = [
|
||||
record('S', '0', '/home/test/.codex/skills/.system/bundle/SKILL.md', common, '1', encoded),
|
||||
record('S', '0', '/home/test/.codex/skills/alias-a/SKILL.md', common, '1', encoded),
|
||||
record('S', '1', '/home/test/.agents/skills/alias-b/SKILL.md', common, '1', encoded),
|
||||
...Array.from({ length: 6 }, (_, i) =>
|
||||
record(
|
||||
'S',
|
||||
'0',
|
||||
`/home/test/.codex/skills/skill-${i}/SKILL.md`,
|
||||
`/physical/skill-${i}/SKILL.md`,
|
||||
'1',
|
||||
encoded
|
||||
)
|
||||
)
|
||||
]
|
||||
const output = record('R', '0', '1') + record('R', '1', '1') + rows.join('')
|
||||
beforeEach(() => {
|
||||
clearSkillDiscoveryCaches()
|
||||
io.run.mockReset()
|
||||
io.plugins.mockClear()
|
||||
io.run.mockResolvedValue({ code: 0, timedOut: false, stdout: output, stderr: '' })
|
||||
})
|
||||
it('keeps both home aliases when a bundled canonical duplicate appears first', async () => {
|
||||
const [a, b, bundle] = await Promise.all([
|
||||
discoverSkillsOnTarget({ ...target, names: ['alias-a'], sourceKinds: ['home'] }, []),
|
||||
discoverSkillsOnTarget({ ...target, names: ['alias-b'], sourceKinds: ['home'] }, []),
|
||||
discoverSkillsOnTarget({ ...target, names: ['bundle'], sourceKinds: ['bundled'] }, [])
|
||||
])
|
||||
expect(io.run).toHaveBeenCalledTimes(1)
|
||||
expect(a.skills.map((s) => s.directoryPath)).toEqual(['/home/test/.codex/skills/alias-a'])
|
||||
expect(b.skills.map((s) => s.directoryPath)).toEqual(['/home/test/.agents/skills/alias-b'])
|
||||
expect(bundle.skills.map((s) => s.directoryPath)).toEqual([
|
||||
'/home/test/.codex/skills/.system/bundle'
|
||||
])
|
||||
expect(a.skills[0].providers).toEqual(['codex'])
|
||||
expect(b.skills[0].providers).toEqual(['agent-skills'])
|
||||
expect(a.skills[0].id).toBe(b.skills[0].id)
|
||||
expect(a.skills[0].sourceKind).toBe('home')
|
||||
})
|
||||
it('cache projections do not contaminate later aliases or source metadata', async () => {
|
||||
const first = await discoverSkillsOnTarget(
|
||||
{ ...target, names: ['alias-a'], sourceKinds: ['home'] },
|
||||
[]
|
||||
)
|
||||
first.skills[0].providers.push('claude')
|
||||
first.skills[0].rootPaths!.push('/poison')
|
||||
first.sources[0].providers.push('claude')
|
||||
const later = await discoverSkillsOnTarget(
|
||||
{ ...target, names: ['alias-a'], sourceKinds: ['home'] },
|
||||
[]
|
||||
)
|
||||
expect(later.skills[0].providers).toEqual(['codex'])
|
||||
expect(later.skills[0].rootPaths).toEqual(['/home/test/.codex/skills'])
|
||||
expect(later.sources[0].providers).not.toContain('claude')
|
||||
expect(io.run).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
it('does not cache failed scans as an empty successful observation', async () => {
|
||||
io.run.mockResolvedValueOnce({ code: 1, timedOut: false, stdout: '', stderr: 'failure' })
|
||||
const req = { ...target, names: ['alias-a'], sourceKinds: ['home' as const] }
|
||||
await expect(discoverSkillsOnTarget(req, [])).rejects.toThrow('skill-discovery-wsl-scan-failed')
|
||||
expect((await discoverSkillsOnTarget(req, [])).skills).toHaveLength(1)
|
||||
expect(io.run).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
@@ -1,98 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { OrcadMigrationCatalogPayload } from '../../shared/orcad-migration/orcad-migration-manifest'
|
||||
|
||||
import { isRecord } from '../../shared/orcad-migration/orcad-migration-manifest-fields'
|
||||
import { ownedEditorFileId } from '../../shared/workspace-session/workspace-session-pane-ownership'
|
||||
import {
|
||||
ORCAD_MIGRATION_PATH_FIELDS,
|
||||
type OrcadMigrationPathFieldKind
|
||||
} from './orcad-migration-path-fields'
|
||||
|
||||
import {
|
||||
parseFieldLocation,
|
||||
rewriteOrcadMigrationPayloadPaths,
|
||||
type OrcadMigrationPathRewriter
|
||||
} from './orcad-migration-path-rewrite'
|
||||
|
||||
const WT = 'repo-1::/src/wt'
|
||||
const SAMPLE: Record<OrcadMigrationPathFieldKind, string> = {
|
||||
absolute: '/src/a',
|
||||
'maybe-absolute': '/src/b',
|
||||
relative: 'rel/c',
|
||||
url: 'file:///src/d',
|
||||
'worktree-id': WT,
|
||||
'owner-key': `worktree:${WT}`,
|
||||
'pty-session-id': `${WT}@@pty-1`,
|
||||
'editor-file-id': ownedEditorFileId('/src/wt/f.ts', WT, 'env-1')
|
||||
}
|
||||
|
||||
/** Mark rewritten relative paths and map /src to /dst so omissions remain visible. */
|
||||
|
||||
function payloadWithEveryField(): OrcadMigrationCatalogPayload {
|
||||
const root: Record<string, unknown> = {
|
||||
repositories: [],
|
||||
projectGroups: [],
|
||||
folderWorkspaces: []
|
||||
}
|
||||
for (const field of ORCAD_MIGRATION_PATH_FIELDS) {
|
||||
place(root, parseFieldLocation(field.at), SAMPLE[field.kind])
|
||||
}
|
||||
// Sparse fixtures suffice because traversal follows the field table, not a full payload schema.
|
||||
return { repositories: [], projectGroups: [], folderWorkspaces: [], ...root }
|
||||
}
|
||||
|
||||
function place(
|
||||
node: Record<string, unknown>,
|
||||
steps: ReturnType<typeof parseFieldLocation>,
|
||||
sample: string
|
||||
): void {
|
||||
const [step, next, ...rest] = steps
|
||||
if (step?.type !== 'prop') {
|
||||
throw new Error('table locations start with a property')
|
||||
}
|
||||
if (!next) {
|
||||
node[step.name] = sample
|
||||
return
|
||||
}
|
||||
node[step.name] = placeInto(node[step.name], [next, ...rest], sample)
|
||||
}
|
||||
|
||||
function placeInto(
|
||||
existing: unknown,
|
||||
steps: ReturnType<typeof parseFieldLocation>,
|
||||
sample: string
|
||||
): unknown {
|
||||
const [step, ...rest] = steps
|
||||
if (!step) {
|
||||
return sample
|
||||
}
|
||||
if (step.type === 'items') {
|
||||
const items = Array.isArray(existing) ? existing : []
|
||||
items[0] = placeInto(items[0], rest, sample)
|
||||
return items
|
||||
}
|
||||
const record = isRecord(existing) ? existing : {}
|
||||
if (step.type === 'keys') {
|
||||
record[sample] = true
|
||||
} else if (step.type === 'values') {
|
||||
record.entry = placeInto(record.entry, rest, sample)
|
||||
} else {
|
||||
record[step.name] = placeInto(record[step.name], rest, sample)
|
||||
}
|
||||
return record
|
||||
}
|
||||
|
||||
describe('orcad migration path-rewrite stage', () => {
|
||||
it('refuses a rewrite that would merge two records', () => {
|
||||
const payload = payloadWithEveryField()
|
||||
const session = payload.dormantState?.workspaceSession
|
||||
expect(session).toBeDefined()
|
||||
session!.tabsByWorktree = { 'repo-1::/src/a': [], 'repo-1::/src/b': [] }
|
||||
const merging: OrcadMigrationPathRewriter = {
|
||||
path: (value) => (value.startsWith('/src') ? '/dst' : value)
|
||||
}
|
||||
expect(() => rewriteOrcadMigrationPayloadPaths(payload, merging)).toThrow(
|
||||
'orcad_migration_path_rewrite_key_collision'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -19,11 +19,6 @@ const MIXED: LeaseRow[] = [
|
||||
{ ptyId: 'exp', state: 'expired' },
|
||||
{ ptyId: 'ter', state: 'terminated' }
|
||||
]
|
||||
const UNRESOLVED: LeaseRow[] = [
|
||||
{ ptyId: 'det', state: 'detached' },
|
||||
{ ptyId: 'exp', state: 'expired' },
|
||||
{ ptyId: 'ter', state: 'terminated' }
|
||||
]
|
||||
|
||||
describe('terminal gate characterization: census-only verdicts', () => {
|
||||
it('a census that throws is bridged as unverifiable, never exited', async () => {
|
||||
@@ -38,47 +33,6 @@ describe('terminal gate characterization: census-only verdicts', () => {
|
||||
hostTerminals: 0
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
[
|
||||
'an attached lease',
|
||||
MIXED,
|
||||
{ verdict: 'live', ptyIds: ['att'], reason: 'terminals on this host are still running' }
|
||||
],
|
||||
[
|
||||
'detached or expired leases',
|
||||
UNRESOLVED,
|
||||
{
|
||||
verdict: 'unverifiable',
|
||||
ptyIds: ['det', 'exp'],
|
||||
reason: 'no SSH relay could confirm these terminals exited'
|
||||
}
|
||||
],
|
||||
[
|
||||
'only terminated leases',
|
||||
[{ ptyId: 'ter', state: 'terminated' }],
|
||||
{
|
||||
verdict: 'unverifiable',
|
||||
ptyIds: [],
|
||||
reason: "no census of every relay on this host's account was taken"
|
||||
}
|
||||
],
|
||||
[
|
||||
'no leases',
|
||||
[],
|
||||
{
|
||||
verdict: 'unverifiable',
|
||||
ptyIds: [],
|
||||
reason: "no census of every relay on this host's account was taken"
|
||||
}
|
||||
]
|
||||
] as const)('without a census, %s blocks', async (_label, leases, expected) => {
|
||||
for (const census of [undefined, null]) {
|
||||
await expect(
|
||||
assessOrcadMigrationTerminals(store([...leases]), 'ssh-1', census)
|
||||
).resolves.toEqual(expected)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('acting on a terminal proof', () => {
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { AgentHookServer } from '../agent-hooks/server'
|
||||
import { makePaneKey } from '../../shared/workspace-layout/stable-pane-id'
|
||||
import { AGENT_HOOK_INFER_INTERRUPT_METHOD } from '../../shared/agent-hook-listener/transport/agent-hook-interrupt-reconciliation'
|
||||
import { bindRemoteClaudeInterruptReconciliation } from './ssh-agent-hook-interrupt-reconciliation'
|
||||
|
||||
vi.mock('../telemetry/client', () => ({ track: vi.fn() }))
|
||||
vi.mock('../telemetry/cohort-classifier', () => ({ getCohortAtEmit: vi.fn(() => ({})) }))
|
||||
const PANE = makePaneKey('tab-1', '11111111-1111-4111-8111-111111111111')
|
||||
const REVISION = '11111111-1111-4111-8111-111111111112'
|
||||
|
||||
function host(revision: string | undefined) {
|
||||
const server = new AgentHookServer()
|
||||
server.ingestRemote(
|
||||
{
|
||||
paneKey: PANE,
|
||||
tabId: 'tab-1',
|
||||
source: 'claude',
|
||||
launchToken: 'launch-a',
|
||||
hostTurnRevision: revision,
|
||||
providerSession: { key: 'session_id', id: 'session-a' },
|
||||
hookEventName: 'UserPromptSubmit',
|
||||
payload: {
|
||||
state: 'working',
|
||||
prompt: 'do work',
|
||||
agentType: 'claude',
|
||||
mainAgent: { state: 'working', stateStartedAt: Date.now() }
|
||||
}
|
||||
},
|
||||
'ssh-owner'
|
||||
)
|
||||
const infer = () => {
|
||||
const row = server.getStatusSnapshotForPane(PANE)[0]
|
||||
if (!row) {
|
||||
throw new Error('Missing row')
|
||||
}
|
||||
return server.inferInterrupt({
|
||||
paneKey: PANE,
|
||||
baselineUpdatedAt: row.receivedAt,
|
||||
baselineStateStartedAt: row.stateStartedAt,
|
||||
baselinePrompt: row.prompt,
|
||||
baselineAgentType: row.agentType,
|
||||
intent: 'ctrl-c'
|
||||
})
|
||||
}
|
||||
return { server, infer }
|
||||
}
|
||||
|
||||
describe('remote interrupt mux and version fences', () => {
|
||||
it.each(['current', 'foreign', 'replaced', 'disposed', 'unsubscribed', 'legacy'])(
|
||||
'%s mux dispatches only with a matching current host proof',
|
||||
(scope) => {
|
||||
const { server, infer } = host(scope === 'legacy' ? undefined : REVISION)
|
||||
const request = vi.fn().mockResolvedValue({ applied: true })
|
||||
const mux = { request, isDisposed: () => scope === 'disposed' }
|
||||
const unsubscribe = bindRemoteClaudeInterruptReconciliation(
|
||||
server,
|
||||
mux,
|
||||
scope === 'foreign' ? 'other-host' : 'ssh-owner',
|
||||
() => scope !== 'replaced'
|
||||
)
|
||||
try {
|
||||
if (scope === 'unsubscribed') {
|
||||
unsubscribe()
|
||||
}
|
||||
expect(infer()).toBe(true)
|
||||
if (scope === 'current') {
|
||||
expect(request).toHaveBeenCalledExactlyOnceWith(AGENT_HOOK_INFER_INTERRUPT_METHOD, {
|
||||
paneKey: PANE,
|
||||
hostTurnRevision: REVISION,
|
||||
launchToken: 'launch-a',
|
||||
providerSession: { key: 'session_id', id: 'session-a' },
|
||||
intent: 'ctrl-c'
|
||||
})
|
||||
} else {
|
||||
expect(request).not.toHaveBeenCalled()
|
||||
}
|
||||
} finally {
|
||||
unsubscribe()
|
||||
server.stop()
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -6,9 +6,6 @@ vi.mock('electron', () => ({ app: { dock: { hide: vi.fn() }, setActivationPolicy
|
||||
const {
|
||||
applyBackgroundActivationPolicy,
|
||||
applyServeDesktopWindowActivationPolicy,
|
||||
isBackgroundLaunch,
|
||||
isOrcadSidecarLaunch,
|
||||
isWindowlessLaunch,
|
||||
showWindowWithoutStealingFocus
|
||||
} = await import('./foreground-activation-policy')
|
||||
|
||||
@@ -26,30 +23,6 @@ function makeWindow(destroyed = false): BrowserWindow & {
|
||||
}
|
||||
}
|
||||
|
||||
describe('isBackgroundLaunch', () => {
|
||||
it('treats the orcad browser sidecar as an explicit background launch', () => {
|
||||
expect(isOrcadSidecarLaunch({ ORCA_ORCAD_SIDECAR: '1' })).toBe(true)
|
||||
expect(isOrcadSidecarLaunch({})).toBe(false)
|
||||
expect(isBackgroundLaunch({ ORCA_ORCAD_SIDECAR: '1', ORCA_E2E_FOREGROUND: '1' })).toBe(true)
|
||||
expect(isWindowlessLaunch({ ORCA_ORCAD_SIDECAR: '1', ORCA_E2E_HEADFUL: '1' })).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isWindowlessLaunch', () => {
|
||||
it('keeps explicit background launches hidden while headful E2E can paint', () => {
|
||||
expect(isWindowlessLaunch({ ORCA_E2E_HEADLESS: '1' })).toBe(true)
|
||||
expect(isWindowlessLaunch({ ORCA_E2E_HEADLESS: '1', ORCA_E2E_HEADFUL: '1' })).toBe(false)
|
||||
expect(isWindowlessLaunch({ ORCA_BACKGROUND_LAUNCH: '1' })).toBe(true)
|
||||
expect(
|
||||
isWindowlessLaunch({
|
||||
ORCA_BACKGROUND_LAUNCH: '1',
|
||||
ORCA_E2E_HEADFUL: '1',
|
||||
ORCA_E2E_FOREGROUND: '1'
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('showWindowWithoutStealingFocus', () => {
|
||||
it('never reveals an explicitly background window', () => {
|
||||
const window = makeWindow()
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { statMock } = vi.hoisted(() => ({
|
||||
statMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('node:fs/promises', () => ({ stat: statMock }))
|
||||
|
||||
import {
|
||||
LOCAL_WORKTREE_PATH_PROBE_TIMEOUT_MS,
|
||||
localWorktreePathsExistOrAreUnverifiable
|
||||
} from './local-worktree-path-presence'
|
||||
|
||||
function fsError(code: string): NodeJS.ErrnoException {
|
||||
return Object.assign(new Error(code), { code })
|
||||
}
|
||||
|
||||
function installFakeAbortSignalTimeout(): AbortController[] {
|
||||
const controllers: AbortController[] = []
|
||||
vi.spyOn(AbortSignal, 'timeout').mockImplementation((timeoutMs) => {
|
||||
const controller = new AbortController()
|
||||
controllers.push(controller)
|
||||
setTimeout(
|
||||
() => controller.abort(new DOMException('The operation timed out.', 'TimeoutError')),
|
||||
timeoutMs
|
||||
)
|
||||
return controller.signal
|
||||
})
|
||||
return controllers
|
||||
}
|
||||
|
||||
describe('local worktree path presence', () => {
|
||||
beforeEach(() => {
|
||||
statMock.mockReset()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('returns one result per distinct path and only treats ENOENT/ENOTDIR as missing', async () => {
|
||||
statMock.mockImplementation(async (pathValue: string) => {
|
||||
if (pathValue === '/missing') {
|
||||
throw fsError('ENOENT')
|
||||
}
|
||||
if (pathValue === '/not-a-directory') {
|
||||
throw fsError('ENOTDIR')
|
||||
}
|
||||
if (pathValue === '/permission-denied') {
|
||||
throw fsError('EACCES')
|
||||
}
|
||||
return { isDirectory: () => true }
|
||||
})
|
||||
|
||||
const result = await localWorktreePathsExistOrAreUnverifiable([
|
||||
'/present',
|
||||
'/missing',
|
||||
'/not-a-directory',
|
||||
'/permission-denied',
|
||||
'/present'
|
||||
])
|
||||
|
||||
expect([...result.entries()]).toEqual([
|
||||
['/present', true],
|
||||
['/missing', false],
|
||||
['/not-a-directory', false],
|
||||
['/permission-denied', true]
|
||||
])
|
||||
expect(statMock).toHaveBeenCalledTimes(4)
|
||||
})
|
||||
|
||||
it('settles timed-out batches without exceeding or leaking the global stat bound', async () => {
|
||||
vi.useFakeTimers()
|
||||
const timeoutControllers = installFakeAbortSignalTimeout()
|
||||
const releaseStats: (() => void)[] = []
|
||||
let returnMissing = false
|
||||
statMock.mockImplementation(() =>
|
||||
returnMissing
|
||||
? Promise.reject(fsError('ENOENT'))
|
||||
: new Promise((resolve) => releaseStats.push(() => resolve({ isDirectory: () => true })))
|
||||
)
|
||||
|
||||
try {
|
||||
const firstPaths = ['/first-1', '/first-2', '/first-3', '/first-4']
|
||||
const first = localWorktreePathsExistOrAreUnverifiable(firstPaths)
|
||||
let firstSettled = false
|
||||
void first.then(() => {
|
||||
firstSettled = true
|
||||
})
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(statMock).toHaveBeenCalledTimes(2)
|
||||
await vi.advanceTimersByTimeAsync(LOCAL_WORKTREE_PATH_PROBE_TIMEOUT_MS - 1)
|
||||
expect(firstSettled).toBe(false)
|
||||
await vi.advanceTimersByTimeAsync(1)
|
||||
await expect(first).resolves.toEqual(
|
||||
new Map(firstPaths.map((pathValue) => [pathValue, true]))
|
||||
)
|
||||
expect(statMock).toHaveBeenCalledTimes(2)
|
||||
|
||||
const secondPaths = ['/second-1', '/second-2', '/second-3']
|
||||
const second = localWorktreePathsExistOrAreUnverifiable(secondPaths)
|
||||
await vi.advanceTimersByTimeAsync(LOCAL_WORKTREE_PATH_PROBE_TIMEOUT_MS)
|
||||
await expect(second).resolves.toEqual(
|
||||
new Map(secondPaths.map((pathValue) => [pathValue, true]))
|
||||
)
|
||||
expect(statMock).toHaveBeenCalledTimes(2)
|
||||
|
||||
returnMissing = true
|
||||
releaseStats.splice(0).forEach((release) => release())
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
await expect(localWorktreePathsExistOrAreUnverifiable(['/after-release'])).resolves.toEqual(
|
||||
new Map([['/after-release', false]])
|
||||
)
|
||||
expect(statMock).toHaveBeenCalledTimes(3)
|
||||
} finally {
|
||||
timeoutControllers.forEach((controller) => controller.abort())
|
||||
releaseStats.splice(0).forEach((release) => release())
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -60,15 +60,6 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('releasing orphaned WSL holders at launch', () => {
|
||||
it('touches nothing for a user who never ran a WSL host', async () => {
|
||||
const { factory } = fakeHolder()
|
||||
await expect(
|
||||
releaseUnwantedWslHolders({ userDataPath, isWanted: () => false, holder: factory })
|
||||
).resolves.toEqual([])
|
||||
// No holder means no process table read and no wsl.exe.
|
||||
expect(factory).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps a live host’s holder and releases only orphans and pending removals', async () => {
|
||||
addHost('Ubuntu', 42000)
|
||||
addHost('Debian', 42001)
|
||||
|
||||
Reference in New Issue
Block a user