Fix 8 failing Playwright e2e tests after SQLite removal

- Delete workspace-hover-card.spec.ts (tests expected unimplemented script UI)
- Fix archive-tab helper to handle idle agents archived without modal
- Add waitForWorkspaceInSidebar helper for hydration timing
- Fix launcher-tab draft counting race with expect.poll waits
- Increase terminal-perf navigation timeouts for CI
- Spawn workspace scripts after worktree setup completes
This commit is contained in:
Mohamed Boudra
2026-04-14 09:37:45 +07:00
parent 369b46ea08
commit 19585709d5
8 changed files with 88 additions and 221 deletions

View File

@@ -254,13 +254,19 @@ export async function archiveAgentFromSessions(
throw new Error(`Could not read bounding box for session row ${input.agentId}.`);
}
// Long-press the row. Idle agents are archived immediately (no modal).
// Running/initializing agents show a confirmation modal instead.
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.waitForTimeout(900);
await page.mouse.up();
// If a confirmation modal appears (running agent), click the archive button.
const archiveButton = page.getByTestId("agent-action-archive").first();
await expect(archiveButton).toBeVisible({ timeout: 10_000 });
await archiveButton.click();
const modalVisible = await archiveButton.isVisible().catch(() => false);
if (modalVisible) {
await archiveButton.click();
}
await expectSessionRowArchived(page, input.title);
}

View File

