""" Tests for app/seed/refractor_tracks.py — seed_refractor_tracks(). What: Verify that the JSON-driven seed function correctly creates, counts, and idempotently updates RefractorTrack 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 refractor 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/refractor_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 RefractorTrack from app.seed.refractor_tracks import seed_refractor_tracks # Path to the JSON fixture that the seed reads from at runtime _JSON_PATH = Path(__file__).parent.parent / "app" / "seed" / "refractor_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 RefractorTrack 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 (refractor disabled for a card type) or have phantom extras. """ seed_refractor_tracks() assert RefractorTrack.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 refractor engine. An unexpected value (e.g. 'pitcher' instead of 'sp') would cause track-lookup misses and silently skip refractor scoring for that role. """ seed_refractor_tracks() expected_types = {d["card_type"] for d in json_tracks} actual_types = {t.card_type for t in RefractorTrack.select()} assert actual_types == expected_types def test_seed_thresholds_ascending(): """For every track, t1 < t2 < t3 < t4. Why: The refractor 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_refractor_tracks() for track in RefractorTrack.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 refractor progression system. """ seed_refractor_tracks() for track in RefractorTrack.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 refractor score. An empty formula would cause either a Python eval error or silently produce 0 for every player, halting all refractor progress. """ seed_refractor_tracks() for track in RefractorTrack.select(): assert track.formula and track.formula.strip(), ( f"{track.name}: formula is empty or whitespace-only" ) def test_seed_idempotent(): """Calling seed_refractor_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 RefractorTrack.name would raise an IntegrityError in PostgreSQL, and in SQLite it would silently create phantom rows that corrupt tier-lookup joins. """ seed_refractor_tracks() seed_refractor_tracks() assert RefractorTrack.select().count() == 3 # --------------------------------------------------------------------------- # T1-4: Seed threshold ordering invariant (t1 < t2 < t3 < t4 + all positive) # --------------------------------------------------------------------------- def test_seed_all_thresholds_strictly_ascending_after_seed(): """After seeding, every track satisfies t1 < t2 < t3 < t4. What: Call seed_refractor_tracks(), then assert the full ordering chain t1 < t2 < t3 < t4 for every row in the database. Also assert that all four thresholds are strictly positive (> 0). Why: The refractor tier engine uses these thresholds as exclusive partition points. If any threshold is out-of-order or zero the tier assignment becomes incorrect or undefined. This test is the authoritative invariant guard; if a JSON edit accidentally violates the ordering this test fails loudly before any cards are affected. Separate from test_seed_thresholds_ascending which was written earlier — this test combines ordering + positivity into a single explicit assertion block and uses more descriptive messages to aid debugging. """ seed_refractor_tracks() for track in RefractorTrack.select(): assert track.t1_threshold > 0, ( f"{track.name}: t1_threshold={track.t1_threshold} is not positive" ) assert track.t2_threshold > 0, ( f"{track.name}: t2_threshold={track.t2_threshold} is not positive" ) assert track.t3_threshold > 0, ( f"{track.name}: t3_threshold={track.t3_threshold} is not positive" ) assert track.t4_threshold > 0, ( f"{track.name}: t4_threshold={track.t4_threshold} is not positive" ) assert ( track.t1_threshold < track.t2_threshold < track.t3_threshold < track.t4_threshold ), ( f"{track.name}: thresholds are not strictly ascending: " f"t1={track.t1_threshold}, t2={track.t2_threshold}, " f"t3={track.t3_threshold}, t4={track.t4_threshold}" ) # --------------------------------------------------------------------------- # T2-10: Duplicate card_type tracks guard # --------------------------------------------------------------------------- def test_seed_each_card_type_has_exactly_one_track(): """Each card_type must appear exactly once across all RefractorTrack rows. What: After seeding, group the rows by card_type and assert that every card_type has a count of exactly 1. Why: RefractorTrack rows are looked up by card_type (e.g. RefractorTrack.get(card_type='batter')). If a card_type appears more than once, Peewee's .get() raises MultipleObjectsReturned, crashing every pack opening and card evaluation for that type. This test acts as a uniqueness contract so that seed bugs or accidental DB drift surface immediately. """ seed_refractor_tracks() from peewee import fn as peewee_fn # Group by card_type and count occurrences query = ( RefractorTrack.select( RefractorTrack.card_type, peewee_fn.COUNT(RefractorTrack.id).alias("cnt") ) .group_by(RefractorTrack.card_type) .tuples() ) for card_type, count in query: assert count == 1, ( f"card_type={card_type!r} has {count} tracks; expected exactly 1" ) 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_refractor_tracks() # Pick the first track and corrupt its t1_threshold first_def = json_tracks[0] track = RefractorTrack.get(RefractorTrack.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 = RefractorTrack.get(RefractorTrack.name == first_def["name"]) assert track_check.t1_threshold == corrupted_value # Re-seed — should restore the JSON value seed_refractor_tracks() restored = RefractorTrack.get(RefractorTrack.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}" )