Files
6krrt/tests/test_route_decisions.py
adlee-was-taken bfb0ff7c00 feat(metrics): persist routing decisions and expose GET /metrics
The router previously recorded only completions (energy_observations),
not the routing decisions behind them, so 'how routing is performing' was
not answerable from data. This adds:

- route_decisions table + idempotent ensure_route_decisions (guarded
  CREATE TABLE IF NOT EXISTS, never regenerates a live DB) gated by
  logging.log_route_decisions; every decision kind (route/dispatch/chat/
  passthrough/local-vision) is persisted best-effort via
  persist_route_decision (never fails a request; only session_key, never
  session_dir). The table is ensured on the write path (mirroring
  proficiency_store._write -> ensure_columns) so a live DB that predates
  the feature migrates safely.
- metrics.py aggregator moved quota_burn/scoring_coverage in from the
  dispatcher (breaking a would-be circular import) and adds
  recent_decisions/per_model/verdict_mix/top_proficiency; /health now
  imports them and GET /metrics exposes the 7-key JSON (window-bounded,
  loopback-only, no auth).
- observed_at indexes on energy_observations/verifications.
2026-08-23 20:20:09 -04:00

661 lines
22 KiB
Python

"""Tests for the route_decisions table, its inline-create helper, and the gate.
The monitoring TUI (see .omo/plans/router-monitoring-tui.md) needs a record of
every routing decision — which model was picked and why — that survives in the
datbase rather than only in the journal. This file pins the three pieces todo #1
adds:
- the `route_decisions` table in schema.sql (columns and the guarded index),
- `dispatcher.ensure_route_decisions(conn)` — the idempotent inline-create
helper that is the *only* way the table appears on a live router.db (the
live DB is never recreated; schema.sql alone is CREATE TABLE IF NOT EXISTS
and silently does nothing to an existing DB),
- the `logging.log_route_decisions` config gate.
The database tests follow the same offline temp-DB pattern as
tests/test_chat_completions.py: a throwaway SQLite file seeded from schema.sql,
never the live router.db.
"""
import json
import sqlite3
from pathlib import Path
import pytest
from starlette.testclient import TestClient
import dispatcher
from dispatcher import Classification, app
import config
import metrics
ROOT = Path(__file__).resolve().parent.parent
SCHEMA_SQL = (ROOT / "schema.sql").read_text()
ROUTE_DECISIONS_COLUMNS = [
"id",
"observed_at",
"kind",
"task_category",
"task_tier",
"required_context_tokens",
"confidence",
"classifier_ms",
"classification_source",
"latency_tolerance",
"candidates_considered",
"selected_model",
"selected_provider",
"runner_up_models",
"est_cost_usd",
"est_proficiency",
"rejected_reason",
"session_key",
"tools",
"images",
"json_mode",
"streamed",
]
def _table_exists(conn: sqlite3.Connection, table: str) -> bool:
row = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name=?",
(table,),
).fetchone()
return row is not None
def _index_exists(conn: sqlite3.Connection, index: str) -> bool:
row = conn.execute(
"SELECT name FROM sqlite_master WHERE type='index' AND name=?",
(index,),
).fetchone()
return row is not None
def _schema_minus_route_decisions() -> str:
"""schema.sql with the route_decisions block removed, for the failure case."""
lines = []
skipping = False
for line in SCHEMA_SQL.splitlines():
stripped = line.strip()
if stripped.startswith("CREATE TABLE IF NOT EXISTS route_decisions"):
skipping = True
continue
if skipping:
# The create ends at the closing paren + semicolon of the table
# statement. Anything still in the table body is skipped.
if stripped == ");":
skipping = False
continue
if "idx_route_decisions_observed" in line:
continue
lines.append(line)
return "\n".join(lines)
# --- schema round-trips -----------------------------------------------------
def test_schema_defines_route_decisions_table():
"""schema.sql declares the table, so a fresh DB from it has it already."""
assert "CREATE TABLE IF NOT EXISTS route_decisions" in SCHEMA_SQL
def test_schema_index_is_guarded():
"""The observed_at index must be IF NOT EXISTS so re-applying is a no-op."""
assert "idx_route_decisions_observed" in SCHEMA_SQL
assert (
"CREATE INDEX IF NOT EXISTS idx_route_decisions_observed "
"ON route_decisions (observed_at)" in SCHEMA_SQL
)
# --- happy path: fresh DB from full schema ----------------------------------
def test_fresh_schema_already_has_table(tmp_path):
conn = sqlite3.connect(tmp_path / "fresh.db")
conn.executescript(SCHEMA_SQL)
assert _table_exists(conn, "route_decisions")
for col in ROUTE_DECISIONS_COLUMNS:
assert col in {r[1] for r in conn.execute("PRAGMA table_info(route_decisions)")}
assert _index_exists(conn, "idx_route_decisions_observed")
conn.close()
def test_ensure_route_decisions_is_idempotent(tmp_path):
"""Fresh DB already has the table; calling the helper twice no-ops."""
conn = sqlite3.connect(tmp_path / "idem.db")
conn.executescript(SCHEMA_SQL)
# Seed some rows in another table so we can prove nothing is dropped.
conn.execute(
"INSERT INTO models (model_id, provider, last_updated) "
"VALUES ('m1', 'neuralwatt', '2026-01-01T00:00:00+00:00')"
)
conn.commit()
dispatcher.ensure_route_decisions(conn) # first call
dispatcher.ensure_route_decisions(conn) # second call: must no-op cleanly
assert _table_exists(conn, "route_decisions")
count = conn.execute("SELECT COUNT(*) FROM models").fetchone()[0]
assert count == 1 # pre-existing rows survived
conn.close()
# --- failure path: pre-existing DB WITHOUT the table ------------------------
def test_ensure_route_decisions_adds_table_without_dropping_rows(tmp_path):
"""A DB that predates the table gets it added; existing rows survive."""
conn = sqlite3.connect(tmp_path / "old.db")
conn.executescript(_schema_minus_route_decisions())
assert not _table_exists(conn, "route_decisions")
# A row in a genuinely existing table, to prove it survives the upgrade.
conn.execute(
"INSERT INTO models (model_id, provider, last_updated) "
"VALUES ('legacy', 'neuralwatt', '2026-01-01T00:00:00+00:00')"
)
conn.commit()
dispatcher.ensure_route_decisions(conn)
assert _table_exists(conn, "route_decisions")
assert _index_exists(conn, "idx_route_decisions_observed")
legacy = conn.execute(
"SELECT model_id FROM models WHERE model_id='legacy'"
).fetchone()
assert legacy is not None # the existing row was not dropped
conn.close()
def test_ensure_route_decisions_allows_insert(tmp_path):
"""After the helper runs, the table actually accepts the documented shape."""
conn = sqlite3.connect(tmp_path / "insert.db")
conn.executescript(_schema_minus_route_decisions())
dispatcher.ensure_route_decisions(conn)
conn.execute(
"""
INSERT INTO route_decisions (
observed_at, kind, task_category, task_tier,
required_context_tokens, confidence, classifier_ms,
classification_source, latency_tolerance, candidates_considered,
selected_model, selected_provider, runner_up_models,
est_cost_usd, est_proficiency, rejected_reason, session_key,
tools, images, json_mode, streamed
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
"2026-01-01T00:00:00+00:00", "route", "coding_general", 2, 500,
0.95, 1868, "classifier", "interactive", 8, "deepseek-v4-flash",
"neuralwatt", '[{"model_id": "gemma-4-31b", "provider": "neuralwatt"}]',
0.00016296, 1.0, None, "sess-hash", 0, 0, 0, 1,
),
)
conn.commit()
kind = conn.execute(
"SELECT kind FROM route_decisions WHERE selected_model='deepseek-v4-flash'"
).fetchone()
assert kind is not None and kind[0] == "route"
conn.close()
def test_persist_ensure_on_write_fixes_live_db_missing_table(tmp_path, monkeypatch):
"""A live router.db without route_decisions gets it on the WRITE path.
F4 scope-fidelity regression: ``persist_route_decision`` INSERTs without
ever calling ``ensure_route_decisions``, so if a live DB lacks the table
and the module-load startup hook did not run (a test harness, a process
that calls persist first, a future lazy-import refactor), every decision
row is silently swallowed (the INSERT raises no-such-table) and /metrics'
recent_decisions 500s. Mirroring proficiency_store._write -> ensure_columns,
the migration must be guaranteed on the write path too, not only at module
load. The temp DB is deliberately the schema-minus-route_decisions shape —
a DB that predates the feature.
"""
db_path = tmp_path / "live-no-table.db"
conn = sqlite3.connect(db_path)
conn.executescript(_schema_minus_route_decisions())
assert not _table_exists(conn, "route_decisions")
conn.close()
monkeypatch.setattr(dispatcher.cfg.database, "path", str(db_path))
monkeypatch.setattr(dispatcher.cfg.logging, "log_route_decisions", True)
# The write itself. Best-effort means it must not raise even on a missing
# table; then the row must actually land and the table must exist.
dispatcher.persist_route_decision(
"route",
classification=Classification(
task_category="coding_general", task_tier=2,
required_context_tokens=100, confidence=0.9,
),
latency_tolerance="interactive",
)
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
assert _table_exists(conn, "route_decisions"), \
"the migration must run on the write path so the table exists"
rows = conn.execute(
"SELECT kind FROM route_decisions WHERE kind='route'"
).fetchall()
assert len(rows) == 1, "the decision row must persist once the table exists"
# /metrics / recent_decisions against the same live-DB shape must not 500:
# reading must succeed now that the table exists.
rec = metrics.recent_decisions(conn)
assert len(rec) == 1
assert rec[0]["kind"] == "route"
conn.close()
# --- config gate ------------------------------------------------------------
def test_log_route_decisions_gate_defaults_on():
"""The key is declared and defaults to on, matching the config.yaml value."""
cfg = config.load_config(str(ROOT / "config.yaml"))
assert cfg.logging.log_route_decisions is True
def test_config_has_log_route_decisions_key():
"""The strict config accepts the key — it must be declared or load fails."""
assert "log_route_decisions" in (ROOT / "config.yaml").read_text()
# =============================================================================
# Todo #2: persist_route_decision wired into every decision path.
# =============================================================================
CHEAP = "cheap-model"
DEAR = "dear-model"
class FakeResponse:
"""Just enough of requests.Response for the dispatcher's provider calls."""
def __init__(self, payload=None, *, status_code=200, lines=None):
self.status_code = status_code
self._payload = payload or {}
self._lines = lines or []
self.text = json.dumps(self._payload)
self.closed = False
def json(self):
return self._payload
def iter_lines(self, decode_unicode=False):
yield from self._lines
def close(self):
self.closed = True
def _completion(model, content="hello there"):
return {
"id": "chatcmpl-dec-1",
"model": model,
"choices": [
{"message": {"role": "assistant", "content": content},
"finish_reason": "stop"}
],
"usage": {"prompt_tokens": 31, "completion_tokens": 12},
"energy": {"energy_kwh": 5.0e-05, "carbon_g_co2eq": 2.4e-03},
"cost": {"request_cost_usd": 4.0e-04},
}
_STREAM_LINES = [
'data: {"id":"chatcmpl-stream-dec","choices":[{"delta":{"content":"hel"}}]}',
"",
'data: {"id":"chatcmpl-stream-dec","choices":[{"delta":{"content":"lo"},'
'"finish_reason":"stop"}],"usage":{"prompt_tokens":31,'
'"completion_tokens":9}}',
"",
"data: [DONE]",
"",
]
def _messages(text="write me a function"):
return [{"role": "user", "content": text}]
def _image_messages():
return [
{
"role": "user",
"content": [
{"type": "text", "text": "what is in this image?"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,AAAA"}},
],
}
]
@pytest.fixture
def decision_router(tmp_path, monkeypatch):
"""A routable dispatcher over a throwaway DB; nothing dials out."""
db_path = tmp_path / "decisions.db"
conn = sqlite3.connect(db_path)
conn.executescript(SCHEMA_SQL)
for model_id, completion_price, vision in (
(CHEAP, 0.30, 1),
(DEAR, 9.00, 0),
):
conn.execute(
"""
INSERT INTO models (
model_id, provider, base_model_id, tier, context_window,
effective_context_window, max_output_tokens,
cost_per_1m_prompt, cost_per_1m_completion,
supports_vision, supports_json_mode,
latency_class, reasoning_mode, context_variant,
access_level, availability, last_updated
) VALUES (?, 'neuralwatt', ?, 2, 262128, 192500, 16384, ?, ?,
?, 1, 'standard', 'default', 'full', 'public', 'active',
'2026-08-22T00:00:00+00:00')
""",
(model_id, model_id, completion_price / 3, completion_price, vision),
)
conn.commit()
conn.close()
monkeypatch.setattr(dispatcher.cfg.database, "path", str(db_path))
monkeypatch.setattr(dispatcher.cfg.verification, "local_llm_enabled", False)
monkeypatch.setattr(dispatcher.cfg.local_vision, "enabled", False)
monkeypatch.setenv("NEURALWATT_API_KEY", "test-key")
# ensure the gate is on for the happy-path tests (default, but pin it).
monkeypatch.setattr(dispatcher.cfg.logging, "log_route_decisions", True)
calls = []
def fake_post(url, headers=None, json=None, stream=False, timeout=None):
calls.append({"url": url, "body": json, "stream": stream})
if stream:
return FakeResponse(lines=_STREAM_LINES)
return FakeResponse(_completion(json["model"]))
monkeypatch.setattr(dispatcher.requests, "post", fake_post)
monkeypatch.setattr(
dispatcher, "classify",
lambda task, context: Classification(
task_category="coding_general", task_tier=2,
required_context_tokens=100, confidence=0.9,
),
)
class _Raw:
text = json.dumps(_completion(CHEAP))
class _Completions:
@property
def with_raw_response(self):
return self
def create(self, **kwargs):
return _Raw()
class _FakeClient:
chat = type("_Chat", (), {"completions": _Completions()})()
monkeypatch.setattr(dispatcher, "_provider_client", lambda provider: _FakeClient())
yield TestClient(app), db_path
def _rows(db_path):
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
rows = conn.execute(
"SELECT * FROM route_decisions ORDER BY id ASC"
).fetchall()
conn.close()
return rows
def _drop_cheap(db_path):
"""Make CHEAP ineligible so DEAR (or nothing) remains."""
conn = sqlite3.connect(db_path)
conn.execute("UPDATE models SET tier = 1 WHERE model_id = ?", (CHEAP,))
conn.commit()
conn.close()
# --- happy paths ------------------------------------------------------------
def test_route_endpoint_persists_one_row(decision_router):
client, db_path = decision_router
resp = client.post(
"/route", json={"task": "write me a function"}
)
assert resp.status_code == 200
rows = _rows(db_path)
assert len(rows) == 1
r = rows[0]
assert r["kind"] == "route"
assert r["selected_model"] == CHEAP
assert r["selected_provider"] == "neuralwatt"
assert r["classification_source"] == "classifier"
assert r["task_category"] == "coding_general"
assert r["task_tier"] == 2
assert r["latency_tolerance"] == "interactive"
assert r["session_key"] is None
assert "session_dir" not in r.keys()
def test_route_endpoint_override_has_classifier_ms_null(decision_router):
client, db_path = decision_router
resp = client.post(
"/route",
json={
"task": "x",
"task_category": "coding_refactor",
"task_tier": 3,
"required_context_tokens": 5000,
},
)
assert resp.status_code == 200
rows = _rows(db_path)
assert len(rows) == 1
r = rows[0]
assert r["classification_source"] == "override"
assert r["classifier_ms"] is None, "an override never consulted the classifier"
assert r["task_category"] == "coding_refactor"
def test_dispatch_endpoint_persists_one_row(decision_router):
client, db_path = decision_router
resp = client.post(
"/dispatch",
json={"task": "write me a function",
"task_category": "coding_general",
"task_tier": 2,
"required_context_tokens": 100},
)
assert resp.status_code == 200
rows = _rows(db_path)
assert len(rows) == 1
r = rows[0]
assert r["kind"] == "dispatch"
assert r["selected_model"] == CHEAP
assert r["classification_source"] == "override"
def test_routed_chat_persists_one_row(decision_router):
client, db_path = decision_router
resp = client.post(
"/v1/chat/completions",
json={"model": "auto", "messages": _messages()},
)
assert resp.status_code == 200
rows = _rows(db_path)
assert len(rows) == 1
r = rows[0]
assert r["kind"] == "chat"
assert r["selected_model"] == CHEAP
assert r["selected_provider"] == "neuralwatt"
assert r["classification_source"] == "classifier"
assert r["session_key"] is not None
# The session key is a hash — never a directory, never content.
assert len(r["session_key"]) == 16
assert "/" not in (r["session_key"] or "")
def test_routed_chat_reroute_keeps_classifier_source(decision_router):
"""Re-routing for measured context must not persist source='override'."""
client, db_path = decision_router
# >100 measured tokens (measured = chars/3), so chat_completions reroutes.
resp = client.post(
"/v1/chat/completions",
json={"model": "auto", "messages": _messages("refactor " + "x " * 600)},
)
assert resp.status_code == 200
rows = _rows(db_path)
assert len(rows) == 1
assert rows[0]["classification_source"] == "classifier", \
"the re-route's source='override' must not overwrite the classifier's"
assert rows[0]["classifier_ms"] is not None
def test_streamed_routed_chat_persists_one_row(decision_router):
client, db_path = decision_router
resp = client.post(
"/v1/chat/completions",
json={"model": "auto", "messages": _messages(), "stream": True},
)
assert resp.status_code == 200
rows = _rows(db_path)
assert len(rows) == 1
assert rows[0]["kind"] == "chat"
assert rows[0]["selected_model"] == CHEAP
assert rows[0]["streamed"] == 1
def test_passthrough_persists_one_row_with_no_nameerror(decision_router):
"""The pre-existing pass-through NameError must stay gone, and a row lands."""
client, db_path = decision_router
resp = client.post(
"/v1/chat/completions",
json={"model": DEAR, "messages": _messages()},
)
assert resp.status_code == 200
rows = _rows(db_path)
assert len(rows) == 1
r = rows[0]
assert r["kind"] == "passthrough"
assert r["selected_model"] == DEAR
assert r["selected_provider"] == "neuralwatt"
assert r["classification_source"] is None
assert r["session_key"] is not None
def test_local_vision_success_persists_one_local_row(decision_router, monkeypatch):
client, db_path = decision_router
_drop_cheap(db_path)
monkeypatch.setattr(dispatcher.cfg.local_vision, "enabled", True)
def fake_post(url, headers=None, json=None, stream=False, timeout=None):
return FakeResponse(
{"choices": [{"message": {"role": "assistant",
"content": "local caption"},
"finish_reason": "stop"}]}
)
monkeypatch.setattr(dispatcher.requests, "post", fake_post)
resp = client.post(
"/v1/chat/completions",
json={"model": "auto", "messages": _image_messages()},
)
assert resp.status_code == 200
rows = _rows(db_path)
assert len(rows) == 1, "one decision, and only one: the local_vision row"
r = rows[0]
assert r["kind"] == "local_vision"
assert r["selected_model"] == dispatcher.cfg.local_vision.model
assert r["selected_provider"] == "local"
assert r["rejected_reason"] is None
assert r["images"] == 1
def test_no_candidate_422_still_persists_a_rejection_row(decision_router):
client, db_path = decision_router
# No vision cloud candidate, local fallback disabled -> 422.
_drop_cheap(db_path)
resp = client.post(
"/v1/chat/completions",
json={"model": "auto", "messages": _image_messages()},
)
assert resp.status_code == 422
rows = _rows(db_path)
assert len(rows) == 1
r = rows[0]
assert r["kind"] == "chat"
assert r["selected_model"] is None
assert r["rejected_reason"] is not None
assert "vision" in r["rejected_reason"]
# --- failure modes: best-effort, config-gated -------------------------------
def test_gate_off_writes_nothing_but_routing_still_200(decision_router):
client, db_path = decision_router
dispatcher.cfg.logging.log_route_decisions = False
try:
resp = client.post("/route", json={"task": "write me a function"})
assert resp.status_code == 200
finally:
dispatcher.cfg.logging.log_route_decisions = True
assert _rows(db_path) == [], "the gate off must leave the table untouched"
def _raise_on_route_decisions_insert(db_path):
real_db = dispatcher._db
class GuardingConn:
def __init__(self, conn):
self._conn = conn
def __getattr__(self, name):
return getattr(self._conn, name)
def execute(self, sql, parameters=()):
if isinstance(sql, str) and "INSERT INTO route_decisions" in sql:
raise sqlite3.OperationalError("database is locked")
return self._conn.execute(sql, parameters)
def wrapped_db():
return GuardingConn(real_db())
return wrapped_db
def test_db_write_failure_never_fails_routing(decision_router, monkeypatch):
"""A locked/read-only DB must not error the request; persistence is best-effort."""
client, db_path = decision_router
monkeypatch.setattr(dispatcher, "_db", _raise_on_route_decisions_insert(db_path))
resp = client.post("/route", json={"task": "write me a function"})
assert resp.status_code == 200, "a failed decision write must never fail routing"
# And the same holds for a routed completion.
resp2 = client.post(
"/v1/chat/completions", json={"model": "auto", "messages": _messages()}
)
assert resp2.status_code == 200