test(FN-4307): add failed-notification grace window coverage

Fusion-Task-Id: FN-4307
Fusion-Task-Lineage: 00140a5a-af93-4905-8570-32d7ecb8a874
This commit is contained in:
Fusion
2026-05-13 15:41:13 -07:00
committed by gsxdsm
parent 6bf90f1cac
commit 8fb7306e5f

View File

@@ -12,10 +12,12 @@ type Listener = (...args: any[]) => void | Promise<void>;
function createStore(settings: Partial<Settings> = {}) {
const listeners = new Map<string, Set<Listener>>();
const tasks = new Map<string, Task>();
const getBucket = (event: string) => listeners.get(event) ?? new Set<Listener>();
return {
getSettings: vi.fn(async () => ({ ntfyEnabled: false, ...settings }) as Settings),
getTask: vi.fn(async (id: string) => tasks.get(id)),
on: vi.fn((event: string, listener: Listener) => {
const bucket = getBucket(event);
bucket.add(listener);
@@ -29,6 +31,9 @@ function createStore(settings: Partial<Settings> = {}) {
void listener(payload);
}
},
setTask(task: Task) {
tasks.set(task.id, task);
},
};
}
@@ -579,4 +584,42 @@ describe("NotificationService", () => {
await first.stop();
await second.stop();
});
it("suppresses transient failed notification after Auto-recovered status clear", async () => {
vi.useFakeTimers();
const store = createStore({ ntfyEnabled: true, ntfyTopic: "topic", failureNotificationMode: "sticky-only", failureNotificationDelayMs: 50 });
const sendNotification = vi.fn(async () => ({ success: true, providerId: "mock" }));
const service = new NotificationService(store as any);
service.registerProvider({ getProviderId: () => "mock", isEventSupported: () => true, sendNotification });
await service.start();
store.setTask(task({ id: "FN-1", status: "failed", column: "in-review" }));
store.emit("task:updated", task({ id: "FN-1", status: "failed", column: "in-review" }));
store.setTask(task({ id: "FN-1", status: "in-review", column: "in-review", log: [{ timestamp: new Date().toISOString(), action: "Auto-recovered: merge deadlock resolved" }] }));
store.emit("task:updated", task({ id: "FN-1", status: "in-review" }));
await vi.advanceTimersByTimeAsync(60);
expect(sendNotification).not.toHaveBeenCalledWith("failed", expect.anything());
await service.stop();
vi.useRealTimers();
});
it("dispatches failed once when failure persists beyond grace window", async () => {
vi.useFakeTimers();
const store = createStore({ ntfyEnabled: true, ntfyTopic: "topic", failureNotificationMode: "sticky-only", failureNotificationDelayMs: 50 });
const sendNotification = vi.fn(async () => ({ success: true, providerId: "mock" }));
const service = new NotificationService(store as any);
service.registerProvider({ getProviderId: () => "mock", isEventSupported: () => true, sendNotification });
await service.start();
const failed = task({ id: "FN-1", status: "failed" });
store.setTask(failed);
store.emit("task:updated", failed);
await vi.advanceTimersByTimeAsync(60);
expect(sendNotification).toHaveBeenCalledTimes(1);
expect(sendNotification).toHaveBeenCalledWith("failed", expect.objectContaining({ taskId: "FN-1" }));
await service.stop();
vi.useRealTimers();
});
});