diff --git a/src/main/persistence.test.ts b/src/main/persistence.test.ts index b59ca6f5986..52562cfc690 100644 --- a/src/main/persistence.test.ts +++ b/src/main/persistence.test.ts @@ -9258,10 +9258,21 @@ describe('Store', () => { expect(store.getWorkspaceSession(hostId).terminalPtyIncarnationsByPaneKey?.[paneKey]).toBe( 'inc-live' ) + // The in-session assertion above is unchanged. Across a RELOAD the SSH case now resolves in + // the local partition: STA-3077 step P gave an SSH pane binding one home, and the load fold + // carries the incarnation with the binding it fences — leaving it behind would strand the CAS + // guard in a partition no reader consults. The preserved value is what this pins, not which + // partition holds it, so the assertion follows the binding. const reloaded = await createStore() - expect(reloaded.getWorkspaceSession(hostId).terminalPtyIncarnationsByPaneKey?.[paneKey]).toBe( - 'inc-live' - ) + const foldedHostId = hostId?.startsWith('ssh:') ? undefined : hostId + expect( + reloaded.getWorkspaceSession(foldedHostId).terminalPtyIncarnationsByPaneKey?.[paneKey] + ).toBe('inc-live') + if (hostId?.startsWith('ssh:')) { + expect( + reloaded.getWorkspaceSession(hostId).terminalPtyIncarnationsByPaneKey?.[paneKey] + ).toBeUndefined() + } } ) diff --git a/src/main/persistence.ts b/src/main/persistence.ts index 80c0b93a1f2..0324a011c84 100644 --- a/src/main/persistence.ts +++ b/src/main/persistence.ts @@ -7290,6 +7290,18 @@ export class Store { layout.ptyIdsByLeafId = {} changed = true } + // The incarnation is the other half of the same fence — `persistPtyBinding` writes it into + // whichever partition it wrote the binding to, so leaving it behind would move the binding + // while its CAS guard stayed in the partition nothing reads. + const incarnations = host?.terminalPtyIncarnationsByPaneKey + if (incarnations && Object.keys(incarnations).length > 0) { + local.terminalPtyIncarnationsByPaneKey = { + ...incarnations, + ...local.terminalPtyIncarnationsByPaneKey + } + host.terminalPtyIncarnationsByPaneKey = {} + changed = true + } } return changed } diff --git a/src/main/ssh-pane-binding-partition.test.ts b/src/main/ssh-pane-binding-partition.test.ts index 667905d3307..a9bb672b376 100644 --- a/src/main/ssh-pane-binding-partition.test.ts +++ b/src/main/ssh-pane-binding-partition.test.ts @@ -251,6 +251,30 @@ describe('STA-3077 step P: the pane binding has one home', () => { }) }) +describe('STA-3077 step P: the fold carries the whole fence', () => { + // `persistPtyBinding` writes the binding and its incarnation into the SAME partition, and the + // incarnation is what its CAS compares. Folding only the binding would move the guard's subject + // to `local` while the guard itself stayed in the partition nothing reads — the CAS would then + // compare undefined against undefined and pass for any incarnation. + it('folds the pane incarnation along with the binding it fences', async () => { + const sshSession = paneSession('pty-1') as unknown as { + terminalPtyIncarnationsByPaneKey: Record + } + sshSession.terminalPtyIncarnationsByPaneKey = { [`${TAB}:${LEAF}`]: 'inc-pty-1' } + const store = await createStore({ + workspaceSession: paneSession('pty-1'), + workspaceSessionsByHostId: { [SSH_PARTITION]: sshSession } + }) + + expect(store.getWorkspaceSession().terminalPtyIncarnationsByPaneKey?.[`${TAB}:${LEAF}`]).toBe( + 'inc-pty-1' + ) + expect( + store.getWorkspaceSession(SSH_PARTITION).terminalPtyIncarnationsByPaneKey?.[`${TAB}:${LEAF}`] + ).toBeUndefined() + }) +}) + describe('STA-3077 step P: every production caller names that one home', () => { const summarize = (source: string): string => source.replace(/\s+/g, ' ').trim().slice(0, 90)