Stage 17 #4: enrich the out-of-app your-turn push + add game-over
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Successful in 34s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m20s

The Telegram 'your turn' notification now names the opponent and recaps their last
move (voiced as the opponent: «{name}: my move — «WORD». Score 120:95» for a scoring
play; a short 'swapped / passed, your turn' otherwise), and a new game-over
notification reports the result + final score when a game ends by any path (closing
play, all-pass, resign, timeout). Scores are recipient-first (the reader's score
leads), 2-4 players (120:95:80).

- schema: YourTurnEvent gains opponent_name/last_action/last_word/score_line
  (appended, backward-compatible); new GameOverEvent{result, score_line}. Go + UI
  bindings regenerated (flatc 23.5.26 + pnpm codegen).
- backend: notify.YourTurn enriched + notify.GameOver; emitMove resolves the mover's
  name and emits per-recipient (your_turn to the next mover, game_over to every seat),
  with recipient-first score lines built in one place.
- gateway: game_over joins the out-of-app whitelist (routing.go).
- connector: render builds the enriched your_turn + game_over text per language (en/ru).
- tests: notify round-trip (enriched + game_over), emit (enriched fields + game_over to
  all seats / per-seat result), connector render (en/ru), routing; integration replay
  (play → your_turn with real name; resign → game_over) green.
- docs: ARCHITECTURE push catalog + out-of-app set, FUNCTIONAL (+ _ru), PLAN tracker.
This commit is contained in:
Ilia Denisov
2026-06-09 01:15:18 +02:00
parent 6956dad354
commit f166ff30fe
19 changed files with 657 additions and 52 deletions
@@ -1,6 +1,7 @@
package render
import (
"strings"
"testing"
flatbuffers "github.com/google/flatbuffers/go"
@@ -46,6 +47,86 @@ func notifyPayload(kind string) []byte {
return b.FinishedBytes()
}
func enrichedYourTurnPayload(id, name, action, word, score string) []byte {
b := flatbuffers.NewBuilder(0)
gid := b.CreateString(id)
n := b.CreateString(name)
a := b.CreateString(action)
w := b.CreateString(word)
s := b.CreateString(score)
scrabblefb.YourTurnEventStart(b)
scrabblefb.YourTurnEventAddGameId(b, gid)
scrabblefb.YourTurnEventAddOpponentName(b, n)
scrabblefb.YourTurnEventAddLastAction(b, a)
scrabblefb.YourTurnEventAddLastWord(b, w)
scrabblefb.YourTurnEventAddScoreLine(b, s)
b.Finish(scrabblefb.YourTurnEventEnd(b))
return b.FinishedBytes()
}
func gameOverPayload(id, result, score string) []byte {
b := flatbuffers.NewBuilder(0)
gid := b.CreateString(id)
r := b.CreateString(result)
s := b.CreateString(score)
scrabblefb.GameOverEventStart(b)
scrabblefb.GameOverEventAddGameId(b, gid)
scrabblefb.GameOverEventAddResult(b, r)
scrabblefb.GameOverEventAddScoreLine(b, s)
b.Finish(scrabblefb.GameOverEventEnd(b))
return b.FinishedBytes()
}
// TestRenderYourTurnEnriched checks the enriched "your turn" body names the opponent, their word
// and the recipient-first score for a scoring play, and uses the shorter phrase for an exchange.
func TestRenderYourTurnEnriched(t *testing.T) {
en, ok := Render("your_turn", enrichedYourTurnPayload(gameID, "Ann", "play", "STOOL", "120:95"), "en")
if !ok {
t.Fatal("expected ok")
}
for _, want := range []string{"Ann", "STOOL", "120:95"} {
if !strings.Contains(en.Text, want) {
t.Errorf("en play text %q missing %q", en.Text, want)
}
}
ru, _ := Render("your_turn", enrichedYourTurnPayload(gameID, "Аня", "play", "СТОЛ", "120:95"), "ru")
for _, want := range []string{"Аня", "СТОЛ", "120:95", "мой ход"} {
if !strings.Contains(ru.Text, want) {
t.Errorf("ru play text %q missing %q", ru.Text, want)
}
}
ex, _ := Render("your_turn", enrichedYourTurnPayload(gameID, "Ann", "exchange", "", ""), "en")
if !strings.Contains(ex.Text, "Ann") || strings.Contains(ex.Text, "«") {
t.Errorf("en exchange text %q should name the opponent and carry no word", ex.Text)
}
}
// TestRenderGameOver checks the game_over body reads from the recipient's perspective in both
// languages and carries the game deep-link.
func TestRenderGameOver(t *testing.T) {
cases := []struct{ result, en, ru string }{
{"won", "won", "выиграли"},
{"lost", "lost", "проиграли"},
{"draw", "draw", "ничья"},
}
for _, tc := range cases {
en, ok := Render("game_over", gameOverPayload(gameID, tc.result, "120:95"), "en")
if !ok {
t.Fatalf("%s: expected ok", tc.result)
}
if en.StartParam != "g"+gameID {
t.Errorf("%s StartParam = %q, want g%s", tc.result, en.StartParam, gameID)
}
if !strings.Contains(strings.ToLower(en.Text), tc.en) || !strings.Contains(en.Text, "120:95") {
t.Errorf("en %s text %q", tc.result, en.Text)
}
ru, _ := Render("game_over", gameOverPayload(gameID, tc.result, "120:95"), "ru")
if !strings.Contains(ru.Text, tc.ru) {
t.Errorf("ru %s text %q missing %q", tc.result, ru.Text, tc.ru)
}
}
}
func TestRenderGameEvents(t *testing.T) {
cases := []struct {
name, kind string