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:
@@ -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" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user