diff --git a/src/renderer/src/components/terminal-pane/pty-pre-handler-buffer.test.ts b/src/renderer/src/components/terminal-pane/pty-pre-handler-buffer.test.ts index 461a41e7faa..6219fc0105c 100644 --- a/src/renderer/src/components/terminal-pane/pty-pre-handler-buffer.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-pre-handler-buffer.test.ts @@ -276,6 +276,20 @@ describe('pre-handler PTY buffer', () => { expect(hasPreHandlerPtyExit(RECYCLED_PTY_ID)).toBe(true) }) + // A malformed incarnation is evidence of nothing. Treating it as a value that disagrees with + // everything would discard the very exits the buffer exists to deliver. + it('treats a malformed incarnation as unknown rather than as a disagreement', () => { + bufferPreHandlerPtyExit(RECYCLED_PTY_ID, 4, { not: 'a string' }) + discardPreHandlerPtyExitFromForeignIncarnation(RECYCLED_PTY_ID, FRESH_INCARNATION_ID) + expect(hasPreHandlerPtyExit(RECYCLED_PTY_ID)).toBe(true) + drainPreHandlerPtyExit(RECYCLED_PTY_ID, vi.fn()) + + clearConsumedPreHandlerPtyExit(RECYCLED_PTY_ID) + bufferPreHandlerPtyExit(RECYCLED_PTY_ID, 5, PRIOR_INCARNATION_ID) + discardPreHandlerPtyExitFromForeignIncarnation(RECYCLED_PTY_ID, 42) + expect(hasPreHandlerPtyExit(RECYCLED_PTY_ID)).toBe(true) + }) + it('re-admits exits for a recycled id whose prior incarnation was consumed', () => { consumePreHandlerPtyState(RECYCLED_PTY_ID) diff --git a/src/renderer/src/components/terminal-pane/pty-pre-handler-buffer.ts b/src/renderer/src/components/terminal-pane/pty-pre-handler-buffer.ts index ee0830c518d..dcf5f0af5dd 100644 --- a/src/renderer/src/components/terminal-pane/pty-pre-handler-buffer.ts +++ b/src/renderer/src/components/terminal-pane/pty-pre-handler-buffer.ts @@ -1,3 +1,4 @@ +import { isPtyIncarnationId } from '../../../../shared/pty-incarnation' import { clampUtf8Tail } from './pty-eager-buffer-clamp' import type { PtyDataMeta } from './pty-dispatcher' @@ -81,9 +82,11 @@ function reserveBoundedPtySlot(map: Map, ptyId: string, cap: numbe * existing id, and re-owning incarnation X is still proof that W's exit was not theirs. */ export function discardPreHandlerPtyExitFromForeignIncarnation( ptyId: string, - incarnationId: string | undefined + incarnationId: unknown ): void { - if (!incarnationId) { + // Why the shared guard and not a truthiness check: anything that is not a well-formed incarnation + // is evidence of nothing, and must read as "unknown" rather than disagree with everything. + if (!isPtyIncarnationId(incarnationId)) { return } const exit = preHandlerPtyExit.get(ptyId) @@ -164,7 +167,11 @@ export function replayPreHandlerPtyData(ptyId: string, observer: (data: string) } } -export function bufferPreHandlerPtyExit(ptyId: string, code: number, incarnationId?: string): void { +export function bufferPreHandlerPtyExit( + ptyId: string, + code: number, + incarnationId?: unknown +): void { if (consumedPreHandlerPtyExits.has(ptyId) || discardedPreHandlerPtyStates.has(ptyId)) { return } @@ -172,7 +179,8 @@ export function bufferPreHandlerPtyExit(ptyId: string, code: number, incarnation preHandlerPtyExit.set(ptyId, { code, sequence: nextPreHandlerPtySequence(), - ...(incarnationId ? { incarnationId } : {}) + // Record only a well-formed incarnation; a malformed one must not become evidence. + ...(isPtyIncarnationId(incarnationId) ? { incarnationId } : {}) }) }