Files
brain-of-reese/tests/unit/test_summary_edit_ui.py
ducoterra ee3efb28c9
Build and Push Containers / build-and-push-app (push) Successful in 4m35s
Build and Push Containers / build-and-push-db (push) Successful in 14s
phase: 106_document_dates
Everything is verified green. Final report:

**Phase 106 — Document dates (final verification pass; all 10 tasks already complete)**

- Verified all phase artifacts: alembic `0020` (dev DB at `0020`), `doc_dates.py`, git `file_commit_dates`, importer `doc_dates_by_root`/`dates_updated`, both entry-point wirings, date APIs + tree `created_at`/`updated_at`, LLM surfaces (prompt block, `read` line 2, appended `ls` field), `apply_recency_boost` in `retrieve()`, UI columns/badge, admin editor, mock-LLM regex — all present and correct; no defects found, no fixes needed.
- `uv run pytest --cov=app --cov-report=term-missing` → **2299 passed, TOTAL 99%** (>90% ✓)
- `uv run pytest tests/e2e/test_document_dates.py -v --no-cov` → **6/6 passed** in isolation (DB up)
- 12 regression E2E suites (retrieval_quality, whole_document_context, agent_document_tools, ls_tree_drilldown, read_truncation_cap, kb_tree, kb_tree_nav, document_viewer, edit_summaries, import_documents, sync_button, hidden_folders_toggle, smoke) → **all green in isolation**
- `uv run ruff check .` → clean; `uv run pyright` → **0 errors, 0 warnings**

**Completion criteria:** 1) non-null `created_at` + 0020 upgrade/downgrade on dev DB ✓ (real-Alembic integration tests) 2) sync refresh/older/manual-persists/content-reset/no sources_meta bump ✓ 3) zip/tar mtime + future→today ✓ 4) LLM date surfaces + cross-check ✓ 5) UI Created/Updated/badge positions ✓ 6) admin editor set+revert round-trip ✓ 7) old-correct-beats-new-similar (defaults & boost-off) + near-tie + `BOR_RECENCY_BOOST=0` byte-identical ✓ 8) full gate ✓ 9) commit/phase-move — left to harness per instructions.

- **Notable:** recency default tuned 0.001 → **0.0007** (task 07 step 5 explicitly permits; measured margins recorded in `test_recency_boost.py` docstring).
- **Next pending phase:** none — `todo/` holds only this phase.
2026-09-13 19:28:05 -04:00

385 lines
18 KiB
Python

