mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 08:52:42 +00:00
Co-op audit fixes: lock scheduler, idempotent payouts, deterministic ties
Audit pass addressed concurrency races, crash-recovery double-pay risks, and non-determinism in tie tallying. CRITICAL — concurrency - coopResolveFloor now acquires advUserLock for every party member (sorted by UserID) before mutating state, so concurrent !coop fund / vote / giftvote commands serialize against the scheduler. CRITICAL — crash idempotency - Add coop_dungeon_runs.last_resolved_day and coop_dungeon_members.member_payout columns (with migration entries) for crash-resume tracking. - Skip resolution only if last_resolved_day >= day AND status is terminal; otherwise re-enter and finish via idempotent operations. - On resume detection (event already has outcome), reuse the saved roll instead of re-rolling — prevents different outcomes on retry. - claimCoopMemberPayout / claimCoopBetPayout: atomic UPDATE...WHERE col IS NULL pattern. Each member/bet can only be paid once; retries are silent no-ops. - createCoopEvent now INSERT OR IGNORE — idempotent on retry. - last_resolved_day is set as the FINAL write per floor. HIGH — bugs - Lazy-create floor event in resolver if missing — covers crash window between lockCoopRun and the original createCoopEvent. - coopTallyVote sorts tied winners alphabetically before fallback (was non-deterministic via Go map iteration). - Removed dead day-1 wipe combat-action refund block — refund check was always false because midnight reset clears state before resolution. LOW - Log slog.Error on malformed JSON in parseCoopVotes / parseCoopFundingMap instead of silently dropping data. Tests - Tally test extended to assert determinism across 50 runs (catches map-iteration leaks). - Added coverage for new tie-with-leader-vote-outside-winners case. - Idempotency-guard sanity check on LastResolvedDay vs CurrentDay. Race detector: clean. Full suite: green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -96,19 +96,19 @@ func TestCoopTallyVote(t *testing.T) {
|
||||
{"clear majority", map[id.UserID]string{leader: "A", other: "A", third: "B"}, "A"},
|
||||
{"tie broken by leader", map[id.UserID]string{leader: "B", other: "A"}, "B"},
|
||||
{"all abstained falls back to A", map[id.UserID]string{}, "A"},
|
||||
{"tie no leader vote falls back deterministically", map[id.UserID]string{other: "A", third: "B"}, ""}, // "A" or "B" — accept either
|
||||
{"tie with no leader vote → lex first (deterministic)", map[id.UserID]string{other: "A", third: "B"}, "A"},
|
||||
{"three-way tie, leader's vote in winners → leader's pick", map[id.UserID]string{leader: "C", other: "A", third: "B"}, "C"},
|
||||
{"tie with leader voting outside winners → lex first", map[id.UserID]string{leader: "C", other: "A", "@4:t": "A", third: "B", "@5:t": "B"}, "A"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
event := &CoopEvent{Votes: tc.votes}
|
||||
got := coopTallyVote(event, leader)
|
||||
if tc.want == "" {
|
||||
if got != "A" && got != "B" {
|
||||
t.Errorf("%s: got %q, want one of A/B", tc.name, got)
|
||||
// Run repeatedly to catch any non-determinism leaking from map iteration.
|
||||
for i := 0; i < 50; i++ {
|
||||
event := &CoopEvent{Votes: tc.votes}
|
||||
got := coopTallyVote(event, leader)
|
||||
if got != tc.want {
|
||||
t.Errorf("%s (iter %d): got %q, want %q", tc.name, i, got, tc.want)
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("%s: got %q, want %q", tc.name, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,6 +163,24 @@ func TestCoopEventMetaConsistency(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoopResolutionIdempotencyGuard(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Sanity check: the LastResolvedDay field on CoopRun is the authoritative
|
||||
// idempotency marker. The resolver short-circuits when LastResolvedDay >=
|
||||
// CurrentDay, so a crash-restart on the same UTC day after the roll has
|
||||
// landed is a safe no-op.
|
||||
run := &CoopRun{CurrentDay: 3, LastResolvedDay: 3}
|
||||
if !(run.LastResolvedDay >= run.CurrentDay) {
|
||||
t.Errorf("expected resolution skip when LastResolvedDay (%d) >= CurrentDay (%d)",
|
||||
run.LastResolvedDay, run.CurrentDay)
|
||||
}
|
||||
run.LastResolvedDay = 2
|
||||
if run.LastResolvedDay >= run.CurrentDay {
|
||||
t.Errorf("expected resolution to proceed when LastResolvedDay (%d) < CurrentDay (%d)",
|
||||
run.LastResolvedDay, run.CurrentDay)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoopParimutuelPayouts(t *testing.T) {
|
||||
t.Parallel()
|
||||
a := id.UserID("@a:t")
|
||||
|
||||
Reference in New Issue
Block a user