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:
parent
5807ac428c
commit
7e86896bc8
5 changed files with 131 additions and 4 deletions
|
|
@ -7,6 +7,7 @@ genuine two-arg ``extend_instructions(source_id, instructions)`` GA signature
|
|||
Critical Fase 1 risk. Pattern: tests/spikes/test_d_verdictstore.py + real SessionContext.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -205,6 +206,58 @@ def test_load_within_caps_is_unchanged(tmp_path) -> None:
|
|||
assert {v.decision for v in loaded} == {"approved", "rejected"}
|
||||
|
||||
|
||||
def test_load_skips_non_utf8_file(tmp_path) -> None:
|
||||
"""T-2.5d: 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 raise — the loader's
|
||||
per-file tolerant contract (the Step-7 loop relies on it). Detach the ``UnicodeDecodeError`` catch
|
||||
→ ``read_text(encoding='utf-8')`` raises → RED. The valid verdict alongside still loads."""
|
||||
(tmp_path / "latin1.json").write_bytes(
|
||||
json.dumps(
|
||||
{
|
||||
"id": "X",
|
||||
"decision": "approved",
|
||||
"rationale": "godkjent på møtet",
|
||||
"proposal_features": {
|
||||
"affected_codes": ["05.2"],
|
||||
"measure_type": "m",
|
||||
"claimed_saving_nok": 200.0,
|
||||
},
|
||||
},
|
||||
ensure_ascii=False,
|
||||
).encode("latin-1")
|
||||
)
|
||||
write_verdict(str(tmp_path), capture_verdict(_feats("Y"), "approved", "fine"))
|
||||
|
||||
loaded = load_verdicts_from_dir(str(tmp_path))
|
||||
assert [v.decision for v in loaded] == ["approved"] # latin1 file skipped, the valid one loads
|
||||
|
||||
|
||||
def test_load_skips_non_iterable_affected_codes(tmp_path) -> None:
|
||||
"""Parity ground-truth: ``verdict_from_dict``'s ``frozenset(affected_codes)`` raises ``TypeError``
|
||||
on a non-iterable (``null``), and ``load_verdicts_from_dir`` SKIPS it. This pins the reference the
|
||||
hitl inbox predicate mirrors (``test_hitl_loadbearing.test_inbox_idset_skips_non_iterable_affected_codes``)
|
||||
so the parity contract cannot silently rot on the loader side."""
|
||||
(tmp_path / "bad.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"id": "X",
|
||||
"decision": "approved",
|
||||
"rationale": "r",
|
||||
"proposal_features": {
|
||||
"affected_codes": None,
|
||||
"measure_type": "m",
|
||||
"claimed_saving_nok": 1.0,
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
write_verdict(str(tmp_path), capture_verdict(_feats("Y"), "approved", "fine"))
|
||||
|
||||
loaded = load_verdicts_from_dir(str(tmp_path))
|
||||
assert [v.decision for v in loaded] == ["approved"] # non-iterable affected_codes skipped
|
||||
|
||||
|
||||
def test_no_inbox_json_uses_approved_with_adjustment() -> None:
|
||||
"""Assumption 3 (TDD guard): no shipped/test ``.json`` uses the promotion-only decision
|
||||
``approved_with_adjustment`` — the vocabulary SKIP would now silently drop it. It lives only in
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue