- API versioning: all REST endpoints prefixed with /api/v1/ - Structured logging: replaced stdlib logging with structlog (console/JSON modes) - Alembic migrations: versioned DB schema with initial migration - Error standardization: global exception handlers for consistent envelope format - Interrupt cleanup: asyncio background task for expired interrupt removal - Integration tests: +30 tests (analytics, replay, openapi, error, session APIs) - Frontend tests: +57 tests (all components, pages, useWebSocket hook) - Backend: 557 tests, 89.75% coverage | Frontend: 80 tests, 16 test files
87 lines
2.6 KiB
Python
87 lines
2.6 KiB
Python
"""Tests for the interrupt cleanup background loop in main.py."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from app.main import _interrupt_cleanup_loop
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.asyncio
|
|
async def test_cleanup_loop_calls_cleanup_expired() -> None:
|
|
"""The loop should call cleanup_expired after each sleep interval."""
|
|
manager = MagicMock()
|
|
manager.cleanup_expired.return_value = ()
|
|
|
|
call_count = 0
|
|
original_sleep = asyncio.sleep
|
|
|
|
async def _fake_sleep(seconds: float) -> None:
|
|
nonlocal call_count
|
|
call_count += 1
|
|
if call_count >= 2:
|
|
raise asyncio.CancelledError
|
|
await original_sleep(0)
|
|
|
|
with patch("app.main.asyncio.sleep", side_effect=_fake_sleep):
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await _interrupt_cleanup_loop(manager, interval=60)
|
|
|
|
assert manager.cleanup_expired.call_count >= 1
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.asyncio
|
|
async def test_cleanup_loop_survives_exceptions() -> None:
|
|
"""The loop should not die when cleanup_expired raises an exception."""
|
|
manager = MagicMock()
|
|
manager.cleanup_expired.side_effect = [RuntimeError("db gone"), ()]
|
|
|
|
call_count = 0
|
|
original_sleep = asyncio.sleep
|
|
|
|
async def _fake_sleep(seconds: float) -> None:
|
|
nonlocal call_count
|
|
call_count += 1
|
|
if call_count >= 3:
|
|
raise asyncio.CancelledError
|
|
await original_sleep(0)
|
|
|
|
with patch("app.main.asyncio.sleep", side_effect=_fake_sleep):
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await _interrupt_cleanup_loop(manager, interval=60)
|
|
|
|
# Should have been called twice: once raising, once returning ()
|
|
assert manager.cleanup_expired.call_count == 2
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.asyncio
|
|
async def test_cleanup_loop_logs_expired_count(capsys: pytest.CaptureFixture[str]) -> None:
|
|
"""The loop should log when expired interrupts are found."""
|
|
fake_record = MagicMock()
|
|
manager = MagicMock()
|
|
manager.cleanup_expired.return_value = (fake_record, fake_record)
|
|
|
|
call_count = 0
|
|
original_sleep = asyncio.sleep
|
|
|
|
async def _fake_sleep(seconds: float) -> None:
|
|
nonlocal call_count
|
|
call_count += 1
|
|
if call_count >= 2:
|
|
raise asyncio.CancelledError
|
|
await original_sleep(0)
|
|
|
|
with patch("app.main.asyncio.sleep", side_effect=_fake_sleep):
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await _interrupt_cleanup_loop(manager, interval=60)
|
|
|
|
captured = capsys.readouterr()
|
|
assert "2 expired interrupt" in captured.out
|