"""Tests für Impressum-Agent v2 (BaseSpecialistAgent).""" from __future__ import annotations import asyncio import pytest from compliance.services.specialist_agents import ( REGISTRY, AgentInput, AgentOutput, ImpressumAgent, Severity, ) from compliance.services.specialist_agents._base import ( FORBIDDEN_OUTPUT_TERMS, lint_output, stable_recommendation_id, ) from compliance.services.specialist_agents._rollup import rollup TESLA_IMPRESSUM = ( "Tesla Germany GmbH\n" "Ludwig-Prandtl-Strasse 25-29\n" "12526 Berlin\n" "Deutschland\n\n" "Email: kontakt@tesla.com\n" "Telefon: +49 89 1250 16 800\n\n" "Management:\n" "Elon Musk\n\n" "Handelsregister: HRB 218904 B, Amtsgericht Charlottenburg\n" ) FULL_IMPRESSUM = ( TESLA_IMPRESSUM + "\nUSt-IdNr: DE123456789\nGeschäftsführer: Max Mustermann\n" ) def _run(coro): return asyncio.get_event_loop().run_until_complete(coro) def test_agent_is_registered(): agent = REGISTRY.get("impressum") assert agent is not None assert agent.doc_type == "impressum" assert len(agent.owned_mc_ids) >= 10 def test_short_text_skipped(): agent = ImpressumAgent() out = _run(agent.evaluate(AgentInput(doc_type="impressum", text="x"))) assert out.mc_total > 0 assert all(c.status == "skipped" for c in out.mc_coverage) assert not out.findings def test_tesla_missing_german_label(monkeypatch): # Skip LLM escalation for unit test (no Ollama in CI) async def _no_cascade(*a, **kw): return None, [] monkeypatch.setattr( "compliance.services.specialist_agents.impressum.agent.cascade", _no_cascade, ) agent = ImpressumAgent() out = _run(agent.evaluate(AgentInput( doc_type="impressum", text=TESLA_IMPRESSUM, ))) field_ids = {f.field_id for f in out.findings} # Tesla pattern: "Management:" matches IMP-MC-006 → present # But IMP-MC-007 (deutsches Label) MUSS fehlen assert "vertretungsberechtigte_label_korrekt" in field_ids # USt fehlt assert "ust_id" in field_ids def test_safetykon_geschaeftsfuehrung_passes(monkeypatch): """Geschäftsführung (statt -führer) muss als deutsches Label akzeptiert werden — sonst False-Positive bei SafetyKon GmbH und ähnlichen.""" async def _no_cascade(*a, **kw): return None, [] monkeypatch.setattr( "compliance.services.specialist_agents.impressum.agent.cascade", _no_cascade, ) txt = ( "SafetyKon GmbH\nMerzhauser Str. 144\n79100 Freiburg\n" "Telefon: 0761/48 98 09 01\nE-Mail: info@safetykon.de\n" "Geschäftsführung: Dr. Oliver Kirchwehm\n" "Handelsregister AG Freiburg HRB 709859\n" "USt-Id: DE288952921\n" ) agent = ImpressumAgent() out = _run(agent.evaluate(AgentInput(doc_type="impressum", text=txt))) field_ids = {f.field_id for f in out.findings} assert "vertretungsberechtigte" not in field_ids assert "vertretungsberechtigte_label_korrekt" not in field_ids def test_full_impressum_has_no_basic_findings(monkeypatch): async def _no_cascade(*a, **kw): return None, [] monkeypatch.setattr( "compliance.services.specialist_agents.impressum.agent.cascade", _no_cascade, ) agent = ImpressumAgent() out = _run(agent.evaluate(AgentInput( doc_type="impressum", text=FULL_IMPRESSUM, ))) # nur scope-dependent fields fehlen (vsbg, odr, redaktion) high = [f for f in out.findings if f.severity == Severity.HIGH.value] assert not high, f"unexpected HIGH findings: {[f.field_id for f in high]}" def test_b2c_scope_adds_vsbg(monkeypatch): async def _no_cascade(*a, **kw): return None, [] monkeypatch.setattr( "compliance.services.specialist_agents.impressum.agent.cascade", _no_cascade, ) agent = ImpressumAgent() out = _run(agent.evaluate(AgentInput( doc_type="impressum", text=TESLA_IMPRESSUM, business_scope=["b2c", "ecommerce"], ))) field_ids = {f.field_id for f in out.findings} assert "verbraucher_streitbeilegung" in field_ids assert "odr_link" in field_ids def test_automotive_scope_auto_detected(monkeypatch): async def _no_cascade(*a, **kw): return None, [] monkeypatch.setattr( "compliance.services.specialist_agents.impressum.agent.cascade", _no_cascade, ) agent = ImpressumAgent() out = _run(agent.evaluate(AgentInput( doc_type="impressum", text=TESLA_IMPRESSUM, ))) field_ids = {f.field_id for f in out.findings} assert "aufsichtsbehoerde" in field_ids # Action MUSS KBA-Hint enthalten aufsicht = next(f for f in out.findings if f.field_id == "aufsichtsbehoerde") assert "KBA" in aufsicht.action or "Kraftfahrt" in aufsicht.action def test_disclaimer_linter_scrubs_forbidden(): from compliance.services.specialist_agents._base import Finding from datetime import datetime, timezone f = Finding( check_id="X", agent="t", agent_version="1", severity=Severity.HIGH, title="Diese Lösung ist rechtssicher und garantiert konform", action="Voll konform machen", ) out = AgentOutput( agent="t", agent_version="1", started_at=datetime.now(timezone.utc), finished_at=datetime.now(timezone.utc), duration_ms=0, findings=[f], ) cleaned = lint_output(out) assert "rechtssicher" not in cleaned.findings[0].title.lower() assert "garantiert" not in cleaned.findings[0].title.lower() assert "linter scrubbed" in cleaned.notes.lower() def test_rollup_bundles_same_action(): from compliance.services.specialist_agents._base import Finding fs = [ Finding(check_id="A", agent="t", agent_version="1", severity=Severity.HIGH, title="Lücke 1", action="AVV mit Anbieter X abschließen"), Finding(check_id="B", agent="t", agent_version="1", severity=Severity.MEDIUM, title="Lücke 2", action="AVV mit Anbieter X abschließen."), Finding(check_id="C", agent="t", agent_version="1", severity=Severity.LOW, title="Lücke 3", action="Etwas anderes machen"), ] recs = rollup(fs) assert len(recs) == 2 bundled = next(r for r in recs if len(r.related_finding_ids) == 2) assert bundled.severity == Severity.HIGH.value assert set(bundled.related_finding_ids) == {"A", "B"} def test_stable_recommendation_id_is_deterministic(): a = stable_recommendation_id("AVV mit Anbieter X abschließen") b = stable_recommendation_id("avv mit anbieter x abschliessen") # case insensitive aber Diakritika strict (deutsch ß ≠ ss) assert len(a) == 16 assert len(b) == 16 def test_forbidden_terms_complete(): """Sanity-Test, dass alle wichtigen Wörter im Linter sind.""" for term in ("rechtssicher", "garantiert", "gesetzeskonform"): assert any(term in t for t in FORBIDDEN_OUTPUT_TERMS)