Files
NetMesh/components/terminal/runtime/terminalSessionAttachment.test.ts
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

2593 lines
85 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import { createRequire } from "node:module";
import type { Terminal as XTerm } from "@xterm/xterm";
import {
FLOW_HIGH_WATER_MARK,
FLOW_CHAR_COUNT_ACK_SIZE,
FLOW_LOW_WATER_MARK,
MAX_TERMINAL_PLAIN_WRITE_CHUNK_BYTES,
XTERM_WRITE_CALLBACK_FAST_PATH_MAX_BYTES,
XTERM_WRITE_CALLBACK_BATCH_BYTES,
} from "./terminalFlowConstants.ts";
import {
attachSessionToTerminal,
getFlowController,
notePendingOutputScrollIfEnabled,
resolveAttachSnapshot,
tryAttachSessionToTerminal,
writeSessionData,
} from "./terminalSessionAttachment.ts";
import { getVisibleTerminalLineTimestampRows } from "./terminalLineTimestamps.ts";
import {
getTerminalOutputPressure,
noteTerminalOutputPressureData,
} from "./terminalOutputPressure.ts";
import {
clearTerminalSessionFlowAck,
flushTerminalSessionFlowAck,
} from "./terminalFlowAckBuffer.ts";
import {
flushTerminalWriteCoalescer,
resetTerminalWriteCoalescer,
} from "./terminalWriteCoalescer.ts";
import {
cancelScheduledUnfocusedRepaint,
flushPendingTerminalWritesBeforeHibernate,
flushPendingTerminalWritesOnResume,
} from "./terminalUnfocusedRepaint.ts";
import {
clearDeferredTerminalWriteAck,
getDeferredTerminalWriteAckBytes,
} from "./terminalWriteAckDeferral.ts";
import { flushTerminalWriteQueueBypassingTimers } from "./terminalWriteQueue.ts";
import { prioritizeTerminalInput } from "./terminalOutputPipeline";
import {
createPromptLineBreakState,
markTerminalCommandCompletionPending,
} from "./promptLineBreak";
test("resolveAttachSnapshot keeps an authoritative empty final snapshot", () => {
assert.equal(resolveAttachSnapshot("", "stale fallback"), "");
assert.equal(resolveAttachSnapshot("fresh", "stale fallback"), "fresh");
assert.equal(resolveAttachSnapshot(undefined, "fallback"), "fallback");
});
const createFakeTerm = (activeType = "normal") => {
const writes: string[] = [];
const markerLines: number[] = [];
const disposedMarkerLines: number[] = [];
let cursorLine = 0;
const active: {
type: string;
viewportY: number;
cursorX: number;
baseY: number;
cursorY: number;
length: number;
getLine: () => { isWrapped: boolean };
} = {
type: activeType,
viewportY: 0,
cursorX: 0,
baseY: 0,
get cursorY() {
return cursorLine;
},
set cursorY(value: number) {
cursorLine = Math.max(0, value);
},
get length() {
return cursorLine + 1;
},
getLine: () => ({ isWrapped: false }),
};
const term = {
rows: 24,
cols: 80,
options: { scrollback: 1000 },
buffer: {
active,
},
write(data: string, callback?: () => void) {
writes.push(data);
if (data.includes("\x1b[?1049h") || data.includes("\x1b[?47h") || data.includes("\x1b[?1047h")) {
active.type = "alternate";
}
if (data.includes("\x1b[?1049l") || data.includes("\x1b[?47l") || data.includes("\x1b[?1047l")) {
active.type = "normal";
}
for (const char of data) {
if (char === "\n") {
cursorLine += 1;
}
}
callback?.();
},
registerMarker(offset: number) {
const line = cursorLine + offset;
markerLines.push(line);
const marker = {
line,
isDisposed: false,
dispose() {
marker.isDisposed = true;
disposedMarkerLines.push(line);
},
};
return marker;
},
scrollToBottom() {},
} as unknown as XTerm;
return { term, writes, markerLines, disposedMarkerLines };
};
const createContext = (showLineTimestamps: boolean, host: Record<string, unknown> = {}) => {
// Production gates markers on host.showLineTimestamps; keep the first arg as
// the host default unless the test overrides host explicitly.
const liveHost = { showLineTimestamps, ...host };
return {
host: liveHost,
// Mirror Terminal.tsx: write path reads hostRef.current for live toggles.
hostRef: { current: liveHost },
terminalSettingsRef: {
current: {
showLineTimestamps,
scrollOnOutput: false,
forcePromptNewLine: false,
},
},
terminalSettings: {
showLineTimestamps,
scrollOnOutput: false,
forcePromptNewLine: false,
},
terminalBackend: {},
sessionRef: { current: "session-1" },
promptLineBreakStateRef: { current: undefined },
};
};
test("terminal output publishes one completion for each pending command at the next prompt", () => {
const { term } = createFakeTerm();
Object.assign(term.buffer.active, {
cursorX: 2,
cursorY: 0,
baseY: 0,
getLine(line: number) {
if (line !== 0) return undefined;
return {
isWrapped: false,
translateToString() { return "$ "; },
};
},
});
const state = createPromptLineBreakState();
const stateRef = { current: state };
markTerminalCommandCompletionPending(stateRef);
markTerminalCommandCompletionPending(stateRef);
let completions = 0;
const ctx = {
...createContext(false),
promptLineBreakStateRef: stateRef,
onCommandCompleted() { completions += 1; },
};
writeSessionData(ctx as never, term, "$ ");
assert.equal(completions, 2);
assert.equal(state.pendingCommandCompletions, 0);
});
const withDocumentVisibility = (
visibilityState: "visible" | "hidden",
run: () => void,
options: { hasFocus?: boolean } = {},
) => {
const hasFocus = options.hasFocus ?? visibilityState === "visible";
const original = Object.getOwnPropertyDescriptor(globalThis, "document");
Object.defineProperty(globalThis, "document", {
configurable: true,
value: {
visibilityState,
hasFocus: () => hasFocus,
},
});
try {
run();
} finally {
if (original) {
Object.defineProperty(globalThis, "document", original);
} else {
Reflect.deleteProperty(globalThis, "document");
}
}
};
type WriteScheduleQueue = {
frames: Array<FrameRequestCallback>;
microtasks: Array<() => void>;
scheduledCount: () => number;
flushScheduled: () => void;
};
const withAnimationFrameQueue = (run: (schedule: WriteScheduleQueue) => void) => {
const originalRequest = Object.getOwnPropertyDescriptor(globalThis, "requestAnimationFrame");
const originalCancel = Object.getOwnPropertyDescriptor(globalThis, "cancelAnimationFrame");
const originalMicrotask = globalThis.queueMicrotask;
const frames: Array<FrameRequestCallback> = [];
const microtasks: Array<() => void> = [];
Object.defineProperty(globalThis, "requestAnimationFrame", {
configurable: true,
value: (callback: FrameRequestCallback) => {
frames.push(callback);
return frames.length;
},
});
Object.defineProperty(globalThis, "cancelAnimationFrame", {
configurable: true,
value: () => {},
});
globalThis.queueMicrotask = (callback: () => void) => {
microtasks.push(callback);
};
const flushScheduled = () => {
while (microtasks.length > 0 || frames.length > 0) {
const pendingMicrotasks = microtasks.splice(0);
for (const task of pendingMicrotasks) {
task();
}
const pendingFrames = frames.splice(0);
for (const frame of pendingFrames) {
frame(0);
}
}
};
try {
run({
frames,
microtasks,
scheduledCount: () => frames.length + microtasks.length,
flushScheduled,
});
} finally {
globalThis.queueMicrotask = originalMicrotask;
if (originalRequest) {
Object.defineProperty(globalThis, "requestAnimationFrame", originalRequest);
} else {
Reflect.deleteProperty(globalThis, "requestAnimationFrame");
}
if (originalCancel) {
Object.defineProperty(globalThis, "cancelAnimationFrame", originalCancel);
} else {
Reflect.deleteProperty(globalThis, "cancelAnimationFrame");
}
}
};
test("notePendingOutputScrollIfEnabled leaves hidden output unmarked when scroll-on-output is disabled", () => {
const pendingOutputScrollRef = { current: false };
notePendingOutputScrollIfEnabled({
terminalSettingsRef: { current: { scrollOnOutput: false } },
pendingOutputScrollRef,
} as never);
assert.equal(pendingOutputScrollRef.current, false);
});
test("notePendingOutputScrollIfEnabled marks hidden output when scroll-on-output is enabled", () => {
const pendingOutputScrollRef = { current: false };
notePendingOutputScrollIfEnabled({
terminalSettingsRef: { current: { scrollOnOutput: true } },
pendingOutputScrollRef,
} as never);
assert.equal(pendingOutputScrollRef.current, true);
});
test("writeSessionData clears renderer backlog while deferring IPC ack", () => {
const term = {
buffer: { active: { type: "normal" } },
write(_data: string, callback?: () => void) {
callback?.();
},
scrollToBottom() {},
} as unknown as XTerm;
const ctx = createContext(false);
const ingressPerWrite = 100;
const writeCount = Math.floor((XTERM_WRITE_CALLBACK_BATCH_BYTES - 1) / ingressPerWrite);
for (let index = 0; index < writeCount; index += 1) {
writeSessionData(ctx as never, term, "x".repeat(ingressPerWrite));
}
flushTerminalWriteCoalescer(term);
for (let guard = 0; guard < 1000 && flushTerminalWriteQueueBypassingTimers(term); guard += 1) {
// A busy full-suite run can cross the queue's turn budget and defer the
// next synchronous fake write. Drain those intentional timer yields before
// asserting the completed-backlog state.
}
const flow = getFlowController(ctx as never, term);
assert.equal(flow.pendingBytes(), 0);
assert.ok(getDeferredTerminalWriteAckBytes(term) > 0);
clearDeferredTerminalWriteAck(term);
});
test("writeSessionData keeps hidden-page xterm parsing asynchronous", () => {
clearTerminalSessionFlowAck("session-1");
const payload = "x".repeat(FLOW_CHAR_COUNT_ACK_SIZE + 1);
const writes: string[] = [];
const pendingCallbacks: Array<() => void> = [];
const writeBuffer = {
flushSync() {
while (pendingCallbacks.length > 0) {
pendingCallbacks.shift()?.();
}
},
};
const term = {
buffer: { active: { type: "normal" } },
_core: { _writeBuffer: writeBuffer },
write(data: string, callback?: () => void) {
writes.push(data);
if (callback) pendingCallbacks.push(callback);
},
scrollToBottom() {},
} as unknown as XTerm;
const acked: number[] = [];
const ctx = {
...createContext(false),
isVisibleRef: { current: true },
sessionRef: { current: "session-1" },
terminalBackend: {
ackSessionFlow: (_sessionId: string, bytes: number) => {
acked.push(bytes);
},
},
};
withDocumentVisibility("hidden", () => {
writeSessionData(ctx as never, term, payload);
});
assert.equal(pendingCallbacks.length, 1);
assert.equal(getFlowController(ctx as never, term).pendingBytes(), payload.length);
pendingCallbacks.shift()?.();
flushTerminalSessionFlowAck("session-1");
assert.equal(writes.join(""), payload);
// Hidden-page output starts promptly, but xterm owns async parser completion.
assert.deepEqual(writes.map((write) => write.length), [payload.length]);
assert.equal(pendingCallbacks.length, 0);
assert.equal(getFlowController(ctx as never, term).pendingBytes(), 0);
assert.equal(getDeferredTerminalWriteAckBytes(term), 0);
assert.equal(acked.reduce((total, bytes) => total + bytes, 0), payload.length);
clearTerminalSessionFlowAck("session-1");
});
test("writeSessionData batches while unfocused-but-visible then drains on idle flush", async () => {
clearTerminalSessionFlowAck("session-1");
const payload = "x".repeat(FLOW_CHAR_COUNT_ACK_SIZE + 1);
const writes: string[] = [];
const pendingCallbacks: Array<() => void> = [];
const writeBuffer = {
flushSync() {
while (pendingCallbacks.length > 0) {
pendingCallbacks.shift()?.();
}
},
};
const term = {
buffer: { active: { type: "normal" } },
_core: { _writeBuffer: writeBuffer },
write(data: string, callback?: () => void) {
writes.push(data);
if (callback) pendingCallbacks.push(callback);
},
scrollToBottom() {},
} as unknown as XTerm;
const acked: number[] = [];
const ctx = {
...createContext(false),
isVisibleRef: { current: true },
sessionRef: { current: "session-1" },
terminalBackend: {
ackSessionFlow: (_sessionId: string, bytes: number) => {
acked.push(bytes);
},
},
};
withDocumentVisibility("visible", () => {
writeSessionData(ctx as never, term, payload);
}, { hasFocus: false });
// Unfocused-but-visible no longer force-flushes every chunk (preserves
// batching / alt-screen frames). Microtask/idle/unfocused timers drain it.
await new Promise((resolve) => { setTimeout(resolve, 90); });
flushTerminalWriteCoalescer(term);
flushTerminalWriteQueueBypassingTimers(term);
while (pendingCallbacks.length > 0) {
pendingCallbacks.shift()?.();
}
flushTerminalSessionFlowAck("session-1");
assert.equal(writes.join(""), payload);
assert.equal(pendingCallbacks.length, 0);
assert.equal(getFlowController(ctx as never, term).pendingBytes(), 0);
assert.equal(getDeferredTerminalWriteAckBytes(term), 0);
assert.equal(acked.reduce((total, bytes) => total + bytes, 0), payload.length);
cancelScheduledUnfocusedRepaint(term);
resetTerminalWriteCoalescer(term);
clearTerminalSessionFlowAck("session-1");
});
test("continuous visible output cannot postpone the first idle drain deadline", () => {
clearTerminalSessionFlowAck("session-1");
const { term } = createFakeTerm();
const ctx = createContext(false);
const originalSetTimeout = globalThis.setTimeout;
const originalClearTimeout = globalThis.clearTimeout;
let now = 0;
let nextTimerId = 1;
const timers = new Map<number, {
callback: () => void;
delay: number;
due: number;
}>();
const advanceBy = (elapsedMs: number) => {
const target = now + elapsedMs;
while (true) {
const next = [...timers.entries()]
// Keep zero-delay queue yields paused. The visible idle deadline is the
// independent safety wake-up under test.
.filter(([, timer]) => timer.delay > 0 && timer.due <= target)
.sort((left, right) => left[1].due - right[1].due)[0];
if (!next) break;
const [id, timer] = next;
timers.delete(id);
now = timer.due;
timer.callback();
}
now = target;
};
globalThis.setTimeout = ((callback, delay = 0, ...args) => {
const id = nextTimerId;
nextTimerId += 1;
const normalizedDelay = Number(delay) || 0;
timers.set(id, {
callback: () => callback(...args),
delay: normalizedDelay,
due: now + normalizedDelay,
});
return id as unknown as ReturnType<typeof setTimeout>;
}) as typeof setTimeout;
globalThis.clearTimeout = ((timer) => {
timers.delete(Number(timer));
}) as typeof clearTimeout;
try {
withAnimationFrameQueue((schedule) => {
writeSessionData(
ctx as never,
term,
"x".repeat(MAX_TERMINAL_PLAIN_WRITE_CHUNK_BYTES + 1024),
);
schedule.flushScheduled();
assert.ok(getFlowController(ctx as never, term).pendingBytes() > 0);
for (let index = 0; index < 4; index += 1) {
advanceBy(5);
writeSessionData(ctx as never, term, "stream");
schedule.flushScheduled();
}
// The first 24 ms deadline must still fire even though chunks arrived at
// 5, 10, 15, and 20 ms. A debounce would postpone it to 44 ms.
advanceBy(4);
assert.equal(getFlowController(ctx as never, term).pendingBytes(), 0);
});
} finally {
advanceBy(100);
globalThis.setTimeout = originalSetTimeout;
globalThis.clearTimeout = originalClearTimeout;
resetTerminalWriteCoalescer(term);
clearTerminalSessionFlowAck("session-1");
}
});
test("a settled visible batch does not lend its idle deadline to the next frame", () => {
clearTerminalSessionFlowAck("session-1");
const { term, writes } = createFakeTerm("alternate");
const ctx = createContext(false);
const originalSetTimeout = globalThis.setTimeout;
const originalClearTimeout = globalThis.clearTimeout;
let now = 0;
let nextTimerId = 1;
const timers = new Map<number, {
callback: () => void;
delay: number;
due: number;
}>();
const advanceBy = (elapsedMs: number) => {
const target = now + elapsedMs;
while (true) {
const next = [...timers.entries()]
.filter(([, timer]) => timer.delay > 0 && timer.due <= target)
.sort((left, right) => left[1].due - right[1].due)[0];
if (!next) break;
const [id, timer] = next;
timers.delete(id);
now = timer.due;
timer.callback();
}
now = target;
};
globalThis.setTimeout = ((callback, delay = 0, ...args) => {
const id = nextTimerId;
nextTimerId += 1;
const normalizedDelay = Number(delay) || 0;
timers.set(id, {
callback: () => callback(...args),
delay: normalizedDelay,
due: now + normalizedDelay,
});
return id as unknown as ReturnType<typeof setTimeout>;
}) as typeof setTimeout;
globalThis.clearTimeout = ((timer) => {
timers.delete(Number(timer));
}) as typeof clearTimeout;
try {
withAnimationFrameQueue((schedule) => {
writeSessionData(ctx as never, term, "frame-one");
advanceBy(16);
schedule.flushScheduled();
assert.deepEqual(writes, ["frame-one"]);
advanceBy(4);
writeSessionData(ctx as never, term, "frame-two");
// The first batch settled at 16 ms. Its old 24 ms safety deadline must
// not flush this new frame before the next animation frame at 32 ms.
advanceBy(4);
assert.deepEqual(writes, ["frame-one"]);
advanceBy(8);
schedule.flushScheduled();
assert.deepEqual(writes, ["frame-one", "frame-two"]);
});
} finally {
advanceBy(100);
globalThis.setTimeout = originalSetTimeout;
globalThis.clearTimeout = originalClearTimeout;
resetTerminalWriteCoalescer(term);
clearTerminalSessionFlowAck("session-1");
}
});
test("Herdr-style frames complete on xterm's normal async path in every visibility state", async (t) => {
const require = createRequire(import.meta.url);
const { Terminal } = require("@xterm/xterm") as {
Terminal: new (options: Record<string, unknown>) => XTerm;
};
const churn = Array.from(
{ length: 7_000 },
(_, index) => `\x1b[${(index % 24) + 1};1Hframe-${String(index).padStart(4, "0")}`,
).join("");
const finalRows = Array.from(
{ length: 24 },
(_, index) => `\x1b[${index + 1};1HHerdr row ${String(index + 1).padStart(2, "0")}`,
).join("");
const payload = `\x1b[?2026h\x1b[2J\x1b[H${churn}\x1b[2J${finalRows}\x1b[?2026l`;
assert.ok(payload.length > XTERM_WRITE_CALLBACK_FAST_PATH_MAX_BYTES);
const scenarios = [
{ name: "visible pane", documentVisibility: "visible" as const, paneVisible: true },
{ name: "hidden pane", documentVisibility: "visible" as const, paneVisible: false },
{ name: "hidden page", documentVisibility: "hidden" as const, paneVisible: true },
{
name: "hidden pane revealed during write",
documentVisibility: "visible" as const,
paneVisible: false,
revealBeforeAck: true,
},
{
name: "hidden page restored during write",
documentVisibility: "hidden" as const,
paneVisible: true,
revealBeforeAck: true,
},
];
for (const scenario of scenarios) {
await t.test(scenario.name, async () => {
const sessionId = `session-${scenario.name.replaceAll(" ", "-")}`;
clearTerminalSessionFlowAck(sessionId);
const term = new Terminal({ cols: 80, rows: 24, scrollback: 1000, allowProposedApi: true });
const writeBuffer = (term as unknown as {
_core: { _writeBuffer: { flushSync: () => void } };
})._core._writeBuffer;
const originalFlushSync = writeBuffer.flushSync.bind(writeBuffer);
let flushSyncCalls = 0;
writeBuffer.flushSync = () => {
flushSyncCalls += 1;
originalFlushSync();
};
let ackedBytes = 0;
let expectedAckBytes = payload.length;
let resolveAck: (() => void) | undefined;
const waitForAck = () => new Promise<void>((resolve, reject) => {
let timer: ReturnType<typeof setTimeout> | undefined;
resolveAck = () => {
if (timer) clearTimeout(timer);
resolve();
};
if (ackedBytes >= expectedAckBytes) {
resolveAck();
return;
}
timer = setTimeout(() => {
resolveAck = undefined;
reject(new Error(`timed out waiting for ${scenario.name}`));
}, 3_000);
});
const ctx = {
...createContext(false),
isVisibleRef: { current: true },
isPaneVisibleRef: { current: scenario.paneVisible },
sessionRef: { current: sessionId },
terminalBackend: {
ackSessionFlow: (_sessionId: string, bytes: number) => {
ackedBytes += bytes;
if (ackedBytes >= expectedAckBytes) resolveAck?.();
},
},
};
try {
withDocumentVisibility(scenario.documentVisibility, () => {
writeSessionData(ctx as never, term, payload);
}, { hasFocus: scenario.documentVisibility === "visible" });
if (scenario.revealBeforeAck) {
ctx.isPaneVisibleRef.current = true;
withDocumentVisibility("visible", () => {
flushPendingTerminalWritesOnResume(term);
}, { hasFocus: true });
}
await waitForAck();
const followUp = "\x1b[24;1H\x1b[2Kafter-input";
expectedAckBytes += followUp.length;
withDocumentVisibility(scenario.documentVisibility, () => {
writeSessionData(ctx as never, term, followUp);
}, { hasFocus: scenario.documentVisibility === "visible" });
await waitForAck();
assert.equal(flushSyncCalls, 0);
assert.equal(term.buffer.active.getLine(0)?.translateToString(true), "Herdr row 01");
assert.equal(term.buffer.active.getLine(22)?.translateToString(true), "Herdr row 23");
assert.equal(term.buffer.active.getLine(23)?.translateToString(true), "after-input");
assert.equal(getFlowController(ctx as never, term).pendingBytes(), 0);
assert.equal(ackedBytes, expectedAckBytes);
} finally {
resetTerminalWriteCoalescer(term);
clearTerminalSessionFlowAck(sessionId);
term.dispose();
}
});
}
});
test("revealing and resizing waits until a Herdr-style frame has completed", async () => {
const require = createRequire(import.meta.url);
const { Terminal } = require("@xterm/xterm") as {
Terminal: new (options: Record<string, unknown>) => XTerm;
};
const term = new Terminal({ cols: 80, rows: 24, scrollback: 1000, allowProposedApi: true });
const writeBuffer = (term as unknown as {
_core: { _writeBuffer: { flushSync: () => void } };
})._core._writeBuffer;
const originalFlushSync = writeBuffer.flushSync.bind(writeBuffer);
let flushSyncCalls = 0;
writeBuffer.flushSync = () => {
flushSyncCalls += 1;
originalFlushSync();
};
const payload = `\x1b[?2026h\x1b[2J\x1b[H${"Herdr resize frame".repeat(5_000)}\x1b[?2026l`;
const ctx = {
...createContext(false),
isVisibleRef: { current: true },
isPaneVisibleRef: { current: false },
sessionRef: { current: "session-resize" },
};
try {
writeSessionData(ctx as never, term, payload);
ctx.isPaneVisibleRef.current = true;
flushPendingTerminalWritesOnResume(term);
const settled = await flushPendingTerminalWritesBeforeHibernate(term);
assert.equal(settled, true);
assert.equal(flushSyncCalls, 0);
assert.equal(getFlowController(ctx as never, term).pendingBytes(), 0);
term.resize(100, 30);
assert.equal(flushSyncCalls, 1);
} finally {
resetTerminalWriteCoalescer(term);
clearTerminalSessionFlowAck("session-resize");
term.dispose();
}
});
test("writeSessionData flushes pending coalesced output with the background fast path", async () => {
clearTerminalSessionFlowAck("session-1");
const pendingPayload = "pending output\n";
const currentPayload = "current\n";
const writes: string[] = [];
const pendingCallbacks: Array<() => void> = [];
const writeBuffer = {
flushSync() {
while (pendingCallbacks.length > 0) {
pendingCallbacks.shift()?.();
}
},
};
const term = {
buffer: { active: { type: "normal" } },
_core: { _writeBuffer: writeBuffer },
write(data: string, callback?: () => void) {
writes.push(data);
if (callback) pendingCallbacks.push(callback);
},
scrollToBottom() {},
} as unknown as XTerm;
const acked: number[] = [];
const expectedAckBytes = pendingPayload.length + currentPayload.length;
let resolveAck: (() => void) | undefined;
const ackDone = new Promise<void>((resolve) => { resolveAck = resolve; });
const ctx = {
...createContext(false),
isVisibleRef: { current: true },
sessionRef: { current: "session-1" },
terminalBackend: {
ackSessionFlow: (_sessionId: string, bytes: number) => {
acked.push(bytes);
if (acked.reduce((total, value) => total + value, 0) >= expectedAckBytes) {
resolveAck?.();
}
},
},
};
withAnimationFrameQueue((schedule) => {
withDocumentVisibility("visible", () => {
writeSessionData(ctx as never, term, pendingPayload);
});
assert.ok(schedule.scheduledCount() >= 1);
assert.deepEqual(writes, []);
withDocumentVisibility("hidden", () => {
writeSessionData(ctx as never, term, currentPayload);
});
});
while (pendingCallbacks.length > 0) {
pendingCallbacks.shift()?.();
}
await Promise.race([
ackDone,
new Promise<never>((_, reject) => {
const timer = setTimeout(() => reject(new Error("timed out waiting for background ACK")), 3_000);
if (typeof timer === "object" && "unref" in timer) timer.unref();
}),
]);
assert.equal(writes.join(""), `${pendingPayload}${currentPayload}`);
assert.deepEqual(
writes.map((write) => write.length),
[
pendingPayload.length,
currentPayload.length,
],
);
assert.equal(pendingCallbacks.length, 0);
assert.equal(getFlowController(ctx as never, term).pendingBytes(), 0);
assert.equal(getDeferredTerminalWriteAckBytes(term), 0);
assert.equal(
acked.reduce((total, bytes) => total + bytes, 0),
pendingPayload.length + currentPayload.length,
);
clearTerminalSessionFlowAck("session-1");
});
test("hidden tab output is written completely while the tab remains hidden", async () => {
clearTerminalSessionFlowAck("session-1");
const lines: string[] = [];
let payloadLength = 0;
while (payloadLength <= MAX_TERMINAL_PLAIN_WRITE_CHUNK_BYTES) {
const lineNumber = lines.length + 1;
const line = `${String(lineNumber).padStart(5)} echo history-${lineNumber}\r\n`;
lines.push(line);
payloadLength += line.length;
}
const payload = lines.join("");
assert.ok(payload.length > MAX_TERMINAL_PLAIN_WRITE_CHUNK_BYTES);
const writes: string[] = [];
const term = {
buffer: { active: { type: "normal" } },
_core: { _writeBuffer: { flushSync() {} } },
write(data: string, callback?: () => void) {
writes.push(data);
callback?.();
},
scrollToBottom() {},
} as unknown as XTerm;
const acked: number[] = [];
const ctx = {
...createContext(false),
isVisibleRef: { current: false },
sessionRef: { current: "session-1" },
terminalBackend: {
ackSessionFlow: (_sessionId: string, bytes: number) => {
acked.push(bytes);
},
},
};
withAnimationFrameQueue(() => {
writeSessionData(ctx as never, term, payload);
});
assert.deepEqual(writes, []);
await new Promise((resolve) => { setTimeout(resolve, 190); });
flushTerminalSessionFlowAck("session-1");
assert.equal(writes.join(""), payload);
assert.equal(getFlowController(ctx as never, term).pendingBytes(), 0);
assert.equal(acked.reduce((total, bytes) => total + bytes, 0), payload.length);
clearTerminalSessionFlowAck("session-1");
});
test("frequent hidden log lines are drained in one complete terminal write", async () => {
const { term, writes, markerLines } = createFakeTerm();
const ctx = {
...createContext(false),
// The renderer stays active when hidden-tab hibernation is disabled, but
// the pane itself is not visible. This is the normal multi-tab runtime.
isVisibleRef: { current: true },
isPaneVisibleRef: { current: false },
};
const chunks = Array.from(
{ length: 8 },
(_, index) => `line-${String(index + 1).padStart(2, "0")}\r\n`,
);
for (const chunk of chunks) {
writeSessionData(ctx as never, term, chunk);
}
assert.deepEqual(writes, []);
await new Promise((resolve) => { setTimeout(resolve, 190); });
assert.deepEqual(writes, [chunks.join("")]);
// Host timestamps off still records the per-second ledger; sparse anchors
// only (≤1/sec), never per-line markers for 8 short lines.
assert.ok(markerLines.length <= 1);
assert.equal(getFlowController(ctx as never, term).pendingBytes(), 0);
resetTerminalWriteCoalescer(term);
});
test("large hidden bursts yield between terminal write slices", () => {
type FakeTimer = {
active: boolean;
callback: () => void;
delay: number;
unref: () => void;
};
const originalSetTimeout = globalThis.setTimeout;
const originalClearTimeout = globalThis.clearTimeout;
const originalDateNow = Date.now;
const timers: FakeTimer[] = [];
globalThis.setTimeout = ((callback: (...args: unknown[]) => void, delay = 0, ...args: unknown[]) => {
const timer: FakeTimer = {
active: true,
callback: () => callback(...args),
delay: Number(delay),
unref() {},
};
timers.push(timer);
return timer as unknown as ReturnType<typeof setTimeout>;
}) as typeof setTimeout;
globalThis.clearTimeout = ((timer: FakeTimer | undefined) => {
if (timer) timer.active = false;
}) as unknown as typeof clearTimeout;
const { term, writes } = createFakeTerm();
const ctx = {
...createContext(false),
isVisibleRef: { current: true },
isPaneVisibleRef: { current: false },
};
const chunks = Array.from({ length: 96 }, () => "x".repeat(4096));
const nextSecondChunk = "y";
const payload = `${chunks.join("")}${nextSecondChunk}`;
let fakeNow = new Date(2026, 0, 1, 12, 0, 59, 800).getTime();
Date.now = () => fakeNow;
try {
for (const chunk of chunks) {
writeSessionData(ctx as never, term, chunk);
}
fakeNow += 1_000;
writeSessionData(ctx as never, term, nextSecondChunk);
assert.deepEqual(writes, []);
const hiddenDrain = timers.find((timer) => timer.active && timer.delay === 160);
assert.ok(hiddenDrain);
hiddenDrain.active = false;
hiddenDrain.callback();
assert.deepEqual(writes, []);
const firstQueueDrain = timers.find((timer) => timer.active && timer.delay === 0);
assert.ok(firstQueueDrain);
firstQueueDrain.active = false;
firstQueueDrain.callback();
assert.equal(writes.length, 1);
assert.ok(writes[0]!.length <= MAX_TERMINAL_PLAIN_WRITE_CHUNK_BYTES);
assert.ok(writes.join("").length < payload.length);
assert.ok(timers.some((timer) => timer.active && timer.delay === 0));
flushPendingTerminalWritesOnResume(term);
assert.equal(writes.join(""), payload);
const followupDrain = timers.find((timer) => timer.active && timer.delay === 160);
if (followupDrain) {
followupDrain.active = false;
followupDrain.callback();
}
} finally {
Date.now = originalDateNow;
globalThis.setTimeout = originalSetTimeout;
globalThis.clearTimeout = originalClearTimeout;
resetTerminalWriteCoalescer(term);
}
});
test("hidden output keeps its arrival second when a batch crosses a clock boundary", () => {
const { term, writes } = createFakeTerm();
const ctx = {
...createContext(true),
isVisibleRef: { current: true },
isPaneVisibleRef: { current: false },
};
const originalDateNow = Date.now;
let fakeNow = new Date(2026, 0, 1, 12, 0, 59, 800).getTime();
Date.now = () => fakeNow;
try {
writeSessionData(ctx as never, term, "first\r\n");
fakeNow = new Date(2026, 0, 1, 12, 1, 0, 20).getTime();
writeSessionData(ctx as never, term, "second\r\n");
flushPendingTerminalWritesOnResume(term);
assert.deepEqual(writes, ["first\r\n", "second\r\n"]);
// Paint only through real content lines (not the empty cursor row after \n).
assert.deepEqual(getVisibleTerminalLineTimestampRows(term), [
{ row: 0, label: "12:00:59" },
{ row: 1, label: "12:01:00" },
]);
} finally {
Date.now = originalDateNow;
resetTerminalWriteCoalescer(term);
}
});
test("visible pressure-batched output keeps its arrival second across a clock boundary", () => {
const { term, writes } = createFakeTerm();
const ctx = {
...createContext(true),
isVisibleRef: { current: true },
isPaneVisibleRef: { current: true },
};
const originalDateNow = Date.now;
let fakeNow = new Date(2026, 0, 1, 12, 0, 59, 800).getTime();
Date.now = () => fakeNow;
try {
withAnimationFrameQueue((schedule) => {
noteTerminalOutputPressureData(term, "x".repeat(20_000));
writeSessionData(ctx as never, term, "first\r\n");
assert.ok(schedule.scheduledCount() >= 1);
assert.deepEqual(writes, []);
fakeNow = new Date(2026, 0, 1, 12, 1, 0, 20).getTime();
writeSessionData(ctx as never, term, "second\r\n");
schedule.flushScheduled();
assert.deepEqual(writes, ["first\r\n", "second\r\n"]);
assert.deepEqual(getVisibleTerminalLineTimestampRows(term), [
{ row: 0, label: "12:00:59" },
{ row: 1, label: "12:01:00" },
]);
});
} finally {
Date.now = originalDateNow;
resetTerminalWriteCoalescer(term);
}
});
test("visible alternate-screen output stays frame-batched across a clock boundary", () => {
const { term, writes } = createFakeTerm("alternate");
const ctx = {
...createContext(false),
isVisibleRef: { current: true },
isPaneVisibleRef: { current: true },
};
const originalDateNow = Date.now;
let fakeNow = new Date(2026, 0, 1, 12, 0, 59, 800).getTime();
Date.now = () => fakeNow;
try {
withAnimationFrameQueue((schedule) => {
writeSessionData(ctx as never, term, "frame-a");
assert.ok(schedule.scheduledCount() >= 1);
assert.deepEqual(writes, []);
fakeNow = new Date(2026, 0, 1, 12, 1, 0, 20).getTime();
writeSessionData(ctx as never, term, "frame-b");
assert.deepEqual(writes, []);
schedule.flushScheduled();
assert.equal(writes.join(""), "frame-aframe-b");
});
} finally {
Date.now = originalDateNow;
resetTerminalWriteCoalescer(term);
}
});
test("normal output before an alternate-screen frame keeps its earlier timestamp", () => {
const { term, writes } = createFakeTerm();
const ctx = {
...createContext(true),
isVisibleRef: { current: true },
isPaneVisibleRef: { current: true },
};
const originalDateNow = Date.now;
let fakeNow = new Date(2026, 0, 1, 12, 0, 59, 800).getTime();
Date.now = () => fakeNow;
try {
withAnimationFrameQueue((schedule) => {
writeSessionData(ctx as never, term, "before\r\n\x1b[?1049hframe-a");
assert.deepEqual(writes, ["before\r\n"]);
fakeNow = new Date(2026, 0, 1, 12, 1, 0, 20).getTime();
writeSessionData(ctx as never, term, "frame-b\x1b[?1049lafter");
schedule.flushScheduled();
assert.equal(
writes.join(""),
"before\r\n\x1b[?1049hframe-aframe-b\x1b[?1049lafter",
);
// before @ :59; after leave alt "after" @ :00 (same buffer line count).
assert.deepEqual(getVisibleTerminalLineTimestampRows(term), [
{ row: 0, label: "12:00:59" },
{ row: 1, label: "12:01:00" },
]);
});
} finally {
Date.now = originalDateNow;
resetTerminalWriteCoalescer(term);
}
});
test("normal output after an alternate-screen frame keeps its earlier timestamp", () => {
const { term, writes } = createFakeTerm("alternate");
const activeBuffer = term.buffer.active as { type: string };
const originalWrite = term.write.bind(term);
term.write = (data: string | Uint8Array, callback?: () => void) => {
const text = typeof data === "string" ? data : new TextDecoder().decode(data);
if (text.includes("\x1b[?1049l")) {
activeBuffer.type = "normal";
}
originalWrite(data, callback);
};
const ctx = {
...createContext(true),
isVisibleRef: { current: true },
isPaneVisibleRef: { current: false },
};
const originalDateNow = Date.now;
let fakeNow = new Date(2026, 0, 1, 12, 0, 59, 800).getTime();
Date.now = () => fakeNow;
try {
writeSessionData(ctx as never, term, "\x1b[?1049lafter\r\n");
fakeNow = new Date(2026, 0, 1, 12, 1, 0, 20).getTime();
writeSessionData(ctx as never, term, "next\r\n");
flushPendingTerminalWritesOnResume(term);
assert.equal(writes.join(""), "\x1b[?1049lafter\r\nnext\r\n");
assert.deepEqual(getVisibleTerminalLineTimestampRows(term), [
{ row: 0, label: "12:00:59" },
{ row: 1, label: "12:01:00" },
]);
} finally {
Date.now = originalDateNow;
resetTerminalWriteCoalescer(term);
}
});
test("hiding a pane preserves the arrival second of already queued output", () => {
const { term, writes } = createFakeTerm();
const ctx = {
...createContext(true),
isVisibleRef: { current: true },
isPaneVisibleRef: { current: true },
};
const originalDateNow = Date.now;
let fakeNow = new Date(2026, 0, 1, 12, 0, 59, 800).getTime();
Date.now = () => fakeNow;
try {
withAnimationFrameQueue(() => {
writeSessionData(ctx as never, term, "first\r\n");
assert.deepEqual(writes, []);
ctx.isPaneVisibleRef.current = false;
fakeNow = new Date(2026, 0, 1, 12, 1, 0, 20).getTime();
writeSessionData(ctx as never, term, "second\r\n");
flushPendingTerminalWritesOnResume(term);
assert.deepEqual(writes, ["first\r\n", "second\r\n"]);
assert.deepEqual(getVisibleTerminalLineTimestampRows(term), [
{ row: 0, label: "12:00:59" },
{ row: 1, label: "12:01:00" },
]);
});
} finally {
Date.now = originalDateNow;
resetTerminalWriteCoalescer(term);
}
});
test("hiding the page preserves the arrival second of already queued output", () => {
const { term, writes } = createFakeTerm();
const ctx = {
...createContext(true),
isVisibleRef: { current: true },
isPaneVisibleRef: { current: true },
};
const originalDateNow = Date.now;
let fakeNow = new Date(2026, 0, 1, 12, 0, 59, 800).getTime();
Date.now = () => fakeNow;
try {
withAnimationFrameQueue(() => {
withDocumentVisibility("visible", () => {
writeSessionData(ctx as never, term, "first\r\n");
});
assert.deepEqual(writes, []);
fakeNow = new Date(2026, 0, 1, 12, 1, 0, 20).getTime();
withDocumentVisibility("hidden", () => {
writeSessionData(ctx as never, term, "second\r\n");
});
flushPendingTerminalWritesOnResume(term);
assert.deepEqual(writes, ["first\r\n", "second\r\n"]);
assert.deepEqual(getVisibleTerminalLineTimestampRows(term), [
{ row: 0, label: "12:00:59" },
{ row: 1, label: "12:01:00" },
]);
});
} finally {
Date.now = originalDateNow;
resetTerminalWriteCoalescer(term);
}
});
test("showing a pane preserves the background arrival second still queued with visible output", () => {
const { term, writes } = createFakeTerm();
const ctx = {
...createContext(true),
isVisibleRef: { current: true },
isPaneVisibleRef: { current: false },
};
const originalDateNow = Date.now;
let fakeNow = new Date(2026, 0, 1, 12, 0, 59, 800).getTime();
Date.now = () => fakeNow;
try {
withAnimationFrameQueue(() => {
writeSessionData(ctx as never, term, "hidden\r\n");
ctx.isPaneVisibleRef.current = true;
writeSessionData(ctx as never, term, "visible-same-second\r\n");
fakeNow = new Date(2026, 0, 1, 12, 1, 0, 20).getTime();
writeSessionData(ctx as never, term, "visible-next-second\r\n");
flushPendingTerminalWritesOnResume(term);
assert.deepEqual(writes, [
"hidden\r\nvisible-same-second\r\n",
"visible-next-second\r\n",
]);
// Per-second ledger + fill-forward through content only (no empty cursor row).
assert.deepEqual(getVisibleTerminalLineTimestampRows(term), [
{ row: 0, label: "12:00:59" },
{ row: 1, label: "12:00:59" },
{ row: 2, label: "12:01:00" },
]);
});
} finally {
Date.now = originalDateNow;
resetTerminalWriteCoalescer(term);
}
});
test("hidden prompt formatting preserves PTY chunk boundaries", async () => {
const require = createRequire(import.meta.url);
const { Terminal } = require("@xterm/xterm") as {
Terminal: new (options: Record<string, unknown>) => XTerm;
};
const term = new Terminal({ cols: 80, rows: 5, scrollback: 20, allowProposedApi: true });
const originalWrite = term.write.bind(term);
let writeCalls = 0;
term.write = ((data: string | Uint8Array, callback?: () => void) => {
writeCalls += 1;
return originalWrite(data, callback);
}) as XTerm["write"];
const promptState = createPromptLineBreakState();
promptState.lastPromptText = "$ ";
promptState.pendingCommand = true;
const settings = {
showLineTimestamps: false,
scrollOnOutput: false,
forcePromptNewLine: true,
};
const ctx = {
...createContext(false),
terminalSettingsRef: { current: settings },
terminalSettings: settings,
promptLineBreakStateRef: { current: promptState },
isVisibleRef: { current: true },
isPaneVisibleRef: { current: false },
};
try {
withAnimationFrameQueue(() => {
writeSessionData(ctx as never, term, "foo");
writeSessionData(ctx as never, term, "$ ");
writeSessionData(ctx as never, term, "notice\r\n");
writeSessionData(ctx as never, term, "bar");
writeSessionData(ctx as never, term, "$ ");
flushPendingTerminalWritesOnResume(term);
});
await flushPendingTerminalWritesBeforeHibernate(term);
assert.equal(term.buffer.active.getLine(0)?.translateToString(true), "foo");
assert.equal(term.buffer.active.getLine(1)?.translateToString(true), "$ notice");
assert.equal(term.buffer.active.getLine(2)?.translateToString(true), "bar");
assert.equal(term.buffer.active.getLine(3)?.translateToString(true), "$");
assert.equal(term.buffer.active.cursorX, 2);
assert.equal(writeCalls, 1);
} finally {
resetTerminalWriteCoalescer(term);
term.dispose();
}
});
test("hidden prompt formatting respects cursor movement before a bare line feed", async () => {
const require = createRequire(import.meta.url);
const { Terminal } = require("@xterm/xterm") as {
Terminal: new (options: Record<string, unknown>) => XTerm;
};
const scenarios: Array<{
convertEol: boolean;
output: string;
promptRow: number;
promptLine?: string;
setup?: string;
}> = [
{ convertEol: false, output: "foo\n", promptRow: 2 },
{ convertEol: true, output: "foo\n", promptRow: 1 },
{ convertEol: false, output: "\x1b[10C\n", promptRow: 2 },
{ convertEol: false, output: "foo\x1b[1G\n", promptRow: 1 },
{ convertEol: false, output: "你好\r\n", promptRow: 1 },
{ convertEol: false, output: "你好\n", promptRow: 2 },
{ convertEol: false, output: "foo\x1b[20h\n", promptRow: 1 },
{ convertEol: true, output: "foo\x1b[20l\n", promptRow: 2 },
{ convertEol: false, output: "foo\x1b[E", promptRow: 1 },
{ convertEol: false, output: "foo\x1b[L\n", promptRow: 1, promptLine: "$ o" },
{ convertEol: false, output: "foo\x1b[M\n", promptRow: 1 },
{
convertEol: false,
setup: "\x1b[3g\x1b[6G\x1bH\x1b[1G",
output: "\x1b[I\n",
promptRow: 2,
},
];
for (const scenario of scenarios) {
const term = new Terminal({
cols: 80,
rows: 5,
scrollback: 20,
allowProposedApi: true,
convertEol: scenario.convertEol,
});
const promptState = createPromptLineBreakState();
promptState.lastPromptText = "$ ";
promptState.pendingCommand = true;
const settings = {
showLineTimestamps: false,
scrollOnOutput: false,
forcePromptNewLine: true,
};
const ctx = {
...createContext(false),
terminalSettingsRef: { current: settings },
terminalSettings: settings,
promptLineBreakStateRef: { current: promptState },
isVisibleRef: { current: true },
isPaneVisibleRef: { current: false },
};
try {
if (scenario.setup) {
term.write(scenario.setup);
await flushPendingTerminalWritesBeforeHibernate(term);
}
withAnimationFrameQueue(() => {
writeSessionData(ctx as never, term, scenario.output);
writeSessionData(ctx as never, term, "$ ");
flushPendingTerminalWritesOnResume(term);
});
await flushPendingTerminalWritesBeforeHibernate(term);
assert.equal(
term.buffer.active.getLine(scenario.promptRow)?.translateToString(true),
scenario.promptLine ?? "$",
JSON.stringify(scenario),
);
assert.equal(term.buffer.active.cursorX, 2);
} finally {
resetTerminalWriteCoalescer(term);
term.dispose();
}
}
});
test("writeSessionData keeps the current perf trace when hidden output is flushed", () => {
const payload = "hidden current output\n";
const writes: string[] = [];
const logs: string[] = [];
const originalInfo = console.info;
const term = {
buffer: { active: { type: "normal" } },
write(data: string, callback?: () => void) {
writes.push(data);
callback?.();
},
scrollToBottom() {},
} as unknown as XTerm;
const ctx = {
...createContext(false),
isVisibleRef: { current: true },
sessionRef: { current: "session-1" },
terminalBackend: {},
};
console.info = (message?: unknown) => {
logs.push(String(message));
};
try {
withAnimationFrameQueue(() => {
withDocumentVisibility("hidden", () => {
writeSessionData(ctx as never, term, payload, payload.length, {
terminalPerf: {
id: "hidden-current",
emittedAt: Date.now(),
chars: payload.length,
lineFeeds: 1,
},
});
});
// Hidden/background path force-flushes the coalescer (writes land now).
assert.deepEqual(writes, [payload]);
});
} finally {
console.info = originalInfo;
}
assert.deepEqual(writes, [payload]);
assert.equal(logs.some((log) => log.includes('"event":"renderer-receive"') && log.includes('"id":"hidden-current"')), true);
assert.equal(logs.some((log) => log.includes('"event":"renderer-write-done"') && log.includes('"id":"hidden-current"')), true);
});
test("writeSessionData drains output after the pane hides before the scheduled frame", async () => {
clearTerminalSessionFlowAck("session-1");
const payload = "x".repeat(XTERM_WRITE_CALLBACK_BATCH_BYTES);
const writes: string[] = [];
const term = {
buffer: { active: { type: "normal" } },
write(data: string, callback?: () => void) {
writes.push(data);
callback?.();
},
scrollToBottom() {},
} as unknown as XTerm;
const ctx = {
...createContext(false),
isVisibleRef: { current: true },
sessionRef: { current: "session-1" },
terminalBackend: {
ackSessionFlow() {},
},
};
let queuedBeforeHide = 0;
withAnimationFrameQueue((schedule) => {
withDocumentVisibility("visible", () => {
writeSessionData(ctx as never, term, payload);
});
queuedBeforeHide = schedule.scheduledCount();
ctx.isVisibleRef.current = false;
// Hidden-pane drain path should take over before the scheduled tick fires.
schedule.flushScheduled();
});
await new Promise((resolve) => { setTimeout(resolve, 90); });
assert.ok(queuedBeforeHide >= 1);
assert.equal(writes.join(""), payload);
assert.equal(getFlowController(ctx as never, term).pendingBytes(), 0);
clearTerminalSessionFlowAck("session-1");
});
test("writeSessionData drains hidden pane output without waiting for reveal", async () => {
clearTerminalSessionFlowAck("session-1");
const payload = "x".repeat(XTERM_WRITE_CALLBACK_FAST_PATH_MAX_BYTES + 1);
const writes: string[] = [];
const term = {
buffer: { active: { type: "normal" } },
write(data: string, callback?: () => void) {
writes.push(data);
callback?.();
},
scrollToBottom() {},
} as unknown as XTerm;
const acked: number[] = [];
const ctx = {
...createContext(false),
isVisibleRef: { current: false },
sessionRef: { current: "session-1" },
terminalBackend: {
ackSessionFlow: (_sessionId: string, bytes: number) => {
acked.push(bytes);
},
},
};
writeSessionData(ctx as never, term, payload);
assert.deepEqual(writes, []);
await new Promise((resolve) => { setTimeout(resolve, 190); });
flushTerminalSessionFlowAck("session-1");
assert.equal(writes.join(""), payload);
assert.equal(getFlowController(ctx as never, term).pendingBytes(), 0);
assert.equal(acked.reduce((total, bytes) => total + bytes, 0), payload.length);
clearTerminalSessionFlowAck("session-1");
});
test("writeSessionData keeps the hidden flush gate after coalescer reset and flushes on reveal", () => {
clearTerminalSessionFlowAck("session-1");
const payload = "x".repeat(XTERM_WRITE_CALLBACK_FAST_PATH_MAX_BYTES + 1);
const writes: string[] = [];
const term = {
buffer: { active: { type: "normal" } },
write(data: string, callback?: () => void) {
writes.push(data);
callback?.();
},
scrollToBottom() {},
} as unknown as XTerm;
const acked: number[] = [];
const ctx = {
...createContext(false),
isVisibleRef: { current: true },
sessionRef: { current: "session-1" },
terminalBackend: {
ackSessionFlow: (_sessionId: string, bytes: number) => {
acked.push(bytes);
},
},
};
getFlowController(ctx as never, term);
resetTerminalWriteCoalescer(term);
withAnimationFrameQueue((schedule) => {
withDocumentVisibility("visible", () => {
writeSessionData(ctx as never, term, payload);
});
assert.ok(schedule.scheduledCount() >= 1);
ctx.isVisibleRef.current = false;
// Gate prevents flush while hidden; cancel/run scheduled tick without writing.
schedule.microtasks.length = 0;
schedule.frames.length = 0;
assert.deepEqual(writes, []);
ctx.isVisibleRef.current = true;
flushPendingTerminalWritesOnResume(term);
});
flushTerminalSessionFlowAck("session-1");
assert.deepEqual(writes, [payload]);
assert.equal(getFlowController(ctx as never, term).pendingBytes(), 0);
assert.equal(getDeferredTerminalWriteAckBytes(term), 0);
assert.equal(acked.reduce((total, bytes) => total + bytes, 0), payload.length);
clearTerminalSessionFlowAck("session-1");
});
test("writeSessionData refreshes pressure visibility when hidden output flushes after reveal", () => {
clearTerminalSessionFlowAck("session-1");
const payload = "x".repeat(XTERM_WRITE_CALLBACK_FAST_PATH_MAX_BYTES + 1);
const writes: string[] = [];
const term = {
buffer: { active: { type: "normal" } },
write(data: string, callback?: () => void) {
writes.push(data);
callback?.();
},
scrollToBottom() {},
} as unknown as XTerm;
const ctx = {
...createContext(false),
isVisibleRef: { current: false },
sessionRef: { current: "session-1" },
terminalBackend: {
ackSessionFlow: () => {},
},
};
getFlowController(ctx as never, term);
resetTerminalWriteCoalescer(term);
withAnimationFrameQueue(() => {
withDocumentVisibility("visible", () => {
writeSessionData(ctx as never, term, payload);
});
// The ingress snapshot records the pane as hidden.
assert.equal(getTerminalOutputPressure(term).background, true);
assert.deepEqual(writes, []);
ctx.isVisibleRef.current = true;
withDocumentVisibility("visible", () => {
flushPendingTerminalWritesOnResume(term);
});
// The batch is actually parsed after reveal, so the flush must clear the
// stale hidden flag: bulk write callbacks (e.g. keyword highlight) gate
// their in-frame viewport recolor on this state (#3272).
assert.deepEqual(writes, [payload]);
assert.equal(getTerminalOutputPressure(term).background, false);
});
clearTerminalSessionFlowAck("session-1");
});
test("hidden tab output marks pending scroll without scrolling immediately", async () => {
const writes: string[] = [];
let scrollCalls = 0;
const term = {
buffer: { active: { type: "normal" } },
_core: { _writeBuffer: { flushSync() {} } },
write(data: string, callback?: () => void) {
writes.push(data);
callback?.();
},
scrollToBottom() {
scrollCalls += 1;
},
} as unknown as XTerm;
const ctx = {
...createContext(false),
isVisibleRef: { current: false },
pendingOutputScrollRef: { current: false },
terminalSettingsRef: {
current: {
showLineTimestamps: false,
scrollOnOutput: true,
forcePromptNewLine: false,
},
},
terminalSettings: {
showLineTimestamps: false,
scrollOnOutput: true,
forcePromptNewLine: false,
},
};
writeSessionData(ctx as never, term, "fresh output");
assert.equal(writes.join(""), "");
assert.equal(ctx.pendingOutputScrollRef.current, false);
await new Promise((resolve) => { setTimeout(resolve, 190); });
assert.equal(writes.join(""), "fresh output");
assert.equal(ctx.pendingOutputScrollRef.current, true);
assert.equal(scrollCalls, 0);
});
test("visible output does not request another scroll when already at the bottom", () => {
let scrollCalls = 0;
const term = {
buffer: {
active: {
type: "normal",
baseY: 10_000,
viewportY: 10_000,
},
},
write(_data: string, callback?: () => void) {
callback?.();
},
scrollToBottom() {
scrollCalls += 1;
},
} as unknown as XTerm;
const ctx = {
...createContext(false),
isVisibleRef: { current: true },
terminalSettingsRef: {
current: {
showLineTimestamps: false,
scrollOnOutput: true,
forcePromptNewLine: false,
},
},
};
writeSessionData(ctx as never, term, "fresh output");
assert.equal(scrollCalls, 0);
});
test("visible output scrolls when the user is viewing earlier output", () => {
let scrollCalls = 0;
const term = {
buffer: {
active: {
type: "normal",
baseY: 10_000,
viewportY: 9_900,
},
},
write(_data: string, callback?: () => void) {
callback?.();
},
scrollToBottom() {
scrollCalls += 1;
},
} as unknown as XTerm;
const ctx = {
...createContext(false),
isVisibleRef: { current: true },
terminalSettingsRef: {
current: {
showLineTimestamps: false,
scrollOnOutput: true,
forcePromptNewLine: false,
},
},
};
writeSessionData(ctx as never, term, "fresh output");
assert.equal(scrollCalls, 1);
});
test("visible output does not scroll when output auto-scroll is disabled", () => {
let scrollCalls = 0;
const term = {
buffer: {
active: {
type: "normal",
baseY: 10_000,
viewportY: 9_900,
},
},
write(_data: string, callback?: () => void) {
callback?.();
},
scrollToBottom() {
scrollCalls += 1;
},
} as unknown as XTerm;
const ctx = {
...createContext(false),
isVisibleRef: { current: true },
};
writeSessionData(ctx as never, term, "fresh output");
assert.equal(scrollCalls, 0);
});
test("writeSessionData flushes deferred IPC acks before small output can leave the source paused", async () => {
clearTerminalSessionFlowAck("session-1");
const term = {
buffer: { active: { type: "normal" } },
write(_data: string, callback?: () => void) {
callback?.();
},
scrollToBottom() {},
} as unknown as XTerm;
let mainUnackedBytes = 0;
let mainPaused = false;
const ctx = {
...createContext(false),
sessionRef: { current: "session-1" },
terminalBackend: {
ackSessionFlow: (_sessionId: string, bytes: number) => {
mainUnackedBytes = Math.max(0, mainUnackedBytes - bytes);
if (mainPaused && mainUnackedBytes <= FLOW_LOW_WATER_MARK) {
mainPaused = false;
}
},
},
};
// Deferred acks flush every time they reach XTERM_WRITE_CALLBACK_BATCH_BYTES,
// which sits far below FLOW_HIGH_WATER_MARK (issue #1961 raised the watermark
// to 1MB), so deferral alone can never push the main process into a pause.
assert.ok(XTERM_WRITE_CALLBACK_BATCH_BYTES < FLOW_HIGH_WATER_MARK);
const chunk = "x".repeat(512);
const chunksPerThresholdFlush = Math.ceil(XTERM_WRITE_CALLBACK_BATCH_BYTES / chunk.length);
const residueChunks = 7;
const writeCount = chunksPerThresholdFlush * 2 + residueChunks;
const expectedDeferredBytes = residueChunks * chunk.length;
assert.ok(expectedDeferredBytes > 0);
assert.ok(expectedDeferredBytes < XTERM_WRITE_CALLBACK_BATCH_BYTES);
for (let index = 0; index < writeCount; index += 1) {
mainUnackedBytes += chunk.length;
if (mainUnackedBytes >= FLOW_HIGH_WATER_MARK) {
mainPaused = true;
}
writeSessionData(ctx as never, term, chunk);
}
flushTerminalWriteCoalescer(term);
assert.equal(mainPaused, false);
assert.equal(mainUnackedBytes, expectedDeferredBytes);
assert.equal(getDeferredTerminalWriteAckBytes(term), expectedDeferredBytes);
await new Promise((resolve) => { setTimeout(resolve, 25); });
assert.equal(mainPaused, false);
assert.equal(mainUnackedBytes, 0);
assert.equal(getDeferredTerminalWriteAckBytes(term), 0);
clearTerminalSessionFlowAck("session-1");
});
test("writeSessionData acks ingress bytes to match main-process trackEmitted", () => {
clearTerminalSessionFlowAck("session-1");
const { term } = createFakeTerm();
const acked: number[] = [];
const ctx = {
...createContext(false),
sessionRef: { current: "session-1" },
terminalBackend: {
ackSessionFlow: (_sessionId: string, bytes: number) => {
acked.push(bytes);
},
},
};
writeSessionData(ctx as never, term, "hello");
flushTerminalWriteCoalescer(term);
const deferred = clearDeferredTerminalWriteAck(term);
if (deferred > 0) {
ctx.terminalBackend.ackSessionFlow!("session-1", deferred);
}
flushTerminalSessionFlowAck("session-1");
assert.deepEqual(acked, [5]);
clearTerminalSessionFlowAck("session-1");
});
test("writeSessionData acks original ingress bytes when display data is expanded", () => {
clearTerminalSessionFlowAck("session-1");
const term = {
buffer: { active: { type: "normal" } },
write(_data: string, callback?: () => void) {
callback?.();
},
scrollToBottom() {},
} as unknown as XTerm;
const acked: number[] = [];
const ctx = {
...createContext(false),
sessionRef: { current: "session-1" },
terminalBackend: {
ackSessionFlow: (_sessionId: string, bytes: number) => {
acked.push(bytes);
},
},
};
writeSessionData(ctx as never, term, "a\nb", 2);
flushTerminalWriteCoalescer(term);
const deferred = clearDeferredTerminalWriteAck(term);
if (deferred > 0) {
ctx.terminalBackend.ackSessionFlow!("session-1", deferred);
}
flushTerminalSessionFlowAck("session-1");
assert.deepEqual(acked, [2]);
clearTerminalSessionFlowAck("session-1");
});
test("writeSessionData batches IPC acks using the VS Code ack size", () => {
clearTerminalSessionFlowAck("session-1");
const term = {
buffer: { active: { type: "normal" } },
write(_data: string, callback?: () => void) {
callback?.();
},
scrollToBottom() {},
} as unknown as XTerm;
const acked: number[] = [];
const ctx = {
...createContext(false),
sessionRef: { current: "session-1" },
terminalBackend: {
ackSessionFlow: (_sessionId: string, bytes: number) => {
acked.push(bytes);
},
},
};
writeSessionData(ctx as never, term, `${"x".repeat(FLOW_CHAR_COUNT_ACK_SIZE)}\n`);
flushTerminalWriteCoalescer(term);
flushTerminalSessionFlowAck("session-1");
assert.deepEqual(acked, [FLOW_CHAR_COUNT_ACK_SIZE, 1]);
clearTerminalSessionFlowAck("session-1");
});
test("writeSessionData records terminal output timestamps without changing output bytes", () => {
const { term, writes, markerLines } = createFakeTerm();
writeSessionData(createContext(false, { showLineTimestamps: true }) as never, term, "hello\r\nnext");
assert.equal(writes.join(""), "hello\r\nnext");
assert.equal((writes.join("").match(/\[\d{2}:\d{2}:\d{2}\]/g) ?? []).length, 0);
// Per-second sparse reflow anchor (not per output line).
assert.equal(markerLines.length, 1);
});
test("writeSessionData uses sparse reflow anchors not per-line markers", () => {
const { term, writes, markerLines } = createFakeTerm();
writeSessionData(createContext(true, { showLineTimestamps: false }) as never, term, "hello\r\nnext");
writeSessionData(createContext(false, { showLineTimestamps: true }) as never, term, "more\r\n");
assert.ok(writes.join("").includes("hello"));
// At most one anchor per stamped wall-clock second across both writes.
assert.ok(markerLines.length <= 2);
assert.ok(markerLines.length >= 1);
});
test("writeSessionData records timestamps for hosts with timestamps enabled", () => {
const { term, writes, markerLines } = createFakeTerm();
writeSessionData(createContext(false, { showLineTimestamps: true }) as never, term, "hello");
assert.equal(writes.join(""), "hello");
assert.equal(markerLines.length, 1);
assert.ok(getVisibleTerminalLineTimestampRows(term).length >= 1);
});
test("writeSessionData skips timestamps on the alternate screen", () => {
const { term, writes, markerLines } = createFakeTerm("alternate");
writeSessionData(createContext(false, { showLineTimestamps: true }) as never, term, "vim screen");
assert.deepEqual(writes, ["vim screen"]);
assert.deepEqual(markerLines, []);
});
test("writeSessionData does not timestamp output that enters alternate screen in the same chunk", () => {
const { term, writes, markerLines } = createFakeTerm();
writeSessionData(createContext(false, { showLineTimestamps: true }) as never, term, "\x1b[?1049hvim screen");
assert.deepEqual(writes, ["\x1b[?1049hvim screen"]);
assert.deepEqual(markerLines, []);
});
test("writeSessionData resumes timestamps after leaving alternate screen in the same chunk", () => {
const { term, writes, markerLines } = createFakeTerm("alternate");
writeSessionData(createContext(false, { showLineTimestamps: true }) as never, term, "\x1b[?1049lprompt");
assert.equal(writes.join(""), "\x1b[?1049lprompt");
assert.equal(markerLines.length, 1);
assert.ok(getVisibleTerminalLineTimestampRows(term).length >= 1);
});
test("writeSessionData inserts erase-scrollback immediately after normal full clear", () => {
const { term, writes } = createFakeTerm();
writeSessionData(createContext(false) as never, term, "\x1b[H\x1b[2Jfresh output");
assert.equal(writes.join(""), "\x1b[H\x1b[2J\x1b[3Jfresh output");
});
test("writeSessionData preserves scrollback after normal full clear when disabled", () => {
const { term, writes } = createFakeTerm();
const ctx = createContext(false);
ctx.terminalSettingsRef.current.clearWipesScrollback = false;
ctx.terminalSettings.clearWipesScrollback = false;
writeSessionData(ctx as never, term, "\x1b[H\x1b[2Jfresh output");
assert.equal(writes.join(""), "\x1b[H\x1b[2Jfresh output");
});
test("writeSessionData does not duplicate existing erase-scrollback after full clear", () => {
const { term, writes } = createFakeTerm();
writeSessionData(createContext(false) as never, term, "\x1b[H\x1b[2J\x1b[3Jfresh output");
assert.equal(writes.join(""), "\x1b[H\x1b[2J\x1b[3Jfresh output");
});
test("writeSessionData does not add erase-scrollback inside synchronized output", () => {
const { term, writes } = createFakeTerm();
writeSessionData(createContext(false) as never, term, "\x1b[?2026h\x1b[H\x1b[2Jframe\x1b[?2026l");
assert.equal(writes.join(""), "\x1b[?2026h\x1b[H\x1b[2Jframe\x1b[?2026l");
});
test("writeSessionData does not wipe scrollback for delayed sync clears after returning to bottom", () => {
const writes: string[] = [];
const active = { type: "normal" as const, viewportY: 0, baseY: 5 };
const term = {
buffer: { active },
write(data: string, callback?: () => void) {
writes.push(data);
callback?.();
},
registerMarker() {
return { line: 0, isDisposed: false, dispose() {} };
},
scrollToBottom() {},
} as unknown as XTerm;
const ctx = createContext(false);
ctx.terminalSettingsRef.current.clearWipesScrollback = true;
ctx.terminalSettings.clearWipesScrollback = true;
// Prior chunk opens the sync block while the user is reading scrollback.
writeSessionData(ctx as never, term, "\x1b[?2026h\x1b[H");
// Delayed clear arrives after the user returns to the live bottom.
active.viewportY = 5;
active.baseY = 5;
writeSessionData(ctx as never, term, "\x1b[2Jframe\x1b[?2026l");
const output = writes.join("");
assert.equal(output.includes("\x1b[3J"), false, output);
assert.equal(output.includes("\x1b[H\x1b[2Jframe"), true, output);
});
test("writeSessionData always uses ledger recording regardless of gutter toggle", () => {
const { term, writes, markerLines } = createFakeTerm();
const ctx = createContext(false, { showLineTimestamps: false });
writeSessionData(ctx as never, term, "before\r\n");
// Per-second sparse reflow anchors (not per-line markers).
assert.equal(markerLines.length, 1);
ctx.host = { showLineTimestamps: true };
ctx.hostRef.current = ctx.host;
writeSessionData(ctx as never, term, "enabled\r\n");
// Same wall-clock second in real runs may still be one stamp; here two writes
// share one second unless Date.now advances — allow ≤2 sparse anchors total.
assert.ok(markerLines.length <= 2, `expected sparse anchors, got ${markerLines.length}`);
ctx.host = { showLineTimestamps: false };
ctx.hostRef.current = ctx.host;
writeSessionData(ctx as never, term, "disabled");
assert.equal(writes.join(""), "before\r\nenabled\r\ndisabled");
// Ledger still has stamps for paint when gutter is shown.
assert.ok(getVisibleTerminalLineTimestampRows(term).length >= 1);
});
test("writeSessionData follows live hostRef toggles without replacing boot host snapshot", () => {
const { term, writes, markerLines } = createFakeTerm();
const bootHost = { showLineTimestamps: false as boolean };
const hostRef = { current: bootHost };
const ctx = {
...createContext(false),
host: bootHost,
hostRef,
};
writeSessionData(ctx as never, term, "boot-off\r\n");
assert.ok(markerLines.length <= 1);
hostRef.current = { showLineTimestamps: true };
assert.equal(bootHost.showLineTimestamps, false);
assert.equal(ctx.host.showLineTimestamps, false);
writeSessionData(ctx as never, term, "live-on\r\n");
assert.equal(writes.join(""), "boot-off\r\nlive-on\r\n");
// Sparse anchors only (≤1 per stamped second).
assert.ok(markerLines.length <= 2);
assert.ok(getVisibleTerminalLineTimestampRows(term).length >= 1);
hostRef.current = { showLineTimestamps: false };
writeSessionData(ctx as never, term, "live-off");
assert.equal(writes.join(""), "boot-off\r\nlive-on\r\nlive-off");
});
test("writeSessionData batches timestamp bookkeeping for bulk line output", () => {
const { term, writes, markerLines } = createFakeTerm();
const payload = `${Array.from({ length: 40 }, () => "x".repeat(80)).join("\n")}\n`;
writeSessionData(createContext(false, { showLineTimestamps: true }) as never, term, payload, payload.length);
flushTerminalWriteCoalescer(term);
for (let guard = 0; guard < 1000 && flushTerminalWriteQueueBypassingTimers(term); guard += 1) {
// Drain cooperative bulk-output timers so the assertion observes the full write plan.
}
assert.equal(writes.join(""), payload);
// One wall-clock second → one ledger stamp + one sparse reflow anchor.
assert.equal(markerLines.length, 1);
const painted = getVisibleTerminalLineTimestampRows(term);
assert.ok(painted.length >= 1);
assert.ok(painted.every((row) => row.label.length > 0));
assert.ok(writes.length >= 1);
});
test("writeSessionData skips timestamp markers under large-output pressure", () => {
const { term, writes, markerLines } = createFakeTerm();
const payload = `${Array.from({ length: 2000 }, () => "x".repeat(1023)).join("\n")}\n`;
writeSessionData(createContext(false, { showLineTimestamps: true }) as never, term, payload, payload.length);
flushTerminalWriteCoalescer(term);
for (let guard = 0; guard < 1000 && flushTerminalWriteQueueBypassingTimers(term); guard += 1) {
// Drain cooperative bulk-output timers.
}
assert.equal(writes.join(""), payload);
assert.equal(markerLines.length, 0);
});
test("attachSessionToTerminal resets timestamp state for a reused terminal", () => {
const { term, writes } = createFakeTerm();
const ctx = {
...createContext(false, { showLineTimestamps: true }),
sessionId: "session-1",
sessionRef: { current: null },
hasConnectedRef: { current: true },
hasRunStartupCommandRef: { current: false },
disposeDataRef: { current: null },
disposeExitRef: { current: null },
fitAddonRef: { current: null },
serializeAddonRef: { current: null },
pendingAuthRef: { current: null },
terminalBackend: {
onSessionData: () => () => {},
onSessionExit: () => () => {},
},
updateStatus: () => {},
setError: () => {},
onSessionExit: () => {},
};
writeSessionData(ctx as never, term, "unfinished");
attachSessionToTerminal(ctx as never, term, "session-2");
writeSessionData(ctx as never, term, "fresh");
assert.equal(writes.length, 2);
assert.equal(writes[1], "fresh");
});
test("attachSessionToTerminal keeps timestamps for scrollback preserved by reconnect", () => {
const { term } = createFakeTerm();
const ctx = {
...createContext(false, { showLineTimestamps: true }),
sessionId: "session-1",
sessionRef: { current: null },
hasConnectedRef: { current: true },
hasRunStartupCommandRef: { current: false },
disposeDataRef: { current: null },
disposeExitRef: { current: null },
fitAddonRef: { current: null },
serializeAddonRef: { current: null },
pendingAuthRef: { current: null },
terminalBackend: {
onSessionData: () => () => {},
onSessionExit: () => () => {},
},
updateStatus: () => {},
setError: () => {},
onSessionExit: () => {},
};
const originalDateNow = Date.now;
try {
Date.now = () => new Date(2026, 0, 1, 12, 0, 59, 800).getTime();
writeSessionData(ctx as never, term, "before reconnect\r\n");
// Reconnect reuses the same xterm (scrollback preserved by the caller).
attachSessionToTerminal(ctx as never, term, "session-2");
Date.now = () => new Date(2026, 0, 1, 12, 1, 30, 0).getTime();
writeSessionData(ctx as never, term, "after reconnect\r\n");
assert.deepEqual(getVisibleTerminalLineTimestampRows(term), [
{ row: 0, label: "12:00:59" },
{ row: 1, label: "12:01:30" },
]);
} finally {
Date.now = originalDateNow;
resetTerminalWriteCoalescer(term);
}
});
test("attachSessionToTerminal clears the backend id before reporting exit", () => {
const { term } = createFakeTerm();
let onExit: ((evt: { reason?: string }) => void) | null = null;
let sessionIdSeenByConsumer: string | null | undefined = "not-called";
const sessionRef = { current: null as string | null };
const ctx = {
...createContext(false),
sessionId: "session-1",
sessionRef,
hasConnectedRef: { current: true },
hasRunStartupCommandRef: { current: false },
disposeDataRef: { current: null },
disposeExitRef: { current: null },
fitAddonRef: { current: null },
serializeAddonRef: { current: null },
pendingAuthRef: { current: null },
terminalBackend: {
onSessionData: () => () => {},
onSessionExit: (_id: string, callback: (evt: { reason?: string }) => void) => {
onExit = callback;
return () => {};
},
},
updateStatus: () => {},
setError: () => {},
onSessionExit: () => {
sessionIdSeenByConsumer = sessionRef.current;
},
};
attachSessionToTerminal(ctx as never, term, "session-1");
assert.equal(sessionRef.current, "session-1");
onExit?.({ reason: "closed" });
assert.equal(sessionRef.current, null);
assert.equal(sessionIdSeenByConsumer, null);
});
test("attachSessionToTerminal drains hidden final output before exit capture", async () => {
const { term, writes } = createFakeTerm();
let onData: ((data: string) => void) | null = null;
let onExit: ((evt: { reason?: string }) => void) | null = null;
let captured = "";
const ctx = {
...createContext(false),
sessionId: "session-1",
sessionRef: { current: null as string | null },
isVisibleRef: { current: true },
isPaneVisibleRef: { current: false },
hasConnectedRef: { current: true },
hasRunStartupCommandRef: { current: false },
disposeDataRef: { current: null },
disposeExitRef: { current: null },
fitAddonRef: { current: null },
serializeAddonRef: { current: { serialize: () => writes.join("") } },
pendingAuthRef: { current: null },
terminalBackend: {
onSessionData: (_id: string, callback: (data: string) => void) => {
onData = callback;
return () => {};
},
onSessionExit: (_id: string, callback: (evt: { reason?: string }) => void) => {
onExit = callback;
return () => {};
},
},
updateStatus: () => {},
setError: () => {},
onTerminalDataCapture: (_sessionId: string, data: string) => {
captured = data;
},
onSessionExit: () => {},
};
attachSessionToTerminal(ctx as never, term, "session-1");
onData?.("final output\r\n");
assert.deepEqual(writes, []);
onExit?.({ reason: "closed" });
await new Promise((resolve) => setTimeout(resolve, 0));
const expected = "final output\r\n\r\n[session closed]\r\n";
assert.equal(writes.join(""), expected);
assert.equal(captured, expected);
resetTerminalWriteCoalescer(term);
});
test("attachSessionToTerminal keeps interrupt-time output visible", () => {
clearTerminalSessionFlowAck("session-1");
const { term, writes } = createFakeTerm();
const acked: number[] = [];
const output: string[] = [];
const logs: string[] = [];
let onData: ((data: string) => void) | null = null;
const ctx = {
...createContext(false),
sessionId: "session-1",
sessionRef: { current: null },
hasConnectedRef: { current: true },
hasRunStartupCommandRef: { current: false },
disposeDataRef: { current: null },
disposeExitRef: { current: null },
fitAddonRef: { current: null },
serializeAddonRef: { current: null },
pendingAuthRef: { current: null },
onTerminalOutput: (chunk: string) => output.push(chunk),
onTerminalLogData: (chunk: string) => logs.push(chunk),
terminalBackend: {
onSessionData: (_id: string, cb: (data: string) => void) => {
onData = cb;
return () => {};
},
onSessionExit: () => () => {},
ackSessionFlow: (_sessionId: string, bytes: number) => {
acked.push(bytes);
},
setSessionFlowPaused: () => {},
},
updateStatus: () => {},
setError: () => {},
onSessionExit: () => {},
};
attachSessionToTerminal(ctx as never, term, "session-1");
const flow = getFlowController(ctx as never, term);
flow.received(FLOW_LOW_WATER_MARK);
prioritizeTerminalInput(
term,
"session-1",
flow,
ctx.terminalBackend,
(callback: () => void) => callback(),
{
reason: "interrupt",
drainStaleOutput: false,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 1000,
},
);
onData?.("old output");
flushTerminalWriteCoalescer(term);
assert.equal(writes.join(""), "old output");
assert.equal(output.join(""), "old output");
assert.equal(logs.join(""), "old output");
assert.deepEqual(acked, []);
onData?.("^");
flushTerminalWriteCoalescer(term);
assert.equal(writes.join(""), "old output^");
assert.equal(output.join(""), "old output^");
assert.equal(logs.join(""), "old output^");
assert.deepEqual(acked, []);
onData?.("C\r\n$ ");
flushTerminalWriteCoalescer(term);
flushTerminalSessionFlowAck("session-1");
assert.equal(writes.join(""), "old output^C\r\n$ ");
assert.equal(output.join(""), "old output^C\r\n$ ");
assert.equal(logs.join(""), "old output^C\r\n$ ");
assert.deepEqual(acked, []);
assert.equal(getDeferredTerminalWriteAckBytes(term), 16);
clearDeferredTerminalWriteAck(term);
clearTerminalSessionFlowAck("session-1");
});
test("attachSessionToTerminal hints for sudo password prompts and fills on confirm", () => {
const { term, writes } = createFakeTerm();
const sent: Array<{ id: string; data: string; automated?: boolean }> = [];
const hints: boolean[] = [];
let onData: ((data: string) => void) | null = null;
const sudoAutofillRef = { current: null };
const ctx = {
...createContext(false),
sessionId: "session-1",
sessionRef: { current: null },
hasConnectedRef: { current: true },
hasRunStartupCommandRef: { current: false },
disposeDataRef: { current: null },
disposeExitRef: { current: null },
fitAddonRef: { current: null },
serializeAddonRef: { current: null },
pendingAuthRef: { current: null },
sudoAutofillRef,
onSudoHint: (active: boolean) => hints.push(active),
terminalBackend: {
onSessionData: (_id: string, cb: (data: string) => void) => {
onData = cb;
return () => {};
},
onSessionExit: () => () => {},
writeToSession: (id: string, data: string, options?: { automated?: boolean }) => {
sent.push({ id, data, automated: options?.automated });
},
},
updateStatus: () => {},
setError: () => {},
onSessionExit: () => {},
};
attachSessionToTerminal(ctx as never, term, "session-1", {
sudoAutofillPassword: "secret",
});
sudoAutofillRef.current?.armForCommand("sudo whoami");
onData?.("sudo whoami\r\n");
onData?.("[sudo] password for alice: ");
// Confirm-to-fill model: detecting the prompt raises a hint but never sends
// the password on its own.
assert.deepEqual(hints, [true]);
assert.deepEqual(sent, []);
assert.equal(writes[0], "sudo whoami\r\n");
assert.equal(writes[1], "[sudo] password for alice: ");
// The password is only written once the user confirms (presses Enter).
sudoAutofillRef.current?.confirmFill();
assert.deepEqual(sent, [{ id: "session-1", data: "secret\n", automated: true }]);
});
test("attachSessionToTerminal does not auto-fill unarmed sudo-looking output", () => {
const { term } = createFakeTerm();
const sent: string[] = [];
let onData: ((data: string) => void) | null = null;
const ctx = {
...createContext(false),
sessionId: "session-1",
sessionRef: { current: null },
hasConnectedRef: { current: true },
hasRunStartupCommandRef: { current: false },
disposeDataRef: { current: null },
disposeExitRef: { current: null },
fitAddonRef: { current: null },
serializeAddonRef: { current: null },
pendingAuthRef: { current: null },
sudoAutofillRef: { current: null },
terminalBackend: {
onSessionData: (_id: string, cb: (data: string) => void) => {
onData = cb;
return () => {};
},
onSessionExit: () => () => {},
writeToSession: (_id: string, data: string) => {
sent.push(data);
},
},
updateStatus: () => {},
setError: () => {},
onSessionExit: () => {},
};
attachSessionToTerminal(ctx as never, term, "session-1", {
sudoAutofillPassword: "secret",
});
onData?.("[sudo] password for alice: ");
assert.deepEqual(sent, []);
});
test("attachSessionToTerminal leaves sudo prompts alone without an autofill password", () => {
const { term } = createFakeTerm();
const sent: string[] = [];
let onData: ((data: string) => void) | null = null;
const ctx = {
...createContext(false),
sessionId: "session-1",
sessionRef: { current: null },
hasConnectedRef: { current: true },
hasRunStartupCommandRef: { current: false },
disposeDataRef: { current: null },
disposeExitRef: { current: null },
fitAddonRef: { current: null },
serializeAddonRef: { current: null },
pendingAuthRef: { current: null },
terminalBackend: {
onSessionData: (_id: string, cb: (data: string) => void) => {
onData = cb;
return () => {};
},
onSessionExit: () => () => {},
writeToSession: (_id: string, data: string) => {
sent.push(data);
},
},
updateStatus: () => {},
setError: () => {},
onSessionExit: () => {},
};
attachSessionToTerminal(ctx as never, term, "session-1");
onData?.("[sudo] password for alice: ");
assert.deepEqual(sent, []);
});
test("tryAttachSessionToTerminal closes orphan sessions after unmount", () => {
const { term } = createFakeTerm();
const closed: string[] = [];
let dataSubscribed = false;
const ctx = {
...createContext(false),
sessionId: "session-1",
sessionRef: { current: null },
hasConnectedRef: { current: false },
hasRunStartupCommandRef: { current: false },
disposeDataRef: { current: null },
disposeExitRef: { current: null },
fitAddonRef: { current: null },
serializeAddonRef: { current: null },
pendingAuthRef: { current: null },
isBootActiveRef: { current: false },
terminalBackend: {
closeSession: (id: string) => {
closed.push(id);
},
onSessionData: () => {
dataSubscribed = true;
return () => {};
},
onSessionExit: () => () => {},
},
updateStatus: () => {},
setError: () => {},
onSessionExit: () => {},
};
const attached = tryAttachSessionToTerminal(ctx as never, term, "backend-session");
assert.equal(attached, false);
assert.deepEqual(closed, ["backend-session"]);
assert.equal(dataSubscribed, false);
assert.equal(ctx.sessionRef.current, null);
});
test("attachSessionToTerminal marks connected on metadata-only or visible first output", () => {
const { term } = createFakeTerm();
const statuses: string[] = [];
const output: Array<{ data: string; sensitive: boolean }> = [];
let onData: ((data: string, meta?: {
moshHandshake?: boolean;
pluginPipelineIngressBytes?: number;
pluginPipelineSensitiveInput?: boolean;
}) => void) | null = null;
const ctx = {
...createContext(false),
sessionId: "session-1",
sessionRef: { current: null as string | null },
hasConnectedRef: { current: false },
hasRunStartupCommandRef: { current: false },
disposeDataRef: { current: null as (() => void) | null },
disposeExitRef: { current: null as (() => void) | null },
fitAddonRef: { current: null },
serializeAddonRef: { current: null },
pendingAuthRef: { current: null },
onTerminalOutput: (data: string, meta?: { pluginPipelineSensitiveInput?: boolean }) => {
output.push({ data, sensitive: meta?.pluginPipelineSensitiveInput === true });
},
terminalBackend: {
onSessionData: (
_id: string,
cb: (data: string, meta?: {
moshHandshake?: boolean;
pluginPipelineIngressBytes?: number;
pluginPipelineSensitiveInput?: boolean;
}) => void,
) => {
onData = cb;
return () => {};
},
onSessionExit: () => () => {},
writeToSession: () => {},
resizeSession: () => {},
setSessionFlowPaused: () => {},
ackSessionFlow: () => {},
},
updateStatus: (status: string) => {
statuses.push(status);
if (status === "connected") ctx.hasConnectedRef.current = true;
},
setError: () => {},
onSessionExit: () => {},
};
attachSessionToTerminal(ctx as never, term, "session-1");
// A plugin may suppress the first banner while still consuming host ingress.
onData?.("", { pluginPipelineIngressBytes: 12, pluginPipelineSensitiveInput: true });
assert.deepEqual(statuses, ["connected"]);
assert.equal(ctx.hasConnectedRef.current, true);
assert.deepEqual(output, [{ data: "", sensitive: true }]);
// Handshake output must dismiss the overlay so interactive prompts are reachable.
onData?.("ssh handshake banner\r\n", { moshHandshake: true });
assert.deepEqual(statuses, ["connected"]);
assert.equal(ctx.hasConnectedRef.current, true);
});
test("Mosh handshake stays connecting until ready unless the backend needs terminal input", () => {
const { term } = createFakeTerm();
const statuses: string[] = [];
let onData: ((data: string, meta?: {
moshHandshake?: boolean;
moshHandshakeRequiresUserInput?: boolean;
}) => void) | null = null;
const ctx = {
...createContext(false),
sessionId: "mosh-session",
sessionRef: { current: null as string | null },
hasConnectedRef: { current: false },
hasRunStartupCommandRef: { current: false },
disposeDataRef: { current: null as (() => void) | null },
disposeExitRef: { current: null as (() => void) | null },
fitAddonRef: { current: null },
serializeAddonRef: { current: null },
pendingAuthRef: { current: null },
terminalBackend: {
onSessionData: (_id: string, cb: typeof onData) => {
onData = cb;
return () => {};
},
onSessionExit: () => () => {},
writeToSession: () => {},
resizeSession: () => {},
setSessionFlowPaused: () => {},
ackSessionFlow: () => {},
},
updateStatus: (status: string) => {
statuses.push(status);
if (status === "connected") ctx.hasConnectedRef.current = true;
},
setError: () => {},
onSessionExit: () => {},
};
attachSessionToTerminal(ctx as never, term, "mosh-session", {
deferConnectionDuringMoshHandshake: true,
});
onData?.("login banner\r\n", { moshHandshake: true });
assert.deepEqual(statuses, []);
onData?.("Password:", { moshHandshake: true });
assert.deepEqual(statuses, []);
onData?.("Verification code:", {
moshHandshake: true,
moshHandshakeRequiresUserInput: true,
});
assert.deepEqual(statuses, ["connected"]);
});