Files
C.E.L_Slide_test2/tests/test_imp47b_coverage_invariant.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

96 lines
4.1 KiB
Python

"""IMP-47B u7 — Post-AI source_section_ids coverage invariant tests.
Scope (this slice):
* Helper ``_check_post_ai_coverage_invariant(units, ai_repair_records)``
(src/phase_z2_pipeline.py) compares the pre-AI superset (unit
``source_section_ids``) to the post-apply superset present on
gather records. Per the AI isolation contract + dropped 절대 룰
(``feedback_ai_isolation_contract``), AI repair must not silently
drop a section.
* The helper returns a structured dict (``pre_ai_section_ids``,
``post_ai_section_ids``, ``dropped_section_ids``, ``status``) so u8
can surface ``status`` through ``slide_status.ai_repair_status``.
u8 slide_status surfacing and u10 E2E no-text-loss assertion are out
of scope for this unit. The helper is pure (no AI call, no IO) so a
synthetic stub-unit / stub-record fixture exercises it directly.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from src.phase_z2_pipeline import _check_post_ai_coverage_invariant
@dataclass
class _StubUnit:
source_section_ids: list[str] = field(default_factory=list)
def _record(source_section_ids: list[str]) -> dict:
"""Minimal gather-record stub — only the field u7 reads."""
return {"source_section_ids": list(source_section_ids)}
# ─── Case 1 : matched coverage → status='ok' ────────────────────────
def test_coverage_invariant_ok_when_records_match_units():
"""Records carry every unit's source_section_ids → no drop, status='ok'."""
units = [_StubUnit(["MOCK_S1", "MOCK_S2"]), _StubUnit(["MOCK_S3"])]
records = [_record(["MOCK_S1", "MOCK_S2"]), _record(["MOCK_S3"])]
result = _check_post_ai_coverage_invariant(units, records)
assert result["status"] == "ok"
assert result["dropped_section_ids"] == []
assert result["pre_ai_section_ids"] == ["MOCK_S1", "MOCK_S2", "MOCK_S3"]
assert result["post_ai_section_ids"] == ["MOCK_S1", "MOCK_S2", "MOCK_S3"]
# ─── Case 2 : record drops a section → status='violated' ────────────
def test_coverage_invariant_violated_when_record_drops_section():
"""If a record loses a unit's section_id (e.g., apply mutation bug),
the invariant reports status='violated' + dropped list (dropped 절대 룰).
"""
units = [_StubUnit(["MOCK_S1", "MOCK_S2"]), _StubUnit(["MOCK_S3"])]
records = [_record(["MOCK_S1"]), _record(["MOCK_S3"])] # MOCK_S2 dropped
result = _check_post_ai_coverage_invariant(units, records)
assert result["status"] == "violated"
assert result["dropped_section_ids"] == ["MOCK_S2"]
assert "MOCK_S2" in result["pre_ai_section_ids"]
assert "MOCK_S2" not in result["post_ai_section_ids"]
# ─── Case 3 : empty inputs → status='ok' (no false positive) ────────
def test_coverage_invariant_ok_on_empty_units_and_records():
"""Empty pipeline (no units / no records) is a vacuous pass —
avoids false-positive 'violated' on edge-case shapes (no AI work).
"""
result = _check_post_ai_coverage_invariant([], [])
assert result["status"] == "ok"
assert result["dropped_section_ids"] == []
assert result["pre_ai_section_ids"] == []
assert result["post_ai_section_ids"] == []
# ─── Case 4 : multiple drops + dedup ────────────────────────────────
def test_coverage_invariant_lists_all_dropped_sections_sorted_and_deduped():
"""Multiple missing sections → dropped_section_ids is sorted + deduped.
Duplicate ids across units / records collapse to a set comparison.
"""
units = [
_StubUnit(["MOCK_S3", "MOCK_S1"]),
_StubUnit(["MOCK_S2", "MOCK_S1"]), # MOCK_S1 duplicate
]
records: list[dict] = [] # full drop — every unit section missing
result = _check_post_ai_coverage_invariant(units, records)
assert result["status"] == "violated"
assert result["dropped_section_ids"] == ["MOCK_S1", "MOCK_S2", "MOCK_S3"]
assert result["pre_ai_section_ids"] == ["MOCK_S1", "MOCK_S2", "MOCK_S3"]
assert result["post_ai_section_ids"] == []