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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user