feat(FN-2279): merge fusion/fn-2279

This commit is contained in:
gsxdsm
2026-04-22 23:01:34 -07:00
parent 2f90889723
commit 0538331c34
5 changed files with 67 additions and 5 deletions

View File

@@ -100,7 +100,7 @@ export const DEFAULT_PROJECT_SETTINGS = {
worktreeRebaseRemote: "",
strictScopeEnforcement: false,
buildRetryCount: 0,
verificationFixRetries: 1,
verificationFixRetries: 3,
buildTimeoutMs: 300_000,
requirePlanApproval: false,
specStalenessEnabled: false,

View File

@@ -1225,7 +1225,7 @@ export interface ProjectSettings {
buildRetryCount?: number;
/** Maximum number of times to attempt in-merge verification fixes when test/build
* commands fail during merge. The fix agent runs on the main branch with the merged
* code to resolve failures before aborting the merge. Default: 1. Set to 0 to disable. */
* code to resolve failures before aborting the merge. Default: 3. Set to 0 to disable. */
verificationFixRetries?: number;
/** Timeout in milliseconds for build commands during merge. Default: 300000 (5 min). */
buildTimeoutMs?: number;

View File

@@ -5901,4 +5901,66 @@ describe("aiMergeTask — in-merge verification fix", () => {
// Should have 3 fix attempts (capped at 3) + 1 merger = 4 calls
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(4);
});
it("default verificationFixRetries (omitted) results in 3 fix attempts", async () => {
mockedExecSync.mockImplementation((cmd: any) => {
const cmdStr = String(cmd);
if (cmdStr.includes("rev-parse --verify")) return Buffer.from("abc123");
if (cmdStr.includes("git log")) return "- feat: something" as any;
if (cmdStr.includes("merge-base")) return Buffer.from("abc123");
if (cmdStr.includes("git diff") && cmdStr.includes("--stat")) return "1 file changed" as any;
if (cmdStr.includes("merge --squash")) return Buffer.from("");
if (cmdStr.includes("vitest run")) {
const err = new Error("Test failed") as any;
err.status = 1;
err.stdout = "";
err.stderr = "";
throw err;
}
if (cmdStr.includes("diff --cached --quiet")) return "1" as any;
if (cmdStr.includes("diff --cached")) return "" as any;
if (cmdStr.includes("branch -d") || cmdStr.includes("branch -D")) return Buffer.from("");
if (cmdStr.includes("worktree remove")) return Buffer.from("");
if (cmdStr === "git rev-parse HEAD" || cmdStr.startsWith("git rev-parse HEAD ")) return "mergedcommit123";
return Buffer.from("");
});
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
return {
session: {
prompt: vi.fn().mockResolvedValue(undefined),
dispose: vi.fn(),
},
} as any;
});
const store = createMockStore(
{ id: "FN-050", worktree: "/tmp/root/.worktrees/KB-050" },
[{ id: "FN-050", worktree: "/tmp/root/.worktrees/KB-050", column: "in-review" } as Task],
);
// Explicitly omit verificationFixRetries to test default behavior
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
...DEFAULT_SETTINGS,
testCommand: "vitest run",
// verificationFixRetries is NOT set — should default to 3
});
await expect(aiMergeTask(store, "/tmp/root", "FN-050")).rejects.toMatchObject({
name: "VerificationError",
});
// Should have 3 fix attempts (default) + 1 merger = 4 calls
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(4);
// Verify the log shows 3 fix attempts (2 log entries per attempt: start + failure)
const logCalls = (store.logEntry as ReturnType<typeof vi.fn>).mock.calls;
const fixAttempts = logCalls.filter((call: any[]) =>
typeof call[1] === "string" && call[1].includes("In-merge verification fix attempt"),
);
// Each attempt produces 2 log entries: "attempt X/3" and "attempt X — verification still fails"
expect(fixAttempts).toHaveLength(6);
expect(fixAttempts[0][1]).toContain("attempt 1/3");
expect(fixAttempts[2][1]).toContain("attempt 2/3");
expect(fixAttempts[4][1]).toContain("attempt 3/3");
});
});

View File

@@ -2041,7 +2041,7 @@ export async function aiMergeTask(
// Try in-merge fix attempts before propagating
if (error.name === "VerificationError") {
const verificationErr = error as VerificationError;
const maxFixRetries = Math.min(settings.verificationFixRetries ?? 1, 3);
const maxFixRetries = Math.min(settings.verificationFixRetries ?? 3, 3);
if (maxFixRetries > 0 && (verificationErr.verificationResult.testResult || verificationErr.verificationResult.buildResult)) {
mergerLog.log(`${taskId}: deterministic verification failed — attempting in-merge fix (up to ${maxFixRetries} attempts)`);
@@ -2098,7 +2098,7 @@ export async function aiMergeTask(
// Check if it's a build verification failure
if (error.message?.includes("Build verification failed")) {
const maxFixRetries = Math.min(settings.verificationFixRetries ?? 1, 3);
const maxFixRetries = Math.min(settings.verificationFixRetries ?? 3, 3);
// Try in-merge fix before falling back to build retry
if (maxFixRetries > 0 && (effectiveTestCommand || effectiveBuildCommand)) {