Files
brain-of-reese/tests/e2e/test_summary_in_viewer.py
T
ducoterra 7fce6572d0
Build and Push Containers / build-and-push-app (push) Successful in 1m45s
Build and Push Containers / build-and-push-db (push) Successful in 13s
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
2026-09-07 12:39:01 -04:00

360 lines
15 KiB
Python

"""Phase 36 E2E (Playwright): the summary and the original document
are visible **together** in the viewer.
Story: ``.agents/user_stories/summary-in-viewer.md``
Run in isolation (DB must be up: ``podman compose up -d db``):
uv run pytest tests/e2e/test_summary_in_viewer.py -v --no-cov
The fixture KB is the story-dedicated ``tests/fixtures/summary_kb/``
directory (the same one the phase-30 suite imports — its machinery is
reused here as closely as possible):
* ``quadlet/qwen-llamacpp.yaml`` — a non-markdown A9 doc. At import the
mock ``lite`` model (``SUMMARY_MODE`` marker, ``tests/e2e/mock_llm.py``)
reduces it to a deterministic 24-token digest, stored on
``documents.summary``. The sentinel ``RESE-SUMMARY-SENTINEL-7f3a`` sits
on the document's LAST line — **outside** the 24-token digest — so it
is a marker for "the original, not the summary".
* ``notes/qwen-llamacpp-notes.md`` — a markdown control doc, never
summarized (phase 30 scope): the viewer must render it exactly as
before, with no Summary panel.
This phase only surfaces the stored field: the content endpoint returns
``summary`` (task 01) and the shared ``renderDocument`` core draws the
labeled ``.doc-summary`` panel above the content on BOTH surfaces (task
02) — the full-page viewer and the chat/sources modal. The tests assert
exactly that contract. Phase 79 supersedes the phase-16 soft rule: the
content endpoint is ``require_user``-gated, so the viewer surfaces and
the API shape pin run under a signed-in session (the shape itself —
``summary`` for the yaml, ``null`` for the markdown control — is
unchanged).
"""
from __future__ import annotations
import asyncio
import re
from pathlib import Path
from threading import Thread
from typing import Any
from playwright.sync_api import Page, expect
from sqlalchemy import text
from app.config import Settings
from app.db import SessionLocal
from app.rag.importer import ImportSummary, import_sources
from app.rag.llm import LLMClient
from e2e.auth_helpers import login
from tests.e2e.mock_llm import TOKEN_RE
REPO = Path(__file__).resolve().parents[2]
FIXTURES = REPO / "tests" / "fixtures" / "summary_kb"
SENTINEL = "RESE-SUMMARY-SENTINEL-7f3a"
SOURCE = "summary_kb"
YAML_PATH = "quadlet/qwen-llamacpp.yaml"
MD_PATH = "notes/qwen-llamacpp-notes.md"
#: Encoded viewer URL query values (slashes come out as %2F, same as the
#: chips / Sources table links build them).
YAML_URL_PATH = "quadlet%2Fqwen-llamacpp.yaml"
MD_URL_PATH = "notes%2Fqwen-llamacpp-notes.md"
# --- Importer + thread helpers (test_document_summaries.py pattern) ---
async def _import_fixtures(mock_port: int) -> ImportSummary:
kwargs: dict[str, Any] = {
"_env_file": None,
"llm_base_url": f"http://127.0.0.1:{mock_port}/v1",
}
settings = Settings(**kwargs) # pyright: ignore[reportCallIssue]
return await import_sources([FIXTURES], LLMClient(settings))
def _run_in_thread(coro: Any) -> Any:
"""Run a coroutine on a worker thread.
Playwright's sync API keeps an asyncio loop running on the test thread,
so ``asyncio.run`` cannot be called directly from a test body.
"""
box: dict[str, Any] = {}
def runner() -> None:
try:
box["value"] = asyncio.run(coro)
except BaseException as e: # noqa: BLE001 — re-raised on the test thread
box["error"] = e
t = Thread(target=runner)
t.start()
t.join()
if "error" in box:
raise box["error"]
return box["value"]
def _reset_db_and_import(mock_llm: int) -> ImportSummary:
"""Truncate the KB (and query log + steering) and re-import the
summary_kb fixtures — yaml summarized, md control not."""
with SessionLocal() as db:
db.execute(text("TRUNCATE chunks, documents, query_log, steering_notes"))
db.commit()
summary = _run_in_thread(_import_fixtures(mock_llm))
assert summary.added == 2 # yaml + md control
assert summary.summaries == 1 and summary.summary_errors == 0
assert summary.errors == 0
return summary
def _expected_summary(content: str, source: str, path: str) -> str:
"""The mock lite model's byte-stable digest + the code pointer line.
Mirrors ``mock_llm.compose_answer``'s ``SUMMARY_MODE`` branch (first
24 tokens of the document content) plus the summarizer's
deterministic ``Source:`` line — no model output is ever trusted.
"""
digest = " ".join(TOKEN_RE.findall(content.lower())[:24])
return f"This document covers {digest}.\nSource: {source}/{path}"
def _summary_lines(source: str, path: str) -> tuple[str, str]:
"""(digest line, pointer line) of the stored summary for a fixture."""
expected = _expected_summary(
(FIXTURES / path).read_text(encoding="utf-8"), source, path
)
digest_line, pointer_line = expected.split("\n", 1)
return digest_line, pointer_line
# ---------------------------------------------------------------------------
# 1. Full page: labeled Summary panel ABOVE the original, both visible
# ---------------------------------------------------------------------------
def test_full_page_shows_summary_and_original_together(
page: Page, app_url: str, mock_llm: int, db_ready: None
) -> None:
"""The dedicated viewer for a summarized yaml shows the labeled
Summary panel (deterministic mock digest) and, below it, the FULL
original content — its tail sentinel, which the 24-token digest
cannot contain, is rendered in the raw ``<pre>``."""
_reset_db_and_import(mock_llm)
page.set_default_timeout(30_000)
login(page, app_url, next="/") # phase 79: the viewer content is gated
digest_line, pointer_line = _summary_lines(SOURCE, YAML_PATH)
page.goto(f"{app_url}/document.html?source={SOURCE}&path={YAML_URL_PATH}")
# Title + meta row unchanged by the phase (badge row is untouched).
expect(page.locator("#doc-title")).to_have_text("qwen-llamacpp")
expect(page.locator("#doc-meta .doc-source-badge")).to_have_text(SOURCE)
expect(page.locator("#doc-meta .format-badge")).to_have_text("yaml")
expect(page.locator("#doc-meta .doc-path")).to_have_text(YAML_PATH)
# Exactly one labeled Summary section — a real <section aria-label>
# with a visible heading (UI Structure Check: labeled section).
panel = page.locator(".doc-summary")
expect(panel).to_have_count(1)
expect(panel).to_be_visible()
expect(panel).to_have_attribute("aria-label", "Summary")
expect(panel.locator(".doc-summary-title")).to_have_text("Summary")
# The panel carries the DETERMINISTIC digest (byte-stable, no real
# LLM) — digest line + code pointer line — and nothing from the raw
# body beyond the first 24 tokens ("healthcheck" is deeper in the
# file, so the panel cannot be the original content).
expect(panel).to_contain_text(digest_line)
expect(panel).to_contain_text(pointer_line)
expect(panel).not_to_contain_text("healthcheck")
# The ORIGINAL is fully visible: the raw yaml in its <pre>, including
# the tail sentinel that sits outside the digest.
pre = page.locator("#doc-content pre.doc-raw")
expect(pre).to_have_count(1)
expect(pre).to_contain_text(SENTINEL)
expect(pre).to_contain_text("healthcheck")
# The panel sits ABOVE the content: first child of #doc-content.
order = page.evaluate(
"() => [...document.querySelector('#doc-content').children]"
".map((el) => el.className)"
)
assert order[0] == "doc-summary", f"panel not first: {order}"
assert "doc-raw" in order and order.index("doc-raw") > order.index("doc-summary")
# Theme (phase 08, dark-red rebrand 2026-08-28): panel on the
# surface colour, title in brand-ink (--brand-ink #fca5a5 on
# --surface #1a0f0f = 9.0:1 ≥4.5:1), and the panel matches the
# raw-content width — it does not break the content layout.
bg = panel.evaluate("el => getComputedStyle(el).backgroundColor")
assert bg == "rgb(26, 15, 15)", f"panel not on the --surface colour (rebrand #1a0f0f): {bg}"
title_color = panel.locator(".doc-summary-title").evaluate(
"el => getComputedStyle(el).color"
)
assert title_color == "rgb(252, 165, 165)", f"title not brand-ink: {title_color}"
panel_box = panel.bounding_box()
pre_box = pre.bounding_box()
assert panel_box is not None and pre_box is not None
assert abs(panel_box["width"] - pre_box["width"]) < 2, (
f"panel width {panel_box['width']} != raw width {pre_box['width']}"
)
assert panel_box["y"] < pre_box["y"], "panel must sit above the content"
# ---------------------------------------------------------------------------
# 2. Modal from the Sources table: same panel + content; "Full page"
# agrees (the two surfaces cannot drift — one shared renderer)
# ---------------------------------------------------------------------------
def test_modal_shows_panel_and_full_page_agrees(
page: Page, app_url: str, mock_llm: int, db_ready: None
) -> None:
"""Admin opens the yaml from the Sources table row: the SAME-PAGE
modal shows the Summary panel + the sentinel-bearing original; the
'Full page' escape hatch lands on the dedicated page with the panel
still there — both surfaces render through one shared core."""
_reset_db_and_import(mock_llm)
page.set_default_timeout(30_000)
login(page, app_url) # phase 16: the Sources catalog is admin-only
digest_line, pointer_line = _summary_lines(SOURCE, YAML_PATH)
row = page.locator("#docs-tbody tr", has_text=YAML_PATH)
expect(row).to_have_count(1)
before = len(page.context.pages)
row.locator("td:nth-child(2) a.doc-link").click()
assert len(page.context.pages) == before, "row link must not open a new tab"
# The modal shows the panel + the original content, in order.
expect(page.locator(".doc-modal")).to_be_visible()
expect(page.locator("#doc-modal-title")).to_have_text("qwen-llamacpp")
modal_panel = page.locator("#doc-modal .doc-summary")
expect(modal_panel).to_have_count(1)
expect(modal_panel).to_be_visible()
expect(modal_panel).to_have_attribute("aria-label", "Summary")
expect(modal_panel).to_contain_text(digest_line)
expect(modal_panel).to_contain_text(pointer_line)
modal_pre = page.locator("#doc-modal pre.doc-raw")
expect(modal_pre).to_have_count(1)
expect(modal_pre).to_contain_text(SENTINEL)
order = page.evaluate(
"() => [...document.querySelector('#doc-modal-content').children]"
".map((el) => el.className)"
)
assert order[0] == "doc-summary", f"panel not first in the modal: {order}"
# "Full page" (the #doc-modal-open escape hatch — target=_blank by
# design) opens the dedicated viewer in a NEW TAB, and the panel is
# there with the same digest and the same sentinel: the surfaces
# agree (one shared renderer, no drift).
with page.expect_popup() as popup_info:
page.click("#doc-modal-open")
full_page = popup_info.value
expect(
full_page
).to_have_url(
re.compile(
r"^" + re.escape(f"{app_url}/document.html")
+ r"\?source=summary_kb&path=quadlet%2Fqwen-llamacpp\.yaml$"
),
timeout=30_000,
)
full_panel = full_page.locator(".doc-summary")
expect(full_panel).to_have_count(1)
expect(full_panel).to_be_visible()
expect(full_panel).to_contain_text(digest_line)
expect(full_panel).to_contain_text(pointer_line)
expect(full_page.locator("#doc-content pre.doc-raw")).to_contain_text(SENTINEL)
# ---------------------------------------------------------------------------
# 3. No-summary control: the markdown doc renders exactly as before
# ---------------------------------------------------------------------------
def test_markdown_doc_has_no_summary_panel(
page: Page, app_url: str, mock_llm: int, db_ready: None
) -> None:
"""Control doc (markdown — never summarized): NO .doc-summary
element on either surface, and the content renders exactly as
before (first child of the content container is the doc body)."""
_reset_db_and_import(mock_llm)
page.set_default_timeout(30_000)
login(page, app_url, next="/") # phase 79: the viewer content is gated
# Full page: no panel, markdown column untouched.
page.goto(f"{app_url}/document.html?source={SOURCE}&path={MD_URL_PATH}")
expect(page.locator(".doc-summary")).to_have_count(0)
expect(page.locator("#doc-content .doc-md")).to_have_count(1)
expect(page.locator("#doc-content")).to_contain_text(
"came out of a week of"
)
order = page.evaluate(
"() => [...document.querySelector('#doc-content').children]"
".map((el) => el.className)"
)
assert order == ["doc-md"], f"markdown doc gained children: {order}"
# Modal: same story — no panel, .doc-md is the sole content child.
# (the session is already signed in — the form login above)
page.goto(f"{app_url}/sources.html")
row = page.locator("#docs-tbody tr", has_text=MD_PATH)
expect(row).to_have_count(1)
row.locator("td:nth-child(2) a.doc-link").click()
expect(page.locator(".doc-modal")).to_be_visible()
expect(page.locator("#doc-modal .doc-summary")).to_have_count(0)
expect(page.locator("#doc-modal .doc-md")).to_have_count(1)
expect(page.locator("#doc-modal-content")).to_contain_text(
"came out of a week of"
)
order = page.evaluate(
"() => [...document.querySelector('#doc-modal-content').children]"
".map((el) => el.className)"
)
assert order == ["doc-md"], f"markdown modal gained children: {order}"
# ---------------------------------------------------------------------------
# 4. API shape (cheap, via the page context's request — signed in since
# phase 79 gated the endpoint)
# ---------------------------------------------------------------------------
def test_content_api_summary_shape(
page: Page, app_url: str, mock_llm: int, db_ready: None
) -> None:
"""``GET /api/documents/content`` carries ``summary`` — the string
for the summarized yaml, ``null`` for the markdown control. Phase 79
superseded the phase-16 soft rule: the endpoint is require_user, so
the pin runs under the form-login session (the shape itself is
unchanged)."""
_reset_db_and_import(mock_llm)
login(page, app_url, next="/")
digest_line, _ = _summary_lines(SOURCE, YAML_PATH)
expected_summary = f"{digest_line}\nSource: {SOURCE}/{YAML_PATH}"
resp = page.context.request.get(
f"{app_url}/api/documents/content?source={SOURCE}&path={YAML_URL_PATH}"
)
assert resp.status == 200
body = resp.json()
assert body["source"] == SOURCE
assert body["path"] == YAML_PATH
assert body["format"] == "yaml"
assert isinstance(body["summary"], str)
assert body["summary"] == expected_summary
assert SENTINEL in body["content"] # full original, never just the digest
resp_md = page.context.request.get(
f"{app_url}/api/documents/content?source={SOURCE}&path={MD_URL_PATH}"
)
assert resp_md.status == 200
md_body = resp_md.json()
assert md_body["format"] == "md"
assert md_body["summary"] is None, "markdown docs never carry a summary"
assert "came out of a week of" in md_body["content"]