mirror of
https://github.com/sstent/AICyclingCoach.git
synced 2026-01-25 16:41:58 +00:00
sync
This commit is contained in:
102
backend/tests/services/test_ai_service.py
Normal file
102
backend/tests/services/test_ai_service.py
Normal 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
|
||||
56
backend/tests/services/test_plan_evolution.py
Normal file
56
backend/tests/services/test_plan_evolution.py
Normal 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
|
||||
81
backend/tests/services/test_workflow_sync.py
Normal file
81
backend/tests/services/test_workflow_sync.py
Normal 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"
|
||||
Reference in New Issue
Block a user