Files
transcription/tests/test_worker.py
T
2026-08-23 18:28:21 -05:00

207 lines
7.0 KiB
Python

import asyncio
import logging
from datetime import UTC
from datetime import datetime
from datetime import timedelta
from typing import cast
import pytest
from sqlalchemy.ext.asyncio import async_sessionmaker
from sqlmodel.ext.asyncio.session import AsyncSession
from transcription.config import Settings
from transcription.errors import AppError
from transcription.errors import ErrorCategory
from transcription.services import ServiceBundle
from transcription.services.sources import SourceService
from transcription.worker import WorkerHealth
from transcription.worker import process_next_queued_job
from transcription.worker import run_worker_loop
@pytest.mark.asyncio
async def test_run_worker_loop_stops_on_non_retriable_exception(monkeypatch, caplog):
"""A programming error before a job is claimed stops the loop instead of spinning.
Regression guard for review log [8]. This previously spun at the poll interval
forever: the fault was classified non-retriable, logged, and then discarded, and
it never reached the per-job retry machinery so nothing capped it. Measured at 20
iterations in 1.2s before the fix.
"""
calls = 0
stop_event = asyncio.Event()
worker_health = WorkerHealth()
async def _fake_process_next_queued_job(*, session=None, session_factory=None, services=None):
nonlocal calls
_ = (session, session_factory, services)
calls += 1
raise RuntimeError("boom")
monkeypatch.setattr("transcription.worker.process_next_queued_job", _fake_process_next_queued_job)
with caplog.at_level(logging.CRITICAL):
await asyncio.wait_for(
run_worker_loop(stop_event=stop_event, poll_interval_seconds=0, worker_health=worker_health),
timeout=5,
)
assert calls == 1
assert "Worker loop stopped after a non-retriable error" in caplog.text
snapshot = worker_health.snapshot()
assert snapshot.state == "failed"
assert snapshot.error_id is not None
assert snapshot.error_category == ErrorCategory.INTERNAL_UNEXPECTED.value
@pytest.mark.asyncio
async def test_run_worker_loop_survives_retriable_exception(monkeypatch, caplog):
"""A retriable fault is still suppressed so transient conditions do not stop work."""
calls = 0
stop_event = asyncio.Event()
worker_health = WorkerHealth()
async def _fake_process_next_queued_job(*, session=None, session_factory=None, services=None):
nonlocal calls
_ = (session, session_factory, services)
calls += 1
if calls == 1:
raise AppError(
"transient",
category=ErrorCategory.EXTERNAL_PROVIDER,
suggestion="retry",
retriable=True,
)
stop_event.set()
return False
monkeypatch.setattr("transcription.worker.process_next_queued_job", _fake_process_next_queued_job)
with caplog.at_level(logging.ERROR):
await run_worker_loop(stop_event=stop_event, poll_interval_seconds=0, worker_health=worker_health)
assert calls == 2
assert "Worker loop exception" in caplog.text
assert worker_health.snapshot().state == "stopped"
@pytest.mark.asyncio
async def test_run_worker_loop_periodically_requeues_stale_processing_jobs(monkeypatch):
stop_event = asyncio.Event()
stale_sweep_calls: list[datetime] = []
class _Jobs:
settings = Settings(openrouter_api_key="test-key", worker_stale_job_seconds=120.0)
async def requeue_stale_processing_jobs(self, *, stale_before, session=None):
_ = session
stale_sweep_calls.append(stale_before)
return 1
class _Bundle:
jobs = _Jobs()
async def aclose(self):
return
monkeypatch.setattr(
"transcription.worker.ServiceBundle.from_session_factory",
classmethod(lambda _cls, _factory=None, **_kwargs: cast(ServiceBundle, _Bundle())),
)
async def _fake_process_next_queued_job(*, session=None, session_factory=None, services=None):
_ = (session, session_factory, services)
stop_event.set()
return False
monkeypatch.setattr("transcription.worker.process_next_queued_job", _fake_process_next_queued_job)
fake_session_factory = cast(async_sessionmaker[AsyncSession], object())
await run_worker_loop(stop_event=stop_event, poll_interval_seconds=0, session_factory=fake_session_factory)
assert len(stale_sweep_calls) >= 1
expected_upper_bound = datetime.now(UTC) - timedelta(seconds=120.0)
assert stale_sweep_calls[0] <= expected_upper_bound
@pytest.mark.asyncio
async def test_run_worker_loop_reuses_one_bundle_across_jobs(monkeypatch):
"""HIGH-02: the provider client is built once per loop, not once per job."""
stop_event = asyncio.Event()
seen: list[object] = []
closed = False
class _Sources:
async def aclose(self):
nonlocal closed
closed = True
bundle = ServiceBundle(sources=cast("SourceService", _Sources()))
monkeypatch.setattr(
"transcription.worker.ServiceBundle.from_session_factory",
classmethod(lambda _cls, _factory=None, **_kwargs: bundle),
)
async def _fake_process_next_queued_job(*, session=None, session_factory=None, services=None):
_ = (session, session_factory)
seen.append(services)
if len(seen) >= 3:
stop_event.set()
return False
return True
monkeypatch.setattr("transcription.worker.process_next_queued_job", _fake_process_next_queued_job)
await run_worker_loop(stop_event=stop_event, poll_interval_seconds=0)
assert len(seen) == 3
assert all(item is bundle for item in seen)
assert closed is True
@pytest.mark.asyncio
async def test_process_next_closes_provider_for_the_bundle_it_owns(monkeypatch):
closed = False
class _Sources:
async def aclose(self):
nonlocal closed
closed = True
bundle = ServiceBundle(sources=cast("SourceService", _Sources()))
monkeypatch.setattr(
"transcription.worker.ServiceBundle.from_session_factory",
classmethod(lambda _cls, _factory=None, **_kwargs: bundle),
)
async def _no_job(*, services, session):
_ = (services, session)
return False
monkeypatch.setattr("transcription.worker.process_next_queued_job_workflow", _no_job)
assert await process_next_queued_job() is False
assert closed is True
@pytest.mark.asyncio
async def test_process_next_leaves_a_caller_owned_bundle_open(monkeypatch):
"""A bundle passed in belongs to the caller and must outlive one job."""
closed = False
class _Sources:
async def aclose(self):
nonlocal closed
closed = True
bundle = ServiceBundle(sources=cast("SourceService", _Sources()))
async def _no_job(*, services, session):
_ = (services, session)
return False
monkeypatch.setattr("transcription.worker.process_next_queued_job_workflow", _no_job)
assert await process_next_queued_job(services=bundle) is False
assert closed is False