From 9a1bef48e4d78cdcab7758d604400f7601fd9dbb Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 2 Oct 2026 19:28:32 -0700 Subject: [PATCH] fix(cursor): resume exact conversation after startup status replay (#24670) * fix(cursor): restore exact conversation after startup snapshot * fix(terminal): preserve ready snapshot reattach and isolate bridge fixtures * test(cursor): seed the real startup bridge after module resets * test(terminal): settle startup snapshots in remote restore fixtures Signed-off-by: Neil --------- Signed-off-by: Neil --- .../server-cursor-normalization.test.ts | 13 + ...-output-restore-unavailable-banner.test.ts | 4 +- ...ty-connection-agent-session-resume.test.ts | 4 +- ...nnection-cold-restore-agent-resume.test.ts | 4 +- ...ty-connection-cold-restore-repaint.test.ts | 4 +- ...ection-cold-restore-resume-command.test.ts | 4 +- ...onnection-command-finished-cleanup.test.ts | 4 +- ...nection-command-finished-late-done.test.ts | 4 +- ...-connection-daemon-snapshot-replay.test.ts | 4 +- ...tion-deferred-reattach-live-output.test.ts | 4 +- ...connection-deferred-ssh-passphrase.test.ts | 4 +- ...-connection-deliberate-sleep-guard.test.ts | 4 +- ...nnection-direct-ssh-reattach-retry.test.ts | 4 +- ...-connection-direct-ssh-spawn-retry.test.ts | 4 +- ...onnection-foreground-agent-routing.test.ts | 4 +- ...nnection-foreground-agent-sampling.test.ts | 4 +- ...connection-foreground-process-exit.test.ts | 4 +- ...on-foreground-routing-confirmation.test.ts | 4 +- ...y-connection-foreground-write-path.test.ts | 4 +- .../pty-connection-fresh-spawn-guards.test.ts | 4 +- .../pty-connection-hibernation-wake.test.ts | 4 +- ...y-connection-hidden-atlas-recovery.test.ts | 4 +- ...tion-hidden-backlog-reconciliation.test.ts | 4 +- ...connection-hidden-backlog-snapshot.test.ts | 4 +- ...ty-connection-hidden-codex-queries.test.ts | 4 +- ...ty-connection-hidden-delivery-gate.test.ts | 4 +- ...y-connection-hidden-output-restore.test.ts | 4 +- ...tion-hidden-query-snapshot-restore.test.ts | 4 +- ...ection-hidden-restore-fit-overflow.test.ts | 4 +- ...ction-hidden-snapshot-live-overlap.test.ts | 4 +- ...ion-hidden-snapshot-resize-signals.test.ts | 4 +- ...nection-hidden-tui-snapshot-replay.test.ts | 4 +- ...n-hook-completion-bell-arbitration.test.ts | 4 +- ...ction-hook-completion-side-effects.test.ts | 4 +- ...y-connection-hook-idle-arbitration.test.ts | 4 +- ...pty-connection-interrupt-inference.test.ts | 4 +- ...-connection-kitty-keyboard-restore.test.ts | 4 +- ...nection-main-side-effect-authority.test.ts | 4 +- ...connection-mode-2031-subscriptions.test.ts | 4 +- ...ction-notification-settings-gating.test.ts | 4 +- ...pty-connection-parked-ssh-snapshot.test.ts | 4 +- ...y-connection-pi-synchronized-input.test.ts | 4 +- ...n-post-dispose-restore-termination.test.ts | 4 +- .../pty-connection-pty-exit-teardown.test.ts | 4 +- ...-connection-queued-startup-consume.test.ts | 4 +- .../pty-connection-reattach-binding.test.ts | 4 +- ...pty-connection-reattach-mode-reset.test.ts | 4 +- ...y-connection-remote-runtime-attach.test.ts | 4 +- ...ection-remote-snapshot-source-grid.test.ts | 4 +- ...y-connection-renderer-risk-repaint.test.ts | 4 +- ...connection-replay-payload-handling.test.ts | 4 +- ...ection-restored-baseline-shortfall.test.ts | 4 +- ...ection-runtime-owner-spawn-routing.test.ts | 4 +- .../pty-connection-session-liveness.test.ts | 4 +- .../pty-connection-setup-split-spawn.test.ts | 4 +- ...-connection-sleeping-resume-banner.test.ts | 4 +- ...connection-spawn-left-pane-unbound.test.ts | 4 +- ...ty-connection-split-cwd-resolution.test.ts | 4 +- ...onnection-ssh-reattach-early-input.test.ts | 4 +- ...nection-ssh-startup-draft-delivery.test.ts | 4 +- ...-connection-stalled-hidden-restore.test.ts | 4 +- ...onnection-startup-command-delivery.test.ts | 4 +- ...-connection-task-complete-dispatch.test.ts | 4 +- ...y-connection-terminal-input-gating.test.ts | 4 +- .../pty-connection-test-environment.ts | 21 +- ...ty-connection-typed-agent-identity.test.ts | 4 +- ...nnection-unanswered-command-finish.test.ts | 4 +- ...-connection-visibility-resume-size.test.ts | 4 +- ...on-visible-pane-output-pause-latch.test.ts | 4 +- ...pty-connection-windows-cjk-repaint.test.ts | 4 +- ...-connection-windows-keyboard-reset.test.ts | 4 +- .../deferred-session-reattach-connect.ts | 43 +++ ...-session-reattach-startup-snapshot.test.ts | 147 ++++++++ .../reattach-payload-context.test.ts | 4 +- ...-payload-ssh-reconnect-model-paint.test.ts | 4 +- ...ote-hidden-output-restore-outcomes.test.ts | 51 +-- ...t-restore-unavailable-banner.repro.test.ts | 51 +-- ...ote-park-reveal-unverifiable-retry.test.ts | 4 +- .../ipc-events/agent-status-ipc-bridge.ts | 12 + .../agent-status-startup-snapshot.ts | 77 ++++ .../src/lib/ai-vault-resume-command.test.ts | 6 +- .../src/lib/ai-vault-resume-command.ts | 2 +- ...t-resume-host-authority-capability.test.ts | 8 + .../agent-resume-host-authority-capability.ts | 2 + .../remote-agent-session-launch.test.ts | 2 +- .../agent-status-cursor-checkpoint.test.ts | 39 ++ .../agent-session-resume-cursor.test.ts | 49 +++ src/shared/agent-session-resume.ts | 8 +- src/shared/protocol-version.ts | 3 + ...ent-session-live-force-exit-resume.spec.ts | 354 +++++++++++------- 90 files changed, 809 insertions(+), 367 deletions(-) create mode 100644 src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-startup-snapshot.test.ts create mode 100644 src/renderer/src/hooks/ipc-events/agent-status-startup-snapshot.ts create mode 100644 src/renderer/src/store/slices/agent-status-cursor-checkpoint.test.ts create mode 100644 src/shared/agent-session-resume-cursor.test.ts diff --git a/src/main/agent-hooks/server-cursor-normalization.test.ts b/src/main/agent-hooks/server-cursor-normalization.test.ts index b2e2472502c..8a93c52e8b1 100644 --- a/src/main/agent-hooks/server-cursor-normalization.test.ts +++ b/src/main/agent-hooks/server-cursor-normalization.test.ts @@ -27,6 +27,19 @@ afterEach(() => { }) describe('Cursor hook normalization', () => { + it('publishes the conversation identity through the owning hook store', () => { + const result = _internals.normalizeHookPayload( + 'cursor', + buildBody({ + hook_event_name: 'beforeSubmitPrompt', + prompt: 'remember the codeword', + conversation_id: 'conversation-742' + }), + 'production' + ) + expect(result?.providerSession).toEqual({ key: 'conversation_id', id: 'conversation-742' }) + }) + it('beforeSubmitPrompt maps to working and captures the prompt', () => { const result = _internals.normalizeHookPayload( 'cursor', diff --git a/src/renderer/src/components/terminal-pane/direct-ssh-hidden-output-restore-unavailable-banner.test.ts b/src/renderer/src/components/terminal-pane/direct-ssh-hidden-output-restore-unavailable-banner.test.ts index 432b6fb9692..ef219280162 100644 --- a/src/renderer/src/components/terminal-pane/direct-ssh-hidden-output-restore-unavailable-banner.test.ts +++ b/src/renderer/src/components/terminal-pane/direct-ssh-hidden-output-restore-unavailable-banner.test.ts @@ -225,14 +225,14 @@ function observeFinalPaneState(drive: SshPaneDrive): { } describe('direct-SSH hidden-output restore abandonment', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() // The reporter's client is WSL2, but the banner path is platform-independent; // pin darwin so the Windows synchronized-output scan stays out of the writes. ;(window.api.platform as unknown as Record).get = vi.fn(() => ({ diff --git a/src/renderer/src/components/terminal-pane/pty-connection-agent-session-resume.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-agent-session-resume.test.ts index 2abbb138e43..c0305769c40 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-agent-session-resume.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-agent-session-resume.test.ts @@ -131,14 +131,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-cold-restore-agent-resume.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-cold-restore-agent-resume.test.ts index 35b37855238..70157e83505 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-cold-restore-agent-resume.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-cold-restore-agent-resume.test.ts @@ -131,14 +131,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-cold-restore-repaint.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-cold-restore-repaint.test.ts index ca68810aa71..6d9c7207d22 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-cold-restore-repaint.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-cold-restore-repaint.test.ts @@ -140,14 +140,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-cold-restore-resume-command.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-cold-restore-resume-command.test.ts index 757f3260fbc..21d5a4189c3 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-cold-restore-resume-command.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-cold-restore-resume-command.test.ts @@ -132,14 +132,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.test.ts index 9c9db1ba02c..f1f56056ab9 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.test.ts @@ -141,14 +141,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-command-finished-late-done.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-command-finished-late-done.test.ts index 2b92e607d22..bddbe195003 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-command-finished-late-done.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-command-finished-late-done.test.ts @@ -132,14 +132,14 @@ function createDeps(overrides: Record = {}) { // A process-lifetime producer (OpenCode 2 `opencode run`) posts the run's Done after the // command-finished fact; the exited-agent drop must keep it, as it keeps a late hook Done. describe('command-finished drop and a Done that lands after it', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-daemon-snapshot-replay.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-daemon-snapshot-replay.test.ts index f03c99fa7dd..549f5794a15 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-daemon-snapshot-replay.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-daemon-snapshot-replay.test.ts @@ -140,14 +140,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-deferred-reattach-live-output.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-deferred-reattach-live-output.test.ts index ed38f6a31a4..ab4a3cd1414 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-deferred-reattach-live-output.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-deferred-reattach-live-output.test.ts @@ -132,14 +132,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-deferred-ssh-passphrase.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-deferred-ssh-passphrase.test.ts index 07dcdec5489..0c8d357f55b 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-deferred-ssh-passphrase.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-deferred-ssh-passphrase.test.ts @@ -138,14 +138,14 @@ function notifyStoreSubscribers(): void { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-deliberate-sleep-guard.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-deliberate-sleep-guard.test.ts index b49a80d9fe3..71cd170be08 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-deliberate-sleep-guard.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-deliberate-sleep-guard.test.ts @@ -128,14 +128,14 @@ function createDeps(overrides: Record = {}) { } describe('deliberate sleep keeps mounted panes cold', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-direct-ssh-reattach-retry.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-direct-ssh-reattach-retry.test.ts index 5f2ec0ad37a..724bcddd15e 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-direct-ssh-reattach-retry.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-direct-ssh-reattach-retry.test.ts @@ -136,14 +136,14 @@ function createDirectSshSplitRetryCommit() { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-direct-ssh-spawn-retry.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-direct-ssh-spawn-retry.test.ts index 298d15d3eeb..6faa8c3867d 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-direct-ssh-spawn-retry.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-direct-ssh-spawn-retry.test.ts @@ -138,14 +138,14 @@ function createDirectSshSplitRetryCommit() { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-routing.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-routing.test.ts index 31cbe3ed444..5954b164b60 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-routing.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-routing.test.ts @@ -139,14 +139,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-sampling.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-sampling.test.ts index f37de995404..54fcc4e898a 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-sampling.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-sampling.test.ts @@ -140,14 +140,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-foreground-process-exit.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-foreground-process-exit.test.ts index 719931b6971..6611360729a 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-foreground-process-exit.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-foreground-process-exit.test.ts @@ -129,14 +129,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty process-exit retirement', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-foreground-routing-confirmation.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-foreground-routing-confirmation.test.ts index 5704fc828f0..c93ef65df38 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-foreground-routing-confirmation.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-foreground-routing-confirmation.test.ts @@ -138,14 +138,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-foreground-write-path.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-foreground-write-path.test.ts index f2dfe0bd637..9975a9b330a 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-foreground-write-path.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-foreground-write-path.test.ts @@ -132,14 +132,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-fresh-spawn-guards.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-fresh-spawn-guards.test.ts index 408ce503b7e..1f0935b8085 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-fresh-spawn-guards.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-fresh-spawn-guards.test.ts @@ -130,14 +130,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-hibernation-wake.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-hibernation-wake.test.ts index 7855e756226..81e57fc6ab5 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-hibernation-wake.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-hibernation-wake.test.ts @@ -130,14 +130,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-hidden-atlas-recovery.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-hidden-atlas-recovery.test.ts index 81891e3d99d..9eac7307886 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-hidden-atlas-recovery.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-hidden-atlas-recovery.test.ts @@ -135,14 +135,14 @@ function expectNoGlobalAtlasRecovery(): void { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-reconciliation.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-reconciliation.test.ts index c08315150ac..edcc0f65849 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-reconciliation.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-reconciliation.test.ts @@ -129,14 +129,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-snapshot.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-snapshot.test.ts index a54c78f87de..01e8554026f 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-snapshot.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-snapshot.test.ts @@ -146,14 +146,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-hidden-codex-queries.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-hidden-codex-queries.test.ts index e9443188b95..b1a2b7b5fa6 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-hidden-codex-queries.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-hidden-codex-queries.test.ts @@ -137,14 +137,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-hidden-delivery-gate.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-hidden-delivery-gate.test.ts index 844db35baee..ff7c64900bc 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-hidden-delivery-gate.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-hidden-delivery-gate.test.ts @@ -134,14 +134,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-hidden-output-restore.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-hidden-output-restore.test.ts index 5835e55fff7..c9160be5f90 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-hidden-output-restore.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-hidden-output-restore.test.ts @@ -130,14 +130,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-hidden-query-snapshot-restore.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-hidden-query-snapshot-restore.test.ts index 7351a7f7456..36a7610ccfd 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-hidden-query-snapshot-restore.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-hidden-query-snapshot-restore.test.ts @@ -128,14 +128,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-hidden-restore-fit-overflow.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-hidden-restore-fit-overflow.test.ts index 5610f557459..b6191338df1 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-hidden-restore-fit-overflow.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-hidden-restore-fit-overflow.test.ts @@ -134,14 +134,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-hidden-snapshot-live-overlap.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-hidden-snapshot-live-overlap.test.ts index 157066f306e..a85f0e79562 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-hidden-snapshot-live-overlap.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-hidden-snapshot-live-overlap.test.ts @@ -128,14 +128,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-hidden-snapshot-resize-signals.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-hidden-snapshot-resize-signals.test.ts index 9a1b488d74b..26391326702 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-hidden-snapshot-resize-signals.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-hidden-snapshot-resize-signals.test.ts @@ -130,14 +130,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-hidden-tui-snapshot-replay.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-hidden-tui-snapshot-replay.test.ts index d9e64472adc..eebe3ffbf42 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-hidden-tui-snapshot-replay.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-hidden-tui-snapshot-replay.test.ts @@ -128,14 +128,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-hook-completion-bell-arbitration.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-hook-completion-bell-arbitration.test.ts index ef49288f3eb..9168b4d817c 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-hook-completion-bell-arbitration.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-hook-completion-bell-arbitration.test.ts @@ -141,14 +141,14 @@ function enableActiveRuntimeEnvironment(environmentId = 'env-1'): void { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-hook-completion-side-effects.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-hook-completion-side-effects.test.ts index 2845be5a64d..4273e6661c7 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-hook-completion-side-effects.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-hook-completion-side-effects.test.ts @@ -149,14 +149,14 @@ function notifyStoreSubscribers(): void { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-hook-idle-arbitration.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-hook-idle-arbitration.test.ts index e7af721d19a..24c35c9e158 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-hook-idle-arbitration.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-hook-idle-arbitration.test.ts @@ -134,14 +134,14 @@ function createDeps(overrides: Record = {}) { // Why: activeRuntimeEnvironmentId exercises the remote-runtime path where the renderer still owns OSC 9999 status. describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-interrupt-inference.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-interrupt-inference.test.ts index c0527b21c65..abe8e7d3449 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-interrupt-inference.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-interrupt-inference.test.ts @@ -130,14 +130,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-kitty-keyboard-restore.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-kitty-keyboard-restore.test.ts index 9faaaf10eb3..cfbb806bb99 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-kitty-keyboard-restore.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-kitty-keyboard-restore.test.ts @@ -135,14 +135,14 @@ function createDeps(overrides: Record = {}) { // Why: xterm and the pane mirror must end every restore on the same kitty flags. describe('connectPanePty kitty keyboard restore', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-main-side-effect-authority.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-main-side-effect-authority.test.ts index d1b618e0330..f96a73320a0 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-main-side-effect-authority.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-main-side-effect-authority.test.ts @@ -145,14 +145,14 @@ function notifyStoreSubscribers(): void { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-mode-2031-subscriptions.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-mode-2031-subscriptions.test.ts index c517a3c63c7..29b82144c97 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-mode-2031-subscriptions.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-mode-2031-subscriptions.test.ts @@ -128,14 +128,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-notification-settings-gating.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-notification-settings-gating.test.ts index d20f81e3700..7b5dfd3c1ce 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-notification-settings-gating.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-notification-settings-gating.test.ts @@ -137,14 +137,14 @@ function notifyStoreSubscribers(): void { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-parked-ssh-snapshot.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-parked-ssh-snapshot.test.ts index 4b23e85e389..9587fceb7a4 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-parked-ssh-snapshot.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-parked-ssh-snapshot.test.ts @@ -139,14 +139,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-pi-synchronized-input.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-pi-synchronized-input.test.ts index 173c721d25c..9a240b32b45 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-pi-synchronized-input.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-pi-synchronized-input.test.ts @@ -132,14 +132,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-post-dispose-restore-termination.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-post-dispose-restore-termination.test.ts index bbdc4a9458a..952e6376bbc 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-post-dispose-restore-termination.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-post-dispose-restore-termination.test.ts @@ -70,13 +70,13 @@ vi.mock('./pty-transport', () => ({ })) describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-pty-exit-teardown.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-pty-exit-teardown.test.ts index 4497bf604d9..91b00e71555 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-pty-exit-teardown.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-pty-exit-teardown.test.ts @@ -164,14 +164,14 @@ function installSleepingCodexResumeState(restoredPtyId?: string) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-queued-startup-consume.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-queued-startup-consume.test.ts index a09e1584567..9a3c0571aff 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-queued-startup-consume.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-queued-startup-consume.test.ts @@ -106,14 +106,14 @@ function spawnOnConnect(transport: MockTransport, ptyId: string): void { // force-park); spending it on a later respawn drops a command queued after the first launch // (STA-4876). describe('connectPanePty queued startup consume', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-reattach-binding.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-reattach-binding.test.ts index 0d8586fcaed..ceca9af2342 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-reattach-binding.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-reattach-binding.test.ts @@ -131,14 +131,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-reattach-mode-reset.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-reattach-mode-reset.test.ts index 6c22586b59f..d5722b214a5 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-reattach-mode-reset.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-reattach-mode-reset.test.ts @@ -157,14 +157,14 @@ function enableActiveRuntimeEnvironment(environmentId = 'env-1'): void { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-remote-runtime-attach.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-remote-runtime-attach.test.ts index 23ec29e6323..cbd4464e65d 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-remote-runtime-attach.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-remote-runtime-attach.test.ts @@ -140,14 +140,14 @@ function enableActiveRuntimeEnvironment(environmentId = 'env-1'): void { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-remote-snapshot-source-grid.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-remote-snapshot-source-grid.test.ts index f4e8d8bd2bb..a07e0ac9e0d 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-remote-snapshot-source-grid.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-remote-snapshot-source-grid.test.ts @@ -183,14 +183,14 @@ async function connectRemotePane(): Promise<{ } describe('pushed remote snapshot replay grid', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-renderer-risk-repaint.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-renderer-risk-repaint.test.ts index a96c5eea47e..8bc6b3be4b1 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-renderer-risk-repaint.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-renderer-risk-repaint.test.ts @@ -135,14 +135,14 @@ function expectNoGlobalAtlasRecovery(): void { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-replay-payload-handling.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-replay-payload-handling.test.ts index 8214432ed82..698b0ee1661 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-replay-payload-handling.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-replay-payload-handling.test.ts @@ -155,14 +155,14 @@ function enableActiveRuntimeEnvironment(environmentId = 'env-1'): void { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-restored-baseline-shortfall.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-restored-baseline-shortfall.test.ts index 5ac0a49fcb6..ea599b25688 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-restored-baseline-shortfall.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-restored-baseline-shortfall.test.ts @@ -117,14 +117,14 @@ vi.mock('./pty-dispatcher', async (importOriginal) => { const BLANK_MODEL_IMAGE = '\x1b[0m\x1b[?25h\x1b[?7h' describe('restored snapshot baseline shortfall (STA-5179)', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-runtime-owner-spawn-routing.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-runtime-owner-spawn-routing.test.ts index 72efb7c104a..6234d07f23f 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-runtime-owner-spawn-routing.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-runtime-owner-spawn-routing.test.ts @@ -131,14 +131,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-session-liveness.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-session-liveness.test.ts index e86da5509c8..8ebc3d65640 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-session-liveness.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-session-liveness.test.ts @@ -158,14 +158,14 @@ function enableActiveRuntimeEnvironment(environmentId = 'env-1'): void { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-setup-split-spawn.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-setup-split-spawn.test.ts index 92438968984..8e0e238aa20 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-setup-split-spawn.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-setup-split-spawn.test.ts @@ -130,14 +130,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-sleeping-resume-banner.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-sleeping-resume-banner.test.ts index f6d0457e76a..a767092c58e 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-sleeping-resume-banner.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-sleeping-resume-banner.test.ts @@ -130,14 +130,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-spawn-left-pane-unbound.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-spawn-left-pane-unbound.test.ts index 1318d9f79e2..1f8a1c7d0fc 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-spawn-left-pane-unbound.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-spawn-left-pane-unbound.test.ts @@ -120,14 +120,14 @@ vi.mock('./pty-dispatcher', async (importOriginal) => { }) describe('fresh spawn leaves a local pane unbound', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-split-cwd-resolution.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-split-cwd-resolution.test.ts index a12dd4a0911..714a7fd6f50 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-split-cwd-resolution.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-split-cwd-resolution.test.ts @@ -79,13 +79,13 @@ vi.mock('./pty-transport', () => ({ })) describe('connectPanePty split cwd resolution', () => { - beforeEach(() => { + beforeEach(async () => { vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-ssh-reattach-early-input.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-ssh-reattach-early-input.test.ts index 9aad6bcb0fc..bc8feb1acae 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-ssh-reattach-early-input.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-ssh-reattach-early-input.test.ts @@ -112,10 +112,10 @@ function connectRestoredSshPane(): { } describe('restored SSH pane input typed while its reattach is in flight', () => { - beforeEach(() => { + beforeEach(async () => { vi.clearAllMocks() mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() seedRestoredSshTab() }) diff --git a/src/renderer/src/components/terminal-pane/pty-connection-ssh-startup-draft-delivery.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-ssh-startup-draft-delivery.test.ts index 59fdc6decd7..4d4ac3d1d4d 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-ssh-startup-draft-delivery.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-ssh-startup-draft-delivery.test.ts @@ -133,14 +133,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-stalled-hidden-restore.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-stalled-hidden-restore.test.ts index a0f06268e97..42422174f1a 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-stalled-hidden-restore.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-stalled-hidden-restore.test.ts @@ -136,14 +136,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-startup-command-delivery.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-startup-command-delivery.test.ts index f85917e28c6..c7170af8b0f 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-startup-command-delivery.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-startup-command-delivery.test.ts @@ -132,14 +132,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-task-complete-dispatch.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-task-complete-dispatch.test.ts index 25b40d86183..b6c9d1853e3 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-task-complete-dispatch.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-task-complete-dispatch.test.ts @@ -147,14 +147,14 @@ function notifyStoreSubscribers(): void { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-terminal-input-gating.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-terminal-input-gating.test.ts index 4a2d2ee1206..c26913c75d9 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-terminal-input-gating.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-terminal-input-gating.test.ts @@ -139,14 +139,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-test-environment.ts b/src/renderer/src/components/terminal-pane/pty-connection-test-environment.ts index d8a9333c359..c4b1838bb25 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-test-environment.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-test-environment.ts @@ -1,10 +1,16 @@ import { vi } from 'vitest' import { resetAgentStartupDelayedDeliveryForTests } from '@/lib/agent-startup-delayed-delivery' +import * as agentStatusStartupSnapshot from '@/hooks/ipc-events/agent-status-startup-snapshot' import { drainFakeTimerWork, flushAsyncTicks } from './pty-connection-test-async' const originalRequestAnimationFrame = globalThis.requestAnimationFrame const originalCancelAnimationFrame = globalThis.cancelAnimationFrame const originalDocument = globalThis.document +type StatusSnapshot = ReturnType< + typeof agentStatusStartupSnapshot.registerAgentStatusStartupSnapshot +> +let statusSnapshot: StatusSnapshot | undefined +let resetStatusSnapshot: StatusSnapshot | undefined export function buildAgentStatusModuleMock( actual: Record @@ -29,7 +35,16 @@ export function buildAgentStatusModuleMock( } } -export function installTerminalTestGlobals(): void { +export async function installTerminalTestGlobals(): Promise { + statusSnapshot?.dispose() + resetStatusSnapshot?.dispose() + statusSnapshot = agentStatusStartupSnapshot.registerAgentStatusStartupSnapshot() + statusSnapshot.settle() + resetStatusSnapshot = undefined + // Import after resetModules so the seeded store and connection share one bridge. + const actual = await import('@/hooks/ipc-events/agent-status-startup-snapshot') + resetStatusSnapshot = actual.registerAgentStatusStartupSnapshot() + resetStatusSnapshot.settle() ;(globalThis as unknown as { window: unknown }).window = { api: { ssh: { @@ -107,6 +122,10 @@ export async function restoreTerminalTestGlobals(): Promise { // continuation throws `ReferenceError: window is not defined` and fails the // whole file (orca#14728, CI-only because it needs a slow enough tick). await flushAsyncTicks(20) + statusSnapshot?.dispose() + resetStatusSnapshot?.dispose() + statusSnapshot = undefined + resetStatusSnapshot = undefined vi.restoreAllMocks() if (originalRequestAnimationFrame) { globalThis.requestAnimationFrame = originalRequestAnimationFrame diff --git a/src/renderer/src/components/terminal-pane/pty-connection-typed-agent-identity.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-typed-agent-identity.test.ts index 73105c0248a..05056d3c73b 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-typed-agent-identity.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-typed-agent-identity.test.ts @@ -139,14 +139,14 @@ function enableActiveRuntimeEnvironment(environmentId = 'env-1'): void { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-unanswered-command-finish.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-unanswered-command-finish.test.ts index e79d09e9857..15ca1f989fb 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-unanswered-command-finish.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-unanswered-command-finish.test.ts @@ -130,14 +130,14 @@ function createDeps(overrides: Record = {}) { } describe('command-finished cleanup when the shell check cannot answer', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-visibility-resume-size.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-visibility-resume-size.test.ts index 6885e3cdd15..f5fe39b9bae 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-visibility-resume-size.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-visibility-resume-size.test.ts @@ -129,14 +129,14 @@ function createDeps(overrides: Record = {}) { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-visible-pane-output-pause-latch.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-visible-pane-output-pause-latch.test.ts index 8cf68aa4188..230b600458d 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-visible-pane-output-pause-latch.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-visible-pane-output-pause-latch.test.ts @@ -155,13 +155,13 @@ async function connectVisibleRemotePane(): Promise<{ } describe('remote pane output pause vs visibility', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-windows-cjk-repaint.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-windows-cjk-repaint.test.ts index 36ff881de7c..c37df9c0e16 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-windows-cjk-repaint.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-windows-cjk-repaint.test.ts @@ -138,14 +138,14 @@ function expectNoGlobalAtlasRecovery(): void { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-windows-keyboard-reset.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-windows-keyboard-reset.test.ts index 22ef4f6618a..01623adb1ae 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-windows-keyboard-reset.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-windows-keyboard-reset.test.ts @@ -142,14 +142,14 @@ function notifyStoreSubscribers(): void { } describe('connectPanePty', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-connect.ts b/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-connect.ts index 3548d3f7b9f..1b1cdb92e0c 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-connect.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-connect.ts @@ -5,11 +5,54 @@ import { toProcessExitStartup } from './process-exit-startup' import { recoverUnverifiableDirectSshReattach } from './direct-ssh-reattach-recovery' import type { ConnectPanePtySession } from './connect-pane-pty-session' +import { + awaitAgentStatusStartupSnapshot, + isAgentStatusStartupSnapshotReady +} from '@/hooks/ipc-events/agent-status-startup-snapshot' + const PANE_OWNER_UNVERIFIED_ERROR = 'terminal_pane_owner_unverified' export function startDeferredSessionReattach( session: ConnectPanePtySession, deferredReattachSessionId: string +): void { + void prepareDeferredSessionReattach(session, deferredReattachSessionId).catch( + (error: unknown) => { + if (!session.disposed) { + session.reportError(error instanceof Error ? error.message : String(error)) + } + } + ) +} + +async function prepareDeferredSessionReattach( + session: ConnectPanePtySession, + deferredReattachSessionId: string +): Promise { + const generation = session.transportStreamGeneration + let waitedForSnapshot = false + if ( + !session.runtimeEnvironmentId && + !session.buildColdRestoreAgentResumeStartup() && + !isAgentStatusStartupSnapshotReady() + ) { + waitedForSnapshot = true + await awaitAgentStatusStartupSnapshot() + } + if ( + session.disposed || + generation !== session.transportStreamGeneration || + (waitedForSnapshot && + session.deps.paneTransportsRef.current.get(session.pane.id) !== session.transport) + ) { + return + } + connectDeferredSessionReattach(session, deferredReattachSessionId) +} + +function connectDeferredSessionReattach( + session: ConnectPanePtySession, + deferredReattachSessionId: string ): void { session.allowInitialIdleCacheSeed = true recordPtyConnectDiagnostic(`pane=${session.pane.id} -> REATTACH ${deferredReattachSessionId}`) diff --git a/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-startup-snapshot.test.ts b/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-startup-snapshot.test.ts new file mode 100644 index 00000000000..b5f3bdd00e1 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-startup-snapshot.test.ts @@ -0,0 +1,147 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { registerAgentStatusStartupSnapshot } from '@/hooks/ipc-events/agent-status-startup-snapshot' +import { startDeferredSessionReattach } from './deferred-session-reattach-connect' +import type { ConnectPanePtySession } from './connect-pane-pty-session' + +vi.mock('../terminal-lifecycle-diagnostics', () => ({ warnTerminalLifecycleAnomaly: vi.fn() })) +vi.mock('./direct-ssh-reattach-recovery', () => ({ recoverUnverifiableDirectSshReattach: vi.fn() })) +vi.mock('./paired-parked-terminal-restore', () => ({ isRemoteRuntimePtyId: () => false })) +vi.mock('./pty-connect-limits', () => ({ + recordPtyConnectDiagnostic: vi.fn(), + isSshSessionGoneError: () => false +})) + +function createSession() { + let startup: { + command: string + agent: string + resumeProviderSession: { key: string; id: string } + } | null = null + const transport = { connect: vi.fn().mockResolvedValue(null), getPtyId: () => 'restored-pty' } + const fixture = { + pane: { id: 1 }, + transport, + disposed: false, + transportStreamGeneration: 1, + deps: { paneTransportsRef: { current: new Map([[1, transport]]) } }, + buildColdRestoreAgentResumeStartup: () => startup, + prepaintParkedSshSnapshot: vi.fn(), + captureTransportOutputCallbacks: () => ({ generation: 1, callbacks: {} }), + beginReattachLiveDataDeferral: vi.fn(), + finishReattachLiveDataDeferral: vi.fn(), + shouldDeclareHiddenAtSpawn: () => true, + handleReattachResult: vi.fn().mockResolvedValue(true), + armDirectSshPaneRetryTimeout: vi.fn(), + reportError: vi.fn(), + isCapturedDirectSshReattachCurrent: () => true, + cols: 80, + rows: 24, + cacheKey: 'tab:leaf' + } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This fixture provides every session field read by deferred reattach; unrelated installers are not exercised. + const session = fixture as unknown as ConnectPanePtySession + return { + fixture, + session, + transport, + replay: () => { + startup = { + command: "cursor-agent '--resume' 'conversation-742'", + agent: 'cursor', + resumeProviderSession: { key: 'conversation_id', id: 'conversation-742' } + } + } + } +} + +describe('cold reattach waits for host identity replay', () => { + beforeEach(() => { + vi.useFakeTimers() + vi.stubGlobal('window', { + api: { pty: { declarePendingPaneSerializer: vi.fn().mockResolvedValue(null) } } + }) + }) + afterEach(() => { + vi.unstubAllGlobals() + vi.useRealTimers() + }) + + it('connects synchronously when host replay already applied before pane registration', () => { + const bridge = registerAgentStatusStartupSnapshot() + bridge.settle() + const { session, fixture, transport } = createSession() + fixture.deps.paneTransportsRef.current.clear() + startDeferredSessionReattach(session, 'live-pty') + expect(transport.connect).toHaveBeenCalledOnce() + fixture.deps.paneTransportsRef.current.set(1, transport) + bridge.dispose() + }) + + it('connects once with the exact conversation after the initial snapshot applies', async () => { + const bridge = registerAgentStatusStartupSnapshot() + const { session, transport, replay } = createSession() + startDeferredSessionReattach(session, 'lost-pty') + expect(transport.connect).not.toHaveBeenCalled() + replay() + bridge.settle() + await vi.advanceTimersByTimeAsync(0) + expect(transport.connect).toHaveBeenCalledOnce() + expect(transport.connect).toHaveBeenCalledWith( + expect.objectContaining({ + command: "cursor-agent '--resume' 'conversation-742'", + resumeProviderSession: { key: 'conversation_id', id: 'conversation-742' }, + initiallyHidden: true + }) + ) + bridge.dispose() + }) + + it.each(['disposed', 'replaced', 'generation'] as const)( + 'never connects an obsolete %s pane after waiting', + async (kind) => { + const bridge = registerAgentStatusStartupSnapshot() + const { session, fixture, transport } = createSession() + startDeferredSessionReattach(session, 'lost-pty') + if (kind === 'disposed') { + fixture.disposed = true + } + if (kind === 'replaced') { + fixture.deps.paneTransportsRef.current.delete(1) + } + if (kind === 'generation') { + fixture.transportStreamGeneration += 1 + } + bridge.settle() + await vi.advanceTimersByTimeAsync(0) + expect(transport.connect).not.toHaveBeenCalled() + bridge.dispose() + } + ) + + it('retains the bounded fallback when the host never answers', async () => { + const bridge = registerAgentStatusStartupSnapshot() + const { session, transport } = createSession() + startDeferredSessionReattach(session, 'lost-pty') + await vi.advanceTimersByTimeAsync(4_999) + expect(transport.connect).not.toHaveBeenCalled() + await vi.advanceTimersByTimeAsync(1) + expect(transport.connect).toHaveBeenCalledOnce() + bridge.dispose() + }) + + it('ignores an old bridge completion after replacement', async () => { + const old = registerAgentStatusStartupSnapshot() + const { session, transport, replay } = createSession() + startDeferredSessionReattach(session, 'lost-pty') + const replacement = registerAgentStatusStartupSnapshot() + old.settle() + old.dispose() + await vi.advanceTimersByTimeAsync(0) + expect(transport.connect).not.toHaveBeenCalled() + replay() + replacement.settle() + await vi.advanceTimersByTimeAsync(0) + expect(transport.connect).toHaveBeenCalledOnce() + replacement.dispose() + }) +}) diff --git a/src/renderer/src/components/terminal-pane/pty-connection/reattach-payload-context.test.ts b/src/renderer/src/components/terminal-pane/pty-connection/reattach-payload-context.test.ts index 46d2d41982d..238043f8250 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/reattach-payload-context.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/reattach-payload-context.test.ts @@ -47,7 +47,9 @@ function createContext(replay: string, attemptGeneration: number): ReattachPaylo } describe('reattach payload context', () => { - beforeEach(() => installTerminalTestGlobals()) + beforeEach(async () => { + await installTerminalTestGlobals() + }) afterEach(async () => restoreTerminalTestGlobals()) diff --git a/src/renderer/src/components/terminal-pane/pty-connection/reattach-payload-ssh-reconnect-model-paint.test.ts b/src/renderer/src/components/terminal-pane/pty-connection/reattach-payload-ssh-reconnect-model-paint.test.ts index bf14e261b2b..f9c731255c3 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/reattach-payload-ssh-reconnect-model-paint.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/reattach-payload-ssh-reconnect-model-paint.test.ts @@ -89,7 +89,9 @@ function paintedBytes(fireLog: FireLog): string { // test over sshReconnectPaintsFromModel stayed green, so the coverage has to run // through createReattachPayloadHandlers rather than the gate itself. describe('reattach payload SSH reconnect model paint', () => { - beforeEach(() => installTerminalTestGlobals()) + beforeEach(async () => { + await installTerminalTestGlobals() + }) afterEach(async () => restoreTerminalTestGlobals()) diff --git a/src/renderer/src/components/terminal-pane/remote-hidden-output-restore-outcomes.test.ts b/src/renderer/src/components/terminal-pane/remote-hidden-output-restore-outcomes.test.ts index 4bdd0bcdc64..bacf885c8fc 100644 --- a/src/renderer/src/components/terminal-pane/remote-hidden-output-restore-outcomes.test.ts +++ b/src/renderer/src/components/terminal-pane/remote-hidden-output-restore-outcomes.test.ts @@ -1,6 +1,9 @@ import type * as React from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { resetAgentStartupDelayedDeliveryForTests } from '@/lib/agent-startup-delayed-delivery' +import { + installTerminalTestGlobals, + restoreTerminalTestGlobals +} from './pty-connection-test-environment' async function flushAsyncTicks(count = 6): Promise { for (let i = 0; i < count; i++) { @@ -8,20 +11,6 @@ async function flushAsyncTicks(count = 6): Promise { } } -async function drainFakeTimerWork(limit = 20): Promise { - await flushAsyncTicks(20) - if (!vi.isFakeTimers()) { - return - } - for (let iteration = 0; iteration < limit && vi.getTimerCount() > 0; iteration += 1) { - await vi.runOnlyPendingTimersAsync() - await flushAsyncTicks(20) - } - vi.clearAllTimers() - await flushAsyncTicks(20) - vi.clearAllTimers() -} - const LEAF_1 = '11111111-1111-4111-8111-111111111111' as const const LEAF_2 = '22222222-2222-4222-8222-222222222222' as const @@ -453,13 +442,10 @@ async function advanceModernRetryProbe(): Promise { } describe('remote hidden-output restore outcomes', () => { - const originalRequestAnimationFrame = globalThis.requestAnimationFrame - const originalCancelAnimationFrame = globalThis.cancelAnimationFrame - const originalDocument = globalThis.document - - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() + await installTerminalTestGlobals() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] @@ -597,30 +583,7 @@ describe('remote hidden-output restore outcomes', () => { globalThis.cancelAnimationFrame = vi.fn() }) - afterEach(async () => { - await drainFakeTimerWork() - vi.useRealTimers() - vi.restoreAllMocks() - if (originalRequestAnimationFrame) { - globalThis.requestAnimationFrame = originalRequestAnimationFrame - } else { - delete (globalThis as { requestAnimationFrame?: typeof requestAnimationFrame }) - .requestAnimationFrame - } - if (originalCancelAnimationFrame) { - globalThis.cancelAnimationFrame = originalCancelAnimationFrame - } else { - delete (globalThis as { cancelAnimationFrame?: typeof cancelAnimationFrame }) - .cancelAnimationFrame - } - if (originalDocument) { - globalThis.document = originalDocument - } else { - delete (globalThis as { document?: Document }).document - } - delete (globalThis as unknown as { window?: unknown }).window - resetAgentStartupDelayedDeliveryForTests() - }) + afterEach(restoreTerminalTestGlobals) it('[modern] repaints a recovered visible pane from the retained host buffer', async () => { const serializeBuffer = vi.fn() diff --git a/src/renderer/src/components/terminal-pane/remote-hidden-output-restore-unavailable-banner.repro.test.ts b/src/renderer/src/components/terminal-pane/remote-hidden-output-restore-unavailable-banner.repro.test.ts index cb49a5e3509..801a259ce0f 100644 --- a/src/renderer/src/components/terminal-pane/remote-hidden-output-restore-unavailable-banner.repro.test.ts +++ b/src/renderer/src/components/terminal-pane/remote-hidden-output-restore-unavailable-banner.repro.test.ts @@ -1,6 +1,9 @@ import type * as React from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { resetAgentStartupDelayedDeliveryForTests } from '@/lib/agent-startup-delayed-delivery' +import { + installTerminalTestGlobals, + restoreTerminalTestGlobals +} from './pty-connection-test-environment' // Deterministic reproduction for issue2-hidden-output-skip: // "[Orca skipped hidden terminal output because main recovery was unavailable.]" @@ -31,20 +34,6 @@ async function flushAsyncTicks(count = 6): Promise { } } -async function drainFakeTimerWork(limit = 20): Promise { - await flushAsyncTicks(20) - if (!vi.isFakeTimers()) { - return - } - for (let iteration = 0; iteration < limit && vi.getTimerCount() > 0; iteration += 1) { - await vi.runOnlyPendingTimersAsync() - await flushAsyncTicks(20) - } - vi.clearAllTimers() - await flushAsyncTicks(20) - vi.clearAllTimers() -} - const LEAF_1 = '11111111-1111-4111-8111-111111111111' as const const LEAF_2 = '22222222-2222-4222-8222-222222222222' as const @@ -482,13 +471,10 @@ function observeFinalPaneState(drive: RemotePaneDrive): { } describe('remote hidden-output restore abandonment (issue2-hidden-output-skip)', () => { - const originalRequestAnimationFrame = globalThis.requestAnimationFrame - const originalCancelAnimationFrame = globalThis.cancelAnimationFrame - const originalDocument = globalThis.document - - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() + await installTerminalTestGlobals() transportFactoryQueue = [] createdTransportOptions = [] storeSubscribers = [] @@ -626,30 +612,7 @@ describe('remote hidden-output restore abandonment (issue2-hidden-output-skip)', globalThis.cancelAnimationFrame = vi.fn() }) - afterEach(async () => { - await drainFakeTimerWork() - vi.useRealTimers() - vi.restoreAllMocks() - if (originalRequestAnimationFrame) { - globalThis.requestAnimationFrame = originalRequestAnimationFrame - } else { - delete (globalThis as { requestAnimationFrame?: typeof requestAnimationFrame }) - .requestAnimationFrame - } - if (originalCancelAnimationFrame) { - globalThis.cancelAnimationFrame = originalCancelAnimationFrame - } else { - delete (globalThis as { cancelAnimationFrame?: typeof cancelAnimationFrame }) - .cancelAnimationFrame - } - if (originalDocument) { - globalThis.document = originalDocument - } else { - delete (globalThis as { document?: Document }).document - } - delete (globalThis as unknown as { window?: unknown }).window - resetAgentStartupDelayedDeliveryForTests() - }) + afterEach(restoreTerminalTestGlobals) // ── Mechanism (GREEN on main): pins the defective transition exactly ────── diff --git a/src/renderer/src/components/terminal-pane/remote-park-reveal-unverifiable-retry.test.ts b/src/renderer/src/components/terminal-pane/remote-park-reveal-unverifiable-retry.test.ts index 60e7079ec94..3f7630ec9e8 100644 --- a/src/renderer/src/components/terminal-pane/remote-park-reveal-unverifiable-retry.test.ts +++ b/src/renderer/src/components/terminal-pane/remote-park-reveal-unverifiable-retry.test.ts @@ -203,13 +203,13 @@ function bannerCount(writes: string[]): number { } describe('parked remote pane reveal with an unverifiable host snapshot', () => { - beforeEach(() => { + beforeEach(async () => { vi.resetModules() vi.clearAllMocks() transportFactoryQueue = [] storeSubscribers = [] mockStoreState = createInitialStoreState(() => mockStoreState) - installTerminalTestGlobals() + await installTerminalTestGlobals() }) afterEach(async () => { diff --git a/src/renderer/src/hooks/ipc-events/agent-status-ipc-bridge.ts b/src/renderer/src/hooks/ipc-events/agent-status-ipc-bridge.ts index 26058a3e9e0..4a43b0c4479 100644 --- a/src/renderer/src/hooks/ipc-events/agent-status-ipc-bridge.ts +++ b/src/renderer/src/hooks/ipc-events/agent-status-ipc-bridge.ts @@ -15,6 +15,8 @@ import type { } from './agent-status-bridge-types' import { shouldRetryPendingAgentStatusesAfterStoreUpdate } from './agent-status-pending-retry-gate' +import { registerAgentStatusStartupSnapshot } from './agent-status-startup-snapshot' + const PENDING_AGENT_STATUS_RETRY_MS = 100 const PENDING_AGENT_STATUS_TTL_MS = 15_000 const MAX_PENDING_AGENT_STATUS_EVENTS = 100 @@ -104,11 +106,15 @@ export function registerAgentStatusIpcBridge(unsubs: (() => void)[]): AgentStatu transientClearWatermarkByConnectionId, enqueuePendingAgentStatus }) + const startupSnapshot = registerAgentStatusStartupSnapshot() let snapshotRequestedForReadyWindow = false let snapshotRequestId = 0 const requestAgentStatusSnapshotIfReady = (): void => { const store = useAppStore.getState() if (!store.workspaceSessionReady) { + if (snapshotRequestedForReadyWindow) { + startupSnapshot.reset() + } snapshotRequestedForReadyWindow = false return } @@ -117,6 +123,7 @@ export function registerAgentStatusIpcBridge(unsubs: (() => void)[]): AgentStatu } const getSnapshot = window.api.agentStatus.getSnapshot if (typeof getSnapshot !== 'function') { + startupSnapshot.settle() return } snapshotRequestedForReadyWindow = true @@ -131,6 +138,7 @@ export function registerAgentStatusIpcBridge(unsubs: (() => void)[]): AgentStatu return } applyAgentStatusBatch(entries.map((data) => ({ data, replay: true }))) + startupSnapshot.settle() const getMigrationUnsupportedSnapshot = window.api.agentStatus.getMigrationUnsupportedSnapshot if (typeof getMigrationUnsupportedSnapshot !== 'function') { @@ -157,6 +165,9 @@ export function registerAgentStatusIpcBridge(unsubs: (() => void)[]): AgentStatu }) .catch((err) => { // Why: stay latched on failure; the store subscriber fires on every update, so resetting here would turn a persistent IPC failure into a retry storm (flag clears on workspaceSessionReady toggle). + if (!disposed && requestId === snapshotRequestId) { + startupSnapshot.settle() + } console.warn('[agent-status] failed to load startup snapshot:', err) }) } @@ -275,6 +286,7 @@ export function registerAgentStatusIpcBridge(unsubs: (() => void)[]): AgentStatu return { disposeAsyncState: () => { disposed = true + startupSnapshot.dispose() snapshotRequestId += 1 if (pendingAgentStatusRetryTimer !== null) { globalThis.clearTimeout(pendingAgentStatusRetryTimer) diff --git a/src/renderer/src/hooks/ipc-events/agent-status-startup-snapshot.ts b/src/renderer/src/hooks/ipc-events/agent-status-startup-snapshot.ts new file mode 100644 index 00000000000..e3c5f6d4fbe --- /dev/null +++ b/src/renderer/src/hooks/ipc-events/agent-status-startup-snapshot.ts @@ -0,0 +1,77 @@ +type SnapshotSignal = { + ready: boolean + claimed: boolean + promise: Promise + resolve: () => void +} + +function createSignal(): SnapshotSignal { + let resolve = (): void => undefined + const promise = new Promise((done) => { + resolve = done + }) + return { ready: false, claimed: false, promise, resolve } +} + +let current = createSignal() + +export function isAgentStatusStartupSnapshotReady(): boolean { + return current.ready +} + +/** Coordinates the bridge's existing replay; it retains no agent rows. */ +export function registerAgentStatusStartupSnapshot(): { + reset: () => void + settle: () => void + dispose: () => void +} { + if (current.claimed) { + current.resolve() + current = createSignal() + } + let owned = current + owned.claimed = true + return { + reset: () => { + if (current !== owned) { + return + } + owned.resolve() + owned = createSignal() + owned.claimed = true + current = owned + }, + settle: () => { + if (current !== owned) { + return + } + owned.ready = true + owned.resolve() + }, + dispose: () => { + if (current !== owned) { + return + } + owned.resolve() + current = createSignal() + } + } +} + +export async function awaitAgentStatusStartupSnapshot(timeoutMs = 5_000): Promise { + const deadline = Date.now() + timeoutMs + while (!current.ready) { + const remaining = deadline - Date.now() + if (remaining <= 0) { + return + } + let timer: ReturnType | undefined + await Promise.race([ + current.promise, + new Promise((resolve) => { + timer = setTimeout(resolve, remaining) + }) + ]) + clearTimeout(timer) + } +} diff --git a/src/renderer/src/lib/ai-vault-resume-command.test.ts b/src/renderer/src/lib/ai-vault-resume-command.test.ts index 5bfed7823db..dd4c6105abf 100644 --- a/src/renderer/src/lib/ai-vault-resume-command.test.ts +++ b/src/renderer/src/lib/ai-vault-resume-command.test.ts @@ -151,9 +151,7 @@ describe('ai vault resume command runtime', () => { ).toBe("claude '--resume' 'session one'") }) - it('follows the live Windows shell for non-resumable agents in the fallback path', () => { - // Why: agents without a TUI startup plan (e.g. cursor) queue through the - // shared-builder fallback, which must quote for the live shell too (#6152). + it('follows the live Windows shell for Cursor resume', () => { const state = makeState({ worktreePath: 'C:\\Users\\alice\\repo' }) expect( @@ -167,7 +165,7 @@ describe('ai vault resume command runtime', () => { codexHome: null } }) - ).toBe("cursor-agent --resume 'session one'") + ).toBe("cursor-agent '--yolo' '--resume' 'session one'") }) it('queues a PowerShell-valid local OMP resume by absolute transcript path', () => { diff --git a/src/renderer/src/lib/ai-vault-resume-command.ts b/src/renderer/src/lib/ai-vault-resume-command.ts index 38f9fa96227..a0789c43b96 100644 --- a/src/renderer/src/lib/ai-vault-resume-command.ts +++ b/src/renderer/src/lib/ai-vault-resume-command.ts @@ -256,7 +256,7 @@ export function getAiVaultAgentProviderSession( if (!isResumableTuiAgent(session.agent)) { return null } - if (session.agent === 'antigravity') { + if (session.agent === 'antigravity' || session.agent === 'cursor') { return { key: 'conversation_id', id: session.sessionId } } if (session.agent === 'pi' || session.agent === 'prime-agent') { diff --git a/src/renderer/src/runtime/agent-resume-host-authority-capability.test.ts b/src/renderer/src/runtime/agent-resume-host-authority-capability.test.ts index 39e3abb6e0a..fc8a80f1c9c 100644 --- a/src/renderer/src/runtime/agent-resume-host-authority-capability.test.ts +++ b/src/renderer/src/runtime/agent-resume-host-authority-capability.test.ts @@ -5,6 +5,7 @@ import { AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY, AGENT_SESSION_DSH_RESUME_RUNTIME_CAPABILITY, AGENT_SESSION_KIMI_RESUME_RUNTIME_CAPABILITY, + AGENT_SESSION_CURSOR_RESUME_RUNTIME_CAPABILITY, AGENT_SESSION_MUSE_RESUME_RUNTIME_CAPABILITY, AGENT_SESSION_OPENCODE2_RESUME_RUNTIME_CAPABILITY, AGENT_SESSION_OMP_RESUME_PATH_RUNTIME_CAPABILITY, @@ -13,6 +14,12 @@ import { import { agentResumeHostAuthorityCapability } from './agent-resume-host-authority-capability' describe('agentResumeHostAuthorityCapability', () => { + it('requires Cursor support from the owning host', () => { + expect(agentResumeHostAuthorityCapability('cursor')).toBe( + AGENT_SESSION_CURSOR_RESUME_RUNTIME_CAPABILITY + ) + expect(RUNTIME_CAPABILITIES).toContain(AGENT_SESSION_CURSOR_RESUME_RUNTIME_CAPABILITY) + }) it('gates Muse resume behind its own advertised capability', () => { expect(agentResumeHostAuthorityCapability('muse')).toBe( AGENT_SESSION_MUSE_RESUME_RUNTIME_CAPABILITY @@ -44,6 +51,7 @@ describe('agentResumeHostAuthorityCapability', () => { claude: undefined, codebuddy: AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY, codex: undefined, + cursor: AGENT_SESSION_CURSOR_RESUME_RUNTIME_CAPABILITY, qoder: AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY, gemini: undefined, antigravity: undefined, diff --git a/src/renderer/src/runtime/agent-resume-host-authority-capability.ts b/src/renderer/src/runtime/agent-resume-host-authority-capability.ts index a5cefe6a74c..4431f30b220 100644 --- a/src/renderer/src/runtime/agent-resume-host-authority-capability.ts +++ b/src/renderer/src/runtime/agent-resume-host-authority-capability.ts @@ -2,6 +2,7 @@ import type { ResumableTuiAgent } from '../../../shared/agent-session-resume' import type { TuiAgent } from '../../../shared/tui-agent' import { AGENT_SESSION_KIMI_RESUME_RUNTIME_CAPABILITY, + AGENT_SESSION_CURSOR_RESUME_RUNTIME_CAPABILITY, AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY, AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY, AGENT_SESSION_DSH_RESUME_RUNTIME_CAPABILITY, @@ -23,6 +24,7 @@ const RESUME_HOST_AUTHORITY_CAPABILITY_BY_AGENT = { claude: undefined, codebuddy: AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY, codex: undefined, + cursor: AGENT_SESSION_CURSOR_RESUME_RUNTIME_CAPABILITY, qoder: AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY, gemini: undefined, antigravity: undefined, diff --git a/src/renderer/src/runtime/remote-agent-session-launch.test.ts b/src/renderer/src/runtime/remote-agent-session-launch.test.ts index dda22ff12fd..99d09068720 100644 --- a/src/renderer/src/runtime/remote-agent-session-launch.test.ts +++ b/src/renderer/src/runtime/remote-agent-session-launch.test.ts @@ -45,7 +45,7 @@ describe('remote agent-session launch routing', () => { expect(legacy).not.toHaveBeenCalled() }) - it.each(['kimi', 'muse', 'codebuddy'] as const)( + it.each(['kimi', 'muse', 'codebuddy', 'cursor'] as const)( 'falls back to legacy when an older host lacks the %s resume capability', async (agent) => { const hostAuthority = vi.fn().mockResolvedValue('structured') diff --git a/src/renderer/src/store/slices/agent-status-cursor-checkpoint.test.ts b/src/renderer/src/store/slices/agent-status-cursor-checkpoint.test.ts new file mode 100644 index 00000000000..7ab9effad71 --- /dev/null +++ b/src/renderer/src/store/slices/agent-status-cursor-checkpoint.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest' +import { createTestStore, makeTab } from './store-test-helpers' + +describe('Cursor quit checkpoint', () => { + it.each(['working', 'done'] as const)( + 'retains the same %s conversation in a folder workspace', + (state) => { + const store = createTestStore() + store.setState({ + tabsByWorktree: { 'folder-1': [makeTab({ id: 'tab-1', worktreeId: 'folder-1' })] } + }) + store + .getState() + .setAgentStatus( + 'tab-1:leaf-1', + { state: 'working', prompt: 'remember the codeword', agentType: 'cursor' }, + 'Cursor', + { updatedAt: 10, stateStartedAt: 10 }, + { tabId: 'tab-1', worktreeId: 'folder-1' }, + { providerSession: { key: 'conversation_id', id: 'conversation-742' } } + ) + if (state === 'done') { + store.getState().setAgentStatus('tab-1:leaf-1', { + state, + prompt: 'remember the codeword', + agentType: 'cursor' + }) + } + store.getState().captureAllSleepingAgentSessions('quit') + expect(store.getState().sleepingAgentSessionsByPaneKey['tab-1:leaf-1']).toMatchObject({ + agent: 'cursor', + state, + worktreeId: 'folder-1', + providerSession: { key: 'conversation_id', id: 'conversation-742' }, + origin: state === 'done' ? 'live' : 'quit' + }) + } + ) +}) diff --git a/src/shared/agent-session-resume-cursor.test.ts b/src/shared/agent-session-resume-cursor.test.ts new file mode 100644 index 00000000000..4ddc2a5db23 --- /dev/null +++ b/src/shared/agent-session-resume-cursor.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from 'vitest' +import { + extractAgentProviderSession, + getAgentResumeArgv, + isResumableTuiAgent +} from './agent-session-resume' +import { buildAgentResumeStartupPlan } from './tui-agent-startup' + +describe('Cursor conversation continuity', () => { + it('captures only the hook conversation identity', () => { + expect(isResumableTuiAgent('cursor')).toBe(true) + expect( + extractAgentProviderSession('cursor', { + conversation_id: 'conversation-742', + session_id: 'tool-session' + }) + ).toEqual({ key: 'conversation_id', id: 'conversation-742' }) + expect(extractAgentProviderSession('cursor', { session_id: 'tool-session' })).toBeNull() + expect(extractAgentProviderSession('cursor', { conversation_id: '--continue' })).toBeNull() + expect( + extractAgentProviderSession('cursor', { conversation_id: 'session\ncommand' }) + ).toBeNull() + }) + + it('resumes the exact conversation instead of the latest one', () => { + expect( + getAgentResumeArgv('cursor', { key: 'conversation_id', id: 'conversation-742' }) + ).toEqual(['cursor-agent', '--resume', 'conversation-742']) + expect(getAgentResumeArgv('cursor', { key: 'session_id', id: 'tool-session' })).toBeNull() + }) + + it.each(['linux', 'darwin', 'win32'] as const)( + 'preserves the configured executable and launch recipe on %s', + (platform) => { + const plan = buildAgentResumeStartupPlan({ + agent: 'cursor', + providerSession: { key: 'conversation_id', id: 'conversation-742' }, + cmdOverrides: { cursor: 'custom-cursor' }, + platform, + agentArgs: '--mode ask' + }) + expect(plan?.launchCommand).toContain('custom-cursor') + expect(plan?.launchCommand).toContain('conversation-742') + expect(plan?.launchCommand).toContain('--resume') + expect(plan?.launchCommand).toContain('--mode') + expect(plan?.launchCommand).not.toContain('--continue') + } + ) +}) diff --git a/src/shared/agent-session-resume.ts b/src/shared/agent-session-resume.ts index 40a3699af3c..fb2dd302975 100644 --- a/src/shared/agent-session-resume.ts +++ b/src/shared/agent-session-resume.ts @@ -20,6 +20,7 @@ export const RESUMABLE_TUI_AGENTS = [ 'omp', 'prime-agent', 'copilot', + 'cursor', 'kimi', 'muse', 'zcode', @@ -264,8 +265,11 @@ export function extractAgentProviderSession( const id = readSessionId(payload, ['session_id', 'sessionId']) return id ? { key: 'session_id', id } : null } + case 'cursor': { + const id = readSessionId(payload, ['conversation_id']) + return id ? { key: 'conversation_id', id } : null + } case 'amp': - case 'cursor': case 'command-code': case 'hermes': return null @@ -283,6 +287,8 @@ export function getAgentResumeArgv( return providerSession.key === 'session_id' ? ['codebuddy', '--resume', id] : null case 'claude': return providerSession.key === 'session_id' ? ['claude', '--resume', id] : null + case 'cursor': + return providerSession.key === 'conversation_id' ? ['cursor-agent', '--resume', id] : null case 'codex': return providerSession.key === 'session_id' ? ['codex', 'resume', id] : null case 'qoder': diff --git a/src/shared/protocol-version.ts b/src/shared/protocol-version.ts index 7bf67847621..650bc85b449 100644 --- a/src/shared/protocol-version.ts +++ b/src/shared/protocol-version.ts @@ -249,6 +249,8 @@ export const AGENT_SESSION_BACKGROUND_TASK_ROW_STOP_CAPABILITY = // Why: adding kimi to RESUMABLE_TUI_AGENTS grows terminal.ensureAgentSession's enum, and an // older host answers the unknown member with invalid_argument — a code the launch fallback does // not retry on — so clients must probe before taking the host-authority path. +export const AGENT_SESSION_CURSOR_RESUME_RUNTIME_CAPABILITY = + 'agent-session.cursor-resume.v1' as const export const AGENT_SESSION_KIMI_RESUME_RUNTIME_CAPABILITY = 'agent-session.kimi-resume.v1' as const export const AGENT_SESSION_OPENCODE2_RESUME_RUNTIME_CAPABILITY = 'agent-session.opencode2-resume.v1' as const @@ -413,6 +415,7 @@ export const RUNTIME_CAPABILITIES = [ AGENT_SESSION_BACKGROUND_TASK_ROW_STOP_CAPABILITY, AGENT_SESSION_BACKGROUND_TASK_CHILD_VIEWS_CAPABILITY, AGENT_SESSION_KIMI_RESUME_RUNTIME_CAPABILITY, + AGENT_SESSION_CURSOR_RESUME_RUNTIME_CAPABILITY, AGENT_SESSION_OPENCODE2_RESUME_RUNTIME_CAPABILITY, AGENT_SESSION_MUSE_RESUME_RUNTIME_CAPABILITY, AGENT_SESSION_DSH_RESUME_RUNTIME_CAPABILITY, diff --git a/tests/e2e/agent-session-live-force-exit-resume.spec.ts b/tests/e2e/agent-session-live-force-exit-resume.spec.ts index b6609c29df7..2491fef72c6 100644 --- a/tests/e2e/agent-session-live-force-exit-resume.spec.ts +++ b/tests/e2e/agent-session-live-force-exit-resume.spec.ts @@ -3,10 +3,10 @@ import { mutateStoppedProfileState } from './helpers/persisted-profile-state' import { execFileSync } from 'node:child_process' -import { existsSync, readFileSync } from 'node:fs' +import { existsSync, readFileSync, writeFileSync } from 'node:fs' import path from 'node:path' import type { ChildProcess } from 'node:child_process' -import type { ElectronApplication } from '@stablyai/playwright-test' +import type { ElectronApplication, Page } from '@stablyai/playwright-test' import { test, expect } from './helpers/orca-app' import { TEST_REPO_PATH_FILE } from './global-setup' import { @@ -144,135 +144,231 @@ function persistedLiveRecordExists(userDataDir: string): boolean { ) } -test.describe.configure({ mode: 'serial' }) - -test('resumes a live agent record after force-exit restart when pane PTY ownership is gone', async (// oxlint-disable-next-line no-empty-pattern -- Playwright's second fixture arg is testInfo; the first must be an object destructure to opt out of the default fixture set. -{}, testInfo) => { - const repoPath = readFileSync(TEST_REPO_PATH_FILE, 'utf-8').trim() - if (!repoPath || !existsSync(repoPath)) { - test.skip(true, 'Global setup did not produce a seeded test repo') - return - } - - const session = createRestartSession(testInfo) - let firstApp: ElectronApplication | null = null - let secondApp: ElectronApplication | null = null - - try { - const firstLaunch = await session.launch() - firstApp = firstLaunch.app - const page = firstLaunch.page - const worktreeId = await attachRepoAndOpenTerminal(page, repoPath) - await waitForSessionReady(page) - // Why: the session writer persists only once hydrationSucceeded flips (not - // just workspaceSessionReady) — see shouldPersistWorkspaceSession — so the - // record write below is a silent no-op until hydration completes. - await expect - .poll(() => page.evaluate(() => window.__store?.getState().hydrationSucceeded === true), { - timeout: 30_000, - message: 'hydrationSucceeded did not become true before persisting the live record' - }) - .toBe(true) - await waitForActiveWorktree(page) - await ensureTerminalVisible(page) - await waitForActiveTerminalManager(page, 30_000) - await waitForPaneCount(page, 1, 30_000) - - const descriptor = await waitForActivePaneHookDescriptor(page) - const ptyId = await waitForActivePanePtyId(page) - const transcriptPath = session.seedCodexResumeRollout(PROVIDER_SESSION_ID, repoPath) - const marker = `AGENT_LIVE_FORCE_EXIT_${Date.now()}` - await execInTerminal(page, ptyId, `echo ${marker}`) - await waitForTerminalOutput(page, marker) - - await page.evaluate( - ({ paneKey, worktreeId: wtId, providerSessionId, transcriptPath }) => { - window.__store?.getState().setAgentStatus( - paneKey, - { state: 'working', prompt: 'finish the task', agentType: 'codex' }, - 'Codex', - undefined, - { worktreeId: wtId }, - { - providerSession: { - key: 'session_id', - id: providerSessionId, - transcriptPath +async function captureOutputRow(page: Page, expected: string, proofPath: string): Promise { + let clip: { x: number; y: number; width: number; height: number } | null = null + await expect + .poll( + async () => { + clip = await page.evaluate((expected) => { + const state = window.__store?.getState() + const tabId = state?.activeTabId + const manager = tabId ? window.__paneManagers?.get(tabId) : null + const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] + const screen = pane?.container.querySelector('.xterm-screen') + if (!pane || !screen) { + return null + } + const buffer = pane.terminal.buffer.active + const bounds = screen.getBoundingClientRect() + const rowHeight = bounds.height / pane.terminal.rows + for (let row = 0; row < pane.terminal.rows; row += 1) { + const text = buffer + .getLine(buffer.viewportY + row) + ?.translateToString(true) + .trim() + if (text === expected) { + return { + x: bounds.x, + y: bounds.y + row * rowHeight, + width: bounds.width, + height: rowHeight + } } } - ) + return null + }, expected) + return clip !== null }, - { - paneKey: descriptor.paneKey, - worktreeId: descriptor.worktreeId, - providerSessionId: PROVIDER_SESSION_ID, - transcriptPath - } + { timeout: 15_000 } ) - - // Exercise quit capture: origin:'quit' changes the live record, triggering the - // hydration-gated writer before polling persisted state. - await page.evaluate(() => window.__store?.getState().captureAllSleepingAgentSessions('quit')) - - // Why: the record reaches disk via the debounced session writer (150ms) plus - // the main-process scheduleSave (up to 5s). Under CI event-loop starvation — - // the same shard drifts renderer timers ~1s — both stages need headroom, so - // poll to 30s (this suite's other readiness budget). On a miss, surface store - // vs disk state to separate a lost write from a merely slow flush. - const persistDeadline = Date.now() + 30_000 - let persisted = false - while (Date.now() < persistDeadline) { - if (persistedLiveRecordExists(session.userDataDir)) { - persisted = true - break - } - await page.waitForTimeout(250) - } - if (!persisted) { - const storeRecords = await page.evaluate( - () => window.__store?.getState().sleepingAgentSessionsByPaneKey - ) - throw new Error( - `Live sleeping-agent record was not persisted before force exit. store=${JSON.stringify( - storeRecords - )} disk=${JSON.stringify( - readPersistedData(session.userDataDir).workspaceSession?.sleepingAgentSessionsByPaneKey - )}` - ) - } - - const daemonPid = readDaemonPid(session.userDataDir) - await forceKillElectronApp(firstApp) - firstApp = null - killPid(daemonPid) - stripPersistedPtyOwnership(session.userDataDir) - - const secondLaunch = await session.launch() - secondApp = secondLaunch.app - await waitForSessionReady(secondLaunch.page) - await expect - .poll( - async () => secondLaunch.page.evaluate(() => window.__store?.getState().activeWorktreeId), - { timeout: 15_000 } - ) - .toBe(worktreeId) - await ensureTerminalVisible(secondLaunch.page) - await waitForActiveTerminalManager(secondLaunch.page, 30_000) - - await waitForTerminalOutput(secondLaunch.page, PROVIDER_SESSION_ID, 30_000) - - const terminalTabCount = await secondLaunch.page.evaluate( - (wtId) => (window.__store?.getState().tabsByWorktree[wtId] ?? []).length, - worktreeId - ) - expect(terminalTabCount).toBe(2) - } finally { - if (secondApp) { - await session.close(secondApp) - } - if (firstApp) { - await forceKillElectronApp(firstApp) - } - await session.dispose() + .toBe(true) + if (!clip) { + throw new Error('Expected output row was not rendered') } -}) + await page.screenshot({ path: proofPath, clip }) +} + +test.describe.configure({ mode: 'serial' }) + +for (const agent of ['codex', 'cursor'] as const) { + const providerSessionKey = agent === 'cursor' ? 'conversation_id' : 'session_id' + test(`resumes a live ${agent} record after force-exit restart when pane PTY ownership is gone`, async (// oxlint-disable-next-line no-empty-pattern -- Playwright's second fixture arg is testInfo; the first must be an object destructure to opt out of the default fixture set. + {}, testInfo) => { + const repoPath = readFileSync(TEST_REPO_PATH_FILE, 'utf-8').trim() + if (!repoPath || !existsSync(repoPath)) { + test.skip(true, 'Global setup did not produce a seeded test repo') + return + } + + const session = createRestartSession(testInfo, { + ORCA_BACKGROUND_LAUNCH: '1', + ORCA_DISABLE_CODEX_TRUST_RPC: '1' + }) + const seedPath = path.join(session.userDataDir, 'orca-data.json') + const seed = JSON.parse(readFileSync(seedPath, 'utf8')) + seed.settings.agentStatusHooksEnabled = false + writeFileSync(seedPath, JSON.stringify(seed)) + let firstApp: ElectronApplication | null = null + let secondApp: ElectronApplication | null = null + + try { + const firstLaunch = await session.launch() + firstApp = firstLaunch.app + const page = firstLaunch.page + expect( + await firstApp.evaluate(({ BrowserWindow }) => + BrowserWindow.getAllWindows().every((window) => !window.isVisible()) + ) + ).toBe(true) + expect( + await page.evaluate(() => window.__store?.getState().settings?.agentStatusHooksEnabled) + ).toBe(false) + expect(await firstApp.evaluate(() => process.env.ORCA_DISABLE_CODEX_TRUST_RPC)).toBe('1') + const worktreeId = await attachRepoAndOpenTerminal(page, repoPath) + await waitForSessionReady(page) + // Why: the session writer persists only once hydrationSucceeded flips (not + // just workspaceSessionReady) — see shouldPersistWorkspaceSession — so the + // record write below is a silent no-op until hydration completes. + await expect + .poll(() => page.evaluate(() => window.__store?.getState().hydrationSucceeded === true), { + timeout: 30_000, + message: 'hydrationSucceeded did not become true before persisting the live record' + }) + .toBe(true) + await waitForActiveWorktree(page) + await ensureTerminalVisible(page) + await waitForActiveTerminalManager(page, 30_000) + await waitForPaneCount(page, 1, 30_000) + + const descriptor = await waitForActivePaneHookDescriptor(page) + const ptyId = await waitForActivePanePtyId(page) + const transcriptPath = + agent === 'codex' + ? session.seedCodexResumeRollout(PROVIDER_SESSION_ID, repoPath) + : undefined + const marker = `AGENT_LIVE_FORCE_EXIT_${Date.now()}` + await execInTerminal(page, ptyId, `echo ${marker}`) + await waitForTerminalOutput(page, marker) + if (agent === 'cursor') { + const before = testInfo.outputPath('cursor-resume-before.png') + await captureOutputRow(page, marker, before) + await testInfo.attach('Cursor fixture before checkpoint', { + path: before, + contentType: 'image/png' + }) + } + + await page.evaluate( + ({ + paneKey, + worktreeId: wtId, + providerSessionId, + transcriptPath, + agent, + providerSessionKey + }) => { + window.__store?.getState().setAgentStatus( + paneKey, + { state: 'working', prompt: 'finish the task', agentType: agent }, + agent === 'cursor' ? 'Cursor' : 'Codex', + undefined, + { worktreeId: wtId }, + { + providerSession: { + key: providerSessionKey, + id: providerSessionId, + transcriptPath + } + } + ) + }, + { + paneKey: descriptor.paneKey, + worktreeId: descriptor.worktreeId, + providerSessionId: PROVIDER_SESSION_ID, + transcriptPath, + agent, + providerSessionKey + } + ) + + // Exercise quit capture: origin:'quit' changes the live record, triggering the + // hydration-gated writer before polling persisted state. + await page.evaluate(() => window.__store?.getState().captureAllSleepingAgentSessions('quit')) + + // Why: the record reaches disk via the debounced session writer (150ms) plus + // the main-process scheduleSave (up to 5s). Under CI event-loop starvation — + // the same shard drifts renderer timers ~1s — both stages need headroom, so + // poll to 30s (this suite's other readiness budget). On a miss, surface store + // vs disk state to separate a lost write from a merely slow flush. + const persistDeadline = Date.now() + 30_000 + let persisted = false + while (Date.now() < persistDeadline) { + if (persistedLiveRecordExists(session.userDataDir)) { + persisted = true + break + } + await page.waitForTimeout(250) + } + if (!persisted) { + const storeRecords = await page.evaluate( + () => window.__store?.getState().sleepingAgentSessionsByPaneKey + ) + throw new Error( + `Live sleeping-agent record was not persisted before force exit. store=${JSON.stringify( + storeRecords + )} disk=${JSON.stringify( + readPersistedData(session.userDataDir).workspaceSession?.sleepingAgentSessionsByPaneKey + )}` + ) + } + + const daemonPid = readDaemonPid(session.userDataDir) + await forceKillElectronApp(firstApp) + firstApp = null + killPid(daemonPid) + stripPersistedPtyOwnership(session.userDataDir) + + const secondLaunch = await session.launch() + secondApp = secondLaunch.app + await waitForSessionReady(secondLaunch.page) + await expect + .poll( + async () => secondLaunch.page.evaluate(() => window.__store?.getState().activeWorktreeId), + { timeout: 15_000 } + ) + .toBe(worktreeId) + await ensureTerminalVisible(secondLaunch.page) + await waitForActiveTerminalManager(secondLaunch.page, 30_000) + + await waitForTerminalOutput(secondLaunch.page, PROVIDER_SESSION_ID, 30_000) + if (agent === 'cursor') { + expect( + await secondApp.evaluate(({ BrowserWindow }) => + BrowserWindow.getAllWindows().every((window) => !window.isVisible()) + ) + ).toBe(true) + const after = testInfo.outputPath('cursor-resume-after.png') + await captureOutputRow(secondLaunch.page, `--resume ${PROVIDER_SESSION_ID}`, after) + await testInfo.attach('Cursor fixture after cold resume dispatch', { + path: after, + contentType: 'image/png' + }) + } + + const terminalTabCount = await secondLaunch.page.evaluate( + (wtId) => (window.__store?.getState().tabsByWorktree[wtId] ?? []).length, + worktreeId + ) + expect(terminalTabCount).toBe(2) + } finally { + if (secondApp) { + await session.close(secondApp) + } + if (firstApp) { + await forceKillElectronApp(firstApp) + } + await session.dispose() + } + }) +}