+59
-148
@@ -5,17 +5,13 @@ import {
|
||||
VIEW_HIGH,
|
||||
VIEW_LOW,
|
||||
before,
|
||||
blocks,
|
||||
bytes,
|
||||
clean,
|
||||
compressTask,
|
||||
contains,
|
||||
grow,
|
||||
key,
|
||||
merge,
|
||||
mergeTask,
|
||||
pieces,
|
||||
render,
|
||||
tooLong,
|
||||
viewBytes,
|
||||
type Line,
|
||||
@@ -67,23 +63,7 @@ test("due ordering reproduces Taelin's push at its list length for t=0..2000", (
|
||||
}
|
||||
});
|
||||
|
||||
test("merging from the first message diverges from push (the recipe's T=10 example)", () => {
|
||||
const text = () => "";
|
||||
// View 0+4, 4+4, 8+1, 9+1 at T=10: push merges 8-9, not 0-7.
|
||||
const view: Line[] = [
|
||||
[2, 0],
|
||||
[2, 1],
|
||||
[0, 8],
|
||||
[0, 9],
|
||||
];
|
||||
expect(merge(view, 10, viewBytes(view, text) - 1, text)).toEqual([
|
||||
[2, 0],
|
||||
[2, 1],
|
||||
[1, 4],
|
||||
]);
|
||||
});
|
||||
|
||||
test("merge skips pairs whose parent is unbuilt", () => {
|
||||
test("merges and compaction context never use unbuilt lines", () => {
|
||||
const built = new Set(["1:0"]);
|
||||
const text = (line: Line) => (line[0] === 0 || built.has(key(line)) ? "" : undefined);
|
||||
const view: Line[] = [
|
||||
@@ -97,98 +77,74 @@ test("merge skips pairs whose parent is unbuilt", () => {
|
||||
[0, 2],
|
||||
[0, 3],
|
||||
]);
|
||||
});
|
||||
|
||||
test("view is a sawtooth: grows past 128 KB, then one batch drops it to 64 KB", () => {
|
||||
const line = "y".repeat(500);
|
||||
const text = () => line;
|
||||
const view: View = { lines: [], batching: false };
|
||||
let peak = 0;
|
||||
let batches = 0;
|
||||
let total = 0;
|
||||
for (let i = 0; i < 2000; i++) {
|
||||
const size = viewBytes(view.lines, text);
|
||||
expect(total).toBe(size);
|
||||
peak = Math.max(peak, size);
|
||||
const grown = grow(view, i, text, total, true);
|
||||
total = grown.total;
|
||||
if (grown.merged) {
|
||||
batches++;
|
||||
expect(size).toBeLessThanOrEqual(VIEW_HIGH);
|
||||
expect(viewBytes(view.lines, text)).toBeLessThanOrEqual(VIEW_LOW);
|
||||
} else expect(view.lines.at(-1)).toEqual([0, i]);
|
||||
}
|
||||
expect(peak).toBeGreaterThan(VIEW_LOW);
|
||||
expect(peak).toBeLessThanOrEqual(VIEW_HIGH + 600);
|
||||
expect(batches).toBeGreaterThan(1);
|
||||
// Lines stay contiguous and cover every message.
|
||||
let next = 0;
|
||||
for (const [l, i] of view.lines) {
|
||||
expect(i * 2 ** l).toBe(next);
|
||||
next += 2 ** l;
|
||||
}
|
||||
expect(next).toBe(2000);
|
||||
});
|
||||
|
||||
test("a batch that cannot reach 64 KB continues at the next message", () => {
|
||||
let parents = false;
|
||||
const text = (line: Line) => (line[0] === 0 || parents ? "z".repeat(500) : undefined);
|
||||
const view: View = { lines: [], batching: false };
|
||||
let total = 0;
|
||||
for (let i = 0; i < 260; i++) {
|
||||
const grown = grow(view, i, text, total, true);
|
||||
expect(grown.merged).toBe(false);
|
||||
total = grown.total;
|
||||
}
|
||||
expect(view.batching).toBe(true);
|
||||
parents = true;
|
||||
// Nothing was built since the last try: the batch waits.
|
||||
const waiting = grow(view, 260, text, total, false);
|
||||
expect(waiting).toEqual({ merged: false, tried: false, total: viewBytes(view.lines, text) });
|
||||
expect(grow(view, 261, text, waiting.total, true).merged).toBe(true);
|
||||
expect(viewBytes(view.lines, text)).toBeLessThanOrEqual(VIEW_LOW);
|
||||
expect(view.batching).toBe(false);
|
||||
});
|
||||
|
||||
test("compaction context stops before the target and at the first unbuilt line", () => {
|
||||
const text = (line: Line) => (key(line) === "0:5" ? undefined : "t");
|
||||
const view: Line[] = [
|
||||
const context = (line: Line) => (key(line) === "0:5" ? undefined : "t");
|
||||
const zoomed: Line[] = [
|
||||
[2, 0],
|
||||
[0, 4],
|
||||
[0, 5],
|
||||
[0, 6],
|
||||
];
|
||||
expect(before(view, 4, text)).toEqual([[2, 0]]);
|
||||
expect(before(view, 7, text)).toEqual([
|
||||
expect(before(zoomed, 4, context)).toEqual([[2, 0]]);
|
||||
expect(before(zoomed, 7, context)).toEqual([
|
||||
[2, 0],
|
||||
[0, 4],
|
||||
]);
|
||||
});
|
||||
|
||||
test("view blocks keep whole 4-line blocks stable as lines are appended", () => {
|
||||
const a = blocks(["1", "2", "3", "4", "5", "6"]);
|
||||
expect(a.whole).toEqual(["<chat>\n1\n2\n3\n4\n"]);
|
||||
expect(a.tail).toBe("5\n6\n</chat>");
|
||||
const b = blocks(["1", "2", "3", "4", "5", "6", "7", "8", "9"]);
|
||||
expect(b.whole[0]).toBe(a.whole[0]!);
|
||||
expect(b.tail).toBe("9\n</chat>");
|
||||
expect(blocks([])).toEqual({ whole: [], tail: "<chat>\n</chat>" });
|
||||
});
|
||||
|
||||
test("lines render with labels and without newlines", () => {
|
||||
expect(render([3, 5], "a\nb")).toBe("40+8|a b");
|
||||
expect(render([0, 7], undefined)).toBe("7+1|(not summarized yet: zoom it)");
|
||||
});
|
||||
|
||||
test("long tool outputs are clipped; other long texts are split", () => {
|
||||
const long = "a".repeat(20_000) + "b".repeat(20_000);
|
||||
const [echo] = pieces("echo", long);
|
||||
expect(echo!.startsWith("a".repeat(15_000))).toBe(true);
|
||||
expect(echo!.endsWith("b".repeat(15_000))).toBe(true);
|
||||
expect(echo!).toContain("10000 characters clipped");
|
||||
expect(pieces("agent", long).join("")).toBe(long);
|
||||
expect(pieces("agent", long)).toHaveLength(2);
|
||||
expect(pieces("user", "short")).toEqual(["short"]);
|
||||
test("view is a sawtooth: grows past 128 KB, one batch drops it to 64 KB, and a short batch continues", () => {
|
||||
{
|
||||
const line = "y".repeat(500);
|
||||
const text = () => line;
|
||||
const view: View = { lines: [], batching: false };
|
||||
let peak = 0;
|
||||
let batches = 0;
|
||||
let total = 0;
|
||||
for (let i = 0; i < 2000; i++) {
|
||||
const size = viewBytes(view.lines, text);
|
||||
expect(total).toBe(size);
|
||||
peak = Math.max(peak, size);
|
||||
const grown = grow(view, i, text, total, true);
|
||||
total = grown.total;
|
||||
if (grown.merged) {
|
||||
batches++;
|
||||
expect(size).toBeLessThanOrEqual(VIEW_HIGH);
|
||||
expect(viewBytes(view.lines, text)).toBeLessThanOrEqual(VIEW_LOW);
|
||||
} else expect(view.lines.at(-1)).toEqual([0, i]);
|
||||
}
|
||||
expect(peak).toBeGreaterThan(VIEW_LOW);
|
||||
expect(peak).toBeLessThanOrEqual(VIEW_HIGH + 600);
|
||||
expect(batches).toBeGreaterThan(1);
|
||||
// Lines stay contiguous and cover every message.
|
||||
let next = 0;
|
||||
for (const [l, i] of view.lines) {
|
||||
expect(i * 2 ** l).toBe(next);
|
||||
next += 2 ** l;
|
||||
}
|
||||
expect(next).toBe(2000);
|
||||
}
|
||||
{
|
||||
let parents = false;
|
||||
const text = (line: Line) => (line[0] === 0 || parents ? "z".repeat(500) : undefined);
|
||||
const view: View = { lines: [], batching: false };
|
||||
let total = 0;
|
||||
for (let i = 0; i < 260; i++) {
|
||||
const grown = grow(view, i, text, total, true);
|
||||
expect(grown.merged).toBe(false);
|
||||
total = grown.total;
|
||||
}
|
||||
expect(view.batching).toBe(true);
|
||||
parents = true;
|
||||
// Nothing was built since the last try: the batch waits.
|
||||
const waiting = grow(view, 260, text, total, false);
|
||||
expect(waiting).toEqual({
|
||||
merged: false,
|
||||
tried: false,
|
||||
total: viewBytes(view.lines, text),
|
||||
});
|
||||
expect(grow(view, 261, text, waiting.total, true).merged).toBe(true);
|
||||
expect(viewBytes(view.lines, text)).toBeLessThanOrEqual(VIEW_LOW);
|
||||
expect(view.batching).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test("tasks are verbatim and Too long cuts at 512 bytes", () => {
|
||||
@@ -272,35 +228,6 @@ test("queue resumes unbuilt messages and ready merges from disk", async () => {
|
||||
expect(started).toEqual(["0:3", "1:0", "1:1", "2:0"]);
|
||||
});
|
||||
|
||||
test("a long unbuilt view grows in linear time and pending batches wait for builds", () => {
|
||||
// Regression: enabling optchat in a long session re-measured and re-merged the whole view per message.
|
||||
const text = () => undefined;
|
||||
const view: View = { lines: [], batching: false };
|
||||
let total = 0;
|
||||
let built = true;
|
||||
const started = performance.now();
|
||||
for (let i = 0; i < 20_000; i++) {
|
||||
const grown = grow(view, i, text, total, built);
|
||||
total = grown.total;
|
||||
if (grown.tried) built = false;
|
||||
}
|
||||
expect(total).toBe(viewBytes(view.lines, text));
|
||||
expect(view.batching).toBe(true);
|
||||
expect(performance.now() - started).toBeLessThan(2_000);
|
||||
});
|
||||
|
||||
test("contains finds exactly the view's lines", () => {
|
||||
const view: Line[] = [
|
||||
[2, 0],
|
||||
[1, 2],
|
||||
[0, 6],
|
||||
];
|
||||
expect(contains(view, [1, 2])).toBe(true);
|
||||
expect(contains(view, [0, 6])).toBe(true);
|
||||
expect(contains(view, [0, 4])).toBe(false);
|
||||
expect(contains(view, [0, 7])).toBe(false);
|
||||
});
|
||||
|
||||
test("a merge is queued once when both halves finish in the same tick", async () => {
|
||||
// Regression: short leaves build synchronously, so both finishes saw the parent unbuilt.
|
||||
const tree = new Set<string>();
|
||||
@@ -319,19 +246,3 @@ test("a merge is queued once when both halves finish in the same tick", async ()
|
||||
await Bun.sleep(30);
|
||||
expect(starts).toEqual(["0:0", "0:1", "1:0"]);
|
||||
});
|
||||
|
||||
test("settle stops waiting once failures leave nothing running", async () => {
|
||||
// Regression: it waited the full timeout though failures retry only at the next message.
|
||||
const queue = new Queue(
|
||||
() => false,
|
||||
async () => {
|
||||
await Bun.sleep(5);
|
||||
return false;
|
||||
},
|
||||
);
|
||||
queue.add(0);
|
||||
const started = Date.now();
|
||||
expect(await queue.settle(1, 2_000)).toBe(false);
|
||||
expect(Date.now() - started).toBeLessThan(500);
|
||||
expect(queue.running).toBe(0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user