Speculative schema from initial Refractor design that was never used — boosts are hardcoded in refractor_boost.py and tier visuals are embedded in CSS templates. Both tables have zero rows on dev and prod. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
216 lines
8.0 KiB
Python
216 lines
8.0 KiB
Python
"""
|
|
Tests for refractor-related models and BattingSeasonStats.
|
|
|
|
Covers WP-01 acceptance criteria:
|
|
- RefractorTrack: CRUD and unique-name constraint
|
|
- RefractorCardState: CRUD, defaults, unique-(player,team) constraint,
|
|
and FK resolution back to RefractorTrack
|
|
- BattingSeasonStats: CRUD with defaults, unique-(player, team, season),
|
|
and in-place stat accumulation
|
|
|
|
Each test class is self-contained: fixtures from conftest.py supply the
|
|
minimal parent rows needed to satisfy FK constraints, and every assertion
|
|
targets a single, clearly-named behaviour so failures are easy to trace.
|
|
"""
|
|
|
|
import pytest
|
|
from peewee import IntegrityError
|
|
from playhouse.shortcuts import model_to_dict
|
|
|
|
from app.db_engine import (
|
|
BattingSeasonStats,
|
|
RefractorCardState,
|
|
RefractorTrack,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# RefractorTrack
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestRefractorTrack:
|
|
"""Tests for the RefractorTrack model.
|
|
|
|
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.
|
|
"""
|
|
|
|
def test_create_track(self, track):
|
|
"""Creating a track persists all fields and they round-trip correctly.
|
|
|
|
Reads back via model_to_dict (recurse=False) to verify the raw
|
|
column values, not Python-object representations, match what was
|
|
inserted.
|
|
"""
|
|
data = model_to_dict(track, recurse=False)
|
|
assert data["name"] == "Batter Track"
|
|
assert data["card_type"] == "batter"
|
|
assert data["formula"] == "pa + tb * 2"
|
|
assert data["t1_threshold"] == 37
|
|
assert data["t2_threshold"] == 149
|
|
assert data["t3_threshold"] == 448
|
|
assert data["t4_threshold"] == 896
|
|
|
|
def test_track_unique_name(self, track):
|
|
"""Inserting a second track with the same name raises IntegrityError.
|
|
|
|
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):
|
|
RefractorTrack.create(
|
|
name="Batter Track", # duplicate
|
|
card_type="sp",
|
|
formula="outs * 3",
|
|
t1_threshold=10,
|
|
t2_threshold=40,
|
|
t3_threshold=120,
|
|
t4_threshold=240,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# RefractorCardState
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
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 RefractorTrack. The model records the current tier (0-4),
|
|
accumulated progress value, and whether the card is fully evolved.
|
|
"""
|
|
|
|
def test_create_card_state(self, player, team, track):
|
|
"""Creating a card state stores all fields and defaults are correct.
|
|
|
|
Defaults under test:
|
|
current_tier → 0 (fresh card, no tier unlocked yet)
|
|
current_value → 0.0 (no formula progress accumulated)
|
|
fully_evolved → False (evolution is not complete at creation)
|
|
last_evaluated_at → None (never evaluated yet)
|
|
"""
|
|
state = RefractorCardState.create(player=player, team=team, track=track)
|
|
|
|
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
|
|
assert fetched.current_tier == 0
|
|
assert fetched.current_value == 0.0
|
|
assert fetched.fully_evolved is False
|
|
assert fetched.last_evaluated_at is None
|
|
|
|
def test_card_state_unique_player_team(self, player, team, track):
|
|
"""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 refractor state per team roster slot, preventing
|
|
duplicate refractor progress rows for the same physical card.
|
|
"""
|
|
RefractorCardState.create(player=player, team=team, track=track)
|
|
with pytest.raises(IntegrityError):
|
|
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 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 = 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"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# BattingSeasonStats
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestBattingSeasonStats:
|
|
"""Tests for BattingSeasonStats, the per-season batting accumulation table.
|
|
|
|
Each row aggregates game-by-game batting stats for one player on one
|
|
team in one season. The three-column unique constraint prevents
|
|
double-counting and ensures a single authoritative row for each
|
|
(player, team, season) combination.
|
|
"""
|
|
|
|
def test_create_season_stats(self, player, team):
|
|
"""Creating a stats row with explicit values stores everything correctly.
|
|
|
|
Also verifies the integer stat defaults (all 0) for columns that
|
|
are not provided, which is the initial state before any games are
|
|
processed.
|
|
"""
|
|
stats = BattingSeasonStats.create(
|
|
player=player,
|
|
team=team,
|
|
season=11,
|
|
games=5,
|
|
pa=20,
|
|
ab=18,
|
|
hits=6,
|
|
doubles=1,
|
|
triples=0,
|
|
hr=2,
|
|
bb=2,
|
|
hbp=0,
|
|
strikeouts=4,
|
|
rbi=5,
|
|
runs=3,
|
|
sb=1,
|
|
cs=0,
|
|
)
|
|
fetched = BattingSeasonStats.get_by_id(stats.id)
|
|
assert fetched.player_id == player.player_id
|
|
assert fetched.team_id == team.id
|
|
assert fetched.season == 11
|
|
assert fetched.games == 5
|
|
assert fetched.pa == 20
|
|
assert fetched.hits == 6
|
|
assert fetched.hr == 2
|
|
assert fetched.strikeouts == 4
|
|
# Nullable meta fields
|
|
assert fetched.last_game is None
|
|
assert fetched.last_updated_at is None
|
|
|
|
def test_season_stats_unique_constraint(self, player, team):
|
|
"""A second row for the same (player, team, season) raises IntegrityError.
|
|
|
|
The unique index on these three columns guarantees that each
|
|
player-team-season combination has exactly one accumulation row,
|
|
preventing duplicate stat aggregation that would inflate totals.
|
|
"""
|
|
BattingSeasonStats.create(player=player, team=team, season=11)
|
|
with pytest.raises(IntegrityError):
|
|
BattingSeasonStats.create(player=player, team=team, season=11)
|
|
|
|
def test_season_stats_increment(self, player, team):
|
|
"""Manually incrementing hits on an existing row persists the change.
|
|
|
|
Simulates the common pattern used by the stats accumulator:
|
|
fetch the row, add the game delta, save. Verifies that save()
|
|
writes back to the database and that subsequent reads reflect the
|
|
updated value.
|
|
"""
|
|
stats = BattingSeasonStats.create(
|
|
player=player,
|
|
team=team,
|
|
season=11,
|
|
hits=10,
|
|
)
|
|
stats.hits += 3
|
|
stats.save()
|
|
|
|
refreshed = BattingSeasonStats.get_by_id(stats.id)
|
|
assert refreshed.hits == 13
|