test(FN-4890): cover board-stall verification and cooldown

Fusion-Task-Id: FN-4890
Fusion-Task-Lineage: d1ae6585-7df0-481f-83c3-c7cdd45daf4f
This commit is contained in:
Fusion (runfusion.ai)
2026-05-17 10:17:00 -07:00
committed by gsxdsm
parent dfeb379a14
commit 5f6dddf863
2 changed files with 36 additions and 2 deletions

View File

@@ -84,4 +84,39 @@ describe("reliability interactions: board stall auto-recovery", () => {
expect(audits).toContain("task:auto-board-stall-unrecovered");
expect(notify).toHaveBeenCalledTimes(1);
});
it("skips unrecovered notification when progress resumes", async () => {
const holder = makeTask("FN-10", { column: "in-progress", paused: true, pausedReason: "waiting" });
const { store, byId } = makeStore([holder]);
const notify = vi.fn(async () => undefined);
const manager = new SelfHealingManager(store, { rootDir: process.cwd(), getExecutingTaskIds: () => new Set(), ntfyNotifier: { notifyBoardStallUnrecovered: notify } });
await manager.runBoardStallAutoRecoverySweep();
byId.set("FN-11", makeTask("FN-11", { column: "todo", blockedBy: "FN-10" }));
await manager.runBoardStallAutoRecoverySweep();
(manager as any).boardStallWindow.transitionsOutOfInProgressInWindow = 1;
(manager as any).maintenanceTickCounter++;
const verification = await manager.runBoardStallAutoRecoverySweep();
expect(verification.unrecovered).toBe(false);
expect(notify).not.toHaveBeenCalled();
});
it("enforces cooldown for repeated unrecovered alerts", async () => {
const holder = makeTask("FN-20", { column: "in-progress", paused: true, pausedReason: "waiting" });
const { store, byId } = makeStore([holder]);
const notify = vi.fn(async () => undefined);
const manager = new SelfHealingManager(store, { rootDir: process.cwd(), getExecutingTaskIds: () => new Set(), ntfyNotifier: { notifyBoardStallUnrecovered: notify } });
await manager.runBoardStallAutoRecoverySweep();
byId.set("FN-21", makeTask("FN-21", { column: "todo", blockedBy: "FN-20" }));
await manager.runBoardStallAutoRecoverySweep();
(manager as any).maintenanceTickCounter++;
await manager.runBoardStallAutoRecoverySweep();
(manager as any).boardStallWindow.pendingVerification = { holderIds: ["FN-20"], followerCount: 1, startedAt: Date.now(), tick: 0 };
(manager as any).maintenanceTickCounter++;
await manager.runBoardStallAutoRecoverySweep();
expect(notify).toHaveBeenCalledTimes(1);
});
});

View File

@@ -2582,7 +2582,7 @@ export class SelfHealingManager {
if (window.pendingVerification && this.maintenanceTickCounter > window.pendingVerification.tick) {
const noProgress = window.transitionsOutOfInProgressInWindow === 0;
if (noProgress) {
const ntfyAllowed = window.lastNtfyAt === null || now - window.lastNtfyAt >= 15 * 60_000;
const ntfyAllowed = window.lastNtfyAt === null || now - window.lastNtfyAt >= BOARD_STALL_NOTIFICATION_COOLDOWN_MS;
let ntfyDispatched = false;
if (ntfyAllowed) {
try {
@@ -2594,7 +2594,6 @@ export class SelfHealingManager {
window.lastNtfyAt = now;
ntfyDispatched = true;
} else {
const settings = await this.store.getSettings();
const enabled = Boolean(settings.ntfyEnabled && settings.ntfyTopic);
const events = resolveNtfyEvents(settings.ntfyEvents);
if (enabled && isNtfyEventEnabled(events, "board-stall-unrecovered")) {