test(FN-4992): add merged PR auto-transition interaction coverage

Fusion-Task-Id: FN-4992
Fusion-Task-Lineage: 133d5e6a-0612-4f67-9105-3b7dbabd5ec0
This commit is contained in:
Fusion (runfusion.ai)
2026-05-17 19:29:50 -07:00
committed by gsxdsm
parent 0fd9d4094d
commit dcb055df6b

View File

@@ -0,0 +1,130 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { EventEmitter } from "node:events";
import type { Settings, Task, TaskStore } from "@fusion/core";
import * as worktreePool from "../../worktree-pool.js";
import { activeSessionRegistry } from "../../active-session-registry.js";
import { SelfHealingManager } from "../../self-healing.js";
function task(overrides: Partial<Task> = {}): Task {
return {
id: "FN-4992-M",
title: "t",
description: "d",
column: "in-review",
branch: "fusion/fn-4992-m",
worktree: "/tmp/test/.worktrees/fn-4992-m",
paused: true,
userPaused: false,
dependencies: [],
steps: [],
currentStep: 0,
log: [],
mergeDetails: { mergeConfirmed: true },
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
} as Task;
}
function store(primary: Task, extras: Task[] = []): TaskStore & EventEmitter {
const emitter = new EventEmitter();
const map = new Map([primary, ...extras].map((t) => [t.id, t]));
const settings = { globalPause: false, enginePaused: false } as Settings;
return Object.assign(emitter, {
getSettings: vi.fn(async () => settings),
getTask: vi.fn(async (id: string) => map.get(id) ?? null),
listTasks: vi.fn(async ({ column }: { column?: Task["column"] } = {}) => {
const all = [...map.values()];
return column ? all.filter((t) => t.column === column) : all;
}),
updateTask: vi.fn(async (id: string, patch: Partial<Task>) => {
const next = { ...(map.get(id) as Task), ...patch } as Task;
map.set(id, next);
return next;
}),
moveTask: vi.fn(async (id: string, column: Task["column"]) => {
const current = map.get(id) as Task;
const next = { ...current, column } as Task;
map.set(id, next);
return next;
}),
logEntry: vi.fn(async () => undefined),
appendAgentLog: vi.fn(async () => undefined),
clearStaleExecutionStartBranchReferences: vi.fn(() => []),
recordRunAuditEvent: vi.fn(async () => undefined),
walCheckpoint: vi.fn(() => ({ busy: 0, log: 0, checkpointed: 0 })),
getRootDir: vi.fn(() => "/tmp/test"),
}) as unknown as TaskStore & EventEmitter;
}
function isInvalidDoneTransitionError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error);
return message.includes("Invalid transition:") && /['"]done['"]/.test(message);
}
async function runMergeConfirmedFinalizePass(s: TaskStore & EventEmitter, taskId: string): Promise<void> {
const current = await s.getTask(taskId);
if (!current || current.column !== "in-review") return;
if (!current.mergeDetails?.mergeConfirmed) return;
await s.updateTask(taskId, { paused: false, status: null, error: null });
try {
await s.moveTask(taskId, "done");
} catch (error) {
if (isInvalidDoneTransitionError(error)) {
const latest = await s.getTask(taskId).catch(() => null);
if (latest && latest.column !== "in-review") {
await s.logEntry(
taskId,
`Merge confirmed finalize skipped: task moved to '${latest.column}' before in-review → done transition`,
);
return;
}
}
throw error;
}
}
describe("reliability interaction (FN-4766/FN-4762): PR merged auto-transition", () => {
beforeEach(() => {
vi.restoreAllMocks();
activeSessionRegistry.clear();
vi.spyOn(worktreePool, "isUsableTaskWorktree").mockResolvedValue(true);
});
it("transitions mergeConfirmed in-review task to done exactly once and keeps second pass as no-op", async () => {
const merged = task();
const dependent = task({ id: "FN-DEP", column: "todo", blockedBy: merged.id, mergeDetails: undefined });
const s = store(merged, [dependent]);
await runMergeConfirmedFinalizePass(s, merged.id);
const manager = new SelfHealingManager(s as any, { rootDir: "/tmp/test" } as any);
await expect(manager.reconcileCompletedTask(merged.id)).resolves.toMatchObject({ blockedByCleared: 1 });
expect((await (s as any).getTask(merged.id)).column).toBe("done");
expect((s as any).moveTask).toHaveBeenCalledTimes(1);
expect((await (s as any).getTask("FN-DEP")).blockedBy).toBeNull();
await runMergeConfirmedFinalizePass(s, merged.id);
expect((s as any).moveTask).toHaveBeenCalledTimes(1);
expect((await (s as any).getTask(merged.id)).status).not.toBe("merging-fix");
expect((await (s as any).getTask(merged.id)).column).toBe("done");
});
it("recovers invalid done transition when latest task already moved out of in-review", async () => {
const merged = task();
const s = store(merged);
const moveError = new Error("Invalid transition: 'in-review' → 'done'");
(s as any).moveTask = vi.fn(async (_id: string, _column: Task["column"]) => {
await (s as any).updateTask(merged.id, { column: "todo" });
throw moveError;
});
await expect(runMergeConfirmedFinalizePass(s, merged.id)).resolves.toBeUndefined();
expect((s as any).moveTask).toHaveBeenCalledTimes(1);
expect((s as any).logEntry).toHaveBeenCalledTimes(1);
expect((s as any).updateTask).toHaveBeenCalledWith(merged.id, { paused: false, status: null, error: null });
});
});