phase: 165_prose_word_wrap
Build and Push Containers / build-and-push-app (push) Successful in 1m44s
Build and Push Containers / build-and-push-db (push) Successful in 11s

All verification complete. Final report:

**Phase 165 — Prose word wrap (final verification pass)**
- Verified: `PROSE_FORMATS={txt,text}` + `isProseDoc` (single-site, image arm wins); `renderDocument` three-way branch (md → `.doc-md`, prose → `.doc-prose` text node, else `pre.doc-raw` byte-identical); `.doc-prose` CSS = 72rem column, `pre-wrap` + `overflow-wrap: anywhere`, tokens only, zero literals; `[data-view="pages"]` hide list includes `.doc-prose` as one grouped declaration; phase-116 scrollbar rules intact and modal-scoped; no `innerHTML` in the prose path.
- Tests/lint (all 100% green):
  - `uv run pytest` → **3854 passed** (unit + integration)
  - `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99%** (>90%)
  - `uv run ruff check .` → All checks passed; `uv run pyright` → **0 errors**
  - `uv run pytest tests/e2e/test_prose_word_wrap.py -v --no-cov` → **5 passed** (isolation)
  - Isolation regressions: document_viewer 8, image_documents 4, pdf_pages_in_sources 4, pdf_strip_additive_selfheal 5, edit_summaries 4, summary_in_viewer 4, chat_answer_clusters 4, garbage_pdf 3, pdf_support 6, sticky_navbar 3, smoke 3 — all passed
- Completion criteria: image description wraps, full text, both surfaces ✔; `.txt` wraps same way ✔; `py`/`json`/code keep `pre.doc-raw` + horizontal overflow + themed modal scrollbar (E2E negative control + pins) ✔; no innerHTML, AA `--ink`/`--surface` pair ✔; full gates green ✔; commit + phase-dir move are harness-owned (not executed per pipeline rules).
- Documentation: README already updated by task 03 (viewer section + image-docs section); full README/AGENTS.md review found no further inconsistencies — **verified consistent, no changes needed**.
- Notable deviations: `.doc-prose` omits `font-family: inherit` from 00_phase D2's snippet — task 01's authoritative "exact property set" excludes it and `div` inherits fonts anyway (behavior identical). PDF deferral (A4) intact: `format == "pdf"` stays in `.doc-raw`, owner call recorded.
- Next pending phase: `166_hide_empty_answer_bubble`.
This commit is contained in:
2026-10-07 22:57:40 -04:00
parent 8d70aa946c
commit b0c7aed613
36 changed files with 2198 additions and 64 deletions
+74 -15
View File
@@ -27,7 +27,10 @@ the short-page stretch. This suite proves the BROWSER behavior:
* the short-page layout is EXACTLY as it was: body stretched to the
viewport, footer pinned to the viewport bottom, the phase-52
pinned composer (``position: sticky; bottom: env(safe-area-inset-
bottom)``) resting in its in-flow slot above the footer.
bottom)``) resting in its in-flow slot above the footer. The pin
is POST-boot (phase 88 keeps the cluster static until app.js adds
``#view-chat.chat-booted``), so test 3 waits for that sentinel
before reading it — the house boot-settle pattern.
Seeding (A3): the long-scroll surfaces are the seeded Sources table
(``tests/fixtures/docs/`` — 13 docs, same harness as the phase-10/26
@@ -37,14 +40,27 @@ short unique docs + one ``doc-long.md`` with a ~40,000-char body).
mock embeddings server (the ``test_document_viewer.py`` pattern) — no
real LLM is involved.
Phase 156 re-target (owner-locked A1): the catalogue card became the
tables' OWN bounded scroller (``.table-wrap`` max-height
min(70vh, 64rem)), so a Sources file level no longer grows the
DOCUMENT past a full viewport — its scroll is the residual chrome
height. Test 1 therefore proves the stick across the page's REAL
scroll range (mid + bottom, card scroller pinned too); the DEEP-scroll
proof (the audit's ~−1264px repro, scroll far past one viewport) is
test 2's long-document surface, which still grows the document many
viewports.
Test → story mapping (Playwright Mapping Rule):
1. ``test_app_header_stuck_at_top_on_sources``
2. ``test_doc_header_stuck_at_top_on_long_document``
1. ``test_app_header_stuck_at_top_on_sources`` — stuck mid-page AND at
the page bottom; the 41-row table scrolls inside the phase-156 card
2. ``test_doc_header_stuck_at_top_on_long_document`` — stuck at the
bottom of a many-viewport scroll (the deep-scroll proof)
3. ``test_short_page_stretch_and_resting_composer_intact``
"""
from __future__ import annotations
import asyncio
import re
from collections.abc import Iterator
from pathlib import Path
from threading import Thread
@@ -223,26 +239,49 @@ def test_app_header_stuck_at_top_on_sources(
page.click('#folders-tbody a.folder-link:text-is("gen")')
expect(page.locator("#docs-tbody tr")).to_have_count(TOTAL_DOCS - 13)
# The precondition the story needs: the page must actually scroll —
# fail loudly if the table ever stops being long enough.
# Phase 156 (owner-locked A1): the 41-row table scrolls INSIDE the
# bounded card (max-height min(70vh, 64rem)) — the card is the rows'
# scroller, and the DOCUMENT keeps the residual page scroll (header
# / stats / toolbar / padding / footer). Preconditions, both fail
# loudly: the card must actually scroll (every row reachable), and
# the page must still scroll at all.
sh = page.evaluate("() => document.documentElement.scrollHeight")
assert sh > 1.5 * VIEWPORT_H, (
f"the Sources table must be long enough to scroll "
f"(scrollHeight={sh}, want > {1.5 * VIEWPORT_H:.0f} at a "
f"{VIEWPORT_W}×{VIEWPORT_H} viewport) — add more generated docs"
assert sh > VIEWPORT_H + 200, (
f"the Sources page must be scrollable (scrollHeight={sh}, want > "
f"{VIEWPORT_H + 200} at a {VIEWPORT_W}×{VIEWPORT_H} viewport) — "
"the page collapsed to a fixed layout"
)
card = page.evaluate(
"""() => {
const c = document.querySelector(
".table-wrap[aria-label='Indexed documents']");
return c ? { sh: c.scrollHeight, ch: c.clientHeight } : null;
}"""
)
assert card is not None and card["sh"] > card["ch"], (
f"the phase-156 card must be the table's scroller "
f"(scrollHeight={None if card is None else card['sh']}, "
f"clientHeight={None if card is None else card['ch']})"
)
max_scroll = sh - VIEWPORT_H
# Mid-page scroll (~1 viewport down — the pre-fix un-pin point): the
# navbar is still stuck at the top.
page.evaluate("() => window.scrollTo(0, 800)")
# Mid-page scroll (half the document's available scroll): the
# navbar is still stuck at the top. The DEEP-scroll proof — a scroll
# far past one viewport, the audit's ~−1264px repro — is test 2's
# long-document surface: phase 156's bounded card left Sources as
# the residual-scroll page.
page.evaluate(f"() => window.scrollTo(0, {max_scroll // 2})")
assert (
page.evaluate("() => window.scrollY") >= VIEWPORT_H - TOP_TOL_PX
abs(page.evaluate("() => window.scrollY") - max_scroll // 2) <= TOP_TOL_PX
), "the test scroll must land"
_assert_top_is_zero(page, ".app-header")
# And at the very bottom (the audit's −1264px repro): stuck, and
# still visible.
# And at the very bottom of the page scroll: stuck, and still
# visible.
page.evaluate("() => window.scrollTo(0, 999999)")
assert page.evaluate("() => window.scrollY") >= max_scroll - TOP_TOL_PX, (
"the test scroll must land at the document bottom"
)
_assert_top_is_zero(page, ".app-header")
expect(page.locator(".app-header")).to_be_visible()
@@ -290,12 +329,32 @@ def test_doc_header_stuck_at_top_on_long_document(
# ---------------------------------------------------------------------------
def _wait_chat_booted(page: Page) -> None:
"""The house boot-settle wait (test_chip_sizing_question_cap.py /
test_mobile_chat_hamburger_boot.py): app.js adds
``#view-chat.chat-booted`` two frames AFTER the boot settles. The
phase-88 pre-boot rule
(``#view-chat:not(.chat-booted) .composer { position: static }``)
keeps the cluster NON-sticky until then — reading the pin before
the sentinel lands races app boot (the flake this removes: the
sticky assertion measured a pre-boot static composer)."""
page.wait_for_function(
"() => document.getElementById('view-chat')?."
"classList.contains('chat-booted')",
timeout=15_000,
)
expect(page.locator("#view-chat")).to_have_class(re.compile(r"\bchat-booted\b"))
def test_short_page_stretch_and_resting_composer_intact(
page: Page, app_url: str
) -> None:
page.set_default_timeout(30_000)
page.goto(app_url)
expect(page.locator("#empty-state")).to_be_visible()
# The pin under test only exists POST-boot (phase 88 defers the
# sticky promotion) — settle the boot BEFORE any measurement.
_wait_chat_booted(page)
# The stretch holds: the body is exactly one viewport tall, and the
# grow must not invent scrollable space.