**Phase 182 — final verification pass (all 8 tasks already complete; this was verification + docs + one defect fix).**
- Verified: `app/rag/verbatim.py` (locator grammar, `splitlines()` slice, line-boundary cap, `resolve_answer_blocks`), `SHOW_TEXT_TOOL` + `shown_texts`, `SHOW_TEXT_CLAUSE`, `done.answer_text` (9 fields, key dropped when empty) + `ChatMessageRecord.answer_text` (≤50, 600 000-char cap counted in the 8 M budget), `verbatim.js` + 4 `{verbatim:true}` renderers, `Containerfile` minify step, `.env.example` 3 vars.
- `uv run python -c "…answer_text, show_text_max_chars, show_text_max_blocks"` → `True 12000 5`; each negative refuses startup naming its field; `0` accepted as inert ✅
- `bash .agents/validate.sh` → **validation OK**: 4320 unit+integration tests green, `TOTAL 6849 17 → 99%` (gate app/ >90% OK), `ruff check .` "All checks passed!", `pyright` 0 errors (re-run after my edits, 08:28).
- E2E, each in isolation `--no-cov`: `test_verbatim_show_text.py` 13 passed (panel bytes == `documents.content`, server caption, clipboard == server string, `/document.html?source=…&path=…`, honest unresolved state, reload+share, `show_image` regression, flag-off app) ✅; green: `test_answer_images` `test_harness_aligned_tools` `test_agent_document_tools` `test_markdown_tables` `test_markdown_inline_links_images` `test_svg_document_preview` `test_smoke` `test_document_viewer` `test_markdown_blockquotes` `test_chat_persistence` `test_thinking_timeline` `test_chat_rag` `test_settings_mobile_value_width`.
- **Defect found & fixed (pre-existing, not phase 182):** `tests/e2e/test_admin_settings_panel.py::test_admin_sees_groups_and_source_badges` failed — its whole-page `assert "reese" not in page.content()` tripped on the shell's onboarding chips, which follow the newest saved chats in the one shared E2E DB (a leftover `reeselink.json` question from a tool suite). Scoped the strict sweep to `.settings-shell` and kept a whole-page `reese:reese` credential needle → 12 passed with the leftovers still present.
- Docs: README fixed 2 inconsistencies — Quick Start omitted the **required** `BOR_ADMIN_PASSWORD`/`BOR_SESSION_SECRET` (the app fail-loud refuses boot) and the broken anchor `#chat--` → `#chat-`; full re-read of README (1816 lines) + AGENTS confirmed the phase-182 feature/settings/log/UI-contract text matches the code, and every `.env.example` var + every `Settings` field is documented.
- Notable: kept `resolve_answer_blocks` synchronous on the SSE terminal path (bounded indexed SELECTs, matching the existing `query_log` precedent and the phase's documented fence-lookup bound); no live-infra changes made.
- Next pending phase: none — `todo/` holds only 182 (its 8 task files are in `complete/`; the harness commits + moves `00_phase.md`).
196 lines
8.0 KiB
Python
196 lines
8.0 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) +
|
|
# docs_token (phase 173, C1: the docs-push remote token reuses the
|
|
# write-only/masked mechanism, llm_api_key-style).
|
|
assert set(SECRETS) == {
|
|
"llm_api_key",
|
|
"admin_password",
|
|
"session_secret",
|
|
"database_url",
|
|
"docs_token",
|
|
}
|
|
|
|
|
|
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_llm_thinking_levels_are_plain_live_overridable_rows() -> None:
|
|
"""Phase 178 (task 01): the two thinking-level rows (the phase-172
|
|
knob, renamed + the new summary level) sit in ``LLM (aipi)`` with
|
|
the ``Chat thinking level`` / ``Summary thinking level`` labels and
|
|
are plain — not ``secret`` (free-form text values, like
|
|
``llm_chat_model``), not ``restart_required`` (the LLM client
|
|
reads the settings per request via ``get_settings()`` — live),
|
|
overridable (the overlay is generic over ``str`` fields)."""
|
|
for key, label in (
|
|
("llm_chat_thinking_level", "Chat thinking level"),
|
|
("llm_summary_thinking_level", "Summary thinking level"),
|
|
):
|
|
meta = CATALOG[key]
|
|
assert meta.group == "LLM (aipi)"
|
|
assert meta.label == label
|
|
assert meta.secret is False
|
|
assert key not in SECRETS
|
|
assert key not in RESTART_REQUIRED
|
|
assert key not in NON_OVERRIDABLE
|
|
|
|
|
|
def test_docs_user_is_a_plain_live_overridable_row() -> None:
|
|
"""Phase 173 (task 01, C1/C5): ``docs_user`` sits in ``Docs push``
|
|
with the ``Docs user`` label and is plain — not ``secret`` (a
|
|
plain username), not ``restart_required`` (the push service reads
|
|
the setting per push via ``get_settings()`` — live), overridable
|
|
(the overlay is generic over ``str`` fields). An empty value is a
|
|
valid state (C5: the anonymous push)."""
|
|
meta = CATALOG["docs_user"]
|
|
assert meta.group == "Docs push"
|
|
assert meta.label == "Docs user"
|
|
assert meta.secret is False
|
|
assert "docs_user" not in SECRETS
|
|
assert "docs_user" not in RESTART_REQUIRED
|
|
assert "docs_user" not in NON_OVERRIDABLE
|
|
|
|
|
|
def test_docs_token_is_a_secret_live_overridable_row() -> None:
|
|
"""Phase 173 (task 01, C1): ``docs_token`` has the ``Docs token``
|
|
label and ``secret=True`` — write-only end-to-end via the EXISTING
|
|
derived mechanism (``SECRETS`` picks it up automatically; the API
|
|
reports ``is_set`` + ``value=null``, a blank PUT 422s, the panel
|
|
renders it masked, zero ``settings.js`` code). Live (the push
|
|
service reads it per push), overridable (generic ``str`` overlay)."""
|
|
meta = CATALOG["docs_token"]
|
|
assert meta.group == "Docs push"
|
|
assert meta.label == "Docs token"
|
|
assert meta.secret is True
|
|
assert "docs_token" in SECRETS
|
|
assert "docs_token" not in RESTART_REQUIRED
|
|
assert "docs_token" not in NON_OVERRIDABLE
|
|
|
|
|
|
def test_verbatim_display_rows_are_plain_live_overridable() -> None:
|
|
"""Phase 182 (task 01, LOCKED A8): the three verbatim-display rows sit
|
|
in ``Agent tools`` (the ``read_max_chars`` group, beside it) with the
|
|
``Answer text`` / ``Show text max chars`` / ``Show text max blocks``
|
|
labels and are plain — not ``secret`` (a bool and two ints), not
|
|
``restart_required`` (the chat request path reads them per turn via
|
|
``get_settings()`` — live), overridable (the overlay is generic over
|
|
``bool``/``int`` fields). ``answer_text`` is the master switch and the
|
|
two budgets accept ``0`` (inert) — a negative value fails startup, not
|
|
the panel."""
|
|
for key, label in (
|
|
("answer_text", "Answer text"),
|
|
("show_text_max_chars", "Show text max chars"),
|
|
("show_text_max_blocks", "Show text max blocks"),
|
|
):
|
|
meta = CATALOG[key]
|
|
assert meta.group == "Agent tools"
|
|
assert meta.label == label
|
|
assert meta.secret is False
|
|
assert meta.restart_required is False
|
|
assert meta.overridable is True
|
|
assert key not in SECRETS
|
|
assert key not in RESTART_REQUIRED
|
|
assert key not in NON_OVERRIDABLE
|
|
|
|
|
|
def test_verbatim_display_field_types() -> None:
|
|
"""Phase 182 (task 01): the master switch is a ``bool`` and the two
|
|
budgets are ``int``s with the LOCKED A8 defaults (``True`` /
|
|
``12000`` / ``5``) — the types the panel renders its input for and the
|
|
overlay coerces from the stored raw string."""
|
|
assert Settings.model_fields["answer_text"].annotation is bool
|
|
assert Settings.model_fields["show_text_max_chars"].annotation is int
|
|
assert Settings.model_fields["show_text_max_blocks"].annotation is int
|
|
assert Settings.model_fields["answer_text"].default is True
|
|
assert Settings.model_fields["show_text_max_chars"].default == 12_000
|
|
assert Settings.model_fields["show_text_max_blocks"].default == 5
|
|
|
|
|
|
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}"
|