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
+198
View File
@@ -0,0 +1,198 @@
"""Integration: the admin token API (phase 79, task 02).
The admin surface for issued access tokens, driven through the real app
(TestClient keeps the cookie jar — the house ``test_auth_api`` admin-login
pattern):
* anonymous → 403 ``admin only`` on all three endpoints (router-level
``require_admin``; a token USER, once task 03 lands, is 403 here too —
pinned in task 03's matrix);
* create → 201 with the plaintext ``token`` (the ONE wire moment it
exists, A4) — and ``GET /tokens`` NEVER exposes it: no ``token`` key,
no ``token_hash`` key, and the hash string itself absent from the
serialized body;
* labels are display-only and NOT unique (two tokens, one label);
* blank/over-long labels → 422 (the house ``ValueError`` pattern);
* revoke → 204, idempotent (re-revoke 204, original stamp kept), the
list shows ``revoked: true`` and the row keeps its ``last_used_at``;
unknown id → 404 ``token not found``;
* the list is newest-first (``created_at desc``).
Real Postgres (``podman compose up -d db``); no LLM involved — tokens
are plain rows, so the suite is deterministic without a fake.
Requires: podman compose up -d db
"""
from __future__ import annotations
import hashlib
import re
import uuid
from collections.abc import Iterator
from datetime import UTC, datetime, timedelta
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import select, text, update
from sqlalchemy.orm import Session
from app.models import ApiToken
TOKEN_SHAPE = re.compile(r"^bor_[0-9a-f]{32}$")
#: A fixed "already used" stamp — the row keeps it through revocation
#: (task 03's mark_used lands later; here the test stamps the column
#: directly to pin the "revoke preserves last_used_at" contract).
USED_STAMP = datetime(2026, 9, 6, 12, 0, 0, tzinfo=UTC)
@pytest.fixture(autouse=True)
def clean_tokens(db: Session) -> Iterator[None]:
"""api_tokens is global state: reset around every test."""
db.execute(text("TRUNCATE api_tokens"))
db.commit()
yield
db.execute(text("TRUNCATE api_tokens"))
db.commit()
def _create(admin_client: TestClient, label: str = "alice") -> dict:
"""POST a token (201) and return the response body."""
r = admin_client.post("/api/tokens", json={"label": label})
assert r.status_code == 201, r.text
return r.json()
def test_anonymous_403_on_all_three(client: TestClient) -> None:
"""Router-level ``require_admin``: every route is 403 for the
unsigned-in caller (one fixed detail — no enumeration)."""
r = client.post("/api/tokens", json={"label": "anon"})
assert r.status_code == 403
assert r.json() == {"detail": "admin only"}
r = client.get("/api/tokens")
assert r.status_code == 403
assert r.json() == {"detail": "admin only"}
r = client.post(f"/api/tokens/{uuid.uuid4()}/revoke")
assert r.status_code == 403
assert r.json() == {"detail": "admin only"}
def test_admin_create_returns_plaintext_exactly_once(
admin_client: TestClient, db: Session
) -> None:
"""201 carries the well-formed plaintext; the row stores only the
hash; the LIST never exposes either the plaintext or the hash."""
body = _create(admin_client, " alice ")
# The 201 shape: exactly the display fields + the plaintext once.
assert set(body) == {"id", "label", "token", "created_at"}
assert body["label"] == "alice" # the service strips
assert TOKEN_SHAPE.fullmatch(body["token"]), body["token"]
datetime.fromisoformat(body["created_at"]) # parses
# The stored row carries only the hash of the FULL token string.
row = db.execute(
select(ApiToken).where(ApiToken.id == uuid.UUID(body["id"]))
).scalar_one()
assert row.token_hash == hashlib.sha256(body["token"].encode("utf-8")).hexdigest()
assert row.token_hash != body["token"]
# The list: no `token` key, no `token_hash` key, and NEITHER the
# plaintext NOR the hash string appears anywhere in the body.
r = admin_client.get("/api/tokens")
assert r.status_code == 200
items = r.json()["tokens"]
assert len(items) == 1
item = items[0]
assert set(item) == {"id", "label", "created_at", "last_used_at", "revoked"}
assert item["id"] == body["id"]
assert item["label"] == "alice"
assert item["last_used_at"] is None # not used yet
assert item["revoked"] is False
serialized = r.text
assert body["token"] not in serialized
assert row.token_hash not in serialized
def test_two_tokens_same_label_both_created(admin_client: TestClient) -> None:
"""Labels are display-only and NOT unique — two tokens may share a
label (the column has no unique constraint, task 01)."""
a = _create(admin_client, "alice")
b = _create(admin_client, "alice")
assert a["id"] != b["id"]
assert a["token"] != b["token"]
items = admin_client.get("/api/tokens").json()["tokens"]
assert len(items) == 2
assert {i["label"] for i in items} == {"alice"}
def test_create_blank_or_overlong_label_422(admin_client: TestClient) -> None:
"""The schema fails loud (house ``ValueError`` pattern): whitespace
only, empty, and >120 chars after strip are all 422."""
for label in ("", " ", "x" * 121):
r = admin_client.post("/api/tokens", json={"label": label})
assert r.status_code == 422, (label, r.status_code, r.text)
# A padded-but-valid label passes (trim-before-constrain).
assert _create(admin_client, " carol ")["label"] == "carol"
def test_revoke_204_idempotent_and_preserves_last_used(
admin_client: TestClient, db: Session
) -> None:
"""Revoke → 204; the list item flips to ``revoked: true`` and keeps
its ``last_used_at``; a second revoke is still 204 with the ORIGINAL
stamp preserved (no re-stamp)."""
body = _create(admin_client, "dave")
token_id = uuid.UUID(body["id"])
# Stamp "already used" directly (task 03's mark_used is API-side).
db.execute(
update(ApiToken).where(ApiToken.id == token_id).values(last_used_at=USED_STAMP)
)
db.commit()
assert admin_client.post(f"/api/tokens/{token_id}/revoke").status_code == 204
item = admin_client.get("/api/tokens").json()["tokens"][0]
assert item["revoked"] is True
assert datetime.fromisoformat(item["last_used_at"]) == USED_STAMP
db.expire_all()
original_stamp = db.execute(
select(ApiToken).where(ApiToken.id == token_id)
).scalar_one().revoked_at
assert original_stamp is not None
# Re-revoke: still 204, the original stamp survives (no re-stamp).
assert admin_client.post(f"/api/tokens/{token_id}/revoke").status_code == 204
db.expire_all()
row = db.execute(select(ApiToken).where(ApiToken.id == token_id)).scalar_one()
assert row.revoked_at == original_stamp
assert datetime.fromisoformat(
admin_client.get("/api/tokens").json()["tokens"][0]["last_used_at"]
) == USED_STAMP
def test_revoke_unknown_id_404(admin_client: TestClient) -> None:
"""One fixed message for every unknown id (no enumeration)."""
r = admin_client.post(f"/api/tokens/{uuid.uuid4()}/revoke")
assert r.status_code == 404
assert r.json() == {"detail": "token not found"}
def test_list_is_newest_first(admin_client: TestClient, db: Session) -> None:
"""``created_at desc, id desc``: forcing a deterministic gap pins the
ordering (transaction timestamps can be coarser than the insert
gap, and uuid4 ids would tie-break randomly)."""
older = _create(admin_client, "first")
newer = _create(admin_client, "second")
db.execute(
update(ApiToken)
.where(ApiToken.id == uuid.UUID(older["id"]))
.values(created_at=datetime.now(UTC) - timedelta(hours=1))
)
db.commit()
items = admin_client.get("/api/tokens").json()["tokens"]
assert [i["id"] for i in items] == [newer["id"], older["id"]]