feat(FN-4688): complete Step 2 — use range stats in landed commit lookup

Fusion-Task-Id: FN-4688
Fusion-Task-Lineage: 49abde1d-3480-4127-beb0-5dadce30b9e1
This commit is contained in:
Fusion (runfusion.ai)
2026-05-15 16:31:24 -07:00
committed by gsxdsm
parent 6c542462ed
commit 9f970ff22c
2 changed files with 70 additions and 12 deletions

View File

@@ -2380,6 +2380,65 @@ describe("SelfHealingManager", () => {
managerWithRecovery.stop();
});
it("uses rebase range shortstat and propagates rebaseBaseSha when mergeDetails provides it", async () => {
const managerWithRecovery = new SelfHealingManager(store, {
rootDir: "/tmp/test-project",
});
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
taskStuckTimeoutMs: 60_000,
});
const staleUpdatedAt = new Date(Date.now() - 61_000).toISOString();
(store.listTasks as ReturnType<typeof vi.fn>).mockResolvedValue([
{
id: "FN-2901",
column: "in-review",
status: "merging",
error: null,
paused: false,
worktree: "/tmp/test-project/.worktrees/fn-2901",
branch: "fusion/fn-2901",
baseCommitSha: "base901",
updatedAt: staleUpdatedAt,
steps: [{ name: "Ship it", status: "done" }],
workflowStepResults: [],
mergeDetails: { rebaseBaseSha: "rebasebase901" },
log: [],
},
]);
mockedExistsSync.mockReturnValue(true);
mockedExecSync.mockImplementation((command) => {
const cmd = String(command);
if (cmd.includes("git log") && cmd.includes("Fusion-Task-Id: FN-2901")) {
return "rangeSha901\u001ffeat: ship something opaque\n" as any;
}
if (cmd.includes("git diff --shortstat") && cmd.includes("rebasebase901..rangeSha901")) {
return " 4 files changed, 104 insertions(+), 1 deletion(-)\n" as any;
}
return "" as any;
});
const result = await managerWithRecovery.recoverInterruptedMergingTasks();
expect(result).toBe(1);
expect(mockedExecSync.mock.calls.some(([cmd]) => String(cmd).includes("git diff --shortstat") && String(cmd).includes("rebasebase901..rangeSha901"))).toBe(true);
expect(mockedExecSync.mock.calls.some(([cmd]) => String(cmd).includes("git show --shortstat") && String(cmd).includes("rangeSha901"))).toBe(false);
expect(store.updateTask).toHaveBeenCalledWith("FN-2901", {
status: null,
error: null,
mergeRetries: 0,
mergeDetails: expect.objectContaining({
commitSha: "rangeSha901",
rebaseBaseSha: "rebasebase901",
filesChanged: 4,
insertions: 104,
deletions: 1,
}),
});
managerWithRecovery.stop();
});
it("finalizes stale merging tasks when baseCommitSha was advanced past the landed commit", async () => {
// Reproduces the case where the merger fast-forward-rebased the task branch
// and updated baseCommitSha to the new HEAD; the bounded `base..HEAD` range

View File

@@ -731,6 +731,7 @@ export class SelfHealingManager {
// miss; (3) catches commits authored before the trailer was introduced.
// ── (1) Stored sha ────────────────────────────────────────────────────
const rebaseBaseSha = task.mergeDetails?.rebaseBaseSha;
const storedSha = task.mergeDetails?.commitSha;
if (storedSha) {
try {
@@ -744,13 +745,12 @@ export class SelfHealingManager {
);
const [sha, subject = "", body = ""] = stdout.trim().split("\x1f");
if (sha && commitOwnedByTask(task.id, task.lineageId, subject, body)) {
const commit: LandedTaskCommit = { sha, subject };
const commit: LandedTaskCommit = { sha, subject, rebaseBaseSha };
try {
const stats = await execAsync(`git show --shortstat --format= ${shellQuote(sha)}`, {
cwd: this.options.rootDir,
maxBuffer: 1024 * 1024,
});
Object.assign(commit, parseShortstat(stats.stdout));
const shortstat = await this.readShortstatForSha(sha, rebaseBaseSha);
if (shortstat) {
Object.assign(commit, shortstat);
}
} catch { /* stats are optional */ }
return commit;
}
@@ -829,13 +829,12 @@ export class SelfHealingManager {
const [sha, subject] = firstLine.split("\x1f");
if (!sha) return null;
const commit: LandedTaskCommit = { sha, subject };
const commit: LandedTaskCommit = { sha, subject, rebaseBaseSha };
try {
const stats = await execAsync(`git show --shortstat --format= ${shellQuote(sha)}`, {
cwd: this.options.rootDir,
maxBuffer: 1024 * 1024,
});
Object.assign(commit, parseShortstat(stats.stdout));
const shortstat = await this.readShortstatForSha(sha, rebaseBaseSha);
if (shortstat) {
Object.assign(commit, shortstat);
}
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
log.warn(