blob: 8b0cfcefa16ced726c31f9ff953247b801bb29c3 [file]
import asyncio
import time
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
from app.service.learning_svc import LearningService
class TestLearningServiceCache:
def test_initial_state_dirty(self):
with patch.object(LearningService, 'add_service', return_value=MagicMock()):
with patch.object(LearningService, 'add_parsers', return_value=[]):
svc = LearningService()
assert svc._model_dirty is True
assert svc._model_built_at == 0.0
def test_invalidate_cache(self):
with patch.object(LearningService, 'add_service', return_value=MagicMock()):
with patch.object(LearningService, 'add_parsers', return_value=[]):
svc = LearningService()
svc._model_dirty = False
svc._model_built_at = time.monotonic()
svc.invalidate_model_cache()
assert svc._model_dirty is True
def test_skip_rebuild_when_cache_valid(self):
with patch.object(LearningService, 'add_service', return_value=MagicMock()):
with patch.object(LearningService, 'add_parsers', return_value=[]):
svc = LearningService()
svc._model_dirty = False
svc._model_built_at = time.monotonic()
mock_data_svc = MagicMock()
mock_data_svc.locate = AsyncMock(return_value=[])
svc.get_service = MagicMock(return_value=mock_data_svc)
svc.get_config = MagicMock(return_value=3600)
asyncio.run(svc.build_model())
mock_data_svc.locate.assert_not_called()
def test_rebuild_when_dirty(self):
with patch.object(LearningService, 'add_service', return_value=MagicMock()):
with patch.object(LearningService, 'add_parsers', return_value=[]):
svc = LearningService()
svc._model_dirty = True
mock_data_svc = MagicMock()
mock_data_svc.locate = AsyncMock(return_value=[])
svc.get_service = MagicMock(return_value=mock_data_svc)
svc.get_config = MagicMock(return_value=3600)
asyncio.run(svc.build_model())
mock_data_svc.locate.assert_called_once()
assert svc._model_dirty is False
assert svc._model_built_at > 0
def test_rebuild_when_ttl_expired(self):
"""Rebuild must occur when cache TTL has expired even if not dirty."""
with patch.object(LearningService, 'add_service', return_value=MagicMock()):
with patch.object(LearningService, 'add_parsers', return_value=[]):
svc = LearningService()
ttl = 60
svc._model_dirty = False
# Set built-at to ttl+1 seconds in the past so it is expired.
svc._model_built_at = time.monotonic() - (ttl + 1)
mock_data_svc = MagicMock()
mock_data_svc.locate = AsyncMock(return_value=[])
svc.get_service = MagicMock(return_value=mock_data_svc)
svc.get_config = MagicMock(return_value=ttl)
asyncio.run(svc.build_model())
mock_data_svc.locate.assert_called_once()
assert svc._model_dirty is False
def test_cache_ttl_as_string_is_coerced(self):
"""get_config() may return a string; it must be coerced to int without error."""
with patch.object(LearningService, 'add_service', return_value=MagicMock()):
with patch.object(LearningService, 'add_parsers', return_value=[]):
svc = LearningService()
svc._model_dirty = True
mock_data_svc = MagicMock()
mock_data_svc.locate = AsyncMock(return_value=[])
svc.get_service = MagicMock(return_value=mock_data_svc)
# Return TTL as a string (common when loaded from config files).
svc.get_config = MagicMock(return_value='3600')
asyncio.run(svc.build_model())
mock_data_svc.locate.assert_called_once()