phase: 91_admin_theme_tab
Build and Push Containers / build-and-push-app (push) Successful in 5m43s
Build and Push Containers / build-and-push-db (push) Successful in 12s

All verification is complete — this pass needed no code changes. Final report:

**Phase 91 — Admin Theme tab: final verification pass (all 6 tasks already in `complete/`)**

- Verified pre-paint theming end-to-end: `ui_settings` store + resolver, admin `GET/PUT /api/ui-settings`, `CachingMiddleware` inline-`<style id="bor-theme">` injection before `</head>` (incl. `/shared/<token>` prefix branch, unit-pinned), CSP sha256 exemption for the inline tag, Theme tab shell + `theme.js` editor, CSS-file theming fully retired.
- No defects found; zero changes made — working tree left exactly as the task executors left it.
- Tests: `uv run pytest --cov=app` → 1841 passed, 0 failed (TOTAL coverage **99%**; theming/ui_settings/caching all 100%); `uv run pytest tests/e2e/test_admin_theme_tab.py -v --no-cov` → **5 passed** in isolation.
- Lint/types: `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings.
- Criteria: (1) unset deployment byte-identical, no `#bor-theme` anywhere — ✓ (unit no-op test + E2E reset byte-compare); `rg "BOR_THEME|themes/"` → single hit is the permitted doc-history comment in `frontend/index.html`. (2) admin-only gate + 403s for anonymous and token users — ✓ (E2E test 3). (3) saved theme inline before `</head>` on every page incl. `/shared/<token>`, computed `--brand` on first paint for admin + anonymous — ✓ (E2E test 2 + unit). (4) reset → byte-identical; 5 contrast pairs warn <4.5:1, non-blocking — ✓ (E2E tests 4–5). (5) suite green, >90% coverage, lint clean — ✓. (6) commit deferred to harness per rules.
- Notable: `.agents/PLAN.md` is absent from the repo — the phase overview's Design section was used as the binding spec; no deviation resulted.
- Next pending phase: **none** — 91 is the last phase in `todo/`.
This commit is contained in:
2026-09-09 17:22:24 -04:00
parent 3095c4c577
commit d22d260b8b
74 changed files with 4448 additions and 675 deletions
+7 -52
View File
@@ -481,70 +481,25 @@ def test_docs_branchs_garbage_ignored_when_repo_unset(
def test_ui_customization_defaults_are_the_phase_61_copy() -> None:
"""UNSET => byte-identical to the phase-61 neutral UI: the locked
phase-61 copy is the DEFAULT (composer placeholder + footer line),
and an empty theme = the built-in dark-tech palette."""
phase-61 copy is the DEFAULT (composer placeholder + footer line).
Phase 91 (task 03): the retired CSS-file theme env var is gone —
``Settings`` no longer has a theme field at all (a leftover value
in a deployment's .env is ignored, not a boot failure)."""
s = _settings()
assert s.input_placeholder == "Ask me anything…"
assert s.footer_text == "Powered by self-hosted models"
assert s.theme == ""
assert "theme" not in type(s).model_fields
def test_ui_customization_env_overrides(monkeypatch: pytest.MonkeyPatch) -> None:
"""The three settings honor their ``BOR_`` env vars
(``BOR_INPUT_PLACEHOLDER`` / ``BOR_FOOTER_TEXT`` / ``BOR_THEME``);
"""The two string settings honor their ``BOR_`` env vars
(``BOR_INPUT_PLACEHOLDER`` / ``BOR_FOOTER_TEXT``);
placeholder/footer accept any string (empty is legal — the brand
layer then keeps the template default)."""
monkeypatch.setenv("BOR_INPUT_PLACEHOLDER", "Ask the vault…")
monkeypatch.setenv("BOR_FOOTER_TEXT", "Powered by my own models")
monkeypatch.setenv("BOR_THEME", "indigo.css")
s = _settings()
assert s.input_placeholder == "Ask the vault…"
assert s.footer_text == "Powered by my own models"
assert s.theme == "indigo.css"
monkeypatch.setenv("BOR_INPUT_PLACEHOLDER", "")
assert _settings().input_placeholder == "" # empty stands
def test_theme_validator_accepts_empty_and_bare_css_filename() -> None:
"""Phase 62 (A5): empty = the built-in palette; a bare lowercase
``.css`` filename (the ``indigo.css`` example) is the only
non-empty shape — dashes/underscores/digits are legal tokens."""
assert _settings().theme == "" # "" passes
assert _settings(theme="indigo.css").theme == "indigo.css"
assert _settings(theme="dark-2026_v2.css").theme == "dark-2026_v2.css"
@pytest.mark.parametrize(
("bad", "match"),
[
# uppercase — the shape is lowercase-only
("Indigo.css", "Indigo.css"),
# path escape — a theme is a filename, never a path
("../evil.css", r"\.\./evil\.css"),
("a/b.css", r"a/b\.css"),
("/abs.css", r"got '/abs\.css'"),
# a missing extension is not a theme file
("indigo", r"got 'indigo'"), # must not match the example text
],
)
def test_theme_validator_rejects_malformed_naming_the_value(
bad: str,
match: str,
) -> None:
"""A typo in ``BOR_THEME`` must kill startup, not silently 404 at
runtime — the rejection names the offending value (the phase-56
fail-loud house style) alongside the allowed shape."""
with pytest.raises(ValidationError, match=match):
_settings(theme=bad)
def test_bor_theme_env_malformed_fails_startup_naming_value(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The startup path: a malformed ``BOR_THEME`` in the environment
fails Settings construction loudly (the app builds its settings at
import time, so this is a refused boot), naming the value — the
E2E boots-check lands in task 05."""
monkeypatch.setenv("BOR_THEME", "../evil.css")
with pytest.raises(ValidationError, match=r"\.\./evil\.css"):
_settings()