phase: 166_hide_empty_answer_bubble
Build and Push Containers / build-and-push-app (push) Successful in 1m41s
Build and Push Containers / build-and-push-db (push) Successful in 11s

**Phase 166 — final verification pass: green.** All 3 task files already in `complete/`, so this was verification + defect/doc fixing.

- **Verified:** `.msg .bubble:empty { display: none; }` exists once in the Messages block with provenance; `frontend/` diff is styles.css only (**no app.js change**); `tests/unit/test_no_empty_answer_bubble.py` 26 pins pass; phase-158/161 anchor pins (thinking_timeline + tool_states + edit_message) **75 passed**.
- **Phase E2E in isolation:** `uv run pytest tests/e2e/test_no_empty_box.py --no-cov` → **5 passed**. Mutation check (rule removed) → 2 failures, so the suite genuinely pins the defect.
- **Regression set, isolated:** thinking_timeline 6 · turn_progress_loader 3 · chat_history 5 · share_chat 4 · save_share_ux 5 · failed_turn_retry 4 · chat_answer_clusters 4 · chat_error_honesty 4 → all pass.
- **Full gates:** `uv run pytest --cov=app --cov-report=term-missing` → **3880 passed, app/ 99%** (gate >90%); `uv run ruff check .` → clean; `uv run pyright` → **0 errors**.
- **Whole E2E tree swept file-by-file (150 files, own pytest process each).** 4 suites were broken by *earlier* phases; fixed and re-run green:
  - `test_kb_tree`, `test_sync_summary_visibility`, `test_folder_summary_voice` — `td:nth-child(4) span` became a strict-mode violation when phase 156 wrapped the Edit label in `.cell-clip` → scoped to `span.kb-desc-text`.
  - `test_search_tool_scope` — pinned the pre-`690fd98` per-run `<summary>`/`open` shape → re-pinned to the ONE `details.reasoning` container + `.tool-calls-summary`.
  - `test_header_consistency` — `.doc-meta` pixel bound (`<34px`) was broken by phase 153's 44px touch-target floor at ≤640px → replaced with a shape-first single-line check that still fails a real wrap.
- **Completion criteria:** mid-turn box hidden ✅ · bubble appears at first delta, phase-130 rows below unchanged ✅ · restored/shared empty answer paints no box ✅ · no app.js change + anchors + full gates ✅ · commit/`complete/` move = harness's job (I ran no git index/history commands).
- **Docs:** README — added the phase-166 "no empty answer box mid-turn" note; fixed a **stale** Thinking lead ("collapsible Thinking blocks", pre-dated the 2026-10-06 one-disclosure refinement); QA section now states E2E isolation is mandatory (shared Postgres + fixed ports 8123/8901) with the per-file sweep loop. AGENTS.md — rule 5 UI checklist now names the phase-166 suppression rule.
- **Notable:** running all of `tests/e2e/` in one pytest session is not supported (fixture/port interleaving causes ~126 spurious failures) — documented in README. Restored the tracked `.agents/screenshots/doc_modal_scrollbar_1280.png` that `test_document_viewer.py` rewrites on every run.
- **Next pending phase:** `167_api_payload_diet`.
This commit is contained in:
2026-10-08 04:27:03 -04:00
parent b0c7aed613
commit ab9391d019
36 changed files with 2348 additions and 93 deletions
+40 -7
View File
@@ -91,6 +91,33 @@ def _box_height(page: Page, selector: str) -> float:
return box["height"]
def _meta_line_count(page: Page, selector: str) -> int:
"""How many VISUAL LINES the row's children occupy: cluster the
children by their vertical center (a 4px tolerance absorbs the
mixed badge/text/native-control heights on one line) and return the
number of clusters. ONE cluster = one line; a wrap produces two (the
second row of children sits a full line lower). Shape-first on
purpose: the row's own height is content-sized by its tallest child
— since phase 153 that is the ≥44px touch-target floor on
`.doc-copy-path` at ≤640px, so a pixel bound on the row would pin
the control's minimum, not the layout."""
centers = page.evaluate(
"""(sel) => [...document.querySelectorAll(sel + ' > *')]
.map(el => { const b = el.getBoundingClientRect();
return b.height > 0 ? b.top + b.height / 2 : null; })
.filter(v => v !== null)""",
selector,
)
assert centers, f"{selector} rendered no visible children"
clusters: list[list[float]] = []
for c in sorted(centers):
if clusters and c - clusters[-1][-1] <= 4:
clusters[-1].append(c)
else:
clusters.append([c])
return len(clusters)
def _header_heights(page: Page, app_url: str) -> dict[str, float]:
"""Measured heights of the sticky top bar on the three pages."""
heights: dict[str, float] = {}
@@ -201,13 +228,19 @@ def test_viewer_header_content_still_fits(
assert _box_height(page, "#doc-title") < 30 # one line at either size
# Meta row: badges + path present and visible, single line.
# The bound catches a WRAP (a second line would be ≥ ~44px),
# not a pixel-exact line height: the row is content-sized by
# its tallest child — the native type=date input (phase 106
# D8), whose metrics are Chromium/font-dependent and render
# the single line at ~26px on this host (the pre-phase 26px
# pin red-lined on the 26.125px measurement, 2026-09-16).
assert _box_height(page, ".doc-meta") < 34
# SHAPE, not pixels: the pre-phase absolute bound (<34px) went
# stale the moment the row gained a control carrying the ≥44px
# touch-target floor (phase 153: .doc-copy-path min-height 44px
# at ≤640px — PLAN §7.1), which makes a single, correctly-wrapped
# mobile line measure 44px. What the contract actually forbids is
# the row STACKING (a wrap grows the header), so the pin reads the
# geometry directly: every child centered on the row's own center
# = ONE line, whatever the tallest control's height (the same
# shape-first reading the 2026-09-16 note above already moved to).
assert _meta_line_count(page, ".doc-meta") == 1, (
"the meta row must stay on ONE line (badges, path, dates and "
"controls centered on the row's own center)"
)
expect(page.locator(".doc-source-badge", has_text="docs")).to_be_visible()
expect(page.locator(".format-badge", has_text="md")).to_be_visible()
expect(page.locator(".doc-path", has_text="homelab/kubernetes.md")).to_be_visible()