From 27930e542a806a220bb26fb8b59c690d12381b0a Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 10 Sep 2026 08:22:26 +0200 Subject: [PATCH] test(app): migrate browser fixtures to owned subscriptions --- ...ent-timeline-pagination-old-daemon.spec.ts | 86 ----- .../owned-subscriptions-old-daemon.spec.ts | 64 ++++ .../app/e2e/browser/plugin-modal-body.spec.ts | 2 + .../schedules-edit-model-hydration.spec.ts | 98 ++---- .../browser/schedules-project-target.spec.ts | 55 ++-- .../browser/workspace-setup-streaming.spec.ts | 3 +- .../e2e/support/helpers/schedule-fake-host.ts | 304 ------------------ .../app/e2e/support/helpers/schedule-host.ts | 81 +++++ .../e2e/support/helpers/workspace-setup.ts | 8 + 9 files changed, 201 insertions(+), 500 deletions(-) delete mode 100644 packages/app/e2e/browser/agent-timeline-pagination-old-daemon.spec.ts create mode 100644 packages/app/e2e/browser/owned-subscriptions-old-daemon.spec.ts delete mode 100644 packages/app/e2e/support/helpers/schedule-fake-host.ts create mode 100644 packages/app/e2e/support/helpers/schedule-host.ts diff --git a/packages/app/e2e/browser/agent-timeline-pagination-old-daemon.spec.ts b/packages/app/e2e/browser/agent-timeline-pagination-old-daemon.spec.ts deleted file mode 100644 index 6ec68a2b1e..0000000000 --- a/packages/app/e2e/browser/agent-timeline-pagination-old-daemon.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { randomUUID } from "node:crypto"; -import { metroTest as test } from "../support/fixtures"; -import { buildCreateAgentPreferences, buildSeededHost } from "../support/helpers/daemon-registry"; -import { startIsolatedHostDaemon } from "../support/helpers/isolated-host-daemon"; -import type { MockAgentWorkspace } from "../support/helpers/mock-agent"; -import { seedWorkspace } from "../support/helpers/seed-client"; -import { - expectTimelinePromptVisible, - holdOlderHistoryPages, - openAgentTimeline, - scrollThroughOlderHistoryPages, -} from "../support/helpers/timeline-pagination"; - -test("does not repeat an assistant block when the current app paginates a published 0.2.5 daemon", async ({ - page, -}) => { - test.setTimeout(120_000); - const serverId = `srv_old_pagination_${randomUUID().replaceAll("-", "").slice(0, 12)}`; - const daemon = await startIsolatedHostDaemon(serverId, { publishedVersion: "0.2.5" }); - const workspace = await seedWorkspace({ - repoPrefix: "timeline-old-daemon-pagination-", - port: daemon.port, - }); - const createdAgent = await workspace.client.createAgent({ - provider: "mock", - cwd: workspace.repoPath, - workspaceId: workspace.workspaceId, - title: "Published daemon pagination regression", - modeId: "load-test", - model: "ten-second-stream", - }); - const agent: MockAgentWorkspace = { - agentId: createdAgent.id, - workspaceId: workspace.workspaceId, - cwd: workspace.repoPath, - client: workspace.client, - cleanup: workspace.cleanup, - }; - - try { - for (let index = 0; index < 40; index += 1) { - await agent.client.sendAgentMessage( - agent.agentId, - `timeline-pagination-older-turn-${index}: emit 1 coalesced agent stream updates`, - ); - await agent.client.waitForFinish(agent.agentId, 15_000); - } - await agent.client.sendAgentMessage(agent.agentId, "build a realistic long mock timeline"); - await agent.client.waitForFinish(agent.agentId, 20_000); - for (let index = 0; index < 20; index += 1) { - await agent.client.sendAgentMessage( - agent.agentId, - `timeline-pagination-turn-${index}: emit 1 coalesced agent stream updates`, - ); - await agent.client.waitForFinish(agent.agentId, 15_000); - } - - const host = buildSeededHost({ - serverId, - endpoint: `127.0.0.1:${daemon.port}`, - nowIso: new Date().toISOString(), - }); - await page.addInitScript( - ({ seededHost, preferences }) => { - localStorage.setItem("@paseo:e2e", "1"); - localStorage.setItem("@paseo:daemon-registry", JSON.stringify([seededHost])); - localStorage.setItem("@paseo:create-agent-preferences", JSON.stringify(preferences)); - }, - { seededHost: host, preferences: buildCreateAgentPreferences() }, - ); - - const history = await holdOlderHistoryPages(page, agent, daemon.port); - await openAgentTimeline(page, agent, serverId); - await expectTimelinePromptVisible( - page, - "timeline-pagination-turn-19: emit 1 coalesced agent stream updates", - ); - await scrollThroughOlderHistoryPages(page, 3, history); - - history.expectRepeatedEntries(); - await history.expectOwnedTextRendered("Now I have a clearer picture."); - } finally { - await agent.cleanup(); - await daemon.close(); - } -}); diff --git a/packages/app/e2e/browser/owned-subscriptions-old-daemon.spec.ts b/packages/app/e2e/browser/owned-subscriptions-old-daemon.spec.ts new file mode 100644 index 0000000000..0f0109ee3c --- /dev/null +++ b/packages/app/e2e/browser/owned-subscriptions-old-daemon.spec.ts @@ -0,0 +1,64 @@ +import { randomUUID } from "node:crypto"; +import { metroTest as test, expect } from "../support/fixtures"; +import { buildCreateAgentPreferences, buildSeededHost } from "../support/helpers/daemon-registry"; +import { startIsolatedHostDaemon } from "../support/helpers/isolated-host-daemon"; +import { buildAgentRoute } from "../support/helpers/mock-agent"; +import { seedWorkspace } from "../support/helpers/seed-client"; + +test("requires a host update before observing a published 0.2.5 daemon", async ({ page }) => { + test.setTimeout(120_000); + const serverId = `srv_old_pagination_${randomUUID().replaceAll("-", "").slice(0, 12)}`; + const daemon = await startIsolatedHostDaemon(serverId, { publishedVersion: "0.2.5" }); + const workspace = await seedWorkspace({ + repoPrefix: "timeline-old-daemon-pagination-", + port: daemon.port, + }); + const createdAgent = await workspace.client.createAgent({ + provider: "mock", + cwd: workspace.repoPath, + workspaceId: workspace.workspaceId, + title: "Published daemon pagination regression", + modeId: "load-test", + model: "ten-second-stream", + }); + const requests: unknown[] = []; + page.on("websocket", (socket) => + socket.on("framesent", ({ payload }) => { + if (typeof payload !== "string") return; + const frame = JSON.parse(payload); + if (frame.type === "session" && frame.message.type !== "ping") requests.push(frame.message); + }), + ); + + try { + const host = buildSeededHost({ + serverId, + endpoint: `127.0.0.1:${daemon.port}`, + nowIso: new Date().toISOString(), + }); + await page.addInitScript( + ({ seededHost, preferences }) => { + localStorage.setItem("@paseo:e2e", "1"); + localStorage.setItem("@paseo:daemon-registry", JSON.stringify([seededHost])); + localStorage.setItem("@paseo:create-agent-preferences", JSON.stringify(preferences)); + }, + { seededHost: host, preferences: buildCreateAgentPreferences() }, + ); + + await page.goto(buildAgentRoute(workspace.workspaceId, createdAgent.id, serverId)); + const updateHost = page.getByText("Update the host to use this version of Paseo.", { + exact: true, + }); + await expect(updateHost).toHaveCount(1); + await expect(updateHost).toBeVisible(); + await expect(page.getByRole("button", { name: "Manage host", exact: true })).toBeVisible(); + expect(requests).toEqual([]); + await page.reload(); + await expect(updateHost).toHaveCount(1); + await expect(updateHost).toBeVisible(); + expect(requests).toEqual([]); + } finally { + await workspace.cleanup(); + await daemon.close(); + } +}); diff --git a/packages/app/e2e/browser/plugin-modal-body.spec.ts b/packages/app/e2e/browser/plugin-modal-body.spec.ts index 6db456b72b..bdd822b9c8 100644 --- a/packages/app/e2e/browser/plugin-modal-body.spec.ts +++ b/packages/app/e2e/browser/plugin-modal-body.spec.ts @@ -14,6 +14,8 @@ async function openExample(page: Page, example: string) { async function closeExample(page: Page) { await page.getByRole("button", { name: "Close", exact: true }).last().click(); + // The launcher is visible behind a closing sheet; wait for dismissal before reopening. + await expect(page.getByText(/^Modal example:/)).toHaveCount(0); await expect(page.getByRole("button", { name: "Open Form", exact: true })).toBeVisible(); } diff --git a/packages/app/e2e/browser/schedules-edit-model-hydration.spec.ts b/packages/app/e2e/browser/schedules-edit-model-hydration.spec.ts index 59b2e51696..de91c88569 100644 --- a/packages/app/e2e/browser/schedules-edit-model-hydration.spec.ts +++ b/packages/app/e2e/browser/schedules-edit-model-hydration.spec.ts @@ -1,12 +1,11 @@ import { expect, test } from "../support/fixtures"; import { gotoAppShell } from "../support/helpers/app"; import { - addFakeScheduleHostAndReload, - buildFakeScheduleHostWorkspace, - FAKE_HOST_MODEL_ID, - FAKE_HOST_MODEL_LABEL, - installFakeScheduleHost, -} from "../support/helpers/schedule-fake-host"; + addScheduleHostAndReload, + createScheduleHost, + SECONDARY_MODEL_ID, + SECONDARY_MODEL_LABEL, +} from "../support/helpers/schedule-host"; import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; import { expectSettled, expectStableHeight } from "../support/helpers/settled"; import { waitForSidebarHydration } from "../support/helpers/workspace-ui"; @@ -32,7 +31,11 @@ interface ScheduleSeedClient { scheduleDelete(input: { id: string }): Promise<{ error: string | null }>; } -async function seedMockSchedule(workspace: SeededWorkspace, name: string): Promise { +async function seedMockSchedule( + workspace: SeededWorkspace, + name: string, + model = "ten-second-stream", +): Promise { const client = workspace.client as unknown as ScheduleSeedClient; const result = await client.scheduleCreate({ prompt: "Say hello from the scheduled agent.", @@ -43,7 +46,7 @@ async function seedMockSchedule(workspace: SeededWorkspace, name: string): Promi config: { provider: "mock", cwd: workspace.repoPath, - model: "ten-second-stream", + model, modeId: "load-test", title: name, }, @@ -66,42 +69,6 @@ async function deleteSeededSchedule(workspace: SeededWorkspace, id: string): Pro .catch(ignoreScheduleDeleteError); } -type FakeScheduleHostSchedule = NonNullable< - Parameters[0]["schedules"] ->[number]; - -function buildFakeHostSchedule(input: { - id: string; - name: string; - cwd: string; -}): FakeScheduleHostSchedule { - const now = "2026-07-01T00:00:00.000Z"; - return { - id: input.id, - name: input.name, - prompt: "Run on the secondary host.", - cadence: { type: "cron", expression: "0 9 * * *" }, - target: { - type: "new-agent", - config: { - provider: "mock", - cwd: input.cwd, - model: FAKE_HOST_MODEL_ID, - modeId: "load-test", - title: input.name, - }, - }, - status: "active", - createdAt: now, - updatedAt: now, - nextRunAt: now, - lastRunAt: null, - pausedAt: null, - expiresAt: null, - maxRuns: null, - }; -} - test.describe("Schedules", () => { const cleanupTasks: Array<() => Promise> = []; @@ -154,34 +121,23 @@ test.describe("Schedules", () => { test("edit form hydrates a non-default host schedule after reload", async ({ page }) => { const workspace = await seedWorkspace({ repoPrefix: "schedule-host-b-hydration-" }); cleanupTasks.push(() => workspace.cleanup()); - const fakeHost = await buildFakeScheduleHostWorkspace(workspace); - const fakePort = String(59_000 + Math.floor(Math.random() * 900)); - const scheduleId = "fake-host-schedule"; - const scheduleName = "Secondary host schedule"; - - await installFakeScheduleHost({ - page, - port: fakePort, - serverId: fakeHost.serverId, - workspace: fakeHost.workspace, - project: fakeHost, - schedules: [ - buildFakeHostSchedule({ - id: scheduleId, - name: scheduleName, - cwd: String(fakeHost.workspace.workspaceDirectory), - }), - ], - }); + const secondary = await createScheduleHost(); + cleanupTasks.push(() => secondary.cleanup()); + const scheduleId = await seedMockSchedule( + secondary, + "Secondary host schedule", + SECONDARY_MODEL_ID, + ); + cleanupTasks.push(() => deleteSeededSchedule(secondary, scheduleId)); await gotoAppShell(page); await waitForSidebarHydration(page); await page.goto(buildSchedulesRoute()); - await addFakeScheduleHostAndReload({ + await addScheduleHostAndReload({ page, - serverId: fakeHost.serverId, - label: "Fake host", - port: fakePort, + serverId: secondary.serverId, + label: secondary.label, + port: secondary.port, }); await page.reload(); @@ -197,14 +153,14 @@ test.describe("Schedules", () => { const modelTrigger = page.getByTestId("schedule-model-trigger"); const modeTrigger = page.getByTestId("schedule-mode-trigger"); - await expect(hostTrigger).toContainText("Fake host", { timeout: 30_000 }); + await expect(hostTrigger).toContainText(secondary.label, { timeout: 30_000 }); await expect(hostTrigger).toBeDisabled(); await expectSettled(hostTrigger); - await expect(projectTrigger).toContainText(fakeHost.projectDisplayName, { timeout: 30_000 }); + await expect(projectTrigger).toContainText(secondary.projectDisplayName, { timeout: 30_000 }); await expectSettled(projectTrigger); - await expect(modelTrigger).toContainText(FAKE_HOST_MODEL_LABEL, { timeout: 30_000 }); + await expect(modelTrigger).toContainText(SECONDARY_MODEL_LABEL, { timeout: 30_000 }); await expectSettled(modelTrigger); - await expect(modeTrigger).toContainText("Load test", { timeout: 30_000 }); + await expect(modeTrigger).toContainText("Load Test", { timeout: 30_000 }); await expectSettled(modeTrigger); await expect(page.getByTestId("cadence-mode")).toHaveCount(0); await expect(page.getByTestId("schedule-cadence-preset-trigger")).toContainText("Daily 9:00"); diff --git a/packages/app/e2e/browser/schedules-project-target.spec.ts b/packages/app/e2e/browser/schedules-project-target.spec.ts index 15d26dde56..4c36eb3985 100644 --- a/packages/app/e2e/browser/schedules-project-target.spec.ts +++ b/packages/app/e2e/browser/schedules-project-target.spec.ts @@ -1,10 +1,6 @@ import { expect, test, type Page } from "../support/fixtures"; import { gotoAppShell } from "../support/helpers/app"; -import { - addFakeScheduleHostAndReload, - buildFakeScheduleHostWorkspace, - installFakeScheduleHost, -} from "../support/helpers/schedule-fake-host"; +import { addScheduleHostAndReload, createScheduleHost } from "../support/helpers/schedule-host"; import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; import { getServerId } from "../support/helpers/server-id"; import { projectEquivalenceViewKey } from "../support/helpers/project-view-key"; @@ -242,20 +238,13 @@ test.describe("Schedules project target", () => { cadenceExpression: "0 9 * * *", }); - const fakeHost = await buildFakeScheduleHostWorkspace(workspace); - const fakePort = String(59_000 + Math.floor(Math.random() * 900)); - await installFakeScheduleHost({ + const secondary = await createScheduleHost(); + cleanupTasks.push(() => secondary.cleanup()); + await addScheduleHostAndReload({ page, - port: fakePort, - serverId: fakeHost.serverId, - workspace: fakeHost.workspace, - project: fakeHost, - }); - await addFakeScheduleHostAndReload({ - page, - serverId: fakeHost.serverId, - label: "Fake host", - port: fakePort, + serverId: secondary.serverId, + label: secondary.label, + port: secondary.port, }); const hostFilterTrigger = page.getByTestId("schedules-host-filter-trigger"); @@ -274,25 +263,17 @@ test.describe("Schedules project target", () => { const workspace = await seedWorkspace({ repoPrefix: "schedule-project-host-model-" }); cleanupTasks.push(() => workspace.cleanup()); const serverId = getServerId(); - const fakeHost = await buildFakeScheduleHostWorkspace(workspace); - const fakePort = String(59_000 + Math.floor(Math.random() * 900)); - - await installFakeScheduleHost({ - page, - port: fakePort, - serverId: fakeHost.serverId, - workspace: fakeHost.workspace, - project: fakeHost, - }); + const secondary = await createScheduleHost(); + cleanupTasks.push(() => secondary.cleanup()); await gotoAppShell(page); await waitForSidebarHydration(page); await page.goto(buildSchedulesRoute()); - await addFakeScheduleHostAndReload({ + await addScheduleHostAndReload({ page, - serverId: fakeHost.serverId, - label: "Fake host", - port: fakePort, + serverId: secondary.serverId, + label: secondary.label, + port: secondary.port, }); await expect(page.getByTestId("schedules-empty")).toBeVisible({ timeout: 30_000 }); await page.getByTestId("schedules-empty-new").click(); @@ -334,8 +315,8 @@ test.describe("Schedules project target", () => { await expectSettled(modeTrigger); await hostTrigger.click(); - await page.getByTestId(`schedule-host-option-${fakeHost.serverId}`).click(); - await expect(hostTrigger).toContainText("Fake host"); + await page.getByTestId(`schedule-host-option-${secondary.serverId}`).click(); + await expect(hostTrigger).toContainText(secondary.label); await expect(projectTrigger).toContainText(/select project/i); await expect(modelTrigger).toHaveCount(0); await expect(thinkingTrigger).toHaveCount(0); @@ -345,9 +326,9 @@ test.describe("Schedules project target", () => { await projectTrigger.click(); await page - .getByTestId(`schedule-project-option-${projectEquivalenceViewKey(fakeHost.projectKey)}`) + .getByTestId(`schedule-project-option-${projectEquivalenceViewKey(secondary.projectKey)}`) .click(); - await expect(projectTrigger).toContainText(fakeHost.projectDisplayName); + await expect(projectTrigger).toContainText(secondary.projectDisplayName); await expectSettled(projectTrigger); await expect(modelTrigger).toContainText(/select model/i); await expectSettled(modelTrigger); @@ -355,7 +336,7 @@ test.describe("Schedules project target", () => { await expect(modeTrigger).toHaveCount(0); await page.getByLabel("Schedule name").fill(`Cross host model ${Date.now()}`); - await page.getByLabel("Prompt").fill("Run on the fake host project."); + await page.getByLabel("Prompt").fill("Run on the secondary host project."); await expect(page.getByRole("button", { name: "Create schedule" })).toBeDisabled(); }); diff --git a/packages/app/e2e/browser/workspace-setup-streaming.spec.ts b/packages/app/e2e/browser/workspace-setup-streaming.spec.ts index 0763bfb332..875ec5197d 100644 --- a/packages/app/e2e/browser/workspace-setup-streaming.spec.ts +++ b/packages/app/e2e/browser/workspace-setup-streaming.spec.ts @@ -87,8 +87,7 @@ test.describe("Workspace setup streaming", () => { try { await seedProjectForWorkspaceSetup(client, repo.path); - // Wait for setup completion via daemon (setup snapshots are per-session, - // so the browser session won't receive progress events). + // The setup client owns progress before creating the workspace. const completed = waitForWorkspaceSetupProgress( client, (payload) => diff --git a/packages/app/e2e/support/helpers/schedule-fake-host.ts b/packages/app/e2e/support/helpers/schedule-fake-host.ts deleted file mode 100644 index df54741fc0..0000000000 --- a/packages/app/e2e/support/helpers/schedule-fake-host.ts +++ /dev/null @@ -1,304 +0,0 @@ -import { type Page } from "@playwright/test"; -import { buildSeededHost } from "./daemon-registry"; -import { wsRoutePatternForPort } from "./daemon-port"; -import { type SeededWorkspace } from "./seed-client"; - -const REGISTRY_KEY = "@paseo:daemon-registry"; -const SEED_NONCE_KEY = "@paseo:e2e-seed-nonce"; -const EXTRA_HOSTS_KEY = "@paseo:e2e-extra-hosts"; -export const FAKE_HOST_MODEL_ID = "fake-host-model"; -export const FAKE_HOST_MODEL_LABEL = "Fake host model"; -export const FAKE_HOST_PROJECT_DISPLAY_NAME = "Fake host project"; - -type WebSocketMessage = string | Buffer; -type SessionRequest = Record & { type?: string; requestId?: string }; - -export interface FakeScheduleHostWorkspace { - serverId: string; - projectId: string; - projectKey: string; - projectDisplayName: string; - workspace: Record; -} - -interface FakeScheduleSummary { - id: string; - name: string | null; - prompt: string; - cadence: { type: "cron"; expression: string }; - target: { - type: "new-agent"; - config: { - provider: "mock"; - cwd: string; - model: string; - modeId: string; - title?: string; - }; - }; - status: "active"; - createdAt: string; - updatedAt: string; - nextRunAt: string | null; - lastRunAt: string | null; - pausedAt: string | null; - expiresAt: string | null; - maxRuns: number | null; -} - -function parseJson(message: WebSocketMessage): unknown { - const raw = typeof message === "string" ? message : message.toString("utf8"); - try { - return JSON.parse(raw); - } catch { - return null; - } -} - -function buildSessionMessage(type: string, payload: Record) { - return JSON.stringify({ - type: "session", - message: { - type, - payload, - }, - }); -} - -function buildFakeProviderEntries(nowIso: string) { - return [ - { - provider: "mock", - label: "Mock", - status: "ready", - enabled: true, - fetchedAt: nowIso, - models: [ - { - provider: "mock", - id: FAKE_HOST_MODEL_ID, - label: FAKE_HOST_MODEL_LABEL, - isDefault: true, - }, - ], - modes: [{ id: "load-test", label: "Load test" }], - defaultModeId: "load-test", - }, - ]; -} - -function readSessionRequest(message: WebSocketMessage): SessionRequest | null { - const parsed = parseJson(message); - if (!parsed || typeof parsed !== "object") { - return null; - } - - const envelope = parsed as { type?: string; message?: SessionRequest }; - if (envelope.type !== "session" || !envelope.message) { - return null; - } - - return envelope.message; -} - -function getRequestId(request: SessionRequest): string { - return typeof request.requestId === "string" ? request.requestId : "fake-request"; -} - -export async function buildFakeScheduleHostWorkspace( - workspace: SeededWorkspace, -): Promise { - const workspaceList = await workspace.client.fetchWorkspaces({ - filter: { projectId: workspace.projectId }, - }); - const baseWorkspace = workspaceList.entries.find((entry) => entry.id === workspace.workspaceId); - if (!baseWorkspace) { - throw new Error(`Failed to load seeded workspace descriptor ${workspace.workspaceId}`); - } - - const projectId = `${workspace.projectId}-fake-host`; - const cwd = `${workspace.repoPath}-fake-host`; - return { - serverId: "schedule-fake-host", - projectId, - projectKey: workspace.projectKey, - projectDisplayName: FAKE_HOST_PROJECT_DISPLAY_NAME, - workspace: { - ...baseWorkspace, - id: `${baseWorkspace.id}-fake-host`, - projectId, - projectDisplayName: FAKE_HOST_PROJECT_DISPLAY_NAME, - projectRootPath: cwd, - workspaceDirectory: cwd, - name: FAKE_HOST_PROJECT_DISPLAY_NAME, - project: undefined, - }, - }; -} - -export async function installFakeScheduleHost(input: { - page: Page; - port: string; - serverId: string; - workspace: Record; - project: Pick; - schedules?: FakeScheduleSummary[]; -}): Promise { - await input.page.routeWebSocket(wsRoutePatternForPort(input.port), (ws) => { - ws.onMessage((message) => { - const parsed = parseJson(message); - if (parsed && typeof parsed === "object" && (parsed as { type?: string }).type === "hello") { - ws.send( - buildSessionMessage("status", { - status: "server_info", - serverId: input.serverId, - hostname: "fake-schedule-host", - version: "0.0.0-e2e", - features: { - providersSnapshot: true, - workspaceMultiplicity: true, - projectList: true, - projectAdd: true, - projectRemove: true, - workspaceRecovery: true, - }, - }), - ); - return; - } - - if (parsed && typeof parsed === "object" && (parsed as { type?: string }).type === "ping") { - ws.send(JSON.stringify({ type: "pong" })); - return; - } - - const request = readSessionRequest(message); - if (!request) { - return; - } - - const requestId = getRequestId(request); - const now = Date.now(); - const nowIso = new Date(now).toISOString(); - switch (request.type) { - case "ping": - ws.send( - buildSessionMessage("pong", { - requestId, - clientSentAt: typeof request.clientSentAt === "number" ? request.clientSentAt : now, - serverReceivedAt: now, - serverSentAt: now, - }), - ); - return; - case "fetch_workspaces_request": - ws.send( - buildSessionMessage("fetch_workspaces_response", { - requestId, - entries: [input.workspace], - emptyProjects: [], - pageInfo: { nextCursor: null, prevCursor: null, hasMore: false }, - }), - ); - return; - case "project.list.request": - ws.send( - buildSessionMessage("project.list.response", { - requestId, - projects: [ - { - projectId: input.project.projectId, - projectKey: input.project.projectKey, - projectDisplayName: input.project.projectDisplayName, - projectRootPath: String(input.workspace.projectRootPath), - projectKind: "git", - }, - ], - }), - ); - return; - case "fetch_agents_request": - ws.send( - buildSessionMessage("fetch_agents_response", { - requestId, - entries: [], - pageInfo: { nextCursor: null, prevCursor: null, hasMore: false }, - }), - ); - return; - case "get_providers_snapshot_request": - ws.send( - buildSessionMessage("get_providers_snapshot_response", { - requestId, - entries: buildFakeProviderEntries(nowIso), - generatedAt: nowIso, - }), - ); - return; - case "refresh_providers_snapshot_request": - ws.send( - buildSessionMessage("refresh_providers_snapshot_response", { - requestId, - acknowledged: true, - }), - ); - return; - case "schedule/list": - ws.send( - buildSessionMessage("schedule/list/response", { - requestId, - schedules: input.schedules ?? [], - error: null, - }), - ); - return; - } - }); - }); -} - -export async function addFakeScheduleHostAndReload(input: { - page: Page; - serverId: string; - label: string; - port: string; -}): Promise { - const host = buildSeededHost({ - serverId: input.serverId, - label: input.label, - endpoint: `127.0.0.1:${input.port}`, - nowIso: new Date().toISOString(), - }); - - await input.page.evaluate( - ({ seededHost, keys }) => { - const nonce = localStorage.getItem(keys.nonce); - if (!nonce) { - throw new Error("Expected the e2e seed nonce before overriding the host registry."); - } - const raw = localStorage.getItem(keys.registry); - const registry: Array<{ serverId: string }> = raw ? JSON.parse(raw) : []; - const nextRegistry = registry.filter((entry) => entry.serverId !== seededHost.serverId); - nextRegistry.push(seededHost); - localStorage.setItem(keys.registry, JSON.stringify(nextRegistry)); - - const rawExtraHosts = localStorage.getItem(keys.extraHosts); - const extraHosts: Array<{ serverId: string }> = rawExtraHosts - ? JSON.parse(rawExtraHosts) - : []; - const nextExtraHosts = extraHosts.filter((entry) => entry.serverId !== seededHost.serverId); - nextExtraHosts.push(seededHost); - localStorage.setItem(keys.extraHosts, JSON.stringify(nextExtraHosts)); - }, - { - seededHost: host, - keys: { - registry: REGISTRY_KEY, - nonce: SEED_NONCE_KEY, - extraHosts: EXTRA_HOSTS_KEY, - }, - }, - ); - - await input.page.reload(); -} diff --git a/packages/app/e2e/support/helpers/schedule-host.ts b/packages/app/e2e/support/helpers/schedule-host.ts new file mode 100644 index 0000000000..6c01c9650e --- /dev/null +++ b/packages/app/e2e/support/helpers/schedule-host.ts @@ -0,0 +1,81 @@ +import { type Page } from "@playwright/test"; +import { buildSeededHost } from "./daemon-registry"; +import { startIsolatedHostDaemon } from "./isolated-host-daemon"; +import { seedWorkspace, type SeededWorkspace } from "./seed-client"; + +const REGISTRY_KEY = "@paseo:daemon-registry"; +const SEED_NONCE_KEY = "@paseo:e2e-seed-nonce"; +const EXTRA_HOSTS_KEY = "@paseo:e2e-extra-hosts"; +export const SECONDARY_MODEL_ID = "one-minute-stream"; +export const SECONDARY_MODEL_LABEL = "One minute stream"; + +export async function createScheduleHost() { + const daemon = await startIsolatedHostDaemon("schedule-secondary-host"); + let workspace: SeededWorkspace | undefined; + try { + workspace = await seedWorkspace({ repoPrefix: "schedule-secondary-", port: daemon.port }); + const seeded = workspace; + await seeded.client.renameProject(seeded.projectId, "Secondary project"); + return { + ...seeded, + projectDisplayName: "Secondary project", + serverId: daemon.serverId, + port: daemon.port, + label: "Secondary host", + cleanup: async () => { + await seeded.cleanup(); + await daemon.close(); + }, + }; + } catch (error) { + await workspace?.cleanup(); + await daemon.close(); + throw error; + } +} + +export async function addScheduleHostAndReload(input: { + page: Page; + serverId: string; + label: string; + port: number; +}): Promise { + const host = buildSeededHost({ + serverId: input.serverId, + label: input.label, + endpoint: `127.0.0.1:${input.port}`, + nowIso: new Date().toISOString(), + }); + + await input.page.evaluate( + ({ seededHost, keys }) => { + const nonce = localStorage.getItem(keys.nonce); + if (!nonce) { + throw new Error("Expected the e2e seed nonce before overriding the host registry."); + } + const raw = localStorage.getItem(keys.registry); + const registry: Array<{ serverId: string }> = raw ? JSON.parse(raw) : []; + const nextRegistry = registry.filter((entry) => entry.serverId !== seededHost.serverId); + nextRegistry.push(seededHost); + localStorage.setItem(keys.registry, JSON.stringify(nextRegistry)); + + const rawExtraHosts = localStorage.getItem(keys.extraHosts); + const extraHosts: Array<{ serverId: string }> = rawExtraHosts + ? JSON.parse(rawExtraHosts) + : []; + const nextExtraHosts = extraHosts.filter((entry) => entry.serverId !== seededHost.serverId); + nextExtraHosts.push(seededHost); + localStorage.setItem(keys.extraHosts, JSON.stringify(nextExtraHosts)); + }, + { + seededHost: host, + keys: { + registry: REGISTRY_KEY, + nonce: SEED_NONCE_KEY, + extraHosts: EXTRA_HOSTS_KEY, + }, + }, + ); + + await input.page.reload(); +} diff --git a/packages/app/e2e/support/helpers/workspace-setup.ts b/packages/app/e2e/support/helpers/workspace-setup.ts index f990e52e86..ee706d7ad7 100644 --- a/packages/app/e2e/support/helpers/workspace-setup.ts +++ b/packages/app/e2e/support/helpers/workspace-setup.ts @@ -20,6 +20,7 @@ type WorkspaceSetupDaemonClient = Pick< | "fetchAgents" | "fetchWorkspaces" | "listTerminals" + | "observeEvents" | "removeProject" | "subscribeRawMessages" >; @@ -35,6 +36,13 @@ export async function connectWorkspaceSetupClient(): Promise({ clientIdPrefix: "workspace-setup", }); + // Establish demand before a caller can launch setup. Client close releases it. + try { + await client.observeEvents(["workspace_setup_progress"]).ready; + } catch (error) { + await client.close(); + throw error; + } return withProjectOwnership(client); }