"""Tests für tools/ingest_sources.py: KV-HTML-Konverter, RIS-Intake, LAW_MAP.""" import sys from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from tools import ingest_sources as ing # noqa: E402 from tools.kb_common import LAW_MAP # noqa: E402 WKO_HTML = """
Räumlicher Geltungsbereich:
Österreichweit
Geltungsdauer:
ab 1.4.2026

Lohnabkommen für Testgewerbe

Inhalt

§ 1 Geltungsbereich

§ 1 Geltungsbereich

Für alle Betriebe
des Testgewerbes.

§ 2 Mindestlöhne

GruppeBetrag
A1€ 2.020,00
B1 | C1€ 2.100,00

Text nach Tabelle.

""" @pytest.fixture() def converted(): soup = ing.prep_soup(WKO_HTML) main = soup.find("main", class_="col-lg-8") meta = ing.parse_kv_meta(main) return meta, ing.blocks_to_md(main) def test_kv_meta_and_geltungsdauer(converted): meta, _ = converted assert meta == [ ("Räumlicher Geltungsbereich", "Österreichweit"), ("Geltungsdauer", "ab 1.4.2026"), ] title = "Lohnordnung Testgewerbe, gültig ab 1.4.2026" stand, tags = ing.derive_stand(title, meta, "test.html") assert stand == "2026-04" and tags == [] def test_kv_stand_fallbacks(): stand, tags = ing.derive_stand("Lohnordnung X 2025", [], "x.html") assert (stand, tags) == ("2025-01", ["stand-jahr"]) stand, tags = ing.derive_stand("Generalkollektivvertrag", [], "undatiert.html") assert (stand, tags) == (ing.CRAWL_STAND, ["stand-geschaetzt"]) stand, _ = ing.derive_stand("KV X", [], "lohnordnung-x-2023.html") assert stand == "2023-01" def test_kv_blocks_heading_toc_table(converted): _, lines = converted md = "\n".join(lines) assert "## Lohnabkommen für Testgewerbe" in md assert "## § 1 Geltungsbereich" in md assert "Für alle Betriebe\ndes Testgewerbes." in md # TOC übersprungen assert "§ 1 Geltungsbereich" not in md assert "Content start" not in md # Tabelle als Markdown, Pipe escaped assert "| Gruppe | Betrag |" in md assert "| --- | --- |" in md assert "| A1 | € 2.020,00 |" in md assert "| B1 \\| C1 | € 2.100,00 |" in md assert "Text nach Tabelle." in md def test_kv_entry_writes_frontmatter(tmp_path, monkeypatch): catalog = {"source": "kv", "updated": None, "next_seq": 1, "entries": {}} stats = {"kv_written": 0, "no_og": [], "no_main": []} p = tmp_path / "lohnordnung-testgewerbe-2026.html" p.write_text(WKO_HTML, encoding="utf-8") slug, md = ing.kv_entry(p, catalog, stats) assert slug.startswith("kv_") assert "id: kv-kvt-001" in md assert "topic: kollektivvertraege" in md assert "stand: 2026-04" in md assert "chapter: \"Lohnordnung\"" in md assert "jahr-2026" in md assert catalog["next_seq"] == 2 def test_doctype_classification(): assert ing.classify_doctype("Zusatz-KV Wachorgane 2026") == "Zusatz-KV" assert ing.classify_doctype("Rahmenkollektivvertrag Textil 2026") == "Rahmen-KV" assert ing.classify_doctype("Gehaltsordnung Information und Consulting") == "Gehaltsordnung" assert ing.classify_doctype("Lohnordnung Friseur/in") == "Lohnordnung" assert ing.classify_doctype("Empfehlung Lohntafeln Handel") == "Empfehlung" assert ing.classify_doctype("Fragen und Antworten KV IT 2026") == "Fragen & Antworten" assert ing.classify_doctype("Kollektivvertrag Handel Angestellte 2026") == "Kollektivvertrag" assert ing.classify_doctype("Dienst- und Besoldungsordnung Privatbahnen") == "Dienst- und Besoldungsordnung" def test_law_map_covers_all_ris_files(): ris_files = {p.stem for p in ing.RIS_DIR.glob("*.md")} assert ris_files == set(LAW_MAP.keys()), "LAW_MAP und .ris/*.md müssen 1:1 passen" def test_ris_entry(tmp_path, monkeypatch): catalog = {"source": "ris", "updated": None, "next_seq": 1, "entries": {}} stats = {"ris_written": 0, "ris_unmapped": []} p = tmp_path / "TestG.md" p.write_text( "# Testgesetz (TestG)\n\n" "Quelle: RIS (https://www.ris.bka.gv.at/), Gesetzesnummer 12345678, " "Stand: 2026-09-13. Es sind nur die zitierten Paragraphen enthalten.\n\n" "## § 1. Geltungsbereich\n\n(1) Dieses Gesetz gilt für alle.\n\n" "## § 2. Anspruch\n\nEs besteht ein Anspruch.\n", encoding="utf-8", ) # LAW_MAP hat kein TestG — Mapping über Monkeypatch injizieren monkeypatch.setitem(ing.LAW_MAP, "TestG", ("normen-sonstige", "Testrecht", 6)) slug, md = ing.ris_entry(p, catalog, stats) assert slug == "ris_testg" assert "id: ris-nso-06" in md assert "stand: 2026-09" in md assert 'legal_bases: ["TestG"]' in md assert "## § 1. Geltungsbereich" in md assert "(1) Dieses Gesetz gilt für alle." in md assert "ris-nso-06" in md # ID-Zeile im Body def _rj_catalog(): return {"source": "rj", "updated": None, "next_seq": 1, "entries": {}} def _rj_stats(): return {"rj_written": 0, "rj_unparsed": [], "rj_empty": []} def test_rj_rechtssatz_preserves_court_date_and_norm(tmp_path, monkeypatch): root = tmp_path / ".rechtsprechung" source = root / "originale" / "JJR_test.md" source.parent.mkdir(parents=True) source.write_text( "# Rechtssatz\n\n" "**Gericht:** OGH\n" "**Entscheidungsdatum:** 15. 7. 1954\n" "**Geschäftszahl:** 4 Ob 100/54\n" "**Norm:** AngG § 26 Z 4\n" "**Rechtssatz:** Ein Austrittsgrund ist unverzüglich geltend zu machen.\n", encoding="utf-8", ) monkeypatch.setattr(ing, "RJ_DIR", root) slug, md = ing.rj_entry(source, _rj_catalog(), _rj_stats()) assert slug == "rj_jjr-test" assert "id: rj-rjs-001" in md assert 'chapter: "OGH-Rechtssatz"' in md assert "stand: 1954-07" in md assert 'legal_bases: ["AngG § 26 Z 4"]' in md assert "## Rechtssatz" in md def test_rj_volltext_splits_long_reasoning_at_paragraphs(tmp_path, monkeypatch): root = tmp_path / ".rechtsprechung" source = root / "originale" / "JJT_test.md" source.parent.mkdir(parents=True) paragraphs = [f"Absatz {i}: " + "x" * 900 for i in range(5)] source.write_text( "# Entscheidung\n\n" "**Gericht:** OGH\n" "**Entscheidungsdatum:** 1. 2. 2020\n" "**Geschäftszahl:** 1 Ob 2/20a\n\n---\n\n" + "\n\n".join(paragraphs), encoding="utf-8", ) monkeypatch.setattr(ing, "RJ_DIR", root) _, md = ing.rj_entry(source, _rj_catalog(), _rj_stats()) assert "## Begründung (1)" in md assert "## Begründung (2)" in md def test_rj_norm_supports_h2_and_flat_wording(tmp_path, monkeypatch): root = tmp_path / ".rechtsprechung" h2_source = root / "gesetze" / "AngG_§15.md" flat_source = root / "gesetze" / "ABGB_§1435.md" h2_source.parent.mkdir(parents=True) h2_source.write_text( "# Angestelltengesetz\n\n**Kurztitel:** Angestelltengesetz\n" "**Abkürzung:** AngG\n**§/Artikel/Anlage:** § 15\n\n---\n\n" "## § 15\n\nWortlaut.\n\n## Zuletzt aktualisiert am\n\n1. 7. 2023\n", encoding="utf-8", ) flat_source.write_text( "# ABGB\n\n**Kurztitel:** ABGB\n**Abkürzung:** ABGB\n" "**§/Artikel/Anlage:** § 1435\n\n---\n\nDer Wortlaut ohne H2.\n", encoding="utf-8", ) monkeypatch.setattr(ing, "RJ_DIR", root) catalog = _rj_catalog() _, h2_md = ing.rj_entry(h2_source, catalog, _rj_stats()) _, flat_md = ing.rj_entry(flat_source, catalog, _rj_stats()) assert "stand: 2023-07" in h2_md assert 'legal_bases: ["AngG § 15"]' in h2_md assert "## § 15 – Wortlaut" in h2_md assert "stand: 2026-09" in flat_md assert "stand-abruf" in flat_md assert "## Normtext" in flat_md def test_rj_eugh_marks_lexetius_copy_as_nonofficial(tmp_path, monkeypatch): root = tmp_path / ".rechtsprechung" source = root / "originale" / "EUGH_C-29-1991.md" source.parent.mkdir(parents=True) source.write_text( "# EuGH C-29/91\n\n" "**URL:** https://www.lexetius.com/1992,1\n" "**Offizielle Fassung:** https://eur-lex.europa.eu/example\n\n---\n\n" "Urteil vom 19. 5. 1992\n\nEntscheidungsgründe.", encoding="utf-8", ) monkeypatch.setattr(ing, "RJ_DIR", root) _, md = ing.rj_entry(source, _rj_catalog(), _rj_stats()) assert 'work: "EuGH – Rechtsprechung (Textwiedergabe lexetius, nicht amtlich)"' in md assert "stand: 1992-05" in md assert "Textwiedergabe (nicht amtlich): https://www.lexetius.com/1992,1" in md assert "amtliche Fassung: https://eur-lex.europa.eu/example" in md