"""Unit: phase-61 text pins — the retired homelab-era copy is GONE, the locked neutral replacements are PRESENT (house pattern: read the files as text, assert substrings — no browser, cf. test_frontend_brand.py). The browser-visible layer is gated by the story suite (``tests/e2e/test_stale_ui_copy.py``); these pins catch a silent regression in the templates / the ``Settings`` defaults without it. """ from __future__ import annotations import re from pathlib import Path FRONTEND = Path(__file__).resolve().parents[2] / "frontend" ROOT = FRONTEND.parent CONFIG_PY = ROOT / "app" / "config.py" #: Every page in the app ships a footer (the phase-39 ``HTML_PAGES`` #: tuple, repeated here so this file stands alone). HTML_PAGES = ( "index.html", "sources.html", "tuning.html", "document.html", "login.html", "git-sources.html", "history.html", "shared.html", "doc-edit.html", ) # --- the locked replacement copy (owner-locked A1 / A2) ---------------- META = "Ask anything about your indexed documents — every answer cites the exact doc." EMPTY_SUB = ( "I've read through your documents — ask me anything, and I'll point " "you at the exact doc. You've got this." ) PLACEHOLDER = "Ask me anything…" PAGE_SUB = ( "Every file indexed from your configured sources — git repositories, " "local directories, and uploaded archives. Press Sync " "sources to pull the latest and re-import." ) FOOTER = "Powered by self-hosted models" CHIPS = [ "What documents are in the knowledge base?", "Which source does each answer come from?", "How do I add a new source?", "Summarize the most recent document.", ] # --- the retired homelab-era copy (must be nowhere) -------------------- OLD_META_FRAG = "the homelab and deployments" OLD_SUB_FRAG = "the homelab and deployment notes" OLD_PLACEHOLDER_FRAG = "Ask me about the homelab" OLD_FOOTER = "Reese's self-hosted models" OLD_GIT_EXAMPLE = "github.com/you/homelab" OLD_CHIPS = [ "How is my Kubernetes cluster set up?", "What's my backup strategy?", "How do I deploy a new service?", "What's currently running in the homelab?", ] def _text(path: Path) -> str: return path.read_text(encoding="utf-8") def _norm(text: str) -> str: """Collapse whitespace runs — the templates wrap long lines, so the locked copy is pinned against the normalized text.""" return re.sub(r"\s+", " ", text).strip() def test_chat_page_old_copy_is_gone() -> None: """index.html: the old meta fragment, the old empty-state sub fragment, the old composer placeholder and the old footer are all retired. (The L176 code comment still says 'homelab' — A3: comments are not user-visible and stay untouched, so only the exact old user-visible strings are pinned.)""" html = _text(FRONTEND / "index.html") for frag in (OLD_META_FRAG, OLD_SUB_FRAG, OLD_PLACEHOLDER_FRAG, OLD_FOOTER): assert frag not in html, f"retired chat copy still present: {frag!r}" def test_sources_page_old_copy_is_gone() -> None: """sources.html: the ~/Homelab + ~/Deployments page-sub citations and the old footer are retired.""" html = _text(FRONTEND / "sources.html") for frag in ("~/Homelab", "~/Deployments", OLD_FOOTER): assert frag not in html, f"retired sources copy still present: {frag!r}" def test_git_sources_page_old_copy_is_gone() -> None: """git-sources.html: the old example repo URL (A3) and the old footer are retired.""" html = _text(FRONTEND / "git-sources.html") for frag in (OLD_GIT_EXAMPLE, OLD_FOOTER): assert frag not in html, f"retired git-sources copy still present: {frag!r}" def test_chat_page_locked_copy_present_exactly_once() -> None: """The three locked (A1) chat strings, each exactly once in index.html (a second copy could drift out of sync).""" html = _norm(_text(FRONTEND / "index.html")) assert html.count(META) == 1, "the locked meta description" assert html.count(EMPTY_SUB) == 1, "the locked empty-state sub" assert html.count(PLACEHOLDER) == 1, "the locked composer placeholder" def test_sources_page_sub_is_the_locked_copy() -> None: """The KB .page-sub (the string the TODO cited by name) reads the locked (A1) copy, including the around Sync sources — pinned inside the .page-sub element, not anywhere in the file.""" html = _text(FRONTEND / "sources.html") m = re.search(r'

(.*?)

', html, re.DOTALL) assert m, "sources.html must keep the .page-sub" assert _norm(m.group(1)) == PAGE_SUB def test_all_nine_footers_are_the_locked_neutral_default() -> None: """Every page carries the locked (A1) footer inside exactly one ``class="footer-text"`` span (the stable hook phase 62's BOR_FOOTER_TEXT env var drives).""" span = f'{FOOTER}' for page in HTML_PAGES: html = _text(FRONTEND / page) assert html.count('class="footer-text"') == 1, ( f"{page}: exactly one .footer-text span" ) assert html.count(span) == 1, f"{page}: the locked neutral footer" def test_config_default_chips_are_the_locked_list_in_order() -> None: """The ``Settings.suggestions`` default (app/config.py) is exactly the four locked (A2) chips, in order (the E2E conftest derives the app-under-test env from this field — it can never drift).""" config = _text(CONFIG_PY) m = re.search(r"suggestions: list\[str\] = \[(.*?)\]", config, re.DOTALL) assert m, "the suggestions default list must exist in app/config.py" block = m.group(1) positions = [] for chip in CHIPS: assert f'"{chip}"' in block, f"locked chip missing from the default: {chip!r}" positions.append(block.index(f'"{chip}"')) assert positions == sorted(positions), "the chips must keep the locked order" def test_old_chips_are_retired_not_relocated() -> None: """The four old default chips are retired, not relocated: they must appear NOWHERE in the app source (app/ + frontend/) — config.py included (the scan covers it).""" for base in (ROOT / "app", FRONTEND): for path in sorted(base.rglob("*")): if not path.is_file() or "__pycache__" in path.parts: continue text = path.read_text(encoding="utf-8") for chip in OLD_CHIPS: assert chip not in text, ( f"retired chip {chip!r} relocated to {path.relative_to(ROOT)}" )