Unblock closed liveness recovery issues in graph reconciliation

This commit is contained in:
Dotta committed 2026-05-17 11:30:44 -05:00
1 parent 986df210a3
commit 53493eb3e9
2 files changed
+63

No files matched your search

@@ -724,4 +724,43 @@ describeEmbeddedPostgres("heartbeat issue graph liveness escalation", () => {
expect(blockers.some((row) => row.blockerIssueId === closedEscalationId)).toBe(false);
expect(blockers.some((row) => row.blockerIssueId === freshEscalation?.id)).toBe(true);
});
it("removes closed liveness escalations from blocker relations during reconciliation", async () => {
await enableAutoRecovery();
const { companyId, blockedIssueId, blockerIssueId } = await seedBlockedChain();
const heartbeat = heartbeatService(db);
const first = await heartbeat.reconcileIssueGraphLiveness();
expect(first.escalationsCreated).toBe(1);
const escalations = await db
.select()
.from(issues)
.where(
and(
eq(issues.companyId, companyId),
eq(issues.originKind, "harness_liveness_escalation"),
),
);
expect(escalations).toHaveLength(1);
await db
.update(issues)
.set({ status: "done", blockedByIssueIds: [] })
.where(eq(issues.id, escalations[0]!.id));
await db
.update(issues)
.set({ status: "done", blockedByIssueIds: [] })
.where(eq(issues.id, blockerIssueId));
const second = await heartbeat.reconcileIssueGraphLiveness();
expect(second.obsoleteRecoveryBlockerRelationsRemoved).toBe(0);
expect(second.doneRecoveryBlockerRelationsRemoved).toBe(1);
const blockers = await db
.select({ blockerIssueId: issueRelations.issueId })
.from(issueRelations)
.where(eq(issueRelations.relatedIssueId, blockedIssueId));
expect(blockers.some((row) => row.blockerIssueId === escalations[0]!.id)).toBe(false);
});
});
+24
View File
@@ -2590,6 +2590,28 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup })
return result;
}
async function retireDoneLivenessRecoveryBlockers() {
const closedRecoveries = await db
.select()
.from(issues)
.where(
and(
eq(issues.originKind, RECOVERY_ORIGIN_KINDS.issueGraphLivenessEscalation),
isNull(issues.hiddenAt),
inArray(issues.status, ["done", "cancelled"]),
),
);
let blockerRelationsRemoved = 0;
for (const recovery of closedRecoveries) {
if (await removeRecoveryBlockerFromSource(recovery)) {
blockerRelationsRemoved += 1;
}
}
return { blockerRelationsRemoved };
}
function normalizeIssueGraphLivenessAutoRecoveryLookbackHours(raw: unknown) {
const numeric = Math.floor(asNumber(raw, DEFAULT_ISSUE_GRAPH_LIVENESS_AUTO_RECOVERY_LOOKBACK_HOURS));
return Math.min(
@@ -2985,6 +3007,7 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup })
const now = new Date();
const cutoff = new Date(now.getTime() - lookbackHours * 60 * 60 * 1000);
const obsoleteRecoveryCleanup = await retireObsoleteLivenessRecoveryIssues(findings);
const doneRecoveryBlockerCleanup = await retireDoneLivenessRecoveryBlockers();
const updatedAtByIssueKey = await loadLivenessDependencyUpdatedAtByIssue(findings);
const result = {
findings: findings.length,
@@ -2999,6 +3022,7 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup })
obsoleteRecoveriesRetired: obsoleteRecoveryCleanup.retired,
obsoleteRecoveriesActiveSkipped: obsoleteRecoveryCleanup.activeSkipped,
obsoleteRecoveryBlockerRelationsRemoved: obsoleteRecoveryCleanup.blockerRelationsRemoved,
doneRecoveryBlockerRelationsRemoved: doneRecoveryBlockerCleanup.blockerRelationsRemoved,
issueIds: [] as string[],
escalationIssueIds: [] as string[],
retiredRecoveryIssueIds: obsoleteRecoveryCleanup.retiredIssueIds,