mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 17:45:53 +01:00
Guard synthetic finalize after failed workspace finalize
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
db695bd2bc
commit
276e0d83c6
2 files changed
+121
-1
No files matched your search
@@ -2078,6 +2078,126 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not synthesize a successful finalize over a failed workspace finalize", async () => {
|
||||
const { companyId, agentId, runId, issueId: blockerIssueId } = await seedRunFixture({
|
||||
agentStatus: "idle",
|
||||
processPid: null,
|
||||
processGroupId: null,
|
||||
});
|
||||
const projectId = randomUUID();
|
||||
const executionWorkspaceId = randomUUID();
|
||||
const dependentIssueId = randomUUID();
|
||||
|
||||
await db.insert(projects).values({
|
||||
id: projectId,
|
||||
companyId,
|
||||
name: "Failed finalize project",
|
||||
status: "in_progress",
|
||||
});
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
mode: "shared_workspace",
|
||||
strategyType: "local_path",
|
||||
name: "Failed finalize workspace",
|
||||
status: "active",
|
||||
providerType: "local_fs",
|
||||
});
|
||||
await db
|
||||
.update(issues)
|
||||
.set({
|
||||
projectId,
|
||||
status: "done",
|
||||
executionWorkspaceId,
|
||||
completedAt: new Date("2026-03-19T00:01:00.000Z"),
|
||||
})
|
||||
.where(eq(issues.id, blockerIssueId));
|
||||
await db.insert(issues).values({
|
||||
id: dependentIssueId,
|
||||
companyId,
|
||||
projectId,
|
||||
title: "Dependent waiting on failed finalize",
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
assigneeAgentId: agentId,
|
||||
responsibleUserId: "responsible-user",
|
||||
issueNumber: 2,
|
||||
identifier: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}-2`,
|
||||
});
|
||||
await db.insert(issueRelations).values({
|
||||
companyId,
|
||||
issueId: blockerIssueId,
|
||||
relatedIssueId: dependentIssueId,
|
||||
type: "blocks",
|
||||
});
|
||||
await db.insert(workspaceOperations).values([
|
||||
{
|
||||
companyId,
|
||||
executionWorkspaceId,
|
||||
heartbeatRunId: runId,
|
||||
issueId: blockerIssueId,
|
||||
phase: "adapter_execute",
|
||||
status: "succeeded",
|
||||
metadata: { qaFixture: "failed-dead-run-finalize", preFinalize: true },
|
||||
startedAt: new Date("2026-03-19T00:00:30.000Z"),
|
||||
finishedAt: new Date("2026-03-19T00:00:45.000Z"),
|
||||
},
|
||||
{
|
||||
companyId,
|
||||
executionWorkspaceId,
|
||||
heartbeatRunId: runId,
|
||||
issueId: blockerIssueId,
|
||||
phase: "workspace_finalize",
|
||||
status: "failed",
|
||||
metadata: { qaFixture: "failed-dead-run-finalize", error: "sync-back failed" },
|
||||
startedAt: new Date("2026-03-19T00:00:50.000Z"),
|
||||
finishedAt: new Date("2026-03-19T00:00:55.000Z"),
|
||||
},
|
||||
]);
|
||||
|
||||
await heartbeatService(db).reapOrphanedRuns();
|
||||
|
||||
const finalizeRows = await db
|
||||
.select({
|
||||
phase: workspaceOperations.phase,
|
||||
status: workspaceOperations.status,
|
||||
metadata: workspaceOperations.metadata,
|
||||
})
|
||||
.from(workspaceOperations)
|
||||
.where(and(
|
||||
eq(workspaceOperations.heartbeatRunId, runId),
|
||||
eq(workspaceOperations.phase, "workspace_finalize"),
|
||||
));
|
||||
expect(finalizeRows).toEqual([{
|
||||
phase: "workspace_finalize",
|
||||
status: "failed",
|
||||
metadata: expect.objectContaining({ qaFixture: "failed-dead-run-finalize" }),
|
||||
}]);
|
||||
|
||||
const after = await issueService(db).getDependencyReadiness(dependentIssueId);
|
||||
expect(after).toMatchObject({
|
||||
allBlockersDone: false,
|
||||
isDependencyReady: false,
|
||||
unresolvedBlockerCount: 1,
|
||||
pendingFinalizeBlockerIssueIds: [blockerIssueId],
|
||||
});
|
||||
|
||||
const reconciled = await heartbeatService(db).reconcileIssueGraphLiveness();
|
||||
expect(reconciled.dependencyWakesHealed).toBe(0);
|
||||
expect(reconciled.dependencyWakeIssueIds).toEqual([]);
|
||||
|
||||
const wake = await db
|
||||
.select({ id: agentWakeupRequests.id })
|
||||
.from(agentWakeupRequests)
|
||||
.where(and(
|
||||
eq(agentWakeupRequests.agentId, agentId),
|
||||
eq(agentWakeupRequests.reason, "issue_blockers_resolved"),
|
||||
))
|
||||
.then((rows) => rows[0] ?? null);
|
||||
expect(wake).toBeNull();
|
||||
});
|
||||
|
||||
it("leaves an existing successful live workspace finalize unchanged when reaping a dead done run", async () => {
|
||||
const { companyId, runId, issueId } = await seedRunFixture({
|
||||
agentStatus: "idle",
|
||||
|
||||
@@ -12249,7 +12249,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
if (
|
||||
!latestOperation ||
|
||||
latestOperation.heartbeatRunId !== run.id ||
|
||||
(latestOperation.phase === "workspace_finalize" && latestOperation.status === "succeeded")
|
||||
latestOperation.phase === "workspace_finalize"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user