phase: 130_chat_answer_clusters
**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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user