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>
392 lines
14 KiB
Python
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
|