paper-dynasty-gameplay-webapp/tests/integration/services/test_ai_service_simple.py
Cal Corum 7fd9079425 CLAUDE: Improve AI Service test coverage from 60% to 72% with comprehensive integration tests
Added targeted integration tests to cover previously uncovered conditional branches
and edge cases in AI decision-making logic:

- test_ai_service_focused_coverage.py: 11 tests for key missing branches
  * Steal opportunity conditions (lines 91, 93, 98-99, 108)
  * Steal to third/home scenarios (lines 129, 157, 161)
  * Defensive alignment logic (lines 438, 480)
  * Tag decision branches (lines 204, 253)

- test_ai_service_final_coverage.py: 10 tests for remaining gaps
  * Complex steal conditions (lines 95, 118-119, 132, 136-137, 141)
  * Late inning steal logic (lines 159, 163)
  * Uncapped advance bounds checking (lines 382-388)
  * Complex defensive scenarios (lines 440-449)

- test_ai_service_coverage.py: Comprehensive coverage tests (unused due to complexity)

Fixed:
- Player model relationship syntax (removed unsupported cascade_delete parameter)
- Existing test assertion in test_ai_service_simple.py for steal to home scenario

Coverage improvement: 369 statements, 147→105 missed lines (60%→72% coverage)
All 49 AI Service tests now pass with comprehensive integration testing.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-29 21:28:43 -05:00

249 lines
9.6 KiB
Python

"""
Simple AIService Integration Test Example
Demonstrates what AIService integration tests would look like with proper setup.
This shows the testing approach for improving coverage from 60% to 90%+.
"""
import pytest
from unittest.mock import Mock
from app.models.ai_responses import JumpResponse
from app.models.position_rating import PositionRating
from app.services.ai_service import AIService
from tests.factories.manager_ai_factory import ManagerAiFactory
from tests.factories.player_factory import PlayerFactory
class TestAIServiceIntegrationExample:
"""Example integration tests that would increase AIService coverage."""
def test_steal_with_real_database_catcher_query(self, db_session):
"""Example: Test steal decision with actual database query for catcher defense."""
# Create real catcher with position rating
catcher = PlayerFactory.create_catcher(db_session, name="Elite Catcher")
catcher_defense = PositionRating(
player_id=catcher.id,
variant=0,
position='C',
arm=9, # Excellent arm
range=7,
error=1
)
db_session.add(catcher_defense)
db_session.commit()
# Create AI and properly mocked game
aggressive_ai = ManagerAiFactory.create(db_session, steal=9, running=8)
mock_game = self._create_proper_game_mock(catcher, db_session)
ai_service = AIService(db_session)
# This executes real database query: session.exec(select(PositionRating)...)
result = ai_service.check_steal_opportunity(aggressive_ai, mock_game, 2)
assert isinstance(result, JumpResponse)
assert result.min_safe is not None
# Real catcher arm (9) affects battery_hold calculation
def test_steal_edge_case_branches(self, db_session):
"""Example: Test uncovered conditional branches in steal logic."""
# Test case that hits line 91: steal > 8 and run_diff <= 5
high_steal_ai = ManagerAiFactory.create(db_session, steal=9)
catcher = PlayerFactory.create_catcher(db_session)
self._create_catcher_defense(catcher, db_session, arm=5)
mock_game = self._create_proper_game_mock(catcher, db_session)
# Set specific game state to hit uncovered branch
mock_game.current_play_or_none.return_value.ai_run_diff = 3 # <= 5
mock_game.current_play_or_none.return_value.starting_outs = 1
ai_service = AIService(db_session)
result = ai_service.check_steal_opportunity(high_steal_ai, mock_game, 2)
# This should hit the uncovered line 91: min_safe = 13 + num_outs = 14
assert result.min_safe == 14
def test_steal_missing_runner_error_handling(self, db_session):
"""Example: Test error handling when runner is missing."""
ai = ManagerAiFactory.create(db_session, steal=5)
catcher = PlayerFactory.create_catcher(db_session)
self._create_catcher_defense(catcher, db_session)
mock_game = self._create_proper_game_mock(catcher, db_session)
# Remove runner to trigger error path
mock_game.current_play_or_none.return_value.on_first = None
ai_service = AIService(db_session)
# This tests the uncovered error handling branch
with pytest.raises(ValueError, match="no runner found on first"):
ai_service.check_steal_opportunity(ai, mock_game, 2)
def _create_proper_game_mock(self, catcher, db_session):
"""Create properly configured game mock with realistic values."""
mock_game = Mock()
mock_play = Mock()
# Create realistic runner
runner = PlayerFactory.create(db_session, name="Fast Runner")
mock_runner = Mock()
mock_runner.player.name = runner.name
mock_runner.card.batterscouting.battingcard.steal_auto = False
mock_runner.card.batterscouting.battingcard.steal_high = 15
mock_runner.card.batterscouting.battingcard.steal_low = 12
# Setup play with proper numeric values (not Mocks)
mock_play.on_first = mock_runner
mock_play.away_score = 4 # Actual number
mock_play.home_score = 2 # Actual number
mock_play.ai_run_diff = 2 # AI is home team, so 2-4 = -2, but we set directly
mock_play.starting_outs = 1
mock_play.outs = 1
# Setup catcher for database query
mock_play.catcher = Mock()
mock_play.catcher.player_id = catcher.id
mock_play.catcher.card.variant = 0
# Setup pitcher
mock_play.pitcher.card.pitcherscouting.pitchingcard.hold = 5
mock_game.current_play_or_none.return_value = mock_play
mock_game.id = 12345
return mock_game
def _create_catcher_defense(self, catcher, db_session, arm=7):
"""Helper to create catcher defensive rating."""
defense = PositionRating(
player_id=catcher.id,
variant=0,
position='C',
arm=arm,
range=6,
error=2
)
db_session.add(defense)
db_session.commit()
return defense
class TestAIServiceCoverageImprovements:
"""Examples of specific tests that would improve coverage metrics."""
def test_steal_to_home_scenario(self, db_session):
"""Test steal to home (base 4) - covers lines 151-171."""
ai = ManagerAiFactory.create(db_session, steal=6)
catcher = PlayerFactory.create_catcher(db_session)
self._create_catcher_defense(catcher, db_session)
mock_game = Mock()
mock_play = Mock()
# Setup for steal to home
runner = PlayerFactory.create(db_session)
mock_runner = Mock()
mock_runner.player.name = runner.name
mock_runner.card.batterscouting.battingcard.steal_low = 10
mock_play.on_third = mock_runner # Runner on third for steal home
mock_play.away_score = 3
mock_play.home_score = 3 # Tied game
mock_play.ai_run_diff = 0 # This triggers home steal logic
mock_play.starting_outs = 1
mock_play.inning_num = 8 # Late inning
mock_play.catcher = Mock()
mock_play.catcher.player_id = catcher.id
mock_play.catcher.card.variant = 0
mock_play.pitcher.card.pitcherscouting.pitchingcard.hold = 4
mock_game.current_play_or_none.return_value = mock_play
mock_game.id = 67890
ai_service = AIService(db_session)
result = ai_service.check_steal_opportunity(ai, mock_game, 4) # Steal home
# This covers the late inning branch in home steal logic (line 158-159)
assert isinstance(result, JumpResponse)
assert result.min_safe == 6 # inning > 7 and steal >= 5 → min_safe = 6
def test_default_case_branch(self, db_session):
"""Test default case in steal logic - covers lines 98-99."""
very_low_steal_ai = ManagerAiFactory.create(db_session, steal=1) # Very low steal
catcher = PlayerFactory.create_catcher(db_session)
self._create_catcher_defense(catcher, db_session)
mock_game = self._create_basic_mock(catcher, db_session)
# Set conditions that hit default case
mock_game.current_play_or_none.return_value.ai_run_diff = 3 # <= 5
mock_game.current_play_or_none.return_value.starting_outs = 0
ai_service = AIService(db_session)
result = ai_service.check_steal_opportunity(very_low_steal_ai, mock_game, 2)
# Should hit default case: min_safe = 17 + num_outs = 17
assert result.min_safe == 17
def test_auto_jump_conditions(self, db_session):
"""Test auto jump logic - covers lines 107-108."""
high_steal_ai = ManagerAiFactory.create(db_session, steal=8) # > 7
catcher = PlayerFactory.create_catcher(db_session)
self._create_catcher_defense(catcher, db_session)
mock_game = self._create_basic_mock(catcher, db_session)
mock_play = mock_game.current_play_or_none.return_value
mock_play.ai_run_diff = 2 # <= 5
mock_play.starting_outs = 1 # < 2
# Set runner to have steal_auto = True
mock_play.on_first.card.batterscouting.battingcard.steal_auto = True
ai_service = AIService(db_session)
result = ai_service.check_steal_opportunity(high_steal_ai, mock_game, 2)
# Should hit run_if_auto_jump = True and steal_auto = True branch
assert result.run_if_auto_jump is True
assert "WILL SEND" in result.ai_note
def _create_basic_mock(self, catcher, db_session):
"""Create basic game mock for testing."""
mock_game = Mock()
mock_play = Mock()
runner = PlayerFactory.create(db_session)
mock_runner = Mock()
mock_runner.player.name = runner.name
mock_runner.card.batterscouting.battingcard.steal_auto = False
mock_runner.card.batterscouting.battingcard.steal_high = 14
mock_play.on_first = mock_runner
mock_play.away_score = 5
mock_play.home_score = 3
mock_play.ai_run_diff = 2
mock_play.starting_outs = 0
mock_play.catcher = Mock()
mock_play.catcher.player_id = catcher.id
mock_play.catcher.card.variant = 0
mock_play.pitcher.card.pitcherscouting.pitchingcard.hold = 5
mock_game.current_play_or_none.return_value = mock_play
mock_game.id = 99999
return mock_game
def _create_catcher_defense(self, catcher, db_session, arm=6):
"""Create catcher defensive rating."""
defense = PositionRating(
player_id=catcher.id,
variant=0,
position='C',
arm=arm,
range=5,
error=2
)
db_session.add(defense)
db_session.commit()
return defense