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>
408 lines
15 KiB
Python
408 lines
15 KiB
Python
"""
|
|
Focused AIService Integration Tests for Specific Coverage Lines
|
|
|
|
Simple, targeted tests to improve coverage from 60% to 85%+ by hitting specific uncovered branches.
|
|
"""
|
|
import pytest
|
|
from unittest.mock import Mock
|
|
|
|
from app.models.ai_responses import JumpResponse, TagResponse, DefenseResponse
|
|
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 TestAIServiceTargetedCoverage:
|
|
"""Targeted tests for specific uncovered lines in AI Service."""
|
|
|
|
def test_steal_to_second_high_steal_branch_line_91(self, db_session):
|
|
"""Test steal > 8 and run_diff <= 5 branch (line 91)."""
|
|
# Create AI with steal = 9 (> 8)
|
|
high_steal_ai = ManagerAiFactory.create(db_session, steal=9)
|
|
catcher = PlayerFactory.create_catcher(db_session)
|
|
self._create_catcher_defense(catcher, db_session, arm=5)
|
|
|
|
# Create simple mock that triggers the specific branch
|
|
mock_game = Mock()
|
|
mock_play = Mock()
|
|
|
|
# Setup runner on first
|
|
mock_runner = Mock()
|
|
mock_runner.player.name = "Fast Runner"
|
|
mock_runner.card.batterscouting.battingcard.steal_auto = False
|
|
mock_runner.card.batterscouting.battingcard.steal_high = 14
|
|
mock_runner.card.batterscouting.battingcard.steal_low = 11
|
|
|
|
mock_play.on_first = mock_runner
|
|
mock_play.starting_outs = 1
|
|
# Set scores for run_diff = 3: away=2, home=5, ai_team='home' -> (2-5)*-1 = 3 <= 5
|
|
mock_play.away_score = 2
|
|
mock_play.home_score = 5
|
|
|
|
# Setup catcher and pitcher
|
|
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 = 12345
|
|
mock_game.ai_team = 'home'
|
|
|
|
ai_service = AIService(db_session)
|
|
result = ai_service.check_steal_opportunity(high_steal_ai, mock_game, 2)
|
|
|
|
# Should hit line 91: min_safe = 13 + num_outs = 14
|
|
assert result.min_safe == 14
|
|
|
|
def test_steal_to_second_medium_steal_branch_line_93(self, db_session):
|
|
"""Test steal > 6 and run_diff <= 5 branch (line 93)."""
|
|
medium_steal_ai = ManagerAiFactory.create(db_session, steal=7)
|
|
catcher = PlayerFactory.create_catcher(db_session)
|
|
self._create_catcher_defense(catcher, db_session, arm=4)
|
|
|
|
mock_game = Mock()
|
|
mock_play = Mock()
|
|
|
|
mock_runner = Mock()
|
|
mock_runner.player.name = "Runner"
|
|
mock_runner.card.batterscouting.battingcard.steal_auto = False
|
|
mock_runner.card.batterscouting.battingcard.steal_high = 13
|
|
mock_runner.card.batterscouting.battingcard.steal_low = 10
|
|
|
|
mock_play.on_first = mock_runner
|
|
mock_play.starting_outs = 0
|
|
# Run diff = 4: away=1, home=5, ai_team='home' -> (1-5)*-1 = 4 <= 5
|
|
mock_play.away_score = 1
|
|
mock_play.home_score = 5
|
|
|
|
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 = 23456
|
|
mock_game.ai_team = 'home'
|
|
|
|
ai_service = AIService(db_session)
|
|
result = ai_service.check_steal_opportunity(medium_steal_ai, mock_game, 2)
|
|
|
|
# Should hit line 93: min_safe = 14 + num_outs = 14
|
|
assert result.min_safe == 14
|
|
|
|
def test_steal_to_second_default_branch_lines_98_99(self, db_session):
|
|
"""Test default case in steal logic (lines 98-99)."""
|
|
low_steal_ai = ManagerAiFactory.create(db_session, steal=1)
|
|
catcher = PlayerFactory.create_catcher(db_session)
|
|
self._create_catcher_defense(catcher, db_session, arm=5)
|
|
|
|
mock_game = Mock()
|
|
mock_play = Mock()
|
|
|
|
mock_runner = Mock()
|
|
mock_runner.player.name = "Slow Runner"
|
|
mock_runner.card.batterscouting.battingcard.steal_auto = False
|
|
mock_runner.card.batterscouting.battingcard.steal_high = 12
|
|
mock_runner.card.batterscouting.battingcard.steal_low = 9
|
|
|
|
mock_play.on_first = mock_runner
|
|
mock_play.starting_outs = 0
|
|
# Run diff = 2: away=3, home=5, ai_team='home' -> (3-5)*-1 = 2 <= 5
|
|
mock_play.away_score = 3
|
|
mock_play.home_score = 5
|
|
|
|
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 = 34567
|
|
mock_game.ai_team = 'home'
|
|
|
|
ai_service = AIService(db_session)
|
|
result = ai_service.check_steal_opportunity(low_steal_ai, mock_game, 2)
|
|
|
|
# Should hit default case: min_safe = 17 + num_outs = 17
|
|
assert result.min_safe == 17
|
|
|
|
def test_steal_auto_jump_logic_line_108(self, db_session):
|
|
"""Test run_if_auto_jump and steal_auto = True logic (line 108)."""
|
|
high_steal_ai = ManagerAiFactory.create(db_session, steal=8)
|
|
catcher = PlayerFactory.create_catcher(db_session)
|
|
self._create_catcher_defense(catcher, db_session, arm=6)
|
|
|
|
mock_game = Mock()
|
|
mock_play = Mock()
|
|
|
|
mock_runner = Mock()
|
|
mock_runner.player.name = "Auto Steal Runner"
|
|
mock_runner.card.batterscouting.battingcard.steal_auto = True # Key for line 108
|
|
mock_runner.card.batterscouting.battingcard.steal_high = 15
|
|
mock_runner.card.batterscouting.battingcard.steal_low = 12
|
|
|
|
mock_play.on_first = mock_runner
|
|
mock_play.starting_outs = 1 # < 2
|
|
# Run diff = 2: away=3, home=5, ai_team='home' -> (3-5)*-1 = 2 <= 5
|
|
mock_play.away_score = 3
|
|
mock_play.home_score = 5
|
|
|
|
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 = 45678
|
|
mock_game.ai_team = 'home'
|
|
|
|
ai_service = AIService(db_session)
|
|
result = ai_service.check_steal_opportunity(high_steal_ai, mock_game, 2)
|
|
|
|
# Should hit line 108: ai_note with "WILL SEND"
|
|
assert result.run_if_auto_jump is True
|
|
assert "WILL SEND" in result.ai_note
|
|
|
|
def test_steal_to_third_steal_10_branch_line_129(self, db_session):
|
|
"""Test steal to third with steal = 10 (line 129)."""
|
|
max_steal_ai = ManagerAiFactory.create(db_session, steal=10)
|
|
catcher = PlayerFactory.create_catcher(db_session)
|
|
self._create_catcher_defense(catcher, db_session, arm=7)
|
|
|
|
mock_game = Mock()
|
|
mock_play = Mock()
|
|
|
|
mock_runner = Mock()
|
|
mock_runner.player.name = "Runner on Second"
|
|
mock_runner.card.batterscouting.battingcard.steal_auto = False
|
|
mock_runner.card.batterscouting.battingcard.steal_low = 12
|
|
|
|
mock_play.on_second = mock_runner # Runner on second for steal to third
|
|
mock_play.starting_outs = 1
|
|
mock_play.away_score = 4
|
|
mock_play.home_score = 5
|
|
|
|
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 = 56789
|
|
mock_game.ai_team = 'home'
|
|
|
|
ai_service = AIService(db_session)
|
|
result = ai_service.check_steal_opportunity(max_steal_ai, mock_game, 3)
|
|
|
|
# Should hit line 129: min_safe = 12 + num_outs = 13
|
|
assert result.min_safe == 13
|
|
|
|
def test_steal_to_home_steal_10_branch_line_157(self, db_session):
|
|
"""Test steal to home with steal = 10 (line 157)."""
|
|
max_steal_ai = ManagerAiFactory.create(db_session, steal=10)
|
|
catcher = PlayerFactory.create_catcher(db_session)
|
|
self._create_catcher_defense(catcher, db_session, arm=5)
|
|
|
|
mock_game = Mock()
|
|
mock_play = Mock()
|
|
|
|
mock_runner = Mock()
|
|
mock_runner.player.name = "Runner on Third"
|
|
mock_runner.card.batterscouting.battingcard.steal_low = 15
|
|
|
|
mock_play.on_third = mock_runner # Runner on third for steal home
|
|
mock_play.starting_outs = 1
|
|
# Tied game (run_diff = 0) to trigger home steal logic at line 151
|
|
mock_play.away_score = 5
|
|
mock_play.home_score = 5 # Tied: (5-5)*-1 = 0, triggers lines 151-171
|
|
mock_play.inning_num = 7
|
|
|
|
mock_play.catcher = Mock()
|
|
mock_play.catcher.player_id = catcher.id
|
|
mock_play.catcher.card.variant = 0
|
|
mock_play.pitcher.card.pitcherscouting.pitchingcard.hold = 3
|
|
|
|
mock_game.current_play_or_none.return_value = mock_play
|
|
mock_game.id = 67890
|
|
mock_game.ai_team = 'home'
|
|
|
|
ai_service = AIService(db_session)
|
|
result = ai_service.check_steal_opportunity(max_steal_ai, mock_game, 4) # Base 4 = home
|
|
|
|
# Should hit line 157: min_safe = 5
|
|
assert result.min_safe == 5
|
|
|
|
def test_steal_to_home_medium_steal_branch_line_161(self, db_session):
|
|
"""Test steal to home with steal > 5 (line 161)."""
|
|
medium_steal_ai = ManagerAiFactory.create(db_session, steal=6)
|
|
catcher = PlayerFactory.create_catcher(db_session)
|
|
self._create_catcher_defense(catcher, db_session, arm=6)
|
|
|
|
mock_game = Mock()
|
|
mock_play = Mock()
|
|
|
|
mock_runner = Mock()
|
|
mock_runner.player.name = "Runner on Third"
|
|
mock_runner.card.batterscouting.battingcard.steal_low = 14
|
|
|
|
mock_play.on_third = mock_runner
|
|
mock_play.starting_outs = 1
|
|
# Close game to trigger home steal logic
|
|
mock_play.away_score = 4
|
|
mock_play.home_score = 4 # Tied: (4-4)*-1 = 0
|
|
mock_play.inning_num = 5 # <= 7, so doesn't hit late inning branch
|
|
|
|
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 = 78901
|
|
mock_game.ai_team = 'home'
|
|
|
|
ai_service = AIService(db_session)
|
|
result = ai_service.check_steal_opportunity(medium_steal_ai, mock_game, 4)
|
|
|
|
# Should hit line 161: min_safe = 7
|
|
assert result.min_safe == 7
|
|
|
|
def test_defense_two_outs_hold_second_line_438(self, db_session):
|
|
"""Test defensive hold with 2 outs and runner on second (line 438)."""
|
|
ai = ManagerAiFactory.create(db_session, defense=5)
|
|
catcher = PlayerFactory.create_catcher(db_session)
|
|
self._create_catcher_defense(catcher, db_session, arm=7)
|
|
|
|
mock_game = Mock()
|
|
mock_play = Mock()
|
|
|
|
runner = PlayerFactory.create(db_session, name="Runner on Second")
|
|
mock_runner = Mock()
|
|
mock_runner.player.name = runner.name
|
|
|
|
mock_play.starting_outs = 2 # Key: 2 outs
|
|
mock_play.on_base_code = 2 # Runner on second only
|
|
mock_play.on_second = mock_runner
|
|
mock_play.on_first = None
|
|
mock_play.on_third = None
|
|
mock_play.ai_run_diff = 1
|
|
|
|
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 = 89012
|
|
|
|
ai_service = AIService(db_session)
|
|
result = ai_service.set_defensive_alignment(ai, mock_game)
|
|
|
|
# Should hit line 438: hold_second = True
|
|
assert result.hold_second is True
|
|
assert "hold" in result.ai_note
|
|
|
|
def test_defense_straight_up_message_line_480(self, db_session):
|
|
"""Test 'play straight up' message (line 480)."""
|
|
ai = ManagerAiFactory.create(db_session, defense=5)
|
|
catcher = PlayerFactory.create_catcher(db_session)
|
|
self._create_catcher_defense(catcher, db_session, arm=4)
|
|
|
|
mock_game = Mock()
|
|
mock_play = Mock()
|
|
|
|
runner = PlayerFactory.create(db_session, name="Regular Runner")
|
|
mock_runner = Mock()
|
|
mock_runner.player.name = runner.name
|
|
mock_runner.card.batterscouting.battingcard.steal_auto = False
|
|
|
|
mock_play.starting_outs = 1
|
|
mock_play.on_base_code = 1 # Runner on first
|
|
mock_play.on_first = mock_runner
|
|
mock_play.on_second = None
|
|
mock_play.on_third = None # No runner on third, so no special alignment
|
|
mock_play.ai_run_diff = 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 = 3
|
|
|
|
mock_game.current_play_or_none.return_value = mock_play
|
|
mock_game.id = 90123
|
|
|
|
ai_service = AIService(db_session)
|
|
result = ai_service.set_defensive_alignment(ai, mock_game)
|
|
|
|
# Should hit line 480: "play straight up"
|
|
assert "play straight up" in result.ai_note
|
|
|
|
def _create_catcher_defense(self, catcher, db_session, arm=5):
|
|
"""Create position rating for catcher."""
|
|
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
|
|
|
|
|
|
class TestAIServiceTagCoverage:
|
|
"""Test tag logic coverage."""
|
|
|
|
def test_tag_from_second_aggressive_running_line_204(self, db_session):
|
|
"""Test tag from second with aggressive running (line 204)."""
|
|
aggressive_ai = ManagerAiFactory.create(
|
|
db_session,
|
|
running=9, # High running
|
|
ahead_aggression=7,
|
|
behind_aggression=3
|
|
)
|
|
|
|
mock_game = Mock()
|
|
mock_play = Mock()
|
|
|
|
mock_play.starting_outs = 1 # Will subtract 2 for min_safe
|
|
mock_play.ai_run_diff = 3 # Ahead, so use ahead_aggression
|
|
|
|
mock_game.current_play_or_none.return_value = mock_play
|
|
mock_game.id = 11111
|
|
|
|
ai_service = AIService(db_session)
|
|
result = ai_service.check_tag_from_second(aggressive_ai, mock_game)
|
|
|
|
# Should hit line 204: min_safe = 4 (adjusted_running >= 8)
|
|
# Final: 4 - 2 (for 1 out) = 2
|
|
assert result.min_safe == 2
|
|
|
|
def test_tag_from_third_tied_game_line_253(self, db_session):
|
|
"""Test tag from third in tied game (line 253)."""
|
|
balanced_ai = ManagerAiFactory.create(
|
|
db_session,
|
|
running=6,
|
|
ahead_aggression=5,
|
|
behind_aggression=5
|
|
)
|
|
|
|
mock_game = Mock()
|
|
mock_play = Mock()
|
|
|
|
mock_play.starting_outs = 1
|
|
mock_play.ai_run_diff = 0 # Tied game, hits line 253
|
|
|
|
mock_game.current_play_or_none.return_value = mock_play
|
|
mock_game.id = 22222
|
|
|
|
ai_service = AIService(db_session)
|
|
result = ai_service.check_tag_from_third(balanced_ai, mock_game)
|
|
|
|
# Should hit line 253: min_safe -= 2
|
|
assert isinstance(result, TagResponse)
|
|
assert result.min_safe is not None |