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
+32 -17
View File
@@ -4,7 +4,10 @@ Uses the real compose Postgres (``db`` fixture) and FastAPI's TestClient:
* GET: 200 with the full field set for a seeded document (all formats);
* GET: ``summary`` surfaced for summarized docs, ``null`` for markdown
(phase 36);
* GET: anonymous access stays 200 (phase 16 soft rule — public viewer);
* GET: user-gated (phase 79 — the phase-16 "public viewer" soft rule is
superseded; the shared chats are the anonymous surface now), so the
shared ``client`` is signed in as the admin for the module's contract
tests and the anonymous 403/401 pins build their own client;
* GET: 404 for an unknown (source, path) pair;
* GET: 404 for traversal-style ``path`` values (no filesystem access → no
leak).
@@ -32,11 +35,23 @@ from sqlalchemy import select, text
from sqlalchemy.orm import Session
import app.api.docs as docs_api
from app.main import app as fastapi_app
from app.models import Chunk, Document
from app.rag.llm import EmbeddingError
from tests.conftest import ADMIN_PASSWORD
from tests.fakes import FakeEmbedder
@pytest.fixture(autouse=True)
def _user_signed_in(client: TestClient) -> None:
"""Phase 79 (task 03): the document-content endpoint is user-gated —
the shared ``client`` signs in as the admin for this module's GET /
PATCH contract tests. The ONE test that pins the anonymous PATCH 403
builds its own client (it must stay unsigned)."""
r = client.post("/api/login", json={"password": ADMIN_PASSWORD})
assert r.status_code == 204, f"admin login failed: {r.status_code} {r.text}"
def _seed_doc(
db,
source: str = "Homelab",
@@ -195,8 +210,8 @@ def test_summary_patch_update_reembeds_summary_chunk(
if not v[3]:
assert v == before[cid] # content chunks untouched, byte for byte
assert fake.calls == [[new]] # one embed call, the new text only
# The public viewer's data source now carries the new summary
# verbatim (anonymous — the viewer stays public, phase 16).
# The viewer's data source now carries the new summary verbatim
# (the ``client`` is signed in — phase 79 gated the viewer).
g = client.get(
"/api/documents/content",
params={"source": "Homelab", "path": "container_gitlab/gitlab-compose.yaml"},
@@ -323,15 +338,17 @@ def test_summary_patch_404_unknown_pair(admin_client: TestClient, db: Session) -
db.commit()
def test_summary_patch_403_anonymous(client: TestClient, db: Session) -> None:
"""The edit affordance is admin-only (phase 57, D4 — the viewer stays
public): an anonymous PATCH gets 403 ``admin only`` and touches
nothing."""
def test_summary_patch_403_anonymous(db: Session) -> None:
"""The edit affordance is admin-only (phase 57, D4 — the viewer is
user-gated since phase 79): an anonymous PATCH gets 403 ``admin
only`` and touches nothing. A FRESH client — the module's autouse
fixture signed the shared one in."""
old = "GitLab CE runs in a Podman compose stack on the homelab NAS."
_seed_yaml_doc(db, summary=old, summary_chunk=True)
doc_id = _doc_id(db, "container_gitlab/gitlab-compose.yaml")
anonymous = TestClient(fastapi_app)
try:
r = client.patch(
r = anonymous.patch(
"/api/documents/summary",
json={
"source": "Homelab",
@@ -417,11 +434,9 @@ def test_content_200_all_fields(client, db) -> None:
def test_content_summary_surfaced_for_summarized_doc(client, db) -> None:
"""A non-markdown document with a phase-30 summary returns it verbatim
(phase 36 — the viewer's data contract gains the nullable field).
Anonymous by design: the ``client`` fixture carries no admin cookie,
so the 200 here re-confirms the phase-16 soft rule (public viewer).
"""
(phase 36 — the viewer's data contract gains the nullable field),
for a signed-in caller (phase 79 — the viewer is token-or-admin; the
anonymous 401 contract is pinned in ``test_auth_api.py``)."""
summary = (
"GitLab CE runs in a Podman compose stack on the homelab NAS with a "
"persistent volume for data and a backup job."
@@ -438,7 +453,7 @@ def test_content_summary_surfaced_for_summarized_doc(client, db) -> None:
"/api/documents/content",
params={"source": "Homelab", "path": "container_gitlab/gitlab-compose.yaml"},
)
assert r.status_code == 200 # anonymous (no cookie) — public viewer
assert r.status_code == 200
body = r.json()
assert body["summary"] == summary # verbatim, no wrapping
assert body["content"] == "services:\n gitlab:\n image: gitlab/gitlab-ce"
@@ -448,8 +463,8 @@ def test_content_summary_surfaced_for_summarized_doc(client, db) -> None:
def test_content_summary_null_for_markdown_doc(client, db) -> None:
"""Markdown documents carry no summary (phase 30) → JSON ``null``, and
anonymous access still returns 200 (phase 16 soft rule)."""
"""Markdown documents carry no summary (phase 30) → JSON ``null``
(signed-in caller — phase 79 gated the viewer)."""
_seed_doc(
db,
path="kubernetes.md",
@@ -461,7 +476,7 @@ def test_content_summary_null_for_markdown_doc(client, db) -> None:
"/api/documents/content",
params={"source": "Homelab", "path": "kubernetes.md"},
)
assert r.status_code == 200 # anonymous (no cookie) — public viewer
assert r.status_code == 200
body = r.json()
assert "summary" in body
assert body["summary"] is None