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 &&
|
||||
(COLUMNS as readonly string[]).includes(toColumn);
|
||||
if (!isEvacuation) {
|
||||
// Legacy flag-OFF branch (useWorkflow === false): both columns are
|
||||
// guaranteed legacy ids here — a non-legacy `toColumn` returns `?? []`
|
||||
// and rejects below, and flag-OFF tasks never hold custom column ids.
|
||||
// The `as Column` is provably safe within this branch (#1403).
|
||||
const validTargets = VALID_TRANSITIONS[task.column as Column] ?? [];
|
||||
/*
|
||||
FNXC:WorkflowColumns 2026-07-05-19:30:
|
||||
Workflow columns graduated to always-on (no experimental flag emitted), so this "flag-OFF"
|
||||
branch is the DEFAULT move path for nearly every project — the strict compat flag reads false
|
||||
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)) {
|
||||
throw new Error(
|
||||
`Invalid transition: '${task.column}' → '${toColumn}'. ` +
|
||||
|
||||
Reference in New Issue
Block a user