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 <neil@stably.ai>

---------

Signed-off-by: Neil <neil@stably.ai>
This commit is contained in:
Neil
2026-10-02 19:28:32 -07:00
committed by GitHub
parent 3ad26486d5
commit 9a1bef48e4
90 changed files with 809 additions and 367 deletions
@@ -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',
@@ -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<string, unknown>).get = vi.fn(() => ({
@@ -131,14 +131,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -131,14 +131,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -140,14 +140,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -132,14 +132,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -141,14 +141,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -132,14 +132,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
// 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 () => {
@@ -140,14 +140,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -132,14 +132,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -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 () => {
@@ -128,14 +128,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
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 () => {
@@ -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 () => {
@@ -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 () => {
@@ -139,14 +139,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -140,14 +140,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -129,14 +129,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty process-exit retirement', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -138,14 +138,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -132,14 +132,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -130,14 +130,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -130,14 +130,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -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 () => {
@@ -129,14 +129,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -146,14 +146,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -137,14 +137,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -134,14 +134,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -130,14 +130,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -128,14 +128,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -134,14 +134,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -128,14 +128,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -130,14 +130,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -128,14 +128,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -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 () => {
@@ -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 () => {
@@ -134,14 +134,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
// 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 () => {
@@ -130,14 +130,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -135,14 +135,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
// 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 () => {
@@ -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 () => {
@@ -128,14 +128,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -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 () => {
@@ -139,14 +139,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -132,14 +132,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -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 () => {
@@ -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 () => {
@@ -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 () => {
@@ -131,14 +131,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -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 () => {
@@ -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 () => {
@@ -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 () => {
@@ -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 () => {
@@ -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 () => {
@@ -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 () => {
@@ -131,14 +131,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -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 () => {
@@ -130,14 +130,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -130,14 +130,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -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 () => {
@@ -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 () => {
@@ -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()
})
@@ -133,14 +133,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -136,14 +136,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -132,14 +132,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -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 () => {
@@ -139,14 +139,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -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<string, unknown>
@@ -29,7 +35,16 @@ export function buildAgentStatusModuleMock(
}
}
export function installTerminalTestGlobals(): void {
export async function installTerminalTestGlobals(): Promise<void> {
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<void> {
// 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
@@ -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 () => {
@@ -130,14 +130,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
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 () => {
@@ -129,14 +129,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
}
describe('connectPanePty', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
transportFactoryQueue = []
createdTransportOptions = []
storeSubscribers = []
mockStoreState = createInitialStoreState(() => mockStoreState)
installTerminalTestGlobals()
await installTerminalTestGlobals()
})
afterEach(async () => {
@@ -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 () => {
@@ -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 () => {
@@ -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 () => {
@@ -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<void> {
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}`)
@@ -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()
})
})
@@ -47,7 +47,9 @@ function createContext(replay: string, attemptGeneration: number): ReattachPaylo
}
describe('reattach payload context', () => {
beforeEach(() => installTerminalTestGlobals())
beforeEach(async () => {
await installTerminalTestGlobals()
})
afterEach(async () => restoreTerminalTestGlobals())
@@ -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())
@@ -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<void> {
for (let i = 0; i < count; i++) {
@@ -8,20 +11,6 @@ async function flushAsyncTicks(count = 6): Promise<void> {
}
}
async function drainFakeTimerWork(limit = 20): Promise<void> {
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<void> {
}
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()
@@ -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<void> {
}
}
async function drainFakeTimerWork(limit = 20): Promise<void> {
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 ──────
@@ -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 () => {
@@ -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)
@@ -0,0 +1,77 @@
type SnapshotSignal = {
ready: boolean
claimed: boolean
promise: Promise<void>
resolve: () => void
}
function createSignal(): SnapshotSignal {
let resolve = (): void => undefined
const promise = new Promise<void>((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<void> {
const deadline = Date.now() + timeoutMs
while (!current.ready) {
const remaining = deadline - Date.now()
if (remaining <= 0) {
return
}
let timer: ReturnType<typeof setTimeout> | undefined
await Promise.race([
current.promise,
new Promise<void>((resolve) => {
timer = setTimeout(resolve, remaining)
})
])
clearTimeout(timer)
}
}
@@ -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', () => {
@@ -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') {
@@ -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,
@@ -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,
@@ -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')
@@ -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'
})
}
)
})
@@ -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')
}
)
})
+7 -1
View File
@@ -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':
+3
View File
@@ -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,
@@ -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<void> {
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()
}
})
}