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
691 lines
19 KiB
JavaScript
691 lines
19 KiB
JavaScript
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
|
|
const { createPtyOutputBuffer } = require("./ptyOutputBuffer.cjs");
|
|
|
|
/** Resolve after one event-loop turn (immediates have run). */
|
|
const tick = () => new Promise((resolve) => setImmediate(resolve));
|
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
const waitFor = async (predicate, timeoutMs = 100) => {
|
|
const deadline = Date.now() + timeoutMs;
|
|
while (!predicate()) {
|
|
if (Date.now() > deadline) {
|
|
throw new Error("Timed out waiting for condition");
|
|
}
|
|
await sleep(1);
|
|
}
|
|
};
|
|
|
|
const withFakeOutputScheduler = (run) => {
|
|
const originalSetImmediate = global.setImmediate;
|
|
const originalClearImmediate = global.clearImmediate;
|
|
const originalSetTimeout = global.setTimeout;
|
|
const originalClearTimeout = global.clearTimeout;
|
|
const originalDateNow = Date.now;
|
|
const immediates = [];
|
|
const timers = [];
|
|
let now = 0;
|
|
|
|
global.setImmediate = (callback) => {
|
|
const handle = { callback, cleared: false };
|
|
immediates.push(handle);
|
|
return handle;
|
|
};
|
|
global.clearImmediate = (handle) => {
|
|
if (handle) handle.cleared = true;
|
|
};
|
|
global.setTimeout = (callback, ms) => {
|
|
const handle = { callback, ms, dueAt: now + ms, cleared: false };
|
|
timers.push(handle);
|
|
return handle;
|
|
};
|
|
global.clearTimeout = (handle) => {
|
|
if (handle) handle.cleared = true;
|
|
};
|
|
Date.now = () => now;
|
|
|
|
try {
|
|
return run({
|
|
immediates,
|
|
timers,
|
|
setNow(value) { now = value; },
|
|
advance(ms) {
|
|
now += ms;
|
|
for (const timer of timers) {
|
|
if (!timer.cleared && timer.dueAt <= now) {
|
|
timer.cleared = true;
|
|
timer.callback();
|
|
}
|
|
}
|
|
},
|
|
});
|
|
} finally {
|
|
global.setImmediate = originalSetImmediate;
|
|
global.clearImmediate = originalClearImmediate;
|
|
global.setTimeout = originalSetTimeout;
|
|
global.clearTimeout = originalClearTimeout;
|
|
Date.now = originalDateNow;
|
|
}
|
|
};
|
|
|
|
test("coalesces data buffered within the same turn into a single send", async () => {
|
|
const sends = [];
|
|
const buffer = createPtyOutputBuffer((data) => sends.push(data));
|
|
|
|
buffer.bufferData("a");
|
|
buffer.bufferData("b");
|
|
buffer.bufferData("c");
|
|
|
|
// Nothing is sent synchronously while still in the same turn.
|
|
assert.equal(sends.length, 0);
|
|
|
|
await tick();
|
|
|
|
assert.deepEqual(sends, ["abc"]);
|
|
});
|
|
|
|
test("flushes within a single event-loop turn (not on a fixed delay)", async () => {
|
|
const sends = [];
|
|
const buffer = createPtyOutputBuffer((data) => sends.push(data));
|
|
|
|
buffer.bufferData("x");
|
|
|
|
// A fixed-interval (e.g. 8ms) buffer would NOT have flushed after one
|
|
// immediate turn. Turn-based flushing must have delivered it by now.
|
|
await tick();
|
|
|
|
assert.deepEqual(sends, ["x"]);
|
|
});
|
|
|
|
test("coalesces network-dribbled bursts after forwarding the first chunk immediately", () => {
|
|
const sends = [];
|
|
withFakeOutputScheduler(({ immediates, timers, setNow }) => {
|
|
const buffer = createPtyOutputBuffer((data) => sends.push(data));
|
|
|
|
buffer.bufferData("Reading database ... 1%");
|
|
immediates.shift().callback();
|
|
assert.deepEqual(sends, ["Reading database ... 1%"]);
|
|
|
|
setNow(1);
|
|
buffer.bufferData("\rReading database ... 2%");
|
|
setNow(2);
|
|
buffer.bufferData("\rReading database ... 3%");
|
|
|
|
assert.deepEqual(sends, ["Reading database ... 1%"]);
|
|
assert.equal(timers.length, 1);
|
|
assert.ok(timers[0].ms > 0 && timers[0].ms <= 4);
|
|
|
|
timers[0].callback();
|
|
assert.deepEqual(sends, [
|
|
"Reading database ... 1%",
|
|
"\rReading database ... 2%\rReading database ... 3%",
|
|
]);
|
|
|
|
setNow(20);
|
|
buffer.bufferData("prompt after idle");
|
|
assert.equal(immediates.length, 1);
|
|
immediates.shift().callback();
|
|
assert.equal(sends.at(-1), "prompt after idle");
|
|
});
|
|
});
|
|
|
|
test("cuts IPC sends for a 1ms network-dribbled apt-style burst", () => {
|
|
let sends = 0;
|
|
let output = "";
|
|
withFakeOutputScheduler(({ immediates, advance }) => {
|
|
const buffer = createPtyOutputBuffer((data) => {
|
|
sends += 1;
|
|
output += data;
|
|
});
|
|
const chunks = Array.from({ length: 1031 }, (_, index) => (
|
|
index % 2 === 0
|
|
? `\rReading database ... ${index % 101}%`
|
|
: `Preparing package-${index} for upgrade\n`
|
|
));
|
|
|
|
for (const chunk of chunks) {
|
|
buffer.bufferData(chunk);
|
|
for (const immediate of immediates.splice(0)) {
|
|
if (!immediate.cleared) immediate.callback();
|
|
}
|
|
advance(1);
|
|
}
|
|
buffer.flush();
|
|
|
|
assert.equal(output, chunks.join(""));
|
|
assert.ok(sends <= 350, `expected at most 350 IPC sends, got ${sends}`);
|
|
});
|
|
});
|
|
|
|
test("paces size-cap flushes with a short flood delay", () => {
|
|
const originalSetTimeout = global.setTimeout;
|
|
const originalClearTimeout = global.clearTimeout;
|
|
const timers = [];
|
|
global.setTimeout = (callback, ms) => {
|
|
const timer = { callback, ms, cleared: false };
|
|
timers.push(timer);
|
|
return timer;
|
|
};
|
|
global.clearTimeout = (timer) => {
|
|
if (timer) timer.cleared = true;
|
|
};
|
|
|
|
const sends = [];
|
|
try {
|
|
const buffer = createPtyOutputBuffer((data) => sends.push(data), {
|
|
maxBufferSize: 4,
|
|
floodFlushDelayMs: 5,
|
|
});
|
|
|
|
buffer.bufferData("ab");
|
|
assert.equal(sends.length, 0); // under cap, still pending
|
|
|
|
buffer.bufferData("cd"); // now "abcd" hits the 4-byte cap
|
|
|
|
// Flood-sized output is paced instead of synchronously spamming IPC.
|
|
assert.deepEqual(sends, []);
|
|
assert.equal(timers.length, 1);
|
|
assert.equal(timers[0].ms, 5);
|
|
|
|
timers[0].callback();
|
|
assert.deepEqual(sends, ["abcd"]);
|
|
} finally {
|
|
global.setTimeout = originalSetTimeout;
|
|
global.clearTimeout = originalClearTimeout;
|
|
}
|
|
});
|
|
|
|
test("hard cap starts a paced drain instead of flushing a whole burst", async () => {
|
|
const sends = [];
|
|
const buffer = createPtyOutputBuffer((data) => sends.push(data), {
|
|
maxBufferSize: 4,
|
|
maxFloodBufferSize: 8,
|
|
floodFlushDelayMs: 1,
|
|
});
|
|
|
|
buffer.bufferData("1234567890abcdefgh");
|
|
|
|
assert.deepEqual(sends, ["12345678"]);
|
|
|
|
await waitFor(() => sends.length >= 3);
|
|
assert.deepEqual(sends, ["12345678", "90abcdef", "gh"]);
|
|
});
|
|
|
|
test("single large accepted append obeys the total pending cap", async () => {
|
|
const sends = [];
|
|
const reports = [];
|
|
const buffer = createPtyOutputBuffer((data) => sends.push(data), {
|
|
maxBufferSize: 4,
|
|
maxFloodBufferSize: 8,
|
|
maxPendingBytes: 16,
|
|
floodFlushDelayMs: 1,
|
|
onPendingBytesChange: (bytes) => reports.push(bytes),
|
|
});
|
|
|
|
buffer.bufferData("0123456789abcdefghij");
|
|
|
|
assert.equal(reports.at(-1), 12);
|
|
assert.deepEqual(sends, ["4567"]);
|
|
|
|
await waitFor(() => sends.join("") === "456789abcdefghij");
|
|
assert.deepEqual(sends, ["4567", "89abcdef", "ghij"]);
|
|
});
|
|
|
|
test("default hard cap keeps flood-sized renderer sends bounded", () => {
|
|
const sends = [];
|
|
const buffer = createPtyOutputBuffer((data) => sends.push(data), {
|
|
maxBufferSize: 4,
|
|
floodFlushDelayMs: 50,
|
|
});
|
|
const hardCap = 768 * 1024;
|
|
const payload = `${"x".repeat(hardCap)}y`;
|
|
|
|
buffer.bufferData(payload);
|
|
|
|
assert.deepEqual(sends.map((send) => send.length), [hardCap]);
|
|
});
|
|
|
|
test("flush() forces a synchronous send and cancels the pending turn", async () => {
|
|
const sends = [];
|
|
const buffer = createPtyOutputBuffer((data) => sends.push(data));
|
|
|
|
buffer.bufferData("hello");
|
|
buffer.flush();
|
|
|
|
assert.deepEqual(sends, ["hello"]);
|
|
|
|
await tick();
|
|
assert.deepEqual(sends, ["hello"]); // not sent twice
|
|
});
|
|
|
|
test("flush() with an empty buffer does not send", async () => {
|
|
const sends = [];
|
|
const buffer = createPtyOutputBuffer((data) => sends.push(data));
|
|
|
|
buffer.flush();
|
|
|
|
assert.equal(sends.length, 0);
|
|
});
|
|
|
|
test("discard() drops pending data and cancels the pending turn", async () => {
|
|
const sends = [];
|
|
const buffer = createPtyOutputBuffer((data) => sends.push(data));
|
|
|
|
buffer.bufferData("tail");
|
|
assert.equal(buffer.discard(), 4);
|
|
|
|
assert.deepEqual(sends, []);
|
|
await tick();
|
|
assert.deepEqual(sends, []);
|
|
});
|
|
|
|
test("flushPaced callback clears paused backlog without materializing it", async () => {
|
|
const sends = [];
|
|
let drained = false;
|
|
const buffer = createPtyOutputBuffer((data) => sends.push(data), {
|
|
maxBufferSize: 4,
|
|
maxFloodBufferSize: 8,
|
|
shouldAcceptOutput: () => false,
|
|
});
|
|
|
|
buffer.bufferData("1234567890abcdefgh");
|
|
buffer.flushPaced(() => {
|
|
drained = true;
|
|
});
|
|
|
|
assert.equal(buffer.discard(), 0);
|
|
await sleep(5);
|
|
|
|
assert.deepEqual(sends, []);
|
|
assert.equal(drained, true);
|
|
});
|
|
|
|
test("takePending() returns pending data without sending it", async () => {
|
|
const sends = [];
|
|
const buffer = createPtyOutputBuffer((data) => sends.push(data));
|
|
|
|
buffer.bufferData("tail");
|
|
|
|
assert.equal(buffer.takePending(), "tail");
|
|
assert.deepEqual(sends, []);
|
|
await tick();
|
|
assert.deepEqual(sends, []);
|
|
});
|
|
|
|
test("takePendingEntry() returns pending data with metadata", () => {
|
|
const sends = [];
|
|
let accept = false;
|
|
const buffer = createPtyOutputBuffer((data, meta) => sends.push({ data, meta }), {
|
|
maxBufferSize: 4,
|
|
maxFloodBufferSize: 8,
|
|
maxPendingBytes: 8,
|
|
shouldAcceptOutput: () => accept,
|
|
});
|
|
|
|
buffer.bufferData(`\x1b[?1049h${"x".repeat(16)}tail`);
|
|
|
|
const pending = buffer.takePendingEntry();
|
|
|
|
assert.equal(pending.data.length, 8);
|
|
assert.deepEqual(pending.meta, {
|
|
droppedOutputMayAffectTerminalState: true,
|
|
droppedOutputAlternateScreenAction: "enter",
|
|
});
|
|
assert.deepEqual(sends, []);
|
|
});
|
|
|
|
test("new unknown dropped-output risk clears stale alternate-screen action metadata", () => {
|
|
const buffer = createPtyOutputBuffer(() => {});
|
|
|
|
buffer.bufferData("first", {
|
|
droppedOutputMayAffectTerminalState: true,
|
|
droppedOutputAlternateScreenAction: "leave",
|
|
});
|
|
buffer.bufferData("second", {
|
|
droppedOutputMayAffectTerminalState: true,
|
|
});
|
|
|
|
assert.deepEqual(buffer.takePendingEntry().meta, {
|
|
droppedOutputMayAffectTerminalState: true,
|
|
});
|
|
});
|
|
|
|
test("buffers incoming data while shouldAcceptOutput returns false", async () => {
|
|
const sends = [];
|
|
let accept = true;
|
|
const buffer = createPtyOutputBuffer((data) => sends.push(data), {
|
|
shouldAcceptOutput: () => accept,
|
|
});
|
|
|
|
buffer.bufferData("before");
|
|
accept = false;
|
|
buffer.bufferData("buffered");
|
|
await tick();
|
|
|
|
assert.deepEqual(sends, []);
|
|
accept = true;
|
|
buffer.flush();
|
|
|
|
assert.deepEqual(sends, ["beforebuffered"]);
|
|
});
|
|
|
|
test("flushes data buffered while output is not accepted in bounded chunks", async () => {
|
|
const sends = [];
|
|
let accept = false;
|
|
const buffer = createPtyOutputBuffer((data) => sends.push(data), {
|
|
maxBufferSize: 4,
|
|
maxFloodBufferSize: 8,
|
|
shouldAcceptOutput: () => accept,
|
|
});
|
|
|
|
buffer.bufferData("1234");
|
|
buffer.bufferData("5678");
|
|
buffer.bufferData("90ab");
|
|
await tick();
|
|
|
|
assert.deepEqual(sends, []);
|
|
accept = true;
|
|
buffer.flush();
|
|
|
|
assert.deepEqual(sends, ["12345678", "90ab"]);
|
|
});
|
|
|
|
test("paused backlog keeps only the newest data after the total pending cap", async () => {
|
|
const sends = [];
|
|
const reports = [];
|
|
let accept = false;
|
|
const buffer = createPtyOutputBuffer((data) => sends.push(data), {
|
|
maxBufferSize: 4,
|
|
maxFloodBufferSize: 8,
|
|
maxPendingBytes: 16,
|
|
shouldAcceptOutput: () => accept,
|
|
onPendingBytesChange: (bytes) => reports.push(bytes),
|
|
});
|
|
|
|
buffer.bufferData("0123456789abcdefghij");
|
|
await tick();
|
|
|
|
assert.equal(reports.at(-1), 16);
|
|
assert.deepEqual(sends, []);
|
|
|
|
accept = true;
|
|
buffer.flushPaced();
|
|
await waitFor(() => sends.join("") === "456789abcdefghij");
|
|
assert.deepEqual(sends, ["4567", "89abcdef", "ghij"]);
|
|
});
|
|
|
|
test("flushPaced releases paused backlog over multiple turns", async () => {
|
|
const sends = [];
|
|
let accept = false;
|
|
const buffer = createPtyOutputBuffer((data) => sends.push(data), {
|
|
maxBufferSize: 4,
|
|
maxFloodBufferSize: 8,
|
|
floodFlushDelayMs: 1,
|
|
shouldAcceptOutput: () => accept,
|
|
});
|
|
|
|
buffer.bufferData("1234567890abcdefgh");
|
|
await tick();
|
|
|
|
assert.deepEqual(sends, []);
|
|
accept = true;
|
|
buffer.flushPaced();
|
|
|
|
assert.deepEqual(sends, ["12345678"]);
|
|
await waitFor(() => sends.length >= 3);
|
|
|
|
assert.deepEqual(sends, ["12345678", "90abcdef", "gh"]);
|
|
});
|
|
|
|
test("fresh output does not cancel an active paced backlog drain", async () => {
|
|
const sends = [];
|
|
let accept = false;
|
|
const buffer = createPtyOutputBuffer((data) => sends.push(data), {
|
|
maxBufferSize: 4,
|
|
maxFloodBufferSize: 8,
|
|
floodFlushDelayMs: 1,
|
|
shouldAcceptOutput: () => accept,
|
|
});
|
|
|
|
buffer.bufferData("1234567890abcdefgh");
|
|
await tick();
|
|
|
|
accept = true;
|
|
buffer.flushPaced();
|
|
assert.deepEqual(sends, ["12345678"]);
|
|
|
|
buffer.bufferData("NEW");
|
|
assert.deepEqual(sends, ["12345678"]);
|
|
|
|
await waitFor(() => sends.length >= 3);
|
|
assert.deepEqual(sends, ["12345678", "90abcdef", "ghNEW"]);
|
|
});
|
|
|
|
test("fresh output during an active paced drain obeys the total pending cap", async () => {
|
|
const sends = [];
|
|
const reports = [];
|
|
const buffer = createPtyOutputBuffer((data) => sends.push(data), {
|
|
maxBufferSize: 4,
|
|
maxFloodBufferSize: 8,
|
|
maxPendingBytes: 16,
|
|
floodFlushDelayMs: 5,
|
|
onPendingBytesChange: (bytes) => reports.push(bytes),
|
|
});
|
|
|
|
buffer.bufferData("0123456789abcdefghij");
|
|
assert.deepEqual(sends, ["4567"]);
|
|
|
|
buffer.bufferData("K".repeat(20));
|
|
assert.equal(reports.at(-1), 16);
|
|
assert.deepEqual(sends, ["4567"]);
|
|
|
|
await waitFor(() => sends.join("") === `4567${"K".repeat(16)}`);
|
|
assert.deepEqual(sends, ["4567", "KKKKKKKK", "KKKKKKKK"]);
|
|
});
|
|
|
|
test("flushPaced callback runs after the paced backlog drains", async () => {
|
|
const sends = [];
|
|
let drained = false;
|
|
const buffer = createPtyOutputBuffer((data) => sends.push(data), {
|
|
maxBufferSize: 4,
|
|
maxFloodBufferSize: 8,
|
|
floodFlushDelayMs: 1,
|
|
});
|
|
|
|
buffer.bufferData("1234567890abcdefgh");
|
|
buffer.flushPaced(() => {
|
|
drained = true;
|
|
});
|
|
|
|
assert.equal(drained, false);
|
|
assert.deepEqual(sends, ["12345678", "90abcdef"]);
|
|
|
|
await waitFor(() => drained);
|
|
assert.deepEqual(sends, ["12345678", "90abcdef", "gh"]);
|
|
});
|
|
|
|
test("flushPaced callback runs when output is paused by discarding pending data", async () => {
|
|
const sends = [];
|
|
const reports = [];
|
|
let drained = false;
|
|
const buffer = createPtyOutputBuffer((data) => sends.push(data), {
|
|
maxBufferSize: 4,
|
|
maxFloodBufferSize: 8,
|
|
shouldAcceptOutput: () => false,
|
|
onPendingBytesChange: (bytes) => reports.push(bytes),
|
|
});
|
|
|
|
buffer.bufferData("1234567890abcdefgh");
|
|
buffer.flushPaced(() => {
|
|
drained = true;
|
|
});
|
|
|
|
assert.equal(drained, true);
|
|
assert.deepEqual(sends, []);
|
|
assert.equal(reports.at(-1), 0);
|
|
});
|
|
|
|
test("flushPaced callback runs if output pauses during a paced drain", async () => {
|
|
const sends = [];
|
|
const reports = [];
|
|
let accept = false;
|
|
let drained = false;
|
|
const buffer = createPtyOutputBuffer((data) => {
|
|
sends.push(data);
|
|
accept = false;
|
|
}, {
|
|
maxBufferSize: 4,
|
|
maxFloodBufferSize: 8,
|
|
floodFlushDelayMs: 1,
|
|
shouldAcceptOutput: () => accept,
|
|
onPendingBytesChange: (bytes) => reports.push(bytes),
|
|
});
|
|
|
|
buffer.bufferData("1234567890abcdefgh");
|
|
accept = true;
|
|
buffer.flushPaced(() => {
|
|
drained = true;
|
|
});
|
|
|
|
assert.equal(drained, false);
|
|
assert.deepEqual(sends, ["12345678"]);
|
|
|
|
await waitFor(() => drained);
|
|
|
|
assert.deepEqual(sends, ["12345678"]);
|
|
assert.equal(reports.at(-1), 0);
|
|
assert.equal(buffer.discard(), 0);
|
|
});
|
|
|
|
test("keeps a single paused append bounded before output is accepted", async () => {
|
|
const sends = [];
|
|
let accept = false;
|
|
const buffer = createPtyOutputBuffer((data) => sends.push(data), {
|
|
maxBufferSize: 4,
|
|
maxFloodBufferSize: 8,
|
|
shouldAcceptOutput: () => accept,
|
|
});
|
|
|
|
buffer.bufferData("1234567890abcdefgh");
|
|
await tick();
|
|
|
|
assert.deepEqual(sends, []);
|
|
accept = true;
|
|
buffer.flush();
|
|
|
|
assert.deepEqual(sends, ["12345678", "90abcdef", "gh"]);
|
|
});
|
|
|
|
test("keeps batching after a flush", async () => {
|
|
const sends = [];
|
|
const buffer = createPtyOutputBuffer((data) => sends.push(data));
|
|
|
|
buffer.bufferData("first");
|
|
await tick();
|
|
|
|
buffer.bufferData("second");
|
|
await waitFor(() => sends.length === 2);
|
|
|
|
assert.deepEqual(sends, ["first", "second"]);
|
|
});
|
|
|
|
test("pending cap marks next send when dropped output may affect terminal state", () => {
|
|
const sends = [];
|
|
let accept = false;
|
|
const buffer = createPtyOutputBuffer((data, meta) => sends.push({ data, meta }), {
|
|
maxBufferSize: 4,
|
|
maxFloodBufferSize: 8,
|
|
maxPendingBytes: 8,
|
|
shouldAcceptOutput: () => accept,
|
|
});
|
|
|
|
buffer.bufferData(`\x1b[?1049h${"x".repeat(16)}tail`);
|
|
|
|
assert.deepEqual(sends, []);
|
|
accept = true;
|
|
buffer.flush();
|
|
|
|
assert.equal(sends[0].meta?.droppedOutputMayAffectTerminalState, true);
|
|
assert.equal(sends[0].meta?.droppedOutputAlternateScreenAction, "enter");
|
|
});
|
|
|
|
test("pending cap does not mark plain dropped output as terminal state risk", () => {
|
|
const sends = [];
|
|
let accept = false;
|
|
const buffer = createPtyOutputBuffer((data, meta) => sends.push({ data, meta }), {
|
|
maxBufferSize: 4,
|
|
maxFloodBufferSize: 8,
|
|
maxPendingBytes: 8,
|
|
shouldAcceptOutput: () => accept,
|
|
});
|
|
|
|
buffer.bufferData("abcdefghijklmnopqrstuv");
|
|
|
|
accept = true;
|
|
buffer.flush();
|
|
|
|
assert.equal(sends[0].meta, undefined);
|
|
});
|
|
|
|
test("pending cap forwards dropped alternate-screen leave as recovery state", () => {
|
|
const sends = [];
|
|
let accept = false;
|
|
const buffer = createPtyOutputBuffer((data, meta) => sends.push({ data, meta }), {
|
|
maxBufferSize: 4,
|
|
maxFloodBufferSize: 8,
|
|
maxPendingBytes: 8,
|
|
shouldAcceptOutput: () => accept,
|
|
});
|
|
|
|
buffer.bufferData(`\x1b[?1049ltail${"x".repeat(16)}`);
|
|
|
|
accept = true;
|
|
buffer.flush();
|
|
|
|
assert.deepEqual(sends[0].meta, {
|
|
droppedOutputMayAffectTerminalState: true,
|
|
droppedOutputAlternateScreenAction: "leave",
|
|
});
|
|
});
|
|
|
|
test("pending cap reports alternate-screen action split across dropped and retained bytes", () => {
|
|
let accept = false;
|
|
const buffer = createPtyOutputBuffer(() => {}, {
|
|
maxBufferSize: 1,
|
|
maxFloodBufferSize: 1,
|
|
maxPendingBytes: "lREADY".length,
|
|
shouldAcceptOutput: () => accept,
|
|
});
|
|
|
|
buffer.bufferData("\x1b[?1049lREADY");
|
|
|
|
const pending = buffer.takePendingEntry();
|
|
|
|
assert.equal(pending.data, "lREADY");
|
|
assert.deepEqual(pending.meta, {
|
|
droppedOutputMayAffectTerminalState: true,
|
|
droppedOutputAlternateScreenAction: "leave",
|
|
});
|
|
});
|
|
|
|
test("pending cap does not mark retained alternate-screen leave as already dropped", () => {
|
|
const sends = [];
|
|
let accept = false;
|
|
const retained = "\x1b[?1049ltail";
|
|
const buffer = createPtyOutputBuffer((data, meta) => sends.push({ data, meta }), {
|
|
maxBufferSize: retained.length,
|
|
maxFloodBufferSize: retained.length,
|
|
maxPendingBytes: retained.length,
|
|
shouldAcceptOutput: () => accept,
|
|
});
|
|
|
|
buffer.bufferData(`${"x".repeat(16)}${retained}`);
|
|
|
|
accept = true;
|
|
buffer.flush();
|
|
|
|
assert.deepEqual(sends[0].meta, {
|
|
droppedOutputMayAffectTerminalState: true,
|
|
});
|
|
});
|