From d55209b81eb490c876fbd3c17877c8d2636d298e Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 14 Apr 2026 17:01:38 +0700 Subject: [PATCH] fix: stabilize workspace e2e routing and cli helper export --- packages/app/e2e/helpers/terminal-perf.ts | 14 ++++----- packages/app/e2e/terminal-performance.spec.ts | 6 ++-- packages/app/e2e/workspace-cwd.spec.ts | 7 ++--- .../app/e2e/workspace-setup-runtime.spec.ts | 30 ++++++++++++++----- packages/cli/src/commands/agent/run.ts | 2 ++ 5 files changed, 37 insertions(+), 22 deletions(-) diff --git a/packages/app/e2e/helpers/terminal-perf.ts b/packages/app/e2e/helpers/terminal-perf.ts index 520307a33..a47790057 100644 --- a/packages/app/e2e/helpers/terminal-perf.ts +++ b/packages/app/e2e/helpers/terminal-perf.ts @@ -83,14 +83,14 @@ export async function connectTerminalClient(): Promise return client; } -export function buildTerminalWorkspaceUrl(cwd: string, terminalId: string): string { +export function buildTerminalWorkspaceUrl(workspaceId: string, terminalId: string): string { const serverId = getServerId(); - const route = buildHostWorkspaceRoute(serverId, cwd); + const route = buildHostWorkspaceRoute(serverId, workspaceId); return `${route}?open=${encodeURIComponent(`terminal:${terminalId}`)}`; } -function buildWorkspaceUrl(cwd: string): string { - return buildHostWorkspaceRoute(getServerId(), cwd); +function buildWorkspaceUrl(workspaceId: string): string { + return buildHostWorkspaceRoute(getServerId(), workspaceId); } export async function getTerminalBufferText(page: Page): Promise { @@ -129,19 +129,19 @@ export async function waitForTerminalContent( export async function navigateToTerminal( page: Page, - input: { cwd: string; terminalId: string }, + input: { workspaceId: string; terminalId: string }, ): Promise { // Boot the app at the workspace route directly. // The fixtures.ts beforeEach addInitScript seeds localStorage on every navigation, // 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); // The workspace layout consumes `?open=...`, returns null during the effect, // then replaces the URL with the clean workspace route after preparing the tab. // On CI, Expo Router's rootNavigationState may take time to initialize, // so we allow a generous timeout here. - const cleanWorkspaceRoute = buildWorkspaceUrl(input.cwd); + const cleanWorkspaceRoute = buildWorkspaceUrl(input.workspaceId); await page.waitForURL( (url) => url.pathname === cleanWorkspaceRoute && !url.searchParams.has("open"), { timeout: 30_000 }, diff --git a/packages/app/e2e/terminal-performance.spec.ts b/packages/app/e2e/terminal-performance.spec.ts index 912704989..1392ca4ac 100644 --- a/packages/app/e2e/terminal-performance.spec.ts +++ b/packages/app/e2e/terminal-performance.spec.ts @@ -20,6 +20,7 @@ const KEYSTROKE_P95_BUDGET_MS = 150; test.describe("Terminal wire performance", () => { let client: TerminalPerfDaemonClient; let tempRepo: { path: string; cleanup: () => Promise }; + let workspaceId: string; test.beforeAll(async () => { 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 const seedResult = await client.openProject(tempRepo.path); if (!seedResult.workspace) throw new Error(seedResult.error ?? "Failed to seed workspace"); + workspaceId = seedResult.workspace.id; }); test.afterAll(async () => { @@ -48,7 +50,7 @@ test.describe("Terminal wire performance", () => { const terminalId = result.terminal.id; try { - await navigateToTerminal(page, { cwd: tempRepo.path, terminalId }); + await navigateToTerminal(page, { workspaceId, terminalId }); await setupDeterministicPrompt(page); const sentinel = `PERF_DONE_${Date.now()}`; @@ -107,7 +109,7 @@ test.describe("Terminal wire performance", () => { const terminalId = result.terminal.id; try { - await navigateToTerminal(page, { cwd: tempRepo.path, terminalId }); + await navigateToTerminal(page, { workspaceId, terminalId }); await setupDeterministicPrompt(page); // Ensure clean prompt state diff --git a/packages/app/e2e/workspace-cwd.spec.ts b/packages/app/e2e/workspace-cwd.spec.ts index 2a8abe1d3..5090e18ae 100644 --- a/packages/app/e2e/workspace-cwd.spec.ts +++ b/packages/app/e2e/workspace-cwd.spec.ts @@ -95,15 +95,12 @@ test.describe("Workspace cwd correctness", () => { if (!workspaceResult.workspace) { 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 // with direct URL navigation to the 2nd+ workspace. await openHomeWithProject(page, repo.path); - const sidebarWorkspace = page.getByRole("button", { name: workspaceName }); - await expect(sidebarWorkspace).toBeVisible({ timeout: 30_000 }); - await sidebarWorkspace.click(); - await waitForTabBar(page); + await navigateToWorkspaceViaSidebar(page, workspaceId); await clickTerminal(page); diff --git a/packages/app/e2e/workspace-setup-runtime.spec.ts b/packages/app/e2e/workspace-setup-runtime.spec.ts index 0ea8fe84f..3315f6f5a 100644 --- a/packages/app/e2e/workspace-setup-runtime.spec.ts +++ b/packages/app/e2e/workspace-setup-runtime.spec.ts @@ -9,6 +9,24 @@ import { openHomeWithProject, } 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 { + 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.configure({ retries: 1 }); @@ -24,6 +42,7 @@ test.describe("Workspace setup runtime authority", () => { cwd: repo.path, worktreeSlug: `setup-chat-${Date.now()}`, }); + const workspaceId = String(workspace.id); const wsInfo = await findWorktreeWorkspaceForProject(client, 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 await openHomeWithProject(page, repo.path); - const wsButton = page.getByRole("button", { name: workspace.name }); - await expect(wsButton).toBeVisible({ timeout: 30_000 }); - await wsButton.click(); + await navigateToWorkspaceViaSidebar(page, workspaceId); await expect(page).toHaveURL(/\/workspace\//, { timeout: 30_000 }); } finally { await client.close(); @@ -61,16 +78,13 @@ test.describe("Workspace setup runtime authority", () => { throw new Error(result.error ?? "Failed to create workspace"); } 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 // navigation for freshly created worktree workspaces can race with // Expo Router hydration, so we use the sidebar which is authoritative). await openHomeWithProject(page, repo.path); - const sidebarWorkspace = page.getByRole("button", { name: workspaceName }); - await expect(sidebarWorkspace).toBeVisible({ timeout: 30_000 }); - await sidebarWorkspace.click(); - await waitForTabBar(page); + await navigateToWorkspaceViaSidebar(page, workspaceId); await clickTerminal(page); diff --git a/packages/cli/src/commands/agent/run.ts b/packages/cli/src/commands/agent/run.ts index be0f73b79..adbbfb6a9 100644 --- a/packages/cli/src/commands/agent/run.ts +++ b/packages/cli/src/commands/agent/run.ts @@ -18,6 +18,8 @@ import { parseDuration } from "../../utils/duration.js"; import { collectMultiple } from "../../utils/command-options.js"; import { resolveProviderAndModel } from "../../utils/provider-model.js"; +export { resolveProviderAndModel } from "../../utils/provider-model.js"; + export function addRunOptions(cmd: Command): Command { return cmd .description("Create and start an agent with a task")