paper-dynasty-gameplay-webapp/tests/integration/services/test_ai_service_simple.py
Cal Corum 559fe73f07 CLAUDE: Complete Player model migration with service layer and integration test infrastructure
## 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>
2025-09-29 16:20:29 -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 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