- 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>
96 lines
4.1 KiB
Python
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"] == []
|