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"

View file

@ -193,6 +193,36 @@ def test_inbox_idset_skips_malformed_features(tmp_path: Path) -> None:
assert [p.run_id for p in hitl.pending(str(outbox), str(inbox))] == ["run-1"]
def test_inbox_idset_skips_non_iterable_affected_codes(tmp_path: Path) -> None:
"""PARITY (wrong-type): an inbox verdict with every required key present but ``affected_codes`` a
NON-ITERABLE (``null``) is skipped the real loader's ``frozenset(pf['affected_codes'])``
(verdicts.py) raises ``TypeError`` and ``load_verdicts_from_dir`` drops it. So ``pending`` must NOT
count it as judged; otherwise ``hitl`` marks the proposal judged while the learning pipeline still
treats it pending a silent false-negative in the operator queue, the exact failure the parity
docstring says it prevents. Detach the ``affected_codes`` iterability check (accept on key-presence
alone) the file clears pending RED. Ground-truth twin:
``test_verdicts.test_load_skips_non_iterable_affected_codes``."""
outbox = tmp_path / "outbox"
inbox = tmp_path / "inbox"
_write_proposal(outbox, "run-1", verdict_id="SHARED-VID")
_write_raw_verdict(
inbox,
"SHARED-VID",
{
"id": "SHARED-VID",
"decision": "approved",
"rationale": "ok",
"proposal_features": {
"affected_codes": None, # non-iterable → real loader's frozenset() raises → skips
"measure_type": "scope_reduction",
"claimed_saving_nok": 200.0,
},
},
)
assert [p.run_id for p in hitl.pending(str(outbox), str(inbox))] == ["run-1"]
# --- transitive import-graph MAF-freedom probe (Step 5) -------------------------------------------
_SRC_DIR = Path(__file__).resolve().parents[1] / "src" / "portfolio_optimiser"

View file

@ -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