* perf(timeline): resume cached history from its exact range Warm launches were repainting cached history and then downloading the same bounded tail to rediscover that nothing changed. Persist synchronization authority only when the complete current canonical window can be encoded losslessly; otherwise retain the existing display-only fallback. * test(timeline): hide replica setup behind E2E helper * fix(timeline): bound persisted resume overflow * test(timeline): cover workspace eviction catch-up order * test(timeline): read persisted range from IndexedDB
292 lines
11 KiB
TypeScript
292 lines
11 KiB
TypeScript
import { expect, test } from "../support/fixtures";
|
|
import {
|
|
expectSameOlderHistoryLoadingOperation,
|
|
expectStableHistoryStartGutter,
|
|
expectTimelineAtHistoryStart,
|
|
expectTimelinePromptCentered,
|
|
expectTimelinePromptNotMounted,
|
|
expectTimelinePromptPositionPreserved,
|
|
expectTimelinePromptVisible,
|
|
holdBootstrapTimelinePage,
|
|
holdDaemonHydration,
|
|
holdOlderHistoryPages,
|
|
makeLoadedTimelineFitViewport,
|
|
openAgentTimeline,
|
|
rememberOlderHistoryLoadingOperation,
|
|
rememberTimelineViewport,
|
|
rememberTimelinePromptPosition,
|
|
reloadAgentTimelineFromPersistedReplica,
|
|
scrollTimelineUntilOlderHistoryIsReachable,
|
|
scrollTimelineToOldestLoadedEdge,
|
|
scrollTimelineToNewestLoadedEdge,
|
|
seedLongMockAgentTimeline,
|
|
scrollThroughOlderHistoryPages,
|
|
seedRealisticOlderTimeline,
|
|
sendLiveTurnBeforeHydration,
|
|
expectTimelineViewportAnchoredAfterPrepend,
|
|
userNavigatesTimelineToHistoryStartWithKeyboard,
|
|
userScrollsTimelineToHistoryStart,
|
|
waitForPersistedCanonicalTimelineRange,
|
|
} from "../support/helpers/timeline-pagination";
|
|
import { trackAgentTimelineRequests } from "../support/helpers/agent-timeline-gate";
|
|
import { seedMockAgentWorkspace } from "../support/helpers/mock-agent";
|
|
|
|
test.describe("Agent timeline pagination", () => {
|
|
test("resumes a persisted canonical timeline after its exact end", async ({ page }) => {
|
|
const prompt = "timeline persisted range resumes without tail replay";
|
|
const agent = await seedMockAgentWorkspace({
|
|
repoPrefix: "timeline-persisted-range-",
|
|
title: "Persisted timeline range",
|
|
initialPrompt: prompt,
|
|
});
|
|
try {
|
|
await agent.client.waitForFinish(agent.agentId, 15_000);
|
|
await openAgentTimeline(page, agent);
|
|
await expectTimelinePromptVisible(page, prompt);
|
|
const range = await waitForPersistedCanonicalTimelineRange(page, agent.agentId);
|
|
|
|
const tracker = await trackAgentTimelineRequests(page, agent.agentId);
|
|
await page.reload();
|
|
await expectTimelinePromptVisible(page, prompt);
|
|
|
|
const request = await tracker.nextRequest();
|
|
expect(request).toEqual({
|
|
direction: "after",
|
|
cursor: { epoch: range.epoch, seq: range.endSeq },
|
|
});
|
|
await tracker.waitForResponse();
|
|
expect(tracker.requests().some((entry) => entry.direction === "tail")).toBe(false);
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|
|
|
|
test("keeps a fixed history-start gutter before, during, and after pagination", async ({
|
|
page,
|
|
}) => {
|
|
const agent = await seedLongMockAgentTimeline({ turns: 40 });
|
|
try {
|
|
const history = await holdOlderHistoryPages(page, agent);
|
|
await openAgentTimeline(page, agent);
|
|
await expectStableHistoryStartGutter(page);
|
|
|
|
await userScrollsTimelineToHistoryStart(page);
|
|
await history.expectRequestedPages(1);
|
|
await expectStableHistoryStartGutter(page);
|
|
|
|
history.releasePage(1);
|
|
await history.expectSettledWithRequestedPages(1);
|
|
await expectStableHistoryStartGutter(page);
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|
|
|
|
test("loads one page each time the user returns to history start", async ({ page }) => {
|
|
test.setTimeout(120_000);
|
|
const agent = await seedLongMockAgentTimeline({ turns: 80 });
|
|
try {
|
|
const history = await holdOlderHistoryPages(page, agent);
|
|
await openAgentTimeline(page, agent);
|
|
await expectTimelinePromptVisible(page, agent.newestPrompt);
|
|
await expectTimelinePromptCentered(page, agent.newestPrompt);
|
|
await expectTimelinePromptNotMounted(page, agent.oldestPrompt);
|
|
|
|
await userScrollsTimelineToHistoryStart(page);
|
|
await history.expectRequestedPages(1);
|
|
history.releasePage(1);
|
|
await history.expectSettledWithRequestedPages(1);
|
|
await expectTimelinePromptCentered(page, agent.firstOlderPagePrompt);
|
|
|
|
await userScrollsTimelineToHistoryStart(page);
|
|
await history.expectRequestedPages(2);
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|
|
|
|
test("loads older history when the user navigates to history start with the keyboard", async ({
|
|
page,
|
|
}) => {
|
|
const agent = await seedLongMockAgentTimeline({ turns: 40 });
|
|
try {
|
|
const history = await holdOlderHistoryPages(page, agent);
|
|
await openAgentTimeline(page, agent);
|
|
|
|
await userNavigatesTimelineToHistoryStartWithKeyboard(page);
|
|
await history.expectRequestedPages(1);
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|
|
|
|
test("does not repeat an assistant block that spans older history pages", async ({ page }) => {
|
|
test.setTimeout(120_000);
|
|
const agent = await seedRealisticOlderTimeline();
|
|
try {
|
|
const history = await holdOlderHistoryPages(page, agent);
|
|
await openAgentTimeline(page, agent);
|
|
await expectTimelinePromptVisible(page, agent.newestPrompt);
|
|
|
|
await scrollThroughOlderHistoryPages(page, 3, history);
|
|
history.expectNoRepeatedEntries();
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|
|
|
|
test("keeps the visible timeline position anchored while prepending a page", async ({ page }) => {
|
|
test.setTimeout(120_000);
|
|
const agent = await seedLongMockAgentTimeline({ turns: 80 });
|
|
try {
|
|
const history = await holdOlderHistoryPages(page, agent);
|
|
await openAgentTimeline(page, agent);
|
|
await userScrollsTimelineToHistoryStart(page);
|
|
await history.expectRequestedPages(1);
|
|
const viewport = await rememberTimelineViewport(page);
|
|
|
|
history.releasePage(1);
|
|
await expectTimelineViewportAnchoredAfterPrepend(page, viewport);
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|
|
|
|
test("keeps visible history anchored when live output grows during a prepend", async ({
|
|
page,
|
|
}) => {
|
|
test.setTimeout(120_000);
|
|
const agent = await seedLongMockAgentTimeline({ turns: 80 });
|
|
try {
|
|
const history = await holdOlderHistoryPages(page, agent);
|
|
await openAgentTimeline(page, agent);
|
|
await userScrollsTimelineToHistoryStart(page);
|
|
await history.expectRequestedPages(1);
|
|
const position = await rememberTimelinePromptPosition(page, agent.initialTailOldestPrompt);
|
|
|
|
await agent.client.sendAgentMessage(
|
|
agent.agentId,
|
|
"timeline live during held older page: emit 20 coalesced agent stream updates",
|
|
);
|
|
await agent.client.waitForFinish(agent.agentId, 15_000);
|
|
history.releasePage(1);
|
|
|
|
await expectTimelinePromptPositionPreserved(page, position);
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|
|
|
|
test("finishes loading an older page while live output continues", async ({ page }) => {
|
|
test.setTimeout(120_000);
|
|
const agent = await seedLongMockAgentTimeline({ turns: 40 });
|
|
try {
|
|
const history = await holdOlderHistoryPages(page, agent);
|
|
await openAgentTimeline(page, agent);
|
|
await userScrollsTimelineToHistoryStart(page);
|
|
await history.expectRequestedPages(1);
|
|
|
|
await agent.client.sendAgentMessage(agent.agentId, "keep streaming while history settles");
|
|
await agent.client.waitForAgentUpsert(
|
|
agent.agentId,
|
|
(snapshot) => snapshot.status === "running",
|
|
);
|
|
history.releasePage(1);
|
|
|
|
await expect(page.getByTestId("load-older-history-spinner")).toBeHidden({ timeout: 5_000 });
|
|
const running = await agent.client.fetchAgents({ scope: "active" });
|
|
expect(running.entries.find((entry) => entry.agent.id === agent.agentId)?.agent.status).toBe(
|
|
"running",
|
|
);
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|
|
|
|
test("keeps the visible timeline anchored when the final page finishes", async ({ page }) => {
|
|
test.setTimeout(120_000);
|
|
const agent = await seedLongMockAgentTimeline({ turns: 40 });
|
|
try {
|
|
const history = await holdOlderHistoryPages(page, agent);
|
|
await openAgentTimeline(page, agent);
|
|
await userScrollsTimelineToHistoryStart(page);
|
|
await history.expectRequestedPages(1);
|
|
const position = await rememberTimelinePromptPosition(page, agent.initialTailOldestPrompt);
|
|
|
|
history.releasePage(1);
|
|
|
|
await expectTimelinePromptPositionPreserved(page, position);
|
|
await history.expectSettledWithRequestedPages(1);
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|
|
|
|
test("continues one loading operation while older pages still leave history start exposed", async ({
|
|
page,
|
|
}) => {
|
|
test.setTimeout(120_000);
|
|
const agent = await seedLongMockAgentTimeline({ turns: 80 });
|
|
try {
|
|
await makeLoadedTimelineFitViewport(page);
|
|
const history = await holdOlderHistoryPages(page, agent);
|
|
await openAgentTimeline(page, agent);
|
|
await scrollTimelineToOldestLoadedEdge(page);
|
|
await history.expectRequestedPages(1);
|
|
const loading = await rememberOlderHistoryLoadingOperation(page);
|
|
|
|
history.releasePage(1);
|
|
await history.expectRequestedPages(2);
|
|
await expectTimelineAtHistoryStart(page);
|
|
await expectSameOlderHistoryLoadingOperation(page, loading);
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|
|
|
|
test("keeps complete loaded history reachable after reload", async ({ page }) => {
|
|
test.setTimeout(120_000);
|
|
const agent = await seedLongMockAgentTimeline({ turns: 80 });
|
|
try {
|
|
await openAgentTimeline(page, agent);
|
|
await scrollTimelineUntilOlderHistoryIsReachable(page, agent.oldestPrompt);
|
|
await expectTimelinePromptVisible(page, agent.oldestPrompt);
|
|
|
|
const hydration = await holdDaemonHydration(page);
|
|
await reloadAgentTimelineFromPersistedReplica(page, agent);
|
|
hydration.release();
|
|
await scrollTimelineUntilOlderHistoryIsReachable(page, agent.oldestPrompt);
|
|
|
|
await expectTimelinePromptVisible(page, agent.oldestPrompt);
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|
|
|
|
test("preserves a live row received before replica hydration", async ({ page }) => {
|
|
test.setTimeout(120_000);
|
|
const agent = await seedLongMockAgentTimeline({ turns: 80 });
|
|
try {
|
|
await openAgentTimeline(page, agent);
|
|
await scrollTimelineUntilOlderHistoryIsReachable(page, agent.oldestPrompt);
|
|
|
|
const hydration = await holdBootstrapTimelinePage(page, agent);
|
|
await reloadAgentTimelineFromPersistedReplica(page, agent);
|
|
await hydration.waitForDelayedResponse();
|
|
const livePrompt = await sendLiveTurnBeforeHydration(agent);
|
|
await expectTimelinePromptVisible(page, livePrompt);
|
|
|
|
hydration.release();
|
|
await hydration.waitForDelayedCatchUp();
|
|
await expectTimelinePromptVisible(page, livePrompt);
|
|
hydration.releaseCatchUp();
|
|
await scrollTimelineUntilOlderHistoryIsReachable(page, agent.oldestPrompt);
|
|
await expectTimelinePromptVisible(page, agent.oldestPrompt);
|
|
await scrollTimelineToNewestLoadedEdge(page);
|
|
await expectTimelinePromptVisible(page, livePrompt);
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|
|
});
|