#!/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=18080 RT_PORT=18081 BASE="http://127.0.0.1:$RT_PORT" FAKE_DIR="$PWD/dev/fake-profile-dir" PASS=0 FAIL=0 cleanup() { kill "${MOCK_PID:-}" "${ROUTER_PID:-}" 2>/dev/null || true rm -f /tmp/mock_pid2 wait 2>/dev/null || true } trap cleanup EXIT ok() { echo " PASS: $1"; PASS=$((PASS+1)); } bad() { echo " FAIL: $1"; FAIL=$((FAIL+1)); } # --- Mock-llama.cpp starten -------------------------------------------------- echo "== Starte Mock-llama.cpp (Port $UP_PORT)" MOCK_PROFILE_DIR="$FAKE_DIR" MOCK_PORT="$UP_PORT" python3 dev/mock_upstream.py >/tmp/mock_upstream.log 2>&1 & MOCK_PID=$! sleep 0.5 # --- Router starten ----------------------------------------------------------- echo "== Starte Router (Port $RT_PORT)" ROUTER_HOST=127.0.0.1 ROUTER_PORT="$RT_PORT" \ UPSTREAM_URL="http://127.0.0.1:$UP_PORT" \ PROFILE_SCRIPT="$PWD/dev/fake-llama-profile.sh" \ PROFILE_DIR="$FAKE_DIR" \ SWITCH_TIMEOUT=30 \ python3 router/ai_profile_router.py >/tmp/router_test.log 2>&1 & ROUTER_PID=$! sleep 0.5 # --- 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-long"}, ids assert ids["qwen-fast"]["context_length"]==73728 assert ids["qwen-medium"]["context_length"]==94208 assert ids["qwen-long"]["context_length"]==131072 ' && ok "drei 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"]==73728, 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 ' && ok "Request wurde weitergeleitet, Modell ersetzt" || bad "Forwarding" # --- 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" # --- 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"]==94208, 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"]==94208, d ' && ok "Status bestätigt medium (ctx 94208)" || 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"]==73728, d ' && ok "Profil fast wieder aktiv" || bad "Profilwechsel fast" # --- 8. Virtuelles Modell triggert Profilwechsel ---------------------------------------- echo "== Test 8: Chat mit qwen-long triggert Wechsel auf long" RESP=$(curl -sf "$BASE/v1/chat/completions" -H "Content-Type: application/json" \ -d '{"model":"qwen-long","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-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") cat /tmp/err9.json; echo [ "$CODE" = "400" ] && ok "400 bei unbekanntem Profil (POST /huge)" || bad "erwartet 400, 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" # --- 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) curl -sf -X POST "$BASE/fast" >/dev/null kill "$MOCK_PID" 2>/dev/null; wait "$MOCK_PID" 2>/dev/null || true sleep 0.5 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 nach ~3 s neu starten (simuliert systemctl restart durch das Profil-Skript) rm -f /tmp/mock_pid2 ( sleep 3 MOCK_PROFILE_DIR="$FAKE_DIR" MOCK_PORT="$UP_PORT" python3 dev/mock_upstream.py >/tmp/mock_upstream2.log 2>&1 & echo $! > /tmp/mock_pid2 ) & 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_pid2 ] && MOCK_PID=$(cat /tmp/mock_pid2) echo "$RESP" | python3 -c ' import json,sys d=json.load(sys.stdin) assert d["profile"]=="fast" and d["model"]=="mock-model-73728", d ' && ok "Recovery: /fast wartet auf Upstream, dann Erfolg" || bad "Recovery" # --- Ergebnis -------------------------------------------------------------------------------------------- echo echo "== Ergebnis: $PASS bestanden, $FAIL fehlgeschlagen ==" [ "$FAIL" -eq 0 ]