"""Unit: the phase-39 brand layer (BOR_APP_NAME → window.BOR_BRAND). No Python logic exists for this task — the behavior lives in ``frontend/assets/brand.js`` + the templates + the page scripts, and it is E2E-gated by the story suite (task 03). Like the other frontend-adjacent unit files, this module pins the JS markers the story depends on, so a silent regression in the brand layer is caught without a browser. """ from __future__ import annotations import re from pathlib import Path FRONTEND = Path(__file__).resolve().parents[2] / "frontend" ROOT = FRONTEND.parent BRAND_JS = FRONTEND / "assets" / "brand.js" APP_JS = FRONTEND / "assets" / "app.js" DOCUMENT_JS = FRONTEND / "assets" / "document.js" CONTAINERFILE = ROOT / "Containerfile" #: Every page in the app ships the brand layer (phase 39: "every visible #: brand string on every page resolves from one place"). HTML_PAGES = ( "index.html", "sources.html", "tuning.html", "document.html", "login.html", "git-sources.html", "history.html", # phase 50: the admin saved-chats page "shared.html", # phase 51: the anonymous shared-conversation page "doc-edit.html", # phase 59: the admin doc edit screen (flow page) ) def _text(path: Path) -> str: return path.read_text(encoding="utf-8") def test_brand_js_is_a_classic_script_with_synchronous_default() -> None: """brand.js is NOT a module (its top level must run at parse time, before the page's module scripts evaluate) and sets window.BOR_BRAND synchronously, BEFORE the /api/config fetch starts.""" js = _text(BRAND_JS) assert not re.search(r"^\s*import\s", js, re.M), ( "brand.js must be a classic script — no import statements" ) assert not re.search(r"^\s*export\s", js, re.M), ( "brand.js must be a classic script — no export statements" ) default_idx = js.find('window.BOR_BRAND = "Brain of Reese"') fetch_idx = js.find('fetch("/api/config"') assert 0 <= default_idx < fetch_idx, ( "the default name must be set at top level before the fetch" ) def test_brand_js_fetches_api_config_no_store() -> None: """The single source of the name is GET /api/config, never cached — a renamed app must not serve a stale name from the HTTP cache — and a fetch failure only warns (the loadHealth house style: the page never breaks).""" js = _text(BRAND_JS) assert 'fetch("/api/config", { cache: "no-store" })' in js assert "console.warn" in js def test_brand_js_defers_dom_application_until_ready() -> None: """The DOM passes run on DOMContentLoaded while parsing, otherwise immediately (the script sits at the end of ).""" js = _text(BRAND_JS) assert 'document.readyState === "loading"' in js assert 'document.addEventListener("DOMContentLoaded", applyBrand)' in js def test_brand_js_reskins_title_brand_text_prose_and_attributes() -> None: """The four DOM passes (phase 39 task 02): the document title, every .brand-text (bold split for "Brain of …" names, plain text otherwise — the name is HTML-escaped before innerHTML), a TreeWalker over the text nodes (script/style nodes rejected — the page source is never rewritten), and the aria-label / placeholder / meta content attributes.""" js = _text(BRAND_JS) assert "document.title.replaceAll" in js assert ".brand-text" in js assert 'name.startsWith("Brain of ")' in js assert "escapeHTML" in js, "the name must be escaped before innerHTML" assert "el.textContent = name" in js, "non-'Brain of ' names render plain" assert "document.createTreeWalker" in js assert "NodeFilter.SHOW_TEXT" in js assert 'tag === "SCRIPT" || tag === "STYLE"' in js assert "replaceAll(BRAND_LITERAL, name)" in js for marker in ('"aria-label"', '"placeholder"', "meta[content]"): assert marker in js, f"the attribute pass must cover {marker}" 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). 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" ) # 5. The composer placeholder (chat page only — the null guard # no-ops on every other page). assert 'document.querySelector("#message-input")' in js assert "input_placeholder" in js # 6. The footer line on all 9 pages (the phase-61 hook) — via # 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/, 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= 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 # 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) {"): assert guard in js, ( f"an empty value must skip its application ({guard})" ) # Independence: the phase-62 block sits AFTER the app_name passes # in the same .then — never gated by the name. assert js.index("// 4. Attributes:") < js.index("// Phase 62"), ( "the customization keys must apply after the app_name block, " "even when the name is the default/empty" ) # The app_name literal default pin still holds. assert 'window.BOR_BRAND = "Brain of Reese"' in js def test_page_scripts_keep_the_default_literal_exactly_once() -> None: """The fallback literal lives in the page scripts' brand() reads — exactly one copy per file (a second copy could drift out of sync).""" assert _text(APP_JS).count('"Brain of Reese"') == 1, ( "app.js: the literal must appear only in the brand() fallback" ) assert _text(DOCUMENT_JS).count('"Brain of Reese"') == 1, ( "document.js: the literal must appear only in the brand() fallback" ) def test_app_js_labels_resolve_the_name_at_call_time() -> None: """Phase 39 task 02: the status labels, the typing label, the elapsed-seconds hint and the tool labels read window.BOR_BRAND through brand() — at CALL time, so a label set after /api/config lands carries the configured name (a module-level const would freeze the default).""" js = _text(APP_JS) assert 'const brand = () => window.BOR_BRAND || "Brain of Reese";' in js assert "`${brand()} is thinking`" in js assert "`${brand()} is answering`" in js assert "`${brand()} is still thinking (${secs}s)`" in js assert "`${brand()} is reading ${argument}`" in js assert "`${brand()} is listing documents`" in js # The frozen module-level literals must be gone (stale-name bug). assert '"Brain of Reese is thinking"' not in js assert '"Brain of Reese is answering"' not in js def test_document_js_titles_resolve_the_name() -> None: """The viewer page titles (render + not-found) carry the resolved name — the module sets them itself, so it must use brand() (the static document.html title is handled by the brand.js title pass).""" js = _text(DOCUMENT_JS) assert 'document.title = `${doc.title} · ${brand()}`' in js assert 'document.title = `Document not found · ${brand()}`' in js assert 'window.BOR_BRAND || "Brain of Reese"' in js def test_every_page_loads_brand_js_before_its_module_script() -> None: """All pages load brand.js as a CLASSIC script, before the page's module script (modules execute after parsing — the synchronous default must be set first).""" for page in HTML_PAGES: html = _text(FRONTEND / page) # Optional leading slash: root-level pages use "assets/brand.js", # but the shared page is served from the NESTED /shared/ # route, where a relative ref would 404 — it uses "/assets/…". m = re.search(r'', html) assert m, f"{page}: brand.js must load" brand_idx = m.start() module_idx = html.find('