From 7408e1befeaaa88084452bd4e9225d72afe79069 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Fri, 13 Nov 2026 17:48:41 -0800 Subject: [PATCH] Exercise full intent against deferred cleanup and in-flight failure --- src/main/ipc/session-owned-field-save.test.ts | 4 +- .../persistence-async-write-syscalls.test.ts | 110 +++++++++++------- 2 files changed, 73 insertions(+), 41 deletions(-) diff --git a/src/main/ipc/session-owned-field-save.test.ts b/src/main/ipc/session-owned-field-save.test.ts index 0fcea101c6c..51257f2fc5f 100644 --- a/src/main/ipc/session-owned-field-save.test.ts +++ b/src/main/ipc/session-owned-field-save.test.ts @@ -48,11 +48,13 @@ it('keeps renderer field saves after runtime startup, sidebar, crew and agent id for (const method of TERMINAL_QUERY_METHODS) { if (method.name === 'terminal.agentId') { await method.handler( - { paneKey: 'field-tab:field-leaf', worktreeId: 'field-repo::/synthetic' }, + { paneKey: 'tab-local:leaf-local', worktreeId: 'repo-local::/fixture/local' }, { runtime } ) } } + await runtime.resolveActiveWorktreeContext() + await runtime.listMobileSessionTabs('id:repo-local::/fixture/local') await runtime.listTerminals() const writes = vi.spyOn(authority, 'writePreparedState') structuredClone(get({})) diff --git a/src/main/persistence/persistence-async-write-syscalls.test.ts b/src/main/persistence/persistence-async-write-syscalls.test.ts index d7b68db7ae1..be592b9f59f 100644 --- a/src/main/persistence/persistence-async-write-syscalls.test.ts +++ b/src/main/persistence/persistence-async-write-syscalls.test.ts @@ -5,14 +5,17 @@ import { } from './loading-store/workspace-sessions/session-snapshot-operations' import { borrowWorkspaceSession } from './loading-store/workspace-sessions/session-host-partitions' import { describe, it, expect, vi } from 'vitest' -import { getDefaultWorkspaceSession } from '../../shared/constants' -import { makeSessionWithTerminalBuffers, TEST_LEAF_1 } from './persistence-session-fixtures' +import { getDefaultPersistedState, getDefaultWorkspaceSession } from '../../shared/constants' +import { makeSessionWithTerminalBuffers } from './persistence-session-fixtures' import type { WorkspaceSessionState } from '../../shared/workspace-session/workspace-session-state-types' import { createWorkerMaintenanceFixture, maintenanceBarrier } from './loading-store/profile-writing/profile-state-maintenance-fixture' +import { clearSshRemotePtyBindingsForLeases } from './leasing-ssh-ptys/ssh-pty-binding-cleanup' +import { scheduleSave } from './loading-store/profile-writing/write-scheduling' +import type { Store } from './loading-store/store' import type { ProfileStateWorkerAuthority } from './profile-state/profile-state-worker-authority' vi.mock('../telemetry/client', () => ({ track: vi.fn() })) vi.mock('../telemetry/cohort-classifier', () => ({ @@ -35,6 +38,29 @@ function pauseCommit(authority: ProfileStateWorkerAuthority) { return { started: started.promise, release: finish.resolve } } +function clearOwnedBinding(store: Store): void { + clearSshRemotePtyBindingsForLeases( + { + state: { + ...getDefaultPersistedState('/synthetic'), + workspaceSession: borrowWorkspaceSession(store) + }, + toComparablePtyId: (_target, ptyId) => ptyId, + scheduleSave: () => scheduleSave(store, ['workspaceSession']) + }, + 'field-test', + [ + { + targetId: 'field-test', + ptyId: 'local-pty', + state: 'terminated', + createdAt: 1, + updatedAt: 1 + } + ] + ) +} + describe('worker persistence avoids synchronous profile filesystem calls', () => { it('rewrites an earlier value after a captured stale generation commits', async () => { const { store, authority, readState } = await createWorkerMaintenanceFixture() @@ -111,53 +137,57 @@ describe('worker persistence avoids synchronous profile filesystem calls', () => } ) - it('keeps an in-place binding full when its write listener publishes a field patch', async () => { - const { store, readState } = await createWorkerMaintenanceFixture() - const input = makeSessionWithTerminalBuffers() - admitDetachedWorkspaceSessionInput(store, input) - setDetachedWorkspaceSession(store, input) - await store.flushPendingOrThrowAsync() - const stop = store.onWorkspaceSessionWritten(() => { - stop() - patchDetachedWorkspaceSession(store, { activeTabId: 'listener' }) - }) - expect( - await store.persistPtyBinding({ - worktreeId: 'local-repo::/local', - tabId: 'local-tab', - leafId: TEST_LEAF_1, - ptyId: 'replacement', - incarnationId: 'replacement-incarnation' - }) - ).toBe(true) - const saved = readState().workspaceSession - expect(saved.terminalLayoutsByTabId['local-tab'].ptyIdsByLeafId[TEST_LEAF_1]).toBe( - 'replacement' - ) - expect(saved.activeTabId).toBe('listener') - }) + it.each([false, true])( + 'persists next-save in-place cleanup with a field publication, reentrant=%s', + async (reentrant) => { + const { store, readState } = await createWorkerMaintenanceFixture() + const input = makeSessionWithTerminalBuffers() + admitDetachedWorkspaceSessionInput(store, input) + setDetachedWorkspaceSession(store, input) + await store.flushPendingOrThrowAsync() + if (reentrant) { + const stop = store.onWorkspaceSessionWritten(() => { + stop() + clearOwnedBinding(store) + }) + } else { + clearOwnedBinding(store) + } + patchDetachedWorkspaceSession(store, { activeTabId: 'next' }) + await store.flushPendingOrThrowAsync() + const saved = readState().workspaceSession + expect(saved.tabsByWorktree['local-repo::/local'][0].ptyId).toBeNull() + expect(saved.terminalLayoutsByTabId['local-tab'].ptyIdsByLeafId).toEqual({}) + expect(saved.activeTabId).toBe('next') + } + ) - it('keeps an in-place binding when a failed field save is retried', async () => { + it('restores a failed field capture without downgrading newer in-place cleanup', async () => { const { store, authority, readState } = await createWorkerMaintenanceFixture() const input = makeSessionWithTerminalBuffers() admitDetachedWorkspaceSessionInput(store, input) setDetachedWorkspaceSession(store, input) await store.flushPendingOrThrowAsync() - vi.spyOn(authority, 'writePreparedState').mockRejectedValueOnce(new Error('known failure')) - patchDetachedWorkspaceSession(store, { activeTabId: 'retry' }) - await expect(store.flushPendingOrThrowAsync()).rejects.toThrow('known failure') - await store.persistPtyBinding({ - worktreeId: 'local-repo::/local', - tabId: 'local-tab', - leafId: TEST_LEAF_1, - ptyId: 'after-failure', - incarnationId: 'after-failure-incarnation' + const started = maintenanceBarrier() + const reject = maintenanceBarrier() + vi.spyOn(authority, 'writePreparedState').mockImplementationOnce(async () => { + started.resolve() + await reject.promise + throw new Error('known failure') }) + patchDetachedWorkspaceSession(store, { activeTabId: 'retry' }) + const failed = expect( + store.flushPendingOrThrowAsync({ drainToStableGeneration: false }) + ).rejects.toThrow('known failure') + await started.promise + clearOwnedBinding(store) + reject.resolve() + await failed + await store.flushPendingOrThrowAsync() const saved = readState().workspaceSession expect(saved.activeTabId).toBe('retry') - expect(saved.terminalLayoutsByTabId['local-tab'].ptyIdsByLeafId[TEST_LEAF_1]).toBe( - 'after-failure' - ) + expect(saved.tabsByWorktree['local-repo::/local'][0].ptyId).toBeNull() + expect(saved.terminalLayoutsByTabId['local-tab'].ptyIdsByLeafId).toEqual({}) }) it('persists an alias retained by a refused layout admission before a detached patch', async () => {