test: manuelles End-to-End-Smoke-Skript gegen OpenRouter (scripts/smoke_e2e.py)

- prueft LLM, TTS und STT live (inkl. TTS->STT-Round-Trip); klare [OK]/[FAIL]-Ausgabe
- bewusst ausserhalb von tests/ (pytest sammelt es nicht ein); macht echte Netz-Aufrufe
- Makefile-Target `make smoke`; Doku in README + BEDIENUNGSANLEITUNG
- live ausgefuehrt: LLM/TTS/STT alle bestanden

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-17 09:49:04 +02:00
commit 7b1acd774d
4 changed files with 148 additions and 4 deletions

View file

@ -364,4 +364,13 @@ make test
```
Alle Tests sollten grün sein. Schlägt etwas fehl, gibt die Ausgabe den genauen
Testnamen und die Ursache an.
Testnamen und die Ursache an. Diese Tests laufen **offline** (mit Platzhaltern).
**Echte Funktion gegen OpenRouter prüfen** (LLM, TTS und STT live):
```bash
make smoke
```
Macht echte Cloud-Aufrufe (geringe Kosten) und meldet pro Modul `[OK]`/`[FAIL]`.
Braucht `OPENROUTER_API_KEY` in der Umgebung.

View file

@ -6,7 +6,7 @@ endif
PORT ?= 8080
HOST ?= 0.0.0.0
.PHONY: ensure-env install run test docker-build
.PHONY: ensure-env install run test smoke docker-build
ensure-env:
@if [ ! -f .env ] && [ -f .env.example ]; then \
@ -24,5 +24,9 @@ run: ensure-env
test: ensure-env
. .venv/bin/activate && pytest tests/
# Echter End-to-End-Test gegen OpenRouter (macht Netz-Aufrufe, kostet wenig).
smoke: ensure-env
. .venv/bin/activate && python scripts/smoke_e2e.py
docker-build:
docker build -t voice-assistant-gateway .

View file

@ -242,11 +242,18 @@ ein (Ebene zwischen Profil und Session). Fremde Sessions → `HTTP 403`.
## Tests
```bash
make test # oder: pytest -q
make test # oder: pytest -q (offline, mit Stubs)
```
Abgedeckt: Config-Profile & Präzedenz, Route-Auflösung, Device Router,
End-to-End (Loopback, 422-Fälle, Session-/Request-Override, `/api/config`).
Auth/Mandanten, Gedächtnis, Streaming, Resilienz, Quota/Notfall.
**Echter End-to-End-Test gegen OpenRouter** (Netz-Aufrufe, geringe Kosten — prüft
LLM, TTS und STT live, inkl. TTS→STT-Round-Trip):
```bash
make smoke # oder: python scripts/smoke_e2e.py
```
## Port ändern

124
scripts/smoke_e2e.py Normal file
View file

@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""End-to-End-Rauchtest gegen die echten Provider (OpenRouter).
ACHTUNG: macht echte Netz-Aufrufe und verursacht (geringe) Kosten. Bewusst NICHT
Teil der pytest-Suite. Manuell ausfuehren:
OPENROUTER_API_KEY=sk-or-... python scripts/smoke_e2e.py
# oder, wenn der Key in ~/.bashrc exportiert ist:
python scripts/smoke_e2e.py
Prueft drei Dinge isoliert gegen OpenRouter:
1. LLM (Chat-Antwort; TTS auf lokalen Stub, um den LLM zu isolieren)
2. TTS (Text -> Audio)
3. STT (Round-Trip: TTS-Audio als WAV zurueck durch die Transkription)
Exit-Code 0 = alles ok, 1 = mindestens ein Test fehlgeschlagen, 2 = kein API-Key.
"""
import io
import os
import sys
import tempfile
import wave
# Defaults setzen, BEVOR die App-Config geladen wird.
os.environ.setdefault("AUTH_ENABLED", "false")
os.environ.setdefault("DB_PATH", os.path.join(tempfile.mkdtemp(), "smoke.db"))
from fastapi.testclient import TestClient # noqa: E402
from app.config import settings # noqa: E402
from app.main import app # noqa: E402
OR = "openrouter"
def _check_key() -> None:
if not settings.openrouter_api_key.strip():
print("FEHLER: OPENROUTER_API_KEY ist nicht gesetzt (Umgebung).")
sys.exit(2)
def _pcm_to_wav(pcm: bytes, sample_rate: int = 24000) -> bytes:
buf = io.BytesIO()
with wave.open(buf, "wb") as w:
w.setnchannels(1)
w.setsampwidth(2)
w.setframerate(sample_rate)
w.writeframes(pcm)
return buf.getvalue()
def main() -> int:
_check_key()
print(f"Modelle: LLM={settings.openrouter_llm_model}, "
f"TTS={settings.openrouter_tts_model} ({settings.openrouter_tts_voice}), "
f"STT={settings.openrouter_stt_model}\n")
client = TestClient(app)
failures = 0
# 1) LLM (TTS via piper-Stub isoliert)
try:
r = client.post(
"/api/chat?debug=true",
json={"text": "Sag bitte in einem kurzen Satz Hallo.",
"llm_provider": OR, "tts_provider": "piper"},
)
answer = r.json().get("trace", {}).get("semantic_response") if r.status_code == 200 else None
if r.status_code == 200 and answer:
print(f"[OK] LLM -> {answer!r}")
else:
failures += 1
print(f"[FAIL] LLM -> HTTP {r.status_code}: {r.text[:200]}")
except Exception as exc: # noqa: BLE001
failures += 1
print(f"[FAIL] LLM -> {exc}")
# 2) TTS
pcm = b""
try:
r = client.post("/api/speak", json={"text": "Guten Tag, schoen dass Sie da sind.",
"tts_provider": OR})
if r.status_code == 200 and r.content:
pcm = r.content
print(f"[OK] TTS -> {len(pcm)} Bytes Audio")
else:
failures += 1
print(f"[FAIL] TTS -> HTTP {r.status_code}: {r.text[:200]}")
except Exception as exc: # noqa: BLE001
failures += 1
print(f"[FAIL] TTS -> {exc}")
# 3) STT (Round-Trip ueber das TTS-Audio)
if pcm:
try:
wav = _pcm_to_wav(pcm)
r = client.post(
"/api/transcribe",
files={"file": ("roundtrip.wav", wav, "audio/wav")},
data={"language": "de", "stt_provider": OR},
)
text = r.json().get("trace", {}).get("raw_transcript") if r.status_code == 200 else None
if r.status_code == 200 and text:
print(f"[OK] STT -> {text!r}")
else:
failures += 1
print(f"[FAIL] STT -> HTTP {r.status_code}: {r.text[:200]}")
except Exception as exc: # noqa: BLE001
failures += 1
print(f"[FAIL] STT -> {exc}")
else:
print("[SKIP] STT -> kein TTS-Audio fuer den Round-Trip")
print()
if failures:
print(f"ERGEBNIS: {failures} Test(s) fehlgeschlagen.")
return 1
print("ERGEBNIS: alle Live-Tests bestanden.")
return 0
if __name__ == "__main__":
raise SystemExit(main())