All gates green. Final report: **Phase 173 — final verification pass (all tasks already complete in `complete/`)** - Verified the full implementation is in the working tree: `docs_user`/`docs_token` settings + catalogue rows (task 01), `docs_repo` PUT 422 guard + `docs_token` write-only pin (task 02), `run_git(env)` + `push_document(user, token)` credential env (task 03), `settings_migrate.py` wired into `create_app()` (task 04), E2E suite (task 05). - **Completion criteria:** 1. GET `/api/admin/settings` lists `docs_user` (plain) + `docs_token` (secret, `value=null`), token never in any body — ✅ `test_docs_token_is_write_only_masked_end_to_end` + E2E sentinel-absent-from-DOM 2. PUT `docs_repo` with `://user:pass@` → 422 naming the fields; clean URL passes — ✅ integration + E2E UI-side 3. Token set → `GIT_CURL_USERNAME`/`GIT_CURL_PASSWORD`/`GIT_TERMINAL_PROMPT=0` on every git step; empty → `env=None` — ✅ mocked-git unit tests 4. Legacy env split once at startup, idempotent, one INFO line, `.env` untouched — ✅ `test_settings_migrate.py` (4 shapes) + `test_docs_push_migrate.py` 5. Full suite green, coverage >90%, lint clean — ✅ 6. E2E isolated: `test_docs_push_creds.py` 4/4; regression set (panel + layout + response_to_docs) 21/21 — ✅ 7. Commit + phase move — harness's job (per instructions, not mine). 8. README/AGENTS consistent — ✅ (below) - Tests/lint: `uv run pytest` exit 0 (4112 passed) · `uv run pytest --cov=app --cov-report=term-missing` TOTAL 99% · `uv run ruff check .` clean · `uv run pyright` 0 errors - Docs: README.md (incl. Docs-push config section, guard/migration paragraph, `.env.example` block) and AGENTS.md verified consistent in full; fixed two stale docstrings in `app/config.py` + `app/core/settings_catalog.py` (`is_set` → the API's actual `has_value` field). - No live-infrastructure changes (no live step in this phase). No next pending phase — `todo/` held only 173.
151 lines
5.8 KiB
Python
151 lines
5.8 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_level_is_a_plain_live_overridable_row() -> None:
|
|
"""Phase 172 (task 01): the new ``llm_thinking_level`` pair sits in
|
|
``LLM (aipi)`` with the ``Thinking level`` label and is plain —
|
|
not ``secret`` (a free-form text value, like ``llm_chat_model``),
|
|
not ``restart_required`` (the LLM client reads the setting per
|
|
request via ``get_settings()`` — live), overridable (the overlay is
|
|
generic over ``str`` fields)."""
|
|
meta = CATALOG["llm_thinking_level"]
|
|
assert meta.group == "LLM (aipi)"
|
|
assert meta.label == "Thinking level"
|
|
assert "llm_thinking_level" not in SECRETS
|
|
assert "llm_thinking_level" not in RESTART_REQUIRED
|
|
assert "llm_thinking_level" 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_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}"
|