FN-6137: convert PR lifecycle built-in to fragment
Convert the built-in PR lifecycle into a reusable fragment instead of a selectable task workflow. - mark builtin:pr-workflow as a fragment and exclude fragments from default-enabled built-ins - hide built-in fragments from the General settings workflow toggles - update core and dashboard tests to assert fragment behavior and reject task selection of the PR lifecycle fragment - add a changeset for the published CLI package Files changed: .changeset/pr-lifecycle-fragment.md | 5 +++++ .../core/src/__tests__/builtin-workflows.test.ts | 24 ++++++++++++++++------ packages/core/src/builtin-workflows.ts | 7 ++++--- packages/core/src/store.ts | 2 +- .../__tests__/WorkflowNodeEditor.test.tsx | 3 ++- .../settings/sections/GeneralSection.tsx | 4 +++- 6 files changed, 33 insertions(+), 12 deletions(-) Fusion-Task-Id: FN-6137 Fusion-Task-Lineage: d959ba3b-2e50-4c9b-a1fa-161776939403
This commit is contained in:
5
.changeset/pr-lifecycle-fragment.md
Normal file
5
.changeset/pr-lifecycle-fragment.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
Convert the built-in PR lifecycle from a selectable task workflow into a reusable workflow-editor fragment template.
|
||||
@@ -16,17 +16,16 @@ import { createTaskStoreTestHarness } from "./store-test-helpers.js";
|
||||
const EXECUTE_NODE_MAX_RETRIES = 2;
|
||||
|
||||
describe("built-in workflows", () => {
|
||||
// Graph-only built-ins (step inversion, KTD-9) model branching/foreach/rework
|
||||
// structure the linear compiler cannot lower to a step list — they run only
|
||||
// under the workflow graph executor. They still must parse as valid IR.
|
||||
const GRAPH_ONLY_BUILTIN_IDS = new Set(["builtin:stepwise-coding", "builtin:pr-workflow"]);
|
||||
// Non-compiler built-ins model graph-only node kinds or reusable fragments the
|
||||
// linear compiler cannot lower to a step list. They still must parse as valid IR.
|
||||
const NON_COMPILABLE_BUILTIN_IDS = new Set(["builtin:stepwise-coding", "builtin:pr-workflow"]);
|
||||
|
||||
it("every built-in has a valid IR; linear built-ins compile without error", () => {
|
||||
expect(BUILTIN_WORKFLOWS.length).toBeGreaterThanOrEqual(4);
|
||||
for (const wf of BUILTIN_WORKFLOWS) {
|
||||
expect(isBuiltinWorkflowId(wf.id)).toBe(true);
|
||||
expect(() => parseWorkflowIr(wf.ir)).not.toThrow();
|
||||
if (!GRAPH_ONLY_BUILTIN_IDS.has(wf.id)) {
|
||||
if (!NON_COMPILABLE_BUILTIN_IDS.has(wf.id)) {
|
||||
expect(() => compileWorkflowToSteps(wf.ir)).not.toThrow();
|
||||
}
|
||||
}
|
||||
@@ -51,6 +50,8 @@ describe("built-in workflows", () => {
|
||||
it("includes the PR lifecycle built-in wiring the PR nodes end to end (U9)", () => {
|
||||
const pr = getBuiltinWorkflow("builtin:pr-workflow");
|
||||
expect(pr).toBeDefined();
|
||||
expect(pr!.kind).toBe("fragment");
|
||||
expect(BUILTIN_WORKFLOWS.some((workflow) => workflow.id === "builtin:pr-workflow")).toBe(true);
|
||||
const ir = parseWorkflowIr(pr!.ir);
|
||||
if (ir.version !== "v2") throw new Error("expected v2");
|
||||
|
||||
@@ -143,7 +144,11 @@ describe("built-in workflows", () => {
|
||||
expect(getBuiltinWorkflow("builtin:coding")?.ir).toBe(BUILTIN_CODING_WORKFLOW_IR);
|
||||
expect(getBuiltinWorkflow("builtin:coding")?.ir).toBe(BUILTIN_CODING_WORKFLOW_IR);
|
||||
expect(BUILTIN_WORKFLOWS.find((workflow) => workflow.id === "builtin:coding")?.ir).toBe(BUILTIN_CODING_WORKFLOW_IR);
|
||||
expect(defaultEnabledBuiltinWorkflowIds()).toEqual(BUILTIN_WORKFLOWS.map((workflow) => workflow.id));
|
||||
expect(defaultEnabledBuiltinWorkflowIds()).toEqual(
|
||||
BUILTIN_WORKFLOWS.filter((workflow) => workflow.kind !== "fragment").map((workflow) => workflow.id),
|
||||
);
|
||||
expect(defaultEnabledBuiltinWorkflowIds()).not.toContain("builtin:pr-workflow");
|
||||
expect(getBuiltinWorkflow("builtin:pr-workflow")!.kind).toBe("fragment");
|
||||
expect(defaultEnabledBuiltinWorkflowIds().slice(0, 5)).toEqual([
|
||||
"builtin:coding",
|
||||
"builtin:quick-fix",
|
||||
@@ -295,5 +300,12 @@ describe("built-in workflows", () => {
|
||||
await store.selectTaskWorkflow(task.id, "builtin:compound-engineering");
|
||||
expect(store.getTaskWorkflowSelection(task.id)?.workflowId).toBe("builtin:compound-engineering");
|
||||
});
|
||||
|
||||
it("rejects selecting the PR lifecycle fragment for a task", async () => {
|
||||
const task = await store.createTask({ description: "T", enabledWorkflowSteps: [] });
|
||||
await expect(store.selectTaskWorkflow(task.id, "builtin:pr-workflow")).rejects.toThrow(
|
||||
"is a fragment and cannot be selected for a task",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ export function isBuiltinWorkflowId(id: string): boolean {
|
||||
}
|
||||
|
||||
export function defaultEnabledBuiltinWorkflowIds(): string[] {
|
||||
return BUILTIN_WORKFLOWS.map((workflow) => workflow.id);
|
||||
return BUILTIN_WORKFLOWS.filter((workflow) => workflow.kind !== "fragment").map((workflow) => workflow.id);
|
||||
}
|
||||
|
||||
export function isBuiltinWorkflowEnabled(id: string, enabledIds?: readonly string[]): boolean {
|
||||
@@ -69,7 +69,8 @@ function linear(spec: BuiltinSpec): WorkflowDefinition {
|
||||
id: spec.id,
|
||||
name: spec.name,
|
||||
description: spec.description,
|
||||
// Built-ins are always selectable workflows, never fragments (KTD-1).
|
||||
// Linear built-ins remain selectable workflows; catalog entries authored
|
||||
// directly below may opt into fragment kind when they are palette templates.
|
||||
kind: "workflow",
|
||||
ir,
|
||||
layout,
|
||||
@@ -215,7 +216,7 @@ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [
|
||||
name: "PR lifecycle (built-in)",
|
||||
description:
|
||||
"The unified PR lifecycle as graph nodes: create the PR, await review, respond to changes (bounded rework loop), gate on auto-merge, then merge — with GitHub reconciliation advancing the await holds. Requires the workflow graph executor.",
|
||||
kind: "workflow",
|
||||
kind: "fragment",
|
||||
ir: BUILTIN_PR_WORKFLOW_IR,
|
||||
layout: {
|
||||
start: { x: 60, y: 160 },
|
||||
|
||||
@@ -13299,7 +13299,7 @@ ${stepsSection}`;
|
||||
|
||||
/** Read (and cache) the full merged workflow-definition set, oldest first.
|
||||
* Built-in templates lead the list and cannot be edited/deleted; built-ins
|
||||
* are always kind "workflow". */
|
||||
* may be selectable workflows or reusable fragments. */
|
||||
private async readAllWorkflowDefinitions(): Promise<WorkflowDefinition[]> {
|
||||
if (this.workflowDefinitionsCache) return this.workflowDefinitionsCache;
|
||||
const rows = this.db.prepare("SELECT * FROM workflows ORDER BY createdAt ASC").all() as Array<{
|
||||
|
||||
@@ -169,7 +169,7 @@ function builtinDef(): WorkflowDefinition {
|
||||
function builtinPrDef(): WorkflowDefinition {
|
||||
return {
|
||||
id: "builtin:pr-workflow",
|
||||
kind: "workflow",
|
||||
kind: "fragment",
|
||||
name: "PR lifecycle (built-in)",
|
||||
description: "Ships with Fusion",
|
||||
ir: BUILTIN_PR_WORKFLOW_IR,
|
||||
@@ -271,6 +271,7 @@ describe("workflow-flow-mapping", () => {
|
||||
expect(BUILTIN_WORKFLOWS.map((workflow) => workflow.id).sort()).toEqual(
|
||||
expect.arrayContaining(["builtin:coding", "builtin:stepwise-coding", "builtin:pr-workflow"]),
|
||||
);
|
||||
expect(BUILTIN_WORKFLOWS.find((workflow) => workflow.id === "builtin:pr-workflow")?.kind).toBe("fragment");
|
||||
|
||||
for (const workflow of BUILTIN_WORKFLOWS) {
|
||||
edgeRenderableAssertion(workflow);
|
||||
|
||||
@@ -48,7 +48,9 @@ export function GeneralSection({
|
||||
fetchWorkflows(projectId, { includeDisabledBuiltins: true })
|
||||
.then((workflows) => {
|
||||
if (!cancelled) {
|
||||
setBuiltinWorkflows(workflows.filter((workflow) => workflow.id.startsWith("builtin:")));
|
||||
setBuiltinWorkflows(
|
||||
workflows.filter((workflow) => workflow.id.startsWith("builtin:") && workflow.kind !== "fragment"),
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
Reference in New Issue
Block a user