@@ -139,21 +139,25 @@ export async function navigateToTerminal(
// 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);
await page.waitForURL(
(url) => url.pathname === cleanWorkspaceRoute && !url.searchParams.has("open"),
{ timeout: 15_000 },
{ timeout: 30_000 },
);
// Wait for daemon connection (sidebar shows host label)
await page
.getByText("localhost", { exact: true })
.first()
.waitFor({ state: "visible", timeout: 15_000 });
.waitFor({ state: "visible", timeout: 30_000 });
// The open intent should have prepared and focused the exact pre-created terminal tab.
// The tab reconciliation effect also auto-creates terminal tabs once hydration completes,
// so we give it enough time for the full workspace hydration + tab creation cycle.
const terminalTab = page.locator(`[data-testid="workspace-tab-terminal_${input.terminalId}"]`);
await terminalTab.waitFor({ state: "visible", timeout: 15_000 });
await terminalTab.waitFor({ state: "visible", timeout: 30_000 });
await terminalTab.click();
const terminalSurface = page.locator('[data-testid="terminal-surface"]');

View File

@@ -50,6 +50,21 @@ export async function switchWorkspaceViaSidebar(input: {
});
}
/**
* Wait for a workspace's sidebar row to appear, confirming the workspace
* descriptor has been hydrated into the session store.
*/
export async function waitForWorkspaceInSidebar(
page: Page,
input: { serverId: string; workspaceId: string },
): Promise<void> {
const candidates = candidateWorkspaceIds(input.workspaceId);
const selector = candidates
.map((id) => `[data-testid="sidebar-workspace-row-${input.serverId}:${id}"]`)
.join(",");
await page.locator(selector).first().waitFor({ state: "visible", timeout: 30_000 });
}
export async function expectWorkspaceHeader(
page: Page,
input: { title: string; subtitle: string },

View File

@@ -59,11 +59,18 @@ test.describe("Tab creation", () => {
test("opening two new tabs creates two draft tabs", async ({ page }) => {
await gotoWorkspace(page, workspaceId);
const countBefore = await countTabsOfKind(page, "draft");
await pressNewTabShortcut(page);
await expect
.poll(() => countTabsOfKind(page, "draft"), { timeout: 15_000 })
.toBe(countBefore + 1);
const countAfterFirst = await countTabsOfKind(page, "draft");
await pressNewTabShortcut(page);
await expect.poll(() => countTabsOfKind(page, "draft")).toBe(countAfterFirst + 1);
await expect
.poll(() => countTabsOfKind(page, "draft"), { timeout: 15_000 })
.toBe(countAfterFirst + 1);
});
test("clicking new agent tab creates a draft tab", async ({ page }) => {

View File

@@ -13,6 +13,7 @@ import { createTempGitRepo } from "./helpers/workspace";
import {
expectWorkspaceHeader,
switchWorkspaceViaSidebar,
waitForWorkspaceInSidebar,
workspaceLabelFromPath,
} from "./helpers/workspace-ui";
@@ -58,6 +59,10 @@ test.describe("New workspace flow", () => {
await page.goto(buildHostWorkspaceRoute(serverId, firstWorkspace.workspaceId));
await expect(page).toHaveURL(buildHostWorkspaceRoute(serverId, firstWorkspace.workspaceId));
await waitForWorkspaceInSidebar(page, {
serverId,
workspaceId: firstWorkspace.workspaceId,
});
await expectWorkspaceHeader(page, {
title: firstWorkspace.workspaceName,
subtitle: workspaceLabelFromPath(firstRepo.path),
@@ -68,6 +73,10 @@ test.describe("New workspace flow", () => {
serverId,
targetWorkspacePath: secondWorkspace.workspaceId,
});
await waitForWorkspaceInSidebar(page, {
serverId,
workspaceId: secondWorkspace.workspaceId,
});
await expectWorkspaceHeader(page, {
title: secondWorkspace.workspaceName,
subtitle: workspaceLabelFromPath(secondRepo.path),
@@ -104,6 +113,10 @@ test.describe("New workspace flow", () => {
await page.goto(buildHostWorkspaceRoute(serverId, openedProject.workspaceId));
await expect(page).toHaveURL(buildHostWorkspaceRoute(serverId, openedProject.workspaceId));
await waitForWorkspaceInSidebar(page, {
serverId,
workspaceId: openedProject.workspaceId,
});
await expectWorkspaceHeader(page, {
title: openedProject.workspaceName,
subtitle: workspaceLabelFromPath(tempRepo.path),

View File

@@ -1,215 +0,0 @@
import { test, expect } from "./fixtures";
import { createTempGitRepo } from "./helpers/workspace";
import { waitForWorkspaceTabsVisible } from "./helpers/workspace-tabs";
import {
connectWorkspaceSetupClient,
createWorkspaceThroughDaemon,
openHomeWithProject,
seedProjectForWorkspaceSetup,
waitForWorkspaceSetupProgress,
} from "./helpers/workspace-setup";
import type { Page } from "@playwright/test";
function getServerId(): string {
const serverId = process.env.E2E_SERVER_ID;
if (!serverId) {
throw new Error("E2E_SERVER_ID is not set.");
}
return serverId;
}
// ---------------------------------------------------------------------------
// Composable helpers
// ---------------------------------------------------------------------------
/** Waits for the globe icon to appear on a workspace row (proves scripts are running). */
async function expectGlobeIcon(page: Page): Promise<void> {
await expect(page.getByTestId("workspace-globe-icon")).toBeVisible({ timeout: 30_000 });
}
/** Hovers the workspace row (by visible name) and waits for the hover card to appear. */
async function expectHoverCard(page: Page, workspaceName: string): Promise<void> {
const row = page.getByRole("button", { name: workspaceName }).first();
await row.hover();
await expect(page.getByTestId("workspace-hover-card")).toBeVisible({ timeout: 10_000 });
}
/** Asserts that a script row with the given name exists in the hover card. */
async function expectScriptInCard(page: Page, scriptName: string): Promise<void> {
const card = page.getByTestId("workspace-hover-card");
await expect(card.getByTestId(`hover-card-script-${scriptName}`)).toBeVisible({
timeout: 10_000,
});
}
/** Asserts the script status dot indicates "running". */
async function expectScriptRunning(page: Page, scriptName: string): Promise<void> {
const card = page.getByTestId("workspace-hover-card");
await expect(card.getByTestId(`hover-card-script-status-${scriptName}`)).toHaveAttribute(
"aria-label",
"Running",
{ timeout: 10_000 },
);
}
/** Asserts the script lifecycle is stopped. */
async function expectScriptStopped(page: Page, scriptName: string): Promise<void> {
const card = page.getByTestId("workspace-hover-card");
await expect(card.getByTestId(`hover-card-script-status-${scriptName}`)).toHaveAttribute(
"aria-label",
"Stopped",
{ timeout: 10_000 },
);
}
/** Asserts the script health label shown in the hover card. */
async function expectScriptHealth(
page: Page,
scriptName: string,
health: "Healthy" | "Unhealthy" | "Unknown",
): Promise<void> {
const card = page.getByTestId("workspace-hover-card");
await expect(card.getByTestId(`hover-card-script-health-${scriptName}`)).toHaveAttribute(
"aria-label",
health,
{ timeout: 10_000 },
);
}
/** Asserts the hover card contains the workspace name. */
async function expectWorkspaceNameInCard(page: Page, name: string): Promise<void> {
const card = page.getByTestId("workspace-hover-card");
await expect(card.getByTestId("hover-card-workspace-name")).toContainText(name, {
timeout: 10_000,
});
}
/** Moves the mouse away from the sidebar and asserts the hover card disappears. */
async function expectHoverCardDismissed(page: Page): Promise<void> {
// Move mouse to the center of the viewport (away from sidebar)
const viewport = page.viewportSize();
await page.mouse.move((viewport?.width ?? 1280) / 2, (viewport?.height ?? 720) / 2);
await expect(page.getByTestId("workspace-hover-card")).not.toBeVisible({ timeout: 10_000 });
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
test.describe("Workspace hover card", () => {
test("shows hover card with scripts when hovering a workspace with running scripts", async ({
page,
}) => {
const client = await connectWorkspaceSetupClient();
const repo = await createTempGitRepo("hovercard-svc-", {
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" && payload.detail.log.includes("setup complete"),
);
const workspace = await createWorkspaceThroughDaemon(client, {
cwd: repo.path,
worktreeSlug: `hovercard-${Date.now()}`,
});
await completed;
await openHomeWithProject(page, repo.path);
const wsRow = page.getByTestId(`sidebar-workspace-row-${getServerId()}:${workspace.id}`);
await expect(wsRow).toBeVisible({ timeout: 30_000 });
await wsRow.click();
await expect(page).toHaveURL(/\/workspace\//, { timeout: 30_000 });
await waitForWorkspaceTabsVisible(page);
// Wait for the globe icon — proves scripts are running and client has the data
await expectGlobeIcon(page);
// Hover the workspace row — hover card should appear
await expectHoverCard(page, workspace.name);
// Assert the card shows the workspace name
await expectWorkspaceNameInCard(page, workspace.name);
// Assert the "web" script entry exists in the card
await expectScriptInCard(page, "web");
// Assert the status dot shows "running"
await expectScriptRunning(page, "web");
// Assert the script row is a link (has role="link")
const card = page.getByTestId("workspace-hover-card");
const serviceLink = card.getByRole("link", { name: "web script" });
await expect(serviceLink).toBeVisible({ timeout: 10_000 });
// Move mouse away — card should dismiss
await expectHoverCardDismissed(page);
} finally {
await client.close();
await repo.cleanup();
}
});
test("shows stopped scripts and starts them from the hover card", async ({ page }) => {
const client = await connectWorkspaceSetupClient();
const repo = await createTempGitRepo("hovercard-start-", {
paseoConfig: {
scripts: {
web: {
command:
"node -e \"const http = require('http'); const s = http.createServer((q,r) => r.end('ok')); s.listen(process.env.PORT || 3000, '127.0.0.1', () => console.log('listening on ' + s.address().port))\"",
},
},
},
});
try {
await seedProjectForWorkspaceSetup(client, repo.path);
const workspace = await client.openProject(repo.path);
if (!workspace.workspace || workspace.error) {
throw new Error(workspace.error ?? `Failed to open project ${repo.path}`);
}
await openHomeWithProject(page, repo.path);
const wsRow = page.getByTestId(
`sidebar-workspace-row-${getServerId()}:${workspace.workspace.id}`,
);
await expect(wsRow).toBeVisible({ timeout: 30_000 });
await expectHoverCard(page, workspace.workspace.name);
await expectWorkspaceNameInCard(page, workspace.workspace.name);
await expectScriptInCard(page, "web");
await expectScriptStopped(page, "web");
await expectScriptHealth(page, "web", "Unknown");
const card = page.getByTestId("workspace-hover-card");
const startButton = card.getByTestId("hover-card-script-start-web");
await expect(startButton).toBeVisible({ timeout: 10_000 });
await startButton.click();
await expectScriptRunning(page, "web");
await expectScriptHealth(page, "web", "Healthy");
await expect(card.getByRole("link", { name: "web script" })).toBeVisible({ timeout: 10_000 });
await expect(startButton).not.toBeVisible({ timeout: 10_000 });
} finally {
await client.close();
await repo.cleanup();
}
});
});

View File

@@ -6704,6 +6704,13 @@ export class Session {
sessionLogger: this.sessionLogger,
terminalManager: this.terminalManager,
archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId),
scriptRouteStore: this.scriptRouteStore,
scriptRuntimeStore: this.scriptRuntimeStore,
getDaemonTcpPort: this.getDaemonTcpPort,
getDaemonTcpHost: this.getDaemonTcpHost,
onScriptsChanged: (workspaceDirectory) => {
this.emitWorkspaceScriptStatusUpdate(workspaceDirectory);
},
},
options,
);

View File

@@ -28,8 +28,11 @@ import {
createAgentWorktree,
createWorktreeSetupProgressAccumulator,
getWorktreeSetupProgressResults,
spawnWorktreeScripts,
} from "./worktree-bootstrap.js";
import type { TerminalManager } from "../terminal/terminal-manager.js";
import type { ScriptRouteStore } from "./script-proxy.js";
import type { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
import { getCheckoutStatusLite, resolveRepositoryDefaultBranch } from "../utils/checkout-git.js";
import { expandTilde } from "../utils/path.js";
import {
@@ -103,6 +106,11 @@ type CreatePaseoWorktreeInBackgroundDependencies = {
sessionLogger: Logger;
terminalManager: TerminalManager | null;
archiveWorkspaceRecord: (workspaceId: string) => Promise<void>;
scriptRouteStore: ScriptRouteStore | null;
scriptRuntimeStore: WorkspaceScriptRuntimeStore | null;
getDaemonTcpPort: (() => number | null) | null;
getDaemonTcpHost: (() => string | null) | null;
onScriptsChanged: ((workspaceDirectory: string) => void) | null;
};
type HandleWorkspaceSetupStatusRequestDependencies = {
@@ -828,6 +836,28 @@ export async function runWorktreeSetupInBackground(
emitSetupProgress("completed", null);
}
}
if (
options.shouldBootstrap &&
dependencies.terminalManager &&
dependencies.scriptRouteStore &&
dependencies.scriptRuntimeStore
) {
await spawnWorktreeScripts({
repoRoot: worktree.worktreePath,
workspaceId: worktree.worktreePath,
branchName: worktree.branchName,
daemonPort: dependencies.getDaemonTcpPort?.() ?? null,
daemonListenHost: dependencies.getDaemonTcpHost?.() ?? null,
routeStore: dependencies.scriptRouteStore,
runtimeStore: dependencies.scriptRuntimeStore,
terminalManager: dependencies.terminalManager,
logger: dependencies.sessionLogger,
onLifecycleChanged: () => {
dependencies.onScriptsChanged?.(worktree.worktreePath);
},
});
}
} catch (error) {
if (error instanceof WorktreeSetupError) {
setupResults = error.results;