fix(game): clear nudges on game completion
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
Nudge badges lingered in the lobby on games that ended by turn-timeout, resignation or forfeit: those paths commit the finish directly, bypassing the move path's per-mover NudgeClearer, so the awaited seat's nudge was never marked read. A finished game's nudges are stale, so clear them all. Add a wired NudgeExpirer (social.ExpireNudges) called from the shared commit finish block — covering every completion path — and from the voidGame recovery path. It clears every seat's nudge bits for the game and leaves chat messages unread; unlike ClearNudges it records no publish-to-read latency, since a completion is an expiry, not a read. An integration test reproduces the timeout case (nudge cleared, chat kept). Docs: ARCHITECTURE §9.1, FUNCTIONAL (+_ru), backend/README.
This commit is contained in:
@@ -138,6 +138,65 @@ func TestNudgeClearedByMove(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGameCompletionExpiresNudgesKeepsChat checks that finishing a game by turn-timeout marks every
|
||||
// pending nudge in it read — the lobby's nudge badge is stale once the game is over — while leaving
|
||||
// real chat messages unread. It reproduces the reported bug: the timeout path commits the finish
|
||||
// directly, bypassing the move path's per-mover nudge clear, so without a completion-driven expiry
|
||||
// the awaited seat's nudge lingered as a badge on the finished game.
|
||||
func TestGameCompletionExpiresNudgesKeepsChat(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
gameSvc := newGameService()
|
||||
socialSvc := newSocialService()
|
||||
gameSvc.SetNudgeExpirer(socialSvc.ExpireNudges)
|
||||
|
||||
seats := []uuid.UUID{provisionAccount(t), provisionAccount(t)}
|
||||
g, err := gameSvc.Create(ctx, game.CreateParams{
|
||||
Variant: engine.VariantEnglish, Seats: seats, TurnTimeout: time.Hour, Seed: openingSeed(t),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
}
|
||||
|
||||
// Seat 1 nudges the to-move seat 0 (the awaited player), and seat 0 posts a real chat message,
|
||||
// which seat 1 then holds unread. So before the timeout each side has exactly one unread entry:
|
||||
// seat 0 a nudge, seat 1 a message — letting HasUnread isolate each kind.
|
||||
if _, err := socialSvc.Nudge(ctx, g.ID, seats[1]); err != nil {
|
||||
t.Fatalf("nudge: %v", err)
|
||||
}
|
||||
if _, err := socialSvc.PostMessage(ctx, g.ID, seats[0], "good luck", ""); err != nil {
|
||||
t.Fatalf("post message: %v", err)
|
||||
}
|
||||
if unread, _ := socialSvc.HasUnread(ctx, g.ID, seats[0]); !unread {
|
||||
t.Fatal("seat 0 should hold the nudge unread before the timeout")
|
||||
}
|
||||
if unread, _ := socialSvc.HasUnread(ctx, g.ID, seats[1]); !unread {
|
||||
t.Fatal("seat 1 should hold the message unread before the timeout")
|
||||
}
|
||||
|
||||
// Age the turn past its deadline and time seat 0 out; an empty away window keeps this
|
||||
// deterministic regardless of the wall clock. The sweep finishes the game through the direct
|
||||
// commit path, never the move path.
|
||||
backdate(t, g.ID, time.Now().UTC().Add(-2*time.Hour))
|
||||
setAway(t, seats[0], "UTC", "00:00", "00:00")
|
||||
if n, err := gameSvc.SweepTimeouts(ctx, time.Now().UTC()); err != nil || n < 1 {
|
||||
t.Fatalf("sweep swept %d (err %v), want >= 1", n, err)
|
||||
}
|
||||
if status, reason := gameStatus(t, gameSvc, g.ID); status != game.StatusFinished || reason != "timeout" {
|
||||
t.Fatalf("game not timed out: status %q reason %q", status, reason)
|
||||
}
|
||||
|
||||
// The nudge is stale on a finished game and must be cleared; the chat message must survive.
|
||||
if unread, _ := socialSvc.HasUnread(ctx, g.ID, seats[0]); unread {
|
||||
t.Error("the nudge should be expired once the game has finished")
|
||||
}
|
||||
if unread, _ := socialSvc.HasUnread(ctx, g.ID, seats[1]); !unread {
|
||||
t.Error("a real chat message must stay unread after the game finishes")
|
||||
}
|
||||
if msg, _ := socialSvc.HasUnreadMessage(ctx, g.ID, seats[1]); !msg {
|
||||
t.Error("the chat message must remain flagged as an unread message after completion")
|
||||
}
|
||||
}
|
||||
|
||||
// TestChatToRobotIsBornRead checks a text message to a disguised robot opponent (a pooled
|
||||
// robot substituted into an ordinary, non-AI game) is born read: the robot never opens the
|
||||
// chat, so the message must not linger unread (skewing the count and the read metric).
|
||||
|
||||
Reference in New Issue
Block a user