* fix(ci): skip unrelated required checks cleanly Matrix jobs had to start no-op runners to preserve interpolated required-check names. Static named jobs let GitHub report genuine skipped conclusions while keeping those names stable. * fix(ci): gate changes to install patching script * fix(ci): route cross-package test contracts narrowly * fix(ci): keep routed contracts merge-blocking * fix(server): exclude nested test utilities from builds * fix(ci): cover app-owned native and browser domains * refactor(ci): align tests with runtime domains Own browser specs, desktop specs, and Electron-only app code by stable directories so PR routing does not depend on filename conventions. Keep desktop integration coverage inside the required desktop check. * fix(ci): keep routing and desktop suites isolated The routing regression must run before dependency installation, and desktop Vitest must not collect Playwright-owned specs. * fix(ci): isolate desktop e2e runtime * perf(test): isolate Playwright workers * perf(test): parallelize Playwright within specs * fix(test): keep Playwright file scheduling * fix(test): share dynamic Metro fixture * fix(test): preserve own-host restart fixtures * fix(ci): preserve routed contracts after rebase * Stabilize Playwright wheel-scroll assertions
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.expectNoRepeatedAssistantEntries();
|
|
} 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();
|
|
}
|
|
});
|
|
});
|