Files
fusion/packages/engine/src/__tests__/self-healing-starved-refinement.test.ts
Fusion (runfusion.ai) 0f9c07454e fix(FN-4662): address test typing and scenario coverage
Fusion-Task-Id: FN-4662
Fusion-Task-Lineage: f3de365a-ccb5-404b-9d0c-f9bc70d2678a
2026-05-15 21:57:57 -07:00

195 lines
9.1 KiB
TypeScript

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<Task> & Pick<Task, "id">): 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<Task>) => {
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 });
}
});
});