test(FN-4662): complete Step 3 — add starvation sweep coverage

Fusion-Task-Id: FN-4662
Fusion-Task-Lineage: f3de365a-ccb5-404b-9d0c-f9bc70d2678a
This commit is contained in:
Fusion (runfusion.ai)
2026-05-15 21:36:54 -07:00
committed by gsxdsm
parent 8a01ba0af8
commit cdd85ba235
3 changed files with 353 additions and 0 deletions

View File

@@ -0,0 +1,82 @@
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 {
return {
id: overrides.id,
title: overrides.id,
description: overrides.id,
priority: "normal",
column: "triage",
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",
...overrides,
} as Task;
}
describe("reliability interaction: starved refinement x approval gate", () => {
it("keeps requirePlanApproval flow after starvation escalation", async () => {
const root = await mkdtemp(join(tmpdir(), "fusion-fn4662-approval-"));
try {
const updates: Array<Record<string, unknown>> = [];
const moves: string[] = [];
const refinement = task({ id: "FN-RA", sourceType: "task_refine" });
const tasks: Task[] = [
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" }),
];
const store: any = {
listTasks: vi.fn().mockResolvedValue(tasks),
updateTask: vi.fn().mockImplementation(async (_id: string, patch: Record<string, unknown>) => {
updates.push(patch);
}),
moveTask: vi.fn().mockImplementation(async (_id: string, to: string) => {
moves.push(to);
}),
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 expect(manager.recoverStarvedRefinementTriageTasks()).resolves.toBe(1);
const taskDir = join(root, ".fusion", "tasks", "FN-RA");
await mkdir(taskDir, { recursive: true });
await writeFile(join(taskDir, "PROMPT.md"), "# FN-RA\n\n## File Scope\n- packages/engine/src/self-healing.ts\n", "utf8");
const triage = new TriageProcessor(store, root);
await (triage as any).finalizeApprovedTask(
refinement,
"# FN-RA\n\n## File Scope\n- packages/engine/src/self-healing.ts\n",
{ requirePlanApproval: true },
);
expect(updates.some((u) => u.priority === "high")).toBe(true);
expect(updates.some((u) => u.status === "awaiting-approval")).toBe(true);
expect(moves).toEqual([]);
vi.useRealTimers();
} finally {
await rm(root, { recursive: true, force: true });
}
});
});

View File

@@ -0,0 +1,78 @@
import { describe, expect, it, vi } from "vitest";
import { mkdtemp, mkdir, rm } 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 triageTask(overrides: Partial<Task> & Pick<Task, "id">): Task {
return {
id: overrides.id,
title: overrides.id,
description: overrides.id,
priority: "low",
column: "triage",
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",
...overrides,
} as Task;
}
describe("reliability interaction: starved refinement x triage poll", () => {
it("escalation surfaces a previously-starved refinement to poll within bounded ticks", async () => {
const root = await mkdtemp(join(tmpdir(), "fusion-fn4662-poll-"));
await mkdir(join(root, ".fusion", "tasks"), { recursive: true });
try {
const tasks: Task[] = [
triageTask({ id: "FN-R1", sourceType: "task_refine" }),
...Array.from({ length: 6 }, (_, idx) => triageTask({ id: `FN-B${idx + 1}`, createdAt: `2026-05-15T09:${String(10 + idx).padStart(2, "0")}:00.000Z`, priority: "normal" })),
triageTask({ id: "FN-P1", column: "todo", sourceType: "dashboard_ui", updatedAt: "2026-05-15T10:15:00.000Z" }),
triageTask({ id: "FN-P2", column: "todo", sourceType: "dashboard_ui", updatedAt: "2026-05-15T10:16:00.000Z" }),
triageTask({ id: "FN-P3", column: "todo", sourceType: "dashboard_ui", updatedAt: "2026-05-15T10:17:00.000Z" }),
];
const store: any = {
getSettings: vi.fn().mockResolvedValue({ maxConcurrent: 1, maxTriageConcurrent: 1, pollIntervalMs: 10_000, globalPause: false, enginePaused: false }),
listTasks: vi.fn().mockImplementation(async () => tasks.map((t) => ({ ...t }))),
updateTask: vi.fn().mockImplementation(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;
}),
logEntry: vi.fn().mockResolvedValue(undefined),
recordRunAuditEvent: vi.fn().mockResolvedValue(undefined),
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 expect(manager.recoverStarvedRefinementTriageTasks()).resolves.toBe(1);
const triage = new TriageProcessor(store, root);
const specifySpy = vi.spyOn(triage, "specifyTask").mockImplementation(async (task) => {
const idx = tasks.findIndex((t) => t.id === task.id);
if (idx >= 0) tasks[idx] = { ...tasks[idx], column: "todo" };
});
(triage as any).running = true;
for (let i = 0; i < 2; i++) {
await (triage as any).poll();
}
expect(specifySpy.mock.calls.some(([t]) => t.id === "FN-R1")).toBe(true);
expect(tasks.find((t) => t.id === "FN-R1")?.column).toBe("todo");
vi.useRealTimers();
} finally {
await rm(root, { recursive: true, force: true });
}
});
});

View File

@@ -0,0 +1,193 @@
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 {
return {
id: overrides.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",
...overrides,
} 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 });
}
});
});