R6(a): de-stage code, docs, READMEs; split stage6_test

Mechanical, behaviour-preserving removal of Stage N / TODO-N / phase (RN)
references from comments, doc-comments, service READMEs, the current-state docs
(ARCHITECTURE, FUNCTIONAL+_ru, TESTING, UI_DESIGN), config-file comments, and the
.fbs/.proto schema comments. PLAN.md / PRERELEASE.md / CLAUDE.md keep the stage
history.

- Rename the only stage-named identifiers: registerStage8 -> registerSocialOps,
  registerStage11 -> registerLinkOps (gateway transcode).
- Split stage6_test.go: TestEmailLoginFlow -> email_test.go,
  TestGuestAutoMatchLeavesNoStats (+ provisionGuest) -> account_test.go.
- Regenerated proto bindings (push.pb.go, telegram_grpc.pb.go) from the de-staged
  .proto comments; FB Go/TS bindings unchanged (flatc strips schema comments).

go build/vet/gofmt clean across modules; integration typecheck and pnpm check green.
This commit is contained in:
Ilia Denisov
2026-06-10 16:56:03 +02:00
parent a372343797
commit 8881214213
156 changed files with 749 additions and 778 deletions
+75 -4
View File
@@ -11,6 +11,8 @@ import (
"github.com/google/uuid"
"scrabble/backend/internal/account"
"scrabble/backend/internal/engine"
"scrabble/backend/internal/game"
)
// TestAccountProvisionByIdentity covers find-or-create semantics, distinct
@@ -78,7 +80,7 @@ func TestAccountProvisionByIdentity(t *testing.T) {
}
// TestGetStatsZeroForFreshAccount checks that an account with no finished games
// reads back the zero statistics rather than an error (the Stage 8 stats screen).
// reads back the zero statistics rather than an error (the stats screen).
func TestGetStatsZeroForFreshAccount(t *testing.T) {
ctx := context.Background()
store := account.NewStore(testDB)
@@ -109,7 +111,7 @@ func identityConfirmed(t *testing.T, kind, externalID string) bool {
// TestProvisionTelegramSeedsNewAccountOnly checks that Telegram first contact
// seeds the new account's language and display name from the launch fields,
// defaults the in-app-only flag on, and never overwrites an existing account on a
// later login (Stage 9 language seeding).
// later login (language seeding).
func TestProvisionTelegramSeedsNewAccountOnly(t *testing.T) {
ctx := context.Background()
store := account.NewStore(testDB)
@@ -196,7 +198,7 @@ func TestServiceLanguageRoundTrip(t *testing.T) {
}
}
// TestHighRateFlagRoundTrip covers the R3 soft high-rate marker: a fresh account
// TestHighRateFlagRoundTrip covers the soft high-rate marker: a fresh account
// is unflagged, FlagHighRate stamps it exactly once (a second sustained episode
// never moves the timestamp), ClearHighRateFlag reverses it, and a re-flag after
// the operator clear takes a fresh timestamp.
@@ -279,7 +281,7 @@ func TestIdentityExternalID(t *testing.T) {
}
}
// TestNotificationsInAppOnlyRoundTrip checks the Stage 9 profile flag persists
// TestNotificationsInAppOnlyRoundTrip checks the profile flag persists
// through UpdateProfile and reads back through GetByID.
func TestNotificationsInAppOnlyRoundTrip(t *testing.T) {
ctx := context.Background()
@@ -311,3 +313,72 @@ func TestNotificationsInAppOnlyRoundTrip(t *testing.T) {
t.Error("GetByID still reports in-app-only after clearing")
}
}
// provisionGuest creates a fresh ephemeral guest account and returns its id.
func provisionGuest(t *testing.T) uuid.UUID {
t.Helper()
acc, err := account.NewStore(testDB).ProvisionGuest(context.Background())
if err != nil {
t.Fatalf("provision guest: %v", err)
}
if !acc.IsGuest {
t.Fatalf("provisioned account %s is not flagged guest", acc.ID)
}
return acc.ID
}
// TestGuestAutoMatchLeavesNoStats drives a guest through a full auto-match game
// against a robot to a natural end and checks the guest holds a seat (the
// game_players foreign key is satisfied) yet accrues no statistics, while the
// durable robot opponent does.
func TestGuestAutoMatchLeavesNoStats(t *testing.T) {
ctx := context.Background()
svc := newGameService()
robots := newRobotService(t, svc)
if err := robots.EnsurePool(ctx); err != nil {
t.Fatalf("ensure pool: %v", err)
}
robotID, err := robots.Pick(engine.VariantEnglish)
if err != nil {
t.Fatalf("pick: %v", err)
}
guest := provisionGuest(t)
seed := openingSeed(t)
g, err := svc.Create(ctx, game.CreateParams{
Variant: engine.VariantEnglish, Seats: []uuid.UUID{guest, robotID},
TurnTimeout: 24 * time.Hour, Seed: seed,
})
if err != nil {
t.Fatalf("create: %v", err)
}
const robotSeat = 1 // seats = [guest, robot]
finished := false
for i := 0; i < 400 && !finished; i++ {
_, toMove, status, err := svc.Participants(ctx, g.ID)
if err != nil {
t.Fatalf("participants: %v", err)
}
if status != game.StatusActive {
finished = true
break
}
if toMove == robotSeat {
setTurnStarted(t, g.ID, daytime.Add(-2*time.Hour))
robots.Drive(ctx, daytime)
continue
}
playHuman(t, ctx, svc, g.ID, guest)
}
if !finished {
t.Fatal("guest game did not finish within the move budget")
}
if _, _, _, _, _, ok := readStats(t, guest); ok {
t.Error("a guest must not accrue a statistics row")
}
if _, _, _, _, _, ok := readStats(t, robotID); !ok {
t.Error("the durable robot opponent should have a statistics row")
}
}