* fix(workspaces): keep restored merged workspaces active Auto-archive previously reacted to every merged pull request snapshot, so restoring a workspace immediately archived it again. Persist the consumed change request with the archive transition so explicit recovery remains authoritative. * test(e2e): clean partial archive scenarios Track workspace resources as soon as creation succeeds so later setup failures archive the worktree and remove the registered project. * test(workspaces): pin archive latch restoration Cover the persisted archive marker across restore in CI and make the real browser assertion observe active workspace stability after refresh. * fix(workspaces): latch restored legacy archives Resolve the current merged change request while restoring records created before the archive marker existed, then persist it with the unarchive mutation. * fix(workspaces): refresh latch on restore When an archived workspace now points at a different merged change request, record that URL during restore so the restored workspace remains active. * test(e2e): make archive latch setup fail loudly Remove authentication-gated skipping and register each external fixture cleanup immediately after allocation so setup failures cannot leak resources. * test(workspaces): include archive latch in reconciliation
176 lines
6.3 KiB
TypeScript
176 lines
6.3 KiB
TypeScript
import { existsSync } from "node:fs";
|
|
import { expect, test, type Page } from "./fixtures";
|
|
import { gotoAppShell } from "./helpers/app";
|
|
import { clickSessionRow, expectSessionRowArchived, openSessions } from "./helpers/archive-tab";
|
|
import { cloneGithubRepoDefaultBranchOnly, createTempGithubRepo } from "./helpers/github-fixtures";
|
|
import {
|
|
archiveWorkspaceFromDaemon,
|
|
connectNewWorkspaceDaemonClient,
|
|
} from "./helpers/new-workspace";
|
|
import { connectSeedClient } from "./helpers/seed-client";
|
|
import { getServerId } from "./helpers/server-id";
|
|
import { waitForSidebarHydration, waitForWorkspaceInSidebar } from "./helpers/workspace-ui";
|
|
|
|
test.describe("Auto-archive after pull request merge", () => {
|
|
test.describe.configure({ retries: 0, timeout: 180_000 });
|
|
|
|
test("manual restore latches and keeps a merged pull request workspace active", async ({
|
|
page,
|
|
}) => {
|
|
const scenario = await createMergedPullRequestScenario();
|
|
|
|
try {
|
|
await scenario.autoArchive();
|
|
await openArchivedWorkspaceFromHistory(page, scenario.agentTitle);
|
|
await restoreWorkspace(page, scenario.workspaceId);
|
|
|
|
await scenario.refreshMergedPullRequest();
|
|
await scenario.expectWorkspaceRemainsActive();
|
|
await waitForWorkspaceInSidebar(page, {
|
|
serverId: getServerId(),
|
|
workspaceId: scenario.workspaceId,
|
|
});
|
|
} finally {
|
|
await scenario.cleanup();
|
|
}
|
|
});
|
|
});
|
|
|
|
interface MergedPullRequestScenario {
|
|
agentTitle: string;
|
|
workspaceId: string;
|
|
autoArchive(): Promise<void>;
|
|
refreshMergedPullRequest(): Promise<void>;
|
|
expectWorkspaceRemainsActive(): Promise<void>;
|
|
cleanup(): Promise<void>;
|
|
}
|
|
|
|
async function createMergedPullRequestScenario(): Promise<MergedPullRequestScenario> {
|
|
const cleanups: Array<() => Promise<unknown>> = [];
|
|
|
|
try {
|
|
const repo = await createTempGithubRepo({
|
|
category: "auto-archive-latch",
|
|
prs: [{ title: "Auto-archive latch", state: "merged" }],
|
|
});
|
|
cleanups.push(() => repo.cleanup());
|
|
const checkout = await cloneGithubRepoDefaultBranchOnly(repo);
|
|
cleanups.push(() => checkout.cleanup());
|
|
const workspaceClient = await connectNewWorkspaceDaemonClient();
|
|
cleanups.push(() => workspaceClient.close());
|
|
const agentClient = await connectSeedClient();
|
|
cleanups.push(() => agentClient.close());
|
|
const previousConfig = await workspaceClient.getDaemonConfig();
|
|
await workspaceClient.patchDaemonConfig({ autoArchiveAfterMerge: false });
|
|
cleanups.push(() =>
|
|
workspaceClient.patchDaemonConfig({
|
|
autoArchiveAfterMerge: previousConfig.config.autoArchiveAfterMerge,
|
|
}),
|
|
);
|
|
|
|
const pullRequest = repo.prs[0];
|
|
if (!pullRequest) {
|
|
throw new Error("Expected the merged pull request fixture");
|
|
}
|
|
const created = await workspaceClient.createPaseoWorktree({
|
|
cwd: checkout.path,
|
|
action: "checkout",
|
|
checkoutSource: {
|
|
kind: "change_request",
|
|
forge: "github",
|
|
number: pullRequest.number,
|
|
},
|
|
worktreeSlug: `auto-archive-latch-${Date.now()}`,
|
|
});
|
|
if (!created.workspace || created.error) {
|
|
throw new Error(created.error ?? "Failed to create merged pull request workspace");
|
|
}
|
|
|
|
const workspace = created.workspace;
|
|
cleanups.push(() => workspaceClient.removeProject(workspace.projectId));
|
|
cleanups.push(() => archiveWorkspaceFromDaemon(workspaceClient, workspace.workspaceDirectory));
|
|
const agentTitle = `Auto-archive latch ${Date.now()}`;
|
|
const agent = await agentClient.createAgent({
|
|
provider: "mock",
|
|
model: "ten-second-stream",
|
|
modeId: "load-test",
|
|
cwd: workspace.workspaceDirectory,
|
|
workspaceId: workspace.id,
|
|
title: agentTitle,
|
|
});
|
|
await agentClient.waitForAgentUpsert(
|
|
agent.id,
|
|
(snapshot) => snapshot.status === "idle",
|
|
30_000,
|
|
);
|
|
|
|
return {
|
|
agentTitle,
|
|
workspaceId: workspace.id,
|
|
autoArchive: async () => {
|
|
await workspaceClient.patchDaemonConfig({ autoArchiveAfterMerge: true });
|
|
await workspaceClient.checkoutRefresh(workspace.workspaceDirectory);
|
|
await expect
|
|
.poll(() => agentClient.fetchAgent({ agentId: agent.id }), { timeout: 30_000 })
|
|
.toMatchObject({ agent: { archivedAt: expect.any(String) } });
|
|
await expect
|
|
.poll(() => existsSync(workspace.workspaceDirectory), { timeout: 30_000 })
|
|
.toBe(false);
|
|
},
|
|
refreshMergedPullRequest: async () => {
|
|
const refresh = await workspaceClient.checkoutRefresh(workspace.workspaceDirectory);
|
|
if (!refresh.success) {
|
|
throw new Error(`Failed to refresh restored workspace: ${String(refresh.error)}`);
|
|
}
|
|
},
|
|
expectWorkspaceRemainsActive: async () => {
|
|
const observationDeadline = Date.now() + 5_000;
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const descriptor = (await workspaceClient.fetchWorkspaces()).entries.find(
|
|
(entry) => entry.id === workspace.id,
|
|
);
|
|
if (!descriptor) {
|
|
return "archived";
|
|
}
|
|
return Date.now() >= observationDeadline ? "active" : "observing";
|
|
},
|
|
{ timeout: 10_000, intervals: [100, 250, 500] },
|
|
)
|
|
.toBe("active");
|
|
},
|
|
cleanup: () => runScenarioCleanups(cleanups),
|
|
};
|
|
} catch (error) {
|
|
await runScenarioCleanups(cleanups);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function runScenarioCleanups(cleanups: Array<() => Promise<unknown>>): Promise<void> {
|
|
while (cleanups.length > 0) {
|
|
await cleanups
|
|
.pop()?.()
|
|
.catch(() => undefined);
|
|
}
|
|
}
|
|
|
|
async function openArchivedWorkspaceFromHistory(page: Page, agentTitle: string): Promise<void> {
|
|
await gotoAppShell(page);
|
|
await waitForSidebarHydration(page);
|
|
await openSessions(page);
|
|
await expectSessionRowArchived(page, agentTitle);
|
|
await clickSessionRow(page, agentTitle);
|
|
await expect(page.getByText("Workspace archived", { exact: true })).toBeVisible({
|
|
timeout: 30_000,
|
|
});
|
|
}
|
|
|
|
async function restoreWorkspace(page: Page, workspaceId: string): Promise<void> {
|
|
const action = page.getByTestId("workspace-recovery-action");
|
|
await expect(action).toHaveText("Restore");
|
|
await action.click();
|
|
await waitForWorkspaceInSidebar(page, { serverId: getServerId(), workspaceId });
|
|
}
|