775 lines
36 KiB
Bash
Executable File
775 lines
36 KiB
Bash
Executable File
#!/bin/bash
|
||
# Lokaler Test des AI Profile Router (Mock-llama.cpp + Fake-Profil-Skript).
|
||
# Startet Mock + Router, führt alle Testfälle aus, beendet beide.
|
||
set -uo pipefail
|
||
cd "$(dirname "$0")/.."
|
||
|
||
UP_PORT="${UP_PORT:-18080}"
|
||
RT_PORT="${RT_PORT:-18081}"
|
||
TTS_PORT="${TTS_PORT:-18082}"
|
||
STT_PORT="${STT_PORT:-18083}"
|
||
BASE="http://127.0.0.1:$RT_PORT"
|
||
FAKE_DIR="$PWD/dev/fake-profile-dir"
|
||
TEST_ROUTER_KEY="test-router-key-0123456789-abcdefghijklmnopqrstuvwxyz"
|
||
PASS=0
|
||
FAIL=0
|
||
|
||
# Produktive Authentifizierung für alle Integrationstests. `command curl`
|
||
# umgeht diese Funktion bei den gezielten anonymen Negativtests.
|
||
curl() { command curl -H "Authorization: Bearer $TEST_ROUTER_KEY" "$@"; }
|
||
|
||
cleanup() {
|
||
CURRENT_MOCK_PID="$(cat /tmp/mock_upstream_pid 2>/dev/null || true)"
|
||
kill "${MOCK_PID:-}" "${CURRENT_MOCK_PID:-}" "${ROUTER_PID:-}" \
|
||
"${TTS_PID:-}" "${STT_PID:-}" 2>/dev/null || true
|
||
rm -f /tmp/mock_pid2 /tmp/mock_upstream_pid /tmp/test-router-state.json \
|
||
/tmp/fake-profile-fail
|
||
cp "$FAKE_DIR/profile-fast.conf.disabled" "$FAKE_DIR/override.conf"
|
||
wait 2>/dev/null || true
|
||
}
|
||
trap cleanup EXIT
|
||
|
||
ok() { echo " PASS: $1"; PASS=$((PASS+1)); }
|
||
bad() { echo " FAIL: $1"; FAIL=$((FAIL+1)); }
|
||
|
||
wait_http() {
|
||
local url="$1" name="$2"
|
||
for _ in $(seq 1 50); do
|
||
curl -sf "$url" >/dev/null 2>&1 && return 0
|
||
sleep 0.1
|
||
done
|
||
echo "FEHLER: $name wurde nicht bereit: $url" >&2
|
||
return 1
|
||
}
|
||
|
||
# --- Mock-llama.cpp starten (über Fake-systemctl) ------------------------------
|
||
echo "== Starte Mock-llama.cpp (Port $UP_PORT)"
|
||
cp "$FAKE_DIR/profile-fast.conf.disabled" "$FAKE_DIR/override.conf"
|
||
rm -f /tmp/test-router-state.json /tmp/mock_upstream_pid
|
||
FAKE_SYSTEMD_PIDFILE=/tmp/mock_upstream_pid \
|
||
FAKE_SYSTEMD_PORT="$UP_PORT" \
|
||
FAKE_SYSTEMD_PROFILE_DIR="$FAKE_DIR" \
|
||
FAKE_SYSTEMD_MOCK="$PWD/dev/mock_upstream.py" \
|
||
FAKE_SYSTEMD_LOG=/tmp/mock_upstream.log \
|
||
bash dev/fake-systemctl.sh start
|
||
sleep 0.5
|
||
MOCK_PID=$(cat /tmp/mock_upstream_pid 2>/dev/null || echo "")
|
||
wait_http "http://127.0.0.1:$UP_PORT/health" "Mock-llama.cpp" || exit 1
|
||
|
||
# --- Router starten -----------------------------------------------------------
|
||
echo "== Starte Router (Port $RT_PORT)"
|
||
rm -rf /tmp/test-images
|
||
ROUTER_HOST=127.0.0.1 ROUTER_PORT="$RT_PORT" \
|
||
ROUTER_AUTH_MODE=required ROUTER_API_KEY="$TEST_ROUTER_KEY" \
|
||
ROUTER_PROFILES_FILE= \
|
||
ROUTER_STATE_FILE=/tmp/test-router-state.json \
|
||
UPSTREAM_URL="http://127.0.0.1:$UP_PORT" \
|
||
PROFILE_SCRIPT="$PWD/dev/fake-llama-profile.sh" \
|
||
PROFILE_DIR="$FAKE_DIR" \
|
||
SWITCH_TIMEOUT=30 \
|
||
SYSTEMCTL_BIN="$PWD/dev/fake-systemctl.sh" \
|
||
FAKE_SYSTEMD_PIDFILE=/tmp/mock_upstream_pid \
|
||
FAKE_SYSTEMD_PORT="$UP_PORT" \
|
||
FAKE_SYSTEMD_PROFILE_DIR="$FAKE_DIR" \
|
||
FAKE_SYSTEMD_MOCK="$PWD/dev/mock_upstream.py" \
|
||
FAKE_SYSTEMD_LOG=/tmp/mock_upstream_fake.log \
|
||
IMAGE_WORKER="$PWD/dev/mock_image_worker.py" \
|
||
IMAGE_PYTHON=python3 \
|
||
IMAGE_DIR=/tmp/test-images \
|
||
IMAGE_WORKER_LOG=/tmp/test_worker.log \
|
||
IMAGE_GEN_TIMEOUT=30 \
|
||
MOCK_WORKER_LOG=/tmp/test_worker_requests.jsonl \
|
||
TTS_WORKER_URL="http://127.0.0.1:$TTS_PORT" \
|
||
STT_WORKER_URL="http://127.0.0.1:$STT_PORT" \
|
||
python3 router/ai_profile_router.py >/tmp/router_test.log 2>&1 &
|
||
ROUTER_PID=$!
|
||
wait_http "$BASE/health" "Router" || {
|
||
cat /tmp/router_test.log >&2
|
||
exit 1
|
||
}
|
||
rm -f /tmp/test_worker_requests.jsonl
|
||
|
||
echo "== Test 0: Authentifizierung + Health/Readiness"
|
||
CODE=$(command curl -s -o /tmp/err0.json -w "%{http_code}" "$BASE/status")
|
||
[ "$CODE" = "401" ] && ok "Status ohne Key → 401" || bad "Status ohne Key: HTTP $CODE"
|
||
CODE=$(command curl -s -o /dev/null -w "%{http_code}" "$BASE/health")
|
||
[ "$CODE" = "200" ] && ok "öffentliche Liveness → 200" || bad "Liveness: HTTP $CODE"
|
||
CODE=$(command curl -s -o /dev/null -w "%{http_code}" "$BASE/ready")
|
||
[ "$CODE" = "200" ] && ok "öffentliche Readiness → 200" || bad "Readiness: HTTP $CODE"
|
||
|
||
# --- Mock-TTS-Worker starten ----------------------------------------------------
|
||
echo "== Starte Mock-TTS-Worker (Port $TTS_PORT)"
|
||
MOCK_TTS_PORT="$TTS_PORT" MOCK_TTS_DELAY=0.1 \
|
||
MOCK_TTS_LOG=/tmp/test_tts_requests.jsonl \
|
||
python3 dev/mock_tts_worker.py >/tmp/mock_tts.log 2>&1 &
|
||
TTS_PID=$!
|
||
wait_http "http://127.0.0.1:$TTS_PORT/status" "Mock-TTS" || exit 1
|
||
rm -f /tmp/test_tts_requests.jsonl
|
||
|
||
# --- Mock-STT-Worker starten ----------------------------------------------------
|
||
echo "== Starte Mock-STT-Worker (Port $STT_PORT)"
|
||
MOCK_STT_PORT="$STT_PORT" MOCK_STT_DELAY=0.1 \
|
||
MOCK_STT_LOG=/tmp/test_stt_requests.jsonl \
|
||
python3 dev/mock_stt_worker.py >/tmp/mock_stt.log 2>&1 &
|
||
STT_PID=$!
|
||
wait_http "http://127.0.0.1:$STT_PORT/status" "Mock-STT" || exit 1
|
||
rm -f /tmp/test_stt_requests.jsonl
|
||
|
||
# --- 1. /v1/models -------------------------------------------------------------
|
||
echo "== Test 1: /v1/models"
|
||
RESP=$(curl -sf "$BASE/v1/models")
|
||
echo "$RESP" | python3 -m json.tool
|
||
echo "$RESP" | python3 -c '
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
ids={m["id"]:m for m in d["data"]}
|
||
assert set(ids)=={"qwen-fast","qwen-medium","qwen-large","qwen-ultra","qwen-uncensored"}, ids
|
||
assert ids["qwen-fast"]["context_length"]==76800
|
||
assert ids["qwen-medium"]["context_length"]==160000
|
||
assert ids["qwen-large"]["context_length"]==192000
|
||
assert ids["qwen-ultra"]["context_length"]==262144
|
||
assert ids["qwen-uncensored"]["context_length"]==80000
|
||
' && ok "fünf virtuelle Modelle mit korrekten Context Windows" || bad "/v1/models"
|
||
|
||
# --- 2. /status -----------------------------------------------------------------
|
||
echo "== Test 2: /status"
|
||
RESP=$(curl -sf "$BASE/status")
|
||
echo "$RESP" | python3 -m json.tool
|
||
echo "$RESP" | python3 -c '
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
assert d["current_profile"]=="fast", d
|
||
assert d["upstream"]["reachable"] is True, d
|
||
assert d["upstream"]["ctx"]==76800, d
|
||
' && ok "Status zeigt Profil fast + erreichbares Upstream" || bad "/status"
|
||
|
||
# --- 3. Normales Forwarding (non-streaming) -------------------------------------
|
||
echo "== Test 3: Forwarding non-streaming (qwen-fast)"
|
||
RESP=$(curl -sf "$BASE/v1/chat/completions" -H "Content-Type: application/json" \
|
||
-d '{"model":"qwen-fast","messages":[{"role":"user","content":"Hallo"}]}')
|
||
echo "$RESP" | python3 -m json.tool
|
||
echo "$RESP" | python3 -c '
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
assert d["model"].startswith("mock-model-"), d
|
||
assert "Mock-Antwort" in d["choices"][0]["message"]["content"], d
|
||
assert d.get("mock_authorization") is None, d
|
||
' && ok "Request wurde weitergeleitet, Modell ersetzt" || bad "Forwarding"
|
||
|
||
echo "== Test 3b: Hermes-Reasoning erreicht das llama.cpp-Chat-Template"
|
||
RESP=$(curl -sf "$BASE/v1/chat/completions" -H "Content-Type: application/json" \
|
||
-d '{"model":"qwen-fast","reasoning_effort":"none","messages":[{"role":"user","content":"Hallo"}]}')
|
||
echo "$RESP" | python3 -c '
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
assert d.get("mock_reasoning_effort") is None, d
|
||
assert d.get("mock_chat_template_kwargs") == {"enable_thinking": False}, d
|
||
' && ok "none wird als enable_thinking=false weitergegeben" || bad "Reasoning none"
|
||
RESP=$(curl -sf "$BASE/v1/chat/completions" -H "Content-Type: application/json" \
|
||
-d '{"model":"qwen-fast","reasoning_effort":"medium","messages":[{"role":"user","content":"Hallo"}]}')
|
||
echo "$RESP" | python3 -c '
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
assert d.get("mock_reasoning_effort") is None, d
|
||
assert d.get("mock_chat_template_kwargs") == {
|
||
"enable_thinking": True, "reasoning_effort": "medium"}, d
|
||
' && ok "medium erreicht chat_template_kwargs" || bad "Reasoning medium"
|
||
|
||
# --- 4. Streaming ----------------------------------------------------------------
|
||
echo "== Test 4: Streaming (SSE)"
|
||
RESP=$(curl -sfN "$BASE/v1/chat/completions" -H "Content-Type: application/json" \
|
||
-d '{"model":"qwen-fast","stream":true,"messages":[{"role":"user","content":"Hallo"}]}')
|
||
echo "$RESP"
|
||
echo "$RESP" | grep -q "data: " && echo "$RESP" | grep -q "\[DONE\]" \
|
||
&& ok "SSE-Stream mit [DONE] erhalten" || bad "Streaming"
|
||
|
||
echo "== Test 4b: Client-Abbruch gibt den Router-Slot frei"
|
||
command curl -H "Authorization: Bearer $TEST_ROUTER_KEY" -sfN \
|
||
"$BASE/v1/chat/completions" -H "Content-Type: application/json" \
|
||
-d '{"model":"qwen-fast","stream":true,"mock_stream_delay":2,"messages":[{"role":"user","content":"Abbruch"}]}' \
|
||
>/tmp/aborted-stream.txt 2>/dev/null &
|
||
ABORT_PID=$!
|
||
sleep 0.4
|
||
kill "$ABORT_PID" 2>/dev/null || true
|
||
wait "$ABORT_PID" 2>/dev/null || true
|
||
for _ in $(seq 1 30); do
|
||
ACTIVE=$(curl -sf "$BASE/status" | python3 -c 'import json,sys; print(json.load(sys.stdin)["qwen"]["active_chats"])')
|
||
[[ $ACTIVE == 0 ]] && break
|
||
sleep 0.1
|
||
done
|
||
CODE=$(curl -s -o /tmp/after-abort.json -w "%{http_code}" "$BASE/v1/chat/completions" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"model":"qwen-fast","messages":[{"role":"user","content":"noch frei?"}]}')
|
||
[[ ${ACTIVE:-1} == 0 && $CODE == 200 ]] \
|
||
&& ok "abgebrochener Stream gibt Lease frei; Folgerequest erfolgreich" \
|
||
|| bad "Stream-Abbruch hinterließ active_chats=${ACTIVE:-?}, HTTP $CODE"
|
||
|
||
if [[ ${ROUTER_TEST_QUICK:-0} == 1 ]]; then
|
||
echo
|
||
echo "== Schnellergebnis: $PASS bestanden, $FAIL fehlgeschlagen =="
|
||
[[ $FAIL -eq 0 ]]
|
||
exit
|
||
fi
|
||
|
||
# --- 5. Tool Calls -----------------------------------------------------------------
|
||
echo "== Test 5: Tool Calls"
|
||
RESP=$(curl -sf "$BASE/v1/chat/completions" -H "Content-Type: application/json" \
|
||
-d '{"model":"qwen-fast","messages":[{"role":"user","content":"Wetter?"}],
|
||
"tools":[{"type":"function","function":{"name":"get_weather","parameters":{}}}]}' )
|
||
echo "$RESP" | python3 -m json.tool
|
||
echo "$RESP" | python3 -c '
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
tc=d["choices"][0]["message"]["tool_calls"]
|
||
assert tc[0]["function"]["name"]=="get_weather", d
|
||
assert d["choices"][0]["finish_reason"]=="tool_calls", d
|
||
' && ok "Tool Calls transparent weitergereicht" || bad "Tool Calls"
|
||
|
||
# --- 6. Profilwechsel fast -> medium ------------------------------------------------
|
||
echo "== Test 6: Profilwechsel fast -> medium"
|
||
RESP=$(curl -sf -X POST "$BASE/medium")
|
||
echo "$RESP" | python3 -m json.tool
|
||
echo "$RESP" | python3 -c '
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
assert d["profile"]=="medium" and d["context_length"]==160000, d
|
||
' && ok "Profil medium aktiv" || bad "Profilwechsel medium"
|
||
curl -sf "$BASE/status" | python3 -c '
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
assert d["current_profile"]=="medium", d
|
||
assert d["upstream"]["ctx"]==160000, d
|
||
' && ok "Status bestätigt medium (ctx 160000)" || bad "Status nach Wechsel"
|
||
|
||
# --- 7. Profilwechsel medium -> fast --------------------------------------------------
|
||
echo "== Test 7: Profilwechsel medium -> fast"
|
||
RESP=$(curl -sf -X POST "$BASE/fast")
|
||
echo "$RESP" | python3 -m json.tool
|
||
echo "$RESP" | python3 -c '
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
assert d["profile"]=="fast" and d["context_length"]==76800, d
|
||
' && ok "Profil fast wieder aktiv" || bad "Profilwechsel fast"
|
||
|
||
# --- 8. Virtuelles Modell triggert Profilwechsel ----------------------------------------
|
||
echo "== Test 8: Chat mit qwen-large triggert Wechsel auf large"
|
||
RESP=$(curl -sf "$BASE/v1/chat/completions" -H "Content-Type: application/json" \
|
||
-d '{"model":"qwen-large","messages":[{"role":"user","content":"Hallo"}]}')
|
||
echo "$RESP" | python3 -m json.tool
|
||
echo "$RESP" | python3 -c '
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
assert d["model"]=="mock-model-192000", d
|
||
' && ok "qwen-large hat Profil large aktiviert und weitergeleitet" || bad "virtuelles Modell"
|
||
|
||
# --- 9. Methoden und ungültiges virtuelles Modell -----------------------------------------
|
||
echo "== Test 9: sichere Profilmethoden + ungültiges virtuelles Modell"
|
||
CODE=$(curl -s -o /tmp/err9.json -w "%{http_code}" "$BASE/large")
|
||
cat /tmp/err9.json; echo
|
||
[ "$CODE" = "405" ] && ok "GET /large verändert kein Profil" || bad "erwartet 405, bekam $CODE"
|
||
|
||
CODE=$(curl -s -o /tmp/err9b.json -w "%{http_code}" -X POST "$BASE/v1/chat/completions" \
|
||
-H "Content-Type: application/json" -d '{"model":"qwen-huge","messages":[]}')
|
||
cat /tmp/err9b.json; echo
|
||
[ "$CODE" = "400" ] && ok "400 bei unbekanntem virtuellen Modell (qwen-huge)" || bad "erwartet 400, bekam $CODE"
|
||
|
||
# Unbekannte einteilige POST-Pfade gehören dem Upstream, nicht dem Profilrouter.
|
||
CODE=$(curl -s -o /tmp/err9c.json -w "%{http_code}" -X POST "$BASE/tokenize" \
|
||
-H "Content-Type: application/json" -d '{"content":"Hallo"}')
|
||
[ "$CODE" = "404" ] && ok "POST /tokenize wurde transparent weitergeleitet" \
|
||
|| bad "erwartet Upstream-404, bekam $CODE"
|
||
|
||
echo medium >/tmp/fake-profile-fail
|
||
CODE=$(curl -s -o /tmp/err9d.json -w "%{http_code}" -X POST "$BASE/medium")
|
||
[ "$CODE" = "503" ] && ok "fehlgeschlagenes Profilskript → 503" \
|
||
|| bad "Profilskript-Fehler: HTTP $CODE"
|
||
rm -f /tmp/fake-profile-fail
|
||
CODE=$(curl -s -o /tmp/chat9e.json -w "%{http_code}" "$BASE/v1/chat/completions" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"model":"qwen-fast","messages":[{"role":"user","content":"noch da?"}]}')
|
||
[ "$CODE" = "200" ] && ok "vorheriges Profil bleibt nach Skriptfehler verfügbar" \
|
||
|| bad "Qwen nach Profilskript-Fehler: HTTP $CODE"
|
||
|
||
# --- 10. llama.cpp down -> 502, danach Recovery ---------------------------------------------------
|
||
echo "== Test 10: Upstream down -> 502, danach Recovery"
|
||
# Profil auf fast setzen (aus Test 8 ist large aktiv)
|
||
curl -sf -X POST "$BASE/fast" >/dev/null
|
||
# Mock stoppen (simuliert Crash) – über Fake-systemctl
|
||
FAKE_SYSTEMD_PIDFILE=/tmp/mock_upstream_pid FAKE_SYSTEMD_PORT="$UP_PORT" \
|
||
bash dev/fake-systemctl.sh stop
|
||
sleep 0.5
|
||
CODE=$(command curl -s -o /dev/null -w "%{http_code}" "$BASE/health")
|
||
[ "$CODE" = "200" ] && ok "Liveness bleibt bei downem Modell 200" || bad "Liveness down: HTTP $CODE"
|
||
CODE=$(command curl -s -o /dev/null -w "%{http_code}" "$BASE/ready")
|
||
[ "$CODE" = "503" ] && ok "Readiness zeigt downes Modell mit 503" || bad "Readiness down: HTTP $CODE"
|
||
CODE=$(curl -s -o /tmp/err10.json -w "%{http_code}" -X POST "$BASE/v1/chat/completions" \
|
||
-H "Content-Type: application/json" -d '{"model":"qwen-fast","messages":[]}')
|
||
cat /tmp/err10.json; echo
|
||
[ "$CODE" = "502" ] && ok "502 bei downem Upstream (Profil bereits aktiv)" || bad "erwartet 502, bekam $CODE"
|
||
|
||
# Mock neu starten (simuliert systemctl restart durch das Profil-Skript)
|
||
FAKE_SYSTEMD_PIDFILE=/tmp/mock_upstream_pid FAKE_SYSTEMD_PORT="$UP_PORT" \
|
||
FAKE_SYSTEMD_PROFILE_DIR="$FAKE_DIR" FAKE_SYSTEMD_MOCK="$PWD/dev/mock_upstream.py" \
|
||
FAKE_SYSTEMD_LOG=/tmp/mock_upstream2.log \
|
||
bash dev/fake-systemctl.sh start
|
||
RESP=$(curl -sf -X POST "$BASE/fast")
|
||
echo "$RESP" | python3 -m json.tool
|
||
# neuen Mock als MOCK_PID übernehmen, damit Cleanup ihn beendet
|
||
[ -f /tmp/mock_upstream_pid ] && MOCK_PID=$(cat /tmp/mock_upstream_pid)
|
||
echo "$RESP" | python3 -c '
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
assert d["profile"]=="fast" and d["model"]=="mock-model-76800", d
|
||
' && ok "Recovery: /fast wartet auf Upstream, dann Erfolg" || bad "Recovery"
|
||
|
||
# --- 11. Bildgenerierung (Mock-Worker) -------------------------------------------------
|
||
echo "== Test 11: POST /v1/images/generations (1024x1024)"
|
||
RESP=$(curl -sf "$BASE/v1/images/generations" -H "Content-Type: application/json" \
|
||
-d '{"prompt":"ein rotes Haus","size":"1024x1024"}')
|
||
echo "$RESP" | python3 -m json.tool
|
||
IMG_NAME=$(echo "$RESP" | python3 -c 'import json,sys; print(json.load(sys.stdin)["data"][0]["url"].rsplit("/",1)[1])')
|
||
echo "$RESP" | python3 -c '
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
assert len(d["data"])==1, d
|
||
assert d["data"][0]["url"].startswith("http://"), d
|
||
' && [ -f "/tmp/test-images/$IMG_NAME" ] \
|
||
&& ok "Bild generiert und gespeichert ($IMG_NAME)" || bad "Bildgenerierung"
|
||
|
||
# --- 12. Bild-Download -----------------------------------------------------------------
|
||
echo "== Test 12: GET /images/<datei>"
|
||
CODE=$(curl -s -o /tmp/test_dl.png -w "%{http_code}" -D /tmp/hdr12.txt "$BASE/images/$IMG_NAME")
|
||
CTYPE=$(grep -i content-type /tmp/hdr12.txt | tr -d "\r")
|
||
[ "$CODE" = "200" ] && [ -s /tmp/test_dl.png ] && echo "$CTYPE" | grep -qi "image/png" \
|
||
&& ok "PNG-Download (200, $CTYPE)" || bad "PNG-Download (Code $CODE, $CTYPE)"
|
||
|
||
# --- 13. Bild-Liste ---------------------------------------------------------------------
|
||
echo "== Test 13: GET /images"
|
||
RESP=$(curl -sf "$BASE/images")
|
||
echo "$RESP" | python3 -m json.tool
|
||
echo "$RESP" | python3 -c "
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
names=[i['name'] for i in d['images']]
|
||
assert '$IMG_NAME' in names, names
|
||
" && ok "Bild in Liste enthalten" || bad "Bild-Liste"
|
||
|
||
# --- 14. Validierung ---------------------------------------------------------------------
|
||
echo "== Test 14: Validierung (Größe, Prompt, n)"
|
||
CODE=$(curl -s -o /tmp/err14a.json -w "%{http_code}" "$BASE/v1/images/generations" \
|
||
-H "Content-Type: application/json" -d '{"prompt":"x","size":"500x500"}')
|
||
cat /tmp/err14a.json; echo
|
||
[ "$CODE" = "400" ] && ok "400 bei ungültiger Größe" || bad "erwartet 400, bekam $CODE"
|
||
|
||
CODE=$(curl -s -o /tmp/err14b.json -w "%{http_code}" "$BASE/v1/images/generations" \
|
||
-H "Content-Type: application/json" -d '{"size":"1024x1024"}')
|
||
cat /tmp/err14b.json; echo
|
||
[ "$CODE" = "400" ] && ok "400 bei fehlendem Prompt" || bad "erwartet 400, bekam $CODE"
|
||
|
||
CODE=$(curl -s -o /tmp/err14c.json -w "%{http_code}" "$BASE/v1/images/generations" \
|
||
-H "Content-Type: application/json" -d '{"prompt":"x","n":9}')
|
||
cat /tmp/err14c.json; echo
|
||
[ "$CODE" = "400" ] && ok "400 bei n=9 (max 4)" || bad "erwartet 400, bekam $CODE"
|
||
|
||
# --- 15. b64_json + n=2 + Seed -------------------------------------------------------------
|
||
echo "== Test 15: response_format=b64_json, n=2, seed"
|
||
RESP=$(curl -sf "$BASE/v1/images/generations" -H "Content-Type: application/json" \
|
||
-d '{"prompt":"zwei Bilder","size":"1024x1024","n":2,"seed":42,"response_format":"b64_json"}')
|
||
echo "$RESP" | python3 -c '
|
||
import json,sys,base64
|
||
d=json.load(sys.stdin)
|
||
assert len(d["data"])==2, d
|
||
for item in d["data"]:
|
||
assert item["url"] is None, item
|
||
png=base64.b64decode(item["b64_json"])
|
||
assert png[:4]==b"\x89PNG", "kein PNG"
|
||
' && ok "2 Bilder als b64_json (gültige PNGs)" || bad "b64_json/n=2"
|
||
|
||
# --- 16. /status zeigt Bild-Zustand ---------------------------------------------------------
|
||
echo "== Test 16: /status mit Bild-Section"
|
||
RESP=$(curl -sf "$BASE/status")
|
||
echo "$RESP" | python3 -m json.tool
|
||
echo "$RESP" | python3 -c '
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
img=d["image"]
|
||
assert img["phase"]=="idle", img
|
||
assert img["worker"]=="stopped", img # Worker wird nach Job beendet
|
||
assert img["model_loaded"] is False, img
|
||
assert img["last_image"], img
|
||
assert img["last_error"] is None, img
|
||
q=d["qwen"]
|
||
assert q["available"] is True, q
|
||
assert q["active_chats"]==0, q
|
||
' && ok "Status: phase=idle, worker=stopped, qwen verfügbar" || bad "Status Bild-Section"
|
||
|
||
# --- 17. Qwen nach Bildgenerierung erreichbar -------------------------------------------------
|
||
echo "== Test 17: Qwen nach Bildgenerierung erreichbar"
|
||
RESP=$(curl -sf "$BASE/v1/chat/completions" -H "Content-Type: application/json" \
|
||
-d '{"model":"qwen-fast","messages":[{"role":"user","content":"Hallo"}]}')
|
||
echo "$RESP" | python3 -c '
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
assert "Mock-Antwort" in d["choices"][0]["message"]["content"], d
|
||
' && ok "Chat funktioniert nach Bildgenerierung" || bad "Chat nach Bild"
|
||
|
||
# --- 18. quality=standard stays on the validated four-step FLUX path ----------------------------
|
||
echo "== Test 18: quality=standard → 4 Steps"
|
||
rm -f /tmp/test_worker_requests.jsonl
|
||
RESP=$(curl -sf "$BASE/v1/images/generations" -H "Content-Type: application/json" \
|
||
-d '{"prompt":"standard test","size":"1024x1024","quality":"standard"}')
|
||
sleep 0.3
|
||
STEPS=$(tail -1 /tmp/test_worker_requests.jsonl 2>/dev/null | python3 -c 'import json,sys; print(json.load(sys.stdin)["steps"])' 2>/dev/null || echo "?")
|
||
[ "$STEPS" = "4" ] && ok "quality=standard → 4 Steps" || bad "erwartet 4 Steps, bekam $STEPS"
|
||
|
||
# --- 19. quality=high stays bounded for the distilled four-step model -----------------------------
|
||
echo "== Test 19: quality=high → 4 Steps"
|
||
rm -f /tmp/test_worker_requests.jsonl
|
||
RESP=$(curl -sf "$BASE/v1/images/generations" -H "Content-Type: application/json" \
|
||
-d '{"prompt":"high test","size":"1024x1024","quality":"high"}')
|
||
sleep 0.3
|
||
STEPS=$(tail -1 /tmp/test_worker_requests.jsonl 2>/dev/null | python3 -c 'import json,sys; print(json.load(sys.stdin)["steps"])' 2>/dev/null || echo "?")
|
||
[ "$STEPS" = "4" ] && ok "quality=high → 4 Steps" || bad "erwartet 4 Steps, bekam $STEPS"
|
||
|
||
# --- 20. ungültige Qualität → 400 ------------------------------------------------------------------
|
||
echo "== Test 20: ungültige Qualität → 400"
|
||
CODE=$(curl -s -o /tmp/err20.json -w "%{http_code}" "$BASE/v1/images/generations" \
|
||
-H "Content-Type: application/json" -d '{"prompt":"x","quality":"bogus"}')
|
||
cat /tmp/err20.json; echo
|
||
[ "$CODE" = "400" ] && ok "400 bei ungültiger Qualität" || bad "erwartet 400, bekam $CODE"
|
||
|
||
# --- 21. Image-Fehler → Qwen wiederhergestellt ------------------------------------------------------
|
||
echo "== Test 21: Image-Fehler → Qwen wiederhergestellt"
|
||
curl -sf -X POST "$BASE/fast" >/dev/null
|
||
CODE=$(curl -s -o /tmp/err21.json -w "%{http_code}" "$BASE/v1/images/generations" \
|
||
-H "Content-Type: application/json" -d '{"prompt":"FAIL","size":"1024x1024"}')
|
||
cat /tmp/err21.json; echo
|
||
sleep 0.5
|
||
RESP=$(curl -sf "$BASE/status")
|
||
echo "$RESP" | python3 -c '
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
assert d["current_profile"]=="fast", d
|
||
assert d["upstream"]["reachable"] is True, d
|
||
assert d["qwen"]["available"] is True, d
|
||
' && ok "Qwen nach Image-Fehler wiederhergestellt (fast, erreichbar)" || bad "Qwen nicht wiederhergestellt"
|
||
|
||
# --- 22. Fast → Image → Fast ------------------------------------------------------------------------
|
||
echo "== Test 22: Fast → Image → Fast"
|
||
curl -sf -X POST "$BASE/fast" >/dev/null
|
||
RESP=$(curl -sf "$BASE/v1/images/generations" -H "Content-Type: application/json" \
|
||
-d '{"prompt":"fast test","size":"1024x1024"}')
|
||
sleep 0.5
|
||
PROFILE=$(curl -sf "$BASE/status" | python3 -c 'import json,sys; print(json.load(sys.stdin)["current_profile"])')
|
||
[ "$PROFILE" = "fast" ] && ok "Fast → Image → Fast" || bad "Profil nach Image: $PROFILE (erwartet fast)"
|
||
|
||
# --- 23. Medium → Image → Medium --------------------------------------------------------------------
|
||
echo "== Test 23: Medium → Image → Medium"
|
||
curl -sf -X POST "$BASE/medium" >/dev/null
|
||
RESP=$(curl -sf "$BASE/v1/images/generations" -H "Content-Type: application/json" \
|
||
-d '{"prompt":"medium test","size":"1024x1024"}')
|
||
sleep 0.5
|
||
PROFILE=$(curl -sf "$BASE/status" | python3 -c 'import json,sys; print(json.load(sys.stdin)["current_profile"])')
|
||
[ "$PROFILE" = "medium" ] && ok "Medium → Image → Medium" || bad "Profil nach Image: $PROFILE (erwartet medium)"
|
||
|
||
# --- 24. Large → Image → Large ----------------------------------------------------------------------
|
||
echo "== Test 24: Large → Image → Large"
|
||
curl -sf -X POST "$BASE/large" >/dev/null
|
||
RESP=$(curl -sf "$BASE/v1/images/generations" -H "Content-Type: application/json" \
|
||
-d '{"prompt":"large test","size":"1024x1024"}')
|
||
sleep 0.5
|
||
PROFILE=$(curl -sf "$BASE/status" | python3 -c 'import json,sys; print(json.load(sys.stdin)["current_profile"])')
|
||
[ "$PROFILE" = "large" ] && ok "Large → Image → Large" || bad "Profil nach Image: $PROFILE (erwartet large)"
|
||
|
||
# --- 25. /status während Image-Job -------------------------------------------------------------------
|
||
echo "== Test 25: /status während Image-Job"
|
||
curl -sf -X POST "$BASE/fast" >/dev/null
|
||
curl -sf "$BASE/v1/images/generations" -H "Content-Type: application/json" \
|
||
-d '{"prompt":"SLOW","size":"1024x1024"}' >/tmp/img25.json 2>&1 &
|
||
IMG_PID=$!
|
||
sleep 1.5
|
||
RESP=$(curl -sf "$BASE/status")
|
||
echo "$RESP" | python3 -m json.tool
|
||
echo "$RESP" | python3 -c '
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
img=d["image"]
|
||
assert img["phase"]!="idle", img
|
||
assert d["qwen"]["available"] is False, d
|
||
' && ok "Status während Image-Job: phase!=idle, qwen unavailable" || bad "Status während Image-Job"
|
||
wait $IMG_PID
|
||
sleep 0.5
|
||
RESP=$(curl -sf "$BASE/status")
|
||
echo "$RESP" | python3 -c '
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
assert d["qwen"]["available"] is True, d
|
||
assert d["image"]["phase"]=="idle", d
|
||
' && ok "Nach Image-Job: qwen verfügbar, phase=idle" || bad "Nach Image-Job"
|
||
|
||
# --- 26. paralleler Chat während Image-Job (wartet, kein 502) ----------------------------------------
|
||
echo "== Test 26: paralleler Chat während Image-Job (wartet, kein 502)"
|
||
curl -sf -X POST "$BASE/fast" >/dev/null
|
||
curl -sf "$BASE/v1/images/generations" -H "Content-Type: application/json" \
|
||
-d '{"prompt":"SLOW","size":"1024x1024"}' >/tmp/img26.json 2>&1 &
|
||
IMG_PID=$!
|
||
sleep 1.5
|
||
START=$(date +%s)
|
||
CODE=$(curl -s -o /tmp/chat26.json -w "%{http_code}" "$BASE/v1/chat/completions" \
|
||
-H "Content-Type: application/json" -d '{"model":"qwen-fast","messages":[{"role":"user","content":"Hallo"}]}')
|
||
END=$(date +%s)
|
||
ELAPSED=$((END-START))
|
||
cat /tmp/chat26.json; echo
|
||
wait $IMG_PID
|
||
[ "$CODE" = "200" ] && [ "$ELAPSED" -ge 2 ] \
|
||
&& ok "Chat wartete ${ELAPSED}s (kein 502), dann 200" || bad "Chat: Code $CODE, ${ELAPSED}s"
|
||
|
||
# --- 27. TTS: /status zeigt tts-Section ---------------------------------------------------------------
|
||
echo "== Test 27: /status mit tts-Section"
|
||
RESP=$(curl -sf "$BASE/status")
|
||
echo "$RESP" | python3 -m json.tool
|
||
echo "$RESP" | python3 -c '
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
tts=d["tts"]
|
||
assert tts["reachable"] is True, tts
|
||
assert tts["ready"] is True, tts
|
||
assert set(tts["voices"])=={"claribel"}, tts
|
||
' && ok "Status: TTS erreichbar, bereit, 2 Stimmen" || bad "Status tts-Section"
|
||
|
||
# --- 28. TTS: POST /v1/audio/speech (wav) ---------------------------------------------------------------
|
||
echo "== Test 28: POST /v1/audio/speech (wav)"
|
||
CODE=$(curl -s -o /tmp/tts28.wav -w "%{http_code}" -D /tmp/hdr28.txt \
|
||
"$BASE/v1/audio/speech" -H "Content-Type: application/json" \
|
||
-d '{"model":"xtts-v2","input":"Hallo Welt","voice":"claribel","response_format":"wav"}')
|
||
CTYPE=$(grep -i content-type /tmp/hdr28.txt | tr -d "\r")
|
||
[ "$CODE" = "200" ] && [ -s /tmp/tts28.wav ] && echo "$CTYPE" | grep -qi "audio/wav" \
|
||
&& ok "TTS wav (200, $CTYPE, $(stat -f%z /tmp/tts28.wav 2>/dev/null || stat -c%s /tmp/tts28.wav) Bytes)" \
|
||
|| bad "TTS wav (Code $CODE, $CTYPE)"
|
||
|
||
# --- 29. TTS: POST /v1/audio/speech (mp3, Default) -------------------------------------------------------
|
||
echo "== Test 29: POST /v1/audio/speech (mp3, Default)"
|
||
CODE=$(curl -s -o /tmp/tts29.mp3 -w "%{http_code}" -D /tmp/hdr29.txt \
|
||
"$BASE/v1/audio/speech" -H "Content-Type: application/json" \
|
||
-d '{"input":"Guten Tag","voice":"claribel"}')
|
||
CTYPE=$(grep -i content-type /tmp/hdr29.txt | tr -d "\r")
|
||
[ "$CODE" = "200" ] && [ -s /tmp/tts29.mp3 ] && echo "$CTYPE" | grep -qi "audio/mpeg" \
|
||
&& ok "TTS mp3 (200, $CTYPE)" || bad "TTS mp3 (Code $CODE, $CTYPE)"
|
||
|
||
# --- 30. TTS: Validierung --------------------------------------------------------------------------------
|
||
echo "== Test 30: TTS-Validierung"
|
||
CODE=$(curl -s -o /tmp/err30a.json -w "%{http_code}" "$BASE/v1/audio/speech" \
|
||
-H "Content-Type: application/json" -d '{"voice":"claribel"}')
|
||
cat /tmp/err30a.json; echo
|
||
[ "$CODE" = "400" ] && ok "400 bei fehlendem input" || bad "erwartet 400, bekam $CODE"
|
||
|
||
CODE=$(curl -s -o /tmp/err30b.json -w "%{http_code}" "$BASE/v1/audio/speech" \
|
||
-H "Content-Type: application/json" -d '{"input":"x","voice":"bogus"}')
|
||
cat /tmp/err30b.json; echo
|
||
[ "$CODE" = "400" ] && ok "400 bei ungültiger Stimme" || bad "erwartet 400, bekam $CODE"
|
||
|
||
CODE=$(curl -s -o /tmp/err30c.json -w "%{http_code}" "$BASE/v1/audio/speech" \
|
||
-H "Content-Type: application/json" -d '{"input":"x","response_format":"ogg"}')
|
||
cat /tmp/err30c.json; echo
|
||
[ "$CODE" = "400" ] && ok "400 bei ungültigem Format" || bad "erwartet 400, bekam $CODE"
|
||
|
||
CODE=$(curl -s -o /tmp/err30d.json -w "%{http_code}" "$BASE/v1/audio/speech" \
|
||
-H "Content-Type: application/json" -d '{"input":"x","model":"gpt-4"}')
|
||
cat /tmp/err30d.json; echo
|
||
[ "$CODE" = "400" ] && ok "400 bei unbekanntem Modell" || bad "erwartet 400, bekam $CODE"
|
||
|
||
# --- 31. TTS: Worker-Fehler → 503 ------------------------------------------------------------------------
|
||
echo "== Test 31: TTS-Worker-Fehler → 503"
|
||
CODE=$(curl -s -o /tmp/err31.json -w "%{http_code}" "$BASE/v1/audio/speech" \
|
||
-H "Content-Type: application/json" -d '{"input":"FAIL","voice":"claribel"}')
|
||
cat /tmp/err31.json; echo
|
||
[ "$CODE" = "503" ] && ok "503 bei TTS-Worker-Fehler" || bad "erwartet 503, bekam $CODE"
|
||
|
||
# --- 32. TTS: Worker down → 503 ---------------------------------------------------------------------------
|
||
echo "== Test 32: TTS-Worker down → 503"
|
||
kill "$TTS_PID" 2>/dev/null || true
|
||
sleep 0.5
|
||
CODE=$(curl -s -o /tmp/err32.json -w "%{http_code}" "$BASE/v1/audio/speech" \
|
||
-H "Content-Type: application/json" -d '{"input":"Hallo","voice":"claribel"}')
|
||
cat /tmp/err32.json; echo
|
||
[ "$CODE" = "503" ] && ok "503 bei downem TTS-Worker" || bad "erwartet 503, bekam $CODE"
|
||
RESP=$(curl -sf "$BASE/status")
|
||
echo "$RESP" | python3 -c '
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
assert d["tts"]["reachable"] is False, d["tts"]
|
||
' && ok "Status: TTS nicht erreichbar" || bad "Status nach TTS-Down"
|
||
|
||
# --- 33. TTS: Worker-Neustart → Recovery -------------------------------------------------------------------
|
||
echo "== Test 33: TTS-Worker-Neustart → Recovery"
|
||
MOCK_TTS_PORT="$TTS_PORT" MOCK_TTS_DELAY=0.1 \
|
||
python3 dev/mock_tts_worker.py >/tmp/mock_tts2.log 2>&1 &
|
||
TTS_PID=$!
|
||
sleep 0.5
|
||
CODE=$(curl -s -o /tmp/tts33.wav -w "%{http_code}" "$BASE/v1/audio/speech" \
|
||
-H "Content-Type: application/json" -d '{"input":"Wieder da","voice":"claribel","response_format":"wav"}')
|
||
[ "$CODE" = "200" ] && [ -s /tmp/tts33.wav ] \
|
||
&& ok "TTS nach Neustart wieder verfügbar" || bad "TTS-Recovery (Code $CODE)"
|
||
|
||
# --- 34. STT: /status zeigt stt-Section ---------------------------------------------------------------
|
||
echo "== Test 34: /status mit stt-Section"
|
||
RESP=$(curl -sf "$BASE/status")
|
||
echo "$RESP" | python3 -m json.tool
|
||
echo "$RESP" | python3 -c '
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
stt=d["stt"]
|
||
assert stt["reachable"] is True, stt
|
||
assert stt["ready"] is True, stt
|
||
' && ok "Status: STT erreichbar, bereit" || bad "Status stt-Section"
|
||
|
||
# --- 35. STT: POST /v1/audio/transcriptions (WAV) ---------------------------------------------------------------
|
||
echo "== Test 35: POST /v1/audio/transcriptions (WAV)"
|
||
# Test-WAV erstellen (leere WAV-Header + Daten)
|
||
python3 -c "
|
||
import struct, wave
|
||
with wave.open('/tmp/stt_test.wav', 'w') as w:
|
||
w.setnchannels(1)
|
||
w.setsampwidth(2)
|
||
w.setframerate(16000)
|
||
w.writeframes(b'\x00' * 16000 * 3) # 3 Sekunden Stille
|
||
"
|
||
CODE=$(curl -s -o /tmp/stt35.json -w "%{http_code}" \
|
||
"$BASE/v1/audio/transcriptions" \
|
||
-F "file=@/tmp/stt_test.wav" \
|
||
-F "model=whisper-1")
|
||
cat /tmp/stt35.json; echo
|
||
[ "$CODE" = "200" ] && python3 -c "import json; d=json.load(open('/tmp/stt35.json')); assert 'text' in d" \
|
||
&& ok "STT WAV (200, text vorhanden)" || bad "STT WAV (Code $CODE)"
|
||
|
||
# --- 36. STT: POST /v1/audio/transcriptions (language=de) -------------------------------------------------------
|
||
echo "== Test 36: POST /v1/audio/transcriptions (language=de)"
|
||
CODE=$(curl -s -o /tmp/stt36.json -w "%{http_code}" \
|
||
"$BASE/v1/audio/transcriptions" \
|
||
-F "file=@/tmp/stt_test.wav" \
|
||
-F "model=whisper-1" \
|
||
-F "language=de")
|
||
cat /tmp/stt36.json; echo
|
||
[ "$CODE" = "200" ] && python3 -c "import json; d=json.load(open('/tmp/stt36.json')); assert 'text' in d" \
|
||
&& ok "STT language=de (200)" || bad "STT language=de (Code $CODE)"
|
||
|
||
# --- 37. STT: unbekanntes Modell → 400 ---------------------------------------------------------------------------
|
||
echo "== Test 37: STT unbekanntes Modell → 400"
|
||
CODE=$(curl -s -o /tmp/err37.json -w "%{http_code}" \
|
||
"$BASE/v1/audio/transcriptions" \
|
||
-F "file=@/tmp/stt_test.wav" \
|
||
-F "model=gpt-4")
|
||
cat /tmp/err37.json; echo
|
||
[ "$CODE" = "400" ] && ok "400 bei unbekanntem STT-Modell" || bad "erwartet 400, bekam $CODE"
|
||
|
||
# --- 38. STT: Worker down → 503 -----------------------------------------------------------------------------------
|
||
echo "== Test 38: STT Worker down → 503"
|
||
kill "$STT_PID" 2>/dev/null || true
|
||
sleep 0.5
|
||
CODE=$(curl -s -o /tmp/err38.json -w "%{http_code}" \
|
||
"$BASE/v1/audio/transcriptions" \
|
||
-F "file=@/tmp/stt_test.wav" \
|
||
-F "model=whisper-1")
|
||
cat /tmp/err38.json; echo
|
||
[ "$CODE" = "503" ] && ok "503 bei downem STT-Worker" || bad "erwartet 503, bekam $CODE"
|
||
RESP=$(curl -sf "$BASE/status")
|
||
echo "$RESP" | python3 -c '
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
assert d["stt"]["reachable"] is False, d["stt"]
|
||
' && ok "Status: STT nicht erreichbar" || bad "Status nach STT-Down"
|
||
|
||
# --- 39. STT: Worker-Neustart → Recovery ---------------------------------------------------------------------------
|
||
echo "== Test 39: STT Worker-Neustart → Recovery"
|
||
MOCK_STT_PORT="$STT_PORT" MOCK_STT_DELAY=0.1 \
|
||
python3 dev/mock_stt_worker.py >/tmp/mock_stt2.log 2>&1 &
|
||
STT_PID=$!
|
||
sleep 0.5
|
||
CODE=$(curl -s -o /tmp/stt39.json -w "%{http_code}" \
|
||
"$BASE/v1/audio/transcriptions" \
|
||
-F "file=@/tmp/stt_test.wav" \
|
||
-F "model=whisper-1")
|
||
[ "$CODE" = "200" ] && ok "STT nach Neustart wieder verfügbar" || bad "STT-Recovery (Code $CODE)"
|
||
|
||
# --- 40. /v1/audio/models ------------------------------------------------------------------------------------------
|
||
echo "== Test 40: GET /v1/audio/models"
|
||
RESP=$(curl -sf "$BASE/v1/audio/models")
|
||
echo "$RESP" | python3 -m json.tool
|
||
echo "$RESP" | python3 -c '
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
ids={m["id"] for m in d["data"]}
|
||
assert "whisper-1" in ids, ids
|
||
assert "xtts-v2" in ids, ids
|
||
' && ok "Audio-Modelle: whisper-1 + xtts-v2" || bad "Audio-Modelle"
|
||
|
||
# --- 41. /v1/audio/voices ------------------------------------------------------------------------------------------
|
||
echo "== Test 41: GET /v1/audio/voices"
|
||
RESP=$(curl -sf "$BASE/v1/audio/voices")
|
||
echo "$RESP" | python3 -m json.tool
|
||
echo "$RESP" | python3 -c '
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
ids={v["id"] for v in d["data"]}
|
||
assert "claribel" in ids, ids
|
||
' && ok "Audio-Voices: claribel" || bad "Audio-Voices"
|
||
|
||
# --- 42. STT + Qwen parallel ----------------------------------------------------------------------------------------
|
||
echo "== Test 42: STT + Qwen parallel"
|
||
# STT-Request im Hintergrund
|
||
curl -sf "$BASE/v1/audio/transcriptions" \
|
||
-F "file=@/tmp/stt_test.wav" \
|
||
-F "model=whisper-1" >/tmp/stt42.json 2>&1 &
|
||
STT_PID42=$!
|
||
sleep 0.2
|
||
# Qwen-Request
|
||
RESP=$(curl -sf "$BASE/v1/chat/completions" -H "Content-Type: application/json" \
|
||
-d '{"model":"qwen-fast","messages":[{"role":"user","content":"Hallo"}]}')
|
||
wait $STT_PID42
|
||
echo "$RESP" | python3 -c '
|
||
import json,sys
|
||
d=json.load(sys.stdin)
|
||
assert "Mock-Antwort" in d["choices"][0]["message"]["content"], d
|
||
' && ok "STT + Qwen parallel (beide 200)" || bad "STT + Qwen parallel"
|
||
|
||
# --- 43. STT + TTS parallel ------------------------------------------------------------------------------------------
|
||
echo "== Test 43: STT + TTS parallel"
|
||
# STT-Request im Hintergrund
|
||
curl -sf "$BASE/v1/audio/transcriptions" \
|
||
-F "file=@/tmp/stt_test.wav" \
|
||
-F "model=whisper-1" >/tmp/stt43.json 2>&1 &
|
||
STT_PID43=$!
|
||
sleep 0.2
|
||
# TTS-Request
|
||
CODE=$(curl -s -o /tmp/tts43.mp3 -w "%{http_code}" \
|
||
"$BASE/v1/audio/speech" -H "Content-Type: application/json" \
|
||
-d '{"input":"Hallo","voice":"claribel"}')
|
||
wait $STT_PID43
|
||
[ "$CODE" = "200" ] && [ -s /tmp/tts43.mp3 ] \
|
||
&& ok "STT + TTS parallel (beide 200)" || bad "STT + TTS parallel (TTS Code $CODE)"
|
||
|
||
# --- 44. Zwei konkurrierende Profilanfragen ------------------------------------------------
|
||
echo "== Test 44: Profil-Lease verhindert Wechsel während eines Chats"
|
||
curl -sf "$BASE/v1/chat/completions" -H "Content-Type: application/json" \
|
||
-d '{"model":"qwen-large","mock_delay":1.0,"messages":[{"role":"user","content":"Groß"}]}' \
|
||
>/tmp/chat44-large.json &
|
||
CHAT44_PID=$!
|
||
sleep 0.2
|
||
curl -sf "$BASE/v1/chat/completions" -H "Content-Type: application/json" \
|
||
-d '{"model":"qwen-medium","messages":[{"role":"user","content":"Mittel"}]}' \
|
||
>/tmp/chat44-medium.json
|
||
wait "$CHAT44_PID"
|
||
python3 -c '
|
||
import json
|
||
large=json.load(open("/tmp/chat44-large.json"))
|
||
medium=json.load(open("/tmp/chat44-medium.json"))
|
||
assert large["mock_ctx"] == 192000, large
|
||
assert medium["mock_ctx"] == 160000, medium
|
||
' && ok "konkurrierende Chats behielten jeweils ihr Profil" \
|
||
|| bad "Profil-Lease bei konkurrierenden Chats"
|
||
|
||
# --- Ergebnis --------------------------------------------------------------------------------------------
|
||
echo
|
||
echo "== Ergebnis: $PASS bestanden, $FAIL fehlgeschlagen =="
|
||
[ "$FAIL" -eq 0 ]
|