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 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 PORT ?= 8080
HOST ?= 0.0.0.0 HOST ?= 0.0.0.0
.PHONY: ensure-env install run test docker-build .PHONY: ensure-env install run test smoke docker-build
ensure-env: ensure-env:
@if [ ! -f .env ] && [ -f .env.example ]; then \ @if [ ! -f .env ] && [ -f .env.example ]; then \
@ -24,5 +24,9 @@ run: ensure-env
test: ensure-env test: ensure-env
. .venv/bin/activate && pytest tests/ . .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:
docker build -t voice-assistant-gateway . docker build -t voice-assistant-gateway .

View file

@ -242,11 +242,18 @@ ein (Ebene zwischen Profil und Session). Fremde Sessions → `HTTP 403`.
## Tests ## Tests
```bash ```bash
make test # oder: pytest -q make test # oder: pytest -q (offline, mit Stubs)
``` ```
Abgedeckt: Config-Profile & Präzedenz, Route-Auflösung, Device Router, 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 ## 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())