feat(FN-4646): complete Step 3 — reconcile landed files in self-healing

Fusion-Task-Id: FN-4646
Fusion-Task-Lineage: f921098e-3407-40fa-984d-720e0c3248c7
This commit is contained in:
Fusion
2026-05-15 13:10:15 -07:00
committed by gsxdsm
parent 3ffc1e689e
commit b6f2e9b4d1
3 changed files with 143 additions and 7 deletions

View File

@@ -23,6 +23,13 @@ function parseShortstat(output: string): { filesChanged: number; insertions: num
};
}
function parseNameOnly(output: string): string[] {
return output
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
}
function makeTask(id: string, repo: string, sha: string, mergeDetails: Task["mergeDetails"]): Task {
return {
id,
@@ -92,7 +99,7 @@ describeIfGit("SelfHealingManager recoverDoneTaskMergeMetadata stale stats", ()
it("repairs stale confirmed stats from live shortstat without changing SHA", async () => {
const { repo, sha } = setupRepo();
const expected = parseShortstat(git(repo, `git show --shortstat --format= ${sha}`));
const task = makeTask("FN-4526-STATS", repo, sha, { filesChanged: 99, insertions: 999, deletions: 999 });
const task = makeTask("FN-4526-STATS", repo, sha, { filesChanged: 99, insertions: 999, deletions: 999, landedFiles: ["a.ts", "b.ts", "c.ts"] });
const tasks = new Map([[task.id, task]]);
const store = createStore(tasks);
const manager = new SelfHealingManager(store, { rootDir: repo, getExecutingTaskIds: () => new Set() });
@@ -104,13 +111,37 @@ describeIfGit("SelfHealingManager recoverDoneTaskMergeMetadata stale stats", ()
expect(repaired.mergeDetails?.filesChanged).toBe(expected.filesChanged);
expect(repaired.mergeDetails?.insertions).toBe(expected.insertions);
expect(repaired.mergeDetails?.deletions).toBe(expected.deletions);
expect((store.logEntry as any).mock.calls.some((call: any[]) => String(call[1]).includes("stale mergeDetails stats repaired"))).toBe(true);
expect((store.logEntry as any).mock.calls.some((call: any[]) => String(call[1]).includes("stale mergeDetails repaired"))).toBe(true);
});
it("FN-4646: repairs stale landed file snapshot and modifiedFiles from live commit", async () => {
const { repo, sha } = setupRepo();
const expected = parseShortstat(git(repo, `git show --shortstat --format= ${sha}`));
const expectedFiles = parseNameOnly(git(repo, `git show --name-only --format= ${sha}`));
const task = makeTask("FN-4526-STATS", repo, sha, {
filesChanged: 99,
insertions: 999,
deletions: 999,
landedFiles: ["a.ts", "b.ts", "c.ts", "d.ts", "e.ts"],
});
task.modifiedFiles = ["a.ts", "b.ts", "c.ts", "d.ts", "e.ts"];
const tasks = new Map([[task.id, task]]);
const store = createStore(tasks);
const manager = new SelfHealingManager(store, { rootDir: repo, getExecutingTaskIds: () => new Set() });
await manager.recoverDoneTaskMergeMetadata();
const repaired = tasks.get(task.id)!;
expect(repaired.mergeDetails?.landedFiles).toEqual(expectedFiles);
expect(repaired.modifiedFiles).toEqual(expectedFiles);
expect(repaired.mergeDetails?.filesChanged).toBe(expected.filesChanged);
expect((store.logEntry as any).mock.calls.some((call: any[]) => String(call[1]).includes("files 5 → 2"))).toBe(true);
});
it("does not rewrite when stats are already correct", async () => {
const { repo, sha } = setupRepo();
const expected = parseShortstat(git(repo, `git show --shortstat --format= ${sha}`));
const task = makeTask("FN-4526-STATS", repo, sha, expected);
const task = makeTask("FN-4526-STATS", repo, sha, { ...expected, landedFiles: parseNameOnly(git(repo, `git show --name-only --format= ${sha}`)) });
const tasks = new Map([[task.id, task]]);
const store = createStore(tasks);
const manager = new SelfHealingManager(store, { rootDir: repo, getExecutingTaskIds: () => new Set() });

View File

@@ -5851,6 +5851,71 @@ describe("recoverDoneTaskMergeMetadata", () => {
manager.stop();
});
it("FN-4646: repairs missing landedFiles on confirmed merge metadata", async () => {
const store = createMockStore();
const manager = new SelfHealingManager(store, { rootDir: "/tmp/test-project" });
(store.listTasks as ReturnType<typeof vi.fn>).mockResolvedValue([
{
id: "FN-4646-A",
column: "done",
paused: false,
mergeDetails: { commitSha: "merge1", mergeConfirmed: true, filesChanged: 2, insertions: 3, deletions: 1, mergeCommitMessage: "msg" },
modifiedFiles: ["a.ts", "b.ts", "c.ts"],
},
]);
mockedExecSync.mockImplementation((command) => {
const cmd = String(command);
if (cmd.includes("merge-base --is-ancestor 'merge1' HEAD")) return "" as any;
if (cmd.includes("log -1 --format=%H%x1f%s%x1f%b 'merge1'")) return "merge1\u001ffix(FN-4646): canonical merge\u001fFusion-Task-Id: FN-4646-A" as any;
if (cmd.includes("show --shortstat --format=") && cmd.includes("merge1")) return "2 files changed, 3 insertions(+), 1 deletion(-)" as any;
if (cmd.includes("Fusion-Task-Id: FN-4646-A")) return "merge1\u001ffix(FN-4646): canonical merge\n" as any;
return "" as any;
});
await manager.recoverDoneTaskMergeMetadata();
expect(store.updateTask).toHaveBeenCalledWith("FN-4646-A", expect.objectContaining({
mergeDetails: expect.objectContaining({ landedFiles: [] }),
modifiedFiles: undefined,
}));
manager.stop();
});
it("FN-4646: repairs differing landedFiles on confirmed merge metadata", async () => {
const store = createMockStore();
const manager = new SelfHealingManager(store, { rootDir: "/tmp/test-project" });
(store.listTasks as ReturnType<typeof vi.fn>).mockResolvedValue([
{
id: "FN-4646-B",
column: "done",
paused: false,
mergeDetails: { commitSha: "merge1", mergeConfirmed: true, filesChanged: 2, insertions: 3, deletions: 1, mergeCommitMessage: "msg", landedFiles: ["a.ts", "b.ts", "c.ts"] },
},
]);
mockedExecSync.mockImplementation((command) => {
const cmd = String(command);
if (cmd.includes("merge-base --is-ancestor 'merge1' HEAD")) return "" as any;
if (cmd.includes("log -1 --format=%H%x1f%s%x1f%b 'merge1'")) return "merge1\u001ffix(FN-4646): canonical merge\u001fFusion-Task-Id: FN-4646-B" as any;
if (cmd.includes("show --shortstat --format=") && cmd.includes("merge1")) return "2 files changed, 3 insertions(+), 1 deletion(-)" as any;
if (cmd.includes("Fusion-Task-Id: FN-4646-B")) return "merge1\u001ffix(FN-4646): canonical merge\n" as any;
return "" as any;
});
await manager.recoverDoneTaskMergeMetadata();
expect(store.updateTask).toHaveBeenCalledWith("FN-4646-B", expect.objectContaining({
mergeDetails: expect.objectContaining({ landedFiles: [] }),
modifiedFiles: undefined,
}));
manager.stop();
});
it("populates rebaseBaseSha from landed commit when missing", async () => {
const store = createMockStore();
const manager = new SelfHealingManager(store, { rootDir: "/tmp/test-project" });