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:
@@ -99,14 +99,16 @@ def test_brand_js_reskins_title_brand_text_prose_and_attributes() -> None:
|
||||
|
||||
def test_brand_js_applies_phase_62_customization_from_the_same_fetch() -> None:
|
||||
"""Phase 62 (owner-locked 2026-09-01, TODO L3): the SAME settled
|
||||
/api/config answer also drives the three customization keys — the
|
||||
#message-input placeholder, every .footer-text node, and the theme
|
||||
stylesheet link (inserted right after the styles.css link, guarded
|
||||
by #theme-override, degrading with a console.warn on 404 — A5).
|
||||
/api/config answer also drives the two customization STRING keys —
|
||||
the #message-input placeholder and every .footer-text node. Phase
|
||||
91 (task 03): the retired CSS-file theme link (the old step 7)
|
||||
and its id-guard / styles.css-finder / onerror-degrade machinery
|
||||
are GONE — color theming is server-side inline injection
|
||||
(app/core/theming.py), never a brand.js DOM write.
|
||||
No second network call: the keys ride the existing boot fetch."""
|
||||
js = _text(BRAND_JS)
|
||||
assert js.count('= fetch("/api/config"') == 1, (
|
||||
"the three keys must ride the existing boot fetch — no new call"
|
||||
"the keys must ride the existing boot fetch — no new call"
|
||||
)
|
||||
# 5. The composer placeholder (chat page only — the null guard
|
||||
# no-ops on every other page).
|
||||
@@ -116,28 +118,14 @@ def test_brand_js_applies_phase_62_customization_from_the_same_fetch() -> None:
|
||||
# textContent: an operator string can't inject markup.
|
||||
assert 'document.querySelectorAll(".footer-text")' in js
|
||||
assert "footer_text" in js
|
||||
# 7. The theme link: /assets/themes/<name>, inserted right after
|
||||
# the styles.css link, tagged #theme-override (the idempotency
|
||||
# guard), with the A5 degradation warn.
|
||||
assert '"/assets/themes/"' in js
|
||||
assert 'link.id = "theme-override"' in js
|
||||
assert 'getElementById("theme-override")' in js
|
||||
assert 'insertAdjacentElement("afterend", link)' in js
|
||||
assert "link.onerror" in js
|
||||
assert '"brand: theme " + themeName' in js
|
||||
# The styles.css finder must survive the phase-33/54 cache-bust
|
||||
# rewrite: the SERVED HTML carries the asset ref with a
|
||||
# ?v=<token> query (and el.href is the absolute URL), so the match
|
||||
# has to run on the RAW attribute path with query/fragment
|
||||
# stripped — el.href.endsWith(…) would silently skip the insertion
|
||||
# (the theme never applied; found by the task-05 E2E).
|
||||
assert 'getAttribute("href")' in js
|
||||
assert "split(/[?#]/)[0]" in js
|
||||
assert 'endsWith("styles.css")' in js
|
||||
assert "el.href.endsWith" not in js
|
||||
# Phase 91 (task 03): the retired theme-link machinery is absent —
|
||||
# no theme key read, no link insertion (the whole step-7 block
|
||||
# lived inside the themeName guard — themeName gone, block gone).
|
||||
assert "themeName" not in js
|
||||
assert 'insertAdjacentElement' not in js
|
||||
# The empty-skip no-op contract: each key is guarded before any
|
||||
# DOM write, so an unset deployment stays byte-identical.
|
||||
for guard in ("if (placeholder) {", "if (footerText) {", "if (themeName) {"):
|
||||
for guard in ("if (placeholder) {", "if (footerText) {"):
|
||||
assert guard in js, (
|
||||
f"an empty value must skip its application ({guard})"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user