test(FN-5003): add post-finalize noop status-write race coverage

Fusion-Task-Id: FN-5003
Fusion-Task-Lineage: c5f1f326-934b-4cfc-8ba9-a2ac70a4f4f4
This commit is contained in:
Fusion (runfusion.ai)
2026-05-17 22:31:24 -07:00
committed by gsxdsm
parent 9ca7a5b90e
commit 18e827dc81

View File

@@ -0,0 +1,167 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { EventEmitter } from "node:events";
import type { Settings, Task, TaskStore } from "@fusion/core";
const testState = vi.hoisted(() => ({
aiMergeTask: vi.fn(),
currentStore: null as (TaskStore & EventEmitter) | null,
}));
vi.mock("../../merger.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../merger.js")>();
return {
...actual,
aiMergeTask: testState.aiMergeTask,
};
});
vi.mock("../../runtimes/in-process-runtime.js", () => ({
InProcessRuntime: vi.fn().mockImplementation(() => ({
start: vi.fn(async () => undefined),
stop: vi.fn(async () => undefined),
getTaskStore: () => testState.currentStore,
getAgentStore: vi.fn(),
getMessageStore: vi.fn(),
getRoutineStore: vi.fn(),
getRoutineRunner: vi.fn(),
getHeartbeatMonitor: vi.fn(),
getTriggerScheduler: vi.fn(),
})),
}));
import { ProjectEngine } from "../../project-engine.js";
function makeTask(overrides: Partial<Task> = {}): Task {
return {
id: "FN-5003",
title: "t",
description: "d",
column: "in-review",
status: "merging",
dependencies: [],
steps: [],
currentStep: 0,
log: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
verificationFailureCount: 0,
...overrides,
} as Task;
}
function createStore(task: Task, sequence: Task[]) {
const emitter = new EventEmitter();
const logs: string[] = [];
const audits: Array<{ mutationType: string; metadata?: Record<string, unknown> }> = [];
let taskIdx = 0;
const store = Object.assign(emitter, {
getSettings: vi.fn(async () => ({
autoMerge: true,
autoResolveConflicts: true,
globalPause: false,
enginePaused: false,
pollIntervalMs: 15_000,
} as Settings)),
listTasks: vi.fn(async () => [task]),
getTask: vi.fn(async () => {
const current = sequence[Math.min(taskIdx, sequence.length - 1)] ?? task;
taskIdx += 1;
return current;
}),
updateTask: vi.fn(async () => undefined),
addTaskComment: vi.fn(async () => undefined),
moveTask: vi.fn(async () => undefined),
logEntry: vi.fn(async (_id: string, message: string) => {
logs.push(message);
}),
getActiveMergingTask: vi.fn(() => null),
createTask: vi.fn(async () => ({ id: "FN-CHILD" })),
on: emitter.on.bind(emitter),
off: emitter.off.bind(emitter),
walCheckpoint: () => ({ busy: 0, log: 0, checkpointed: 0 }),
archiveTaskAndCleanup: async () => ({}),
clearStaleExecutionStartBranchReferences: () => [],
updateSettings: async () => ({}),
mergeTask: async () => undefined,
getRootDir: () => "",
recordRunAuditEvent: vi.fn(async (input: { mutationType: string; metadata?: Record<string, unknown> }) => {
audits.push({ mutationType: input.mutationType, metadata: input.metadata });
}),
}) as unknown as TaskStore & EventEmitter;
return { store, logs, audits };
}
async function runMergeCycle(engine: ProjectEngine, taskId: string): Promise<void> {
const privateEngine = engine as unknown as {
mergeQueue: string[];
mergeActive: Set<string>;
drainMergeQueue: () => Promise<void>;
};
privateEngine.mergeActive.add(taskId);
privateEngine.mergeQueue.push(taskId);
await privateEngine.drainMergeQueue();
}
describe("post-finalize verification noop status-write guard", () => {
beforeEach(() => {
vi.clearAllMocks();
testState.aiMergeTask.mockReset();
testState.currentStore = null;
});
it.each([
{ name: "under-cap", failureCount: 1, blockedStatus: "merging-fix" },
{ name: "at-cap", failureCount: 2, blockedStatus: "failed" },
])("keeps done task unchanged on $name write path", async ({ failureCount, blockedStatus }) => {
const verificationError = new Error("Deterministic test verification failed: no-op race");
verificationError.name = "VerificationError";
testState.aiMergeTask.mockRejectedValueOnce(verificationError);
const inReviewTask = makeTask({ verificationFailureCount: failureCount });
const doneTask = makeTask({
column: "done",
status: null,
verificationFailureCount: failureCount,
mergeDetails: { mergeConfirmed: true, commitSha: "abcdef1234567890" },
});
const { store, logs, audits } = createStore(inReviewTask, [inReviewTask, inReviewTask, inReviewTask, doneTask]);
testState.currentStore = store;
const engine = new ProjectEngine(
{
projectId: "proj_test",
workingDirectory: process.cwd(),
isolationMode: "in-process",
maxConcurrent: 1,
maxWorktrees: 1,
},
{} as never,
{ skipNotifier: true },
);
await runMergeCycle(engine, inReviewTask.id);
expect(store.updateTask).not.toHaveBeenCalledWith(
inReviewTask.id,
expect.objectContaining({ status: blockedStatus }),
);
expect(store.moveTask).not.toHaveBeenCalledWith(inReviewTask.id, "in-progress");
expect(store.createTask).not.toHaveBeenCalledWith(
expect.objectContaining({ source: expect.objectContaining({ sourceType: "recovery" }) }),
);
const noopLogs = logs.filter((entry) =>
entry.includes("[verification] post-finalize VerificationError on already-done task — no action"),
);
expect(noopLogs).toHaveLength(1);
const noopAudits = audits.filter((event) => event.mutationType === "task:post-finalize-verification-no-op");
expect(noopAudits).toHaveLength(1);
expect(noopAudits[0]?.metadata).toEqual(expect.objectContaining({
failedCommand: null,
exitCode: null,
}));
});
});