test(FN-5046): complete Step 3 — cover terminal-only failure notification behavior

Fusion-Task-Id: FN-5046
Fusion-Task-Lineage: 0748fd0e-70c3-44b9-a599-0df5255d4d60
This commit is contained in:
Fusion (runfusion.ai)
2026-05-18 07:20:11 -07:00
committed by gsxdsm
parent 7167eebe6b
commit c31bf013df

View File

@@ -127,6 +127,100 @@ describe("NotificationService deferred failure notifications", () => {
await service.stop();
});
it("terminal-only suppresses non-terminal failures after grace", async () => {
const { store, service, sendNotification } = await setup({
failureNotificationMode: "terminal-only",
failureNotificationDelayMs: 50,
});
store.setTask(task({ id: "FN-1", status: "failed", paused: false, column: "in-progress" }));
store.emit("task:updated", task({ id: "FN-1", status: "failed", paused: false, column: "in-progress" }));
await vi.advanceTimersByTimeAsync(50);
expect(sendNotification).not.toHaveBeenCalledWith("failed", expect.anything());
expect(service.getMetrics().failureNotificationSuppressedCount).toBe(1);
expect(schedulerLog.log).toHaveBeenCalledWith("[notify] FN-1 non-terminal failure — suppressed (mode=terminal-only)");
await service.stop();
});
it("terminal-only dispatches when failed task is paused", async () => {
const { store, service, sendNotification } = await setup({
failureNotificationMode: "terminal-only",
failureNotificationDelayMs: 50,
});
store.setTask(task({ id: "FN-1", status: "failed", paused: true, column: "in-progress" }));
store.emit("task:updated", task({ id: "FN-1", status: "failed", paused: false, column: "in-progress" }));
await vi.advanceTimersByTimeAsync(50);
expect(sendNotification).toHaveBeenCalledTimes(1);
expect(sendNotification).toHaveBeenCalledWith("failed", expect.objectContaining({ taskId: "FN-1" }));
await service.stop();
});
it("terminal-only dispatches when failed task is in-review", async () => {
const { store, service, sendNotification } = await setup({
failureNotificationMode: "terminal-only",
failureNotificationDelayMs: 50,
});
store.setTask(task({ id: "FN-1", status: "failed", paused: false, column: "in-review" }));
store.emit("task:updated", task({ id: "FN-1", status: "failed", paused: false, column: "todo" }));
await vi.advanceTimersByTimeAsync(50);
expect(sendNotification).toHaveBeenCalledTimes(1);
expect(sendNotification).toHaveBeenCalledWith("failed", expect.objectContaining({ taskId: "FN-1" }));
await service.stop();
});
it("terminal-only still uses recovery suppression when task self-recovers before grace", async () => {
const { store, service, sendNotification } = await setup({
failureNotificationMode: "terminal-only",
failureNotificationDelayMs: 50,
});
store.setTask(task({ id: "FN-1", status: "failed", paused: false, column: "in-progress" }));
store.emit("task:updated", task({ id: "FN-1", status: "failed", paused: false, column: "in-progress" }));
store.setTask(task({ id: "FN-1", status: undefined, column: "done" }));
store.emit("task:moved", { task: task({ id: "FN-1", status: undefined, column: "done" }), from: "in-progress", to: "done" });
await vi.advanceTimersByTimeAsync(50);
expect(sendNotification).not.toHaveBeenCalledWith("failed", expect.anything());
expect(service.getMetrics().failureNotificationSuppressedCount).toBe(1);
await service.stop();
});
it("sticky-only still notifies persistent failed tasks", async () => {
const { store, service, sendNotification } = await setup({
failureNotificationMode: "sticky-only",
failureNotificationDelayMs: 50,
});
store.setTask(task({ id: "FN-1", status: "failed", paused: false, column: "in-progress" }));
store.emit("task:updated", task({ id: "FN-1", status: "failed", paused: false, column: "in-progress" }));
await vi.advanceTimersByTimeAsync(50);
expect(sendNotification).toHaveBeenCalledWith("failed", expect.objectContaining({ taskId: "FN-1" }));
await service.stop();
});
it("terminal-only with delay 0 still uses deferred path and suppresses non-terminal", async () => {
const { store, service, sendNotification } = await setup({
failureNotificationMode: "terminal-only",
failureNotificationDelayMs: 0,
});
store.setTask(task({ id: "FN-1", status: "failed", paused: false, column: "in-progress" }));
store.emit("task:updated", task({ id: "FN-1", status: "failed", paused: false, column: "in-progress" }));
expect(sendNotification).not.toHaveBeenCalled();
expect(service.getPendingFailureCount()).toBe(1);
await vi.advanceTimersByTimeAsync(0);
expect(sendNotification).not.toHaveBeenCalledWith("failed", expect.anything());
expect(service.getMetrics().failureNotificationSuppressedCount).toBe(1);
await service.stop();
});
it("stop clears pending timers without firing", async () => {
const { store, service, sendNotification } = await setup();
store.setTask(task({ id: "FN-1", status: "failed" }));