fix(FN-7591): allow moving tasks out of custom workflow columns (Coding (Ideas))
Moving a card out of a non-legacy workflow column — e.g. Coding (Ideas) "ideas" → "todo" — was rejected with "Invalid transition: 'ideas' → 'todo'. Valid targets: none". Workflow columns graduated to always-on but moveTaskInternal's compat-flag legacy branch (the default path, since no experimental flag is emitted) validated every move against the legacy VALID_TRANSITIONS table, which is keyed only by the built-in column ids. Default-workflow moves survived by coincidence (its ids ARE the legacy ids); a task in a custom column had no key so every move was rejected. The legacy branch now resolves a non-legacy source column's targets from the task's own workflow adjacency (resolveAllowedColumns), while keeping the legacy bare-Error contract intact for legacy columns (transition-parity / characterization suites unchanged). Adds a regression test covering the ideas -> todo -> in-progress -> in-review chain and non-adjacent rejection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
7
.changeset/coding-ideas-custom-column-move.md
Normal file
7
.changeset/coding-ideas-custom-column-move.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Fix "Invalid transition" error when moving cards out of a custom workflow column like Coding (Ideas) → Ideas.
|
||||||
|
category: fix
|
||||||
|
dev: moveTaskInternal's compat-flag legacy path validated moves against the legacy VALID_TRANSITIONS table, which is keyed only by the built-in column ids; a task in a non-legacy workflow column (e.g. "ideas") had no key and every move was rejected. The legacy branch now resolves a non-legacy source column's targets from the task's own workflow adjacency (resolveAllowedColumns) while preserving the legacy bare-Error contract for legacy columns.
|
||||||
59
packages/core/src/__tests__/coding-ideas-move.test.ts
Normal file
59
packages/core/src/__tests__/coding-ideas-move.test.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
|
import { createTaskStoreTestHarness } from "./store-test-helpers.js";
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:WorkflowColumns 2026-07-05-19:10:
|
||||||
|
Regression for the disappearing move on custom workflow columns. Workflow columns graduated to
|
||||||
|
always-on (no experimental flag emitted), but moveTaskInternal still gated its workflow path on the
|
||||||
|
retired strict compatibility flag, so it fell back to the legacy VALID_TRANSITIONS table — which is
|
||||||
|
keyed only by the legacy column ids. A task in a non-legacy column (Coding (Ideas) → "ideas") could
|
||||||
|
not move: "Invalid transition: 'ideas' → 'todo'. Valid targets: none".
|
||||||
|
|
||||||
|
Surface enumeration (invariant: a move is validated by the task's WORKFLOW adjacency, not the legacy
|
||||||
|
table, on a default project with NO experimental flag set):
|
||||||
|
- Custom intake column forward move: ideas → todo is allowed.
|
||||||
|
- Full custom-column chain onward: todo → in-progress → in-review all succeed (→ done is gated by the
|
||||||
|
workflow's own merge trait, which is orthogonal to transition adjacency and excluded here).
|
||||||
|
- Non-adjacent move still rejects with the workflow's targets (ideas → in-progress).
|
||||||
|
- Holds for both user- and engine-sourced moves.
|
||||||
|
- Default workflow (legacy column ids) is unchanged (parity), verified in move-task-characterization.
|
||||||
|
*/
|
||||||
|
describe("Coding (Ideas) custom-column moves (workflow-columns graduation)", () => {
|
||||||
|
const harness = createTaskStoreTestHarness();
|
||||||
|
beforeEach(harness.beforeEach);
|
||||||
|
afterEach(harness.afterEach);
|
||||||
|
|
||||||
|
it("moves an ideas-workflow task from the ideas intake column to todo", async () => {
|
||||||
|
const store = harness.store();
|
||||||
|
const task = await store.createTask({ description: "idea", workflowId: "builtin:coding-ideas" });
|
||||||
|
expect(task.column).toBe("ideas");
|
||||||
|
|
||||||
|
const moved = await store.moveTask(task.id, "todo", { moveSource: "user" });
|
||||||
|
expect(moved.column).toBe("todo");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("advances an ideas task along the Coding (Ideas) custom-column chain", async () => {
|
||||||
|
const store = harness.store();
|
||||||
|
const task = await store.createTask({ description: "idea", workflowId: "builtin:coding-ideas" });
|
||||||
|
|
||||||
|
await store.moveTask(task.id, "todo", { moveSource: "user" });
|
||||||
|
await store.moveTask(task.id, "in-progress", { moveSource: "user" });
|
||||||
|
const inReview = await store.moveTask(task.id, "in-review", { moveSource: "user", allowDirectInReviewMove: true });
|
||||||
|
expect(inReview.column).toBe("in-review");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still rejects a non-adjacent move out of the ideas column", async () => {
|
||||||
|
const store = harness.store();
|
||||||
|
const task = await store.createTask({ description: "idea", workflowId: "builtin:coding-ideas" });
|
||||||
|
await expect(
|
||||||
|
store.moveTask(task.id, "in-progress", { moveSource: "user" }),
|
||||||
|
).rejects.toThrow(/Invalid transition: 'ideas' → 'in-progress'/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows the ideas → todo move from an engine source too", async () => {
|
||||||
|
const store = harness.store();
|
||||||
|
const task = await store.createTask({ description: "idea", workflowId: "builtin:coding-ideas" });
|
||||||
|
const moved = await store.moveTask(task.id, "todo", { moveSource: "engine" });
|
||||||
|
expect(moved.column).toBe("todo");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7676,11 +7676,22 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS}
|
|||||||
!sourceIsLegacy &&
|
!sourceIsLegacy &&
|
||||||
(COLUMNS as readonly string[]).includes(toColumn);
|
(COLUMNS as readonly string[]).includes(toColumn);
|
||||||
if (!isEvacuation) {
|
if (!isEvacuation) {
|
||||||
// Legacy flag-OFF branch (useWorkflow === false): both columns are
|
/*
|
||||||
// guaranteed legacy ids here — a non-legacy `toColumn` returns `?? []`
|
FNXC:WorkflowColumns 2026-07-05-19:30:
|
||||||
// and rejects below, and flag-OFF tasks never hold custom column ids.
|
Workflow columns graduated to always-on (no experimental flag emitted), so this "flag-OFF"
|
||||||
// The `as Column` is provably safe within this branch (#1403).
|
branch is the DEFAULT move path for nearly every project — the strict compat flag reads false
|
||||||
const validTargets = VALID_TRANSITIONS[task.column as Column] ?? [];
|
because nothing sets it. Legacy columns (triage/todo/in-progress/in-review/done/archived) are
|
||||||
|
validated verbatim by VALID_TRANSITIONS, preserving the legacy bare-Error contract. But a task
|
||||||
|
can legitimately sit in a NON-legacy workflow column now (e.g. Coding (Ideas) → "ideas"), which
|
||||||
|
VALID_TRANSITIONS cannot key — the old code returned `?? []` and rejected EVERY move out of it
|
||||||
|
("Invalid transition: 'ideas' → 'todo'. Valid targets: none"). Resolve a non-legacy source
|
||||||
|
column's targets from the task's own workflow adjacency instead, still throwing the same
|
||||||
|
legacy-style bare Error (not TransitionRejectionError) so the flag-OFF characterization contract
|
||||||
|
holds for legacy columns.
|
||||||
|
*/
|
||||||
|
const validTargets = sourceIsLegacy
|
||||||
|
? (VALID_TRANSITIONS[task.column as Column] ?? [])
|
||||||
|
: resolveAllowedColumns(this.resolveTaskWorkflowIrSync(id), task.column);
|
||||||
if (!validTargets.includes(toColumn as Column)) {
|
if (!validTargets.includes(toColumn as Column)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Invalid transition: '${task.column}' → '${toColumn}'. ` +
|
`Invalid transition: '${task.column}' → '${toColumn}'. ` +
|
||||||
|
|||||||
Reference in New Issue
Block a user