702e7a6333
CI / build-sha-integrity (push) Failing after 4s
CI / validate-canonical-controls (push) Successful in 11s
CI / loc-budget (push) Successful in 13s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m21s
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 29s
CI / detect-changes (push) Successful in 8s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
Safetykon-Bug: 'Geschäftsführung:' (Sammelbegriff für GF einer GmbH)
matched das alte Pattern 'Geschäftsführer' nicht — False-Positive
IMPRESSUM-AGENT-VERTRETUNGSBERECHTIGTE_LABEL_KORREKT.
Pattern erweitert: Geschäftsführer|Geschäftsführung|Geschäftsführerin
+ Vorstand|Vorstandsvorsitzender + Inhaber|persönlich haftend.
Test test_safetykon_geschaeftsfuehrung_passes ergänzt (11/11 grün).
frontend: SlotCard zeigt jetzt Badge bei 0/0/0-Slots
('Dokument konnte nicht geladen werden') statt silent-fail, +
bei 0 Findings ein 'alle MCs OK'-Badge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
208 lines
6.9 KiB
Python
208 lines
6.9 KiB
Python
"""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)
|