"""The profiles page can see, and set, which profile bare `auto` resolves to. `routing.default_profile` decides what every client that does not name a profile actually gets -- including all 13 opencode agents, which send bare `llm-router/auto`. It was only reachable from a dropdown on the Controls page, and the profiles page could not even show which profile was live. """ from __future__ import annotations import sqlite3 from pathlib import Path import pytest import yaml from fastapi import FastAPI from starlette.testclient import TestClient import admin from config import load_config ROOT = Path(__file__).resolve().parent.parent SCHEMA_SQL = (ROOT / "config" / "schema.sql").read_text() @pytest.fixture def client(tmp_path): cfg_dir = tmp_path / "config" cfg_dir.mkdir() doc = yaml.safe_load((ROOT / "config" / "config.yaml").read_text()) (cfg_dir / "config.yaml").write_text(yaml.safe_dump(doc, sort_keys=False)) db_path = tmp_path / "t.db" conn = sqlite3.connect(db_path) conn.executescript(SCHEMA_SQL) admin.ensure_admin_tables(conn) conn.commit() conn.close() def _db(): c = sqlite3.connect(db_path) c.row_factory = sqlite3.Row return c cfg = load_config(cfg_dir / "config.yaml") app = FastAPI() app.include_router(admin.build_router(cfg, _db, str(tmp_path)), prefix="/admin") return TestClient(app), cfg_dir def _profiles(c): return {p["name"]: p for p in c.get("/admin/api/profiles").json()} def test_exactly_one_profile_is_marked_default(client): c, _ = client ps = _profiles(c) flagged = [n for n, p in ps.items() if p["is_default"]] assert flagged == ["default"], flagged def test_setting_the_default_moves_the_flag(client): c, _ = client resp = c.post("/admin/api/config/routing.default_profile", json={"value": "onlycheaps"}) assert resp.status_code == 200, resp.text ps = _profiles(c) assert ps["onlycheaps"]["is_default"] is True assert ps["default"]["is_default"] is False assert [n for n, p in ps.items() if p["is_default"]] == ["onlycheaps"] def test_the_flag_reflects_the_overlay_without_a_restart(client): """Read from the config store, not from cfg. cfg binds at import, so sourcing this from cfg would leave the page showing the old profile as default immediately after setting it -- the one moment the operator is looking straight at it. """ c, cfg_dir = client c.post("/admin/api/config/routing.default_profile", json={"value": "batch"}) assert "batch" in (cfg_dir / "config.local.yaml").read_text() assert _profiles(c)["batch"]["is_default"] is True def test_an_unknown_profile_is_still_refused(client): """The button reuses the allowlisted config endpoint, so it inherits the validation that already exists rather than a second rule that can drift.""" c, _ = client resp = c.post("/admin/api/config/routing.default_profile", json={"value": "ghostprofile"}) assert resp.status_code == 422 assert "not a known profile" in resp.text assert _profiles(c)["default"]["is_default"] is True def test_a_configured_profile_can_be_made_default(client): """Not just built-ins -- an overlay profile is a valid target too.""" c, _ = client assert c.post("/admin/api/profiles/", json={"name": "cheapo", "max_cost_per_1m_completion": 0.4} ).status_code == 200 assert c.post("/admin/api/config/routing.default_profile", json={"value": "cheapo"}).status_code == 200 assert _profiles(c)["cheapo"]["is_default"] is True def test_deleting_the_default_is_still_refused(client): """The UI disables the button; the endpoint must still hold the line.""" c, _ = client c.post("/admin/api/profiles/", json={"name": "cheapo", "min_tier": 1}) c.post("/admin/api/config/routing.default_profile", json={"value": "cheapo"}) resp = c.delete("/admin/api/profiles/cheapo") assert resp.status_code == 422 assert "default_profile" in resp.text def test_setting_the_default_applies_to_the_live_config(client): """The write is not deferred to a restart, and the endpoint says so. `build_router(cfg, ...)` is handed the SAME RouterConfig object the dispatcher holds, and `_resolve_profile` reads `cfg.routing.default_profile` per request -- so mutating it lands on the very next route. Verified end to end against a live instance: POST the key, then `/route` reports the new profile with no bounce. """ c, _ = client before = c.get("/admin/api/active-profile").json() assert before["active"] == "default" assert c.post("/admin/api/config/routing.default_profile", json={"value": "batch"}).status_code == 200 # `active` is read off the running cfg; `boots_to` is read off disk. Both # move, and `active` moving is the half a restart would otherwise be for. after = c.get("/admin/api/active-profile").json() assert after["active"] == "batch" assert after["boots_to"] == "batch" def test_the_endpoint_promises_no_restart_for_the_default_profile(client): """The message is the page's only source for the restart claim. The profiles page used to assert "A restart is required before dispatch sees this change" after every save, unconditionally, while this endpoint was already returning the opposite. Operators were told to bounce a service that had picked the change up on the previous request. The page now renders whatever comes back here, so this string is load-bearing. """ c, _ = client body = c.post("/admin/api/config/routing.default_profile", json={"value": "batch"}).json() assert "no restart" in body["message"].lower() def test_a_key_that_does_need_a_restart_still_says_so(client): """The narrow exemption must stay narrow. Only routing.default_profile has a live counterpart. Every other allowlisted key genuinely does need the bounce, so the restart message has to remain the default answer rather than disappearing wholesale. """ c, _ = client body = c.post("/admin/api/config/logging.level", json={"value": "DEBUG"}).json() assert "restart is required" in body["message"].lower() def test_the_profiles_page_never_hardcodes_a_restart_claim(): """Guarding the actual defect: a claim the page made on its own. Three separate strings said a reload was needed -- a sticky banner, the page subtitle, and the delete dialog -- none of them consulting the API. The banner is now driven by the response `message`; the two prose lines are gone. Asserted as an absence, since the failure mode is the claim coming back, not the fix being undone. """ html = (ROOT / "admin" / "frontend" / "profiles.html").read_text() # Comments are stripped first: the code comment explaining this fix quotes # the old string, and a test that fails on its own rationale is a test # nobody keeps. live = "\n".join( line for line in html.splitlines() if not line.lstrip().startswith("//") ) assert "_showRestartHint" not in live assert "A restart is required before dispatch sees this change" not in live assert "service reload is required" not in live.lower() # The banner must read the server rather than decide for itself. assert "_saveNotice = (result && result.message)" in html