"""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 catalog load is re-entrant: the phase-97 ``loadTree`` (one fetch of ``GET /api/docs/tree``) renders through ``renderLevel``, which clears BOTH row containers at the top before filling them, so a refresh from a populated level into a sparser (or empty) one 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']*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']*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. (Phase 88: the guard is ``if (wasMounted && !opts.boot)`` — the boot show is the view's first display, so the code now matches the documented contract; only the boot call site passes ``boot: true``, pinned in test_mobile_chat_hamburger_boot.py.)""" 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 && !opts.boot)") 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). The body is sliced to the listener's close: the block-arrow forms (phase 99 task 02: the rag view's history-alignment step runs before the load) close with ``});``, the inline-arrow forms close with ``());`` (the slice then runs to the NEXT ``});`` in the file — or to its end — harmless, the inline call sits at the slice's start).""" 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})" ) end = js.find("});", listener) body = js[listener:] if end == -1 else js[listener : end + 3] assert listener_call in body, ( 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 (+ phase 97 task 04, + phase 99 task 02): the RAG (knowledge base) view re-fetches on a user-initiated re-show — sources.js listens and re-runs ``loadTree()`` (the phase-97 catalog load: ONE fetch of ``GET /api/docs/tree``), now AFTER the phase-99 history alignment (adopt the entry's kb / reset to the top — a block-arrow listener, hence the ``});`` close in the pin helper below). The load is race-tokened (phase 79) and the RE-ENTRANT render — ``renderLevel`` clears BOTH row containers at the TOP before filling them (the History pattern from task 01, extended to the folders table) — so a refresh from a populated level into a sparser (or empty) result replaces the rows instead of leaving ghost rows.""" js = _asset("sources.js") _pin_refresh_listener( js, "const admin = await fetchIsAdmin();", "loadTree()", "sources.js" ) load = js.find("async function loadTree()") assert load != -1, "loadTree must exist" body = js[load : js.find("\n }", load)] assert 'fetch("/api/docs/tree")' in body, "the load must fetch the tree endpoint" assert "++loadSeq" in body, "the race token stays (phase 79)" render = js.find("function renderLevel()") assert render != -1, "renderLevel must exist" render_body = js[render : js.find("\n }", render)] for container in ("foldersTbody", "tbody"): clear_i = render_body.find(f"{container}.replaceChildren()") append_i = render_body.find(f"{container}.appendChild") assert 0 <= clear_i < append_i, ( f"{container}: the clear must precede the fill (a populated → " "sparser 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('
", 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("", 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 'Refresh' 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 full width with the label visible (the icon-only squeeze is gone — the glyph joins the label).""" 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: var(--brand-hover); 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 { width: 100%; }" in mbody, ( "full width below 640px (the icon-only squeeze is gone)" ) assert ".history-refresh svg { display: block; }" in mbody, ( "the glyph joins the visible label 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('
", 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("
", view) assert view < main_end, "the view section lives inside the single main" body = html[view:main_end] assert "

Access tokens

" in body gate = re.search(r']*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']*id="tokens-status"[^>]*role="status"[^>]*>', body) create = re.search(r']*id="token-create"[^>]*>', body) assert create and "hidden" in create.group(0), ( "the create row ships hidden (anonymous-safe)" ) assert re.search(r']*id="token-label"[^>]*>', body) assert re.search(r']*id="token-generate"[^>]*>', body) once = re.search(r']*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']*id="token-once-value"[^>]*readonly[^>]*>', body) assert re.search(r']*id="token-once-copy"[^>]*>', body) wrap = re.search(r']*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']*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 'Actions' in body # ---------- phase 101 task 02: the tokens split (active/revoked # tables, the per-table search, the required name) ---------- def test_tokens_view_split_scaffold_in_the_shell() -> None: """Phase 101 task 02 (D1/D3/D4): the #view-tokens skeleton is the SPLIT — the active table is FOUR columns (the Status ```` is gone, the Actions header stays visually-hidden), its empty row is ``colspan=4``, and a NEW ship-hidden #tokens-no-match-row (``colspan=4``, empty ```` — the text is JS-filled) ships in the same tbody; the per-table search inputs (#token-search-active between the once-block and the active table wrap, #token-search- revoked in the revoked section) ship hidden with ``type=search`` + aria-label + the .token-search class; the NEW revoked section (heading + search + the four-column table with the Revoked ```` + the ship-hidden no-match row) ships hidden and sits AFTER the active table's wrap; and the create row's input is the REQUIRED name (aria-label "Token name", placeholder "e.g. alice — required").""" html = _html() view = html.find('
", view) body = html[view:main_end] # The active table: EXACTLY four columns — and no Status header # anywhere in the view (D1: the table IS the status). table_i = body.find('') assert table_i != -1, "the active #tokens-table must exist" thead = body[table_i:body.find("", table_i)] assert thead.count('' not in body, ( "the Status column is gone from BOTH tables (D1)" ) assert '' in body, ( "the revoked table carries the Revoked column" ) # The active tbody: the empty row (colspan=4) + the NEW no-match # row (ship-hidden, colspan=4, its ', body, re.S) assert empty and 'colspan="4"' in empty.group(0), ( "the active empty row is colspan=4" ) no_match = re.search(r']*id="tokens-no-match-row"[^>]*hidden>', body) assert no_match, "the active no-match row ships hidden" no_match_td = re.search( r']*id="tokens-no-match-row"[^>]*hidden>\s*\s*', body, ) assert no_match_td, "the no-match row is colspan=4 with an EMPTY
') == 4, ( "the active table is four columns (Label | Created | Last used | Actions)" ) assert 'StatusRevoked text JS-filled). empty = re.search(r']*id="tokens-empty-row"[^>]*hidden>.*?
\s*
" # The per-table search inputs (D4): type=search, aria-labeled, the # house .token-search class, BOTH ship hidden. for sid, label in ( ("token-search-active", "Search active tokens"), ("token-search-revoked", "Search revoked tokens"), ): m = re.search(rf']*id="{sid}"[^>]*>', body, re.S) assert m, f"missing the #{sid} search input" tag = m.group(0) assert 'type="search"' in tag, f"#{sid} must be a search input" assert f'aria-label="{label}"' in tag, f"#{sid} must be labeled" assert 'class="token-search"' in tag, f"#{sid} must carry the house class" assert "hidden" in tag, f"#{sid} ships hidden (anonymous-safe)" # The create row: the name is REQUIRED (D3) — the new aria-label + # placeholder. label_in = re.search(r']*id="token-label"[^>]*>', body, re.S) assert label_in, "the create row's name input must exist" assert 'aria-label="Token name"' in label_in.group(0) assert 'placeholder="e.g. alice — required"' in label_in.group(0) # The active search sits BETWEEN the once-block and the active # table's wrap. assert ( body.find('id="token-once"') < body.find('id="token-search-active"') < body.find('id="tokens-table-wrap"') ), "the active search is between the once-block and the table wrap" # The revoked section (D1): heading + search + wrap — ALL ship # hidden — and it sits BELOW the active table's wrap. heading = re.search( r']*id="tokens-revoked-heading"[^>]*>Revoked tokens', body ) assert heading and "hidden" in heading.group(0), ( "the revoked sub-heading ships hidden with its visible text" ) assert 'class="tokens-revoked-heading"' in heading.group(0) wrap = re.search(r']*id="tokens-revoked-wrap"[^>]*>', body) assert wrap and "hidden" in wrap.group(0), ("the revoked wrap ships hidden") assert 'role="region"' in wrap.group(0) and 'tabindex="0"' in wrap.group(0) assert 'aria-label="Revoked tokens"' in wrap.group(0) assert 'id="tokens-revoked-tbody"' in body assert "Revoked tokens — newest first" in body, "the revoked table caption" assert re.search( r']*id="tokens-revoked-no-match-row"[^>]*hidden>', body ), "the revoked no-match row ships hidden" assert body.find('id="tokens-table-wrap"') < body.find( 'id="tokens-revoked-heading"' ), "the revoked section sits BELOW the active table's wrap" def test_tokens_js_split_search_and_required_name() -> None: """Phase 101 task 02 (D1/D3/D4) in tokens.js: the module state carries the persistent per-table queries (initialized ""), the scoped lookups cover the new ids, ``loadTokens`` SPLITS the fetched list by ``tok.revoked`` (the server order kept per table), shows the active empty row iff zero ACTIVE rows, shows the revoked section (heading + search + wrap) iff ≥1 revoked row via the ``setRevokedSectionVisible`` helper, and RE-APPLIES both filters after every render (a load never loses the queries); ``makeRow`` takes the table (the revoked variant renders the revoked_at date, no actions); ``applyFilter`` is pure DOM (case-insensitive label substring over the data rows — the state rows excluded — the no-match copy quotes the ORIGINAL query in textContent); the input listeners are armed in the ADMIN branch (after the whoami gate) and set the module query + applyFilter with NO fetch; and a blank name is refused client-side — the exact announce line, the re-focus, the early return BEFORE any fetch, and the deleted "token" fallback.""" js = _asset("tokens.js") # Module state: the persistent queries, initialized once. assert re.search(r"let activeQuery = \"\";", js), ( "the active search query is module state (initialized '')" ) assert re.search(r"let revokedQuery = \"\";", js), ( "the revoked search query is module state (initialized '')" ) # The new scoped lookups (the phase-76 root-scoping contract). for sid in ( "tokens-no-match-row", "token-search-active", "tokens-revoked-heading", "token-search-revoked", "tokens-revoked-wrap", "tokens-revoked-tbody", "tokens-revoked-no-match-row", ): assert f'querySelector("#{sid}")' in js, f"missing the #{sid} lookup" # makeRow: the table parameter + the revoked variant (the # revoked_at date cell, no actions). assert "function makeRow(tok, table)" in js, "makeRow takes the table" assert 'table === "revoked"' in js, "the revoked variant branches on the table" assert "revoked_at" in js, "the Revoked cell renders tok.revoked_at" # loadTokens: the split (per-table appends, server order kept), # the section helper call, the BOTH filters re-applied after the # render. load = js.find("async function loadTokens()") assert load != -1, "loadTokens must exist" load_body = js[load:js.find("\n }", load)] assert 'makeRow(tok, "active")' in load_body, "active rows render into the active table" assert 'makeRow(tok, "revoked")' in load_body, "revoked rows render into the revoked table" split_i = load_body.find(".revoked") assert split_i != -1, "the split keys off tok.revoked (the D5 bool)" assert "setRevokedSectionVisible(revoked.length)" in load_body, ( "the revoked section shows iff ≥1 revoked row" ) fetch_i = load_body.find('fetch("/api/tokens")') active_apply = load_body.find("applyFilter(tbody, noMatchRow, activeQuery)") revoked_apply = load_body.find( "applyFilter(revokedTbody, revokedNoMatchRow, revokedQuery)" ) assert 0 <= fetch_i < active_apply < revoked_apply, ( "BOTH filters re-apply after the fetch + render (D4: a re-render " "never loses the queries)" ) # The section show/hide helper: heading + search + wrap together. helper = js.find("function setRevokedSectionVisible") assert helper != -1, "the setRevokedSectionVisible(n) helper must exist" helper_body = js[helper:js.find("\n }", helper)] for name in ("revokedHeading.hidden", "searchRevoked.hidden", "revokedWrap.hidden"): assert name in helper_body, f"the section helper must toggle {name}" # applyFilter: pure DOM — case-insensitive label substring over # the data rows (the no-match/empty state rows excluded), the # no-match row visible ⟺ non-empty query + zero visible rows, its # textContent carries the ORIGINAL query in quotes. f = js.find("function applyFilter(") assert f != -1, "applyFilter must exist" f_body = js[f:js.find("\n }", f)] assert ".toLowerCase()" in f_body, "the match is case-insensitive" assert "tr === targetNoMatchRow" in f_body, ( "the no-match row is never treated as a data row" ) assert "emptyRow" in f_body, "the empty-state row is not a data row" assert ".tokens-label-cell" in f_body, ( "the label cell is the filter's data source" ) assert 'No tokens match "${' in f_body, ( "the no-match copy (the user's original query in quotes, textContent)" ) assert "innerHTML" not in f_body, "applyFilter is textContent-only" # The input listeners: armed in the ADMIN branch only (after the # whoami gate), they set the module query + applyFilter — NO # fetch (D4 is client-side). gate_i = js.find("if (!(await fetchIsAdmin()))") assert gate_i != -1 for i, var in ( (js.find('searchActive.addEventListener("input"'), "activeQuery"), (js.find('searchRevoked.addEventListener("input"'), "revokedQuery"), ): assert i != -1, f"the {var} input listener must be armed" assert gate_i < i, f"the {var} listener is armed in the ADMIN branch (after the gate)" seg = js[i:js.find("});", i)] assert f"{var} =" in seg, f"the listener writes the {var} module state" assert "fetch(" not in seg, "the search is client-side (no fetch)" branch = js[gate_i:js.find("return;", gate_i)] assert "searchActive.hidden = true" in branch, ( "the anonymous branch keeps the active search hidden" ) assert "searchActive.hidden = false" in js, ( "the admin branch reveals the active search (with the create row)" ) # The required name (D3): the exact line + re-focus + the early # return BEFORE any fetch; the old fallback is GONE from the file. gen = js.find("async function generateToken()") assert gen != -1, "generateToken must exist" gen_body = js[gen:] check_i = gen_body.find("if (!label)") fetch_i = gen_body.find('fetch("/api/tokens"') assert 0 <= check_i < fetch_i, ( "the blank-name check runs BEFORE any request (D3: the request " "simply doesn't happen)" ) assert 'announce("Give the token a name first.")' in gen_body, ( "the exact D3 live-region line" ) assert "labelInput.focus()" in gen_body, "the name input re-focuses" assert '|| "token"' not in js, ( "the old blank-label 'token' fallback is DELETED (D3)" ) def test_tokens_split_css_pins() -> None: """Phase 101 task 02: styles.css carries the .token-search surface (full width, the ≥44px target, the house input family — --line hairline, --surface fill, ink text — no new hue) and the .tokens-revoked-heading sub-heading (the phase-97 .kb-level h2 voice: mono, 1rem, brand-ink); the empty/no-match rows' styling stays CLASS-based (.tokens-empty-row — both tables covered).""" css = _asset("styles.css") block = re.search(r"\.token-search \{([\s\S]*?)\n\}", css) assert block, "styles.css must style .token-search" body = block.group(1) assert "width: 100%" in body, "the search input is full width" assert "min-height: 44px" in body, "the ≥44px touch target" assert "border: 1px solid var(--line)" in body, "the house input hairline" assert "background: var(--surface)" in body, "the house input surface" heading = re.search(r"\.tokens-revoked-heading \{([\s\S]*?)\n\}", css) assert heading, "styles.css must style .tokens-revoked-heading" hbody = heading.group(1) assert "font-family: var(--mono)" in hbody, "the .kb-level h2 voice (mono)" assert "font-size: 1rem" in hbody assert "color: var(--brand-ink)" in hbody, "brand-ink — AA on the page background" assert re.search(r"\.tokens-empty-row td \{", css), ( "the empty/no-match rows' styling is CLASS-based (both tables)" ) # ---------- phase 101 task 03: the Regenerate control (rotation) ---------- def test_tokens_js_regenerate_control() -> None: """Phase 101 task 03 (D2) in tokens.js: the active row's Actions cell appends the Regenerate control BEFORE the Revoke control, and EACH control owns its OWN .tokens-actions wrapper span (a confirm in one never clobbers the other — the shared cell hosts two independent confirm scopes); makeRegenerateControl is a structural mirror of makeRevokeControl — the .token-regenerate button (label "Regenerate", aria-label "Regenerate token: