From 9d1c5ac5f49a8482130de0e3adfb79a43ed46d87 Mon Sep 17 00:00:00 2001 From: Swyam Sharma Date: Sun, 21 Jun 2026 15:19:28 +0530 Subject: [PATCH 1/2] fix: stabilize frontend playwright e2e --- frontend/e2e/history-nav.spec.ts | 2 +- frontend/e2e/inspector-toggle.spec.ts | 2 +- frontend/e2e/workbench.spec.ts | 38 +++++++++++++++++---------- frontend/playwright.config.ts | 26 +++++++++++++++--- 4 files changed, 48 insertions(+), 20 deletions(-) diff --git a/frontend/e2e/history-nav.spec.ts b/frontend/e2e/history-nav.spec.ts index 536d7d471..63507c8eb 100644 --- a/frontend/e2e/history-nav.spec.ts +++ b/frontend/e2e/history-nav.spec.ts @@ -7,7 +7,7 @@ test("titlebar back/forward arrows traverse history", async ({ page }) => { await expect(page.getByText("Projects")).toBeVisible(); // Navigate: home → session view (in-app push). - await page.getByRole("button", { name: "Open refactor-mux" }).click(); + await page.getByRole("button", { name: "Open Split terminal mux responsibilities" }).click(); await expect(page).toHaveURL(/sessions\/refactor-mux/); const back = page.getByRole("button", { name: "Go back" }); diff --git a/frontend/e2e/inspector-toggle.spec.ts b/frontend/e2e/inspector-toggle.spec.ts index ff6460b2c..4b48ec5d4 100644 --- a/frontend/e2e/inspector-toggle.spec.ts +++ b/frontend/e2e/inspector-toggle.spec.ts @@ -10,7 +10,7 @@ import { expect, test } from "@playwright/test"; // the real rrp + CSS pipeline, which the mocked unit tests can't exercise. test("topbar button collapses and reopens the inspector rail", async ({ page }) => { await page.goto("/"); - await page.getByRole("button", { name: "Open refactor-mux" }).click(); + await page.getByRole("button", { name: "Open Split terminal mux responsibilities" }).click(); await expect(page).toHaveURL(/sessions\/refactor-mux/); // Fresh profile: the rail must mount open, not get toggled shut by diff --git a/frontend/e2e/workbench.spec.ts b/frontend/e2e/workbench.spec.ts index fe6d506cb..5561c9516 100644 --- a/frontend/e2e/workbench.spec.ts +++ b/frontend/e2e/workbench.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test"; +import { expect, test, type Page } from "@playwright/test"; // The Playwright web server runs `dev:web` (VITE_NO_ELECTRON=1), so // useWorkspaceQuery serves the deterministic preview fixtures from @@ -7,24 +7,34 @@ import { expect, test } from "@playwright/test"; test("renders the orchestrator-first workbench shell", async ({ page }) => { await page.goto("/"); - // The single pinned Orchestrator anchor + the Projects group + a name-only worker row. - await expect(page.getByRole("button", { name: "Orchestrator", exact: true })).toBeVisible(); + // The single pinned Orchestrator anchor + the Projects group + current title-based worker rows. + await expect(page.getByRole("button", { name: "Orchestrator board" })).toBeVisible(); await expect(page.getByText("Projects")).toBeVisible(); - await expect(page.getByRole("button", { name: "fix-webgl-fallback", exact: true })).toBeVisible(); - // Orchestrator side rail = the quiet Workers list. - await expect(page.getByText("Workers", { exact: true })).toBeVisible(); + await expect( + page.getByRole("button", { name: "Open Restore fallback renderer after WebGL init fails" }), + ).toBeVisible(); + await expect(page.getByRole("button", { name: "Open Split terminal mux responsibilities" })).toBeVisible(); }); test("deep-links into a worker session", async ({ page }) => { - await page.goto("/#/workspaces/api-gateway/sessions/refactor-mux"); - // Worker view = emdash three-pane with the Git review rail. - await expect(page.getByText("Changed")).toBeVisible(); - await expect(page.getByRole("button", { name: /Commit & Push/ })).toBeVisible(); + await page.goto("/#/projects/api-gateway/sessions/refactor-mux"); + // Worker view = terminal preview plus current Summary inspector rail. + await expectSessionDetail(page); }); -test("drilling into a worker opens its Git review rail", async ({ page }) => { +test("drilling into a worker opens its session detail view", async ({ page }) => { await page.goto("/"); - await page.getByRole("button", { name: "refactor-mux", exact: true }).click(); - await expect(page.getByRole("button", { name: /Commit & Push/ })).toBeVisible(); - await expect(page.getByText("internal/mux/terminal_mux.go")).toBeVisible(); + await page.getByRole("button", { name: "Open Split terminal mux responsibilities" }).click(); + await expect(page).toHaveURL(/projects\/api-gateway\/sessions\/refactor-mux/); + await expectSessionDetail(page); }); + +async function expectSessionDetail(page: Page) { + const inspector = page.getByTestId("inspector"); + await expect(inspector).toBeVisible(); + await expect(page.getByText("Split terminal mux responsibilities")).toBeVisible(); + await expect(inspector.getByText("feat/refactor-mux")).toBeVisible(); + await expect( + page.getByTestId("terminal").getByText("Browser preview renders a static terminal surface."), + ).toBeVisible(); +} diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index b69f4e4bb..7903cb5de 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -1,15 +1,33 @@ import { defineConfig } from "@playwright/test"; +const e2ePort = parseE2EPort(process.env.PLAYWRIGHT_E2E_PORT); +const e2eHost = "127.0.0.1"; +const webServerEnv = Object.fromEntries( + Object.entries(process.env).filter( + (entry): entry is [string, string] => entry[0].startsWith("VITE_") && typeof entry[1] === "string", + ), +); +webServerEnv.VITE_AO_API_BASE_URL = `http://${e2eHost}:${e2ePort}`; + +function parseE2EPort(value: string | undefined): number { + const port = Number(value ?? 5174); + if (!Number.isInteger(port) || port < 1 || port > 65535) { + throw new Error(`PLAYWRIGHT_E2E_PORT must be an integer TCP port, got ${value ?? "5174"}`); + } + return port; +} + export default defineConfig({ testDir: "e2e", use: { - baseURL: "http://127.0.0.1:5173", + baseURL: `http://${e2eHost}:${e2ePort}`, }, webServer: { // dev:web serves the renderer alone (VITE_NO_ELECTRON=1) — no Electron child to // launch, which is all the browser-based e2e suite needs. - command: "npm run dev:web -- --port 5173", - port: 5173, - reuseExistingServer: !process.env.CI, + command: `npm run dev:web -- --host ${e2eHost} --port ${e2ePort} --strictPort`, + env: webServerEnv, + port: e2ePort, + reuseExistingServer: false, }, }); From d93e8dd6d017e1a850d9885f7e4a201fc0737af8 Mon Sep 17 00:00:00 2001 From: Swyam Sharma Date: Sun, 21 Jun 2026 16:00:33 +0530 Subject: [PATCH 2/2] fix: silence preview event stream in e2e --- frontend/src/renderer/lib/event-transport.test.ts | 15 +++++++++++++++ frontend/src/renderer/lib/event-transport.ts | 11 +++++++++++ 2 files changed, 26 insertions(+) diff --git a/frontend/src/renderer/lib/event-transport.test.ts b/frontend/src/renderer/lib/event-transport.test.ts index 90a599009..a47bc2794 100644 --- a/frontend/src/renderer/lib/event-transport.test.ts +++ b/frontend/src/renderer/lib/event-transport.test.ts @@ -70,10 +70,25 @@ beforeEach(() => { }); afterEach(() => { + vi.unstubAllEnvs(); delete (globalThis as unknown as { EventSource?: unknown }).EventSource; }); describe("createEventTransport", () => { + it("skips the SSE stream in deterministic browser preview mode", () => { + vi.stubEnv("VITE_NO_ELECTRON", "1"); + + const disconnect = createEventTransport(fakeQueryClient()).connect(); + + expect(EventSourceStub.instances).toHaveLength(0); + expect(onStatusMock).not.toHaveBeenCalled(); + expect(subscribeApiBaseUrlMock).not.toHaveBeenCalled(); + expect(getEventsConnectionState()).toBe("idle"); + + disconnect(); + expect(getEventsConnectionState()).toBe("idle"); + }); + it("opens a single SSE connection to the current base URL on connect", () => { createEventTransport(fakeQueryClient()).connect(); diff --git a/frontend/src/renderer/lib/event-transport.ts b/frontend/src/renderer/lib/event-transport.ts index feb9f1385..63657a4f6 100644 --- a/frontend/src/renderer/lib/event-transport.ts +++ b/frontend/src/renderer/lib/event-transport.ts @@ -16,6 +16,10 @@ const SSE_RETRY_MS = 5_000; // constants still work. const EVENTSOURCE_CLOSED = 2; +function usesDeterministicPreviewData(): boolean { + return import.meta.env.VITE_NO_ELECTRON === "1"; +} + // CDC event types the daemon pushes over the SSE stream (see // backend/internal/cdc/event.go). The SSE writer tags each frame with // `event: `, so named events bypass EventSource.onmessage and must be @@ -42,6 +46,13 @@ const CDC_EVENT_TYPES = [ export function createEventTransport(queryClient: QueryClient): EventTransport { return { connect() { + if (usesDeterministicPreviewData()) { + setEventsConnectionState("idle"); + return () => { + setEventsConnectionState("idle"); + }; + } + let debounce: ReturnType | undefined; let retryTimer: ReturnType | undefined; let source: EventSource | undefined;