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
+28 -6
View File
@@ -130,8 +130,24 @@ def _assert_no_doc_overflow(page: Page, label: str) -> None:
def _tab_outline_walk(page: Page, max_tabs: int = 60) -> list[dict[str, str]]:
"""Real keyboard Tab walk; returns each focused element's outline."""
first_key: str | None = None
"""Real keyboard Tab walk; returns each focused element's outline.
The walk covers a FULL focus cycle (it starts wherever Chromium
resumes after the skip-link check — mid-document, right after
#main — and ends when focus wraps back to the FIRST element it
visited). The wrap is detected by TRUE element identity: each
newly focused element is stamped with a unique ``data-tabwalk-id``
and the cycle ends when a STAMPED element is focused again.
Phase 104 (task 04, 2026-09-12): the pre-104 heuristic keyed on
the first 24 chars of the element's text — two chips sharing that
prefix (e.g. the two identical "How is my Kubernetes cluster set
up? write a long answer" opener chips left in ``saved_chats`` by
test_pinned_composer.py, which runs suites in sequence on the
shared e2e DB) collided and cut the walk short at two entries,
flaking ``len(seen) >= 3``. Verified pre-existing on the
pre-phase-104 tree (the mid-document Tab start is Chromium
behavior, not a phase-104 change)."""
first_id: str | None = None
seen: list[dict[str, str]] = []
for _ in range(max_tabs):
page.keyboard.press("Tab")
@@ -142,7 +158,13 @@ def _tab_outline_walk(page: Page, max_tabs: int = 60) -> list[dict[str, str]]:
const cls = String(el.className).split(" ")[0];
const label = (el.getAttribute("aria-label")
|| el.textContent || "").trim().slice(0, 24);
let id = el.getAttribute("data-tabwalk-id");
if (!id) {
id = "tw" + ((window.__twSeq = (window.__twSeq || 0) + 1));
el.setAttribute("data-tabwalk-id", id);
}
return {
id: id,
key: el.tagName + "#" + (el.id || "") + "." + cls + ":" + label,
outline_style: cs.outlineStyle,
outline_width: cs.outlineWidth,
@@ -151,11 +173,11 @@ def _tab_outline_walk(page: Page, max_tabs: int = 60) -> list[dict[str, str]]:
)
if info["key"].startswith("BODY"):
continue # focus has not entered the document yet
if first_key is None:
first_key = info["key"]
if first_id is None:
first_id = info["id"]
seen.append(info)
if len(seen) > 1 and info["key"] == first_key:
break # wrapped back to the first focusable
if len(seen) > 1 and info["id"] == first_id:
break # wrapped back to the first focusable (same ELEMENT)
return seen