Fix workflow commit atomicity, error path leak, and UI error boundary
Quality Gate / gate (push) Failing after 48s

Phase 1 of docs/reviews/2026-08-23-code-review.md.

HIGH-01: process_queued_job committed page evidence and the terminal job
status in separate transactions, so a crash between them left a transcript
persisted against a job stuck in PROCESSING that the worker never reclaims.
The final page's write is now deferred into _finalize_batch_outcome so it
shares the terminal transaction. Intermediate pages remain individually
durable, and the terminal commit is shielded against cancellation the same
way per-page writes already were.

HIGH-04: added tests/integration/test_pipeline_atomicity.py covering both
Transaction B and Transaction C. Confirmed failing against the previous
implementation before the fix.

HIGH-03: classify_unexpected_error interpolated the raw exception into
AppError.message, which the UI renders and the API serializes, leaking the
database path from OperationalError. message is now generic. Because message
also feeds format_error_detail, which writes evidence records, the root cause
is preserved on a new internal-only AppError.detail field rather than
discarded.

HIGH-02: replaced 8 hand-rolled ui.notify error calls in home_page and
people_page with error_presenter.show_error, restoring the correlation
error_id, canonical category, and suggestion. Added an AST guard to
test_ui_boundaries.py so pages cannot hand-roll error notifications again.

Docs updated per documentation-sync: the message/detail split in
docs/error_handling.md and the multi-page atomicity rule in
services.instructions.md.

Verification: ruff clean, 381 tests passing, ty unchanged at 10 known
SQLAlchemy descriptor false positives.

Co-authored-by: Copilot App <[email protected]>
This commit is contained in:
Jim Lancaster
2026-08-23 18:02:04 -05:00
co-authored by Copilot App
parent 8d3c60fce1
commit de18c2e9da
9 changed files with 356 additions and 23 deletions
+29 -1
View File
@@ -7,6 +7,7 @@ from transcription.errors import ErrorCategory
from transcription.errors import build_error_envelope
from transcription.errors import canonical_error_category
from transcription.errors import classify_unexpected_error
from transcription.errors import format_error_detail
from transcription.errors import new_error_id
@@ -45,10 +46,37 @@ class TestAppErrorHelpers:
assert isinstance(err, AppError)
assert err.category == ErrorCategory.INTERNAL_UNEXPECTED
assert "unit.test" in err.message
assert "boom" in err.message
# The raw exception text must stay out of the user-facing message: it is rendered
# by the UI presenter and serialized into API envelopes, and unexpected exceptions
# routinely embed local filesystem paths.
assert "boom" not in err.message
assert err.suggestion
assert err.error_id
def test_unexpected_error_does_not_leak_filesystem_paths(self):
"""User-facing and API-facing text must not carry local filesystem paths.
`.github/instructions/error-handling.instructions.md` forbids leaking local
filesystem paths in user-facing output. A SQLAlchemy OperationalError embeds the
database path and an OSError embeds the storage root, so the generic catch-all
path is where that leak would occur. The cause is retained on `detail`, which is
internal-only, so evidence records and logs keep full diagnostic value.
"""
secret_path = r"C:\Github\transcription\data\transcription.db"
exc = OSError(f"unable to open database file: {secret_path}")
err = classify_unexpected_error(exc, operation="worker.process_job")
envelope = build_error_envelope(err)
assert secret_path not in err.message
assert secret_path not in envelope.message
assert secret_path not in err.suggestion
# Internal surfaces keep the root cause.
assert err.detail is not None
assert secret_path in err.detail
assert secret_path in format_error_detail(err)
def test_envelope_categories_use_canonical_contract_values(self):
"""API/UI envelope categories are normalized to canonical short identifiers."""
expected_mapping = {