refactor: engineering improvements -- API versioning, structured logging, Alembic, error standardization, test coverage
- 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
This commit is contained in:
86
backend/tests/unit/test_interrupt_cleanup.py
Normal file
86
backend/tests/unit/test_interrupt_cleanup.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user