phase: 91_admin_theme_tab
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:
@@ -9,6 +9,14 @@ Phase 80 note: the suggestions pins are the exception — the chips are
|
||||
the last 3 questions asked once any are saved, so the env-override
|
||||
pin (the override is the SEED) needs an empty ``saved_chats``;
|
||||
the full state matrix lives in ``test_suggestions_api.py``.
|
||||
|
||||
Phase 91 (task 01) note: the ``/api/config`` pins are now DB-backed —
|
||||
the three UI strings are the EFFECTIVE values (the ``ui_settings`` row
|
||||
over the env values, B1), resolved through a short-lived session, so
|
||||
the pins take the ``db`` fixture (skip when the stack is down) and
|
||||
start from an empty ``ui_settings`` table (the env-only-deployment
|
||||
state; the DB-over-env behaviour itself is pinned in
|
||||
test_ui_settings_api.py).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -22,6 +30,14 @@ from app.config import get_settings
|
||||
from tests.conftest import ADMIN_PASSWORD
|
||||
|
||||
|
||||
def _clear_ui_settings(db: Session) -> None:
|
||||
"""The env-only-deployment state for the /api/config pins: no
|
||||
ui_settings row, so the effective strings are the env values
|
||||
(phase 91, task 01)."""
|
||||
db.execute(text("DELETE FROM ui_settings"))
|
||||
db.commit()
|
||||
|
||||
|
||||
def test_health_reports_ok(client) -> None:
|
||||
r = client.get("/api/health")
|
||||
assert r.status_code == 200
|
||||
@@ -31,34 +47,41 @@ def test_health_reports_ok(client) -> None:
|
||||
assert body["version"]
|
||||
|
||||
|
||||
def test_config_returns_default_app_metadata(client) -> None:
|
||||
"""GET /api/config is public (anonymous) and returns exactly six
|
||||
def test_config_returns_default_app_metadata(client, db: Session) -> None:
|
||||
"""GET /api/config is public (anonymous) and returns exactly five
|
||||
keys — the phase-39 app metadata, the phase-59 docs flag (inert
|
||||
false while BOR_DOCS_REPO is empty — the "Save as doc" gating),
|
||||
and the phase-62 UI customization strings (composer placeholder,
|
||||
footer line, theme file name)."""
|
||||
footer line). Phase 91: with an empty ui_settings table the
|
||||
effective strings are the env defaults (B1 — DB-over-env, the row
|
||||
absent here); the retired CSS-file theming's ``theme`` key is gone
|
||||
(task 03 — the five keys are the entire contract)."""
|
||||
_clear_ui_settings(db)
|
||||
r = client.get("/api/config")
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert set(body) == {
|
||||
"app_name", "version", "docs_repo_configured",
|
||||
"input_placeholder", "footer_text", "theme",
|
||||
"input_placeholder", "footer_text",
|
||||
}
|
||||
assert body["app_name"] == "Brain of Reese"
|
||||
assert body["version"] == get_settings().app_version
|
||||
assert body["docs_repo_configured"] is False
|
||||
# Phase 62: UNSET => the phase-61 neutral copy stands (the
|
||||
# byte-identical contract); an empty theme = the built-in palette.
|
||||
# byte-identical contract).
|
||||
assert body["input_placeholder"] == "Ask me anything…"
|
||||
assert body["footer_text"] == "Powered by self-hosted models"
|
||||
assert body["theme"] == ""
|
||||
|
||||
|
||||
def test_config_follows_overridden_app_name(client) -> None:
|
||||
"""GET /api/config reflects a Settings override (e.g. BOR_APP_NAME)."""
|
||||
def test_config_follows_overridden_app_name(client, db: Session) -> None:
|
||||
"""GET /api/config reflects a Settings override (e.g. BOR_APP_NAME).
|
||||
Phase 91: the override is the ENV side of the DB-over-env resolver —
|
||||
with an empty ui_settings row the effective app_name is the
|
||||
overridden env value."""
|
||||
from app.config import Settings
|
||||
from app.main import app as fastapi_app
|
||||
|
||||
_clear_ui_settings(db)
|
||||
fastapi_app.dependency_overrides[get_settings] = lambda: Settings(
|
||||
app_name="Brain of Testy"
|
||||
)
|
||||
@@ -68,7 +91,7 @@ def test_config_follows_overridden_app_name(client) -> None:
|
||||
body = r.json()
|
||||
assert set(body) == {
|
||||
"app_name", "version", "docs_repo_configured",
|
||||
"input_placeholder", "footer_text", "theme",
|
||||
"input_placeholder", "footer_text",
|
||||
}
|
||||
assert body["app_name"] == "Brain of Testy"
|
||||
assert body["version"] == "0.1.0"
|
||||
@@ -77,18 +100,21 @@ def test_config_follows_overridden_app_name(client) -> None:
|
||||
fastapi_app.dependency_overrides.clear()
|
||||
|
||||
|
||||
def test_config_serves_ui_customization_overrides(client) -> None:
|
||||
"""Phase 62: the three UI customization keys mirror Settings
|
||||
overrides (``BOR_INPUT_PLACEHOLDER`` / ``BOR_FOOTER_TEXT`` /
|
||||
``BOR_THEME``) verbatim — the values the frontend brand layer
|
||||
applies at boot, so this dict is the whole contract."""
|
||||
def test_config_serves_ui_customization_overrides(client, db: Session) -> None:
|
||||
"""Phase 62: the UI customization string keys mirror Settings
|
||||
overrides (``BOR_INPUT_PLACEHOLDER`` / ``BOR_FOOTER_TEXT``) — the
|
||||
values the frontend brand layer applies at boot, so this dict is
|
||||
the whole contract. Phase 91: placeholder + footer are the
|
||||
EFFECTIVE strings — the env overrides win with an empty
|
||||
ui_settings row (B1); the retired theming's ``theme`` key is gone
|
||||
(task 03)."""
|
||||
from app.config import Settings
|
||||
from app.main import app as fastapi_app
|
||||
|
||||
_clear_ui_settings(db)
|
||||
fastapi_app.dependency_overrides[get_settings] = lambda: Settings(
|
||||
input_placeholder="Ask the vault…",
|
||||
footer_text="Powered by my own models",
|
||||
theme="indigo.css",
|
||||
)
|
||||
try:
|
||||
r = client.get("/api/config")
|
||||
@@ -96,16 +122,15 @@ def test_config_serves_ui_customization_overrides(client) -> None:
|
||||
body = r.json()
|
||||
assert set(body) == {
|
||||
"app_name", "version", "docs_repo_configured",
|
||||
"input_placeholder", "footer_text", "theme",
|
||||
"input_placeholder", "footer_text",
|
||||
}
|
||||
assert body["input_placeholder"] == "Ask the vault…"
|
||||
assert body["footer_text"] == "Powered by my own models"
|
||||
assert body["theme"] == "indigo.css"
|
||||
finally:
|
||||
fastapi_app.dependency_overrides.clear()
|
||||
|
||||
|
||||
def test_config_docs_flag_tracks_settings(client) -> None:
|
||||
def test_config_docs_flag_tracks_settings(client, db: Session) -> None:
|
||||
"""Phase 59 (task 05): ``docs_repo_configured`` mirrors
|
||||
``settings.docs_configured`` — a real bool (never a truthy string)
|
||||
that flips true the moment BOR_DOCS_REPO is non-empty: that flag is
|
||||
@@ -113,6 +138,7 @@ def test_config_docs_flag_tracks_settings(client) -> None:
|
||||
from app.config import Settings
|
||||
from app.main import app as fastapi_app
|
||||
|
||||
_clear_ui_settings(db)
|
||||
fastapi_app.dependency_overrides[get_settings] = lambda: Settings(
|
||||
app_name="Brain of Testy",
|
||||
docs_repo="/srv/docs-repo",
|
||||
@@ -206,6 +232,10 @@ def test_suggestions_honors_bor_suggestions_env_override(
|
||||
# shell-body marker (the Tokens view section is inside the
|
||||
# shell; the per-view title is client-side now).
|
||||
("/tokens.html", 'id="view-tokens"'), # phase 79: shell route
|
||||
# Phase 91 (task 04): /theme.html is a SHELL route too — the
|
||||
# shell-body marker (the Theme view section is inside the
|
||||
# shell; the per-view title is client-side now).
|
||||
("/theme.html", 'id="view-theme"'), # phase 91: shell route
|
||||
("/shared.html", "Shared conversation"), # phase 51: anonymous shared page
|
||||
],
|
||||
)
|
||||
@@ -246,6 +276,7 @@ def test_index_page_no_cache_with_versioned_asset_refs(client) -> None:
|
||||
["/sources.html", "/document.html", "/login.html", "/tuning.html",
|
||||
"/git-sources.html", "/history.html", # phase 50: + History (shell route, task 03)
|
||||
"/tokens.html", # phase 79 task 06: + Tokens (shell route)
|
||||
"/theme.html", # phase 91 task 04: + Theme (shell route)
|
||||
"/shared.html"], # phase 51: + the anonymous shared page
|
||||
)
|
||||
def test_html_pages_no_cache_with_versioned_refs(client, path: str) -> None:
|
||||
@@ -271,6 +302,11 @@ def test_html_pages_no_cache_with_versioned_refs(client, path: str) -> None:
|
||||
# client-side title: the pin asserts the shell never carries
|
||||
# the per-view title statically (the router writes it).
|
||||
("/tokens.html", 'id="view-tokens"', "Access tokens · Brain of Reese"), # phase 79 task 06
|
||||
# phase 91 task 04: the seventh view — there was never a
|
||||
# standalone theme.html, so "old_title" is the router's
|
||||
# client-side title: the pin asserts the shell never carries
|
||||
# the per-view title statically (the router writes it).
|
||||
("/theme.html", 'id="view-theme"', "Theme · Brain of Reese"), # phase 91 task 04
|
||||
],
|
||||
)
|
||||
def test_shell_routes_serve_the_shell_no_cache_versioned(
|
||||
|
||||
Reference in New Issue
Block a user