fix(FN-000): harden workflow merge classification

Address PR #1579 feedback by classifying PR merge statuses explicitly and preventing diagnostic audit failures from re-running a completed merge primitive.
This commit is contained in:
gsxdsm
2026-06-11 08:05:29 -07:00
parent 53d644714c
commit 283c64dfee
2 changed files with 45 additions and 6 deletions

View File

@@ -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" },
});
});
});

View File

@@ -13,11 +13,15 @@ export async function runWorkflowMergeAttemptNode(
): Promise<WorkflowNodeResult> {
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 };