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