"""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" )