Files
brain-of-reese/tests/unit/test_frontend_router.py
T
ducoterra 7fce6572d0
Build and Push Containers / build-and-push-app (push) Successful in 1m45s
Build and Push Containers / build-and-push-db (push) Successful in 13s
feat: phases 77–80 — navbar view refresh, static background, API tokens, history suggestion chips
Single consolidated commit for four completed, validated phases (77, 78,
79, 80). The pipeline run left all work uncommitted because the harness
commits only with PHASE_COMMIT=1 while child executors are forbidden from
committing; the phases themselves all passed validation and moved to
.agents/phases/complete/.

Phase 77 — navbar view refresh
- router.js dispatches bor:view-refresh on re-show / active re-click /
  popstate (gated on wasMounted; first show and boot exempt)
- History / RAG / Sources / Tuning re-fetch on refresh (admin branch);
  Chat deliberately excluded (stream survival)
- History "Refresh" button (admin-only, in-flight disable + status line)
- New story suite tests/e2e/test_navbar_refresh.py (7 tests)

Phase 78 — static background
- Removed the animated glow layers; static 44px grid over the flat --bg
  canvas; default and reduced-motion renders byte-identical
- Updated background/theme E2E suites; removed bg-glow test pins

Phase 79 — API tokens
- api_tokens model + migration 0012; hash-only token service
- Admin tokens API + Tokens admin view; POST /api/token-auth;
  live-revoking require_user on chat / suggestions / document content
- Frontend token gate with localStorage cache; anonymous E2E suites
  migrated to token login
- New story suite tests/e2e/test_api_tokens.py (9 tests)

Phase 80 — history suggestion chips
- last_questions() endpoint with SEED fallback; startNewChat() refetch
- Seed-semantics docs (config.py, .env.example, README)
- Integration state matrix + E2E suite rewritten to the 4 chip states

