test(FN-4523): complete Step 4 — add completion fan-out coverage

Fusion-Task-Id: FN-4523
Fusion-Task-Lineage: c7a02071-f911-4c42-a174-cdcf2453fd23
This commit is contained in:
Fusion
2026-05-14 13:46:56 -07:00
committed by gsxdsm
parent 8fe1559454
commit cc18c4f780
2 changed files with 251 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
import { describe, expect, it, vi } from "vitest";
vi.mock("node:child_process", () => ({
exec: vi.fn((_: string, optsOrCb: unknown, cbMaybe?: (err: unknown, stdout: string, stderr: string) => void) => {
const cb = typeof optsOrCb === "function" ? optsOrCb : cbMaybe;
cb?.(null, "", "");
}),
execSync: vi.fn(),
}));
import { EventEmitter } from "node:events";
import type { Task, TaskStore } from "@fusion/core";
import { SelfHealingManager } from "../../self-healing.js";
function task(id: string, overrides: Partial<Task> = {}): Task {
return {
id,
title: id,
description: id,
column: "todo",
dependencies: [],
steps: [],
currentStep: 0,
log: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
} as Task;
}
function storeWith(tasks: Task[]): TaskStore & EventEmitter {
const map = new Map(tasks.map((t) => [t.id, t]));
const emitter = new EventEmitter();
return Object.assign(emitter, {
getSettings: vi.fn(async () => ({ globalPause: false, enginePaused: false })),
listTasks: vi.fn(async (opts?: { column?: Task["column"]; includeArchived?: boolean }) => {
const all = [...map.values()];
if (!opts?.column) return all;
return all.filter((t) => t.column === opts.column);
}),
getTask: vi.fn(async (id: string) => map.get(id)),
updateTask: vi.fn(async (id: string, patch: Partial<Task>) => {
map.set(id, { ...map.get(id)!, ...patch } as Task);
return map.get(id);
}),
logEntry: vi.fn(async () => undefined),
}) as unknown as TaskStore & EventEmitter;
}
describe("reliability interaction: completion fan-out x stale blockedBy sweep", () => {
it("does not double-clear same dependent", async () => {
const blocker = task("FN-B", { column: "done" });
const dependent = task("FN-D", { column: "todo", blockedBy: "FN-B" });
const store = storeWith([blocker, dependent]);
const manager = new SelfHealingManager(store, { rootDir: "/repo" });
await manager.reconcileCompletedTask("FN-B");
const firstCalls = (store as any).updateTask.mock.calls.length;
await manager.clearStaleBlockedBy();
const secondCalls = (store as any).updateTask.mock.calls.length;
expect((await store.getTask("FN-D"))?.blockedBy).toBeNull();
expect(firstCalls).toBe(1);
expect(secondCalls).toBe(1);
});
});

View File

