refactor: rename evolution system to refractor
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) <noreply@anthropic.com>
This commit is contained in:
parent
0b6e85fff9
commit
b7dec3f231
@ -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,
|
||||
)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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}
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
@ -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,
|
||||
15
migrations/2026-03-23_rename_evolution_to_refractor.sql
Normal file
15
migrations/2026-03-23_rename_evolution_to_refractor.sql
Normal file
@ -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;
|
||||
@ -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)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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",
|
||||
@ -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}"
|
||||
@ -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
|
||||
@ -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
|
||||
Loading…
Reference in New Issue
Block a user