gpt-5.3 codex review: Phase 7 and the addition of the new test-effectiveness-auditor skill.
Quality Gate / gate (push) Failing after 12s

This commit is contained in:
Jim Lancaster
2026-08-20 11:50:10 -05:00
parent 443a1e29c8
commit 7c4300f9c2
21 changed files with 831 additions and 119 deletions
+25 -2
View File
@@ -4,12 +4,13 @@ from fastapi import FastAPI
from fastapi.testclient import TestClient
from transcription.api.health import router
from transcription.worker import WorkerHealthSnapshot
class TestHealthEndpoint:
"""Verify /healthz endpoint behavior."""
def test_healthz_returns_ok_status(self):
def test_healthz_returns_ok_status_with_unknown_worker_when_uninitialized(self):
"""GET /healthz returns a healthy status payload."""
app = FastAPI()
app.include_router(router)
@@ -18,4 +19,26 @@ class TestHealthEndpoint:
response = client.get("/healthz")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
assert response.json() == {"status": "ok", "worker": {"state": "unknown"}}
def test_healthz_returns_worker_failure_metadata_when_available(self):
app = FastAPI()
app.state.worker_health = WorkerHealthSnapshot(
state="failed",
error_id="abc12345",
error_category="internal_unexpected_error",
)
app.include_router(router)
client = TestClient(app)
response = client.get("/healthz")
assert response.status_code == 200
assert response.json() == {
"status": "ok",
"worker": {
"state": "failed",
"error_id": "abc12345",
"error_category": "internal_unexpected_error",
},
}
@@ -1,11 +1,11 @@
source: Book Two - page 02.jpg
provider: openrouter
model: google/gemini-2.5-flash
model: openai/gpt-5.3-codex
---
[document body typewritten]
BY WAY OF INTRODUCTION:-
These few paragraphs of introduction may help you read BOOK 2 which covers a wider range than did BOOK 1 (Pioneer Days).
These few paragraphs of introduotion [sic] may help you read BOOK 2 which covers a wider range than did BOOK 1 (Pioneer Days).
BOOK 1 had 54 pages; 14 chapters. BOOK 2 has 70 pages; 18 chapters. BOOK 1 consisted largely of first generation family history. BOOK 2 throws more light on the second generation. Sidney promises a BOOK 3 and that may begin to do justice to the third generation. We suggest that Sidney get the help of Louis Shinn who has a chapter in this book (Chapter 16 - The Last 25 Years on the Doumecq Plains. Louis has the gift of seeing, recalling and telling. One sentence in his chapter gives a great tribute to the Doumecqers--so far as he knows no one on the Doumecq Plains went on relief during the depression. That in a nutshell shows the sturdy character of the residents of the Doumecq Plains.
@@ -13,5 +13,6 @@ We promised in BOOK 1 that in BOOK 2 we would give the story of the trip of John
Some who get this book will consider the group picture the best thing in the book. It took a lot of preliminary photographing to reduce some pictures, enlarge others and bring out the tin types. We wish that instead of 44 faces we could have given 88. Do not blame Ethel Cochran-Shinn for the selection. She furnished enough pictures but we had to take only part of them. We think there are great possibilities in reproducing old pictures. We wish we had a Pickard group. Some Pickard descendant may wish to make a collection.
We are much impressed with the future possibilities of getting a complete geneol-ogy of the Pickard family. Mr. Cochran has a fine chapter on the Pickards but to date we have not had the pleasure of finding all of the family dates. We had intended to give more family data in this book but it takes time to get the correct dates. Often times it requires trips to cemeteries to get dates on the tombstones. Winter is no time to collect dates on tombstones.
-2-
We are much impressed with the future possibilities of getting a complete genealogy of the Pickard family. Mr. Cochran has a fine chapter on the Pickards but to date we have not had the pleasure of finding all of the family dates. We had intended to give more family data in this book but it takes time to get the correct dates. Often times it requires trips to cemeteries to get dates on the tombstones. Winter is no time to collect dates on tombstones.
~2~
@@ -1,13 +1,13 @@
source: Omie Writes Home.pdf
provider: openrouter
model: google/gemini-2.5-flash
model: openai/gpt-5.3-codex
---
[document body mixed]
JOHN E. COCHRAN
FAMILY ASSOCIATION
[document body typeset]
Family Only
[handwritten: Home | Sibling's Stories | 1st Cousins | Ancestor's Stories | Reunion History | Next Reunion | JECMEF]
Home | Sibling's Stories | 1st Cousins | Ancestor's Stories | Reunion History | Next Reunion | JECMEF
OMIE WRITES HOME
Ed. Note: The following letter was written by Omie Cochran in Nome, Alaska and sent to her
sister, Ethel Shinn, in Canfield, Idaho in 1923. It has been stored away these 63 years in the
original envelope with its 2 cent stamp. The letter has a number of references to the Shinn
@@ -16,7 +16,9 @@ Miss Saville was the nurse at the Nome Hospital that was mentioned in the articl
the family newsletter two years ago.
Nome Alaska August 26, 1923
My Dear Ethel et al.
I don't know when I did write or when you did
but I am going to write now however and never
the less. But I wish I could talk (I can yet but I
@@ -26,8 +28,6 @@ rascal of yours would fairly sparkle with
listening. Can't I see him listening now to all the
yarns we told last summer?
[photo: Five people, appearing to be native Alaskan, in winter clothing, standing on a beach with a dog sled in front of them and a boat in the background]
You see, we-Miss Saville and I, took a trip north
on the Buford and it was very interesting. We
went north thru the Bering Strait into the Arctic and as far as the Ice Pack. There the captain
@@ -35,4 +35,84 @@ of our craft and some other mighty hunters went out first in kayaks and later in
shot seven walrus. When they also took a movie man and camera, so you will likely see all
this in the movies before I get to tell you. They came back on board and the ship went up
along the icebergs and the walrus were 'heisted' on board by the use of the crane which loads
tons of freight and the beasts were
tons of freight and the beasts were so huge that they made the pulleys just creak. They were
over 12 feet long, as big around as three cows, had no feet but toenails on their flippers or
flappers, no head but their body just suddenly ended with a hole for a mouth and big bristles
[photograph of people standing outdoors in snow]
all around it similar to a currycomb in coarseness; no ears but huge tusks of ivory. They are
the most repulsive looking animals imaginable and tho I have always read about them I never
expect such disagreeable looking creatures. They had a rough brown hairy skin and some of
them looked warty. They must have weighed two ton at least. Ere we got them back to Nome
to the natives they were getting extremely odiferous—in fact, you could scarcely stay on the
ship with any degree of comfort unless you had per chance lost your sense of smell.
Then we went north to a few minutes beyond the 70th degree of latitude and thot for awhile
we would go to Wrangell Island where some men from Steffonsons ship were supposed to be
stranded but we didn't get there and instead stopped at a small native village at Cape Serdz in
Siberia. These Eskimo were very primitive. One white squaw man lived there and had for 23
years. He was a Swede--who else could. Their houses were circular and built up with dirt 2 or
3 feet and then skins were stretched over it and weighted down with rocks. Inside, the room
was partitioned off at the sides with skins for sleeping quarters. In the main part they had the
fire on the ground and the fish drying on lines and the skins hanging around and the dogs and
babies and children. They wore skin clothes entirely. The women's were made like bloomers
and were heavily padded for warmth. They wore high mukluks and really looked very
comfortable. The babies were in fur skins with the fur inside and they looked like little Teddy
bears with faces. I guess they had never seen white women, not so many at one time anyway.
We went ashore in 2 life boats and a launch pulling them. On the launch was a six piece band
playing and the rear of the last life boat was the movie man. 'Twas very thrilling.
The other place we stopped was at Whalen, a trading post in Siberia. There these 'towerists'
went wild. They rushed helter-skelter, hither and thither, here and there, trying to find
something to buy. Prices raised right before your eyes. One would but something for $1.00
and the next might have to pay $2.00, $4.00 or $10.00. That made no difference. They had to
have it. One man I was sort of taking care of, tho he had his son along for the purpose,
bought 2 ivory tusks, 1 pup, 2 moccasins, 3 or 4 billikens, 6 or 8 ivory and silver rings, one
fishing line, hooks, floats, etc. and two bird slings. The slings have rocks at the end and the
little natives throw them at the flocks of geese and ducks which fly close over the village and
the slings entangle their wings and legs, sometimes more than one, and they can't fly. They
come down and the natives capture them. There was more junk brot aboard than baggage, I
do believe. And they say that at the first stop it was worse than here. The red flag was flying
over Whalen and the Russian soldiers were there—a few, one or two or three, I forget the
number.
We got home yesterday morning at 5 a.m. but missed the first lighter in so had to stay out
until 2:30. The girls had prepared a big meal for us and invited up the Hartfords and then let
us talk. Miss Saville talked quite a bit. Any how if you folks don't like this I don't care, it is
all I had to write about and I know Buster'ud listen anyway and I'd soak ole Peter's head if he
didn't and Polly would in my lap and I don't know much about the youngest one of yours so
likely he would be squawling. But we did surely enjoy our trip and were gone just long enuf.
I expect there were 150 passengers on board and almost or more of the crew and helpers. We
had a stateroom down next to the kitchen and 'twas pretty fierce for odor at times.
I have had jobs nearly all summer but not very much in them. Next week, September 4,
school opens. I wish they would wait for a week but you know these school men. Wouldn't
make any special difference I suppose for I would just fritter away the time but still one likes
to postpone the inevitable.
I just now stopped and re-read your letter and it sure was a bird. I don't especially blame Ray
for reading over your shoulder. It would seem, then that you have bright children. Maybe
they do know something about Geography. But it is ridiculous to speak of Louis finishing the
eighth grade. Why you and I were grown children when we finished and he is only a baby. I
am rather afraid he doesn't know much. I quite remember your little timid Maurice and how
he shifted his affection from his Aunt Om and yelled and howled and screamed steadily for a
week while his mother went to S.S. Ask him is he recalls this little interview. Do you suppose
he does?
Ever hear from Zen? or Inez? They don't seem to be very writing inclined tho Zen has done
well this year. Even sent me a telegram a few weeks ago. Well, if anything else ever happens,
I'll write again. Don't suppose it ever will, tho.
Lots of love to all,
Ome
Reprinted from Cochran Chronicles, Volume 9, Number 1, November 1986
© JECFA 1986
Up
jecochranclan.org ~ Contact webmaster
@@ -1,31 +1,32 @@
source: Rod Moser Letter - p1.jpg
provider: openrouter
model: google/gemini-2.5-flash
model: openai/gpt-5.3-codex
---
[document body mixed]
JOHN ISBILL
R. T. MOSER
JOHN ISBILL R. T. MOSER
ISBILL & MOSER
DEALERS IN
GENERAL MERCHANDISE
[handwritten: Vonore, Tenn. Jan'y 27 - 1913]
[handwritten: Dear Much Aunt Louing
Vonore, Tenn., [handwritten: July 27-] 191[handwritten: 2]
[handwritten: Dear Uncle Aunt & Cousins
I was at home a
few nights ago & saw a
letter from you folks, so
letter from your folks, so
I decided to write you
a few lines myself &
I am contemplating a
a few lines in regards of
I am [contemplating?] a
trip out west next summer
& would like of [illegible] to go
& want [lots?] of [olders?] to go
where I am from.
Am getting
up in years & unmarried
up in years & wondering,
so you see the object of
my trip, is to get a wife
of there is any old maids
or widows out there, I
want you to kiss them
at my [illegible] for me at home
as soon as I get there]
If there is any old maids
or widows out there I
want you to [hire?] them
at [one?] [find?] me at there
as soon as I get there.]
+127 -26
View File
@@ -1,36 +1,137 @@
from __future__ import annotations
from contextlib import asynccontextmanager
import pytest
from transcription.config import Settings
from transcription.services.base import ServiceBase
class TestServiceBase:
class TestInitialization:
def test_initializes_with_defaults(self):
"""Test initialization with default session factory and queue."""
def test_initializes_with_custom_session_factory(self):
"""Test initialization with a provided session factory."""
class _TrackingSession:
def __init__(self) -> None:
self.commits = 0
self.flushes = 0
self.refreshed: list[object] = []
def test_initializes_with_custom_queue(self):
"""Test initialization with a provided queue."""
async def commit(self) -> None:
self.commits += 1
class TestSessionScope:
@pytest.mark.asyncio
async def test_uses_provided_session(self):
"""Test that session scope reuses a provided session."""
async def flush(self) -> None:
self.flushes += 1
@pytest.mark.asyncio
async def test_creates_new_session_when_none_provided(self):
"""Test that session scope creates a new session when none is provided."""
async def refresh(self, obj: object) -> None:
self.refreshed.append(obj)
class TestContextManagerBehavior:
@pytest.mark.asyncio
async def test_yields_session(self):
"""Test that session scope yields a usable session object."""
@pytest.mark.asyncio
async def test_multiple_operations(self):
"""Test multiple operations within a single session scope."""
def _settings() -> Settings:
return Settings(_env_file=None, openrouter_api_key="test-key")
class TestEdgeCases:
@pytest.mark.asyncio
async def test_handles_exception_propagation(self):
"""Test exception propagation behavior inside session scope."""
def test_initializes_with_defaults(monkeypatch):
settings = _settings()
session_factory = object()
monkeypatch.setattr("transcription.services.base.get_settings", lambda: settings)
monkeypatch.setattr(
"transcription.services.base.resolve_session_factory",
lambda **kwargs: session_factory,
)
service = ServiceBase()
assert service.settings is settings
assert service.session_factory is session_factory
def test_initializes_with_custom_session_factory():
settings = _settings()
session_factory = object()
service = ServiceBase(settings=settings, session_factory=session_factory)
assert service.settings is settings
assert service.session_factory is session_factory
@pytest.mark.asyncio
async def test_session_scope_reuses_provided_session(monkeypatch):
captured: dict[str, object | None] = {}
provided_session = object()
yielded = object()
service = ServiceBase(settings=_settings(), session_factory=object())
@asynccontextmanager
async def _fake_session_scope(*, session_factory=None, session=None):
captured["session_factory"] = session_factory
captured["session"] = session
yield yielded if session is None else session
monkeypatch.setattr("transcription.services.base.session_scope", _fake_session_scope)
async with service._session_scope(session=provided_session) as active:
assert active is provided_session
assert captured == {"session_factory": service.session_factory, "session": provided_session}
@pytest.mark.asyncio
async def test_session_scope_creates_owned_session_when_none_provided(monkeypatch):
captured: dict[str, object | None] = {}
owned_session = object()
service = ServiceBase(settings=_settings(), session_factory=object())
@asynccontextmanager
async def _fake_session_scope(*, session_factory=None, session=None):
captured["session_factory"] = session_factory
captured["session"] = session
yield owned_session
monkeypatch.setattr("transcription.services.base.session_scope", _fake_session_scope)
async with service._session_scope() as active:
assert active is owned_session
assert captured == {"session_factory": service.session_factory, "session": None}
@pytest.mark.asyncio
async def test_session_scope_propagates_exceptions(monkeypatch):
service = ServiceBase(settings=_settings(), session_factory=object())
@asynccontextmanager
async def _fake_session_scope(*, session_factory=None, session=None):
_ = (session_factory, session)
yield object()
monkeypatch.setattr("transcription.services.base.session_scope", _fake_session_scope)
with pytest.raises(RuntimeError, match="boom"):
async with service._session_scope():
raise RuntimeError("boom")
@pytest.mark.asyncio
async def test_finalize_commits_for_service_owned_session():
service = ServiceBase(settings=_settings(), session_factory=object())
session = _TrackingSession()
refreshed = object()
await service._finalize(session=session, caller_session=None, refresh=(refreshed,))
assert session.commits == 1
assert session.flushes == 0
assert session.refreshed == [refreshed]
@pytest.mark.asyncio
async def test_finalize_flushes_for_caller_owned_session():
service = ServiceBase(settings=_settings(), session_factory=object())
session = _TrackingSession()
refreshed = object()
await service._finalize(session=session, caller_session=object(), refresh=(refreshed,))
assert session.commits == 0
assert session.flushes == 1
assert session.refreshed == [refreshed]
+5 -1
View File
@@ -87,11 +87,15 @@ async def test_promotion_rejects_unrelated_attempt(default_session_factory):
services = _services(default_session_factory, settings)
source = await _seed_source(services)
with pytest.raises(CandidatePromotionError):
with pytest.raises(CandidatePromotionError) as exc_info:
await services.evidence.promote_machine_attempt(
source_id=source.id,
execution_attempt_id=uuid4(),
)
error = exc_info.value
assert error.category.value == "validation_error"
assert "successful transcription attempt" in error.message
assert "Select an available successful candidate" in error.suggestion
@pytest.mark.integration
+2 -2
View File
@@ -54,7 +54,7 @@ class TestAppLifespan:
@asynccontextmanager
async def _worker_lifespan(**_kwargs):
calls.append("worker_start")
yield object(), object()
yield object(), object(), object()
calls.append("worker_stop")
monkeypatch.setattr("transcription.app.worker_consumer_lifespan", _worker_lifespan)
@@ -110,7 +110,7 @@ class TestAppLifespan:
@asynccontextmanager
async def _worker_lifespan(**_kwargs):
calls.append("worker_start")
yield object(), object()
yield object(), object(), object()
calls.append("worker_stop")
monkeypatch.setattr("transcription.app.worker_consumer_lifespan", _worker_lifespan)
+17
View File
@@ -1,5 +1,7 @@
"""Tests for transcription.config — settings loading, provider defaults, and paths."""
import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path
from typing import Any
@@ -8,6 +10,7 @@ from pydantic import ValidationError
from transcription.config import Provider
from transcription.config import Settings
from transcription.config import configure_logging
from transcription.config import parse_cli_settings
@@ -141,6 +144,20 @@ class TestPathSettings:
settings = _make_settings()
assert isinstance(settings.upload_dir, Path)
assert isinstance(settings.prompt_dir, Path)
assert isinstance(settings.log_dir, Path)
assert isinstance(settings.database_backup_dir, Path)
def test_configure_logging_writes_rotating_file_logs_to_configured_directory(tmp_path):
log_dir = tmp_path / "logs"
settings = _make_settings(log_dir=log_dir, log_file_name="app.log")
configure_logging(settings)
root_handlers = logging.getLogger().handlers
file_handlers = [handler for handler in root_handlers if isinstance(handler, RotatingFileHandler)]
assert file_handlers
assert Path(file_handlers[0].baseFilename) == log_dir / "app.log"
class TestWorkerReliabilitySettings:
+16 -1
View File
@@ -34,7 +34,22 @@ async def test_distinct_urls_produce_distinct_engines_and_eviction_is_targeted()
@pytest.mark.asyncio
async def test_disposing_an_unregistered_url_is_a_noop():
await dispose_engine("sqlite+aiosqlite:///./.registry-test-never-created.db")
never_created = "sqlite+aiosqlite:///./.registry-test-never-created.db"
engine_a = get_engine(URL_A)
engine_b = get_engine(URL_B)
factory_a = get_session_factory(URL_A)
factory_b = get_session_factory(URL_B)
await dispose_engine(never_created)
await dispose_session_factory(never_created)
assert get_engine(URL_A) is engine_a
assert get_engine(URL_B) is engine_b
assert get_session_factory(URL_A) is factory_a
assert get_session_factory(URL_B) is factory_b
await dispose_session_factory(URL_A)
await dispose_session_factory(URL_B)
@pytest.mark.asyncio
+93
View File
@@ -2,8 +2,11 @@
from __future__ import annotations
import re
from pathlib import Path
from transcription.config import Settings
PROJECT_ROOT = Path(__file__).resolve().parents[1]
@@ -97,3 +100,93 @@ def test_canonical_authority_references_are_present():
if absent:
missing[relative_path] = absent
assert missing == {}
def _declared_env_example_keys(*, include_commented: bool) -> set[str]:
text = _read(".env.example")
pattern = r"^\s*#?\s*([A-Z0-9_]+)\s*=" if include_commented else r"^\s*([A-Z0-9_]+)\s*="
return {match.group(1) for match in re.finditer(pattern, text, flags=re.MULTILINE)}
def _active_env_example_values() -> dict[str, str]:
text = _read(".env.example")
return {
key: value.strip()
for key, value in re.findall(r"^\s*([A-Z0-9_]+)\s*=\s*(.*)$", text, flags=re.MULTILINE)
}
def _normalize_env_path_value(value: str | None) -> str | None:
if value is None:
return None
normalized = value.strip().replace("\\", "/")
while normalized.startswith("./"):
normalized = normalized[2:]
return normalized
def test_env_example_keys_match_runtime_settings_contract():
"""Guard against .env.example drift from Settings keys."""
declared = _declared_env_example_keys(include_commented=True)
settings_keys = {field.upper() for field in Settings.model_fields if field != "database"}
database_keys = {
"DATABASE__DRIVER",
"DATABASE__PATH",
"DATABASE__HOST",
"DATABASE__PORT",
"DATABASE__DATABASE",
"DATABASE__USER",
"DATABASE__PASSWORD",
}
allowed = settings_keys | database_keys
missing = sorted(allowed - declared)
unknown = sorted(declared - allowed)
assert missing == []
assert unknown == []
def test_env_example_default_values_match_settings_defaults():
"""Uncommented .env.example entries should mirror in-code defaults."""
defaults = Settings(_env_file=None, openrouter_api_key="placeholder-key")
expected = {
"HOST": defaults.host,
"PORT": str(defaults.port),
"LOG_LEVEL": defaults.log_level,
"RELOAD": str(defaults.reload).lower(),
"LOG_DIR": str(defaults.log_dir),
"LOG_FILE_NAME": defaults.log_file_name,
"LOG_FILE_MAX_BYTES": str(defaults.log_file_max_bytes),
"LOG_FILE_BACKUP_COUNT": str(defaults.log_file_backup_count),
"PROVIDER": defaults.provider.value,
"PROVIDER_MODEL": defaults.provider_model or "",
"DEFAULT_PROMPT_NAME": defaults.default_prompt_name,
"ENVIRONMENT": defaults.environment,
"DATABASE__DRIVER": defaults.database.driver,
"DATABASE__PATH": getattr(defaults.database, "path", ""),
"BOOTSTRAP_SCHEMA_ON_STARTUP": str(defaults.bootstrap_schema_on_startup).lower(),
"SQLITE_CHECK_SAME_THREAD": str(defaults.sqlite_check_same_thread).lower(),
"UPLOAD_DIR": str(defaults.upload_dir),
"PROMPT_DIR": str(defaults.prompt_dir),
"HOMEPAGE_DIR": str(defaults.homepage_dir),
"DATABASE_BACKUP_DIR": str(defaults.database_backup_dir),
"WORKER_MAX_RETRIES": str(defaults.worker_max_retries),
"WORKER_PROVIDER_TIMEOUT_SECONDS": str(defaults.worker_provider_timeout_seconds),
"WORKER_MIN_TRANSCRIPTION_CHARS": str(defaults.worker_min_transcription_chars),
"WORKER_MIN_TRANSCRIPTION_LINES": str(defaults.worker_min_transcription_lines),
"WORKER_FAIL_ON_FINISH_REASON_LENGTH": str(defaults.worker_fail_on_finish_reason_length).lower(),
}
active = _active_env_example_values()
path_like_keys = {"LOG_DIR", "UPLOAD_DIR", "PROMPT_DIR", "HOMEPAGE_DIR", "DATABASE_BACKUP_DIR"}
mismatches = {
key: {
"expected": _normalize_env_path_value(expected_value) if key in path_like_keys else expected_value,
"actual": _normalize_env_path_value(active.get(key)) if key in path_like_keys else active.get(key),
}
for key, expected_value in expected.items()
if (
(_normalize_env_path_value(active.get(key)) if key in path_like_keys else active.get(key))
!= (_normalize_env_path_value(expected_value) if key in path_like_keys else expected_value)
)
}
assert mismatches == {}
+10 -2
View File
@@ -8,6 +8,7 @@ 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
@@ -23,6 +24,7 @@ async def test_run_worker_loop_stops_on_non_retriable_exception(monkeypatch, cap
"""
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
@@ -34,12 +36,16 @@ async def test_run_worker_loop_stops_on_non_retriable_exception(monkeypatch, cap
with caplog.at_level(logging.CRITICAL):
await asyncio.wait_for(
run_worker_loop(stop_event=stop_event, poll_interval_seconds=0),
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
@@ -47,6 +53,7 @@ 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
@@ -65,10 +72,11 @@ async def test_run_worker_loop_survives_retriable_exception(monkeypatch, caplog)
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)
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