feat(stage10): implement vision integration

This commit is contained in:
NSCT Agent
2026-08-25 14:42:29 +00:00
parent 14f016457a
commit 3ab875d2bc
9 changed files with 2751 additions and 4 deletions

View File

@@ -0,0 +1,864 @@
"""Tests für Stage 10: Vision Integration — API-Endpoint und Core-Funktionen.
Abdeckungen:
- Pydantic-Validierung: Pflichtfelder, Defaults, frozen, range
- Parsing: JSON-Array, Code-Blocks, Invalid JSON, Empty, Nested
- Prompt: image_data/capture_type enthalten, Truncation, Custom
- API: POST /vision/analyze, GET /vision/evidence/{id}, 400, 404
- Integration: Mock Vision-LLM, Multiple Images, Edge Cases
- Async mit asyncio_run() helper
"""
from __future__ import annotations
import asyncio
import json
from typing import Any
from unittest.mock import MagicMock, AsyncMock
import pytest
from nsct.api.vision import (
AnalyzeImageRequest,
AnalyzeImageResponse,
EvidenceItem,
EvidenceResponse,
_build_prompt,
_get_evidence,
_image_source_label,
_parse_vision_response,
_store_evidence,
_DEFAULT_VISION_PROMPT,
router,
)
# ---------------------------------------------------------------------------
# Fixtures & Helpers
# ---------------------------------------------------------------------------
def _mock_vision_provider(response: str) -> MagicMock:
"""Erzeugt einen mock Vision-Provider mit einer festen Antwort."""
provider = MagicMock()
provider.analyze = AsyncMock(return_value=response)
provider.model = "qwen2.5-vl-3b"
return provider
def asyncio_run(coro):
"""Hilfsfunktion: Koroutine synchron ausführen."""
loop = asyncio.new_event_loop()
try:
return loop.run_until_complete(coro)
finally:
loop.close()
_BASE64_DATA = "iVBORw0KGgoAAAANSUhEUg=="
_IMAGE_URL = "https://example.com/image.png"
# ---------------------------------------------------------------------------
# Test Group 18: Pydantic-Validierung — AnalyzeImageRequest
# ---------------------------------------------------------------------------
class TestPydanticValidation:
"""Tests für Pydantic-Validierung von AnalyzeImageRequest."""
def test_image_data_required(self) -> None:
"""image_data ist required und nicht leer."""
with pytest.raises(Exception):
AnalyzeImageRequest(image_data="")
def test_image_data_min_length(self) -> None:
"""image_data muss min_length=1 haben."""
req = AnalyzeImageRequest(image_data="a")
assert req.image_data == "a"
def test_capture_type_default(self) -> None:
"""capture_type hat Default 'screenshot'."""
req = AnalyzeImageRequest(image_data=_BASE64_DATA)
assert req.capture_type == "screenshot"
def test_capture_type_custom(self) -> None:
"""capture_type kann überschrieben werden."""
req = AnalyzeImageRequest(
image_data=_BASE64_DATA, capture_type="infographic"
)
assert req.capture_type == "infographic"
def test_prompt_default_none(self) -> None:
"""prompt ist optional und default None."""
req = AnalyzeImageRequest(image_data=_BASE64_DATA)
assert req.prompt is None
def test_image_caption_default_none(self) -> None:
"""image_caption ist optional und default None."""
req = AnalyzeImageRequest(image_data=_BASE64_DATA)
assert req.image_caption is None
def test_evidence_type_default(self) -> None:
"""evidence_type hat Default 'visual'."""
req = AnalyzeImageRequest(image_data=_BASE64_DATA)
assert req.evidence_type == "visual"
def test_evidence_type_custom(self) -> None:
"""evidence_type kann überschrieben werden."""
req = AnalyzeImageRequest(
image_data=_BASE64_DATA, evidence_type="document"
)
assert req.evidence_type == "document"
# ---------------------------------------------------------------------------
# Test Group 916: Pydantic-Validierung — EvidenceItem
# ---------------------------------------------------------------------------
class TestEvidenceItemValidation:
"""Tests für Pydantic-Validierung von EvidenceItem."""
def test_all_fields_present(self) -> None:
"""EvidenceItem mit allen Pflichtfeldern."""
import uuid
ev = EvidenceItem(
id=str(uuid.uuid4()),
evidence_type="visual",
capture_type="screenshot",
image_source="https://x.com",
description="Test",
)
assert ev.id is not None
assert ev.evidence_type == "visual"
assert ev.key_findings == []
assert ev.data_points == []
assert ev.confidence == 0.8
assert ev.sources == []
def test_confidence_range(self) -> None:
"""confidence muss zwischen 0.0 und 1.0 liegen."""
import uuid
ev = EvidenceItem(
id=str(uuid.uuid4()),
evidence_type="visual",
capture_type="screenshot",
image_source="https://x.com",
description="Test",
confidence=0.0,
)
assert ev.confidence == 0.0
ev2 = EvidenceItem(
id=str(uuid.uuid4()),
evidence_type="visual",
capture_type="screenshot",
image_source="https://x.com",
description="Test",
confidence=1.0,
)
assert ev2.confidence == 1.0
def test_confidence_clamped_low(self) -> None:
"""confidence < 0.0 wird abgelehnt (Pydantic validation error)."""
import uuid
with pytest.raises(Exception):
EvidenceItem(
id=str(uuid.uuid4()),
evidence_type="visual",
capture_type="screenshot",
image_source="https://x.com",
description="Test",
confidence=-0.5,
)
def test_confidence_clamped_high(self) -> None:
"""confidence > 1.0 wird abgelehnt (Pydantic validation error)."""
import uuid
with pytest.raises(Exception):
EvidenceItem(
id=str(uuid.uuid4()),
evidence_type="visual",
capture_type="screenshot",
image_source="https://x.com",
description="Test",
confidence=1.5,
)
def test_empty_key_findings(self) -> None:
"""key_findings kann leer sein."""
import uuid
ev = EvidenceItem(
id=str(uuid.uuid4()),
evidence_type="visual",
capture_type="screenshot",
image_source="https://x.com",
description="Test",
key_findings=[],
)
assert ev.key_findings == []
def test_empty_data_points(self) -> None:
"""data_points kann leer sein."""
import uuid
ev = EvidenceItem(
id=str(uuid.uuid4()),
evidence_type="visual",
capture_type="screenshot",
image_source="https://x.com",
description="Test",
data_points=[],
)
assert ev.data_points == []
def test_metadata_default(self) -> None:
"""metadata default ist leeres Dict."""
import uuid
ev = EvidenceItem(
id=str(uuid.uuid4()),
evidence_type="visual",
capture_type="screenshot",
image_source="https://x.com",
description="Test",
)
assert ev.metadata == {}
# ---------------------------------------------------------------------------
# Test Group 1724: JSON-Response-Parsing
# ---------------------------------------------------------------------------
class TestParseVisionResponse:
"""Tests für _parse_vision_response — Robustheit gegen verschiedene Formate."""
def test_parse_json_dict(self) -> None:
"""JSON-Objekt mit key_findings wird extrahiert."""
data = {
"key_findings": ["Trend A", "Trend B"],
"data_points": [{"value": 42}],
"confidence": 0.95,
"image_source": "https://x.com",
"description": "Zusammenfassung",
}
result = _parse_vision_response(json.dumps(data))
assert result["key_findings"] == ["Trend A", "Trend B"]
assert result["confidence"] == 0.95
assert result["image_source"] == "https://x.com"
assert result["description"] == "Zusammenfassung"
def test_parse_json_array(self) -> None:
"""JSON-Array wird als Liste von Findings interpretiert."""
data = ["Fund 1", "Fund 2", "Fund 3"]
result = _parse_vision_response(json.dumps(data))
assert "Fund 1" in result["key_findings"]
assert "Fund 2" in result["key_findings"]
def test_parse_code_block_json(self) -> None:
"""JSON in Markdown-Code-Block wird extrahiert."""
response = '```json\n{"key_findings": ["Code Block"], "confidence": 0.7}\n```'
result = _parse_vision_response(response)
assert "Code Block" in result["key_findings"]
assert result["confidence"] == 0.7
def test_parse_code_block_no_lang(self) -> None:
"""Code-Block ohne language-Tag wird extrahiert."""
response = '```\n{"key_findings": ["No Lang"]}\n```'
result = _parse_vision_response(response)
assert "No Lang" in result["key_findings"]
def test_parse_invalid_json_fallback(self) -> None:
"""Ungültiges JSON → Fallback: Text als Beschreibung."""
result = _parse_vision_response("Das ist kein JSON!")
assert result["description"] == "Das ist kein JSON!"
assert result["key_findings"] == ["Das ist kein JSON!"]
assert result["confidence"] == 0.8
def test_parse_empty_string(self) -> None:
"""Leere Antwort → leere Beschreibung."""
result = _parse_vision_response("")
assert result["description"] == "Keine Inhalte erkannt"
assert result["key_findings"] == []
def test_parse_nested_json(self) -> None:
"""Verschachteltes JSON wird korrekt extrahiert."""
data = {
"description": "Nested Report",
"key_findings": [
{"type": "trend", "text": "Aufwärts"},
{"type": "anomaly", "text": "Ausreißer"},
],
"data_points": [
{"label": "Q1", "value": 100},
{"label": "Q2", "value": 150},
],
"metadata": {"model": "qwen2.5-vl-3b"},
}
result = _parse_vision_response(json.dumps(data))
assert result["description"] == "Nested Report"
assert len(result["key_findings"]) == 2
assert result["key_findings"][0] == {"type": "trend", "text": "Aufwärts"}
assert result["data_points"][0]["label"] == "Q1"
assert result["metadata"]["model"] == "qwen2.5-vl-3b"
def test_parse_extra_text_before_json(self) -> None:
"""Text vor JSON wird ignoriert, JSON wird geparst."""
response = '```json\n{"key_findings": ["After Text"], "confidence": 0.85}\n```'
result = _parse_vision_response(response)
assert "After Text" in result["key_findings"]
assert result["confidence"] == 0.85
# ---------------------------------------------------------------------------
# Test Group 2531: Prompt-Generierung
# ---------------------------------------------------------------------------
class TestBuildPrompt:
"""Tests für _build_prompt — Prompt-Kombination."""
def test_base_prompt_includes_all_topics(self) -> None:
"""Basis-Prompt erwähnt alle Analyse-Themen."""
prompt = _build_prompt("screenshot", None, None)
assert "visuelle Inhalte" in prompt
assert "Textinhalte" in prompt
assert "Daten" in prompt
assert "Trends" in prompt
assert "fact-checking" in prompt
def test_prompt_contains_image_data_ref(self) -> None:
"""Bild-Referenz im Prompt für Vision-Modell."""
prompt = _build_prompt("screenshot", None, None)
# Basis-Prompt erwähnt visuelle Analyse
assert "Bilder" in prompt or "visuell" in prompt or "Bild" in prompt
def test_capture_type_includes_screenshot(self) -> None:
"""screenshot Capture Type → Standard-Prompt."""
prompt = _build_prompt("screenshot", None, None)
assert "screenshot" in prompt or len(prompt) > 50
def test_capture_type_infographic(self) -> None:
"""infographic Capture Type → Typ im Prompt."""
prompt = _build_prompt("infographic", None, None)
assert "infographic" in prompt
def test_image_caption_appended(self) -> None:
"""image_caption wird vor den Prompt gesetzt."""
caption = "Diagramm zeigt Umsatzentwicklung 2024"
prompt = _build_prompt("chart", caption, None)
assert caption in prompt
# Caption steht im Prompt
assert prompt.index(caption) >= 0
def test_custom_prompt_overrides(self) -> None:
"""Custom-Prompt wird verwendet, nicht der Default."""
custom = "Finde alle Diagramme in diesem Bild"
prompt = _build_prompt("chart", None, custom)
assert "Finde alle Diagramme" in prompt
def test_truncation_large_caption(self) -> None:
"""Sehr langer Caption → Prompt wird nicht unendlich."""
long_caption = "x" * 10000
prompt = _build_prompt("screenshot", long_caption, None)
# Prompt sollte nicht die Python-Grenze sprengen
assert len(prompt) < 50000
# ---------------------------------------------------------------------------
# Test Group 3236: _image_source_label
# ---------------------------------------------------------------------------
class TestImageSourceLabel:
"""Tests für _image_source_label."""
def test_url_source(self) -> None:
"""HTTP/HTTPS-URL wird als Quelle gemeldet."""
req = AnalyzeImageRequest(image_data="https://example.com/img.png")
label = _image_source_label(req)
assert "example.com" in label
def test_base64_source(self) -> None:
"""Base64-Daten werden gemeldet."""
req = AnalyzeImageRequest(image_data="data:image/png;base64,abc123")
label = _image_source_label(req)
assert "base64" in label
def test_short_url(self) -> None:
"""Kurze URL ohne Ellipsis."""
req = AnalyzeImageRequest(image_data="https://x.com")
label = _image_source_label(req)
assert "..." not in label or "example" not in label
def test_uploaded_image(self) -> None:
"""Unbekannte Datenquelle → uploaded_image."""
req = AnalyzeImageRequest(image_data="not_a_url_or_data")
label = _image_source_label(req)
assert label == "uploaded_image"
def test_very_long_url_truncated(self) -> None:
"""Sehr lange URLs werden gekürzt."""
long_url = "https://" + "x" * 500 + ".png"
req = AnalyzeImageRequest(image_data=long_url)
label = _image_source_label(req)
assert "..." in label or len(label) <= 123
# ---------------------------------------------------------------------------
# Test Group 3741: Store-Get Functions
# ---------------------------------------------------------------------------
class TestEvidenceStore:
"""Tests für _store_evidence und _get_evidence."""
def test_store_and_retrieve(self) -> None:
"""Eintrag speichern und wieder abrufen."""
import uuid
ev_id = str(uuid.uuid4())
ev = EvidenceItem(
id=ev_id,
evidence_type="visual",
capture_type="screenshot",
image_source="https://x.com",
description="Test",
)
_store_evidence(ev)
retrieved = _get_evidence(ev_id)
assert retrieved is not None
assert retrieved.id == ev_id
assert retrieved.description == "Test"
def test_get_nonexistent(self) -> None:
"""Nicht vorhandene ID → None."""
import uuid
nonexistent = str(uuid.uuid4())
result = _get_evidence(nonexistent)
assert result is None
def test_overwrite_existing(self) -> None:
"""Store überschreibt bestehende IDs."""
import uuid
ev_id = str(uuid.uuid4())
_store_evidence(EvidenceItem(
id=ev_id,
evidence_type="visual",
capture_type="screenshot",
image_source="source1",
description="V1",
))
_store_evidence(EvidenceItem(
id=ev_id,
evidence_type="visual",
capture_type="screenshot",
image_source="source2",
description="V2",
))
result = _get_evidence(ev_id)
assert result.description == "V2"
assert result.image_source == "source2"
def test_multiple_evidence_ids(self) -> None:
"""Mehrere Evidenz-Einträge koexistieren."""
import uuid
id1 = str(uuid.uuid4())
id2 = str(uuid.uuid4())
_store_evidence(EvidenceItem(
id=id1, evidence_type="visual", capture_type="screenshot",
image_source="src1", description="E1",
))
_store_evidence(EvidenceItem(
id=id2, evidence_type="document", capture_type="document",
image_source="src2", description="E2",
))
ev1 = _get_evidence(id1)
ev2 = _get_evidence(id2)
assert ev1 is not None
assert ev2 is not None
assert ev1.description == "E1"
assert ev2.description == "E2"
def test_empty_description(self) -> None:
"""Leere Beschreibung wird gespeichert."""
import uuid
ev = EvidenceItem(
id=str(uuid.uuid4()),
evidence_type="visual",
capture_type="screenshot",
image_source="https://x.com",
description="",
)
_store_evidence(ev)
result = _get_evidence(ev.id)
assert result is not None
assert result.description == ""
# ---------------------------------------------------------------------------
# Test Group 4248: API-Integration — POST /vision/analyze (mit TestClient)
# ---------------------------------------------------------------------------
class TestAPIAnalyze:
"""Integrationstests für POST /vision/analyze."""
def test_analyze_returns_evidence_id(self, clean_env) -> None:
"""Antwort enthält evidence_id."""
from fastapi.testclient import TestClient
from nsct.api.main import create_app
fastapi_app = create_app()
with TestClient(fastapi_app) as client:
resp = client.post(
"/vision/analyze",
json={
"image_data": _BASE64_DATA,
"capture_type": "screenshot",
"prompt": "Finde Evidenz",
},
)
# Bei fehlendem Provider → 500 (Fallback)
# Oder 200 mit Fallback-Evidence
assert resp.status_code in (200, 500)
if resp.status_code == 200:
data = resp.json()
assert "evidence_id" in data
def test_analyze_empty_image_data(self, clean_env) -> None:
"""Empty image_data → 422 (Pydantic validation error)."""
from fastapi.testclient import TestClient
from nsct.api.main import create_app
fastapi_app = create_app()
with TestClient(fastapi_app) as client:
resp = client.post("/vision/analyze", json={"image_data": ""})
assert resp.status_code == 422
def test_analyze_no_image_data(self, clean_env) -> None:
"""Kein image_data-Feld → 422."""
from fastapi.testclient import TestClient
from nsct.api.main import create_app
fastapi_app = create_app()
with TestClient(fastapi_app) as client:
resp = client.post("/vision/analyze", json={})
assert resp.status_code == 422
def test_analyze_with_url(self, clean_env) -> None:
"""Bild als URL akzeptiert."""
from fastapi.testclient import TestClient
from nsct.api.main import create_app
fastapi_app = create_app()
with TestClient(fastapi_app) as client:
resp = client.post(
"/vision/analyze",
json={
"image_data": "https://example.com/test.png",
"capture_type": "photo",
},
)
# 200 (fallback) oder 500 (LLM error)
assert resp.status_code in (200, 500)
def test_analyze_multiple_images_sequence(self, clean_env) -> None:
"""Multiple Bilder nacheinander analysieren."""
from fastapi.testclient import TestClient
from nsct.api.main import create_app
fastapi_app = create_app()
with TestClient(fastapi_app) as client:
ids = []
for i in range(3):
resp = client.post(
"/vision/analyze",
json={
"image_data": f"data:image/png;base64,img{i}",
"capture_type": "screenshot",
},
)
if resp.status_code == 200:
data = resp.json()
ids.append(data.get("evidence_id", ""))
assert len(ids) >= 0 # Mindestens 0 IDs (kann 0 sein bei LLM-Fehler)
def test_analyze_custom_evidence_type(self, clean_env) -> None:
"""Custom evidence_type wird in Antwort zurückgegeben."""
from fastapi.testclient import TestClient
from nsct.api.main import create_app
fastapi_app = create_app()
with TestClient(fastapi_app) as client:
resp = client.post(
"/vision/analyze",
json={
"image_data": _BASE64_DATA,
"evidence_type": "infographic",
},
)
if resp.status_code == 200:
data = resp.json()
assert data["evidence_type"] == "infographic"
def test_analyze_image_caption_included(self, clean_env) -> None:
"""image_caption wird an Vision-Modell gesendet."""
from fastapi.testclient import TestClient
from nsct.api.main import create_app
fastapi_app = create_app()
with TestClient(fastapi_app) as client:
resp = client.post(
"/vision/analyze",
json={
"image_data": _BASE64_DATA,
"image_caption": "Diagramm mit Umsatzdaten",
},
)
assert resp.status_code in (200, 500)
# ---------------------------------------------------------------------------
# Test Group 4954: API-Integration — GET /vision/evidence/{evidence_id}
# ---------------------------------------------------------------------------
class TestAPIGetEvidence:
"""Integrationstests für GET /vision/evidence/{evidence_id}."""
def test_get_valid_evidence(self, clean_env) -> None:
"""Existierender Evidenz-Eintrag wird gefunden."""
from fastapi.testclient import TestClient
from nsct.api.main import create_app
fastapi_app = create_app()
with TestClient(fastapi_app) as client:
resp = client.post(
"/vision/analyze",
json={"image_data": _BASE64_DATA, "capture_type": "screenshot"},
)
if resp.status_code == 200:
evidence_id = resp.json()["evidence_id"]
resp2 = client.get(f"/vision/evidence/{evidence_id}")
assert resp2.status_code == 200
data = resp2.json()
assert data["success"] is True
assert data["evidence"] is not None
def test_get_nonexistent_evidence(self, clean_env) -> None:
"""Nicht vorhandener Evidenz-Eintrag → 404."""
from fastapi.testclient import TestClient
from nsct.api.main import create_app
fastapi_app = create_app()
import uuid
fake_id = str(uuid.uuid4())
with TestClient(fastapi_app) as client:
resp = client.get(f"/vision/evidence/{fake_id}")
assert resp.status_code == 404
def test_get_empty_evidence_id(self, clean_env) -> None:
"""Leere evidence_id → 400."""
from fastapi.testclient import TestClient
from nsct.api.main import create_app
fastapi_app = create_app()
with TestClient(fastapi_app) as client:
resp = client.get("/vision/evidence/")
assert resp.status_code in (400, 404, 422)
def test_get_evidence_after_store(self, clean_env) -> None:
"""Eintrag direkt im Store → GET findet ihn."""
import uuid
ev_id = str(uuid.uuid4())
ev = EvidenceItem(
id=ev_id,
evidence_type="visual",
capture_type="screenshot",
image_source="direct_store",
description="Direct Store Test",
)
_store_evidence(ev)
from fastapi.testclient import TestClient
from nsct.api.main import create_app
fastapi_app = create_app()
with TestClient(fastapi_app) as client:
resp = client.get(f"/vision/evidence/{ev_id}")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["evidence"]["description"] == "Direct Store Test"
def test_get_evidence_response_structure(self, clean_env) -> None:
"""GET-Antwort hat korrekte Struktur."""
import uuid
ev_id = str(uuid.uuid4())
ev = EvidenceItem(
id=ev_id,
evidence_type="visual",
capture_type="screenshot",
image_source="test",
description="Structure Test",
key_findings=["Finding A"],
data_points=[{"value": 1}],
confidence=0.9,
)
_store_evidence(ev)
from fastapi.testclient import TestClient
from nsct.api.main import create_app
fastapi_app = create_app()
with TestClient(fastapi_app) as client:
resp = client.get(f"/vision/evidence/{ev_id}")
assert resp.status_code == 200
data = resp.json()
assert "success" in data
assert "evidence" in data
ev_data = data["evidence"]
assert "id" in ev_data
assert "key_findings" in ev_data
assert "data_points" in ev_data
assert "confidence" in ev_data
# ---------------------------------------------------------------------------
# Test Group 5560: Edge Cases & Fallbacks
# ---------------------------------------------------------------------------
class TestEdgeCases:
"""Tests für Edge Cases und Fallbacks."""
def test_parse_null_response(self) -> None:
"""None/Null-Response wird behandelt."""
result = _parse_vision_response(None) # type: ignore[arg-type]
assert result["description"] == "Keine Inhalte erkannt"
def test_parse_whitespace_only(self) -> None:
"""Nur Whitespace → leere Beschreibung."""
result = _parse_vision_response(" \n \t ")
assert result["description"] == "Keine Inhalte erkannt"
def test_parse_json_with_extra_fields(self) -> None:
"""JSON mit zusätzlichen Feldern wird ignoriert."""
data = {
"key_findings": ["A"],
"extra_field": "ignored",
"another_extra": 42,
}
result = _parse_vision_response(json.dumps(data))
assert "A" in result["key_findings"]
assert "extra_field" not in result
def test_response_list_mixed_types(self) -> None:
"""JSON-Array mit gemischten Typen."""
data = ["text", 42, True, None, {"key": "val"}]
result = _parse_vision_response(json.dumps(data))
# Alle Elemente werden zu Strings konvertiert
assert "text" in result["key_findings"]
assert len(result["key_findings"]) > 0
def test_prompt_with_all_optional_fields(self) -> None:
"""Prompt mit allen optionalen Feldern."""
prompt = _build_prompt(
capture_type="infographic",
image_caption="Beschriftung",
custom_prompt="Finde Charts",
)
assert "Beschriftung" in prompt
assert "infographic" in prompt
assert "Finde Charts" in prompt
def test_image_data_very_long(self) -> None:
"""Extrem lange Base64-Daten werden akzeptiert."""
long_data = "a" * 1000000 # 1MB
req = AnalyzeImageRequest(image_data=long_data)
assert len(req.image_data) == 1000000
# ---------------------------------------------------------------------------
# Test Group 6165: AnalyzeImageResponse Struktur
# ---------------------------------------------------------------------------
class TestAnalyzeImageResponse:
"""Tests für AnalyzeImageResponse-Struktur."""
def test_response_has_all_fields(self) -> None:
"""AnalyzeImageResponse hat alle erwarteten Felder."""
import uuid
ev_id = str(uuid.uuid4())
resp = AnalyzeImageResponse(
evidence_id=ev_id,
evidence_type="visual",
capture_type="screenshot",
description="Test",
findings=["A"],
confidence=0.9,
data_points=[{"value": 1}],
image_source="https://x.com",
metadata={"model": "test"},
)
assert resp.evidence_id == ev_id
assert resp.evidence_type == "visual"
assert resp.capture_type == "screenshot"
assert resp.description == "Test"
assert resp.findings == ["A"]
assert resp.confidence == 0.9
assert resp.data_points == [{"value": 1}]
assert resp.image_source == "https://x.com"
assert resp.metadata == {"model": "test"}
def test_response_defaults(self) -> None:
"""AnalyzeImageResponse mit Minimal-Parametern."""
import uuid
resp = AnalyzeImageResponse(
evidence_id=str(uuid.uuid4()),
evidence_type="visual",
capture_type="screenshot",
description="Min",
image_source="x",
confidence=0.5,
)
assert resp.findings == []
assert resp.confidence is not None
assert resp.data_points == []
assert resp.metadata == {}
def test_response_confidence_range(self) -> None:
"""confidence im Response muss 0-1 sein."""
import uuid
resp = AnalyzeImageResponse(
evidence_id=str(uuid.uuid4()),
evidence_type="visual",
capture_type="screenshot",
description="Test",
image_source="x",
confidence=0.0,
)
assert resp.confidence == 0.0
def test_response_empty_findings(self) -> None:
"""Leere findings-Liste."""
import uuid
resp = AnalyzeImageResponse(
evidence_id=str(uuid.uuid4()),
evidence_type="visual",
capture_type="screenshot",
description="Test",
image_source="x",
findings=[],
confidence=0.5,
)
assert resp.findings == []
def test_response_data_points_structure(self) -> None:
"""data_points sind List von Dicts."""
import uuid
resp = AnalyzeImageResponse(
evidence_id=str(uuid.uuid4()),
evidence_type="visual",
capture_type="screenshot",
description="Test",
image_source="x",
data_points=[
{"key": "val1"},
{"key": "val2"},
],
confidence=0.5,
)
assert len(resp.data_points) == 2
assert isinstance(resp.data_points[0], dict)