import { describe, expect, it } from "vitest"; import { decideRecovery } from "../src/services/recoveryPolicy.js"; import { normalizeAutonomyState, retryAutonomyBlockerIfDue } from "../src/repositories/autonomyRepository.js"; describe("bounded Auto Mode recovery policy", () => { it("walks through retry, cooldown and alternate recovery before escalation", () => { const sequence = [1, 2, 3, 4, 5, 6].map((recoveryAttempts) => decideRecovery({ failureCount: recoveryAttempts, recoveryAttempts, recoveryStartedAt: "2026-09-12T10:00:00.000Z", maxRecoveryMinutes: 120, now: Date.parse("2026-09-12T10:10:00.000Z"), }).stage); expect(sequence).toEqual(["retry", "retry", "cooldown", "retry", "alternate_recovery", "escalated"]); }); it("stops on the wall-clock budget even if attempts are low", () => { expect(decideRecovery({ failureCount: 1, recoveryAttempts: 1, recoveryStartedAt: "2026-09-12T08:00:00.000Z", maxRecoveryMinutes: 30, now: Date.parse("2026-09-12T08:31:00.000Z"), })).toMatchObject({ stage: "escalated", exhausted: true, reason: "recovery_wall_clock_limit" }); }); it("escalates a persisted blocker when its cooldown wakes after the wall-clock budget", () => { const state = normalizeAutonomyState({ enabled: true, maxRecoveryMinutes: 30, recoveryStartedAt: "2026-09-12T08:00:00.000Z", blocker: { at: "2026-09-12T08:00:00.000Z", phaseId: "phase-1", reason: "Runner unavailable", kind: "retryable", retryAt: "2026-09-12T08:31:00.000Z", recoveryStage: "retry", recoveryAttempts: 1, recoveryStartedAt: "2026-09-12T08:00:00.000Z" }, roadmap: { id: "roadmap-1", title: "Release", status: "blocked", currentPhaseIndex: 0, importedAt: "2026-09-12T08:00:00.000Z", phases: [{ id: "phase-1", title: "Build", objective: "Build", status: "blocked", items: [] }] }, }); const result = retryAutonomyBlockerIfDue(state, Date.parse("2026-09-12T08:31:00.000Z")); expect(result.blocker).toMatchObject({ recoveryStage: "escalated" }); expect(result.status).toBe("blocked"); expect(result.roadmap?.status).toBe("blocked"); }); });