mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-12 02:43:43 +00:00
feat(ranked): make tier thresholds durable instead of compiled in
Task 8.22. Tier bands lived in domain.DefaultTierPolicy(), compiled into every API binary, so retuning one meant building and rolling a new image -- least attractive exactly when it is most needed, as the rating distribution settles after launch. Bands now live in a tier_bands table, seeded by the migration with the exact policy the binaries hardcode, so this changes durable state without changing behaviour. Retuning is a rolling restart rather than a rebuild. Three properties the loader deliberately holds: - A malformed durable policy stops startup. Falling back on error would silently mis-tier every player, which is worse than not starting. - An empty table is supported and falls back to the compiled default, so an operator can truncate back to known-good without a deploy, and a fresh database works before the seed is reviewed. - PROVISIONAL is rejected as a band. It is derived from ranked game count, not rating, so a band claiming it would be unreachable at best and would shadow a real tier at worst. Bands stay backend-owned; clients still receive only the resulting label, per docs/MATCHMAKING.md. UNIQUE(min_rating) rejects two bands sharing a threshold, catching an ambiguous policy before NewTierPolicy does. testkit-api loads it too, so the control-plane integration scripts exercise the durable path rather than the compiled default. Integration tests cover the seeded policy matching the compiled one, retuning taking effect from the database alone, truncation falling back, and each invalid-policy shape being rejected. Verified they fail against a loader that ignores durable bands. The other two parts of 8.22 needed no work: the client UI already renders tier, provisional status, ranked games and the season countdown, and reconnect transport is 8.42's, dependent on live backend events.
This commit is contained in:
@@ -2338,3 +2338,98 @@ func TestPostgreSQLSteamLoginResolvesDurableIdentities(t *testing.T) {
|
||||
t.Fatal("a banned identity signed in through the production path")
|
||||
}
|
||||
}
|
||||
|
||||
// Tier thresholds were compiled into every API binary, so retuning a band
|
||||
// meant building and rolling a new image -- least attractive exactly when it
|
||||
// is most needed, as the rating distribution settles after launch.
|
||||
func TestPostgreSQLTierPolicyIsDurableAndOverridesTheCompiledDefault(t *testing.T) {
|
||||
db := openIntegrationPostgres(t)
|
||||
applyIntegrationMigrations(t, db)
|
||||
|
||||
ctx := context.Background()
|
||||
// A settled, non-provisional profile, so RankedTier consults the bands
|
||||
// rather than short-circuiting to PROVISIONAL.
|
||||
settled := domain.RankedProfile{Rating: domain.Rating{Value: 1550}, RankedGames: 50}
|
||||
|
||||
// The seed migration must reproduce the compiled launch policy exactly,
|
||||
// so introducing durable bands changes storage without changing behaviour.
|
||||
loaded, err := LoadTierPolicy(ctx, db)
|
||||
if err != nil {
|
||||
t.Fatalf("load seeded policy: %v", err)
|
||||
}
|
||||
seededTier, err := domain.RankedTier(settled, loaded)
|
||||
if err != nil {
|
||||
t.Fatalf("tier from seeded policy: %v", err)
|
||||
}
|
||||
compiledTier, err := domain.RankedTier(settled, domain.DefaultTierPolicy())
|
||||
if err != nil {
|
||||
t.Fatalf("tier from compiled policy: %v", err)
|
||||
}
|
||||
if seededTier != compiledTier || seededTier != domain.RankTierGold {
|
||||
t.Fatalf("seeded policy tier = %q, compiled = %q, want GOLD", seededTier, compiledTier)
|
||||
}
|
||||
|
||||
// Retuning a band must take effect from the database alone. GOLD moves
|
||||
// first: UNIQUE(min_rating) rejects two bands sharing a threshold, which
|
||||
// is a deliberate early guard against an ambiguous policy.
|
||||
if _, err := db.ExecContext(ctx, `UPDATE tier_bands SET min_rating = 1400 WHERE tier = 'GOLD'`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := db.ExecContext(ctx, `UPDATE tier_bands SET min_rating = 1500 WHERE tier = 'PLATINUM'`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
retuned, err := LoadTierPolicy(ctx, db)
|
||||
if err != nil {
|
||||
t.Fatalf("load retuned policy: %v", err)
|
||||
}
|
||||
retunedTier, err := domain.RankedTier(settled, retuned)
|
||||
if err != nil {
|
||||
t.Fatalf("tier from retuned policy: %v", err)
|
||||
}
|
||||
if retunedTier != domain.RankTierPlatinum {
|
||||
t.Fatalf("retuned tier = %q, want PLATINUM; durable bands did not take effect", retunedTier)
|
||||
}
|
||||
|
||||
// An empty table is a supported state: an operator can truncate it to
|
||||
// return to known-good defaults without a deploy.
|
||||
if _, err := db.ExecContext(ctx, `DELETE FROM tier_bands`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fallback, err := LoadTierPolicy(ctx, db)
|
||||
if err != nil {
|
||||
t.Fatalf("load empty policy: %v", err)
|
||||
}
|
||||
fallbackTier, err := domain.RankedTier(settled, fallback)
|
||||
if err != nil {
|
||||
t.Fatalf("tier from fallback policy: %v", err)
|
||||
}
|
||||
if fallbackTier != domain.RankTierGold {
|
||||
t.Fatalf("fallback tier = %q, want the compiled GOLD", fallbackTier)
|
||||
}
|
||||
}
|
||||
|
||||
// A malformed durable policy must stop startup rather than silently mis-tier
|
||||
// every player, so these are load errors and not best-effort skips.
|
||||
func TestPostgreSQLInvalidTierPolicyIsRejectedRatherThanIgnored(t *testing.T) {
|
||||
db := openIntegrationPostgres(t)
|
||||
applyIntegrationMigrations(t, db)
|
||||
ctx := context.Background()
|
||||
|
||||
for name, mutate := range map[string]string{
|
||||
// NewTierPolicy requires the lowest band at or below zero; without it
|
||||
// a player below the floor has no tier at all.
|
||||
"no floor band": `DELETE FROM tier_bands WHERE tier = 'BRONZE'`,
|
||||
"unknown tier": `INSERT INTO tier_bands (tier, min_rating) VALUES ('MYTHIC', 3000)`,
|
||||
"provisional": `INSERT INTO tier_bands (tier, min_rating) VALUES ('PROVISIONAL', 2500)`,
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
applyIntegrationMigrations(t, db)
|
||||
if _, err := db.ExecContext(ctx, mutate); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := LoadTierPolicy(ctx, db); err == nil {
|
||||
t.Fatalf("%s was accepted as a durable tier policy", name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user