feat: check-assets ↔ KI gekoppelt — byte-getauschte Bilder neu bewerten

Da bekannte Bilder aus Effizienzgründen nicht jeden Scan neu geladen werden
(url_hashes), prüfte die KI einen späteren Byte-Tausch eines bekannten Bildes
nicht von selbst erneut. Lücke geschlossen: Erkennt die Datei-Prüfung
(check-assets) eine geänderte Bilddatei, wird deren URL über
BaselineManager.invalidate_ai_url_hashes aus der Hash-Erinnerung entfernt →
nächster Scan lädt das Bild neu, neue Bytes = neuer Hash = Cache-Miss = die KI
bewertet den Inhalt neu.

Einhängung in cmd_scan (vor run_ai_analysis → Neubewertung noch im selben Scan)
und cmd_check_assets. Integritäts- und Inhaltsprüfung greifen so ineinander.

Tests: 260 grün (+2: invalidate entfernt nur die genannten URLs; geändertes
Bild wird nach Invalidierung neu geladen und beanstandet).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-13 04:22:38 +02:00
commit 78dc79ceab
4 changed files with 80 additions and 2 deletions

View file

@ -287,6 +287,49 @@ class TestImages:
m2.assert_not_called()
# ---------------------------------------------------------------------------
# check-assets ↔ KI-Kopplung: geänderte Bilder werden neu bewertet
# ---------------------------------------------------------------------------
class TestAssetAiCoupling:
def _cfg_img(self):
cfg = _cfg()
cfg["ai_analysis"]["text"]["enabled"] = False
cfg["ai_analysis"]["image"]["enabled"] = True
return cfg
def test_invalidate_removes_only_given_urls(self, tmp_path):
bm = BaselineManager(tmp_path)
bm.save_ai_ledger({"entries": {}, "url_hashes": {"a": "h1", "b": "h2", "c": "h3"}})
assert bm.invalidate_ai_url_hashes(["b", "x"]) == 1 # x ist nicht vorhanden
assert set(bm.load_ai_ledger()["url_hashes"]) == {"a", "c"}
def test_changed_image_reanalyzed_after_invalidation(self, tmp_path, monkeypatch):
"""Byte-Tausch eines bekannten Bildes → invalidieren → KI lädt+bewertet neu."""
monkeypatch.setenv("OPENROUTER_API_KEY", "test")
bm = BaselineManager(tmp_path)
cfg = self._cfg_img()
snap = _snap("kurz", img=[{"url": "https://x.de/logo.png", "class": "internal"}])
with patch("scanner.ai_analyzer.fetch_asset_hashes",
return_value={"https://x.de/logo.png": {"sha256": "OLD", "size": 1, "error": None}}), \
patch("scanner.ai_analyzer._openrouter_chat", return_value=_verdict()):
run_ai_analysis(cfg, bm, snap, {})
# check-assets erkennt Byte-Änderung → Kopplung invalidiert die URL
assert bm.invalidate_ai_url_hashes(["https://x.de/logo.png"]) == 1
new = _verdict("pornography", "high", 0.95)
with patch("scanner.ai_analyzer.fetch_asset_hashes",
return_value={"https://x.de/logo.png": {"sha256": "NEW", "size": 1, "error": None}}) as f2, \
patch("scanner.ai_analyzer._openrouter_chat", return_value=new) as c2:
res = run_ai_analysis(cfg, bm, snap, {})
f2.assert_called() # neu geladen (URL war invalidiert)
c2.assert_called() # neue Bytes (NEW) → Cache-Miss → neu analysiert
assert res["api_calls"] == 1
assert len(res["findings"]) == 1 # jetzt beanstandet
# ---------------------------------------------------------------------------
# Scoring: cap at yellow
# ---------------------------------------------------------------------------