diff --git a/packages/app/e2e/browser/agent-profile-modeless-preferences.spec.ts b/packages/app/e2e/browser/agent-profile-modeless-preferences.spec.ts index 2558d3b427..a10afd2cca 100644 --- a/packages/app/e2e/browser/agent-profile-modeless-preferences.spec.ts +++ b/packages/app/e2e/browser/agent-profile-modeless-preferences.spec.ts @@ -9,7 +9,7 @@ import { seedModelProvider, } from "../support/helpers/agent-profiles"; import { gotoAppShell } from "../support/helpers/app"; -import { daemonWsRoutePattern } from "../support/helpers/daemon-port"; +import { captureWorkspaceAgentRequest } from "../support/helpers/creation"; import { openGlobalNewWorkspaceComposer, selectNewWorkspaceProject, @@ -22,53 +22,6 @@ const CREATE_AGENT_PREFERENCES_KEY = "@paseo:create-agent-preferences"; const MODELESS_PROVIDER = "modeless-profile-e2e"; const MODELESS_MODEL = "pi-profile-model"; -type WebSocketMessage = string | Buffer; - -interface CreateAgentRequestMessage { - type: "create_agent_request"; - config?: { - provider?: unknown; - modeId?: unknown; - featureValues?: unknown; - }; -} - -function getSessionMessage(message: WebSocketMessage): Record | null { - const rawMessage = typeof message === "string" ? message : message.toString("utf8"); - try { - const envelope = JSON.parse(rawMessage) as { type?: unknown; message?: unknown }; - return envelope.type === "session" && envelope.message && typeof envelope.message === "object" - ? (envelope.message as Record) - : null; - } catch { - return null; - } -} - -async function recordAndBlockCreateAgentRequest(page: Page): Promise<{ - waitForRequest(): Promise; -}> { - let resolveRequest: ((message: CreateAgentRequestMessage) => void) | null = null; - const request = new Promise((resolve) => { - resolveRequest = resolve; - }); - - await page.routeWebSocket(daemonWsRoutePattern(), (ws) => { - const server = ws.connectToServer(); - ws.onMessage((message) => { - const sessionMessage = getSessionMessage(message); - if (sessionMessage?.type === "create_agent_request") { - resolveRequest?.(sessionMessage as unknown as CreateAgentRequestMessage); - return; - } - server.send(message); - }); - server.onMessage((message) => ws.send(message)); - }); - - return { waitForRequest: () => request }; -} - async function seedPoisonedModelessPreference(page: Page): Promise { await page.addInitScript( ({ preferencesKey, provider, model }) => { @@ -143,7 +96,7 @@ test.describe("Agent profiles repair modeless provider preferences", () => { ], }); await seedPoisonedModelessPreference(page); - const createAgentRecorder = await recordAndBlockCreateAgentRequest(page); + const createAgentRecorder = await captureWorkspaceAgentRequest(page, { block: true }); try { await gotoAppShell(page); @@ -206,7 +159,7 @@ test.describe("Agent profiles repair modeless provider preferences", () => { }, ], }); - const createAgentRecorder = await recordAndBlockCreateAgentRequest(page); + const createAgentRecorder = await captureWorkspaceAgentRequest(page, { block: true }); try { await gotoAppShell(page); diff --git a/packages/app/e2e/browser/new-workspace-codex-mode-preferences.spec.ts b/packages/app/e2e/browser/new-workspace-codex-mode-preferences.spec.ts index 327666ecb8..956340db74 100644 --- a/packages/app/e2e/browser/new-workspace-codex-mode-preferences.spec.ts +++ b/packages/app/e2e/browser/new-workspace-codex-mode-preferences.spec.ts @@ -1,9 +1,10 @@ -import { expect, test, type Page } from "../support/fixtures"; +import { expect, type Page } from "../support/fixtures"; +import { test } from "../support/creation-fixtures"; import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; import { connectDaemonClient } from "../support/helpers/daemon-client-loader"; import type { FormPreferences } from "@/create-agent-preferences/preferences"; import { gotoAppShell } from "../support/helpers/app"; -import { daemonWsRoutePattern } from "../support/helpers/daemon-port"; +import { captureWorkspaceAgentRequest } from "../support/helpers/creation"; import { openAgentRoute } from "../support/helpers/mock-agent"; import { openGlobalNewWorkspaceComposer, @@ -17,40 +18,6 @@ import { waitForSidebarHydration } from "../support/helpers/workspace-ui"; const CREATE_AGENT_PREFERENCES_KEY = "@paseo:create-agent-preferences"; -type WebSocketMessage = string | Buffer; - -interface CreateAgentRequestMessage { - type: "create_agent_request"; - config?: { - provider?: unknown; - modeId?: unknown; - }; -} - -function parseWebSocketJson(message: WebSocketMessage): unknown { - const rawMessage = typeof message === "string" ? message : message.toString("utf8"); - try { - return JSON.parse(rawMessage); - } catch { - return null; - } -} - -function getSessionMessage(message: WebSocketMessage): Record | null { - const envelope = parseWebSocketJson(message); - if (!envelope || typeof envelope !== "object") { - return null; - } - const maybeEnvelope = envelope as { type?: unknown; message?: unknown }; - if (maybeEnvelope.type !== "session" || !maybeEnvelope.message) { - return null; - } - if (typeof maybeEnvelope.message !== "object") { - return null; - } - return maybeEnvelope.message as Record; -} - async function seedCodexDefaultPermissionPreferences(page: Page, cwd: string): Promise { const client = await connectDaemonClient({ clientIdPrefix: "codex-mode-preferences", @@ -142,44 +109,15 @@ async function expectThinkingOptionsFit(page: Page): Promise { await expect(page.getByTestId("combobox-desktop-container")).toHaveCount(0, { timeout: 5_000 }); } -async function recordAndBlockCreateAgentRequests(page: Page): Promise<{ - waitForCreateAgentRequest(): Promise; -}> { - let resolveRequest: ((message: CreateAgentRequestMessage) => void) | null = null; - const createAgentSeen = new Promise((resolve) => { - resolveRequest = resolve; - }); - - await page.routeWebSocket(daemonWsRoutePattern(), (ws) => { - const server = ws.connectToServer(); - - ws.onMessage((message) => { - const sessionMessage = getSessionMessage(message); - if (sessionMessage?.type === "create_agent_request") { - resolveRequest?.(sessionMessage as unknown as CreateAgentRequestMessage); - return; - } - server.send(message); - }); - - server.onMessage((message) => { - ws.send(message); - }); - }); - - return { - waitForCreateAgentRequest: () => createAgentSeen, - }; -} - test.describe("New workspace Codex mode preferences", () => { test.describe.configure({ timeout: 240_000 }); test("keeps Full Access as the global Codex mode after the workspace draft auto-submit handoff", async ({ page, + startup, }) => { const seeded = await seedWorkspace({ repoPrefix: "codex-mode-preferences-" }); - const createAgentRecorder = await recordAndBlockCreateAgentRequests(page); + const createAgentRecorder = await captureWorkspaceAgentRequest(page, { block: false }); try { await seedCodexDefaultPermissionPreferences(page, seeded.repoPath); await gotoAppShell(page); @@ -200,8 +138,12 @@ test.describe("New workspace Codex mode preferences", () => { ).toBeVisible(); await submitNewWorkspacePrompt(page, "Keep Codex full access selected globally."); - const createAgentRequest = await createAgentRecorder.waitForCreateAgentRequest(); + const createAgentRequest = await createAgentRecorder.waitForRequest(); + await expect(page).toHaveURL(/\/workspace\//); + await expect( + page.getByText("Keep Codex full access selected globally.", { exact: true }).first(), + ).toBeVisible(); expect(createAgentRequest.config).toMatchObject({ provider: "codex", modeId: "full-access", @@ -209,6 +151,8 @@ test.describe("New workspace Codex mode preferences", () => { await expect .poll(() => readCodexModePreference(page), { timeout: 10_000 }) .toBe("full-access"); + await startup.fail(); + await expect(page.getByText(/Creation startup failed for test/).first()).toBeVisible(); } finally { await seeded.cleanup(); } diff --git a/packages/app/e2e/support/helpers/agent-timeline-gate.ts b/packages/app/e2e/support/helpers/agent-timeline-gate.ts index 91b6867cf0..690a3067ae 100644 --- a/packages/app/e2e/support/helpers/agent-timeline-gate.ts +++ b/packages/app/e2e/support/helpers/agent-timeline-gate.ts @@ -1,4 +1,5 @@ import type { Page } from "@playwright/test"; +import { loadSessionMessageReaders } from "./new-workspace"; import { daemonWsRoutePattern, wsRoutePatternForPort } from "./daemon-port"; type WebSocketMessage = string | Buffer; @@ -321,6 +322,7 @@ export async function holdRewindCompletion( export async function delayCreatedAgentInitialTailResponse( page: Page, ): Promise { + const frames = await loadSessionMessageReaders(); let createdAgentId: string | null = null; let releaseRequested = false; let delayedResponseSeen = false; @@ -350,19 +352,24 @@ export async function delayCreatedAgentInitialTailResponse( }); server.onMessage((message) => { - const sessionMessage = getSessionMessage(message); - const payload = sessionMessage ? getPayload(sessionMessage) : null; - if (sessionMessage?.type === "status" && payload?.status === "agent_created") { - const agentId = payload.agentId; - if (typeof agentId === "string") { - createdAgentId = agentId; - resolveCreatedAgent?.(agentId); - } + const sessionMessage = frames.server(message); + let createdId: unknown; + if (sessionMessage?.type === "agent.create.response") { + createdId = sessionMessage.payload.agent?.id; + } else if ( + sessionMessage?.type === "status" && + sessionMessage.payload.status === "agent_created" + ) { + createdId = sessionMessage.payload.agentId; + } + if (typeof createdId === "string") { + createdAgentId = createdId; + resolveCreatedAgent?.(createdId); } if (sessionMessage?.type === "fetch_agent_timeline_response") { - const agentId = payload?.agentId; - const direction = payload?.direction; + const agentId = sessionMessage.payload.agentId; + const direction = sessionMessage.payload.direction; if ( !delayedResponseSeen && typeof agentId === "string" && diff --git a/packages/app/e2e/support/helpers/creation.ts b/packages/app/e2e/support/helpers/creation.ts index c3dfedca44..c29bbeeeeb 100644 --- a/packages/app/e2e/support/helpers/creation.ts +++ b/packages/app/e2e/support/helpers/creation.ts @@ -1,3 +1,4 @@ +import type { WorkspaceCreateRequest } from "@getpaseo/protocol/messages"; import { expect, type Page } from "@playwright/test"; import { daemonWsRoutePattern } from "./daemon-port"; import { gotoAppShell } from "./app"; @@ -19,6 +20,29 @@ import { getServerId } from "./server-id"; import { WORKSPACE_DECK_MAX_MOUNTED_WORKSPACES } from "@/screens/workspace/workspace-deck-retention"; import type { installDaemonWebSocketGate } from "./daemon-websocket-gate"; +/** Capture the submitted agent options; optionally stop provisioning for wire-only assertions. */ +export async function captureWorkspaceAgentRequest(page: Page, options: { block: boolean }) { + const frames = await loadSessionMessageReaders(); + type AgentIntent = NonNullable; + let resolveRequest!: (agent: AgentIntent) => void; + const request = new Promise((resolve) => { + resolveRequest = resolve; + }); + await page.routeWebSocket(daemonWsRoutePattern(), (ws) => { + const server = ws.connectToServer(); + ws.onMessage((frame) => { + const message = frames.client(frame); + if (message?.type === "workspace.create.request" && message.agent) { + resolveRequest(message.agent); + if (options.block) return; + } + server.send(frame); + }); + server.onMessage((frame) => ws.send(frame)); + }); + return { waitForRequest: () => request }; +} + export async function pressSubmitBeforeTheNextRender(page: Page, name: string): Promise { const create = page.getByRole("button", { name, exact: true }); await expect(create).toBeEnabled();