diff --git a/packages/engine/src/__tests__/workflow-merge-nodes.test.ts b/packages/engine/src/__tests__/workflow-merge-nodes.test.ts index e32c7499c4..d241c25826 100644 --- a/packages/engine/src/__tests__/workflow-merge-nodes.test.ts +++ b/packages/engine/src/__tests__/workflow-merge-nodes.test.ts @@ -31,10 +31,22 @@ describe("workflow merge nodes", () => { outcome: "failure", value: "file-scope-violation", }); + expect(classifyMergePrimitiveResult({ status: "merged-requested" }, undefined, "failure")).toEqual({ + outcome: "success", + value: "merged-requested", + }); + expect(classifyMergePrimitiveResult({ status: "stale-head" }, undefined, "failure")).toEqual({ + outcome: "failure", + value: "stale-head", + }); expect(classifyMergePrimitiveResult(undefined, "transient-failure", "failure")).toEqual({ outcome: "success", value: "transient-failure", }); + expect(classifyMergePrimitiveResult(undefined, "merged-requested", "failure")).toEqual({ + outcome: "success", + value: "merged-requested", + }); }); it("runs the existing merge primitive and emits a workflow capability audit event", async () => { @@ -58,4 +70,21 @@ describe("workflow merge nodes", () => { contextPatch: { mergedBranch: "main", "workflow:merge-status": "merged" }, }); }); + + it("does not retry the merge primitive when audit fails after classification", async () => { + const audit = vi.fn().mockRejectedValue(new Error("audit unavailable")); + const requestMerge = vi.fn().mockResolvedValue({ + outcome: "success", + data: { status: "merged" }, + }); + + const result = await runWorkflowMergeAttemptNode({ primitives: { requestMerge, audit } }, ctx, task); + + expect(requestMerge).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + outcome: "success", + value: "merged", + contextPatch: { "workflow:merge-status": "merged" }, + }); + }); }); diff --git a/packages/engine/src/workflow-merge-nodes.ts b/packages/engine/src/workflow-merge-nodes.ts index 886c59ec8e..bdcdcf554a 100644 --- a/packages/engine/src/workflow-merge-nodes.ts +++ b/packages/engine/src/workflow-merge-nodes.ts @@ -13,11 +13,15 @@ export async function runWorkflowMergeAttemptNode( ): Promise { const result = await deps.primitives.requestMerge(ctx, task); const classified = classifyMergePrimitiveResult(result.data, result.value, result.outcome); - await deps.primitives.audit(ctx, { - type: "workflow-merge-node", - message: `workflow merge node classified ${classified.value ?? classified.outcome}`, - metadata: { taskId: task.id, primitiveOutcome: result.outcome, primitiveValue: result.value, primitiveData: result.data }, - }); + try { + await deps.primitives.audit(ctx, { + type: "workflow-merge-node", + message: `workflow merge node classified ${classified.value ?? classified.outcome}`, + metadata: { taskId: task.id, primitiveOutcome: result.outcome, primitiveValue: result.value, primitiveData: result.data }, + }); + } catch { + // Audit is diagnostic; a transient audit failure must not re-run the merge primitive. + } return { outcome: classified.outcome, value: classified.value, @@ -42,7 +46,13 @@ export function classifyMergePrimitiveResult( if (data?.status === "failed") { return classifyMergeFailure(data.reason); } - if (value === "transient-failure" || value === "manual-required" || value === "stale-head" || value === "not-actionable") { + if (data?.status === "merged-requested") { + return { outcome: "success", value: "merged-requested" }; + } + if (data?.status === "stale-head") { + return { outcome: primitiveOutcome, value: "stale-head" }; + } + if (value === "transient-failure" || value === "manual-required" || value === "stale-head" || value === "not-actionable" || value === "merged-requested") { return { outcome: "success", value }; } return { outcome: primitiveOutcome, value };