KV/RIS-Erweiterung: 614 WKO-KV-Dokumente + 59 RIS-Gesetze (D9)
- Korpus 601 -> 1274 Layer-2-Eintraege: kv-kvt-001...614 (ein Cluster kollektivvertraege, Branche als Tag) und ris-<cluster-prefix>-nn auf der bestehenden Cluster-Map + 7 neue Cluster (zvr/avr/agg/lst/abo/ nso/kvt); LAW_MAP dokumentiert die 59 Gesetz-Zuordnungen. - kv/ris-Eintraege sind quellentreu generiert (D9) - Gesetze sind amtliche Werke, KV-Lohntabellen zahlenexakt; Tool-Output in tools/ (ingest_sources.py, build_registry.py, kb_common.py), eingefrorene ID-Kataloge tools/catalogs/*.json (nur Metadaten). - kb.json/INDEX.md regeneriert (1274 Eintraege, 76 Cluster); agent/kb.py: ID-Raeume kv|ris, source akzeptiert html-only. - Tests 41 -> 49 (Konverter, LAW_MAP-Abdeckung, neue ID-Raeume, Korpus-Integrationszahl).
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
"""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
|
||||
+31
-2
@@ -67,8 +67,37 @@ 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}
|
||||
assert len(entries) == 601
|
||||
# 601 kuratierte (lb/wk) + 614 WKO-KV + 59 RIS-Gesetze
|
||||
assert len(entries) == 1274
|
||||
assert "lb-atz-07" in ids and "wk-akt-01" in ids
|
||||
assert "kv-kvt-001" in ids and "ris-url-01" 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)
|
||||
assert any(s.title.startswith("Kernwerte") for s in atz.sections)
|
||||
|
||||
|
||||
def test_new_id_spaces_and_html_source(tmp_path):
|
||||
"""kv-/ris-IDs und html-only-Quellenangabe 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"
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user