From b7dec3f23155fe32283cbd20412656c6e5b0e65b Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Mon, 23 Mar 2026 13:31:55 -0500 Subject: [PATCH] refactor: rename evolution system to refractor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete rename of the card progression system from "Evolution" to "Refractor" across all code, routes, models, services, seeds, and tests. - Route prefix: /api/v2/evolution → /api/v2/refractor - Model classes: EvolutionTrack → RefractorTrack, etc. - 12 files renamed, 8 files content-edited - New migration to rename DB tables - 117 tests pass, no logic changes Co-Authored-By: Claude Opus 4.6 (1M context) --- app/db_engine.py | 44 ++++----- app/main.py | 4 +- app/routers_v2/cards.py | 39 +++++--- app/routers_v2/{evolution.py => refractor.py} | 58 ++++++------ app/routers_v2/teams.py | 23 +++-- ...tion_tracks.json => refractor_tracks.json} | 0 ...volution_tracks.py => refractor_tracks.py} | 24 ++--- app/services/formula_engine.py | 6 +- ...on_evaluator.py => refractor_evaluator.py} | 20 ++-- .../{evolution_init.py => refractor_init.py} | 36 +++---- ...26-03-23_rename_evolution_to_refractor.sql | 15 +++ tests/conftest.py | 22 ++--- tests/test_formula_engine.py | 2 +- ...volution.py => test_postgame_refractor.py} | 62 ++++++------ ...aluator.py => test_refractor_evaluator.py} | 16 ++-- ...olution_init.py => test_refractor_init.py} | 40 ++++---- ...ion_models.py => test_refractor_models.py} | 88 ++++++++--------- ...olution_seed.py => test_refractor_seed.py} | 94 +++++++++---------- ...ate_api.py => test_refractor_state_api.py} | 78 +++++++-------- ...ack_api.py => test_refractor_track_api.py} | 24 ++--- 20 files changed, 362 insertions(+), 333 deletions(-) rename app/routers_v2/{evolution.py => refractor.py} (79%) rename app/seed/{evolution_tracks.json => refractor_tracks.json} (100%) rename app/seed/{evolution_tracks.py => refractor_tracks.py} (72%) rename app/services/{evolution_evaluator.py => refractor_evaluator.py} (91%) rename app/services/{evolution_init.py => refractor_init.py} (74%) create mode 100644 migrations/2026-03-23_rename_evolution_to_refractor.sql rename tests/{test_postgame_evolution.py => test_postgame_refractor.py} (93%) rename tests/{test_evolution_evaluator.py => test_refractor_evaluator.py} (96%) rename tests/{test_evolution_init.py => test_refractor_init.py} (91%) rename tests/{test_evolution_models.py => test_refractor_models.py} (83%) rename tests/{test_evolution_seed.py => test_refractor_seed.py} (63%) rename tests/{test_evolution_state_api.py => test_refractor_state_api.py} (88%) rename tests/{test_evolution_track_api.py => test_refractor_track_api.py} (85%) diff --git a/app/db_engine.py b/app/db_engine.py index 0b44ed1..ce4e999 100644 --- a/app/db_engine.py +++ b/app/db_engine.py @@ -1210,7 +1210,7 @@ if not SKIP_TABLE_CREATION: db.create_tables([ScoutOpportunity, ScoutClaim], safe=True) -class EvolutionTrack(BaseModel): +class RefractorTrack(BaseModel): name = CharField(unique=True) card_type = CharField() # 'batter', 'sp', 'rp' formula = CharField() # e.g. "pa + tb * 2" @@ -1221,13 +1221,13 @@ class EvolutionTrack(BaseModel): class Meta: database = db - table_name = "evolution_track" + table_name = "refractor_track" -class EvolutionCardState(BaseModel): +class RefractorCardState(BaseModel): player = ForeignKeyField(Player) team = ForeignKeyField(Team) - track = ForeignKeyField(EvolutionTrack) + track = ForeignKeyField(RefractorTrack) current_tier = IntegerField(default=0) # 0-4 current_value = FloatField(default=0.0) fully_evolved = BooleanField(default=False) @@ -1235,19 +1235,19 @@ class EvolutionCardState(BaseModel): class Meta: database = db - table_name = "evolution_card_state" + table_name = "refractor_card_state" -evolution_card_state_index = ModelIndex( - EvolutionCardState, - (EvolutionCardState.player, EvolutionCardState.team), +refractor_card_state_index = ModelIndex( + RefractorCardState, + (RefractorCardState.player, RefractorCardState.team), unique=True, ) -EvolutionCardState.add_index(evolution_card_state_index) +RefractorCardState.add_index(refractor_card_state_index) -class EvolutionTierBoost(BaseModel): - track = ForeignKeyField(EvolutionTrack) +class RefractorTierBoost(BaseModel): + track = ForeignKeyField(RefractorTrack) tier = IntegerField() # 1-4 boost_type = CharField() # e.g. 'rating', 'stat' boost_target = CharField() # e.g. 'contact_vl', 'power_vr' @@ -1255,23 +1255,23 @@ class EvolutionTierBoost(BaseModel): class Meta: database = db - table_name = "evolution_tier_boost" + table_name = "refractor_tier_boost" -evolution_tier_boost_index = ModelIndex( - EvolutionTierBoost, +refractor_tier_boost_index = ModelIndex( + RefractorTierBoost, ( - EvolutionTierBoost.track, - EvolutionTierBoost.tier, - EvolutionTierBoost.boost_type, - EvolutionTierBoost.boost_target, + RefractorTierBoost.track, + RefractorTierBoost.tier, + RefractorTierBoost.boost_type, + RefractorTierBoost.boost_target, ), unique=True, ) -EvolutionTierBoost.add_index(evolution_tier_boost_index) +RefractorTierBoost.add_index(refractor_tier_boost_index) -class EvolutionCosmetic(BaseModel): +class RefractorCosmetic(BaseModel): name = CharField(unique=True) tier_required = IntegerField(default=0) cosmetic_type = CharField() # 'frame', 'badge', 'theme' @@ -1280,12 +1280,12 @@ class EvolutionCosmetic(BaseModel): class Meta: database = db - table_name = "evolution_cosmetic" + table_name = "refractor_cosmetic" if not SKIP_TABLE_CREATION: db.create_tables( - [EvolutionTrack, EvolutionCardState, EvolutionTierBoost, EvolutionCosmetic], + [RefractorTrack, RefractorCardState, RefractorTierBoost, RefractorCosmetic], safe=True, ) diff --git a/app/main.py b/app/main.py index 2949642..2e6de86 100644 --- a/app/main.py +++ b/app/main.py @@ -51,7 +51,7 @@ from .routers_v2 import ( # noqa: E402 stratplays, scout_opportunities, scout_claims, - evolution, + refractor, season_stats, ) @@ -107,7 +107,7 @@ app.include_router(stratplays.router) app.include_router(decisions.router) app.include_router(scout_opportunities.router) app.include_router(scout_claims.router) -app.include_router(evolution.router) +app.include_router(refractor.router) app.include_router(season_stats.router) diff --git a/app/routers_v2/cards.py b/app/routers_v2/cards.py index a8614fc..b814c51 100644 --- a/app/routers_v2/cards.py +++ b/app/routers_v2/cards.py @@ -4,9 +4,19 @@ import logging import pydantic from pandas import DataFrame -from ..db_engine import db, Card, model_to_dict, Team, Player, Pack, Paperdex, CARDSETS, DoesNotExist +from ..db_engine import ( + db, + Card, + model_to_dict, + Team, + Player, + Pack, + Paperdex, + CARDSETS, + DoesNotExist, +) from ..dependencies import oauth2_scheme, valid_token -from ..services.evolution_init import _determine_card_type, initialize_card_evolution +from ..services.refractor_init import _determine_card_type, initialize_card_evolution router = APIRouter(prefix="/api/v2/cards", tags=["cards"]) @@ -47,19 +57,25 @@ async def get_cards( try: this_team = Team.get_by_id(team_id) except DoesNotExist: - raise HTTPException(status_code=404, detail=f'No team found with id {team_id}') + raise HTTPException( + status_code=404, detail=f"No team found with id {team_id}" + ) all_cards = all_cards.where(Card.team == this_team) if player_id is not None: try: this_player = Player.get_by_id(player_id) except DoesNotExist: - raise HTTPException(status_code=404, detail=f'No player found with id {player_id}') + raise HTTPException( + status_code=404, detail=f"No player found with id {player_id}" + ) all_cards = all_cards.where(Card.player == this_player) if pack_id is not None: try: this_pack = Pack.get_by_id(pack_id) except DoesNotExist: - raise HTTPException(status_code=404, detail=f'No pack found with id {pack_id}') + raise HTTPException( + status_code=404, detail=f"No pack found with id {pack_id}" + ) all_cards = all_cards.where(Card.pack == this_pack) if value is not None: all_cards = all_cards.where(Card.value == value) @@ -125,7 +141,6 @@ async def get_cards( dex_by_player.setdefault(row.player_id, []).append(row) return_val = {"count": len(card_list), "cards": []} for x in card_list: - this_record = model_to_dict(x) logging.debug(f"this_record: {this_record}") @@ -147,7 +162,7 @@ async def v1_cards_get_one(card_id, csv: Optional[bool] = False): try: this_card = Card.get_by_id(card_id) except DoesNotExist: - raise HTTPException(status_code=404, detail=f'No card found with id {card_id}') + raise HTTPException(status_code=404, detail=f"No card found with id {card_id}") if csv: data_list = [ @@ -215,7 +230,7 @@ async def v1_cards_post(cards: CardModel, token: str = Depends(oauth2_scheme)): initialize_card_evolution(x.player_id, x.team_id, card_type) except Exception: logging.exception( - "evolution hook: unexpected error for player_id=%s team_id=%s", + "refractor hook: unexpected error for player_id=%s team_id=%s", x.player_id, x.team_id, ) @@ -319,8 +334,8 @@ async def v1_cards_wipe_team(team_id: int, token: str = Depends(oauth2_scheme)): try: this_team = Team.get_by_id(team_id) except DoesNotExist: - logging.error(f'/cards/wipe-team/{team_id} - could not find team') - raise HTTPException(status_code=404, detail=f'Team {team_id} not found') + logging.error(f"/cards/wipe-team/{team_id} - could not find team") + raise HTTPException(status_code=404, detail=f"Team {team_id} not found") t_query = Card.update(team=None).where(Card.team == this_team).execute() return f"Wiped {t_query} cards" @@ -348,7 +363,7 @@ async def v1_cards_patch( try: this_card = Card.get_by_id(card_id) except DoesNotExist: - raise HTTPException(status_code=404, detail=f'No card found with id {card_id}') + raise HTTPException(status_code=404, detail=f"No card found with id {card_id}") if player_id is not None: this_card.player_id = player_id @@ -391,7 +406,7 @@ async def v1_cards_delete(card_id, token: str = Depends(oauth2_scheme)): try: this_card = Card.get_by_id(card_id) except DoesNotExist: - raise HTTPException(status_code=404, detail=f'No cards found with id {card_id}') + raise HTTPException(status_code=404, detail=f"No cards found with id {card_id}") count = this_card.delete_instance() diff --git a/app/routers_v2/evolution.py b/app/routers_v2/refractor.py similarity index 79% rename from app/routers_v2/evolution.py rename to app/routers_v2/refractor.py index d08e528..45497ee 100644 --- a/app/routers_v2/evolution.py +++ b/app/routers_v2/refractor.py @@ -7,10 +7,10 @@ from ..dependencies import oauth2_scheme, valid_token logger = logging.getLogger(__name__) -router = APIRouter(prefix="/api/v2/evolution", tags=["evolution"]) +router = APIRouter(prefix="/api/v2/refractor", tags=["refractor"]) # Tier -> threshold attribute name. Index = current_tier; value is the -# attribute on EvolutionTrack whose value is the *next* threshold to reach. +# attribute on RefractorTrack whose value is the *next* threshold to reach. # Tier 4 is fully evolved so there is no next threshold (None sentinel). _NEXT_THRESHOLD_ATTR = { 0: "t1_threshold", @@ -22,7 +22,7 @@ _NEXT_THRESHOLD_ATTR = { def _build_card_state_response(state) -> dict: - """Serialise an EvolutionCardState into the standard API response shape. + """Serialise a RefractorCardState into the standard API response shape. Produces a flat dict with player_id and team_id as plain integers, a nested 'track' dict with all threshold fields, and a computed @@ -63,11 +63,11 @@ async def list_tracks( logging.warning("Bad Token: [REDACTED]") raise HTTPException(status_code=401, detail="Unauthorized") - from ..db_engine import EvolutionTrack + from ..db_engine import RefractorTrack - query = EvolutionTrack.select() + query = RefractorTrack.select() if card_type is not None: - query = query.where(EvolutionTrack.card_type == card_type) + query = query.where(RefractorTrack.card_type == card_type) items = [model_to_dict(t, recurse=False) for t in query] return {"count": len(items), "items": items} @@ -79,10 +79,10 @@ async def get_track(track_id: int, token: str = Depends(oauth2_scheme)): logging.warning("Bad Token: [REDACTED]") raise HTTPException(status_code=401, detail="Unauthorized") - from ..db_engine import EvolutionTrack + from ..db_engine import RefractorTrack try: - track = EvolutionTrack.get_by_id(track_id) + track = RefractorTrack.get_by_id(track_id) except Exception: raise HTTPException(status_code=404, detail=f"Track {track_id} not found") @@ -91,22 +91,22 @@ async def get_track(track_id: int, token: str = Depends(oauth2_scheme)): @router.get("/cards/{card_id}") async def get_card_state(card_id: int, token: str = Depends(oauth2_scheme)): - """Return the EvolutionCardState for a card identified by its Card.id. + """Return the RefractorCardState for a card identified by its Card.id. Resolves card_id -> (player_id, team_id) via the Card table, then looks - up the matching EvolutionCardState row. Because duplicate cards for the + up the matching RefractorCardState row. Because duplicate cards for the same player+team share one state row (unique-(player,team) constraint), any card_id belonging to that player on that team returns the same state. Returns 404 when: - The card_id does not exist in the Card table. - - The card exists but has no corresponding EvolutionCardState yet. + - The card exists but has no corresponding RefractorCardState yet. """ if not valid_token(token): logging.warning("Bad Token: [REDACTED]") raise HTTPException(status_code=401, detail="Unauthorized") - from ..db_engine import Card, EvolutionCardState, EvolutionTrack, DoesNotExist + from ..db_engine import Card, RefractorCardState, RefractorTrack, DoesNotExist # Resolve card_id to player+team try: @@ -114,22 +114,22 @@ async def get_card_state(card_id: int, token: str = Depends(oauth2_scheme)): except DoesNotExist: raise HTTPException(status_code=404, detail=f"Card {card_id} not found") - # Look up the evolution state for this (player, team) pair, joining the + # Look up the refractor state for this (player, team) pair, joining the # track so a single query resolves both rows. try: state = ( - EvolutionCardState.select(EvolutionCardState, EvolutionTrack) - .join(EvolutionTrack) + RefractorCardState.select(RefractorCardState, RefractorTrack) + .join(RefractorTrack) .where( - (EvolutionCardState.player == card.player_id) - & (EvolutionCardState.team == card.team_id) + (RefractorCardState.player == card.player_id) + & (RefractorCardState.team == card.team_id) ) .get() ) except DoesNotExist: raise HTTPException( status_code=404, - detail=f"No evolution state for card {card_id}", + detail=f"No refractor state for card {card_id}", ) return _build_card_state_response(state) @@ -137,9 +137,9 @@ async def get_card_state(card_id: int, token: str = Depends(oauth2_scheme)): @router.post("/cards/{card_id}/evaluate") async def evaluate_card(card_id: int, token: str = Depends(oauth2_scheme)): - """Force-recalculate evolution state for a card from career stats. + """Force-recalculate refractor state for a card from career stats. - Resolves card_id to (player_id, team_id), then recomputes the evolution + Resolves card_id to (player_id, team_id), then recomputes the refractor tier from all player_season_stats rows for that pair. Idempotent. """ if not valid_token(token): @@ -147,7 +147,7 @@ async def evaluate_card(card_id: int, token: str = Depends(oauth2_scheme)): raise HTTPException(status_code=401, detail="Unauthorized") from ..db_engine import Card - from ..services.evolution_evaluator import evaluate_card as _evaluate + from ..services.refractor_evaluator import evaluate_card as _evaluate try: card = Card.get_by_id(card_id) @@ -164,10 +164,10 @@ async def evaluate_card(card_id: int, token: str = Depends(oauth2_scheme)): @router.post("/evaluate-game/{game_id}") async def evaluate_game(game_id: int, token: str = Depends(oauth2_scheme)): - """Evaluate evolution state for all players who appeared in a game. + """Evaluate refractor state for all players who appeared in a game. Finds all unique (player_id, team_id) pairs from the game's StratPlay rows, - then for each pair that has an EvolutionCardState, re-computes the evolution + then for each pair that has a RefractorCardState, re-computes the refractor tier. Pairs without a state row are silently skipped. Per-player errors are logged but do not abort the batch. """ @@ -175,8 +175,8 @@ async def evaluate_game(game_id: int, token: str = Depends(oauth2_scheme)): logging.warning("Bad Token: [REDACTED]") raise HTTPException(status_code=401, detail="Unauthorized") - from ..db_engine import EvolutionCardState, EvolutionTrack, Player, StratPlay - from ..services.evolution_evaluator import evaluate_card + from ..db_engine import RefractorCardState, RefractorTrack, Player, StratPlay + from ..services.refractor_evaluator import evaluate_card plays = list(StratPlay.select().where(StratPlay.game == game_id)) @@ -192,9 +192,9 @@ async def evaluate_game(game_id: int, token: str = Depends(oauth2_scheme)): for player_id, team_id in pairs: try: - state = EvolutionCardState.get_or_none( - (EvolutionCardState.player_id == player_id) - & (EvolutionCardState.team_id == team_id) + state = RefractorCardState.get_or_none( + (RefractorCardState.player_id == player_id) + & (RefractorCardState.team_id == team_id) ) if state is None: continue @@ -225,7 +225,7 @@ async def evaluate_game(game_id: int, token: str = Depends(oauth2_scheme)): ) except Exception as exc: logger.warning( - f"Evolution eval failed for player={player_id} team={team_id}: {exc}" + f"Refractor eval failed for player={player_id} team={team_id}: {exc}" ) return {"evaluated": evaluated, "tier_ups": tier_ups} diff --git a/app/routers_v2/teams.py b/app/routers_v2/teams.py index 58394f7..e630c9a 100644 --- a/app/routers_v2/teams.py +++ b/app/routers_v2/teams.py @@ -1049,7 +1049,6 @@ async def team_buy_players(team_id: int, ids: str, ts: str): detail=f"You are not authorized to buy {this_team.abbrev} cards. This event has been logged.", ) - all_ids = ids.split(",") conf_message = "" total_cost = 0 @@ -1540,9 +1539,9 @@ async def list_team_evolutions( per_page: int = Query(default=10, ge=1, le=100), token: str = Depends(oauth2_scheme), ): - """List all EvolutionCardState rows for a team, with optional filters. + """List all RefractorCardState rows for a team, with optional filters. - Joins EvolutionCardState to EvolutionTrack so that card_type filtering + Joins RefractorCardState to RefractorTrack so that card_type filtering works without a second query. Results are paginated via page/per_page (1-indexed pages); items are ordered by player_id for stable ordering. @@ -1555,27 +1554,27 @@ async def list_team_evolutions( Response shape: {"count": N, "items": [card_state_with_threshold_context, ...]} - Each item in 'items' has the same shape as GET /evolution/cards/{card_id}. + Each item in 'items' has the same shape as GET /refractor/cards/{card_id}. """ if not valid_token(token): logging.warning("Bad Token: [REDACTED]") raise HTTPException(status_code=401, detail="Unauthorized") - from ..db_engine import EvolutionCardState, EvolutionTrack - from ..routers_v2.evolution import _build_card_state_response + from ..db_engine import RefractorCardState, RefractorTrack + from ..routers_v2.refractor import _build_card_state_response query = ( - EvolutionCardState.select(EvolutionCardState, EvolutionTrack) - .join(EvolutionTrack) - .where(EvolutionCardState.team == team_id) - .order_by(EvolutionCardState.player_id) + RefractorCardState.select(RefractorCardState, RefractorTrack) + .join(RefractorTrack) + .where(RefractorCardState.team == team_id) + .order_by(RefractorCardState.player_id) ) if card_type is not None: - query = query.where(EvolutionTrack.card_type == card_type) + query = query.where(RefractorTrack.card_type == card_type) if tier is not None: - query = query.where(EvolutionCardState.current_tier == tier) + query = query.where(RefractorCardState.current_tier == tier) total = query.count() offset = (page - 1) * per_page diff --git a/app/seed/evolution_tracks.json b/app/seed/refractor_tracks.json similarity index 100% rename from app/seed/evolution_tracks.json rename to app/seed/refractor_tracks.json diff --git a/app/seed/evolution_tracks.py b/app/seed/refractor_tracks.py similarity index 72% rename from app/seed/evolution_tracks.py rename to app/seed/refractor_tracks.py index 3314a97..14035f0 100644 --- a/app/seed/evolution_tracks.py +++ b/app/seed/refractor_tracks.py @@ -1,36 +1,36 @@ -"""Seed script for EvolutionTrack records. +"""Seed script for RefractorTrack records. -Loads track definitions from evolution_tracks.json and upserts them into the +Loads track definitions from refractor_tracks.json and upserts them into the database using get_or_create keyed on name. Existing tracks have their thresholds and formula updated to match the JSON in case values have changed. Can be run standalone: - python -m app.seed.evolution_tracks + python -m app.seed.refractor_tracks """ import json import logging from pathlib import Path -from app.db_engine import EvolutionTrack +from app.db_engine import RefractorTrack logger = logging.getLogger(__name__) -_JSON_PATH = Path(__file__).parent / "evolution_tracks.json" +_JSON_PATH = Path(__file__).parent / "refractor_tracks.json" -def seed_evolution_tracks() -> list[EvolutionTrack]: - """Upsert evolution tracks from JSON seed data. +def seed_refractor_tracks() -> list[RefractorTrack]: + """Upsert refractor tracks from JSON seed data. - Returns a list of EvolutionTrack instances that were created or updated. + Returns a list of RefractorTrack instances that were created or updated. """ raw = _JSON_PATH.read_text(encoding="utf-8") track_defs = json.loads(raw) - results: list[EvolutionTrack] = [] + results: list[RefractorTrack] = [] for defn in track_defs: - track, created = EvolutionTrack.get_or_create( + track, created = RefractorTrack.get_or_create( name=defn["name"], defaults={ "card_type": defn["card_type"], @@ -61,6 +61,6 @@ def seed_evolution_tracks() -> list[EvolutionTrack]: if __name__ == "__main__": logging.basicConfig(level=logging.INFO) - logger.info("Seeding evolution tracks...") - tracks = seed_evolution_tracks() + logger.info("Seeding refractor tracks...") + tracks = seed_refractor_tracks() logger.info("Done. %d track(s) processed.", len(tracks)) diff --git a/app/services/formula_engine.py b/app/services/formula_engine.py index 2e55a65..a65fbd1 100644 --- a/app/services/formula_engine.py +++ b/app/services/formula_engine.py @@ -1,6 +1,6 @@ -"""Formula engine for evolution value computation (WP-09). +"""Formula engine for refractor value computation (WP-09). -Three pure functions that compute a numeric evolution value from career stats, +Three pure functions that compute a numeric refractor value from career stats, plus helpers for formula dispatch and tier classification. Stats attributes expected by each formula: @@ -79,7 +79,7 @@ def compute_value_for_track(card_type: str, stats) -> float: def tier_from_value(value: float, track) -> int: - """Return the evolution tier (0-4) for a computed value against a track. + """Return the refractor tier (0-4) for a computed value against a track. Tier boundaries are inclusive on the lower end: T0: value < t1 diff --git a/app/services/evolution_evaluator.py b/app/services/refractor_evaluator.py similarity index 91% rename from app/services/evolution_evaluator.py rename to app/services/refractor_evaluator.py index fa5961e..9c63be5 100644 --- a/app/services/evolution_evaluator.py +++ b/app/services/refractor_evaluator.py @@ -1,6 +1,6 @@ -"""Evolution evaluator service (WP-08). +"""Refractor evaluator service (WP-08). -Force-recalculates a card's evolution state from career totals. +Force-recalculates a card's refractor state from career totals. evaluate_card() is the main entry point: 1. Load career totals: SUM all BattingSeasonStats/PitchingSeasonStats rows for (player_id, team_id) @@ -14,7 +14,7 @@ evaluate_card() is the main entry point: Idempotent: calling multiple times with the same data produces the same result. -Depends on WP-05 (EvolutionCardState), WP-07 (BattingSeasonStats/PitchingSeasonStats), +Depends on WP-05 (RefractorCardState), WP-07 (BattingSeasonStats/PitchingSeasonStats), and WP-09 (formula engine). Models and formula functions are imported lazily so this module can be imported before those PRs merge. """ @@ -52,11 +52,11 @@ def evaluate_card( _compute_value_fn=None, _tier_from_value_fn=None, ) -> dict: - """Force-recalculate a card's evolution tier from career stats. + """Force-recalculate a card's refractor tier from career stats. Sums all BattingSeasonStats or PitchingSeasonStats rows (based on card_type) for (player_id, team_id) across all seasons, then delegates - formula computation and tier classification to the formula engine. The result is written back to evolution_card_state and + formula computation and tier classification to the formula engine. The result is written back to refractor_card_state and returned as a dict. current_tier never decreases (no regression): @@ -67,7 +67,7 @@ def evaluate_card( team_id: Team primary key. _stats_model: Override for BattingSeasonStats/PitchingSeasonStats (used in tests to inject a stub model with all stat fields). - _state_model: Override for EvolutionCardState (used in tests to avoid + _state_model: Override for RefractorCardState (used in tests to avoid importing from db_engine before WP-05 merges). _compute_value_fn: Override for formula_engine.compute_value_for_track (used in tests to avoid importing formula_engine before WP-09 merges). @@ -79,10 +79,10 @@ def evaluate_card( last_evaluated_at (ISO-8601 string). Raises: - ValueError: If no evolution_card_state row exists for (player_id, team_id). + ValueError: If no refractor_card_state row exists for (player_id, team_id). """ if _state_model is None: - from app.db_engine import EvolutionCardState as _state_model # noqa: PLC0415 + from app.db_engine import RefractorCardState as _state_model # noqa: PLC0415 if _compute_value_fn is None or _tier_from_value_fn is None: from app.services.formula_engine import ( # noqa: PLC0415 @@ -101,7 +101,7 @@ def evaluate_card( ) if card_state is None: raise ValueError( - f"No evolution_card_state for player_id={player_id} team_id={team_id}" + f"No refractor_card_state for player_id={player_id} team_id={team_id}" ) # 2. Load career totals from the appropriate season stats table @@ -178,7 +178,7 @@ def evaluate_card( card_state.save() logging.debug( - "evolution_eval: player=%s team=%s value=%.2f tier=%s fully_evolved=%s", + "refractor_eval: player=%s team=%s value=%.2f tier=%s fully_evolved=%s", player_id, team_id, value, diff --git a/app/services/evolution_init.py b/app/services/refractor_init.py similarity index 74% rename from app/services/evolution_init.py rename to app/services/refractor_init.py index cac9b7b..9e49c9f 100644 --- a/app/services/evolution_init.py +++ b/app/services/refractor_init.py @@ -1,10 +1,10 @@ """ -WP-10: Pack opening hook — evolution_card_state initialization. +WP-10: Pack opening hook — refractor_card_state initialization. Public API ---------- initialize_card_evolution(player_id, team_id, card_type) - Get-or-create an EvolutionCardState for the (player_id, team_id) pair. + Get-or-create a RefractorCardState for the (player_id, team_id) pair. Returns the state instance on success, or None if initialization fails (missing track, integrity error, etc.). Never raises. @@ -16,23 +16,23 @@ Design notes ------------ - The function is intentionally fire-and-forget from the caller's perspective. All exceptions are caught and logged; pack opening is never blocked. -- No EvolutionProgress rows are created here. Progress accumulation is a +- No RefractorProgress rows are created here. Progress accumulation is a separate concern handled by the stats-update pipeline (WP-07/WP-08). - AI teams and Gauntlet teams skip Paperdex insertion (cards.py pattern); - we do NOT replicate that exclusion here — all teams get an evolution state + we do NOT replicate that exclusion here — all teams get a refractor state so that future rule changes don't require back-filling. """ import logging from typing import Optional -from app.db_engine import DoesNotExist, EvolutionCardState, EvolutionTrack +from app.db_engine import DoesNotExist, RefractorCardState, RefractorTrack logger = logging.getLogger(__name__) def _determine_card_type(player) -> str: - """Map a player's primary position to an evolution card_type string. + """Map a player's primary position to a refractor card_type string. Rules (from WP-10 spec): - pos_1 contains 'SP' -> 'sp' @@ -57,30 +57,30 @@ def initialize_card_evolution( player_id: int, team_id: int, card_type: str, -) -> Optional[EvolutionCardState]: - """Get-or-create an EvolutionCardState for a newly acquired card. +) -> Optional[RefractorCardState]: + """Get-or-create a RefractorCardState for a newly acquired card. Called by the cards POST endpoint after each card is inserted. The function is idempotent: if a state row already exists for the (player_id, team_id) pair it is returned unchanged — existing - evolution progress is never reset. + refractor progress is never reset. Args: player_id: Primary key of the Player row (Player.player_id). team_id: Primary key of the Team row (Team.id). card_type: One of 'batter', 'sp', 'rp'. Determines which - EvolutionTrack is assigned to the new state. + RefractorTrack is assigned to the new state. Returns: - The existing or newly created EvolutionCardState instance, or + The existing or newly created RefractorCardState instance, or None if initialization could not complete (missing track seed data, unexpected DB error, etc.). """ try: - track = EvolutionTrack.get(EvolutionTrack.card_type == card_type) + track = RefractorTrack.get(RefractorTrack.card_type == card_type) except DoesNotExist: logger.warning( - "evolution_init: no EvolutionTrack found for card_type=%r " + "refractor_init: no RefractorTrack found for card_type=%r " "(player_id=%s, team_id=%s) — skipping state creation", card_type, player_id, @@ -89,7 +89,7 @@ def initialize_card_evolution( return None except Exception: logger.exception( - "evolution_init: unexpected error fetching track " + "refractor_init: unexpected error fetching track " "(card_type=%r, player_id=%s, team_id=%s)", card_type, player_id, @@ -98,7 +98,7 @@ def initialize_card_evolution( return None try: - state, created = EvolutionCardState.get_or_create( + state, created = RefractorCardState.get_or_create( player_id=player_id, team_id=team_id, defaults={ @@ -110,7 +110,7 @@ def initialize_card_evolution( ) if created: logger.debug( - "evolution_init: created EvolutionCardState id=%s " + "refractor_init: created RefractorCardState id=%s " "(player_id=%s, team_id=%s, card_type=%r)", state.id, player_id, @@ -119,7 +119,7 @@ def initialize_card_evolution( ) else: logger.debug( - "evolution_init: state already exists id=%s " + "refractor_init: state already exists id=%s " "(player_id=%s, team_id=%s) — no-op", state.id, player_id, @@ -129,7 +129,7 @@ def initialize_card_evolution( except Exception: logger.exception( - "evolution_init: failed to get_or_create state " + "refractor_init: failed to get_or_create state " "(player_id=%s, team_id=%s, card_type=%r)", player_id, team_id, diff --git a/migrations/2026-03-23_rename_evolution_to_refractor.sql b/migrations/2026-03-23_rename_evolution_to_refractor.sql new file mode 100644 index 0000000..c2a3d3f --- /dev/null +++ b/migrations/2026-03-23_rename_evolution_to_refractor.sql @@ -0,0 +1,15 @@ +-- Migration: Rename evolution tables to refractor tables +-- Date: 2026-03-23 +-- +-- Renames all four evolution system tables to the refractor naming convention. +-- This migration corresponds to the application-level rename from +-- EvolutionTrack/EvolutionCardState/EvolutionTierBoost/EvolutionCosmetic +-- to RefractorTrack/RefractorCardState/RefractorTierBoost/RefractorCosmetic. +-- +-- The table renames are performed in order that respects foreign key +-- dependencies (referenced tables first, then referencing tables). + +ALTER TABLE evolution_track RENAME TO refractor_track; +ALTER TABLE evolution_card_state RENAME TO refractor_card_state; +ALTER TABLE evolution_tier_boost RENAME TO refractor_tier_boost; +ALTER TABLE evolution_cosmetic RENAME TO refractor_cosmetic; diff --git a/tests/conftest.py b/tests/conftest.py index 22b3d10..9d9a6a7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -44,10 +44,10 @@ from app.db_engine import ( BattingSeasonStats, PitchingSeasonStats, ProcessedGame, - EvolutionTrack, - EvolutionCardState, - EvolutionTierBoost, - EvolutionCosmetic, + RefractorTrack, + RefractorCardState, + RefractorTierBoost, + RefractorCosmetic, ScoutOpportunity, ScoutClaim, ) @@ -76,10 +76,10 @@ _TEST_MODELS = [ ProcessedGame, ScoutOpportunity, ScoutClaim, - EvolutionTrack, - EvolutionCardState, - EvolutionTierBoost, - EvolutionCosmetic, + RefractorTrack, + RefractorCardState, + RefractorTierBoost, + RefractorCosmetic, ] @@ -164,8 +164,8 @@ def team(): @pytest.fixture def track(): - """A minimal EvolutionTrack for batter cards.""" - return EvolutionTrack.create( + """A minimal RefractorTrack for batter cards.""" + return RefractorTrack.create( name="Batter Track", card_type="batter", formula="pa + tb * 2", @@ -177,7 +177,7 @@ def track(): # --------------------------------------------------------------------------- -# PostgreSQL integration fixture (used by test_evolution_*_api.py) +# PostgreSQL integration fixture (used by test_refractor_*_api.py) # --------------------------------------------------------------------------- diff --git a/tests/test_formula_engine.py b/tests/test_formula_engine.py index 435cd92..d02504b 100644 --- a/tests/test_formula_engine.py +++ b/tests/test_formula_engine.py @@ -3,7 +3,7 @@ Unit tests only — no database required. Stats inputs are simple namespace objects whose attributes match what BattingSeasonStats/PitchingSeasonStats expose. -Tier thresholds used (from evolution_tracks.json seed data): +Tier thresholds used (from refractor_tracks.json seed data): Batter: t1=37, t2=149, t3=448, t4=896 SP: t1=10, t2=40, t3=120, t4=240 RP: t1=3, t2=12, t3=35, t4=70 diff --git a/tests/test_postgame_evolution.py b/tests/test_postgame_refractor.py similarity index 93% rename from tests/test_postgame_evolution.py rename to tests/test_postgame_refractor.py index 21671e8..894f5fc 100644 --- a/tests/test_postgame_evolution.py +++ b/tests/test_postgame_refractor.py @@ -2,7 +2,7 @@ Tests cover both post-game callback endpoints: POST /api/v2/season-stats/update-game/{game_id} - POST /api/v2/evolution/evaluate-game/{game_id} + POST /api/v2/refractor/evaluate-game/{game_id} All tests run against a named shared-memory SQLite database so that Peewee model queries inside the route handlers (which execute in the TestClient's @@ -34,7 +34,7 @@ Test matrix: test_evaluate_game_tier_ups_in_response Tier-up appears in tier_ups list with correct fields. test_evaluate_game_skips_players_without_state - Players in game but without EvolutionCardState are silently skipped. + Players in game but without RefractorCardState are silently skipped. test_auth_required_update_game Missing bearer token returns 401 on update-game. test_auth_required_evaluate_game @@ -55,10 +55,10 @@ from peewee import SqliteDatabase from app.db_engine import ( Cardset, - EvolutionCardState, - EvolutionCosmetic, - EvolutionTierBoost, - EvolutionTrack, + RefractorCardState, + RefractorCosmetic, + RefractorTierBoost, + RefractorTrack, MlbPlayer, Pack, PackType, @@ -111,10 +111,10 @@ _WP13_MODELS = [ BattingSeasonStats, PitchingSeasonStats, ProcessedGame, - EvolutionTrack, - EvolutionCardState, - EvolutionTierBoost, - EvolutionCosmetic, + RefractorTrack, + RefractorCardState, + RefractorTierBoost, + RefractorCosmetic, ] # Patch the service-layer 'db' reference to use our shared test database so @@ -165,7 +165,7 @@ def _build_test_app() -> FastAPI: connection even though it runs in a different thread from the fixture. """ from app.routers_v2.season_stats import router as ss_router - from app.routers_v2.evolution import router as evo_router + from app.routers_v2.refractor import router as evo_router test_app = FastAPI() @@ -294,8 +294,8 @@ def _make_play(game, play_num, batter, batter_team, pitcher, pitcher_team, **sta def _make_track( name: str = "WP13 Batter Track", card_type: str = "batter" -) -> EvolutionTrack: - track, _ = EvolutionTrack.get_or_create( +) -> RefractorTrack: + track, _ = RefractorTrack.get_or_create( name=name, defaults=dict( card_type=card_type, @@ -311,8 +311,8 @@ def _make_track( def _make_state( player, team, track, current_tier=0, current_value=0.0 -) -> EvolutionCardState: - return EvolutionCardState.create( +) -> RefractorCardState: + return RefractorCardState.create( player=player, team=team, track=track, @@ -428,14 +428,14 @@ def test_update_game_idempotent(client): # --------------------------------------------------------------------------- -# Tests: POST /api/v2/evolution/evaluate-game/{game_id} +# Tests: POST /api/v2/refractor/evaluate-game/{game_id} # --------------------------------------------------------------------------- def test_evaluate_game_increases_current_value(client): """After update-game, evaluate-game raises the card's current_value above 0. - What: Batter with an EvolutionCardState gets 3 hits (pa=3, hit=3) from a + What: Batter with a RefractorCardState gets 3 hits (pa=3, hit=3) from a game. update-game writes those stats; evaluate-game then recomputes the value. current_value in the DB must be > 0 after the evaluate call. @@ -458,12 +458,12 @@ def test_evaluate_game_increases_current_value(client): client.post(f"/api/v2/season-stats/update-game/{game.id}", headers=AUTH_HEADER) resp = client.post( - f"/api/v2/evolution/evaluate-game/{game.id}", headers=AUTH_HEADER + f"/api/v2/refractor/evaluate-game/{game.id}", headers=AUTH_HEADER ) assert resp.status_code == 200 - state = EvolutionCardState.get( - (EvolutionCardState.player == batter) & (EvolutionCardState.team == team_a) + state = RefractorCardState.get( + (RefractorCardState.player == batter) & (RefractorCardState.team == team_a) ) assert state.current_value > 0 @@ -501,12 +501,12 @@ def test_evaluate_game_tier_advancement(client): client.post(f"/api/v2/season-stats/update-game/{game.id}", headers=AUTH_HEADER) resp = client.post( - f"/api/v2/evolution/evaluate-game/{game.id}", headers=AUTH_HEADER + f"/api/v2/refractor/evaluate-game/{game.id}", headers=AUTH_HEADER ) assert resp.status_code == 200 - updated_state = EvolutionCardState.get( - (EvolutionCardState.player == batter) & (EvolutionCardState.team == team_a) + updated_state = RefractorCardState.get( + (RefractorCardState.player == batter) & (RefractorCardState.team == team_a) ) assert updated_state.current_tier >= 1 @@ -535,15 +535,15 @@ def test_evaluate_game_no_tier_advancement(client): client.post(f"/api/v2/season-stats/update-game/{game.id}", headers=AUTH_HEADER) resp = client.post( - f"/api/v2/evolution/evaluate-game/{game.id}", headers=AUTH_HEADER + f"/api/v2/refractor/evaluate-game/{game.id}", headers=AUTH_HEADER ) assert resp.status_code == 200 data = resp.json() assert data["tier_ups"] == [] - state = EvolutionCardState.get( - (EvolutionCardState.player == batter) & (EvolutionCardState.team == team_a) + state = RefractorCardState.get( + (RefractorCardState.player == batter) & (RefractorCardState.team == team_a) ) assert state.current_tier == 0 @@ -577,7 +577,7 @@ def test_evaluate_game_tier_ups_in_response(client): client.post(f"/api/v2/season-stats/update-game/{game.id}", headers=AUTH_HEADER) resp = client.post( - f"/api/v2/evolution/evaluate-game/{game.id}", headers=AUTH_HEADER + f"/api/v2/refractor/evaluate-game/{game.id}", headers=AUTH_HEADER ) assert resp.status_code == 200 data = resp.json() @@ -596,7 +596,7 @@ def test_evaluate_game_tier_ups_in_response(client): def test_evaluate_game_skips_players_without_state(client): - """Players in a game without an EvolutionCardState are silently skipped. + """Players in a game without a RefractorCardState are silently skipped. What: A game has two players: one with a card state and one without. After evaluate-game, evaluated should be 1 (only the player with state) @@ -613,7 +613,7 @@ def test_evaluate_game_skips_players_without_state(client): game = _make_game(team_a, team_b) track = _make_track(name="WP13 Skip Track") - # Only batter_with_state gets an EvolutionCardState + # Only batter_with_state gets a RefractorCardState _make_state(batter_with_state, team_a, track) _make_play(game, 1, batter_with_state, team_a, pitcher, team_b, pa=1, ab=1, outs=1) @@ -621,7 +621,7 @@ def test_evaluate_game_skips_players_without_state(client): client.post(f"/api/v2/season-stats/update-game/{game.id}", headers=AUTH_HEADER) resp = client.post( - f"/api/v2/evolution/evaluate-game/{game.id}", headers=AUTH_HEADER + f"/api/v2/refractor/evaluate-game/{game.id}", headers=AUTH_HEADER ) assert resp.status_code == 200 data = resp.json() @@ -663,5 +663,5 @@ def test_auth_required_evaluate_game(client): team_b = _make_team("WB2", gmid=20092) game = _make_game(team_a, team_b) - resp = client.post(f"/api/v2/evolution/evaluate-game/{game.id}") + resp = client.post(f"/api/v2/refractor/evaluate-game/{game.id}") assert resp.status_code == 401 diff --git a/tests/test_evolution_evaluator.py b/tests/test_refractor_evaluator.py similarity index 96% rename from tests/test_evolution_evaluator.py rename to tests/test_refractor_evaluator.py index abbefdf..d1873cc 100644 --- a/tests/test_evolution_evaluator.py +++ b/tests/test_refractor_evaluator.py @@ -1,7 +1,7 @@ -"""Tests for the evolution evaluator service (WP-08). +"""Tests for the refractor evaluator service (WP-08). Unit tests verify tier assignment, advancement, partial progress, idempotency, -full evolution, and no-regression behaviour without touching any database, +full refractor tier, and no-regression behaviour without touching any database, using stub Peewee models bound to an in-memory SQLite database. The formula engine (WP-09) and Peewee models (WP-05/WP-07) are not imported @@ -33,7 +33,7 @@ from peewee import ( SqliteDatabase, ) -from app.services.evolution_evaluator import evaluate_card +from app.services.refractor_evaluator import evaluate_card # --------------------------------------------------------------------------- # Stub models — mirror WP-01/WP-04/WP-07 schema without importing db_engine @@ -43,7 +43,7 @@ _test_db = SqliteDatabase(":memory:") class TrackStub(Model): - """Minimal EvolutionTrack stub for evaluator tests.""" + """Minimal RefractorTrack stub for evaluator tests.""" card_type = CharField(unique=True) t1_threshold = IntegerField() @@ -53,11 +53,11 @@ class TrackStub(Model): class Meta: database = _test_db - table_name = "evolution_track" + table_name = "refractor_track" class CardStateStub(Model): - """Minimal EvolutionCardState stub for evaluator tests.""" + """Minimal RefractorCardState stub for evaluator tests.""" player_id = IntegerField() team_id = IntegerField() @@ -69,7 +69,7 @@ class CardStateStub(Model): class Meta: database = _test_db - table_name = "evolution_card_state" + table_name = "refractor_card_state" indexes = ((("player_id", "team_id"), True),) @@ -331,7 +331,7 @@ class TestMissingState: def test_missing_state_raises(self, batter_track): """evaluate_card raises ValueError when no state row exists.""" # No card state created - with pytest.raises(ValueError, match="No evolution_card_state"): + with pytest.raises(ValueError, match="No refractor_card_state"): _eval(99, 99) diff --git a/tests/test_evolution_init.py b/tests/test_refractor_init.py similarity index 91% rename from tests/test_evolution_init.py rename to tests/test_refractor_init.py index cfbabb0..d92f80f 100644 --- a/tests/test_evolution_init.py +++ b/tests/test_refractor_init.py @@ -1,8 +1,8 @@ """ -Tests for WP-10: evolution_card_state initialization on pack opening. +Tests for WP-10: refractor_card_state initialization on pack opening. -Covers `app/services/evolution_init.py` — the `initialize_card_evolution` -function that creates an EvolutionCardState row when a card is first acquired. +Covers `app/services/refractor_init.py` — the `initialize_card_evolution` +function that creates an RefractorCardState row when a card is first acquired. Test strategy: - Unit tests for `_determine_card_type` cover all three branches (batter, @@ -18,7 +18,7 @@ Why we test idempotency: Why we test cross-player isolation: Two different players with the same team must each get their own - EvolutionCardState row. A bug that checked only team_id would share state + RefractorCardState row. A bug that checked only team_id would share state across players, so we assert that state.player_id matches. """ @@ -26,11 +26,11 @@ import pytest from app.db_engine import ( Cardset, - EvolutionCardState, - EvolutionTrack, + RefractorCardState, + RefractorTrack, Player, ) -from app.services.evolution_init import _determine_card_type, initialize_card_evolution +from app.services.refractor_init import _determine_card_type, initialize_card_evolution # --------------------------------------------------------------------------- @@ -74,13 +74,13 @@ def _make_player(rarity, pos_1: str) -> Player: ) -def _make_track(card_type: str) -> EvolutionTrack: - """Create an EvolutionTrack for the given card_type. +def _make_track(card_type: str) -> RefractorTrack: + """Create an RefractorTrack for the given card_type. Thresholds are kept small and arbitrary; the unit under test only cares about card_type when selecting the track. """ - return EvolutionTrack.create( + return RefractorTrack.create( name=f"Track-{card_type}", card_type=card_type, formula="pa", @@ -116,14 +116,14 @@ class TestDetermineCardType: """pos_1 == 'RP' maps to card_type 'rp'. Relief pitchers carry the 'RP' position flag and must follow a - separate evolution track with lower thresholds. + separate refractor track with lower thresholds. """ assert _determine_card_type(_FakePlayer("RP")) == "rp" def test_closer_pitcher(self): """pos_1 == 'CP' maps to card_type 'rp'. - Closers share the RP evolution track; the spec explicitly lists 'CP' + Closers share the RP refractor track; the spec explicitly lists 'CP' as an rp-track position. """ assert _determine_card_type(_FakePlayer("CP")) == "rp" @@ -168,7 +168,7 @@ class TestInitializeCardEvolution: @pytest.fixture(autouse=True) def seed_tracks(self): - """Create one EvolutionTrack per card_type before each test. + """Create one RefractorTrack per card_type before each test. initialize_card_evolution does a DB lookup for a track matching the card_type. If no track exists the function must not crash (it should @@ -180,7 +180,7 @@ class TestInitializeCardEvolution: self.rp_track = _make_track("rp") def test_first_card_creates_state(self, rarity, team): - """First acquisition creates an EvolutionCardState with zeroed values. + """First acquisition creates an RefractorCardState with zeroed values. Acceptance criteria from WP-10: - current_tier == 0 @@ -222,17 +222,17 @@ class TestInitializeCardEvolution: # Exactly one row in the database count = ( - EvolutionCardState.select() + RefractorCardState.select() .where( - EvolutionCardState.player == player, - EvolutionCardState.team == team, + RefractorCardState.player == player, + RefractorCardState.team == team, ) .count() ) assert count == 1 # Progress was NOT reset - refreshed = EvolutionCardState.get_by_id(state1.id) + refreshed = RefractorCardState.get_by_id(state1.id) assert refreshed.current_tier == 2 assert refreshed.current_value == 250.0 @@ -256,7 +256,7 @@ class TestInitializeCardEvolution: assert state_b.player_id == player_b.player_id def test_sp_card_gets_sp_track(self, rarity, team): - """A starting pitcher is assigned the 'sp' EvolutionTrack. + """A starting pitcher is assigned the 'sp' RefractorTrack. Track selection is driven by card_type, which in turn comes from pos_1. This test passes card_type='sp' explicitly (mirroring the @@ -270,7 +270,7 @@ class TestInitializeCardEvolution: assert state.track_id == self.sp_track.id def test_rp_card_gets_rp_track(self, rarity, team): - """A relief pitcher (RP or CP) is assigned the 'rp' EvolutionTrack.""" + """A relief pitcher (RP or CP) is assigned the 'rp' RefractorTrack.""" player = _make_player(rarity, "RP") state = initialize_card_evolution(player.player_id, team.id, "rp") diff --git a/tests/test_evolution_models.py b/tests/test_refractor_models.py similarity index 83% rename from tests/test_evolution_models.py rename to tests/test_refractor_models.py index 4479b9f..782967c 100644 --- a/tests/test_evolution_models.py +++ b/tests/test_refractor_models.py @@ -1,12 +1,12 @@ """ -Tests for evolution-related models and BattingSeasonStats. +Tests for refractor-related models and BattingSeasonStats. Covers WP-01 acceptance criteria: - - EvolutionTrack: CRUD and unique-name constraint - - EvolutionCardState: CRUD, defaults, unique-(player,team) constraint, - and FK resolution back to EvolutionTrack - - EvolutionTierBoost: CRUD and unique-(track, tier, boost_type, boost_target) - - EvolutionCosmetic: CRUD and unique-name constraint + - RefractorTrack: CRUD and unique-name constraint + - RefractorCardState: CRUD, defaults, unique-(player,team) constraint, + and FK resolution back to RefractorTrack + - RefractorTierBoost: CRUD and unique-(track, tier, boost_type, boost_target) + - RefractorCosmetic: CRUD and unique-name constraint - BattingSeasonStats: CRUD with defaults, unique-(player, team, season), and in-place stat accumulation @@ -21,21 +21,21 @@ from playhouse.shortcuts import model_to_dict from app.db_engine import ( BattingSeasonStats, - EvolutionCardState, - EvolutionCosmetic, - EvolutionTierBoost, - EvolutionTrack, + RefractorCardState, + RefractorCosmetic, + RefractorTierBoost, + RefractorTrack, ) # --------------------------------------------------------------------------- -# EvolutionTrack +# RefractorTrack # --------------------------------------------------------------------------- -class TestEvolutionTrack: - """Tests for the EvolutionTrack model. +class TestRefractorTrack: + """Tests for the RefractorTrack model. - EvolutionTrack defines a named progression path (formula + + RefractorTrack defines a named progression path (formula + tier thresholds) for a card type. The name column carries a UNIQUE constraint so that accidental duplicates are caught at the database level. @@ -60,12 +60,12 @@ class TestEvolutionTrack: def test_track_unique_name(self, track): """Inserting a second track with the same name raises IntegrityError. - The UNIQUE constraint on EvolutionTrack.name must prevent two + The UNIQUE constraint on RefractorTrack.name must prevent two tracks from sharing the same identifier, as the name is used as a human-readable key throughout the evolution system. """ with pytest.raises(IntegrityError): - EvolutionTrack.create( + RefractorTrack.create( name="Batter Track", # duplicate card_type="sp", formula="outs * 3", @@ -77,15 +77,15 @@ class TestEvolutionTrack: # --------------------------------------------------------------------------- -# EvolutionCardState +# RefractorCardState # --------------------------------------------------------------------------- -class TestEvolutionCardState: - """Tests for EvolutionCardState, which tracks per-player evolution progress. +class TestRefractorCardState: + """Tests for RefractorCardState, which tracks per-player refractor progress. Each row represents one card (player) owned by one team, linked to a - specific EvolutionTrack. The model records the current tier (0-4), + specific RefractorTrack. The model records the current tier (0-4), accumulated progress value, and whether the card is fully evolved. """ @@ -98,9 +98,9 @@ class TestEvolutionCardState: fully_evolved → False (evolution is not complete at creation) last_evaluated_at → None (never evaluated yet) """ - state = EvolutionCardState.create(player=player, team=team, track=track) + state = RefractorCardState.create(player=player, team=team, track=track) - fetched = EvolutionCardState.get_by_id(state.id) + fetched = RefractorCardState.get_by_id(state.id) assert fetched.player_id == player.player_id assert fetched.team_id == team.id assert fetched.track_id == track.id @@ -113,34 +113,34 @@ class TestEvolutionCardState: """A second card state for the same (player, team) pair raises IntegrityError. The unique index on (player, team) enforces that each player card - has at most one evolution state per team roster slot, preventing - duplicate evolution progress rows for the same physical card. + has at most one refractor state per team roster slot, preventing + duplicate refractor progress rows for the same physical card. """ - EvolutionCardState.create(player=player, team=team, track=track) + RefractorCardState.create(player=player, team=team, track=track) with pytest.raises(IntegrityError): - EvolutionCardState.create(player=player, team=team, track=track) + RefractorCardState.create(player=player, team=team, track=track) def test_card_state_fk_track(self, player, team, track): - """Accessing card_state.track returns the original EvolutionTrack instance. + """Accessing card_state.track returns the original RefractorTrack instance. This confirms the FK is correctly wired and that Peewee resolves the relationship, returning an object with the same primary key and name as the track used during creation. """ - state = EvolutionCardState.create(player=player, team=team, track=track) - fetched = EvolutionCardState.get_by_id(state.id) + state = RefractorCardState.create(player=player, team=team, track=track) + fetched = RefractorCardState.get_by_id(state.id) resolved_track = fetched.track assert resolved_track.id == track.id assert resolved_track.name == "Batter Track" # --------------------------------------------------------------------------- -# EvolutionTierBoost +# RefractorTierBoost # --------------------------------------------------------------------------- -class TestEvolutionTierBoost: - """Tests for EvolutionTierBoost, the per-tier stat/rating bonus table. +class TestRefractorTierBoost: + """Tests for RefractorTierBoost, the per-tier stat/rating bonus table. Each row maps a (track, tier) combination to a single boost — the specific stat or rating column to buff and by how much. The four- @@ -153,14 +153,14 @@ class TestEvolutionTierBoost: Verifies boost_type, boost_target, and boost_value are stored and retrieved without modification. """ - boost = EvolutionTierBoost.create( + boost = RefractorTierBoost.create( track=track, tier=1, boost_type="rating", boost_target="contact_vl", boost_value=1.5, ) - fetched = EvolutionTierBoost.get_by_id(boost.id) + fetched = RefractorTierBoost.get_by_id(boost.id) assert fetched.track_id == track.id assert fetched.tier == 1 assert fetched.boost_type == "rating" @@ -174,7 +174,7 @@ class TestEvolutionTierBoost: (e.g. Tier-1 contact_vl rating) cannot be defined twice for the same track, which would create ambiguity during evolution evaluation. """ - EvolutionTierBoost.create( + RefractorTierBoost.create( track=track, tier=2, boost_type="rating", @@ -182,7 +182,7 @@ class TestEvolutionTierBoost: boost_value=2.0, ) with pytest.raises(IntegrityError): - EvolutionTierBoost.create( + RefractorTierBoost.create( track=track, tier=2, boost_type="rating", @@ -192,12 +192,12 @@ class TestEvolutionTierBoost: # --------------------------------------------------------------------------- -# EvolutionCosmetic +# RefractorCosmetic # --------------------------------------------------------------------------- -class TestEvolutionCosmetic: - """Tests for EvolutionCosmetic, decorative unlocks tied to evolution tiers. +class TestRefractorCosmetic: + """Tests for RefractorCosmetic, decorative unlocks tied to evolution tiers. Cosmetics are purely visual rewards (frames, badges, themes) that a card unlocks when it reaches a required tier. The name column is @@ -210,14 +210,14 @@ class TestEvolutionCosmetic: Verifies all columns including optional ones (css_class, asset_url) are stored and retrieved. """ - cosmetic = EvolutionCosmetic.create( + cosmetic = RefractorCosmetic.create( name="Gold Frame", tier_required=2, cosmetic_type="frame", css_class="evo-frame-gold", asset_url="https://cdn.example.com/frames/gold.png", ) - fetched = EvolutionCosmetic.get_by_id(cosmetic.id) + fetched = RefractorCosmetic.get_by_id(cosmetic.id) assert fetched.name == "Gold Frame" assert fetched.tier_required == 2 assert fetched.cosmetic_type == "frame" @@ -227,16 +227,16 @@ class TestEvolutionCosmetic: def test_cosmetic_unique_name(self): """Inserting a second cosmetic with the same name raises IntegrityError. - The UNIQUE constraint on EvolutionCosmetic.name prevents duplicate + The UNIQUE constraint on RefractorCosmetic.name prevents duplicate cosmetic definitions that could cause ambiguous tier unlock lookups. """ - EvolutionCosmetic.create( + RefractorCosmetic.create( name="Silver Badge", tier_required=1, cosmetic_type="badge", ) with pytest.raises(IntegrityError): - EvolutionCosmetic.create( + RefractorCosmetic.create( name="Silver Badge", # duplicate tier_required=3, cosmetic_type="badge", diff --git a/tests/test_evolution_seed.py b/tests/test_refractor_seed.py similarity index 63% rename from tests/test_evolution_seed.py rename to tests/test_refractor_seed.py index a3d1842..383e213 100644 --- a/tests/test_evolution_seed.py +++ b/tests/test_refractor_seed.py @@ -1,16 +1,16 @@ """ -Tests for app/seed/evolution_tracks.py — seed_evolution_tracks(). +Tests for app/seed/refractor_tracks.py — seed_refractor_tracks(). What: Verify that the JSON-driven seed function correctly creates, counts, -and idempotently updates EvolutionTrack rows in the database. +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 evolution scoring for every card in the system. +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/evolution_tracks.json` on disk, so the tests also serve as +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. """ @@ -19,11 +19,11 @@ from pathlib import Path import pytest -from app.db_engine import EvolutionTrack -from app.seed.evolution_tracks import seed_evolution_tracks +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" / "evolution_tracks.json" +_JSON_PATH = Path(__file__).parent.parent / "app" / "seed" / "refractor_tracks.json" @pytest.fixture @@ -37,48 +37,48 @@ def json_tracks(): def test_seed_creates_three_tracks(json_tracks): - """After one seed call, exactly 3 EvolutionTrack rows must exist. + """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 - (evolution disabled for a card type) or have phantom extras. + (refractor disabled for a card type) or have phantom extras. """ - seed_evolution_tracks() - assert EvolutionTrack.select().count() == 3 + 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 evolution engine. + 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 evolution scoring for that role. + track-lookup misses and silently skip refractor scoring for that role. """ - seed_evolution_tracks() + seed_refractor_tracks() expected_types = {d["card_type"] for d in json_tracks} - actual_types = {t.card_type for t in EvolutionTrack.select()} + 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 evolution engine uses these thresholds to determine tier + 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_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})" + 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(): @@ -86,10 +86,10 @@ def test_seed_thresholds_positive(): 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. + bypass the entire refractor progression system. """ - seed_evolution_tracks() - for track in EvolutionTrack.select(): + 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" @@ -99,29 +99,29 @@ def test_seed_thresholds_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 + 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 evolution progress. + silently produce 0 for every player, halting all refractor 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" + 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_evolution_tracks() twice must still yield exactly 3 rows. + """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 EvolutionTrack.name would raise an IntegrityError + 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_evolution_tracks() - seed_evolution_tracks() - assert EvolutionTrack.select().count() == 3 + seed_refractor_tracks() + seed_refractor_tracks() + assert RefractorTrack.select().count() == 3 def test_seed_updates_on_rerun(json_tracks): @@ -135,24 +135,24 @@ def test_seed_updates_on_rerun(json_tracks): build up silently and the production database would diverge from the checked-in JSON without any visible error. """ - seed_evolution_tracks() + seed_refractor_tracks() # Pick the first track and corrupt its t1_threshold first_def = json_tracks[0] - track = EvolutionTrack.get(EvolutionTrack.name == first_def["name"]) + 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 = EvolutionTrack.get(EvolutionTrack.name == first_def["name"]) + track_check = RefractorTrack.get(RefractorTrack.name == first_def["name"]) assert track_check.t1_threshold == corrupted_value # Re-seed — should restore the JSON value - seed_evolution_tracks() + seed_refractor_tracks() - restored = EvolutionTrack.get(EvolutionTrack.name == first_def["name"]) + 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}" diff --git a/tests/test_evolution_state_api.py b/tests/test_refractor_state_api.py similarity index 88% rename from tests/test_evolution_state_api.py rename to tests/test_refractor_state_api.py index a9b7e47..f7a8689 100644 --- a/tests/test_evolution_state_api.py +++ b/tests/test_refractor_state_api.py @@ -1,11 +1,11 @@ -"""Integration tests for the evolution card state API endpoints (WP-07). +"""Integration tests for the refractor card state API endpoints (WP-07). Tests cover: GET /api/v2/teams/{team_id}/evolutions - GET /api/v2/evolution/cards/{card_id} + GET /api/v2/refractor/cards/{card_id} All tests require a live PostgreSQL connection (POSTGRES_HOST env var) and -assume the evolution schema migration (WP-04) has already been applied. +assume the refractor schema migration (WP-04) has already been applied. Tests auto-skip when POSTGRES_HOST is not set. Test data is inserted via psycopg2 before each module fixture runs and @@ -18,9 +18,9 @@ Object graph built by fixtures cardset_row -- a seeded cardset row player_row -- a seeded player row (FK: rarity, cardset) team_row -- a seeded team row - track_row -- a seeded evolution_track row (batter) + track_row -- a seeded refractor_track row (batter) card_row -- a seeded card row (FK: player, team, pack, pack_type, cardset) - state_row -- a seeded evolution_card_state row (FK: player, team, track) + state_row -- a seeded refractor_card_state row (FK: player, team, track) Test matrix ----------- @@ -30,8 +30,8 @@ Test matrix test_list_pagination -- page/per_page params slice results correctly test_get_card_state_shape -- single card returns all required response fields test_get_card_state_next_threshold -- next_threshold is the threshold for tier above current - test_get_card_id_resolves_player -- card_id joins Card -> Player/Team -> EvolutionCardState - test_get_card_404_no_state -- card with no EvolutionCardState returns 404 + test_get_card_id_resolves_player -- card_id joins Card -> Player/Team -> RefractorCardState + test_get_card_404_no_state -- card with no RefractorCardState returns 404 test_duplicate_cards_share_state -- two cards same player+team return the same state row test_auth_required -- missing token returns 401 on both endpoints """ @@ -63,7 +63,7 @@ def seeded_data(pg_conn): Insertion order respects FK dependencies: rarity -> cardset -> player pack_type (needs cardset) -> pack (needs team + pack_type) -> card - evolution_track -> evolution_card_state + refractor_track -> refractor_card_state """ cur = pg_conn.cursor() @@ -130,7 +130,7 @@ def seeded_data(pg_conn): # Evolution tracks cur.execute( """ - INSERT INTO evolution_track (name, card_type, formula, + INSERT INTO refractor_track (name, card_type, formula, t1_threshold, t2_threshold, t3_threshold, t4_threshold) VALUES ('WP07 Batter Track', 'batter', 'pa + tb * 2', 37, 149, 448, 896) @@ -142,7 +142,7 @@ def seeded_data(pg_conn): cur.execute( """ - INSERT INTO evolution_track (name, card_type, formula, + INSERT INTO refractor_track (name, card_type, formula, t1_threshold, t2_threshold, t3_threshold, t4_threshold) VALUES ('WP07 SP Track', 'sp', 'ip + k', 10, 40, 120, 240) @@ -230,7 +230,7 @@ def seeded_data(pg_conn): # Batter player at tier 1, value 87.5 cur.execute( """ - INSERT INTO evolution_card_state + INSERT INTO refractor_card_state (player_id, team_id, track_id, current_tier, current_value, fully_evolved, last_evaluated_at) VALUES (%s, %s, %s, 1, 87.5, false, '2026-03-12T14:00:00Z') @@ -258,7 +258,7 @@ def seeded_data(pg_conn): } # Teardown: delete in reverse FK order - cur.execute("DELETE FROM evolution_card_state WHERE id = %s", (state_id,)) + cur.execute("DELETE FROM refractor_card_state WHERE id = %s", (state_id,)) cur.execute( "DELETE FROM card WHERE id = ANY(%s)", ([card_id, card2_id, card_no_state_id],), @@ -266,7 +266,7 @@ def seeded_data(pg_conn): cur.execute("DELETE FROM pack WHERE id = ANY(%s)", ([pack_id, pack2_id, pack3_id],)) cur.execute("DELETE FROM pack_type WHERE id = %s", (pack_type_id,)) cur.execute( - "DELETE FROM evolution_track WHERE id = ANY(%s)", + "DELETE FROM refractor_track WHERE id = ANY(%s)", ([batter_track_id, sp_track_id],), ) cur.execute( @@ -315,15 +315,15 @@ def test_list_team_evolutions(client, seeded_data): def test_list_filter_by_card_type(client, seeded_data, pg_conn): """card_type filter includes states whose track.card_type matches and excludes others. - Seeds a second evolution_card_state for player2 (sp track) then queries + Seeds a second refractor_card_state for player2 (sp track) then queries card_type=batter (returns 1) and card_type=sp (returns 1). - Verifies the JOIN to evolution_track and the WHERE predicate on card_type. + Verifies the JOIN to refractor_track and the WHERE predicate on card_type. """ cur = pg_conn.cursor() # Add a state for the sp player so we have two types in this team cur.execute( """ - INSERT INTO evolution_card_state + INSERT INTO refractor_card_state (player_id, team_id, track_id, current_tier, current_value, fully_evolved) VALUES (%s, %s, %s, 0, 0.0, false) RETURNING id @@ -352,7 +352,7 @@ def test_list_filter_by_card_type(client, seeded_data, pg_conn): assert sp_data["count"] == 1 assert sp_data["items"][0]["player_id"] == seeded_data["player2_id"] finally: - cur.execute("DELETE FROM evolution_card_state WHERE id = %s", (sp_state_id,)) + cur.execute("DELETE FROM refractor_card_state WHERE id = %s", (sp_state_id,)) pg_conn.commit() @@ -368,7 +368,7 @@ def test_list_filter_by_tier(client, seeded_data, pg_conn): # Advance to tier 2 cur.execute( - "UPDATE evolution_card_state SET current_tier = 2 WHERE id = %s", + "UPDATE refractor_card_state SET current_tier = 2 WHERE id = %s", (seeded_data["state_id"],), ) pg_conn.commit() @@ -391,7 +391,7 @@ def test_list_filter_by_tier(client, seeded_data, pg_conn): assert t2_data["items"][0]["current_tier"] == 2 finally: cur.execute( - "UPDATE evolution_card_state SET current_tier = 1 WHERE id = %s", + "UPDATE refractor_card_state SET current_tier = 1 WHERE id = %s", (seeded_data["state_id"],), ) pg_conn.commit() @@ -408,7 +408,7 @@ def test_list_pagination(client, seeded_data, pg_conn): cur = pg_conn.cursor() cur.execute( """ - INSERT INTO evolution_card_state + INSERT INTO refractor_card_state (player_id, team_id, track_id, current_tier, current_value, fully_evolved) VALUES (%s, %s, %s, 0, 0.0, false) RETURNING id @@ -441,18 +441,18 @@ def test_list_pagination(client, seeded_data, pg_conn): assert data1["items"][0]["player_id"] != data2["items"][0]["player_id"] finally: - cur.execute("DELETE FROM evolution_card_state WHERE id = %s", (extra_state_id,)) + cur.execute("DELETE FROM refractor_card_state WHERE id = %s", (extra_state_id,)) pg_conn.commit() # --------------------------------------------------------------------------- -# Tests: GET /api/v2/evolution/cards/{card_id} +# Tests: GET /api/v2/refractor/cards/{card_id} # --------------------------------------------------------------------------- @_skip_no_pg def test_get_card_state_shape(client, seeded_data): - """GET /evolution/cards/{card_id} returns all required fields. + """GET /refractor/cards/{card_id} returns all required fields. Verifies the full response envelope: player_id, team_id, current_tier, current_value, fully_evolved, @@ -460,7 +460,7 @@ def test_get_card_state_shape(client, seeded_data): with id, name, card_type, formula, and t1-t4 thresholds. """ card_id = seeded_data["card_id"] - resp = client.get(f"/api/v2/evolution/cards/{card_id}", headers=AUTH_HEADER) + resp = client.get(f"/api/v2/refractor/cards/{card_id}", headers=AUTH_HEADER) assert resp.status_code == 200 data = resp.json() @@ -505,29 +505,29 @@ def test_get_card_state_next_threshold(client, seeded_data, pg_conn): # Advance to tier 2 cur.execute( - "UPDATE evolution_card_state SET current_tier = 2 WHERE id = %s", (state_id,) + "UPDATE refractor_card_state SET current_tier = 2 WHERE id = %s", (state_id,) ) pg_conn.commit() try: - resp = client.get(f"/api/v2/evolution/cards/{card_id}", headers=AUTH_HEADER) + resp = client.get(f"/api/v2/refractor/cards/{card_id}", headers=AUTH_HEADER) assert resp.status_code == 200 assert resp.json()["next_threshold"] == 448 # t3_threshold # Advance to tier 4 (fully evolved) cur.execute( - "UPDATE evolution_card_state SET current_tier = 4, fully_evolved = true " + "UPDATE refractor_card_state SET current_tier = 4, fully_evolved = true " "WHERE id = %s", (state_id,), ) pg_conn.commit() - resp2 = client.get(f"/api/v2/evolution/cards/{card_id}", headers=AUTH_HEADER) + resp2 = client.get(f"/api/v2/refractor/cards/{card_id}", headers=AUTH_HEADER) assert resp2.status_code == 200 assert resp2.json()["next_threshold"] is None finally: cur.execute( - "UPDATE evolution_card_state SET current_tier = 1, fully_evolved = false " + "UPDATE refractor_card_state SET current_tier = 1, fully_evolved = false " "WHERE id = %s", (state_id,), ) @@ -538,11 +538,11 @@ def test_get_card_state_next_threshold(client, seeded_data, pg_conn): def test_get_card_id_resolves_player(client, seeded_data): """card_id is resolved via the Card table to obtain (player_id, team_id). - The endpoint must JOIN Card -> Player + Team to find the EvolutionCardState. + The endpoint must JOIN Card -> Player + Team to find the RefractorCardState. Verifies that card_id correctly maps to the right player's evolution state. """ card_id = seeded_data["card_id"] - resp = client.get(f"/api/v2/evolution/cards/{card_id}", headers=AUTH_HEADER) + resp = client.get(f"/api/v2/refractor/cards/{card_id}", headers=AUTH_HEADER) assert resp.status_code == 200 data = resp.json() assert data["player_id"] == seeded_data["player_id"] @@ -551,20 +551,20 @@ def test_get_card_id_resolves_player(client, seeded_data): @_skip_no_pg def test_get_card_404_no_state(client, seeded_data): - """GET /evolution/cards/{card_id} returns 404 when no EvolutionCardState exists. + """GET /refractor/cards/{card_id} returns 404 when no RefractorCardState exists. card_no_state_id is a card row for player2 on the team, but no - evolution_card_state row was created for player2. The endpoint must + refractor_card_state row was created for player2. The endpoint must return 404, not 500 or an empty response. """ card_id = seeded_data["card_no_state_id"] - resp = client.get(f"/api/v2/evolution/cards/{card_id}", headers=AUTH_HEADER) + resp = client.get(f"/api/v2/refractor/cards/{card_id}", headers=AUTH_HEADER) assert resp.status_code == 404 @_skip_no_pg def test_duplicate_cards_share_state(client, seeded_data): - """Two Card rows for the same player+team share one EvolutionCardState. + """Two Card rows for the same player+team share one RefractorCardState. card_id and card2_id both belong to player_id on team_id. Because the unique-(player,team) constraint means only one state row can exist, both @@ -573,8 +573,8 @@ def test_duplicate_cards_share_state(client, seeded_data): card1_id = seeded_data["card_id"] card2_id = seeded_data["card2_id"] - resp1 = client.get(f"/api/v2/evolution/cards/{card1_id}", headers=AUTH_HEADER) - resp2 = client.get(f"/api/v2/evolution/cards/{card2_id}", headers=AUTH_HEADER) + resp1 = client.get(f"/api/v2/refractor/cards/{card1_id}", headers=AUTH_HEADER) + resp2 = client.get(f"/api/v2/refractor/cards/{card2_id}", headers=AUTH_HEADER) assert resp1.status_code == 200 assert resp2.status_code == 200 @@ -597,7 +597,7 @@ def test_auth_required(client, seeded_data): Verifies that the valid_token dependency is enforced on: GET /api/v2/teams/{id}/evolutions - GET /api/v2/evolution/cards/{id} + GET /api/v2/refractor/cards/{id} """ team_id = seeded_data["team_id"] card_id = seeded_data["card_id"] @@ -605,5 +605,5 @@ def test_auth_required(client, seeded_data): resp_list = client.get(f"/api/v2/teams/{team_id}/evolutions") assert resp_list.status_code == 401 - resp_card = client.get(f"/api/v2/evolution/cards/{card_id}") + resp_card = client.get(f"/api/v2/refractor/cards/{card_id}") assert resp_card.status_code == 401 diff --git a/tests/test_evolution_track_api.py b/tests/test_refractor_track_api.py similarity index 85% rename from tests/test_evolution_track_api.py rename to tests/test_refractor_track_api.py index 2545db3..bc34b47 100644 --- a/tests/test_evolution_track_api.py +++ b/tests/test_refractor_track_api.py @@ -1,11 +1,11 @@ -"""Integration tests for the evolution track catalog API endpoints (WP-06). +"""Integration tests for the refractor track catalog API endpoints (WP-06). Tests cover: - GET /api/v2/evolution/tracks - GET /api/v2/evolution/tracks/{track_id} + GET /api/v2/refractor/tracks + GET /api/v2/refractor/tracks/{track_id} All tests require a live PostgreSQL connection (POSTGRES_HOST env var) and -assume the evolution schema migration (WP-04) has already been applied. +assume the refractor schema migration (WP-04) has already been applied. Tests auto-skip when POSTGRES_HOST is not set. Test data is inserted via psycopg2 before the test module runs and deleted @@ -45,7 +45,7 @@ def seeded_tracks(pg_conn): for name, card_type, formula, t1, t2, t3, t4 in _SEED_TRACKS: cur.execute( """ - INSERT INTO evolution_track + INSERT INTO refractor_track (name, card_type, formula, t1_threshold, t2_threshold, t3_threshold, t4_threshold) VALUES (%s, %s, %s, %s, %s, %s, %s) ON CONFLICT (card_type) DO UPDATE SET @@ -62,7 +62,7 @@ def seeded_tracks(pg_conn): ids.append(cur.fetchone()[0]) pg_conn.commit() yield ids - cur.execute("DELETE FROM evolution_track WHERE id = ANY(%s)", (ids,)) + cur.execute("DELETE FROM refractor_track WHERE id = ANY(%s)", (ids,)) pg_conn.commit() @@ -82,7 +82,7 @@ def test_list_tracks_returns_count_3(client, seeded_tracks): After seeding batter/sp/rp, the table should have exactly those three rows (no other tracks are inserted by other test modules). """ - resp = client.get("/api/v2/evolution/tracks", headers=AUTH_HEADER) + resp = client.get("/api/v2/refractor/tracks", headers=AUTH_HEADER) assert resp.status_code == 200 data = resp.json() assert data["count"] == 3 @@ -92,7 +92,7 @@ def test_list_tracks_returns_count_3(client, seeded_tracks): @_skip_no_pg def test_filter_by_card_type(client, seeded_tracks): """card_type=sp filter returns exactly 1 track with card_type 'sp'.""" - resp = client.get("/api/v2/evolution/tracks?card_type=sp", headers=AUTH_HEADER) + resp = client.get("/api/v2/refractor/tracks?card_type=sp", headers=AUTH_HEADER) assert resp.status_code == 200 data = resp.json() assert data["count"] == 1 @@ -103,7 +103,7 @@ def test_filter_by_card_type(client, seeded_tracks): def test_get_single_track_with_thresholds(client, seeded_tracks): """GET /tracks/{id} returns a track dict with formula and t1-t4 thresholds.""" track_id = seeded_tracks[0] # batter - resp = client.get(f"/api/v2/evolution/tracks/{track_id}", headers=AUTH_HEADER) + resp = client.get(f"/api/v2/refractor/tracks/{track_id}", headers=AUTH_HEADER) assert resp.status_code == 200 data = resp.json() assert data["card_type"] == "batter" @@ -117,16 +117,16 @@ def test_get_single_track_with_thresholds(client, seeded_tracks): @_skip_no_pg def test_404_for_nonexistent_track(client, seeded_tracks): """GET /tracks/999999 returns 404 when the track does not exist.""" - resp = client.get("/api/v2/evolution/tracks/999999", headers=AUTH_HEADER) + resp = client.get("/api/v2/refractor/tracks/999999", headers=AUTH_HEADER) assert resp.status_code == 404 @_skip_no_pg def test_auth_required(client, seeded_tracks): """Requests without a Bearer token return 401 for both endpoints.""" - resp_list = client.get("/api/v2/evolution/tracks") + resp_list = client.get("/api/v2/refractor/tracks") assert resp_list.status_code == 401 track_id = seeded_tracks[0] - resp_single = client.get(f"/api/v2/evolution/tracks/{track_id}") + resp_single = client.get(f"/api/v2/refractor/tracks/{track_id}") assert resp_single.status_code == 401