Files
breakpilot-compliance/backend-compliance/tests/test_pattern_library.py
T
Benjamin Admin ca8c388f37
CI / detect-changes (push) Successful in 5s
CI / branch-name (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Failing after 4s
CI / validate-canonical-controls (push) Successful in 11s
CI / loc-budget (push) Successful in 14s
CI / go-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / test-go (push) Has been skipped
CI / nodejs-build (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 29s
CI / test-python-document-crawler (push) Has been skipped
feat(agents): Semantic-Validator + Auto-Learning-Pattern-Library
Sprint 1.10 — Semantic-Validator (User-Vorgabe 2026-06-09):
  - Statt unendlich Regex-Pattern fuer jede Schreibweise zu pflegen
    (Tel/Telefon/Telefonnr/Phone/Fon/Funkanschluss/…), nutzen wir
    bei MC-MISS einen LLM-Call: 'Ist die Pflichtangabe semantisch
    doch da, nur unter abweichendem Label?'
  - Bei LLM-Treffer: HIGH/MEDIUM-Finding wird zu LOW demoted,
    Empfehlung wird zu 'Best-Practice Umbenennung: Management ->
    Geschaeftsfuehrer' (mit STANDARD_LABELS-Mapping).
  - 1 LLM-Call pro Slot statt N: cost-effizient.

Sprint 1.11 — Auto-Learning-Pattern-Library:
  - Jedes Label das SVL findet wird in JSON persistiert:
    /tmp/breakpilot/agent_learned_patterns.json
  - Beim naechsten Run prueft der Agent zuerst gelernte Patterns
    BEVOR er das HIGH-Finding emittiert -> kein LLM-Call mehr.
  - Asymptotisch 0 LLM-Calls fuer haeufige Edge-Cases.
  - Halluzinations-Schutz: prune_low_confidence() loescht Patterns
    mit <0.5 Avg-Confidence nach 100 Beobachtungen.
  - Idempotent: gleicher (field_id, label, agent) -> Counter +1.

Tests: 40/40 gruen (10 Pattern-Library + 7 SVL + 13 GT + 11 v2).

STANDARD_LABELS-Map deckt Impressum + Cookie-Policy. Spaeter
erweiterbar fuer DSE, AGB, Widerrufs-Agenten.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 08:16:21 +02:00

109 lines
3.3 KiB
Python

"""Tests für die Auto-Learning-Pattern-Library."""
from __future__ import annotations
import json
import pytest
@pytest.fixture
def tmp_lib(tmp_path, monkeypatch):
p = tmp_path / "patterns.json"
monkeypatch.setenv("AGENT_PATTERN_LIBRARY", str(p))
import compliance.services.specialist_agents._pattern_library as lib
lib._invalidate_cache()
yield lib, p
lib._invalidate_cache()
def test_record_creates_file(tmp_lib):
lib, p = tmp_lib
assert not p.exists()
lib.record("kontakt_telefon", "Telefonnr.", 0.9, "impressum")
assert p.exists()
data = json.loads(p.read_text())
assert len(data["patterns"]) == 1
assert data["patterns"][0]["label_used"] == "Telefonnr."
assert data["patterns"][0]["observed_count"] == 1
def test_record_increments_existing(tmp_lib):
lib, _ = tmp_lib
lib.record("kontakt_telefon", "Telefonnr.", 0.9, "impressum")
lib.record("kontakt_telefon", "Telefonnr.", 0.85, "impressum")
lib.record("kontakt_telefon", "telefonnr.", 0.8, "impressum") # case-i
raws = lib.list_all()
assert len(raws) == 1
assert raws[0]["observed_count"] == 3
def test_record_separate_per_field_id(tmp_lib):
lib, _ = tmp_lib
lib.record("kontakt_telefon", "Tel", 0.9, "impressum")
lib.record("kontakt_email", "Tel", 0.9, "impressum")
assert len(lib.list_all()) == 2
def test_record_empty_inputs_noop(tmp_lib):
lib, p = tmp_lib
lib.record("", "Tel", 0.9, "impressum")
lib.record("kontakt_telefon", "", 0.9, "impressum")
lib.record("kontakt_telefon", "Tel", 0.9, "")
assert not p.exists()
def test_load_patterns_returns_compiled_regex(tmp_lib):
lib, _ = tmp_lib
lib.record("kontakt_telefon", "Telefonnr.", 0.9, "impressum")
pats = lib.load_patterns_for("kontakt_telefon", "impressum")
assert len(pats) == 1
m = pats[0].search("Hier: Telefonnr. 0761/12345")
assert m is not None
def test_load_patterns_filters_low_confidence(tmp_lib):
lib, _ = tmp_lib
lib.record("kontakt_telefon", "WeakLabel", 0.3, "impressum")
pats = lib.load_patterns_for(
"kontakt_telefon", "impressum", min_avg_confidence=0.5,
)
assert pats == []
# observed_count filter
pats = lib.load_patterns_for(
"kontakt_telefon", "impressum", min_observed=2,
)
assert pats == []
def test_label_to_regex_telefon():
from compliance.services.specialist_agents._pattern_library import (
_label_to_regex,
)
rx = _label_to_regex("Telefonnr.")
import re
assert re.search(rx, "Telefonnr. 0761/12345", re.I)
assert re.search(rx, "Telefonnr 0761", re.I)
def test_label_to_regex_email():
from compliance.services.specialist_agents._pattern_library import (
_label_to_regex,
)
rx = _label_to_regex("Mailadresse")
import re
assert re.search(rx, "Mailadresse: x@y.de", re.I)
def test_prune_low_confidence_keeps_recent(tmp_lib):
lib, _ = tmp_lib
lib.record("kontakt_telefon", "Tel", 0.9, "impressum")
pruned = lib.prune_low_confidence(min_runs_before_prune=100)
assert pruned == 0 # Nur einmal observed → noch nicht prunen
assert len(lib.list_all()) == 1
def test_load_patterns_for_nonexistent_returns_empty(tmp_lib):
lib, _ = tmp_lib
assert lib.load_patterns_for("ghost", "impressum") == []