Files
noteflow/tests/application/services/analytics/test_cache_invalidation.py
Travis Vasceannie 2641a9fc03
Some checks failed
CI / test-python (push) Failing after 17m22s
CI / test-rust (push) Has been cancelled
CI / test-typescript (push) Has been cancelled
optimization
2026-01-25 01:40:14 +00:00

222 lines
8.0 KiB
Python

"""Integration tests for analytics cache invalidation on meeting completion.
Tests cover:
- Complete meeting → analytics cache invalidated → next query hits DB
- Cache miss is logged after invalidation
"""
from __future__ import annotations
from typing import Final
from unittest.mock import AsyncMock, MagicMock
from uuid import uuid4
from noteflow.application.services.analytics import AnalyticsService
from noteflow.domain.entities.analytics import (
AnalyticsOverview,
SpeakerStat,
)
EXPECTED_TOTAL_MEETINGS: Final[int] = 1
EXPECTED_TOTAL_DURATION: Final[float] = 1800.0
EXPECTED_TOTAL_WORDS: Final[int] = 2500
EXPECTED_TOTAL_SEGMENTS: Final[int] = 50
EXPECTED_SPEAKER_COUNT: Final[int] = 2
EXPECTED_EMPTY_CACHE: Final[int] = 0
EXPECTED_SINGLE_CACHE_ENTRY: Final[int] = 1
EXPECTED_TWO_CACHE_ENTRIES: Final[int] = 2
EXPECTED_DB_CALLS_FIRST: Final[int] = 1
EXPECTED_DB_CALLS_AFTER_CACHE_HIT: Final[int] = 1
EXPECTED_DB_CALLS_AFTER_INVALIDATION: Final[int] = 2
SPEAKER_ALICE_TIME: Final[float] = 900.0
SPEAKER_ALICE_SEGMENTS: Final[int] = 25
SPEAKER_ALICE_MEETINGS: Final[int] = 1
SPEAKER_ALICE_CONFIDENCE: Final[float] = 0.95
SPEAKER_BOB_TIME: Final[float] = 900.0
SPEAKER_BOB_SEGMENTS: Final[int] = 25
SPEAKER_BOB_MEETINGS: Final[int] = 1
SPEAKER_BOB_CONFIDENCE: Final[float] = 0.93
CACHE_TTL_SECONDS: Final[int] = 60
async def _setup_mock_uow_with_overview(
analytics_uow_factory: MagicMock,
sample_overview: AnalyticsOverview,
) -> MagicMock:
"""Create a mock UoW configured with overview data."""
mock_uow = MagicMock()
mock_uow.__aenter__ = AsyncMock(return_value=mock_uow)
mock_uow.__aexit__ = AsyncMock(return_value=None)
mock_uow.analytics.get_overview_fast = AsyncMock(return_value=sample_overview)
analytics_uow_factory.return_value = mock_uow
return mock_uow
async def _setup_mock_uow_with_speaker_stats(
analytics_uow_factory: MagicMock,
sample_overview: AnalyticsOverview,
sample_speaker_stats: list[SpeakerStat],
) -> MagicMock:
"""Create a mock UoW configured with speaker stats."""
mock_uow = MagicMock()
mock_uow.__aenter__ = AsyncMock(return_value=mock_uow)
mock_uow.__aexit__ = AsyncMock(return_value=None)
mock_uow.analytics.get_overview_fast = AsyncMock(return_value=sample_overview)
mock_uow.analytics.get_speaker_stats_fast = AsyncMock(return_value=sample_speaker_stats)
analytics_uow_factory.return_value = mock_uow
return mock_uow
async def _verify_cache_hit_then_db_hit(
mock_uow: MagicMock,
) -> None:
"""Verify cache was hit first, then DB was hit after invalidation."""
assert (
mock_uow.analytics.get_overview_fast.call_count == EXPECTED_DB_CALLS_FIRST
), "First query should hit DB"
assert (
mock_uow.analytics.get_overview_fast.call_count == EXPECTED_DB_CALLS_AFTER_INVALIDATION
), "Second query should hit DB after invalidation"
assert (
mock_uow.analytics.get_overview_fast.call_count == EXPECTED_DB_CALLS_AFTER_INVALIDATION
), "Second query should hit DB after invalidation"
async def _verify_independent_workspace_caches(
mock_uow: MagicMock,
) -> None:
"""Verify caches are independent - invalidating one doesn't affect other."""
assert (
mock_uow.analytics.get_overview_fast.call_count == EXPECTED_TWO_CACHE_ENTRIES
), "Should have cached two workspaces"
class TestAnalyticsCacheInvalidation:
"""Integration tests for analytics cache invalidation flow."""
async def test_meeting_completion_invalidates_cache_integration(
self,
analytics_service: AnalyticsService,
analytics_uow_factory: MagicMock,
sample_overview: AnalyticsOverview,
) -> None:
"""Test complete flow: meeting completion → cache invalidation → DB hit."""
workspace_id = uuid4()
mock_uow = await _setup_mock_uow_with_overview(analytics_uow_factory, sample_overview)
result1 = await analytics_service.get_overview(workspace_id)
assert result1.total_meetings == EXPECTED_TOTAL_MEETINGS, (
"First query should return overview"
)
analytics_service.invalidate_cache(workspace_id)
result2 = await analytics_service.get_overview(workspace_id)
assert result2.total_meetings == EXPECTED_TOTAL_MEETINGS, (
"Second query should return cached data"
)
await _verify_cache_hit_then_db_hit(mock_uow)
async def test_invalidate_cache_clears_all_cache_types(
self,
analytics_service: AnalyticsService,
analytics_uow_factory: MagicMock,
sample_overview: AnalyticsOverview,
sample_speaker_stats: list[SpeakerStat],
) -> None:
"""Test that invalidation clears overview, speaker, and entity caches."""
workspace_id = uuid4()
mock_uow = await _setup_mock_uow_with_speaker_stats(
analytics_uow_factory, sample_overview, sample_speaker_stats
)
await analytics_service.get_overview(workspace_id)
await analytics_service.get_speaker_stats(workspace_id)
assert (
mock_uow.analytics.get_overview_fast.call_count == EXPECTED_DB_CALLS_FIRST
), "Overview should have been queried once"
assert (
mock_uow.analytics.get_speaker_stats_fast.call_count == EXPECTED_DB_CALLS_FIRST
), "Speaker stats should have been queried once"
analytics_service.invalidate_cache(workspace_id)
await analytics_service.get_overview(workspace_id)
await analytics_service.get_speaker_stats(workspace_id)
assert (
mock_uow.analytics.get_overview_fast.call_count == EXPECTED_DB_CALLS_AFTER_INVALIDATION
), "Overview should query DB after invalidation"
assert (
mock_uow.analytics.get_speaker_stats_fast.call_count
== EXPECTED_DB_CALLS_AFTER_INVALIDATION
), "Speaker stats should query DB after invalidation"
async def test_invalidate_cache_with_none_clears_all_workspaces(
self,
analytics_service: AnalyticsService,
analytics_uow_factory: MagicMock,
sample_overview: AnalyticsOverview,
) -> None:
"""Test that invalidate_cache(None) clears all workspaces."""
workspace_id_1 = uuid4()
workspace_id_2 = uuid4()
mock_uow = await _setup_mock_uow_with_overview(analytics_uow_factory, sample_overview)
await analytics_service.get_overview(workspace_id_1)
await analytics_service.get_overview(workspace_id_2)
assert (
mock_uow.analytics.get_overview_fast.call_count == EXPECTED_TWO_CACHE_ENTRIES
), "Should have queried DB twice"
analytics_service.invalidate_cache(None)
await analytics_service.get_overview(workspace_id_1)
await analytics_service.get_overview(workspace_id_2)
expected_calls_after_invalidation = EXPECTED_TWO_CACHE_ENTRIES + EXPECTED_TWO_CACHE_ENTRIES
assert (
mock_uow.analytics.get_overview_fast.call_count == expected_calls_after_invalidation
), "Should query DB again after invalidating all"
async def test_invalidate_cache_preserves_other_workspaces(
self,
analytics_service: AnalyticsService,
analytics_uow_factory: MagicMock,
sample_overview: AnalyticsOverview,
) -> None:
"""Test that invalidating one workspace preserves others."""
workspace_id_1 = uuid4()
workspace_id_2 = uuid4()
mock_uow = await _setup_mock_uow_with_overview(analytics_uow_factory, sample_overview)
await analytics_service.get_overview(workspace_id_1)
await analytics_service.get_overview(workspace_id_2)
await _verify_independent_workspace_caches(mock_uow)
analytics_service.invalidate_cache(workspace_id_1)
await analytics_service.get_overview(workspace_id_1)
await analytics_service.get_overview(workspace_id_2)
assert (
mock_uow.analytics.get_overview_fast.call_count == EXPECTED_TWO_CACHE_ENTRIES * 2
), "Invalidating one workspace should not affect other's cache"