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:
parent
1d338810b1
commit
7b1acd774d
4 changed files with 148 additions and 4 deletions
|
|
@ -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.
|
||||
|
|
|
|||
6
Makefile
6
Makefile
|
|
@ -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 .
|
||||
|
|
|
|||
11
README.md
11
README.md
|
|
@ -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
124
scripts/smoke_e2e.py
Normal 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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue