fix(cli): the node-override guard never fired on a renamed board, so mid-flight changes were allowed (#3019)
## The node-override guard never fired on a renamed board
`fn_task_update` called the guard with no options:
```ts
const validation = validateNodeOverrideChange(task, normalizedNodeId ?? null);
```
so `wipColumns` fell back to its documented default of
`{"in-progress"}`. On a board whose WIP lane is named anything else,
`wipColumns.has(task.column)` is false, the mid-flight check passes, and
**an operator can change the node override on a running task** —
precisely what that guard exists to refuse, in its own words:
> "Is this task executing right now?" — keyed on the literal, a renamed
board let an operator change the node override MID-FLIGHT on a running
task, which is exactly what this guard exists to refuse.
That note is attached to the `wipColumns` option added for this purpose.
The CLI simply never passed it.
## Two assumptions in the guard's own docs that did not hold
```
Both callers supply them. An omitted set keeps the legacy id, which is what a caller
without cheap IR access (a CLI tool, a route with only a task row) still gets.
```
1. **"Both callers"** — this is a *third* one, and it was in
`check-lane-wiring`'s known-unwired baseline the whole time.
2. **"a CLI tool … without cheap IR access"** — this handler is async
and has already awaited `store.getTask`, so one more resolve costs
exactly what `resolveTaskLifecycleColumns` already costs elsewhere **in
this same file** (the linked-lineage label at ~1239). The assumption was
reasonable in general and wrong here.
Passed present-but-conditionally-valued rather than as a conditional
argument: an omitted set still keeps the documented legacy default, and
only that shape is visible to `lane-wiring-census`, which matches an
object-literal argument and cannot see a ternary.
## Coverage — stated rather than implied
**There is no new unit test.** The regression guard is the ratchet
itself, and it is a real revert-proof: with the wiring removed,
```
[check-lane-wiring] call sites not passing a resolved lane argument INCREASED:
packages/cli/src/extension.ts: 1 unwired now, baseline allows 0
```
Verified by actually reverting it, not by assuming. Baseline re-recorded
19 → 18 in the same commit, so the allowance cannot be regrown into.
A behavioural test would need a custom workflow definition persisted
*and* selected inside the integration harness to get a card resting in a
renamed WIP lane. That is worth doing and I would take it as follow-up
harness work — but it is not part of this fix, and I would rather name
the gap than let "85 passed" imply coverage I did not write.
## Verification (measured)
- **85 passed** across `extension.test.ts`,
`extension-experiment-finalize.test.ts`,
`task-list-board-columns.test.ts`
- `tsc --noEmit`, `eslint` — clean
- `check-lane-wiring` (18, none added), `lifecycle-column-census
--strict`, `check-inert-flag-seams`, `check-fnxc-future-dates`,
`check:changesets` — green
Changeset included (`patch`): `packages/cli` is the published
`@runfusion/fusion` and this changes guard behaviour operators rely on.
This commit is contained in:
7
.changeset/cli-node-override-wip-lane.md
Normal file
7
.changeset/cli-node-override-wip-lane.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Fix the node-override guard not blocking mid-flight changes on boards with a renamed WIP lane.
|
||||
category: fix
|
||||
dev: `fn_task_update` called `validateNodeOverrideChange` without options, so `wipColumns` fell back to the literal `{"in-progress"}` and the running-task check never fired on a renamed board. It now resolves the task's own WIP and COMPLETE lanes via `resolveTaskLifecycleColumns`.
|
||||
@@ -1819,7 +1819,30 @@ export default function kbExtension(pi: ExtensionAPI) {
|
||||
store.updateTask all exhibit identical behavior.
|
||||
*/
|
||||
const normalizedNodeId = normalizeNullableStringInput(params.nodeId);
|
||||
const validation = validateNodeOverrideChange(task, normalizedNodeId ?? null);
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-12:20:
|
||||
Supply this task's resolved WIP and COMPLETE lanes — without them the guard does not fire.
|
||||
|
||||
`validateNodeOverrideChange` defaults `wipColumns` to `{"in-progress"}`, so on a board whose
|
||||
WIP lane is named anything else `wipColumns.has(task.column)` is false and the mid-flight
|
||||
check passes. An operator could then change the node override on a RUNNING task, which is
|
||||
precisely what that guard exists to refuse (see its own note in node-override-guard.ts).
|
||||
|
||||
The guard's options doc says "Both callers supply them" and assumes a CLI tool has no cheap
|
||||
IR access. Neither held here: this is a third caller, and it is an async handler that has
|
||||
already awaited `store.getTask`, so one more resolve is the same cost `resolveTaskLifecycleColumns`
|
||||
is already paid for elsewhere in this file (the linked-lineage label at ~1239).
|
||||
|
||||
Passed as present-but-conditionally-valued rather than a conditional argument: an omitted
|
||||
set keeps the documented legacy default, and only this shape is visible to
|
||||
scripts/lib/lane-wiring-census.mjs, which matches an object-literal argument and cannot see a
|
||||
ternary. This site was a known-unwired entry in that gate's baseline.
|
||||
*/
|
||||
const nodeOverrideLifecycle = await resolveTaskLifecycleColumns(store, task.id);
|
||||
const validation = validateNodeOverrideChange(task, normalizedNodeId ?? null, {
|
||||
wipColumns: nodeOverrideLifecycle?.wip ? new Set([nodeOverrideLifecycle.wip]) : undefined,
|
||||
completeColumns: nodeOverrideLifecycle?.complete ? new Set([nodeOverrideLifecycle.complete]) : undefined,
|
||||
});
|
||||
if (!validation.allowed) {
|
||||
return {
|
||||
content: [{ type: "text", text: validation.message ?? "Node override change blocked" }],
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
"packages/dashboard/app/hooks/useBlockerFanout.ts": 1,
|
||||
"packages/cli/src/commands/dashboard-tui/app.tsx": 1,
|
||||
"packages/cli/src/commands/dashboard-tui/bucket-mapping.ts": 1,
|
||||
"packages/cli/src/extension.ts": 1,
|
||||
"plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.tsx": 1,
|
||||
"plugins/fusion-plugin-even-realities-glasses/src/routes/board-routes.ts": 1
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user