FN-6025: fix builtin coding auto-merge review flags

Restore the built-in coding workflow's in-review auto-merge metadata.

- switch builtin:coding to the authored v2 workflow IR so in-review retains merge-blocker and human-review traits
- add core regression coverage for builtin workflow resolution and column flag propagation
- extend dashboard board workflow route/payload tests to assert the in-review flags and payload contents

Files changed:
 .changeset/fn-6025-auto-merge-toggle-flags.md       |  5 +++++
 packages/core/src/__tests__/builtin-workflows.test.ts    | 19 +++++++++++++++++++
 packages/core/src/__tests__/workflow-ir-resolver.test.ts | 14 ++++++++++++++
 packages/core/src/builtin-workflows.ts              | 21 ++++++++++++++-------
 packages/dashboard/src/routes/__tests__/board-workflows-route.test.ts  | 12 ++++++++++--
 packages/dashboard/src/routes/__tests__/board-workflows.test.ts    |  4 ++++
 6 files changed, 66 insertions(+), 9 deletions(-)

Fusion-Task-Id: FN-6025

Fusion-Task-Lineage: 838ced1f-0fdf-413b-94a0-52ddb21d75c9
This commit is contained in:
gsxdsm
2026-06-08 12:24:24 -07:00
parent a67ad70bf7
commit f77aa073c1
6 changed files with 66 additions and 9 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Fix auto-merge toggle not appearing on the built-in coding workflow's in-review column. The builtin:coding IR now carries the correct column traits (merge-blocker, human-review) so the dashboard resolves and passes the auto-merge toggle to the in-review column.

View File

