fix(robot): show the per-game name in REST game views, not the account name
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 50s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m18s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 50s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m18s
The seat display-name snapshot only reached the live-event path (seatNames);
the REST DTO layer still resolved seat names from the account store
(fillSeatNames), so the game screen and lobby list showed the robot's seeded
account name ("Женя") while the your_turn toast showed its per-game name
("Звёздный_Барс2"). Carry the snapshot into gameDTOFromGame and have
fillSeatNames fall back to the account only for a seat with no snapshot (a
pre-snapshot legacy row). Friends and invitations keep account names (the
persistent identity, not a per-game disguise).
This commit is contained in:
@@ -77,8 +77,9 @@ type moveRecordDTO struct {
|
|||||||
Total int `json:"total"`
|
Total int `json:"total"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// seatDTO is one seat's public standing. DisplayName is resolved from the account
|
// seatDTO is one seat's public standing. DisplayName is the seat's display-name snapshot
|
||||||
// store by the handler (the game domain keys seats by account id only).
|
// (the per-game name, frozen when the seat was taken); fillSeatNames fills it from the
|
||||||
|
// account store only when a seat has no snapshot yet (a pre-snapshot legacy row).
|
||||||
type seatDTO struct {
|
type seatDTO struct {
|
||||||
Seat int `json:"seat"`
|
Seat int `json:"seat"`
|
||||||
AccountID string `json:"account_id"`
|
AccountID string `json:"account_id"`
|
||||||
@@ -199,19 +200,21 @@ const awayTimeLayout = "15:04"
|
|||||||
func gameDTOFromGame(g game.Game) gameDTO {
|
func gameDTOFromGame(g game.Game) gameDTO {
|
||||||
seats := make([]seatDTO, 0, len(g.Seats))
|
seats := make([]seatDTO, 0, len(g.Seats))
|
||||||
for _, s := range g.Seats {
|
for _, s := range g.Seats {
|
||||||
// An open game's still-empty opponent seat has no account: emit an empty id (the
|
// An open game's still-empty opponent seat has no account: emit an empty id (and an
|
||||||
// display name is left empty by fillSeatNames) so the client shows "searching for
|
// empty display name) so the client shows "searching for opponent" rather than the
|
||||||
// opponent" rather than the nil-UUID.
|
// nil-UUID. DisplayName carries the seat's per-game snapshot; fillSeatNames fills
|
||||||
|
// only the seats still without one from the account store.
|
||||||
accountID := ""
|
accountID := ""
|
||||||
if s.AccountID != uuid.Nil {
|
if s.AccountID != uuid.Nil {
|
||||||
accountID = s.AccountID.String()
|
accountID = s.AccountID.String()
|
||||||
}
|
}
|
||||||
seats = append(seats, seatDTO{
|
seats = append(seats, seatDTO{
|
||||||
Seat: s.Seat,
|
Seat: s.Seat,
|
||||||
AccountID: accountID,
|
AccountID: accountID,
|
||||||
Score: s.Score,
|
DisplayName: s.DisplayName,
|
||||||
HintsUsed: s.HintsUsed,
|
Score: s.Score,
|
||||||
IsWinner: s.IsWinner,
|
HintsUsed: s.HintsUsed,
|
||||||
|
IsWinner: s.IsWinner,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
last := g.TurnStartedAt
|
last := g.TurnStartedAt
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -111,3 +112,35 @@ func TestMoveRecordDTOFrom(t *testing.T) {
|
|||||||
t.Fatalf("move dto mismatch: %+v", dto)
|
t.Fatalf("move dto mismatch: %+v", dto)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestGameDTOCarriesSeatSnapshot checks the DTO carries each seat's display-name
|
||||||
|
// snapshot (so a disguised robot's per-game name reaches the REST views, not just live
|
||||||
|
// events) rather than leaving it for an account lookup.
|
||||||
|
func TestGameDTOCarriesSeatSnapshot(t *testing.T) {
|
||||||
|
g := game.Game{
|
||||||
|
Variant: engine.VariantEnglish, Players: 2, TurnTimeout: time.Hour,
|
||||||
|
Seats: []game.Seat{
|
||||||
|
{Seat: 0, AccountID: uuid.New(), DisplayName: "Звёздный_Барс2"},
|
||||||
|
{Seat: 1, AccountID: uuid.New(), DisplayName: "Тест"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
dto := gameDTOFromGame(g)
|
||||||
|
if dto.Seats[0].DisplayName != "Звёздный_Барс2" || dto.Seats[1].DisplayName != "Тест" {
|
||||||
|
t.Fatalf("seat snapshot not carried into DTO: %+v", dto.Seats)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFillSeatNamesKeepsSnapshot checks fillSeatNames leaves a seat whose snapshot is
|
||||||
|
// already set untouched (no account lookup), so the per-game name is not overwritten by
|
||||||
|
// the robot's seeded account name.
|
||||||
|
func TestFillSeatNamesKeepsSnapshot(t *testing.T) {
|
||||||
|
s := &Server{} // accounts is unused: a present snapshot must skip the lookup
|
||||||
|
dto := &gameDTO{Seats: []seatDTO{
|
||||||
|
{Seat: 0, AccountID: uuid.New().String(), DisplayName: "Звёздный_Барс2"},
|
||||||
|
{Seat: 1, AccountID: uuid.New().String(), DisplayName: "Тест"},
|
||||||
|
}}
|
||||||
|
s.fillSeatNames(context.Background(), dto, map[string]string{})
|
||||||
|
if dto.Seats[0].DisplayName != "Звёздный_Барс2" || dto.Seats[1].DisplayName != "Тест" {
|
||||||
|
t.Fatalf("fillSeatNames overwrote a seat snapshot: %+v", dto.Seats)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -66,10 +66,15 @@ type complaintRequest struct {
|
|||||||
Note string `json:"note"`
|
Note string `json:"note"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// fillSeatNames resolves each seat's display name from the account store, memoising
|
// fillSeatNames fills each seat's display name from the account store — memoising across
|
||||||
// across seats and games within one request.
|
// seats and games within one request — for the seats that carry no per-game snapshot yet
|
||||||
|
// (a pre-snapshot legacy row). A seat whose snapshot is already set is left untouched, so
|
||||||
|
// a disguised robot keeps its per-game name rather than reverting to its account name.
|
||||||
func (s *Server) fillSeatNames(ctx context.Context, g *gameDTO, memo map[string]string) {
|
func (s *Server) fillSeatNames(ctx context.Context, g *gameDTO, memo map[string]string) {
|
||||||
for i := range g.Seats {
|
for i := range g.Seats {
|
||||||
|
if g.Seats[i].DisplayName != "" {
|
||||||
|
continue // the seat already carries its per-game snapshot
|
||||||
|
}
|
||||||
id := g.Seats[i].AccountID
|
id := g.Seats[i].AccountID
|
||||||
name, ok := memo[id]
|
name, ok := memo[id]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
Reference in New Issue
Block a user