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
410 lines
11 KiB
TypeScript
410 lines
11 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import type { Terminal as XTerm } from "@xterm/xterm";
|
|
|
|
import {
|
|
MAX_WRITE_QUEUE_ITEMS,
|
|
abortTerminalWriteQueue,
|
|
enqueueTerminalWrite,
|
|
flushTerminalWriteQueueBypassingTimers,
|
|
getTerminalWriteQueueDepth,
|
|
isTerminalWriteQueueInFloodMode,
|
|
setTerminalWriteQueueDropHandler,
|
|
} from "./terminalWriteQueue.ts";
|
|
|
|
const createFakeTerm = () => ({}) as XTerm;
|
|
const waitForQueuedWriteYield = async () => {
|
|
await new Promise((resolve) => { setTimeout(resolve, 0); });
|
|
};
|
|
const waitForQueuedWrites = async (count: number) => {
|
|
for (let index = 0; index < count; index += 1) {
|
|
await waitForQueuedWriteYield();
|
|
}
|
|
};
|
|
|
|
test("enqueueTerminalWrite serializes writes in order", () => {
|
|
const term = createFakeTerm();
|
|
const order: number[] = [];
|
|
|
|
enqueueTerminalWrite(term, 1, (done) => {
|
|
order.push(1);
|
|
done();
|
|
});
|
|
enqueueTerminalWrite(term, 1, (done) => {
|
|
order.push(2);
|
|
done();
|
|
});
|
|
|
|
assert.deepEqual(order, [1, 2]);
|
|
});
|
|
|
|
test("deferStart queues a write until the next task", async () => {
|
|
const term = createFakeTerm();
|
|
const order: number[] = [];
|
|
|
|
enqueueTerminalWrite(term, 1, (done) => {
|
|
order.push(1);
|
|
done();
|
|
}, { deferStart: true });
|
|
|
|
assert.deepEqual(order, []);
|
|
await new Promise((resolve) => { setTimeout(resolve, 0); });
|
|
assert.deepEqual(order, [1]);
|
|
});
|
|
|
|
test("yieldAfter lets the event loop run between queued write chunks", async () => {
|
|
const term = createFakeTerm();
|
|
const order: number[] = [];
|
|
|
|
enqueueTerminalWrite(term, 1, (done) => {
|
|
order.push(1);
|
|
done();
|
|
}, { deferStart: true, yieldAfter: true });
|
|
enqueueTerminalWrite(term, 1, (done) => {
|
|
order.push(2);
|
|
done();
|
|
});
|
|
|
|
await waitForQueuedWriteYield();
|
|
assert.deepEqual(order, [1]);
|
|
await waitForQueuedWriteYield();
|
|
assert.deepEqual(order, [1, 2]);
|
|
});
|
|
|
|
test("flushTerminalWriteQueueBypassingTimers drains deferred queue steps immediately", () => {
|
|
const term = createFakeTerm();
|
|
const order: number[] = [];
|
|
|
|
enqueueTerminalWrite(term, 1, (done) => {
|
|
order.push(1);
|
|
done();
|
|
}, { deferStart: true, yieldAfter: true });
|
|
enqueueTerminalWrite(term, 1, (done) => {
|
|
order.push(2);
|
|
done();
|
|
});
|
|
|
|
assert.deepEqual(order, []);
|
|
assert.equal(flushTerminalWriteQueueBypassingTimers(term), true);
|
|
assert.deepEqual(order, [1, 2]);
|
|
assert.equal(flushTerminalWriteQueueBypassingTimers(term), false);
|
|
});
|
|
|
|
test("marks flood mode and coalesces queued writes when item cap is exceeded", async () => {
|
|
const term = createFakeTerm();
|
|
const dropped: number[] = [];
|
|
const order: number[] = [];
|
|
let releaseFirst: (() => void) | null = null;
|
|
|
|
enqueueTerminalWrite(term, 10, (done) => {
|
|
releaseFirst = done;
|
|
});
|
|
|
|
for (let index = 0; index < MAX_WRITE_QUEUE_ITEMS + 1; index += 1) {
|
|
enqueueTerminalWrite(
|
|
term,
|
|
10,
|
|
(done) => {
|
|
order.push(index);
|
|
done();
|
|
},
|
|
{ onDropped: (bytes) => dropped.push(bytes) },
|
|
);
|
|
}
|
|
|
|
assert.deepEqual(dropped, []);
|
|
assert.equal(isTerminalWriteQueueInFloodMode(term), true);
|
|
assert.equal(getTerminalWriteQueueDepth(term), 1);
|
|
releaseFirst?.();
|
|
await waitForQueuedWrites(MAX_WRITE_QUEUE_ITEMS + 1);
|
|
assert.deepEqual(order, Array.from({ length: MAX_WRITE_QUEUE_ITEMS + 1 }, (_, index) => index));
|
|
});
|
|
|
|
test("setTerminalWriteQueueDropHandler only reports explicit queue aborts", () => {
|
|
const term = createFakeTerm();
|
|
const dropped: number[] = [];
|
|
let completed = 0;
|
|
let releaseFirst: (() => void) | null = null;
|
|
|
|
setTerminalWriteQueueDropHandler(term, (bytes) => dropped.push(bytes));
|
|
enqueueTerminalWrite(term, 10, (done) => {
|
|
releaseFirst = done;
|
|
});
|
|
|
|
for (let index = 0; index < MAX_WRITE_QUEUE_ITEMS + 1; index += 1) {
|
|
enqueueTerminalWrite(term, 10, (done) => {
|
|
completed += 1;
|
|
done();
|
|
});
|
|
}
|
|
|
|
assert.deepEqual(dropped, []);
|
|
assert.equal(isTerminalWriteQueueInFloodMode(term), true);
|
|
abortTerminalWriteQueue(term);
|
|
assert.deepEqual(dropped, [MAX_WRITE_QUEUE_ITEMS * 10 + 10]);
|
|
releaseFirst?.();
|
|
assert.equal(completed, 0);
|
|
});
|
|
|
|
test("abortTerminalWriteQueue cancels remaining merged writes while one is in flight", () => {
|
|
const term = createFakeTerm();
|
|
const dropped: number[] = [];
|
|
const order: number[] = [];
|
|
let releaseFirst: (() => void) | null = null;
|
|
|
|
enqueueTerminalWrite(term, 10, (done) => {
|
|
releaseFirst = done;
|
|
});
|
|
|
|
for (let index = 0; index < MAX_WRITE_QUEUE_ITEMS + 1; index += 1) {
|
|
enqueueTerminalWrite(
|
|
term,
|
|
10,
|
|
(done) => {
|
|
order.push(index);
|
|
if (index === 0) {
|
|
abortTerminalWriteQueue(term, (bytes) => dropped.push(bytes));
|
|
}
|
|
done();
|
|
},
|
|
);
|
|
}
|
|
|
|
releaseFirst?.();
|
|
|
|
assert.deepEqual(order, [0]);
|
|
assert.deepEqual(dropped, [MAX_WRITE_QUEUE_ITEMS * 10]);
|
|
});
|
|
|
|
test("aborted yield timers do not clear a replacement write queue", async () => {
|
|
const term = createFakeTerm();
|
|
const order: string[] = [];
|
|
let releaseReplacement: (() => void) | null = null;
|
|
|
|
for (let index = 0; index < MAX_WRITE_QUEUE_ITEMS + 1; index += 1) {
|
|
enqueueTerminalWrite(
|
|
term,
|
|
10,
|
|
(done) => {
|
|
order.push(`old-${index}`);
|
|
done();
|
|
},
|
|
{ deferStart: true, yieldAfter: true },
|
|
);
|
|
}
|
|
|
|
await waitForQueuedWriteYield();
|
|
assert.deepEqual(order, ["old-0"]);
|
|
abortTerminalWriteQueue(term);
|
|
|
|
enqueueTerminalWrite(term, 10, (done) => {
|
|
order.push("replacement-active");
|
|
releaseReplacement = done;
|
|
});
|
|
await waitForQueuedWriteYield();
|
|
enqueueTerminalWrite(term, 10, (done) => {
|
|
order.push("replacement-pending");
|
|
done();
|
|
});
|
|
|
|
assert.deepEqual(order, ["old-0", "replacement-active"]);
|
|
releaseReplacement?.();
|
|
assert.deepEqual(order, ["old-0", "replacement-active", "replacement-pending"]);
|
|
});
|
|
|
|
test("merges passive flood backlog items without dropping output", async () => {
|
|
const term = createFakeTerm();
|
|
const dropped: number[] = [];
|
|
const order: number[] = [];
|
|
let releaseFirst: (() => void) | null = null;
|
|
|
|
enqueueTerminalWrite(term, 10, (done) => {
|
|
releaseFirst = done;
|
|
});
|
|
|
|
for (let index = 0; index < MAX_WRITE_QUEUE_ITEMS + 10; index += 1) {
|
|
enqueueTerminalWrite(
|
|
term,
|
|
10,
|
|
(done) => {
|
|
order.push(index);
|
|
done();
|
|
},
|
|
{ onDropped: (bytes) => dropped.push(bytes) },
|
|
);
|
|
}
|
|
|
|
assert.equal(getTerminalWriteQueueDepth(term) < MAX_WRITE_QUEUE_ITEMS + 10, true);
|
|
assert.deepEqual(dropped, []);
|
|
releaseFirst?.();
|
|
await waitForQueuedWrites(MAX_WRITE_QUEUE_ITEMS + 10);
|
|
assert.deepEqual(order, Array.from({ length: MAX_WRITE_QUEUE_ITEMS + 10 }, (_, index) => index));
|
|
});
|
|
|
|
test("merged flood backlog still yields between synchronous write steps", async () => {
|
|
const term = createFakeTerm();
|
|
const order: number[] = [];
|
|
|
|
for (let index = 0; index < MAX_WRITE_QUEUE_ITEMS + 1; index += 1) {
|
|
enqueueTerminalWrite(
|
|
term,
|
|
10,
|
|
(done) => {
|
|
order.push(index);
|
|
done();
|
|
},
|
|
{ deferStart: true, yieldAfter: true },
|
|
);
|
|
}
|
|
|
|
assert.equal(getTerminalWriteQueueDepth(term), 1);
|
|
await waitForQueuedWriteYield();
|
|
assert.deepEqual(order, [0]);
|
|
await waitForQueuedWriteYield();
|
|
assert.deepEqual(order, [0, 1]);
|
|
});
|
|
|
|
test("merged flood of many tiny sync steps does not blow the call stack", async () => {
|
|
const term = createFakeTerm();
|
|
const order: number[] = [];
|
|
let releaseFirst: (() => void) | null = null;
|
|
const stepCount = 10_000;
|
|
|
|
enqueueTerminalWrite(term, 1, (done) => {
|
|
releaseFirst = done;
|
|
});
|
|
for (let index = 0; index < stepCount; index += 1) {
|
|
enqueueTerminalWrite(term, 1, (done) => {
|
|
order.push(index);
|
|
done();
|
|
});
|
|
}
|
|
|
|
assert.equal(isTerminalWriteQueueInFloodMode(term), true);
|
|
assert.doesNotThrow(() => {
|
|
releaseFirst?.();
|
|
});
|
|
// Cooperative yields every 32 sync hops — drain with timer flushes.
|
|
for (let guard = 0; guard < 500 && order.length < stepCount; guard += 1) {
|
|
await waitForQueuedWriteYield();
|
|
}
|
|
assert.equal(order.length, stepCount);
|
|
assert.equal(order[0], 0);
|
|
assert.equal(order[stepCount - 1], stepCount - 1);
|
|
});
|
|
|
|
test("merged flood backlog honors per-step yieldAfter, not every shard", async () => {
|
|
const term = createFakeTerm();
|
|
const order: number[] = [];
|
|
let releaseFirst: (() => void) | null = null;
|
|
|
|
// Hold the first write so later shards merge into one flood item.
|
|
enqueueTerminalWrite(term, 1, (done) => {
|
|
releaseFirst = done;
|
|
});
|
|
|
|
for (let index = 0; index < MAX_WRITE_QUEUE_ITEMS + 1; index += 1) {
|
|
// Only every 4th step asks to yield — merge must preserve that, not force
|
|
// a timer after each shard (writeLargeTerminalBatch drain-budget pacing).
|
|
const yieldAfter = index % 4 === 3;
|
|
enqueueTerminalWrite(
|
|
term,
|
|
10,
|
|
(done) => {
|
|
order.push(index);
|
|
done();
|
|
},
|
|
{ yieldAfter },
|
|
);
|
|
}
|
|
|
|
assert.equal(getTerminalWriteQueueDepth(term), 1);
|
|
assert.equal(isTerminalWriteQueueInFloodMode(term), true);
|
|
assert.deepEqual(order, []);
|
|
|
|
releaseFirst?.();
|
|
// First four shards run in one turn (yieldAfter only on index 3).
|
|
assert.deepEqual(order, [0, 1, 2, 3]);
|
|
await waitForQueuedWriteYield();
|
|
assert.deepEqual(order, [0, 1, 2, 3, 4, 5, 6, 7]);
|
|
});
|
|
|
|
test("flushTerminalWriteQueueBypassingTimers drains merged flood backlog without timer yields", () => {
|
|
const term = createFakeTerm();
|
|
const order: number[] = [];
|
|
|
|
for (let index = 0; index < MAX_WRITE_QUEUE_ITEMS + 1; index += 1) {
|
|
enqueueTerminalWrite(
|
|
term,
|
|
10,
|
|
(done) => {
|
|
order.push(index);
|
|
done();
|
|
},
|
|
{ deferStart: true, yieldAfter: true },
|
|
);
|
|
}
|
|
|
|
assert.equal(getTerminalWriteQueueDepth(term), 1);
|
|
assert.equal(flushTerminalWriteQueueBypassingTimers(term), true);
|
|
assert.deepEqual(order, Array.from({ length: MAX_WRITE_QUEUE_ITEMS + 1 }, (_, index) => index));
|
|
});
|
|
|
|
test("write queue yields when a drain reaches its byte budget", async () => {
|
|
const term = createFakeTerm();
|
|
const order: number[] = [];
|
|
let releaseFirst: (() => void) | null = null;
|
|
|
|
enqueueTerminalWrite(term, 0, (done) => {
|
|
releaseFirst = done;
|
|
});
|
|
|
|
for (let index = 0; index < 4; index += 1) {
|
|
enqueueTerminalWrite(
|
|
term,
|
|
10,
|
|
(done) => {
|
|
order.push(index);
|
|
done();
|
|
},
|
|
{ maxDrainBytes: 20 },
|
|
);
|
|
}
|
|
|
|
assert.deepEqual(order, []);
|
|
releaseFirst?.();
|
|
assert.deepEqual(order, [0, 1]);
|
|
await waitForQueuedWriteYield();
|
|
assert.deepEqual(order, [0, 1, 2, 3]);
|
|
});
|
|
|
|
test("abortTerminalWriteQueue drops pending bytes and reports dropped count", () => {
|
|
const term = createFakeTerm();
|
|
const dropped: number[] = [];
|
|
let started = false;
|
|
|
|
enqueueTerminalWrite(term, 40, () => {
|
|
started = true;
|
|
});
|
|
enqueueTerminalWrite(term, 60, () => {}, { onDropped: (bytes) => dropped.push(bytes) });
|
|
abortTerminalWriteQueue(term, (bytes) => dropped.push(bytes));
|
|
|
|
assert.equal(started, true);
|
|
assert.deepEqual(dropped, [60]);
|
|
});
|
|
|
|
test("abortTerminalWriteQueue reports ingress accounting instead of display scheduling bytes", () => {
|
|
const term = createFakeTerm();
|
|
const dropped: number[] = [];
|
|
|
|
enqueueTerminalWrite(term, 10, () => {});
|
|
enqueueTerminalWrite(term, 3, () => {}, {
|
|
dropBytes: 80,
|
|
onDropped: (bytes) => dropped.push(bytes),
|
|
});
|
|
abortTerminalWriteQueue(term);
|
|
|
|
assert.deepEqual(dropped, [80]);
|
|
});
|