fix(tui): kept ProcessTerminal and window-title writes headless under the test runtime
- `ProcessTerminal` now captures a per-instance `#headless` from `isTerminalHeadless()` at construction and re-reads it in `start()`; when set, `#safeWrite`, `start`, `stop`, `drainInput`, and `setProgress` short-circuit, so frame paints, `start()` probes (OSC 11 / DA1 / kitty), the progress keepalive, stdin raw mode, SIGWINCH, and teardown escapes no longer reach a real terminal during `bun test` (previously `#safeWrite` only skipped on `!isTTY`, so an interactive terminal leaked stray boxes and probe queries). - Gated the `emergencyTerminalRestore()` blind-restore branch and `TerminalInfo.sendNotification` on the headless flag, and guarded `setTerminalTitle`/`pushTerminalTitle`/`popTerminalTitle` in `title-generator.ts`. - Opted the terminal-contract suites (terminal-appearance, notifications, emergency-restore-altscreen, issue-2034 write gate, process-terminal-render-harness, render-stress-harness) back into real I/O with `setTerminalHeadless(false)`, restoring on teardown. - Added `process-terminal-headless.test.ts` asserting zero writes under a forced TTY when headless and real frame/probe output after opt-out.
This commit is contained in:
@@ -36,6 +36,7 @@
|
||||
- Fixed messaging a `parked` subagent that was restored from disk (Agent Hub scan, or a resumed/restarted session) failing with `cannot be revived (no reviver registered)` even though its transcript was intact. Such refs carry a session file but no in-memory reviver — the executor's live reviver closure dies with the spawning turn/process — so IRC sends and Agent Hub focus refused them. `AgentLifecycleManager.ensureLive` now cold-revives them through a persisted-subagent reviver factory (installed by the top-level interactive/RPC session) that rebuilds the subagent from its JSONL the way `--resume` rebuilds a session: it reopens the file and replays it through `createAgentSession`, but sources the runtime contract from a now-readable `session_init` record (`SessionManager.peekSessionInit`) so tools, system prompt, output schema, and kind are restored rather than resurrected as a default top-level session. `session_init` now also persists the effective `spawns` allowlist and read-summarization flag so a cold revive keeps the original capability surface (old files without them deny re-spawning rather than defaulting to wildcard). Isolated runs and pre-`session_init` files whose recorded workspace no longer exists stay transcript-only (`history://`).
|
||||
|
||||
### Fixed
|
||||
- Fixed the terminal window-title OSC writes (`setTerminalTitle`/`pushTerminalTitle`/`popTerminalTitle`) leaking escape sequences to a developer's terminal during `bun test`; they now skip when the terminal is headless (the test-runtime default), matching the `ProcessTerminal` render/probe suppression so interactive-mode tests no longer paint to the real terminal
|
||||
|
||||
- Fixed empty CLI sessions being retained after opening `omp` and exiting without a prompt ([#2800](https://github.com/can1357/oh-my-pi/issues/2800)).
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import * as path from "node:path";
|
||||
|
||||
import { type Api, type AssistantMessage, completeSimple, type Model, type Tool } from "@oh-my-pi/pi-ai";
|
||||
import { logger, prompt } from "@oh-my-pi/pi-utils";
|
||||
import { isTerminalHeadless, logger, prompt } from "@oh-my-pi/pi-utils";
|
||||
import type { ModelRegistry } from "../config/model-registry";
|
||||
|
||||
import { resolveRoleSelection } from "../config/model-resolver";
|
||||
@@ -391,7 +391,7 @@ export function formatSessionTerminalTitle(sessionName: string | undefined, cwd?
|
||||
* Set the terminal title using OSC 0 (sets both tab and window title). Unsupported terminals ignore it.
|
||||
*/
|
||||
export function setTerminalTitle(title: string): void {
|
||||
if (!process.stdout.isTTY) return;
|
||||
if (!process.stdout.isTTY || isTerminalHeadless()) return;
|
||||
process.stdout.write(`\x1b]0;${sanitizeTerminalTitlePart(title) ?? DEFAULT_TERMINAL_TITLE}\x07`);
|
||||
}
|
||||
|
||||
@@ -403,7 +403,7 @@ export function setSessionTerminalTitle(sessionName: string | undefined, cwd?: s
|
||||
* Save the current terminal title on terminals that support xterm window ops.
|
||||
*/
|
||||
export function pushTerminalTitle(): void {
|
||||
if (!process.stdout.isTTY) return;
|
||||
if (!process.stdout.isTTY || isTerminalHeadless()) return;
|
||||
process.stdout.write("\x1b[22;2t");
|
||||
}
|
||||
|
||||
@@ -411,6 +411,6 @@ export function pushTerminalTitle(): void {
|
||||
* Restore the previously saved terminal title on terminals that support xterm window ops.
|
||||
*/
|
||||
export function popTerminalTitle(): void {
|
||||
if (!process.stdout.isTTY) return;
|
||||
if (!process.stdout.isTTY || isTerminalHeadless()) return;
|
||||
process.stdout.write("\x1b[23;2t");
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
- Fixed inline math span detection so escaped dollars and currency-like patterns (such as `$5` and `$10`) are not converted as math
|
||||
- Fixed Mermaid diagram rendering in Markdown code blocks to clip each ASCII line to content width before wrapping, preventing preformatted diagram rows from fragmenting
|
||||
- Fixed fullscreen overlays losing keyboard focus to hidden prompt surfaces, which could make settings unresponsive while a background approval request was pending ([#2789](https://github.com/can1357/oh-my-pi/issues/2789)).
|
||||
- Fixed `bun test` runs inside a real terminal leaking TUI output: `ProcessTerminal` now honors a headless test-runtime default, so frame paints, `start()` capability probes (OSC 11 / DA1 / kitty), the progress keepalive, notifications, and teardown escapes no longer reach the developer's terminal, and stdin raw mode is never engaged. Previously `#safeWrite` only skipped on `!process.stdout.isTTY`, so a developer running the suite in an interactive terminal saw stray status/editor boxes and probe queries. Terminal-contract suites opt back into real I/O via `setTerminalHeadless(false)`
|
||||
|
||||
## [16.0.2] - 2026-06-16
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { encodeSixel } from "@oh-my-pi/pi-natives";
|
||||
import { $env, isBunTestRuntime } from "@oh-my-pi/pi-utils";
|
||||
import { $env, isBunTestRuntime, isTerminalHeadless } from "@oh-my-pi/pi-utils";
|
||||
import {
|
||||
detectKittyUnicodePlaceholdersSupport,
|
||||
getKittyGraphics,
|
||||
@@ -97,7 +97,7 @@ export class TerminalInfo {
|
||||
}
|
||||
|
||||
sendNotification(message: string | TerminalNotification): void {
|
||||
if (isNotificationSuppressed()) return;
|
||||
if (isNotificationSuppressed() || isTerminalHeadless()) return;
|
||||
process.stdout.write(this.formatNotification(message));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { dlopen, FFIType, ptr } from "bun:ffi";
|
||||
import * as fs from "node:fs";
|
||||
import { $env, isBunTestRuntime, logger } from "@oh-my-pi/pi-utils";
|
||||
import { $env, isBunTestRuntime, isTerminalHeadless, logger } from "@oh-my-pi/pi-utils";
|
||||
import { setKittyProtocolActive } from "./keys";
|
||||
import { StdinBuffer } from "./stdin-buffer";
|
||||
import { NotifyProtocol, setCellDimensions, setOsc99Supported, TERMINAL } from "./terminal-capabilities";
|
||||
@@ -249,7 +249,7 @@ export function emergencyTerminalRestore(): void {
|
||||
altScreenActive = false;
|
||||
}
|
||||
terminal.showCursor();
|
||||
} else if (terminalEverStarted) {
|
||||
} else if (terminalEverStarted && !isTerminalHeadless()) {
|
||||
// Blind restore only if we know a terminal was started but lost track of it
|
||||
// This avoids writing escape sequences for non-TUI commands (grep, commit, etc.)
|
||||
process.stdout.write(
|
||||
@@ -404,6 +404,10 @@ export class ProcessTerminal implements Terminal {
|
||||
#stdinBuffer?: StdinBuffer;
|
||||
#stdinDataHandler?: (data: string) => void;
|
||||
#dead = false;
|
||||
// Captured at construction and re-read at start(): when true, every real
|
||||
// terminal side effect (writes, probes, raw mode, SIGWINCH, timers) is
|
||||
// suppressed. Defaults on under `bun test` — see isTerminalHeadless().
|
||||
#headless = isTerminalHeadless();
|
||||
#writeLogPath = $env.PI_TUI_WRITE_LOG || "";
|
||||
#stdoutErrorCleanup?: () => void;
|
||||
#stdoutErrorHandler = (err: Error) => {
|
||||
@@ -459,6 +463,13 @@ export class ProcessTerminal implements Terminal {
|
||||
this.#inputHandler = onInput;
|
||||
this.#resizeHandler = onResize;
|
||||
|
||||
// Headless (tests): suppress every real-terminal side effect. Skip raw
|
||||
// mode, stdin listeners, capability probes, SIGWINCH, and emergency-restore
|
||||
// ownership; #safeWrite is also a no-op, so frame paints and teardown
|
||||
// escapes never reach the developer's terminal during `bun test`.
|
||||
this.#headless = isTerminalHeadless();
|
||||
if (this.#headless) return;
|
||||
|
||||
// Register for emergency cleanup
|
||||
activeTerminal = this;
|
||||
terminalEverStarted = true;
|
||||
@@ -1134,6 +1145,7 @@ export class ProcessTerminal implements Terminal {
|
||||
}
|
||||
|
||||
async drainInput(maxMs = 1000, idleMs = 50): Promise<void> {
|
||||
if (this.#headless) return;
|
||||
if (this.#kittyProtocolActive) {
|
||||
// Disable Kitty keyboard protocol first so any late key releases
|
||||
// do not generate new Kitty escape sequences.
|
||||
@@ -1176,6 +1188,7 @@ export class ProcessTerminal implements Terminal {
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (this.#headless) return;
|
||||
// Unregister from emergency cleanup
|
||||
if (activeTerminal === this) {
|
||||
activeTerminal = null;
|
||||
@@ -1303,6 +1316,7 @@ export class ProcessTerminal implements Terminal {
|
||||
}
|
||||
|
||||
#safeWrite(data: string): void {
|
||||
if (this.#headless) return;
|
||||
if (this.#dead) return;
|
||||
// Skip control sequences when stdout isn't a TTY (piped output, tests, log
|
||||
// files). They serve no purpose there and would surface as visible noise.
|
||||
@@ -1385,6 +1399,7 @@ export class ProcessTerminal implements Terminal {
|
||||
}
|
||||
|
||||
setProgress(active: boolean): void {
|
||||
if (this.#headless) return;
|
||||
if (active) {
|
||||
this.#safeWrite(TERMINAL_PROGRESS_ACTIVE_SEQUENCE);
|
||||
if (!this.#progressTimer) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from "bun:test";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
|
||||
import { emergencyTerminalRestore, ProcessTerminal, setAltScreenActive } from "@oh-my-pi/pi-tui/terminal";
|
||||
import { setTerminalHeadless } from "@oh-my-pi/pi-utils";
|
||||
|
||||
// Regression coverage for the Windows shell-handoff corruption on exit:
|
||||
// `emergencyTerminalRestore()` used to write DECRST 1049 ("leave alternate
|
||||
@@ -16,6 +17,11 @@ import { emergencyTerminalRestore, ProcessTerminal, setAltScreenActive } from "@
|
||||
const stdinIsTtyDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
|
||||
const stdoutIsTtyDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
|
||||
const stdinSetRawModeDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "setRawMode");
|
||||
// This suite asserts the real emergencyTerminalRestore() write path, so it opts
|
||||
// out of the test-default headless suppression. Restored in afterEach (not the
|
||||
// helper) so the blind restore branch — gated on !isTerminalHeadless() — still
|
||||
// runs while the test drives emergencyTerminalRestore() after terminal.stop().
|
||||
let previousHeadless = false;
|
||||
|
||||
function restoreProperty(target: object, key: string, descriptor: PropertyDescriptor | undefined): void {
|
||||
if (descriptor) {
|
||||
@@ -48,8 +54,13 @@ function startCapturedTerminal() {
|
||||
}
|
||||
|
||||
describe("emergencyTerminalRestore alt-screen gating", () => {
|
||||
beforeEach(() => {
|
||||
previousHeadless = setTerminalHeadless(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setAltScreenActive(false);
|
||||
setTerminalHeadless(previousHeadless);
|
||||
vi.restoreAllMocks();
|
||||
restoreProperty(process.stdin, "isTTY", stdinIsTtyDescriptor);
|
||||
restoreProperty(process.stdout, "isTTY", stdoutIsTtyDescriptor);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
|
||||
import { chunkForConPTY, ProcessTerminal } from "@oh-my-pi/pi-tui/terminal";
|
||||
import { setTerminalHeadless } from "@oh-my-pi/pi-utils";
|
||||
|
||||
// Regression test for https://github.com/can1357/oh-my-pi/issues/2034
|
||||
//
|
||||
@@ -161,6 +162,9 @@ describe("issue #2034: chunk large terminal writes on Windows ConPTY", () => {
|
||||
const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
|
||||
const originalWslDistro = Bun.env.WSL_DISTRO_NAME;
|
||||
const originalWslInterop = Bun.env.WSL_INTEROP;
|
||||
// This block drives the real ProcessTerminal#write path, so it opts out of
|
||||
// the test-default headless suppression for the duration of each case.
|
||||
let previousHeadless = false;
|
||||
|
||||
function setEnv(key: string, value: string | undefined): void {
|
||||
if (value === undefined) delete Bun.env[key];
|
||||
@@ -173,10 +177,12 @@ describe("issue #2034: chunk large terminal writes on Windows ConPTY", () => {
|
||||
// Clear WSL markers by default; tests opt in.
|
||||
setEnv("WSL_DISTRO_NAME", undefined);
|
||||
setEnv("WSL_INTEROP", undefined);
|
||||
previousHeadless = setTerminalHeadless(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
setTerminalHeadless(previousHeadless);
|
||||
if (platformDescriptor) Object.defineProperty(process, "platform", platformDescriptor);
|
||||
if (stdinIsTtyDescriptor) Object.defineProperty(process.stdin, "isTTY", stdinIsTtyDescriptor);
|
||||
else Reflect.deleteProperty(process.stdin, "isTTY");
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
setOsc99Supported,
|
||||
TERMINAL,
|
||||
} from "@oh-my-pi/pi-tui/terminal-capabilities";
|
||||
import { setTerminalHeadless } from "@oh-my-pi/pi-utils";
|
||||
|
||||
const stdinIsTtyDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
|
||||
const stdoutIsTtyDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
|
||||
@@ -54,13 +55,19 @@ function setupProcessTerminal() {
|
||||
return { terminal, writes, received };
|
||||
}
|
||||
|
||||
// setupProcessTerminal() drives the real ProcessTerminal start()/probe path, so
|
||||
// these cases opt out of the test-default headless suppression.
|
||||
let previousHeadless = false;
|
||||
|
||||
describe("terminal notifications", () => {
|
||||
beforeEach(() => {
|
||||
setOsc99Supported(false);
|
||||
previousHeadless = setTerminalHeadless(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
setTerminalHeadless(previousHeadless);
|
||||
setOsc99Supported(false);
|
||||
mutableTerminal.notifyProtocol = originalNotifyProtocol;
|
||||
restoreEnv("PI_TUI_OSC99_PROBE", originalOsc99Probe);
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
|
||||
import { ProcessTerminal } from "@oh-my-pi/pi-tui/terminal";
|
||||
import { isTerminalHeadless, setTerminalHeadless } from "@oh-my-pi/pi-utils";
|
||||
|
||||
// Regression: running `bun test` inside a real TTY used to paint the TUI frame,
|
||||
// the start() capability probes (OSC 11 / DA1 / kitty), and the editor/status
|
||||
// box straight to the developer's terminal — `#safeWrite` only skipped on
|
||||
// `!process.stdout.isTTY`, which is false in an interactive terminal. The
|
||||
// headless default (on under the test runtime) must suppress every real
|
||||
// terminal side effect regardless of isTTY, while an explicit opt-out still
|
||||
// drives the real pipeline for the terminal-contract suites.
|
||||
|
||||
const stdinIsTtyDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
|
||||
const stdoutIsTtyDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
|
||||
const stdinSetRawModeDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "setRawMode");
|
||||
|
||||
function restoreProperty(target: object, key: string, descriptor: PropertyDescriptor | undefined): void {
|
||||
if (descriptor) {
|
||||
Object.defineProperty(target, key, descriptor);
|
||||
return;
|
||||
}
|
||||
delete (target as Record<string, unknown>)[key];
|
||||
}
|
||||
|
||||
describe("ProcessTerminal headless suppression", () => {
|
||||
let writes: string[];
|
||||
let rawModeCalls: number;
|
||||
|
||||
beforeEach(() => {
|
||||
writes = [];
|
||||
rawModeCalls = 0;
|
||||
// Force a real TTY: the exact condition under which the leak surfaced.
|
||||
Object.defineProperty(process.stdin, "isTTY", { value: true, configurable: true });
|
||||
Object.defineProperty(process.stdout, "isTTY", { value: true, configurable: true });
|
||||
Object.defineProperty(process.stdin, "setRawMode", {
|
||||
value: () => {
|
||||
rawModeCalls++;
|
||||
return process.stdin;
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
vi.spyOn(process, "kill").mockReturnValue(true);
|
||||
vi.spyOn(process.stdin, "resume").mockImplementation(() => process.stdin);
|
||||
vi.spyOn(process.stdin, "pause").mockImplementation(() => process.stdin);
|
||||
vi.spyOn(process.stdin, "setEncoding").mockImplementation(() => process.stdin);
|
||||
vi.spyOn(process.stdout, "write").mockImplementation(chunk => {
|
||||
writes.push(typeof chunk === "string" ? chunk : chunk.toString());
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
restoreProperty(process.stdin, "isTTY", stdinIsTtyDescriptor);
|
||||
restoreProperty(process.stdout, "isTTY", stdoutIsTtyDescriptor);
|
||||
restoreProperty(process.stdin, "setRawMode", stdinSetRawModeDescriptor);
|
||||
});
|
||||
|
||||
it("writes nothing to a real TTY while headless (the bun-test default)", () => {
|
||||
expect(isTerminalHeadless()).toBe(true);
|
||||
|
||||
const terminal = new ProcessTerminal();
|
||||
// start() would emit OSC 11 + DA1 + kitty probes; write() a TUI frame;
|
||||
// setProgress() the OSC 9;4 keepalive; stop() the teardown escapes.
|
||||
terminal.start(
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
terminal.write("frame paint");
|
||||
terminal.setProgress(true);
|
||||
terminal.stop();
|
||||
|
||||
expect(writes).toEqual([]);
|
||||
expect(rawModeCalls).toBe(0);
|
||||
});
|
||||
|
||||
it("drives the real terminal once a suite opts out via setTerminalHeadless(false)", () => {
|
||||
const previous = setTerminalHeadless(false);
|
||||
const terminal = new ProcessTerminal();
|
||||
try {
|
||||
terminal.start(
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
terminal.write("frame paint");
|
||||
|
||||
const emitted = writes.join("");
|
||||
expect(emitted).toContain("frame paint");
|
||||
// start() probes the background color (OSC 11) for dark/light detection.
|
||||
expect(emitted).toContain("\x1b]11;?");
|
||||
} finally {
|
||||
terminal.stop();
|
||||
setTerminalHeadless(previous);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { vi } from "bun:test";
|
||||
import { type Component, TUI } from "@oh-my-pi/pi-tui";
|
||||
import { ProcessTerminal } from "@oh-my-pi/pi-tui/terminal";
|
||||
import { setTerminalHeadless } from "@oh-my-pi/pi-utils";
|
||||
|
||||
// Pristine descriptors, captured once at module load. Every dispose() restores
|
||||
// to these so the harness is full-suite safe across repeated create/dispose
|
||||
@@ -70,6 +71,10 @@ export function createProcessTerminalRenderHarness(
|
||||
initialColumns = 100,
|
||||
initialRows = 30,
|
||||
): ProcessTerminalRenderHarness {
|
||||
// This harness exercises the real ProcessTerminal I/O pipeline, so it opts
|
||||
// out of the test-default headless suppression and restores the prior value
|
||||
// on dispose.
|
||||
const previousHeadless = setTerminalHeadless(false);
|
||||
Object.defineProperty(process.stdin, "isTTY", { value: true, configurable: true });
|
||||
Object.defineProperty(process.stdout, "isTTY", { value: true, configurable: true });
|
||||
Object.defineProperty(process.stdin, "setRawMode", { value: vi.fn(), configurable: true });
|
||||
@@ -92,7 +97,13 @@ export function createProcessTerminalRenderHarness(
|
||||
const tui = new TUI(terminal);
|
||||
const probe = new WidthProbe();
|
||||
tui.addChild(probe);
|
||||
tui.start();
|
||||
try {
|
||||
tui.start();
|
||||
} catch (err) {
|
||||
// A start() regression must not poison the worker with headless=false.
|
||||
setTerminalHeadless(previousHeadless);
|
||||
throw err;
|
||||
}
|
||||
|
||||
const settle = () => Bun.sleep(SETTLE_MS);
|
||||
|
||||
@@ -118,6 +129,7 @@ export function createProcessTerminalRenderHarness(
|
||||
},
|
||||
dispose() {
|
||||
tui.stop();
|
||||
setTerminalHeadless(previousHeadless);
|
||||
for (const spy of spies) spy.mockRestore();
|
||||
for (const [target, key, descriptor] of PRISTINE) {
|
||||
if (descriptor) Object.defineProperty(target, key, descriptor);
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
visibleWidth,
|
||||
wrapTextWithAnsi,
|
||||
} from "@oh-my-pi/pi-tui/utils";
|
||||
import { setTerminalHeadless } from "@oh-my-pi/pi-utils";
|
||||
import { StressRenderScheduler } from "./render-stress-scheduler";
|
||||
import { VirtualTerminal } from "./virtual-terminal";
|
||||
|
||||
@@ -4039,6 +4040,10 @@ export async function runNoReflowResizeNotificationRegression(): Promise<void> {
|
||||
});
|
||||
Object.defineProperty(process, "kill", { value: () => true, configurable: true });
|
||||
|
||||
// Exercises the real ProcessTerminal stdin/stdout pipeline; opt out of
|
||||
// the test-default headless suppression inside the try so the finally
|
||||
// below always restores the prior value, even on a start()/render throw.
|
||||
let previousHeadless = false;
|
||||
const term = new ProcessTerminal();
|
||||
const scheduler = new StressRenderScheduler();
|
||||
const tui = new TUI(term, true, { renderScheduler: scheduler });
|
||||
@@ -4048,6 +4053,7 @@ export async function runNoReflowResizeNotificationRegression(): Promise<void> {
|
||||
tui.addChild(component);
|
||||
|
||||
try {
|
||||
previousHeadless = setTerminalHeadless(false);
|
||||
tui.start();
|
||||
await scheduler.drain(drainTarget);
|
||||
|
||||
@@ -4082,6 +4088,7 @@ export async function runNoReflowResizeNotificationRegression(): Promise<void> {
|
||||
restoreOwnProperty(process.stdin, "pause", stdinPause);
|
||||
restoreOwnProperty(process.stdout, "write", stdoutWrite);
|
||||
restoreOwnProperty(process, "kill", processKill);
|
||||
setTerminalHeadless(previousHeadless);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
getTerminalInfo,
|
||||
setCellDimensions,
|
||||
} from "@oh-my-pi/pi-tui/terminal-capabilities";
|
||||
import { setTerminalHeadless } from "@oh-my-pi/pi-utils";
|
||||
|
||||
const stdinIsTtyDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
|
||||
const stdoutIsTtyDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
|
||||
@@ -17,6 +18,10 @@ const stdinSetRawModeDescriptor = Object.getOwnPropertyDescriptor(process.stdin,
|
||||
const originalWslDistroName = Bun.env.WSL_DISTRO_NAME;
|
||||
const originalWslInterop = Bun.env.WSL_INTEROP;
|
||||
|
||||
// These suites drive the real ProcessTerminal start()/probe pipeline, so they
|
||||
// opt out of the test-default headless suppression and restore it per case.
|
||||
let previousHeadless = false;
|
||||
|
||||
function restoreProperty(target: object, key: string, descriptor: PropertyDescriptor | undefined): void {
|
||||
if (descriptor) {
|
||||
Object.defineProperty(target, key, descriptor);
|
||||
@@ -38,11 +43,13 @@ describe("ProcessTerminal OSC 11 appearance detection", () => {
|
||||
Object.defineProperty(process.stdin, "isTTY", { value: true, configurable: true });
|
||||
Object.defineProperty(process.stdout, "isTTY", { value: true, configurable: true });
|
||||
Object.defineProperty(process.stdin, "setRawMode", { value: vi.fn(), configurable: true });
|
||||
previousHeadless = setTerminalHeadless(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
setTerminalHeadless(previousHeadless);
|
||||
restoreProperty(process.stdin, "isTTY", stdinIsTtyDescriptor);
|
||||
restoreProperty(process.stdout, "isTTY", stdoutIsTtyDescriptor);
|
||||
restoreProperty(process.stdin, "setRawMode", stdinSetRawModeDescriptor);
|
||||
@@ -449,11 +456,13 @@ describe("ProcessTerminal DECRQM + in-band resize (DEC 2026/2048)", () => {
|
||||
Object.defineProperty(process.stdout, "isTTY", { value: true, configurable: true });
|
||||
Object.defineProperty(process.stdin, "setRawMode", { value: vi.fn(), configurable: true });
|
||||
originalCellDims = { ...getCellDimensions() };
|
||||
previousHeadless = setTerminalHeadless(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
setTerminalHeadless(previousHeadless);
|
||||
restoreProperty(process.stdin, "isTTY", stdinIsTtyDescriptor);
|
||||
restoreProperty(process.stdout, "isTTY", stdoutIsTtyDescriptor);
|
||||
restoreProperty(process.stdin, "setRawMode", stdinSetRawModeDescriptor);
|
||||
|
||||
Reference in New Issue
Block a user