Files
paseo/packages/server/src/server/agent/agent-timeline-store.test.ts
T
paseo-bot[bot] 1aef1ec662 test(server): isolate timeline fetch responsiveness from runner work (#8197)
* test(server): isolate timeline fetch responsiveness from runner work

Measure the existing cold tail workload in a separate process after fixture collection. Keep the 50ms gate and row assertions, and reproduce unrelated callback CPU contaminating the old measurement.

* test(server): keep timeline probe private and exercise overlapping CPU

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-29 12:21:29 +00:00

430 lines
14 KiB
TypeScript

import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { selectReferenceProjectedTimelinePage } from "./test-utils/timeline-page-reference.js";
import type { AgentTimelineItem } from "./agent-sdk-types.js";
import { InMemoryAgentTimelineStore } from "./agent-timeline-store.js";
describe("InMemoryAgentTimelineStore", () => {
it("resets stale forward cursors to complete available turns", () => {
const store = new InMemoryAgentTimelineStore();
store.initialize("agent", { epoch: "current" });
for (let index = 0; index < 60; index++)
store.append(
"agent",
{
type: "tool_call",
callId: `call-${index}`,
name: "shell",
status: "completed",
error: null,
detail: { type: "unknown", input: null, output: null },
},
{ turnId: "run" },
);
const page = store.fetch("agent", {
direction: "after",
cursor: { epoch: "old", seq: 10 },
limit: 40,
wholeTurns: true,
});
expect(page.rows).toHaveLength(60);
expect(page).toMatchObject({
reset: true,
staleCursor: true,
startSeq: 1,
endSeq: 60,
hasOlder: false,
hasNewer: false,
});
});
it("clamps an overshooting before cursor into the bounded tail window", () => {
const store = new InMemoryAgentTimelineStore();
store.initialize("agent-1", {
epoch: "epoch-1",
nextSeq: 8,
rows: [
{
seq: 5,
timestamp: "2026-01-01T00:00:00.000Z",
item: { type: "assistant_message", text: "five", messageId: "five" },
},
{
seq: 6,
timestamp: "2026-01-01T00:00:01.000Z",
item: { type: "assistant_message", text: "six", messageId: "six" },
},
{
seq: 7,
timestamp: "2026-01-01T00:00:02.000Z",
item: { type: "assistant_message", text: "seven", messageId: "seven" },
},
],
});
const result = store.fetch("agent-1", {
direction: "before",
cursor: { epoch: "epoch-1", seq: 100 },
limit: 2,
});
expect(result).toMatchObject({
epoch: "epoch-1",
direction: "before",
reset: false,
staleCursor: false,
gap: false,
window: { minSeq: 5, maxSeq: 7, nextSeq: 8 },
hasOlder: true,
hasNewer: false,
rows: [
{
seq: 6,
timestamp: "2026-01-01T00:00:01.000Z",
item: { type: "assistant_message", text: "six", messageId: "six" },
},
{
seq: 7,
timestamp: "2026-01-01T00:00:02.000Z",
item: { type: "assistant_message", text: "seven", messageId: "seven" },
},
],
});
});
it("returns a bounded reset window when an after cursor is behind retained history", () => {
const store = new InMemoryAgentTimelineStore();
store.initialize("agent-1", {
epoch: "epoch-1",
nextSeq: 8,
rows: [
{
seq: 5,
timestamp: "2026-01-01T00:00:00.000Z",
item: { type: "assistant_message", text: "five", messageId: "five" },
},
{
seq: 6,
timestamp: "2026-01-01T00:00:01.000Z",
item: { type: "assistant_message", text: "six", messageId: "six" },
},
{
seq: 7,
timestamp: "2026-01-01T00:00:02.000Z",
item: { type: "assistant_message", text: "seven", messageId: "seven" },
},
],
});
const result = store.fetch("agent-1", {
direction: "after",
cursor: { epoch: "epoch-1", seq: 1 },
limit: 1,
});
expect(result).toMatchObject({
epoch: "epoch-1",
direction: "after",
reset: true,
staleCursor: false,
gap: true,
window: { minSeq: 5, maxSeq: 7, nextSeq: 8 },
hasOlder: true,
hasNewer: false,
rows: [
{
seq: 7,
timestamp: "2026-01-01T00:00:02.000Z",
item: { type: "assistant_message", text: "seven", messageId: "seven" },
},
],
});
});
});
describe("projected timeline retention", () => {
it("retains one full tool state while streaming every source update", () => {
const store = new InMemoryAgentTimelineStore();
store.initialize("agent");
for (let seq = 1; seq <= 2000; seq++) {
const item = {
type: "tool_call" as const,
callId: "child",
name: "task",
status: "running" as const,
error: null,
detail: { type: "plain_text" as const, label: "Child", text: "x".repeat(seq * 128) },
};
expect(store.append("agent", item)).toMatchObject({ seq, item });
}
const result = store.fetch("agent", { limit: 0 });
expect(result.rows).toHaveLength(1);
expect(JSON.stringify(result.rows).length).toBeLessThan(260_000);
expect(result.window.maxSeq).toBe(2000);
});
it("catches up a mid-message cursor with the complete projected message", () => {
const store = new InMemoryAgentTimelineStore();
store.initialize("agent", { epoch: "e" });
store.append("agent", { type: "assistant_message", messageId: "m", text: "A" });
store.append("agent", { type: "assistant_message", messageId: "m", text: "B" });
expect(
store.fetch("agent", { direction: "after", cursor: { epoch: "e", seq: 1 } }).rows,
).toMatchObject([
{
item: { text: "AB" },
seqStart: 1,
seqEnd: 2,
sourceSeqRanges: [{ startSeq: 1, endSeq: 2 }],
},
]);
});
});
describe("projected sequence ownership", () => {
it("preserves fetched coverage when another source chunk arrives", () => {
const store = new InMemoryAgentTimelineStore();
store.initialize("a");
store.append("a", { type: "assistant_message", text: "A" });
const before = store.fetch("a");
store.append("a", { type: "assistant_message", text: "B" });
expect(before.rows[0]).toMatchObject({
item: { text: "A" },
seqEnd: 1,
sourceSeqRanges: [{ startSeq: 1, endSeq: 1 }],
});
expect(store.fetch("a").rows[0]).toMatchObject({
item: { text: "AB" },
seqEnd: 2,
sourceSeqRanges: [{ startSeq: 1, endSeq: 2 }],
});
});
it("preserves source positions when seeding a projected history whose last update is an earlier tool", () => {
const store = new InMemoryAgentTimelineStore();
store.initialize("a");
const tool = {
type: "tool_call" as const,
callId: "t",
name: "shell",
error: null,
detail: { type: "plain_text" as const, label: "work" },
};
store.append("a", { ...tool, status: "running" });
store.append("a", { type: "assistant_message", text: "Answer" });
store.append("a", { ...tool, status: "completed" });
store.initialize("b", { rows: store.getRows("a") });
expect(store.getLastItem("b")).toMatchObject({ type: "tool_call", status: "completed" });
expect(store.getLastAssistantMessage("b")).toBe("Answer");
expect(store.append("b", { type: "user_message", text: "next" }).seq).toBe(4);
expect(store.getLastItem("b")).toEqual({ type: "user_message", text: "next" });
expect(store.fetch("b").rows.map((row) => row.seqStart)).toEqual([1, 2, 4]);
});
it("returns the last projected assistant message without joining different messages", () => {
const store = new InMemoryAgentTimelineStore();
store.initialize("a");
store.append("a", { type: "assistant_message", messageId: "first", text: "First" });
store.append("a", { type: "assistant_message", messageId: "second", text: "Sec" });
store.append("a", { type: "assistant_message", messageId: "second", text: "ond" });
expect(store.getLastAssistantMessage("a")).toBe("Second");
});
});
it("includes transitive tool updates in a contiguous projected tail", () => {
const store = new InMemoryAgentTimelineStore();
store.initialize("a");
const tool = (callId: string, status: "running" | "completed") => ({
type: "tool_call" as const,
callId,
name: "shell",
status,
error: null,
detail: { type: "plain_text" as const, label: "work" },
});
store.append("a", tool("first", "running"));
store.append("a", tool("second", "running"));
store.append("a", { type: "user_message", text: "Continue" });
store.append("a", tool("first", "completed"));
store.append("a", { type: "assistant_message", text: "Answer" });
store.append("a", tool("second", "completed"));
const tail = store.fetch("a", { limit: 1 });
expect(tail.rows.map((row) => row.seqStart)).toEqual([1, 2, 3, 5]);
expect(tail).toMatchObject({ startSeq: 1, endSeq: 6, hasOlder: false });
});
async function measureTimelineTail(wholeTurns: boolean) {
const fixture = fileURLToPath(
new URL("./test-utils/timeline-responsiveness.ts", import.meta.url),
);
const { stdout, stderr } = await promisify(execFile)(
process.execPath,
["--expose-gc", "--import", "tsx", fixture, String(wholeTurns)],
{ timeout: 15_000 },
);
process.stderr.write(stderr);
return JSON.parse(stdout) as {
result: ReturnType<InMemoryAgentTimelineStore["fetch"]>;
blockingMs: number;
};
}
it("excludes unrelated test-runner CPU from the timeline fetch budget", async () => {
let unrelatedWork: ReturnType<typeof setImmediate>;
const burnCpu = () => {
const threadCpuUsage: (previous?: NodeJS.CpuUsage) => NodeJS.CpuUsage = Reflect.get(
process,
"threadCpuUsage",
);
const start = threadCpuUsage();
let elapsed: NodeJS.CpuUsage;
do {
elapsed = threadCpuUsage(start);
} while (elapsed.user + elapsed.system < 80_000);
unrelatedWork = setImmediate(burnCpu);
};
unrelatedWork = setImmediate(burnCpu);
try {
const { result: tail, blockingMs } = await measureTimelineTail(false);
expect(tail.rows).toHaveLength(200);
expect(tail.startSeq).toBe(99_801);
expect(tail.endSeq).toBe(100_000);
expect(blockingMs).toBeLessThan(50);
} finally {
clearImmediate(unrelatedWork);
}
});
it.each([false, true])(
"serves tail 200 from 100,000 retained rows without stalling the loop (wholeTurns=%s)",
async (wholeTurns) => {
const { result: tail, blockingMs } = await measureTimelineTail(wholeTurns);
expect(tail.rows).toHaveLength(200);
expect(tail.startSeq).toBe(99_801);
expect(tail.endSeq).toBe(100_000);
expect(blockingMs).toBeLessThan(50);
},
);
it("matches the previous reprojecting selector on seeded randomized timelines", () => {
let seed = 0x76aabbcc;
const random = (bound: number) => {
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0;
return Math.floor((seed / 0x1_0000_0000) * bound);
};
for (let scenario = 0; scenario < 100; scenario++) {
const store = new InMemoryAgentTimelineStore();
store.initialize("a", { epoch: "e" });
for (let seq = 1; seq <= 100; seq++) {
const identity = String(random(8));
const text = String(seq);
const items: AgentTimelineItem[] = [
{ type: "user_message", text },
{ type: "assistant_message", text },
{ type: "assistant_message", text, messageId: identity },
{ type: "reasoning", text },
{
type: "tool_call",
callId: identity,
name: "shell",
status: "running",
error: null,
detail: { type: "plain_text", label: text },
},
{
type: "plugin",
id: identity,
pluginId: "test",
kind: "state",
version: 1,
data: { text },
},
];
store.append("a", items[random(items.length)], { turnId: String(random(3)) });
}
// Include rehydration of already-projected rows, with non-monotonic seqEnd.
store.initialize("b", { rows: store.getRows("a"), epoch: "e" });
for (const agentId of ["a", "b"]) {
assertSelectionMatchesReference(store, agentId);
store.append(agentId, { type: "assistant_message", text: "later" });
assertSelectionMatchesReference(store, agentId);
}
}
});
function assertSelectionMatchesReference(store: InMemoryAgentTimelineStore, agentId: string): void {
for (const direction of ["tail", "before", "after"] as const) {
for (const limit of [0, 1, 7, 200]) {
for (const cursorSeq of [0, 1, 25, 50, 99, 100, 150]) {
const actual = store.fetch(agentId, {
direction,
limit,
cursor: { epoch: "e", seq: cursorSeq },
});
const expected = selectReferenceProjectedTimelinePage({
rows: store.getRows(agentId),
bounds: actual.window,
direction,
limit,
cursorSeq,
});
expect({
...expected,
entries: undefined,
rows: expected.entries.map((entry) => Object.assign({ seq: entry.seqEnd }, entry)),
}).toEqual({
entries: undefined,
rows: actual.rows,
startSeq: actual.startSeq,
endSeq: actual.endSeq,
hasOlder: actual.hasOlder,
hasNewer: actual.hasNewer,
});
}
}
}
}
it("continues a scroll across appends and lifecycle collapse without losing source coverage", () => {
const store = new InMemoryAgentTimelineStore();
store.initialize("a", { epoch: "e" });
const tool: AgentTimelineItem = {
type: "tool_call",
callId: "t",
name: "shell",
status: "running",
error: null,
detail: { type: "plain_text", label: "work" },
};
store.append("a", tool);
for (let seq = 2; seq <= 5; seq++) store.append("a", { type: "user_message", text: String(seq) });
const first = store.fetch("a", { direction: "after", cursor: { epoch: "e", seq: 0 }, limit: 2 });
expect(first.endSeq).toBe(2);
store.append("a", { ...tool, status: "completed" });
store.append("a", { type: "assistant_message", text: "Done" });
expect(first.rows[0].item).toMatchObject({ status: "running" });
const second = store.fetch("a", {
direction: "after",
cursor: { epoch: "e", seq: first.endSeq! },
limit: 2,
});
expect(second.rows.map((row) => row.seqStart)).toEqual([3, 4]);
expect(second.endSeq).toBe(4);
const third = store.fetch("a", {
direction: "after",
cursor: { epoch: "e", seq: second.endSeq! },
limit: 2,
});
expect(third.rows.map((row) => row.seqStart)).toEqual([1, 5]);
expect(third.rows[0].item).toMatchObject({ status: "completed" });
expect(third.endSeq).toBe(6);
const last = store.fetch("a", {
direction: "after",
cursor: { epoch: "e", seq: third.endSeq! },
limit: 2,
});
expect(last.rows.map((row) => row.item)).toEqual([{ type: "assistant_message", text: "Done" }]);
expect(last.endSeq).toBe(7);
expect(last.hasNewer).toBe(false);
});