Files
NetMesh/components/terminal/runtime/terminalOutputPipeline.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

1394 lines
37 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import test from "node:test";
import type { Terminal as XTerm } from "@xterm/xterm";
import { createOutputFlowController } from "./outputFlowController.ts";
import {
armTerminalInterruptDisplayGate,
filterTerminalInterruptDisplayOutput,
prioritizeTerminalInput,
releaseTerminalFlowOutputForTerm,
teardownTerminalOutputPipeline,
} from "./terminalOutputPipeline.ts";
import { FLOW_LOW_WATER_MARK } from "./terminalFlowConstants.ts";
import {
enqueueCoalescedTerminalWrite,
flushTerminalWriteCoalescer,
getTerminalWriteCoalescerPendingBytes,
resetTerminalWriteCoalescer,
} from "./terminalWriteCoalescer.ts";
import {
enqueueTerminalWrite,
getTerminalWriteQueueDepth,
} from "./terminalWriteQueue.ts";
import { accumulateDeferredTerminalWriteAck } from "./terminalWriteAckDeferral.ts";
import { clearTerminalSessionFlowAck } from "./terminalFlowAckBuffer.ts";
const createFakeTerm = () => ({}) as XTerm;
test("teardownTerminalOutputPipeline resumes renderer pause and clears backlog", () => {
const term = createFakeTerm();
const events: string[] = [];
const flow = createOutputFlowController({
highWaterMark: 50,
lowWaterMark: 10,
onPause: () => events.push("pause"),
onResume: () => events.push("resume"),
});
const backend = {
setSessionFlowPaused: (_sessionId: string, paused: boolean) => {
events.push(paused ? "ipc-pause" : "ipc-resume");
},
ackSessionFlow: () => {},
};
flow.received(60);
enqueueTerminalWrite(term, 20, (done) => done());
teardownTerminalOutputPipeline(
{ terminalBackend: backend, sessionRef: { current: "sess-1" } } as never,
term,
"sess-1",
flow,
);
assert.deepEqual(events, ["pause", "resume", "ipc-resume"]);
});
test("releaseTerminalFlowOutputForTerm resumes renderer pause without a flow controller", () => {
const term = createFakeTerm();
const events: string[] = [];
const backend = {
setSessionFlowPaused: (_sessionId: string, paused: boolean) => {
events.push(paused ? "ipc-pause" : "ipc-resume");
},
ackSessionFlow: () => {},
};
releaseTerminalFlowOutputForTerm(term, backend, "sess-1", undefined);
assert.deepEqual(events, ["ipc-resume"]);
});
test("ordinary input priority preserves queued display output", () => {
const term = createFakeTerm();
const acked: number[] = [];
const deferred: Array<() => void> = [];
let completed = 0;
const flow = createOutputFlowController({
highWaterMark: 100,
lowWaterMark: 20,
onPause: () => {},
onResume: () => {},
});
const backend = {
setSessionFlowPaused: () => {},
ackSessionFlow: (_sessionId: string, bytes: number) => {
acked.push(bytes);
},
};
flow.received(FLOW_LOW_WATER_MARK + 80);
enqueueTerminalWrite(term, 50, () => {});
enqueueTerminalWrite(term, 30, (done) => {
completed += 1;
done();
});
const priority = prioritizeTerminalInput(
term,
"sess-1",
flow,
backend,
(callback: () => void) => deferred.push(callback),
);
assert.equal(priority.scheduledBackendResume, false);
assert.equal(priority.ackAfterInputBytes, 0);
assert.equal(getTerminalWriteQueueDepth(term), 1);
assert.deepEqual(acked, []);
assert.deepEqual(deferred, []);
assert.equal(completed, 0);
});
test("prioritizeTerminalInput flushes deferred xterm write ack bytes", () => {
clearTerminalSessionFlowAck("sess-1");
const term = createFakeTerm();
const acked: number[] = [];
const deferred: Array<() => void> = [];
const flow = createOutputFlowController({
highWaterMark: 100,
lowWaterMark: 20,
onPause: () => {},
onResume: () => {},
});
const backend = {
ackSessionFlow: (_sessionId: string, bytes: number) => {
acked.push(bytes);
},
setSessionFlowPaused: () => {},
};
accumulateDeferredTerminalWriteAck(term, 42);
prioritizeTerminalInput(
term,
"sess-1",
flow,
backend,
(callback: () => void) => deferred.push(callback),
);
assert.deepEqual(acked, []);
deferred[0]!();
assert.deepEqual(acked, [42]);
assert.equal(flow.pendingBytes(), 0);
clearTerminalSessionFlowAck("sess-1");
});
test("ordinary input priority leaves queued backlog intact", () => {
const term = createFakeTerm();
const events: string[] = [];
const deferred: Array<() => void> = [];
const flow = createOutputFlowController({
highWaterMark: 100,
lowWaterMark: 20,
onPause: () => events.push("pause"),
onResume: () => events.push("resume"),
});
const backend = {
setSessionFlowPaused: (_sessionId: string, paused: boolean) => {
events.push(paused ? "ipc-pause" : "ipc-resume");
},
ackSessionFlow: () => {},
};
flow.received(FLOW_LOW_WATER_MARK + 1024);
let release: (() => void) | null = null;
enqueueTerminalWrite(term, 30, (done) => {
release = done;
});
const priority = prioritizeTerminalInput(
term,
"sess-1",
flow,
backend,
(callback: () => void) => deferred.push(callback),
);
assert.equal(priority.scheduledBackendResume, false);
assert.equal(priority.writeQueueDepth, 0);
assert.equal(getTerminalWriteQueueDepth(term), 0);
assert.equal(events.includes("ipc-resume"), false);
assert.deepEqual(deferred, []);
events.push("input-forwarded");
release?.();
assert.deepEqual(events, ["pause", "input-forwarded"]);
});
test("ordinary input priority does not drop queued bytes or resume paused output", () => {
const term = createFakeTerm();
const events: string[] = [];
const deferred: Array<() => void> = [];
const flow = createOutputFlowController({
highWaterMark: 100,
lowWaterMark: 20,
onPause: () => events.push("pause"),
onResume: () => events.push("resume"),
});
const backend = {
setSessionFlowPaused: (_sessionId: string, paused: boolean) => {
events.push(paused ? "ipc-pause" : "ipc-resume");
},
ackSessionFlow: () => {},
};
flow.received(110);
enqueueTerminalWrite(term, 10, () => {});
enqueueTerminalWrite(term, 100, () => {});
const priority = prioritizeTerminalInput(
term,
"sess-1",
flow,
backend,
(callback: () => void) => deferred.push(callback),
);
assert.equal(priority.scheduledBackendResume, false);
assert.equal(priority.ackAfterInputBytes, 0);
assert.deepEqual(events, ["pause"]);
assert.equal(getTerminalWriteQueueDepth(term), 1);
events.push("input-forwarded");
assert.deepEqual(deferred, []);
assert.deepEqual(events, ["pause", "input-forwarded"]);
});
test("ordinary input priority preserves a pending coalesced backlog above the pressure threshold", () => {
clearTerminalSessionFlowAck("sess-1");
const scheduler = globalThis as typeof globalThis & {
requestAnimationFrame?: (callback: FrameRequestCallback) => number;
cancelAnimationFrame?: (handle: number) => void;
};
const originalRequestAnimationFrame = scheduler.requestAnimationFrame;
const originalCancelAnimationFrame = scheduler.cancelAnimationFrame;
scheduler.requestAnimationFrame = () => 1;
scheduler.cancelAnimationFrame = () => {};
const term = createFakeTerm();
const payload = "x".repeat(FLOW_LOW_WATER_MARK + 1024);
const written: string[] = [];
const events: string[] = [];
const deferred: Array<() => void> = [];
const flow = createOutputFlowController({
highWaterMark: 100,
lowWaterMark: 20,
onPause: () => events.push("pause"),
onResume: () => events.push("resume"),
});
const backend = {
setSessionFlowPaused: (_sessionId: string, paused: boolean) => {
events.push(paused ? "ipc-pause" : "ipc-resume");
},
ackSessionFlow: () => {},
};
try {
flow.received(payload.length);
enqueueCoalescedTerminalWrite(
term,
payload,
(data, ingressBytes) => {
written.push(data);
flow.written(ingressBytes);
},
payload.length,
);
const priority = prioritizeTerminalInput(
term,
"sess-1",
flow,
backend,
(callback: () => void) => deferred.push(callback),
);
assert.equal(priority.scheduledBackendResume, false);
assert.equal(flow.isPaused(), true);
assert.deepEqual(events, ["pause"]);
assert.deepEqual(deferred, []);
assert.equal(getTerminalWriteCoalescerPendingBytes(term), payload.length);
events.push("input-forwarded");
flushTerminalWriteCoalescer(term);
assert.equal(written.join(""), payload);
// Payload is under the Tabby-sized unbroken shard (~128KB), so flush is one write.
assert.equal(written.length, 1);
assert.equal(flow.pendingBytes(), 0);
} finally {
resetTerminalWriteCoalescer(term);
scheduler.requestAnimationFrame = originalRequestAnimationFrame;
scheduler.cancelAnimationFrame = originalCancelAnimationFrame;
clearTerminalSessionFlowAck("sess-1");
}
});
test("ordinary input priority preserves pending line-edit echo when flushing deferred acks", () => {
clearTerminalSessionFlowAck("sess-edit");
const scheduler = globalThis as typeof globalThis & {
requestAnimationFrame?: (callback: FrameRequestCallback) => number;
cancelAnimationFrame?: (handle: number) => void;
};
const originalRequestAnimationFrame = scheduler.requestAnimationFrame;
const originalCancelAnimationFrame = scheduler.cancelAnimationFrame;
scheduler.requestAnimationFrame = () => 1;
scheduler.cancelAnimationFrame = () => {};
const term = createFakeTerm();
const echoedHistoryCommand = "systemctl dddd";
const written: string[] = [];
const acked: number[] = [];
const deferred: Array<() => void> = [];
const flow = createOutputFlowController({
highWaterMark: 100,
lowWaterMark: 20,
onPause: () => {},
onResume: () => {},
});
const backend = {
ackSessionFlow: (_sessionId: string, bytes: number) => {
acked.push(bytes);
},
setSessionFlowPaused: () => {},
};
try {
accumulateDeferredTerminalWriteAck(term, 2);
flow.received(echoedHistoryCommand.length);
enqueueCoalescedTerminalWrite(
term,
echoedHistoryCommand,
(data, ingressBytes) => {
written.push(data);
flow.written(ingressBytes);
},
echoedHistoryCommand.length,
);
assert.equal(getTerminalWriteCoalescerPendingBytes(term), echoedHistoryCommand.length);
const priority = prioritizeTerminalInput(
term,
"sess-edit",
flow,
backend,
(callback: () => void) => deferred.push(callback),
{ reason: "input" },
);
assert.equal(priority.ackAfterInputBytes, 2);
assert.equal(flow.pendingBytes(), echoedHistoryCommand.length);
assert.equal(getTerminalWriteCoalescerPendingBytes(term), echoedHistoryCommand.length);
deferred[0]!();
assert.deepEqual(acked, [2]);
flushTerminalWriteCoalescer(term);
assert.deepEqual(written, [echoedHistoryCommand]);
assert.equal(flow.pendingBytes(), 0);
} finally {
resetTerminalWriteCoalescer(term);
scheduler.requestAnimationFrame = originalRequestAnimationFrame;
scheduler.cancelAnimationFrame = originalCancelAnimationFrame;
clearTerminalSessionFlowAck("sess-edit");
}
});
test("ordinary input priority keeps queued visible output intact", () => {
clearTerminalSessionFlowAck("sess-input");
const term = createFakeTerm();
const acked: number[] = [];
const deferred: Array<() => void> = [];
const order: string[] = [];
const flow = createOutputFlowController({
highWaterMark: 100,
lowWaterMark: 20,
onPause: () => {},
onResume: () => {},
});
const backend = {
ackSessionFlow: (_sessionId: string, bytes: number) => {
acked.push(bytes);
},
setSessionFlowPaused: () => {},
};
flow.received(FLOW_LOW_WATER_MARK + 200);
let releaseFirst: (() => void) | null = null;
enqueueTerminalWrite(term, 20, (done) => {
order.push("first");
releaseFirst = done;
});
enqueueTerminalWrite(term, 30, (done) => {
order.push("second");
done();
});
accumulateDeferredTerminalWriteAck(term, 7);
const priority = prioritizeTerminalInput(
term,
"sess-input",
flow,
backend,
(callback: () => void) => deferred.push(callback),
{ reason: "input" },
);
assert.equal(priority.ackAfterInputBytes, 7);
assert.equal(priority.writeQueueDepth, 1);
assert.equal(flow.pendingBytes(), FLOW_LOW_WATER_MARK + 200);
assert.deepEqual(order, ["first"]);
deferred[0]!();
assert.deepEqual(acked, [7]);
releaseFirst?.();
assert.deepEqual(order, ["first", "second"]);
clearTerminalSessionFlowAck("sess-input");
});
test("interrupt priority drains queued display output for ssh-like interrupts", () => {
const term = createFakeTerm();
const events: string[] = [];
const backend = {
ackSessionFlow: (_sessionId: string, bytes: number) => {
events.push(`ack:${bytes}`);
},
setSessionFlowPaused: (_sessionId: string, paused: boolean) => {
events.push(paused ? "ipc-pause" : "ipc-resume");
},
};
const flow = createOutputFlowController({
highWaterMark: 100,
lowWaterMark: 20,
onPause: () => events.push("pause"),
onResume: () => {},
});
flow.received(FLOW_LOW_WATER_MARK + 1024);
enqueueTerminalWrite(term, 20, () => {});
enqueueTerminalWrite(term, 30, () => {});
const priority = prioritizeTerminalInput(
term,
"sess-1",
flow,
backend,
(callback: () => void) => {
events.push("input-forwarded");
callback();
},
{
reason: "interrupt",
drainStaleOutput: true,
now: 4000,
quietMs: 100,
promptQuietMs: 80,
maxDrainMs: 1000,
},
);
assert.equal(priority.skippedReason, undefined);
assert.equal(priority.scheduledBackendResume, true);
assert.equal(priority.ackAfterInputBytes, 30);
assert.equal(priority.writeQueueDepth, 1);
assert.equal(flow.pendingBytes(), 0);
assert.deepEqual(events, ["pause", "input-forwarded", "ack:30", "ipc-resume"]);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "old output", { now: 4001 }),
{ accepted: false, data: "", droppedBytes: 10, reason: "draining" },
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "more old^C\r\n$ ", { now: 4002 }),
{ accepted: true, data: "^C\r\n$ ", droppedBytes: 8, reason: "interrupt-echo" },
);
});
test("interrupt display drain preserves alternate-screen exit controls", () => {
const term = createFakeTerm();
const backend = {
ackSessionFlow: () => {},
setSessionFlowPaused: () => {},
};
const flow = createOutputFlowController({
highWaterMark: 100,
lowWaterMark: 20,
onPause: () => {},
onResume: () => {},
});
flow.received(FLOW_LOW_WATER_MARK + 1);
prioritizeTerminalInput(
term,
"sess-1",
flow,
backend,
(callback: () => void) => callback(),
{
reason: "interrupt",
drainStaleOutput: true,
now: 6100,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 1000,
},
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "stale frame\x1b[?1049l", { now: 6101 }),
{
accepted: true,
data: "\x1b[?1049l",
droppedBytes: "stale frame".length,
reason: "draining",
},
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "stale frame\x1b[?1049l\r\n$ ", { now: 6200 }),
{
accepted: true,
data: "\x1b[?1049l$ ",
droppedBytes: "stale frame\r\n".length,
reason: "prompt-gap",
},
);
});
test("interrupt display drain excludes preserved restore from held password prefixes", () => {
const term = createFakeTerm();
armTerminalInterruptDisplayGate(term, {
now: 6250,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 2500,
});
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "stale\n\x1b[?1049lPass", { now: 6251 }),
{
accepted: true,
data: "\x1b[?1049l",
droppedBytes: "stale\n".length,
reason: "draining",
},
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "word: ", { now: 6252 }),
{
accepted: true,
data: "Password: ",
droppedBytes: 0,
reason: "prompt-gap",
},
);
});
test("interrupt display drain excludes last of multiple restores from password prefixes", () => {
const term = createFakeTerm();
armTerminalInterruptDisplayGate(term, {
now: 6255,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 2500,
});
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "\x1b[?25hstale\n\x1b[?1049lPass", { now: 6256 }),
{
accepted: true,
data: "\x1b[?25h\x1b[?1049l",
droppedBytes: "stale\n".length,
reason: "draining",
},
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "word: ", { now: 6257 }),
{
accepted: true,
data: "Password: ",
droppedBytes: 0,
reason: "prompt-gap",
},
);
});
test("interrupt display drain does not peel restore suffix from later password prefixes", () => {
const term = createFakeTerm();
armTerminalInterruptDisplayGate(term, {
now: 6260,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 2500,
});
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "\x1b[?1049l\nlogin pass", { now: 6261 }),
{
accepted: true,
data: "\x1b[?1049l",
droppedBytes: 1,
reason: "draining",
},
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "word: ", { now: 6262 }),
{
accepted: true,
data: "login password: ",
droppedBytes: 0,
reason: "prompt-gap",
},
);
});
test("interrupt display drain does not leak incomplete SGR CSI into shell prompts", () => {
const term = createFakeTerm();
armTerminalInterruptDisplayGate(term, {
now: 6270,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 2500,
});
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "old\x1b[31", { now: 6271 }),
{
accepted: false,
data: "",
droppedBytes: "old\x1b[31".length,
reason: "draining",
},
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "$ ", { now: 6400 }),
{
accepted: true,
data: "$ ",
droppedBytes: 0,
reason: "prompt-gap",
},
);
});
test("interrupt display drain preserves split alternate-screen exit controls", () => {
clearTerminalSessionFlowAck("sess-1");
const term = createFakeTerm();
const backend = {
ackSessionFlow: () => {},
setSessionFlowPaused: () => {},
};
const flow = createOutputFlowController({
highWaterMark: 100,
lowWaterMark: 20,
onPause: () => {},
onResume: () => {},
});
flow.received(FLOW_LOW_WATER_MARK + 1);
prioritizeTerminalInput(
term,
"sess-1",
flow,
backend,
(callback: () => void) => callback(),
{
reason: "interrupt",
drainStaleOutput: true,
now: 7100,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 1000,
},
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "stale frame\x1b[?104", { now: 7101 }),
{
accepted: false,
data: "",
droppedBytes: "stale frame".length,
reason: "draining",
},
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "9l^C\r\n$ ", { now: 7102 }),
{
accepted: true,
data: "\x1b[?1049l^C\r\n$ ",
droppedBytes: 0,
reason: "interrupt-echo",
},
);
});
test("interrupt display drain preserves restore controls on split caret echo", () => {
clearTerminalSessionFlowAck("sess-1");
const term = createFakeTerm();
const backend = {
ackSessionFlow: () => {},
setSessionFlowPaused: () => {},
};
const flow = createOutputFlowController({
highWaterMark: 100,
lowWaterMark: 20,
onPause: () => {},
onResume: () => {},
});
flow.received(FLOW_LOW_WATER_MARK + 1);
prioritizeTerminalInput(
term,
"sess-1",
flow,
backend,
(callback: () => void) => callback(),
{
reason: "interrupt",
drainStaleOutput: true,
now: 7150,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 1000,
},
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "stale frame\x1b[?104", { now: 7151 }),
{
accepted: false,
data: "",
droppedBytes: "stale frame".length,
reason: "draining",
},
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "9l^", { now: 7152 }),
{
accepted: true,
data: "\x1b[?1049l",
droppedBytes: 1,
reason: "draining",
},
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "C\r\n$ ", { now: 7153 }),
{
accepted: true,
data: "^C\r\n$ ",
droppedBytes: 0,
acceptedBytes: 5,
reason: "interrupt-echo",
},
);
});
test("interrupt display drain does not preserve unsafe combined private modes", () => {
clearTerminalSessionFlowAck("sess-1");
const term = createFakeTerm();
const backend = {
ackSessionFlow: () => {},
setSessionFlowPaused: () => {},
};
const flow = createOutputFlowController({
highWaterMark: 100,
lowWaterMark: 20,
onPause: () => {},
onResume: () => {},
});
flow.received(FLOW_LOW_WATER_MARK + 1);
prioritizeTerminalInput(
term,
"sess-1",
flow,
backend,
(callback: () => void) => callback(),
{
reason: "interrupt",
drainStaleOutput: true,
now: 7200,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 1000,
},
);
const unsafeSequence = "\x1b[?1049;25h";
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, `stale frame${unsafeSequence}^C\r\n$ `, {
now: 7201,
}),
{
accepted: true,
data: "^C\r\n$ ",
droppedBytes: "stale frame".length + unsafeSequence.length,
reason: "interrupt-echo",
},
);
});
test("interrupt display drain accepts prompt candidates with OSC title and spaces", () => {
clearTerminalSessionFlowAck("sess-1");
const term = createFakeTerm();
const backend = {
ackSessionFlow: () => {},
setSessionFlowPaused: () => {},
};
const flow = createOutputFlowController({
highWaterMark: 100,
lowWaterMark: 20,
onPause: () => {},
onResume: () => {},
});
flow.received(FLOW_LOW_WATER_MARK + 1);
prioritizeTerminalInput(
term,
"sess-1",
flow,
backend,
(callback: () => void) => callback(),
{
reason: "interrupt",
drainStaleOutput: true,
now: 7300,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 1000,
},
);
assert.equal(
filterTerminalInterruptDisplayOutput(term, "old output", { now: 7301 }).accepted,
false,
);
const prompt = "\x1b]0;~/My Project\x07~/My Project$ ";
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, prompt, { now: 7400 }),
{
accepted: true,
data: prompt,
droppedBytes: 0,
reason: "prompt-gap",
},
);
});
test("interrupt display drain accepts split OSC prompt candidates", () => {
clearTerminalSessionFlowAck("sess-1");
const term = createFakeTerm();
const backend = {
ackSessionFlow: () => {},
setSessionFlowPaused: () => {},
};
const flow = createOutputFlowController({
highWaterMark: 100,
lowWaterMark: 20,
onPause: () => {},
onResume: () => {},
});
flow.received(FLOW_LOW_WATER_MARK + 1);
prioritizeTerminalInput(
term,
"sess-1",
flow,
backend,
(callback: () => void) => callback(),
{
reason: "interrupt",
drainStaleOutput: true,
now: 7500,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 1000,
},
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "old output\x1b]0;~/My ", { now: 7501 }),
{
accepted: false,
data: "",
droppedBytes: "old output".length,
reason: "draining",
},
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "Project\x07~/My Project$ ", { now: 7600 }),
{
accepted: true,
data: "\x1b]0;~/My Project\x07~/My Project$ ",
droppedBytes: 0,
reason: "prompt-gap",
},
);
});
test("interrupt priority skips stale display drain but still flushes deferred acks", () => {
clearTerminalSessionFlowAck("sess-deferred");
const term = createFakeTerm();
const acked: number[] = [];
const deferred: Array<() => void> = [];
const backend = {
ackSessionFlow: (_sessionId: string, bytes: number) => {
acked.push(bytes);
},
setSessionFlowPaused: () => {},
};
accumulateDeferredTerminalWriteAck(term, 42);
const priority = prioritizeTerminalInput(
term,
"sess-deferred",
undefined,
backend,
(callback: () => void) => deferred.push(callback),
{
reason: "interrupt",
drainStaleOutput: false,
now: 5000,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 100,
},
);
assert.equal(priority.skippedReason, undefined);
assert.equal(priority.deferredAckBytes, 42);
assert.equal(priority.scheduledBackendResume, true);
assert.equal(priority.ackAfterInputBytes, 42);
assert.equal(deferred.length, 1);
deferred[0]!();
assert.deepEqual(acked, [42]);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "KeyboardInterrupt\r\n$ ", { now: 5001 }),
{
accepted: true,
data: "KeyboardInterrupt\r\n$ ",
droppedBytes: 0,
reason: "inactive",
},
);
clearTerminalSessionFlowAck("sess-deferred");
});
test("interrupt priority leaves display output alone below pressure threshold", () => {
clearTerminalSessionFlowAck("sess-low-pressure");
const term = createFakeTerm();
const flow = createOutputFlowController({
highWaterMark: 100,
lowWaterMark: 20,
onPause: () => {},
onResume: () => {},
});
const backend = {
ackSessionFlow: () => {},
setSessionFlowPaused: () => {},
};
flow.received(119);
const priority = prioritizeTerminalInput(
term,
"sess-low-pressure",
flow,
backend,
(callback: () => void) => callback(),
{
reason: "interrupt",
drainStaleOutput: true,
now: 7800,
},
);
assert.equal(priority.skippedReason, "below-threshold");
assert.deepEqual(
filterTerminalInterruptDisplayOutput(
term,
"Type :qa and press <Enter> to exit Vim",
{ now: 7801 },
),
{
accepted: true,
data: "Type :qa and press <Enter> to exit Vim",
droppedBytes: 0,
reason: "inactive",
},
);
clearTerminalSessionFlowAck("sess-low-pressure");
});
test("interrupt display drain accepts a sudo password prompt (#2010)", () => {
const term = createFakeTerm();
armTerminalInterruptDisplayGate(term, {
now: 8000,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 2500,
});
assert.equal(
filterTerminalInterruptDisplayOutput(term, "Reading package lists...\n", { now: 8001 }).accepted,
false,
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "[sudo] password for alice: ", { now: 8010 }),
{
accepted: true,
data: "[sudo] password for alice: ",
droppedBytes: 0,
reason: "password-prompt",
},
);
});
test("interrupt display drain accepts localized password prompts (#2010)", () => {
for (const prompt of ["Password: ", "[sudo] alice 的密码:", "输入密码", "用户 的密码"]) {
const term = createFakeTerm();
armTerminalInterruptDisplayGate(term, {
now: 8100,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 2500,
});
assert.equal(
filterTerminalInterruptDisplayOutput(term, "stale\n", { now: 8101 }).accepted,
false,
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, prompt, { now: 8110 }),
{
accepted: true,
data: prompt,
droppedBytes: 0,
reason: "password-prompt",
},
`expected password prompt to resume drain: ${JSON.stringify(prompt)}`,
);
}
});
test("interrupt display drain holds a split Password: prompt across chunks (#2010)", () => {
const term = createFakeTerm();
armTerminalInterruptDisplayGate(term, {
now: 8300,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 2500,
});
assert.equal(
filterTerminalInterruptDisplayOutput(term, "stale\n", { now: 8301 }).accepted,
false,
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "Pass", { now: 8302 }),
{ accepted: false, data: "", droppedBytes: 0, reason: "draining" },
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "word: ", { now: 8303 }),
{
accepted: true,
data: "Password: ",
droppedBytes: 0,
reason: "prompt-gap",
},
);
});
test("interrupt display drain holds a split ANSI-colored Password: prompt", () => {
const term = createFakeTerm();
const red = "\x1b[31m";
const reset = "\x1b[0m";
armTerminalInterruptDisplayGate(term, {
now: 8350,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 2500,
});
assert.equal(
filterTerminalInterruptDisplayOutput(term, "stale\n", { now: 8351 }).accepted,
false,
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, `${red}Pass`, { now: 8352 }),
{ accepted: false, data: "", droppedBytes: 0, reason: "draining" },
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, `word:${reset} `, { now: 8353 }),
{
accepted: true,
data: `${red}Password:${reset} `,
droppedBytes: 0,
reason: "prompt-gap",
},
);
});
test("interrupt display drain holds a password prefix split before trailing ANSI", () => {
const term = createFakeTerm();
const red = "\x1b[31m";
const reset = "\x1b[0m";
armTerminalInterruptDisplayGate(term, {
now: 8370,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 2500,
});
assert.equal(
filterTerminalInterruptDisplayOutput(term, "stale\n", { now: 8371 }).accepted,
false,
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, `${red}Pass\x1b[`, { now: 8372 }),
{ accepted: false, data: "", droppedBytes: 0, reason: "draining" },
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, `0mword: `, { now: 8373 }),
{
accepted: true,
data: `${red}Pass${reset}word: `,
droppedBytes: 0,
reason: "prompt-gap",
},
);
});
test("interrupt display drain holds a password prefix split mid CSI parameter", () => {
const term = createFakeTerm();
const red = "\x1b[31m";
const reset = "\x1b[0m";
armTerminalInterruptDisplayGate(term, {
now: 8380,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 2500,
});
assert.equal(
filterTerminalInterruptDisplayOutput(term, "stale\n", { now: 8381 }).accepted,
false,
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, `${red}Pass\x1b[0`, { now: 8382 }),
{ accepted: false, data: "", droppedBytes: 0, reason: "draining" },
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, `mword: `, { now: 8383 }),
{
accepted: true,
data: `${red}Pass${reset}word: `,
droppedBytes: 0,
reason: "prompt-gap",
},
);
});
test("interrupt display drain discards a held password prefix that does not complete", () => {
const term = createFakeTerm();
armTerminalInterruptDisplayGate(term, {
now: 8400,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 2500,
});
assert.equal(
filterTerminalInterruptDisplayOutput(term, "stale\n", { now: 8401 }).accepted,
false,
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "Pass", { now: 8402 }),
{ accepted: false, data: "", droppedBytes: 0, reason: "draining" },
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "$ ", { now: 8600 }),
{
accepted: true,
data: "$ ",
droppedBytes: 4,
reason: "prompt-gap",
},
);
});
test("interrupt display drain discards CSI finals when mid-CSI password prefix fails", () => {
const term = createFakeTerm();
armTerminalInterruptDisplayGate(term, {
now: 8420,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 2500,
});
assert.equal(
filterTerminalInterruptDisplayOutput(term, "stale\n", { now: 8421 }).accepted,
false,
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "Pass\x1b[0", { now: 8422 }),
{ accepted: false, data: "", droppedBytes: 0, reason: "draining" },
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "m$ ", { now: 8600 }),
{
accepted: true,
data: "$ ",
droppedBytes: "Pass\x1b[0".length + 1,
reason: "prompt-gap",
},
);
});
test("interrupt display drain does not hold ordinary password-related output", () => {
const term = createFakeTerm();
armTerminalInterruptDisplayGate(term, {
now: 8700,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 2500,
});
assert.equal(
filterTerminalInterruptDisplayOutput(term, "stale\n", { now: 8701 }).accepted,
false,
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "Password authentication failed", { now: 8702 }),
{
accepted: false,
data: "",
droppedBytes: "Password authentication failed".length,
reason: "draining",
},
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "$ ", { now: 8900 }),
{
accepted: true,
data: "$ ",
droppedBytes: 0,
reason: "prompt-gap",
},
);
});
test("interrupt display drain holds split password prompts with leading text", () => {
const term = createFakeTerm();
armTerminalInterruptDisplayGate(term, {
now: 9000,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 2500,
});
assert.equal(
filterTerminalInterruptDisplayOutput(term, "stale\n", { now: 9001 }).accepted,
false,
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "alice@host's pass", { now: 9002 }),
{ accepted: false, data: "", droppedBytes: 0, reason: "draining" },
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "word: ", { now: 9003 }),
{
accepted: true,
data: "alice@host's password: ",
droppedBytes: 0,
reason: "prompt-gap",
},
);
});
test("interrupt display drain accepts a fresh one-chunk password prompt before the quiet gap", () => {
const term = createFakeTerm();
armTerminalInterruptDisplayGate(term, {
now: 9100,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 2500,
});
assert.equal(
filterTerminalInterruptDisplayOutput(term, "old flood\n", { now: 9101 }).accepted,
false,
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "Password: ", { now: 9110 }),
{
accepted: true,
data: "Password: ",
droppedBytes: 0,
reason: "password-prompt",
},
);
});
test("interrupt display drain accepts a last-line password prompt after a large stale prefix", () => {
const term = createFakeTerm();
armTerminalInterruptDisplayGate(term, {
now: 9120,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 2500,
promptCandidateBytes: 512,
});
assert.equal(
filterTerminalInterruptDisplayOutput(term, "stale\n", { now: 9121 }).accepted,
false,
);
const largeChunk = `${"x".repeat(600)}\n[sudo] password for alice: `;
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, largeChunk, { now: 9130 }),
{
accepted: true,
data: "[sudo] password for alice: ",
droppedBytes: 601,
reason: "password-prompt",
},
);
});
test("interrupt display drain does not hold short ASCII trailing letters as password prefixes", () => {
const term = createFakeTerm();
armTerminalInterruptDisplayGate(term, {
now: 9300,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 2500,
});
assert.equal(
filterTerminalInterruptDisplayOutput(term, "stale\n", { now: 9301 }).accepted,
false,
);
// Full-width "password" must not lower ASCII minLen to 1, or "copy p" is held
// and a later "assword:" chunk can bypass quiet-gap.
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "copy p", { now: 9302 }),
{
accepted: false,
data: "",
droppedBytes: "copy p".length,
reason: "draining",
},
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "assword: ", { now: 9310 }),
{
accepted: false,
data: "",
droppedBytes: "assword: ".length,
reason: "draining",
},
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "$ ", { now: 9500 }),
{
accepted: true,
data: "$ ",
droppedBytes: 0,
reason: "prompt-gap",
},
);
});
test("interrupt display drain rejects held password-prefix completion across a line break", () => {
const term = createFakeTerm();
armTerminalInterruptDisplayGate(term, {
now: 9200,
quietMs: 500,
promptQuietMs: 80,
maxDrainMs: 2500,
});
assert.equal(
filterTerminalInterruptDisplayOutput(term, "stale\n", { now: 9201 }).accepted,
false,
);
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "Pass", { now: 9202 }),
{ accepted: false, data: "", droppedBytes: 0, reason: "draining" },
);
// A newline before Password: discards the held "Pass" prefix (not a same-line
// completion), but the fresh complete password line is still preserved.
assert.deepEqual(
filterTerminalInterruptDisplayOutput(term, "\nPassword: ", { now: 9210 }),
{
accepted: true,
data: "Password: ",
droppedBytes: 4 + 1,
reason: "password-prompt",
},
);
});