Fix Playwright workspace isolation (#1627)

* Fix Playwright workspace isolation

* Clean up empty project E2E cleanup

* Clean up worktree restore E2E projects
This commit is contained in:
Mohamed Boudra
2026-06-20 08:59:21 +00:00
committed by GitHub
parent ba8fe261ee
commit 4354ad3e27
19 changed files with 119 additions and 43 deletions
+1
View File
@@ -130,6 +130,7 @@ Test suites in this repo are heavy. Running them in bulk freezes the machine, es
- Never re-run a suite another agent already reported green. - Never re-run a suite another agent already reported green.
- For full-suite confidence, push to CI and check GitHub Actions. - For full-suite confidence, push to CI and check GitHub Actions.
- Never run the full Playwright E2E suite locally — defer whole-suite verification to CI. Targeted Playwright specs are allowed when you changed or need to prove that specific flow. - Never run the full Playwright E2E suite locally — defer whole-suite verification to CI. Targeted Playwright specs are allowed when you changed or need to prove that specific flow.
- App Playwright specs share one isolated daemon per run. Helpers that create projects or workspaces must remove the daemon project record during cleanup, not only delete the temp directory. Agent helpers must pass the intended `workspaceId` through to agent creation; never infer ownership from `cwd`.
## Agent authentication in tests ## Agent authentication in tests
+4 -6
View File
@@ -12,7 +12,7 @@ import { startRunningMockAgent } from "./helpers/composer";
test.describe("Agent stream UI", () => { test.describe("Agent stream UI", () => {
test("auto-scroll sticks to bottom across token bursts", async ({ page }) => { test("auto-scroll sticks to bottom across token bursts", async ({ page }) => {
test.setTimeout(120_000); test.setTimeout(120_000);
const { client, repo } = await startRunningMockAgent(page, { const agent = await startRunningMockAgent(page, {
prefix: "stream-scroll-", prefix: "stream-scroll-",
model: "one-minute-stream", model: "one-minute-stream",
prompt: "Stream for auto-scroll test.", prompt: "Stream for auto-scroll test.",
@@ -21,14 +21,13 @@ test.describe("Agent stream UI", () => {
await awaitAssistantMessage(page); await awaitAssistantMessage(page);
await expectScrollFollowsNewContent(page); await expectScrollFollowsNewContent(page);
} finally { } finally {
await client.close(); await agent.cleanup();
await repo.cleanup();
} }
}); });
test("working-indicator transitions to copy-button when stream ends", async ({ page }) => { test("working-indicator transitions to copy-button when stream ends", async ({ page }) => {
test.setTimeout(60_000); test.setTimeout(60_000);
const { client, repo } = await startRunningMockAgent(page, { const agent = await startRunningMockAgent(page, {
prefix: "stream-indicator-", prefix: "stream-indicator-",
model: "ten-second-stream", model: "ten-second-stream",
prompt: "Stream briefly for indicator transition test.", prompt: "Stream briefly for indicator transition test.",
@@ -39,8 +38,7 @@ test.describe("Agent stream UI", () => {
await expectAgentIdle(page, 30_000); await expectAgentIdle(page, 30_000);
await expectTurnCopyButton(page); await expectTurnCopyButton(page);
} finally { } finally {
await client.close(); await agent.cleanup();
await repo.cleanup();
} }
}); });
+17
View File
@@ -26,15 +26,26 @@ import {
test.describe("Archive tab reconciliation", () => { test.describe("Archive tab reconciliation", () => {
let client: Awaited<ReturnType<typeof connectSeedClient>>; let client: Awaited<ReturnType<typeof connectSeedClient>>;
let tempRepo: { path: string; cleanup: () => Promise<void> }; let tempRepo: { path: string; cleanup: () => Promise<void> };
let projectId: string;
let workspaceId: string;
test.describe.configure({ timeout: 300_000 }); test.describe.configure({ timeout: 300_000 });
test.beforeAll(async () => { test.beforeAll(async () => {
tempRepo = await createTempGitRepo("archive-tab-"); tempRepo = await createTempGitRepo("archive-tab-");
client = await connectSeedClient(); client = await connectSeedClient();
const created = await client.createWorkspace({
source: { kind: "directory", path: tempRepo.path },
});
if (!created.workspace) {
throw new Error(created.error ?? `Failed to create workspace ${tempRepo.path}`);
}
projectId = created.workspace.projectId;
workspaceId = created.workspace.id;
}); });
test.afterAll(async () => { test.afterAll(async () => {
await client?.removeProject(projectId).catch(() => undefined);
await client?.close().catch(() => undefined); await client?.close().catch(() => undefined);
await tempRepo?.cleanup(); await tempRepo?.cleanup();
}); });
@@ -42,10 +53,12 @@ test.describe("Archive tab reconciliation", () => {
test("non-UI archive prunes the archived tab across open pages and reload", async ({ page }) => { test("non-UI archive prunes the archived tab across open pages and reload", async ({ page }) => {
const archived = await createIdleAgent(client, { const archived = await createIdleAgent(client, {
cwd: tempRepo.path, cwd: tempRepo.path,
workspaceId,
title: `cli-archive-${randomUUID().slice(0, 8)}`, title: `cli-archive-${randomUUID().slice(0, 8)}`,
}); });
const surviving = await createIdleAgent(client, { const surviving = await createIdleAgent(client, {
cwd: tempRepo.path, cwd: tempRepo.path,
workspaceId,
title: `cli-control-${randomUUID().slice(0, 8)}`, title: `cli-control-${randomUUID().slice(0, 8)}`,
}); });
const passivePage = await page.context().newPage(); const passivePage = await page.context().newPage();
@@ -81,10 +94,12 @@ test.describe("Archive tab reconciliation", () => {
test("Sessions archive prunes the archived tab across open pages", async ({ page }) => { test("Sessions archive prunes the archived tab across open pages", async ({ page }) => {
const archived = await createIdleAgent(client, { const archived = await createIdleAgent(client, {
cwd: tempRepo.path, cwd: tempRepo.path,
workspaceId,
title: `ui-archive-${randomUUID().slice(0, 8)}`, title: `ui-archive-${randomUUID().slice(0, 8)}`,
}); });
const surviving = await createIdleAgent(client, { const surviving = await createIdleAgent(client, {
cwd: tempRepo.path, cwd: tempRepo.path,
workspaceId,
title: `ui-control-${randomUUID().slice(0, 8)}`, title: `ui-control-${randomUUID().slice(0, 8)}`,
}); });
const passivePage = await page.context().newPage(); const passivePage = await page.context().newPage();
@@ -111,10 +126,12 @@ test.describe("Archive tab reconciliation", () => {
test("clicking an archived session unarchives it and opens the agent", async ({ page }) => { test("clicking an archived session unarchives it and opens the agent", async ({ page }) => {
const archived = await createIdleAgent(client, { const archived = await createIdleAgent(client, {
cwd: tempRepo.path, cwd: tempRepo.path,
workspaceId,
title: `unarchive-archived-${randomUUID().slice(0, 8)}`, title: `unarchive-archived-${randomUUID().slice(0, 8)}`,
}); });
const surviving = await createIdleAgent(client, { const surviving = await createIdleAgent(client, {
cwd: tempRepo.path, cwd: tempRepo.path,
workspaceId,
title: `unarchive-control-${randomUUID().slice(0, 8)}`, title: `unarchive-control-${randomUUID().slice(0, 8)}`,
}); });
@@ -193,7 +193,7 @@ test.describe("Composer attachments", () => {
page, page,
}) => { }) => {
test.setTimeout(120_000); test.setTimeout(120_000);
const { client, repo } = await startRunningMockAgent(page, { const agent = await startRunningMockAgent(page, {
prefix: "attach-queue-", prefix: "attach-queue-",
model: "one-minute-stream", model: "one-minute-stream",
prompt: "Stay running for queue test.", prompt: "Stay running for queue test.",
@@ -205,8 +205,7 @@ test.describe("Composer attachments", () => {
await expectQueuedMessageButton(page); await expectQueuedMessageButton(page);
await expectComposerDraft(page, ""); await expectComposerDraft(page, "");
} finally { } finally {
await client.close(); await agent.cleanup();
await repo.cleanup();
} }
}); });
@@ -214,7 +213,7 @@ test.describe("Composer attachments", () => {
page, page,
}) => { }) => {
test.setTimeout(120_000); test.setTimeout(120_000);
const { client, repo } = await startRunningMockAgent(page, { const agent = await startRunningMockAgent(page, {
prefix: "attach-interrupt-", prefix: "attach-interrupt-",
model: "ten-second-stream", model: "ten-second-stream",
prompt: "Stay running for interrupt test.", prompt: "Stay running for interrupt test.",
@@ -226,8 +225,7 @@ test.describe("Composer attachments", () => {
await expectAgentIdle(page, 15_000); await expectAgentIdle(page, 15_000);
await expectComposerDraft(page, "preserve me"); await expectComposerDraft(page, "preserve me");
} finally { } finally {
await client.close(); await agent.cleanup();
await repo.cleanup();
} }
}); });
@@ -4,7 +4,7 @@ import { gotoAppShell } from "./helpers/app";
import { connectSeedClient, seedWorkspace } from "./helpers/seed-client"; import { connectSeedClient, seedWorkspace } from "./helpers/seed-client";
import { getServerId } from "./helpers/server-id"; import { getServerId } from "./helpers/server-id";
import { createTempGitRepo } from "./helpers/workspace"; import { createTempGitRepo } from "./helpers/workspace";
import { waitForNoProjectsInSidebar, waitForSidebarHydration } from "./helpers/workspace-ui"; import { waitForSidebarHydration } from "./helpers/workspace-ui";
function workspaceRowTestId(workspaceId: string): string { function workspaceRowTestId(workspaceId: string): string {
return `sidebar-workspace-row-${getServerId()}:${workspaceId}`; return `sidebar-workspace-row-${getServerId()}:${workspaceId}`;
@@ -66,6 +66,13 @@ async function addProjectFromPicker(page: Page, projectPath: string): Promise<st
return testId!.replace("sidebar-project-row-", ""); return testId!.replace("sidebar-project-row-", "");
} }
async function waitForSidebarProjectListReady(page: Page): Promise<void> {
await page
.locator('[data-testid="sidebar-project-empty-state"], [data-testid^="sidebar-project-row-"]')
.first()
.waitFor({ state: "visible", timeout: 60_000 });
}
// Projects are parents in the sidebar. Archiving the last workspace leaves the // Projects are parents in the sidebar. Archiving the last workspace leaves the
// project row in place with a ghost "+ New workspace" child row. // project row in place with a ghost "+ New workspace" child row.
test.describe("Project with no workspaces persists", () => { test.describe("Project with no workspaces persists", () => {
@@ -76,7 +83,7 @@ test.describe("Project with no workspaces persists", () => {
try { try {
await gotoAppShell(page); await gotoAppShell(page);
await waitForNoProjectsInSidebar(page); await waitForSidebarProjectListReady(page);
projectId = await addProjectFromPicker(page, repo.path); projectId = await addProjectFromPicker(page, repo.path);
const projectRow = page.getByTestId(`sidebar-project-row-${projectId}`); const projectRow = page.getByTestId(`sidebar-project-row-${projectId}`);
@@ -92,7 +99,7 @@ test.describe("Project with no workspaces persists", () => {
expect(workspaces.entries).toEqual([]); expect(workspaces.entries).toEqual([]);
} finally { } finally {
if (projectId) { if (projectId) {
await removeProjectFromSidebar(page, projectId).catch(() => undefined); await client.removeProject(projectId).catch(() => undefined);
} }
await client.close().catch(() => undefined); await client.close().catch(() => undefined);
await repo.cleanup().catch(() => undefined); await repo.cleanup().catch(() => undefined);
@@ -163,7 +170,7 @@ test.describe("Project remove", () => {
await expect(projectRow).toHaveCount(0, { timeout: 30_000 }); await expect(projectRow).toHaveCount(0, { timeout: 30_000 });
await page.reload(); await page.reload();
await waitForSidebarHydration(page); await waitForSidebarProjectListReady(page);
await expect(projectRow).toHaveCount(0, { timeout: 30_000 }); await expect(projectRow).toHaveCount(0, { timeout: 30_000 });
const readded = await workspace.client.addProject(workspace.repoPath); const readded = await workspace.client.addProject(workspace.repoPath);
@@ -179,7 +186,6 @@ test.describe("Project remove", () => {
page.getByTestId(`sidebar-project-new-workspace-row-${workspace.projectId}`), page.getByTestId(`sidebar-project-new-workspace-row-${workspace.projectId}`),
).toBeVisible({ timeout: 30_000 }); ).toBeVisible({ timeout: 30_000 });
} finally { } finally {
await removeProjectFromSidebar(page, workspace.projectId).catch(() => undefined);
await workspace.cleanup(); await workspace.cleanup();
} }
}); });
+3 -13
View File
@@ -35,10 +35,6 @@ function buildSeededStoragePayload() {
* idle agent from the same client it uses for everything else. * idle agent from the same client it uses for everything else.
*/ */
export interface IdleAgentSeedClient { export interface IdleAgentSeedClient {
createWorkspace(input: { source: { kind: "directory"; path: string } }): Promise<{
workspace: { id: string } | null;
error: string | null;
}>;
createAgent(options: { createAgent(options: {
provider: string; provider: string;
model: string; model: string;
@@ -56,20 +52,14 @@ export interface IdleAgentSeedClient {
export async function createIdleAgent( export async function createIdleAgent(
client: IdleAgentSeedClient, client: IdleAgentSeedClient,
input: { cwd: string; title: string }, input: { cwd: string; workspaceId: string; title: string },
): Promise<ArchiveTabAgent> { ): Promise<ArchiveTabAgent> {
const createdWorkspace = await client.createWorkspace({
source: { kind: "directory", path: input.cwd },
});
if (!createdWorkspace.workspace) {
throw new Error(createdWorkspace.error ?? `Failed to create workspace ${input.cwd}`);
}
const created = await client.createAgent({ const created = await client.createAgent({
provider: "opencode", provider: "opencode",
model: "opencode/gpt-5-nano", model: "opencode/gpt-5-nano",
modeId: "bypassPermissions", modeId: "bypassPermissions",
cwd: input.cwd, cwd: input.cwd,
workspaceId: createdWorkspace.workspace.id, workspaceId: input.workspaceId,
title: input.title, title: input.title,
}); });
const snapshot = await client.waitForAgentUpsert( const snapshot = await client.waitForAgentUpsert(
@@ -84,7 +74,7 @@ export async function createIdleAgent(
id: created.id, id: created.id,
title: input.title, title: input.title,
cwd: input.cwd, cwd: input.cwd,
workspaceId: createdWorkspace.workspace.id, workspaceId: input.workspaceId,
}; };
} }
+21 -5
View File
@@ -149,6 +149,7 @@ export async function selectGithubOption(
export interface MockAgentSetup { export interface MockAgentSetup {
client: SeedDaemonClient; client: SeedDaemonClient;
repo: Awaited<ReturnType<typeof createTempGitRepo>>; repo: Awaited<ReturnType<typeof createTempGitRepo>>;
cleanup: () => Promise<void>;
} }
/** Create a temp repo, start a mock agent, navigate to it, and wait for it to be running. */ /** Create a temp repo, start a mock agent, navigate to it, and wait for it to be running. */
@@ -166,20 +167,29 @@ export async function startRunningMockAgent(
if (!createdWorkspace.workspace) { if (!createdWorkspace.workspace) {
throw new Error(createdWorkspace.error ?? "Failed to create workspace"); throw new Error(createdWorkspace.error ?? "Failed to create workspace");
} }
const workspace = createdWorkspace.workspace;
const agent = await client.createAgent({ const agent = await client.createAgent({
provider: "mock", provider: "mock",
cwd: repo.path, cwd: repo.path,
workspaceId: createdWorkspace.workspace.id, workspaceId: workspace.id,
model: opts.model, model: opts.model,
}); });
const agentUrl = `${buildHostWorkspaceRoute(serverId, createdWorkspace.workspace.id)}?open=${encodeURIComponent(`agent:${agent.id}`)}`; const agentUrl = `${buildHostWorkspaceRoute(serverId, workspace.id)}?open=${encodeURIComponent(`agent:${agent.id}`)}`;
await page.goto(agentUrl); await page.goto(agentUrl);
await expectComposerVisible(page); await expectComposerVisible(page);
await client.sendAgentMessage(agent.id, opts.prompt); await client.sendAgentMessage(agent.id, opts.prompt);
await expect(page.getByRole("button", { name: /stop|cancel/i }).first()).toBeVisible({ await expect(page.getByRole("button", { name: /stop|cancel/i }).first()).toBeVisible({
timeout: 30_000, timeout: 30_000,
}); });
return { client, repo }; return {
client,
repo,
cleanup: async () => {
await client.removeProject(workspace.projectId).catch(() => undefined);
await client.close().catch(() => undefined);
await repo.cleanup().catch(() => undefined);
},
};
} }
export interface GithubWorkspaceHandle { export interface GithubWorkspaceHandle {
@@ -198,8 +208,14 @@ export async function openGithubWorkspace(
if (!createdWorkspace.workspace) { if (!createdWorkspace.workspace) {
throw new Error(createdWorkspace.error ?? `Failed to create workspace ${repoPath}`); throw new Error(createdWorkspace.error ?? `Failed to create workspace ${repoPath}`);
} }
const workspace = createdWorkspace.workspace;
await gotoAppShell(page); await gotoAppShell(page);
await selectWorkspaceInSidebar(page, createdWorkspace.workspace.id); await selectWorkspaceInSidebar(page, workspace.id);
await waitForTabBar(page); await waitForTabBar(page);
return { cleanup: () => client.close().catch(() => undefined) }; return {
cleanup: async () => {
await client.removeProject(workspace.projectId).catch(() => undefined);
await client.close().catch(() => undefined);
},
};
} }
@@ -18,6 +18,7 @@ type NewWorkspaceDaemonClient = Pick<
| "getPaseoWorktreeList" | "getPaseoWorktreeList"
| "getDaemonConfig" | "getDaemonConfig"
| "patchDaemonConfig" | "patchDaemonConfig"
| "removeProject"
>; >;
type CreateWorkspacePayload = Awaited<ReturnType<NewWorkspaceDaemonClient["createWorkspace"]>>; type CreateWorkspacePayload = Awaited<ReturnType<NewWorkspaceDaemonClient["createWorkspace"]>>;
+3
View File
@@ -14,6 +14,7 @@ export interface AgentHandle {
page: Page; page: Page;
client: SeedDaemonClient; client: SeedDaemonClient;
agentId: string; agentId: string;
projectId: string;
workspaceId: string; workspaceId: string;
cwd: string; cwd: string;
provider: RewindFlowProvider; provider: RewindFlowProvider;
@@ -187,6 +188,7 @@ export async function launchAgent(input: {
page: input.page, page: input.page,
client, client,
agentId: agent.id, agentId: agent.id,
projectId: createdWorkspace.workspace.projectId,
workspaceId: createdWorkspace.workspace.id, workspaceId: createdWorkspace.workspace.id,
cwd: input.cwd, cwd: input.cwd,
provider: input.provider, provider: input.provider,
@@ -196,6 +198,7 @@ export async function launchAgent(input: {
} }
export async function closeAgent(handle: AgentHandle): Promise<void> { export async function closeAgent(handle: AgentHandle): Promise<void> {
await handle.client.removeProject(handle.projectId).catch(() => undefined);
await handle.client.close().catch(() => undefined); await handle.client.close().catch(() => undefined);
} }
+8 -5
View File
@@ -30,6 +30,7 @@ export interface SeedDaemonClient {
} | null; } | null;
error: string | null; error: string | null;
}>; }>;
removeProject(projectId: string): Promise<{ removedWorkspaceIds: string[] }>;
fetchWorkspaces(options?: { filter?: { projectId?: string } }): Promise<{ fetchWorkspaces(options?: { filter?: { projectId?: string } }): Promise<{
entries: SeedWorkspaceDescriptor[]; entries: SeedWorkspaceDescriptor[];
}>; }>;
@@ -199,15 +200,17 @@ export async function seedWorkspace(options: {
if (!created.workspace) { if (!created.workspace) {
throw new Error(created.error ?? `Failed to create workspace ${project.path}`); throw new Error(created.error ?? `Failed to create workspace ${project.path}`);
} }
const workspace = created.workspace;
return { return {
client, client,
repoPath: project.path, repoPath: project.path,
workspaceId: created.workspace.id, workspaceId: workspace.id,
workspaceName: created.workspace.name, workspaceName: workspace.name,
workspaceDirectory: created.workspace.workspaceDirectory, workspaceDirectory: workspace.workspaceDirectory,
projectId: created.workspace.projectId, projectId: workspace.projectId,
projectDisplayName: created.workspace.projectDisplayName, projectDisplayName: workspace.projectDisplayName,
cleanup: async () => { cleanup: async () => {
await client.removeProject(workspace.projectId).catch(() => undefined);
await client.close().catch(() => undefined); await client.close().catch(() => undefined);
await project.cleanup().catch(() => undefined); await project.cleanup().catch(() => undefined);
}, },
+5
View File
@@ -28,15 +28,18 @@ function sleep(ms: number): Promise<void> {
export class TerminalE2EHarness { export class TerminalE2EHarness {
readonly client: SeedDaemonClient; readonly client: SeedDaemonClient;
readonly tempRepo: TempRepo; readonly tempRepo: TempRepo;
readonly projectId: string;
readonly workspaceId: string; readonly workspaceId: string;
private constructor(input: { private constructor(input: {
client: SeedDaemonClient; client: SeedDaemonClient;
tempRepo: TempRepo; tempRepo: TempRepo;
projectId: string;
workspaceId: string; workspaceId: string;
}) { }) {
this.client = input.client; this.client = input.client;
this.tempRepo = input.tempRepo; this.tempRepo = input.tempRepo;
this.projectId = input.projectId;
this.workspaceId = input.workspaceId; this.workspaceId = input.workspaceId;
} }
@@ -54,11 +57,13 @@ export class TerminalE2EHarness {
return new TerminalE2EHarness({ return new TerminalE2EHarness({
client, client,
tempRepo, tempRepo,
projectId: seedResult.workspace.projectId,
workspaceId: seedResult.workspace.id, workspaceId: seedResult.workspace.id,
}); });
} }
async cleanup(): Promise<void> { async cleanup(): Promise<void> {
await this.client.removeProject(this.projectId).catch(() => {});
await this.client.close().catch(() => {}); await this.client.close().catch(() => {});
await this.tempRepo.cleanup().catch(() => {}); await this.tempRepo.cleanup().catch(() => {});
} }
@@ -37,6 +37,7 @@ export function createWithWorkspace(page: Page): WithWorkspaceHandle {
let client: WorkspaceSetupDaemonClient | null = null; let client: WorkspaceSetupDaemonClient | null = null;
const repos: Array<{ cleanup: () => Promise<void> }> = []; const repos: Array<{ cleanup: () => Promise<void> }> = [];
const worktrees: WorktreeRecord[] = []; const worktrees: WorktreeRecord[] = [];
const projectIds = new Set<string>();
const withWorkspace: WithWorkspace = async (options) => { const withWorkspace: WithWorkspace = async (options) => {
if (!client) { if (!client) {
@@ -64,6 +65,7 @@ export function createWithWorkspace(page: Page): WithWorkspaceHandle {
if (!added.project) { if (!added.project) {
throw new Error(added.error ?? `Failed to add project ${repo.path}`); throw new Error(added.error ?? `Failed to add project ${repo.path}`);
} }
projectIds.add(added.project.projectId);
} }
const created = await client.createWorkspace({ const created = await client.createWorkspace({
@@ -73,6 +75,7 @@ export function createWithWorkspace(page: Page): WithWorkspaceHandle {
throw new Error(created.error ?? `Failed to create workspace ${workspacePath}`); throw new Error(created.error ?? `Failed to create workspace ${workspacePath}`);
} }
const workspaceId = created.workspace.id; const workspaceId = created.workspace.id;
projectIds.add(created.workspace.projectId);
return { return {
workspaceId, workspaceId,
@@ -88,6 +91,11 @@ export function createWithWorkspace(page: Page): WithWorkspaceHandle {
return { return {
withWorkspace, withWorkspace,
cleanup: async () => { cleanup: async () => {
if (client) {
for (const projectId of projectIds) {
await client.removeProject(projectId).catch(() => undefined);
}
}
for (const { repoPath, worktreePath } of worktrees) { for (const { repoPath, worktreePath } of worktrees) {
try { try {
execSync(`git worktree remove ${JSON.stringify(worktreePath)} --force`, { execSync(`git worktree remove ${JSON.stringify(worktreePath)} --force`, {
@@ -19,6 +19,7 @@ type WorkspaceSetupDaemonClient = Pick<
| "fetchAgents" | "fetchAgents"
| "fetchWorkspaces" | "fetchWorkspaces"
| "listTerminals" | "listTerminals"
| "removeProject"
| "subscribeRawMessages" | "subscribeRawMessages"
>; >;
@@ -54,10 +54,12 @@ test.describe("Settings toggle tab regression", () => {
try { try {
const firstAgent = await createIdleAgent(workspace.client, { const firstAgent = await createIdleAgent(workspace.client, {
cwd: workspace.repoPath, cwd: workspace.repoPath,
workspaceId: workspace.workspaceId,
title: `settings-toggle-a-${Date.now()}`, title: `settings-toggle-a-${Date.now()}`,
}); });
const secondAgent = await createIdleAgent(workspace.client, { const secondAgent = await createIdleAgent(workspace.client, {
cwd: workspace.repoPath, cwd: workspace.repoPath,
workspaceId: workspace.workspaceId,
title: `settings-toggle-b-${Date.now()}`, title: `settings-toggle-b-${Date.now()}`,
}); });
@@ -95,10 +97,12 @@ test.describe("Settings toggle tab regression", () => {
try { try {
const firstAgent = await createIdleAgent(workspace.client, { const firstAgent = await createIdleAgent(workspace.client, {
cwd: workspace.repoPath, cwd: workspace.repoPath,
workspaceId: workspace.workspaceId,
title: `agent-route-refresh-a-${Date.now()}`, title: `agent-route-refresh-a-${Date.now()}`,
}); });
const secondAgent = await createIdleAgent(workspace.client, { const secondAgent = await createIdleAgent(workspace.client, {
cwd: workspace.repoPath, cwd: workspace.repoPath,
workspaceId: workspace.workspaceId,
title: `agent-route-refresh-b-${Date.now()}`, title: `agent-route-refresh-b-${Date.now()}`,
}); });
@@ -34,6 +34,7 @@ test.describe("Workspace agent tab rename", () => {
const initialTitle = `agent-rename-${randomUUID().slice(0, 8)}`; const initialTitle = `agent-rename-${randomUUID().slice(0, 8)}`;
const agent = await createIdleAgent(workspace.client, { const agent = await createIdleAgent(workspace.client, {
cwd: workspace.repoPath, cwd: workspace.repoPath,
workspaceId: workspace.workspaceId,
title: initialTitle, title: initialTitle,
}); });
@@ -204,6 +204,7 @@ test.describe("Workspace navigation regression", () => {
try { try {
const agent = await createIdleAgent(workspace.client, { const agent = await createIdleAgent(workspace.client, {
cwd: workspace.repoPath, cwd: workspace.repoPath,
workspaceId: workspace.workspaceId,
title: `workspace-reconnect-${Date.now()}`, title: `workspace-reconnect-${Date.now()}`,
}); });
@@ -306,6 +307,7 @@ test.describe("Workspace navigation regression", () => {
try { try {
const agent = await createIdleAgent(workspace.client, { const agent = await createIdleAgent(workspace.client, {
cwd: workspace.repoPath, cwd: workspace.repoPath,
workspaceId: workspace.workspaceId,
title: `workspace-refresh-route-${Date.now()}`, title: `workspace-refresh-route-${Date.now()}`,
}); });
await injectDesktopBridge(page, { await injectDesktopBridge(page, {
@@ -344,10 +346,12 @@ test.describe("Workspace navigation regression", () => {
try { try {
const firstAgent = await createIdleAgent(firstWorkspace.client, { const firstAgent = await createIdleAgent(firstWorkspace.client, {
cwd: firstWorkspace.repoPath, cwd: firstWorkspace.repoPath,
workspaceId: firstWorkspace.workspaceId,
title: `workspace-nav-a-${Date.now()}`, title: `workspace-nav-a-${Date.now()}`,
}); });
const secondAgent = await createIdleAgent(secondWorkspace.client, { const secondAgent = await createIdleAgent(secondWorkspace.client, {
cwd: secondWorkspace.repoPath, cwd: secondWorkspace.repoPath,
workspaceId: secondWorkspace.workspaceId,
title: `workspace-nav-b-${Date.now()}`, title: `workspace-nav-b-${Date.now()}`,
}); });
@@ -18,6 +18,7 @@ test.describe("Workspace pane mounting", () => {
try { try {
const agent = await createIdleAgent(workspace.client, { const agent = await createIdleAgent(workspace.client, {
cwd: workspace.repoPath, cwd: workspace.repoPath,
workspaceId: workspace.workspaceId,
title: `pane-remount-${Date.now()}`, title: `pane-remount-${Date.now()}`,
}); });
@@ -20,6 +20,7 @@ test.describe("Worktree restore after daemon restart", () => {
let worktreeClient: Awaited<ReturnType<typeof connectNewWorkspaceDaemonClient>>; let worktreeClient: Awaited<ReturnType<typeof connectNewWorkspaceDaemonClient>>;
let tempRepo: { path: string; cleanup: () => Promise<void> }; let tempRepo: { path: string; cleanup: () => Promise<void> };
const createdWorktreeDirectories = new Set<string>(); const createdWorktreeDirectories = new Set<string>();
const createdProjectIds = new Set<string>();
test.describe.configure({ retries: 0, timeout: 180_000 }); test.describe.configure({ retries: 0, timeout: 180_000 });
@@ -34,6 +35,10 @@ test.describe("Worktree restore after daemon restart", () => {
await archiveWorkspaceFromDaemon(worktreeClient, directory).catch(() => undefined); await archiveWorkspaceFromDaemon(worktreeClient, directory).catch(() => undefined);
} }
createdWorktreeDirectories.clear(); createdWorktreeDirectories.clear();
for (const projectId of createdProjectIds) {
await worktreeClient.removeProject(projectId).catch(() => undefined);
}
createdProjectIds.clear();
await client?.close().catch(() => undefined); await client?.close().catch(() => undefined);
await worktreeClient?.close().catch(() => undefined); await worktreeClient?.close().catch(() => undefined);
await tempRepo?.cleanup().catch(() => undefined); await tempRepo?.cleanup().catch(() => undefined);
@@ -49,15 +54,18 @@ test.describe("Worktree restore after daemon restart", () => {
// the History table cells must show after restore — never "main". // the History table cells must show after restore — never "main".
const worktreeSlug = `restart-restore-${randomUUID().slice(0, 8)}`; const worktreeSlug = `restart-restore-${randomUUID().slice(0, 8)}`;
await openProjectViaDaemon(worktreeClient, tempRepo.path); const project = await openProjectViaDaemon(worktreeClient, tempRepo.path);
createdProjectIds.add(project.projectKey);
const worktree = await createWorktreeViaDaemon(worktreeClient, { const worktree = await createWorktreeViaDaemon(worktreeClient, {
cwd: tempRepo.path, cwd: tempRepo.path,
slug: worktreeSlug, slug: worktreeSlug,
}); });
createdProjectIds.add(worktree.projectKey);
createdWorktreeDirectories.add(worktree.workspaceDirectory); createdWorktreeDirectories.add(worktree.workspaceDirectory);
const agent = await createIdleAgent(client, { const agent = await createIdleAgent(client, {
cwd: worktree.workspaceDirectory, cwd: worktree.workspaceDirectory,
workspaceId: worktree.workspaceId,
title: `restart-restore-${randomUUID().slice(0, 8)}`, title: `restart-restore-${randomUUID().slice(0, 8)}`,
}); });
expect(existsSync(worktree.workspaceDirectory)).toBe(true); expect(existsSync(worktree.workspaceDirectory)).toBe(true);
+13 -2
View File
@@ -25,6 +25,7 @@ test.describe("Worktree restore", () => {
let worktreeClient: Awaited<ReturnType<typeof connectNewWorkspaceDaemonClient>>; let worktreeClient: Awaited<ReturnType<typeof connectNewWorkspaceDaemonClient>>;
let tempRepo: { path: string; cleanup: () => Promise<void> }; let tempRepo: { path: string; cleanup: () => Promise<void> };
const createdWorktreeDirectories = new Set<string>(); const createdWorktreeDirectories = new Set<string>();
const createdProjectIds = new Set<string>();
test.describe.configure({ timeout: 120_000 }); test.describe.configure({ timeout: 120_000 });
@@ -39,6 +40,10 @@ test.describe("Worktree restore", () => {
await archiveWorkspaceFromDaemon(worktreeClient, directory).catch(() => undefined); await archiveWorkspaceFromDaemon(worktreeClient, directory).catch(() => undefined);
} }
createdWorktreeDirectories.clear(); createdWorktreeDirectories.clear();
for (const projectId of createdProjectIds) {
await worktreeClient.removeProject(projectId).catch(() => undefined);
}
createdProjectIds.clear();
await client?.close().catch(() => undefined); await client?.close().catch(() => undefined);
await worktreeClient?.close().catch(() => undefined); await worktreeClient?.close().catch(() => undefined);
await tempRepo?.cleanup().catch(() => undefined); await tempRepo?.cleanup().catch(() => undefined);
@@ -48,15 +53,18 @@ test.describe("Worktree restore", () => {
page, page,
}) => { }) => {
const serverId = getServerId(); const serverId = getServerId();
await openProjectViaDaemon(worktreeClient, tempRepo.path); const project = await openProjectViaDaemon(worktreeClient, tempRepo.path);
createdProjectIds.add(project.projectKey);
const worktree = await createWorktreeViaDaemon(worktreeClient, { const worktree = await createWorktreeViaDaemon(worktreeClient, {
cwd: tempRepo.path, cwd: tempRepo.path,
slug: `restore-inplace-${randomUUID().slice(0, 8)}`, slug: `restore-inplace-${randomUUID().slice(0, 8)}`,
}); });
createdProjectIds.add(worktree.projectKey);
createdWorktreeDirectories.add(worktree.workspaceDirectory); createdWorktreeDirectories.add(worktree.workspaceDirectory);
const agent = await createIdleAgent(client, { const agent = await createIdleAgent(client, {
cwd: worktree.workspaceDirectory, cwd: worktree.workspaceDirectory,
workspaceId: worktree.workspaceId,
title: `restore-inplace-${randomUUID().slice(0, 8)}`, title: `restore-inplace-${randomUUID().slice(0, 8)}`,
}); });
expect(existsSync(worktree.workspaceDirectory)).toBe(true); expect(existsSync(worktree.workspaceDirectory)).toBe(true);
@@ -90,15 +98,18 @@ test.describe("Worktree restore", () => {
page, page,
}) => { }) => {
const serverId = getServerId(); const serverId = getServerId();
await openProjectViaDaemon(worktreeClient, tempRepo.path); const project = await openProjectViaDaemon(worktreeClient, tempRepo.path);
createdProjectIds.add(project.projectKey);
const worktree = await createWorktreeViaDaemon(worktreeClient, { const worktree = await createWorktreeViaDaemon(worktreeClient, {
cwd: tempRepo.path, cwd: tempRepo.path,
slug: `restore-recreate-${randomUUID().slice(0, 8)}`, slug: `restore-recreate-${randomUUID().slice(0, 8)}`,
}); });
createdProjectIds.add(worktree.projectKey);
createdWorktreeDirectories.add(worktree.workspaceDirectory); createdWorktreeDirectories.add(worktree.workspaceDirectory);
const agent = await createIdleAgent(client, { const agent = await createIdleAgent(client, {
cwd: worktree.workspaceDirectory, cwd: worktree.workspaceDirectory,
workspaceId: worktree.workspaceId,
title: `restore-recreate-${randomUUID().slice(0, 8)}`, title: `restore-recreate-${randomUUID().slice(0, 8)}`,
}); });
expect(existsSync(worktree.workspaceDirectory)).toBe(true); expect(existsSync(worktree.workspaceDirectory)).toBe(true);