**Phase 168 — Admin Settings panel — final verification: PASS** 1. **Implemented/verified:** `/settings.html` (admin-only) with the 75-field catalogue (`app/core/settings_catalog.py`), DB-over-env overrides in Postgres (`app_settings`, migration 0028 at head, `settings_overlay.py` resolver, `app/api/admin_settings.py` — 4 admin-only routes), live-apply on save with no restart + restart-required badges, Reset/Clear all, write-only secrets, `.env`-never-written, `BOR_SETTINGS_DB=0` kill switch. 2. **Phase E2E: 12/12** — run with `E2E_APP_PORT=8200` (ports 8123/8124 are held by an unrelated "undercooked" project; no process killed, no code change — the documented env override). 3. **Regression set: all 7 suites green in isolation** (theme tab, API tokens, configurable brand, global tuning, admin auth, cache busting, wide desktop column — 38 tests). 4. **Full suite: unit+integration exit 0 (4066 passed), app/ coverage 99%** (gate >90%); **ruff clean, pyright 0 errors / 0 warnings.** 5. **Log contract (rule 10) verified live:** per-turn + per-response byte-budget lines present; settings write lines carry key + action, **never the value**. 6. **Byte-budget audit: all endpoints under the 262,144 B budget**; settings catalogue 23,513 B (gzip 3,326). 7. **Defect fixed (one):** criterion 5 had no integration pin — added `test_bad_row_inserted_directly_never_bricks_boot` to `tests/integration/test_admin_settings_api.py` (malformed row inserted directly → fresh `create_app()` boots, row skipped with logged reason, rest of table applies, `/api/health` 200) — 12/12 green. 8. **Docs pass:** `README.md` and `AGENTS.md` reviewed in full — both consistent with the as-built feature (Settings page table, Settings section, Configuration-reference note, `.env.example` block); **no changes required**. 9. **Cleanup:** audit app killed (by PID); `app_settings` left empty (0 rows); all changes left in the working tree (no commit, per protocol). 10. **Completion criteria:** all 7 checkboxes verified green (criterion 7's commit is the harness's). 11. **Next pending phase:** `170_edit_bubble_sizing`.
101 lines
3.5 KiB
Python
101 lines
3.5 KiB
Python
"""Phase 168 (task 02): the settings catalogue is complete and consistent.
|
|
|
|
The Settings panel (task 05) is GENERATED from
|
|
``app/core/settings_catalog.py`` — never hand-written — so these pins
|
|
are load-bearing: ``set(CATALOG) == set(Settings.model_fields)`` fails
|
|
the build the moment a new ``Settings`` field lands without its
|
|
metadata (it can never silently disappear from the panel), the group
|
|
table is closed both directions, and the capability sets are exactly
|
|
the LOCKED decisions (A3/A4).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from app.config import Settings
|
|
from app.core.settings_catalog import (
|
|
CATALOG,
|
|
GROUPS,
|
|
NON_OVERRIDABLE,
|
|
RESTART_REQUIRED,
|
|
SECRETS,
|
|
SettingMeta,
|
|
env_var,
|
|
)
|
|
|
|
|
|
def test_catalog_covers_exactly_model_fields() -> None:
|
|
"""The completeness pin (both directions, the diff in the message)."""
|
|
catalog_keys = set(CATALOG)
|
|
field_names = set(Settings.model_fields)
|
|
missing = sorted(field_names - catalog_keys)
|
|
extra = sorted(catalog_keys - field_names)
|
|
assert not missing, (
|
|
f"Settings fields missing from the catalogue (invisible in the "
|
|
f"panel): {missing}"
|
|
)
|
|
assert not extra, (
|
|
f"Catalogue entries with no Settings field (stale): {extra}"
|
|
)
|
|
assert set(CATALOG) == set(Settings.model_fields)
|
|
|
|
|
|
def test_groups_closed_both_directions() -> None:
|
|
catalog_groups = {meta.group for meta in CATALOG.values()}
|
|
only_in_groups = sorted(set(GROUPS) - catalog_groups)
|
|
only_in_catalog = sorted(catalog_groups - set(GROUPS))
|
|
assert not only_in_groups, f"GROUPS entries with no catalogue member: {only_in_groups}"
|
|
assert not only_in_catalog, f"Catalogue groups missing from GROUPS: {only_in_catalog}"
|
|
|
|
|
|
def test_every_meta_carries_its_own_key() -> None:
|
|
for key, meta in CATALOG.items():
|
|
assert isinstance(meta, SettingMeta)
|
|
assert meta.key == key
|
|
|
|
|
|
def test_secrets_are_exactly_the_masked_set() -> None:
|
|
# LOCKED A4: write-only, masked — the 3 named secrets +
|
|
# database_url (read-only, but its URL carries credentials).
|
|
assert set(SECRETS) == {
|
|
"llm_api_key",
|
|
"admin_password",
|
|
"session_secret",
|
|
"database_url",
|
|
}
|
|
|
|
|
|
def test_non_overridable_is_exactly_locked_a3() -> None:
|
|
# LOCKED A3: database_url builds the engine that would read the
|
|
# override (chicken-and-egg brick); embedding_dim is fixed in the
|
|
# chunks table by A6 (a dim override would corrupt retrieval).
|
|
assert set(NON_OVERRIDABLE) == {"database_url", "embedding_dim"}
|
|
|
|
|
|
def test_restart_required_is_the_boot_wired_set() -> None:
|
|
# LOCKED A4: logging, the gzip middleware (compress_min_bytes), the
|
|
# static mount (static_dir), create_engine (db_pool_*), and
|
|
# SessionMiddleware (session_*).
|
|
assert set(RESTART_REQUIRED) == {
|
|
"log_level",
|
|
"compress_min_bytes",
|
|
"static_dir",
|
|
"db_pool_size",
|
|
"db_pool_max_overflow",
|
|
"db_pool_recycle",
|
|
"session_secret",
|
|
"session_max_age",
|
|
"session_cookie",
|
|
}
|
|
|
|
|
|
def test_env_var_is_bare_key_upper_prefixed() -> None:
|
|
assert env_var("relevance_threshold") == "BOR_RELEVANCE_THRESHOLD"
|
|
for key in CATALOG:
|
|
assert env_var(key).startswith("BOR_")
|
|
|
|
|
|
def test_labels_nonempty_and_unique() -> None:
|
|
labels = [meta.label for meta in CATALOG.values()]
|
|
assert all(label.strip() for label in labels), "empty label in the catalogue"
|
|
duplicates = sorted({label for label in labels if labels.count(label) > 1})
|
|
assert not duplicates, f"duplicate labels: {duplicates}"
|