From c8a08de96187b4dbc488d6050548081694eee9a4 Mon Sep 17 00:00:00 2001 From: brennanb2025 Date: Fri, 24 Apr 2026 17:36:44 -0700 Subject: [PATCH] refactor(term): document and test mode 2031 unsubscribe replay safety Explain why the `CSI ?2031l` handler is intentionally not replay-guarded (we only push CSI 997 on subscribe, so clearing stale bookkeeping during replay is a no-op a later real `?2031h` restores) and extend the test to actually exercise the replay path, not just the non-replay path. --- .../terminal-pane/terminal-appearance.test.ts | 13 +++++++++++++ .../components/terminal-pane/terminal-appearance.ts | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/src/renderer/src/components/terminal-pane/terminal-appearance.test.ts b/src/renderer/src/components/terminal-pane/terminal-appearance.test.ts index ea074546f04..7f26f10a977 100644 --- a/src/renderer/src/components/terminal-pane/terminal-appearance.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-appearance.test.ts @@ -245,6 +245,7 @@ describe('installMode2031Handlers', () => { // on subscribe, never on unsubscribe. const h = setup() try { + // Non-replay path: subscribe then unsubscribe clears state. await writeSync(h.term, '\x1b[?2031h') h.paneLastThemeMode.set(1, 'dark') expect(h.paneMode2031.get(1)).toBe(true) @@ -252,6 +253,18 @@ describe('installMode2031Handlers', () => { await writeSync(h.term, '\x1b[?2031l') expect(h.paneMode2031.has(1)).toBe(false) expect(h.paneLastThemeMode.has(1)).toBe(false) + + // Replay path: resubscribe, then receive `?2031l` during a replay + // window. The `l` handler must still clear — this is the invariant + // promised by the test name. + await writeSync(h.term, '\x1b[?2031h') + h.paneLastThemeMode.set(1, 'dark') + expect(h.paneMode2031.get(1)).toBe(true) + + replayIntoTerminal(h.pane, h.replayingPanesRef, '\x1b[?2031l') + await new Promise((resolve) => h.term.write('', resolve)) + expect(h.paneMode2031.has(1)).toBe(false) + expect(h.paneLastThemeMode.has(1)).toBe(false) } finally { h.dispose() } diff --git a/src/renderer/src/components/terminal-pane/terminal-appearance.ts b/src/renderer/src/components/terminal-pane/terminal-appearance.ts index b68afbd8cc7..56128671bf7 100644 --- a/src/renderer/src/components/terminal-pane/terminal-appearance.ts +++ b/src/renderer/src/components/terminal-pane/terminal-appearance.ts @@ -86,6 +86,10 @@ export function installMode2031Handlers(deps: Mode2031HandlerDeps): IDisposable[ } return false }), + // Why no replay guard on the unsubscribe branch: clearing stale bookkeeping + // is harmless. We only push CSI 997 on subscribe, never on unsubscribe, so + // even if a cold-restore replay carries `?2031l`, this handler just deletes + // map entries that a later real `?2031h` will re-populate normally. deps.parser.registerCsiHandler({ prefix: '?', final: 'l' }, (params) => { if (hasMode2031(params)) { deps.paneMode2031.delete(deps.paneId)