diff --git a/server/src/__tests__/heartbeat-issue-liveness-escalation.test.ts b/server/src/__tests__/heartbeat-issue-liveness-escalation.test.ts index 369bb05c2b7..10ad103cd44 100644 --- a/server/src/__tests__/heartbeat-issue-liveness-escalation.test.ts +++ b/server/src/__tests__/heartbeat-issue-liveness-escalation.test.ts @@ -289,10 +289,23 @@ describeEmbeddedPostgres("heartbeat issue graph liveness escalation", () => { const heartbeat = heartbeatService(db); const first = await heartbeat.reconcileIssueGraphLiveness(); - const second = await heartbeat.reconcileIssueGraphLiveness(); expect(first.escalationsCreated).toBe(1); + const [sourceAfterFirst] = await db + .select({ updatedAt: issues.updatedAt }) + .from(issues) + .where(eq(issues.id, blockedIssueId)); + const eventsAfterFirst = await db.select().from(activityLog).where(eq(activityLog.companyId, companyId)); + expect(eventsAfterFirst.filter((event) => event.action === "issue.blockers.updated")).toHaveLength(1); + + const second = await heartbeat.reconcileIssueGraphLiveness(); + expect(second.escalationsCreated).toBe(0); + const [sourceAfterSecond] = await db + .select({ updatedAt: issues.updatedAt }) + .from(issues) + .where(eq(issues.id, blockedIssueId)); + expect(sourceAfterSecond?.updatedAt.getTime()).toBe(sourceAfterFirst?.updatedAt.getTime()); const escalations = await db .select() @@ -345,7 +358,7 @@ describeEmbeddedPostgres("heartbeat issue graph liveness escalation", () => { projectWorkspaceSourceIssueId: blockerIssueId, }, }); - expect(events.some((event) => event.action === "issue.blockers.updated")).toBe(true); + expect(events.filter((event) => event.action === "issue.blockers.updated")).toHaveLength(1); }); it("skips budget-blocked direct owners and assigns recovery to the manager fallback", async () => { diff --git a/server/src/services/recovery/service.ts b/server/src/services/recovery/service.ts index f544f4085fa..9ccc66ed376 100644 --- a/server/src/services/recovery/service.ts +++ b/server/src/services/recovery/service.ts @@ -2250,10 +2250,16 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup }) }) { const blockerIds = await existingBlockerIssueIds(input.issue.companyId, input.issue.id); const nextBlockerIds = [...new Set([...blockerIds, input.escalationIssueId])]; + const isAlreadyBlockedByEscalation = blockerIds.includes(input.escalationIssueId); + const isAlreadyBlocked = input.issue.status === "blocked"; + if (isAlreadyBlockedByEscalation && isAlreadyBlocked) { + return input.issue; + } + const update: Partial & { blockedByIssueIds: string[] } = { blockedByIssueIds: nextBlockerIds, }; - if (input.issue.status !== "blocked") { + if (!isAlreadyBlocked) { update.status = "blocked"; }