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
This commit is contained in:
@@ -44,29 +44,69 @@ def _script_srcs(path: Path) -> list[str]:
|
||||
# ---------- header.js: the module itself ----------
|
||||
|
||||
|
||||
def test_header_module_exports_the_three_functions() -> None:
|
||||
"""header.js must export the three functions every page script
|
||||
imports (fetchIsAdmin / initSharedHeader / clearChatStorage)."""
|
||||
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 `adminPromise`
|
||||
marker — 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: a failure resolves to
|
||||
false."""
|
||||
"""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+adminPromise\s*=\s*null", js), (
|
||||
"module-level adminPromise marker missing"
|
||||
assert re.search(r"let\s+whoamiPromise\s*=\s*null", js), (
|
||||
"module-level whoamiPromise marker missing"
|
||||
)
|
||||
assert 'fetch("/api/whoami")' in js
|
||||
assert "if (!adminPromise)" in js, "fetchIsAdmin must reuse the stored promise"
|
||||
assert "return adminPromise" in js
|
||||
assert ".catch(() => false)" in js, "network failure must resolve to anonymous"
|
||||
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:
|
||||
@@ -77,7 +117,11 @@ def test_init_shared_header_toggles_only_elements_that_exist() -> None:
|
||||
fn = js.find("function initSharedHeader")
|
||||
assert fn != -1
|
||||
body = js[fn : js.find("\n}", fn)]
|
||||
assert "await fetchIsAdmin()" in body
|
||||
# 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)
|
||||
@@ -410,7 +454,10 @@ def test_app_js_delegates_the_shared_controls_to_header_module() -> None:
|
||||
(`./header.js`) so esbuild can bundle it into the image."""
|
||||
js = _text(APP_JS)
|
||||
assert 'from "./header.js"' in js
|
||||
assert "fetchIsAdmin" in js and "initSharedHeader" 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"
|
||||
)
|
||||
@@ -419,7 +466,7 @@ def test_app_js_delegates_the_shared_controls_to_header_module() -> None:
|
||||
)
|
||||
assert "loadAuthState" not in js, "loadAuthState was deleted in phase 19"
|
||||
assert "function applyAuthState" in js, "chat-page tuning gate stays"
|
||||
assert "isAdmin = await fetchIsAdmin();" in js
|
||||
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, (
|
||||
|
||||
Reference in New Issue
Block a user