All gates re-verified green. Report: - **Verified (all 4 tasks already complete; this was the final pass):** `effective_settings` resolves the 3 brand strings panel-row → tab-row → env (`applied_override_keys`, presence-not-value); `PUT /api/ui-settings` ignores string body fields; Theme-tab form is colors-only (17 pickers, no `#theme-app-name`/`#theme-placeholder`/`#theme-footer`); the ONE shared `applyBrandFromConfig` re-applies title/`.brand-text`/footer/placeholder after a panel save-reset and a tab save-reset; boot path byte-identical (single `/api/config`). - **Completion criteria:** - strings order panel→tab→env, both branches — ✅ `tests/unit/test_theming.py::test_effective_app_name_prefers_settings_panel_row_over_ui_settings_row`, `..._fall_back_to_ui_settings_row_without_panel_row`, `..._per_field_panel_then_tab_then_env`, `..._ignore_panel_rows_when_the_overlay_is_off` - PUT ignores string fields + no 3 inputs in the tab — ✅ `tests/integration/test_ui_settings_api.py::test_put_ignores_string_fields_in_body` (+2) and the E2E `test_theme_tab_dom_is_colors_only` - live apply without reload + incident regression — ✅ `tests/e2e/test_app_name_settings_fix.py` tests 1–4 - byte-identical boot, no double-apply — ✅ `tests/unit/test_brand_live.py` markers + E2E "exactly one /api/config" - suite green, coverage >90%, ruff+pyright clean — ✅ - no behavior change in prior phases — ✅ (palette, phase-168 panel, env fallback suites all green) - atomic commit — **left to the harness** (pipeline rule overrides per-phase committing; no git write run) - **Gates:** `uv run pytest --cov=app --cov-report=term-missing` → **4630 passed, TOTAL 6941 16 99%**, exit 0 · `uv run pytest tests/e2e/test_app_name_settings_fix.py -v --no-cov` → **6 passed** in isolation · regression E2E (isolated, one process each, all exit 0): `test_smoke` `test_configurable_brand` `test_admin_theme_tab` `test_theme_save_and_coverage` `test_theme_semantic_completion` `test_themed_favicon` `test_dark_tech_theme` `test_admin_settings_panel` `test_admin_settings_layout` `test_settings_mobile_value_width` · `uv run ruff check .` → All checks passed · `uv run pyright` → 0 errors. - **Documentation:** README — fixed "saving one repaints *every* open page" (only the open document repaints; other pages on next load) and a broken paragraph wrap in *Customizing the look*; AGENTS.md rule 16 — `test_chunks_hnsw_recall.py` now described accurately (index paths forced with `enable_seqscan`/`indexonlyscan`/`bitmapscan` off, unforced plan asserted only on a pristine relation); `tests/unit/test_theme_frontend.py` — two stale section headers ("20 form fields"/"20 inputs" → 17 color). No AGENTS.md settings-overlay/resolution-order clause exists, so per the phase none was added. Full env-var cross-check (`BOR_*` in `app/config.py` vs README) → 0 gaps; 0 missing file references. - **Notable:** the working tree also carries this phase's flake hardening of the phase-188 `tests/integration/test_chunks_hnsw_recall.py` (verified legit and green). Live infra untouched; shared DB left clean (0 `app_settings`, 0 `ui_settings`). **Flagged, not changed:** `settings_overlay.applied_override_keys` lets a `ProgrammingError` (missing `app_settings`) propagate from public `GET /api/config`, where `load_overrides` deliberately fails soft — only reachable on an un-migrated (0027) DB, outside this phase's pinned A1 semantics, so left for the owner to rule on rather than silently deviating. - **Next pending phase:** `191_source_name_labels_tags`.
409 lines
19 KiB
Python
409 lines
19 KiB
Python
"""Unit: the live-brand-on-save marker pins (phase 190, tasks 01 + 03).
|
|
|
|
Defect (b) of the phase (``TODO.md`` L3, the stale half): a save in
|
|
either admin surface never re-applied the brand strings to the OPEN
|
|
document. ``settings.js`` re-GETed the catalogue after a save but never
|
|
touched the brand; ``theme.js``'s save ran ``applyServedTheme`` (the
|
|
phase-92 COLOR reconcile) and stopped there; ``brand.js`` applied the
|
|
strings ONCE at boot from ``/api/config`` — so the open page kept the old
|
|
name until a manual reload.
|
|
|
|
Task 01 pinned that as the ABSENT behavior; **task 03 inverted it**, and
|
|
this module now pins the fix:
|
|
|
|
* ``APPLY_BRAND`` (``applyBrandFromConfig``) is ONE function in
|
|
``brand.js`` — the single owner of the brand DOM — exported on the
|
|
layer's existing ``window`` surface and called by the BOOT
|
|
(``applyBrand``) and by both admin surfaces. The boot is the
|
|
EXTRACTION, not a duplication: the four DOM writes exist once;
|
|
* ``settings.js`` re-applies after a save / per-row Reset / Clear all
|
|
that touched one of the three brand keys, and after nothing else;
|
|
* ``theme.js`` re-applies after Save and after Reset (a reset can make a
|
|
legacy baked-in string fall back, so the page must re-read it);
|
|
* a FAILED re-apply leaves the DOM at its last applied state — the
|
|
phase-168 "never a blanked panel" precedent applied to the brand.
|
|
|
|
House JS convention (``test_frontend_brand.py`` /
|
|
``test_settings_frontend_view.py`` / ``test_save_chat_ui.py``): the
|
|
browser behavior is pinned at source-marker level here, E2E in the
|
|
phase's ``tests/e2e/test_app_name_settings_fix.py`` (task 04).
|
|
|
|
``BRAND_DOM_TOKENS`` (the four writes that constitute "applying the
|
|
brand strings": the document title, the ``.brand-text`` header, the
|
|
``#message-input`` composer placeholder, the ``.footer-text`` line) stays
|
|
the ownership pin it was in task 01 — those belong to the ONE shared pass
|
|
in ``brand.js`` and must NEVER be duplicated inside ``settings.js`` / the
|
|
``theme.js`` save paths. The live-apply is asserted by presence of the
|
|
shared CALL, never by a second copy of the DOM path.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
ASSETS = Path(__file__).resolve().parents[2] / "frontend" / "assets"
|
|
BRAND_JS = ASSETS / "brand.js"
|
|
SETTINGS_JS = ASSETS / "settings.js"
|
|
THEME_JS = ASSETS / "theme.js"
|
|
|
|
#: The ONE shared brand pass task 03 extracted out of ``brand.js`` (the
|
|
#: phase-190 contract name) — boot and live-apply run this function.
|
|
APPLY_BRAND = "applyBrandFromConfig"
|
|
|
|
#: The four DOM writes that ARE "re-applying the brand strings" — the
|
|
#: shared pass owns them; a save surface that writes one of these
|
|
#: directly has duplicated the brand path (the defect the phase fixes
|
|
#: must not be re-introduced as a second copy).
|
|
BRAND_DOM_TOKENS = ("document.title", "brand-text", "message-input", "footer-text")
|
|
|
|
#: The three keys the Settings panel owns and whose save/reset therefore
|
|
#: re-applies the brand (phase 190 A1). A save of any other row must not.
|
|
BRAND_KEYS = ("app_name", "input_placeholder", "footer_text")
|
|
|
|
#: The two statements every live re-apply is made of, pinned verbatim so
|
|
#: the counts below count CALL SITES and not prose (the house
|
|
#: ``= fetch("/api/config"`` idiom in ``test_frontend_brand.py``). Both
|
|
#: surfaces write them identically — the shared semantics, one shape.
|
|
CONFIG_FETCH = 'const r = await fetch("/api/config", { cache: "no-store" });'
|
|
SHARED_CALL = "window.applyBrandFromConfig(await r.json());"
|
|
|
|
# The three settings.js settle paths (save / per-row Reset / clear-all) —
|
|
# each ends its success arm with the catalogue re-GET (phase 168,
|
|
# untouched) followed by the brand re-apply (phase 190 task 03, additive).
|
|
SETTINGS_SETTLE_PATHS = (
|
|
"async function saveSettings()",
|
|
"async function resetRow(",
|
|
"async function confirmClearAll(",
|
|
)
|
|
|
|
|
|
def _read(path: Path) -> str:
|
|
return path.read_text(encoding="utf-8")
|
|
|
|
|
|
def _body(js: str, header: str, closer: str = "\n }") -> str:
|
|
"""The source of the function whose declaration starts at ``header``
|
|
up to its closing brace — the house slice idiom
|
|
(``test_settings_frontend_view.py``): these view modules nest their
|
|
functions at two spaces inside ``mount``, so the first ``\\n }``
|
|
after the header closes it (nested blocks close at four).
|
|
``closer`` opens the same idiom to top-level functions (``brand.js``,
|
|
whose blocks close at column 0)."""
|
|
start = js.index(header)
|
|
end = js.find(closer, start)
|
|
assert end != -1, f"unterminated function body for {header!r}"
|
|
return js[start:end]
|
|
|
|
|
|
def _every_marker_preceded_by(body: str, marker: str, before: str) -> None:
|
|
"""Every ``marker`` occurrence sits after some ``before``
|
|
occurrence (the order pin for a body with more than one of each)."""
|
|
positions = []
|
|
start = 0
|
|
while (i := body.find(marker, start)) != -1:
|
|
positions.append(i)
|
|
start = i + 1
|
|
assert positions, f"marker {marker!r} missing from the body"
|
|
for pos in positions:
|
|
assert body.rfind(before, 0, pos) != -1, (
|
|
f"{marker!r} must never run before {before!r}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# brand.js — the ONE shared pass, exported, and the boot runs IT
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_brand_js_exports_the_shared_pass() -> None:
|
|
"""Task 03's extraction: the brand DOM lives in ONE named function
|
|
``applyBrandFromConfig(cfg)`` and reaches the page's MODULE scripts
|
|
through the layer's existing export surface — the ``window`` globals
|
|
it has always had (``window.BOR_BRAND``, ``window.BOR_CONFIG_PROMISE``).
|
|
``brand.js`` stays a classic script (no ``export`` statement —
|
|
``test_frontend_brand.py`` owns that pin), so the assignment IS the
|
|
export."""
|
|
js = _read(BRAND_JS)
|
|
assert "function applyBrandFromConfig(cfg) {" in js, (
|
|
"the shared pass is a named function taking the /api/config payload"
|
|
)
|
|
assert "window.applyBrandFromConfig = applyBrandFromConfig;" in js, (
|
|
"the shared pass is exported on the window surface settings.js / "
|
|
"theme.js already read (window.BOR_BRAND, window.BOR_CONFIG_PROMISE)"
|
|
)
|
|
|
|
|
|
def test_boot_call_site_is_the_extraction_not_a_second_copy() -> None:
|
|
"""The boot behavior is byte-identical (same request, same
|
|
assignments, same order) because the boot RUNS the shared pass:
|
|
``applyBrand`` awaits the parse-time promise and hands the settled
|
|
payload over — its own body contains NONE of the four DOM writes and
|
|
each of them occurs exactly ONCE in the file (the extraction, not a
|
|
duplication)."""
|
|
js = _read(BRAND_JS)
|
|
boot = _body(js, "function applyBrand() {", closer="\n}")
|
|
assert "applyBrandFromConfig(cfg)" in boot, "the boot calls the shared pass"
|
|
assert "BOR_CONFIG_PROMISE.then" in boot, "the boot still rides the parse-time fetch"
|
|
for token in BRAND_DOM_TOKENS:
|
|
assert token not in boot, (
|
|
f"the boot must not carry its own copy of the {token!r} write — "
|
|
"the shared pass is the ONE owner"
|
|
)
|
|
# Each of the four brand DOM writes exists exactly once in the file.
|
|
assert js.count('document.querySelectorAll(".brand-text")') == 1
|
|
assert js.count('document.querySelectorAll(".footer-text")') == 1
|
|
assert js.count('document.querySelector("#message-input")') == 1
|
|
# …and every `document.title` write sits inside the shared pass.
|
|
shared = _body(js, "function applyBrandFromConfig(cfg) {", closer="\n}")
|
|
assert js.count("document.title =") == shared.count("document.title =") >= 1
|
|
# The boot wiring is the phase-39 one (pinned in test_frontend_brand.py).
|
|
assert 'document.addEventListener("DOMContentLoaded", applyBrand)' in js
|
|
assert js.count('= fetch("/api/config"') == 1, (
|
|
"the boot fetch stays the file's only fetch — the SURFACES fetch, "
|
|
"brand.js never fetches twice"
|
|
)
|
|
|
|
|
|
def test_the_shared_pass_reapplies_over_the_name_already_shown() -> None:
|
|
"""A LIVE re-apply runs against a document whose literal passes
|
|
already rewrote the default literal into the previous name — so the
|
|
shared pass also replaces ``window.BOR_BRAND_APPLIED`` (the title,
|
|
the prose text nodes, the aria-label/placeholder/meta attributes:
|
|
the three places the literal passes touch). At boot the global is
|
|
``""`` so the clause is skipped and the boot path stays exactly the
|
|
phase-39 sequence (the byte-identical requirement)."""
|
|
js = _read(BRAND_JS)
|
|
shared = _body(js, "function applyBrandFromConfig(cfg) {", closer="\n}")
|
|
assert 'window.BOR_BRAND_APPLIED = "";' in js, (
|
|
"the previous-name global ships empty — at boot the clause never runs"
|
|
)
|
|
assert "window.BOR_BRAND_APPLIED" in shared, "the shared pass reads the last applied name"
|
|
assert "if (previous && previous !== name) {" in shared, (
|
|
"the re-apply clause is gated on a previous name that differs "
|
|
"(the boot no-op stays byte-identical)"
|
|
)
|
|
assert "document.title.replaceAll(previous, name)" in shared
|
|
assert "node.nodeValue.replaceAll(previous, name)" in shared
|
|
assert 'window.BOR_BRAND_APPLIED = name;' in shared, (
|
|
"the pass records what the document now shows, so the NEXT "
|
|
"re-apply replaces the right string"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# settings.js — the panel re-applies, gated on the three brand keys
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_settings_js_settle_paths_reapply_the_brand() -> None:
|
|
"""The INVERTED task-01 pin: every settle path of the Settings panel
|
|
(Save, the per-row Reset, Clear all) re-applies the brand to the open
|
|
document through the shared pass — AFTER the phase-168 catalogue
|
|
re-GET, which stays the catalogue's own refresh (untouched). Clear
|
|
all passes all three keys (clearing hands every brand string back to
|
|
the environment); the Reset passes its own key; Save passes the keys
|
|
that actually landed — including the prefix of a batch that stopped
|
|
at a later failure (K4), because that value DID take effect."""
|
|
js = _read(SETTINGS_JS)
|
|
assert APPLY_BRAND in js, (
|
|
"task 03 inverted the task-01 absence pin: the panel now reaches "
|
|
"the shared brand pass"
|
|
)
|
|
for header in SETTINGS_SETTLE_PATHS:
|
|
body = _body(js, header)
|
|
assert "await reapplyBrand(" in body, (
|
|
f"{header} must re-apply the brand on settle"
|
|
)
|
|
assert "await loadSettings()" in body, (
|
|
f"the catalogue re-GET stays ({header}) — the phase-168 "
|
|
"refresh the re-apply EXTENDS, never replaces"
|
|
)
|
|
_every_marker_preceded_by(body, "await reapplyBrand(", "await loadSettings()")
|
|
save = _body(js, "async function saveSettings()")
|
|
assert "await reapplyBrand(saved)" in save, (
|
|
"Save re-applies the keys that landed (the K4 prefix included)"
|
|
)
|
|
assert "await reapplyBrand([key])" in _body(js, "async function resetRow("), (
|
|
"the per-row Reset re-applies the key it handed back to the env"
|
|
)
|
|
assert "await reapplyBrand(BRAND_KEYS)" in _body(
|
|
js, "async function confirmClearAll("
|
|
), "Clear all re-applies every brand key"
|
|
|
|
|
|
def _brand_keys_line(js: str) -> str:
|
|
"""The ``BRAND_KEYS`` declaration (the gate's data)."""
|
|
start = js.index("const BRAND_KEYS = [")
|
|
end = js.index("];", start)
|
|
return js[start:end]
|
|
|
|
|
|
def test_settings_js_gates_the_reapply_on_the_three_brand_keys() -> None:
|
|
"""The gate itself, in ONE helper: only ``app_name`` /
|
|
``input_placeholder`` / ``footer_text`` re-apply. A save of any of
|
|
the ~72 other rows costs no extra request and no DOM churn — and the
|
|
helper is the single place the panel reaches ``/api/config``, so the
|
|
surfaces cannot drift (the pin: exactly one shared-pass call and one
|
|
config fetch in the file)."""
|
|
js = _read(SETTINGS_JS)
|
|
helper = _body(js, "async function reapplyBrand(")
|
|
keys_line = _brand_keys_line(js)
|
|
assert tuple(BRAND_KEYS) == (
|
|
"app_name",
|
|
"input_placeholder",
|
|
"footer_text",
|
|
), "the phase-190 brand trio is the panel's own key set"
|
|
for key in BRAND_KEYS:
|
|
assert f'"{key}"' in keys_line, (
|
|
f"the gate must name {key!r} — otherwise its save never re-applies"
|
|
)
|
|
assert "if (!keys.some((key) => BRAND_KEYS.includes(key))) return;" in helper, (
|
|
"a settle that touched no brand key makes NO re-apply request"
|
|
)
|
|
assert js.count(SHARED_CALL) == 1, (
|
|
"ONE shared-pass call site in the panel — inside the helper"
|
|
)
|
|
assert js.count(CONFIG_FETCH) == 1, (
|
|
"ONE /api/config fetch in the panel module (the helper's)"
|
|
)
|
|
assert '{ cache: "no-store" }' in helper, (
|
|
"never a cached brand — the same no-store read the boot makes"
|
|
)
|
|
|
|
|
|
def test_settings_js_reapply_is_single_flight_under_the_existing_guard() -> None:
|
|
"""The §7.4 never-stale guard, EXTENDED rather than forked: the Save
|
|
button's own ``saveInFlight`` flag still owns the save (the re-apply
|
|
is awaited before it drops, so a second Save cannot start while the
|
|
config fetch is out), and the ONE flag inside the shared helper covers
|
|
the settle paths that reach it — a second Save / Reset / Clear all
|
|
never stacks a second ``/api/config`` fetch."""
|
|
js = _read(SETTINGS_JS)
|
|
helper = _body(js, "async function reapplyBrand(")
|
|
assert "let brandReapplyInFlight = false;" in js
|
|
assert "if (brandReapplyInFlight) return;" in helper, (
|
|
"one re-apply at a time — a second settle never stacks a fetch"
|
|
)
|
|
assert "brandReapplyInFlight = true;" in helper
|
|
finally_arm = helper[helper.index("} finally {") :]
|
|
assert "brandReapplyInFlight = false;" in finally_arm, (
|
|
"the flag clears in the finally — a failed re-apply can never "
|
|
"strand the guard on (the house stuck-button contract)"
|
|
)
|
|
save = _body(js, "async function saveSettings()")
|
|
guard_off = save.index("saveInFlight = false;")
|
|
assert save.index("await reapplyBrand(saved)") < guard_off, (
|
|
"the brand re-apply sits INSIDE the saveInFlight scope — the "
|
|
"existing guard's reach, not a parallel one"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# theme.js — the tab re-applies after Save and after Reset
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_theme_js_save_and_reset_reapply_the_brand() -> None:
|
|
"""The Theme tab's two settle paths run the SAME shared pass after
|
|
the phase-92 palette reconcile: the tab no longer EDITS the strings
|
|
(A1) but a Reset nulls the stored row, and a legacy baked-in string
|
|
then FALLS BACK (panel row → env) — the open document must re-read
|
|
it without a reload. One code path for both surfaces' semantics
|
|
(``reapplyBrand``, called by Save and by Reset)."""
|
|
js = _read(THEME_JS)
|
|
save = _body(js, "async function saveTheme()")
|
|
reset = _body(js, "async function resetTheme()")
|
|
for name, body in (("saveTheme", save), ("resetTheme", reset)):
|
|
assert "await reapplyBrand()" in body, (
|
|
f"{name} re-applies the brand strings on success"
|
|
)
|
|
assert body.index("applyServedTheme(settings)") < body.index(
|
|
"await reapplyBrand()"
|
|
), "the palette reconcile keeps its place; the brand follows it"
|
|
assert js.count(SHARED_CALL) == 1, (
|
|
"ONE shared-pass call site in the tab module — inside its helper"
|
|
)
|
|
assert js.count(CONFIG_FETCH) == 1, (
|
|
"ONE /api/config fetch in the tab module, and the SAME statement "
|
|
"the panel uses (one shared semantics, one shape)"
|
|
)
|
|
helper = _body(js, "async function reapplyBrand()")
|
|
assert "if (brandReapplyInFlight) return;" in helper, (
|
|
"one re-apply at a time across Save and Reset (§7.4)"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# the failure arm — never a blanked brand (both surfaces)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_a_failed_reapply_leaves_the_dom_at_its_last_applied_state() -> None:
|
|
"""The phase-168 "never a blanked panel" precedent, applied to the
|
|
brand: in BOTH helpers a non-2xx returns BEFORE the shared pass runs,
|
|
and a network/JSON failure is swallowed with no DOM write at all —
|
|
the open page keeps the name, footer and placeholder it already
|
|
shows, and the save's own status / error line is the visibility."""
|
|
for path in (SETTINGS_JS, THEME_JS):
|
|
js = _read(path)
|
|
helper = _body(js, "async function reapplyBrand(")
|
|
assert "if (!r.ok) return;" in helper, (
|
|
f"{path.name}: a non-2xx /api/config must never reach the DOM"
|
|
)
|
|
assert helper.index("if (!r.ok) return;") < helper.index(
|
|
SHARED_CALL
|
|
), f"{path.name}: the shared pass runs only on a settled payload"
|
|
assert "} catch {" in helper, (
|
|
f"{path.name}: an unreachable app / non-JSON body is caught, "
|
|
"not thrown (the brand never blanks)"
|
|
)
|
|
catch_arm = helper[helper.index("} catch {") : helper.index("} finally {")]
|
|
for token in BRAND_DOM_TOKENS:
|
|
assert token not in catch_arm, (
|
|
f"{path.name}: the failure arm writes nothing ({token!r})"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# the ownership pins carried unchanged from task 01
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_settings_js_never_writes_the_brand_dom_directly() -> None:
|
|
"""The shared-pass ownership pin — GREEN before, during AND after the
|
|
phase: the four brand DOM writes (title, ``.brand-text``, the composer
|
|
placeholder, ``.footer-text``) belong to the ONE brand path in
|
|
``brand.js``; the Settings panel must never duplicate one of them
|
|
inline. Task 03's live-apply goes through the shared
|
|
``applyBrandFromConfig`` call, so this file stays free of the tokens
|
|
even after the fix (no second copy of the DOM path)."""
|
|
js = _read(SETTINGS_JS)
|
|
for token in BRAND_DOM_TOKENS:
|
|
assert token not in js, (
|
|
f"settings.js must not own the brand DOM write {token!r} — "
|
|
"the shared brand.js pass is its ONE owner"
|
|
)
|
|
|
|
|
|
def test_theme_js_save_and_reset_reconcile_colors_only() -> None:
|
|
"""GREEN pre-phase AND through the phase: ``saveTheme``'s (and
|
|
``resetTheme``'s) success path calls ``applyServedTheme`` — the
|
|
phase-92 COLOR reconcile — and neither writes the brand DOM (no
|
|
title, no ``.brand-text``, no composer placeholder, no footer line).
|
|
Task 03's live brand reaches the document through the shared call,
|
|
which is exactly what this pin keeps true."""
|
|
js = _read(THEME_JS)
|
|
save = _body(js, "async function saveTheme()")
|
|
reset = _body(js, "async function resetTheme()")
|
|
assert "applyServedTheme(settings)" in save, (
|
|
"the success path keeps the phase-92 color reconcile"
|
|
)
|
|
assert "applyServedTheme(settings)" in reset, (
|
|
"the reset path keeps the phase-92 color reconcile"
|
|
)
|
|
for name, body in (("saveTheme", save), ("resetTheme", reset)):
|
|
for token in BRAND_DOM_TOKENS:
|
|
assert token not in body, (
|
|
f"{name} must stay colors-only at the DOM level — "
|
|
f"the brand write {token!r} belongs to the shared brand pass"
|
|
)
|