Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ The collector keeps bounded in-memory buffers: 60 minutes of GPU, host and KPI s
| `GET /api/snapshot` | one-shot full JSON (state plus history) |
| `GET /api/stream` | SSE: 1 Hz `tick` plus push events |
| `GET /api/history?range=…` | rollup rows, minute and hour |
| `GET /api/requests?range=…` | raw request rows, capped at 1000 |
| `GET /api/requests?since=…&limit=…` | raw request rows, newest first, capped at 1000 |
| `GET /api/spans?range=…` | model load spans |
| `GET /api/leaderboard?range=…` | per-model statistics |
| `GET /api/storage` | database size, retention, projections |
Expand Down Expand Up @@ -228,7 +228,7 @@ python3 -m http.server 8792 # → http://localho

The UI files are ES modules, so `node --check` does not apply to them. The smoke scripts import and run them, which is the parse-and-run check.

The two `test_hostinfo.py` skips are platform gates: the `/proc` backend runs on Linux and the Win32 ctypes backend runs only on Windows. On a Linux box the Windows path is read, not executed, so its numbers stay unverified until you run the suite on Windows.
The `test_hostinfo.py` platform gates skip the Windows ctypes backend on Linux and the `/proc` backend on Windows. On a Linux box the Windows path is read, not executed, so its numbers stay unverified until you run the suite on Windows.

## Origin

Expand Down
6 changes: 4 additions & 2 deletions collector/test_hostinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ def test_processes_shape(self):
self.assertEqual(set(p), {"name", "pid", "rss_gb", "cmd"})
self.assertIn(p["name"], ("llama-server", "ninfer-serve", "strata"))
self.assertGreater(p["pid"], 0)
self.assertTrue(p["rss_gb"] is None or p["rss_gb"] > 0)
# rss_gb is rounded to 2 dp in GB, so a tiny launcher/wrapper
# process legitimately reports 0.0
self.assertTrue(p["rss_gb"] is None or p["rss_gb"] >= 0)
self.assertLessEqual(len(p["cmd"]), 120)

def test_processes_matches_fake_engine(self):
Expand Down Expand Up @@ -117,7 +119,7 @@ def test_processes_matches_fake_engine(self):
p = hit[0]
self.assertEqual(p["name"], "llama-server")
self.assertIn("llama-server", p["cmd"])
self.assertTrue(p["rss_gb"] is None or p["rss_gb"] > 0)
self.assertTrue(p["rss_gb"] is None or p["rss_gb"] >= 0)
finally:
proc.kill()
proc.wait()
Expand Down
59 changes: 58 additions & 1 deletion collector/test_speculum.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
"""Tests for KPI model attribution: engine_model_name, and the tps_models field
kpi_pass emits so the UI can name the model behind the decode rate."""
kpi_pass emits so the UI can name the model behind the decode rate. Also the
/api/requests query contract (`since`, `limit`, and its 400s)."""

import unittest

Expand Down Expand Up @@ -101,5 +102,61 @@ def test_host_shorthand(self):
"http://gpu.lan:8080")


class _FakeHist:
"""Records the (since, limit) the API hands to the history layer."""

def __init__(self):
self.calls = []

def requests(self, since, limit=1000):
self.calls.append((since, limit))
return []


class _JsonRecorder(speculum.Handler):
"""A Handler with its socket stripped out: records what _json would send,
so the request-API query validation runs without a real server."""

def __init__(self):
self.responses = []

def _json(self, obj, code=200):
self.responses.append((code, obj))


class TestRequestsApiContract(unittest.TestCase):
"""GET /api/requests reads `since` (epoch seconds) and `limit`; the README
documents those two params. History.requests then clamps limit to 1000."""

def setUp(self):
self._real_hist = speculum.HIST
self.fake = _FakeHist()
speculum.HIST = self.fake

def tearDown(self):
speculum.HIST = self._real_hist

def test_defaults_since_zero_and_limit_1000(self):
h = _JsonRecorder()
h._requests_api("")
self.assertEqual(self.fake.calls, [(0, 1000)])
self.assertEqual(h.responses[-1][0], 200)

def test_since_and_limit_are_passed_through(self):
h = _JsonRecorder()
h._requests_api("since=1700000000&limit=50")
self.assertEqual(self.fake.calls, [(1700000000, 50)])
self.assertEqual(h.responses[-1][0], 200)

def test_non_integer_or_zero_limit_is_rejected(self):
for query in ("since=abc", "limit=x", "limit=0"):
with self.subTest(query=query):
h = _JsonRecorder()
h._requests_api(query)
self.assertEqual(h.responses[-1], (400, {
"error": "since and limit must be integers"}))
self.assertEqual(self.fake.calls, []) # nothing was queried


if __name__ == "__main__":
unittest.main()
Loading