Files
paseo/packages/app/e2e/new-workspace-codex-mode-preferences.spec.ts
2026-07-29 02:13:13 +00:00

236 lines
7.8 KiB
TypeScript

import { expect, test, type Page } from "./fixtures";
import { gotoAppShell } from "./helpers/app";
import { daemonWsRoutePattern } from "./helpers/daemon-port";
import { openAgentRoute } from "./helpers/mock-agent";
import {
openGlobalNewWorkspaceComposer,
selectNewWorkspaceProject,
submitNewWorkspacePrompt,
} from "./helpers/new-workspace";
import { expectNoTruncation } from "./helpers/no-truncation";
import { escapeRegex } from "./helpers/regex";
import { seedWorkspace } from "./helpers/seed-client";
import { getServerId } from "./helpers/server-id";
import { waitForSidebarHydration } from "./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<string, unknown> | 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<string, unknown>;
}
async function seedCodexDefaultPermissionPreferences(page: Page, serverId: string): Promise<void> {
await page.addInitScript(
({ preferencesKey, serverId: seededServerId }) => {
localStorage.setItem(
preferencesKey,
JSON.stringify({
serverId: seededServerId,
provider: "codex",
providerPreferences: {
codex: {
model: "gpt-5.4-mini",
mode: "auto",
thinkingByModel: {
"gpt-5.4-mini": "low",
},
},
mock: {
model: "ten-second-stream",
},
},
}),
);
},
{ preferencesKey: CREATE_AGENT_PREFERENCES_KEY, serverId },
);
}
async function readCodexModePreference(page: Page): Promise<unknown> {
return page.evaluate((preferencesKey) => {
const raw = localStorage.getItem(preferencesKey);
if (!raw) return null;
const parsed = JSON.parse(raw) as {
providerPreferences?: Record<string, { mode?: unknown }>;
};
return parsed.providerPreferences?.codex?.mode ?? null;
}, CREATE_AGENT_PREFERENCES_KEY);
}
async function selectMode(page: Page, label: string): Promise<void> {
const modeControl = page.getByTestId("mode-control").first();
await expect(modeControl).toBeVisible({ timeout: 30_000 });
await modeControl.click();
const popup = page.getByTestId("combobox-desktop-container").last();
await expect(popup).toBeVisible({ timeout: 10_000 });
await expectNoTruncation(popup);
const searchInput = page.getByRole("textbox", { name: /search mode/i });
await expect(searchInput).toBeVisible({ timeout: 10_000 });
await searchInput.fill(label);
const option = popup.getByText(new RegExp(`^${escapeRegex(label)}$`, "i")).first();
await expect(option).toBeVisible({ timeout: 10_000 });
await option.click({ force: true });
await expect(searchInput).not.toBeVisible({ timeout: 5_000 });
}
async function expectThinkingOptionsFit(page: Page): Promise<void> {
const thinkingControl = page.getByTestId("agent-thinking-selector").first();
await expect(thinkingControl).toBeVisible({ timeout: 30_000 });
await thinkingControl.click();
const popup = page.getByTestId("combobox-desktop-container").last();
await expect(popup).toBeVisible({ timeout: 10_000 });
await expectNoTruncation(popup);
await page.keyboard.press("Escape");
await expect(page.getByTestId("combobox-desktop-container")).toHaveCount(0, { timeout: 5_000 });
}
async function recordAndBlockCreateAgentRequests(page: Page): Promise<{
waitForCreateAgentRequest(): Promise<CreateAgentRequestMessage>;
}> {
let resolveRequest: ((message: CreateAgentRequestMessage) => void) | null = null;
const createAgentSeen = new Promise<CreateAgentRequestMessage>((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,
}) => {
const serverId = getServerId();
const seeded = await seedWorkspace({ repoPrefix: "codex-mode-preferences-" });
const createAgentRecorder = await recordAndBlockCreateAgentRequests(page);
await seedCodexDefaultPermissionPreferences(page, serverId);
try {
await gotoAppShell(page);
await waitForSidebarHydration(page);
await openGlobalNewWorkspaceComposer(page);
await selectNewWorkspaceProject(page, {
projectKey: seeded.projectKey,
projectDisplayName: seeded.projectDisplayName,
});
await expect(page.getByTestId("mode-control").first()).toContainText("Default permissions", {
timeout: 30_000,
});
await expectThinkingOptionsFit(page);
await selectMode(page, "Full access");
await expect(page.getByTestId("mode-control").first()).toContainText("Full access");
await submitNewWorkspacePrompt(page, "Keep Codex full access selected globally.");
const createAgentRequest = await createAgentRecorder.waitForCreateAgentRequest();
expect(createAgentRequest.config).toMatchObject({
provider: "codex",
modeId: "full-access",
});
await expect
.poll(() => readCodexModePreference(page), { timeout: 10_000 })
.toBe("full-access");
} finally {
await seeded.cleanup();
}
});
test("uses the live Codex agent mode as the next New Workspace default", async ({ page }) => {
const serverId = getServerId();
const seeded = await seedWorkspace({ repoPrefix: "codex-live-mode-preferences-" });
await seedCodexDefaultPermissionPreferences(page, serverId);
try {
const agent = await seeded.client.createAgent({
provider: "codex",
cwd: seeded.repoPath,
workspaceId: seeded.workspaceId,
title: "Codex live mode preference e2e",
modeId: "auto",
model: "gpt-5.4-mini",
});
await openAgentRoute(page, {
workspaceId: seeded.workspaceId,
agentId: agent.id,
});
await expect(page.getByTestId("mode-control").first()).toContainText("Default permissions", {
timeout: 30_000,
});
await selectMode(page, "Full access");
await expect(page.getByTestId("mode-control").first()).toContainText("Full access", {
timeout: 30_000,
});
await openGlobalNewWorkspaceComposer(page);
await selectNewWorkspaceProject(page, {
projectKey: seeded.projectKey,
projectDisplayName: seeded.projectDisplayName,
});
await expect(page.getByTestId("mode-control").first()).toContainText("Full access", {
timeout: 30_000,
});
} finally {
await seeded.cleanup();
}
});
});