* fix(app): keep timeline pagination duplicate-free Older daemons page projected history by canonical sequence ranges, so one projected block can appear in adjacent responses. Accept each block on the page containing its projection anchor while still advancing across empty projected pages.\n\nProjected siblings can share a provider message ID, so render identity also includes the timeline cursor instead of collapsing distinct blocks onto one layout row. * fix(e2e): preserve isolated daemon launch contract Keep the source-daemon spawn path recognizable to the repository launch-contract check while selecting the built supervisor only for published package fixtures. * fix(app): preserve stable timeline item identity
260 lines
9.4 KiB
TypeScript
260 lines
9.4 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,
|
|
} from "../support/helpers/timeline-pagination";
|
|
|
|
test.describe("Agent timeline pagination", () => {
|
|
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();
|
|
}
|
|
});
|
|
});
|