feat(FN-2659): add merger verification agent log events
- Emit merger agent log entries when deterministic verification starts, runs commands, and completes successfully - Record tool_result/tool_error details for verification command outcomes including timing and truncated output summaries - Add agent-log coverage for in-merge verification fix lifecycle events (start, retry, success, and failure) - Extend merger deterministic verification tests to assert start/success and failure agent log entries
This commit is contained in:
@@ -3355,6 +3355,130 @@ describe("aiMergeTask — deterministic merge verification", () => {
|
||||
expect(verificationOrder).toEqual(["test", "build"]);
|
||||
});
|
||||
|
||||
it("writes verification start/success entries to agent log", 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")) return Buffer.from("");
|
||||
if (cmdStr.includes("diff --cached --quiet")) return "1" as any;
|
||||
if (cmdStr.includes("diff --cached") && !cmdStr.includes("--quiet")) 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";
|
||||
if (cmdStr.includes("show --shortstat")) return "1 file changed, 1 insertion(+)" 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",
|
||||
});
|
||||
|
||||
const result = await aiMergeTask(store, "/tmp/root", "FN-050");
|
||||
|
||||
expect(result.merged).toBe(true);
|
||||
expect(store.appendAgentLog).toHaveBeenCalledWith(
|
||||
"FN-050",
|
||||
"Running deterministic merge verification (test: vitest run)",
|
||||
"text",
|
||||
undefined,
|
||||
"merger",
|
||||
);
|
||||
expect(store.appendAgentLog).toHaveBeenCalledWith(
|
||||
"FN-050",
|
||||
"Running test command",
|
||||
"tool",
|
||||
"vitest run",
|
||||
"merger",
|
||||
);
|
||||
|
||||
const appendAgentLogCalls = (store.appendAgentLog as ReturnType<typeof vi.fn>).mock.calls;
|
||||
const successCall = appendAgentLogCalls.find(
|
||||
([task, message, type]) => task === "FN-050"
|
||||
&& message === "test command succeeded (exit 0)"
|
||||
&& type === "tool_result",
|
||||
);
|
||||
expect(successCall).toBeTruthy();
|
||||
expect(successCall?.[3]).toMatch(/^\d+ms$/);
|
||||
expect(successCall?.[4]).toBe("merger");
|
||||
|
||||
expect(store.appendAgentLog).toHaveBeenCalledWith(
|
||||
"FN-050",
|
||||
"Deterministic merge verification passed",
|
||||
"text",
|
||||
undefined,
|
||||
"merger",
|
||||
);
|
||||
});
|
||||
|
||||
it("writes verification failure output summaries to agent log", 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 error = new Error("Test failed") as any;
|
||||
error.status = 1;
|
||||
error.stdout = "FAIL: some test failed";
|
||||
error.stderr = "";
|
||||
throw error;
|
||||
}
|
||||
if (cmdStr.includes("diff --cached --quiet")) return "1" 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",
|
||||
verificationFixRetries: 0,
|
||||
});
|
||||
|
||||
await expect(aiMergeTask(store, "/tmp/root", "FN-050")).rejects.toThrow(
|
||||
"Deterministic test verification failed",
|
||||
);
|
||||
|
||||
const appendAgentLogCalls = (store.appendAgentLog as ReturnType<typeof vi.fn>).mock.calls;
|
||||
const failureCall = appendAgentLogCalls.find(
|
||||
([task, message, type]) => task === "FN-050"
|
||||
&& message === "test command failed (exit 1)"
|
||||
&& type === "tool_error",
|
||||
);
|
||||
expect(failureCall).toBeTruthy();
|
||||
expect(failureCall?.[3]).toContain("full output available in engine logs");
|
||||
expect(failureCall?.[4]).toBe("merger");
|
||||
});
|
||||
|
||||
it("fails merge when testCommand fails and does not move task to done", async () => {
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
mockedExecSync.mockImplementation((cmd: any) => {
|
||||
|
||||
@@ -655,12 +655,12 @@ async function runDeterministicVerification(
|
||||
(hasTestCommand ? ` [test:${testSourceLabel} ${normalizedTestCommand}]` : "") +
|
||||
(hasBuildCommand ? ` [build:${buildSourceLabel} ${normalizedBuildCommand}]` : ""),
|
||||
);
|
||||
await store.logEntry(
|
||||
taskId,
|
||||
const deterministicVerificationMessage =
|
||||
"Running deterministic merge verification" +
|
||||
(hasTestCommand ? ` (test${testSource === "inferred" ? " [inferred]" : ""}: ${normalizedTestCommand})` : "") +
|
||||
(hasBuildCommand ? ` (build${buildSource === "inferred" ? " [inferred]" : ""}: ${normalizedBuildCommand})` : ""),
|
||||
);
|
||||
(hasBuildCommand ? ` (build${buildSource === "inferred" ? " [inferred]" : ""}: ${normalizedBuildCommand})` : "");
|
||||
await store.logEntry(taskId, deterministicVerificationMessage);
|
||||
await store.appendAgentLog(taskId, deterministicVerificationMessage, "text", undefined, "merger");
|
||||
|
||||
// Run test command first if configured
|
||||
if (hasTestCommand) {
|
||||
@@ -677,6 +677,13 @@ async function runDeterministicVerification(
|
||||
`Deterministic test verification failed (exit ${testResult.exitCode}) — see prior [verification] entry for truncated output`,
|
||||
"VerificationError",
|
||||
);
|
||||
await store.appendAgentLog(
|
||||
taskId,
|
||||
"Verification failed",
|
||||
"tool_error",
|
||||
`exit ${testResult.exitCode}`,
|
||||
"merger",
|
||||
);
|
||||
throw new VerificationError(
|
||||
`Deterministic test verification failed for ${taskId}`,
|
||||
result,
|
||||
@@ -699,6 +706,13 @@ async function runDeterministicVerification(
|
||||
`Deterministic build verification failed (exit ${buildResult.exitCode}) — see prior [verification] entry for truncated output`,
|
||||
"VerificationError",
|
||||
);
|
||||
await store.appendAgentLog(
|
||||
taskId,
|
||||
"Verification failed",
|
||||
"tool_error",
|
||||
`exit ${buildResult.exitCode}`,
|
||||
"merger",
|
||||
);
|
||||
throw new VerificationError(
|
||||
`Deterministic build verification failed for ${taskId}`,
|
||||
result,
|
||||
@@ -708,6 +722,7 @@ async function runDeterministicVerification(
|
||||
|
||||
mergerLog.log(`${taskId}: deterministic verification passed`);
|
||||
await store.logEntry(taskId, "Deterministic merge verification passed");
|
||||
await store.appendAgentLog(taskId, "Deterministic merge verification passed", "text", undefined, "merger");
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -722,6 +737,7 @@ async function runVerificationCommand(
|
||||
throwIfAborted(signal, taskId);
|
||||
mergerLog.log(`${taskId}: running ${type} command: ${command}`);
|
||||
await store.logEntry(taskId, `[verification] Running ${type} command: ${command}`);
|
||||
await store.appendAgentLog(taskId, `Running ${type} command`, "tool", command, "merger");
|
||||
|
||||
const result: VerificationCommandResult = {
|
||||
command,
|
||||
@@ -748,15 +764,30 @@ async function runVerificationCommand(
|
||||
result.success = true;
|
||||
|
||||
const verificationDurationMs = Date.now() - verificationStartedAt;
|
||||
const timingDetail = `${verificationDurationMs}ms`;
|
||||
if (bufferOverflow) {
|
||||
mergerLog.log(`${taskId}: ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`);
|
||||
await store.logEntry(
|
||||
taskId,
|
||||
`[timing] [verification] ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`,
|
||||
);
|
||||
await store.appendAgentLog(
|
||||
taskId,
|
||||
`${type} command succeeded (exit 0)`,
|
||||
"tool_result",
|
||||
timingDetail,
|
||||
"merger",
|
||||
);
|
||||
} else {
|
||||
mergerLog.log(`${taskId}: ${type} command succeeded in ${verificationDurationMs}ms`);
|
||||
await store.logEntry(taskId, `[timing] [verification] ${type} command succeeded (exit 0) in ${verificationDurationMs}ms`);
|
||||
await store.appendAgentLog(
|
||||
taskId,
|
||||
`${type} command succeeded (exit 0)`,
|
||||
"tool_result",
|
||||
timingDetail,
|
||||
"merger",
|
||||
);
|
||||
}
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
@@ -779,6 +810,13 @@ async function runVerificationCommand(
|
||||
taskId,
|
||||
`[timing] [verification] ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`,
|
||||
);
|
||||
await store.appendAgentLog(
|
||||
taskId,
|
||||
`${type} command succeeded (exit 0)`,
|
||||
"tool_result",
|
||||
`${verificationDurationMs}ms`,
|
||||
"merger",
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -791,6 +829,13 @@ async function runVerificationCommand(
|
||||
taskId,
|
||||
`[timing] [verification] ${type} command failed (exit ${result.exitCode}) after ${verificationDurationMs}ms:\n${summary}`,
|
||||
);
|
||||
await store.appendAgentLog(
|
||||
taskId,
|
||||
`${type} command failed (exit ${result.exitCode})`,
|
||||
"tool_error",
|
||||
summary,
|
||||
"merger",
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -890,6 +935,13 @@ A merge has been applied and the verification command failed. Your job is to fix
|
||||
taskId,
|
||||
`In-merge verification fix agent started (model: ${describeModel(session)}, runId: ${runId ?? "unknown"}, agentId: ${agentId})`,
|
||||
);
|
||||
await store.appendAgentLog(
|
||||
taskId,
|
||||
`Fix agent started (model: ${describeModel(session)})`,
|
||||
"text",
|
||||
undefined,
|
||||
"merger",
|
||||
);
|
||||
|
||||
try {
|
||||
// Build the fix prompt
|
||||
@@ -926,6 +978,13 @@ ${failureContext.output.slice(0, VERIFICATION_LOG_MAX_CHARS)}
|
||||
taskId,
|
||||
`Re-running deterministic merge verification (attempt ${fixAttemptNumber ?? "unknown"})`,
|
||||
);
|
||||
await store.appendAgentLog(
|
||||
taskId,
|
||||
`Re-running verification (attempt ${fixAttemptNumber ?? "unknown"})`,
|
||||
"text",
|
||||
undefined,
|
||||
"merger",
|
||||
);
|
||||
const reRunResult = await runVerificationCommand(
|
||||
store,
|
||||
rootDir,
|
||||
@@ -946,6 +1005,7 @@ ${failureContext.output.slice(0, VERIFICATION_LOG_MAX_CHARS)}
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
mergerLog.warn(`${taskId}: in-merge fix agent error: ${errorMessage}`);
|
||||
await store.logEntry(taskId, "In-merge verification fix agent encountered an error", errorMessage);
|
||||
await store.appendAgentLog(taskId, "Fix agent encountered an error", "tool_error", errorMessage, "merger");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2401,6 +2461,13 @@ export async function aiMergeTask(
|
||||
if (maxFixRetries > 0 && (verificationErr.verificationResult.testResult || verificationErr.verificationResult.buildResult)) {
|
||||
mergerLog.log(`${taskId}: deterministic verification failed — attempting in-merge fix (up to ${maxFixRetries} attempts)`);
|
||||
await store.logEntry(taskId, `Verification failed during merge — attempting in-merge fix (up to ${maxFixRetries} attempts)`);
|
||||
await store.appendAgentLog(
|
||||
taskId,
|
||||
`Verification failed — attempting in-merge fix (up to ${maxFixRetries} attempts)`,
|
||||
"text",
|
||||
undefined,
|
||||
"merger",
|
||||
);
|
||||
|
||||
// Extract failure context from the VerificationError
|
||||
const failedResult = verificationErr.verificationResult.testResult?.success === false
|
||||
@@ -2416,6 +2483,13 @@ export async function aiMergeTask(
|
||||
const fixAttemptStartedAt = Date.now();
|
||||
mergerLog.log(`${taskId}: in-merge verification fix attempt ${fixAttempt}/${maxFixRetries}`);
|
||||
await store.logEntry(taskId, `In-merge verification fix attempt ${fixAttempt}/${maxFixRetries}`);
|
||||
await store.appendAgentLog(
|
||||
taskId,
|
||||
`In-merge verification fix attempt ${fixAttempt}/${maxFixRetries}`,
|
||||
"text",
|
||||
undefined,
|
||||
"merger",
|
||||
);
|
||||
|
||||
throwIfAborted(options.signal, taskId);
|
||||
fixSuccess = await attemptInMergeVerificationFix(
|
||||
@@ -2438,11 +2512,25 @@ export async function aiMergeTask(
|
||||
if (fixSuccess) {
|
||||
mergerLog.log(`${taskId}: in-merge verification fix succeeded on attempt ${fixAttempt} in ${fixAttemptDurationMs}ms`);
|
||||
await store.logEntry(taskId, `[timing] In-merge verification fix succeeded on attempt ${fixAttempt} in ${fixAttemptDurationMs}ms — verification now passes`);
|
||||
await store.appendAgentLog(
|
||||
taskId,
|
||||
`In-merge verification fix succeeded on attempt ${fixAttempt}`,
|
||||
"tool_result",
|
||||
`${fixAttemptDurationMs}ms — verification now passes`,
|
||||
"merger",
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
mergerLog.warn(`${taskId}: in-merge verification fix attempt ${fixAttempt} — verification still fails (${fixAttemptDurationMs}ms)`);
|
||||
await store.logEntry(taskId, `[timing] In-merge verification fix attempt ${fixAttempt} — verification still fails (${fixAttemptDurationMs}ms)`);
|
||||
await store.appendAgentLog(
|
||||
taskId,
|
||||
`In-merge verification fix attempt ${fixAttempt} failed`,
|
||||
"tool_error",
|
||||
`${fixAttemptDurationMs}ms — verification still fails`,
|
||||
"merger",
|
||||
);
|
||||
}
|
||||
|
||||
if (fixSuccess) {
|
||||
@@ -2467,6 +2555,13 @@ export async function aiMergeTask(
|
||||
if (maxFixRetries > 0 && (effectiveTestCommand || effectiveBuildCommand)) {
|
||||
mergerLog.log(`${taskId}: build verification failed — attempting in-merge fix`);
|
||||
await store.logEntry(taskId, `Build verification failed during merge — attempting in-merge fix`);
|
||||
await store.appendAgentLog(
|
||||
taskId,
|
||||
"Build verification failed — attempting in-merge fix",
|
||||
"text",
|
||||
undefined,
|
||||
"merger",
|
||||
);
|
||||
|
||||
const fixCommand = effectiveBuildCommand || effectiveTestCommand!;
|
||||
const fixType = effectiveBuildCommand ? "build" as const : "test" as const;
|
||||
@@ -2476,6 +2571,13 @@ export async function aiMergeTask(
|
||||
const fixAttemptStartedAt = Date.now();
|
||||
mergerLog.log(`${taskId}: in-merge verification fix attempt ${fixAttempt}/${maxFixRetries}`);
|
||||
await store.logEntry(taskId, `In-merge verification fix attempt ${fixAttempt}/${maxFixRetries}`);
|
||||
await store.appendAgentLog(
|
||||
taskId,
|
||||
`In-merge verification fix attempt ${fixAttempt}/${maxFixRetries}`,
|
||||
"text",
|
||||
undefined,
|
||||
"merger",
|
||||
);
|
||||
|
||||
throwIfAborted(options.signal, taskId);
|
||||
fixSuccess = await attemptInMergeVerificationFix(
|
||||
@@ -2498,9 +2600,23 @@ export async function aiMergeTask(
|
||||
if (fixSuccess) {
|
||||
mergerLog.log(`${taskId}: in-merge verification fix succeeded on attempt ${fixAttempt} in ${fixAttemptDurationMs}ms`);
|
||||
await store.logEntry(taskId, `[timing] In-merge verification fix succeeded on attempt ${fixAttempt} in ${fixAttemptDurationMs}ms`);
|
||||
await store.appendAgentLog(
|
||||
taskId,
|
||||
`In-merge verification fix succeeded on attempt ${fixAttempt}`,
|
||||
"tool_result",
|
||||
`${fixAttemptDurationMs}ms`,
|
||||
"merger",
|
||||
);
|
||||
break;
|
||||
}
|
||||
await store.logEntry(taskId, `[timing] In-merge verification fix attempt ${fixAttempt} — verification still fails (${fixAttemptDurationMs}ms)`);
|
||||
await store.appendAgentLog(
|
||||
taskId,
|
||||
`In-merge verification fix attempt ${fixAttempt} failed`,
|
||||
"tool_error",
|
||||
`${fixAttemptDurationMs}ms — verification still fails`,
|
||||
"merger",
|
||||
);
|
||||
}
|
||||
|
||||
if (fixSuccess) {
|
||||
|
||||
Reference in New Issue
Block a user