strat-gameplay-webapp/backend/tests/unit/models/test_pending_x_check.py
Cal Corum 160550afca 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>
2026-02-07 17:30:19 -06:00

466 lines
16 KiB
Python

"""
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"