243 lines
9.1 KiB
Python
243 lines
9.1 KiB
Python
"""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 = """<html><head>
|
||
<meta property="og:title" content="Lohnordnung Testgewerbe, gültig ab 1.4.2026"/>
|
||
<meta property="og:url" content="https://www.wko.at/kollektivvertrag/test"/>
|
||
</head><body>
|
||
<main data-gtm-block="main">
|
||
<header><div class="social-media-share"><button>Teilen</button></div></header>
|
||
<div class="container"><div class="row">
|
||
<main class="col-lg-8" data-gtm-element="article">
|
||
<div class="meta-details"><dl class="dl-kvs">
|
||
<div><dt>Räumlicher Geltungsbereich:</dt><dd> Österreichweit </dd></div>
|
||
<div><dt>Geltungsdauer:</dt><dd>ab 1.4.2026</dd></div>
|
||
</dl></div>
|
||
<!-- Content start -->
|
||
<h3>Lohnabkommen für Testgewerbe</h3>
|
||
<h4>Inhalt</h4>
|
||
<p><a href="#s1">§ 1 Geltungsbereich</a></p>
|
||
<h4>§ 1 Geltungsbereich</h4>
|
||
<p>Für alle Betriebe<br/>des Testgewerbes.</p>
|
||
<h4>§ 2 Mindestlöhne</h4>
|
||
<table><tr><th>Gruppe</th><th>Betrag</th></tr>
|
||
<tr><td>A1</td><td>€ 2.020,00</td></tr>
|
||
<tr><td>B1 | C1</td><td>€ 2.100,00</td></tr></table>
|
||
<p>Text nach Tabelle.</p>
|
||
</main>
|
||
</div></div></main></body></html>"""
|
||
|
||
|
||
@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</a>" 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
|