strat-gameplay-webapp/backend/tests/unit/terminal_client/test_commands.py
Cal Corum 440adf2c26 CLAUDE: Update REPL for new GameState and standardize UV commands
Updated terminal client REPL to work with refactored GameState structure
where current_batter/pitcher/catcher are now LineupPlayerState objects
instead of integer IDs. Also standardized all documentation to properly
show 'uv run' prefixes for Python commands.

REPL Updates:
- terminal_client/display.py: Access lineup_id from LineupPlayerState objects
- terminal_client/repl.py: Fix typos (self.current_game → self.current_game_id)
- tests/unit/terminal_client/test_commands.py: Create proper LineupPlayerState
  objects in test fixtures (2 tests fixed, all 105 terminal client tests passing)

Documentation Updates (100+ command examples):
- CLAUDE.md: Updated pytest examples to use 'uv run' prefix
- terminal_client/CLAUDE.md: Updated ~40 command examples
- tests/CLAUDE.md: Updated all test commands (unit, integration, debugging)
- app/*/CLAUDE.md: Updated test and server startup commands (5 files)

All Python commands now consistently use 'uv run' prefix to align with
project's UV migration, improving developer experience and preventing
confusion about virtual environment activation.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 09:59:13 -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)
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