"""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"] == []