phase: 157_chat_chip_cap
Build and Push Containers / build-and-push-app (push) Successful in 2m19s
Build and Push Containers / build-and-push-db (push) Successful in 11s

All verification is green. Final report:

**Phase 157 — final verification pass (all 3 tasks already complete in `complete/`)**

- Verified the implemented work matches the design: `CHIP_CAP = 90` constant + display-only cap in `renderChips` (`slice(0,90).trimEnd() + "…"`), `title`/`aria-label`/click payload byte-unchanged; unit pins (89/90/91 letter & space, full-text invariant, short-string guard, Maybe-try single-path guard); phase-104 E2E updated to the cap contract
- Full suite `uv run pytest --cov=app --cov-report=term-missing`: **3578 passed**, app/ coverage **99%** (>90% ✓)
- `uv run ruff check .` clean; `uv run pyright`: 0 errors
- Dedicated E2E `uv run pytest tests/e2e/test_chat_chip_cap.py -v --no-cov`: **4/4 passed** in isolation (DB up)
- Cross-phase regression: phase-104 E2E 6/6, phase 05/80/103 chip suite 8/8 (seed chips <90 chars — cap is a no-op, assertions hold)
- Criteria: ≤91-char capped display ✓ (byte-for-byte pin) · click submits FULL 120-char question (user bubble) ✓ · `title` + clipped-case `aria-label` carry full text ✓ (375px viewport) · <90-char byte-identical ✓ (unit + live DOM) · commit: deferred to the harness (per harness protocol, overrides AGENTS.md rule 8 for this pass)
- Documentation: README.md reviewed in full — every `.env.example` var documented, all referenced scripts/pages/commands verified against the tree, the phase's chip-cap note is in the Chat section; AGENTS.md verified (commands, test files, layout-rule citations — phase 157 adds no new layout principle) → **verified consistent, no changes needed**
- No live-infrastructure changes were required or made
- Next pending phase: **158_thinking_timeline**
This commit is contained in:
2026-10-04 22:18:31 -04:00
parent 1513981c1c
commit 3f752fb77f
21 changed files with 1423 additions and 45 deletions
+50 -31
View File
@@ -16,7 +16,11 @@ The contract (owner decisions A1–A6, recorded in 00_phase.md):
multi-line "chonk") — long text ellipsizes at the row edge, and the
FULL text is recoverable via the native ``title`` tooltip on every
chip plus the ``aria-label`` accessible name when the visible text is
actually clipped;
actually clipped; phase 157 (owner 2026-10-03, A1) EXTENDS this:
the chip's DISPLAYED text is capped at 90 characters
(``slice(0, 90).trimEnd() + "…"``) — the cap is the PRIMARY length
guard, the single-line CSS clip the secondary; the ``title`` /
``aria-label`` full-text contract is unchanged;
* **A3 — the hard cap through the input path:** the composer textarea
carries ``maxlength="4000"`` mirroring the pre-existing server cap
(``ChatRequest.message max_length=4000`` — the server 422s beyond);
@@ -34,17 +38,21 @@ The contract (owner decisions A1–A6, recorded in 00_phase.md):
The states pinned here (six tests):
1. **truncated-chip** (A1/A2 core) — a saved chat whose FIRST user
question is LONG (720 chars — a readable repeated phrase, 300+ per
the phase) with a short follow-up → a fresh page load shows EXACTLY
ONE onboarding chip (the phase-103 opener semantics — the follow-up
never surfaces): computed ``white-space: nowrap`` /
``overflow: hidden`` / ``text-overflow: ellipsis``, visually clipped
(``scrollWidth > clientWidth``), single line (``44 <=
offsetHeight <= 60`` — a one-line pill sits at the 44px
``min-height`` floor, border-box; a wrapped two-liner is ~65px —
the chonk), and
``title`` + ``aria-label`` == the FULL long text;
1. **capped-chip** (A1/A2 core + the phase-157 extension) — a saved
chat whose FIRST user question is LONG (720 chars — a readable
repeated phrase, 300+ per the phase) with a short follow-up → a
fresh page load shows EXACTLY ONE onboarding chip (the phase-103
opener semantics — the follow-up never surfaces): computed
``white-space: nowrap`` / ``overflow: hidden`` /
``text-overflow: ellipsis`` (the phase-104 secondary guard), the
DISPLAY capped at 90 chars (``slice(0, 90).trimEnd() + "…"`` — the
phase-157 primary guard), FITTING the 72rem column at the 1280px
viewport (``scrollWidth <= clientWidth`` — the cap is what keeps
the chip compact), single line (``44 <= offsetHeight <= 60`` — a
one-line pill sits at the 44px ``min-height`` floor, border-box;
a wrapped two-liner is ~65px — the chonk), ``title`` == the FULL
long text, and NO ``aria-label`` (not visually clipped — the
attribute is absent by design);
2. **seed-contrast** — a fresh deployment's (seed) short chips have
``title`` set AND NO ``aria-label`` (not truncated — the attribute
is absent by design; the ``textContent`` already carries the full
@@ -231,22 +239,26 @@ def _wait_chat_booted(page: Page) -> None:
def test_long_chip_is_single_line_ellipsized_with_full_text_tooltip(
page: Page, app_url: str, db_ready: None
) -> None:
"""A1/A2 core: a saved chat whose FIRST user question is 720 chars
(a readable repeated phrase) with a short follow-up → a fresh page
load shows EXACTLY ONE onboarding chip (the phase-103 opener
semantics — the follow-up never surfaces), and it is:
"""A1/A2 core + the phase-157 extension: a saved chat whose FIRST
user question is 720 chars (a readable repeated phrase) with a
short follow-up → a fresh page load shows EXACTLY ONE onboarding
chip (the phase-103 opener semantics — the follow-up never
surfaces), and it is:
* ONE line — computed ``white-space: nowrap``, ``overflow: hidden``,
``text-overflow: ellipsis``;
* visually clipped — ``scrollWidth > clientWidth`` (720 chars of
~7px/char far exceeds the 46rem column);
``text-overflow: ellipsis`` (the phase-104 secondary guard);
* display-capped — the visible text is
``slice(0, 90).trimEnd() + "…"`` (the phase-157 PRIMARY guard —
a 91-char display no longer stretches the chip to the row edge);
* FITS the 72rem column at the 1280px viewport — NOT visually
clipped (``scrollWidth <= clientWidth``);
* the pill, not the chonk — ``44 <= offsetHeight <= 60`` (a
one-line pill sits at the 44px ``min-height`` floor, border-box
— the global ``* { box-sizing: border-box }`` makes the 44px
the OUTER height; a wrapped two-liner is ~65px);
* the FULL text one hover away — ``title`` == the full long text;
* the clipped-case accessible name — ``aria-label`` == the full
long text.
* no ``aria-label`` — the chip is not visually clipped, so the
attribute stays absent (the phase-104 pattern).
"""
page.set_default_timeout(30_000)
login(page, app_url, next="/")
@@ -268,9 +280,11 @@ def test_long_chip_is_single_line_ellipsized_with_full_text_tooltip(
chips = page.locator("#suggestions .suggestion-chip")
expect(chips.first).to_be_visible(timeout=30_000)
expect(chips).to_have_count(1, timeout=15_000)
# The chip carries the EXACT full opener in the DOM (the clipping
# is visual — textContent is never truncated).
assert chips.first.inner_text().strip() == LONG_OPENER
# Phase 157 (A1): the DISPLAY is capped at 90 chars — the 720-char
# opener shows as slice(0, 90).trimEnd() + "…" (91 chars max);
# the full text stays in the title (asserted below).
assert chips.first.inner_text().strip() == LONG_OPENER[:90].rstrip() + "…"
assert len(chips.first.inner_text().strip()) <= 91
chip = chips.first
# A1 — the single-line computed contract (the phase-104 CSS).
@@ -291,22 +305,27 @@ def test_long_chip_is_single_line_ellipsized_with_full_text_tooltip(
dims = chip.evaluate(
"el => ({ sw: el.scrollWidth, cw: el.clientWidth, oh: el.offsetHeight })"
)
assert dims["sw"] > dims["cw"], (
"a 720-char question must be visually clipped in the 46rem column"
assert dims["sw"] <= dims["cw"], (
"the 91-char capped display must FIT the 72rem column at the "
"1280px viewport — the cap is the PRIMARY guard, the CSS "
"clip the secondary (a 720-char display is what used to "
"stretch the chip to the row edge)"
)
assert 44 <= dims["oh"] <= 60, (
f"one-line pill: 44px min-height floor (border-box), a wrapped "
f"two-liner is ~65px — got {dims['oh']}px"
)
# A2 — the full text is one hover away (the native tooltip), and
# the accessible name of the CLIPPED chip is the full text.
# A2 — the full text is one hover away (the native tooltip).
assert chip.get_attribute("title") == LONG_OPENER, (
"the title tooltip must carry the FULL question (owner A2)"
)
assert chip.get_attribute("aria-label") == LONG_OPENER, (
"a clipped chip's aria-label must be the full text (the "
"source-chip pattern)"
# Not visually clipped (the capped display fits) → NO aria-label
# (the phase-104 pattern: the attribute is set only when the
# visible text is clipped).
assert chip.get_attribute("aria-label") is None, (
"a chip whose display FITS its pill must stay attribute-free "
"— aria-label is set only when visually clipped (phase 104)"
)