test(FN-4545): add unit coverage for contamination and delivery handlers
Fusion-Task-Id: FN-4545 Fusion-Task-Lineage: 95108429-618e-4a6d-9fa9-7ac2596665a2
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { Task } from "@fusion/core";
|
||||
import { AutoRecoveryDispatcher } from "../auto-recovery.js";
|
||||
import { ContaminationAutoRecoveryHandler } from "../auto-recovery-handlers/contamination.js";
|
||||
|
||||
const baseTask = { id: "FN-1", column: "in-progress", recoveryRetryCount: 0 } as Task;
|
||||
|
||||
describe("ContaminationAutoRecoveryHandler", () => {
|
||||
it("skips when userPaused", async () => {
|
||||
const taskStore = { moveTask: vi.fn(), updateTask: vi.fn() } as any;
|
||||
const runAudit = { database: vi.fn(), git: vi.fn(), filesystem: vi.fn() } as any;
|
||||
const handler = new ContaminationAutoRecoveryHandler({ taskStore, runAudit, repoDir: process.cwd() });
|
||||
await handler.issueRetry({ class: "branch-cross-contamination", taskId: "FN-1", pausedReason: "branch-cross-contamination" }, { action: "retry", rationale: "mode-programmatic", auditMetadata: {}, legacyPausedReason: "x" }, { task: { ...baseTask, userPaused: true } as Task, retryCount: 0, settings: { mode: "programmatic", maxRetries: 3 } });
|
||||
expect(taskStore.moveTask).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("requeues and clears paused state", async () => {
|
||||
const taskStore = { moveTask: vi.fn(), updateTask: vi.fn() } as any;
|
||||
const runAudit = { database: vi.fn(), git: vi.fn(), filesystem: vi.fn() } as any;
|
||||
const handler = new ContaminationAutoRecoveryHandler({ taskStore, runAudit, repoDir: process.cwd() });
|
||||
await handler.issueRetry({ class: "branch-cross-contamination", taskId: "FN-1", pausedReason: "branch-cross-contamination", evidence: { ownCommits: 0, foreignAttributedCommits: 2 } }, { action: "retry", rationale: "mode-programmatic", auditMetadata: {}, legacyPausedReason: "x" }, { task: { ...baseTask } as Task, retryCount: 1, settings: { mode: "programmatic", maxRetries: 3 } });
|
||||
expect(taskStore.moveTask).toHaveBeenCalledWith("FN-1", "todo", expect.objectContaining({ preserveWorktree: true }));
|
||||
expect(taskStore.updateTask).toHaveBeenCalledWith("FN-1", expect.objectContaining({ paused: false, pausedReason: null, error: null }));
|
||||
expect(runAudit.database).toHaveBeenCalledWith(expect.objectContaining({ type: "contamination:retry-issued" }));
|
||||
});
|
||||
|
||||
it("mode off does not call handler", async () => {
|
||||
const issueRetry = vi.fn();
|
||||
const dispatcher = new AutoRecoveryDispatcher({ taskStore: {} as any, auditEmitter: { database: vi.fn(), git: vi.fn(), filesystem: vi.fn() }, handlers: { issueRetry } });
|
||||
const decision = await dispatcher.dispatch({ class: "branch-cross-contamination", taskId: "FN-1", pausedReason: "branch-cross-contamination" }, { task: baseTask, retryCount: 0, settings: { mode: "off", maxRetries: 3 } });
|
||||
expect(decision.action).toBe("pause");
|
||||
expect(issueRetry).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { MessageDeliveryAutoRecoveryHandler } from "../auto-recovery-handlers/message-delivery.js";
|
||||
|
||||
describe("MessageDeliveryAutoRecoveryHandler", () => {
|
||||
it("returns delivered on first attempt", async () => {
|
||||
const runAudit = { database: vi.fn(), git: vi.fn(), filesystem: vi.fn() } as any;
|
||||
const handler = new MessageDeliveryAutoRecoveryHandler({ runAudit, sleep: vi.fn() });
|
||||
const result = await handler.runWithBoundedRetry({ run: async () => ({ id: "m1" }), correlation: { kind: "direct", fromAgentId: "a1", toId: "a2" } }, { mode: "programmatic", maxRetries: 3 });
|
||||
expect(result.outcome).toBe("delivered");
|
||||
});
|
||||
|
||||
it("retries transient failures then delivers", async () => {
|
||||
const runAudit = { database: vi.fn(), git: vi.fn(), filesystem: vi.fn() } as any;
|
||||
const sleep = vi.fn(async () => {});
|
||||
const handler = new MessageDeliveryAutoRecoveryHandler({ runAudit, sleep });
|
||||
const run = vi
|
||||
.fn<() => Promise<{ id: string }>>()
|
||||
.mockRejectedValueOnce(Object.assign(new Error("SQLITE_BUSY"), { code: "SQLITE_BUSY" }))
|
||||
.mockRejectedValueOnce(new Error("timeout"))
|
||||
.mockResolvedValue({ id: "m2" });
|
||||
const result = await handler.runWithBoundedRetry({ run, correlation: { kind: "room", fromAgentId: "a1", roomId: "r1" } }, { mode: "programmatic", maxRetries: 3 });
|
||||
expect(result.outcome).toBe("delivered");
|
||||
expect(run).toHaveBeenCalledTimes(3);
|
||||
expect(runAudit.database).toHaveBeenCalledWith(expect.objectContaining({ type: "message-delivery:retry-issued" }));
|
||||
});
|
||||
|
||||
it("parks permanent errors without retry", async () => {
|
||||
const runAudit = { database: vi.fn(), git: vi.fn(), filesystem: vi.fn() } as any;
|
||||
const run = vi.fn().mockRejectedValue(new Error("Room membership required"));
|
||||
const handler = new MessageDeliveryAutoRecoveryHandler({ runAudit, sleep: vi.fn(async () => {}) });
|
||||
const result = await handler.runWithBoundedRetry({ run, correlation: { kind: "room", fromAgentId: "a1", roomId: "r1" } }, { mode: "programmatic", maxRetries: 3 });
|
||||
expect(result.outcome).toBe("parked");
|
||||
expect(run).toHaveBeenCalledTimes(1);
|
||||
expect(runAudit.database).toHaveBeenCalledWith(expect.objectContaining({ type: "message-delivery:park" }));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user