WIP: snapshot workspace execution refactor baseline

This commit is contained in:
Mohamed Boudra
2026-04-01 10:35:06 +07:00
parent e026223c80
commit d8243bfb72
47 changed files with 2219 additions and 169 deletions
+15 -5
View File
@@ -30,7 +30,9 @@ export async function waitForTabBar(page: Page): Promise<void> {
/** Return all tab test IDs currently in the tab bar. */
export async function getTabTestIds(page: Page): Promise<string[]> {
const tabs = page.locator('[data-testid^="workspace-tab-"]');
const tabs = page.locator(
'[data-testid^="workspace-tab-"]:not([data-testid^="workspace-tab-context-"])',
);
const count = await tabs.count();
const ids: string[] = [];
for (let i = 0; i < count; i++) {
@@ -49,7 +51,11 @@ export async function countTabsOfKind(page: Page, kind: string): Promise<number>
/** Return the currently active tab's test ID (the one with aria-selected or focus styling). */
export async function getActiveTabTestId(page: Page): Promise<string | null> {
// Active tab has the focused highlight — check for the aria-selected or data-active attribute
const activeTab = page.locator('[data-testid^="workspace-tab-"][aria-selected="true"]').first();
const activeTab = page
.locator(
'[data-testid^="workspace-tab-"]:not([data-testid^="workspace-tab-context-"])[aria-selected="true"]',
)
.first();
if (await activeTab.isVisible().catch(() => false)) {
return activeTab.getAttribute("data-testid");
}
@@ -111,7 +117,7 @@ export async function clickNewChat(page: Page): Promise<void> {
/** Click the "Terminal" tile on the launcher panel. */
export async function clickTerminal(page: Page): Promise<void> {
const button = page.getByRole("button", { name: "Terminal" }).first();
const button = page.getByRole("button", { name: "Terminal", exact: true }).first();
await expect(button).toBeVisible({ timeout: 10_000 });
await button.click();
}
@@ -132,8 +138,12 @@ export async function waitForTabWithTitle(
timeout = 30_000,
): Promise<void> {
const matcher = typeof title === "string" ? new RegExp(title, "i") : title;
await expect(page.locator('[data-testid^="workspace-tab-"]').filter({ hasText: matcher }).first())
.toBeVisible({ timeout });
await expect(
page
.locator('[data-testid^="workspace-tab-"]:not([data-testid^="workspace-tab-context-"])')
.filter({ hasText: matcher })
.first(),
).toBeVisible({ timeout });
}
/** Assert the new-tab '+' button is visible and there is only one. */
@@ -7,6 +7,12 @@ import { buildHostWorkspaceRoute } from "../../src/utils/host-routes";
export type TerminalPerfDaemonClient = {
connect(): Promise<void>;
close(): Promise<void>;
openProject(
cwd: string,
): Promise<{
workspace: { id: number; name: string; projectRootPath: string } | null;
error: string | null;
}>;
createTerminal(
cwd: string,
name?: string,
@@ -0,0 +1,51 @@
import { expect, type Page } from "@playwright/test";
import {
clickNewChat,
clickProviderTile,
clickTerminal,
countTabsOfKind,
getTabTestIds,
waitForTabWithTitle,
} from "./launcher";
import { setupDeterministicPrompt, waitForTerminalContent } from "./terminal-perf";
function terminalSurface(page: Page) {
return page.locator('[data-testid="terminal-surface"]').first();
}
function composerInput(page: Page) {
return page.getByRole("textbox", { name: "Message agent..." }).first();
}
export async function expectTerminalCwd(page: Page, expectedPath: string): Promise<void> {
const terminal = terminalSurface(page);
await expect(terminal).toBeVisible({ timeout: 20_000 });
await terminal.click();
await setupDeterministicPrompt(page, `SENTINEL_${Date.now()}`);
await terminal.pressSequentially("pwd\n", { delay: 0 });
await waitForTerminalContent(page, (text) => text.includes(expectedPath), 10_000);
}
export async function createStandaloneTerminalFromLauncher(page: Page): Promise<void> {
const tabIdsBefore = await getTabTestIds(page);
const launcherCountBefore = await countTabsOfKind(page, "launcher");
await clickTerminal(page);
await expect(terminalSurface(page)).toBeVisible({ timeout: 20_000 });
await expect.poll(() => countTabsOfKind(page, "launcher")).toBe(launcherCountBefore - 1);
await expect.poll(async () => (await getTabTestIds(page)).length).toBe(tabIdsBefore.length);
}
export async function createTerminalAgentFromLauncher(page: Page, providerLabel: string): Promise<void> {
await clickProviderTile(page, providerLabel);
await expect(page.getByTestId("terminal-agent-loading")).toHaveCount(0, { timeout: 30_000 });
await expect(terminalSurface(page)).toBeVisible({ timeout: 30_000 });
await waitForTabWithTitle(page, /new agent/i);
}
export async function createAgentChatFromLauncher(page: Page): Promise<void> {
await clickNewChat(page);
await expect(composerInput(page)).toBeVisible({ timeout: 15_000 });
await expect(composerInput(page)).toBeEditable({ timeout: 15_000 });
await expect(page.getByTestId("agent-loading")).toHaveCount(0);
await expect(page.getByRole("button", { name: "New Chat" })).toHaveCount(0);
}
+165
View File
@@ -0,0 +1,165 @@
import path from "node:path";
import { randomUUID } from "node:crypto";
import { pathToFileURL } from "node:url";
import { expect, type Page } from "@playwright/test";
import { gotoAppShell } from "./app";
type WorkspaceSetupProgressPayload = {
status: "running" | "completed" | "failed";
detail: { commands: string[]; log: string };
error: string | null;
};
type WorkspaceSetupRawMessage = {
type: string;
payload?: WorkspaceSetupProgressPayload;
};
type WorkspaceSetupDaemonClient = {
connect(): Promise<void>;
close(): Promise<void>;
openProject(
cwd: string,
): Promise<{ workspace: { id: string; name: string } | null; error: string | null }>;
createPaseoWorktree(
input: { cwd: string; worktreeSlug?: string },
): Promise<{ workspace: { id: string; name: string } | null; error: string | null }>;
subscribeRawMessages(handler: (message: WorkspaceSetupRawMessage) => void): () => void;
};
export type { WorkspaceSetupDaemonClient, WorkspaceSetupProgressPayload };
function getDaemonWsUrl(): string {
const daemonPort = process.env.E2E_DAEMON_PORT;
if (!daemonPort) {
throw new Error("E2E_DAEMON_PORT is not set.");
}
return `ws://127.0.0.1:${daemonPort}/ws`;
}
async function loadDaemonClientConstructor(): Promise<
new (config: { url: string; clientId: string; clientType: "cli" }) => WorkspaceSetupDaemonClient
> {
const repoRoot = path.resolve(process.cwd(), "../..");
const moduleUrl = pathToFileURL(
path.join(repoRoot, "packages/server/dist/server/server/exports.js"),
).href;
const mod = (await import(moduleUrl)) as {
DaemonClient: new (config: {
url: string;
clientId: string;
clientType: "cli";
}) => WorkspaceSetupDaemonClient;
};
return mod.DaemonClient;
}
export async function connectWorkspaceSetupClient(): Promise<WorkspaceSetupDaemonClient> {
const DaemonClient = await loadDaemonClientConstructor();
const client = new DaemonClient({
url: getDaemonWsUrl(),
clientId: `workspace-setup-${randomUUID()}`,
clientType: "cli",
});
await client.connect();
return client;
}
export async function seedProjectForWorkspaceSetup(
client: WorkspaceSetupDaemonClient,
repoPath: string,
): Promise<void> {
const result = await client.openProject(repoPath);
if (!result.workspace || result.error) {
throw new Error(result.error ?? `Failed to open project ${repoPath}`);
}
}
export function projectNameFromPath(repoPath: string): string {
return repoPath.replace(/\/+$/, "").split("/").filter(Boolean).pop() ?? repoPath;
}
export async function openHomeWithProject(page: Page, repoPath: string): Promise<void> {
await gotoAppShell(page);
await expect(createWorkspaceButton(page, repoPath)).toBeVisible({ timeout: 30_000 });
}
function createWorkspaceButton(page: Page, repoPath: string) {
return page.getByRole("button", {
name: `Create a new workspace for ${projectNameFromPath(repoPath)}`,
});
}
async function revealWorkspaceButton(page: Page, repoPath: string): Promise<void> {
await page.getByTestId(`sidebar-project-row-${repoPath}`).hover();
}
export async function createWorkspaceFromSidebar(page: Page, repoPath: string): Promise<void> {
await revealWorkspaceButton(page, repoPath);
await expect(createWorkspaceButton(page, repoPath)).toBeEnabled({ timeout: 30_000 });
await createWorkspaceButton(page, repoPath).click();
await expect(page).toHaveURL(/\/workspace\//, { timeout: 30_000 });
}
export async function expectSetupPanel(page: Page): Promise<void> {
await expect(page.getByText("Workspace setup", { exact: true })).toBeVisible({ timeout: 30_000 });
}
export async function expectSetupStatus(
page: Page,
status: "Running" | "Completed" | "Failed",
): Promise<void> {
await expect(page.getByTestId("workspace-setup-status")).toContainText(status, {
timeout: 30_000,
});
}
export async function expectSetupLogContains(page: Page, text: string): Promise<void> {
await expect(page.getByTestId("workspace-setup-log")).toContainText(text, {
timeout: 30_000,
});
}
export async function expectNoSetupMessage(page: Page): Promise<void> {
await expect(page.getByText("No setup commands ran for this workspace.", { exact: true })).toBeVisible({
timeout: 30_000,
});
}
export async function createWorkspaceThroughDaemon(
client: WorkspaceSetupDaemonClient,
input: { cwd: string; worktreeSlug: string },
): Promise<{ id: string; name: string }> {
const result = await client.createPaseoWorktree(input);
if (!result.workspace || result.error) {
throw new Error(result.error ?? `Failed to create workspace for ${input.cwd}`);
}
return result.workspace;
}
export async function waitForWorkspaceSetupProgress(
client: WorkspaceSetupDaemonClient,
predicate: (payload: WorkspaceSetupProgressPayload) => boolean,
timeoutMs = 30_000,
): Promise<WorkspaceSetupProgressPayload> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
unsubscribe();
reject(new Error(`Timed out waiting for workspace_setup_progress after ${timeoutMs}ms`));
}, timeoutMs);
const unsubscribe = client.subscribeRawMessages((message) => {
if (message.type !== "workspace_setup_progress") {
return;
}
if (!message.payload) {
return;
}
if (!predicate(message.payload)) {
return;
}
clearTimeout(timeout);
unsubscribe();
resolve(message.payload);
});
});
}
+22 -1
View File
@@ -10,7 +10,11 @@ type TempRepo = {
export const createTempGitRepo = async (
prefix = "paseo-e2e-",
options?: { withRemote?: boolean },
options?: {
withRemote?: boolean;
paseoConfig?: Record<string, unknown>;
files?: Array<{ path: string; content: string }>;
},
): Promise<TempRepo> => {
// Keep E2E repo paths short so terminal prompt + typed commands stay visible without zsh clipping.
const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp";
@@ -22,7 +26,24 @@ export const createTempGitRepo = async (
execSync('git config user.name "Paseo E2E"', { cwd: repoPath, stdio: "ignore" });
execSync("git config commit.gpgsign false", { cwd: repoPath, stdio: "ignore" });
await writeFile(path.join(repoPath, "README.md"), "# Temp Repo\n");
if (options?.paseoConfig) {
await writeFile(
path.join(repoPath, "paseo.json"),
JSON.stringify(options.paseoConfig, null, 2),
);
}
for (const file of options?.files ?? []) {
const filePath = path.join(repoPath, file.path);
await mkdir(path.dirname(filePath), { recursive: true });
await writeFile(filePath, file.content);
}
execSync("git add README.md", { cwd: repoPath, stdio: "ignore" });
if (options?.paseoConfig) {
execSync("git add paseo.json", { cwd: repoPath, stdio: "ignore" });
}
for (const file of options?.files ?? []) {
execSync(`git add ${JSON.stringify(file.path)}`, { cwd: repoPath, stdio: "ignore" });
}
execSync('git commit -m "Initial commit"', { cwd: repoPath, stdio: "ignore" });
if (withRemote) {
+18 -11
View File
@@ -28,12 +28,19 @@ import {
// ─── Shared state ──────────────────────────────────────────────────────────
let tempRepo: { path: string; cleanup: () => Promise<void> };
let workspaceId: string;
let seedClient: TerminalPerfDaemonClient;
test.beforeAll(async () => {
tempRepo = await createTempGitRepo("launcher-e2e-");
seedClient = await connectTerminalClient();
const result = await seedClient.openProject(tempRepo.path);
if (!result.workspace) throw new Error(result.error ?? "Failed to seed workspace");
workspaceId = String(result.workspace.id);
});
test.afterAll(async () => {
if (seedClient) await seedClient.close();
if (tempRepo) await tempRepo.cleanup();
});
@@ -45,7 +52,7 @@ test.describe("Launcher tab", () => {
test("Cmd+T opens launcher panel with New Chat, Terminal, and provider tiles", async ({
page,
}) => {
await gotoWorkspace(page, tempRepo.path);
await gotoWorkspace(page, workspaceId);
await pressNewTabShortcut(page);
@@ -56,7 +63,7 @@ test.describe("Launcher tab", () => {
});
test("opening two new tabs creates two launcher tabs", async ({ page }) => {
await gotoWorkspace(page, tempRepo.path);
await gotoWorkspace(page, workspaceId);
await pressNewTabShortcut(page);
await waitForLauncherPanel(page);
@@ -70,7 +77,7 @@ test.describe("Launcher tab", () => {
});
test("clicking New Chat replaces launcher in-place with draft tab", async ({ page }) => {
await gotoWorkspace(page, tempRepo.path);
await gotoWorkspace(page, workspaceId);
await clickNewTabButton(page);
await waitForLauncherPanel(page);
@@ -97,7 +104,7 @@ test.describe("Launcher tab", () => {
test("clicking Terminal replaces launcher with standalone terminal", async ({ page }) => {
test.setTimeout(45_000);
await gotoWorkspace(page, tempRepo.path);
await gotoWorkspace(page, workspaceId);
await clickNewTabButton(page);
await waitForLauncherPanel(page);
@@ -121,7 +128,7 @@ test.describe("Launcher tab", () => {
test("clicking a provider tile replaces launcher with terminal agent tab", async ({ page }) => {
test.setTimeout(45_000);
await gotoWorkspace(page, tempRepo.path);
await gotoWorkspace(page, workspaceId);
await clickNewTabButton(page);
await waitForLauncherPanel(page);
@@ -175,7 +182,7 @@ test.describe("Launcher tab", () => {
});
test("tab bar shows a single + button per pane", async ({ page }) => {
await gotoWorkspace(page, tempRepo.path);
await gotoWorkspace(page, workspaceId);
await assertSingleNewTabButton(page);
});
});
@@ -204,7 +211,7 @@ test.describe("Terminal title propagation", () => {
try {
// Navigate to workspace and open the terminal
await gotoWorkspace(page, tempRepo.path);
await gotoWorkspace(page, workspaceId);
await clickNewTabButton(page);
await waitForLauncherPanel(page);
await clickTerminal(page);
@@ -236,7 +243,7 @@ test.describe("Terminal title propagation", () => {
const terminalId = result.terminal.id;
try {
await gotoWorkspace(page, tempRepo.path);
await gotoWorkspace(page, workspaceId);
await clickNewTabButton(page);
await waitForLauncherPanel(page);
await clickTerminal(page);
@@ -272,7 +279,7 @@ test.describe("Terminal title propagation", () => {
test.describe("Launcher transitions (no flash)", () => {
test("New Chat transition has no blank intermediate tab state", async ({ page }) => {
await gotoWorkspace(page, tempRepo.path);
await gotoWorkspace(page, workspaceId);
await clickNewTabButton(page);
await waitForLauncherPanel(page);
@@ -301,7 +308,7 @@ test.describe("Launcher transitions (no flash)", () => {
test("Terminal transition completes within visual budget", async ({ page }) => {
test.setTimeout(30_000);
await gotoWorkspace(page, tempRepo.path);
await gotoWorkspace(page, workspaceId);
await clickNewTabButton(page);
await waitForLauncherPanel(page);
@@ -321,7 +328,7 @@ test.describe("Launcher transitions (no flash)", () => {
});
test("New Chat click → composer appears without launcher flash", async ({ page }) => {
await gotoWorkspace(page, tempRepo.path);
await gotoWorkspace(page, workspaceId);
await clickNewTabButton(page);
await waitForLauncherPanel(page);
+178
View File
@@ -0,0 +1,178 @@
import { execSync } from "node:child_process";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { test, expect } from "./fixtures";
import { gotoAppShell } from "./helpers/app";
import { createTempGitRepo } from "./helpers/workspace";
import { expectWorkspaceHeader } from "./helpers/workspace-ui";
import { connectWorkspaceSetupClient } 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 (expected from Playwright globalSetup).");
}
return serverId;
}
function getWorkspaceRowTestId(workspaceId: string): string {
return `sidebar-workspace-row-${getServerId()}:${workspaceId}`;
}
function escapeRegex(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function setGitHubRemote(repoPath: string): void {
execSync("git remote set-url origin https://github.com/test-owner/test-repo.git", {
cwd: repoPath,
stdio: "ignore",
});
}
async function createTempDirectory(prefix = "paseo-e2e-dir-") {
const dirPath = await mkdtemp(path.join(process.platform === "win32" ? tmpdir() : "/tmp", prefix));
await writeFile(path.join(dirPath, "README.md"), "# Temp Directory\n");
return {
path: dirPath,
cleanup: async () => {
await rm(dirPath, { recursive: true, force: true });
},
};
}
async function openProjectViaDaemon(
client: Awaited<ReturnType<typeof connectWorkspaceSetupClient>>,
cwd: string,
): Promise<{ id: string; name: string }> {
const result = await client.openProject(cwd);
if (!result.workspace || result.error) {
throw new Error(result.error ?? `Failed to open project ${cwd}`);
}
return result.workspace;
}
async function openWorkspaceFromSidebar(page: import("@playwright/test").Page, workspaceId: string) {
const row = page.getByTestId(getWorkspaceRowTestId(workspaceId));
await expect(row).toBeVisible({ timeout: 30_000 });
await row.click();
await expect(page).toHaveURL(/\/workspace\//, { timeout: 30_000 });
return row;
}
async function waitForSidebarProject(
page: import("@playwright/test").Page,
projectName: string,
) {
const row = page
.getByRole("button", {
name: new RegExp(escapeRegex(projectName), "i"),
})
.first();
await expect(row).toBeVisible({ timeout: 30_000 });
return row;
}
async function waitForSidebarWorkspace(page: import("@playwright/test").Page, workspaceId: string) {
const row = page.getByTestId(getWorkspaceRowTestId(workspaceId));
await expect(row).toBeVisible({ timeout: 30_000 });
return row;
}
test.describe("Sidebar workspace list", () => {
test("project with GitHub remote shows owner/repo name in sidebar", async ({ page }) => {
const client = await connectWorkspaceSetupClient();
const repo = await createTempGitRepo("sidebar-remote-", { withRemote: true });
try {
setGitHubRemote(repo.path);
const workspace = await openProjectViaDaemon(client, repo.path);
await gotoAppShell(page);
await waitForSidebarProject(page, "test-owner/test-repo");
await waitForSidebarWorkspace(page, workspace.id);
const projectRow = page
.locator('[data-testid^="sidebar-project-row-"]')
.filter({ hasText: "test-owner/test-repo" })
.first();
await expect(projectRow).toBeVisible({ timeout: 30_000 });
await expect(projectRow).not.toContainText(path.basename(repo.path));
} finally {
await client.close();
await repo.cleanup();
}
});
test("project shows workspace under it", async ({ page }) => {
const client = await connectWorkspaceSetupClient();
const repo = await createTempGitRepo("sidebar-workspace-under-project-");
try {
const workspace = await openProjectViaDaemon(client, repo.path);
await gotoAppShell(page);
await waitForSidebarProject(page, path.basename(repo.path));
await waitForSidebarWorkspace(page, workspace.id);
} finally {
await client.close();
await repo.cleanup();
}
});
test("non-git project shows directory name", async ({ page }) => {
const client = await connectWorkspaceSetupClient();
const project = await createTempDirectory("sidebar-directory-");
try {
await openProjectViaDaemon(client, project.path);
await gotoAppShell(page);
const projectRow = await waitForSidebarProject(page, path.basename(project.path));
await expect(projectRow).toContainText(path.basename(project.path));
} finally {
await client.close();
await project.cleanup();
}
});
test("workspace header shows correct title and subtitle", async ({ page }) => {
const client = await connectWorkspaceSetupClient();
const repo = await createTempGitRepo("sidebar-header-", { withRemote: true });
try {
setGitHubRemote(repo.path);
const workspace = await openProjectViaDaemon(client, repo.path);
await gotoAppShell(page);
await waitForSidebarProject(page, "test-owner/test-repo");
await waitForSidebarWorkspace(page, workspace.id);
await openWorkspaceFromSidebar(page, workspace.id);
await expectWorkspaceHeader(page, {
title: workspace.name,
subtitle: "test-owner/test-repo",
});
} finally {
await client.close();
await repo.cleanup();
}
});
test("git project shows branch name in workspace row", async ({ page }) => {
const client = await connectWorkspaceSetupClient();
const repo = await createTempGitRepo("sidebar-branch-");
try {
const workspace = await openProjectViaDaemon(client, repo.path);
await gotoAppShell(page);
await waitForSidebarProject(page, path.basename(repo.path));
expect(workspace.name).toBe("main");
await expect(await waitForSidebarWorkspace(page, workspace.id)).toContainText("main");
} finally {
await client.close();
await repo.cleanup();
}
});
});
@@ -24,6 +24,9 @@ test.describe("Terminal wire performance", () => {
test.beforeAll(async () => {
tempRepo = await createTempGitRepo("perf-");
client = await connectTerminalClient();
// 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");
});
test.afterAll(async () => {
+107
View File
@@ -0,0 +1,107 @@
import { execSync } from "node:child_process";
import path from "node:path";
import { expect, test } from "./fixtures";
import {
clickNewTabButton,
clickTerminal,
gotoWorkspace,
waitForLauncherPanel,
} from "./helpers/launcher";
import {
setupDeterministicPrompt,
waitForTerminalContent,
} from "./helpers/terminal-perf";
import { createTempGitRepo } from "./helpers/workspace";
import { connectWorkspaceSetupClient, seedProjectForWorkspaceSetup } from "./helpers/workspace-setup";
test.describe("Workspace cwd correctness", () => {
test("main checkout workspace opens terminals in the project root", async ({ page }) => {
test.setTimeout(60_000);
const client = await connectWorkspaceSetupClient();
const repo = await createTempGitRepo("workspace-cwd-main-");
try {
await seedProjectForWorkspaceSetup(client, repo.path);
const workspaceResult = await client.openProject(repo.path);
if (!workspaceResult.workspace) {
throw new Error(workspaceResult.error ?? `Failed to open project ${repo.path}`);
}
const workspaceId = String(workspaceResult.workspace.id);
await gotoWorkspace(page, workspaceId);
await clickNewTabButton(page);
await waitForLauncherPanel(page);
await clickTerminal(page);
const terminal = page.locator('[data-testid="terminal-surface"]');
await expect(terminal.first()).toBeVisible({ timeout: 20_000 });
await terminal.first().click();
await setupDeterministicPrompt(page, `PWD_READY_${Date.now()}`);
await terminal.first().pressSequentially("pwd\n", { delay: 0 });
await waitForTerminalContent(page, (text) => text.includes(repo.path), 10_000);
} finally {
await client.close();
await repo.cleanup();
}
});
test("worktree workspace opens terminals in the worktree directory", async ({ page }) => {
test.setTimeout(90_000);
const client = await connectWorkspaceSetupClient();
const repo = await createTempGitRepo("workspace-cwd-worktree-");
const worktreePath = path.join(
"/tmp",
`paseo-wt-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
const branchName = `workspace-cwd-${Date.now()}`;
let worktreeCreated = false;
try {
await seedProjectForWorkspaceSetup(client, repo.path);
execSync(`git worktree add ${JSON.stringify(worktreePath)} -b ${JSON.stringify(branchName)} main`, {
cwd: repo.path,
stdio: "ignore",
});
worktreeCreated = true;
const workspaceResult = await client.openProject(worktreePath);
if (!workspaceResult.workspace) {
throw new Error(workspaceResult.error ?? `Failed to open project ${worktreePath}`);
}
const workspaceId = String(workspaceResult.workspace.id);
await gotoWorkspace(page, workspaceId);
await clickNewTabButton(page);
await waitForLauncherPanel(page);
await clickTerminal(page);
const terminal = page.locator('[data-testid="terminal-surface"]');
await expect(terminal.first()).toBeVisible({ timeout: 20_000 });
await terminal.first().click();
await setupDeterministicPrompt(page, `PWD_READY_${Date.now()}`);
await terminal.first().pressSequentially("pwd\n", { delay: 0 });
await waitForTerminalContent(page, (text) => text.includes(worktreePath), 10_000);
} finally {
if (worktreeCreated) {
try {
execSync(`git worktree remove ${JSON.stringify(worktreePath)} --force`, {
cwd: repo.path,
stdio: "ignore",
});
} catch {
// Best-effort cleanup so test failures preserve the original error.
}
}
await client.close();
await repo.cleanup();
}
});
});
@@ -0,0 +1,242 @@
import { execSync } from "node:child_process";
import path from "node:path";
import { test } from "./fixtures";
import {
clickNewTabButton,
gotoWorkspace,
waitForLauncherPanel,
} from "./helpers/launcher";
import { createTempGitRepo } from "./helpers/workspace";
import {
createAgentChatFromLauncher,
createStandaloneTerminalFromLauncher,
createTerminalAgentFromLauncher,
expectTerminalCwd,
} from "./helpers/workspace-lifecycle";
import { connectWorkspaceSetupClient, seedProjectForWorkspaceSetup } from "./helpers/workspace-setup";
test.describe("Workspace lifecycle", () => {
// The first test after a spec-file switch can intermittently fail because
// the shared daemon still holds stale sessions from the previous spec.
// One retry is enough for the daemon to stabilize.
test.describe.configure({ retries: 1 });
test.describe("Main checkout", () => {
test("creates a terminal agent via provider tile", async ({ page }) => {
test.setTimeout(60_000);
const client = await connectWorkspaceSetupClient();
const repo = await createTempGitRepo("lifecycle-main-agent-");
try {
await seedProjectForWorkspaceSetup(client, repo.path);
const workspaceResult = await client.openProject(repo.path);
if (!workspaceResult.workspace) {
throw new Error(workspaceResult.error ?? `Failed to open project ${repo.path}`);
}
const workspaceId = String(workspaceResult.workspace.id);
await gotoWorkspace(page, workspaceId);
await clickNewTabButton(page);
await waitForLauncherPanel(page);
await createTerminalAgentFromLauncher(page, "Claude");
} finally {
await client.close();
await repo.cleanup();
}
});
test("creates an agent chat via New Chat", async ({ page }) => {
test.setTimeout(60_000);
const client = await connectWorkspaceSetupClient();
const repo = await createTempGitRepo("lifecycle-main-chat-");
try {
await seedProjectForWorkspaceSetup(client, repo.path);
const workspaceResult = await client.openProject(repo.path);
if (!workspaceResult.workspace) {
throw new Error(workspaceResult.error ?? `Failed to open project ${repo.path}`);
}
const workspaceId = String(workspaceResult.workspace.id);
await gotoWorkspace(page, workspaceId);
await clickNewTabButton(page);
await waitForLauncherPanel(page);
await createAgentChatFromLauncher(page);
} finally {
await client.close();
await repo.cleanup();
}
});
test("creates a terminal with correct CWD", async ({ page }) => {
test.setTimeout(60_000);
const client = await connectWorkspaceSetupClient();
const repo = await createTempGitRepo("lifecycle-main-shell-");
try {
await seedProjectForWorkspaceSetup(client, repo.path);
const workspaceResult = await client.openProject(repo.path);
if (!workspaceResult.workspace) {
throw new Error(workspaceResult.error ?? `Failed to open project ${repo.path}`);
}
const workspaceId = String(workspaceResult.workspace.id);
await gotoWorkspace(page, workspaceId);
await clickNewTabButton(page);
await waitForLauncherPanel(page);
await createStandaloneTerminalFromLauncher(page);
await expectTerminalCwd(page, repo.path);
} finally {
await client.close();
await repo.cleanup();
}
});
});
test.describe("Worktree workspace", () => {
test("creates a terminal agent via provider tile", async ({ page }) => {
test.setTimeout(90_000);
const client = await connectWorkspaceSetupClient();
const repo = await createTempGitRepo("lifecycle-wt-agent-");
const worktreePath = path.join(
"/tmp",
`paseo-wt-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
const branchName = `lifecycle-wt-agent-${Date.now()}`;
let worktreeCreated = false;
try {
await seedProjectForWorkspaceSetup(client, repo.path);
execSync(`git worktree add ${JSON.stringify(worktreePath)} -b ${JSON.stringify(branchName)} main`, {
cwd: repo.path,
stdio: "ignore",
});
worktreeCreated = true;
const workspaceResult = await client.openProject(worktreePath);
if (!workspaceResult.workspace) {
throw new Error(workspaceResult.error ?? `Failed to open project ${worktreePath}`);
}
const workspaceId = String(workspaceResult.workspace.id);
await gotoWorkspace(page, workspaceId);
await clickNewTabButton(page);
await waitForLauncherPanel(page);
await createTerminalAgentFromLauncher(page, "Claude");
} finally {
if (worktreeCreated) {
try {
execSync(`git worktree remove ${JSON.stringify(worktreePath)} --force`, {
cwd: repo.path,
stdio: "ignore",
});
} catch {
// Best-effort cleanup so test failures preserve the original error.
}
}
await client.close();
await repo.cleanup();
}
});
test("creates an agent chat via New Chat", async ({ page }) => {
test.setTimeout(90_000);
const client = await connectWorkspaceSetupClient();
const repo = await createTempGitRepo("lifecycle-wt-chat-");
const worktreePath = path.join(
"/tmp",
`paseo-wt-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
const branchName = `lifecycle-wt-chat-${Date.now()}`;
let worktreeCreated = false;
try {
await seedProjectForWorkspaceSetup(client, repo.path);
execSync(`git worktree add ${JSON.stringify(worktreePath)} -b ${JSON.stringify(branchName)} main`, {
cwd: repo.path,
stdio: "ignore",
});
worktreeCreated = true;
const workspaceResult = await client.openProject(worktreePath);
if (!workspaceResult.workspace) {
throw new Error(workspaceResult.error ?? `Failed to open project ${worktreePath}`);
}
const workspaceId = String(workspaceResult.workspace.id);
await gotoWorkspace(page, workspaceId);
await clickNewTabButton(page);
await waitForLauncherPanel(page);
await createAgentChatFromLauncher(page);
} finally {
if (worktreeCreated) {
try {
execSync(`git worktree remove ${JSON.stringify(worktreePath)} --force`, {
cwd: repo.path,
stdio: "ignore",
});
} catch {
// Best-effort cleanup so test failures preserve the original error.
}
}
await client.close();
await repo.cleanup();
}
});
test("creates a terminal with correct CWD", async ({ page }) => {
test.setTimeout(90_000);
const client = await connectWorkspaceSetupClient();
const repo = await createTempGitRepo("lifecycle-wt-shell-");
const worktreePath = path.join(
"/tmp",
`paseo-wt-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
const branchName = `lifecycle-wt-shell-${Date.now()}`;
let worktreeCreated = false;
try {
await seedProjectForWorkspaceSetup(client, repo.path);
execSync(`git worktree add ${JSON.stringify(worktreePath)} -b ${JSON.stringify(branchName)} main`, {
cwd: repo.path,
stdio: "ignore",
});
worktreeCreated = true;
const workspaceResult = await client.openProject(worktreePath);
if (!workspaceResult.workspace) {
throw new Error(workspaceResult.error ?? `Failed to open project ${worktreePath}`);
}
const workspaceId = String(workspaceResult.workspace.id);
await gotoWorkspace(page, workspaceId);
await clickNewTabButton(page);
await waitForLauncherPanel(page);
await createStandaloneTerminalFromLauncher(page);
await expectTerminalCwd(page, worktreePath);
} finally {
if (worktreeCreated) {
try {
execSync(`git worktree remove ${JSON.stringify(worktreePath)} --force`, {
cwd: repo.path,
stdio: "ignore",
});
} catch {
// Best-effort cleanup so test failures preserve the original error.
}
}
await client.close();
await repo.cleanup();
}
});
});
});