@@ -0,0 +1,186 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { EventEmitter } from "node:events";
import type { Settings, Task, TaskStore } from "@fusion/core";
const { execMock, existsSyncMock } = vi.hoisted(() => ({
execMock: vi.fn(),
existsSyncMock: vi.fn(() => false),
}));
vi.mock("node:child_process", () => ({ exec: execMock, execSync: vi.fn() }));
vi.mock("node:fs", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs")>();
return { ...actual, existsSync: existsSyncMock };
});
const { uniqueCommitsMock } = vi.hoisted(() => ({
uniqueCommitsMock: vi.fn(async () => ({ commits: [], mainRef: "main", degraded: false })),
}));
vi.mock("../branch-conflicts.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../branch-conflicts.js")>();
return { ...actual, listUniqueBranchCommits: uniqueCommitsMock };
});
const { logger } = vi.hoisted(() => ({
logger: { log: vi.fn(), warn: vi.fn(), error: vi.fn() },
}));
vi.mock("../logger.js", () => ({ createLogger: vi.fn(() => logger) }));
import { SelfHealingManager } from "../self-healing.js";
function makeTask(id: string, overrides: Partial<Task> = {}): Task {
return {
id,
title: id,
description: id,
column: "todo",
dependencies: [],
steps: [],
currentStep: 0,
log: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
} as Task;
}
function createStore(tasks: Task[], settings?: Partial<Settings>): TaskStore & EventEmitter {
const map = new Map(tasks.map((t) => [t.id, t]));
const emitter = new EventEmitter();
const cfg: Settings = { globalPause: false, enginePaused: false } as Settings;
Object.assign(cfg, settings ?? {});
return Object.assign(emitter, {
getSettings: vi.fn(async () => cfg),
listTasks: vi.fn(async (opts?: { column?: Task["column"]; includeArchived?: boolean }) => {
const all = [...map.values()];
if (!opts?.column) return all;
return all.filter((t) => t.column === opts.column);
}),
getTask: vi.fn(async (id: string) => map.get(id)),
updateTask: vi.fn(async (id: string, patch: Partial<Task>) => {
const task = map.get(id)!;
map.set(id, { ...task, ...patch } as Task);
return map.get(id);
}),
moveTask: vi.fn(async (id: string, column: Task["column"]) => {
const task = map.get(id)!;
const from = task.column;
const next = { ...task, column, worktree: undefined } as Task;
map.set(id, next);
emitter.emit("task:moved", { task: next, from, to: column, source: "engine" });
}),
logEntry: vi.fn(async () => undefined),
}) as unknown as TaskStore & EventEmitter;
}
describe("self-healing completion fan-out", () => {
beforeEach(() => {
vi.clearAllMocks();
execMock.mockImplementation((cmd: string, _opts: unknown, cb: (err: unknown, stdout: string, stderr: string) => void) => {
cb(null, "", "");
});
});
it("clears/advances dependents and respects paused in-review", async () => {
const blocker = makeTask("FN-B", { column: "done", branch: "fusion/fn-b" });
const other = makeTask("FN-OTHER", { column: "todo" });
const clearTodo = makeTask("FN-CLEAR", { blockedBy: "FN-B", column: "todo" });
const queuedTodo = makeTask("FN-QUEUE", { blockedBy: "FN-B", column: "todo", dependencies: ["FN-B", "FN-OTHER"], status: "queued" as any });
const inProgress = makeTask("FN-P", { blockedBy: "FN-B", column: "in-progress" });
const pausedReview = makeTask("FN-PAUSE", { blockedBy: "FN-B", column: "in-review", paused: true });
const store = createStore([blocker, other, clearTodo, queuedTodo, inProgress, pausedReview]);
const mgr = new SelfHealingManager(store, { rootDir: "/repo" });
const res = await mgr.reconcileCompletedTask("FN-B");
expect(res.blockedByCleared).toBe(3);
expect((await store.getTask("FN-CLEAR"))?.blockedBy).toBeNull();
expect((await store.getTask("FN-CLEAR"))?.status).toBeNull();
expect((await store.getTask("FN-QUEUE"))?.blockedBy).toBe("FN-OTHER");
expect((await store.getTask("FN-QUEUE"))?.status).toBe("queued");
expect((await store.getTask("FN-P"))?.blockedBy).toBeNull();
expect((await store.getTask("FN-PAUSE"))?.blockedBy).toBe("FN-B");
expect((store as any).logEntry).toHaveBeenCalledWith(
"FN-CLEAR",
expect.stringContaining("FN-4523"),
);
});
it("removes worktree from hint and is idempotent when missing", async () => {
existsSyncMock.mockImplementation((p: string) => p === "/wt/fn-b");
const blocker = makeTask("FN-B", { column: "done", branch: "fusion/fn-b" });
const store = createStore([blocker]);
const mgr = new SelfHealingManager(store, { rootDir: "/repo" });
const first = await mgr.reconcileCompletedTask("FN-B", { worktreeHint: "/wt/fn-b" });
expect(first.worktreeRemoved).toBe(true);
expect(execMock.mock.calls.some((c) => String(c[0]).includes("git worktree remove --force '/wt/fn-b'"))).toBe(true);
existsSyncMock.mockReturnValue(false);
execMock.mockImplementation((cmd: string, _opts: unknown, cb: (err: unknown, stdout: string, stderr: string) => void) => {
if (cmd.includes("git worktree list --porcelain")) cb(null, "", "");
else cb(null, "", "");
});
const second = await mgr.reconcileCompletedTask("FN-B");
expect(second.worktreeRemoved).toBe(false);
const rmCalls = execMock.mock.calls.filter((c) => String(c[0]).includes("git worktree remove --force"));
expect(rmCalls).toHaveLength(1);
});
it("derives worktree from worktree list and skips branch delete when unique commits exist", async () => {
existsSyncMock.mockImplementation((p: string) => String(p).includes("/wt/fn-c"));
uniqueCommitsMock.mockResolvedValue({ commits: [{ sha: "abc", subject: "x" }], mainRef: "main", degraded: false });
execMock.mockImplementation((cmd: string, _opts: unknown, cb: (err: unknown, stdout: string, stderr: string) => void) => {
cb(null, "", "");
});
const blocker = makeTask("FN-C", { column: "done", branch: "fusion/fn-c" });
const store = createStore([blocker]);
const mgr = new SelfHealingManager(store, { rootDir: "/repo" });
vi.spyOn(mgr as any, "findWorktreePathForBranch").mockResolvedValue("/wt/fn-c");
const out = await mgr.reconcileCompletedTask("FN-C");
expect(execMock.mock.calls.some((c) => String(c[0]).includes("git worktree remove --force '/wt/fn-c'"))).toBe(true);
expect(out.branchRemoved).toBe(false);
expect(execMock.mock.calls.some((c) => String(c[0]).includes("git branch -D"))).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining("skip deletion"));
});
it("globalPause short-circuits", async () => {
const blocker = makeTask("FN-B", { column: "done" });
const dependent = makeTask("FN-D", { blockedBy: "FN-B", column: "todo" });
const store = createStore([blocker, dependent], { globalPause: true });
const mgr = new SelfHealingManager(store, { rootDir: "/repo" });
const out = await mgr.reconcileCompletedTask("FN-B");
expect(out).toEqual({ blockedByCleared: 0, worktreeRemoved: false, branchRemoved: false });
expect((store as any).updateTask).not.toHaveBeenCalled();
});
it("recoverAlreadyMergedReviewTasks calls reconcile with worktreeHint", async () => {
const t = makeTask("FN-R", { column: "in-review", status: "failed" as any, mergeRetries: 3, branch: "fusion/fn-r", worktree: "/wt/fn-r" });
const store = createStore([t]);
const mgr = new SelfHealingManager(store, { rootDir: "/repo", getExecutingTaskIds: () => new Set() });
vi.spyOn(mgr as any, "findAlreadyMergedTaskCommit").mockResolvedValue({ sha: "abc123", strategy: "trailer" });
const spy = vi.spyOn(mgr, "reconcileCompletedTask").mockResolvedValue({ blockedByCleared: 0, worktreeRemoved: false, branchRemoved: false });
await mgr.recoverAlreadyMergedReviewTasks();
expect(spy).toHaveBeenCalledWith("FN-R", { worktreeHint: "/wt/fn-r" });
});
it("wires and unwires task:moved listener", async () => {
const t = makeTask("FN-L", { column: "in-review" });
const store = createStore([t]);
const mgr = new SelfHealingManager(store, { rootDir: "/repo" });
const spy = vi.spyOn(mgr, "reconcileCompletedTask").mockResolvedValue({ blockedByCleared: 0, worktreeRemoved: false, branchRemoved: false });
mgr.start();
store.emit("task:moved", { task: t, from: "in-review", to: "done", source: "user" });
store.emit("task:moved", { task: t, from: "done", to: "archived", source: "engine" });
store.emit("task:moved", { task: t, from: "in-review", to: "todo", source: "user" });
await Promise.resolve();
expect(spy).toHaveBeenCalledTimes(2);
expect(spy).toHaveBeenNthCalledWith(1, "FN-L");
mgr.stop();
store.emit("task:moved", { task: t, from: "in-review", to: "done", source: "user" });
await Promise.resolve();
expect(spy).toHaveBeenCalledTimes(2);
});
});