CLAUDE: Add comprehensive unit tests for PendingXCheck model
Test coverage: - Creation with minimal fields (required only) - Creation with optional fields (SPD, result selection, DECIDE) - Field validation (d20, d6, chart_row, error_result, etc.) - Range constraints (d20: 1-20, d6: 1-6, bases: proper values) - Mutability during workflow (can update selections) Results: - 19 new tests, all passing - Total: 1005 unit tests passing (was 986) - PendingXCheck model fully validated Next: Create XCheckWizard frontend component Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
defa06653d
commit
160550afca
465
backend/tests/unit/models/test_pending_x_check.py
Normal file
465
backend/tests/unit/models/test_pending_x_check.py
Normal file
@ -0,0 +1,465 @@
|
||||
"""
|
||||
Unit tests for PendingXCheck model.
|
||||
|
||||
Tests the interactive x-check state model including validation,
|
||||
field constraints, and workflow state tracking.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.models.game_models import PendingXCheck
|
||||
|
||||
|
||||
class TestPendingXCheckCreation:
|
||||
"""Test PendingXCheck model creation and basic validation."""
|
||||
|
||||
def test_create_minimal_pending_x_check(self):
|
||||
"""Should create PendingXCheck with required fields only."""
|
||||
pending = PendingXCheck(
|
||||
position="SS",
|
||||
ab_roll_id="test123",
|
||||
d20_roll=12,
|
||||
d6_individual=[3, 4, 5],
|
||||
d6_total=12,
|
||||
chart_row=["G1", "G2", "G3", "SI1", "SI2"],
|
||||
chart_type="infield",
|
||||
defender_lineup_id=42,
|
||||
)
|
||||
|
||||
assert pending.position == "SS"
|
||||
assert pending.ab_roll_id == "test123"
|
||||
assert pending.d20_roll == 12
|
||||
assert pending.d6_individual == [3, 4, 5]
|
||||
assert pending.d6_total == 12
|
||||
assert pending.chart_row == ["G1", "G2", "G3", "SI1", "SI2"]
|
||||
assert pending.chart_type == "infield"
|
||||
assert pending.defender_lineup_id == 42
|
||||
|
||||
# Optional fields should be None
|
||||
assert pending.spd_d20 is None
|
||||
assert pending.selected_result is None
|
||||
assert pending.error_result is None
|
||||
assert pending.decide_runner_base is None
|
||||
assert pending.decide_target_base is None
|
||||
assert pending.decide_advance is None
|
||||
assert pending.decide_throw is None
|
||||
assert pending.decide_d20 is None
|
||||
|
||||
def test_create_with_spd_d20(self):
|
||||
"""Should create PendingXCheck with SPD d20 pre-rolled."""
|
||||
pending = PendingXCheck(
|
||||
position="C",
|
||||
ab_roll_id="test456",
|
||||
d20_roll=10,
|
||||
d6_individual=[2, 3, 4],
|
||||
d6_total=9,
|
||||
chart_row=["G1", "SPD", "G3", "SI1", "SI2"],
|
||||
chart_type="catcher",
|
||||
spd_d20=15,
|
||||
defender_lineup_id=99,
|
||||
)
|
||||
|
||||
assert pending.spd_d20 == 15
|
||||
assert "SPD" in pending.chart_row
|
||||
|
||||
def test_create_with_result_selection(self):
|
||||
"""Should create PendingXCheck with player selections."""
|
||||
pending = PendingXCheck(
|
||||
position="LF",
|
||||
ab_roll_id="test789",
|
||||
d20_roll=18,
|
||||
d6_individual=[5, 5, 6],
|
||||
d6_total=16,
|
||||
chart_row=["F1", "F2", "F2", "F3", "F3"],
|
||||
chart_type="outfield",
|
||||
defender_lineup_id=7,
|
||||
selected_result="F2",
|
||||
error_result="E1",
|
||||
)
|
||||
|
||||
assert pending.selected_result == "F2"
|
||||
assert pending.error_result == "E1"
|
||||
|
||||
def test_create_with_decide_data(self):
|
||||
"""Should create PendingXCheck with DECIDE workflow data."""
|
||||
pending = PendingXCheck(
|
||||
position="2B",
|
||||
ab_roll_id="test999",
|
||||
d20_roll=8,
|
||||
d6_individual=[3, 3, 3],
|
||||
d6_total=9,
|
||||
chart_row=["G1", "G2", "G3", "SI1", "SI2"],
|
||||
chart_type="infield",
|
||||
defender_lineup_id=14,
|
||||
selected_result="G2",
|
||||
error_result="NO",
|
||||
decide_runner_base=2,
|
||||
decide_target_base=3,
|
||||
decide_advance=True,
|
||||
decide_throw="runner",
|
||||
decide_d20=17,
|
||||
)
|
||||
|
||||
assert pending.decide_runner_base == 2
|
||||
assert pending.decide_target_base == 3
|
||||
assert pending.decide_advance is True
|
||||
assert pending.decide_throw == "runner"
|
||||
assert pending.decide_d20 == 17
|
||||
|
||||
|
||||
class TestPendingXCheckValidation:
|
||||
"""Test field validation for PendingXCheck."""
|
||||
|
||||
def test_d20_roll_must_be_1_to_20(self):
|
||||
"""Should reject d20 values outside 1-20 range."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
PendingXCheck(
|
||||
position="SS",
|
||||
ab_roll_id="test",
|
||||
d20_roll=0, # Invalid
|
||||
d6_individual=[1, 2, 3],
|
||||
d6_total=6,
|
||||
chart_row=["G1", "G2", "G3", "SI1", "SI2"],
|
||||
chart_type="infield",
|
||||
defender_lineup_id=1,
|
||||
)
|
||||
assert "d20_roll" in str(exc_info.value)
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
PendingXCheck(
|
||||
position="SS",
|
||||
ab_roll_id="test",
|
||||
d20_roll=21, # Invalid
|
||||
d6_individual=[1, 2, 3],
|
||||
d6_total=6,
|
||||
chart_row=["G1", "G2", "G3", "SI1", "SI2"],
|
||||
chart_type="infield",
|
||||
defender_lineup_id=1,
|
||||
)
|
||||
|
||||
def test_d6_individual_must_have_exactly_3_dice(self):
|
||||
"""Should reject d6_individual with wrong number of dice."""
|
||||
with pytest.raises(ValidationError):
|
||||
PendingXCheck(
|
||||
position="SS",
|
||||
ab_roll_id="test",
|
||||
d20_roll=10,
|
||||
d6_individual=[1, 2], # Too few
|
||||
d6_total=3,
|
||||
chart_row=["G1", "G2", "G3", "SI1", "SI2"],
|
||||
chart_type="infield",
|
||||
defender_lineup_id=1,
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
PendingXCheck(
|
||||
position="SS",
|
||||
ab_roll_id="test",
|
||||
d20_roll=10,
|
||||
d6_individual=[1, 2, 3, 4], # Too many
|
||||
d6_total=10,
|
||||
chart_row=["G1", "G2", "G3", "SI1", "SI2"],
|
||||
chart_type="infield",
|
||||
defender_lineup_id=1,
|
||||
)
|
||||
|
||||
def test_d6_individual_values_must_be_1_to_6(self):
|
||||
"""Should reject d6 values outside 1-6 range."""
|
||||
with pytest.raises(ValidationError):
|
||||
PendingXCheck(
|
||||
position="SS",
|
||||
ab_roll_id="test",
|
||||
d20_roll=10,
|
||||
d6_individual=[0, 2, 3], # 0 invalid
|
||||
d6_total=5,
|
||||
chart_row=["G1", "G2", "G3", "SI1", "SI2"],
|
||||
chart_type="infield",
|
||||
defender_lineup_id=1,
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
PendingXCheck(
|
||||
position="SS",
|
||||
ab_roll_id="test",
|
||||
d20_roll=10,
|
||||
d6_individual=[1, 7, 3], # 7 invalid
|
||||
d6_total=11,
|
||||
chart_row=["G1", "G2", "G3", "SI1", "SI2"],
|
||||
chart_type="infield",
|
||||
defender_lineup_id=1,
|
||||
)
|
||||
|
||||
def test_d6_total_must_be_3_to_18(self):
|
||||
"""Should reject d6_total outside 3-18 range."""
|
||||
with pytest.raises(ValidationError):
|
||||
PendingXCheck(
|
||||
position="SS",
|
||||
ab_roll_id="test",
|
||||
d20_roll=10,
|
||||
d6_individual=[1, 1, 1],
|
||||
d6_total=2, # Invalid
|
||||
chart_row=["G1", "G2", "G3", "SI1", "SI2"],
|
||||
chart_type="infield",
|
||||
defender_lineup_id=1,
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
PendingXCheck(
|
||||
position="SS",
|
||||
ab_roll_id="test",
|
||||
d20_roll=10,
|
||||
d6_individual=[6, 6, 6],
|
||||
d6_total=19, # Invalid
|
||||
chart_row=["G1", "G2", "G3", "SI1", "SI2"],
|
||||
chart_type="infield",
|
||||
defender_lineup_id=1,
|
||||
)
|
||||
|
||||
def test_chart_row_must_have_exactly_5_columns(self):
|
||||
"""Should reject chart_row with wrong number of columns."""
|
||||
with pytest.raises(ValidationError):
|
||||
PendingXCheck(
|
||||
position="SS",
|
||||
ab_roll_id="test",
|
||||
d20_roll=10,
|
||||
d6_individual=[3, 3, 3],
|
||||
d6_total=9,
|
||||
chart_row=["G1", "G2", "G3"], # Too few
|
||||
chart_type="infield",
|
||||
defender_lineup_id=1,
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
PendingXCheck(
|
||||
position="SS",
|
||||
ab_roll_id="test",
|
||||
d20_roll=10,
|
||||
d6_individual=[3, 3, 3],
|
||||
d6_total=9,
|
||||
chart_row=["G1", "G2", "G3", "SI1", "SI2", "Extra"], # Too many
|
||||
chart_type="infield",
|
||||
defender_lineup_id=1,
|
||||
)
|
||||
|
||||
def test_chart_type_must_be_valid(self):
|
||||
"""Should reject invalid chart_type values."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
PendingXCheck(
|
||||
position="SS",
|
||||
ab_roll_id="test",
|
||||
d20_roll=10,
|
||||
d6_individual=[3, 3, 3],
|
||||
d6_total=9,
|
||||
chart_row=["G1", "G2", "G3", "SI1", "SI2"],
|
||||
chart_type="invalid", # Must be infield/outfield/catcher
|
||||
defender_lineup_id=1,
|
||||
)
|
||||
assert "chart_type" in str(exc_info.value)
|
||||
|
||||
def test_error_result_must_be_valid(self):
|
||||
"""Should reject invalid error_result values."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
PendingXCheck(
|
||||
position="SS",
|
||||
ab_roll_id="test",
|
||||
d20_roll=10,
|
||||
d6_individual=[3, 3, 3],
|
||||
d6_total=9,
|
||||
chart_row=["G1", "G2", "G3", "SI1", "SI2"],
|
||||
chart_type="infield",
|
||||
defender_lineup_id=1,
|
||||
error_result="INVALID", # Must be NO/E1/E2/E3/RP
|
||||
)
|
||||
assert "error_result" in str(exc_info.value)
|
||||
|
||||
def test_decide_throw_must_be_valid(self):
|
||||
"""Should reject invalid decide_throw values."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
PendingXCheck(
|
||||
position="SS",
|
||||
ab_roll_id="test",
|
||||
d20_roll=10,
|
||||
d6_individual=[3, 3, 3],
|
||||
d6_total=9,
|
||||
chart_row=["G1", "G2", "G3", "SI1", "SI2"],
|
||||
chart_type="infield",
|
||||
defender_lineup_id=1,
|
||||
decide_throw="invalid", # Must be runner/first
|
||||
)
|
||||
assert "decide_throw" in str(exc_info.value)
|
||||
|
||||
def test_decide_runner_base_must_be_1_to_3(self):
|
||||
"""Should reject decide_runner_base outside 1-3 range."""
|
||||
with pytest.raises(ValidationError):
|
||||
PendingXCheck(
|
||||
position="SS",
|
||||
ab_roll_id="test",
|
||||
d20_roll=10,
|
||||
d6_individual=[3, 3, 3],
|
||||
d6_total=9,
|
||||
chart_row=["G1", "G2", "G3", "SI1", "SI2"],
|
||||
chart_type="infield",
|
||||
defender_lineup_id=1,
|
||||
decide_runner_base=0, # Invalid
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
PendingXCheck(
|
||||
position="SS",
|
||||
ab_roll_id="test",
|
||||
d20_roll=10,
|
||||
d6_individual=[3, 3, 3],
|
||||
d6_total=9,
|
||||
chart_row=["G1", "G2", "G3", "SI1", "SI2"],
|
||||
chart_type="infield",
|
||||
defender_lineup_id=1,
|
||||
decide_runner_base=4, # Invalid (home is target, not source)
|
||||
)
|
||||
|
||||
def test_decide_target_base_must_be_2_to_4(self):
|
||||
"""Should reject decide_target_base outside 2-4 range."""
|
||||
with pytest.raises(ValidationError):
|
||||
PendingXCheck(
|
||||
position="SS",
|
||||
ab_roll_id="test",
|
||||
d20_roll=10,
|
||||
d6_individual=[3, 3, 3],
|
||||
d6_total=9,
|
||||
chart_row=["G1", "G2", "G3", "SI1", "SI2"],
|
||||
chart_type="infield",
|
||||
defender_lineup_id=1,
|
||||
decide_target_base=1, # Invalid (can't advance backwards)
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
PendingXCheck(
|
||||
position="SS",
|
||||
ab_roll_id="test",
|
||||
d20_roll=10,
|
||||
d6_individual=[3, 3, 3],
|
||||
d6_total=9,
|
||||
chart_row=["G1", "G2", "G3", "SI1", "SI2"],
|
||||
chart_type="infield",
|
||||
defender_lineup_id=1,
|
||||
decide_target_base=5, # Invalid
|
||||
)
|
||||
|
||||
def test_spd_d20_must_be_1_to_20(self):
|
||||
"""Should reject spd_d20 outside 1-20 range."""
|
||||
with pytest.raises(ValidationError):
|
||||
PendingXCheck(
|
||||
position="C",
|
||||
ab_roll_id="test",
|
||||
d20_roll=10,
|
||||
d6_individual=[3, 3, 3],
|
||||
d6_total=9,
|
||||
chart_row=["G1", "SPD", "G3", "SI1", "SI2"],
|
||||
chart_type="catcher",
|
||||
defender_lineup_id=1,
|
||||
spd_d20=0, # Invalid
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
PendingXCheck(
|
||||
position="C",
|
||||
ab_roll_id="test",
|
||||
d20_roll=10,
|
||||
d6_individual=[3, 3, 3],
|
||||
d6_total=9,
|
||||
chart_row=["G1", "SPD", "G3", "SI1", "SI2"],
|
||||
chart_type="catcher",
|
||||
defender_lineup_id=1,
|
||||
spd_d20=21, # Invalid
|
||||
)
|
||||
|
||||
def test_decide_d20_must_be_1_to_20(self):
|
||||
"""Should reject decide_d20 outside 1-20 range."""
|
||||
with pytest.raises(ValidationError):
|
||||
PendingXCheck(
|
||||
position="SS",
|
||||
ab_roll_id="test",
|
||||
d20_roll=10,
|
||||
d6_individual=[3, 3, 3],
|
||||
d6_total=9,
|
||||
chart_row=["G1", "G2", "G3", "SI1", "SI2"],
|
||||
chart_type="infield",
|
||||
defender_lineup_id=1,
|
||||
decide_d20=0, # Invalid
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
PendingXCheck(
|
||||
position="SS",
|
||||
ab_roll_id="test",
|
||||
d20_roll=10,
|
||||
d6_individual=[3, 3, 3],
|
||||
d6_total=9,
|
||||
chart_row=["G1", "G2", "G3", "SI1", "SI2"],
|
||||
chart_type="infield",
|
||||
defender_lineup_id=1,
|
||||
decide_d20=21, # Invalid
|
||||
)
|
||||
|
||||
|
||||
class TestPendingXCheckMutability:
|
||||
"""Test that PendingXCheck allows mutation during workflow."""
|
||||
|
||||
def test_can_update_selected_result(self):
|
||||
"""Should allow updating selected_result after creation."""
|
||||
pending = PendingXCheck(
|
||||
position="SS",
|
||||
ab_roll_id="test",
|
||||
d20_roll=10,
|
||||
d6_individual=[3, 3, 3],
|
||||
d6_total=9,
|
||||
chart_row=["G1", "G2", "G3", "SI1", "SI2"],
|
||||
chart_type="infield",
|
||||
defender_lineup_id=1,
|
||||
)
|
||||
|
||||
assert pending.selected_result is None
|
||||
|
||||
# Should be able to mutate
|
||||
pending.selected_result = "G2"
|
||||
assert pending.selected_result == "G2"
|
||||
|
||||
def test_can_update_error_result(self):
|
||||
"""Should allow updating error_result after creation."""
|
||||
pending = PendingXCheck(
|
||||
position="SS",
|
||||
ab_roll_id="test",
|
||||
d20_roll=10,
|
||||
d6_individual=[3, 3, 3],
|
||||
d6_total=9,
|
||||
chart_row=["G1", "G2", "G3", "SI1", "SI2"],
|
||||
chart_type="infield",
|
||||
defender_lineup_id=1,
|
||||
)
|
||||
|
||||
pending.error_result = "E1"
|
||||
assert pending.error_result == "E1"
|
||||
|
||||
def test_can_update_decide_fields(self):
|
||||
"""Should allow updating DECIDE fields during workflow."""
|
||||
pending = PendingXCheck(
|
||||
position="SS",
|
||||
ab_roll_id="test",
|
||||
d20_roll=10,
|
||||
d6_individual=[3, 3, 3],
|
||||
d6_total=9,
|
||||
chart_row=["G1", "G2", "G3", "SI1", "SI2"],
|
||||
chart_type="infield",
|
||||
defender_lineup_id=1,
|
||||
)
|
||||
|
||||
# Simulate DECIDE workflow
|
||||
pending.decide_runner_base = 2
|
||||
pending.decide_target_base = 3
|
||||
pending.decide_advance = True
|
||||
pending.decide_throw = "first"
|
||||
|
||||
assert pending.decide_runner_base == 2
|
||||
assert pending.decide_target_base == 3
|
||||
assert pending.decide_advance is True
|
||||
assert pending.decide_throw == "first"
|
||||
Loading…
Reference in New Issue
Block a user