Files
llm-monitor/tests/test_proxy.py
T
ducoterra 48bc92b9af
Build and Push Container / build-and-push (push) Successful in 38s
initial import: read-only gufo + vLLM dashboard
Single-file FastAPI proxy plus a no-CDN vanilla-JS page for watching a gufo
server and a vLLM server side by side: one Prometheus parser (class Prom) with
per-engine field maps, measured Δcounter/Δt rates for engines that export no
rate gauge, and strict read-only GET-only routing. Engines are opt-in via
*_BASE_URL — unset means no panel, no poll, and a 404 {"configured": false}
without dialling.

Includes the container image and CI so the dashboard can be deployed as an
image rather than a checkout:

- Containerfile: uv-locked two-stage build on python:3.14-slim, non-root
  (uid 10001), binds 8090 (8000 is vLLM's port in this fleet), healthcheck on
  /api/config so a dead engine never marks the dashboard unhealthy. No frontend
  build stage — static/index.html is bundler-free by design and is copied as-is.
- .gitea/workflows/build-and-push.yml: publishes
  gitea.reeseapps.com/Experimental/llm-monitor on main + release tags.

114 tests, no network and no real engines required.

LLM-Generated: true
2026-10-16 12:49:45 -04:00

300 lines
13 KiB
Python

"""Proxy routes: read-only guarantee, upstream failure translation, JSON shape.
Nothing here touches a socket — see conftest.FakeUpstream. The point is to pin
the contract the browser depends on: what passes through, what becomes JSON, and
exactly how an engine outage shows up.
"""
from __future__ import annotations
import json
import httpx
import pytest
import app as app_module
from tests.conftest import GUFO_FIXTURE, SLOTS_FIXTURE, VLLM_FIXTURE, response
API_PATHS = ["/api/gufo/slots", "/api/gufo/metrics", "/api/vllm/metrics", "/api/config"]
GUFO_PATHS = ["/api/gufo/slots", "/api/gufo/metrics"]
VLLM_PATHS = ["/api/vllm/metrics"]
# --------------------------------------------------------------------------- #
# read-only by construction
# --------------------------------------------------------------------------- #
@pytest.mark.parametrize("method", ["post", "put", "patch", "delete"])
@pytest.mark.parametrize("path", API_PATHS)
def test_no_writing_verbs(client, method: str, path: str) -> None:
assert getattr(client, method)(path).status_code == 405
def test_only_get_reaches_upstream(client, upstream) -> None:
upstream.route("http://gufo.test/slots", response(200, SLOTS_FIXTURE.encode()))
client.get("/api/gufo/slots")
upstream.route("http://gufo.test/metrics", response(200, GUFO_FIXTURE.encode()))
client.get("/api/gufo/metrics")
assert upstream.methods == {"GET"}
# --------------------------------------------------------------------------- #
# /api/gufo/slots — passthrough
# --------------------------------------------------------------------------- #
def test_slots_pass_through_byte_for_byte(client, upstream) -> None:
upstream.route(
"http://gufo.test/slots", response(200, SLOTS_FIXTURE.encode(), "application/json")
)
r = client.get("/api/gufo/slots")
assert r.status_code == 200
assert r.content.decode() == SLOTS_FIXTURE # not re-serialised, not reformatted
assert r.headers["content-type"].startswith("application/json")
assert "no-store" in r.headers["cache-control"]
assert r.json()["slots"][0]["id_task"] == 12
def test_slots_accepts_a_bare_array_payload(client, upstream) -> None:
"""The deployed gufo returns `[{...}]`, not `{"slots": [...]}`. The proxy must not
care — it forwards whatever JSON the engine sent, unchanged."""
bare = '[{"id":0,"is_processing":true,"id_task":7,"next_token":[{"n_decoded":3,"n_remain":9}]}]'
upstream.route("http://gufo.test/slots", response(200, bare.encode(), "application/json"))
r = client.get("/api/gufo/slots")
assert r.status_code == 200
assert r.json() == json.loads(bare)
assert isinstance(r.json(), list)
def test_upstream_content_type_is_passed_through(client, upstream) -> None:
# whatever gufo claims, the browser gets the same thing — no re-typing
upstream.route(
"http://gufo.test/slots",
response(200, SLOTS_FIXTURE.encode(), "application/stream+json"),
)
assert client.get("/api/gufo/slots").headers["content-type"] == "application/stream+json"
def test_slots_falls_back_to_the_alias(client, upstream) -> None:
# /slots 404s (the stub's default for unrouted URLs), the alias answers
upstream.route("http://gufo.test/v1/slots", response(200, SLOTS_FIXTURE.encode(), "application/json"))
r = client.get("/api/gufo/slots")
assert r.status_code == 200
assert upstream.requested_urls == ["http://gufo.test/slots", "http://gufo.test/v1/slots"]
def test_slots_reports_missing_endpoints(client, upstream) -> None:
r = client.get("/api/gufo/slots")
assert r.status_code == 502
assert "/slots" in r.json()["detail"] and "/v1/slots" in r.json()["detail"]
def test_slots_does_not_swallow_a_server_error(client, upstream) -> None:
upstream.route("http://gufo.test/slots", response(500, b"boom"))
r = client.get("/api/gufo/slots")
assert r.status_code == 502 # engine is unhealthy: that is a bad gateway, not a fallback
assert "HTTP 500" in r.json()["detail"]
assert upstream.requested_urls == ["http://gufo.test/slots"] # no pointless alias retry
# --------------------------------------------------------------------------- #
# /metrics — Prometheus -> JSON
# --------------------------------------------------------------------------- #
def test_gufo_metrics_endpoint(client, upstream) -> None:
upstream.route("http://gufo.test/metrics", response(200, GUFO_FIXTURE.encode()))
body = client.get("/api/gufo/metrics").json()
assert body["ok"] is True
assert body["engine"] == "gufo"
assert body["upstream"] == "http://gufo.test/metrics"
assert body["series"] == 8 # 7 llamacpp:* series + the legacy llama:* one
assert body["unparsed"] == 0
assert body["metrics"]["requests_processing"]["value"] == 2.0
assert body["scraped_at"] > 0
def test_vllm_metrics_endpoint(client, upstream) -> None:
upstream.route("http://vllm.test/metrics", response(200, VLLM_FIXTURE.encode()))
m = client.get("/api/vllm/metrics").json()["metrics"]
assert m["num_requests_running"]["value"] == 3.0
assert m["waiting_capacity"]["value"] == 5.0
assert m["time_to_first_token_seconds"]["mean"] == pytest.approx(1.0)
def test_garbage_exposition_is_ok_with_no_series(client, upstream) -> None:
upstream.route("http://vllm.test/metrics", response(200, b"not prometheus at all\n"))
body = client.get("/api/vllm/metrics").json()
assert body["ok"] is True
assert body["series"] == 0 and body["unparsed"] == 1
assert body["metrics"]["num_requests_running"]["value"] is None # renders as n/a
# --------------------------------------------------------------------------- #
# engine outage translation (the per-engine health contract)
# --------------------------------------------------------------------------- #
def test_unreachable_engine_is_a_502_with_a_readable_detail(client, upstream) -> None:
upstream.route("http://vllm.test/metrics", httpx.ConnectError("connection refused"))
r = client.get("/api/vllm/metrics")
assert r.status_code == 502
assert r.json()["ok"] is False
assert "unreachable" in r.json()["detail"]
def test_slow_engine_is_a_504(client, upstream) -> None:
upstream.route("http://gufo.test/metrics", httpx.TimeoutException("too slow"))
r = client.get("/api/gufo/metrics")
assert r.status_code == 504
assert "timed out" in r.json()["detail"]
def test_one_engine_being_down_does_not_affect_the_other(client, upstream) -> None:
upstream.route("http://gufo.test/slots", response(200, SLOTS_FIXTURE.encode(), "application/json"))
upstream.route("http://vllm.test/metrics", httpx.ConnectError("connection refused"))
assert client.get("/api/vllm/metrics").status_code == 502
assert client.get("/api/gufo/slots").status_code == 200
# --------------------------------------------------------------------------- #
# auth + config
# --------------------------------------------------------------------------- #
def test_no_authorization_header_without_keys(client, upstream) -> None:
upstream.route("http://gufo.test/slots", response(200, b"{}", "application/json"))
upstream.route("http://vllm.test/metrics", response(200, b"", "text/plain"))
client.get("/api/gufo/slots")
client.get("/api/vllm/metrics")
assert all("authorization" not in headers for _url, headers in upstream.calls)
def test_bearer_tokens_are_per_engine(client, upstream, monkeypatch) -> None:
monkeypatch.setattr(app_module, "GUFO_API_KEY", "sk-gufo")
monkeypatch.setattr(app_module, "VLLM_API_KEY", "sk-vllm")
upstream.route("http://gufo.test/slots", response(200, b"{}", "application/json"))
upstream.route("http://vllm.test/metrics", response(200, b"", "text/plain"))
client.get("/api/gufo/slots")
client.get("/api/vllm/metrics")
assert upstream.calls[0][1]["Authorization"] == "Bearer sk-gufo"
assert upstream.calls[1][1]["Authorization"] == "Bearer sk-vllm"
def test_config_never_leaks_keys(client, monkeypatch) -> None:
monkeypatch.setattr(app_module, "GUFO_API_KEY", "sk-super-secret")
r = client.get("/api/config")
assert "sk-super-secret" not in r.text
body = r.json()
assert body["gufo_auth"] is True and body["vllm_auth"] is False
assert body["poll_ms"] == app_module.POLL_MS
# --------------------------------------------------------------------------- #
# opt-in engines: an unset *_BASE_URL means this deployment does not run that server
# --------------------------------------------------------------------------- #
@pytest.mark.parametrize(
"raw, expected",
[
("http://box:1", "http://box:1"),
("http://box:1/", "http://box:1"), # never a doubled //metrics
(" http://box:1 ", "http://box:1"),
("", ""), # set-but-empty is 'not configured', not 'http:///metrics'
(" ", ""),
],
)
def test_base_url_env_parsing(monkeypatch, raw: str, expected: str) -> None:
monkeypatch.setenv("TEST_ENGINE_BASE_URL", raw)
assert app_module._base_url_from_env("TEST_ENGINE_BASE_URL") == expected
# and unset means the same as blank: no default host is invented
monkeypatch.delenv("TEST_ENGINE_BASE_URL")
assert app_module._base_url_from_env("TEST_ENGINE_BASE_URL") == ""
@pytest.mark.parametrize("path", GUFO_PATHS + VLLM_PATHS)
def test_unset_base_url_is_404_not_configured(client, upstream, monkeypatch, path: str) -> None:
"""A server nobody runs must not look like a server that is down: no upstream call,
no 502, and a detail that names the env var to set."""
engine = "gufo" if "gufo" in path else "vllm"
monkeypatch.setattr(app_module, f"{engine.upper()}_BASE_URL", "")
r = client.get(path)
assert r.status_code == 404
body = r.json()
assert body["ok"] is False
assert body["configured"] is False
assert body["engine"] == engine
assert f"{engine.upper()}_BASE_URL" in body["detail"]
assert "no such route" not in body["detail"] # distinct from an unknown path
assert upstream.requested_urls == [] # nothing was dialled
def test_one_configured_engine_is_unaffected_by_the_other_being_absent(
client, upstream, monkeypatch
) -> None:
"""The vLLM-only recipient of this repo gets a working dashboard, not a half-broken one."""
monkeypatch.setattr(app_module, "GUFO_BASE_URL", "")
upstream.route("http://vllm.test/metrics", response(200, VLLM_FIXTURE.encode()))
assert client.get("/api/gufo/metrics").status_code == 404
assert client.get("/api/gufo/slots").status_code == 404
body = client.get("/api/vllm/metrics").json()
assert body["ok"] is True
assert upstream.requested_urls == ["http://vllm.test/metrics"]
def test_config_reports_which_engines_exist(client, monkeypatch) -> None:
body = client.get("/api/config").json()
assert body["gufo_enabled"] is True and body["vllm_enabled"] is True
monkeypatch.setattr(app_module, "GUFO_BASE_URL", "")
body = client.get("/api/config").json()
assert body["gufo_enabled"] is False
assert body["gufo_base_url"] == ""
assert body["vllm_enabled"] is True # the other engine keeps its section
def test_lifespan_owns_one_shared_client(client) -> None:
"""Entering lifespan installs exactly one AsyncClient on app.state and closes it
on shutdown — the poll loop depends on connection reuse, and a leaked client
would keep sockets open after Ctrl-C."""
from fastapi.testclient import TestClient
with TestClient(app_module.app) as running:
shared = app_module.app.state.client
assert isinstance(shared, httpx.AsyncClient)
assert not shared.is_closed
assert running.get("/api/config").status_code == 200
assert app_module.app.state.client is shared
assert shared.is_closed
# --------------------------------------------------------------------------- #
# page + misc
# --------------------------------------------------------------------------- #
def test_dashboard_is_served(client) -> None:
r = client.get("/")
assert r.status_code == 200
assert "text/html" in r.headers["content-type"]
assert "LLM Monitor" in r.text
def test_dashboard_layout_is_driven_by_the_config_flags(client) -> None:
"""The page must render from /api/config's *_enabled flags instead of hardcoding two
engines: an absent engine loses its panel, its strip card, its polling and its share
of the fleet totals, and nothing configured at all shows the setup card. Asserted on
the shipped HTML because the layout logic lives in the inline script."""
html = client.get("/").text
# one generic engine list, revealed from the backend's flags
assert "const ENGINE_IDS" in html and "_enabled`" in html
assert "isEnabled(job.engine)" in html # an absent engine is never polled
# what gets hidden, and what shows instead when there is nothing to show
for marker in ("panel-${engine}", "strip-${engine}", 'id="no-engines"'):
assert marker in html
def test_dashboard_loads_no_third_party_assets(client) -> None:
"""The engines live on an internet-disconnected network: no CDN, no webfonts,
no build step. Only same-origin requests are allowed from the page."""
import re
html = client.get("/").text
refs = re.findall(r"""(?:src|href)\s*=\s*["']([^"']+)["']""", html)
external = [r for r in refs if r.startswith(("http://", "//"))]
assert external == [], f"page references external assets: {external}"
def test_unknown_route_is_json(client) -> None:
r = client.get("/api/nope")
assert r.status_code == 404
assert "no such route" in r.json()["detail"]