import { existsSync } from "node:fs"; import path from "node:path"; import { buildHostWorkspaceRoute } from "@/utils/host-routes"; import { expect, test, type Page } from "./fixtures"; import { gotoAppShell } from "./helpers/app"; import { archiveWorkspaceFromDaemon, archiveLocalWorkspaceFromDaemon, assertNewWorkspaceSidebarAndHeader, clickNewWorkspaceButton, connectNewWorkspaceDaemonClient, createWorktreeViaDaemon, expectComposerGithubAttachmentPill, expectStartingRefPickerTriggerPr, openNewWorkspaceComposer, openStartingRefPicker, openProjectViaDaemon, selectBranchInPicker, selectGitHubPrInPicker, } from "./helpers/new-workspace"; import { createTempGitRepo, readWorktreeBranchInfo } from "./helpers/workspace"; import { expectSidebarWorkspaceSelected, expectWorkspaceHeader, switchWorkspaceViaSidebar, waitForSidebarHydration, waitForWorkspaceInSidebar, workspaceLabelFromPath, } from "./helpers/workspace-ui"; type WebSocketMessage = string | Buffer; 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; } function getStringField(input: Record, key: string): string | null { const value = input[key]; return typeof value === "string" ? value : null; } async function delayBrowserAgentCreatedStatus(page: Page) { const daemonPort = process.env.E2E_DAEMON_PORT; if (!daemonPort) { throw new Error("E2E_DAEMON_PORT is not set."); } const daemonPortPattern = new RegExp(`:${daemonPort.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`); const createRequestIds = new Set(); const delayedForwards: Array<() => void> = []; let releaseRequested = false; let resolveCreateRequest: (() => void) | null = null; let resolveDelayedCreatedStatus: (() => void) | null = null; const createRequestSeen = new Promise((resolve) => { resolveCreateRequest = resolve; }); const delayedCreatedStatusSeen = new Promise((resolve) => { resolveDelayedCreatedStatus = resolve; }); await page.routeWebSocket(daemonPortPattern, (ws) => { const server = ws.connectToServer(); ws.onMessage((message) => { const sessionMessage = getSessionMessage(message); if (sessionMessage?.type === "create_agent_request") { const requestId = getStringField(sessionMessage, "requestId"); if (requestId) { createRequestIds.add(requestId); resolveCreateRequest?.(); } } server.send(message); }); server.onMessage((message) => { const sessionMessage = getSessionMessage(message); const payload = sessionMessage?.type === "status" && typeof sessionMessage.payload === "object" ? (sessionMessage.payload as Record) : null; const requestId = payload ? getStringField(payload, "requestId") : null; if (payload?.status === "agent_created" && requestId && createRequestIds.has(requestId)) { resolveDelayedCreatedStatus?.(); if (releaseRequested) { ws.send(message); return; } delayedForwards.push(() => ws.send(message)); return; } ws.send(message); }); }); return { release() { releaseRequested = true; for (const forward of delayedForwards.splice(0)) { forward(); } }, waitForCreateRequest: () => createRequestSeen, waitForDelayedCreatedStatus: () => delayedCreatedStatusSeen, }; } test.describe("New workspace flow", () => { let client: Awaited>; const localWorkspaceIds = new Set(); const createdWorktreeIds = new Set(); test.describe.configure({ timeout: 240_000 }); test.beforeEach(async () => { client = await connectNewWorkspaceDaemonClient(); }); test.afterEach(async () => { if (client) { for (const workspaceId of createdWorktreeIds) { await archiveWorkspaceFromDaemon(client, workspaceId).catch(() => undefined); } for (const workspaceId of localWorkspaceIds) { await archiveLocalWorkspaceFromDaemon(client, workspaceId).catch(() => undefined); } } createdWorktreeIds.clear(); localWorkspaceIds.clear(); await client?.close().catch(() => undefined); }); test("sidebar workspace navigation updates URL and header", async ({ page }) => { const serverId = process.env.E2E_SERVER_ID; if (!serverId) { throw new Error("E2E_SERVER_ID is not set."); } const firstRepo = await createTempGitRepo("workspace-nav-a-"); const secondRepo = await createTempGitRepo("workspace-nav-b-"); try { const firstWorkspace = await openProjectViaDaemon(client, firstRepo.path); const secondWorkspace = await openProjectViaDaemon(client, secondRepo.path); localWorkspaceIds.add(firstWorkspace.workspaceId); localWorkspaceIds.add(secondWorkspace.workspaceId); await gotoAppShell(page); await waitForSidebarHydration(page); await switchWorkspaceViaSidebar({ page, serverId, targetWorkspacePath: firstWorkspace.workspaceId, }); await expectWorkspaceHeader(page, { title: firstWorkspace.workspaceName, subtitle: firstWorkspace.projectDisplayName, }); await switchWorkspaceViaSidebar({ page, serverId, targetWorkspacePath: secondWorkspace.workspaceId, }); await waitForWorkspaceInSidebar(page, { serverId, workspaceId: secondWorkspace.workspaceId, }); await expectWorkspaceHeader(page, { title: secondWorkspace.workspaceName, subtitle: secondWorkspace.projectDisplayName, }); await switchWorkspaceViaSidebar({ page, serverId, targetWorkspacePath: firstWorkspace.workspaceId, }); await expectWorkspaceHeader(page, { title: firstWorkspace.workspaceName, subtitle: firstWorkspace.projectDisplayName, }); } finally { await secondRepo.cleanup(); await firstRepo.cleanup(); } }); test("same-project workspaces switch content without requiring refresh", async ({ page }) => { const serverId = process.env.E2E_SERVER_ID; if (!serverId) { throw new Error("E2E_SERVER_ID is not set."); } const repo = await createTempGitRepo("workspace-nav-same-project-"); try { const rootWorkspace = await openProjectViaDaemon(client, repo.path); const worktreeWorkspace = await createWorktreeViaDaemon(client, { cwd: repo.path, slug: `nav-${Date.now()}`, }); localWorkspaceIds.add(rootWorkspace.workspaceId); createdWorktreeIds.add(worktreeWorkspace.workspaceId); await gotoAppShell(page); await waitForSidebarHydration(page); await switchWorkspaceViaSidebar({ page, serverId, targetWorkspacePath: rootWorkspace.workspaceId, }); await expectWorkspaceHeader(page, { title: rootWorkspace.workspaceName, subtitle: rootWorkspace.projectDisplayName, }); await expectSidebarWorkspaceSelected({ page, serverId, workspaceId: rootWorkspace.workspaceId, }); await switchWorkspaceViaSidebar({ page, serverId, targetWorkspacePath: worktreeWorkspace.workspaceId, }); await expectWorkspaceHeader(page, { title: worktreeWorkspace.workspaceName, subtitle: worktreeWorkspace.projectDisplayName, }); await expectSidebarWorkspaceSelected({ page, serverId, workspaceId: worktreeWorkspace.workspaceId, }); await expectSidebarWorkspaceSelected({ page, serverId, workspaceId: rootWorkspace.workspaceId, selected: false, }); await switchWorkspaceViaSidebar({ page, serverId, targetWorkspacePath: rootWorkspace.workspaceId, }); await expectWorkspaceHeader(page, { title: rootWorkspace.workspaceName, subtitle: rootWorkspace.projectDisplayName, }); await expectSidebarWorkspaceSelected({ page, serverId, workspaceId: rootWorkspace.workspaceId, }); await expectSidebarWorkspaceSelected({ page, serverId, workspaceId: worktreeWorkspace.workspaceId, selected: false, }); } finally { await repo.cleanup(); } }); test("clicking new workspace redirects, renders header, shows sidebar row, and keeps one agent tab", async ({ page, }) => { const serverId = process.env.E2E_SERVER_ID; if (!serverId) { throw new Error("E2E_SERVER_ID is not set."); } const tempRepo = await createTempGitRepo("new-workspace-"); try { const openedProject = await openProjectViaDaemon(client, tempRepo.path); localWorkspaceIds.add(openedProject.workspaceId); await gotoAppShell(page); await waitForSidebarHydration(page); await switchWorkspaceViaSidebar({ page, serverId, targetWorkspacePath: openedProject.workspaceId, }); await expectWorkspaceHeader(page, { title: openedProject.workspaceName, subtitle: openedProject.projectDisplayName, }); await clickNewWorkspaceButton(page, { projectKey: openedProject.projectKey, projectDisplayName: openedProject.projectDisplayName, }); const createdWorkspace = await assertNewWorkspaceSidebarAndHeader(page, { serverId, previousWorkspaceId: openedProject.workspaceId, projectDisplayName: openedProject.projectDisplayName, }); createdWorktreeIds.add(createdWorkspace.workspaceId); expect(createdWorkspace.workspaceId).not.toBe(openedProject.workspaceId); await expect(page).toHaveURL( buildHostWorkspaceRoute(serverId, createdWorkspace.workspaceId), { timeout: 30_000, }, ); const createdWorkspaceRow = page.getByTestId( `sidebar-workspace-row-${serverId}:${createdWorkspace.workspaceId}`, ); await expect(createdWorkspaceRow).toBeVisible({ timeout: 30_000 }); await expectWorkspaceHeader(page, { title: workspaceLabelFromPath(createdWorkspace.workspaceId), subtitle: openedProject.projectDisplayName, }); const activeWorkspaceDeckEntry = page .getByTestId(`workspace-deck-entry-${serverId}:${createdWorkspace.workspaceId}`) .filter({ visible: true }); await expect(activeWorkspaceDeckEntry).toBeVisible({ timeout: 30_000 }); const agentTabs = activeWorkspaceDeckEntry.locator('[data-testid^="workspace-tab-agent_"]'); await expect(agentTabs).toHaveCount(1, { timeout: 30_000 }); // Workspace setup may auto-open a setup tab that steals focus, // hiding the agent panel (display:none removes it from the // accessibility tree). Click the agent tab to ensure it's active. await agentTabs.first().click(); const composer = page.getByRole("textbox", { name: "Message agent..." }); await expect(composer).toBeVisible({ timeout: 30_000 }); } finally { await tempRepo.cleanup(); } }); test("redirects to the optimistic draft tab before agent creation resolves", async ({ page }) => { const serverId = process.env.E2E_SERVER_ID; if (!serverId) { throw new Error("E2E_SERVER_ID is not set."); } const tempRepo = await createTempGitRepo("new-workspace-optimistic-"); const agentCreatedDelay = await delayBrowserAgentCreatedStatus(page); try { const openedProject = await openProjectViaDaemon(client, tempRepo.path); localWorkspaceIds.add(openedProject.workspaceId); await gotoAppShell(page); await waitForSidebarHydration(page); await switchWorkspaceViaSidebar({ page, serverId, targetWorkspacePath: openedProject.workspaceId, }); await expectWorkspaceHeader(page, { title: openedProject.workspaceName, subtitle: openedProject.projectDisplayName, }); await openNewWorkspaceComposer(page, { projectKey: openedProject.projectKey, projectDisplayName: openedProject.projectDisplayName, }); const createButton = page .getByTestId("message-input-root") .getByRole("button", { name: "Create" }); await expect(createButton).toBeVisible({ timeout: 30_000 }); await createButton.click(); await agentCreatedDelay.waitForCreateRequest(); await agentCreatedDelay.waitForDelayedCreatedStatus(); const createdWorkspace = await assertNewWorkspaceSidebarAndHeader(page, { serverId, previousWorkspaceId: openedProject.workspaceId, projectDisplayName: openedProject.projectDisplayName, }); createdWorktreeIds.add(createdWorkspace.workspaceId); await expect(page).toHaveURL( buildHostWorkspaceRoute(serverId, createdWorkspace.workspaceId), { timeout: 30_000, }, ); const activeWorkspaceDeckEntry = page .getByTestId(`workspace-deck-entry-${serverId}:${createdWorkspace.workspaceId}`) .filter({ visible: true }); await expect(activeWorkspaceDeckEntry).toBeVisible({ timeout: 30_000 }); const draftTabs = activeWorkspaceDeckEntry.locator('[data-testid^="workspace-tab-draft_"]'); await expect(draftTabs).toHaveCount(1, { timeout: 30_000 }); await expect( activeWorkspaceDeckEntry.locator('[data-testid^="workspace-tab-agent_"]'), ).toHaveCount(0); agentCreatedDelay.release(); await expect( activeWorkspaceDeckEntry.locator('[data-testid^="workspace-tab-agent_"]'), ).toHaveCount(1, { timeout: 30_000 }); } finally { agentCreatedDelay.release(); await tempRepo.cleanup(); } }); test("selected branch becomes the base of a new workspace worktree", async ({ page }) => { const serverId = process.env.E2E_SERVER_ID; if (!serverId) { throw new Error("E2E_SERVER_ID is not set."); } const tempRepo = await createTempGitRepo("new-workspace-ref-", { branches: ["main", "dev"], }); try { const openedProject = await openProjectViaDaemon(client, tempRepo.path); localWorkspaceIds.add(openedProject.workspaceId); await gotoAppShell(page); await waitForSidebarHydration(page); await switchWorkspaceViaSidebar({ page, serverId, targetWorkspacePath: openedProject.workspaceId, }); await expectWorkspaceHeader(page, { title: openedProject.workspaceName, subtitle: openedProject.projectDisplayName, }); await openNewWorkspaceComposer(page, { projectKey: openedProject.projectKey, projectDisplayName: openedProject.projectDisplayName, }); await openStartingRefPicker(page); await selectBranchInPicker(page, "dev"); const createButton = page .getByTestId("message-input-root") .getByRole("button", { name: "Create" }); await expect(createButton).toBeVisible({ timeout: 30_000 }); await createButton.click(); const createdWorkspace = await assertNewWorkspaceSidebarAndHeader(page, { serverId, previousWorkspaceId: openedProject.workspaceId, projectDisplayName: openedProject.projectDisplayName, }); createdWorktreeIds.add(createdWorkspace.workspaceId); expect(existsSync(createdWorkspace.workspaceId)).toBe(true); const branchInfo = await readWorktreeBranchInfo({ worktreePath: createdWorkspace.workspaceId, }); expect(branchInfo.currentBranch).toBe(path.basename(createdWorkspace.workspaceId)); expect(branchInfo.hasAncestor(tempRepo.branchHeads.main)).toBe(true); expect(branchInfo.hasAncestor(tempRepo.branchHeads.dev)).toBe(true); } finally { await tempRepo.cleanup(); } }); test("selected GitHub PR shows PR context in the trigger and composer", async ({ page }) => { const tempRepo = await createTempGitRepo("new-workspace-pr-ref-"); try { const openedProject = await openProjectViaDaemon(client, tempRepo.path); localWorkspaceIds.add(openedProject.workspaceId); await gotoAppShell(page); await waitForSidebarHydration(page); await openNewWorkspaceComposer(page, { projectKey: openedProject.projectKey, projectDisplayName: openedProject.projectDisplayName, }); await openStartingRefPicker(page); await selectGitHubPrInPicker(page, 515); await expectStartingRefPickerTriggerPr(page, { number: 515, title: "Review selected start ref", headRef: "feature/start-from-pr", }); await expectComposerGithubAttachmentPill(page, { number: 515, title: "Review selected start ref", }); } finally { await tempRepo.cleanup(); } }); });