Files
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

370 lines
17 KiB
Python

"""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