test(server): cover the reopen-vs-reap lifecycle race

Add reaper regression coverage for the reverse-ordering race:
- the sweep does not archive a reopened workspace while the source issue is
  still terminal; the workspace stays active and the worktree stays intact;
- the sweep clears the stale reopen flag once the source issue leaves the
  terminal state, so a later terminal cycle can reap the workspace.

Assert the reopen sets the reopen-pending flag on the active row.

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Priya RamanandPaperclip committed 2026-08-13 01:46:26 +00:00
1 parent f382df24b7
commit 6317d295c7
2 files changed
+74

No files matched your search

@@ -21,6 +21,7 @@ import {
EXECUTION_WORKSPACE_LIFECYCLE_GENERATION_METADATA_KEY,
EXECUTION_WORKSPACE_REOPEN_FAILED_REASON,
executionWorkspaceService,
metadataHasReopenPendingConsumption,
readExecutionWorkspaceLifecycleGeneration,
} from "../services/execution-workspaces.js";
@@ -176,6 +177,9 @@ describeEmbeddedPostgres("reopen archived isolated execution workspace", () => {
expect(row?.cleanupReason).toBeNull();
expect(row?.cleanupEligibleAt).toBeNull();
expect(readExecutionWorkspaceLifecycleGeneration(row?.metadata as Record<string, unknown> | null)).toBe(4);
// The reopen flags the row so the terminal reaper does not archive and
// destroy the rebuilt worktree before the caller consumes it.
expect(metadataHasReopenPendingConsumption(row?.metadata as Record<string, unknown> | null)).toBe(true);
// The reopen never changes the issue-to-workspace link.
const issueRow = await db.select().from(issues).where(eq(issues.id, issueId)).then((rows) => rows[0] ?? null);
@@ -28,9 +28,12 @@ import {
startEmbeddedPostgresTestDatabase,
} from "./helpers/embedded-postgres.js";
import {
EXECUTION_WORKSPACE_LIFECYCLE_GENERATION_METADATA_KEY,
EXECUTION_WORKSPACE_REOPEN_PENDING_METADATA_KEY,
executionWorkspaceService,
deriveExecutionWorkspaceDeliveryState,
mergeExecutionWorkspaceConfig,
metadataHasReopenPendingConsumption,
readExecutionWorkspaceConfig,
} from "../services/execution-workspaces.ts";
import { issueService } from "../services/issues.ts";
@@ -952,6 +955,73 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
await expect(fs.access(cleanupMarker)).rejects.toThrow();
});
it("does not reap a reopened workspace while the source issue is still terminal", async () => {
// Reproduce the reverse-ordering race. A resume reopens the archived
// workspace and publishes it active, but the route has not yet changed the
// source issue out of the terminal state. The sweep must not archive and
// destroy the rebuilt worktree in this window.
const seeded = await seedTerminalWorkspace({ mergedPr: true });
await db
.update(executionWorkspaces)
.set({
status: "active",
closedAt: null,
cleanupReason: null,
cleanupEligibleAt: null,
metadata: {
createdByRuntime: true,
[EXECUTION_WORKSPACE_LIFECYCLE_GENERATION_METADATA_KEY]: 4,
[EXECUTION_WORKSPACE_REOPEN_PENDING_METADATA_KEY]: true,
},
})
.where(eq(executionWorkspaces.id, seeded.executionWorkspaceId));
const sweep = await svc.sweepTerminalWorkspaces();
const [workspace] = await db
.select({ status: executionWorkspaces.status, metadata: executionWorkspaces.metadata })
.from(executionWorkspaces)
.where(eq(executionWorkspaces.id, seeded.executionWorkspaceId));
expect(sweep).toMatchObject({ archived: 0, skippedReopened: 1 });
expect(workspace?.status).toBe("active");
// The reopen flag stays until the source issue leaves the terminal state.
expect(metadataHasReopenPendingConsumption(workspace?.metadata as Record<string, unknown> | null)).toBe(true);
// The rebuilt worktree is intact.
await expect(fs.access(seeded.worktreePath)).resolves.toBeUndefined();
});
it("clears the reopen flag once the source issue leaves the terminal state", async () => {
// The resume transition committed, so the source issue is non-terminal. The
// sweep clears the stale reopen flag so a later terminal cycle can reap the
// workspace normally.
const seeded = await seedTerminalWorkspace({ mergedPr: true });
await db
.update(executionWorkspaces)
.set({
metadata: {
createdByRuntime: true,
[EXECUTION_WORKSPACE_LIFECYCLE_GENERATION_METADATA_KEY]: 4,
[EXECUTION_WORKSPACE_REOPEN_PENDING_METADATA_KEY]: true,
},
})
.where(eq(executionWorkspaces.id, seeded.executionWorkspaceId));
await db
.update(issues)
.set({ status: "in_progress" })
.where(eq(issues.id, seeded.sourceIssueId));
const sweep = await svc.sweepTerminalWorkspaces();
const [workspace] = await db
.select({ status: executionWorkspaces.status, metadata: executionWorkspaces.metadata })
.from(executionWorkspaces)
.where(eq(executionWorkspaces.id, seeded.executionWorkspaceId));
expect(sweep).toMatchObject({ archived: 0, skippedNonTerminalTree: 1 });
expect(workspace?.status).toBe("active");
expect(metadataHasReopenPendingConsumption(workspace?.metadata as Record<string, unknown> | null)).toBe(false);
await expect(fs.access(seeded.worktreePath)).resolves.toBeUndefined();
});
it("holds Git index and ref locks across terminal cleanup", async () => {
const seeded = await seedTerminalWorkspace({ mergedPr: true });
await db.update(executionWorkspaces).set({