From daf61ea0132b7074adcfab456b807e6ca4526f88 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 24 Aug 2026 10:51:51 -0700 Subject: [PATCH] test(terminal): model resume record clear at PTY spawn --- ...worktree-reactivation-tab-forkbomb.test.ts | 21 ++++++++++++++++--- ...t-session-mirror-frame-ordering-harness.ts | 14 ++++++++++++- ...n-mirror-hydration-frame-ordering.test.tsx | 3 ++- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/lib/worktree-reactivation-tab-forkbomb.test.ts b/src/renderer/src/lib/worktree-reactivation-tab-forkbomb.test.ts index a0c24f24517..d6fab33faee 100644 --- a/src/renderer/src/lib/worktree-reactivation-tab-forkbomb.test.ts +++ b/src/renderer/src/lib/worktree-reactivation-tab-forkbomb.test.ts @@ -3,6 +3,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { useAppStore, type AppState } from '@/store' import { activateAndRevealWorktree } from './worktree-activation' import { makeCreatedAgentWorktree as makeWorktree } from '@/lib/worktree-activation-created-agent-test-state' +import { consumeQueuedStartupAndClearSleepingRecord } from '@/components/terminal-pane/use-terminal-pane-lifecycle' const initialAppStoreState = useAppStore.getState() @@ -89,12 +90,26 @@ describe('STA-1111 worktree reopen does not fork-bomb tabs', () => { expect(state.automaticAgentResumeClaimsByTabId[tabs[0]!.id]?.providerSession).toEqual( providerSession ) - expect(state.sleepingAgentSessionsByPaneKey[paneKey]).toBeUndefined() + // Activation queues the replacement; the record is cleared only after + // its first PTY spawn consumes that startup command. if (reopen === 0) { - expect(state.consumeTabStartupCommand(tabs[0]!.id)?.resumeProviderSession).toEqual( - providerSession + expect(state.sleepingAgentSessionsByPaneKey[paneKey]).toBeDefined() + const consumed = state.consumeTabStartupCommand(tabs[0]!.id) + expect(consumed?.resumeProviderSession).toEqual(providerSession) + // The real pane lifecycle invokes this on the fresh PTY boundary. + consumeQueuedStartupAndClearSleepingRecord( + tabs[0]!.id, + () => consumed, + (identityPaneKey) => + useAppStore.getState().sleepingAgentSessionsByPaneKey[identityPaneKey], + useAppStore.getState().clearSleepingAgentSession ) + expect(useAppStore.getState().sleepingAgentSessionsByPaneKey[paneKey]).toBeUndefined() + } else { + // Later captures are stale replays of the already claimed session and + // are discarded without forking another replacement tab. + expect(state.sleepingAgentSessionsByPaneKey[paneKey]).toBeUndefined() } } }) diff --git a/src/renderer/src/runtime/host-session-mirror-frame-ordering-harness.ts b/src/renderer/src/runtime/host-session-mirror-frame-ordering-harness.ts index dd852ec3367..041396d4f11 100644 --- a/src/renderer/src/runtime/host-session-mirror-frame-ordering-harness.ts +++ b/src/renderer/src/runtime/host-session-mirror-frame-ordering-harness.ts @@ -3,6 +3,7 @@ import { afterEach, beforeEach, expect, vi } from 'vitest' import type { PublicKnownRuntimeEnvironment } from '../../../shared/runtime-environments' import { makePaneKey } from '../../../shared/stable-pane-id' import { resetStaleDocumentVisibilityForTesting } from '@/components/terminal-pane/stale-document-visibility' +import { consumeQueuedStartupAndClearSleepingRecord } from '@/components/terminal-pane/use-terminal-pane-lifecycle' import { useAppStore, type AppState } from '@/store' import { BG_MIRROR_TAB_ID, @@ -155,7 +156,6 @@ export function tabIds(worktreeId: string): string[] { * consumed the record launched nothing. */ export function expectReplayedResume(paneKey: string, worktreeId: string, sessionId: string): void { const state = useAppStore.getState() - expect(state.sleepingAgentSessionsByPaneKey[paneKey]).toBeUndefined() const claimedTabIds = Object.keys(state.automaticAgentResumeClaimsByTabId) expect(claimedTabIds).toHaveLength(1) const replacementTabId = claimedTabIds[0]! @@ -165,6 +165,18 @@ export function expectReplayedResume(paneKey: string, worktreeId: string, sessio launchAgent: 'codex', providerSession: { key: 'session_id', id: sessionId } }) + + // The mirror verdict only justifies the replacement launch. The sleeping + // record remains retryable until the replacement's first PTY spawn consumes + // its queued startup; model that concrete execution-host boundary here. + expect(state.sleepingAgentSessionsByPaneKey[paneKey]).toBeDefined() + consumeQueuedStartupAndClearSleepingRecord( + replacementTabId, + state.consumeTabStartupCommand, + (identityPaneKey) => useAppStore.getState().sleepingAgentSessionsByPaneKey[identityPaneKey], + useAppStore.getState().clearSleepingAgentSession + ) + expect(useAppStore.getState().sleepingAgentSessionsByPaneKey[paneKey]).toBeUndefined() } /** Registers the shared harness reset for the receipt-era describe blocks. */ diff --git a/src/renderer/src/runtime/host-session-mirror-hydration-frame-ordering.test.tsx b/src/renderer/src/runtime/host-session-mirror-hydration-frame-ordering.test.tsx index 6c3b7388d63..3bb9d7f5194 100644 --- a/src/renderer/src/runtime/host-session-mirror-hydration-frame-ordering.test.tsx +++ b/src/renderer/src/runtime/host-session-mirror-hydration-frame-ordering.test.tsx @@ -138,7 +138,6 @@ describe('mirrored-pane resume deferral against real stream frames', () => { // claims the session. Asserting only the record would pass for a replay // that cleared it and launched nothing. const replayed = useAppStore.getState() - expect(replayed.sleepingAgentSessionsByPaneKey[backgroundPaneKey]).toBeUndefined() const backgroundTabIds = tabIds(BG_WT) expect(backgroundTabIds).toHaveLength(1) expect(backgroundTabIds[0]).not.toBe(BG_MIRROR_TAB_ID) @@ -146,6 +145,8 @@ describe('mirrored-pane resume deferral against real stream frames', () => { launchAgent: 'codex', providerSession: { key: 'session_id', id: 'codex-session-bg-1' } }) + expect(replayed.sleepingAgentSessionsByPaneKey[backgroundPaneKey]).toBeDefined() + expectReplayedResume(backgroundPaneKey, BG_WT, 'codex-session-bg-1') }) // The three tests below pin the remaining release sites one frame at a time: