"""Tests for notes-coverage guard. These tests pin the deterministic behaviour of the coverage check that ``_generate_section_text`` and ``run_inspector_tool_loop`` use to decide whether to regenerate. The whole point of the guard is to be reliable without an LLM, so the assertions are exact thresholds, not loose bounds. """ from __future__ import annotations from app.generator.notes_coverage import ( coverage_regenerate_hint, coverage_report, extract_bullet_facts, ) def test_extract_bullet_facts_drops_stopword_only() -> None: facts = extract_bullet_facts(["the and of"]) assert facts == [] def test_extract_bullet_facts_keeps_factish_tokens() -> None: facts = extract_bullet_facts(["damp at rear elevation; postcode SW1A 1AA"]) assert len(facts) == 1 keys = facts[0].key_tokens assert "damp" in keys assert "rear" in keys assert "elevation" in keys assert "SW1A1AA" in keys def test_coverage_report_full_match() -> None: bullets = ["roof tiles slipped at south elevation"] text = "Roof tiles slipped at the south elevation; replacement recommended." rep = coverage_report(bullets, text) assert rep.coverage_ratio == 1.0 assert rep.bullets_poorly_covered == 0 assert not rep.needs_regenerate def test_coverage_report_detects_dropped_facts() -> None: bullets = [ "boiler 22 years old, no service record", "consumer unit lacks RCD protection on lighting circuits", "rising damp at rear hallway", ] text = "The property is generally well maintained." rep = coverage_report(bullets, text) assert rep.bullets_total == 3 assert rep.bullets_poorly_covered == 3 assert rep.coverage_ratio < 0.3 assert rep.needs_regenerate hint = coverage_regenerate_hint(rep) assert "COVERAGE VIOLATION" in hint assert "boiler" in hint or "rcd" in hint.lower() or "damp" in hint.lower() def test_coverage_report_stemming_does_not_misflag() -> None: """Bullet says 'cracks' but text says 'cracking' — should still cover.""" bullets = ["cracks to rear chimney stack"] text = "Cracking observed to the rear chimney stack." rep = coverage_report(bullets, text) assert rep.coverage_ratio >= 0.7 def test_coverage_report_empty_bullets_safe() -> None: rep = coverage_report([], "any text") assert rep.bullets_total == 0 assert rep.coverage_ratio == 1.0 assert not rep.needs_regenerate def test_coverage_regenerate_hint_lists_missing_tokens() -> None: bullets = ["smoke alarm missing in hallway", "lead paint on window frames"] text = "General property condition fair." rep = coverage_report(bullets, text) hint = coverage_regenerate_hint(rep) assert "smoke" in hint.lower() or "alarm" in hint.lower() assert "lead" in hint.lower() or "paint" in hint.lower()