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,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)
|
||||
|
||||
Reference in New Issue
Block a user