* Initial plan * Fix security vulnerabilities: MD5→SHA-256, XSS via dangerouslySetInnerHTML/innerHTML, insecure randomness, CodeQL config Co-authored-by: TLimoges33 <125313326+TLimoges33@users.noreply.github.com> * Clean up README: remove decorative emojis for a professional tone Remove all emojis from section headers, list item prefixes, and decorative positions. Replace ✅ phase status markers with '(Complete)' text. Keep the ⭐ in the final call-to-action line. No changes to links, badges, code blocks, or technical content. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: remove emoji characters from CONTRIBUTING.md Remove all emoji from section headers and closing line while preserving links, code blocks, and technical content. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: remove emoji characters from documentation files Remove all emoji characters from 8 documentation files in docs/. Replace status-marker checkmarks (✅) with '(Done)' text. Remove decorative emojis from headers and body text entirely. Preserve emojis inside code blocks unchanged. Clean up trailing whitespace introduced by removals. Files modified: - DEPLOYMENT_GUIDE.md - IMPLEMENTATION_PLAN.md - MILESTONE_6_SUMMARY.md - PRODUCTION_ROADMAP.md - PROJECT_STATUS.md - REPOSITORY_ENHANCEMENT.md - ROADMAP.md - SECURITY_AUDIT_ROADMAP.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: remove emoji characters from documentation files Remove all emoji characters from 9 markdown files while preserving code block content (box-drawing characters, indentation). Emojis removed from headers, list items, and body text across READMEs, issue templates, PR template, runbook, and mobile docs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove excessive emoji from all documentation for professional presentation Co-authored-by: TLimoges33 <125313326+TLimoges33@users.noreply.github.com> * Fix PluginWidget initial state and remove || true from security audit steps Co-authored-by: TLimoges33 <125313326+TLimoges33@users.noreply.github.com> * Remediate all failing CI checks: update deprecated actions, fix npm vulnerabilities, fix migrations YAML Co-authored-by: SynOSdev <257853113+SynOSdev@users.noreply.github.com> * Fix all remaining CI failures: Node 18→20, fix test API contract, fix pytest version, fix Postgres health checks Co-authored-by: SynOSdev <257853113+SynOSdev@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: TLimoges33 <125313326+TLimoges33@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: SynOSdev <257853113+SynOSdev@users.noreply.github.com>
219 lines
7.7 KiB
Python
219 lines
7.7 KiB
Python
"""
|
|
Comprehensive test suite for LifeRPG AI functionality.
|
|
Tests HuggingFace AI integration, natural language processing, and predictions.
|
|
"""
|
|
|
|
import pytest
|
|
import asyncio
|
|
from unittest.mock import Mock, patch, AsyncMock
|
|
import sys
|
|
import os
|
|
|
|
# Add the backend directory to Python path
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
|
|
|
try:
|
|
from huggingface_ai import HuggingFaceAI
|
|
AI_AVAILABLE = True
|
|
except ImportError:
|
|
AI_AVAILABLE = False
|
|
|
|
# Conditionally skip individual tests instead of module-level skip
|
|
pytestmark = pytest.mark.skipif(not AI_AVAILABLE, reason="AI dependencies not available")
|
|
|
|
|
|
class TestHuggingFaceAI:
|
|
"""Test the core HuggingFace AI service functionality."""
|
|
|
|
@pytest.fixture
|
|
def ai_service(self):
|
|
"""Create an AI service instance for testing."""
|
|
return HuggingFaceAI()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ai_service_initialization(self, ai_service):
|
|
"""Test that AI service initializes correctly."""
|
|
assert ai_service is not None
|
|
assert hasattr(ai_service, 'parse_habit_from_text')
|
|
assert hasattr(ai_service, 'get_habit_suggestions')
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_habit_parsing_basic(self, ai_service):
|
|
"""Test basic habit parsing functionality."""
|
|
test_inputs = [
|
|
"I want to drink water daily",
|
|
"Exercise for 30 minutes three times a week",
|
|
"Read for 15 minutes before bed"
|
|
]
|
|
|
|
for test_input in test_inputs:
|
|
result = await ai_service.parse_habit_from_text(test_input)
|
|
|
|
# Verify basic structure
|
|
assert isinstance(result, dict)
|
|
assert 'title' in result
|
|
assert 'cadence' in result
|
|
|
|
# Verify non-empty values
|
|
assert len(result['title']) > 0
|
|
assert result['cadence'] in ['daily', 'weekly', 'monthly']
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_habit_parsing_edge_cases(self, ai_service):
|
|
"""Test habit parsing with edge cases."""
|
|
edge_cases = [
|
|
"", # Empty string
|
|
"a", # Single character
|
|
"This is a very long sentence that doesn't really describe a habit but just keeps going on and on without any clear habit-related content", # Long non-habit text
|
|
"123 456 789", # Only numbers
|
|
]
|
|
|
|
for test_input in edge_cases:
|
|
result = await ai_service.parse_habit_from_text(test_input)
|
|
|
|
# Should handle gracefully without crashing
|
|
assert isinstance(result, dict)
|
|
# May have default values for edge cases
|
|
assert 'title' in result
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_suggestion_generation(self, ai_service):
|
|
"""Test AI-powered suggestion generation."""
|
|
user_habits = ['exercise', 'reading']
|
|
user_data = {
|
|
'preferences': ['health', 'productivity']
|
|
}
|
|
|
|
suggestions = await ai_service.get_habit_suggestions(user_habits, user_data)
|
|
|
|
assert isinstance(suggestions, list)
|
|
assert len(suggestions) > 0
|
|
|
|
for suggestion in suggestions:
|
|
assert isinstance(suggestion, str)
|
|
assert len(suggestion) > 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_success_prediction(self, ai_service):
|
|
"""Test habit success prediction functionality."""
|
|
habit_data = {
|
|
'title': 'Morning Exercise',
|
|
'cadence': 'daily',
|
|
'difficulty': 2,
|
|
}
|
|
user_history = [
|
|
{'completed': True},
|
|
{'completed': True},
|
|
{'completed': False},
|
|
{'completed': True},
|
|
]
|
|
|
|
prediction = await ai_service.predict_habit_success(habit_data, user_history)
|
|
|
|
assert isinstance(prediction, dict)
|
|
assert 'success_probability' in prediction
|
|
assert 0 <= prediction['success_probability'] <= 1
|
|
assert 'insights' in prediction
|
|
assert isinstance(prediction['insights'], list)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_performance_benchmarks(self, ai_service):
|
|
"""Test that AI operations complete within reasonable time limits."""
|
|
import time
|
|
|
|
test_text = "I want to exercise daily"
|
|
|
|
# Test parsing speed
|
|
start_time = time.time()
|
|
result = await ai_service.parse_habit_from_text(test_text)
|
|
parsing_time = time.time() - start_time
|
|
|
|
# Should complete within 5 seconds (generous for CI)
|
|
assert parsing_time < 5.0
|
|
assert result is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_error_handling(self, ai_service):
|
|
"""Test that AI service handles errors gracefully."""
|
|
|
|
# Test with problematic inputs that might cause model errors
|
|
problematic_inputs = [
|
|
None,
|
|
{"not": "a string"},
|
|
["list", "instead", "of", "string"]
|
|
]
|
|
|
|
for bad_input in problematic_inputs:
|
|
try:
|
|
result = await ai_service.parse_habit_from_text(bad_input)
|
|
# If it doesn't raise an error, should return a safe default
|
|
assert isinstance(result, dict)
|
|
except (TypeError, ValueError, AttributeError):
|
|
# These exceptions are acceptable for bad inputs
|
|
pass
|
|
|
|
def test_local_models_attribute(self, ai_service):
|
|
"""Test that local models dictionary is initialized."""
|
|
assert hasattr(ai_service, 'local_models')
|
|
assert isinstance(ai_service.local_models, dict)
|
|
|
|
|
|
class TestAIIntegration:
|
|
"""Integration tests for AI features with the broader system."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_full_ai_pipeline(self):
|
|
"""Test the complete AI pipeline from input to output."""
|
|
if not AI_AVAILABLE:
|
|
pytest.skip("AI dependencies not available")
|
|
|
|
ai_service = HuggingFaceAI()
|
|
|
|
# Simulate full user interaction
|
|
user_input = "I want to meditate for 10 minutes every morning"
|
|
|
|
# Parse habit
|
|
habit_data = await ai_service.parse_habit_from_text(user_input)
|
|
assert habit_data['title']
|
|
assert habit_data['cadence']
|
|
|
|
# Generate suggestions
|
|
suggestions = await ai_service.get_habit_suggestions(
|
|
[habit_data['title']],
|
|
{'preferences': ['wellness', 'morning_routine']}
|
|
)
|
|
assert len(suggestions) > 0
|
|
|
|
# Predict success
|
|
prediction = await ai_service.predict_habit_success(habit_data, [])
|
|
assert 0 <= prediction['success_probability'] <= 1
|
|
|
|
|
|
class TestAIFallbacks:
|
|
"""Test fallback mechanisms when AI fails or is unavailable."""
|
|
|
|
def test_ai_disabled_fallback(self):
|
|
"""Test system behavior when AI features are disabled."""
|
|
# Simulate AI disabled scenario
|
|
with patch.dict(os.environ, {'AI_FEATURES_ENABLED': 'false'}):
|
|
# System should still function with manual habit creation
|
|
assert True # Placeholder for actual fallback tests
|
|
|
|
@patch('huggingface_ai.HuggingFaceAI')
|
|
def test_model_loading_failure(self, mock_ai):
|
|
"""Test behavior when AI models fail to load."""
|
|
mock_ai.side_effect = Exception("Model loading failed")
|
|
|
|
# Should handle gracefully and provide fallback
|
|
try:
|
|
ai_service = HuggingFaceAI()
|
|
# Should not crash the application
|
|
assert True
|
|
except Exception:
|
|
pytest.fail("AI service should handle model loading failures gracefully")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Run tests with: python -m pytest test_ai_comprehensive.py -v
|
|
pytest.main([__file__, "-v", "--tb=short"])
|