Rebuild profile router as secure recoverable v2

This commit is contained in:
Mikei386
2026-08-20 13:41:00 +02:00
parent bdd6c08643
commit fd4a3055e8
18 changed files with 1162 additions and 129 deletions
+91 -13
View File
@@ -4,18 +4,27 @@
set -uo pipefail
cd "$(dirname "$0")/.."
UP_PORT=18080
RT_PORT=18081
TTS_PORT=18082
STT_PORT=18083
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() {
kill "${MOCK_PID:-}" "${ROUTER_PID:-}" "${TTS_PID:-}" "${STT_PID:-}" 2>/dev/null || true
rm -f /tmp/mock_pid2 /tmp/mock_upstream_pid
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
@@ -23,8 +32,20 @@ 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" \
@@ -33,11 +54,15 @@ 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" \
@@ -58,16 +83,27 @@ 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=$!
sleep 0.5
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=$!
sleep 0.5
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 ----------------------------------------------------
@@ -76,7 +112,7 @@ 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=$!
sleep 0.5
wait_http "http://127.0.0.1:$STT_PORT/status" "Mock-STT" || exit 1
rm -f /tmp/test_stt_requests.jsonl
# --- 1. /v1/models -------------------------------------------------------------
@@ -115,6 +151,7 @@ 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"
# --- 4. Streaming ----------------------------------------------------------------
@@ -176,17 +213,34 @@ d=json.load(sys.stdin)
assert d["model"]=="mock-model-131072", d
' && ok "qwen-long hat Profil long aktiviert und weitergeleitet" || bad "virtuelles Modell"
# --- 9. Ungültiges Profil ------------------------------------------------------------------
echo "== Test 9: Ungültiges Profil"
CODE=$(curl -s -o /tmp/err9.json -w "%{http_code}" -X POST "$BASE/huge")
# --- 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/long")
cat /tmp/err9.json; echo
[ "$CODE" = "400" ] && ok "400 bei unbekanntem Profil (POST /huge)" || bad "erwartet 400, bekam $CODE"
[ "$CODE" = "405" ] && ok "GET /long 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 long aktiv)
@@ -195,6 +249,10 @@ curl -sf -X POST "$BASE/fast" >/dev/null
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
@@ -641,6 +699,26 @@ 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-long","mock_delay":1.0,"messages":[{"role":"user","content":"Lang"}]}' \
>/tmp/chat44-long.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
long=json.load(open("/tmp/chat44-long.json"))
medium=json.load(open("/tmp/chat44-medium.json"))
assert long["mock_ctx"] == 131072, long
assert medium["mock_ctx"] == 94208, medium
' && ok "konkurrierende Chats behielten jeweils ihr Profil" \
|| bad "Profil-Lease bei konkurrierenden Chats"
# --- Ergebnis --------------------------------------------------------------------------------------------
echo
echo "== Ergebnis: $PASS bestanden, $FAIL fehlgeschlagen =="