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
+57 -20
View File
@@ -10,12 +10,19 @@ set (``tests/e2e/conftest.py``); the shared ``tests/e2e/auth_helpers.py::login``
performs the real form login on /login.html.
Test → story mapping (Playwright Mapping Rule):
1. ``test_anonymous_chat_without_tuning``
2. ``test_anonymous_sources_gated_viewer_open``
1. ``test_anonymous_chat_gated_no_tuning``
2. ``test_anonymous_sources_and_viewer_gated``
3. ``test_login_wrong_password_shows_error``
4. ``test_admin_login_unlocks_sources_and_tuning``
5. ``test_logout_returns_to_anonymous``
6. ``test_login_page_a11y``
Phase 79 (API tokens): the anonymous pins moved to the gated contract —
``POST /api/chat`` and ``GET /api/documents/content`` are
``require_user`` (401 ``authentication required`` for anonymous; the
phase-16 "the viewer stays open" soft rule is SUPERSEDED, shared chats
are the only open surface). The password sign-in / sign-out /
wrong-password assertions are UNCHANGED.
"""
from __future__ import annotations
@@ -37,7 +44,6 @@ REPO = Path(__file__).resolve().parents[2]
FIXTURES = REPO / "tests" / "fixtures" / "docs"
QUESTION = "How is my Kubernetes cluster set up?"
MOCK_ANSWER_MARKER = "Deterministic mock answer for E2E"
DOC_TITLE = "Kubernetes Homelab Cluster"
DOC_VIEWER_URL = "/document.html?source=docs&path=homelab%2Fkubernetes.md"
@@ -88,14 +94,12 @@ def _ask(page: Page, question: str) -> None:
# ---------------------------------------------------------------------------
# 1. Anonymous: chat works, the tuning UI is gone, Sign in is offered
# 1. Anonymous: chat is GATED (phase 79), the tuning UI is gone, Sign in
# is offered
# ---------------------------------------------------------------------------
def test_anonymous_chat_without_tuning(
page: Page, app_url: str, mock_llm: int, db_ready: None
) -> None:
_reset_db(mock_llm, seed=True)
def test_anonymous_chat_gated_no_tuning(page: Page, app_url: str, db_ready: None) -> None:
page.set_default_timeout(30_000)
page.goto(app_url)
@@ -108,31 +112,48 @@ def test_anonymous_chat_without_tuning(
expect(page.locator("#sign-in-link")).to_have_attribute("href", "/login.html?next=/")
expect(page.locator("#sign-out-btn")).to_be_hidden()
# Chat still streams a grounded answer (with source chips) for
# anonymous visitors…
_ask(page, QUESTION)
expect(page.locator(".msg.brain .source-chip", has_text="kubernetes.md")).to_have_count(1)
# Phase 79: the phase-16 "anonymous chat still streams" pin is
# SUPERSEDED — POST /api/chat is require_user-gated and the
# anonymous browser's own fetch gets the 401 contract (the in-app
# token gate that locks this UI is task 05's surface; the API
# contract is the stable half of the pin).
anon_chat = page.evaluate(
"""async () => {
const r = await fetch('/api/chat', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({message: 'hello?'}),
});
return {status: r.status, body: await r.json()};
}"""
)
assert anon_chat["status"] == 401, anon_chat
assert anon_chat["body"] == {"detail": "authentication required"}, anon_chat
# …but the tuning UI is completely gone: no Tune button (new or
# …and the server agrees the visitor is anonymous.
who = page.evaluate("() => fetch('/api/whoami').then((r) => r.json())")
assert who == {"authenticated": False, "role": "anonymous"}
# The tuning UI is completely gone: no Tune button (new or
# restored), no Tuning toggle or panel in the DOM at all.
expect(page.locator(".msg.brain .tune-btn")).to_have_count(0)
expect(page.locator("#steering-toggle")).to_have_count(0)
expect(page.locator("#steering-panel")).to_have_count(0)
# A reload (the phase-14 restore path) must not bring it back.
# A reload must not bring it back.
page.reload()
expect(page.locator(".msg.brain .bubble").last).to_contain_text(MOCK_ANSWER_MARKER)
expect(page.locator(".msg.brain .tune-btn")).to_have_count(0)
expect(page.locator("#steering-toggle")).to_have_count(0)
expect(page.locator("#sign-in-link")).to_be_visible()
# ---------------------------------------------------------------------------
# 2. Anonymous: Sources gated, the document viewer stays open (soft rule)
# 2. Anonymous: Sources gated AND the document viewer's DATA is gated
# (phase 79 supersedes the phase-16 soft rule)
# ---------------------------------------------------------------------------
def test_anonymous_sources_gated_viewer_open(
def test_anonymous_sources_and_viewer_gated(
page: Page, app_url: str, mock_llm: int, db_ready: None
) -> None:
_reset_db(mock_llm, seed=True)
@@ -160,10 +181,26 @@ def test_anonymous_sources_gated_viewer_open(
# …and NO /api/docs call was ever made.
assert api_docs_calls == [], f"anonymous sources page called /api/docs: {api_docs_calls}"
# The soft rule: any seeded document still opens by direct URL.
# Phase 79 (task 05): the phase-16 soft rule ("any seeded document
# still opens by direct URL") is SUPERSEDED — the content endpoint
# is require_user-gated. The page DOCUMENT still loads (anonymous
# gets the HTML), but the GATED DATA does not: the API refuses
# with 401, and the viewer shows the inline token gate
# (#doc-auth-gate) instead of a content error — the content fetch
# never runs, so no not-found card either.
page.goto(app_url + DOC_VIEWER_URL)
expect(page.locator("#doc-title")).to_have_text(DOC_TITLE, timeout=15_000)
expect(page.locator("#doc-content")).not_to_be_empty()
expect(page.locator("#doc-auth-gate")).to_be_visible(timeout=15_000)
expect(page.locator("#doc-not-found")).to_be_hidden()
expect(page.locator("#doc-title")).to_have_text("Loading…")
anon_content = page.evaluate(
"""async () => {
const r = await fetch(
'/api/documents/content?source=docs&path=homelab%2Fkubernetes.md');
return {status: r.status, body: await r.json()};
}"""
)
assert anon_content["status"] == 401, anon_content
assert anon_content["body"] == {"detail": "authentication required"}, anon_content
# ---------------------------------------------------------------------------