fix(kanban): enforce terminal/owner guards — closes #137 #4
No reviewers
Labels
No labels
blocked:verify-failed
bug
in-progress
triage-approved
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Mark/kanban!4
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "137-kanban-guards-and-watcher-branches"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements the four-guard state model spec (plan §1, Vikunja #137) on Mark/kanban. Every rule is enforced in code with explicit guards — no convention-only enforcement.
Rule-to-guard mapping
donestate setsterminal=true; transitions out ofdoneare rejected at the guard layer regardless of sourceserver/transition_guards.ts(computeFlagUpdatesForTransition, line 91 setsterminal: 1on done entry; line 132 rejects done→X withkind: done_is_terminal); wired intoserver/storage.ts::transitionTask(line 290 atomic flag update); surfaced as 409 byserver/routes.tsfour_guards_integration.test.ts:142— 2 testsblockedcards only return to active viakanban_unblock; nothing else flips them backserver/watcher.ts::branch_blocked(line 150 — unsubscribe, only re-add onkanban_unblockevent indeliverKanbanEvent, line 287); storage mirror atserver/storage.ts::transitionTask(line 236 — rejectsblocked→activewith typed error referencingkanbanUnblock); sole handler atserver/storage.ts::kanbanUnblock(line 55 — only callable surface that flips blocked→assigned)four_guards_integration.test.ts:251— 3 testsrunningis owned exclusively by the watcher; no other actor may transition a running cardserver/transition_guards.ts::computeFlagUpdatesForTransition(line 92 — setsownerto caller on running entry);server/transition_guards.ts::assertCallerAllowedForRunning(rejects non-watcher withkind: foreign_running_write, missing/null caller withkind: missing_caller_for_running);server/watcher.ts::branch_running(line 170 — assertion at tick entry:card.owner !== WATCHER_PRINCIPALthrowsWatcherError{kind: foreign_running_write}, sweep catches per-card); HTTP trust boundary atserver/principal.ts::deriveCallerFromRequest(body-supplied caller ignored, server-derived fromX-Internal-Actorheader, closed set:watcheronly)four_guards_integration.test.ts:382— 5 testsserver/watcher.ts::branch_done(line 131 — drop_done action;_watchSetmutation only at line 181 [legit running dispatch] and line 287 [kanban_unblockarm]); terminal flag is the persistence signal checked at sweep time;KANBAN_EVENT_CATALOGexposes no event that re-adds a done cardfour_guards_integration.test.ts:552— 3 testsSupporting tests
Beyond the four named integration tests above, the diff also includes:
server/__tests__/transition_guards.test.ts— 23 pure-logic guard testsserver/__tests__/principal.test.ts— 16 tests for the HTTP trust-boundary helper (forged-but-lies, missing header, header smuggling, closed-set rejection)server/__tests__/routes.test.ts— 21 tests with caller/owner flow including HTTP surface (409 vs 400 precedence)server/__tests__/storage.test.ts— 35 tests incl. legacy-DB migrationTotal: 157 tests, all passing on branch HEAD
a0bc081d9a53d77d9a1e294ab275ef64864c377f.Diff stats vs.
origin/main13 files changed, 3612 insertions(+), 22 deletions(-).
shared/schema.ts—terminal(integer, default 0) +owner(text, nullable) columns ontasks;callerfield dropped frominsertTransitionSchema(body field silently stripped at the zod layer)migrations/0001_add_terminal_owner.sql— schema migration with safe defaultsscripts/run-migrations.ts— migrations runnerserver/transition_guards.ts(new) — pure-logic guard module with typedTransitionGuardError{kind:…}server/principal.ts(new) — server-derived caller trust boundary (deriveCallerFromRequest,deriveOwnerFromRequest)server/storage.ts—transitionTaskruns the guard before ALLOWED_TRANSITIONS;kanbanUnblock()storage method (sole handler for blocked→assigned);setTaskFlags()for non-transition flag mutationserver/routes.ts—X-Internal-Actorheader → caller derivation; PATCH/flagsroute; 409 surface for guard errorsserver/watcher.ts(new) — in-processWatcherclass withSweep()three-branch logic +deliverKanbanEvent()catalogserver/__tests__/four_guards_integration.test.ts(new) — 17 tests across 5 describe blocks covering the four named cases (a)(b)(c)(d) plus (e) owner-assertion failing-input surfaceAcceptance
kanban_unblockis the only path out ofblocked(storage-layer mirror + watcher catalog + HTTP route all agree)doneverified by test (5 sweeps, no re-subscribe on a done card)caller=watcher(no header) reaches storage ascaller=unknownand is rejectedCloses #137 (Vikunja issue tracker). Tracked locally as Mark/kanban#5.
Refs: kanban t_d3f147f9, t_e49638bb, t_002084ad, t_c657a9cc, t_383734ad, t_fc944a5a.
Pull request closed