[Init] Initial commit - NetMesh terminal manager
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
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
This commit is contained in:
690
electron/bridges/ptyOutputBuffer.test.cjs
Normal file
690
electron/bridges/ptyOutputBuffer.test.cjs
Normal file
@@ -0,0 +1,690 @@
|
||||
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,
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user