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
+58 -54
View File
@@ -1,4 +1,4 @@
"""Phase 08 E2E (Playwright): dark tech theme — palette, no emoji, animated
"""Phase 08 E2E (Playwright): dark tech theme — palette, no emoji, static
background, reduced motion, behavior intact, all assets local.
Story: ``.agents/user_stories/dark-tech-theme.md``
@@ -13,14 +13,17 @@ Test → story mapping (Playwright Mapping Rule):
(same contrast helper as Phase 07).
2. ``test_no_emoji_in_chrome`` — neither page's ``innerText`` nor raw
``outerHTML`` contains any emoji code point.
3. ``test_animated_background`` — the background layers (fixed,
pointer-events none, carrying images): the grid (``body::before``)
is STATIC (phase 25, owner 2026-08-25: no movement) and the three
glow spots run their opacity-only fades ``bg-glow-a/b/c`` at
26s/34s/42s (``body::after`` + the two ``html`` pseudo-layers).
3. ``test_static_background`` — the background is fully STATIC (phase
78, owner direction: the animated glow layers were removed as too
resource-intensive): the grid (``body::before``) is a static
texture (fixed, pointer-events none, image still painted,
``animationName: none``) and the three glow pseudo-layers
(``body::after`` + the two ``html`` pseudo-layers) are deleted —
no background-image, no animation, no box.
4. ``test_reduced_motion_honored`` — a context with
``reduced_motion="reduce"`` → ``animation-name: none`` on all four
layers (the static grid + spot images remain).
``reduced_motion="reduce"`` → the grid stays painted and static
(``animation-name: none``) and the deleted glow pseudo-layers carry
no image or animation.
5. ``test_behavior_unchanged_smoke`` — on-topic question streams an answer
+ a source chip + the send button recovers (state machine intact under
the new skin).
@@ -36,7 +39,6 @@ from pathlib import Path
from threading import Thread
from typing import Any
import pytest
from playwright.sync_api import Browser, Page, expect
from sqlalchemy import text
@@ -156,11 +158,6 @@ def _assert_aa(pair: Any, label: str) -> None:
assert ratio >= 4.5, f"contrast {label}: {fg} on {bg} = {ratio:.2f}:1 (< 4.5:1)"
def _seconds(value: str) -> float:
"""Chromium reports animation durations as "60s" — parse as seconds."""
return float(str(value).replace("s", ""))
# --------------------------------------------------------------------------
# Tests (story → test mapping, see module docstring)
# --------------------------------------------------------------------------
@@ -176,8 +173,8 @@ def test_dark_palette_and_contrast(
# The visible page background is the <html> canvas (rgb(15, 10, 10)
# = #0f0a0a — the 2026-08-28 dark-red rebrand; it was the phase-08
# #0a0e17 canvas). <body> itself stays transparent so the
# z-index:-1 grid/glow layers (test_animated_background) are not
# painted over.
# z-index:-1 grid layer (test_static_background) is not painted
# over.
bg = page.evaluate(
"() => getComputedStyle(document.documentElement).backgroundColor"
)
@@ -187,8 +184,10 @@ def test_dark_palette_and_contrast(
f"{path}: body must stay transparent (the background layers need to show)"
)
# Chat page pairs.
page.goto(f"{app_url}/")
# Chat page pairs. Phase 79: the onboarding chips are drawn from
# /api/suggestions (require_user-gated) — the visitor signs in
# first; the palette pins are auth-independent.
login(page, app_url, next="/")
page.locator("#suggestions .suggestion-chip").first.wait_for(state="visible", timeout=10_000)
pairs = page.evaluate(
"""() => {
@@ -260,16 +259,15 @@ def test_no_emoji_in_chrome(page: Page, app_url: str, db_ready: None) -> None:
assert not hits, f"emoji in {label} on {path}: {sorted(named)}"
def test_animated_background(page: Page, app_url: str, db_ready: None) -> None:
"""AC3: the background is subtly animated, pure CSS, zero JS, and can
never block or dim content. Phase-25 contract (owner 2026-08-25:
"not moving. Different bright spots should slowly fade in and out")
supersedes the phase-08 recipe: the grid (``body::before``) is a
STATIC texture (``animationName: none``, image still painted) and
the three glow spots each run their own opacity-only fade —
``body::after`` → ``bg-glow-a`` 26s, ``html::before`` →
``bg-glow-b`` 34s, ``html::after`` → ``bg-glow-c`` 42s — all fixed,
pointer-events:none, carrying an image."""
def test_static_background(page: Page, app_url: str, db_ready: None) -> None:
"""AC3: the background is fully static, pure CSS, zero JS, and can
never block or dim content. Phase-78 contract (owner direction,
TODO.md L4: the animated background was removed as too
resource-intensive) supersedes the phase-25 recipe: the grid
(``body::before``) is a STATIC texture (``animationName: none``,
image still painted, fixed, pointer-events:none), and the three
glow spots (``body::after`` + the two ``html`` pseudo-layers) are
DELETED — no background-image, no animation, no box."""
page.goto(app_url)
report = page.evaluate(
"""() => {
@@ -278,7 +276,6 @@ def test_animated_background(page: Page, app_url: str, db_ready: None) -> None:
return {
image: cs.backgroundImage,
anim: cs.animationName,
duration: cs.animationDuration,
position: cs.position,
pointerEvents: cs.pointerEvents,
};
@@ -291,34 +288,32 @@ def test_animated_background(page: Page, app_url: str, db_ready: None) -> None:
};
}"""
)
for key in ("grid", "glowA", "glowB", "glowC"):
info = report[key]
assert info["image"] != "none", f"{key} must carry a background image"
assert info["position"] == "fixed", f"{key} must be position:fixed"
assert info["pointerEvents"] == "none", f"{key} must not intercept input"
# The phase-25 recipe: a static grid + three out-of-phase spot fades.
assert report["grid"]["anim"] == "none", (
f"the grid must be static (got {report['grid']['anim']!r})"
grid = report["grid"]
assert grid["image"] != "none", (
"the static grid must carry a background image"
)
for key, name, seconds in (
("glowA", "bg-glow-a", 26.0),
("glowB", "bg-glow-b", 34.0),
("glowC", "bg-glow-c", 42.0),
):
assert grid["anim"] == "none", f"the grid must be static (got {grid['anim']!r})"
assert grid["position"] == "fixed", "the grid must be position:fixed"
assert grid["pointerEvents"] == "none", "the grid must not intercept input"
# The phase-78 recipe: the animated glow layers are gone entirely.
for key in ("glowA", "glowB", "glowC"):
info = report[key]
assert info["anim"] == name, f"{key} must run {name} (got {info['anim']!r})"
assert _seconds(info["duration"]) == pytest.approx(seconds), (
f"{key} must run its {seconds:.0f}s fade (got {info['duration']!r})"
assert info["image"] == "none", (
f"{key}: the deleted glow layer must carry no background image "
f"(got {info['image']!r})"
)
assert info["anim"] == "none", (
f"{key}: the deleted glow layer must not animate (got {info['anim']!r})"
)
def test_reduced_motion_honored(
browser: Browser, app_url: str, db_ready: None
) -> None:
"""AC4: with prefers-reduced-motion: reduce ALL FOUR background
layers stop animating (animation-name: none) — the static grid +
spot images remain (phase 25 stills the two html pseudo-layers as
well as the body pair)."""
"""AC4: with prefers-reduced-motion: reduce the background is
already fully static (phase 78 deleted the animated glow layers) —
the grid stays painted and static (``animation-name: none``) and
the deleted glow pseudo-layers carry no image or animation."""
context = browser.new_context(
reduced_motion="reduce", viewport={"width": 1280, "height": 800}
)
@@ -339,12 +334,21 @@ def test_reduced_motion_honored(
};
}"""
)
for key in ("grid", "glowA", "glowB", "glowC"):
grid = report["grid"]
assert grid["anim"] == "none", (
f"the grid must stay static under reduced motion (got {grid['anim']!r})"
)
assert grid["image"] != "none", (
"grid: the static background image must remain visible"
)
for key in ("glowA", "glowB", "glowC"):
assert report[key]["anim"] == "none", (
f"{key} must not animate under reduced motion (got {report[key]['anim']!r})"
f"{key}: no glow animation may exist under reduced motion "
f"(got {report[key]['anim']!r})"
)
assert report[key]["image"] != "none", (
f"{key}: the static background image must remain visible"
assert report[key]["image"] == "none", (
f"{key}: no glow image may exist under reduced motion "
f"(got {report[key]['image']!r})"
)
finally:
context.close()
@@ -358,7 +362,7 @@ def test_behavior_unchanged_smoke(
button recovers (never stale)."""
_seed_kb(mock_llm)
page.set_default_timeout(30_000)
page.goto(app_url)
login(page, app_url, next="/") # phase 79: chat is require_user-gated
expect(page.locator("#kb-banner")).to_be_hidden()
page.fill("#message-input", QUESTION)