feat: phases 77–80 — navbar view refresh, static background, API tokens, history suggestion chips
Build and Push Containers / build-and-push-app (push) Successful in 1m45s
Build and Push Containers / build-and-push-db (push) Successful in 13s

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
This commit is contained in:
2026-09-07 12:39:01 -04:00
parent 495d042a98
commit 7fce6572d0
215 changed files with 10142 additions and 1643 deletions
+369
View File
@@ -0,0 +1,369 @@
"""Unit: the in-app token gate (phase 79, task 05).
Source-level house pattern (read the JS sources, no browser): pins the
gate module's wiring — the ``bor.token`` localStorage key, the
SILENT-RE-AUTH-BEFORE-WHOAMI order, the failed-re-auth key drop, the
cache-invalidation choice — the header's full-whoami plumbing
(``fetchWhoami`` exported, ``fetchIsAdmin`` delegating, the SINGLE
``fetch("/api/whoami")`` call site, the sign-out binding dropping the
cached token), and the shell/viewer HTML wiring (the gate ships hidden
+ inert, the form/input/error ids, the admin link, the boot order
token-gate.js AFTER router.js). The browser flows (gate → unlock →
cached reload → revoked drop → sign-out) are E2E-pinned by
``tests/e2e/test_api_tokens.py`` (phase 79, task 07).
"""
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"
DOCUMENT_JS = ASSETS / "document.js"
TOKEN_GATE_JS = ASSETS / "token-gate.js"
INDEX_HTML = FRONTEND / "index.html"
DOCUMENT_HTML = FRONTEND / "document.html"
def _text(path: Path) -> str:
assert path.is_file(), f"missing frontend file: {path}"
return path.read_text(encoding="utf-8")
# ---------- token-gate.js: the module itself ----------
def test_token_gate_module_exists_and_exports_mount_gate() -> None:
"""token-gate.js is an ES module exposing mountGate(lockRoot,
onAuthed) — the reusable mount point (the shell passes #main, the
viewer passes its content wrapper)."""
js = _text(TOKEN_GATE_JS)
assert "export async function mountGate" in js
# Relative import for the single-evaluation design (esbuild inlines
# it into the page bundles; the Containerfile parity pin covers the
# image build).
assert 'from "./header.js"' in js
assert '"/assets/header.js"' not in js
def test_token_gate_uses_the_bor_token_localstorage_key() -> None:
"""The owner's sentence: "cache that token in browser storage". The
cached key is the LITERAL bor.token — read at mount (silent
re-auth), written on a successful login, dropped on a failed
re-auth and on sign out (the header binding). Every localStorage
access is try/catch (the fail-silence storage contract)."""
js = _text(TOKEN_GATE_JS)
assert '"bor.token"' in js, "the bor.token localStorage key literal"
# The three accesses (read at mount, write on login, drop on a
# failed re-auth) all go through the key constant — try/catch each
# (the fail-silence storage contract: private mode degrades to
# "re-enter the token each visit", never to a broken gate).
assert "localStorage.getItem(TOKEN_KEY)" in js
assert "localStorage.setItem(TOKEN_KEY" in js
assert "localStorage.removeItem(TOKEN_KEY)" in js
assert js.count("try {") >= 3
assert js.count("catch") >= 3
def test_silent_reauth_happens_before_the_whoami_check() -> None:
"""Source order: the cached token is re-sent to POST /api/token-auth
BEFORE the whoami role check — the re-auth (re)sets the session
cookie before any whoami settles, so the role check sees the
post-auth role (no stale anonymous for a returning token user)."""
js = _text(TOKEN_GATE_JS)
reauth = js.find("(1) SILENT RE-AUTH — before the whoami check")
role_check = js.find("(2) ROLE CHECK — header.js's fetchWhoami()")
assert -1 < reauth < role_check, "the silent re-auth must precede the whoami check"
assert 'fetch("/api/token-auth"' in js
# The re-auth block reads the cached token and posts it, all before
# the role check's fetchWhoami.
cached_read = js.find("readCachedToken()")
assert -1 < cached_read < role_check
# The role check goes through header.js's SHARED cached promise
# (one /api/whoami per page load in dev).
assert "await fetchWhoami()" in js
def test_failed_silent_reauth_drops_the_cached_key() -> None:
"""A failed silent re-auth (revoked / unknown / network) removes
the key — the token may have been revoked — before the mount falls
through to the role check. The remove call sits in the (1) block,
so a dead cached token can never linger in localStorage."""
js = _text(TOKEN_GATE_JS)
reauth = js.find("(1) SILENT RE-AUTH — before the whoami check")
role_check = js.find("(2) ROLE CHECK — header.js's fetchWhoami()")
assert -1 < reauth < role_check
block = js[reauth:role_check]
assert "removeToken()" in block, "the failure path must drop the key"
# removeToken itself hits the real localStorage.removeItem (inside
# its own try/catch).
fn = js.find("const removeToken")
body = js[fn : js.find("\n};", fn)]
assert "localStorage.removeItem(TOKEN_KEY)" in body
def test_gate_ships_hidden_and_revealed_as_an_inert_pair() -> None:
"""The gate ships hidden + inert (the phase-16 ship-hidden pattern
— an authenticated boot never shows it for a frame) and the JS
always toggles hidden AND inert together (the WCAG inert-pair
contract): revealing drops BOTH, hiding re-adds BOTH."""
js = _text(TOKEN_GATE_JS)
fn = js.find("export async function mountGate")
assert fn != -1
body = js[fn:]
# Reveal: drop hidden AND inert.
assert "gate.hidden = false" in body
assert "gate.inert = false" in body
# Hide: re-add hidden AND inert.
assert "gate.hidden = true" in body
assert "gate.inert = true" in body
# The lock root is locked (inert) when the gate shows and unlocked
# when auth settles — the locked app never receives focus.
assert "lockRoot.inert = true" in body
assert "lockRoot.inert = false" in body
def test_gate_submit_caches_then_invalidates_the_whoami_cache() -> None:
"""Form submit: 204 → cache the token, THEN invalidate the module
whoami cache (resetWhoami) and re-fetch through fetchWhoami — the
documented choice (a direct re-fetch would leave header.js's
boot-fired anonymous cache stale for the header re-boot). 401 →
the role=alert error line, the input cleared + re-focused."""
js = _text(TOKEN_GATE_JS)
fn = js.find("form.addEventListener(\"submit\"")
assert fn != -1
block = js[fn:]
store = block.find("storeToken(token)")
reset = block.find("resetWhoami()")
refetch = block.find("await fetchWhoami()")
assert -1 < store < reset < refetch, (
"cache → invalidate → re-fetch: the order the contract pins"
)
# The error path: 401 → showError() — the role=alert line revealed,
# the input cleared + re-focused (defined once at mount, called from
# the failure branch).
assert "showError()" in block
fn_show = js.find("const showError")
show = js[fn_show : js.find("\n };", fn_show)]
assert "error.hidden = false" in show
assert "input.value = \"\"" in show
assert "input.focus()" in show
def test_gate_finds_its_markup_by_class() -> None:
"""The gate markup differs only in ids across the two pages
(#auth-gate / #doc-auth-gate) — the module finds it by the shared
.auth-gate CLASS (the ONE section on the page), and the token
input by name (the form field, not the id)."""
js = _text(TOKEN_GATE_JS)
assert 'querySelector(".auth-gate")' in js
assert 'input[name="token"]' in js
# ---------- header.js: the full-whoami plumbing ----------
def test_header_fetch_whoami_is_the_single_call_site() -> None:
"""The string fetch("/api/whoami") appears in header.js EXACTLY
ONCE (the single-request contract — the file's comments also
mention whoami, so the pin is on the fetch call, not the word);
fetchWhoami is exported and fetchIsAdmin delegates to it (a token
user reads false from fetchIsAdmin — the admin surfaces key off
role === "admin")."""
js = _text(HEADER_JS)
assert 'fetch("/api/whoami")' in js
assert js.count('fetch("/api/whoami")') == 1, (
"no second whoami call site may enter header.js"
)
assert "export function fetchWhoami" in js
fn = js.find("export function fetchIsAdmin")
assert fn != -1
body = js[fn : js.find("\n}", fn)]
assert "fetchWhoami()" in body, "fetchIsAdmin must delegate to fetchWhoami"
assert 'w.role === "admin"' in body
def test_sign_out_binding_drops_the_cached_token_before_reload() -> None:
"""The sign-out binding (header.js, module-owned) removes
localStorage["bor.token"] — try/catch, the fail-silence storage
contract — AFTER the logout POST and BEFORE the reload: one
logout clears the server session AND the cached token, so a
signing-out token user meets the gate again on the next load."""
js = _text(HEADER_JS)
logout = js.find('fetch("/api/logout", { method: "POST" })')
drop = js.find('localStorage.removeItem("bor.token")')
reload = js.find("window.location.reload()")
assert -1 < logout < drop < reload, (
"sign out: logout → drop bor.token → reload (the order the contract pins)"
)
def test_init_shared_header_keys_the_pair_off_authenticated() -> None:
"""Phase 79: initSharedHeader's auth PAIR (Sign in / Sign out) keys
off the authenticated role — a token user (role "user") gets
sign-in hidden + sign-out visible; the admin-ONLY surfaces (nav
links, steering refresh) still key off role === "admin" (a user
gets the anonymous branch: links hidden, the panel REMOVED,
/api/steering never fetched). Admin/anonymous stays
byte-identical to phase 16/19."""
js = _text(HEADER_JS)
fn = js.find("function initSharedHeader")
assert fn != -1
body = js[fn : js.find("\n}", fn)]
assert 'link.hidden = signedIn' in body
assert "btn.hidden = !signedIn" in body
assert "navSources.hidden = !admin" in body
assert "steeringPanel?.remove();" in body
# ---------- index.html: the shell gate + boot order ----------
def _script_srcs(html: str) -> list[str]:
return re.findall(r'<script[^>]*src="([^"]+)"', html)
def test_shell_loads_token_gate_after_router() -> None:
"""index.html loads token-gate.js as a module AFTER app.js and
router.js (boot order: brand.js classic → app.js module →
router.js module → token-gate.js module) — the Containerfile
bundles it (the parity pin in
tests/integration/test_containerfile_assets.py covers the image).
No page loads it BEFORE app.js (the gate's boot call is awaited
by app.js's boot IIFE)."""
html = _text(INDEX_HTML)
srcs = _script_srcs(html)
tag = [s for s in srcs if "token-gate.js" in s]
assert tag, "the shell must load the token-gate module"
order = [
srcs.index("assets/brand.js"),
srcs.index("/assets/app.js"),
srcs.index("/assets/router.js"),
srcs.index(tag[0]),
]
assert order == sorted(order), (
f"boot order brand.js → app.js → router.js → token-gate.js broken: {srcs}"
)
m = re.search(r'<script[^>]*src="[^"]*token-gate\.js"[^>]*>', html)
assert m and 'type="module"' in m.group(0), "token-gate.js is an ES module"
def test_shell_gate_markup_ships_hidden_inert() -> None:
"""The shell's gate (body-level, AFTER #main) ships hidden + inert
with the full contract: the #auth-gate section labelled by its h2
("Enter your access token"), the sub line, the labelled form with
the mono token input (autocomplete off — a token must never be
offered by the password manager) and the Sign in submit, the
role=alert error line (hidden, the owner-locked copy), and the
"Sign in as admin" link (the header's ?next= convention, the
no-JS fallback)."""
html = _text(INDEX_HTML)
# The section: body-level, after #main (before the footer).
tag = re.search(r'<section[^>]*id="auth-gate"[^>]*>', html)
assert tag, "the shell must carry the #auth-gate section"
assert "hidden" in tag.group(0) and "inert" in tag.group(0), (
"the gate ships hidden + inert (the ship-hidden pattern)"
)
assert 'class="auth-gate"' in tag.group(0)
assert 'aria-labelledby="auth-gate-title"' in tag.group(0)
main_end = html.find("</main>")
assert main_end < html.find('id="auth-gate"'), "the gate sits AFTER #main"
# The content (the #sources-gate visual language).
assert '<h2 id="auth-gate-title">Enter your access token</h2>' in html
assert "Shared chats stay open" in html
form = re.search(r'<form[^>]*id="auth-gate-form"[^>]*>', html)
assert form, "the gate form"
assert '<label class="visually-hidden" for="auth-gate-input">Access token</label>' in html
inp = re.search(r'<input[^>]*id="auth-gate-input"[^>]*>', html)
assert inp, "the token input"
for attr in (
'name="token"',
'type="text"',
'autocomplete="off"',
'autocapitalize="none"',
'spellcheck="false"',
"required",
):
assert attr in inp.group(0), f"the token input must carry {attr}"
assert 'type="submit"' in html and "Sign in" in html
err = re.search(r'<p[^>]*class="auth-gate-error"[^>]*id="auth-gate-error"[^>]*>', html)
assert err, "the error line"
assert 'role="alert"' in err.group(0) and "hidden" in err.group(0)
assert "That token isn" in html, "the owner-locked error copy"
assert 'href="/login.html?next=/"' in html, "the admin link (?next= convention)"
def test_shell_boots_the_gate_from_app_js() -> None:
"""app.js awaits mountGate(#main, no-op) at boot — BEFORE its
initSharedHeader — so a silent re-auth lands before the first
whoami fires (the header sees the post-auth role deterministically).
In the shell, onAuthed needs no view work: the lazy views mount on
first show exactly as today (mount-once, hide-forever untouched)."""
js = _text(APP_JS)
assert 'from "./token-gate.js"' in js
gate_i = js.find('mountGate(document.getElementById("main"), () => {})')
assert gate_i != -1, "the shell's boot call (no-op onAuthed)"
init_i = js.find("await initSharedHeader();", gate_i)
assert -1 < gate_i < init_i, "the gate settles BEFORE the header boots"
# ---------- document.html / document.js: the viewer gate ----------
def test_viewer_gate_markup_reuses_the_shell_copy_renamed() -> None:
"""document.html carries the SAME gate markup as the shell, the ids
renamed (#doc-auth-gate / #doc-auth-gate-form / #doc-auth-gate-input
/ #doc-auth-gate-error / #doc-auth-gate-title) — hidden + inert,
the labelled form + input + role=alert error + admin link."""
html = _text(DOCUMENT_HTML)
tag = re.search(r'<section[^>]*id="doc-auth-gate"[^>]*>', html)
assert tag, "the viewer must carry the #doc-auth-gate section"
assert "hidden" in tag.group(0) and "inert" in tag.group(0)
assert 'class="auth-gate"' in tag.group(0)
assert 'aria-labelledby="doc-auth-gate-title"' in tag.group(0)
assert '<h2 id="doc-auth-gate-title">Enter your access token</h2>' in html
assert re.search(r'<form[^>]*id="doc-auth-gate-form"[^>]*>', html)
assert re.search(
r'<input[^>]*id="doc-auth-gate-input"[^>]*name="token"[^>]*>', html
) or re.search(
r'<input[^>]*name="token"[^>]*id="doc-auth-gate-input"[^>]*>', html
), "the viewer's token input"
assert re.search(r'id="doc-auth-gate-error"[^>]*role="alert"', html) or re.search(
r'role="alert"[^>]*id="doc-auth-gate-error"', html
)
def test_viewer_wires_the_gate_around_the_existing_boot() -> None:
"""document.js wires mountGate(#main, onAuthed) — onAuthed runs the
content load for a SIGNED-IN role only (anonymous never fetches the
gated content; the inline gate is the surface) — and the shared
header boots in the .then AFTER the gate settles, for EVERY role
(the gate locks #main, not the header — the anonymous contract is
byte-identical to the shell). Awaiting the gate first is what makes
the header race-free: the settled whoami is the single request both
the gate and the header reuse (no second whoami, no stale bar)."""
js = _text(DOCUMENT_JS)
assert 'from "./token-gate.js"' in js
gate_i = js.find('mountGate(document.getElementById("main")')
assert gate_i != -1
boot = js[gate_i : gate_i + 400]
assert "load()" in boot, "onAuthed runs the content load (signed-in role only)"
# The header boots AFTER the gate settles (the .then) — not before
# it, not inside onAuthed: one settled whoami for gate + header.
load_i = boot.find("load()")
then_i = boot.find(".then(")
assert -1 < load_i < then_i, ("onAuthed (load) comes before the header .then")
assert "initSharedHeader()" in boot[then_i:], (
"the header must boot on the settled whoami, after the gate"
)
# The bare boot call is gone — the ONLY load(); statement in the
# file is the one inside the gate's onAuthed callback.
assert js.count("load();") == 1, (
"the un-gated load() call must be gone (onAuthed is the only caller)"
)
# The whoami single-request contract survives: no direct whoami
# fetch in the viewer script (header.js's cached promise).
assert 'fetch("/api/whoami")' not in js