All verification complete. Final report: **Phase 172 final verification pass — all tasks (01–05) were already complete; verified the whole phase, no code fixes needed.** - **Verified implementation:** `Settings.llm_thinking_level: str = "low"` + `CATALOG` row (`LLM (aipi)` / "Thinking level", plain: not secret/restart/overridable-exempt) + `.env.example` line under `BOR_LLM_CHAT_MODEL` + README table row; `_thinking_extra_body()` → `reasoning_effort` via `extra_body` on `chat()` + `chat_stream()` when trimmed-non-empty, key absent (byte-identical) when empty, embeddings untouched; per-turn line gains `thinking=<value>` (`-` when empty) before `thinking_chars`. - **Tests/lint/coverage (all green):** `uv run pytest` → 4084 passed · `uv run pytest --cov=app --cov-report=term-missing` → 4084 passed, `app/` **99%** (>90%) · `uv run ruff check .` → All checks passed · `uv run pyright` → 0 errors, 0 warnings. - **Completion criteria:** (1) `pytest -k "config and thinking"` 4 passed + `.env.example` grep ✓ (2) live `GET /api/admin/settings` curl shows the row (str, `low`, `default`) + integration PUT/DELETE round-trip ✓ (3) 9 mocked-client unit tests assert `extra_body` present/absent ✓ (4) log-line tests assert `low`/`high`/`-`/whitespace ✓ (5) suite + coverage + lint ✓ (6) `test_thinking_level.py` 3/3 in isolation; `test_admin_settings_panel.py` 12/12; `test_admin_settings_layout.py` 5/5 ✓ (7) atomic commit + phase move = the harness's job (task files already in `complete/`; I ran no git index/history commands). - **Documentation:** README.md reviewed in full — verified consistent (env table complete vs `.env.example`, catalog restart/non-overridable claims re-verified at runtime); AGENTS.md reviewed in full — verified consistent (no rule affected, A5 = no UI change). No changes needed. **Flag (not fixable here):** PLAN.md §9's per-turn-line spec predates the phase-172 `thinking=` field; PLAN.md is off-limits per protocol. - **Next pending phase:** none — `todo/` contains only phase 172 (the harness moves it on this pass's success).
605 lines
27 KiB
Python
605 lines
27 KiB
Python
"""Integration: the admin settings panel's read/write surface (phase 168,
|
|
task 04) — ``/api/admin/settings`` GET / PUT ``/{key}`` / DELETE
|
|
``/{key}`` / DELETE all, driven through the real app (TestClient keeps
|
|
the cookie jar — the house ``test_ui_settings_api`` /
|
|
``test_tokens_api`` pattern).
|
|
|
|
The pins (00_phase, Testing & Quality):
|
|
|
|
* the auth matrix — anonymous AND a signed-in token USER get 403 on ALL
|
|
four routes (the router-wide ``require_admin`` closes the surface on
|
|
every method), the admin 200s on all four;
|
|
* the shape — ``items`` length == ``len(Settings.model_fields)`` (75),
|
|
groups in ``GROUPS`` order (the stable order the E2E pins against),
|
|
``totals.count`` / ``overridden`` correct, ``overlay_enabled`` true;
|
|
on an empty table a field whose ``BOR_*`` var the test env sets
|
|
reports ``source="env"``, an untouched one ``source="default"`` and
|
|
none reports ``"db"``;
|
|
* write → live — a saved ``relevance_threshold`` is in
|
|
``get_settings()`` with NO restart (the cache clear + task-03 hooks
|
|
are the whole job), the row is in the table with a non-null
|
|
``updated_at``, and the DB value reaches the ANONYMOUS
|
|
``/api/config`` endpoint through the same overlay;
|
|
* errors — unknown key 404; the two LOCKED A3 keys 422 naming the env
|
|
var; a bad shape 422 naming the field with NOTHING stored; a
|
|
cross-field pair that only breaks in combination (the second PUT 422s
|
|
and the first row is unchanged);
|
|
* masking (LOCKED A4) — a stored secret reports ``masked``/``has_value``
|
|
only, the plaintext appears NOWHERE in the raw response text, a blank
|
|
secret PUT 422s, and a DELETE clears it;
|
|
* reset — a DELETE ``/{key}`` restores the env value (idempotent);
|
|
DELETE ``""`` clears everything (``{"cleared": 3}``);
|
|
* the kill switch (D6) — ``BOR_SETTINGS_DB=0``: ``overlay_enabled``
|
|
false, a PUT still stores the row, ``get_settings()`` still reports
|
|
the env value — the switch disables the mechanism, not the panel;
|
|
* the byte budget — the whole catalogue stays under the phase-167 INFO
|
|
bar (64 KB), so it never floods the log;
|
|
* fail-soft boot — a malformed row inserted DIRECTLY into the table
|
|
(bypassing the API's 422 gate) never bricks: a fresh ``create_app()``
|
|
runs with it in the table, the row is skipped with a logged reason,
|
|
the rest of the table still applies, and ``/api/health`` is 200.
|
|
|
|
The module's teardown leaves NO row behind (the same discipline
|
|
``test_admin_theme_tab.py`` applies to ``ui_settings`` — a surviving
|
|
row would retune every later suite through the overlay).
|
|
|
|
Real Postgres (``podman compose up -d db``); no LLM involved.
|
|
|
|
Requires: podman compose up -d db
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from collections.abc import Iterator
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from sqlalchemy import text
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.api import chat as chat_api
|
|
from app.config import Settings, get_settings, invalidate_settings
|
|
from app.core import settings_overlay
|
|
from app.core.settings_catalog import CATALOG, GROUPS
|
|
from app.core.settings_overlay import register_invalidator
|
|
from app.main import app as fastapi_app
|
|
from app.main import create_app
|
|
from tests.conftest import ADMIN_PASSWORD
|
|
|
|
API = "/api/admin/settings"
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def clean_state(
|
|
db: Session, monkeypatch: pytest.MonkeyPatch
|
|
) -> Iterator[None]:
|
|
"""A clean ``app_settings`` table + the overlay switch ON + a fresh
|
|
overlay/hook slate (the ``test_admin_settings_live`` idiom: reset
|
|
the process-wide hook list and re-wire what ``create_app()``
|
|
wires, so a save in this module runs the chat runtime reset).
|
|
Teardown deletes the rows + invalidates + clears the cache — NO row
|
|
may survive this module (a surviving row would retune every later
|
|
suite through the overlay).
|
|
"""
|
|
settings_overlay.reset_state_for_tests()
|
|
register_invalidator(chat_api.reset_runtime_caches)
|
|
db.execute(text("DELETE FROM app_settings"))
|
|
db.commit()
|
|
monkeypatch.setenv("BOR_SETTINGS_DB", "1")
|
|
invalidate_settings()
|
|
yield
|
|
db.execute(text("DELETE FROM app_settings"))
|
|
db.commit()
|
|
invalidate_settings()
|
|
get_settings.cache_clear()
|
|
|
|
|
|
def _admin_client() -> TestClient:
|
|
"""A fresh client signed in as the admin (the ``_admin_client``
|
|
pattern from ``test_ui_settings_api``)."""
|
|
c = TestClient(fastapi_app)
|
|
r = c.post("/api/login", json={"password": ADMIN_PASSWORD})
|
|
assert r.status_code == 204, f"admin login failed: {r.status_code} {r.text}"
|
|
return c
|
|
|
|
|
|
def _token_user_client(admin: TestClient, anon: TestClient) -> TestClient:
|
|
"""A signed-in token USER (role ``"user"`` — issue the token the way
|
|
``test_tokens_api`` does): the only non-admin identity, 403 on this
|
|
surface on every method (the phase-79 token matrix contract)."""
|
|
r = admin.post("/api/tokens", json={"label": "alice"})
|
|
assert r.status_code == 201, r.text
|
|
token = r.json()["token"]
|
|
assert anon.post("/api/token-auth", json={"token": token}).status_code == 204
|
|
return anon
|
|
|
|
|
|
def _item(body: dict, key: str) -> dict:
|
|
"""One catalogue item by key (the GET body's ``items`` list)."""
|
|
for item in body["items"]:
|
|
if item["key"] == key:
|
|
return item
|
|
raise AssertionError(f"catalogue item missing: {key}")
|
|
|
|
|
|
def test_auth_matrix_all_four_routes(client: TestClient, db: Session) -> None:
|
|
"""Anonymous AND token user: 403 ``admin only`` on ALL four routes
|
|
(the router-wide dependency closes the surface on every method);
|
|
the admin passes on all four."""
|
|
admin = _admin_client()
|
|
token_user = _token_user_client(admin, client)
|
|
|
|
for caller in (client, token_user):
|
|
r = caller.get(API)
|
|
assert r.status_code == 403 and r.json() == {"detail": "admin only"}
|
|
r = caller.put(f"{API}/relevance_threshold", json={"value": "0.5"})
|
|
assert r.status_code == 403 and r.json() == {"detail": "admin only"}
|
|
r = caller.delete(f"{API}/relevance_threshold")
|
|
assert r.status_code == 403 and r.json() == {"detail": "admin only"}
|
|
r = caller.delete(API)
|
|
assert r.status_code == 403 and r.json() == {"detail": "admin only"}
|
|
|
|
r = admin.get(API)
|
|
assert r.status_code == 200
|
|
r = admin.put(f"{API}/top_n_docs", json={"value": "3"})
|
|
assert r.status_code == 200, r.text
|
|
r = admin.delete(f"{API}/top_n_docs")
|
|
assert r.status_code == 200, r.text
|
|
r = admin.delete(API)
|
|
assert r.status_code == 200, r.text
|
|
assert r.json() == {"cleared": 0}
|
|
assert db.execute(text("SELECT count(*) FROM app_settings")).scalar() == 0
|
|
|
|
|
|
def test_catalogue_shape_and_sources_on_empty_table(admin_client) -> None:
|
|
"""The whole catalogue, grouped and stable: 75 items (exactly
|
|
``Settings.model_fields``), groups in ``GROUPS`` order (each group a
|
|
contiguous block, the block order = ``GROUPS``), ``totals`` correct,
|
|
``overlay_enabled`` true. On an empty table: the env-var-set field
|
|
reports ``"env"``, the untouched one ``"default"`` and NOTHING
|
|
reports ``"db"``."""
|
|
r = admin_client.get(API)
|
|
assert r.status_code == 200, r.text
|
|
body = r.json()
|
|
items = body["items"]
|
|
assert len(items) == len(Settings.model_fields)
|
|
assert body["totals"] == {
|
|
"count": len(Settings.model_fields),
|
|
"overridden": 0,
|
|
}
|
|
assert body["overlay_enabled"] is True
|
|
|
|
# The stable order: each group a contiguous block, block order GROUPS.
|
|
groups_seen: list[str] = []
|
|
for item in items:
|
|
assert item["group"] in GROUPS
|
|
if not groups_seen or groups_seen[-1] != item["group"]:
|
|
groups_seen.append(item["group"])
|
|
assert groups_seen == [g for g in GROUPS if g in groups_seen]
|
|
assert len(groups_seen) == len(set(groups_seen))
|
|
# Every item carries the full contract (the panel generates from it).
|
|
for item in items:
|
|
assert set(item) == {
|
|
"key", "env_var", "group", "label", "type", "secret",
|
|
"restart_required", "overridable", "source", "value",
|
|
"masked", "has_value", "has_override", "db_value", "default",
|
|
}
|
|
assert item["env_var"] == "BOR_" + item["key"].upper()
|
|
assert item["source"] in ("db", "env", "default")
|
|
|
|
# Empty table: env / default / never db.
|
|
sources = {item["key"]: item["source"] for item in items}
|
|
assert "db" not in sources.values()
|
|
assert sources["input_placeholder"] == "env" # BOR_… set in tests/conftest.py
|
|
assert sources["docs_work_dir"] == "default" # no BOR_… in the test env
|
|
assert sources["relevance_threshold"] == "env" # pinned 0.30 in conftest
|
|
|
|
|
|
def test_write_is_live_without_restart(admin_client, db: Session) -> None:
|
|
"""Write → live (no restart): the saved value is in ``get_settings()``
|
|
in the SAME process, the row is in the table with a non-null
|
|
``updated_at``, the item reports ``source="db"`` and
|
|
``totals.overridden == 1`` — and the DB value reaches the ANONYMOUS
|
|
``/api/config`` endpoint through the same overlay (the DB-over-env
|
|
rule is deployment-wide, not panel-local)."""
|
|
assert get_settings().relevance_threshold == 0.30 # the conftest env value
|
|
|
|
r = admin_client.put(f"{API}/relevance_threshold", json={"value": "0.5"})
|
|
assert r.status_code == 200, r.text
|
|
body = r.json()
|
|
assert body["key"] == "relevance_threshold"
|
|
assert body["source"] == "db"
|
|
assert body["has_override"] is True
|
|
assert body["db_value"] == "0.5"
|
|
assert get_settings().relevance_threshold == 0.5 # NO restart (task 03)
|
|
|
|
row = db.execute(
|
|
text("SELECT value, updated_at FROM app_settings WHERE key = 'relevance_threshold'")
|
|
).first()
|
|
assert row is not None
|
|
assert row[0] == "0.5"
|
|
assert row[1] is not None # the server-side now()
|
|
|
|
r = admin_client.get(API)
|
|
assert r.json()["totals"]["overridden"] == 1
|
|
|
|
# Visible through the public config endpoint (no auth) — the same
|
|
# overlay retunes every consumer at once (D2).
|
|
anon = TestClient(fastapi_app)
|
|
r = anon.get("/api/config")
|
|
assert r.status_code == 200
|
|
assert r.json()["input_placeholder"] == "Ask me anything…"
|
|
r = admin_client.put(f"{API}/input_placeholder", json={"value": "Type your question…"})
|
|
assert r.status_code == 200, r.text
|
|
r = anon.get("/api/config")
|
|
assert r.status_code == 200
|
|
assert r.json()["input_placeholder"] == "Type your question…"
|
|
|
|
|
|
def test_put_errors_404_and_422(admin_client, db: Session) -> None:
|
|
"""The write-path guards (LOCKED A5, A3, A4): unknown key 404; the
|
|
two A3 keys 422 naming the env var; a bad shape 422 naming the FIELD
|
|
with NOTHING stored; a cross-field pair that only breaks in
|
|
combination — the second PUT 422s and the first row is unchanged."""
|
|
r = admin_client.put(f"{API}/bogus_key", json={"value": "1"})
|
|
assert r.status_code == 404
|
|
assert r.json() == {"detail": "Unknown setting bogus_key"}
|
|
|
|
r = admin_client.put(f"{API}/database_url", json={"value": "postgresql://x"})
|
|
assert r.status_code == 422
|
|
assert "BOR_DATABASE_URL is not settable from the database" in r.json()["detail"]
|
|
r = admin_client.put(f"{API}/embedding_dim", json={"value": "384"})
|
|
assert r.status_code == 422
|
|
assert "BOR_EMBEDDING_DIM is not settable from the database" in r.json()["detail"]
|
|
|
|
# A bad shape: names the field, stores nothing.
|
|
r = admin_client.put(f"{API}/relevance_threshold", json={"value": "abc"})
|
|
assert r.status_code == 422
|
|
assert "relevance_threshold" in r.json()["detail"]
|
|
assert db.execute(text("SELECT count(*) FROM app_settings")).scalar() == 0
|
|
|
|
# The one list[str] field (K2): a malformed JSON array is a 422
|
|
# naming the field (the _coerce ValueError branch), nothing stored.
|
|
r = admin_client.put(f"{API}/suggestions", json={"value": "[not json"})
|
|
assert r.status_code == 422
|
|
assert "suggestions" in r.json()["detail"]
|
|
assert db.execute(text("SELECT count(*) FROM app_settings")).scalar() == 0
|
|
# A well-formed JSON array stores fine (the JSON round-trip works).
|
|
r = admin_client.put(
|
|
f"{API}/suggestions", json={"value": '["How do I deploy gitea?"]'}
|
|
)
|
|
assert r.status_code == 200, r.text
|
|
assert r.json()["db_value"] == '["How do I deploy gitea?"]'
|
|
assert db.execute(
|
|
text("SELECT value FROM app_settings WHERE key = 'suggestions'")
|
|
).scalar() == '["How do I deploy gitea?"]'
|
|
# (Clean up the row — the cross-field pin below counts rows.)
|
|
assert admin_client.delete(f"{API}/suggestions").status_code == 200
|
|
|
|
# The cross-field pair (only breaks in combination): store
|
|
# relevance_threshold=0.2 (valid alone), then lexical_support_floor=0.9
|
|
# — above the DB-overridden threshold: the SECOND PUT 422s and the
|
|
# first row is unchanged.
|
|
r = admin_client.put(f"{API}/relevance_threshold", json={"value": "0.2"})
|
|
assert r.status_code == 200, r.text
|
|
r = admin_client.put(f"{API}/lexical_support_floor", json={"value": "0.9"})
|
|
assert r.status_code == 422
|
|
assert "lexical_support_floor" in r.json()["detail"]
|
|
value = db.execute(
|
|
text("SELECT value FROM app_settings WHERE key = 'relevance_threshold'")
|
|
).scalar()
|
|
assert value == "0.2" # the first row is unchanged
|
|
assert db.execute(text("SELECT count(*) FROM app_settings")).scalar() == 1
|
|
# The failed write did not disturb the live view.
|
|
assert get_settings().relevance_threshold == 0.2
|
|
|
|
|
|
def test_secrets_are_masked_and_write_only(admin_client, db: Session) -> None:
|
|
"""LOCKED A4: a stored secret reports ``masked``/``has_value`` only —
|
|
``value``/``db_value`` are ``None`` and the plaintext appears NOWHERE
|
|
in the raw response text (asserted on ``response.text``, not the
|
|
parsed model); a blank secret PUT 422s (a self-lockout — DELETE is
|
|
the explicit clear); a DELETE clears it."""
|
|
db.execute(
|
|
text("INSERT INTO app_settings (key, value) VALUES ('llm_api_key', :v)"),
|
|
{"v": "sk-secret-12345"},
|
|
)
|
|
db.commit()
|
|
invalidate_settings()
|
|
|
|
r = admin_client.get(API)
|
|
assert r.status_code == 200
|
|
item = _item(r.json(), "llm_api_key")
|
|
assert item["masked"] is True
|
|
assert item["value"] is None
|
|
assert item["db_value"] is None
|
|
assert item["has_value"] is True # a non-empty value is in effect
|
|
assert item["has_override"] is True
|
|
assert item["source"] == "db"
|
|
assert item["default"] == "" # masked: the code default is never shown
|
|
assert "sk-secret-12345" not in r.text # NOWHERE in the raw body
|
|
# The other write-only fields are masked too (set in the test env).
|
|
for key in ("admin_password", "session_secret", "database_url"):
|
|
other = _item(r.json(), key)
|
|
assert other["masked"] is True and other["value"] is None
|
|
|
|
# A blank secret PUT is a 422 (an empty API key / session secret
|
|
# would self-lockout — DELETE is the explicit clear).
|
|
r = admin_client.put(f"{API}/llm_api_key", json={"value": " "})
|
|
assert r.status_code == 422
|
|
assert "BOR_LLM_API_KEY must not be blank" in r.json()["detail"]
|
|
assert db.execute(
|
|
text("SELECT value FROM app_settings WHERE key = 'llm_api_key'")
|
|
).scalar() == "sk-secret-12345" # unchanged
|
|
|
|
# A DELETE clears the row — the effective value falls back to the
|
|
# env value (whatever this deployment's env/.env provides — the
|
|
# test only pins that it is NOT the stored secret and that the
|
|
# item reports the fallback state).
|
|
r = admin_client.delete(f"{API}/llm_api_key")
|
|
assert r.status_code == 200
|
|
assert r.json()["source"] == "default" # no BOR_LLM_API_KEY in the PROCESS env
|
|
assert r.json()["has_override"] is False
|
|
assert r.json()["value"] is None
|
|
assert r.json()["has_value"] == bool(get_settings().llm_api_key)
|
|
assert get_settings().llm_api_key != "sk-secret-12345" # the override is gone
|
|
assert db.execute(text("SELECT count(*) FROM app_settings")).scalar() == 0
|
|
|
|
|
|
def test_reset_to_env_and_clear_all(admin_client, db: Session) -> None:
|
|
"""Reset semantics: DELETE ``/{key}`` deletes the row (idempotent — a
|
|
double Reset is a success, not a 404) and the env value wins again;
|
|
DELETE ``""`` clears every row (``{"cleared": 3}``) and
|
|
``totals.overridden`` is back to 0."""
|
|
r = admin_client.put(f"{API}/relevance_threshold", json={"value": "0.5"})
|
|
assert r.status_code == 200, r.text
|
|
assert get_settings().relevance_threshold == 0.5
|
|
|
|
r = admin_client.delete(f"{API}/relevance_threshold")
|
|
assert r.status_code == 200, r.text
|
|
assert r.json()["source"] == "env" # BOR_RELEVANCE_THRESHOLD is set (0.30)
|
|
assert r.json()["has_override"] is False
|
|
assert get_settings().relevance_threshold == 0.30 # the env value wins again
|
|
# Idempotent: the double Reset is a success, not a 404.
|
|
r = admin_client.delete(f"{API}/relevance_threshold")
|
|
assert r.status_code == 200, r.text
|
|
|
|
# Three rows, then "Clear all overrides".
|
|
for key, value in (("relevance_threshold", "0.5"), ("llm_retries", "7"), ("top_n_docs", "3")):
|
|
r = admin_client.put(f"{API}/{key}", json={"value": value})
|
|
assert r.status_code == 200, r.text
|
|
assert db.execute(text("SELECT count(*) FROM app_settings")).scalar() == 3
|
|
|
|
r = admin_client.delete(API)
|
|
assert r.status_code == 200
|
|
assert r.json() == {"cleared": 3}
|
|
assert db.execute(text("SELECT count(*) FROM app_settings")).scalar() == 0
|
|
assert get_settings().relevance_threshold == 0.30 # env values win again
|
|
r = admin_client.get(API)
|
|
assert r.json()["totals"]["overridden"] == 0
|
|
|
|
|
|
def test_kill_switch_disables_the_mechanism_not_the_panel(
|
|
admin_client, db: Session, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""D6: with ``BOR_SETTINGS_DB=0`` the catalogue reports
|
|
``overlay_enabled`` false (the panel shows a banner instead of
|
|
pretending a save took effect), a PUT STILL stores the row, and
|
|
``get_settings()`` still reports the ENV value — the switch disables
|
|
the mechanism, not the panel."""
|
|
r = admin_client.put(f"{API}/relevance_threshold", json={"value": "0.5"})
|
|
assert r.status_code == 200, r.text
|
|
assert get_settings().relevance_threshold == 0.5 # the switch was ON
|
|
# Deterministic env base for the pin (machine-independent):
|
|
monkeypatch.setenv("BOR_LLM_RETRIES", "3")
|
|
invalidate_settings()
|
|
|
|
monkeypatch.setenv("BOR_SETTINGS_DB", "0")
|
|
invalidate_settings()
|
|
|
|
r = admin_client.get(API)
|
|
assert r.status_code == 200
|
|
assert r.json()["overlay_enabled"] is False
|
|
assert r.json()["totals"]["overridden"] == 0 # no row is IN EFFECT
|
|
item = _item(r.json(), "relevance_threshold")
|
|
assert item["source"] == "env" # the row exists but is not in effect
|
|
assert item["has_override"] is True
|
|
assert item["db_value"] == "0.5" # the stored string is visible (non-secret)
|
|
assert get_settings().relevance_threshold == 0.30 # the env value stands
|
|
|
|
# The panel stays writable: the PUT stores the row …
|
|
r = admin_client.put(f"{API}/llm_retries", json={"value": "7"})
|
|
assert r.status_code == 200, r.text
|
|
assert db.execute(text("SELECT count(*) FROM app_settings")).scalar() == 2
|
|
# … but the env value stays in effect while the switch is off.
|
|
assert get_settings().llm_retries == 3 # the env value (pinned 3 above)
|
|
|
|
|
|
def test_catalogue_stays_under_the_byte_budget(admin_client) -> None:
|
|
"""The whole catalogue stays under the phase-167 INFO bar (64 KB —
|
|
``BOR_LOG_LARGE_RESPONSE_BYTES``), so ``GET /api/admin/settings``
|
|
never floods the log (75 items ≈ 20 KB; the audit script re-verifies
|
|
the 256 KB budget in task 06)."""
|
|
r = admin_client.get(API)
|
|
assert r.status_code == 200
|
|
assert len(r.content) < 65_536
|
|
|
|
|
|
def test_unknown_delete_is_a_404(admin_client) -> None:
|
|
"""A DELETE of an UNKNOWN SETTING is a 404 like the PUT (a typo in
|
|
the key is a client bug; the idempotency pin is about a known key
|
|
whose row is already gone — the double Reset in
|
|
``test_reset_to_env_and_clear_all``)."""
|
|
r = admin_client.delete(f"{API}/bogus_key")
|
|
assert r.status_code == 404
|
|
assert r.json() == {"detail": "Unknown setting bogus_key"}
|
|
|
|
|
|
def test_write_log_lines_carry_never_the_value(
|
|
admin_client, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
"""AGENTS rule 10 / A4: one line per write (``settings action=…`` on
|
|
the ``app.api.admin_settings`` logger) carrying the key and the
|
|
action — NEVER the value (a secret write must not leak the
|
|
plaintext into the log)."""
|
|
caplog.set_level(logging.INFO, logger="app.api.admin_settings")
|
|
|
|
r = admin_client.put(f"{API}/llm_api_key", json={"value": "sk-log-secret-xyz"})
|
|
assert r.status_code == 200, r.text
|
|
set_lines = [rec.message for rec in caplog.records
|
|
if rec.name == "app.api.admin_settings"]
|
|
assert "settings action=set key=llm_api_key status=200" in set_lines
|
|
assert "sk-log-secret-xyz" not in "\n".join(set_lines)
|
|
|
|
caplog.clear()
|
|
r = admin_client.delete(f"{API}/llm_api_key")
|
|
assert r.status_code == 200
|
|
assert "settings action=clear key=llm_api_key" in [
|
|
rec.message for rec in caplog.records
|
|
if rec.name == "app.api.admin_settings"
|
|
]
|
|
|
|
caplog.clear()
|
|
admin_client.put(f"{API}/top_n_docs", json={"value": "3"})
|
|
r = admin_client.delete(API)
|
|
assert r.status_code == 200 and r.json() == {"cleared": 1}
|
|
assert "settings action=clear_all count=1" in [
|
|
rec.message for rec in caplog.records
|
|
if rec.name == "app.api.admin_settings"
|
|
]
|
|
|
|
|
|
def test_bad_row_inserted_directly_never_bricks_boot(
|
|
db: Session, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
"""Completion criterion (00_phase, item 5): a malformed row written
|
|
straight into ``app_settings`` (bypassing the API's 422 gate — the
|
|
operator SQL, the failed migration) must never brick boot. A FRESH
|
|
``create_app()`` — the full boot path: fail-loud auth gate, resolver
|
|
prime, middleware wiring — runs with the bad row present, the row is
|
|
SKIPPED with a logged reason, the rest of the table still applies,
|
|
and ``/api/health`` is 200 (A5)."""
|
|
# One bad row (the API's 422 gate would refuse it) + one good row:
|
|
# the bad one is skipped, the good one stands.
|
|
db.execute(text(
|
|
"INSERT INTO app_settings (key, value)"
|
|
" VALUES ('relevance_threshold', 'not-a-number')"
|
|
))
|
|
db.execute(text(
|
|
"INSERT INTO app_settings (key, value) VALUES ('top_n_docs', '3')"
|
|
))
|
|
db.commit()
|
|
caplog.set_level(logging.ERROR, logger="app.settings")
|
|
|
|
# The boot path with the bad row in the table — must not raise.
|
|
app = create_app()
|
|
|
|
s = get_settings()
|
|
env_only = Settings() # the env value the skipped key falls back to
|
|
assert s.relevance_threshold == env_only.relevance_threshold
|
|
assert s.top_n_docs == 3 # the rest of the table still applies
|
|
assert any(
|
|
"settings override ignored" in rec.message
|
|
and "relevance_threshold" in rec.message
|
|
for rec in caplog.records
|
|
)
|
|
|
|
r = TestClient(app).get("/api/health")
|
|
assert r.status_code == 200
|
|
|
|
|
|
def test_catalog_covers_every_settings_field(admin_client) -> None:
|
|
"""The catalogue's keys are exactly ``Settings.model_fields`` (the
|
|
API-level face of the unit completeness pin — a new setting cannot
|
|
be missing from the panel's data)."""
|
|
r = admin_client.get(API)
|
|
assert r.status_code == 200
|
|
assert {item["key"] for item in r.json()["items"]} == set(Settings.model_fields)
|
|
assert set(CATALOG) == set(Settings.model_fields)
|
|
|
|
|
|
def test_llm_thinking_level_row_contract(
|
|
admin_client: TestClient, db: Session, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""Phase 172 (task 02): the new ``llm_thinking_level`` row behaves
|
|
exactly like ``llm_chat_model`` — the machinery is generic over
|
|
the catalogue (D4 / A5), so the per-key contract is: (a) present in
|
|
the GET body (group ``LLM (aipi)``, ``str``, effective ``low``
|
|
with no row in ``app_settings``, ``env_var`` named); (b) PUT stores
|
|
the RAW string in ``app_settings`` (the overlay's env-native
|
|
string form) and the response reports the new effective value;
|
|
(c) the EMPTY value is storable (the off switch, A2/A4 — the API
|
|
stores as-is, no allowlist); (d) DELETE resets to the env value;
|
|
(e) the non-admin 403 gate applies (asserted once here; the full
|
|
four-route matrix is ``test_auth_matrix_all_four_routes``).
|
|
``test_catalogue_shape_and_sources_on_empty_table`` +
|
|
``test_catalog_covers_every_settings_field`` already pin the row's
|
|
PRESENCE generically — these cases pin the per-key behavior.
|
|
"""
|
|
# A deterministic base (machine-independent, the kill-switch test's
|
|
# idiom): the code default, no row in the table.
|
|
monkeypatch.delenv("BOR_LLM_THINKING_LEVEL", raising=False)
|
|
invalidate_settings()
|
|
assert get_settings().llm_thinking_level == "low"
|
|
|
|
# (a) the row: group, type, env var, effective ``low`` with NO row
|
|
# in app_settings, not secret, not restart-required (the LLM layer
|
|
# reads the setting per request — live).
|
|
r = admin_client.get(API)
|
|
assert r.status_code == 200, r.text
|
|
item = _item(r.json(), "llm_thinking_level")
|
|
assert item["group"] == "LLM (aipi)"
|
|
assert item["label"] == "Thinking level"
|
|
assert item["type"] == "str"
|
|
assert item["env_var"] == "BOR_LLM_THINKING_LEVEL"
|
|
assert item["secret"] is False
|
|
assert item["restart_required"] is False
|
|
assert item["value"] == "low"
|
|
assert item["source"] in ("default", "env") # no row in app_settings
|
|
assert item["has_override"] is False
|
|
assert item["db_value"] is None
|
|
assert item["default"] == "low"
|
|
|
|
# (b) PUT stores the RAW string (the overlay's D1 — env-native
|
|
# string form) and the response reports the new effective value.
|
|
r = admin_client.put(f"{API}/llm_thinking_level", json={"value": "high"})
|
|
assert r.status_code == 200, r.text
|
|
assert r.json()["value"] == "high"
|
|
assert r.json()["source"] == "db"
|
|
assert r.json()["has_override"] is True
|
|
assert r.json()["db_value"] == "high"
|
|
assert db.execute(
|
|
text("SELECT value FROM app_settings WHERE key = 'llm_thinking_level'")
|
|
).scalar() == "high" # stored raw, exactly as sent
|
|
assert get_settings().llm_thinking_level == "high" # live, no restart
|
|
|
|
# (c) the EMPTY value is storable (the off switch, A2/A4 — the
|
|
# API stores as-is; no allowlist, no value validation): the row
|
|
# holds the empty string and the effective value is ``""``.
|
|
r = admin_client.put(f"{API}/llm_thinking_level", json={"value": ""})
|
|
assert r.status_code == 200, r.text
|
|
assert r.json()["value"] == ""
|
|
assert r.json()["has_value"] is False # the effective value is empty
|
|
assert db.execute(
|
|
text("SELECT value FROM app_settings WHERE key = 'llm_thinking_level'")
|
|
).scalar() == "" # stored as-is (the off switch)
|
|
assert get_settings().llm_thinking_level == ""
|
|
|
|
# (d) DELETE: the row is gone and the effective value is back to
|
|
# the env value (``low`` — the base pinned above).
|
|
r = admin_client.delete(f"{API}/llm_thinking_level")
|
|
assert r.status_code == 200, r.text
|
|
assert r.json()["value"] == "low"
|
|
assert r.json()["has_override"] is False
|
|
assert db.execute(
|
|
text("SELECT count(*) FROM app_settings WHERE key = 'llm_thinking_level'")
|
|
).scalar() == 0 # the row is gone
|
|
assert get_settings().llm_thinking_level == "low"
|
|
|
|
# (e) the non-admin 403 gate still applies (asserted once for this
|
|
# key — the router-wide dependency is pinned on ALL four routes in
|
|
# ``test_auth_matrix_all_four_routes``).
|
|
anon = TestClient(fastapi_app)
|
|
r = anon.put(f"{API}/llm_thinking_level", json={"value": "high"})
|
|
assert r.status_code == 403 and r.json() == {"detail": "admin only"}
|