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) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-17 00:42:00 -07:00
parent b779785286
commit c3e98d193f
3 changed files with 116 additions and 1 deletions

View File

@@ -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).

View File

@@ -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 () => { it("refineTask works for an in-review source task in backend mode", async () => {
const h = await makeHarness(); const h = await makeHarness();
try { try {

View File

@@ -45,7 +45,26 @@ export async function refineTaskImpl(store: TaskStore, id: string, feedback: str
sourceLabel = firstLine ? firstLine.replace(/\s+/g, " ") : sourceTask.id; 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) => { createTaskWithId: async (newId) => {
// FN-5077: keep deterministic "Refinement" fallback when normalized refinement label is unusable (null). // FN-5077: keep deterministic "Refinement" fallback when normalized refinement label is unusable (null).
const normalizedTitle = normalizeTitleForTaskId(`Refinement: ${sourceLabel}`, newId); const normalizedTitle = normalizeTitleForTaskId(`Refinement: ${sourceLabel}`, newId);
@@ -82,6 +101,9 @@ export async function refineTaskImpl(store: TaskStore, id: string, feedback: str
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
attachments: sourceTask.attachments ? [...sourceTask.attachments] : undefined, attachments: sourceTask.attachments ? [...sourceTask.attachments] : undefined,
...(pendingWorkflowSelection
? { enabledWorkflowSteps: pendingWorkflowSelection.stepIds }
: {}),
}; };
await store.maybeResolveTombstonedTaskId(newId, {}, "refineTask"); await store.maybeResolveTombstonedTaskId(newId, {}, "refineTask");
@@ -116,6 +138,24 @@ export async function refineTaskImpl(store: TaskStore, id: string, feedback: str
return newTask; 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<Task> { export async function updateTaskDependenciesImpl(store: TaskStore, id: string, mutation: TaskDependencyMutation, runContext?: RunMutationContext,): Promise<Task> {