test: drop test-only production hooks; enforce deterministic async testing - #70
Conversation
…sting Replace bespoke test instrumentation and timing-based test synchronization with the established observable-event WaitGroup discipline, and separate internal-mechanism unit tests from external-API e2e tests. Production: - Remove VVManager.SetBumpObserver/onBump (the bespoke test-only hook) and revert increment() to its plain `defer mu.Unlock()`. The VV bump is already observable through the standard storage AfterWrite (it persists to pivot/vv/<key>) and the external /activity endpoint, so no special hook is warranted. Tests: - clock_drift: assert the data-sync effect (a present-time write wins over a future-timestamped one — only possible if VV ordering, not the wall clock, governs) instead of internal VV counters; existing NodeWg/PivotWg already synchronize it, so no wait-on-bump is needed. - offline_sync: TestOfflineNodeWriteAndSync asserts the data effect; TestVersionVectorActivityEndpoint waits on the VV-persistence storage write (standard AfterWrite, via setupOfflineServers' opt-in observer) and asserts the VV through the external /activity endpoint — no VVManager polling. - handlers_internal + vv_path_scope: replace time.Sleep / Eventually(pendingLen) with a WaitGroup signalled when the watch callback finishes processing each event (the path that could double-bump). Reframe the HandlerWriteTracker unit tests to assert the public Mark/Consume/ConsumeBumpSkip contract, and remove the test-only pendingLen/bumpPendingLen accessors. - version_e2e: wait on the node's pivot/status feed (the external cluster-status API the health check broadcasts to) instead of polling GetPivotInfo on a sleep loop. - vv_merge: wait on the mock leader's request handler instead of Eventually(len(received)). Justified unchanged (race/stress or correct failure-deadline patterns, not result synchronization): nodehealth_race_test (race tests — sleeps are concurrency windows for -race and negative assertions, real sync is via channels/WaitGroups), remote_context_test (channel-based success sync; time.After is the failure deadline), trigger/syncer_init (sync.Once first-hit on the unbounded coalescer stream). Verified: full -race suite green; 40x serial + 60x parallel -race of the converted tests with zero hangs or flakes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CBosch101
left a comment
There was a problem hiding this comment.
Removes the test-only VVManager.SetBumpObserver/onBump hook and converts timing- and internal-state-based test synchronization to observable-event WaitGroups. Verdict: APPROVE.
Verified
increment()revert is clean — thenodeID == ""early return is now covered bydefer m.mu.Unlock(), so neither path double-unlocks; build +go vetclean, no remaining references toonBump/SetBumpObserveranywhere in the tree.- Removing
SetBumpObserveris backward-compatible — it was documented test-only with no production callers, so the public surface loses nothing a consumer could be relying on. - WaitGroup Add/Done counts balance: full suite green under
-race(a wrong count would hang or panic), and the converted tests stay green across repeated-raceruns.watchProcessedcorrectly firesDone()only on glob-matching events, so VV-persistence/tombstone writes to other prefixes don't inflate the count.
Non-blocking
awaitPivotStatus—version_e2e_test.go.gotis overwritten on every message, so the returned value is the last message seen, not necessarily the one that satisfiedPivotProtocol != "unknown"; capture the value insideonce.Doto make it airtight against a later status delivery between the signal and the read.
🤖 Generated with Claude Code
… CI hang) The prior commit's awaitPivotStatus subscribed to the node's pivot/status feed and waited for a detected protocol. That hangs: pivot/status broadcasts only on a status CHANGE, so the single unknown→detected transition can be missed in the subscription's connect window (initial snapshot still "unknown", the one change broadcast lost) — after which no further broadcast arrives and the wait never returns. CI hit this (TestE2E_VersionSync_NodeDetectsIncompatiblePivotProtocol ran 48s and blew the 60s package timeout); reproduced locally 4/6 under parallel -race load. Protocol detection is a level (current state of an async background health check), not a reliably observable edge, so read the level: awaitDetectedProtocol polls GetPivotInfo until it leaves "unknown" with a bounded deadline. Documented why this is the right tool here rather than an event wait, and the PR's triage table moves version_e2e to the justified bucket. Verified: the parallel -race load that hung 4/6 now passes all 6. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CBosch101
left a comment
There was a problem hiding this comment.
Removes the test-only VVManager.SetBumpObserver/onBump hook and converts timing/internal-state test sync to observable-event WaitGroups. One blocker: a comment that contradicts the code it sits above.
Verified
increment()revert is sound —defer m.mu.Unlock()covers both thenodeID == ""early return and the normal path; no double-unlock, build +go vetclean, zero remainingonBump/SetBumpObserverreferences in the tree.watchProcessedfiresDone()only on glob-matching events, so VV-persistence writes (to thevv/prefix) don't inflate the count; Add/Done balance holds across the converted tests under repeated-raceruns, including the twoversion_e2eprotocol-detection tests that previously hung.
Blockers
- Comment contradicts the code —
version_e2e_test.go:275-276and:304-305say "Wait (event-driven) … observed through the node's pivot/status feed", but they callawaitDetectedProtocol, whose own doc (:57) states it "deliberately polls rather than subscribing to an event." The 963af6b rewrite swapped subscribe→poll but left the old call-site comments. In a PR whose thesis is event-driven vs polling, a comment asserting the opposite of what runs is actively misleading — reword both to describe the bounded poll.
🤖 Generated with Claude Code
…poll The 963af6b rewrite swapped subscribe->poll but left the two awaitDetectedProtocol call-site comments asserting "event-driven ... observed through the node's pivot/status feed" — the opposite of what the helper now does. Reword both to describe the bounded poll and defer to awaitDetectedProtocol's own doc for the rationale. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts: # handlers_internal_test.go # vv_path_scope_test.go
CBosch101
left a comment
There was a problem hiding this comment.
Removes the test-only VVManager.SetBumpObserver/onBump hook and converts timing- and internal-state-based test sync to observable-event WaitGroups. The prior blocker — version_e2e_test.go call-site comments asserting "event-driven … pivot/status feed" above a helper that polls — is resolved in f55636f: both sites (:288-290, :317-319) now describe the bounded poll and defer to awaitDetectedProtocol's own doc. Verdict: APPROVE.
Verified
increment()revert is behavior-preserving —m.mu.Lock()+defer m.mu.Unlock()covers both thenodeID == ""early return and the normal path;saveToStoragestill runs under the lock as before; only the test-only callback is gone. NoonBump/SetBumpObserver/pendingLen/bumpPendingLenreferences remain anywhere in the tree; build +go vetclean.watchProcessedfiresDone()only on glob-matching events (key.Match), so VV-persistence writes to thepivot/vv/prefix don't inflate the count; Add/Done balance holds across the converted tests under repeated-raceruns (a wrong count would hang).- Removing the exported
SetBumpObserveris the only public-surface change; it was documented test-only with no production callers, so downstream consumers lose nothing they could rely on.
Summary
A reviewer flagged that the
VVManager.SetBumpObservertest hook (merged in #66) was a bespoke instrumentation that broke from the established storage-callback convention and made a test assert on internal state rather than the available API. This PR removes that hook and treats it as a codebase-wide pattern: a hunt-and-seek for the same two smells — test-only production surface and timing-based async synchronization — across the test suite.Production change
VVManager.SetBumpObserver/onBumpand revertincrement()to its plaindefer mu.Unlock(). The VV bump is already observable through the standard storageAfterWrite(it persists topivot/vv/<key>) and the external/activityendpoint — no special hook is warranted. Net deletion of production API.Test changes (every clear violation fixed)
NodeWg/PivotWgalready synchronize it.TestOfflineNodeWriteAndSyncasserts the data effect;TestVersionVectorActivityEndpointwaits on the VV-persistence storage write (standardAfterWrite, via asetupOfflineServersopt-in) and asserts the VV through the external/activityendpoint.time.Sleep/Eventually(pendingLen)with a WaitGroup signalled when the watch callback finishes processing each event (the path that could double-bump); reframe theHandlerWriteTrackertests to assert the publicMark/Consume/ConsumeBumpSkipcontract; remove the test-onlypendingLen/bumpPendingLenaccessors.time.Sleeploop with a single bounded poll of the node's detected protocol level (awaitDetectedProtocol). Protocol detection is async-background level state whose change-onlypivot/statusbroadcast can't be reliably awaited (a subscribe-and-wait hung CI), so a bounded poll of the current level is the robust tool — see the audit row below. Both call-site comments describe the poll, not an event subscription.Eventually(len(received)).Triaged audit
version_vector.goSetBumpObserver/onBumptest-only prod APIhandlers_internal_test.gopendingLen/bumpPendingLentest-only accessorsclock_drift_test.gooffline_sync_test.go×3Eventually(VVManager.Get)/activity+ AfterWrite waithandlers_internal_test.gotime.Sleep×3 +Eventually(pendingLen)×2vv_path_scope_test.goEventually(pendingLen),Eventually(VV)version_e2e_test.gotime.Sleeppoll ×2pivot/statusbroadcast cannot be reliably awaited (subscribing hung CI), so a bounded poll of the current level is the robust toolvv_merge_test.goEventually(len(received))nodehealth_race_test.gotime.Sleep,select+time.After-raceand negative assertions; real sync is channel/WaitGroup basedremote_context_test.goselect+time.Aftertime.Afteris the failure deadline, not a sync mechanismtrigger_test.go/syncer_init_test.gosync.OncearoundDone*_bench_test.goVerification
Full suite green under
-race. The converted tests ran 40× serial + 60× parallel under-racewith zero hangs or flakes (a wrong WaitGroup count would hang).🤖 Generated with Claude Code