import { daemonTest, test, expect } from "../support/fixtures"; import { createTempGitRepo } from "../support/helpers/workspace"; import { closeSetupTab, waitForWorkspaceTabsVisible, expectFailedSetupTabSeededInMainPane, expectSetupTabNotSeeded, expectNoTerminalTabs, clickFirstTerminalTab, expectFirstTerminalTabContains, } from "../support/helpers/workspace-tabs"; import { clickNewChat } from "../support/helpers/launcher"; import { expectComposerVisible } from "../support/helpers/composer"; import { openFileExplorer, expectExplorerEntryVisible } from "../support/helpers/file-explorer"; import { expectTerminalSurfaceVisible, waitForTerminalAttached, } from "../support/helpers/terminal-perf"; import { connectWorkspaceSetupClient, setupCommandOutput, createWorkspaceThroughDaemon, expectSetupPanel, openHomeWithProject, navigateToWorkspaceViaSidebar, leaveWorkspaceViaHistory, openWorkspaceScriptsMenu, startWorkspaceScriptFromMenu, closeWorkspaceScriptsMenu, seedProjectForWorkspaceSetup, waitForWorkspaceSetupProgress, observeSetupPanelTraffic, } from "../support/helpers/workspace-setup"; interface WorkspaceScriptStarter { startWorkspaceScript( workspaceId: string, scriptName: string, ): Promise<{ workspaceId: string; scriptName: string; terminalId: string | null; error: string | null; }>; } test.describe("Workspace setup streaming", () => { test("does not seed the setup tab while workspace setup is running", async ({ page }) => { const client = await connectWorkspaceSetupClient(); const repo = await createTempGitRepo("setup-open-", { paseoConfig: { worktree: { setup: [ "sh -c 'echo starting setup; for i in $(seq 1 30); do echo tick $i; sleep 1; done; echo setup complete'", ], }, }, }); try { await seedProjectForWorkspaceSetup(client, repo.path); const workspace = await createWorkspaceThroughDaemon(client, { cwd: repo.path, worktreeSlug: `setup-open-${Date.now()}`, }); await openHomeWithProject(page, repo.path); await navigateToWorkspaceViaSidebar(page, workspace.id); await expectSetupTabNotSeeded(page, workspace.id); } finally { await client.close(); await repo.cleanup(); } }); test("runs setup through the sidebar and leaves the workspace usable", async ({ page }) => { const client = await connectWorkspaceSetupClient(); const repo = await createTempGitRepo("setup-ui-flow-", { paseoConfig: { worktree: { setup: [ "sh -c 'echo starting setup; sleep 1; echo loading dependencies; sleep 1; echo setup complete'", ], }, }, files: [{ path: "src/index.ts", content: "export const ready = true;\n" }], }); try { await seedProjectForWorkspaceSetup(client, repo.path); // The setup client owns progress before creating the workspace. const completed = waitForWorkspaceSetupProgress( client, (payload) => payload.status === "completed" && setupCommandOutput(payload.detail).includes("setup complete"), ); const workspace = await createWorkspaceThroughDaemon(client, { cwd: repo.path, worktreeSlug: `setup-ui-flow-${Date.now()}`, }); await completed; await openHomeWithProject(page, repo.path); await navigateToWorkspaceViaSidebar(page, workspace.id); await expectSetupTabNotSeeded(page, workspace.id); await expectSetupPanel(page); await waitForWorkspaceTabsVisible(page); await clickNewChat(page); await expectComposerVisible(page, { timeout: 30_000 }); await openFileExplorer(page); await expectExplorerEntryVisible(page, "README.md"); await expectExplorerEntryVisible(page, "src"); } finally { await client.close(); await repo.cleanup(); } }); test("setup panel releases details while retained behind another tab and resubscribes when shown", async ({ page, }) => { const client = await connectWorkspaceSetupClient(); const repo = await createTempGitRepo("setup-membership-", { paseoConfig: { worktree: { setup: ["echo setup complete"] } }, }); const traffic = observeSetupPanelTraffic(page); try { await seedProjectForWorkspaceSetup(client, repo.path); const completed = waitForWorkspaceSetupProgress( client, (payload) => payload.status === "completed", ); const workspace = await createWorkspaceThroughDaemon(client, { cwd: repo.path, worktreeSlug: "setup-membership", }); await completed; await openHomeWithProject(page, repo.path); await navigateToWorkspaceViaSidebar(page, workspace.id); await expectSetupTabNotSeeded(page, workspace.id); await traffic.expectNoDemand(); await expectSetupPanel(page); await traffic.expectSubscribed(workspace.id, 1); await expect(page.getByTestId("workspace-setup-log").first()).toContainText("setup complete"); await clickNewChat(page); await expectComposerVisible(page); await traffic.expectReleased(0); await expectSetupPanel(page); await traffic.expectSubscribed(workspace.id, 2); await closeSetupTab(page, workspace.id); await traffic.expectReleased(1); } finally { await client.close(); await repo.cleanup(); } }); daemonTest("streams running and completed setup snapshots for a successful setup", async () => { const client = await connectWorkspaceSetupClient(); const repo = await createTempGitRepo("setup-success-", { paseoConfig: { worktree: { setup: ["sh -c 'echo starting setup; sleep 2; echo setup complete'"], }, }, }); try { await seedProjectForWorkspaceSetup(client, repo.path); const initialRunning = waitForWorkspaceSetupProgress( client, (payload) => payload.status === "running" && payload.detail.commands.length === 0, ); const runningWithOutput = waitForWorkspaceSetupProgress( client, (payload) => payload.status === "running" && setupCommandOutput(payload.detail).includes("starting setup"), ); const completed = waitForWorkspaceSetupProgress( client, (payload) => payload.status === "completed" && setupCommandOutput(payload.detail).includes("setup complete"), ); await createWorkspaceThroughDaemon(client, { cwd: repo.path, worktreeSlug: "workspace-setup-success", }); const initialPayload = await initialRunning; const runningPayload = await runningWithOutput; const completedPayload = await completed; expect(initialPayload.detail.log).toBe(""); expect(setupCommandOutput(runningPayload.detail)).toContain("starting setup"); expect(setupCommandOutput(completedPayload.detail)).toContain("setup complete"); expect(completedPayload.error).toBeNull(); } finally { await client.close(); await repo.cleanup(); } }); test("seeds a failed setup tab once in the main pane", async ({ page }) => { const client = await connectWorkspaceSetupClient(); const repo = await createTempGitRepo("setup-failure-", { paseoConfig: { worktree: { setup: ["sh -c 'echo starting setup; sleep 2; echo setup failed 1>&2; exit 1'"], }, }, }); try { await seedProjectForWorkspaceSetup(client, repo.path); const failed = waitForWorkspaceSetupProgress( client, (payload) => payload.status === "failed" && setupCommandOutput(payload.detail).includes("setup failed"), ); const workspace = await createWorkspaceThroughDaemon(client, { cwd: repo.path, worktreeSlug: "workspace-setup-failure", }); const failedPayload = await failed; expect(setupCommandOutput(failedPayload.detail)).toContain("starting setup"); expect(setupCommandOutput(failedPayload.detail)).toContain("setup failed"); expect(failedPayload.error).toMatch(/failed/i); await openHomeWithProject(page, repo.path); await navigateToWorkspaceViaSidebar(page, workspace.id); await waitForWorkspaceTabsVisible(page); await expectFailedSetupTabSeededInMainPane(page, workspace.id); await closeSetupTab(page, workspace.id); await leaveWorkspaceViaHistory(page); await navigateToWorkspaceViaSidebar(page, workspace.id); await expectSetupTabNotSeeded(page, workspace.id); } finally { await client.close(); await repo.cleanup(); } }); test("keeps a workspace usable when its setup config cannot be parsed", async ({ page }) => { const client = await connectWorkspaceSetupClient(); const repo = await createTempGitRepo("setup-invalid-config-", { files: [{ path: "paseo.json", content: "{ invalid json\n" }], }); try { await seedProjectForWorkspaceSetup(client, repo.path); const failed = waitForWorkspaceSetupProgress( client, (payload) => payload.status === "failed", ); const workspace = await createWorkspaceThroughDaemon(client, { cwd: repo.path, worktreeSlug: "workspace-setup-invalid-config", }); const failedPayload = await failed; expect(failedPayload.error).toContain("Failed to parse paseo.json"); expect(failedPayload.detail.commands).toEqual([]); await openHomeWithProject(page, repo.path); await navigateToWorkspaceViaSidebar(page, workspace.id); await expectFailedSetupTabSeededInMainPane(page, workspace.id); await expect(page.getByText(/Failed to parse paseo\.json at/)).toBeVisible(); await closeSetupTab(page, workspace.id); await clickNewChat(page); await expectComposerVisible(page); await openFileExplorer(page); await expectExplorerEntryVisible(page, "paseo.json"); } finally { await client.close(); await repo.cleanup(); } }); daemonTest("emits a completed empty snapshot when no setup commands exist", async () => { const client = await connectWorkspaceSetupClient(); const repo = await createTempGitRepo("setup-none-"); try { await seedProjectForWorkspaceSetup(client, repo.path); const completed = waitForWorkspaceSetupProgress( client, (payload) => payload.status === "completed" && payload.detail.commands.length === 0 && payload.detail.log === "", ); await createWorkspaceThroughDaemon(client, { cwd: repo.path, worktreeSlug: "workspace-setup-none", }); const completedPayload = await completed; expect(completedPayload.error).toBeNull(); expect(completedPayload.detail.commands).toEqual([]); expect(completedPayload.detail.log).toBe(""); } finally { await client.close(); await repo.cleanup(); } }); test("launches script terminals from the workspace scripts menu", async ({ page }) => { test.setTimeout(90_000); const client = await connectWorkspaceSetupClient(); const repo = await createTempGitRepo("setup-svc-ui-", { paseoConfig: { worktree: { setup: ["sh -c 'echo bootstrapping; sleep 1; echo setup complete'"], }, scripts: { web: { command: "node -e \"const http = require('http'); const s = http.createServer((q,r) => r.end('ok')); s.listen(process.env.PORT || 3000, () => console.log('listening on ' + s.address().port))\"", }, }, }, }); try { await seedProjectForWorkspaceSetup(client, repo.path); // Wait for setup completion via daemon (setup snapshots are per-session) const completed = waitForWorkspaceSetupProgress( client, (payload) => payload.status === "completed" && setupCommandOutput(payload.detail).includes("setup complete"), ); const workspace = await createWorkspaceThroughDaemon(client, { cwd: repo.path, worktreeSlug: `setup-svc-${Date.now()}`, }); await completed; await openHomeWithProject(page, repo.path); await navigateToWorkspaceViaSidebar(page, workspace.id); await waitForWorkspaceTabsVisible(page); await expectNoTerminalTabs(page); await openWorkspaceScriptsMenu(page); await startWorkspaceScriptFromMenu(page, "web"); await closeWorkspaceScriptsMenu(page); await clickFirstTerminalTab(page); await expectTerminalSurfaceVisible(page, { timeout: 10_000 }); await waitForTerminalAttached(page); await expectFirstTerminalTabContains(page, "web"); } finally { await client.close(); await repo.cleanup(); } }); daemonTest("launches workspace scripts through an explicit daemon request", async () => { const client = await connectWorkspaceSetupClient(); const repo = await createTempGitRepo("setup-scripts-", { paseoConfig: { worktree: { setup: ["sh -c 'echo bootstrapping; sleep 1; echo setup complete'"], }, scripts: { editor: { command: "node -e \"console.log('editor ready'); setInterval(() => {}, 1000)\"", }, }, }, }); try { await seedProjectForWorkspaceSetup(client, repo.path); const completed = waitForWorkspaceSetupProgress( client, (payload) => payload.status === "completed" && setupCommandOutput(payload.detail).includes("setup complete"), ); const result = await client.createPaseoWorktree({ cwd: repo.path, worktreeSlug: "workspace-setup-scripts", }); if (!result.workspace) { throw new Error(result.error ?? "Failed to create workspace"); } const workspaceDir = result.workspace.workspaceDirectory; const workspaceId = result.workspace.id; await completed; const scriptClient = client as typeof client & WorkspaceScriptStarter; const startResult = await scriptClient.startWorkspaceScript(workspaceId, "editor"); expect(startResult).toMatchObject({ workspaceId, scriptName: "editor", terminalId: expect.any(String), error: null, }); const findEditorTerminal = (terminal: { name: string }) => terminal.name === "editor"; await expect .poll(async () => { const terminals = await client.listTerminals(workspaceDir); return terminals.terminals.find(findEditorTerminal) ?? null; }) .toMatchObject({ id: expect.any(String), name: "editor", }); } finally { await client.close(); await repo.cleanup(); } }); });