Fix sWAR cap validation for /ilmove and /dropadd commands

- Add sWAR cap validation to TransactionBuilder.validate_transaction()
- Use team-specific salary_cap from Team.salary_cap field
- Fall back to config.swar_cap_limit (32.0) if team has no custom cap
- Add major_league_swar_cap field to RosterValidationResult
- Update major_league_swar_status to show / with cap limit
- Add 4 new tests for sWAR cap validation

This fixes a bug where IL moves could put a team over their sWAR cap
because the validation only checked roster counts, not sWAR limits.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-01-06 13:17:05 -06:00
parent 4bdfe3ee0a
commit 889b3a4e2d
3 changed files with 99 additions and 8 deletions

View File

@ -1 +1 @@
2.25.5
2.25.6

View File

@ -87,6 +87,7 @@ class RosterValidationResult:
minor_league_limit: int = 6
major_league_swar: float = 0.0
minor_league_swar: float = 0.0
major_league_swar_cap: float = 32.0 # Team's sWAR cap limit
pre_existing_ml_swar_change: float = 0.0
pre_existing_mil_swar_change: float = 0.0
pre_existing_transaction_count: int = 0
@ -110,7 +111,10 @@ class RosterValidationResult:
@property
def major_league_swar_status(self) -> str:
"""Status string for major league sWAR."""
return f"📊 Major League sWAR: {self.major_league_swar:.2f}"
if self.major_league_swar > self.major_league_swar_cap:
return f"❌ Major League sWAR: {self.major_league_swar:.2f}/{self.major_league_swar_cap:.1f} (Over cap!)"
else:
return f"✅ Major League sWAR: {self.major_league_swar:.2f}/{self.major_league_swar_cap:.1f}"
@property
def minor_league_swar_status(self) -> str:
@ -447,9 +451,18 @@ class TransactionBuilder:
errors.append(f"Minor League roster would have {projected_mil_size} players (limit: {mil_limit})")
suggestions.append(f"Drop {projected_mil_size - mil_limit} MiL player(s) to make roster legal")
elif projected_mil_size < 0:
is_legal = False
is_legal = False
errors.append("Cannot have negative players on Minor League roster")
# Validate sWAR cap
# Use team-specific cap if set, otherwise fall back to config default
team_swar_cap = self.team.salary_cap if self.team.salary_cap is not None else config.swar_cap_limit
if projected_ml_swar > team_swar_cap:
is_legal = False
errors.append(f"Major League sWAR would be {projected_ml_swar:.2f} (cap: {team_swar_cap:.1f})")
over_cap = projected_ml_swar - team_swar_cap
suggestions.append(f"Remove {over_cap:.2f} sWAR from ML roster to be under cap")
# Add suggestions for empty transaction
if not self.moves:
suggestions.append("Add player moves to build your transaction")
@ -465,6 +478,7 @@ class TransactionBuilder:
minor_league_limit=mil_limit,
major_league_swar=projected_ml_swar,
minor_league_swar=projected_mil_swar,
major_league_swar_cap=team_swar_cap,
pre_existing_ml_swar_change=pre_existing_ml_swar_change,
pre_existing_mil_swar_change=pre_existing_mil_swar_change,
pre_existing_transaction_count=pre_existing_count

View File

@ -24,7 +24,7 @@ from tests.factories import PlayerFactory, TeamFactory
class TestTransactionBuilder:
"""Test TransactionBuilder core functionality."""
@pytest.fixture
def mock_team(self):
"""Create a mock team for testing."""
@ -33,9 +33,10 @@ class TestTransactionBuilder:
abbrev='WV',
sname='Black Bears',
lname='West Virginia Black Bears',
season=12
season=12,
salary_cap=50.0 # Set high cap to avoid sWAR validation failures in roster tests
)
@pytest.fixture
def mock_player(self):
"""Create a mock player for testing."""
@ -410,7 +411,83 @@ class TestTransactionBuilder:
assert validation.major_league_count == 24 # No changes
assert len(validation.suggestions) == 1
assert "Add player moves" in validation.suggestions[0]
@pytest.mark.asyncio
async def test_validate_transaction_over_swar_cap(self, mock_roster):
"""Test validation fails when sWAR exceeds team cap."""
# Create a team with a low salary cap (30.0)
low_cap_team = Team(
id=499,
abbrev='WV',
sname='Black Bears',
lname='West Virginia Black Bears',
season=12,
salary_cap=30.0 # Low cap - roster has 24 * 1.5 = 36.0 sWAR
)
builder = TransactionBuilder(low_cap_team, user_id=12345)
with patch.object(builder, '_current_roster', mock_roster):
with patch.object(builder, '_roster_loaded', True):
validation = await builder.validate_transaction()
# Should fail due to sWAR cap (36.0 > 30.0)
assert validation.is_legal is False
assert validation.major_league_swar == 36.0 # 24 players * 1.5 wara
assert validation.major_league_swar_cap == 30.0
assert any("sWAR would be 36.00 (cap: 30.0)" in error for error in validation.errors)
assert any("Remove 6.00 sWAR" in suggestion for suggestion in validation.suggestions)
@pytest.mark.asyncio
async def test_validate_transaction_under_swar_cap(self, mock_team, mock_roster):
"""Test validation passes when sWAR is under team cap."""
# mock_team has salary_cap=50.0, roster has 36.0 sWAR
builder = TransactionBuilder(mock_team, user_id=12345)
with patch.object(builder, '_current_roster', mock_roster):
with patch.object(builder, '_roster_loaded', True):
validation = await builder.validate_transaction()
# Should pass - 36.0 < 50.0
assert validation.is_legal is True
assert validation.major_league_swar == 36.0
assert validation.major_league_swar_cap == 50.0
@pytest.mark.asyncio
async def test_swar_status_display_over_cap(self):
"""Test sWAR status display shows correct format when over cap."""
validation = RosterValidationResult(
is_legal=False,
major_league_count=26,
minor_league_count=6,
warnings=[],
errors=[],
suggestions=[],
major_league_swar=35.5,
minor_league_swar=3.0,
major_league_swar_cap=33.5
)
assert "" in validation.major_league_swar_status
assert "35.50/33.5" in validation.major_league_swar_status
assert "Over cap!" in validation.major_league_swar_status
@pytest.mark.asyncio
async def test_swar_status_display_under_cap(self):
"""Test sWAR status display shows correct format when under cap."""
validation = RosterValidationResult(
is_legal=True,
major_league_count=26,
minor_league_count=6,
warnings=[],
errors=[],
suggestions=[],
major_league_swar=31.0,
minor_league_swar=3.0,
major_league_swar_cap=33.5
)
assert "" in validation.major_league_swar_status
assert "31.00/33.5" in validation.major_league_swar_status
assert "Over cap!" not in validation.major_league_swar_status
@pytest.mark.asyncio
async def test_submit_transaction_empty(self, builder):
"""Test submitting empty transaction fails."""