1. recomputeFingerprint now hashes a JSON-serialized (not naive string-
concatenated, to avoid delimiter-collision) sorted list of
{id, direction, ruleId, ruleKind, ruleKeyword} per planned change — the
rule kind/keyword are exactly what gets written into
excluded_rule_kind/excluded_rule_keyword, so a plan that affects the
same ids/directions via a since-edited rule must now be rejected as
stale, not silently accepted. New tests:
TestRecomputeFingerprintChangesWhenRuleEvidenceChanges (edits the rule
row directly between preview and execute, since the API has no edit
endpoint, and asserts RECOMPUTE_PREVIEW_STALE with nothing written and
no log row) and TestRecomputeFingerprintStableAcrossUnchangedPreviews
(two previews of the same data yield the same fingerprint and execute
succeeds).
2. New server/app/goauto/sybproductfilter/recompute_mysql_integration_test.go,
gated on GOAUTO_IT_MYSQL_DSN (t.Skip when unset, so `go test` is
unaffected normally). It creates a uniquely named throwaway database
(zz_goauto_it_340_<ts>), migrates it, and drops it in t.Cleanup — never
touches an existing database. Two real-MySQL, two-connection scenarios
reproduce the exact race the phase-3 fix closes: connection A takes its
REPEATABLE-READ snapshot via the planning step, connection B takes the
row's FOR UPDATE lock and holds it (confirmed via a channel) while A's
write phase is proven to actually block on that same lock (asserted via
a wait window), B then inserts a purchase_task / active return_match
and commits, and A is asserted to unblock, see it, and skip the row.
Verified locally against the dev MySQL server (this session never
printed the password: read via a shell one-liner into an env var,
exported only for the go test invocation): both tests PASS with the
phase-3 fix in place. Temporarily reverted purchaseTaskLockedQuery to a
plain (non-locking) read (not committed) and reran —
TestRecomputeConcurrentPurchaseTaskUnderRealMySQL correctly FAILED
("expected A to skip the row ... got {PDDToExcluded:1 SkippedHasTask:0}"),
proving the test is meaningful; restored and diffed byte-identical
against a backup before rerunning to confirm both tests pass again.
Confirmed via `SHOW DATABASES LIKE 'zz_goauto_it_%'` (empty) that every
throwaway database, across all these runs, was actually dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F