* fix(app): validate persisted client state Persisted replica data could satisfy TypeScript through an assertion while missing fields required by renderers. Treat every AsyncStorage value as untrusted input, clear values that fail strict schemas, and version the replica DTO so incompatible caches cannot hydrate. * fix(app): preserve validated legacy state * fix(app): preserve validated preferences Preserve supported legacy location fields and every shipped theme while keeping unknown persisted fields rejected. Align browser test seeds with the current preference shape so strict validation exercises the intended provider. * fix(app): migrate legacy agent preferences Released clients stored host/location fields and per-provider thinkingOptionId together. Migrate those exact strict bytes into current preferences without admitting unknown fields. * fix(app): preserve drafts with legacy reviews Accept the exact strict shape released for workspace review attachments so migration can discard that obsolete attachment without clearing every persisted draft.
235 lines
7.9 KiB
TypeScript
235 lines
7.9 KiB
TypeScript
import { expect, test, type Page } from "../support/fixtures";
|
|
import type { FormPreferences } from "@/create-agent-preferences/preferences";
|
|
import { gotoAppShell } from "../support/helpers/app";
|
|
import { daemonWsRoutePattern } from "../support/helpers/daemon-port";
|
|
import { openAgentRoute } from "../support/helpers/mock-agent";
|
|
import {
|
|
openGlobalNewWorkspaceComposer,
|
|
selectNewWorkspaceProject,
|
|
submitNewWorkspacePrompt,
|
|
} from "../support/helpers/new-workspace";
|
|
import { expectNoTruncation } from "../support/helpers/no-truncation";
|
|
import { escapeRegex } from "../support/helpers/regex";
|
|
import { seedWorkspace } from "../support/helpers/seed-client";
|
|
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<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): Promise<void> {
|
|
await page.addInitScript(
|
|
({ preferencesKey }) => {
|
|
localStorage.setItem(
|
|
preferencesKey,
|
|
JSON.stringify({
|
|
provider: "codex",
|
|
providerPreferences: {
|
|
codex: {
|
|
model: "gpt-5.4-mini",
|
|
mode: "auto",
|
|
thinkingByModel: {
|
|
"gpt-5.4-mini": "low",
|
|
},
|
|
},
|
|
mock: {
|
|
model: "ten-second-stream",
|
|
},
|
|
},
|
|
} satisfies FormPreferences),
|
|
);
|
|
},
|
|
{ preferencesKey: CREATE_AGENT_PREFERENCES_KEY },
|
|
);
|
|
}
|
|
|
|
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.getByRole("button", { name: /^Select agent mode \(/ });
|
|
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 seeded = await seedWorkspace({ repoPrefix: "codex-mode-preferences-" });
|
|
const createAgentRecorder = await recordAndBlockCreateAgentRequests(page);
|
|
await seedCodexDefaultPermissionPreferences(page);
|
|
|
|
try {
|
|
await gotoAppShell(page);
|
|
await waitForSidebarHydration(page);
|
|
await openGlobalNewWorkspaceComposer(page);
|
|
await selectNewWorkspaceProject(page, {
|
|
projectKey: seeded.projectKey,
|
|
projectDisplayName: seeded.projectDisplayName,
|
|
});
|
|
|
|
await expect(
|
|
page.getByRole("button", { name: "Select agent mode (Default permissions)" }),
|
|
).toBeVisible({ timeout: 30_000 });
|
|
await expectThinkingOptionsFit(page);
|
|
await selectMode(page, "Full access");
|
|
await expect(
|
|
page.getByRole("button", { name: "Select agent mode (Full access)" }),
|
|
).toBeVisible();
|
|
|
|
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 seeded = await seedWorkspace({ repoPrefix: "codex-live-mode-preferences-" });
|
|
await seedCodexDefaultPermissionPreferences(page);
|
|
|
|
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.getByRole("button", { name: "Select agent mode (Default permissions)" }),
|
|
).toBeVisible({ timeout: 30_000 });
|
|
|
|
await selectMode(page, "Full access");
|
|
await expect(
|
|
page.getByRole("button", { name: "Select agent mode (Full access)" }),
|
|
).toBeVisible({ timeout: 30_000 });
|
|
|
|
await openGlobalNewWorkspaceComposer(page);
|
|
await selectNewWorkspaceProject(page, {
|
|
projectKey: seeded.projectKey,
|
|
projectDisplayName: seeded.projectDisplayName,
|
|
});
|
|
|
|
await expect(
|
|
page.getByRole("button", { name: "Select agent mode (Full access)" }),
|
|
).toBeVisible({ timeout: 30_000 });
|
|
} finally {
|
|
await seeded.cleanup();
|
|
}
|
|
});
|
|
});
|