diff --git a/server/src/__tests__/issues-service.test.ts b/server/src/__tests__/issues-service.test.ts index 0d4fd35b825..20ef32aa722 100644 --- a/server/src/__tests__/issues-service.test.ts +++ b/server/src/__tests__/issues-service.test.ts @@ -3103,6 +3103,139 @@ describeEmbeddedPostgres("issueService blockers and dependency wake readiness", }); }); + it("keeps a legacy blocker finalize barrier when a later sibling has run-attributed workspace ops", async () => { + const companyId = randomUUID(); + const assigneeAgentId = randomUUID(); + const projectId = randomUUID(); + const projectWorkspaceId = randomUUID(); + const executionWorkspaceId = randomUUID(); + const siblingRunId = randomUUID(); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + requireBoardApprovalForNewAgents: false, + }); + await db.insert(agents).values({ + id: assigneeAgentId, + companyId, + name: "QA", + role: "qa", + status: "active", + adapterType: "claude_local", + adapterConfig: {}, + runtimeConfig: {}, + permissions: {}, + }); + await db.insert(projects).values({ + id: projectId, + companyId, + name: "Shared workspace project", + status: "in_progress", + }); + await db.insert(projectWorkspaces).values({ + id: projectWorkspaceId, + companyId, + projectId, + name: "Shared workspace", + sourceType: "local_path", + visibility: "default", + isPrimary: true, + }); + await db.insert(executionWorkspaces).values({ + id: executionWorkspaceId, + companyId, + projectId, + projectWorkspaceId, + mode: "isolated_workspace", + strategyType: "git_worktree", + name: "Shared exec workspace", + status: "active", + providerType: "git_worktree", + }); + + const blockerId = randomUUID(); + const siblingId = randomUUID(); + const dependentId = randomUUID(); + await db.insert(issues).values([ + { + id: blockerId, + companyId, + projectId, + title: "Legacy predecessor", + status: "done", + priority: "medium", + executionWorkspaceId, + }, + { + id: siblingId, + companyId, + projectId, + title: "Later sibling", + status: "in_progress", + priority: "medium", + executionWorkspaceId, + }, + { + id: dependentId, + companyId, + projectId, + title: "Dependent", + status: "blocked", + priority: "medium", + assigneeAgentId, + }, + ]); + await db.insert(heartbeatRuns).values({ + id: siblingRunId, + companyId, + agentId: assigneeAgentId, + invocationSource: "assignment", + triggerDetail: "system", + status: "running", + contextSnapshot: { issueId: siblingId, taskId: siblingId }, + }); + await db.insert(workspaceOperations).values([ + { + companyId, + executionWorkspaceId, + phase: "worktree_prepare", + status: "succeeded", + startedAt: new Date("2026-05-23T22:00:00.000Z"), + }, + { + companyId, + executionWorkspaceId, + heartbeatRunId: siblingRunId, + phase: "worktree_prepare", + status: "succeeded", + startedAt: new Date("2026-05-23T22:15:00.000Z"), + }, + ]); + await svc.update(dependentId, { blockedByIssueIds: [blockerId] }); + + await expect(svc.getDependencyReadiness(dependentId)).resolves.toMatchObject({ + isDependencyReady: false, + pendingFinalizeBlockerIssueIds: [blockerId], + unresolvedBlockerIssueIds: [blockerId], + }); + + await db.insert(workspaceOperations).values({ + companyId, + executionWorkspaceId, + phase: "workspace_finalize", + status: "succeeded", + startedAt: new Date("2026-05-23T22:20:00.000Z"), + }); + + await expect(svc.getDependencyReadiness(dependentId)).resolves.toMatchObject({ + isDependencyReady: true, + pendingFinalizeBlockerIssueIds: [], + unresolvedBlockerIssueIds: [], + }); + }); + it("keeps a reused-workspace blocker ready when a later sibling operation is unfinalized", async () => { const companyId = randomUUID(); const assigneeAgentId = randomUUID(); diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index 78ec84becce..1949e716d61 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -697,7 +697,6 @@ async function listPendingFinalizeBlockerIssueIds( const latestByBlockerIssueId = new Map(); const latestLegacyByWorkspaceId = new Map(); - const workspacesWithRunAttributedOps = new Set(); for (const row of rows) { if (!row.executionWorkspaceId) continue; const blockerIds = blockerIdsByWorkspaceId.get(row.executionWorkspaceId); @@ -716,7 +715,6 @@ async function listPendingFinalizeBlockerIssueIds( } continue; } - workspacesWithRunAttributedOps.add(row.executionWorkspaceId); for (const blockerIssueId of runIssueIds) { if (!blockerIds.has(blockerIssueId)) continue; const current = latestByBlockerIssueId.get(blockerIssueId); @@ -733,9 +731,7 @@ async function listPendingFinalizeBlockerIssueIds( for (const target of targets) { const workspaceId = target.executionWorkspaceId; const latest = latestByBlockerIssueId.get(target.blockerIssueId) ?? - (workspaceId && !workspacesWithRunAttributedOps.has(workspaceId) - ? latestLegacyByWorkspaceId.get(workspaceId) - : null); + (workspaceId ? latestLegacyByWorkspaceId.get(workspaceId) : null); if (!latest) continue; // no blocker-specific workspace ops recorded -> no finalize barrier if (latest.phase === "workspace_finalize" && latest.status === "succeeded") continue; pending.add(target.blockerIssueId);