"""Baseline no-header route_decisions snapshot (Task 3, pre-wiring fixture). A committed test cannot read a gitignored ``.omo/evidence/`` path on a clean checkout, so the pre-wiring ``route_decisions`` row for a HEADERLESS request is frozen as an ordinary JSON fixture under ``tests/fixtures/`` and committed as its OWN commit BEFORE the wiring that adds conversation identity. The Success Criteria [H6] for identity wiring requires this snapshot to reproduce byte-identically after wiring: a no-header request must still land under the same session fingerprint with NULL agent/parent_key, so nothing about the row may change. Columns that record measured wall-clock time are excluded (``classifier_ms``, ``router_wall_seconds``, ``router_ttft_seconds``, ``observed_at``); ``id`` is excluded as it is an autoincrement. This module owns the shared dump/compare helpers so ``test_conversation_wiring.py`` can reuse the exact same normalization when it asserts the byte-identical snapshot as acceptance case 3. """ import json import sqlite3 from pathlib import Path import pytest from starlette.testclient import TestClient import dispatcher from config import RoutingProfile from dispatcher import Classification, app FIXTURES_DIR = Path(__file__).parent / "fixtures" SNAPSHOT_PATH = FIXTURES_DIR / "route_decision_no_header.json" # Columns that record measured wall-clock time (or an autoincrement id) must # never appear in the snapshot, or the frozen row would drift run to run. # [H6] requires router_wall_seconds and router_ttft_seconds in this list. EXCLUDED_COLS = ( "id", "observed_at", "classifier_ms", "router_wall_seconds", "router_ttft_seconds", ) CHEAP = "cheap-model" DEAR = "dear-model" class FakeResponse: """Just enough of requests.Response for the dispatcher path.""" def __init__(self, payload=None, *, status_code=200): self.status_code = status_code self._payload = payload or {} self.text = json.dumps(self._payload) self.headers = {} self.request = None def json(self): return self._payload def _completion(model): return { "id": "chatcmpl-test", "object": "chat.completion", "choices": [ { "index": 0, "message": {"role": "assistant", "content": "ok"}, "finish_reason": "stop", } ], "usage": {"prompt_tokens": 30, "completion_tokens": 5}, "model": model, } def make_router(tmp_path, monkeypatch): """Point the dispatcher at a throwaway catalog with nothing dialled out. Mirrors the ``router`` fixture in ``test_chat_completions.py``: two models with a classifying stub and a fake requests.post, so one routed POST is deterministic and offline. Returns ``(TestClient, db_path)``. """ db_path = tmp_path / "test.db" conn = sqlite3.connect(db_path) schema = (Path(__file__).resolve().parent.parent / "config" / "schema.sql").read_text() conn.executescript(schema) 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.setattr(dispatcher.cfg.session_cache, "enabled", False) monkeypatch.setattr(dispatcher.cfg.objective, "incumbent_cache_pricing", False) monkeypatch.setattr(dispatcher.cfg.exploration, "epsilon", 0.0) monkeypatch.setenv("NEURALWATT_API_KEY", "test-key") def fake_post(url, headers=None, json=None, stream=False, timeout=None): 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, ), ) return TestClient(app), db_path def no_header_payload(): return {"model": "auto", "messages": [{"role": "user", "content": "write a function"}]} def post_no_header(client): """POST one headerless routed request, returning the raw response.""" return client.post("/v1/chat/completions", json=no_header_payload()) def normalized_route_decision(db_path): """The single route_decisions row as a normalized dict for snapshotting. Returns the row with ``id``/``observed_at``/``classifier_ms``/ ``router_wall_seconds``/``router_ttft_seconds`` dropped and None-values kept (so agent/parent_key NULL is visible and byte-stable). """ conn = sqlite3.connect(db_path) conn.row_factory = sqlite3.Row row = conn.execute( "SELECT * FROM route_decisions ORDER BY id DESC LIMIT 1" ).fetchone() conn.close() assert row is not None, "expected exactly one route_decisions row" return { key: row[key] for key in row.keys() if key not in EXCLUDED_COLS and not key.startswith("__") } def serialize(data): """Byte-stable JSON serialization for the snapshot comparison.""" return json.dumps(data, sort_keys=True, indent=2, ensure_ascii=True) + "\n" def assert_matches_snapshot(db_path): """Assert the current no-header row reproduces the committed snapshot.""" current = normalized_route_decision(db_path) assert SNAPSHOT_PATH.exists(), ( f"snapshot fixture missing; run the generator to write {SNAPSHOT_PATH}" ) expected = json.loads(SNAPSHOT_PATH.read_text()) assert current == expected, ( "no-header route_decisions row drifted from committed snapshot:\n" f"current={serialize(current)}\n" f"expected={serialize(expected)}" ) # Byte-identical: the serialized forms must match exactly. assert serialize(current) == SNAPSHOT_PATH.read_text() def test_no_header_route_decision_matches_committed_snapshot(tmp_path, monkeypatch): """A headerless request reproduces the committed pre-wiring snapshot.""" client, db_path = make_router(tmp_path, monkeypatch) resp = post_no_header(client) assert resp.status_code == 200, resp.text assert_matches_snapshot(db_path)