"""IMP-47B u3 — override-selected reject frames are admitted as provisional. Scope (this slice): Helper `_apply_frame_override_to_unit` (src/phase_z2_pipeline.py) covers the three probe layers used by the `--override-frame` path: 1. ``v4_candidates`` exact match (non-reject; existing behaviour). 2. Full 32 V4 judgments probe (reject inclusive) — when the user picks a reject frame, the unit is promoted to ``provisional=True`` with ``label="reject"`` so Step 12 (IMP-47B u4) admits the AI repair path. 3. Raw fall-through (template_id only) — no provisional promotion, no label mutation. Frame visual / contract stay untouched per the AI isolation contract (frame auto-swap forbidden — AI re-places content into the existing frame only). Sibling test confirms a non-reject override still goes through the v4_candidates path without provisional promotion. Synthetic naming convention mirrors tests/test_phase_z2_imp30_first_render.py (MOCK_ prefix mandatory, no real catalog template_id / frame_id leakage). """ from __future__ import annotations from dataclasses import dataclass, field from typing import Optional from src.phase_z2_pipeline import _apply_frame_override_to_unit @dataclass class _StubCandidate: template_id: str frame_id: str frame_number: int confidence: float label: str @dataclass class _StubUnit: source_section_ids: list[str] frame_template_id: Optional[str] = None frame_id: Optional[str] = None frame_number: int = 0 confidence: float = 0.0 label: Optional[str] = None provisional: bool = False v4_candidates: list = field(default_factory=list) def _v4_with_reject(section_id: str, target_tid: str) -> dict: """Synthetic V4 dict with target_tid mapped to a reject judgment. Mirrors the production V4 schema surface (``mdx_sections`` → ``judgments_full32`` → list of judgment dicts with template_id / frame_id / frame_number / confidence / label). Two judgments so we can also assert that the helper picks the reject entry rather than the first non-reject one when the template_ids differ. """ return { "mdx_sections": { section_id: { "judgments_full32": [ { "template_id": "MOCK_T_other", "frame_id": "F_other", "frame_number": 1, "confidence": 0.85, "label": "use_as_is", }, { "template_id": target_tid, "frame_id": "F_reject", "frame_number": 32, "confidence": 0.40, "label": "reject", }, ], }, }, } # ─── Case 1 : reject override → provisional promotion ──────────── def test_override_to_reject_judgment_marks_unit_provisional(): """User picks a reject frame → unit.label=reject, provisional=True. Frame metadata is sourced from the reject judgment (frame_id / frame_number / confidence) so Step 9 metadata stays consistent. """ unit = _StubUnit( source_section_ids=["MOCK_S1"], frame_template_id="MOCK_T_auto", frame_id="F_auto", frame_number=5, confidence=0.90, label="use_as_is", provisional=False, ) v4 = _v4_with_reject("MOCK_S1", "MOCK_T_reject") meta = _apply_frame_override_to_unit(unit, "MOCK_T_reject", v4) assert meta == "v4_reject_judgment_provisional" assert unit.frame_template_id == "MOCK_T_reject" assert unit.frame_id == "F_reject" assert unit.frame_number == 32 assert unit.confidence == 0.40 assert unit.label == "reject" assert unit.provisional is True # ─── Case 2 : non-reject override → existing v4_candidates path ─── def test_override_to_v4_candidate_keeps_non_provisional(): """User picks a non-reject candidate → existing v4_candidates path. Helper takes the early v4_candidates branch without consulting the full 32 judgments. provisional remains False (normal-path AI=0 contract — IMP-30 / IMP-47B router gate intact for this unit). """ unit = _StubUnit( source_section_ids=["MOCK_S2"], frame_template_id="MOCK_T_auto", frame_id="F_auto", frame_number=3, confidence=0.95, label="use_as_is", provisional=False, v4_candidates=[ _StubCandidate( template_id="MOCK_T_pick", frame_id="F_pick", frame_number=2, confidence=0.85, label="light_edit", ), ], ) v4 = {"mdx_sections": {}} # full-judgment probe must NOT be reached meta = _apply_frame_override_to_unit(unit, "MOCK_T_pick", v4) assert meta == "v4_candidates" assert unit.frame_template_id == "MOCK_T_pick" assert unit.frame_id == "F_pick" assert unit.label == "light_edit" assert unit.provisional is False # ─── Case 3 : unknown template → raw fall-through (no provisional) ─ def test_override_unknown_template_falls_through_without_provisional(): """Template ID absent from v4_candidates AND from judgments_full32 → raw_template_id_only path. No provisional flag, no label change. """ unit = _StubUnit( source_section_ids=["MOCK_S3"], frame_template_id="MOCK_T_auto", frame_id="F_auto", frame_number=4, confidence=0.92, label="use_as_is", provisional=False, ) v4 = {"mdx_sections": {}} meta = _apply_frame_override_to_unit(unit, "MOCK_T_unknown", v4) assert meta == "raw_template_id_only" assert unit.frame_template_id == "MOCK_T_unknown" # frame_id / label unchanged — caller's print path warns on this case. assert unit.frame_id == "F_auto" assert unit.label == "use_as_is" assert unit.provisional is False