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:
7
.changeset/fn-8188-refinement-workflow-steps.md
Normal file
7
.changeset/fn-8188-refinement-workflow-steps.md
Normal 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).
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
Reference in New Issue
Block a user