"""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 = """
Lohnabkommen für Testgewerbe
Inhalt
§ 1 Geltungsbereich
§ 1 Geltungsbereich
Für alle Betriebe
des Testgewerbes.
§ 2 Mindestlöhne
| Gruppe | Betrag |
| 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