test(FN-5093): complete Step 5 — add reliability interaction backstop

Fusion-Task-Id: FN-5093
Fusion-Task-Lineage: fe96617a-88f8-4a3c-b96c-c516d2206331
This commit is contained in:
Fusion (runfusion.ai)
2026-05-19 03:41:43 -07:00
committed by gsxdsm
parent 275cee9c78
commit 8029227451

View File

@@ -0,0 +1,143 @@
import { EventEmitter } from "node:events";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { Task, TaskStore } from "@fusion/core";
import { SelfHealingManager } from "../../self-healing.js";
function createStore(task: Task, settings: Record<string, unknown> = {}): TaskStore & EventEmitter {
const emitter = new EventEmitter() as TaskStore & EventEmitter;
(emitter as any).getSettings = vi.fn().mockResolvedValue({
autoMerge: true,
globalPause: false,
enginePaused: false,
taskStuckTimeoutMs: 60_000,
inReviewStallDeadlockThreshold: 3,
inReviewStalledThresholdMs: 3_600_000,
stalePausedReviewThresholdMs: 3_600_000,
...settings,
});
(emitter as any).listTasks = vi.fn().mockImplementation(async ({ column }: { column?: string } = {}) => {
if (!column || task.column === column) return [task];
return [];
});
(emitter as any).logEntry = vi.fn().mockImplementation(async (_taskId: string, action: string) => {
task.log = task.log ?? [];
task.log.push({ timestamp: new Date(Date.now()).toISOString(), action });
});
(emitter as any).updateTask = vi.fn().mockImplementation(async (_taskId: string, updates: Partial<Task>) => {
Object.assign(task, updates, { updatedAt: new Date(Date.now()).toISOString() });
});
(emitter as any).moveTask = vi.fn().mockImplementation(async (_id: string, column: string) => {
task.column = column as any;
task.updatedAt = new Date(Date.now()).toISOString();
});
(emitter as any).recordRunAuditEvent = vi.fn().mockResolvedValue(undefined);
return emitter;
}
describe("reliability interactions: in-review-stalled detector", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
function baseTask(overrides: Record<string, unknown> = {}): Task {
return {
id: "FN-5093-RI",
column: "in-review",
paused: false,
status: "failed",
error: "Failed to create worktree after 3 attempts: Branch fusion/fn-5093-ri conflict could not be auto-resolved",
branch: "fusion/fn-5093-ri",
worktree: "/tmp/missing-fn-5093-ri",
mergeDetails: {},
mergeRetries: 0,
steps: [{ name: "merge", status: "done" }],
workflowStepResults: [],
updatedAt: "2026-01-01T00:00:00.000Z",
columnMovedAt: "2026-01-01T00:00:00.000Z",
log: [],
dependencies: [],
...overrides,
} as any;
}
it("reason-driven stall suppresses quiet detector until reason entry ages out", async () => {
const task = baseTask();
const store = createStore(task, { inReviewStalledThresholdMs: 3_600_000, taskStuckTimeoutMs: 48 * 3_600_000 });
const manager = new SelfHealingManager(store, { rootDir: "/tmp/repo" });
vi.setSystemTime(new Date("2026-01-01T02:00:00.000Z"));
expect(await manager.surfaceInReviewStalls()).toBe(1);
expect(await manager.surfaceInReviewStalled()).toBe(0);
vi.setSystemTime(new Date("2026-01-01T04:00:00.000Z"));
expect(await manager.surfaceInReviewStalled()).toBe(1);
expect(task.log.some((entry) => entry.action.startsWith("In-review stalled surfaced [in-review-stalled]"))).toBe(true);
manager.stop();
});
it("paused in-review tasks are owned by stale-paused-review detector", async () => {
const task = baseTask({ paused: true, pausedReason: "manual-hold", status: "failed", error: null });
const store = createStore(task);
const manager = new SelfHealingManager(store, { rootDir: "/tmp/repo" });
vi.setSystemTime(new Date("2026-01-01T02:00:00.000Z"));
expect(await manager.surfaceStalePausedReviews()).toBe(1);
expect(await manager.surfaceInReviewStalled()).toBe(0);
manager.stop();
});
it("ghost-review recovery can move task to todo after quiet detector logs first", async () => {
const task = baseTask({
status: null,
error: null,
steps: [],
inReviewStall: undefined,
worktree: "/tmp/wt",
});
const store = createStore(task, { inReviewStalledThresholdMs: 3_600_000, taskStuckTimeoutMs: 12 * 3_600_000 });
const manager = new SelfHealingManager(store, { rootDir: "/tmp/repo", getExecutingTaskIds: () => new Set() });
vi.setSystemTime(new Date("2026-01-01T05:00:00.000Z"));
expect(await manager.surfaceInReviewStalled()).toBe(1);
expect(task.column).toBe("in-review");
vi.setSystemTime(new Date("2026-01-01T13:00:00.000Z"));
expect(await manager.recoverGhostReviewTasks()).toBe(1);
expect(task.column).toBe("todo");
expect(await manager.surfaceInReviewStalled()).toBe(0);
manager.stop();
});
it("deadlock-disposed paused task is ignored by quiet detector", async () => {
const task = baseTask();
const store = createStore(task, { inReviewStallDeadlockThreshold: 1 });
const manager = new SelfHealingManager(store, { rootDir: "/tmp/repo" });
vi.setSystemTime(new Date("2026-01-01T02:00:00.000Z"));
expect(await manager.surfaceInReviewStalls()).toBe(1);
expect(task.paused).toBe(true);
expect(task.pausedReason).toBe("in-review-stall-deadlock");
expect(await manager.surfaceInReviewStalled()).toBe(0);
manager.stop();
});
it("autoMerge false suppresses both detectors", async () => {
const task = baseTask();
const store = createStore(task, { autoMerge: false });
const manager = new SelfHealingManager(store, { rootDir: "/tmp/repo" });
vi.setSystemTime(new Date("2026-01-01T02:00:00.000Z"));
expect(await manager.surfaceInReviewStalls()).toBe(0);
expect(await manager.surfaceInReviewStalled()).toBe(0);
manager.stop();
});
});