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 () => {
|
||||
const h = await makeHarness();
|
||||
try {
|
||||
|
||||
@@ -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<Task> {
|
||||
|
||||
Reference in New Issue
Block a user