phase: 130_chat_answer_clusters
Build and Push Containers / build-and-push-app (push) Successful in 2m27s
Build and Push Containers / build-and-push-db (push) Successful in 12s

**Phase 130 — final verification pass: 100% green, complete.**

- **Verified:** chips-only row (no chip for image docs) → images-only row (`.msg-images`) → related row → `.msg-actions` last; `getActionsRow` used by all 3 buttons + stopped/failed notes; `markLastRetryable` save-re-append hack gone; in-place chip fallback on image-load failure; CSS right-align with per-button auto margins removed — implementation matches the design in every detail.
- **Completion criteria:** (1) mixed citation renders chips→images→actions-last, DOM-asserted ✅ (2) all 3 buttons in one right-aligned `.msg-actions`; non-admin sees Retry-only; restore path identical ✅ (3) 404-bytes figure → plain chip, unit-pinned ✅ (4) E2E isolated ✅ (5) no behavior change (modal affordances, related tier, ARIA names) ✅
- **Tests/lint/coverage:** `uv run pytest tests/e2e/test_chat_answer_clusters.py -v --no-cov` → 4 passed; `uv run pytest --cov=app --cov-report=term-missing` → 3061 passed, **99%** app coverage; the 4 E2E files this phase modified (test_answer_images, test_image_documents, test_pdf_support, test_responsive_polish) → 22 passed in isolation; `uv run ruff check .` clean, `uv run pyright` → 0 errors.
- **Docs:** README.md — verified consistent in full (page table vs routes, env-var coverage vs `.env.example`, phase-130 cluster wording already in tree); no changes needed. AGENTS.md — fixed one pre-existing inconsistency: rule 5 still cited the retired "centered 46rem chat column"; now states the 72rem container contract (phase 100).
- **Decisions:** PLAN.md §7.1 still says 46rem, but it's owner-managed (phase 100 records the supersession pending owner sign-off) and out of my editable scope — AGENTS.md now cross-references the supersession.
- **Next pending phase:** `131_read_line_ranges` (todo/).
This commit is contained in:
2026-09-28 07:49:37 -04:00
parent 2d0f608246
commit c88c65641a
46 changed files with 3266 additions and 351 deletions
+42 -20
View File
@@ -139,8 +139,9 @@ def test_app_js_button_gates_on_admin_and_configured() -> None:
"""The single guard: admin (the whoami gate Tune uses) AND
docs_repo_configured — otherwise the function injects NOTHING
(anonymous, or unconfigured admin, or deflected scope — same as
Tune). One button per bubble; the .msg-meta row is reused (or
created plain) and a role=list row gets a listitem button (ARIA)."""
Tune). One button per bubble; the button joins the bubble's
.msg-actions row (getActionsRow — phase 130 task 02; the role=list
row gets a listitem button, ARIA)."""
js = _text(APP_JS)
fn_idx = js.find("function appendSaveAsDocButton(wrap, markdown) {")
assert fn_idx != -1, "appendSaveAsDocButton missing"
@@ -354,23 +355,36 @@ def test_app_js_post_payload_and_navigation() -> None:
assert "Couldn't save the answer as a doc" in fn_body
def test_app_js_retry_landing_keeps_save_rightmost() -> None:
"""markLastRetryable re-appends the save button AFTER the Retry
button lands on the same (last) bubble — the auto-margined buttons
split the row's free space between them, so DOM order decides the
right edge: "Save as doc" stays the bottom-right action even on the
last bubble (which also carries Retry)."""
def test_app_js_retry_landing_has_no_save_reappend_hack() -> None:
"""Phase 130 (task 02): markLastRetryable re-appends the Retry
button into the last bubble's .msg-actions row (appendRetryButton →
getActionsRow) and NO LONGER re-appends the save button after it —
the row's row-level right-align (phase 130 task 03) right-aligns
the WHOLE cluster, and the DOM order inside the row is the
insertion order (the live call sites append Tune → Retry →
Save-as-doc per their gates). The phase-59 "auto margins split the
free space" hack is gone with the per-button auto margins it worked
around."""
js = _text(APP_JS)
fn_idx = js.find("function markLastRetryable() {")
assert fn_idx != -1
fn_end = js.find("/* Phase 59 (owner-locked 2026-08-31, TODO.md L3): the bottom-right", fn_idx)
fn_body = js[fn_idx:fn_end]
assert "appendRetryButton(lastBrainWrap);" in fn_body
assert 'lastBrainWrap.querySelector(".save-as-doc-btn")' in fn_body
# "saveDocBtn" — NOT "saveBtn": phase 55 pins the Save pill's
# identifier gone from app.js (substring), so the local stays distinct.
assert "saveDocBtn.parentElement.appendChild(saveDocBtn)" in fn_body
assert "saveBtn" not in _text(APP_JS), (
assert "appendRetryButton(lastBrainWrap);" in fn_body, (
"the Retry button is re-appended to the last brain bubble"
)
assert "saveDocBtn" not in fn_body, (
"the phase-59 save-button re-append hack is gone — the actions "
"row's right-align (phase 130 task 03) replaces it"
)
assert "parentElement" not in fn_body, (
"no DOM re-parenting of the save button anymore"
)
# "saveDocBtn" is gone from the WHOLE file (the hack had no other
# home); "saveBtn" (the phase-55 retired Save pill's identifier)
# stays absent too.
assert "saveDocBtn" not in js
assert "saveBtn" not in js, (
"the phase-55 pin: no saveBtn identifier in app.js"
)
@@ -381,16 +395,24 @@ def test_app_js_retry_landing_keeps_save_rightmost() -> None:
def test_styles_css_save_as_doc_btn_is_right_aligned() -> None:
""".save-as-doc-btn exists, carries the bottom-right declaration
(margin-inline-start: auto) and the .tune-btn visual family (pill,
>=44px target, line border, ink-soft palette); :focus-visible is
the global rule, the hover rule is per-class."""
""".save-as-doc-btn exists in the .tune-btn visual family (pill,
>=44px target, line border, ink-soft palette) with NO per-button
auto margin — phase 130 moved the bottom-right requirement to the
ROW (the .msg-meta.msg-actions justify right-aligns the whole
actions cluster; the cluster-level pin lives in
test_frontend_cluster_css). :focus-visible is the global rule, the
hover rule is per-class."""
css = _text(STYLES_CSS)
m = re.search(r"\.save-as-doc-btn \{[^}]*\}", css)
assert m, "the .save-as-doc-btn rule is missing"
block = m.group(0)
assert "margin-inline-start: auto;" in block, (
"the bottom-right requirement lives on the button's class"
assert "margin-inline-start: auto" not in block and "margin-left: auto" not in block, (
"phase 130: the per-button auto margin is gone — the row's "
".msg-actions justify does the bottom right"
)
row = re.search(r"\.msg-meta\.msg-actions \{[^}]*\}", css)
assert row and "justify-content: flex-end" in row.group(0), (
"the bottom-right requirement now lives on the .msg-actions row"
)
assert "min-height: 44px;" in block # WCAG touch target (the family)
assert "border-radius: 999px;" in block