Files
brain-of-reese/tests/unit/test_shared_header.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

762 lines
36 KiB
Python

"""Unit: the shared header module contract (phase 19).
The browser behavior is E2E-covered (tests/e2e/test_shared_header.py);
here we pin the source-level wiring — the header.js exports, the cached
whoami promise, the per-page HTML ids (anonymous-safe hidden-by-default
controls), the sign-out binding move out of app.js, the SINGLE
module-owned New chat binding (phase 34 task 02) + the sign-in
?next= rewrite, and the viewer-bar CSS — so a silent regression is
catched without a browser.
"""
from __future__ import annotations
import re
from pathlib import Path
FRONTEND = Path(__file__).resolve().parents[2] / "frontend"
ASSETS = FRONTEND / "assets"
HEADER_JS = ASSETS / "header.js"
APP_JS = ASSETS / "app.js"
SOURCES_JS = ASSETS / "sources.js"
DOCUMENT_JS = ASSETS / "document.js"
LOGIN_JS = ASSETS / "login.js"
TUNING_JS = ASSETS / "tuning.js"
HISTORY_JS = ASSETS / "history.js"
STYLES_CSS = ASSETS / "styles.css"
# Phase 76 (task 02): SOURCES_HTML + GIT_SOURCES_HTML dropped — both
# views are folded into the shell (index.html); both files are deleted.
INDEX_HTML = FRONTEND / "index.html"
DOCUMENT_HTML = FRONTEND / "document.html"
LOGIN_HTML = FRONTEND / "login.html"
def _text(path: Path) -> str:
assert path.is_file(), f"missing frontend file: {path}"
return path.read_text(encoding="utf-8")
def _script_srcs(path: Path) -> list[str]:
return re.findall(r'<script[^>]*src="([^"]+)"', _text(path))
# ---------- header.js: the module itself ----------
def test_header_module_exports_the_header_functions() -> None:
"""header.js must export the functions every page script imports
(fetchWhoami — the phase-79 canonical call — fetchIsAdmin, its
phase-16/19 backward-compatible delegation, initSharedHeader,
clearChatStorage) plus resetWhoami (the phase-79 cache
invalidation the token gate uses after a mid-page auth)."""
js = _text(HEADER_JS)
assert "export function fetchWhoami" in js
assert "export function fetchIsAdmin" in js
assert "export function resetWhoami" in js
assert "export async function initSharedHeader" in js
assert "export function clearChatStorage" in js
def test_whoami_fetch_is_cached_in_a_module_level_promise() -> None:
"""The whoami fetch is cached in the module-level `whoamiPromise`
marker (phase 79, task 05: it stores the FULL response —
{ authenticated, role } — not just the admin flag) — first call
stores the promise, later calls return it, so a page makes exactly
ONE /api/whoami request per load no matter how many consumers
await it. Anonymous-safe: non-2xx / network failure / malformed
body all resolve to { authenticated: false, role: "anonymous" }.
The string `fetch("/api/whoami")` appears in this file EXACTLY
ONCE — the single-request contract (the rest of the frontend goes
through fetchWhoami/fetchIsAdmin)."""
js = _text(HEADER_JS)
assert re.search(r"let\s+whoamiPromise\s*=\s*null", js), (
"module-level whoamiPromise marker missing"
)
assert js.count('fetch("/api/whoami")') == 1, (
"the SINGLE /api/whoami call site lives in header.js exactly once"
)
assert "if (!whoamiPromise)" in js, "fetchWhoami must reuse the stored promise"
assert "return whoamiPromise" in js
assert 'role: "anonymous"' in js, "the anonymous fallback carries the role"
assert ".catch(() => ANONYMOUS_WHOAMI)" in js, (
"network failure must resolve to the anonymous role"
)
def test_fetch_is_admin_delegates_to_fetch_whoami() -> None:
"""Phase 79 (task 05): fetchIsAdmin() is a thin delegation —
fetchWhoami().then(w => w.role === "admin"): SAME single request,
all phase-16/19 callers keep working, and a token user (role
"user") reads FALSE here (the admin-only surfaces key off
role === "admin", never off `authenticated`)."""
js = _text(HEADER_JS)
fn = js.find("export function fetchIsAdmin")
assert fn != -1
body = js[fn : js.find("\n}", fn)]
assert "fetchWhoami().then((w) => w.role === \"admin\")" in body
def test_reset_whoami_clears_the_module_cache() -> None:
"""Phase 79 (task 05): the token gate changes the session MID-PAGE
(silent re-auth / interactive login) — resetWhoami() drops the
cached promise so the NEXT fetchWhoami() is a fresh post-auth
request (a boot-fired pre-auth whoami would still say anonymous)."""
js = _text(HEADER_JS)
fn = js.find("export function resetWhoami")
assert fn != -1
body = js[fn : js.find("\n}", fn)]
assert "whoamiPromise = null" in body
def test_init_shared_header_toggles_only_elements_that_exist() -> None:
"""initSharedHeader awaits the cached whoami, toggles ONLY the
controls present on the page (querySelector, null-safe), and returns
the admin flag for reuse."""
js = _text(HEADER_JS)
fn = js.find("function initSharedHeader")
assert fn != -1
body = js[fn : js.find("\n}", fn)]
# Phase 79 (task 05): the header boots on the FULL whoami — the
# auth pair keys off the authenticated role (admin OR token user),
# the admin-only surfaces off role === "admin".
assert "await fetchWhoami()" in body
assert 'whoami.role === "admin"' in body
for selector in ("#nav-sources", "#nav-git-sources", "#nav-tuning"):
assert f'querySelector("{selector}")' in body
# Sign in: both the bar copy AND the mobile dropdown copy (phase 46)
# carry .sign-in-link — one class-based toggle (with the ?next= href
# rewrite on every copy) covers both.
assert 'querySelectorAll(".sign-in-link")' in body
# Sign out: both the bar copy AND the mobile dropdown copy (phase 46)
# carry .sign-out-btn — one class-based toggle covers both.
assert 'querySelectorAll(".sign-out-btn")' in body
assert "return admin" in body, "callers may reuse the flag"
def test_clear_chat_storage_removes_the_phase14_key_silently() -> None:
"""clearChatStorage removes the SAME phase-14 key as app.js, inside
a try/catch (private mode / storage errors are swallowed — the
navigation still happens)."""
js = _text(HEADER_JS)
fn = js.find("function clearChatStorage")
assert fn != -1
body = js[fn : js.find("\n}", fn)]
assert 'localStorage.removeItem("bor.chat.v1")' in body
assert "try" in body and "catch" in body
def test_sign_out_binding_lives_in_the_shared_module() -> None:
"""The sign-out click binding (disable → POST /api/logout → reload)
is owned by header.js at module import — exactly one implementation
for every page that loads it. It binds to ALL .sign-out-btn
elements (the bar copy for desktop + the mobile dropdown copy for
≤640px, phase 46), so both copies log out."""
js = _text(HEADER_JS)
assert js.count('querySelectorAll(".sign-out-btn")') >= 2, (
"the boot toggle + the click binding both use the class (bar + mobile copy)"
)
assert "btn.addEventListener(\"click\"" in js
assert "btn.disabled = true" in js
assert 'fetch("/api/logout", { method: "POST" })' in js
assert "window.location.reload()" in js
# ---------- HTML wiring: one shared bar on every page ----------
def test_nav_sources_ships_hidden_on_every_nav_page() -> None:
"""Phase 19 UX revision (owner permission 2026-08-23), completed on
all five pages by phase 34 task 03 (owner confirmation 2026-08-26):
the RAG nav link is hidden for anonymous — so it SHIPS with the
hidden attribute (anonymous-safe default) on every page (they all
carry the nav now, viewer included).
Phase 76 (task 01): the page list is the post-shell set — the
folded Tuning view's header copy is gone with tuning.html (the
shell's ONE header is INDEX_HTML's). Phase 76 (task 02): the RAG
+ Sources view files drop out too (the shell's ONE header covers
all its views); task 03 drops History."""
for html in (INDEX_HTML, DOCUMENT_HTML, LOGIN_HTML):
text = _text(html)
assert re.search(r'id="nav-sources"[^>]*\bhidden\b', text), (
f"{html.name}: #nav-sources must ship hidden"
)
def test_shell_and_standalone_pages_share_the_header_control_order() -> None:
"""Phase 34 task 03 (owner confirmation 2026-08-26) + phase 35/46
(the sixth page, git-sources): every page ships the IDENTICAL
header control inventory in the IDENTICAL order — brand, the mobile
hamburger, nav [Chat, #nav-sources, #nav-git-sources, #nav-tuning],
Sign in, Sign out (the #steering-toggle was removed from the navbar
at owner request, 2026-08-28) — inside the shared .header-inner row
(the document viewer's row 1). The #sync-btn (RAG view only)
and the #new-chat-btn (chat view only, moved from the navbar at
owner request 2026-08-28) are NOT part of the shared bar anymore.
Only the current-page is-active nav marker and the static ?next=
fallback may differ per page (task 05's story E2E pins the
rendered result).
Phase 76 (task 02): the post-shell set — the folded views' header
copies are gone (the shell's ONE header covers all its views); the
RAG/Sources files are deleted, task 03 drops History."""
markers = (
'class="brand"',
'<nav class="app-nav"',
'href="/"',
'id="nav-sources"',
'id="nav-git-sources"',
'id="nav-tuning"',
'id="sign-in-link"',
'id="sign-out-btn"',
)
for html in (
INDEX_HTML,
DOCUMENT_HTML,
LOGIN_HTML,
): # phase 76 (tasks 01/02): the folded view files are gone — the shell's ONE header stands in
text = _text(html)
start = text.find('<div class="container header-inner">')
assert start != -1, f"{html.name}: missing the shared .header-inner row"
region = text[start : text.find("</header>", start)]
missing = [m for m in markers if m not in region]
assert not missing, f"{html.name}: header controls missing {missing}"
for m in markers:
assert region.count(m) == 1, f"{html.name}: {m} must appear exactly once"
# Same order on every page: each control follows the previous one.
pos = -1
for m in markers:
idx = region.find(m, pos + 1)
assert idx > pos, f"{html.name}: {m} out of order in the shared bar"
pos = idx
def test_all_five_pages_carry_the_steering_panel() -> None:
"""Phase 34 task 03: the #steering-panel section (+ the
#steering-announcer live region) ships on every page — after
#kb-banner in the chat shell, first child of <main> on the other
four pages — ship hidden, driven by assets/header.js.
Phase 76 (task 01): the folded Tuning view's panel COPY is dropped
(the shell keeps the chat's ONE instance — duplicate ids are not
legal). Phase 76 (task 02): the folded RAG + Sources view copies
are dropped with their files (the shell's ONE panel stands in);
task 03 drops History. The page list is the post-shell set."""
for html in (INDEX_HTML, DOCUMENT_HTML, LOGIN_HTML):
text = _text(html)
tag = re.search(r'<section[^>]*id="steering-panel"[^>]*>', text)
assert tag, f"{html.name}: missing the #steering-panel section"
assert re.search(r'\bhidden\b', tag.group(0)), "the panel ships hidden"
assert 'id="steering-list"' in text
assert 'id="steering-empty"' in text
assert re.search(r'<p[^>]*id="steering-announcer"[^>]*role="status"[^>]*>', text), (
f"{html.name}: missing the #steering-announcer live region"
)
# The announcer follows the panel (the copied index.html block).
assert text.find('id="steering-panel"') < text.find('id="steering-announcer"')
def test_nav_tuning_ships_hidden_and_unstamped_on_the_shell() -> None:
"""Phase 27 + phase 76 (task 01) — the shell form of this pin: the
"Tuning" nav link is admin-only, so it SHIPS hidden in the shell's
ONE header (revealed by initSharedHeader once whoami says admin).
The old page-level active stamp (is-active + aria-current on
tuning.html's own link) is GONE — the router is the SINGLE WRITER
of the active state (client-side, per view); only the Chat link
may carry a static stamp. The shell loads markdown.js (classic) +
app.js + router.js (modules) with NO direct header.js <script> tag
(single-evaluation design), and no direct tuning.js tag (the router
lazy-imports it on first show — mount-once)."""
text = _text(INDEX_HTML)
tag = re.search(r'<a[^>]*id="nav-tuning"[^>]*>', text)
assert tag, "the shell must carry the #nav-tuning nav link"
assert "hidden" in tag.group(0), "#nav-tuning must ship hidden (admin-only)"
assert "is-active" not in tag.group(0), (
"no static active stamp — the router is the single writer"
)
assert 'aria-current="page"' not in tag.group(0)
srcs = _script_srcs(INDEX_HTML)
assert [s for s in srcs if "header.js" in s] == [], "no direct header.js <script> tag"
assert [s for s in srcs if "markdown.js" in s]
assert [s for s in srcs if "router.js" in s], "the shell loads the router module"
assert [s for s in srcs if "tuning.js" in s] == [], (
"no direct tuning.js tag — the router lazy-imports the view module"
)
def test_viewer_carries_the_standard_nav() -> None:
"""Phase 34 task 03 (owner confirmation 2026-08-26): the document
viewer carries the SAME standard bar as every other page — row 1 is
the shared .header-inner block, so the nav (Chat + the admin-only
#nav-sources / #nav-tuning links, ship hidden) is there too. No nav
link is "current" on the viewer: a document is a detail view
reachable from chat or Sources, and the phase-13 back link (row 2)
carries the return affordance. The phase-19 single-row bar
(.doc-header-actions) is superseded by the two-row layout —
.doc-titlebar keeps #doc-back / #doc-title / #doc-meta, so
document.js needs no render change."""
text = _text(DOCUMENT_HTML)
chat = re.search(r'<a[^>]*href="/"[^>]*>Chat</a>', text)
assert chat, "the viewer bar carries the standard nav"
assert "is-active" not in chat.group(0), "no nav link is current on the viewer"
for link in ("nav-sources", "nav-tuning"):
tag = re.search(rf'<a[^>]*id="{link}"[^>]*>', text)
assert tag, f"the viewer bar must carry #{link}"
assert "hidden" in tag.group(0), f"#{link} must ship hidden (admin-only)"
assert "is-active" not in tag.group(0)
# Row 1 is the standard bar inside the two-row viewer header.
assert re.search(r'<header[^>]*class="doc-header"', text), "the header keeps .doc-header"
assert 'class="app-header"' in text, "row 1 reuses the .app-header bar"
assert 'doc-header-inner' not in text, "the old single-row wrapper is gone"
assert 'doc-header-actions' not in text, "the old actions wrapper is gone"
# Row 2: the .doc-titlebar keeps the back link + title + meta.
assert 'class="doc-titlebar"' in text, "row 2 must be the .doc-titlebar"
assert 'id="doc-back"' in text
assert 'id="doc-title"' in text
assert 'id="doc-meta"' in text
back = re.search(r'<a[^>]*id="doc-back"[^>]*href="/sources.html"', text)
assert back, "the back link keeps its /sources.html no-JS fallback"
def test_sources_view_and_viewer_carry_the_shared_controls() -> None:
"""Sources (the shell's RAG view, phase 76 task 02) AND the document
viewer carry the Sign in / Sign out pair (both starting hidden —
initSharedHeader reveals exactly one after whoami), and their
scripts load header.js (phase 23: via the relative import, not a
direct script tag). The New chat button is chat-view only — in the
shell its ONE instance lives in the chat view (hidden + inert on
every other view); the viewer carries none (the single binding is
pinned in test_new_chat_binding_is_single_and_module_owned).
The RAG view module does NOT boot the shell's header (it boots once
via app.js) — it keeps fetchIsAdmin() for its admin gate (zero
extra requests)."""
for html in (INDEX_HTML, DOCUMENT_HTML):
text = _text(html)
assert re.search(r'id="sign-in-link"[^>]*\bhidden\b', text)
assert re.search(r'id="sign-out-btn"[^>]*\bhidden\b', text)
# The New chat button: exactly ONE instance in the shell (the chat
# view's) — none on the viewer.
assert _text(INDEX_HTML).count('id="new-chat-btn"') == 1
assert 'id="new-chat-btn"' not in _text(DOCUMENT_HTML), (
"the New chat button is chat-view only (owner request)"
)
assert 'from "./header.js"' in _text(DOCUMENT_JS), (
"document.js: page script must load the shared header module"
)
# The RAG view module (the phase-76 view-module branch):
# relative import, NO header boot, the fetchIsAdmin gate.
rag_js = _text(SOURCES_JS)
assert 'from "./header.js"' in rag_js
import_lines = [
line for line in rag_js.splitlines() if line.strip().startswith("import")
]
assert all("initSharedHeader" not in line for line in import_lines), (
"the view must not import the header boot — the shell's header boots "
"via the chat module (app.js) at shell boot"
)
assert "await initSharedHeader()" not in rag_js, (
"the view must not re-boot the shell's header"
)
assert "fetchIsAdmin()" in rag_js, "the admin gate keeps the shared cached promise"
assert "new-chat-btn" not in rag_js, (
"no #new-chat-btn binding (the module owns the single instance)"
)
assert 'fetch("/api/whoami")' not in rag_js
# Each page's Sign in link returns to ITS OWN page after login.
assert 'href="/login.html?next=/sources.html"' in _text(INDEX_HTML)
assert 'href="/login.html?next=/document.html"' in _text(DOCUMENT_HTML)
def test_header_module_loads_before_the_page_script() -> None:
"""Phase 23 (owner-confirmed single-evaluation design): NO page loads
header.js with a direct <script> tag anymore. Each page script
imports it relatively (`from "./header.js"`) — a hoisted import that
the browser evaluates BEFORE the page script body runs, and that the
image bundler inlines into the page bundle. The sign-out binding and
the whoami cache therefore exist when the page script boots, and
header.js can never be evaluated twice on a page (a tag + import pair
would double-bind the sign-out listener)."""
cases = [
(INDEX_HTML, "app.js"),
(DOCUMENT_HTML, "document.js"),
(LOGIN_HTML, "login.js"),
]
for html, page_script in cases:
srcs = _script_srcs(html)
assert [s for s in srcs if "header.js" in s] == [], (
f"{html.name}: no direct header.js <script> tag (single-evaluation design)"
)
assert [s for s in srcs if page_script in s], (
f"{html.name}: must load {page_script}"
)
js = _text(ASSETS / page_script)
assert 'from "./header.js"' in js, (
f"{page_script}: must import the shared header module relatively"
)
assert 'from "/assets/header.js"' not in js, (
f"{page_script}: absolute header import would break the esbuild bundle"
)
# Phase 76 (task 02): the folded RAG view module is NOT directly
# loaded by the shell — the router lazy-imports it on first show
# (mount-once, like the Tuning view module); it still imports
# header.js relatively (single-evaluation design).
shell_srcs = _script_srcs(INDEX_HTML)
assert [s for s in shell_srcs if "sources.js" in s] == [], (
"no direct sources.js <script> tag — the router lazy-imports the view"
)
assert [s for s in shell_srcs if "router.js" in s], (
"the router is the loader of the folded view modules"
)
js = _text(SOURCES_JS)
assert 'from "./header.js"' in js
assert 'from "/assets/header.js"' not in js
def test_login_page_carries_the_full_header() -> None:
"""Phase 34 task 03 (owner confirmation 2026-08-26): the noted
boundary is reversed by owner decision — the login page finally
carries the FULL shared header: the nav with #nav-sources /
#nav-git-sources / #nav-tuning (same ship-hidden markup as the
other pages) plus the Sign in / Sign out pair (ship hidden —
initSharedHeader reveals exactly one after whoami; the static
?next= fallback is the login page itself). The Tuning toggle is NOT
among them — removed from the navbar at owner request (2026-08-28) —
and neither is the Sync button or the New chat button (page-specific
controls, not shared-bar ones). No nav link is "current" on the
auth page."""
text = _text(LOGIN_HTML)
for marker in (
'id="nav-sources"',
'id="nav-git-sources"',
'id="nav-tuning"',
'id="sign-in-link"',
'id="sign-out-btn"',
):
assert marker in text, f"login.html must carry {marker} (phase 34 full header)"
assert 'id="sync-btn"' not in text, "the Sync button lives on the Sources page only"
assert 'id="new-chat-btn"' not in text, "the New chat button is chat-page only"
assert 'href="/login.html?next=/login.html"' in text, (
"the login page's Sign in returns to the login page (no-JS fallback)"
)
for tag in re.findall(r'<a[^>]*class="nav-link[^"]*"[^>]*>', text):
assert "is-active" not in tag, "no nav link is current on the login page"
# ---------- page-script adaptations ----------
def test_app_js_delegates_the_shared_controls_to_header_module() -> None:
"""app.js imports the shared module, runs initSharedHeader() at boot
(BEFORE the phase-14 restore), takes its isAdmin from the cached
fetchIsAdmin(), and owns NO whoami fetch and NO sign-out binding
anymore (both moved to header.js). Phase 23: the import is relative
(`./header.js`) so esbuild can bundle it into the image."""
js = _text(APP_JS)
assert 'from "./header.js"' in js
# Phase 79 (task 05): app.js reads the FULL whoami (the same cached
# promise) — the auth pair off `authenticated`, the admin-only
# surfaces off role === "admin".
assert "fetchWhoami" in js and "initSharedHeader" in js
assert "signOutBtn.addEventListener" not in js, (
"the sign-out binding moved to header.js"
)
assert 'fetch("/api/whoami")' not in js, (
"app.js must not fetch whoami itself — header.js caches it (one request/page)"
)
assert "loadAuthState" not in js, "loadAuthState was deleted in phase 19"
assert "function applyAuthState" in js, "chat-page tuning gate stays"
assert 'who.role === "admin"' in js and "who.authenticated" in js
init_idx = js.find("await initSharedHeader();")
restore_idx = js.find("restoreConversation();")
assert -1 < init_idx < restore_idx, (
"header init must run before the phase-14 restore"
)
def test_login_js_uses_the_shared_fetch_is_admin() -> None:
"""login.js switches its whoami check to the shared cached promise
(one request per page) and calls initSharedHeader for the Sources
link; its already-admin → redirect behavior is unchanged. Phase 23:
the import is relative (`./header.js`)."""
js = _text(LOGIN_JS)
assert 'from "./header.js"' in js
assert "fetchIsAdmin" in js
assert "fetchIsAdmin()" in js
assert "initSharedHeader()" in js
assert 'fetch("/api/whoami")' not in js
assert "window.location.replace(safeNext())" in js
def test_new_chat_binding_is_single_and_module_owned() -> None:
"""Phase 34 task 02 + owner rework (2026-08-28): header.js owns the
SINGLE #new-chat-btn binding (module import, like the sign-out
binding). The button now lives ONLY on the chat page (inside
.chat-shell, above #messages — moved from the navbar at owner
request), so the click ALWAYS dispatches window "bor:new-chat" and
app.js acts through its own in-flight-turn guard + list reset: no
#messages branch, no clearChatStorage + navigate-to-"/" path
anymore. NO page script binds #new-chat-btn, and each page still
runs initSharedHeader() on the shared cached whoami. Phase 23: the
import is relative (`./header.js`)."""
js = _text(HEADER_JS)
assert 'querySelector("#new-chat-btn")' in js
assert "newChatBtn.addEventListener" in js
assert "if (newChatBtn)" in js, "a page without the button is a no-op"
assert 'new CustomEvent("bor:new-chat")' in js
assert 'querySelector("#messages")' not in js, (
"no chat-page branch — the button is chat-page only (owner request)"
)
assert 'window.location.href = "/"' not in js, (
"no navigate-to-chat branch left (the button left the navbar)"
)
doc_js = _text(DOCUMENT_JS)
assert 'from "./header.js"' in doc_js
assert "initSharedHeader()" in doc_js
assert "new-chat-btn" not in doc_js, (
"document.js: no #new-chat-btn binding (the module owns it)"
)
assert 'fetch("/api/whoami")' not in doc_js, (
"document.js: whoami goes through the shared cached promise"
)
# Phase 76 (task 02): the RAG view module is the VIEW-MODULE branch
# (like the Tuning view module below) — it does not boot the shell's
# header, binds no #new-chat-btn, and gates on the shared cached
# promise.
rag_js = _text(SOURCES_JS)
assert 'from "./header.js"' in rag_js
assert "await initSharedHeader()" not in rag_js
assert "fetchIsAdmin()" in rag_js, "the admin gate keeps the shared cached promise"
assert "new-chat-btn" not in rag_js, (
"sources.js: no #new-chat-btn binding (the module owns it)"
)
assert 'fetch("/api/whoami")' not in rag_js, (
"sources.js: whoami goes through the shared cached promise"
)
# Phase 76 (task 01): the folded Tuning VIEW module no longer boots
# the header — in the shell it runs exactly once, via the chat
# module (app.js) at shell boot. The view keeps fetchIsAdmin() for
# its admin gate (the SAME cached whoami promise — zero extra
# requests) and imports the module relatively (single-evaluation).
tuning_js = _text(TUNING_JS)
assert 'from "./header.js"' in tuning_js
# No CALL to the header boot — the import line carries no
# initSharedHeader and no `await initSharedHeader()` call site
# exists (a docstring may name it; a call may not).
import_lines = [
line for line in tuning_js.splitlines() if line.strip().startswith("import")
]
assert all("initSharedHeader" not in line for line in import_lines), (
"the view must not import the header boot — the shell's header boots "
"via the chat module (app.js) at shell boot"
)
assert "await initSharedHeader()" not in tuning_js, (
"the view must not re-boot the shell's header"
)
assert "fetchIsAdmin()" in tuning_js, "the admin gate keeps the shared cached promise"
assert "new-chat-btn" not in tuning_js
assert 'fetch("/api/whoami")' not in tuning_js
# Phase 76 (task 03): the folded History VIEW module — the same
# view-module contract: no header boot, no #new-chat-btn binding,
# the admin gate on the shared cached whoami promise (zero extra
# requests), the relative import for single evaluation.
history_js = _text(HISTORY_JS)
assert 'from "./header.js"' in history_js
import_lines = [
line for line in history_js.splitlines() if line.strip().startswith("import")
]
assert all("initSharedHeader" not in line for line in import_lines), (
"the view must not import the header boot — the shell's header boots "
"via the chat module (app.js) at shell boot"
)
assert "await initSharedHeader()" not in history_js, (
"the view must not re-boot the shell's header"
)
assert "fetchIsAdmin()" in history_js, "the admin gate keeps the shared cached promise"
assert "new-chat-btn" not in history_js
assert 'fetch("/api/whoami")' not in history_js
app_js = _text(APP_JS)
assert 'window.addEventListener("bor:new-chat", startNewChat)' in app_js
assert "newChatBtn.addEventListener" not in app_js, (
"app.js acts off the module's event, not its own binding"
)
def test_init_shared_header_rewrites_sign_in_next_to_current_page() -> None:
"""Phase 34 task 02: initSharedHeader points #sign-in-link at
/login.html?next=<current pathname> (default "/") — the admin lands
back on the page they signed in from. The page markup keeps its own
static ?next= as the no-JS fallback.
Phase 51 (owner-locked 2026-08-29, TODO.md L6): the ONE exception —
the NESTED /shared/<token> page rewrites to the APP ROOT ("/")
instead of the shared URL: a guest signing in from a shared page
returns to the app root, not to a public link."""
js = _text(HEADER_JS)
fn = js.find("function initSharedHeader")
assert fn != -1
body = js[fn : js.find("\n}", fn)]
# raw pathname (always a query-safe "/…" string — never "//", and ?
# # / spaces stay percent-encoded inside it; login.js safeNext
# re-validates), same shape as the static markup fallbacks
assert 'const nextPath = window.location.pathname || "/";' in body
assert 'link.href = "/login.html?next=" + signInNext;' in body
# the shared-page exception: /shared/<token> → the app root
assert 'nextPath.startsWith("/shared/") ? "/" : nextPath' in body
# ---------- phase 34 task 01: the steering controls move to the module ----------
# (task 02 — the sync machine + New chat + sign-in next — is pinned in
# test_sync_button.py / above)
def test_header_module_owns_the_steering_panel() -> None:
"""header.js owns the steering panel behavior (moved from app.js in
phase 34 task 01): the module-level null-safe element refs, the
newest-first textContent render (XSS contract), the labeled
per-note delete, and the announcer — and NO toggle wiring anymore
(the navbar #steering-toggle was removed at owner request,
2026-08-28; note management lives on /tuning.html)."""
js = _text(HEADER_JS)
for selector in (
"#steering-panel",
"#steering-list",
"#steering-empty",
"#steering-announcer",
):
assert f'querySelector("{selector}")' in js, f"missing {selector} ref"
assert "function renderSteeringPanel" in js
assert "text.className = \"steering-note-text\"" in js
assert "text.textContent = n.note" in js, (
"XSS contract: the note renders via textContent, never innerHTML"
)
assert 'del.setAttribute("aria-label", `Delete tuning note: ${n.note}`)' in js
assert "async function deleteSteeringNote" in js
assert 'fetch(`/api/steering/${encodeURIComponent(id)}`, { method: "DELETE" })' in js
assert "refreshSteering()" in js # the admin boot refresh + the form's save
assert "steeringToggle" not in js, "the navbar toggle is gone (2026-08-28)"
def test_header_module_exports_refresh_and_announce_steering() -> None:
"""refreshSteering() (fetch + render; non-2xx / unreachable API →
the empty list state) and announceSteering() (the polite live
region) are exported for the chat page's per-bubble Tune form.
"""
js = _text(HEADER_JS)
assert "export async function refreshSteering" in js
fn = js.find("function refreshSteering")
assert fn != -1
body = js[fn : js.find("\n}", fn)]
assert 'fetch("/api/steering")' in body
assert "renderSteeringPanel(notes)" in body
assert "export function announceSteering" in js
ann = js.find("function announceSteering")
assert ann != -1
ann_body = js[ann : js.find("\n}", ann)]
assert "steeringAnnouncer.textContent = message" in ann_body
def test_init_shared_header_gates_the_steering_surface() -> None:
"""Inside initSharedHeader: admin → the list refreshes (the panel
ships hidden and is only kept fresh; only when the page ships the
panel markup); anonymous → the panel is REMOVED from the DOM
(phase-16 'absent, not hidden') and /api/steering is never fetched.
The navbar toggle was removed at owner request (2026-08-28) — no
unhide / remove of a toggle may be left behind."""
js = _text(HEADER_JS)
fn = js.find("function initSharedHeader")
assert fn != -1
body = js[fn : js.find("\n}", fn)]
assert "if (steeringPanel) refreshSteering();" in body
assert "steeringToggle" not in body
assert "steeringPanel?.remove();" in body
def test_app_js_no_longer_owns_the_steering_panel() -> None:
"""app.js keeps only the chat-specific per-bubble Tune button +
inline form: the panel refs + logic are gone (header.js owns them
now), and the form's success path awaits the module's
refreshSteering() (announcing through the module's
announceSteering()). The form's POST /api/steering + error handling
stay in app.js, untouched."""
js = _text(APP_JS)
for gone in (
"steeringToggle",
"steeringCount",
"steeringPanel",
"steeringList",
"steeringEmpty",
"steeringAnnouncer",
"loadSteering",
"renderSteeringPanel",
"deleteSteeringNote",
"setSteeringPanel",
'querySelector("#steering-toggle")',
):
assert gone not in js, f"{gone!r} must be gone from app.js (header.js owns it)"
# the chat-specific part survives and is wired to the shared module
assert "function appendTuneButton" in js
assert "function openTuneForm" in js
assert "TUNE_ICON" in js
assert 'from "./header.js"' in js
assert "refreshSteering" in js and "announceSteering" in js
assert "await refreshSteering()" in js
assert 'fetch("/api/steering", {' in js # the form's POST still lives here
# ---------- viewer two-row-header CSS (phase 34 task 04) ----------
def test_viewer_two_row_header_css() -> None:
"""Phase 34 task 04: the viewer header is two rows — row 1 reuses
the .app-header / .header-inner rules verbatim (the pinned
--header-h height still applies to row 1), row 2 is the
.doc-titlebar: a quiet --line border-top separator, a flex
.container row, its own content-sized height. The old single-row
.doc-header-actions / .doc-header-inner rules are gone, .doc-header
no longer pins a fixed height, and .doc-title-block keeps
min-width: 0 so #doc-title / #doc-meta ellipsize in the row."""
css = _text(STYLES_CSS)
block = re.search(r"\.doc-titlebar\s*\{([^}]*)\}", css)
assert block, "styles.css must define .doc-titlebar"
body = block.group(1)
assert "border-top: 1px solid var(--line)" in body, (
"the titlebar separates from row 1 with the quiet hairline"
)
assert "height: var(--header-h)" not in body, (
"the titlebar row is content-sized (title line + meta line)"
)
assert re.search(r"\.doc-titlebar\s*\.container\s*\{[^}]*display:\s*flex", css), (
"the titlebar row must be a flex row (back link + title block)"
)
assert re.search(r"\.doc-header-actions\s*\{", css) is None, (
"the old single-row actions cluster rule is gone"
)
assert re.search(r"\.doc-header-inner\s*\{", css) is None, (
"the old single-row wrapper rule is gone"
)
header_block = re.search(r"\.doc-header\s*\{([^}]*)\}", css)
assert header_block, "styles.css must still style the .doc-header header element"
assert "height: var(--header-h)" not in header_block.group(1), (
"the two-row header is content-sized — row 1 keeps the pinned height"
)
assert re.search(r"\.doc-title-block\s*\{[^}]*min-width:\s*0", css), (
"the title block must keep clipping while the row stays tidy"
)
def test_failed_sync_button_carries_the_error_look() -> None:
"""Phase 34 task 04: the #sync-btn now lives on every page, and the
non-Sources pages have no error banner — the failed state must be
visible on the button itself. header.js adds .is-error (with the
sanitized error in title / aria-label); styles.css must render it
with the phase-08 error pair (--err-ink on --err-bg ≈9.1:1,
--err-line border — never the amber deflection accent)."""
css = _text(STYLES_CSS)
block = re.search(r"\.sync-btn\.is-error\s*\{([^}]*)\}", css)
assert block, "styles.css must define the failed .sync-btn look"
body = block.group(1)
assert "var(--err-bg)" in body
assert "var(--err-ink)" in body
assert "var(--accent-" not in body, "the amber deflection accent is never on errors"