From 379ad0c5cdbb2a40fa57f111f97da72cb19e3d58 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:33:42 -0400 Subject: [PATCH] test(terminal-surfaces): pin why main's pane republish is deferred to a microtask (#31333) The deferral keeps a terminal's connected flip, noted inside syncWindowGraph's record update, from re-entering syncWindowGraph and recursing. Layout-owner (switch-on) builds only. --- .../surface/main-terminal-surfaces.ts | 2 +- .../terminal-surface-replies-main.test.ts | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/main/runtime/terminal/surface/main-terminal-surfaces.ts b/src/main/runtime/terminal/surface/main-terminal-surfaces.ts index eeb2c130d5b..ff9b31c86fd 100644 --- a/src/main/runtime/terminal/surface/main-terminal-surfaces.ts +++ b/src/main/runtime/terminal/surface/main-terminal-surfaces.ts @@ -165,7 +165,7 @@ class MainSurfacePublisher { this.schedule({ force: true }) } - /** A layout operation may have moved a pane; main's panes are installed once per task. */ + /** Deferred so a connected flip, noted inside syncWindowGraph's record update, cannot recurse into it. */ private schedule(opts: { force?: boolean } = {}): void { this.forceNext ||= opts.force === true if (this.scheduled) { diff --git a/src/main/runtime/terminal/surface/terminal-surface-replies-main.test.ts b/src/main/runtime/terminal/surface/terminal-surface-replies-main.test.ts index 703c11d8ba2..b2451a9e69f 100644 --- a/src/main/runtime/terminal/surface/terminal-surface-replies-main.test.ts +++ b/src/main/runtime/terminal/surface/terminal-surface-replies-main.test.ts @@ -11,6 +11,7 @@ import { SURFACE_PTYS, SURFACE_TAB_ID, captureSurfaceReplies, + mountedSurfaceGraph, recordedSurfaceReplies, surfaceRuntime } from './terminal-surface-replies.test-fixture' @@ -212,4 +213,41 @@ describe('terminal replies from main (switch on)', () => { paneKey: `${SURFACE_TAB_ID}:${HEADLESS_SECOND_LEAF_ID}` }) }) + + it('reinstall after a graph sync reconnects a terminal, never inside it', async () => { + const { runtime } = surfaceRuntime() + runtime.attachWindow(1) + runtime.syncWindowGraph(1, mountedSurfaceGraph()) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: ptysById is the runtime's PTY registry; the test edits the record it keeps. + const { ptysById } = runtime as unknown as { ptysById: Map } + // Disconnected, not exited: the graph's leaf loop records it connected again. + ptysById.get('pty-a')!.connected = false + const installs: { depth: number; connected: boolean | undefined }[] = [] + let depth = 0 + const sync = runtime.syncWindowGraph.bind(runtime) + vi.spyOn(runtime, 'syncWindowGraph').mockImplementation((windowId, graph) => { + installs.push({ depth, connected: ptysById.get('pty-a')?.connected }) + depth += 1 + try { + return sync(windowId, graph) + } finally { + depth -= 1 + } + }) + + runtime.syncWindowGraph(1, mountedSurfaceGraph()) + expect(installs).toHaveLength(1) + await Promise.resolve() + + // One deferred reinstall, outside the sync, after the record says connected. + expect(installs).toEqual([ + { depth: 0, connected: false }, + { depth: 0, connected: true } + ]) + const listed = await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`) + expect(listed.terminals.map(({ ptyId, connected }) => ({ ptyId, connected }))).toEqual([ + { ptyId: 'pty-a', connected: true }, + { ptyId: 'pty-b', connected: true } + ]) + }) })