## Player Model Migration - Migrate Player model from Discord app following Model/Service Architecture pattern - Extract all business logic from Player model to PlayerService - Create pure data model with PostgreSQL relationships (Cardset, PositionRating) - Implement comprehensive PlayerFactory with specialized methods for test data ## PlayerService Implementation - Extract 5 business logic methods from original Player model: - get_batter_card_url() - batting card URL retrieval - get_pitcher_card_url() - pitching card URL retrieval - generate_name_card_link() - markdown link generation - get_formatted_name_with_description() - name formatting - get_player_description() - description from object or dict - Follow BaseService pattern with dependency injection and logging ## Comprehensive Testing - 35 passing Player tests (14 model + 21 service tests) - PlayerFactory with specialized methods (batting/pitching cards, positions) - Test isolation following factory pattern and db_session guidelines - Fix PostgreSQL integer overflow in test ID generation ## Integration Test Infrastructure - Create integration test framework for improving service coverage - Design AIService integration tests targeting uncovered branches - Demonstrate real database query testing with proper isolation - Establish patterns for testing complex game scenarios ## Service Coverage Analysis - Current service coverage: 61% overall - PlayerService: 100% coverage (excellent migration example) - AIService: 60% coverage (improvement opportunities identified) - Integration test strategy designed to achieve 90%+ coverage ## Database Integration - Update Cardset model to include players relationship - Update PositionRating model with proper Player foreign key - Maintain all existing relationships and constraints - Demonstrate data isolation and automatic cleanup in tests ## Test Suite Status - 137 tests passing, 0 failures (maintained 100% pass rate) - Added 35 new tests while preserving all existing functionality - Integration test infrastructure ready for coverage improvements 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
249 lines
9.6 KiB
Python
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 steal > 5 branch in home steal logic (line 160)
|
|
assert isinstance(result, JumpResponse)
|
|
assert result.min_safe == 7 # steal > 5 → min_safe = 7
|
|
|
|
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 |