From 099ca4274987c7b2ba42281fae388a3f45372817 Mon Sep 17 00:00:00 2001 From: Huang-404-Q <13165998033@163.com> Date: Mon, 17 Aug 2026 17:56:12 +0800 Subject: [PATCH] fix(tests): isolate TUI scrollback tests from terminal multiplexers isMultiplexerSession() treats TMUX, STY, ZELLIJ, HERDR_ENV=1, the CMUX_* markers and a tmux/screen TERM as authoritative, and routes rendering down the path that cannot rebuild scrollback. Nine tests across four files assert the destructive full-paint behavior and fail for anyone running the suite inside tmux, screen, Zellij or CMUX. component-render.test.ts already cleared HERDR_ENV for exactly this reason but covered only one of the nine signals. Replace it with a shared helper that clears the whole family and restores it afterwards, and call it from the other three files. --- .../test/helpers/terminal-multiplexer.ts | 40 +++++++++++++++++++ .../test/streaming-preview-height.test.ts | 3 ++ .../test/tool-execution-write-repaint.test.ts | 3 ++ packages/tui/test/component-render.test.ts | 14 ++----- .../tui/test/helpers/terminal-multiplexer.ts | 40 +++++++++++++++++++ packages/tui/test/overlay-scroll.test.ts | 3 ++ 6 files changed, 92 insertions(+), 11 deletions(-) create mode 100644 packages/coding-agent/test/helpers/terminal-multiplexer.ts create mode 100644 packages/tui/test/helpers/terminal-multiplexer.ts diff --git a/packages/coding-agent/test/helpers/terminal-multiplexer.ts b/packages/coding-agent/test/helpers/terminal-multiplexer.ts new file mode 100644 index 000000000..93381b49c --- /dev/null +++ b/packages/coding-agent/test/helpers/terminal-multiplexer.ts @@ -0,0 +1,40 @@ +import { afterEach, beforeEach } from "bun:test"; + +/** + * Neutralize every terminal-multiplexer signal for the calling test file. + * + * `isInsideTerminalMultiplexer()` treats `TMUX`, `STY`, `ZELLIJ`, + * `HERDR_ENV=1`, the `CMUX_*` markers and a `tmux`/`screen` `TERM` as + * authoritative, and `isMultiplexerSession()` then routes rendering down the + * path that cannot rebuild scrollback. Tests that assert the destructive + * full-paint behavior otherwise fail for anyone running the suite inside tmux, + * screen, Zellij or CMUX. Restores whatever was set afterwards. + */ +export function withoutTerminalMultiplexer(): void { + const KEYS = [ + "TMUX", + "STY", + "ZELLIJ", + "HERDR_ENV", + "CMUX_WORKSPACE_ID", + "CMUX_SURFACE_ID", + "CMUX_REMOTE_TRANSPORT", + "TERM", + ] as const; + const previous = new Map(); + + beforeEach(() => { + for (const key of KEYS) { + previous.set(key, Bun.env[key]); + delete Bun.env[key]; + } + }); + + afterEach(() => { + for (const [key, value] of previous) { + if (value === undefined) delete Bun.env[key]; + else Bun.env[key] = value; + } + previous.clear(); + }); +} diff --git a/packages/coding-agent/test/streaming-preview-height.test.ts b/packages/coding-agent/test/streaming-preview-height.test.ts index 33e06a501..6b4fe19d1 100644 --- a/packages/coding-agent/test/streaming-preview-height.test.ts +++ b/packages/coding-agent/test/streaming-preview-height.test.ts @@ -11,6 +11,7 @@ import { previewWindowRows } from "@oh-my-pi/pi-coding-agent/tools/render-utils" import { TUI, visibleWidth } from "@oh-my-pi/pi-tui"; import { removeWithRetries } from "@oh-my-pi/pi-utils"; import { VirtualTerminal } from "../../tui/test/virtual-terminal"; +import { withoutTerminalMultiplexer } from "./helpers/terminal-multiplexer"; // The streaming edit preview is a fixed-height tail window ("cursor"): the last // EDIT_STREAMING_PREVIEW_LINES rows of the recomputed diff are pinned to the @@ -21,6 +22,8 @@ import { VirtualTerminal } from "../../tui/test/virtual-terminal"; // whole change segments grew and shrank tick to tick (the stutter), and the // earlier high-water fix padded the deficit with blank rows (the "large // rectangle that is half empty" regression). The tail window has neither. +withoutTerminalMultiplexer(); + describe("streaming edit preview height (stable, full tail window)", () => { const oldBlock = ["function foo() {", " const x = 1;", " return x;", "}"].join("\n"); const tail = ["", "function bar() {", " return 2;", "}", "", "function baz() {", " return 3;", "}", ""].join("\n"); diff --git a/packages/coding-agent/test/tool-execution-write-repaint.test.ts b/packages/coding-agent/test/tool-execution-write-repaint.test.ts index d011987e5..2a7d1593e 100644 --- a/packages/coding-agent/test/tool-execution-write-repaint.test.ts +++ b/packages/coding-agent/test/tool-execution-write-repaint.test.ts @@ -4,6 +4,7 @@ import { initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; import { type Component, TUI } from "@oh-my-pi/pi-tui"; import { StressRenderScheduler } from "../../tui/test/render-stress-scheduler"; import { VirtualTerminal } from "../../tui/test/virtual-terminal"; +import { withoutTerminalMultiplexer } from "./helpers/terminal-multiplexer"; function writeArgs(lineCount: number) { return { @@ -31,6 +32,8 @@ function plainBuffer(term: VirtualTerminal): string[] { .filter(Boolean); } +withoutTerminalMultiplexer(); + describe("ToolExecutionComponent write repaint seam", () => { const components: ToolExecutionComponent[] = []; diff --git a/packages/tui/test/component-render.test.ts b/packages/tui/test/component-render.test.ts index d027c27bf..d4bfe5dd0 100644 --- a/packages/tui/test/component-render.test.ts +++ b/packages/tui/test/component-render.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it } from "bun:test"; +import { describe, expect, it } from "bun:test"; import { type Component, Container, @@ -10,20 +10,12 @@ import { type NativeScrollbackReplay, TUI, } from "@oh-my-pi/pi-tui"; +import { withoutTerminalMultiplexer } from "./helpers/terminal-multiplexer"; import { StressRenderScheduler } from "./render-stress-scheduler"; import { defaultEditorTheme } from "./test-themes"; import { VirtualTerminal } from "./virtual-terminal"; -const ORIGINAL_HERDR_ENV = Bun.env.HERDR_ENV; - -beforeEach(() => { - delete Bun.env.HERDR_ENV; -}); - -afterEach(() => { - if (ORIGINAL_HERDR_ENV === undefined) delete Bun.env.HERDR_ENV; - else Bun.env.HERDR_ENV = ORIGINAL_HERDR_ENV; -}); +withoutTerminalMultiplexer(); // Behavioral tests for TUI.requestComponentRender: a component whose own // content changed (spinner frame, blink) asks for a component-scoped frame. diff --git a/packages/tui/test/helpers/terminal-multiplexer.ts b/packages/tui/test/helpers/terminal-multiplexer.ts new file mode 100644 index 000000000..93381b49c --- /dev/null +++ b/packages/tui/test/helpers/terminal-multiplexer.ts @@ -0,0 +1,40 @@ +import { afterEach, beforeEach } from "bun:test"; + +/** + * Neutralize every terminal-multiplexer signal for the calling test file. + * + * `isInsideTerminalMultiplexer()` treats `TMUX`, `STY`, `ZELLIJ`, + * `HERDR_ENV=1`, the `CMUX_*` markers and a `tmux`/`screen` `TERM` as + * authoritative, and `isMultiplexerSession()` then routes rendering down the + * path that cannot rebuild scrollback. Tests that assert the destructive + * full-paint behavior otherwise fail for anyone running the suite inside tmux, + * screen, Zellij or CMUX. Restores whatever was set afterwards. + */ +export function withoutTerminalMultiplexer(): void { + const KEYS = [ + "TMUX", + "STY", + "ZELLIJ", + "HERDR_ENV", + "CMUX_WORKSPACE_ID", + "CMUX_SURFACE_ID", + "CMUX_REMOTE_TRANSPORT", + "TERM", + ] as const; + const previous = new Map(); + + beforeEach(() => { + for (const key of KEYS) { + previous.set(key, Bun.env[key]); + delete Bun.env[key]; + } + }); + + afterEach(() => { + for (const [key, value] of previous) { + if (value === undefined) delete Bun.env[key]; + else Bun.env[key] = value; + } + previous.clear(); + }); +} diff --git a/packages/tui/test/overlay-scroll.test.ts b/packages/tui/test/overlay-scroll.test.ts index d0148b43b..c6fe7ab42 100644 --- a/packages/tui/test/overlay-scroll.test.ts +++ b/packages/tui/test/overlay-scroll.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import { type Component, CURSOR_MARKER, type Focusable, type OverlayFocusOwner, TUI } from "@oh-my-pi/pi-tui"; +import { withoutTerminalMultiplexer } from "./helpers/terminal-multiplexer"; import { VirtualTerminal } from "./virtual-terminal"; class LineComponent implements Component { @@ -167,6 +168,8 @@ async function settleResize(term: VirtualTerminal): Promise { await flushRender(term); } +withoutTerminalMultiplexer(); + describe("TUI overlays", () => { let savedTerminalEnv: Record = {}; beforeEach(() => {