fix(db): wrap ctx.Err() + soften doc comment (bookshelf-adzpx) #1436
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "bd-bookshelf-adzpx"
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
Two non-blocking review minors from code review in internal/db/db.go MigrationStatus:
Line 185: Wrap ctx.Err() return with fmt.Errorf and %w to match the wrapping style of other error returns in the function. This enables proper error chain inspection via errors.Is().
Lines 161-165: Soften the MigrationStatus doc comment to accurately reflect that the context check is only a pre-call guard—golang-migrate's m.Version() does not accept a context, so cancellation doesn't interrupt an in-flight query.
The existing test for context cancellation (db_test.go:225-233) continues to pass because errors.Is(err, context.Canceled) correctly unwraps %w-wrapped errors.
Test plan
Closes bead bookshelf-adzpx on merge.
Two non-blocking review minors from code review: 1. Line 185: wrap ctx.Err() return with fmt.Errorf and %w to match the style of other error returns in MigrationStatus (e.g., lines 169, 174, 179, 194). This enables proper error chain inspection via errors.Is() on the context error. 2. Lines 161-165: soften the doc comment to accurately reflect that the context check is a pre-call guard only; golang-migrate's m.Version() does not accept a context, so the cancellation check does not interrupt an in-flight query. The existing test at db_test.go:225-233 ("respects context cancellation") still passes because errors.Is(err, context.Canceled) correctly unwraps the %w-wrapped error to find the sentinel. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tRKybTpfjQ4SxmNdVFLHiSecurity Review — PR #1436 (bookshelf-adzpx)
Scope:
internal/db/db.go(2-line change toMigrationStatusdoc comment + error wrapping) andinternal/wfengine/simple_workflows_test.go(test-only timeout bump). Very low security surface, reviewed adversarially per the focus areas requested.Error wrapping (
db.go:184) —fmt.Errorf("migration status check cancelled: %w", ctx.Err())wrapsctx.Err()(alwayscontext.Canceledorcontext.DeadlineExceeded, both static stdlib sentinels) with a static, non-parameterized prefix string. No request data, credentials, DSN, or migration internals are interpolated.errors.Is(err, context.Canceled)callers remain unaffected since%wpreserves the chain. No information disclosure.Doc comment change (
db.go:162-165) — comment-only edit describing cancellation-check semantics more precisely (checks before querying, does not interrupt an in-flight query). No code/behavior change from the previous comment; accurate description of the existingselect { case <-ctx.Done(): ... }pre-check that was already in place.Exposure surface —
MigrationStatusis called only fromcmd/pergamum/migrate.go(thepergamum migrate statusCLI subcommand), not from any HTTP handler. Confirmed no unauthenticated or authenticated web route reaches this function; this PR does not add any new call site. No authz surface introduced or altered.Test file (
simple_workflows_test.go) — addstester.WithTestTimeout(60*time.Second)toNewWorkflowTesterconstruction to fix a CI flake (idle-timeout race under CPU starvation). Test-only, no production code path, no secrets, no assertions weakened — the workflow behavior under test is unchanged, only the tester's idle-timeout budget.Secrets — none found; no logging of sensitive values in either file.
No findings.
REVIEW VERDICT: 0 blocker, 0 major, 0 minor
Adversarial review of PR #1436 (bookshelf-adzpx).
Scope confirmed: (1)
%w-wrap ofctx.Err()inMigrationStatus, (2) softened doc comment, (3) bundled flake fix addingtester.WithTestTimeout(60*time.Second)to theEnrichWorkflowtester ininternal/wfengine/simple_workflows_test.go. The bundled flake fix is treated as intentional per the dispatch brief (same precedented pattern as bookshelf-19lt/#397).1.
%wwrap — internal/db/db.go:183Correct:
errors.Is(err, context.Canceled)still resolves through the wrap. Verified against the existing (unchanged) integration testinternal/db/db_test.go:225-233("respects context cancellation"), which asserts exactlyerrors.Is(err, context.Canceled)— passes with the new wrap.2. Doc comment — internal/db/db.go:161-164
Accurate:
m.Version()(golang-migrate) takes nocontext.Contextparam, so theselect{case <-ctx.Done(): ...}at line 182 is a pre-call guard only, exactly as the new comment states. Old comment ("accepts a context to honor cancellation signals") overstated it as if in-flight cancellation were honored.3. WithTestTimeout flake fix — internal/wfengine/simple_workflows_test.go
Matches the established bookshelf-19lt idiom exactly (
tester.WithTestTimeout(60*time.Second), same call site pattern asbulk_workflow_test.go:809,recalc_scores_workflow_test.gox8). Only the tester construction changed — the "when activity fails"Itblock (Expect(workflowErr).To(HaveOccurred())) and the permanent-error stub (gowf.NewPermanentError) are untouched, so the assertion still exercises real activity-failure→workflow-error propagation, not a weakened/no-op check. No masking of a real workflow bug: the change only raises the tester's idle-panic threshold, it doesn't alter retry policy, activity registration, or the assertion.4. New/affected test coverage for db.go
No new test was added in this diff for the wrap —
internal/db/db_test.gois untouched (git diff confirms no changes to that file). The pre-existing integration test at db_test.go:225-233 already assertserrors.Is(err, context.Canceled), black-box (package db_test), and continues to pass with the wrapped error.internal/dbis excluded from the unit-coverage gate (integration-tested only, per bead comment), consistent with project convention — no coverage regression introduced.[MINOR] internal/db/db.go:183 — double-wrapped, slightly redundant error message
The new wrap text
"migration status check cancelled: %w"combined with the caller's existing wrap incmd/pergamum/migrate.go:137("migration status: %w") produces a slightly redundant final message:migration status: migration status check cancelled: context canceled. Also a minor style deviation from the sibling wraps in the same function, which use short noun-phrases ("migrate source","migrate driver","migrate init","migrate version") rather than a verb-phrase ("... cancelled"). Not worth blocking — purely cosmetic; could tighten tofmt.Errorf("migrate status: %w", ctx.Err())for consistency with siblings and to avoid the doubled "migration status" wording, but functionally harmless.No blockers or majors found. The bundled flake fix is verified against the precedented 19lt/#397 pattern and does not weaken the test's real assertions.
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 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