Files
6krrt/tests/test_admin_allowlist_and_fallback_contracts.py
adlee-was-taken 281f4fbe28 fix(admin): validate cloud base_urls, and let an allowlist note be corrected
Sweep findings #8 and #10. #9 turned out not to be a defect -- see below.

#8. CloudFallbackConfig types base_url as a plain str, so RouterConfig
accepted "notaurl" and it landed in the overlay, failing only later at
classify time -- when the local classifier is already down and this block is
the thing meant to save the request. POST /api/providers/{name} has always
rejected the same mistake with _validate_provider_url; both the cloud
fallback card and the classifier card's cloud_primary now use it too.

#10. Allowlist add was INSERT OR IGNORE, so re-adding an entry with a
corrected note returned 200 carrying the ORIGINAL note. There was no way to
fix a note through the API or the UI. Now an upsert on (provider, model_id)
that updates the note and leaves added_at alone: correcting a note is not a
re-admission.

#9 was my error, not the code's. The sweep flagged DELETE returning 200 for
an absent allowlist entry as a false confirmation, and I changed it to 404 --
which broke test_allowlist_delete_idempotent, a test that pins that exact
behaviour with a docstring saying so. That is a deliberate decision, so it
is reverted, with a comment at the endpoint pointing at the test.

The inconsistency the finding noticed is still real: profile delete and
model-override delete both 404 on a missing target while this one does not.
Which way that should resolve is a decision rather than a bug fix, so it
goes to the sweep plan as an open question instead of being settled here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRQXz5SYZYVWscxS1QqF6U
2026-09-08 18:39:17 -04:00

158 lines
6.0 KiB
Python

"""Endpoints that reported success without doing anything.
Sweep findings #8, #9 and #10. All three share a shape: a write path that
answers 200 while the thing the caller asked for did not happen.
"""
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):
"""Rooted at a throwaway config dir so no write touches the real overlay."""
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
ALLOWLISTED = "openrouter"
# --- #9: NOT a defect -- the idempotent delete is deliberate ---------------
#
# The sweep flagged DELETE returning 200 for an absent entry as a silent
# false confirmation. It is not an oversight: test_allowlist_delete_idempotent
# in test_admin_allowlist.py pins exactly that behaviour, docstring and all.
# The inconsistency with profile delete and model-override delete (both 404)
# is real and is recorded as an open question in the sweep plan; resolving it
# is a decision, not a bug fix, so nothing here changes it.
def test_deleting_an_absent_entry_is_a_deliberate_200(client):
c, _ = client
resp = c.delete(f"/admin/api/providers/{ALLOWLISTED}/allowlist/nope/nope")
assert resp.status_code == 200, resp.text
def test_deleting_a_real_entry_removes_it(client):
c, _ = client
c.post(f"/admin/api/providers/{ALLOWLISTED}/allowlist",
json={"model_id": "vendor/m", "note": "n"})
resp = c.delete(f"/admin/api/providers/{ALLOWLISTED}/allowlist/vendor/m")
assert resp.status_code == 200, resp.text
ids = [e["model_id"]
for e in c.get(f"/admin/api/providers/{ALLOWLISTED}/allowlist").json()]
assert "vendor/m" not in ids
# --- #10: re-adding could not correct a note -------------------------------
def test_re_adding_updates_the_note(client):
c, _ = client
c.post(f"/admin/api/providers/{ALLOWLISTED}/allowlist",
json={"model_id": "vendor/m", "note": "first"})
resp = c.post(f"/admin/api/providers/{ALLOWLISTED}/allowlist",
json={"model_id": "vendor/m", "note": "corrected"})
assert resp.status_code == 200, resp.text
assert resp.json()["note"] == "corrected"
entry = [e for e in c.get(f"/admin/api/providers/{ALLOWLISTED}/allowlist").json()
if e["model_id"] == "vendor/m"][0]
assert entry["note"] == "corrected"
def test_re_adding_does_not_duplicate_the_row(client):
c, _ = client
for note in ("a", "b", "c"):
c.post(f"/admin/api/providers/{ALLOWLISTED}/allowlist",
json={"model_id": "vendor/m", "note": note})
rows = [e for e in c.get(f"/admin/api/providers/{ALLOWLISTED}/allowlist").json()
if e["model_id"] == "vendor/m"]
assert len(rows) == 1
def test_re_adding_keeps_the_original_added_at(client):
"""Correcting a note is not a re-admission."""
c, _ = client
first = c.post(f"/admin/api/providers/{ALLOWLISTED}/allowlist",
json={"model_id": "vendor/m", "note": "first"}).json()
second = c.post(f"/admin/api/providers/{ALLOWLISTED}/allowlist",
json={"model_id": "vendor/m", "note": "second"}).json()
assert second["added_at"] == first["added_at"]
# --- #8: an unusable base_url persisted ------------------------------------
def test_cloud_fallback_rejects_a_non_url(client):
c, cfg_dir = client
resp = c.post("/admin/api/cloud-fallback-config",
json={"cloud_fallback": {"base_url": "notaurl",
"model": "m",
"api_key_env": "K"}})
assert resp.status_code == 422, resp.text
assert "http(s)" in resp.text
overlay = cfg_dir / "config.local.yaml"
if overlay.exists():
assert "notaurl" not in overlay.read_text()
def test_cloud_fallback_accepts_a_real_url(client):
c, cfg_dir = client
resp = c.post("/admin/api/cloud-fallback-config",
json={"cloud_fallback": {"base_url": "https://api.example.com/v1",
"model": "m",
"api_key_env": "K"}})
assert resp.status_code == 200, resp.text
assert "api.example.com" in (cfg_dir / "config.local.yaml").read_text()
def test_cloud_primary_rejects_a_non_url(client):
"""The classifier card had the same gap as the fallback card."""
c, _ = client
resp = c.post("/admin/api/classifier-config",
json={"mode": "cloud_llm", "cloud_primary_auto": False,
"cloud_primary": {"base_url": "ftp://nope", "model": "m"}})
assert resp.status_code == 422, resp.text
assert "http(s)" in resp.text
def test_clearing_the_fallback_still_works(client):
"""The None path is the documented remove signal and must stay reachable."""
c, cfg_dir = client
c.post("/admin/api/cloud-fallback-config",
json={"cloud_fallback": {"base_url": "https://api.example.com/v1",
"model": "m", "api_key_env": "K"}})
resp = c.post("/admin/api/cloud-fallback-config", json={"cloud_fallback": None})
assert resp.status_code == 200, resp.text
assert "api.example.com" not in (cfg_dir / "config.local.yaml").read_text()