130 lines
6.9 KiB
TypeScript
130 lines
6.9 KiB
TypeScript
import type { Locator, Page } from "@playwright/test";
|
|
|
|
import { expect, fakeServerState, gotoWorkbench, saveScreenshot, test } from "../fixtures/test";
|
|
import { selectors, sessionTab } from "../fixtures/selectors";
|
|
|
|
type TracePagePayload = {
|
|
events?: Array<{ seq?: number; projectedSeq?: number; terminal?: boolean; status?: string; label?: string }>;
|
|
eventCount?: number;
|
|
hasMore?: boolean;
|
|
nextProjectedSeq?: number | null;
|
|
nextCursor?: string | null;
|
|
range?: Record<string, unknown>;
|
|
[key: string]: unknown;
|
|
};
|
|
|
|
test.describe("Workbench golden e2e chain", () => {
|
|
test.use({ scenarioId: "live-rest-backfill-without-terminal-sse" });
|
|
|
|
test("new session reaches sealed final response, survives reload and archives by authority", async ({ page }, testInfo) => {
|
|
await gotoWorkbench(page);
|
|
|
|
await page.locator(selectors.sessionCreate).click();
|
|
const activeTab = page.locator('.session-tab[data-active="true"]').first();
|
|
await expect(activeTab).toHaveAttribute("data-session-id", /^ses_/u);
|
|
const sessionId = await activeTab.getAttribute("data-session-id");
|
|
expect(sessionId).toBeTruthy();
|
|
if (!sessionId) throw new Error("expected active session id");
|
|
|
|
await page.locator(selectors.commandInput).fill("hi");
|
|
await expect(page.locator(selectors.commandSend)).toBeEnabled();
|
|
await page.locator(selectors.commandSend).click();
|
|
|
|
const agentCard = page.locator(`${selectors.messageCard}[data-role="agent"]`).last();
|
|
await expect(page.locator(sessionTab(sessionId))).toHaveAttribute("data-running", "true");
|
|
await expect(agentCard).toHaveAttribute("data-status", "running");
|
|
await expect(agentCard.locator(selectors.traceTimeline)).toHaveAttribute("data-status", "running");
|
|
await saveScreenshot(page, testInfo, "golden-running");
|
|
|
|
await expect(agentCard).toHaveAttribute("data-status", "completed", { timeout: 12_000 });
|
|
await expect(agentCard).toContainText("fake AgentRun completed after REST backfill without terminal SSE.");
|
|
await expect(page.locator(sessionTab(sessionId))).toHaveAttribute("data-running", "false");
|
|
|
|
const trace = agentCard.locator(selectors.traceTimeline);
|
|
await expect(trace).toHaveAttribute("data-status", "completed");
|
|
await openTrace(trace);
|
|
await expect(trace.locator(`${selectors.traceRow}[data-terminal="true"]`)).toBeVisible();
|
|
|
|
const tracePage = await fetchTracePage(page, "trc_live_backfill", "afterProjectedSeq=4&limit=10");
|
|
expect(projectedSeqs(tracePage)).toEqual([5, 6, 7]);
|
|
expect(tracePage.range).toMatchObject({ afterProjectedSeq: 4, fromProjectedSeq: 5, toProjectedSeq: 7, returned: 3, total: 7 });
|
|
expect(tracePage.nextProjectedSeq).toBe(7);
|
|
expectNoLegacyTraceCursorFields(tracePage);
|
|
expect(tracePage.events?.at(-1)?.terminal).toBe(true);
|
|
await saveScreenshot(page, testInfo, "golden-completed");
|
|
|
|
await page.reload();
|
|
await expect(page.locator(sessionTab(sessionId))).toHaveAttribute("data-active", "true");
|
|
await expect(page.locator(`${selectors.messageCard}[data-role="agent"][data-status="completed"]`).last()).toContainText("fake AgentRun completed after REST backfill without terminal SSE.");
|
|
await saveScreenshot(page, testInfo, "golden-reloaded");
|
|
|
|
await page.locator(sessionTab("ses_completed")).click();
|
|
await expect(page.locator(sessionTab("ses_completed"))).toHaveAttribute("data-active", "true");
|
|
await page.locator(sessionTab(sessionId)).click();
|
|
await expect(page.locator(sessionTab(sessionId))).toHaveAttribute("data-active", "true");
|
|
await expect(page.locator(`${selectors.messageCard}[data-role="agent"][data-status="completed"]`).last()).toContainText("fake AgentRun completed after REST backfill without terminal SSE.");
|
|
|
|
await page.locator(selectors.sessionDelete).click();
|
|
await expect(page.locator(sessionTab(sessionId))).toHaveCount(0);
|
|
const detail = await page.request.get(`/v1/workbench/sessions/${encodeURIComponent(sessionId)}`);
|
|
expect(detail.status()).toBe(200);
|
|
const detailBody = await detail.json() as { session?: { status?: string } };
|
|
expect(detailBody.session?.status).toBe("archived");
|
|
|
|
const state = await fakeServerState(page) as { legacyRequestLedger?: unknown[]; chatRequests?: unknown[] };
|
|
expect(state.legacyRequestLedger).toEqual([]);
|
|
expect(state.chatRequests).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
test.describe("long trace projected pagination", () => {
|
|
test.use({ scenarioId: "scroll-follow-long-trace" });
|
|
|
|
test("long trace fixture pages only by projected cursor", async ({ page }, testInfo) => {
|
|
await gotoWorkbench(page, "/workbench/sessions/ses_scroll_follow");
|
|
|
|
const firstPage = await fetchTracePage(page, "trc_scroll_follow", "afterProjectedSeq=0&limit=25");
|
|
expect(projectedSeqs(firstPage)).toEqual(Array.from({ length: 25 }, (_, index) => index + 1));
|
|
expect(firstPage.range).toMatchObject({ afterProjectedSeq: 0, fromProjectedSeq: 1, toProjectedSeq: 25, returned: 25, total: 90 });
|
|
expect(firstPage.nextProjectedSeq).toBe(25);
|
|
expect(firstPage.nextCursor).toBe("projected:25");
|
|
expectNoLegacyTraceCursorFields(firstPage);
|
|
|
|
const laterPage = await fetchTracePage(page, "trc_scroll_follow", "cursor=projected:70&limit=10");
|
|
expect(projectedSeqs(laterPage)).toEqual(Array.from({ length: 10 }, (_, index) => index + 71));
|
|
expect(laterPage.range).toMatchObject({ afterProjectedSeq: 70, fromProjectedSeq: 71, toProjectedSeq: 80, returned: 10, total: 90 });
|
|
expect(laterPage.nextProjectedSeq).toBe(80);
|
|
expect(laterPage.nextCursor).toBe("projected:80");
|
|
expectNoLegacyTraceCursorFields(laterPage);
|
|
|
|
await expect(page.locator(`${selectors.traceTimeline}[data-status="running"]`).last()).toBeVisible();
|
|
await saveScreenshot(page, testInfo, "long-trace-projected-pagination");
|
|
});
|
|
});
|
|
|
|
async function fetchTracePage(page: Page, traceId: string, query: string): Promise<TracePagePayload> {
|
|
const response = await page.request.get(`/v1/workbench/traces/${encodeURIComponent(traceId)}/events?${query}`);
|
|
expect(response.status()).toBe(200);
|
|
return await response.json() as TracePagePayload;
|
|
}
|
|
|
|
function projectedSeqs(payload: TracePagePayload): number[] {
|
|
return (payload.events ?? []).map((event) => Number(event.projectedSeq ?? event.seq));
|
|
}
|
|
|
|
function expectNoLegacyTraceCursorFields(payload: TracePagePayload): void {
|
|
expect(payload).not.toHaveProperty("nextSeq");
|
|
expect(payload).not.toHaveProperty("nextSinceSeq");
|
|
expect(payload).not.toHaveProperty("sinceSeq");
|
|
expect(payload.range).not.toHaveProperty("afterSeq");
|
|
expect(payload.range).not.toHaveProperty("fromSeq");
|
|
expect(payload.range).not.toHaveProperty("toSeq");
|
|
expect(payload.range).not.toHaveProperty("sinceSeq");
|
|
}
|
|
|
|
async function openTrace(trace: Locator): Promise<void> {
|
|
const details = trace.locator("details.trace-disclosure");
|
|
const open = await details.evaluate((element) => (element as HTMLDetailsElement).open).catch(() => false);
|
|
if (!open) await trace.locator("summary.trace-disclosure-summary").click();
|
|
}
|