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>
This commit is contained in:
180
tests/test_imp47b_override_provisional.py
Normal file
180
tests/test_imp47b_override_provisional.py
Normal file
@@ -0,0 +1,180 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user