From c3e98d193f34f01c3e344f92313d807ba69582d7 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 17 Jul 2026 00:42:00 -0700 Subject: [PATCH] FN-8188: inherit default workflow steps for refinements Make refinement tasks inherit configured default workflow steps and persist their selection. - Seed refinement tasks with enabled default workflow steps - Persist inherited workflow selections after task creation - Cover populated, empty, and absent default workflows in PostgreSQL tests - Add a patch changeset for the refinement workflow fix Files changed: .changeset/fn-8188-refinement-workflow-steps.md | 7 +++ packages/core/src/__tests__/postgres/refine-duplicate-task.pg.test.ts | 68 ++++++++++++++++++++++ packages/core/src/task-store/update-task-deps.ts | 42 ++++++++++++- 3 files changed, 116 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-8188 Fusion-Task-Lineage: aed1e63a-f828-426c-9d3d-3a4ba17443c1 Co-authored-by: Fusion (runfusion.ai) --- .../fn-8188-refinement-workflow-steps.md | 7 ++ .../postgres/refine-duplicate-task.pg.test.ts | 68 +++++++++++++++++++ .../core/src/task-store/update-task-deps.ts | 42 +++++++++++- 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 .changeset/fn-8188-refinement-workflow-steps.md diff --git a/.changeset/fn-8188-refinement-workflow-steps.md b/.changeset/fn-8188-refinement-workflow-steps.md new file mode 100644 index 0000000000..538e0ef341 --- /dev/null +++ b/.changeset/fn-8188-refinement-workflow-steps.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Refinement tasks now inherit the default workflow's optional review steps. +category: fix +dev: refineTaskImpl seeds enabledWorkflowSteps via materializeDefaultWorkflowSteps() and records the workflow selection, mirroring createTask (FN-8188). diff --git a/packages/core/src/__tests__/postgres/refine-duplicate-task.pg.test.ts b/packages/core/src/__tests__/postgres/refine-duplicate-task.pg.test.ts index 5e5cf32a00..702321b919 100644 --- a/packages/core/src/__tests__/postgres/refine-duplicate-task.pg.test.ts +++ b/packages/core/src/__tests__/postgres/refine-duplicate-task.pg.test.ts @@ -56,6 +56,74 @@ pgDescribe("refineTask / duplicateTask backend mode (PostgreSQL)", () => { } }); + /* + * FNXC:WorkflowOptionalSteps 2026-07-16-00:00: + * FN-8188 requires refinements to use the same project-default optional-group + * seed and persisted selection as createTask, including empty and absent defaults. + */ + it("refineTask inherits default-on workflow groups and selection like createTask", async () => { + const h = await makeHarness(); + try { + await h.store.setDefaultWorkflowId("builtin:coding"); + const source = await h.store.createTask({ + title: "Completed source", + description: "Original completed work", + column: "done", + }); + const control = await h.store.createTask({ description: "Fresh control task" }); + + const refined = await h.store.refineTask(source.id, "Please add stronger review coverage"); + + expect((await h.store.getTask(control.id)).enabledWorkflowSteps).toEqual(["plan-review", "code-review"]); + expect((await h.store.getTask(refined.id)).enabledWorkflowSteps).toEqual(["plan-review", "code-review"]); + expect(await h.store.getTaskWorkflowSelectionAsync(refined.id)).toEqual({ + workflowId: "builtin:coding", + stepIds: ["plan-review", "code-review"], + }); + } finally { + await teardown(); + } + }); + + it("refineTask persists empty default workflow groups and tolerates no default workflow", async () => { + const h = await makeHarness(); + try { + await h.store.setDefaultWorkflowId("builtin:marketing"); + const marketingSource = await h.store.createTask({ + title: "Marketing source", + description: "Completed marketing work", + column: "done", + }); + const marketingRefinement = await h.store.refineTask(marketingSource.id, "Update the campaign copy"); + + expect((await h.store.getTask(marketingRefinement.id)).enabledWorkflowSteps).toEqual([]); + expect(await h.store.getTaskWorkflowSelectionAsync(marketingRefinement.id)).toEqual({ + workflowId: "builtin:marketing", + stepIds: [], + }); + + await h.store.setDefaultWorkflowId(null); + const noDefaultSource = await h.store.createTask({ + title: "No-default source", + description: "Completed work without a configured workflow", + column: "done", + }); + const noDefaultControl = await h.store.createTask({ description: "Fresh task without a configured workflow" }); + const noDefaultRefinement = await h.store.refineTask(noDefaultSource.id, "Tighten the final copy"); + + // FNXC:WorkflowOptionalSteps 2026-07-16-00:00: PostgreSQL normalizes omitted + // JSONB enabled_workflow_steps to [] for both creation paths, while the fresh + // refinement object retains the unset field when no default is configured. + expect(noDefaultRefinement.enabledWorkflowSteps).toBeUndefined(); + expect((await h.store.getTask(noDefaultRefinement.id)).enabledWorkflowSteps).toEqual( + (await h.store.getTask(noDefaultControl.id)).enabledWorkflowSteps, + ); + expect(await h.store.getTaskWorkflowSelectionAsync(noDefaultRefinement.id)).toBeUndefined(); + } finally { + await teardown(); + } + }); + it("refineTask works for an in-review source task in backend mode", async () => { const h = await makeHarness(); try { diff --git a/packages/core/src/task-store/update-task-deps.ts b/packages/core/src/task-store/update-task-deps.ts index ffee31369e..69e22f9923 100644 --- a/packages/core/src/task-store/update-task-deps.ts +++ b/packages/core/src/task-store/update-task-deps.ts @@ -45,7 +45,26 @@ export async function refineTaskImpl(store: TaskStore, id: string, feedback: str sourceLabel = firstLine ? firstLine.replace(/\s+/g, " ") : sourceTask.id; } - return store.createTaskWithDistributedReservation({ description: feedback.trim() }, { + /* + * FNXC:WorkflowOptionalSteps 2026-07-16-00:00: + * FN-8188 requires refinements to inherit create-time default-workflow seeding so + * default-on optional groups, including plan-review and code-review, gate them + * exactly as they gate newly created tasks. + */ + let pendingWorkflowSelection: { workflowId: string; stepIds: string[] } | undefined; + try { + const inherited = await store.materializeDefaultWorkflowSteps(); + if (inherited) { + pendingWorkflowSelection = inherited; + } + } catch (err) { + storeLog.warn("Failed to apply default workflow during refinement task creation", { + phase: "refineTask:default-workflow", + error: err instanceof Error ? err.message : String(err), + }); + } + + const newTask = await store.createTaskWithDistributedReservation({ description: feedback.trim() }, { createTaskWithId: async (newId) => { // FN-5077: keep deterministic "Refinement" fallback when normalized refinement label is unusable (null). const normalizedTitle = normalizeTitleForTaskId(`Refinement: ${sourceLabel}`, newId); @@ -82,6 +101,9 @@ export async function refineTaskImpl(store: TaskStore, id: string, feedback: str createdAt: now, updatedAt: now, attachments: sourceTask.attachments ? [...sourceTask.attachments] : undefined, + ...(pendingWorkflowSelection + ? { enabledWorkflowSteps: pendingWorkflowSelection.stepIds } + : {}), }; await store.maybeResolveTombstonedTaskId(newId, {}, "refineTask"); @@ -116,6 +138,24 @@ export async function refineTaskImpl(store: TaskStore, id: string, feedback: str return newTask; }, }); + + // Record the inherited selection only after its task row exists, matching createTask. + if (pendingWorkflowSelection) { + try { + await store.writeTaskWorkflowSelection( + newTask.id, + pendingWorkflowSelection.workflowId, + pendingWorkflowSelection.stepIds, + ); + } catch (err) { + storeLog.warn("Failed to record inherited workflow selection", { + taskId: newTask.id, + error: err instanceof Error ? err.message : String(err), + }); + } + } + + return newTask; } export async function updateTaskDependenciesImpl(store: TaskStore, id: string, mutation: TaskDependencyMutation, runContext?: RunMutationContext,): Promise {