- test_evolution_models: 12 tests for EvolutionTrack, EvolutionCardState, EvolutionTierBoost, EvolutionCosmetic, and PlayerSeasonStats models - test_evolution_seed: 7 tests for seed idempotency, thresholds, formulas - test_season_stats_update: 6 tests for batting/pitching aggregation, Decision integration, double-count prevention, multi-game accumulation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
160 lines
6.2 KiB
Python
160 lines
6.2 KiB
Python
"""
|
|
Tests for app/seed/evolution_tracks.py — seed_evolution_tracks().
|
|
|
|
What: Verify that the JSON-driven seed function correctly creates, counts,
|
|
and idempotently updates EvolutionTrack rows in the database.
|
|
|
|
Why: The seed is the single source of truth for track configuration. A
|
|
regression here (duplicates, wrong thresholds, missing formula) would
|
|
silently corrupt evolution scoring for every card in the system.
|
|
|
|
Each test operates on a fresh in-memory SQLite database provided by the
|
|
autouse `setup_test_db` fixture in conftest.py. The seed reads its data
|
|
from `app/seed/evolution_tracks.json` on disk, so the tests also serve as
|
|
a light integration check between the JSON file and the Peewee model.
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from app.db_engine import EvolutionTrack
|
|
from app.seed.evolution_tracks import seed_evolution_tracks
|
|
|
|
# Path to the JSON fixture that the seed reads from at runtime
|
|
_JSON_PATH = Path(__file__).parent.parent / "app" / "seed" / "evolution_tracks.json"
|
|
|
|
|
|
@pytest.fixture
|
|
def json_tracks():
|
|
"""Load the raw JSON definitions so tests can assert against them.
|
|
|
|
This avoids hardcoding expected values — if the JSON changes, tests
|
|
automatically follow without needing manual updates.
|
|
"""
|
|
return json.loads(_JSON_PATH.read_text(encoding="utf-8"))
|
|
|
|
|
|
def test_seed_creates_three_tracks(json_tracks):
|
|
"""After one seed call, exactly 3 EvolutionTrack rows must exist.
|
|
|
|
Why: The JSON currently defines three card-type tracks (batter, sp, rp).
|
|
If the count is wrong the system would either be missing tracks
|
|
(evolution disabled for a card type) or have phantom extras.
|
|
"""
|
|
seed_evolution_tracks()
|
|
assert EvolutionTrack.select().count() == 3
|
|
|
|
|
|
def test_seed_correct_card_types(json_tracks):
|
|
"""The set of card_type values persisted must match the JSON exactly.
|
|
|
|
Why: card_type is used as a discriminator throughout the evolution engine.
|
|
An unexpected value (e.g. 'pitcher' instead of 'sp') would cause
|
|
track-lookup misses and silently skip evolution scoring for that role.
|
|
"""
|
|
seed_evolution_tracks()
|
|
expected_types = {d["card_type"] for d in json_tracks}
|
|
actual_types = {t.card_type for t in EvolutionTrack.select()}
|
|
assert actual_types == expected_types
|
|
|
|
|
|
def test_seed_thresholds_ascending():
|
|
"""For every track, t1 < t2 < t3 < t4.
|
|
|
|
Why: The evolution engine uses these thresholds to determine tier
|
|
boundaries. If they are not strictly ascending, tier comparisons
|
|
would produce incorrect or undefined results (e.g. a player could
|
|
simultaneously satisfy tier 3 and not satisfy tier 2).
|
|
"""
|
|
seed_evolution_tracks()
|
|
for track in EvolutionTrack.select():
|
|
assert (
|
|
track.t1_threshold < track.t2_threshold
|
|
), f"{track.name}: t1 ({track.t1_threshold}) >= t2 ({track.t2_threshold})"
|
|
assert (
|
|
track.t2_threshold < track.t3_threshold
|
|
), f"{track.name}: t2 ({track.t2_threshold}) >= t3 ({track.t3_threshold})"
|
|
assert (
|
|
track.t3_threshold < track.t4_threshold
|
|
), f"{track.name}: t3 ({track.t3_threshold}) >= t4 ({track.t4_threshold})"
|
|
|
|
|
|
def test_seed_thresholds_positive():
|
|
"""All tier threshold values must be strictly greater than zero.
|
|
|
|
Why: A zero or negative threshold would mean a card starts the game
|
|
already evolved (tier >= 1 at 0 accumulated stat points), which would
|
|
bypass the entire progression system.
|
|
"""
|
|
seed_evolution_tracks()
|
|
for track in EvolutionTrack.select():
|
|
assert track.t1_threshold > 0, f"{track.name}: t1_threshold is not positive"
|
|
assert track.t2_threshold > 0, f"{track.name}: t2_threshold is not positive"
|
|
assert track.t3_threshold > 0, f"{track.name}: t3_threshold is not positive"
|
|
assert track.t4_threshold > 0, f"{track.name}: t4_threshold is not positive"
|
|
|
|
|
|
def test_seed_formula_present():
|
|
"""Every persisted track must have a non-empty formula string.
|
|
|
|
Why: The formula is evaluated at runtime to compute a player's evolution
|
|
score. An empty formula would cause either a Python eval error or
|
|
silently produce 0 for every player, halting all evolution progress.
|
|
"""
|
|
seed_evolution_tracks()
|
|
for track in EvolutionTrack.select():
|
|
assert (
|
|
track.formula and track.formula.strip()
|
|
), f"{track.name}: formula is empty or whitespace-only"
|
|
|
|
|
|
def test_seed_idempotent():
|
|
"""Calling seed_evolution_tracks() twice must still yield exactly 3 rows.
|
|
|
|
Why: The seed is designed to be safe to re-run (e.g. as part of a
|
|
migration or CI bootstrap). If it inserts duplicates on a second call,
|
|
the unique constraint on EvolutionTrack.name would raise an IntegrityError
|
|
in PostgreSQL, and in SQLite it would silently create phantom rows that
|
|
corrupt tier-lookup joins.
|
|
"""
|
|
seed_evolution_tracks()
|
|
seed_evolution_tracks()
|
|
assert EvolutionTrack.select().count() == 3
|
|
|
|
|
|
def test_seed_updates_on_rerun(json_tracks):
|
|
"""A second seed call must restore any manually changed threshold to the JSON value.
|
|
|
|
What: Seed once, manually mutate a threshold in the DB, then seed again.
|
|
Assert that the threshold is now back to the JSON-defined value.
|
|
|
|
Why: The seed must act as the authoritative source of truth. If
|
|
re-seeding does not overwrite local changes, configuration drift can
|
|
build up silently and the production database would diverge from the
|
|
checked-in JSON without any visible error.
|
|
"""
|
|
seed_evolution_tracks()
|
|
|
|
# Pick the first track and corrupt its t1_threshold
|
|
first_def = json_tracks[0]
|
|
track = EvolutionTrack.get(EvolutionTrack.name == first_def["name"])
|
|
original_t1 = track.t1_threshold
|
|
corrupted_value = original_t1 + 9999
|
|
track.t1_threshold = corrupted_value
|
|
track.save()
|
|
|
|
# Confirm the corruption took effect before re-seeding
|
|
track_check = EvolutionTrack.get(EvolutionTrack.name == first_def["name"])
|
|
assert track_check.t1_threshold == corrupted_value
|
|
|
|
# Re-seed — should restore the JSON value
|
|
seed_evolution_tracks()
|
|
|
|
restored = EvolutionTrack.get(EvolutionTrack.name == first_def["name"])
|
|
assert restored.t1_threshold == first_def["t1_threshold"], (
|
|
f"Expected t1_threshold={first_def['t1_threshold']} after re-seed, "
|
|
f"got {restored.t1_threshold}"
|
|
)
|