Bookdrop: dispatch metadatacopied on save-success, not optimistically (bookshelf-v3bi0) #1435
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "bd-bookshelf-v3bi0"
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?
Summary
Code-review MINOR follow-up on merged PR #1424.
_autoSaveinbookdrop_file_editor_controller.jsdispatched thebookdrop:metadatacopiedCustomEvent unconditionally right after firing thesave request, so the review controller auto-checked the row's checkbox even
when the underlying
_doSave()POST later failed — the row read "selected"though the copy was never persisted server-side.
Fix:
_doSavenow accepts adispatchCopyEventflag (set by_autoSave,threaded through the in-flight-retry path) and only fires
bookdrop:metadatacopiedfrom_onSaveSuccess— i.e. only after the saverequest actually resolves
ok: true. On failure (non-ok response or networkerror) the event is never dispatched, so the row is never left auto-checked
for an unpersisted copy.
Test plan
AUTO-SAVE: bookdrop:metadatacopied dispatchVitest describe block:METADATA COPIED EVENTspecs (bookshelf-j8pvj) tomock a resolving
fetchand await the save's promise chain, since dispatchis no longer synchronous.
npm run coverage— 4752 tests pass, 100% statements/branches/functions/lines.Closes bead bookshelf-v3bi0 on merge.
Adversarial security review of PR #1435 (bookshelf-v3bi0).
Scope:
static/js/controllers/bookdrop_file_editor_controller.js+ its Vitest spec only — thebookdrop:metadatacopiedCustomEvent now dispatches on save success instead of optimistically.Findings:
[MINOR] static/js/controllers/bookdrop_file_editor_controller.js:433 —
_pendingDispatchCopyis a per-controller instance flag, not per-fieldWhen a copy-triggered save collides with an in-flight save and a second
_doSavecall queues behind it (_savePendingAfterInflight),_pendingDispatchCopyis OR'd across whichever calls queued in that window (ctrl._pendingDispatchCopy || !!dispatchCopyEvent). This is a UI-correctness nit (the review row could auto-check slightly earlier/later than the exact field that succeeded), not a security issue — no cross-user or cross-proposal leak, sinceproposalIdValueon the dispatched event is scoped to the mounted controller's own row. No fix required before merge; flag only if the review-row auto-check behavior is later found flaky under rapid-copy usage.Security assessment:
innerHTMLtouched; the eventdetail.proposalIdis echoed from an already-renderedproposalIdValueback into a same-page CustomEvent, never re-inserted into markup.REVIEW VERDICT: 0 blocker, 0 major, 1 minor
Adversarial review of PR #1435 (bookshelf-v3bi0).
[MAJOR] static/js/controllers/bookdrop_file_editor_controller.js:489-495 — network-error retry path drops/leaks the queued dispatchCopyEvent flag, defeating the PR's own guarantee in a race
The
.catch()branch (network error) does not mirror the.then()branch's flag handling. Compare:.then()(lines 480-484, correct):.catch()(lines 489-495, buggy):Two concrete failure modes from this one line:
Lost dispatch (silent regression of the review workflow). Scenario:
_doSave(true)(a copy-triggered save) is in flight; while it's pending, another_doSave()gets queued behind it and sets_pendingDispatchCopy = true. If the in-flight request network-errors, the.catch()branch replays the queued save viactrl._doSave()— passingundefinedinstead oftrue. The replayed save can still succeed and persist the copied fields, but_onSaveSuccess(payload, undefined)will never dispatchbookdrop:metadatacopied, so the row is never auto-checked even though the copy was persisted server-side.Stale flag causes a spurious future dispatch (reintroduces the exact bug this PR fixes). Because
_pendingDispatchCopyis lefttrue(never reset tofalsein the catch branch, unlike the then branch), the next unrelated_doSave()call that happens to race with an in-flight save will OR against that staletrue(line 433:ctrl._pendingDispatchCopy = ctrl._pendingDispatchCopy || !!dispatchCopyEvent) and inherit it. A later plain field-edit autosave (not a copy action at all) can then end up dispatchingbookdrop:metadatacopiedon success — silently auto-checking a row for a save that was never a copy. This is precisely the "row reads as selected but wasn't a real copy" failure class bookshelf-v3bi0 was filed to eliminate, reintroduced via the untested retry path.Neither path is covered by the new tests — all four new specs in
bookdrop_file_editor_controller.test.jsuse a fetch mock that resolves on the very first call with no queued follow-up save, so the in-flight-queueing branch (lines 432-433, 489-495) is never exercised by any added test.Fix: mirror the
.then()branch's handling in.catch():Add a Vitest spec that queues a copy-triggered
_doSave(true)behind an in-flight save whose fetch rejects, then resolves the replayed saveok:true, and asserts the event fires exactly once (proves fix #1) — plus a spec proving a later unrelated plain save does NOT dispatch after that network-error race (proves fix #2, i.e._pendingDispatchCopyis reset).[MINOR] static/js/test/bookdrop_file_editor_controller.test.js:848-892 — new specs only prove "eventually dispatches/doesn't," not "never fires synchronously before the fetch resolves"
The four new specs (and the updated METADATA COPIED EVENT specs) all
await Promise.resolve()three times before asserting, so they can't distinguish "dispatch moved into the resolved-fetch.then()" from a hypothetical implementation that dispatches synchronously at click time but the assertion happens to run after. The failure-path specs (fetch resolvesok:false/ rejects) are still a legitimate regression test for the original bug (an unconditional/optimistic dispatch would have fired regardless of outcome, failing those twonot.toHaveBeenCalled()assertions), so this is not vacuous — but a stronger spec would additionally asserthandlerhas NOT been called synchronously right after the click, before the firstawait, to positively pin "no dispatch before the request resolves" rather than inferring it only from the failure-path specs. Non-blocking; existing tests are far from tautological/self-validating.REVIEW VERDICT: 0 blocker, 1 major, 1 minor
Adversarial review of PR #1435 (bookshelf-v3bi0).
[MAJOR] static/js/controllers/bookdrop_file_editor_controller.js:489-495 — network-error retry path drops/leaks the queued dispatchCopyEvent flag, defeating the PR's own guarantee in a race
The
.catch()branch (network error) does not mirror the.then()branch's flag handling. Compare:.then()(lines 480-484, correct):.catch()(lines 489-495, buggy):Two concrete failure modes from this one line:
Lost dispatch (silent regression of the review workflow). Scenario:
_doSave(true)(a copy-triggered save) is in flight; while it's pending, another_doSave()gets queued behind it and sets_pendingDispatchCopy = true. If the in-flight request network-errors, the.catch()branch replays the queued save viactrl._doSave()— passingundefinedinstead oftrue. The replayed save can still succeed and persist the copied fields, but_onSaveSuccess(payload, undefined)will never dispatchbookdrop:metadatacopied, so the row is never auto-checked even though the copy was persisted server-side.Stale flag causes a spurious future dispatch (reintroduces the exact bug this PR fixes). Because
_pendingDispatchCopyis lefttrue(never reset tofalsein the catch branch, unlike the then branch), the next unrelated_doSave()call that happens to race with an in-flight save will OR against that staletrue(line 433:ctrl._pendingDispatchCopy = ctrl._pendingDispatchCopy || !!dispatchCopyEvent) and inherit it. A later plain field-edit autosave (not a copy action at all) can then end up dispatchingbookdrop:metadatacopiedon success — silently auto-checking a row for a save that was never a copy. This is precisely the "row reads as selected but wasn't a real copy" failure class bookshelf-v3bi0 was filed to eliminate, reintroduced via the untested retry path.Neither path is covered by the new tests — all four new specs in
bookdrop_file_editor_controller.test.jsuse a fetch mock that resolves on the very first call with no queued follow-up save, so the in-flight-queueing branch (lines 432-433, 489-495) is never exercised by any added test.Fix: mirror the
.then()branch's handling in.catch():Add a Vitest spec that queues a copy-triggered
_doSave(true)behind an in-flight save whose fetch rejects, then resolves the replayed saveok:true, and asserts the event fires exactly once (proves fix #1) — plus a spec proving a later unrelated plain save does NOT dispatch after that network-error race (proves fix #2, i.e._pendingDispatchCopyis reset).[MINOR] static/js/test/bookdrop_file_editor_controller.test.js:848-892 — new specs only prove "eventually dispatches/doesn't," not "never fires synchronously before the fetch resolves"
The four new specs (and the updated METADATA COPIED EVENT specs) all await Promise.resolve() three times before asserting, so they can't distinguish "dispatch moved into the resolved-fetch .then()" from a hypothetical implementation that dispatches synchronously at click time but the assertion happens to run after. The failure-path specs (fetch resolves ok:false / rejects) are still a legitimate regression test for the original bug (an unconditional/optimistic dispatch would have fired regardless of outcome, failing those two not.toHaveBeenCalled() assertions), so this is not vacuous — but a stronger spec would additionally assert handler has NOT been called synchronously right after the click, before the first await, to positively pin "no dispatch before the request resolves" rather than inferring it only from the failure-path specs. Non-blocking; existing tests are far from tautological/self-validating.
REVIEW VERDICT: 0 blocker, 1 major, 1 minor