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
+53 -4
View File
@@ -1,11 +1,25 @@
"""Integration tests: HTTP API surface (no database required)."""
"""Integration tests: HTTP API surface (mostly no database required).
Phase 79 note: the user-gated endpoints (chat, suggestions) are driven
here by a signed-in ADMIN client — the admin path of ``require_user``
short-circuits before any DB touch, so this module stays database-free
(the 401 auth contract itself is pinned in ``test_auth_api.py``).
Phase 80 note: the suggestions pins are the exception — the chips are
the last 3 questions asked once any are saved, so the env-override
pin (the override is the SEED) needs an empty ``saved_chats``;
the full state matrix lives in ``test_suggestions_api.py``.
"""
from __future__ import annotations
import json
import pytest
from sqlalchemy import text
from sqlalchemy.orm import Session
from app.config import get_settings
from tests.conftest import ADMIN_PASSWORD
def test_health_reports_ok(client) -> None:
@@ -114,16 +128,28 @@ def test_config_docs_flag_tracks_settings(client) -> None:
def test_suggestions_returns_list(client) -> None:
# Phase 79: the chips are user-gated — sign in as the admin first
# (the test's purpose is the list shape, not the auth contract).
# Phase 80: the chips are the last 3 questions asked OR the seed —
# the per-state exact lists are pinned in test_suggestions_api.py;
# here the DB-free shape pin holds in EVERY state: a list of
# non-blank strings (1–3 chips once questions exist, the seed
# while none do).
assert client.post("/api/login", json={"password": ADMIN_PASSWORD}).status_code == 204
r = client.get("/api/suggestions")
assert r.status_code == 200
suggestions = r.json()["suggestions"]
assert isinstance(suggestions, list)
assert len(suggestions) >= 3
assert all(isinstance(s, str) and s.strip() for s in suggestions)
def test_suggestions_honors_bor_suggestions_env_override(monkeypatch) -> None:
"""GET /api/suggestions reflects the BOR_SUGGESTIONS JSON env override."""
def test_suggestions_honors_bor_suggestions_env_override(
monkeypatch, db: Session
) -> None:
"""GET /api/suggestions reflects the BOR_SUGGESTIONS JSON env
override — as the SEED (phase 80): it appears while ZERO questions
have been saved, so the pin needs an empty ``saved_chats`` (the
full state matrix is test_suggestions_api.py)."""
from fastapi.testclient import TestClient
from app.main import create_app
@@ -134,13 +160,22 @@ def test_suggestions_honors_bor_suggestions_env_override(monkeypatch) -> None:
"How do I deploy a service?",
"What proxy fronts reeseapps.com?",
]
db.execute(text("TRUNCATE saved_chats"))
db.commit()
get_settings.cache_clear()
try:
monkeypatch.setenv("BOR_SUGGESTIONS", json.dumps(override))
fresh_client = TestClient(create_app())
finally:
get_settings.cache_clear()
db.execute(text("TRUNCATE saved_chats"))
db.commit()
# Phase 79: sign the fresh client in as the admin (the chips are
# user-gated; the override's value is what this test pins).
assert fresh_client.post(
"/api/login", json={"password": ADMIN_PASSWORD}
).status_code == 204
r = fresh_client.get("/api/suggestions")
assert r.status_code == 200
assert r.json() == {"suggestions": override}
@@ -167,6 +202,10 @@ def test_suggestions_honors_bor_suggestions_env_override(monkeypatch) -> None:
# shell-body marker (the History view section is inside the
# shell; the old standalone page's title is client-side now).
("/history.html", 'id="view-history"'), # phase 76: shell route (was "Saved chats")
# Phase 79 (task 06): /tokens.html is a SHELL route too — the
# shell-body marker (the Tokens view section is inside the
# shell; the per-view title is client-side now).
("/tokens.html", 'id="view-tokens"'), # phase 79: shell route
("/shared.html", "Shared conversation"), # phase 51: anonymous shared page
],
)
@@ -206,6 +245,7 @@ def test_index_page_no_cache_with_versioned_asset_refs(client) -> None:
"path",
["/sources.html", "/document.html", "/login.html", "/tuning.html",
"/git-sources.html", "/history.html", # phase 50: + History (shell route, task 03)
"/tokens.html", # phase 79 task 06: + Tokens (shell route)
"/shared.html"], # phase 51: + the anonymous shared page
)
def test_html_pages_no_cache_with_versioned_refs(client, path: str) -> None:
@@ -226,6 +266,11 @@ def test_html_pages_no_cache_with_versioned_refs(client, path: str) -> None:
("/sources.html", 'id="view-rag"', "Sources · Brain of Reese"),
("/git-sources.html", 'id="view-git-sources"', "Git sources · Brain of Reese"),
("/history.html", 'id="view-history"', "Saved chats · Brain of Reese"), # phase 76 task 03
# phase 79 task 06: the sixth view — there was never a
# standalone tokens.html, so "old_title" is the router's
# client-side title: the pin asserts the shell never carries
# the per-view title statically (the router writes it).
("/tokens.html", 'id="view-tokens"', "Access tokens · Brain of Reese"), # phase 79 task 06
],
)
def test_shell_routes_serve_the_shell_no_cache_versioned(
@@ -374,5 +419,9 @@ def test_ui_chrome_has_no_emoji(client, path: str) -> None:
def test_chat_requires_message(client) -> None:
"""The empty-message 422 validation pin (phase 79: the anonymous
caller now 401s BEFORE validation — sign in as the admin so this
test keeps testing validation, not the auth contract)."""
assert client.post("/api/login", json={"password": ADMIN_PASSWORD}).status_code == 204
r = client.post("/api/chat", json={"message": ""})
assert r.status_code == 422