This commit is contained in:
2025-09-10 11:46:57 -07:00
parent 2cc2b4c9ce
commit f443e7a64e
33 changed files with 887 additions and 1467 deletions

View File

@@ -0,0 +1,102 @@
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
from app.services.ai_service import AIService, AIServiceError
from app.models.workout import Workout
import json
@pytest.mark.asyncio
async def test_analyze_workout_success():
"""Test successful workout analysis with valid API response"""
mock_db = MagicMock()
mock_prompt = MagicMock()
mock_prompt.format.return_value = "test prompt"
ai_service = AIService(mock_db)
ai_service.prompt_manager.get_active_prompt = AsyncMock(return_value=mock_prompt)
test_response = json.dumps({
"performance_summary": "Good workout",
"suggestions": ["More recovery"]
})
with patch('httpx.AsyncClient.post') as mock_post:
mock_post.return_value = AsyncMock(
status_code=200,
json=lambda: {"choices": [{"message": {"content": test_response}}]}
)
workout = Workout(activity_type="cycling", duration_seconds=3600)
result = await ai_service.analyze_workout(workout)
assert "performance_summary" in result
assert len(result["suggestions"]) == 1
@pytest.mark.asyncio
async def test_generate_plan_success():
"""Test plan generation with structured response"""
mock_db = MagicMock()
ai_service = AIService(mock_db)
ai_service.prompt_manager.get_active_prompt = AsyncMock(return_value="Plan prompt: {rules} {goals}")
test_plan = {
"weeks": [{"workouts": ["ride"]}],
"focus": "endurance"
}
with patch('httpx.AsyncClient.post') as mock_post:
mock_post.return_value = AsyncMock(
status_code=200,
json=lambda: {"choices": [{"message": {"content": json.dumps(test_plan)}}]}
)
result = await ai_service.generate_plan([], {})
assert "weeks" in result
assert result["focus"] == "endurance"
@pytest.mark.asyncio
async def test_api_retry_logic():
"""Test API request retries on failure"""
mock_db = MagicMock()
ai_service = AIService(mock_db)
with patch('httpx.AsyncClient.post') as mock_post:
mock_post.side_effect = Exception("API failure")
with pytest.raises(AIServiceError):
await ai_service._make_ai_request("test")
assert mock_post.call_count == 3
@pytest.mark.asyncio
async def test_invalid_json_handling():
"""Test graceful handling of invalid JSON responses"""
mock_db = MagicMock()
ai_service = AIService(mock_db)
with patch('httpx.AsyncClient.post') as mock_post:
mock_post.return_value = AsyncMock(
status_code=200,
json=lambda: {"choices": [{"message": {"content": "invalid{json"}}]}
)
result = await ai_service.parse_rules_from_natural_language("test")
assert "raw_rules" in result
assert not result["structured"]
@pytest.mark.asyncio
async def test_code_block_parsing():
"""Test extraction of JSON from code blocks"""
mock_db = MagicMock()
ai_service = AIService(mock_db)
test_response = "```json\n" + json.dumps({"max_rides": 4}) + "\n```"
with patch('httpx.AsyncClient.post') as mock_post:
mock_post.return_value = AsyncMock(
status_code=200,
json=lambda: {"choices": [{"message": {"content": test_response}}]}
)
result = await ai_service.evolve_plan({})
assert "max_rides" in result
assert result["max_rides"] == 4

View File