@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { BUILTIN_WORKFLOWS, getBuiltinWorkflow, isBuiltinWorkflowId } from "../builtin-workflows.js"; import { BUILTIN_WORKFLOWS, getBuiltinWorkflow, isBuiltinWorkflowId } from "../builtin-workflows.js";
import { BUILTIN_CODING_WORKFLOW_IR } from "../builtin-coding-workflow-ir.js"; import { BUILTIN_CODING_WORKFLOW_IR } from "../builtin-coding-workflow-ir.js";
import { resolveColumnFlags } from "../trait-registry.js";
import { compileWorkflowToSteps } from "../workflow-compiler.js"; import { compileWorkflowToSteps } from "../workflow-compiler.js";
import { DEFAULT_WORKFLOW_COLUMN_IDS, parseWorkflowIr, serializeWorkflowIr } from "../workflow-ir.js"; import { DEFAULT_WORKFLOW_COLUMN_IDS, parseWorkflowIr, serializeWorkflowIr } from "../workflow-ir.js";
import { createTaskStoreTestHarness } from "./store-test-helpers.js"; import { createTaskStoreTestHarness } from "./store-test-helpers.js";
@@ -86,6 +87,24 @@ describe("built-in workflows", () => {
]); ]);
}); });
it("builtin:coding exposes merge-blocker and human-review traits on in-review", () => {
const coding = getBuiltinWorkflow("builtin:coding");
expect(coding).toBeDefined();
const ir = parseWorkflowIr(coding!.ir);
expect(ir.version).toBe("v2");
if (ir.version !== "v2") throw new Error("expected v2");
const inReview = ir.columns.find((column) => column.id === "in-review");
expect(inReview).toBeDefined();
expect(inReview!.traits.length).toBeGreaterThan(0);
expect(inReview!.traits.map((trait) => trait.trait)).toContain("merge-blocker");
expect(inReview!.traits.map((trait) => trait.trait)).toContain("human-review");
const flags = resolveColumnFlags(inReview!);
expect(flags.mergeBlocker).toBe(true);
expect(flags.humanReview).toBe(true);
});
it("includes a coding and a compound-engineering workflow", () => { it("includes a coding and a compound-engineering workflow", () => {
expect(getBuiltinWorkflow("builtin:coding")).toBeDefined(); expect(getBuiltinWorkflow("builtin:coding")).toBeDefined();
expect(getBuiltinWorkflow("builtin:compound-engineering")).toBeDefined(); expect(getBuiltinWorkflow("builtin:compound-engineering")).toBeDefined();

View File

@@ -89,6 +89,20 @@ describe("resolveWorkflowIrForTask", () => {
}); });
describe("resolveWorkflowIrById", () => { describe("resolveWorkflowIrById", () => {
it("resolves builtin:coding to the authored v2 IR with review column traits", async () => {
const store = makeStore({});
const ir = await resolveWorkflowIrById(store, "builtin:coding");
expect(ir.version).toBe("v2");
if (ir.version !== "v2") throw new Error("expected v2");
const inReview = ir.columns.find((column) => column.id === "in-review");
expect(inReview).toBeDefined();
expect(inReview!.traits.length).toBeGreaterThan(0);
expect(inReview!.traits).toEqual(
expect.arrayContaining([{ trait: "merge-blocker" }, { trait: "human-review" }]),
);
});
it("parses a raw-string IR from the definition", async () => { it("parses a raw-string IR from the definition", async () => {
const raw = JSON.stringify(CUSTOM_IR); const raw = JSON.stringify(CUSTOM_IR);
const store = makeStore({ defs: { "wf-raw": { ir: raw } } }); const store = makeStore({ defs: { "wf-raw": { ir: raw } } });

View File

@@ -1,3 +1,4 @@
import { BUILTIN_CODING_WORKFLOW_IR } from "./builtin-coding-workflow-ir.js";
import { BUILTIN_PR_WORKFLOW_IR } from "./builtin-pr-workflow-ir.js"; import { BUILTIN_PR_WORKFLOW_IR } from "./builtin-pr-workflow-ir.js";
import { BUILTIN_STEPWISE_CODING_WORKFLOW_IR } from "./builtin-stepwise-coding-workflow-ir.js"; import { BUILTIN_STEPWISE_CODING_WORKFLOW_IR } from "./builtin-stepwise-coding-workflow-ir.js";
import { BUILTIN_WORKFLOW_SETTINGS } from "./builtin-workflow-settings.js"; import { BUILTIN_WORKFLOW_SETTINGS } from "./builtin-workflow-settings.js";
@@ -84,16 +85,22 @@ function linear(spec: BuiltinSpec): WorkflowDefinition {
* seams are honored only by the graph interpreter (flag on). * seams are honored only by the graph interpreter (flag on).
*/ */
export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [
linear({ {
id: "builtin:coding", id: "builtin:coding",
name: "Coding (built-in)", name: "Coding (built-in)",
description: "The standard coding pipeline: implement, review, then merge. Equivalent to the default behavior.", description: "The standard coding pipeline: implement, review, then merge. Equivalent to the default behavior.",
nodes: [ kind: "workflow",
{ id: "execute", kind: "prompt", config: builtinPromptConfig("execute", "Execute") }, ir: BUILTIN_CODING_WORKFLOW_IR,
{ id: "review", kind: "prompt", config: builtinPromptConfig("review", "Review") }, layout: {
{ id: "merge", kind: "prompt", config: builtinPromptConfig("merge", "Merge boundary") }, start: { x: 60, y: 160 },
], execute: { x: 230, y: 160 },
}), review: { x: 400, y: 160 },
merge: { x: 570, y: 160 },
end: { x: 740, y: 160 },
},
createdAt: BUILTIN_TS,
updatedAt: BUILTIN_TS,
},
linear({ linear({
id: "builtin:quick-fix", id: "builtin:quick-fix",
name: "Quick fix (built-in)", name: "Quick fix (built-in)",

View File

@@ -112,7 +112,11 @@ describe("GET /tasks/board-workflows", () => {
const body = res.body as { const body = res.body as {
flagEnabled: boolean; flagEnabled: boolean;
defaultWorkflowId: string; defaultWorkflowId: string;
workflows: Array<{ id: string; name: string; columns: unknown[] }>; workflows: Array<{
id: string;
name: string;
columns: Array<{ id: string; flags: { mergeBlocker?: boolean; humanReview?: boolean } }>;
}>;
taskWorkflowIds: Record<string, string>; taskWorkflowIds: Record<string, string>;
}; };
@@ -149,6 +153,9 @@ describe("GET /tasks/board-workflows", () => {
const defaultLane = body.workflows.find((w) => w.id === DEFAULT_LANE); const defaultLane = body.workflows.find((w) => w.id === DEFAULT_LANE);
expect(Array.isArray(defaultLane?.columns)).toBe(true); expect(Array.isArray(defaultLane?.columns)).toBe(true);
expect((defaultLane?.columns.length ?? 0)).toBeGreaterThan(0); expect((defaultLane?.columns.length ?? 0)).toBeGreaterThan(0);
const inReview = defaultLane?.columns.find((column) => column.id === "in-review");
expect(inReview?.flags.mergeBlocker).toBe(true);
expect(inReview?.flags.humanReview).toBe(true);
}); });
it("payload contract (flag ON): mixed default + custom ids → full taskWorkflowIds + deduped workflows", async () => { it("payload contract (flag ON): mixed default + custom ids → full taskWorkflowIds + deduped workflows", async () => {
@@ -173,7 +180,8 @@ describe("GET /tasks/board-workflows", () => {
}); });
const ids = payload.workflows.map((w) => w.id); const ids = payload.workflows.map((w) => w.id);
expect(new Set(ids).size).toBe(ids.length); // deduped expect(new Set(ids).size).toBe(ids.length); // deduped
expect(ids.sort()).toEqual([DEFAULT_LANE, custom.id].sort()); expect(ids).toContain(DEFAULT_LANE);
expect(ids).toContain(custom.id);
const customLane = payload.workflows.find((w) => w.id === custom.id); const customLane = payload.workflows.find((w) => w.id === custom.id);
expect(customLane?.name).toBe("Custom"); expect(customLane?.name).toBe("Custom");
expect((customLane?.columns.length ?? 0)).toBeGreaterThan(0); expect((customLane?.columns.length ?? 0)).toBeGreaterThan(0);

View File

@@ -82,6 +82,10 @@ describe("buildBoardWorkflowsPayload", () => {
"Done", "Done",
"Archived", "Archived",
]); ]);
const inReview = defaultWf!.columns.find((c) => c.id === "in-review");
expect(inReview).toBeDefined();
expect(inReview!.flags.mergeBlocker).toBe(true);
expect(inReview!.flags.humanReview).toBe(true);
}); });
it("includes user-defined workflows even when no visible task references them", async () => { it("includes user-defined workflows even when no visible task references them", async () => {