Two measurement surfaces on /metrics. Neither is read by routing, and a test asserts that against the module source: a live incumbent-cache-pricing experiment is running, and a series that reached the ranker would confound it. cost_calibration -- routing.estimated_cost against the provider's own bill, per (provider, model), joined on request_id. The scale error is not the finding: a uniform overestimate reorders nothing, because the ranking is a comparison and every candidate moves together. The SPREAD does reorder, so that is the headline figure. Live over 168h, 4,152 joined requests: est/billed runs 1.62x (qwen/qwen3.6-35b-a3b, openrouter) to 12.86x (qwen3.6-35b-fast, neuralwatt) -- a 7.9x spread, wider than the 5x the brief was written against. The factors are REPORTED, not applied; whether they are stable enough to trust is the question this exists to answer, and this project has already mistaken one moment of a moving per-model quantity for a constant. The join needs the model as well as the request id. On the live database 5 of 4,143 rows pair a decision that selected qwen3.6-35b (neuralwatt) with a completion billed by qwen/qwen3.6-35b-a3b (openrouter) -- a cross-provider failover, and one model's estimate against another's bill. latency -- p50/p95 of router_wall_seconds and router_ttft_seconds, which landed recently and nothing read. These are the router's own clock, not the provider's duration_seconds, which is why OpenRouter is visible here at all: it reports no duration. z-ai/glm-5.3-flash, a model with "flash" in its name, measures p50 9.60s / p95 40.08s to first token against 1.58s / 3.09s for deepseek-v4-flash on NeuralWatt. Reported, never scored. The wall and TTFT sample counts are kept independent because TTFT is streaming-only by nature. No new warning class, deliberately. Every group is out of band on both series today, so a divergence warning would fire on all of them from the first run -- bare presence, which is the anti-pattern rejection_warnings exists to avoid -- and a warning is a form of trust these factors have not yet earned. Four knobs under objective, all report-only, all recorded in DELIBERATELY_NOT_IN_ADMIN with the reason: they change what /metrics shows, and not even what it warns about. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRQXz5SYZYVWscxS1QqF6U
704 lines
25 KiB
Python
704 lines
25 KiB
Python
"""Tests for the GET /metrics endpoint on dispatcher.
|
|
|
|
Seeds a throwaway temp DB and asserts on the actual JSON returned by a real
|
|
TestClient GET (never a mock-call assertion), to defeat
|
|
``misleading_success_output``.
|
|
|
|
Also covers the ``/events/decisions`` Server-Sent Events endpoint, which
|
|
streams routing decisions to the TUI without polling.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sqlite3
|
|
from datetime import datetime, timedelta, timezone
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from starlette.testclient import TestClient
|
|
|
|
import dispatcher
|
|
import events
|
|
from config import load_config
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
SCHEMA_SQL = (ROOT / "config" / "schema.sql").read_text()
|
|
CFG = load_config(str(ROOT / "config" / "config.yaml"))
|
|
|
|
|
|
def _now() -> datetime:
|
|
return datetime.now(timezone.utc)
|
|
|
|
|
|
def _make_db(tmp_path: Path, extra_sql: str = "") -> sqlite3.Connection:
|
|
conn = sqlite3.connect(str(tmp_path / "test.db"))
|
|
conn.row_factory = sqlite3.Row
|
|
conn.executescript(SCHEMA_SQL + extra_sql)
|
|
return conn
|
|
|
|
|
|
def _seed_models(conn: sqlite3.Connection) -> None:
|
|
for model_id, tier, context, cost, vision in (
|
|
("cheap", 2, 262128, 0.30, 1),
|
|
("dear", 2, 262128, 9.00, 0),
|
|
("tiny", 1, 131072, 0.10, 1),
|
|
):
|
|
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', ?, ?, ?, 192500, 16384, ?, ?,
|
|
?, 1, 'standard', 'default', 'full', 'public', 'active',
|
|
'2026-08-22T00:00:00+00:00')
|
|
""",
|
|
(model_id, model_id, tier, context, cost, cost / 3, vision),
|
|
)
|
|
conn.commit()
|
|
|
|
|
|
def _seed_decision(conn: sqlite3.Connection) -> None:
|
|
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,
|
|
session_key, tools, images, json_mode, streamed,
|
|
flex_preference, flex_swapped, flex_forced
|
|
) VALUES (?, 'route', 'coding_general', 2, 100, 0.95, 200,
|
|
'classifier', 'interactive', 5, 'cheap', 'neuralwatt',
|
|
'[{"model_id":"dear","provider":"neuralwatt"}]',
|
|
0.001, 0.9, 'abc123', 0, 0, 0, 0,
|
|
'auto', 0, 1)
|
|
""",
|
|
(_now().isoformat(),),
|
|
)
|
|
conn.commit()
|
|
|
|
|
|
def _seed_energy(conn: sqlite3.Connection) -> None:
|
|
now = _now()
|
|
conn.execute(
|
|
"INSERT INTO energy_observations "
|
|
"(model_id, provider, task_category, completion_tokens, energy_kwh, "
|
|
"cost_usd, carbon_g_co2eq, attribution_ratio, observed_at) "
|
|
"VALUES ('cheap', 'neuralwatt', 'coding_general', 100, 5.0e-05, 0.001, "
|
|
"2.4e-03, 0.25, ?)",
|
|
((now - timedelta(days=2)).isoformat(),),
|
|
)
|
|
conn.commit()
|
|
|
|
|
|
def _seed_verification(conn: sqlite3.Connection) -> None:
|
|
conn.execute(
|
|
"INSERT INTO verifications (model_id, provider, kind, verdict, observed_at) "
|
|
"VALUES ('cheap', 'neuralwatt', 'structural', 'ok', ?)",
|
|
(_now().isoformat(),),
|
|
)
|
|
conn.commit()
|
|
|
|
|
|
def _seed_proficiency(conn: sqlite3.Connection) -> None:
|
|
conn.execute(
|
|
"INSERT INTO proficiency (model_id, provider, category, blended_score, "
|
|
"source, last_updated) "
|
|
"VALUES ('cheap', 'neuralwatt', 'coding_general', 0.9, "
|
|
"'self_eval_thin', '2026-01-01T00:00:00+00:00')",
|
|
)
|
|
conn.commit()
|
|
|
|
|
|
def _seed_local_energy(conn: sqlite3.Connection) -> None:
|
|
conn.execute(
|
|
"INSERT INTO local_energy_observations "
|
|
"(observed_at, model_id, call_type, avg_power_watts, duration_seconds, "
|
|
"energy_kwh, cost_usd, carbon_g_co2eq, meter) "
|
|
"VALUES (?, 'mistral-nemo:12b', 'classify', 150.0, 1.0, "
|
|
"0.00015, 1.8e-05, 0.06, 'nvidia-smi')",
|
|
(_now().isoformat(),),
|
|
)
|
|
conn.commit()
|
|
|
|
|
|
@pytest.fixture
|
|
def seeded_client(tmp_path, monkeypatch):
|
|
"""A TestClient wired to a seeded temp DB, at /metrics."""
|
|
conn = _make_db(tmp_path)
|
|
_seed_models(conn)
|
|
for i in range(60): # exceed the 50 cap
|
|
_seed_decision(conn)
|
|
_seed_energy(conn)
|
|
_seed_verification(conn)
|
|
_seed_proficiency(conn)
|
|
conn.close()
|
|
|
|
monkeypatch.setattr(dispatcher.cfg.database, "path", str(tmp_path / "test.db"))
|
|
monkeypatch.setattr(dispatcher.cfg.verification, "local_llm_enabled", False)
|
|
monkeypatch.setattr(dispatcher.cfg.routing, "require_vision", False)
|
|
monkeypatch.setattr(dispatcher.cfg.local_energy, "enabled", False)
|
|
monkeypatch.setenv("NEURALWATT_API_KEY", "test-key")
|
|
|
|
with TestClient(dispatcher.app) as client:
|
|
yield client
|
|
|
|
|
|
def test_metrics_endpoint_has_all_top_level_keys(seeded_client):
|
|
"""GET /metrics returns 200 with every required top-level key."""
|
|
resp = seeded_client.get("/metrics")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
for key in (
|
|
"quota",
|
|
"coverage",
|
|
"recent_decisions",
|
|
"per_model",
|
|
"verdict_mix",
|
|
"top_proficiency",
|
|
"local_energy",
|
|
"pinch",
|
|
"cost_calibration",
|
|
"latency",
|
|
"generated_at",
|
|
):
|
|
assert key in data, f"missing top-level key {key!r}"
|
|
|
|
|
|
def test_metrics_recent_decisions_capped_and_carries_provider(seeded_client):
|
|
"""recent_decisions is capped at 50 and each row has selected_provider."""
|
|
resp = seeded_client.get("/metrics")
|
|
data = resp.json()
|
|
decisions = data["recent_decisions"]
|
|
assert isinstance(decisions, list)
|
|
assert len(decisions) <= 50
|
|
assert len(decisions) > 0
|
|
for row in decisions:
|
|
assert "selected_provider" in row
|
|
# DESC order: the first decision has the largest id.
|
|
first_id = decisions[0]["id"]
|
|
for row in decisions[1:]:
|
|
assert row["id"] <= first_id
|
|
|
|
|
|
def test_metrics_aggregations_are_populated(seeded_client):
|
|
"""per_model / verdict_mix / top_proficiency reflect seeded data."""
|
|
resp = seeded_client.get("/metrics")
|
|
data = resp.json()
|
|
assert isinstance(data["per_model"], list)
|
|
assert any(r["model_id"] == "cheap" for r in data["per_model"])
|
|
assert data["per_model"][0]["calls"] == 1
|
|
assert data["verdict_mix"]["ok"] == 1
|
|
assert isinstance(data["top_proficiency"], list)
|
|
assert data["top_proficiency"][0]["model_id"] == "cheap"
|
|
assert data["quota"] is not None
|
|
|
|
|
|
def test_metrics_recent_decisions_carry_flex_columns(seeded_client):
|
|
"""Each recent_decisions row carries the three flex telemetry columns."""
|
|
resp = seeded_client.get("/metrics")
|
|
data = resp.json()
|
|
decisions = data["recent_decisions"]
|
|
assert decisions
|
|
for row in decisions:
|
|
assert "flex_preference" in row
|
|
assert "flex_swapped" in row
|
|
assert "flex_forced" in row
|
|
assert decisions[0]["flex_preference"] == "auto"
|
|
assert decisions[0]["flex_swapped"] == 0
|
|
assert decisions[0]["flex_forced"] == 1
|
|
|
|
|
|
def test_metrics_exposes_flex_default(seeded_client):
|
|
"""The configured flex default is reported (under coverage)."""
|
|
resp = seeded_client.get("/metrics")
|
|
data = resp.json()
|
|
assert "flex_default" in data["coverage"]
|
|
assert data["coverage"]["flex_default"] in ("no-flex", "auto", "prefer-flex", "force-flex")
|
|
|
|
|
|
def test_metrics_contains_no_session_dir(seeded_client):
|
|
"""The JSON must never name session_dir or expose conversation text."""
|
|
body = seeded_client.get("/metrics").text
|
|
assert "session_dir" not in body
|
|
|
|
|
|
def test_metrics_quota_carries_accounts_shape(seeded_client):
|
|
"""GET /metrics quota carries period/accounts/spend/alarm blocks."""
|
|
resp = seeded_client.get("/metrics")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["quota"] is not None
|
|
quota = data["quota"]
|
|
for key in ("period", "accounts", "spend", "alarm"):
|
|
assert key in quota, f"missing quota key {key!r}"
|
|
|
|
period = quota["period"]
|
|
for key in ("start", "next_reset", "elapsed_fraction", "source"):
|
|
assert key in period, f"missing period key {key!r}"
|
|
|
|
accounts = quota["accounts"]
|
|
assert isinstance(accounts, list)
|
|
for acc in accounts:
|
|
for key in ("provider", "shape", "spend_usd"):
|
|
assert key in acc, f"missing account key {key!r}"
|
|
assert acc["shape"] in ("metered_plan", "prepaid_credit", "self_hosted", "unmetered")
|
|
|
|
spend = quota["spend"]
|
|
for key in ("by_provider_usd", "total_usd", "estimated_usd", "estimate_ratio"):
|
|
assert key in spend, f"missing spend key {key!r}"
|
|
|
|
|
|
def test_metrics_quota_survives_missing_provider_balance_table(tmp_path, monkeypatch):
|
|
"""A live DB predating provider_balance_observations must not 500 /metrics."""
|
|
conn = _make_db(tmp_path)
|
|
_seed_models(conn)
|
|
_seed_decision(conn)
|
|
conn.execute("DROP TABLE provider_balance_observations")
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
monkeypatch.setattr(dispatcher.cfg.database, "path", str(tmp_path / "test.db"))
|
|
monkeypatch.setattr(dispatcher.cfg.verification, "local_llm_enabled", False)
|
|
monkeypatch.setattr(dispatcher.cfg.routing, "require_vision", False)
|
|
monkeypatch.setattr(dispatcher.cfg.local_energy, "enabled", False)
|
|
monkeypatch.setenv("NEURALWATT_API_KEY", "test-key")
|
|
|
|
with TestClient(dispatcher.app) as client:
|
|
resp = client.get("/metrics")
|
|
assert resp.status_code == 200
|
|
quota = resp.json()["quota"]
|
|
assert "accounts" in quota
|
|
for acc in quota["accounts"]:
|
|
# Should not crash: missing table means pool is None/burn is None
|
|
assert acc["shape"] in ("metered_plan", "prepaid_credit", "self_hosted", "unmetered")
|
|
|
|
|
|
def test_metrics_local_energy_omitted_when_disabled(seeded_client):
|
|
"""With metering off the section is omitted — independent of deployment config."""
|
|
resp = seeded_client.get("/metrics")
|
|
data = resp.json()
|
|
assert "local_energy" in data
|
|
assert data["local_energy"] is None
|
|
|
|
|
|
@pytest.mark.parametrize("local_energy_enabled", [False, True])
|
|
def test_metrics_local_energy_follows_enabled_flag(
|
|
tmp_path, monkeypatch, local_energy_enabled
|
|
):
|
|
"""The suite must not couple to deployment config: both flag values are exercised."""
|
|
conn = _make_db(tmp_path)
|
|
_seed_models(conn)
|
|
if local_energy_enabled:
|
|
_seed_local_energy(conn)
|
|
conn.close()
|
|
|
|
monkeypatch.setattr(dispatcher.cfg.database, "path", str(tmp_path / "test.db"))
|
|
monkeypatch.setattr(dispatcher.cfg.verification, "local_llm_enabled", False)
|
|
monkeypatch.setattr(dispatcher.cfg.routing, "require_vision", False)
|
|
monkeypatch.setattr(
|
|
dispatcher.cfg.local_energy, "enabled", local_energy_enabled
|
|
)
|
|
monkeypatch.setattr(dispatcher.cfg.local_energy, "tariff_usd_per_kwh", 0.12)
|
|
monkeypatch.setenv("NEURALWATT_API_KEY", "test-key")
|
|
|
|
with TestClient(dispatcher.app) as client:
|
|
resp = client.get("/metrics")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
|
|
if local_energy_enabled:
|
|
assert isinstance(data["local_energy"], dict)
|
|
assert data["local_energy"]["calls_30d"] == 1
|
|
assert "by_type" in data["local_energy"]
|
|
else:
|
|
assert data["local_energy"] is None
|
|
|
|
|
|
def test_metrics_local_energy_enabled_reflects_seeded_row(
|
|
tmp_path, monkeypatch
|
|
):
|
|
"""When enabled and a row is seeded, local_energy carries expected totals."""
|
|
conn = _make_db(tmp_path)
|
|
_seed_models(conn)
|
|
_seed_local_energy(conn)
|
|
conn.close()
|
|
|
|
monkeypatch.setattr(dispatcher.cfg.database, "path", str(tmp_path / "test.db"))
|
|
monkeypatch.setattr(dispatcher.cfg.verification, "local_llm_enabled", False)
|
|
monkeypatch.setattr(dispatcher.cfg.routing, "require_vision", False)
|
|
monkeypatch.setattr(dispatcher.cfg.local_energy, "enabled", True)
|
|
monkeypatch.setattr(dispatcher.cfg.local_energy, "tariff_usd_per_kwh", 0.12)
|
|
monkeypatch.setenv("NEURALWATT_API_KEY", "test-key")
|
|
|
|
with TestClient(dispatcher.app) as client:
|
|
resp = client.get("/metrics")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
le = data["local_energy"]
|
|
assert isinstance(le, dict)
|
|
assert "metered_kwh_30d" in le
|
|
assert "metered_cost_usd_30d" in le
|
|
assert "calls_30d" in le
|
|
assert "by_type" in le
|
|
assert "reset_date" in le
|
|
assert le["metered_kwh_30d"] == round(0.00015, 5)
|
|
assert le["metered_cost_usd_30d"] == round(1.8e-05, 5)
|
|
assert le["calls_30d"] == 1
|
|
assert "classify" in le["by_type"]
|
|
assert le["by_type"]["classify"]["calls"] == 1
|
|
assert le["by_type"]["classify"]["kwh"] == round(0.00015, 5)
|
|
assert le["by_type"]["classify"]["cost_usd"] == round(1.8e-05, 5)
|
|
|
|
|
|
def test_metrics_empty_db_returns_200(monkeypatch, tmp_path):
|
|
"""Fresh empty temp DB: 200 with empty arrays, no exception."""
|
|
conn = _make_db(tmp_path)
|
|
conn.close()
|
|
monkeypatch.setattr(dispatcher.cfg.database, "path", str(tmp_path / "test.db"))
|
|
monkeypatch.setattr(dispatcher.cfg.verification, "local_llm_enabled", False)
|
|
monkeypatch.setattr(dispatcher.cfg.routing, "require_vision", False)
|
|
monkeypatch.setattr(dispatcher.cfg.local_energy, "enabled", False)
|
|
monkeypatch.setenv("NEURALWATT_API_KEY", "test-key")
|
|
|
|
with TestClient(dispatcher.app) as client:
|
|
resp = client.get("/metrics")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["recent_decisions"] == []
|
|
assert data["per_model"] == []
|
|
assert data["verdict_mix"] == {}
|
|
assert data["top_proficiency"] == []
|
|
assert "local_energy" in data
|
|
assert data["local_energy"] is None
|
|
assert "generated_at" in data
|
|
|
|
|
|
# admin_model_overrides is NOT in schema.sql — dispatcher creates it at
|
|
# startup via ensure_route_decisions-style migration, so endpoint tests that
|
|
# need it build it with extra_sql (pattern from tests/test_metrics.py).
|
|
_ADMIN_TABLE_SQL = """
|
|
CREATE TABLE IF NOT EXISTS admin_model_overrides (
|
|
model_id TEXT NOT NULL,
|
|
provider TEXT NOT NULL,
|
|
availability TEXT NOT NULL,
|
|
reason TEXT,
|
|
updated_at TEXT NOT NULL,
|
|
PRIMARY KEY (model_id, provider)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_admin_model_overrides_availability
|
|
ON admin_model_overrides (availability);
|
|
"""
|
|
|
|
|
|
def test_metrics_coverage_warnings_include_rejection_and_capability(
|
|
tmp_path, monkeypatch
|
|
):
|
|
"""GET /metrics surfaces the new rejection and capability warnings.
|
|
|
|
Seeds the 2026-09-04 incident state — kimi-k3 and kimi-k3-fast (the only
|
|
vision-capable tier-1 models) deprecated via admin overrides, plus two
|
|
image rejections — into the endpoint's temp DB, and asserts the warnings
|
|
reach the same coverage.warnings list the admin bell and TUI render.
|
|
The rejection warning is the deterministic assertion; on this exact seed
|
|
the capability warning fires too.
|
|
"""
|
|
now = _now().isoformat()
|
|
conn = _make_db(tmp_path, extra_sql=_ADMIN_TABLE_SQL)
|
|
for model_id, vision, eff_ctx in (
|
|
("kimi-k3", 1, 782324),
|
|
("kimi-k3-fast", 1, 782324),
|
|
("deepseek-v4-flash", 0, 262128),
|
|
):
|
|
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', ?, 1, ?, ?, 16384, 0.30, 0.10,
|
|
?, 1, 'standard', 'default', 'full', 'public', 'active',
|
|
?)
|
|
""",
|
|
(model_id, model_id, eff_ctx, eff_ctx, vision, now),
|
|
)
|
|
for model_id in ("kimi-k3", "kimi-k3-fast"):
|
|
conn.execute(
|
|
"INSERT INTO admin_model_overrides "
|
|
"(model_id, provider, availability, reason, updated_at) "
|
|
"VALUES (?, 'neuralwatt', 'deprecated', 'cost', ?)",
|
|
(model_id, now),
|
|
)
|
|
for tokens in (242486, 195000):
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO route_decisions (
|
|
observed_at, kind, task_category, task_tier,
|
|
required_context_tokens, latency_tolerance,
|
|
selected_model, selected_provider, rejected_reason,
|
|
tools, images, json_mode, streamed
|
|
) VALUES (?, 'chat', 'coding_general', 1, 200000,
|
|
'interactive', NULL, NULL, ?, 0, 1, 0, 0)
|
|
""",
|
|
(
|
|
now,
|
|
f"context >= {tokens} tokens; "
|
|
f"vision-capable model (request carries image(s))",
|
|
),
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
monkeypatch.setattr(dispatcher.cfg.database, "path", str(tmp_path / "test.db"))
|
|
monkeypatch.setattr(dispatcher.cfg.verification, "local_llm_enabled", False)
|
|
monkeypatch.setattr(dispatcher.cfg.routing, "require_vision", False)
|
|
monkeypatch.setattr(dispatcher.cfg.local_energy, "enabled", False)
|
|
monkeypatch.setenv("NEURALWATT_API_KEY", "test-key")
|
|
|
|
with TestClient(dispatcher.app) as client:
|
|
resp = client.get("/metrics")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
warnings = data["coverage"]["warnings"]
|
|
assert len(warnings) > 0
|
|
assert any("rejection" in w for w in warnings)
|
|
# The capability warning is secondary per the plan (it depends on the
|
|
# seeded models) but this seed reproduces the incident, so it fires.
|
|
assert any("vision" in w for w in warnings)
|
|
|
|
|
|
def _sse_frame(line: str | bytes) -> dict:
|
|
"""Parse one ``data: <json>`` SSE line and return the JSON payload."""
|
|
if isinstance(line, bytes):
|
|
line = line.decode()
|
|
assert line.startswith("data: "), f"unexpected SSE frame: {line!r}"
|
|
return json.loads(line[len("data: ") :])
|
|
|
|
|
|
def test_events_decisions_returns_sse_headers(seeded_client, monkeypatch):
|
|
"""The endpoint announces text/event-stream and no-cache headers.
|
|
|
|
The real stream is unbounded (it blocks for heartbeats), so the generator
|
|
is stubbed to a bounded one to let TestClient read the whole body without
|
|
hanging. The live replay/stream behaviour is covered by the deterministic
|
|
generator test below.
|
|
"""
|
|
events.clear()
|
|
try:
|
|
|
|
def _bounded():
|
|
yield "retry: 3000\n\n"
|
|
yield "data: {\"id\": 9}\n\n"
|
|
|
|
monkeypatch.setattr(dispatcher, "_decision_event_stream", _bounded)
|
|
resp = seeded_client.get("/events/decisions")
|
|
assert resp.status_code == 200
|
|
assert "text/event-stream" in resp.headers["content-type"]
|
|
assert "no-cache" in resp.headers.get("cache-control", "")
|
|
assert "data: {\"id\": 9}" in resp.text
|
|
finally:
|
|
events.clear()
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_decision_event_stream_replays_then_streams_live():
|
|
"""Drive the generator directly: replay first, then a live publish arrives
|
|
as the next frame. No HTTP client or threads, so it is deterministic."""
|
|
events.clear()
|
|
try:
|
|
events.publish_decision(
|
|
{"id": 1, "selected_model": "cheap", "task_category": "coding"}
|
|
)
|
|
stream = dispatcher._decision_event_stream()
|
|
retry = await anext(stream)
|
|
assert retry.startswith("retry:")
|
|
replayed = await anext(stream)
|
|
assert _sse_frame(replayed)["id"] == 1
|
|
|
|
events.publish_decision(
|
|
{"id": 2, "selected_model": "tiny", "task_category": "debugging"}
|
|
)
|
|
live = await anext(stream)
|
|
assert _sse_frame(live)["id"] == 2
|
|
finally:
|
|
events.clear()
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_persist_route_decision_publishes_profile_in_sse_payload(
|
|
tmp_path, monkeypatch,
|
|
):
|
|
import asyncio
|
|
|
|
events.clear()
|
|
db_path = tmp_path / "profile-sse.db"
|
|
conn = sqlite3.connect(db_path)
|
|
conn.executescript(SCHEMA_SQL)
|
|
conn.close()
|
|
|
|
monkeypatch.setattr(dispatcher.cfg.database, "path", str(db_path))
|
|
monkeypatch.setattr(dispatcher.cfg.logging, "log_route_decisions", True)
|
|
|
|
sse_queue: asyncio.Queue[dict] = asyncio.Queue()
|
|
try:
|
|
events.subscribe_sse(sse_queue, replay=True)
|
|
|
|
dispatcher.persist_route_decision(
|
|
"route",
|
|
classification=dispatcher.RouteResponse(
|
|
classification=dispatcher.Classification(
|
|
task_category="coding_general", task_tier=2,
|
|
required_context_tokens=100, confidence=0.9,
|
|
),
|
|
latency_tolerance="interactive",
|
|
profile="batch",
|
|
selected=dispatcher.Candidate(
|
|
model_id="cheap", provider="neuralwatt",
|
|
tier=2, latency_class="standard", reasoning_mode="default",
|
|
context_variant="full", effective_context_window=128000,
|
|
composite=0.5, cost_score=0.5, proficiency_score=0.9,
|
|
),
|
|
candidates_considered=1,
|
|
),
|
|
latency_tolerance="interactive",
|
|
selected_model="cheap",
|
|
selected_provider="neuralwatt",
|
|
)
|
|
|
|
decision = await asyncio.wait_for(sse_queue.get(), timeout=2.0)
|
|
assert decision["profile"] == "batch"
|
|
finally:
|
|
events.unsubscribe_sse(sse_queue)
|
|
events.clear()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# content_fault_warnings unit tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
from types import SimpleNamespace as _SN # noqa: E402
|
|
|
|
|
|
def _cfw_cfg():
|
|
return _SN(
|
|
objective=_SN(
|
|
rejection_warning_window_hours=1,
|
|
rejection_warning_baseline_hours=24,
|
|
rejection_warning_min_count=6,
|
|
)
|
|
)
|
|
|
|
|
|
def _cfw_conn(tmp_path):
|
|
conn = sqlite3.connect(str(tmp_path / "cfw_test.db"))
|
|
conn.row_factory = sqlite3.Row
|
|
conn.execute("""
|
|
CREATE TABLE verifications (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
model_id TEXT NOT NULL,
|
|
provider TEXT NOT NULL,
|
|
request_id TEXT,
|
|
task_category TEXT,
|
|
kind TEXT NOT NULL,
|
|
verdict TEXT NOT NULL,
|
|
detail TEXT,
|
|
completion_tokens INTEGER,
|
|
observed_at TEXT NOT NULL,
|
|
model_attributable INTEGER DEFAULT 1
|
|
)
|
|
""")
|
|
conn.commit()
|
|
return conn
|
|
|
|
|
|
def _cfw_insert(conn, model, provider, observed_at, attributable=1):
|
|
conn.execute(
|
|
"INSERT INTO verifications "
|
|
"(model_id, provider, task_category, request_id, kind, verdict, "
|
|
" detail, completion_tokens, model_attributable, observed_at) "
|
|
"VALUES (?, ?, 'coding_general', ?, 'structural', 'malformed', "
|
|
" 'empty response', 0, ?, ?)",
|
|
(model, provider, f"req-{model}", attributable, observed_at),
|
|
)
|
|
|
|
|
|
def test_content_fault_no_malformed_returns_empty(tmp_path):
|
|
from metrics import content_fault_warnings
|
|
conn = _cfw_conn(tmp_path)
|
|
try:
|
|
assert content_fault_warnings(conn, _cfw_cfg()) == []
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def test_content_fault_high_rate_triggers_known_warning(tmp_path):
|
|
from metrics import content_fault_warnings
|
|
conn = _cfw_conn(tmp_path)
|
|
try:
|
|
now = datetime.now(timezone.utc)
|
|
for i in range(7):
|
|
_cfw_insert(conn, "bad-model", "openrouter",
|
|
(now - timedelta(minutes=i)).isoformat())
|
|
conn.commit()
|
|
warnings = content_fault_warnings(conn, _cfw_cfg())
|
|
assert len(warnings) >= 1
|
|
assert any("bad-model" in w for w in warnings)
|
|
assert any("content fault:" in w for w in warnings)
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def test_content_fault_novel_pattern_triggers_new_warning(tmp_path):
|
|
from metrics import content_fault_warnings
|
|
conn = _cfw_conn(tmp_path)
|
|
try:
|
|
now = datetime.now(timezone.utc)
|
|
for i in range(3):
|
|
_cfw_insert(conn, "novel-model", "openrouter",
|
|
(now - timedelta(minutes=i)).isoformat())
|
|
conn.commit()
|
|
warnings = content_fault_warnings(conn, _cfw_cfg())
|
|
assert len(warnings) >= 1
|
|
assert any("new content fault:" in w for w in warnings)
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def test_content_fault_low_count_does_not_trigger(tmp_path):
|
|
from metrics import content_fault_warnings
|
|
conn = _cfw_conn(tmp_path)
|
|
try:
|
|
now = datetime.now(timezone.utc)
|
|
_cfw_insert(conn, "low-model", "openrouter",
|
|
(now - timedelta(hours=20)).isoformat())
|
|
for i in range(3):
|
|
_cfw_insert(conn, "low-model", "openrouter",
|
|
(now - timedelta(minutes=i)).isoformat())
|
|
conn.commit()
|
|
assert content_fault_warnings(conn, _cfw_cfg()) == []
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def test_content_fault_non_attributable_excluded(tmp_path):
|
|
from metrics import content_fault_warnings
|
|
conn = _cfw_conn(tmp_path)
|
|
try:
|
|
now = datetime.now(timezone.utc)
|
|
for i in range(10):
|
|
_cfw_insert(conn, "tool-model", "openrouter",
|
|
(now - timedelta(minutes=i)).isoformat(),
|
|
attributable=0)
|
|
conn.commit()
|
|
assert content_fault_warnings(conn, _cfw_cfg()) == []
|
|
finally:
|
|
conn.close()
|