feat(FN-4559): complete remaining recovery, tests, and docs

Fusion-Task-Id: FN-4559
Fusion-Task-Lineage: 9276f683-657c-4604-aaef-143da488b287
This commit is contained in:
Fusion
2026-05-15 00:35:19 -07:00
committed by gsxdsm
parent 941213ec14
commit 38267f479f
7 changed files with 283 additions and 3 deletions

View File

@@ -0,0 +1,95 @@
import { describe, it, expect } from "vitest";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { execSync } from "node:child_process";
import { EventEmitter } from "node:events";
import type { Settings, Task, TaskStore } from "@fusion/core";
import { commitOrAmendMergeWithFixes } from "../../merger.js";
import { SelfHealingManager } from "../../self-healing.js";
function git(dir: string, cmd: string): string {
return execSync(cmd, { cwd: dir, stdio: "pipe" }).toString().trim();
}
function makeStore(task: Task, settings: Partial<Settings> = {}): TaskStore & EventEmitter {
const emitter = new EventEmitter();
const allSettings = { globalPause: false, enginePaused: false, ...settings } as Settings;
return Object.assign(emitter, {
getSettings: async () => allSettings,
listTasks: async ({ column }: { column?: string } = {}) => (column ? [task].filter((t) => t.column === column) : [task]),
updateTask: async (_id: string, updates: Partial<Task>) => Object.assign(task, updates),
moveTask: async (_id: string, column: Task["column"]) => { task.column = column; },
logEntry: async () => undefined,
getTask: async () => task,
walCheckpoint: () => ({ busy: 0, log: 0, checkpointed: 0 }),
archiveTaskAndCleanup: async () => ({}),
clearStaleExecutionStartBranchReferences: () => [],
updateSettings: async () => allSettings,
mergeTask: async () => undefined,
getRootDir: () => "",
recordRunAuditEvent: async () => undefined,
}) as unknown as TaskStore & EventEmitter;
}
describe("verification-fix already-on-main reliability interactions (real git)", () => {
it("recovers no-content finalize and allows self-healing done transition", async () => {
const dir = mkdtempSync(join(tmpdir(), "fn-4559-ri-"));
try {
git(dir, "git init -b main");
git(dir, 'git config user.email "test@example.com"');
git(dir, 'git config user.name "Test"');
git(dir, "git commit --allow-empty -m init");
git(dir, "git commit --allow-empty -m 'feat(FN-4545): unrelated'");
const unrelatedSha = git(dir, "git rev-parse HEAD");
writeFileSync(join(dir, "file.txt"), "task\n");
git(dir, "git add file.txt");
git(dir, "git commit -m 'feat(FN-4553): landed' -m 'Fusion-Task-Id: FN-4553'");
const landedSha = git(dir, "git rev-parse HEAD");
git(dir, "git commit --allow-empty -m 'chore: post'");
const preAttemptHeadSha = git(dir, "git rev-parse HEAD");
git(dir, `git branch fusion/fn-4553 ${unrelatedSha}`);
const finalized = await commitOrAmendMergeWithFixes(
dir,
"FN-4553",
"fusion/fn-4553",
"feat(FN-4553): merge",
true,
preAttemptHeadSha,
"",
);
expect(finalized.ok && finalized.reason === "branch-already-merged-on-main").toBe(true);
if (finalized.ok && finalized.reason === "branch-already-merged-on-main") {
expect(finalized.mergeSha).toBe(landedSha);
}
const task = {
id: "FN-4553",
title: "t",
description: "d",
column: "in-review",
dependencies: [],
steps: [],
currentStep: 0,
log: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
branch: "fusion/fn-4553",
baseBranch: "main",
} as Task;
const store = makeStore(task);
const manager = new SelfHealingManager(store, { rootDir: dir, getExecutingTaskIds: () => new Set() });
await (manager as any).recoverBranchMisboundInReviewTasks();
expect(task.column).toBe("done");
const pausedStore = makeStore({ ...task, id: "FN-4553B", column: "in-review", branch: "fusion/fn-4553" } as Task, { globalPause: true });
const pausedMgr = new SelfHealingManager(pausedStore, { rootDir: dir, getExecutingTaskIds: () => new Set() });
await expect((pausedMgr as any).recoverBranchMisboundInReviewTasks()).resolves.toBe(0);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});

View File

@@ -235,6 +235,40 @@ describeIfGit("SelfHealingManager recoverAlreadyMergedReviewTasks (real git)", (
expect(existsSync(worktreePath)).toBe(true);
});
it("recovers misbound in-review branch when main already carries task trailer", async () => {
const repo = setupRepo();
mkdirSync(path.join(repo, "src"), { recursive: true });
writeFileSync(path.join(repo, "src", "other.txt"), "other\n", "utf-8");
git(repo, "git add src/other.txt && git commit -m 'feat: unrelated' -m 'Fusion-Task-Id: FN-OTHER'");
const unrelatedSha = git(repo, "git rev-parse HEAD");
writeFileSync(path.join(repo, "src", "misbound.txt"), "landed\n", "utf-8");
git(repo, "git add src/misbound.txt && git commit -m 'feat: landed' -m 'Fusion-Task-Id: FN-TEST-MISBOUND'");
const landedSha = git(repo, "git rev-parse HEAD");
const worktreePath = path.join(repo, ".worktrees", "fn-test-misbound");
mkdirSync(path.dirname(worktreePath), { recursive: true });
git(repo, `git branch fusion/fn-test-misbound ${unrelatedSha}`);
git(repo, `git worktree add ${JSON.stringify(worktreePath)} fusion/fn-test-misbound`);
const tasks: TaskMap = new Map([
["FN-TEST-MISBOUND", makeTask({ id: "FN-TEST-MISBOUND", column: "in-review", paused: false, baseBranch: "main", branch: "fusion/fn-test-misbound", worktree: worktreePath })],
]);
const store = createStore(tasks);
const manager = new SelfHealingManager(store, { rootDir: repo, getExecutingTaskIds: () => new Set() });
await (manager as any).runMaintenance();
const task = tasks.get("FN-TEST-MISBOUND")!;
expect(task.column).toBe("done");
expect(task.branch).toBeNull();
expect(task.worktree).toBeNull();
expect(task.mergeDetails?.commitSha).toBe(landedSha);
expect((store as any).recordRunAuditEvent).toHaveBeenCalledWith(
expect.objectContaining({ mutationType: "task:auto-recover-branch-misbound", target: "FN-TEST-MISBOUND" }),
);
});
it("is idempotent across two maintenance passes", async () => {
const repo = setupRepo();
mkdirSync(path.join(repo, "src"), { recursive: true });