fix: stabilize workspace e2e routing and cli helper export

This commit is contained in:
Mohamed Boudra
2026-04-14 17:01:38 +07:00
parent 0f0efa1707
commit d55209b81e
5 changed files with 37 additions and 22 deletions
+7 -7
View File
@@ -83,14 +83,14 @@ export async function connectTerminalClient(): Promise<TerminalPerfDaemonClient>
return client; return client;
} }
export function buildTerminalWorkspaceUrl(cwd: string, terminalId: string): string { export function buildTerminalWorkspaceUrl(workspaceId: string, terminalId: string): string {
const serverId = getServerId(); const serverId = getServerId();
const route = buildHostWorkspaceRoute(serverId, cwd); const route = buildHostWorkspaceRoute(serverId, workspaceId);
return `${route}?open=${encodeURIComponent(`terminal:${terminalId}`)}`; return `${route}?open=${encodeURIComponent(`terminal:${terminalId}`)}`;
} }
function buildWorkspaceUrl(cwd: string): string { function buildWorkspaceUrl(workspaceId: string): string {
return buildHostWorkspaceRoute(getServerId(), cwd); return buildHostWorkspaceRoute(getServerId(), workspaceId);
} }
export async function getTerminalBufferText(page: Page): Promise<string> { export async function getTerminalBufferText(page: Page): Promise<string> {
@@ -129,19 +129,19 @@ export async function waitForTerminalContent(
export async function navigateToTerminal( export async function navigateToTerminal(
page: Page, page: Page,
input: { cwd: string; terminalId: string }, input: { workspaceId: string; terminalId: string },
): Promise<void> { ): Promise<void> {
// Boot the app at the workspace route directly. // Boot the app at the workspace route directly.
// The fixtures.ts beforeEach addInitScript seeds localStorage on every navigation, // The fixtures.ts beforeEach addInitScript seeds localStorage on every navigation,
// so the daemon registry is already configured when the app starts. // so the daemon registry is already configured when the app starts.
const workspaceRoute = buildTerminalWorkspaceUrl(input.cwd, input.terminalId); const workspaceRoute = buildTerminalWorkspaceUrl(input.workspaceId, input.terminalId);
await page.goto(workspaceRoute); await page.goto(workspaceRoute);
// The workspace layout consumes `?open=...`, returns null during the effect, // The workspace layout consumes `?open=...`, returns null during the effect,
// then replaces the URL with the clean workspace route after preparing the tab. // then replaces the URL with the clean workspace route after preparing the tab.
// On CI, Expo Router's rootNavigationState may take time to initialize, // On CI, Expo Router's rootNavigationState may take time to initialize,
// so we allow a generous timeout here. // so we allow a generous timeout here.
const cleanWorkspaceRoute = buildWorkspaceUrl(input.cwd); const cleanWorkspaceRoute = buildWorkspaceUrl(input.workspaceId);
await page.waitForURL( await page.waitForURL(
(url) => url.pathname === cleanWorkspaceRoute && !url.searchParams.has("open"), (url) => url.pathname === cleanWorkspaceRoute && !url.searchParams.has("open"),
{ timeout: 30_000 }, { timeout: 30_000 },
@@ -20,6 +20,7 @@ const KEYSTROKE_P95_BUDGET_MS = 150;
test.describe("Terminal wire performance", () => { test.describe("Terminal wire performance", () => {
let client: TerminalPerfDaemonClient; let client: TerminalPerfDaemonClient;
let tempRepo: { path: string; cleanup: () => Promise<void> }; let tempRepo: { path: string; cleanup: () => Promise<void> };
let workspaceId: string;
test.beforeAll(async () => { test.beforeAll(async () => {
tempRepo = await createTempGitRepo("perf-"); tempRepo = await createTempGitRepo("perf-");
@@ -27,6 +28,7 @@ test.describe("Terminal wire performance", () => {
// Seed the workspace in the daemon so the app can resolve the path // Seed the workspace in the daemon so the app can resolve the path
const seedResult = await client.openProject(tempRepo.path); const seedResult = await client.openProject(tempRepo.path);
if (!seedResult.workspace) throw new Error(seedResult.error ?? "Failed to seed workspace"); if (!seedResult.workspace) throw new Error(seedResult.error ?? "Failed to seed workspace");
workspaceId = seedResult.workspace.id;
}); });
test.afterAll(async () => { test.afterAll(async () => {
@@ -48,7 +50,7 @@ test.describe("Terminal wire performance", () => {
const terminalId = result.terminal.id; const terminalId = result.terminal.id;
try { try {
await navigateToTerminal(page, { cwd: tempRepo.path, terminalId }); await navigateToTerminal(page, { workspaceId, terminalId });
await setupDeterministicPrompt(page); await setupDeterministicPrompt(page);
const sentinel = `PERF_DONE_${Date.now()}`; const sentinel = `PERF_DONE_${Date.now()}`;
@@ -107,7 +109,7 @@ test.describe("Terminal wire performance", () => {
const terminalId = result.terminal.id; const terminalId = result.terminal.id;
try { try {
await navigateToTerminal(page, { cwd: tempRepo.path, terminalId }); await navigateToTerminal(page, { workspaceId, terminalId });
await setupDeterministicPrompt(page); await setupDeterministicPrompt(page);
// Ensure clean prompt state // Ensure clean prompt state
+2 -5
View File
@@ -95,15 +95,12 @@ test.describe("Workspace cwd correctness", () => {
if (!workspaceResult.workspace) { if (!workspaceResult.workspace) {
throw new Error(workspaceResult.error ?? `Failed to open project ${worktreePath}`); throw new Error(workspaceResult.error ?? `Failed to open project ${worktreePath}`);
} }
const workspaceName = workspaceResult.workspace.name; const workspaceId = String(workspaceResult.workspace.id);
// Use sidebar navigation to avoid Expo Router hydration issues // Use sidebar navigation to avoid Expo Router hydration issues
// with direct URL navigation to the 2nd+ workspace. // with direct URL navigation to the 2nd+ workspace.
await openHomeWithProject(page, repo.path); await openHomeWithProject(page, repo.path);
const sidebarWorkspace = page.getByRole("button", { name: workspaceName }); await navigateToWorkspaceViaSidebar(page, workspaceId);
await expect(sidebarWorkspace).toBeVisible({ timeout: 30_000 });
await sidebarWorkspace.click();
await waitForTabBar(page);
await clickTerminal(page); await clickTerminal(page);
@@ -9,6 +9,24 @@ import {
openHomeWithProject, openHomeWithProject,
} from "./helpers/workspace-setup"; } from "./helpers/workspace-setup";
function getServerId(): string {
const serverId = process.env.E2E_SERVER_ID;
if (!serverId) {
throw new Error("E2E_SERVER_ID is not set.");
}
return serverId;
}
async function navigateToWorkspaceViaSidebar(
page: import("@playwright/test").Page,
workspaceId: string,
): Promise<void> {
const row = page.getByTestId(`sidebar-workspace-row-${getServerId()}:${workspaceId}`);
await expect(row).toBeVisible({ timeout: 30_000 });
await row.click();
await waitForTabBar(page);
}
test.describe("Workspace setup runtime authority", () => { test.describe("Workspace setup runtime authority", () => {
test.describe.configure({ retries: 1 }); test.describe.configure({ retries: 1 });
@@ -24,6 +42,7 @@ test.describe("Workspace setup runtime authority", () => {
cwd: repo.path, cwd: repo.path,
worktreeSlug: `setup-chat-${Date.now()}`, worktreeSlug: `setup-chat-${Date.now()}`,
}); });
const workspaceId = String(workspace.id);
const wsInfo = await findWorktreeWorkspaceForProject(client, repo.path); const wsInfo = await findWorktreeWorkspaceForProject(client, repo.path);
expect(wsInfo.workspaceDirectory).not.toBe(repo.path); expect(wsInfo.workspaceDirectory).not.toBe(repo.path);
@@ -31,9 +50,7 @@ test.describe("Workspace setup runtime authority", () => {
// Navigate to the workspace via sidebar // Navigate to the workspace via sidebar
await openHomeWithProject(page, repo.path); await openHomeWithProject(page, repo.path);
const wsButton = page.getByRole("button", { name: workspace.name }); await navigateToWorkspaceViaSidebar(page, workspaceId);
await expect(wsButton).toBeVisible({ timeout: 30_000 });
await wsButton.click();
await expect(page).toHaveURL(/\/workspace\//, { timeout: 30_000 }); await expect(page).toHaveURL(/\/workspace\//, { timeout: 30_000 });
} finally { } finally {
await client.close(); await client.close();
@@ -61,16 +78,13 @@ test.describe("Workspace setup runtime authority", () => {
throw new Error(result.error ?? "Failed to create workspace"); throw new Error(result.error ?? "Failed to create workspace");
} }
const workspaceDir = result.workspace.workspaceDirectory; const workspaceDir = result.workspace.workspaceDirectory;
const workspaceName = result.workspace.name; const workspaceId = String(result.workspace.id);
// Navigate to the worktree workspace via sidebar click (direct URL // Navigate to the worktree workspace via sidebar click (direct URL
// navigation for freshly created worktree workspaces can race with // navigation for freshly created worktree workspaces can race with
// Expo Router hydration, so we use the sidebar which is authoritative). // Expo Router hydration, so we use the sidebar which is authoritative).
await openHomeWithProject(page, repo.path); await openHomeWithProject(page, repo.path);
const sidebarWorkspace = page.getByRole("button", { name: workspaceName }); await navigateToWorkspaceViaSidebar(page, workspaceId);
await expect(sidebarWorkspace).toBeVisible({ timeout: 30_000 });
await sidebarWorkspace.click();
await waitForTabBar(page);
await clickTerminal(page); await clickTerminal(page);
+2
View File
@@ -18,6 +18,8 @@ import { parseDuration } from "../../utils/duration.js";
import { collectMultiple } from "../../utils/command-options.js"; import { collectMultiple } from "../../utils/command-options.js";
import { resolveProviderAndModel } from "../../utils/provider-model.js"; import { resolveProviderAndModel } from "../../utils/provider-model.js";
export { resolveProviderAndModel } from "../../utils/provider-model.js";
export function addRunOptions(cmd: Command): Command { export function addRunOptions(cmd: Command): Command {
return cmd return cmd
.description("Create and start an agent with a task") .description("Create and start an agent with a task")