Files
pv-agent/tests/test_ingest_sources.py
T

243 lines
9.1 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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