fix(s51): close 2 review MINORs — non-UTF-8 skip + affected_codes parity

Both live in the tolerant inbox-read path and both contradicted the module's
own "skipped, never raised" / loader-parity docstrings (S5.1 review: ALLOW,
2 MINOR, non-gating):

- UnicodeDecodeError (a *.json hand-saved in Latin-1 with Norwegian æ/ø/å is
  invalid UTF-8) now SKIPPED in hitl._load_json_dict AND
  verdicts.load_verdicts_from_dir — was: crashed pending/route with a raw
  traceback (a ValueError subclass, caught by neither OSError nor
  JSONDecodeError). Fix symmetric across both readers.
- hitl._inbox_verdict_ids now skips a non-iterable affected_codes exactly as
  load_verdicts_from_dir does (frozenset() raises TypeError) — was: marked the
  proposal judged on key-presence alone → a silent false-negative in the
  operator pending queue. hitl-only: verdicts is the correct reference.

TDD: 3 RED-then-green (both UnicodeDecodeError twins + the parity gap) + 1
ground-truth pin locking the loader side so the parity cannot rot.

Gate: pytest → 389 passed, 4 skipped (was 385); ruff check + format clean; mypy clean.
This commit is contained in:
Kjell Tore Guttormsen 2026-07-15 20:59:01 +02:00
commit 7e86896bc8
5 changed files with 131 additions and 4 deletions

View file

@ -128,6 +128,36 @@ def test_pending_tolerant_to_missing_dir_and_foreign_files(tmp_path: Path) -> No
assert [p.run_id for p in result] == ["run-1"]
def test_pending_tolerant_to_non_utf8_inbox_file(tmp_path: Path) -> None:
"""A hand-authored inbox verdict saved in Latin-1 (Norwegian ``æ/ø/å``) is valid JSON bytes but
INVALID UTF-8; it matches the ``*.json`` glob yet must be SKIPPED, never crash ``pending``/``route``
with a raw traceback the module's "skipped, never raised" contract. Realistic in a Norwegian
domain. Its ``id`` equals the proposal's ``verdict_id``, so IF it were (wrongly) read it would clear
the queue ``[]``; a clean skip keeps the proposal pending ``["run-1"]`` uniquely proves it was
skipped, not read and not raised. Before the ``UnicodeDecodeError`` catch, this ERRORs (RED)."""
outbox = tmp_path / "outbox"
inbox = tmp_path / "inbox"
_write_proposal(outbox, "run-1", verdict_id="v1")
inbox.mkdir()
(inbox / "latin1.json").write_bytes(
json.dumps(
{
"id": "v1",
"decision": "approved",
"rationale": "godkjent på møtet",
"proposal_features": {
"affected_codes": ["05.2"],
"measure_type": "scope_reduction",
"claimed_saving_nok": 200.0,
},
},
ensure_ascii=False,
).encode("latin-1")
)
assert [p.run_id for p in hitl.pending(str(outbox), str(inbox))] == ["run-1"]
def test_pending_skips_orphan_proposal_without_outcome(tmp_path: Path) -> None:
"""A proposal file with no matching outcome (orphan from a half-written live run) is skipped."""
outbox = tmp_path / "outbox"