"""Unit: the phase-76 shell router contract (task 01).
The browser behavior is E2E-gated (the phase-76 story suite —
``test_nav_switch_keeps_stream.py`` — plus the tuning-adjacent suites);
here we pin the router.js / index.html source-level invariants the
"views of one document" architecture depends on, so a silent
regression is caught without a browser (house pattern:
``tests/unit/test_frontend_hidden_tab.py`` reads JS source and
asserts on its mechanisms).
Pinned design (phase 76 overview + task 01):
* the VIEW map — pathname → view name — is the ONLY set of paths the
click interceptor may swallow (every other link keeps its real,
document-level navigation);
* a switch is ``history.pushState`` + show/hide — NEVER a document
load (no ``location.assign`` / ``location.href`` / ``location.replace``
/ ``location.reload`` anywhere in the module);
* mount-once per view: the lazy module is imported on FIRST show only,
the guard runs before the import and is set only after ``mount``
resolves;
* hidden views carry BOTH ``hidden`` AND ``inert`` (WCAG — a hidden
view must not receive focus or keyboard traversal);
* the router is the SINGLE WRITER of the ``.nav-link`` active state
(``is-active`` + ``aria-current="page"``), of ``document.title``, and
of the per-view ````;
* focus lands on the target view ONLY on user-initiated switches
(navbar click / popstate) — never on initial boot (no focus steal);
* the shell markup: ONE main holding the view sections, only the Chat
link statically active, boot order brand.js → app.js → router.js,
and the chat view needs no module import (app.js ran at shell boot).
Phase 76 task 04 (the header is shell-owned): the shell's header is the
canonical one — the old per-page header copies (with their static
active stamps) are gone with the four folded view documents, so
``is-active`` occurs EXACTLY ONCE in the whole of index.html (on the
Chat link), and header.js carries NO ``is-active`` write: the whoami
auth gate + the mobile hamburger are its only nav responsibilities,
and the router is the SINGLE runtime writer of the active state.
Phase 77 task 01 (the re-show refresh hook): a user-initiated re-show
of an ALREADY-MOUNTED view dispatches ``bor:view-refresh`` on the
view's section — gated on the pre-mount ``wasMounted`` capture, so the
first show (the mount) and boot never fire it (the mount's own load is
the first fetch); a re-click of the active view's own nav link
dispatches the event instead of a bare return (no ``pushState`` — the
URL is already that view's path); the History view listens (armed only
in the admin branch, after the whoami gate — anonymous never fetches).
Phase 77 task 02 (the other data views join the refresh): RAG
(``sources.js``), Sources (``git-sources.js``) and Tuning
(``tuning.js``) each listen for ``bor:view-refresh`` on their root and
re-run their existing load (armed only in the admin branch, after the
whoami gate — the same gate guard as History). ``sources.js``'s
``loadDocs`` clears the tbody's rows at the TOP (before the fetch —
the History pattern), so a refresh from a populated list into an empty
result leaves no ghost rows. The Chat view (``app.js``) does NOT
listen — the negative pin: the in-flight SSE stream and the local
conversation must survive every switch (the phase-76 contract), so
the exclusion is a contract, not an oversight.
Phase 77 task 03 (the explicit History refresh control, TODO.md L3):
the History page-head becomes a flex row (scoped to ``#view-history``
— the other four views' page-heads are untouched) carrying the
``#history-refresh`` button (``aria-label="Refresh saved chats"``,
the aria-hidden house refresh glyph + the visible "Refresh" label —
the phase-46 auth-link convention) OUTSIDE the table wrap (reachable
while the empty state shows). history.js binds it in the admin branch
only (the anonymous branch hides it — no dead control beside the
gate); the click disables the button (no double-fire in flight),
reruns the re-entrant ``loadChats()`` and re-enables on success AND
failure (the finally). The outcome lands in ``#history-status``:
``Saved chats refreshed.`` on success (a 0-row fetch is a success) —
and the failure lines now live INSIDE ``loadChats`` (the house copy:
"is the app reachable?" / "try again."), so every caller of a failed
load sees it (the §7.4 never-stale contract). styles.css reuses the
``.new-chat-btn`` visual language (brand pill, ≥44px, hover,
:disabled) and goes icon-only below 640px.
"""
from __future__ import annotations
import re
from pathlib import Path
FRONTEND = Path(__file__).resolve().parents[2] / "frontend"
ASSETS = FRONTEND / "assets"
ROUTER_JS = ASSETS / "router.js"
INDEX_HTML = FRONTEND / "index.html"
def _js() -> str:
assert ROUTER_JS.is_file(), f"missing {ROUTER_JS}"
return ROUTER_JS.read_text(encoding="utf-8")
def _html() -> str:
assert INDEX_HTML.is_file(), f"missing {INDEX_HTML}"
return INDEX_HTML.read_text(encoding="utf-8")
# ---------- the VIEW map: the only interceptable paths ----------
def test_view_map_covers_the_shell_paths() -> None:
"""The VIEW map is pathname → view name: the shell's own two URLs
("/" and "/index.html") are the chat view, plus one entry per
folded view (tasks 01–03: tuning, rag, git-sources, history;
phase 79 task 06: tokens; phase 91 task 04: theme — all six
non-chat navbar views are in)."""
js = _js()
view_start = js.find("const VIEW = {")
assert view_start != -1, "the VIEW map must exist"
view_body = js[view_start : js.find("\n}", view_start)]
assert '"/": "chat"' in view_body, "the app root is the chat view"
assert '"/index.html": "chat"' in view_body, (
"the shell's alternate URL is the chat view too (HTML_PAGES)"
)
assert '"/tuning.html": "tuning"' in view_body, (
"task 01 folds the Tuning view into the shell"
)
assert '"/history.html": "history"' in view_body, (
"task 03 folds the History view into the shell"
)
assert '"/tokens.html": "tokens"' in view_body, (
"phase 79 task 06 folds the Tokens view into the shell"
)
assert '"/theme.html": "theme"' in view_body, (
"phase 91 task 04 folds the Theme view into the shell"
)
# The view names are the #view- section slugs in index.html.
for name in ("chat", "tuning", "history", "tokens", "theme"):
assert f'id="view-{name}"' in _html(), f"missing the #view-{name} section"
def test_interceptor_matches_only_view_map_paths() -> None:
"""The delegated nav click handler intercepts ONLY a link whose href
is in VIEW — the `in VIEW` guard runs BEFORE preventDefault, and a
non-view link (login, the viewer, the not-yet-folded views) falls
through to its real, document-level navigation."""
js = _js()
fn = js.find('nav.addEventListener("click"')
assert fn != -1, "the delegated click handler on the nav must exist"
body = js[fn : js.find("\n });", fn)]
guard = body.find("in VIEW")
prevent = body.find("e.preventDefault()")
assert 0 <= guard < prevent, (
"the VIEW membership guard must run BEFORE the preventDefault"
)
assert 'closest("a.nav-link")' in body, (
"only the .nav-link family is considered (auth links are untouched)"
)
def test_switches_use_pushstate_not_document_navigation() -> None:
"""A view switch is history.pushState (same-document) — the module
must contain NO document-level navigation primitive: no
location.assign, no location.href write, no location.replace, no
location.reload (the phase-48 abort path lives in app.js, not here)."""
js = _js()
assert "history.pushState" in js, "the switch must pushState"
for banned in ("location.assign", "location.href", "location.replace", "location.reload"):
assert banned not in js, f"{banned} is a document load — the switch is same-document"
# The pushState is the click handler's (the popstate path only READS
# the location — it never writes it).
fn = js.find('nav.addEventListener("click"')
body = js[fn : js.find("\n });", fn)]
assert "history.pushState" in body
def test_popstate_switches_views() -> None:
"""Back / forward re-runs the switch for the pathname in history —
user-initiated (focus + top landing included)."""
js = _js()
fn = js.find('window.addEventListener("popstate"')
assert fn != -1, "the popstate listener must exist"
body = js[fn : js.find("});", fn)]
assert "location.pathname" in body, "popstate resolves the view from the pathname"
assert "userInitiated: true" in body, "back/forward is a user-initiated switch"
# ---------- mount-once, hide-forever ----------
def test_mount_once_guard_runs_before_import_and_after_mount() -> None:
"""A non-chat view's module is imported on FIRST show only: the
`mounted[name]` guard is checked BEFORE the lazy import, the import
+ `await module.mount(root)` run once, and the guard is set only
AFTER mount resolves (a failed mount may retry on the next show)."""
js = _js()
fn = js.find("async function switchTo")
assert fn != -1, "switchTo must exist"
body = js[fn : js.find("\n}", fn)]
guard = body.find("if (!mounted[name])")
load = body.find("await load()")
mount = body.find("await mod.mount(root)")
set_guard = body.find("mounted[name] = true")
assert 0 <= guard < load < mount < set_guard, (
"guard → lazy import → mount → set guard (in that order)"
)
# The guard map starts with chat mounted (app.js ran at shell boot).
assert re.search(r"const mounted = \{ chat: true \}", js), (
"chat starts mounted — it needs no module import"
)
def test_only_non_chat_views_have_lazy_modules() -> None:
"""VIEW_MODULES lazy-imports the non-chat views only — the view
modules are STATIC specifiers (so the Containerfile's esbuild
stage can inline them into the router bundle) and there is NO
import of app.js (the chat view needs no module — it ran at shell
boot)."""
js = _js()
mods_start = js.find("const VIEW_MODULES = {")
assert mods_start != -1, "the lazy module map must exist"
mods_body = js[mods_start : js.find("\n}", mods_start)]
assert 'tuning: () => import("./tuning.js")' in mods_body, (
"the Tuning view module is lazy-imported on first show"
)
assert 'history: () => import("./history.js")' in mods_body, (
"the History view module is lazy-imported on first show"
)
assert 'tokens: () => import("./tokens.js")' in mods_body, (
"the Tokens view module is lazy-imported on first show"
)
assert 'theme: () => import("./theme.js")' in mods_body, (
"the Theme view module is lazy-imported on first show (phase 91)"
)
assert '"chat"' not in mods_body, "the chat view has no lazy module"
assert 'import("./app.js")' not in js, "app.js must never be lazy-imported"
# ---------- show / hide: hidden AND inert ----------
def test_hidden_views_get_both_hidden_and_inert() -> None:
"""Show = drop hidden AND inert; hide = add BOTH — the same
comparison drives both attributes in one loop, so a view can never
be visible-but-inert or inert-but-visible (WCAG: a hidden view must
not receive focus or keyboard traversal)."""
js = _js()
fn = js.find("async function switchTo")
body = js[fn : js.find("\n}", fn)]
loop = body.find("Object.entries(viewEls)")
assert loop != -1, "the show/hide loop must walk every view"
loop_body = body[loop : body.find("\n }", loop)]
hidden_i = loop_body.find("el.hidden =")
inert_i = loop_body.find("el.inert =")
assert 0 <= hidden_i < inert_i, "both attributes are set in the same loop"
assert loop_body.count("viewName !== name") == 2, (
"one comparison drives hidden AND inert (they can never drift)"
)
# ---------- the router is the single writer ----------
def test_router_writes_active_state_title_and_meta() -> None:
"""The router stamps the .nav-link active state (is-active +
aria-current, removed on the inactive links), document.title, and
the per-view meta description — values carried over from the old
pages' s (the tuning title/description survive the fold).
Phase 76 (task 02): the title/meta are composed through
titleFor()/descFor() — the per-view value with the brand literal
replaced by window.BOR_BRAND (phase 39): the lazy view import
defers switchTo past brand.js's one-time DOM pass, so a literal
stamp would overwrite a configured deployment's name; composing
at write time is a no-op for the default deployment."""
js = _js()
fn = js.find("async function switchTo")
body = js[fn : js.find("\n}", fn)]
assert 'querySelectorAll("a.nav-link")' in body, "the writer walks the nav links"
assert 'classList.toggle("is-active"' in body, "is-active is stamped + removed"
assert 'setAttribute("aria-current", "page")' in body
assert 'removeAttribute("aria-current")' in body
assert "document.title = titleFor(name)" in body
assert "metaDesc.content = descFor(name)" in body
# The carried-over values (the old pages' s — default form).
assert 'chat: "Brain of Reese"' in js
assert 'tuning: "Global Tuning · Brain of Reese"' in js
assert "Manage the global tuning notes that steer every Brain of Reese answer." in js
assert 'history: "Saved chats · Brain of Reese"' in js
assert "Saved chats — every conversation is saved automatically, one click back." in js
assert 'tokens: "Access tokens · Brain of Reese"' in js
assert "Generate and revoke the API tokens that let people use the app." in js
assert 'theme: "Theme · Brain of Reese"' in js
assert (
"Set the palette and branding — the theme is baked into every served page, "
"live on the first paint."
) in js
# The brand composition (phase 39's window.BOR_BRAND, read at
# write time — never a hardcoded stamp).
assert 'window.BOR_BRAND || "Brain of Reese"' in js
assert 'TITLES[view].replaceAll("Brain of Reese", brandName())' in js
assert 'DESCRIPTIONS[view].replaceAll("Brain of Reese", brandName())' in js
def test_focus_only_on_user_initiated_switches() -> None:
"""The target view is focused ONLY when the switch is
user-initiated (navbar click / popstate) — the boot switch passes
userInitiated:false, so a page load never steals focus. The top
landing (scrollTo 0,0) rides the same flag. (Phase 88: the opts
object also carries the boot flag — the boot call site passes
``boot: true`` so the boot show never fires the view refresh; the
focus gate reads ``opts.userInitiated``.)"""
js = _js()
fn = js.find("async function switchTo")
body = js[fn : js.find("\n}", fn)]
flag = body.rfind("if (opts.userInitiated)")
focus = body.find("root.focus(")
scroll = body.find("window.scrollTo(0, 0)")
assert 0 <= flag < scroll < focus, "focus + top landing sit inside the flag"
# Boot is NOT user-initiated (no focus steal on load).
boot = js.find("switchTo(bootName")
assert boot != -1 and "userInitiated: false" in js[boot : boot + 60]
# The click handler IS user-initiated.
click = js.find('nav.addEventListener("click"')
click_body = js[click : js.find("\n });", click)]
assert "userInitiated: true" in click_body
# ---------- the shell markup + boot order ----------
def test_shell_markup_has_one_main_two_views_and_chat_only_active() -> None:
"""index.html: ONE main#main holds the view sections; the Tuning
section ships hidden AND inert (the a11y pair); ONLY the Chat link
carries the static active stamp (the router is the single writer —
no view other than chat may ship statically active)."""
html = _html()
assert html.count('id="main"') == 1, "the shell has exactly one main"
main = html.find('')
assert main != -1
view_chat = html.find('", main)
assert main < view_chat < view_tuning < main_end, (
"both view sections live inside the single main (chat first)"
)
tuning_tag = html[view_tuning : html.find(">", view_tuning)]
assert "hidden" in tuning_tag and "inert" in tuning_tag, (
"the folded view ships hidden AND inert"
)
assert 'tabindex="-1"' in html[view_chat : html.find(">", view_chat)]
assert 'tabindex="-1"' in tuning_tag, "the target view is focusable"
# Only the Chat link is statically active (exactly one stamp, on Chat).
assert html.count('class="nav-link is-active"') == 1, (
"only ONE nav link may ship statically active"
)
active = html.find('Chat')
assert active != -1, "the static active stamp is the Chat link"
# The tuning nav link ships hidden (admin-only) and UNstamped.
tuning_match = re.search(r']*id="nav-tuning"[^>]*>', html)
assert tuning_match, "the shell must carry the #nav-tuning nav link"
tuning_link = tuning_match.group(0)
assert "hidden" in tuning_link, "#nav-tuning ships hidden (admin-only)"
assert "is-active" not in tuning_link, "no static active stamp on the Tuning link"
# The Tokens nav link (phase 79 task 06) ships hidden (admin-only)
# and UNstamped too — the router is the single writer of the active
# state, and a token user (role "user") must never see the link
# (header.js reveals it for admin only).
tokens_match = re.search(r']*id="nav-tokens"[^>]*>', html)
assert tokens_match, "the shell must carry the #nav-tokens nav link"
tokens_link = tokens_match.group(0)
assert "hidden" in tokens_link, "#nav-tokens ships hidden (admin-only)"
assert "is-active" not in tokens_link, "no static active stamp on the Tokens link"
# The Theme nav link (phase 91 task 04) ships hidden (admin-only)
# and UNstamped too — the router is the single writer of the active
# state, and a token user (role "user") must never see the link
# (header.js reveals it for admin only).
theme_match = re.search(r']*id="nav-theme"[^>]*>', html)
assert theme_match, "the shell must carry the #nav-theme nav link"
theme_link = theme_match.group(0)
assert "hidden" in theme_link, "#nav-theme ships hidden (admin-only)"
assert "is-active" not in theme_link, "no static active stamp on the Theme link"
# ---------- phase 76 task 04: the header is shell-owned ----------
def test_shell_carries_exactly_one_static_is_active_on_chat() -> None:
"""Phase 76 task 04: the shell's ONE header is the canonical header —
the folded pages' header copies (which stamped is-active statically
in their own markup) are gone, so ``is-active`` occurs EXACTLY ONCE
in the whole of index.html, on the Chat link (the default view).
A second stamp anywhere (a leaked page copy, a non-chat view
shipping statically active) would break the router's single-writer
contract the moment it disagrees with a switch."""
html = _html()
assert html.count("is-active") == 1, (
"index.html must carry is-active exactly once (the Chat link's stamp)"
)
i = html.find("is-active")
tag_start = html.rfind("", tag_start)
tag = html[tag_start:tag_end]
assert tag.startswith(' None:
"""Phase 76 task 04: header.js is NOT a writer of the nav's active
state — it never was (the old pages stamped is-active statically in
their OWN markup; the shell's ONE header is the only header left)
— and it must never become one: the whoami auth gate (the sign-in/
sign-out pair + the admin-only nav links' hidden attributes) and the
mobile hamburger are its only nav responsibilities, and neither
touches the active state. The string is absent from the module
entirely; the SINGLE runtime writer is the router (pinned in
test_router_writes_active_state_title_and_meta)."""
header_js = (ASSETS / "header.js").read_text(encoding="utf-8")
assert "is-active" not in header_js, (
"header.js must carry no is-active write — the router is the "
"SINGLE WRITER of the active state (the shell markup ships the "
"one static stamp on the Chat link)"
)
def test_boot_order_is_brand_app_router() -> None:
"""The shell's script boot order: brand.js (classic) FIRST, then
app.js (the chat view module — runs at shell boot exactly as
today), then router.js (module) — the router may only see a
fully-booted chat view."""
html = _html()
srcs = re.findall(r'