diff --git a/ocr-service/test_metrics.py b/ocr-service/test_metrics.py index d8099a24..1e6ab7fa 100644 --- a/ocr-service/test_metrics.py +++ b/ocr-service/test_metrics.py @@ -347,13 +347,21 @@ async def test_ocr_training_runs_total_incremented_with_recognition_success_labe @pytest.mark.asyncio async def test_ocr_training_runs_total_incremented_with_recognition_error_label(fresh_metrics): - """When /train's inner runner raises, the error counter bumps and the exception propagates.""" - async def fake_to_thread(func, *args, **kwargs): - raise RuntimeError("ketos train failed (exit 1): synthetic") + """When ketos exits non-zero, the error counter bumps and the exception propagates. + + Uses the narrowest available seam — `subprocess.run` returning a failing + CompletedProcess — instead of stubbing the asyncio.to_thread boundary, + so the test exercises the real _run_training error path. + """ + from subprocess import CompletedProcess + + failing_proc = CompletedProcess( + args=["ketos"], returncode=1, stdout="", stderr="synthetic ketos failure" + ) with patch("main.TRAINING_TOKEN", "secret-token"), \ patch("main._models_ready", True), \ - patch("main.asyncio.to_thread", side_effect=fake_to_thread): + patch("main.subprocess.run", return_value=failing_proc): transport = ASGITransport(app=app, raise_app_exceptions=False) async with AsyncClient(transport=transport, base_url="http://test") as client: response = await client.post(