"""Tests: Layer-2-Parsing, Sektionen, Cross-Ref-Integrität, kb.json-Gate.""" import json from pathlib import Path import pytest from agent.kb import KbValidationError, load_entry, load_kb, parse_frontmatter, split_sections from tests.conftest import DOC_ATZ, MIN_IDS, write_mini_kb def test_parse_frontmatter_and_sections(): meta, body = parse_frontmatter(DOC_ATZ) assert meta["id"] == "lb-min-01" assert meta["stand"] == "2026-01" sections = split_sections(body) titles = [s.title for s in sections] assert "Zusammenfassung" in titles assert titles[1].startswith("Kernwerte & Fristen") assert all(s.text for s in sections) def test_split_sections_ignores_h1_and_intro(): sections = split_sections("# Titel\n\n*Quellzeile*\n\n## A\n\nText A\n\n## B\n\nText B") assert [s.title for s in sections] == ["A", "B"] assert sections[0].text == "Text A" def test_load_entry_minimal_doc(tmp_path): p = tmp_path / "doc.md" p.write_text(DOC_ATZ, encoding="utf-8") e = load_entry(p) assert e.id == "lb-min-01" assert e.tags == ["altersteilzeit", "lohnausgleich"] assert e.cross_refs == ["lb-min-02"] assert len(e.sections) >= 3 def test_gate_rejects_out_of_sync_registry(tmp_path): root = write_mini_kb(tmp_path / "kb") kb = json.loads((root / "kb.json").read_text(encoding="utf-8")) kb["entries"].append({"id": "lb-min-99"}) (root / "kb.json").write_text(json.dumps(kb), encoding="utf-8") with pytest.raises(KbValidationError, match="out of sync"): load_kb(root) def test_gate_rejects_dangling_cross_refs(tmp_path): root = write_mini_kb(tmp_path / "kb") doc = root / "dokumente" / "altersteilzeit_uberblick.md" doc.write_text( DOC_ATZ.replace('cross_refs: ["lb-min-02"]', 'cross_refs: ["lb-min-42"]'), encoding="utf-8", ) with pytest.raises(KbValidationError, match="dangling cross_refs"): load_kb(root) def test_gate_rejects_invalid_stand(tmp_path): root = write_mini_kb(tmp_path / "kb") doc = root / "dokumente" / "altersteilzeit_uberblick.md" doc.write_text(DOC_ATZ.replace("stand: 2026-01", "stand: Jänner 2026"), encoding="utf-8") with pytest.raises(KbValidationError, match="not YYYY-MM"): load_kb(root) def test_real_corpus_loads_and_matches_registry(): """Integrationstest gegen die echte Wissensbasis (Gate inklusive).""" entries = load_kb("wissensbasis", verify_registry=True) ids = {e.id for e in entries} # 601 kuratierte (lb/wk) + 614 WKO-KV + 59 RIS-Gesetze + 348 Rechtsprechungsquellen assert len(entries) == 1622 assert "lb-atz-07" in ids and "wk-akt-01" in ids assert "kv-kvt-001" in ids and "ris-url-01" in ids and "rj-rjs-001" in ids atz = [e for e in entries if e.id == "lb-atz-07"][0] assert atz.topic == "altersteilzeit" assert any(s.title.startswith("Kernwerte") for s in atz.sections) def test_new_id_spaces_and_html_source(tmp_path): """kv-/ris-/rj-IDs und html-/text-Quellenangaben sind gültig; Unbekanntes nicht.""" kv = DOC_ATZ.replace("id: lb-min-01", "id: kv-kvt-001").replace( 'source:\n pdf: ".lexis360/Lexis360_test_atz.pdf"\n' ' text: ".lexis360/md/test_atz.md"', 'source:\n html: ".firecrawl/wko-kv/docs/test.html"', ) p = tmp_path / "kv_doc.md" p.write_text(kv, encoding="utf-8") assert load_entry(p).id == "kv-kvt-001" ris = DOC_ATZ.replace("id: lb-min-01", "id: ris-url-01").replace( 'source:\n pdf: ".lexis360/Lexis360_test_atz.pdf"\n' ' text: ".lexis360/md/test_atz.md"', 'source:\n text: ".ris/Test.md"', ) p2 = tmp_path / "ris_doc.md" p2.write_text(ris, encoding="utf-8") assert load_entry(p2).id == "ris-url-01" rj = DOC_ATZ.replace("id: lb-min-01", "id: rj-rjs-001").replace( 'source:\n pdf: ".lexis360/Lexis360_test_atz.pdf"\n' ' text: ".lexis360/md/test_atz.md"', 'source:\n text: ".rechtsprechung/originale/Test.md"', ) p3 = tmp_path / "rj_doc.md" p3.write_text(rj, encoding="utf-8") assert load_entry(p3).id == "rj-rjs-001" bad = DOC_ATZ.replace("id: lb-min-01", "id: xx-min-01") p3 = tmp_path / "bad.md" p3.write_text(bad, encoding="utf-8") with pytest.raises(KbValidationError, match="invalid id"): load_entry(p3)