feat(FN-4962): complete Step 4 — dashboard fallback and reliability coverage
Fusion-Task-Id: FN-4962 Fusion-Task-Lineage: de4ce54b-c5e4-40f9-a048-677adac3e1a0
This commit is contained in:
committed by
gsxdsm
parent
40f9aa99e6
commit
d13ee7f526
@@ -0,0 +1,84 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { EventEmitter } from "node:events";
|
||||
import type { Task, TaskStore } from "@fusion/core";
|
||||
import { SelfHealingManager } from "../../self-healing.js";
|
||||
import * as worktreePoolModule from "../../worktree-pool.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 makeStore(tasks: Task[]): TaskStore & EventEmitter {
|
||||
const map = new Map(tasks.map((t) => [t.id, t]));
|
||||
return Object.assign(new EventEmitter(), {
|
||||
getSettings: vi.fn(async () => ({ globalPause: false, enginePaused: false })),
|
||||
listTasks: vi.fn(async () => [...map.values()]),
|
||||
updateTask: vi.fn(async (id: string, patch: Partial<Task>) => {
|
||||
map.set(id, { ...(map.get(id) as Task), ...patch });
|
||||
return map.get(id);
|
||||
}),
|
||||
getTask: vi.fn(async (id: string) => map.get(id)),
|
||||
recordRunAuditEvent: vi.fn(async () => undefined),
|
||||
moveTask: vi.fn(async () => undefined),
|
||||
logEntry: vi.fn(async () => undefined),
|
||||
}) as unknown as TaskStore & EventEmitter;
|
||||
}
|
||||
|
||||
describe("reliability interactions: worktree metadata reconcile", () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("defers while task is in executingTaskIds (recoverOrphaned/resume interaction)", async () => {
|
||||
const store = makeStore([task("FN-1", { column: "in-review", worktree: "/missing", branch: null })]);
|
||||
vi.spyOn(worktreePoolModule, "getRegisteredWorktreeBranchMap").mockResolvedValue(new Map([["fusion/fn-1", "/live"]]));
|
||||
|
||||
const manager = new SelfHealingManager(store, {
|
||||
rootDir: "/repo",
|
||||
getExecutingTaskIds: () => new Set(["FN-1"]),
|
||||
});
|
||||
|
||||
const repaired = await manager.reconcileTaskWorktreeMetadata();
|
||||
expect(repaired).toBe(0);
|
||||
expect((store as any).updateTask).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rebinds exactly once after deferred pass (acquireTaskWorktree interaction)", async () => {
|
||||
const store = makeStore([task("FN-2", { column: "todo", worktree: "/missing", branch: null })]);
|
||||
vi.spyOn(worktreePoolModule, "getRegisteredWorktreeBranchMap").mockResolvedValue(new Map([["fusion/fn-2", "/live"]]));
|
||||
|
||||
let executing = true;
|
||||
const manager = new SelfHealingManager(store, {
|
||||
rootDir: "/repo",
|
||||
getExecutingTaskIds: () => (executing ? new Set(["FN-2"]) : new Set<string>()),
|
||||
});
|
||||
|
||||
expect(await manager.reconcileTaskWorktreeMetadata()).toBe(0);
|
||||
executing = false;
|
||||
expect(await manager.reconcileTaskWorktreeMetadata()).toBe(1);
|
||||
expect((store as any).updateTask).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("skips done tasks during periodic reconcile (completion fan-out owns done lifecycle)", async () => {
|
||||
const store = makeStore([task("FN-3", { column: "done", worktree: "/missing", branch: null })]);
|
||||
vi.spyOn(worktreePoolModule, "getRegisteredWorktreeBranchMap").mockResolvedValue(new Map([["fusion/fn-3", "/live"]]));
|
||||
|
||||
const manager = new SelfHealingManager(store, { rootDir: "/repo" });
|
||||
const repaired = await manager.reconcileTaskWorktreeMetadata();
|
||||
|
||||
expect(repaired).toBe(0);
|
||||
expect((store as any).updateTask).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user