import { expect, test } from "@playwright/test"; import type { ControlWorker } from "@/lib/control/workers"; import { enableScopedE2EAuth, scopedPath } from "./helpers/auth"; import { fulfillJson, mockBaseChrome, mockControlSessionBootstrap, } from "./helpers/automation-control-plane-fixtures"; function currentWorker( id: string, status: ControlWorker["status"], output: string ): ControlWorker { return { id, worktreeId: id, branch: `mogplex/task/current/${id}`, status, error: status === "failed" ? "Worker stopped before finishing." : null, updatedAt: "2026-08-14T00:00:01Z", events: [ { id: `${id}-start`, type: "tool_started", toolName: "Command", message: "Started", payload: { toolCallId: id, input: { command: `check ${id}` } }, createdAt: "2026-08-14T00:00:00Z", }, { id: `${id}-end`, type: "tool_finished", toolName: "Command", message: "Finished", payload: { toolCallId: id, state: status === "failed" ? "error" : "done", output, }, createdAt: "2026-08-14T00:00:01Z", }, ], }; } test("a follow-up preserves one continuous conversation with live command progress and stop", async ({ page, }, testInfo) => { const browserErrors: string[] = []; page.on("pageerror", (error) => browserErrors.push(error.message)); await enableScopedE2EAuth(page); await mockBaseChrome(page); await mockControlSessionBootstrap(page); let currentWorkers: ControlWorker[] = []; await page.route("**/api/control/workers?*", (route) => fulfillJson(route, { workers: [ ...currentWorkers, ...["ci", "docs", "accessibility", "links", "cleanup"].map((task) => ({ id: `old-worker-${task}`, worktreeId: task, branch: `mogplex/task/old-request/${task}`, status: "failed", error: "Worker could not authenticate. Check its AI connection before retrying.", updatedAt: "2026-08-13T00:00:00Z", events: [ { id: `${task}-start`, type: "tool_started", toolName: "Command", message: "Started", payload: { toolCallId: task, input: { command: "old command" } }, createdAt: "2026-08-13T00:00:00Z", }, { id: `${task}-end`, type: "tool_finished", toolName: "Command", message: "Failed", payload: { toolCallId: task, state: "error", output: "Previous batch output", }, createdAt: "2026-08-13T00:00:01Z", }, ], })), ], }) ); await page.route("**/api/connections", (route) => fulfillJson(route, { connections: [] }) ); await page.addInitScript(() => { class FixtureEventSource extends EventTarget { private listener: (event: Event) => void; constructor(url: string) { super(); this.listener = (event: Event) => { const detail = (event as CustomEvent<{ table: string }>).detail; if (decodeURIComponent(url).includes(detail.table)) this.dispatchEvent( new MessageEvent("message", { data: JSON.stringify(detail) }) ); }; window.addEventListener("fixture-table-event", this.listener); queueMicrotask(() => this.dispatchEvent(new Event("open"))); } close() { window.removeEventListener("fixture-table-event", this.listener); } } Object.defineProperty(window, "EventSource", { value: FixtureEventSource }); const original = window.fetch.bind(window); let count = 0; window.fetch = async (input, init) => { if (!String(input).endsWith("/api/control/chat")) return original(input, init); count++; const encoder = new TextEncoder(); return new Response( new ReadableStream({ start(controller) { const send = (chunk: object) => controller.enqueue( encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`) ); send({ type: "start", messageId: `answer-${count}` }); if (count === 1) { send({ type: "text-start", id: "t" }); send({ type: "text-delta", id: "t", delta: "Earlier analysis. ".repeat(100), }); send({ type: "text-end", id: "t" }); send({ type: "tool-input-available", toolCallId: "earlier-read", toolName: "read_file", input: { path: "app/page.tsx" }, }); send({ type: "tool-output-available", toolCallId: "earlier-read", output: { content: "Earlier source content" }, }); send({ type: "finish" }); controller.enqueue(encoder.encode("data: [DONE]\n\n")); controller.close(); } else { send({ type: "tool-input-available", toolCallId: "cmd", toolName: "run_command", input: { command: "pnpm test" }, }); init?.signal?.addEventListener( "abort", () => controller.error(new DOMException("Aborted", "AbortError")), { once: true } ); } }, }), { headers: { "content-type": "text/event-stream", "x-vercel-ai-ui-message-stream": "v1", }, } ); }; }); await page.goto(scopedPath("control")); await page .getByPlaceholder("Ask anything or run a command...") .fill("Review this repository"); await page.getByRole("button", { name: "Start mission" }).click(); const composer = page.getByPlaceholder( "Ask for follow-up changes or attach images" ); await expect( page.getByRole("button", { name: "Send", exact: true }) ).toBeVisible(); await page.getByRole("button", { name: /^Agent commands/ }).click(); await expect(page.getByTestId("control-terminal-activity")).toContainText( "Previous batch output" ); await page.getByRole("button", { name: "Close terminal" }).click(); const earlierTool = page .getByTestId("tool-activity") .filter({ hasText: "Reading file" }); await earlierTool.locator("summary").click(); await expect(earlierTool.locator("details")).toHaveAttribute("open", ""); await composer.fill("Fix the failing tests"); await page.getByRole("button", { name: "Send", exact: true }).click(); const progress = page.getByRole("status", { name: "Current activity" }); await expect(progress).toContainText("Running command"); await expect(progress).toContainText("pnpm test"); const workers = page.getByRole("region", { name: "Mission workers" }); await expect(workers).toContainText("5 workers failed"); await expect(workers.getByRole("link", { name: /View work/ })).toHaveCount(0); await expect(page.getByTestId("control-terminal-activity")).not.toContainText( "Previous batch output" ); await expect( page.getByRole("region", { name: "Latest request" }) ).toContainText("Fix the failing tests"); await expect(page.getByText(/Earlier analysis/)).toHaveCount(1); await expect(earlierTool.locator("details")).toHaveAttribute("open", ""); await expect( page.getByRole("button", { name: /Earlier conversation/i }) ).toHaveCount(0); await expect(page.getByRole("log", { name: "Conversation" })).toHaveCount(1); await expect( page.getByRole("button", { name: "Stop", exact: true }) ).toHaveText("Stop"); await page.screenshot({ path: testInfo.outputPath("follow-up-desktop.png") }); await page.setViewportSize({ width: 390, height: 844 }); const navigation = await page .getByRole("button", { name: "Open sessions", exact: true }) .boundingBox(); const title = await page .getByRole("banner") .getByText("Review this repository", { exact: true }) .boundingBox(); expect(navigation).not.toBeNull(); expect(title).not.toBeNull(); // Navigation shares the breadcrumb row rather than consuming another phone row. expect( Math.abs( navigation!.y + navigation!.height / 2 - title!.y - title!.height / 2 ) ).toBeLessThan(2); await expect(progress).toBeInViewport(); await expect( page .getByRole("region", { name: "Latest request" }) .getByText("Fix the failing tests") ).toBeInViewport(); await expect( page.getByRole("button", { name: "Stop", exact: true }) ).toBeInViewport(); await page.screenshot({ path: testInfo.outputPath("follow-up-mobile.png") }); currentWorkers = [ currentWorker("tests", "streaming", "Current tests progress"), currentWorker("lint", "streaming", "Current lint progress"), ]; const refreshWorkers = () => page.evaluate(() => window.dispatchEvent( new CustomEvent("fixture-table-event", { detail: { table: "external_agent_runs", op: "UPDATE" }, }) ) ); await refreshWorkers(); await page .getByRole("button", { name: "Open terminal", exact: true }) .click(); await page.getByRole("tab", { name: "Agent commands", exact: true }).click(); const terminal = page.getByTestId("control-terminal-activity"); await expect(terminal).toContainText("Current tests progress"); await expect(terminal).toContainText("Current lint progress"); currentWorkers = [ currentWorker("tests", "success", "Current tests passed"), currentWorker("lint", "failed", "Current lint failed"), ]; await refreshWorkers(); await expect(terminal).toContainText("Current tests passed"); await expect(terminal).toContainText("Current lint failed"); await expect(terminal).not.toContainText("Previous batch output"); await page.getByRole("button", { name: "Close terminal" }).click(); await expect(workers).toContainText("6 workers failed"); await expect( page.getByRole("button", { name: "Stop", exact: true }) ).toBeVisible(); await page.getByRole("button", { name: "Stop", exact: true }).click(); await expect(progress).toHaveCount(0); await expect( page .getByRole("region", { name: "Latest request" }) .getByTestId("tool-activity") ).toContainText("Status unconfirmed"); // Earlier turns remain part of this transcript on a phone, with no panel to open. await page.getByText(/Earlier analysis/).scrollIntoViewIfNeeded(); await expect(page.getByText(/Earlier analysis/)).toBeVisible(); await page.screenshot({ path: testInfo.outputPath("conversation-history-mobile.png"), }); await composer.fill("Also check the build"); await page.getByRole("button", { name: "Send", exact: true }).click(); await expect( page.getByRole("region", { name: "Latest request" }) ).toContainText("Also check the build"); await expect( page.getByText("Fix the failing tests", { exact: true }) ).toHaveCount(1); await expect(page.getByText(/Earlier analysis/)).toHaveCount(1); await expect(earlierTool.locator("details")).toHaveAttribute("open", ""); await expect(page.getByRole("log", { name: "Conversation" })).toHaveCount(1); await page.getByRole("button", { name: "Stop", exact: true }).click(); expect(browserErrors).toEqual([]); });