strat-gameplay-webapp/backend/tests/unit/terminal_client/test_commands.py
Cal Corum beb939b32a CLAUDE: Fix all unit test failures and implement 100% test requirement
Test Fixes (609/609 passing):
- Fixed DiceSystem API to accept team_id/player_id parameters for audit trails
- Fixed dice roll history timing issue in test
- Fixed terminal client mock to match resolve_play signature (X-Check params)
- Fixed result chart test mocks with missing pitching fields
- Fixed flaky test by using groundball_a (exists in both batting/pitching)

Documentation Updates:
- Added Testing Policy section to backend/CLAUDE.md
- Added Testing Policy section to tests/CLAUDE.md
- Documented 100% unit test requirement before commits
- Added git hook setup instructions

Git Hook System:
- Created .git-hooks/pre-commit script (enforces 100% test pass)
- Created .git-hooks/install-hooks.sh (easy installation)
- Created .git-hooks/README.md (hook documentation)
- Hook automatically runs all unit tests before each commit
- Blocks commits if any test fails

All 609 unit tests now passing (100%)
Integration tests have known asyncpg connection issues (documented)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 19:35:21 -06:00

392 lines
14 KiB
Python

"""
Unit tests for terminal client shared commands.
"""
import pytest
from uuid import uuid4
from unittest.mock import AsyncMock, MagicMock, patch
from terminal_client.commands import GameCommands
from app.models.game_models import GameState
@pytest.fixture
def game_commands():
"""Create GameCommands instance with mocked dependencies."""
commands = GameCommands()
commands.db_ops = AsyncMock()
return commands
@pytest.mark.asyncio
async def test_create_new_game_success(game_commands):
"""Test successful game creation."""
game_id = uuid4()
with patch('terminal_client.commands.state_manager') as mock_sm:
with patch('terminal_client.commands.game_engine') as mock_ge:
with patch('terminal_client.commands.uuid4', return_value=game_id):
with patch('terminal_client.commands.Config') as mock_config:
# Setup mocks
from app.models.game_models import LineupPlayerState
mock_batter = LineupPlayerState(
lineup_id=1,
card_id=100,
position='CF',
batting_order=1
)
mock_state = GameState(
game_id=game_id,
league_id='sba',
home_team_id=1,
away_team_id=2,
current_batter=mock_batter,
inning=1,
half='top'
)
mock_sm.create_game = AsyncMock(return_value=mock_state)
mock_ge.start_game = AsyncMock(return_value=mock_state)
# Execute
gid, success = await game_commands.create_new_game()
# Verify
assert success is True
assert gid == game_id
mock_sm.create_game.assert_called_once()
mock_ge.start_game.assert_called_once()
mock_config.set_current_game.assert_called_once_with(game_id)
@pytest.mark.asyncio
async def test_create_new_game_with_pd_league(game_commands):
"""Test game creation with PD league."""
game_id = uuid4()
with patch('terminal_client.commands.state_manager') as mock_sm:
with patch('terminal_client.commands.game_engine') as mock_ge:
with patch('terminal_client.commands.uuid4', return_value=game_id):
with patch('terminal_client.commands.Config') as mock_config:
# Setup mocks
from app.models.game_models import LineupPlayerState
mock_batter = LineupPlayerState(
lineup_id=1,
card_id=200,
position='SS',
batting_order=1
)
mock_state = GameState(
game_id=game_id,
league_id='pd',
home_team_id=3,
away_team_id=5,
current_batter=mock_batter,
inning=1,
half='top'
)
mock_sm.create_game = AsyncMock(return_value=mock_state)
mock_ge.start_game = AsyncMock(return_value=mock_state)
# Execute
gid, success = await game_commands.create_new_game(
league='pd',
home_team=3,
away_team=5
)
# Verify
assert success is True
assert gid == game_id
# Check that PD-specific lineup creation was called
assert game_commands.db_ops.add_pd_lineup_card.call_count == 18 # 9 per team
@pytest.mark.asyncio
async def test_create_new_game_failure(game_commands):
"""Test game creation failure."""
with patch('terminal_client.commands.state_manager') as mock_sm:
with patch('terminal_client.commands.uuid4', return_value=uuid4()):
# Setup mocks to fail
mock_sm.create_game = AsyncMock(side_effect=Exception("Database error"))
# Execute
gid, success = await game_commands.create_new_game()
# Verify
assert success is False
@pytest.mark.asyncio
async def test_submit_defensive_decision_success(game_commands):
"""Test successful defensive decision submission."""
game_id = uuid4()
with patch('terminal_client.commands.game_engine') as mock_ge:
mock_state = MagicMock()
mock_ge.submit_defensive_decision = AsyncMock(return_value=mock_state)
success = await game_commands.submit_defensive_decision(
game_id=game_id,
alignment='shifted_left',
hold_runners=[1, 2]
)
assert success is True
mock_ge.submit_defensive_decision.assert_called_once()
@pytest.mark.asyncio
async def test_submit_defensive_decision_failure(game_commands):
"""Test defensive decision submission failure."""
game_id = uuid4()
with patch('terminal_client.commands.game_engine') as mock_ge:
mock_ge.submit_defensive_decision = AsyncMock(side_effect=Exception("Invalid decision"))
success = await game_commands.submit_defensive_decision(
game_id=game_id,
alignment='invalid_alignment'
)
assert success is False
@pytest.mark.asyncio
async def test_submit_offensive_decision_success(game_commands):
"""Test successful offensive decision submission."""
game_id = uuid4()
with patch('terminal_client.commands.game_engine') as mock_ge:
mock_state = MagicMock()
mock_ge.submit_offensive_decision = AsyncMock(return_value=mock_state)
success = await game_commands.submit_offensive_decision(
game_id=game_id,
approach='power',
steal_attempts=[2],
hit_and_run=True
)
assert success is True
mock_ge.submit_offensive_decision.assert_called_once()
@pytest.mark.asyncio
async def test_submit_offensive_decision_failure(game_commands):
"""Test offensive decision submission failure."""
game_id = uuid4()
with patch('terminal_client.commands.game_engine') as mock_ge:
mock_ge.submit_offensive_decision = AsyncMock(side_effect=Exception("Invalid decision"))
success = await game_commands.submit_offensive_decision(
game_id=game_id,
approach='invalid_approach'
)
assert success is False
@pytest.mark.asyncio
async def test_resolve_play_success(game_commands):
"""Test successful play resolution."""
game_id = uuid4()
with patch('terminal_client.commands.game_engine') as mock_ge:
with patch('terminal_client.commands.display'):
# Setup mock result with proper attributes
mock_result = MagicMock()
mock_result.description = "Single to center field"
mock_result.outs_recorded = 0
mock_result.runs_scored = 1
# Setup mock state
mock_state = MagicMock()
mock_state.away_score = 1
mock_state.home_score = 0
mock_ge.resolve_play = AsyncMock(return_value=mock_result)
mock_ge.get_game_state = AsyncMock(return_value=mock_state)
success = await game_commands.resolve_play(game_id)
assert success is True
mock_ge.resolve_play.assert_called_once_with(game_id, None, None, None, None)
mock_ge.get_game_state.assert_called_once_with(game_id)
@pytest.mark.asyncio
async def test_resolve_play_game_not_found(game_commands):
"""Test play resolution when game is not found."""
game_id = uuid4()
with patch('terminal_client.commands.game_engine') as mock_ge:
mock_result = MagicMock()
mock_ge.resolve_play = AsyncMock(return_value=mock_result)
mock_ge.get_game_state = AsyncMock(return_value=None) # Game not found
success = await game_commands.resolve_play(game_id)
assert success is False
@pytest.mark.asyncio
async def test_resolve_play_failure(game_commands):
"""Test play resolution failure."""
game_id = uuid4()
with patch('terminal_client.commands.game_engine') as mock_ge:
mock_ge.resolve_play = AsyncMock(side_effect=Exception("Resolution error"))
success = await game_commands.resolve_play(game_id)
assert success is False
@pytest.mark.asyncio
async def test_quick_play_rounds_success(game_commands):
"""Test successful quick play execution."""
game_id = uuid4()
with patch('terminal_client.commands.game_engine') as mock_ge:
with patch('terminal_client.commands.asyncio.sleep', new_callable=AsyncMock):
with patch('terminal_client.commands.display'):
# Setup mocks
mock_state = MagicMock()
mock_state.status = "active"
mock_state.away_score = 0
mock_state.home_score = 0
mock_state.inning = 1
mock_state.half = "top"
mock_state.outs = 0
mock_result = MagicMock()
mock_result.description = "Groundout"
mock_ge.get_game_state = AsyncMock(return_value=mock_state)
mock_ge.submit_defensive_decision = AsyncMock()
mock_ge.submit_offensive_decision = AsyncMock()
mock_ge.resolve_play = AsyncMock(return_value=mock_result)
# Execute 3 plays
plays_completed = await game_commands.quick_play_rounds(game_id, count=3)
# Verify
assert plays_completed == 3
assert mock_ge.resolve_play.call_count == 3
@pytest.mark.asyncio
async def test_quick_play_rounds_game_ends(game_commands):
"""Test quick play when game ends mid-execution."""
game_id = uuid4()
with patch('terminal_client.commands.game_engine') as mock_ge:
with patch('terminal_client.commands.asyncio.sleep', new_callable=AsyncMock):
with patch('terminal_client.commands.display'):
# Setup mocks - game becomes completed after 2 plays
mock_state_active_1 = MagicMock()
mock_state_active_1.status = "active"
mock_state_active_1.away_score = 0
mock_state_active_1.home_score = 0
mock_state_active_1.inning = 1
mock_state_active_1.half = "top"
mock_state_active_1.outs = 0
mock_state_active_2 = MagicMock()
mock_state_active_2.status = "active"
mock_state_active_2.away_score = 1
mock_state_active_2.home_score = 0
mock_state_active_2.inning = 1
mock_state_active_2.half = "top"
mock_state_active_2.outs = 0
mock_state_completed = MagicMock()
mock_state_completed.status = "completed"
mock_result = MagicMock()
mock_result.description = "Game winning hit"
# Setup get_game_state to return:
# 1. active (before play 1)
# 2. active (after play 1)
# 3. active (before play 2)
# 4. active (after play 2)
# 5. completed (before play 3 - should stop)
# 6. completed (final state query)
mock_ge.get_game_state = AsyncMock(
side_effect=[
mock_state_active_1, # Before play 1
mock_state_active_2, # After play 1
mock_state_active_2, # Before play 2
mock_state_active_2, # After play 2
mock_state_completed, # Before play 3 - stops here
mock_state_completed # Final state query
]
)
mock_ge.submit_defensive_decision = AsyncMock()
mock_ge.submit_offensive_decision = AsyncMock()
mock_ge.resolve_play = AsyncMock(return_value=mock_result)
# Execute 5 plays but should stop at 2
plays_completed = await game_commands.quick_play_rounds(game_id, count=5)
# Verify - should only complete 2 plays
assert plays_completed == 2
assert mock_ge.resolve_play.call_count == 2
@pytest.mark.asyncio
async def test_show_game_status_success(game_commands):
"""Test showing game status successfully."""
game_id = uuid4()
with patch('terminal_client.commands.game_engine') as mock_ge:
mock_state = MagicMock()
mock_ge.get_game_state = AsyncMock(return_value=mock_state)
success = await game_commands.show_game_status(game_id)
assert success is True
mock_ge.get_game_state.assert_called_once_with(game_id)
@pytest.mark.asyncio
async def test_show_game_status_not_found(game_commands):
"""Test showing game status when game not found."""
game_id = uuid4()
with patch('terminal_client.commands.game_engine') as mock_ge:
mock_ge.get_game_state = AsyncMock(return_value=None)
success = await game_commands.show_game_status(game_id)
assert success is False
@pytest.mark.asyncio
async def test_show_box_score_success(game_commands):
"""Test showing box score successfully."""
game_id = uuid4()
with patch('terminal_client.commands.game_engine') as mock_ge:
mock_state = MagicMock()
mock_ge.get_game_state = AsyncMock(return_value=mock_state)
success = await game_commands.show_box_score(game_id)
assert success is True
mock_ge.get_game_state.assert_called_once_with(game_id)
@pytest.mark.asyncio
async def test_show_box_score_not_found(game_commands):
"""Test showing box score when game not found."""
game_id = uuid4()
with patch('terminal_client.commands.game_engine') as mock_ge:
mock_ge.get_game_state = AsyncMock(return_value=None)
success = await game_commands.show_box_score(game_id)
assert success is False