import { expect, type Page } from "@playwright/test"; import { buildHostAgentDetailRoute } from "@/utils/host-routes"; import { test } from "../support/fixtures"; import { seedWorkspace, type SeedDaemonClient } from "../support/helpers/seed-client"; import { getServerId } from "../support/helpers/server-id"; import { observeTimelineSubscriptions } from "../support/helpers/timeline-delivery"; import { waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs"; import { installDaemonWebSocketGate } from "../support/helpers/daemon-websocket-gate"; import { runWorkspaceActionFromCommandCenter } from "../support/helpers/command-center-workspace-actions"; import { expectAgentIdle, expectInlineWorkingIndicator, expectTurnCopyButton, } from "../support/helpers/agent-stream"; import { expectReconnectingToastDebounced, expectReconnectingToastGone, expectReconnectingToastVisible, } from "../support/helpers/workspace-ui"; interface ViewedTimelineScenario { client: SeedDaemonClient; workspaceId: string; firstAgentId: string; secondAgentId: string; cleanup(): Promise; } async function seedViewedTimelineScenario( options: { firstAgentModel?: string } = {}, ): Promise { const workspace = await seedWorkspace({ repoPrefix: "viewed-timelines-" }); const createAgent = (title: string, model = "ten-second-stream") => workspace.client.createAgent({ provider: "mock", cwd: workspace.repoPath, workspaceId: workspace.workspaceId, title, modeId: "load-test", model, }); const [firstAgent, secondAgent] = await Promise.all([ createAgent("First viewed chat", options.firstAgentModel), createAgent("Second viewed chat"), ]); return { client: workspace.client, workspaceId: workspace.workspaceId, firstAgentId: firstAgent.id, secondAgentId: secondAgent.id, cleanup: workspace.cleanup, }; } async function openAgent(page: Page, scenario: ViewedTimelineScenario, agentId: string) { await page.goto(buildHostAgentDetailRoute(getServerId(), agentId, scenario.workspaceId)); await page.waitForURL( (url) => url.pathname.includes("/workspace/") && !url.searchParams.has("open"), ); await waitForWorkspaceTabsVisible(page); } async function selectAgent(page: Page, title: string) { await page.getByRole("button", { name: title, exact: true }).click(); } async function enableMoveTabShortcut(page: Page) { await page.addInitScript(() => { Object.defineProperty(navigator, "platform", { get: () => "MacIntel" }); }); } async function moveActiveTabRight(page: Page) { await page.keyboard.press("Meta+Alt+Shift+ArrowRight"); } async function commitMessage(scenario: ViewedTimelineScenario, agentId: string, prompt: string) { await scenario.client.sendAgentMessage(agentId, prompt); const finish = await scenario.client.waitForFinish(agentId, 30_000); expect(finish.status).toBe("idle"); } async function startVisibleTurn( page: Page, scenario: ViewedTimelineScenario, prompt: string, ): Promise { await scenario.client.sendAgentMessage(scenario.firstAgentId, prompt); await expect(page.getByText(prompt, { exact: true })).toBeVisible(); await expectInlineWorkingIndicator(page); } async function expectAgentConsistentlyIdle(page: Page, title: string): Promise { const tab = page.getByRole("button", { name: title, exact: true }); await expect(tab.locator('[data-status-bucket="running"]')).toHaveCount(0); await expectAgentIdle(page); await expect(page.getByTestId("turn-working-indicator")).toHaveCount(0); await expectTurnCopyButton(page); } test.describe("Viewed agent timelines", () => { test("a turn that finishes while hidden reopens with consistently idle chrome", async ({ page, }) => { test.setTimeout(150_000); const subscriptions = observeTimelineSubscriptions(page); const scenario = await seedViewedTimelineScenario({ firstAgentModel: "one-minute-stream" }); try { await openAgent(page, scenario, scenario.firstAgentId); await startVisibleTurn(page, scenario, "Finish after this chat becomes hidden."); await selectAgent(page, "Second viewed chat"); await subscriptions.waitForSubscribedAgents([scenario.firstAgentId, scenario.secondAgentId]); const finish = await scenario.client.waitForFinish(scenario.firstAgentId, 90_000); expect(finish.status).toBe("idle"); await selectAgent(page, "First viewed chat"); await expectAgentConsistentlyIdle(page, "First viewed chat"); } finally { await scenario.cleanup(); } }); test("a hidden hot chat stays current", async ({ page }) => { test.setTimeout(60_000); const subscriptions = observeTimelineSubscriptions(page); const scenario = await seedViewedTimelineScenario(); try { await openAgent(page, scenario, scenario.firstAgentId); await selectAgent(page, "Second viewed chat"); await subscriptions.waitForSubscribedAgents([scenario.firstAgentId, scenario.secondAgentId]); await commitMessage( scenario, scenario.firstAgentId, "Committed while the first chat is hidden.", ); await expect( page.getByText("Committed while the first chat is hidden.", { exact: true }), ).toHaveCount(0); await selectAgent(page, "First viewed chat"); await expect( page.getByText("Committed while the first chat is hidden.", { exact: true }), ).toBeVisible(); await expect(page.getByText("(end of synthetic stream)", { exact: true })).toBeVisible(); } finally { await scenario.cleanup(); } }); test("two visible split chats both stay current", async ({ page }) => { const scenario = await seedViewedTimelineScenario(); try { await enableMoveTabShortcut(page); await openAgent(page, scenario, scenario.firstAgentId); await runWorkspaceActionFromCommandCenter(page, "Split pane right"); await selectAgent(page, "Second viewed chat"); await moveActiveTabRight(page); await expect( page.getByRole("button", { name: "First viewed chat", exact: true }), ).toBeVisible(); await expect( page.getByRole("button", { name: "Second viewed chat", exact: true }), ).toBeVisible(); await expect(page.getByRole("textbox", { name: "Message agent..." })).toHaveCount(2); await commitMessage(scenario, scenario.firstAgentId, "First visible pane update."); await expect(page.getByText("First visible pane update.", { exact: true })).toBeVisible(); await expect( page.getByRole("button", { name: "Second viewed chat", exact: true }), ).toBeVisible(); } finally { await scenario.cleanup(); } }); test("a visible chat catches up after reconnecting", async ({ page }) => { const gate = await installDaemonWebSocketGate(page); const scenario = await seedViewedTimelineScenario(); try { await openAgent(page, scenario, scenario.firstAgentId); await expect(page.getByRole("button", { name: "First viewed chat" })).toHaveAttribute( "aria-selected", "true", ); await gate.drop(); await expectReconnectingToastDebounced(page); await expectReconnectingToastVisible(page); await commitMessage(scenario, scenario.firstAgentId, "Committed while the chat reconnects."); await expect( page.getByText("Committed while the chat reconnects.", { exact: true }), ).toHaveCount(0); gate.restore(); await expectReconnectingToastGone(page); const recoveredMessage = page.getByText("Committed while the chat reconnects.", { exact: true, }); await expect(recoveredMessage).toHaveCount(1); await expect(recoveredMessage).toBeVisible(); } finally { gate.restore(); await scenario.cleanup(); } }); test("preserves reconnecting toast through retained tab switches", async ({ page }) => { const gate = await installDaemonWebSocketGate(page); const scenario = await seedViewedTimelineScenario(); try { await openAgent(page, scenario, scenario.firstAgentId); await selectAgent(page, "Second viewed chat"); await expect(page.getByRole("textbox", { name: "Message agent..." })).toBeVisible(); await selectAgent(page, "First viewed chat"); await gate.drop(); await expectReconnectingToastVisible(page); await selectAgent(page, "Second viewed chat"); await expectReconnectingToastVisible(page, { timeout: 500 }); } finally { gate.restore(); await scenario.cleanup(); } }); });