feat(FN-3312): run full verification after in-merge fix
Added comprehensive test coverage for the merger module (124 lines in `merger.test.ts`), covering the in-merge fix from this branch with assertions that verify the corrected behavior. Fusion-Task-Id: FN-3312
This commit is contained in:
@@ -6994,6 +6994,130 @@ describe("aiMergeTask — in-merge verification fix", () => {
|
|||||||
expect(fixAgentCall[0].systemPrompt).toContain("verification fix agent");
|
expect(fixAgentCall[0].systemPrompt).toContain("verification fix agent");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("runs full test+build verification after a test-failure fix", async () => {
|
||||||
|
let vitestRuns = 0;
|
||||||
|
let buildRuns = 0;
|
||||||
|
let statusReads = 0;
|
||||||
|
|
||||||
|
mockedExecSync.mockImplementation((cmd: any) => {
|
||||||
|
const cmdStr = String(cmd);
|
||||||
|
if (cmdStr.includes("rev-parse --verify")) return Buffer.from("abc123");
|
||||||
|
if (cmdStr === "git rev-parse HEAD" || cmdStr.startsWith("git rev-parse HEAD ")) return "mergedcommit123";
|
||||||
|
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("status --porcelain")) {
|
||||||
|
statusReads += 1;
|
||||||
|
return statusReads === 1 ? "" : " M src/fix.ts";
|
||||||
|
}
|
||||||
|
if (cmdStr.includes("vitest run")) {
|
||||||
|
vitestRuns += 1;
|
||||||
|
if (vitestRuns === 1) {
|
||||||
|
const err = new Error("Test failed") as any;
|
||||||
|
err.status = 1;
|
||||||
|
err.stdout = "";
|
||||||
|
err.stderr = "";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return Buffer.from("");
|
||||||
|
}
|
||||||
|
if (cmdStr.includes("pnpm build")) {
|
||||||
|
buildRuns += 1;
|
||||||
|
return Buffer.from("");
|
||||||
|
}
|
||||||
|
if (cmdStr.includes("diff --cached --quiet")) return "1" as any;
|
||||||
|
if (cmdStr.includes("diff --cached")) return "0" as any;
|
||||||
|
if (cmdStr.includes("show --shortstat")) return "3 files changed, 10 insertions(+), 2 deletions(-)" as any;
|
||||||
|
if (cmdStr.includes("branch -d") || cmdStr.includes("branch -D")) return Buffer.from("");
|
||||||
|
if (cmdStr.includes("worktree remove")) return Buffer.from("");
|
||||||
|
return Buffer.from("");
|
||||||
|
});
|
||||||
|
|
||||||
|
mockedCreateFnAgent.mockResolvedValue({ 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],
|
||||||
|
);
|
||||||
|
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({ ...DEFAULT_SETTINGS, testCommand: "vitest run", buildCommand: "pnpm build", verificationFixRetries: 1 });
|
||||||
|
|
||||||
|
await expect(aiMergeTask(store, "/tmp/root", "FN-050")).rejects.toThrow();
|
||||||
|
expect(buildRuns).toBeGreaterThanOrEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retries when test-failure fix passes tests but full rerun fails on build", async () => {
|
||||||
|
let vitestRuns = 0;
|
||||||
|
let statusReads = 0;
|
||||||
|
mockedExecSync.mockImplementation((cmd: any) => {
|
||||||
|
const cmdStr = String(cmd);
|
||||||
|
if (cmdStr.includes("status --porcelain")) return ++statusReads === 1 ? "" : " M src/fix.ts";
|
||||||
|
if (cmdStr.includes("vitest run")) { if (++vitestRuns === 1) { const err = new Error("Test failed") as any; err.status = 1; throw err; } return Buffer.from(""); }
|
||||||
|
if (cmdStr.includes("pnpm build")) { const err = new Error("Build failed") as any; err.status = 1; throw err; }
|
||||||
|
if (cmdStr.includes("rev-parse --verify")) return Buffer.from("abc123");
|
||||||
|
if (cmdStr === "git rev-parse HEAD" || cmdStr.startsWith("git rev-parse HEAD ")) return "mergedcommit123";
|
||||||
|
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("diff --cached --quiet")) return "1" as any;
|
||||||
|
if (cmdStr.includes("diff --cached")) return "" as any;
|
||||||
|
return Buffer.from("");
|
||||||
|
});
|
||||||
|
mockedCreateFnAgent.mockResolvedValue({ 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]);
|
||||||
|
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({ ...DEFAULT_SETTINGS, testCommand: "vitest run", buildCommand: "pnpm build", verificationFixRetries: 2 });
|
||||||
|
await expect(aiMergeTask(store, "/tmp/root", "FN-050")).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("runs full test+build verification after a build-failure fix", async () => {
|
||||||
|
let buildRuns = 0;
|
||||||
|
let statusReads = 0;
|
||||||
|
mockedExecSync.mockImplementation((cmd: any) => {
|
||||||
|
const cmdStr = String(cmd);
|
||||||
|
if (cmdStr.includes("status --porcelain")) return ++statusReads === 1 ? "" : " M src/fix.ts";
|
||||||
|
if (cmdStr.includes("vitest run")) return Buffer.from("");
|
||||||
|
if (cmdStr.includes("pnpm build")) { if (++buildRuns === 1) { const err = new Error("Build failed") as any; err.status = 1; throw err; } return Buffer.from(""); }
|
||||||
|
if (cmdStr.includes("rev-parse --verify")) return Buffer.from("abc123");
|
||||||
|
if (cmdStr === "git rev-parse HEAD" || cmdStr.startsWith("git rev-parse HEAD ")) return "mergedcommit123";
|
||||||
|
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("diff --cached --quiet")) return "1" as any;
|
||||||
|
if (cmdStr.includes("diff --cached")) return "0" as any;
|
||||||
|
return Buffer.from("");
|
||||||
|
});
|
||||||
|
mockedCreateFnAgent.mockResolvedValue({ 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]);
|
||||||
|
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({ ...DEFAULT_SETTINGS, testCommand: "vitest run", buildCommand: "pnpm build", verificationFixRetries: 1, buildRetryCount: 0 });
|
||||||
|
await expect(aiMergeTask(store, "/tmp/root", "FN-050")).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retries when build-failure fix keeps build green but breaks tests in full rerun", async () => {
|
||||||
|
let buildRuns = 0;
|
||||||
|
let statusReads = 0;
|
||||||
|
mockedExecSync.mockImplementation((cmd: any) => {
|
||||||
|
const cmdStr = String(cmd);
|
||||||
|
if (cmdStr.includes("status --porcelain")) return ++statusReads === 1 ? "" : " M src/fix.ts";
|
||||||
|
if (cmdStr.includes("vitest run")) { const err = new Error("Test failed") as any; err.status = 1; throw err; }
|
||||||
|
if (cmdStr.includes("pnpm build")) { if (++buildRuns === 1) { const err = new Error("Build failed") as any; err.status = 1; throw err; } return Buffer.from(""); }
|
||||||
|
if (cmdStr.includes("rev-parse --verify")) return Buffer.from("abc123");
|
||||||
|
if (cmdStr === "git rev-parse HEAD" || cmdStr.startsWith("git rev-parse HEAD ")) return "mergedcommit123";
|
||||||
|
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("diff --cached --quiet")) return "1" as any;
|
||||||
|
if (cmdStr.includes("diff --cached")) return "" as any;
|
||||||
|
return Buffer.from("");
|
||||||
|
});
|
||||||
|
mockedCreateFnAgent.mockResolvedValue({ 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]);
|
||||||
|
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({ ...DEFAULT_SETTINGS, testCommand: "vitest run", buildCommand: "pnpm build", verificationFixRetries: 2, buildRetryCount: 0 });
|
||||||
|
await expect(aiMergeTask(store, "/tmp/root", "FN-050")).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
it("logs fix-agent startup metadata, streams callbacks, and logs rerun lifecycle", async () => {
|
it("logs fix-agent startup metadata, streams callbacks, and logs rerun lifecycle", async () => {
|
||||||
let capturedFixOptions: any;
|
let capturedFixOptions: any;
|
||||||
|
|
||||||
|
|||||||
@@ -769,8 +769,10 @@ async function attemptInMergeVerificationFix(
|
|||||||
options: MergerOptions,
|
options: MergerOptions,
|
||||||
mergeRunContext?: Pick<EngineRunContext, "runId" | "agentId">,
|
mergeRunContext?: Pick<EngineRunContext, "runId" | "agentId">,
|
||||||
fixAttemptNumber?: number,
|
fixAttemptNumber?: number,
|
||||||
_testCommand?: string,
|
testCommand?: string,
|
||||||
_buildCommand?: string,
|
buildCommand?: string,
|
||||||
|
testSource?: "explicit" | "inferred",
|
||||||
|
buildSource?: "explicit" | "inferred",
|
||||||
fixModifiedFiles?: Set<string>,
|
fixModifiedFiles?: Set<string>,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
// Snapshot the working tree before doing anything so the diff reflects only
|
// Snapshot the working tree before doing anything so the diff reflects only
|
||||||
@@ -834,7 +836,7 @@ Do not refactor, rename broadly, or make opportunistic improvements.
|
|||||||
1. Read the error output carefully to understand what is failing before editing anything
|
1. Read the error output carefully to understand what is failing before editing anything
|
||||||
2. Before assuming a code fix is needed, check whether the failure is caused by stale/missing build artifacts in a sibling workspace package — typical signatures: \`Failed to resolve import "./X.js"\` pointing into another package's \`dist/\`, \`Cannot find module\`, or \`ERR_MODULE_NOT_FOUND\` referencing a workspace-internal path. In that case, rebuild the affected package(s) (e.g. \`pnpm --filter <pkg> build\`, or \`pnpm --filter "<scope>/*" build\` for a group) and re-run verification before editing source files.
|
2. Before assuming a code fix is needed, check whether the failure is caused by stale/missing build artifacts in a sibling workspace package — typical signatures: \`Failed to resolve import "./X.js"\` pointing into another package's \`dist/\`, \`Cannot find module\`, or \`ERR_MODULE_NOT_FOUND\` referencing a workspace-internal path. In that case, rebuild the affected package(s) (e.g. \`pnpm --filter <pkg> build\`, or \`pnpm --filter "<scope>/*" build\` for a group) and re-run verification before editing source files.
|
||||||
3. Make targeted fixes to the failing code path
|
3. Make targeted fixes to the failing code path
|
||||||
4. After fixing, run the verification command to confirm the fix works
|
4. After fixing, verify your changes keep both deterministic test and build commands passing
|
||||||
5. Do NOT make any git commits — just fix the code
|
5. Do NOT make any git commits — just fix the code
|
||||||
6. You MAY modify any files needed to make the verification pass, including files unrelated to this task's original change. Pre-existing build/test breakage on the base branch is in scope: fix it. Prefer the smallest change that makes verification green.
|
6. You MAY modify any files needed to make the verification pass, including files unrelated to this task's original change. Pre-existing build/test breakage on the base branch is in scope: fix it. Prefer the smallest change that makes verification green.
|
||||||
7. If you cannot fix the issue within scope, explain why and what evidence indicates a deeper/root problem`,
|
7. If you cannot fix the issue within scope, explain why and what evidence indicates a deeper/root problem`,
|
||||||
@@ -897,7 +899,7 @@ ${failureContext.output.slice(0, VERIFICATION_LOG_MAX_CHARS)}
|
|||||||
## Instructions
|
## Instructions
|
||||||
1. Read the error output and identify the root cause
|
1. Read the error output and identify the root cause
|
||||||
2. Make targeted fixes to resolve the failure
|
2. Make targeted fixes to resolve the failure
|
||||||
3. Run the verification command \`${failureContext.command}\` to confirm your fix works
|
3. Use \`${failureContext.command}\` while iterating, but ensure your final changes keep both deterministic test and build commands passing
|
||||||
4. If the fix doesn't work, try a different approach
|
4. If the fix doesn't work, try a different approach
|
||||||
5. Do NOT make any git commits`;
|
5. Do NOT make any git commits`;
|
||||||
|
|
||||||
@@ -964,16 +966,24 @@ ${failureContext.output.slice(0, VERIFICATION_LOG_MAX_CHARS)}
|
|||||||
undefined,
|
undefined,
|
||||||
"merger",
|
"merger",
|
||||||
);
|
);
|
||||||
const reRunResult = await runVerificationCommand(
|
try {
|
||||||
store,
|
await runDeterministicVerification(
|
||||||
rootDir,
|
store,
|
||||||
taskId,
|
rootDir,
|
||||||
failureContext.command,
|
taskId,
|
||||||
failureContext.type,
|
testCommand,
|
||||||
options.signal,
|
buildCommand,
|
||||||
);
|
testSource,
|
||||||
|
buildSource,
|
||||||
return reRunResult.success;
|
options.signal,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof VerificationError) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// Flush buffered output before disposal so fix-attempt activity is visible.
|
// Flush buffered output before disposal so fix-attempt activity is visible.
|
||||||
await logger.flush();
|
await logger.flush();
|
||||||
@@ -4973,6 +4983,8 @@ export async function aiMergeTask(
|
|||||||
fixAttempt,
|
fixAttempt,
|
||||||
effectiveTestCommand,
|
effectiveTestCommand,
|
||||||
effectiveBuildCommand,
|
effectiveBuildCommand,
|
||||||
|
effectiveTestSource,
|
||||||
|
effectiveBuildSource,
|
||||||
verificationFixModifiedFiles,
|
verificationFixModifiedFiles,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -5090,6 +5102,8 @@ export async function aiMergeTask(
|
|||||||
fixAttempt,
|
fixAttempt,
|
||||||
effectiveTestCommand,
|
effectiveTestCommand,
|
||||||
effectiveBuildCommand,
|
effectiveBuildCommand,
|
||||||
|
effectiveTestSource,
|
||||||
|
effectiveBuildSource,
|
||||||
buildFixModifiedFiles,
|
buildFixModifiedFiles,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user