fix(assignments): settle an assignment save on its own generation counter (#3017) - #3041
Conversation
…terrupts a save A background board refetch that flips readOnly back to writable runs load(), which shares CardAssignmentField's single generation counter with save(). The in-flight PUT's finally then no-ops, so saving stays true, locked stays true and the saving-change the card editor reads to refuse every close affordance stays latched at true for the life of the mount. Both new specs fail against current main. Refs #3017
…nter CardAssignmentField shared one generation counter between load() and save(). The readOnly watcher is reachable while a PUT is unanswered - a background board refetch that re-reports write permission mid-save runs load() - and that bumped the shared counter, so the save's finally no-opped. saving stayed true, locked stayed true, and the saving-change the card editor reads to refuse every close affordance (#2977/#2981) stayed latched at true until navigation remounted the field, trapping the user in the editor. Split the counter per request kind. Each request still rejects its own stale body - a superseded save, a read left behind by a newer read - and the card-identity watcher and unmount bump both, so a receipt for a card the editor has left is still dropped. A read cannot invalidate a write it does not contend with: a non-refreshing load() only replaces the participant list, and the refreshing one is unreachable while saving holds. The #2982 permission lock, its release rules and load()'s stale-response rejection are unchanged. Closes #3017
…ew spec The spread of the Record-typed mock store loses its index signature through reactive(), so vue-tsc rejected the currentBoardCards read. Refs #3017
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…nds after it Review of #3041 found the mirror of the #3017 overlap: with load() and save() on separate counters, the save's catch body now runs, so a reload that settles AFTER a refused PUT could clear the needsRefresh the refusal had just set. That withdrew the Refresh current assignments button while the alert still told the user to press it, and re-enabled Save against the version the 409 invalidated. load() clears saveFailure before it starts, so anything there when its body runs came from a save that settled during it. Yield needsRefresh in that case. The spec fails without the guard. Also correct the counter comment: the read body the split makes safe is the refreshing branch, which is unreachable while saving holds - not "only the participant list" - and name needsRefresh as the one shared piece of state. Refs #3017
Review triage (one fresh-context adversarial pass, head
|
Base change: #3032 landed, re-proved against it (head
|
Closes #3017
Summary
#3017 was an unverified, source-traced hypothesis. It reproduces. Both new specs fail against
mainand pass with the fix.CardAssignmentFieldshared onegenerationcounter betweenload()andsave().load()bumps it at its start;
save()'sfinallyclearedsavingonly when its ownrequeststillequalled it. The
readOnlywatcher callsload()whenever the parent reports write permissionagain, and that watcher is reachable while a PUT is unanswered — a background board refetch that
re-reports the board mid-save is exactly the trigger the issue names. The save's
finallythenno-opped:
savingstayedtruefor the life of the mount, so theSaving assignments… this change was sent and cannot be discarded.status never cleared;lockedstayedtrue, so the participant selector and Save assignments stayed dead;saving-changeemit stayed latched attrue, and since Add multiple card assignments and explicit import mapping #2977/Prevent assignment discard from misrepresenting an in-flight save #2981CardModalrefusesevery close affordance while an assignment save is reported in flight — header close,
Escape, the Escape stack, the backdrop, the thinking-deck link and archive recovery — so the
editor became un-closable until navigation remounted it;
baselinewas never updated and the field stayed dirty,which keeps the host's own save and archive disabled and raises a discard prompt on every close
path.
Fix
One counter per request kind (
loadGeneration,saveGeneration) — the "separate savegeneration" option in the acceptance. A read cannot invalidate a write it does not contend with:
a non-refreshing
load()only replaces the participant list, and the refreshing one isunreachable while
savingholds (the Refresh button andsave()'s own guard are both disabled byloading/saving). Each counter still rejects its own stale bodies — a superseded save, a readleft behind by a newer read — and the card-identity watcher and
onBeforeUnmountbump both,so a receipt for a card the editor has left is still dropped.
Unchanged on purpose:
load()'s stale-response rejection, the #2982 403 permission lock, itssticky-class rules and its
readOnly-transition release, and the three-gatebusy/locked/draftLockedsplit.Scope
CardAssignmentField.vueand its spec, plus oneintegration assertion in
CardModalAssignmentSave.spec.ts.CardModal.vueis not touched — its assignmentread-onlyexpression belongs to PR fix(cards): one server-authoritative permission read for every card editor write gate #3032([Frontend][Cards] Parent, archive and assignment gates still read an omitted board canWrite as read-only while the type selector resolves it server-side (from PR #3026 review) #3028). The new modal spec drives the flip through a reactive board-store stub instead, so it
reads that expression without editing it.
answer) are untouched neighbouring slices.
Verification
Run in the worktree at head
87c6203f8, fromfrontend/taskdeck-web:npx vitest --run --maxWorkers=2 src/tests/components/CardAssignmentField.spec.ts src/tests/components/CardModalAssignmentSave.spec.ts src/tests/components/CardModal.spec.ts— 3 files, 81 tests passed.CardAssignmentField.spec.ts— 2 failed / 15 passed; both failuresexpected [ true ] to deeply equal [ false ]on the lastsaving-changepayload.CardModalAssignmentSave.spec.ts— 1 failed / 10 passed; the save-pending notice(
card-assignment-save-pending-dismiss, "Keep editing") was still on screen after the PUTsettled, i.e. the editor was still refusing to close.
npm run typecheck(vue-tsc -b) — clean.npm run build—947 modules transformed,built in 3.28s.NOT run:
tests/e2e/card-assignments.spec.ts(Playwright, needs a running stack); the rest ofthe frontend unit suite (bare
vitest --runOOMs on this box, perCLAUDE.md); anything backend —no backend file is touched.
Docs: no
docs/STATUS.mdchange.STATUS.mdrecords assignments (#2240) as not shipped, sothis fixes a defect in an unshipped surface rather than changing shipped reality — the same call
the #2981/#2982 commits on this component made.