"""Unit: the admin summary-edit affordance in the viewer (phase 57,
task 02).
The browser behavior itself is E2E-gated by the phase-57 story suite;
like the other frontend-adjacent unit files (test_save_chat_ui.py
pattern), this module pins the JS/CSS markers the edit contract depends
on, so a silent regression is caught without a browser:
* the ``docAdminReady()`` gate — the module-cached /api/whoami promise
(header.js's ``fetchIsAdmin``, the SAME single request per page the
shared header makes — no second whoami call site in document.js);
non-admin / fetch failure → NO button, NO wiring (the public viewer
is byte-for-byte the phase-36 section: the bare ``section.append
(title, body)`` construction stays first, the admin affordance is a
post-render ``.then`` on the gate promise);
* the editor construction — the header row (``.doc-summary-head`` with
the bare h2 + a real ``type="button"`` Edit), the swap-in
``<textarea class="doc-summary-editor">`` prefilled via ``.value``
(never innerHTML — XSS contract), Save / Cancel buttons, and the
``role="status"`` ``aria-live="polite"`` live region;
* the exact PATCH call — ``/api/documents/summary`` with method PATCH
and the ``{source, path, summary}`` body (the pair from the doc
object — the same values the modal core carries);
* the outcomes — success re-renders the text node via ``textContent``
("Summary updated."), an empty save that clears removes the panel
("Summary cleared." — the renderer only draws it for non-empty
summaries), Cancel restores the text node, and a failure (non-ok OR
network) keeps the editor open with the user's text and shows
neutral retry copy (phase-55 convention); the double-click guard
releases in the ``finally`` — never stale;
* styles.css — the five new ``.doc-summary-*`` classes (plus the two
layout wrappers) on the house dark-tech palette (phase-08 tokens),
8rem-min editor, 24px+ edit target, the ``[hidden]`` override,
``:focus-visible`` via the global outline rule, no CDN.
"""
from __future__ import annotations
import re
from pathlib import Path
FRONTEND = Path(__file__).resolve().parents[2] / "frontend"
DOCUMENT_JS = FRONTEND / "assets" / "document.js"
STYLES_CSS = FRONTEND / "assets" / "styles.css"
def _js() -> str:
return DOCUMENT_JS.read_text(encoding="utf-8")
def _css() -> str:
return STYLES_CSS.read_text(encoding="utf-8")
def _fn(js: str, name: str) -> str:
"""The source of a (possibly async, possibly nested) function via
balanced-brace counting (works for top-level and the editor's
inner helpers alike)."""
for prefix in ("async function ", "function "):
start = js.find(f"{prefix}{name}(")
if start != -1:
depth = 0
for i in range(js.find("{", start), len(js)):
if js[i] == "{":
depth += 1
elif js[i] == "}":
depth -= 1
if depth == 0:
return js[start : i + 1]
raise AssertionError(f"unbalanced braces in {name}()")
raise AssertionError(f"{name}() must exist in document.js")
# ---------- the admin gate (D4: the viewer stays public) ----------
def test_doc_admin_ready_wraps_the_cached_whoami_promise() -> None:
"""docAdminReady() exists and resolves the module-cached whoami
promise (header.js's fetchIsAdmin — one request per page, shared
with initSharedHeader) to a strict boolean: a non-admin OR any
fetch failure resolves false (the anonymous viewer)."""
js = _js()
body = _fn(js, "docAdminReady")
assert "await fetchIsAdmin()" in body, (
"the gate must reuse the cached whoami promise (no new call site)"
)
assert "=== true" in body, "a strict boolean — only an authenticated admin"
assert "} catch {" in body and "return false" in body, (
"any failure resolves false — the anonymous viewer"
)
def test_no_second_whoami_call_site_in_document_js() -> None:
"""document.js never fetches /api/whoami itself: header.js's
fetchIsAdmin is the SINGLE whoami call site for the whole frontend
(the cached promise), so the gate adds no request of its own — and
no admin-only network call exists for anonymous visitors (the only
admin call, the PATCH, lives inside the wired editor)."""
js = _js()
assert 'fetch("/api/whoami")' not in js, (
"whoami must come from the header.js cached promise"
)
assert 'from "./header.js"' in js and "fetchIsAdmin" in js
def test_gate_runs_after_the_phase36_base_construction() -> None:
"""The .doc-summary section is built for EVERYONE exactly as phase
36 (the anonymous byte-for-byte shape): the base construction
(className, the bare h2 label, the .doc-summary-text node, the
append) precedes the gate call, and the admin wiring runs ONLY in
the gate's success branch (``if (admin) wireSummaryEdit(...)``).
The summary gate is searched AFTER the section mount — phase 106
(task 09) added the date editor's own ``docAdminReady()`` gate
earlier in renderDocument (one gate per admin affordance, both on
the same cached whoami promise)."""
js = _js()
base = js.find('section.className = "doc-summary"')
label = js.find('title.textContent = "Summary"')
text_node = js.find('body.className = "doc-summary-text"')
append = js.find("section.append(title, body)")
mount = js.find("contentEl.appendChild(section)")
gate = js.find("void docAdminReady().then(", mount)
wiring = js.find("if (admin) wireSummaryEdit(section, doc);")
assert 0 < base < label < text_node < append < mount < gate < wiring, (
"phase-36 base construction first; the admin affordance is a "
"post-render gate branch (anonymous DOM is never touched)"
)
assert "wireSummaryEdit(section, doc)" in js[gate:]
# ---------- the editor construction ----------
def test_edit_button_is_a_real_button_in_the_header_row() -> None:
"""The Edit affordance: a real ``type="button"`` with the visible
text "Edit" and the .doc-summary-edit class, added to a
.doc-summary-head row that keeps the bare h2 label (label left,
button right — the anonymous section keeps its bare h2)."""
body = _fn(_js(), "wireSummaryEdit")
assert 'editBtn.type = "button"' in body
assert 'editBtn.className = "doc-summary-edit"' in body
assert 'editBtn.textContent = "Edit"' in body
assert 'head.className = "doc-summary-head"' in body
assert "head.append(title, editBtn)" in body
# The header row REPLACES the bare h2 as the section's first child.
assert "section.replaceChildren(head, body)" in body
def test_editor_swap_builds_textarea_save_cancel_and_live_region() -> None:
"""Edit swaps the .doc-summary-text node for the inline editor:
a <textarea class="doc-summary-editor"> prefilled via ``.value``
(NEVER innerHTML — the XSS contract), Save / Cancel real
type=buttons, and a <p class="doc-summary-status" role="status"
aria-live="polite"> live region. The Edit button hides while the
editor is open (no re-open mid-edit) and the textarea gets focus.
The ENTIRE wiring is textContent/.value-only — no innerHTML
anywhere (summary text is user-storable)."""
js = _js()
body = _fn(js, "wireSummaryEdit")
assert 'editor.className = "doc-summary-editor"' in body
assert (
'editor.value = typeof doc.summary === "string" ? doc.summary : ""' in body
), "prefill via .value — value, not innerHTML"
assert 'saveBtn.type = "button"' in body
assert 'saveBtn.className = "doc-summary-save"' in body
assert 'saveBtn.textContent = "Save"' in body
assert 'cancelBtn.type = "button"' in body
assert 'cancelBtn.className = "doc-summary-cancel"' in body
assert 'cancelBtn.textContent = "Cancel"' in body
assert 'status.className = "doc-summary-status"' in body
assert 'status.setAttribute("role", "status")' in body
assert 'status.setAttribute("aria-live", "polite")' in body
# The swap: text node out, editor parts in, focus in.
assert "section.replaceChildren(head, editor, actions, status)" in body
assert "editor.focus()" in body
hide = body.find("editBtn.hidden = true")
swap = body.find("section.replaceChildren(head, editor, actions, status)")
focus = body.find("editor.focus()")
assert -1 < hide < swap < focus, "hide Edit → swap → focus the textarea"
# The bindings.
assert 'editBtn.addEventListener("click", openEditor)' in body
assert 'cancelBtn.addEventListener("click", () => closeEditor(""))' in body
# XSS contract: the whole affordance is textContent/.value only
# (comments stripped — the word may appear in a note, never in code).
code = re.sub(r"//.*?$|/\*.*?\*/", "", body, flags=re.S | re.M)
assert "innerHTML" not in code, "XSS contract: no innerHTML in the wiring"
# ---------- the PATCH round-trip ----------
def test_save_patches_the_exact_endpoint_with_the_doc_pair() -> None:
"""Save → PATCH /api/documents/summary (the phase-57 admin
endpoint) with the EXACT body shape {source, path, summary} — the
pair from the doc object (the same values the modal core carries),
JSON content type. This is the ONLY admin-only call in document.js
(exactly one call site, inside the wired editor — anonymous
visitors never have it)."""
js = _js()
assert js.count('fetch("/api/documents/summary"') == 1, (
"exactly one PATCH call site (inside wireSummaryEdit)"
)
body = _fn(js, "wireSummaryEdit")
fetch_i = body.find('fetch("/api/documents/summary"')
assert fetch_i != -1, "the PATCH must live in the wired editor"
assert 'method: "PATCH"' in body[fetch_i:]
assert '"Content-Type": "application/json"' in body[fetch_i:]
assert (
"JSON.stringify({ source: doc.source, path: doc.path, summary: value })"
in body
), "the exact body shape: {source, path, summary}"
def test_save_success_rerenders_text_node_and_announces() -> None:
"""A 200 update syncs the doc object (a later re-open prefills the
CURRENT summary), announces "Summary updated." through
closeEditor — which re-renders the text node via textContent ONLY
(XSS contract) from the doc object."""
body = _fn(_js(), "wireSummaryEdit")
ok_i = body.find("if (!res.ok)")
json_i = body.find("await res.json()")
null_i = body.find("if (data.summary === null)")
sync_i = body.find("doc.summary = data.summary")
announce_i = body.find('closeEditor("Summary updated.")')
assert -1 < ok_i < json_i < null_i < sync_i < announce_i, (
"non-ok checked first → JSON → clear branch → doc sync → announce"
)
close = _fn(body, "closeEditor")
assert "body.textContent = doc.summary" in close, (
"the display state re-renders the text node from the doc object"
)
def test_empty_save_clears_and_removes_the_panel() -> None:
"""An empty save that clears (response summary === null, D4) syncs
the doc object, announces "Summary cleared." in the live region,
and removes the panel a short beat LATER (setTimeout — the
confirmation stays readable before the panel leaves the DOM; the
renderer only draws it for non-empty summaries). Failures keep the
panel (their slices carry no removal)."""
body = _fn(_js(), "wireSummaryEdit")
null_i = body.find("if (data.summary === null)")
sync_i = body.find("doc.summary = null")
announce_i = body.find('status.textContent = "Summary cleared."')
remove_i = body.find("setTimeout(() => section.remove(), 2000)")
assert -1 < null_i < sync_i < announce_i < remove_i, (
"the null branch: sync → announce → delayed removal"
)
# Exactly two removals in the whole affordance, both tied to a
# CLEARED state (the clear branch + the closeEditor empty guard —
# no failure path removes the panel).
assert body.count("section.remove()") == 2
def test_cancel_restores_the_text_node() -> None:
"""Cancel restores the display state: the .doc-summary-text node
back in the section, re-rendered from the doc object (the CURRENT
stored summary — the node was never mutated, only swapped out),
the (empty) live region kept, and the Edit button un-hidden +
re-focused (focus returns to the opener). A summary that is GONE
(a clear landed while the editor was open — Cancel right after a
successful empty save) never renders an empty panel: the guard
drops the panel instead."""
body = _fn(_js(), "wireSummaryEdit")
close = _fn(body, "closeEditor")
assert 'status.textContent = message' in close
assert "editBtn.hidden = false" in close
guard = 'typeof doc.summary !== "string" || doc.summary.trim() === ""'
guard_i = close.find(guard)
remove_i = close.find("section.remove()")
sync_i = close.find("body.textContent = doc.summary")
restore_i = close.find("section.replaceChildren(head, body, status)")
focus_i = close.find("editBtn.focus()")
assert -1 < guard_i < remove_i < sync_i < restore_i < focus_i, (
"empty guard first; otherwise re-render → restore → focus"
)
assert 'cancelBtn.addEventListener("click", () => closeEditor(""))' in body
def test_failure_keeps_the_editor_with_neutral_copy() -> None:
"""A failed Save (non-ok HTTP OR network) keeps the editor open
with the user's text (no swap back, no panel removal) and shows
neutral retry copy (phase-55 convention — no sign-in wording):
"…try again." for a non-ok response, "…is the app reachable?" for
the network path."""
body = _fn(_js(), "wireSummaryEdit")
nonok = body.find("if (!res.ok)")
neutral = body.find("Couldn't update the summary — try again.")
assert -1 < nonok < neutral, "the non-ok branch lands on the neutral copy"
catch_i = body.find("} catch {")
reachable = body.find("Couldn't update the summary — is the app reachable?")
assert -1 < catch_i < reachable, "the network path carries the reachable? copy"
assert "signed in" not in body, "no sign-in wording (neutral retry copy)"
# The two FAILURE branches (the non-ok early return and the network
# catch) never restore or remove — the editor stays open with the
# user's text (only the clear branch removes the panel).
nonok_slice = body[nonok:body.find("await res.json()")]
assert "section.remove()" not in nonok_slice
assert "closeEditor" not in nonok_slice
catch_slice = body[catch_i:body.find("finally")]
assert "section.remove()" not in catch_slice
assert "closeEditor" not in catch_slice
def test_save_double_click_guard_releases_in_finally() -> None:
"""One PATCH at a time: Save disables itself BEFORE the fetch and
re-enables in the ``finally`` (every outcome — success, clear,
non-ok, network — never leaves a stale disabled button, PLAN §7.4)."""
body = _fn(_js(), "wireSummaryEdit")
disable_i = body.find("saveBtn.disabled = true")
fetch_i = body.find('fetch("/api/documents/summary"')
finally_i = body.find("finally")
enable_i = body.find("saveBtn.disabled = false")
assert -1 < disable_i < fetch_i < finally_i < enable_i, (
"disable before the fetch; re-enable in the finally"
)
assert body.count("saveBtn.disabled = true") == 1
assert body.count("saveBtn.disabled = false") == 1
# ---------- styles.css ----------
def test_new_summary_edit_classes_present() -> None:
"""styles.css carries the five task-named .doc-summary-* classes
(plus the two layout wrappers the wiring emits) — the house
dark-tech palette (phase-08 tokens), system fonts, no CDN."""
css = _css()
for cls in (
".doc-summary-edit",
".doc-summary-editor",
".doc-summary-save",
".doc-summary-cancel",
".doc-summary-status",
".doc-summary-head",
".doc-summary-actions",
):
assert f"{cls} " in css or f"{cls}." in css or f"{cls}[" in css, (
f"styles.css must style {cls}"
)
assert "url(http" not in css and "@import url(" not in css, (
"no CDN (AGENTS.md rule 6)"
)
def test_summary_edit_css_targets_and_palette() -> None:
"""The house AA palette on the edit affordance: the edit target is
24px+ with a 3px global-outline focus (the global :focus-visible
rule — no local override needed); the editor is a full-width block
textarea with the 8rem min-height; the save pill is the solid
brand family (--bg on --brand = 5.2:1, AA, borderless); the ghost
buttons ride the ink-soft 5.1:1-on-surface pair; the status line
is ink-soft (AA). The [hidden] override must beat the edit
button's display rule (the editor hides Edit while open)."""
css = _css()
edit = css[css.find(".doc-summary-edit {") :]
edit = edit[: edit.find("\n}")]
assert "min-height: 24px" in edit, "the 24px+ edit target (task 02)"
assert "var(--line)" in edit and "var(--ink-soft)" in edit
hidden = css.find(".doc-summary-edit[hidden]")
assert hidden != -1 and "display: none" in css[hidden : hidden + 80], (
"the hidden attr must beat the base display rule"
)
editor = css[css.find(".doc-summary-editor {") :]
editor = editor[: editor.find("\n}")]
for prop in (
"display: block",
"width: 100%",
"min-height: 8rem",
"resize: vertical",
"var(--bg)",
"var(--ink)",
):
assert prop in editor, f".doc-summary-editor must keep {prop}"
save = css[css.find(".doc-summary-save {") :]
save = save[: save.find("\n}")]
assert "background: var(--brand)" in save and "color: var(--bg)" in save
assert "border: 0" in save, "the solid brand pill family (Save/Share)"
status = css[css.find(".doc-summary-status {") :]
status = status[: status.find("\n}")]
assert "var(--ink-soft)" in status, "AA status copy on --surface (5.1:1)"
# :focus-visible via the GLOBAL 3px outline rule (no local
# suppression anywhere for these controls).
assert ":focus-visible {" in css
assert "outline: 3px solid var(--brand)" in css