import { describe, expect, it, vi } from "vitest"; import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { Task } from "@fusion/core"; import { SelfHealingManager } from "../self-healing.js"; import { TriageProcessor } from "../triage.js"; function task(overrides: Partial & Pick): Task { const { id, ...rest } = overrides; return { id, title: overrides.id, description: overrides.id, column: "triage", priority: "normal", dependencies: [], steps: [], currentStep: 0, status: null, createdAt: "2026-05-15T10:00:00.000Z", updatedAt: "2026-05-15T10:00:00.000Z", columnMovedAt: "2026-05-15T10:00:00.000Z", ...rest, } as Task; } describe("SelfHealingManager.recoverStarvedRefinementTriageTasks", () => { it("escalates starved refinements once and emits run-audit", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-05-15T11:00:00.000Z")); const tasks: Task[] = [ task({ id: "FN-R1", sourceType: "task_refine", createdAt: "2026-05-15T10:00:00.000Z", updatedAt: "2026-05-15T10:00:00.000Z", priority: "low" }), task({ id: "FN-P1", column: "todo", sourceType: "dashboard_ui", updatedAt: "2026-05-15T10:15:00.000Z" }), task({ id: "FN-P2", column: "todo", sourceType: "dashboard_ui", updatedAt: "2026-05-15T10:16:00.000Z" }), task({ id: "FN-P3", column: "todo", sourceType: "dashboard_ui", updatedAt: "2026-05-15T10:17:00.000Z" }), ]; const updateTask = vi.fn(async (id: string, patch: Partial) => { const idx = tasks.findIndex((t) => t.id === id); tasks[idx] = { ...tasks[idx], ...patch, updatedAt: new Date().toISOString() } as Task; }); const recordRunAuditEvent = vi.fn().mockResolvedValue(undefined); const store: any = { getSettings: vi.fn().mockResolvedValue({ globalPause: false, enginePaused: false }), listTasks: vi.fn().mockResolvedValue(tasks), updateTask, logEntry: vi.fn().mockResolvedValue(undefined), recordRunAuditEvent, on: () => {}, removeListener: () => {}, }; const manager = new SelfHealingManager(store, { rootDir: process.cwd(), getPlanningTaskIds: () => new Set() }); await expect(manager.recoverStarvedRefinementTriageTasks()).resolves.toBe(1); await expect(manager.recoverStarvedRefinementTriageTasks()).resolves.toBe(0); expect(updateTask).toHaveBeenCalledTimes(1); expect(updateTask).toHaveBeenCalledWith("FN-R1", { priority: "normal" }); expect(recordRunAuditEvent).toHaveBeenCalledTimes(1); expect(recordRunAuditEvent.mock.calls[0][0]).toMatchObject({ mutationType: "task:auto-recover-starved-refinement", target: "FN-R1" }); vi.useRealTimers(); }); it("does not escalate non-refinement triage tasks", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-05-15T11:00:00.000Z")); const store: any = { listTasks: vi.fn().mockResolvedValue([ task({ id: "FN-NON", sourceType: "dashboard_ui", updatedAt: "2026-05-15T10:00:00.000Z" }), task({ id: "FN-P1", column: "todo", sourceType: "dashboard_ui", updatedAt: "2026-05-15T10:15:00.000Z" }), task({ id: "FN-P2", column: "todo", sourceType: "dashboard_ui", updatedAt: "2026-05-15T10:16:00.000Z" }), task({ id: "FN-P3", column: "todo", sourceType: "dashboard_ui", updatedAt: "2026-05-15T10:17:00.000Z" }), ]), updateTask: vi.fn(), logEntry: vi.fn(), recordRunAuditEvent: vi.fn(), on: () => {}, removeListener: () => {}, }; const manager = new SelfHealingManager(store, { rootDir: process.cwd(), getPlanningTaskIds: () => new Set() }); await expect(manager.recoverStarvedRefinementTriageTasks()).resolves.toBe(0); expect(store.updateTask).not.toHaveBeenCalled(); vi.useRealTimers(); }); it("does not escalate refinements under grace", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-05-15T11:00:00.000Z")); const store: any = { listTasks: vi.fn().mockResolvedValue([ task({ id: "FN-YOUNG", sourceType: "task_refine", createdAt: "2026-05-15T10:55:00.000Z", updatedAt: "2026-05-15T10:55:00.000Z" }), task({ id: "FN-P1", column: "todo", sourceType: "dashboard_ui", updatedAt: "2026-05-15T10:56:00.000Z" }), task({ id: "FN-P2", column: "todo", sourceType: "dashboard_ui", updatedAt: "2026-05-15T10:57:00.000Z" }), task({ id: "FN-P3", column: "todo", sourceType: "dashboard_ui", updatedAt: "2026-05-15T10:58:00.000Z" }), ]), updateTask: vi.fn(), logEntry: vi.fn(), recordRunAuditEvent: vi.fn(), on: () => {}, removeListener: () => {}, }; const manager = new SelfHealingManager(store, { rootDir: process.cwd(), getPlanningTaskIds: () => new Set() }); await expect(manager.recoverStarvedRefinementTriageTasks()).resolves.toBe(0); expect(store.updateTask).not.toHaveBeenCalled(); vi.useRealTimers(); }); it("does not escalate aged refinements when peer progress threshold is not met", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-05-15T11:00:00.000Z")); const store: any = { listTasks: vi.fn().mockResolvedValue([ task({ id: "FN-IDLE", sourceType: "task_refine", createdAt: "2026-05-15T10:00:00.000Z", updatedAt: "2026-05-15T10:00:00.000Z" }), ]), updateTask: vi.fn(), logEntry: vi.fn(), recordRunAuditEvent: vi.fn(), on: () => {}, removeListener: () => {}, }; const manager = new SelfHealingManager(store, { rootDir: process.cwd(), getPlanningTaskIds: () => new Set() }); await expect(manager.recoverStarvedRefinementTriageTasks()).resolves.toBe(0); expect(store.updateTask).not.toHaveBeenCalled(); vi.useRealTimers(); }); it("does not escalate paused refinements", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-05-15T11:00:00.000Z")); const store: any = { listTasks: vi.fn().mockResolvedValue([ task({ id: "FN-PAUSED", sourceType: "task_refine", paused: true, createdAt: "2026-05-15T10:00:00.000Z", updatedAt: "2026-05-15T10:00:00.000Z" }), task({ id: "FN-P1", column: "todo", sourceType: "dashboard_ui", updatedAt: "2026-05-15T10:15:00.000Z" }), task({ id: "FN-P2", column: "todo", sourceType: "dashboard_ui", updatedAt: "2026-05-15T10:16:00.000Z" }), task({ id: "FN-P3", column: "todo", sourceType: "dashboard_ui", updatedAt: "2026-05-15T10:17:00.000Z" }), ]), updateTask: vi.fn(), logEntry: vi.fn(), recordRunAuditEvent: vi.fn(), on: () => {}, removeListener: () => {}, }; const manager = new SelfHealingManager(store, { rootDir: process.cwd(), getPlanningTaskIds: () => new Set() }); await expect(manager.recoverStarvedRefinementTriageTasks()).resolves.toBe(0); expect(store.updateTask).not.toHaveBeenCalled(); vi.useRealTimers(); }); it("preserves approval gate flow (never direct todo) after escalation", async () => { const root = await mkdtemp(join(tmpdir(), "fusion-fn4662-")); try { const refinement = task({ id: "FN-RG", sourceType: "task_refine" }); const updateTask = vi.fn().mockResolvedValue(undefined); const moveTask = vi.fn().mockResolvedValue(undefined); const store: any = { listTasks: vi.fn().mockResolvedValue([ refinement, task({ id: "FN-P1", column: "todo", sourceType: "dashboard_ui", updatedAt: "2026-05-15T10:15:00.000Z" }), task({ id: "FN-P2", column: "todo", sourceType: "dashboard_ui", updatedAt: "2026-05-15T10:16:00.000Z" }), task({ id: "FN-P3", column: "todo", sourceType: "dashboard_ui", updatedAt: "2026-05-15T10:17:00.000Z" }), ]), updateTask, moveTask, logEntry: vi.fn().mockResolvedValue(undefined), recordRunAuditEvent: vi.fn().mockResolvedValue(undefined), parseDependenciesFromPrompt: vi.fn().mockResolvedValue([]), parseStepsFromPrompt: vi.fn().mockResolvedValue([]), on: () => {}, off: () => {}, removeListener: () => {}, }; vi.useFakeTimers(); vi.setSystemTime(new Date("2026-05-15T11:00:00.000Z")); const manager = new SelfHealingManager(store, { rootDir: root, getPlanningTaskIds: () => new Set() }); await manager.recoverStarvedRefinementTriageTasks(); expect(moveTask).not.toHaveBeenCalled(); const taskDir = join(root, ".fusion", "tasks", "FN-RG"); await mkdir(taskDir, { recursive: true }); await writeFile(join(taskDir, "PROMPT.md"), "# FN-RG\n\n## File Scope\n- packages/engine/src/self-healing.ts\n", "utf-8"); const processor = new TriageProcessor(store, root); await (processor as any).finalizeApprovedTask(refinement, "# FN-RG\n\n## File Scope\n- packages/engine/src/self-healing.ts\n", { requirePlanApproval: true }); expect(updateTask).toHaveBeenCalledWith("FN-RG", expect.objectContaining({ status: "awaiting-approval" })); expect(moveTask).not.toHaveBeenCalled(); vi.useRealTimers(); } finally { await rm(root, { recursive: true, force: true }); } }); });