@@ -0,0 +1,56 @@
import pytest
from unittest.mock import AsyncMock, MagicMock
from app.services.plan_evolution import PlanEvolutionService
from app.models.plan import Plan
from app.models.analysis import Analysis
from datetime import datetime
@pytest.mark.asyncio
async def test_evolve_plan_with_valid_analysis():
"""Test plan evolution with approved analysis and suggestions"""
mock_db = AsyncMock()
mock_plan = Plan(
id=1,
version=1,
jsonb_plan={"weeks": []},
parent_plan_id=None
)
mock_analysis = Analysis(
approved=True,
jsonb_feedback={"suggestions": ["More recovery"]}
)
service = PlanEvolutionService(mock_db)
service.ai_service.evolve_plan = AsyncMock(return_value={"weeks": [{"recovery": True}]})
result = await service.evolve_plan_from_analysis(mock_analysis, mock_plan)
assert result.version == 2
assert result.parent_plan_id == 1
mock_db.add.assert_called_once()
mock_db.commit.assert_awaited_once()
@pytest.mark.asyncio
async def test_evolution_skipped_for_unapproved_analysis():
"""Test plan evolution is skipped for unapproved analysis"""
mock_db = AsyncMock()
mock_analysis = Analysis(approved=False)
service = PlanEvolutionService(mock_db)
result = await service.evolve_plan_from_analysis(mock_analysis, MagicMock())
assert result is None
@pytest.mark.asyncio
async def test_evolution_history_retrieval():
"""Test getting plan evolution history"""
mock_db = AsyncMock()
mock_db.execute.return_value.scalars.return_value = [
Plan(version=1), Plan(version=2)
]
service = PlanEvolutionService(mock_db)
history = await service.get_plan_evolution_history(1)
assert len(history) == 2
assert history[0].version == 1

View File

@@ -0,0 +1,81 @@
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from app.services.workout_sync import WorkoutSyncService
from app.models.workout import Workout
from app.models.garmin_sync_log import GarminSyncLog
from datetime import datetime, timedelta
import asyncio
@pytest.mark.asyncio
async def test_successful_sync():
"""Test successful sync of new activities"""
mock_db = AsyncMock()
mock_garmin = MagicMock()
mock_garmin.get_activities.return_value = [{'activityId': '123'}]
mock_garmin.get_activity_details.return_value = {'metrics': 'data'}
service = WorkoutSyncService(mock_db)
service.garmin_service = mock_garmin
result = await service.sync_recent_activities()
assert result == 1
mock_db.add.assert_called()
mock_db.commit.assert_awaited()
@pytest.mark.asyncio
async def test_duplicate_activity_handling():
"""Test skipping duplicate activities"""
mock_db = AsyncMock()
mock_db.execute.return_value.scalar_one_or_none.return_value = True
mock_garmin = MagicMock()
mock_garmin.get_activities.return_value = [{'activityId': '123'}]
service = WorkoutSyncService(mock_db)
service.garmin_service = mock_garmin
result = await service.sync_recent_activities()
assert result == 0
@pytest.mark.asyncio
async def test_activity_detail_retry_logic():
"""Test retry logic for activity details"""
mock_db = AsyncMock()
mock_garmin = MagicMock()
mock_garmin.get_activities.return_value = [{'activityId': '123'}]
mock_garmin.get_activity_details.side_effect = [Exception(), {'metrics': 'data'}]
service = WorkoutSyncService(mock_db)
service.garmin_service = mock_garmin
result = await service.sync_recent_activities()
assert mock_garmin.get_activity_details.call_count == 2
assert result == 1
@pytest.mark.asyncio
async def test_auth_error_handling():
"""Test authentication error handling"""
mock_db = AsyncMock()
mock_garmin = MagicMock()
mock_garmin.get_activities.side_effect = Exception("Auth failed")
service = WorkoutSyncService(mock_db)
service.garmin_service = mock_garmin
with pytest.raises(Exception):
await service.sync_recent_activities()
sync_log = mock_db.add.call_args[0][0]
assert sync_log.status == "auth_error"
@pytest.mark.asyncio
async def test_get_sync_status():
"""Test retrieval of latest sync status"""
mock_db = AsyncMock()
mock_log = GarminSyncLog(status="success")
mock_db.execute.return_value.scalar_one_or_none.return_value = mock_log
service = WorkoutSyncService(mock_db)
result = await service.get_latest_sync_status()
assert result.status == "success"