Files
C.E.L_Slide_test2/tests/test_imp47b_override_provisional.py
kyeongmin 1186ad8ae2 feat(#76): IMP-47B reject-as-AI-adaptation activation (u1~u13 backend + tests)
- u1~u9: AI fallback infrastructure (router/prompts/schema/validator) + Step 12 hook
- u10: e2e reject chain (writes final.html with AI-repaired slot, full coverage)
- u11: frontend wiring deferred to follow-up commit (split from IMP-41 hunks)
- u12: coverage_invariant guard
- u13: cache save gate (visual_check PASS + user_approved/auto_cache) — Codex #22 verified

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 00:19:10 +09:00

181 lines
6.0 KiB
Python

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