phase: 104_chip_sizing_question_cap
Build and Push Containers / build-and-push-app (push) Successful in 1m48s
Build and Push Containers / build-and-push-db (push) Successful in 12s

All completion criteria verified. Final report:

**Phase 104 — final verification pass: all green**
- Verified (no code changes needed): single-line ellipsized `.suggestion-chip` CSS + deleted `.maybe-try` override, `renderChips` full-text `title` + `aria-label`-when-clipped, `maxlength="4000"` + `#char-count` counter + `handleSend` over-cap guard, unit pins file, schemas boundary pins, dedicated E2E suite.
- E2E (isolation): `uv run pytest tests/e2e/test_chip_sizing_question_cap.py -v --no-cov` → **6 passed**; regressions: `test_suggestion_chips.py` 8 passed, `test_pinned_composer.py` 4 passed, `test_responsive_polish.py` 7 passed, `test_chat_history.py` 5 passed.
- `uv run pytest` → **2102 passed**; `--cov=app` → **99%** (>90%); `uv run ruff check . && uv run pyright` → clean, 0 errors.
- Criteria: chip E2E (single-line, clipped, title+aria-label full text) ✅; paste caps at exactly 4,000, send streams, counter hides ✅; programmatic 5,000-char fill → banner, no turn, text kept ✅; 4,000/4,001 boundary pinned + HTML maxlength == JS constant cross-file pin ✅.
- Diff scope: `frontend/`, new unit file, `tests/unit/test_schemas.py`, new E2E file, phase files — **no `app/` diff, no migration, no `shared.js` diff**.
- Deviations: 4 regression test files touched — 2 genuine DOM-pin conflicts from the new `#char-count` child (explicitly anticipated by the overview) + 3 documented **pre-existing E2E flake fixes** (smooth-scroll race, tab-walk heuristic, 10 ms timeout), each verified pre-existing on the pre-phase-104 tree.
- No commit made (harness commits per the execution protocol override).
- Next pending phase: `98_sync_summary_visibility`.
This commit is contained in:
2026-09-12 19:45:00 -04:00
parent 1f1c01c9f7
commit ecc921098a
31 changed files with 1874 additions and 36 deletions
+23 -10
View File
@@ -312,12 +312,13 @@ def test_chat_bottom_unit_is_last_child_of_the_chat_shell() -> None:
"""Phase 65 (task 02, owner-locked A1): the LAST element child of
`.chat-shell` is the `.chat-bottom` wrapper — NO id (nothing in JS
binds it; the bindings live on the inner elements, the move is pure
HTML/CSS) — holding exactly the `.chat-actions` row and then the
`#composer` form, in that order: the row + composer are ONE sticky
unit, and the wrapper owns the shell's bottom slot, so the sticky
shift range is still that column's box (a sibling after it would
carve the range away and re-break the pin). The composer form keeps
`novalidate` and its contract ids."""
HTML/CSS) — holding the `.chat-actions` row, the phase-104
`#char-count` counter, and the `#composer` form, in that order: the
row + counter + composer are ONE sticky unit, and the wrapper owns
the shell's bottom slot, so the sticky shift range is still that
column's box (a sibling after it would carve the range away and
re-break the pin). The composer form keeps `novalidate` and its
contract ids."""
shell = _tree().find("chat-shell")
last = shell["children"][-1]
assert last["tag"] == "div" and (
@@ -330,17 +331,29 @@ def test_chat_bottom_unit_is_last_child_of_the_chat_shell() -> None:
"elements"
)
kids = last["children"]
assert len(kids) == 2, (
"the unit holds exactly two element children: .chat-actions, then "
"#composer"
assert len(kids) == 3, (
"the unit holds exactly three element children: .chat-actions, "
"then #char-count (phase 104), then #composer"
)
row, form = kids
row, counter, form = kids
assert row["tag"] == "div" and (
row["attrs"].get("class") or ""
).split() == ["chat-actions"], (
"the first child is the .chat-actions row (the New chat → Share "
"DOM order is pinned by test_save_chat_ui.py)"
)
# Phase 104 (owner 2026-09-12): the question-length counter — a
# hidden-by-default <p> between the row and the composer (zero
# height while hidden; a new child INSIDE the unit breaks no pin —
# the sticky geometry below is untouched).
assert counter["tag"] == "p" and counter["attrs"].get("id") == "char-count", (
"the second child is the phase-104 #char-count counter"
)
assert (counter["attrs"].get("class") or "") == "char-count"
assert "hidden" in counter["attrs"], (
"the counter ships hidden — it appears only from 80% of the "
"4,000-char cap (app.js updateCharCount)"
)
assert form["tag"] == "form" and form["attrs"].get("id") == "composer"
assert "novalidate" in form["attrs"], (
"phase 48: the composer form stays `novalidate` (a `required` "