Also included: phase-76 report artifacts and the repo restore-test-db
skill (previously untracked), scripts/* ruff fixes from phase 77.

Final gate state (phase 80 final pass, covers everything above):
- uv run pytest --cov=app → 1637 passed, 0 failed, app/ coverage 99%
- uv run ruff check . && uv run pyright → clean, 0 errors
- Per-phase story E2E suites green in isolation
2026-09-07 12:39:01 -04:00

865 lines
42 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 ``<meta name="description">``;
* 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 — all five 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"
)
# The view names are the #view-<name> section slugs in index.html.
for name in ("chat", "tuning", "history", "tokens"):
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 '"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' <head>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' <head>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
# 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."""
js = _js()
fn = js.find("async function switchTo")
body = js[fn : js.find("\n}", fn)]
flag = body.rfind("if (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('<main id="main" class="app-main" tabindex="-1">')
assert main != -1
view_chat = html.find('<section class="view" id="view-chat"')
view_tuning = html.find('<section class="view" id="view-tuning"')
main_end = html.find("</main>", 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('<a href="/" class="nav-link is-active" aria-current="page">Chat</a>')
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'<a[^>]*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'<a[^>]*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"
# ---------- 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("<a ", 0, i)
tag_end = html.find(">", tag_start)
tag = html[tag_start:tag_end]
assert tag.startswith('<a href="/"'), (
"the single static active stamp must be the Chat link (href=\"/\")"
)
def test_header_js_never_writes_the_active_state() -> 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'<script[^>]*src="([^"]+)"', html)
assert "assets/brand.js" in srcs, "brand.js (classic) still ships"
assert srcs.index("assets/brand.js") < srcs.index("/assets/app.js") < srcs.index(
"/assets/router.js"
), "boot order: brand.js → app.js → router.js"
router_tag_match = re.search(r'<script[^>]*src="/assets/router\.js"[^>]*>', html)
assert router_tag_match, "the shell must load the router module"
router_tag = router_tag_match.group(0)
assert 'type="module"' in router_tag, "router.js is an ES module"
# No CDN: every asset reference is local (AGENTS.md rule 6).
assert 'src="http' not in html and 'href="http' not in html
# ---------- phase 77 task 01: the re-show refresh hook ----------
def test_reshow_dispatches_view_refresh_gated_on_pre_mount_capture() -> None:
"""Phase 77: a user-initiated re-show of an already-mounted view
dispatches the ``bor:view-refresh`` CustomEvent on the view's
section. The dispatch site is INSIDE the ``if (wasMounted)`` guard,
and the ``wasMounted`` capture runs BEFORE the mount-once set
(``mounted[name] = true``) — so the first show (the mount) and boot
never dispatch: the mount's own load is the first fetch. Event
order: the view is visible and the head/nav state is written
BEFORE the refresh fires, and the focus/scroll tail runs after."""
js = _js()
assert '"bor:view-refresh"' in js, "the refresh event literal must exist"
fn = js.find("async function switchTo")
assert fn != -1, "switchTo must exist"
body = js[fn : js.find("\n}", fn)]
capture = body.find("const wasMounted = mounted[name]")
mount_set = body.find("mounted[name] = true")
assert 0 <= capture < mount_set, (
"the wasMounted capture must precede the mount-once set "
"(first show is exempt from the refresh)"
)
gate = body.find("if (wasMounted)")
dispatch = body.find('root.dispatchEvent(new CustomEvent("bor:view-refresh"))')
assert 0 <= gate < dispatch < gate + 120, (
"the dispatch must sit inside the wasMounted guard"
)
show_loop = body.find("Object.entries(viewEls)")
title_write = body.find("document.title = titleFor(name)")
current_set = body.find("current = name")
focus = body.find("root.focus(")
assert show_loop < title_write < current_set < gate < dispatch < focus, (
"visible → head/nav state → refresh dispatched → focus/scroll tail"
)
def test_active_view_reclick_dispatches_refresh_not_bare_return() -> None:
"""Phase 77: a re-click of the ACTIVE view's own nav link is a
re-fetch, not a no-op — the ``name === current`` branch dispatches
the refresh event on that view's section and returns. It must NOT
pushState (the URL is already this view's path) and must NOT
re-run the switch (no re-mount)."""
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)]
branch = body.find("if (name === current)")
assert branch != -1, "the active re-click branch must exist"
branch_end = body.find("}", branch)
branch_body = body[branch : branch_end + 1]
assert 'new CustomEvent("bor:view-refresh")' in branch_body, (
"the re-click branch must dispatch the refresh event (not a bare return)"
)
assert "history.pushState" not in branch_body, (
"the re-click must NOT pushState — the URL is already this view's path"
)
assert "switchTo" not in branch_body, "the re-click must NOT re-run the switch"
assert "return" in branch_body, "the re-click still returns early (menu closes)"
def test_history_view_listens_for_view_refresh_in_admin_branch_only() -> None:
"""Phase 77: the History view re-fetches on a user-initiated
re-show — history.js registers a ``bor:view-refresh`` listener on
the view's root that re-runs the (now re-entrant) ``loadChats()``.
The listener is armed only AFTER the whoami gate passes: anonymous
shows the gate and never fetches (the phase-50 contract the story
E2E pins), and the ``started`` flag means the listener can only
re-run a load the mount already made."""
history_js = (ASSETS / "history.js").read_text(encoding="utf-8")
assert 'addEventListener("bor:view-refresh"' in history_js, (
"history.js must listen for the refresh event on the view root"
)
gate = history_js.find("if (!(await fetchIsAdmin()))")
listener = history_js.find('addEventListener("bor:view-refresh"')
assert 0 <= gate < listener, (
"the listener is armed only in the ADMIN branch (after the gate)"
)
assert re.search(r"if \(started\)\s+loadChats\(\)", history_js), (
"the listener is gated on the first load (started)"
)
# Re-entrancy: a re-load drops the data rows (except the hidden
# empty-state row) before fetching — the list is replaced, not
# duplicated.
load = history_js.find("async function loadChats()")
assert load != -1, "loadChats must exist"
load_body = history_js[load : history_js.find("\n }", load)]
assert "tr !== emptyRow" in load_body and "tr.remove()" in load_body, (
"loadChats must remove the data rows (the empty row stays) first"
)
clear_i = load_body.find("tr !== emptyRow")
fetch_i = load_body.find('fetch("/api/chats")')
assert 0 <= clear_i < fetch_i, "the row clearing precedes the fetch"
# ---------- phase 77 task 02: RAG / Sources / Tuning re-fetch; chat stays out ----------
def _asset(name: str) -> str:
path = ASSETS / name
assert path.is_file(), f"missing {path}"
return path.read_text(encoding="utf-8")
def _pin_refresh_listener(js: str, gate: str, listener_call: str, name: str) -> None:
"""Shared shape of the task-02 pin: the view module listens for
``bor:view-refresh`` on its own root, the listener re-runs the
view's existing load, and the listener is armed ONLY in the ADMIN
branch — after the whoami gate (anonymous never fetches)."""
listener = js.find('addEventListener("bor:view-refresh"')
assert listener != -1, f"{name} must listen for the refresh event on the view root"
gate_i = js.find(gate)
assert 0 <= gate_i < listener, (
f"{name}: the listener must be armed in the ADMIN branch (after {gate!r})"
)
assert listener_call in js[listener : listener + 120], (
f"{name}: the listener must re-run the view's load ({listener_call!r})"
)
def test_rag_view_refetches_on_reshow() -> None:
"""Phase 77 task 02: the RAG (knowledge base) view re-fetches on a
user-initiated re-show — sources.js listens and re-runs
``loadDocs()``. ``loadDocs`` is now re-entrant: the tbody's rows
are cleared at the TOP, before the fetch (the History pattern from
task 01), so a refresh from a populated list into an empty result
replaces the list instead of leaving ghost rows."""
js = _asset("sources.js")
_pin_refresh_listener(
js, "const admin = await fetchIsAdmin();", "() => loadDocs()", "sources.js"
)
load = js.find("async function loadDocs()")
assert load != -1, "loadDocs must exist"
body = js[load : js.find("\n }", load)]
clear_i = body.find("tbody.replaceChildren()")
fetch_i = body.find('fetch("/api/docs")')
assert 0 <= clear_i < fetch_i, (
"the row clearing must precede the fetch (a populated → empty refresh "
"must not leave ghost rows)"
)
def test_git_sources_view_refetches_on_reshow() -> None:
"""Phase 77 task 02: the Sources (git-sources) view re-fetches on a
user-initiated re-show — git-sources.js listens and re-runs
``loadSources()``. A re-call resets ALL THREE list states: the
populated render (renderSources replaces the tbody + re-syncs the
empty state) and the load error (``hideLoadError()`` runs on the
success path BEFORE rendering, so an error followed by a
successful refresh clears it)."""
js = _asset("git-sources.js")
_pin_refresh_listener(
js, "const admin = await fetchIsAdmin();", "() => loadSources()", "git-sources.js"
)
load = js.find("async function loadSources()")
assert load != -1, "loadSources must exist"
body = js[load : js.find("\n }", load)]
hide_i = body.find("hideLoadError()")
render_i = body.find("renderSources(")
assert 0 <= hide_i < render_i, (
"the success path must clear the load error before rendering "
"(an error followed by a successful refresh clears the error)"
)
render = js.find("function renderSources(")
assert render != -1, "renderSources must exist"
render_body = js[render : js.find("\n }", render)]
assert "tbody.replaceChildren()" in render_body, (
"a re-render replaces the list (the populated state resets)"
)
def test_tuning_view_refetches_on_reshow() -> None:
"""Phase 77 task 02: the Tuning view re-fetches on a user-initiated
re-show — tuning.js listens and re-runs ``loadNotes()``. A re-call
replaces the list (renderNotes clears it first); a FAILED refresh
keeps the last rendered list — loadNotes's documented contract
(progressive enhancement, never a blanked panel), unchanged by the
listener (it just calls the function)."""
js = _asset("tuning.js")
_pin_refresh_listener(
js, "if (await fetchIsAdmin())", "() => loadNotes()", "tuning.js"
)
render = js.find("function renderNotes(")
assert render != -1, "renderNotes must exist"
render_body = js[render : js.find("\n }", render)]
assert 'tuneList.textContent = ""' in render_body, (
"a re-render clears the list first (the re-call replaces it)"
)
def test_chat_view_does_not_listen_for_view_refresh() -> None:
"""Negative pin: app.js (the chat view) must NOT listen for
``bor:view-refresh`` — the in-flight SSE stream and the local
conversation survive EVERY switch (the phase-76 contract the
stream E2E pins). The exclusion is a contract, not an oversight;
the deliberate-exclusion comment lives at the chat view's
module-scope state in app.js."""
js = _asset("app.js")
assert 'addEventListener("bor:view-refresh"' not in js, (
"the chat view must NOT listen for the refresh event — its "
"in-flight stream and local conversation must survive every "
"switch (phase 76)"
)
assert "bor:view-refresh" in js, (
"the exclusion is documented at the chat view's module-scope state"
)
# ---------- phase 77 task 03: the History refresh button ----------
def _history_view(html: str) -> str:
"""The shell's History view section (the test_history_page pattern):
from the #view-history open tag to the container main's close
(the view is the shell's LAST view section)."""
start = html.find('<section class="view" id="view-history"')
assert start != -1, "the #view-history section must be in the shell"
end = html.find("</main>", start)
assert end != -1, "the container main must close after the view"
return html[start:end]
def test_history_refresh_button_markup_lives_in_the_page_head() -> None:
"""Phase 77 task 03 (TODO.md L3): the History page-head carries the
explicit refresh control — #history-refresh, a ``type="button"``
``.history-refresh`` with the accessible name
``aria-label="Refresh saved chats"``, the house inline-SVG refresh
glyph (aria-hidden) and the visible "Refresh" label (the phase-46
auth-link convention: label visible ≥640px, icon-only below — the
aria-label keeps the name in both). It sits INSIDE the view's
.page-head and BEFORE the table wrap (outside it — the button must
stay reachable while the empty state is showing)."""
view = _history_view(_html())
btn_i = view.find('id="history-refresh"')
assert btn_i != -1, "the #history-refresh button must exist"
tag_start = view.rfind("<button", 0, btn_i)
tag_end = view.find(">", btn_i)
tag = view[tag_start:tag_end]
assert 'type="button"' in tag, "a plain button (no form submit)"
assert 'class="history-refresh"' in tag
assert 'aria-label="Refresh saved chats"' in tag, ("the accessible name")
tail = view[tag_end:tag_end + 600]
assert 'aria-hidden="true"' in tail, "the refresh glyph must be aria-hidden"
assert '<span class="history-refresh-label">Refresh</span>' in tail, (
"the visible Refresh label (icon-only below 640px, label above)"
)
head_i = view.find('class="page-head"')
wrap_i = view.find('id="history-table-wrap"')
assert -1 < head_i < btn_i < wrap_i, (
"the button sits in the page-head, before (OUTSIDE) the table wrap"
)
def test_history_refresh_button_binding_admin_only_with_outcome_lines() -> None:
"""Phase 77 task 03: history.js binds #history-refresh in the ADMIN
branch only — the anonymous branch HIDES the button (the gate is
what anonymous sees; no dead control beside the sign-in gate) and
still fetches nothing. The click handler disables the button
BEFORE the fetch (no double-fire while in flight) and delegates to
the re-entrant load; the re-enable sits in a ``finally`` (success
AND failure — a click can never leave the button stuck disabled).
The success line is ``Saved chats refreshed.``; the failure lines
live INSIDE ``loadChats`` itself — the house copy (network:
"is the app reachable?"; non-2xx: "try again.") — so every caller
of a failed load (the mount's first load, a re-show, the button)
sees the outcome in #history-status."""
js = _asset("history.js")
gate = js.find("if (!(await fetchIsAdmin()))")
assert gate != -1
branch = js[gate:js.find("return;", gate)]
assert "refreshBtn.hidden = true" in branch, (
"the anonymous branch hides the button (no dead control)"
)
admin_after = js[js.find("return;", gate):]
bind = admin_after.find('refreshBtn.addEventListener("click"')
assert bind != -1, "the refresh binding must exist in the admin branch"
handler = admin_after[bind:admin_after.find(");", bind)]
assert "refreshBtn.disabled = true" in handler, (
"the click disables the button before the fetch (no double-fire)"
)
# refreshChats is defined alongside loadChats (before the gate) —
# its BODY is pinned on the whole file, its BINDING on the admin
# branch above (the hoisted function is only reachable from the
# admin-branch binding: the anonymous branch never references it).
fn = js.find("async function refreshChats()")
assert fn != -1, "refreshChats must exist"
fn_body = js[fn:js.find("\n }", fn)]
assert "loadChats()" in fn_body, "the button re-runs the (re-entrant) load"
assert "finally" in fn_body and "refreshBtn.disabled = false" in fn_body, (
"the button re-enables on success AND failure (the finally)"
)
assert '"Saved chats refreshed."' in fn_body, "the exact success line"
load = js.find("async function loadChats()")
load_body = js[load:js.find("\n }", load)]
assert "Couldn't load saved chats — is the app reachable?" in load_body, (
"the network-error line lives in loadChats (every caller sees it)"
)
assert "Couldn't load saved chats — try again." in load_body, (
"the non-2xx line lives in loadChats (every caller sees it)"
)
def test_history_refresh_button_css_reuses_the_new_chat_language() -> None:
"""Phase 77 task 03 (styles.css): .history-refresh reuses the
.new-chat-btn visual language — the solid brand pill (--bg text on
--brand, 5.2:1 ≥ WCAG 4.5:1), the ≥44px target, the lightened
hover fill, the dimmed :disabled (the in-flight state), the glyph
hidden on desktop (the label carries the pill) — and the global
:focus-visible ring applies (no button-scoped focus override).
The page-head flex row is SCOPED to #view-history (the other four
views' page-heads are untouched). Below 640px the pill goes
icon-only (the phase-46 auth-link convention — the aria-label
keeps the accessible name)."""
css = _asset("styles.css")
block = re.search(r"\.history-refresh \{([\s\S]*?)\n\}", css)
assert block, "styles.css must style .history-refresh"
body = block.group(1)
assert "background: var(--brand)" in body, "the .new-chat-btn brand fill"
assert "color: var(--bg)" in body, "--bg text on --brand (5.2:1, AA)"
assert "min-height: 44px" in body, "the comfortable touch target"
assert "border-radius: 999px" in body and "border: 0" in body, "the pill"
assert ".history-refresh:hover { background: #f55a72; color: var(--bg); }" in css
assert ".history-refresh:disabled { opacity: 0.6; cursor: wait; }" in css, (
"the in-flight disabled state is dimmed (the house language)"
)
assert ".history-refresh svg { width: 16px; height: 16px; display: none; }" in css, (
"desktop: the label carries the pill (the glyph is hidden)"
)
row = re.search(r"#view-history \.page-head \{([\s\S]*?)\n\}", css)
assert row and "display: flex" in row.group(1), (
"the page-head flex row is scoped to the History view"
)
mobile = re.search(r"@media \(max-width: 640px\) \{([\s\S]*?)\n\}\n", css)
assert mobile, "the 640px media query must exist"
mbody = mobile.group(1)
assert ".history-refresh-label { display: none; }" in mbody, (
"icon-only below 640px (the phase-46 convention)"
)
assert ".history-refresh svg { display: block; }" in mbody, (
"the glyph is the whole control below 640px"
)
# ---------- phase 79 task 06: the Tokens view (generate · list · revoke) ----------
def test_tokens_view_module_contract() -> None:
"""Phase 79 task 06: tokens.js follows the phase-76 view-module
contract — ``export async function mount(root)`` is the entry, the
whoami gate (``fetchIsAdmin``) runs in mount and the anonymous
branch shows the gate + hides the table + RETURNS with NO
/api/tokens request (the router 403s anonymous), the
``bor:view-refresh`` listener is armed ONLY in the ADMIN branch
(after the gate) and re-runs the re-entrant ``loadTokens()``
(gated on the ``started`` flag), and ``loadTokens`` hides the
once-block and clears the data rows BEFORE the fetch — the
plaintext is never re-shown and the list is replaced, not
duplicated. Every cell is textContent: the file never touches
innerHTML (XSS-safe by construction)."""
js = _asset("tokens.js")
assert "export async function mount(root)" in js, (
"mount(root) must be the module's entry (the phase-76 fold)"
)
assert 'import { fetchIsAdmin } from "./header.js";' in js, (
"the view imports ONLY the shared cached whoami promise"
)
mount_i = js.find("export async function mount(root)")
gate_i = js.find("if (!(await fetchIsAdmin()))")
assert 0 <= mount_i < gate_i, "the whoami gate must run in mount"
# The anonymous branch: gate in, table out, then a bare return —
# and NO fetch call anywhere inside it.
branch = js[gate_i:js.find("return;", gate_i)]
assert "fetch(" not in branch, (
"the anonymous branch must not fetch anything"
)
assert "tableWrap.hidden = true" in branch
assert "gateEl.hidden = false" in branch
# The re-show refresh: armed in the ADMIN branch only (after the
# gate), gated on the first load (started), re-running loadTokens.
listener = js.find('addEventListener("bor:view-refresh"')
assert 0 <= gate_i < listener, (
"the refresh listener is armed only in the ADMIN branch (after the gate)"
)
assert re.search(r"if \(started\)\s+loadTokens\(\)", js), (
"the listener is gated on the first load (started)"
)
# loadTokens: re-entrant — the once-block hides and the data rows
# (except the hidden empty-state row) are dropped BEFORE the fetch.
load = js.find("async function loadTokens()")
assert load != -1, "loadTokens must exist"
load_body = js[load:js.find("\n }", load)]
hide_i = load_body.find("onceBlock.hidden = true")
clear_i = load_body.find("tr !== emptyRow")
fetch_i = load_body.find('fetch("/api/tokens")')
assert 0 <= hide_i < clear_i < fetch_i, (
"once-block hide + row clearing must precede the fetch "
"(a re-render never re-shows the plaintext; the list is replaced)"
)
assert "innerHTML" not in js, (
"every cell is textContent — no innerHTML anywhere (XSS-safe)"
)
def test_tokens_view_scaffold_in_the_shell() -> None:
"""Phase 79 task 06: the shell carries the #view-tokens section —
hidden AND inert + focusable (the WCAG pair, AGENTS.md rule 5) —
with the page-head (h1 \"Access tokens\"), the #tokens-gate (the
#history-gate pattern, ship-hidden, its Sign in returning to the
Tokens view), the role=\"status\" live region, the create row
(label input + Generate — ship-hidden, anonymous-safe), the
#token-once block (ship-hidden — only a 201 reveals it), and the
full-width table (AGENTS.md rule 5) with the visually-hidden
Actions header + the hidden #tokens-empty-row."""
html = _html()
view = html.find('<section class="view" id="view-tokens"')
assert view != -1, "the #view-tokens section must be in the shell"
tag_end = html.find(">", view)
tag = html[view:tag_end]
assert "hidden" in tag and "inert" in tag, (
"the folded view ships hidden AND inert"
)
assert 'tabindex="-1"' in tag, "the target view is focusable"
main_end = html.find("</main>", view)
assert view < main_end, "the view section lives inside the single main"
body = html[view:main_end]
assert "<h1>Access tokens</h1>" in body
gate = re.search(r'<section[^>]*id="tokens-gate"[^>]*>', body)
assert gate and "hidden" in gate.group(0), "#tokens-gate must ship hidden"
assert 'href="/login.html?next=/tokens.html"' in body, (
"the gate's Sign in returns to the Tokens view (no-JS fallback)"
)
assert re.search(r'<span[^>]*id="tokens-status"[^>]*role="status"[^>]*>', body)
create = re.search(r'<div[^>]*id="token-create"[^>]*>', body)
assert create and "hidden" in create.group(0), (
"the create row ships hidden (anonymous-safe)"
)
assert re.search(r'<input[^>]*id="token-label"[^>]*>', body)
assert re.search(r'<button[^>]*id="token-generate"[^>]*>', body)
once = re.search(r'<div[^>]*id="token-once"[^>]*>', body)
assert once and "hidden" in once.group(0), (
"the once-block ships hidden (only a 201 reveals it)"
)
assert re.search(r'<input[^>]*id="token-once-value"[^>]*readonly[^>]*>', body)
assert re.search(r'<button[^>]*id="token-once-copy"[^>]*>', body)
wrap = re.search(r'<div[^>]*id="tokens-table-wrap"[^>]*>', body)
assert wrap and 'role="region"' in wrap.group(0) and 'tabindex="0"' in wrap.group(0)
assert 'id="tokens-tbody"' in body
assert re.search(r'<tr[^>]*id="tokens-empty-row"[^>]*hidden>', body)
# The Actions column header is visually-hidden (the row buttons
# carry their own aria-labels — the history-table convention).
assert '<th scope="col"><span class="visually-hidden">Actions</span></th>' in body