FN-7190: fix linear built-in workflow dispatch
Ensure selectable linear built-in workflows carry the default queue traits needed for Todo release. - Convert linear built-in workflow IR to v2 with cloned canonical coding workflow columns. - Guard synthesized linear built-ins so todo retains hold-capacity dispatch traits. - Cover quick-fix, review-heavy, design, and compound-engineering Todo release behavior with regression tests. - Document that selectable built-ins dispatch through capacity-released queue columns. - Add a patch changeset for the published CLI package. Files changed: .changeset/fn-7190-linear-builtin-todo-hold.md | 7 ++ docs/workflow-editor.md | 2 +- docs/workflow-steps.md | 5 + .../core/src/__tests__/builtin-workflows.test.ts | 110 +++++++++++++++++++++ packages/core/src/builtin-workflows.ts | 46 ++++++--- packages/engine/src/__tests__/hold-release.test.ts | 28 ++++++ 6 files changed, 181 insertions(+), 17 deletions(-) Fusion-Task-Id: FN-7190 Fusion-Task-Lineage: a0a40365-4bd5-49a8-8ae4-5e313cf1d4e0 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7190-linear-builtin-todo-hold.md
Normal file
7
.changeset/fn-7190-linear-builtin-todo-hold.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Fix Compound Engineering, Quick fix, and Review-heavy workflow tasks getting stuck in Todo.
|
||||||
|
category: fix
|
||||||
|
dev: linear() built-in workflows now synthesize the canonical default column traits (hold(capacity) on todo, wip on in-progress, merge on in-review) matching BUILTIN_CODING_WORKFLOW_IR, so the hold/release sweep dispatches their todo cards. Fixes FN-7190.
|
||||||
@@ -138,7 +138,7 @@ Fusion ships built-in workflows as read-only references:
|
|||||||
- `builtin:stepwise-coding` — a graph variant that models per-step parse, execute, review, and rework structure.
|
- `builtin:stepwise-coding` — a graph variant that models per-step parse, execute, review, and rework structure.
|
||||||
- `builtin:design` — a UI-heavy work path with a gated design/UX review before standard review and merge.
|
- `builtin:design` — a UI-heavy work path with a gated design/UX review before standard review and merge.
|
||||||
|
|
||||||
Built-ins can be viewed, exported, and used as templates, but their graph, columns, field declarations, and setting declarations are not editable. Their per-project setting **values** are editable from the Settings panel's Values tab.
|
Built-ins can be viewed, exported, and used as templates, but their graph, columns, field declarations, and setting declarations are not editable. Their per-project setting **values** are editable from the Settings panel's Values tab. Selectable built-ins all use a capacity-released queue column (`todo` or a workflow-specific backlog) that dispatches to the active WIP column through the standard hold/release sweep.
|
||||||
|
|
||||||
To customize behavior, create a workflow from **Blank** or copy a built-in/custom workflow with **Duplicate to customize**. Tasks select a workflow by workflow id. Agents and automation can discover workflows with `fn_workflow_list`, assign one to an existing task with `fn_workflow_select`, or pass `workflow_id` when creating tasks through `fn_task_create` / delegation tools.
|
To customize behavior, create a workflow from **Blank** or copy a built-in/custom workflow with **Duplicate to customize**. Tasks select a workflow by workflow id. Agents and automation can discover workflows with `fn_workflow_list`, assign one to an existing task with `fn_workflow_select`, or pass `workflow_id` when creating tasks through `fn_task_create` / delegation tools.
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ Agents may select or change a workflow only when the user explicitly requested t
|
|||||||
|
|
||||||
FNXC:Docs 2026-06-21-12:00:
|
FNXC:Docs 2026-06-21-12:00:
|
||||||
FN-6906 makes non-coding built-in prompts artifact-oriented: marketing drafts, lead enrichment/outreach, and design previews are persisted with fn_task_document_write, while fn_artifact_register remains conditional until the artifact tool is available.
|
FN-6906 makes non-coding built-in prompts artifact-oriented: marketing drafts, lead enrichment/outreach, and design previews are persisted with fn_task_document_write, while fn_artifact_register remains conditional until the artifact tool is available.
|
||||||
|
|
||||||
|
FNXC:WorkflowRuntime 2026-06-28-08:10:
|
||||||
|
Selectable built-in workflows must share the canonical dispatch traits: their held work enters through a capacity-released `todo`/backlog column and moves to the first WIP execution column via the hold/release sweep, so non-default built-ins do not need a separate dispatcher.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
Fusion workflows define the task lifecycle policy that moves work from an idea to delivery. The default coding path is **Plan/Triage → Execute → Workflow steps → Review → Merge**, but that path is now represented as a workflow selection rather than only as fixed engine behavior. A task with no explicit workflow resolves to `builtin:coding`; an explicit missing/corrupt custom workflow fails closed instead of silently falling back.
|
Fusion workflows define the task lifecycle policy that moves work from an idea to delivery. The default coding path is **Plan/Triage → Execute → Workflow steps → Review → Merge**, but that path is now represented as a workflow selection rather than only as fixed engine behavior. A task with no explicit workflow resolves to `builtin:coding`; an explicit missing/corrupt custom workflow fails closed instead of silently falling back.
|
||||||
@@ -51,6 +54,8 @@ Decision-only or investigation tasks can also declare `noCommitsExpected` / `**N
|
|||||||
| PR lifecycle | `builtin:pr-workflow` | Reusable PR lifecycle graph fragment (create PR → await review → respond → gate → merge); it is a fragment, not directly selectable as a task workflow. |
|
| PR lifecycle | `builtin:pr-workflow` | Reusable PR lifecycle graph fragment (create PR → await review → respond → gate → merge); it is a fragment, not directly selectable as a task workflow. |
|
||||||
| Lead generation | `builtin:lead-generation` | Selectable business workflow for sourcing, qualifying, enriching, and contacting leads with custom lead fields, stage columns, and reviewable enrichment/outreach task documents; requires the workflow graph executor for custom board columns. |
|
| Lead generation | `builtin:lead-generation` | Selectable business workflow for sourcing, qualifying, enriching, and contacting leads with custom lead fields, stage columns, and reviewable enrichment/outreach task documents; requires the workflow graph executor for custom board columns. |
|
||||||
|
|
||||||
|
Every selectable built-in workflow uses a capacity-released hold column (`todo` or a workflow-specific backlog) for queued work and a WIP execution column for active work, so the hold/release sweep performs the normal `todo`/backlog → in-progress dispatch across the catalog.
|
||||||
|
|
||||||
### Skill-backed workflow steps
|
### Skill-backed workflow steps
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ import { DEFAULT_WORKFLOW_COLUMN_IDS, parseWorkflowIr, serializeWorkflowIr } fro
|
|||||||
import { createSharedTaskStoreTestHarness } from "./store-test-helpers.js";
|
import { createSharedTaskStoreTestHarness } from "./store-test-helpers.js";
|
||||||
|
|
||||||
const EXECUTE_NODE_MAX_RETRIES = 2;
|
const EXECUTE_NODE_MAX_RETRIES = 2;
|
||||||
|
const LINEAR_BUILTIN_IDS = [
|
||||||
|
"builtin:quick-fix",
|
||||||
|
"builtin:review-heavy",
|
||||||
|
"builtin:design",
|
||||||
|
"builtin:compound-engineering",
|
||||||
|
] as const;
|
||||||
|
|
||||||
function browserVerificationInnerConfig(ir: { nodes: Array<{ id: string; kind: string; config?: Record<string, unknown> }> }): Record<string, unknown> {
|
function browserVerificationInnerConfig(ir: { nodes: Array<{ id: string; kind: string; config?: Record<string, unknown> }> }): Record<string, unknown> {
|
||||||
const group = ir.nodes.find((node) => node.id === BROWSER_VERIFICATION_GROUP_ID);
|
const group = ir.nodes.find((node) => node.id === BROWSER_VERIFICATION_GROUP_ID);
|
||||||
@@ -25,6 +31,13 @@ function browserVerificationInnerConfig(ir: { nodes: Array<{ id: string; kind: s
|
|||||||
return template?.nodes?.find((node) => node.id === BROWSER_VERIFICATION_STEP_NODE_ID)?.config ?? {};
|
return template?.nodes?.find((node) => node.id === BROWSER_VERIFICATION_STEP_NODE_ID)?.config ?? {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function columnTraitMatrix(ir: { columns: Array<{ id: string; traits: Array<{ trait: string; config?: unknown }> }> }): Array<{
|
||||||
|
id: string;
|
||||||
|
traits: Array<{ trait: string; config?: unknown }>;
|
||||||
|
}> {
|
||||||
|
return ir.columns.map((column) => ({ id: column.id, traits: column.traits }));
|
||||||
|
}
|
||||||
|
|
||||||
describe("built-in workflows", () => {
|
describe("built-in workflows", () => {
|
||||||
// Non-compiler built-ins model graph-only node kinds or reusable fragments the
|
// 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.
|
// linear compiler cannot lower to a step list. They still must parse as valid IR.
|
||||||
@@ -142,6 +155,103 @@ describe("built-in workflows", () => {
|
|||||||
expect(serializeWorkflowIr(coding!.ir)).toBe(serializeWorkflowIr(BUILTIN_CODING_WORKFLOW_IR));
|
expect(serializeWorkflowIr(coding!.ir)).toBe(serializeWorkflowIr(BUILTIN_CODING_WORKFLOW_IR));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("linear built-ins use the canonical trait-bearing default columns", () => {
|
||||||
|
expect(BUILTIN_CODING_WORKFLOW_IR.version).toBe("v2");
|
||||||
|
if (BUILTIN_CODING_WORKFLOW_IR.version !== "v2") throw new Error("expected coding v2");
|
||||||
|
const canonicalColumns = columnTraitMatrix(BUILTIN_CODING_WORKFLOW_IR);
|
||||||
|
|
||||||
|
for (const workflowId of LINEAR_BUILTIN_IDS) {
|
||||||
|
const workflow = getBuiltinWorkflow(workflowId);
|
||||||
|
expect(workflow, workflowId).toBeDefined();
|
||||||
|
const ir = parseWorkflowIr(workflow!.ir);
|
||||||
|
expect(ir.version, workflowId).toBe("v2");
|
||||||
|
if (ir.version !== "v2") throw new Error(`expected ${workflowId} v2`);
|
||||||
|
|
||||||
|
expect(columnTraitMatrix(ir), workflowId).toEqual(canonicalColumns);
|
||||||
|
const todo = ir.columns.find((column) => column.id === "todo");
|
||||||
|
expect(todo?.traits).toContainEqual({ trait: "hold", config: { release: "capacity" } });
|
||||||
|
expect(todo?.traits).toContainEqual({ trait: "reset-on-entry" });
|
||||||
|
expect(ir.columns.find((column) => column.id === "in-progress")?.traits.map((trait) => trait.trait)).toContain("wip");
|
||||||
|
expect(ir.columns.find((column) => column.id === "in-review")?.traits.map((trait) => trait.trait)).toContain("merge");
|
||||||
|
}
|
||||||
|
|
||||||
|
const quickFix = parseWorkflowIr(getBuiltinWorkflow("builtin:quick-fix")!.ir);
|
||||||
|
if (quickFix.version !== "v2") throw new Error("expected quick-fix v2");
|
||||||
|
expect(quickFix.nodes.find((node) => node.id === "execute")?.column).toBe("in-progress");
|
||||||
|
expect(quickFix.nodes.find((node) => node.id === "merge")?.column).toBe("in-review");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hand-authored built-in workflow columns stay on their authored trait sets", () => {
|
||||||
|
const expected = new Map([
|
||||||
|
[
|
||||||
|
"builtin:coding",
|
||||||
|
[
|
||||||
|
{ id: "triage", traits: ["intake"] },
|
||||||
|
{ id: "todo", traits: ["hold", "reset-on-entry"] },
|
||||||
|
{ id: "in-progress", traits: ["wip", "abort-on-exit", "timing"] },
|
||||||
|
{ id: "in-review", traits: ["merge-blocker", "human-review", "stall-detection", "merge"] },
|
||||||
|
{ id: "done", traits: ["complete"] },
|
||||||
|
{ id: "archived", traits: ["archived"] },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"builtin:marketing",
|
||||||
|
[
|
||||||
|
{ id: "ideation", traits: ["intake"] },
|
||||||
|
{ id: "backlog", traits: ["hold", "reset-on-entry"] },
|
||||||
|
{ id: "drafting", traits: ["wip", "abort-on-exit", "timing"] },
|
||||||
|
{ id: "editorial-review", traits: ["merge-blocker", "human-review", "stall-detection", "merge"] },
|
||||||
|
{ id: "published", traits: ["complete"] },
|
||||||
|
{ id: "archived", traits: ["archived"] },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"builtin:stepwise-coding",
|
||||||
|
[
|
||||||
|
{ id: "triage", traits: ["intake"] },
|
||||||
|
{ id: "todo", traits: ["hold", "reset-on-entry"] },
|
||||||
|
{ id: "in-progress", traits: ["wip", "abort-on-exit", "timing"] },
|
||||||
|
{ id: "in-review", traits: ["merge-blocker", "human-review", "stall-detection", "merge"] },
|
||||||
|
{ id: "done", traits: ["complete"] },
|
||||||
|
{ id: "archived", traits: ["archived"] },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"builtin:lead-generation",
|
||||||
|
[
|
||||||
|
{ id: "triage", traits: ["intake"] },
|
||||||
|
{ id: "sourcing", traits: ["timing"] },
|
||||||
|
{ id: "qualification", traits: ["wip", "timing"] },
|
||||||
|
{ id: "enrichment", traits: ["timing"] },
|
||||||
|
{ id: "outreach", traits: ["human-review", "stall-detection"] },
|
||||||
|
{ id: "converted", traits: ["complete"] },
|
||||||
|
{ id: "archived", traits: ["archived"] },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"builtin:pr-workflow",
|
||||||
|
[
|
||||||
|
{ id: "triage", traits: ["intake"] },
|
||||||
|
{ id: "in-progress", traits: ["wip", "timing"] },
|
||||||
|
{ id: "await-review", traits: ["merge-blocker", "stall-detection"] },
|
||||||
|
{ id: "done", traits: ["complete"] },
|
||||||
|
{ id: "archived", traits: ["archived"] },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (const [workflowId, expectedColumns] of expected) {
|
||||||
|
const workflow = getBuiltinWorkflow(workflowId)!;
|
||||||
|
const ir = parseWorkflowIr(workflow.ir);
|
||||||
|
expect(ir.version, workflowId).toBe("v2");
|
||||||
|
if (ir.version !== "v2") throw new Error(`expected ${workflowId} v2`);
|
||||||
|
expect(
|
||||||
|
ir.columns.map((column) => ({ id: column.id, traits: column.traits.map((trait) => trait.trait) })),
|
||||||
|
workflowId,
|
||||||
|
).toEqual(expectedColumns);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("builtin:coding catalog IR exposes canonical columns, placements, and settings", () => {
|
it("builtin:coding catalog IR exposes canonical columns, placements, and settings", () => {
|
||||||
const coding = getBuiltinWorkflow("builtin:coding")!;
|
const coding = getBuiltinWorkflow("builtin:coding")!;
|
||||||
const ir = parseWorkflowIr(coding.ir);
|
const ir = parseWorkflowIr(coding.ir);
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { BUILTIN_STEPWISE_CODING_WORKFLOW_IR } from "./builtin-stepwise-coding-w
|
|||||||
import { BUILTIN_WORKFLOW_SETTINGS } from "./builtin-workflow-settings.js";
|
import { BUILTIN_WORKFLOW_SETTINGS } from "./builtin-workflow-settings.js";
|
||||||
import { builtinPromptConfig } from "./builtin-workflow-prompts.js";
|
import { builtinPromptConfig } from "./builtin-workflow-prompts.js";
|
||||||
import type { WorkflowDefinition } from "./workflow-definition-types.js";
|
import type { WorkflowDefinition } from "./workflow-definition-types.js";
|
||||||
import type { WorkflowIr, WorkflowIrNode } from "./workflow-ir-types.js";
|
import type { WorkflowIr, WorkflowIrColumn, WorkflowIrNode } from "./workflow-ir-types.js";
|
||||||
import { DEFAULT_WORKFLOW_COLUMN_IDS, parseWorkflowIr } from "./workflow-ir.js";
|
import { parseWorkflowIr } from "./workflow-ir.js";
|
||||||
|
|
||||||
/** Prefix marking a workflow as a read-only built-in template. */
|
/** Prefix marking a workflow as a read-only built-in template. */
|
||||||
export const BUILTIN_WORKFLOW_ID_PREFIX = "builtin:";
|
export const BUILTIN_WORKFLOW_ID_PREFIX = "builtin:";
|
||||||
@@ -51,8 +51,6 @@ interface BuiltinSpec {
|
|||||||
nodes: Array<{ id: string; kind: WorkflowIr["nodes"][number]["kind"]; config?: Record<string, unknown> }>;
|
nodes: Array<{ id: string; kind: WorkflowIr["nodes"][number]["kind"]; config?: Record<string, unknown> }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const V1_LINEAR_NODE_KINDS = new Set<WorkflowIrNode["kind"]>(["start", "prompt", "script", "gate", "end"]);
|
|
||||||
|
|
||||||
function defaultColumnForLinearNode(node: WorkflowIrNode): string {
|
function defaultColumnForLinearNode(node: WorkflowIrNode): string {
|
||||||
const seam = node.config?.seam;
|
const seam = node.config?.seam;
|
||||||
if (seam === "execute") return "in-progress";
|
if (seam === "execute") return "in-progress";
|
||||||
@@ -61,6 +59,19 @@ function defaultColumnForLinearNode(node: WorkflowIrNode): string {
|
|||||||
return "todo";
|
return "todo";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canonicalBuiltinWorkflowColumns(): WorkflowIrColumn[] {
|
||||||
|
if (BUILTIN_CODING_WORKFLOW_IR.version !== "v2") {
|
||||||
|
throw new Error("builtin coding workflow must be v2 to provide canonical columns");
|
||||||
|
}
|
||||||
|
return BUILTIN_CODING_WORKFLOW_IR.columns.map((column) => ({
|
||||||
|
...column,
|
||||||
|
traits: column.traits.map((trait) => ({
|
||||||
|
...trait,
|
||||||
|
config: trait.config ? { ...trait.config } : undefined,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/** Build a linear IR (start → nodes… → end) with simple x-spaced layout. */
|
/** Build a linear IR (start → nodes… → end) with simple x-spaced layout. */
|
||||||
function linear(spec: BuiltinSpec): WorkflowDefinition {
|
function linear(spec: BuiltinSpec): WorkflowDefinition {
|
||||||
const nodes: WorkflowIr["nodes"] = [
|
const nodes: WorkflowIr["nodes"] = [
|
||||||
@@ -82,20 +93,23 @@ function linear(spec: BuiltinSpec): WorkflowDefinition {
|
|||||||
nodes.forEach((node, i) => {
|
nodes.forEach((node, i) => {
|
||||||
layout[node.id] = { x: 60 + i * 170, y: 160 };
|
layout[node.id] = { x: 60 + i * 170, y: 160 };
|
||||||
});
|
});
|
||||||
const hasV2OnlyNode = nodes.some((node) => !V1_LINEAR_NODE_KINDS.has(node.kind));
|
/*
|
||||||
const ir = hasV2OnlyNode
|
* FNXC:Workflows 2026-06-28-00:00:
|
||||||
? parseWorkflowIr({
|
* Linear built-ins must mirror BUILTIN_CODING_WORKFLOW_IR column traits because the post-cutover hold/release sweep is the only todo→in-progress dispatcher. Both formerly-v1 linear graphs (quick-fix, review-heavy, design) and v2-only compound-engineering need todo hold(capacity), in-progress wip, and in-review merge traits or their cards strand in Todo.
|
||||||
version: "v2",
|
*/
|
||||||
name: spec.name,
|
const ir = parseWorkflowIr({
|
||||||
columns: DEFAULT_WORKFLOW_COLUMN_IDS.map((id) => ({ id, name: id, traits: [] })),
|
version: "v2",
|
||||||
nodes: nodes.map((node) => (node.column ? node : { ...node, column: defaultColumnForLinearNode(node) })),
|
name: spec.name,
|
||||||
edges,
|
columns: canonicalBuiltinWorkflowColumns(),
|
||||||
})
|
nodes: nodes.map((node) => (node.column ? node : { ...node, column: defaultColumnForLinearNode(node) })),
|
||||||
: parseWorkflowIr({ version: "v1", name: spec.name, nodes, edges });
|
edges,
|
||||||
|
});
|
||||||
|
if (ir.version !== "v2" || !ir.columns.find((column) => column.id === "todo")?.traits.some((trait) => trait.trait === "hold")) {
|
||||||
|
throw new Error(`linear built-in workflow '${spec.id}' must synthesize a hold-capacity todo column`);
|
||||||
|
}
|
||||||
// Attach the moved-key settings catalog (U1/U3, R4) so every built-in workflow
|
// Attach the moved-key settings catalog (U1/U3, R4) so every built-in workflow
|
||||||
// carries its declarations through the resolver path (resolveWorkflowIrById →
|
// carries its declarations through the resolver path (resolveWorkflowIrById →
|
||||||
// resolveEffectiveSettings). v1 graphs upgrade to v2 on parse, so the parsed IR
|
// resolveEffectiveSettings). Defaults are byte-equal to legacy
|
||||||
// is v2 and can carry `settings`. Defaults are byte-equal to legacy
|
|
||||||
// DEFAULT_PROJECT_SETTINGS literals, so this is behavior-inert.
|
// DEFAULT_PROJECT_SETTINGS literals, so this is behavior-inert.
|
||||||
if (ir.version === "v2") {
|
if (ir.version === "v2") {
|
||||||
ir.settings = BUILTIN_WORKFLOW_SETTINGS;
|
ir.settings = BUILTIN_WORKFLOW_SETTINGS;
|
||||||
|
|||||||
@@ -63,6 +63,12 @@ function setTransitionPending(store: TaskStore, taskId: string, toColumn: string
|
|||||||
}
|
}
|
||||||
|
|
||||||
const noReserveDeps: HoldReleaseDeps = { now: () => Date.now() };
|
const noReserveDeps: HoldReleaseDeps = { now: () => Date.now() };
|
||||||
|
const LINEAR_BUILTIN_WORKFLOW_IDS = [
|
||||||
|
"builtin:compound-engineering",
|
||||||
|
"builtin:quick-fix",
|
||||||
|
"builtin:review-heavy",
|
||||||
|
"builtin:design",
|
||||||
|
] as const;
|
||||||
|
|
||||||
describe("hold-release sweep (U6)", () => {
|
describe("hold-release sweep (U6)", () => {
|
||||||
let rootDir = "";
|
let rootDir = "";
|
||||||
@@ -96,6 +102,28 @@ describe("hold-release sweep (U6)", () => {
|
|||||||
return task.id;
|
return task.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it("releases default and linear built-in workflow todo cards into in-progress", async () => {
|
||||||
|
await store.updateSettings({ maxConcurrent: 10 } as Parameters<typeof store.updateSettings>[0]);
|
||||||
|
const defaultWorkflowTask = await seedTodoCard();
|
||||||
|
const selectedTasks: string[] = [];
|
||||||
|
|
||||||
|
// Pre-fix, linear() synthesized trait-less default columns, so isHeldTask()
|
||||||
|
// returned false here and these selected tasks were silently skipped forever.
|
||||||
|
for (const workflowId of LINEAR_BUILTIN_WORKFLOW_IDS) {
|
||||||
|
const task = await store.createTask({ description: `card ${workflowId}` });
|
||||||
|
setSelection(store, task.id, workflowId);
|
||||||
|
setColumn(store, task.id, "todo");
|
||||||
|
selectedTasks.push(task.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await runHoldReleaseSweep(store, noReserveDeps);
|
||||||
|
expect(result.released).toEqual(expect.arrayContaining([defaultWorkflowTask, ...selectedTasks]));
|
||||||
|
|
||||||
|
for (const taskId of [defaultWorkflowTask, ...selectedTasks]) {
|
||||||
|
expect((await store.getTask(taskId))?.column).toBe("in-progress");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("ignores stale workflowColumns=false and still releases held default-workflow cards", async () => {
|
it("ignores stale workflowColumns=false and still releases held default-workflow cards", async () => {
|
||||||
await store.updateGlobalSettings({ experimentalFeatures: { workflowColumns: false } });
|
await store.updateGlobalSettings({ experimentalFeatures: { workflowColumns: false } });
|
||||||
const id = await seedTodoCard();
|
const id = await seedTodoCard();
|
||||||
|
|||||||
Reference in New Issue
Block a user