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:
@@ -206,13 +206,13 @@ def test_no_horizontal_overflow_at_viewports(
|
||||
for width, height in VIEWPORTS:
|
||||
page = browser.new_page(viewport={"width": width, "height": height})
|
||||
try:
|
||||
page.goto(f"{app_url}/")
|
||||
login(page, app_url, next="/") # phase 79: the chips need a session
|
||||
page.locator("#suggestions .suggestion-chip").first.wait_for(
|
||||
state="visible", timeout=10_000
|
||||
)
|
||||
_assert_no_doc_overflow(page, f"chat @ {width}px")
|
||||
|
||||
login(page, app_url, next="/sources.html") # phase 16: admin-only
|
||||
page.goto(f"{app_url}/sources.html") # phase 16: admin-only
|
||||
page.locator("#docs-tbody tr").first.wait_for(state="visible", timeout=10_000)
|
||||
_assert_no_doc_overflow(page, f"sources @ {width}px")
|
||||
finally:
|
||||
@@ -309,10 +309,62 @@ def test_a11y_landmarks_and_labels(
|
||||
page: Page, app_url: str, db_ready: None
|
||||
) -> None:
|
||||
"""AC3: landmarks, working skip link, labeled input, named controls,
|
||||
and a visible :focus-visible outline on every keyboard focusable."""
|
||||
and a visible :focus-visible outline on every keyboard focusable.
|
||||
|
||||
Phase 79 (task 05): the anonymous visitor meets the token gate —
|
||||
the page body is inert behind it, and the gate's reveal contract
|
||||
puts focus on the token input — so the skip-link / full-tab-walk
|
||||
contract is checked for the SIGNED-IN admin (byte-identical to
|
||||
before), and the gate gets its own focus contract: reveal focuses
|
||||
the input, and input + submit both show the global 3px
|
||||
:focus-visible outline."""
|
||||
|
||||
def _outline() -> dict[str, str]:
|
||||
return page.evaluate(
|
||||
"""() => {
|
||||
const el = document.activeElement;
|
||||
const cs = getComputedStyle(el);
|
||||
return {id: el.id,
|
||||
cls: String(el.className).split(" ")[0],
|
||||
outline_style: cs.outlineStyle,
|
||||
outline_width: cs.outlineWidth};
|
||||
}"""
|
||||
)
|
||||
|
||||
def _assert_outlined(info: dict[str, str], label: str, path: str) -> None:
|
||||
width_px = float(info["outline_width"].replace("px", ""))
|
||||
assert info["outline_style"] == "solid" and width_px >= 2, (
|
||||
f"no visible focus outline on the gate {label} on {path} "
|
||||
f"({info['outline_style']} {info['outline_width']})"
|
||||
)
|
||||
|
||||
# --- Anonymous: the token gate's focus contract (phase 79) --------
|
||||
for path in ("/", "/sources.html"):
|
||||
page.goto(f"{app_url}{path}")
|
||||
page.wait_for_load_state("networkidle")
|
||||
expect(page.locator("#auth-gate")).to_be_visible(timeout=30_000)
|
||||
# Reveal contract: focus lands on the token input — the first
|
||||
# (and only) body control an anonymous visitor can act on.
|
||||
info = _outline()
|
||||
assert info["id"] == "auth-gate-input", (
|
||||
f"gate reveal must focus the token input ({path}), on {info['id']!r}"
|
||||
)
|
||||
_assert_outlined(info, "input", path)
|
||||
# Tab from the input lands on the gate submit, also outlined.
|
||||
page.keyboard.press("Tab")
|
||||
info = _outline()
|
||||
assert info["cls"] == "auth-gate-submit", (
|
||||
f"Tab from the gate input must land on the gate submit ({path}), "
|
||||
f"on {info['id'] or info['cls']!r}"
|
||||
)
|
||||
_assert_outlined(info, "submit", path)
|
||||
|
||||
# --- Signed-in: the original AC3 contract (admin, gate hidden) ----
|
||||
login(page, app_url)
|
||||
for path in ("/", "/sources.html"):
|
||||
page.goto(f"{app_url}{path}")
|
||||
page.wait_for_load_state("networkidle")
|
||||
expect(page.locator("#auth-gate")).to_be_hidden()
|
||||
|
||||
# Landmarks (PLAN §7.2).
|
||||
assert page.locator("header.app-header").count() == 1, f"header missing on {path}"
|
||||
@@ -380,8 +432,10 @@ def test_contrast_pairs_pass_aa(
|
||||
) -> None:
|
||||
"""AC4: every PLAN §7.2 color pair computed from the live computed
|
||||
styles meets WCAG 2.1 AA (>= 4.5:1)."""
|
||||
# Chat page: ink/surface, white/brand, chip-ink/chip-bg, deflection pair.
|
||||
page.goto(f"{app_url}/")
|
||||
# Chat page: ink/surface, white/brand, chip-ink/chip-bg, deflection
|
||||
# pair. Phase 79: the chips need /api/suggestions (require_user-
|
||||
# gated) — sign in first; the color 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(
|
||||
"""() => {
|
||||
@@ -451,7 +505,7 @@ def test_reduced_motion_respected(
|
||||
# Control: default context — the dots run the `typing` animation.
|
||||
page = browser.new_page(viewport={"width": 1280, "height": 800})
|
||||
try:
|
||||
page.goto(app_url)
|
||||
login(page, app_url, next="/") # phase 79: chat is require_user-gated
|
||||
page.fill("#message-input", SLOW_QUESTION)
|
||||
page.click("#send-btn")
|
||||
expect(page.locator(TYPING)).to_be_visible(timeout=1_000)
|
||||
@@ -470,7 +524,7 @@ def test_reduced_motion_respected(
|
||||
)
|
||||
rpage = context.new_page()
|
||||
try:
|
||||
rpage.goto(app_url)
|
||||
login(rpage, app_url, next="/") # phase 79: chat is require_user-gated
|
||||
rpage.fill("#message-input", SLOW_QUESTION)
|
||||
rpage.click("#send-btn")
|
||||
expect(rpage.locator(TYPING)).to_be_visible(timeout=1_000)
|
||||
@@ -538,7 +592,7 @@ def test_long_content_wraps_without_overflow(
|
||||
# Chat @ 375px: unbroken 60-char tokens wrap inside both bubbles.
|
||||
chat = browser.new_page(viewport={"width": 375, "height": 812})
|
||||
try:
|
||||
chat.goto(app_url)
|
||||
login(chat, app_url, next="/") # phase 79: chat is require_user-gated
|
||||
chat.set_default_timeout(30_000)
|
||||
chat.fill("#message-input", f"what do the notes say about {'x' * 60}")
|
||||
chat.click("#send-btn")
|
||||
|
||||
Reference in New Issue
Block a user