Files
paseo/packages/app/e2e/agent-timeline-pagination.spec.ts
T
Mohamed BoudraandGitHub 504b687f89 Keep older chat history and image previews stable (#2490)
* fix(app): keep timeline history and previews stable

Treat persisted timeline replicas as display-only so authoritative pagination always comes from the daemon. Require renewed user intent between older-history pages unless the viewport genuinely remains at the history edge.

Model assistant image acquisition as loading, loaded, or failed so recreating a preview URL cannot be rendered as an error.

* fix(app): keep file image previews current

* fix(app): stabilize history pagination edges

* fix(app): recover transient timeline loads

* fix(app): retain assistant image previews

* Stabilize history pagination lifecycle

* Stabilize history settlement and image previews

* Retry file images after reconnect

* Keep active previews and pagination requests alive

* Replace image hook tests with typed ports

* Integrate timeline hydration with submission authority

* Harden pre-hydration timeline reconciliation

* Make rewind E2E use real scroll intent

* Preserve live timeline state through hydration

* Keep mounted image attachments retained

* Protect preview persistence and lifecycle hydration

* Preserve idless live assistant continuations

* Preserve timeline rows across delayed hydration

* test(app): cover real assistant image files

* Preserve assistant tool ordering during hydration
2026-07-28 22:29:03 +02:00

200 lines
7.3 KiB
TypeScript

import { expect, test } from "./fixtures";
import {
expectSameOlderHistoryLoadingOperation,
expectTimelineAtHistoryStart,
expectTimelinePromptNotMounted,
expectTimelinePromptPositionPreserved,
expectTimelinePromptVisible,
holdBootstrapTimelinePage,
holdDaemonHydration,
holdOlderHistoryPages,
makeLoadedTimelineFitViewport,
openAgentTimeline,
rememberOlderHistoryLoadingOperation,
rememberTimelineViewport,
rememberTimelinePromptPosition,
reloadAgentTimelineFromPersistedReplica,
scrollTimelineUntilOlderHistoryIsReachable,
scrollTimelineToNewestLoadedEdge,
seedLongMockAgentTimeline,
sendLiveTurnBeforeHydration,
expectTimelineViewportAnchoredAfterPrepend,
userScrollsTimelineToHistoryStart,
} from "./helpers/timeline-pagination";
test.describe("Agent timeline pagination", () => {
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 expectTimelinePromptNotMounted(page, agent.oldestPrompt);
await userScrollsTimelineToHistoryStart(page);
await history.expectRequestedPages(1);
history.releasePage(1);
await history.expectSettledWithRequestedPages(1);
await userScrollsTimelineToHistoryStart(page);
await history.expectRequestedPages(2);
} 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 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();
}
});
});