Files
brain-of-reese/tests/unit/test_kb_tree_ui.py
T
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

1534 lines
76 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Unit: the RAG view's drill-down catalog tree (phase 97, task 04).
The RAG (Knowledge base) view lists the catalog the way the agent's
``ls`` sees it (the phase-94 concept, ONE end to end): sources at the
top, then per level the subfolders (recursive count + the STORED
description) and the level's files (the ``#docs-table`` — phase 106,
task 08, D8, added the Created column BEFORE Indexed, making it the
6-column contract). The view's catalog load is now
``loadTree()``: ONE fetch of ``GET /api/docs/tree`` (the full recursive
tree in a single payload — task 02), then client-side drill navigation
(no per-level fetch, no URL change).
This module pins the source-level contract (the house pattern of
``tests/unit/test_sync_button.py`` — read the frontend files as text,
no browser; the behavior is E2E-covered by the phase's dedicated
suite, task 06):
* the shell's RAG view ships the static tree skeleton (``#kb-crumb``,
``#kb-level`` + ``#kb-level-title``/``#kb-level-summary``,
``#folders-wrap`` + ``#folders-table`` — ONE table for every level,
the Folder | Documents | Updated | Description head — phase 106
(task 08, D8) added the Updated column (the subtree's MAX document
created_at, D9) BETWEEN Documents and Description) in order after
``#stat-cards`` and BEFORE the file table (now the 6-column
Source | Path | Title | Chunks | Created | Indexed head — phase 106
(task 08, D8) added the Created column BEFORE Indexed);
* the exact ``/api/docs/tree`` fetch (and the flat ``/api/docs`` fetch
is gone from the view module);
* the drill state (``current`` / ``kbTree``), the state transitions
(source row click, folder row click, breadcrumb up, top reset), and
the aria-current last segment;
* the textContent contract (no ``innerHTML`` with document-derived
data — the module's standing rule);
* the top-level semantics (the rows ARE the sources — the ``ls()``
equivalence — and the file table is hidden at the top);
* the level block's ls rule (hidden when no description is stored);
* the stat cards' whole-tree walk;
* the empty-state semantic (``#sources-empty`` ONLY on zero sources —
the deliberate phase-97 change: a registered 0-document source
renders its row instead);
* the never-stale reset-to-top fallback (PLAN §7.4 — a vanished
location resets ``current`` to the top BEFORE rendering);
* ``loadTree`` wired at exactly the three refresh points (view-refresh,
sync success, upload success) plus the boot load — and the
anonymous branch still never fetches the catalog.
Phase 97 (task 05) adds the FOLDER-DESCRIPTION EDITOR (the phase-57
affordance, mirrored) — the source pins this module gains for it:
* the static ``#kb-level-edit`` button in the level block (type=button,
the .kb-summary-edit class, the "Edit" label, after the description
``<p>`` — in the .kb-level-body the editor swaps inside);
* the Description cell of EVERY source/folder row (``makeDescCell``,
called from both row builders) — the stored description as a text
node + the ALWAYS-present Edit button (a description can be CREATED
where none is stored: no gate on the stored value);
* the shared editor (``wireDescriptionEdit``): the swap builds a
textarea prefilled via ``.value`` (never innerHTML — the XSS
contract), Save / Cancel, and the ``role="status"``
``aria-live="polite"`` live region;
* the exact PATCH — ``/api/folders/summary``, method PATCH, body
``{ source, folder_path, summary }`` with ``folder_path ""`` for the
source root (exactly one call site in the module);
* the outcomes — success updates the in-memory ``kbTree`` node IN
PLACE (no re-fetch) + re-renders the text via textContent + "
Description updated."; a cleared echo (summary null) empties the
text (row cell) / hides the level block (the phase-57 announcement
beat, guarded) + "Description cleared."; a failure (non-ok OR
network) keeps the editor open with neutral retry copy (phase-55);
Cancel restores the text node without a fetch;
* the double-click guard (disabled before the fetch, re-enabled in the
``finally``); the level editor's ``getTarget()`` getter + the
``reset()`` handle called on every re-render (PLAN §7.4);
* the five ``.kb-summary-*`` classes in styles.css (house palette, no
new hue in the phase-97 block).
Phase 98 (task 04) adds the "SUMMARY PENDING" markers — the pins this
module gains for them:
* the row Description cell's THREE text states (``makeDescCell``):
stored → the stored text (NEVER the marker); no stored +
``summary_pending`` → the ``kb-summary-pending`` class on the
existing text span + the exact ``Summary pending`` copy + the exact
D4 title (textContent/title only — the house rule); neither → the
empty cell (the ls rule, unchanged) — with the Edit button
UNCONDITIONAL in all three (a manual save creates the row);
* the in-place clear: the editor's success path sets
``node.summary_pending = false`` immediately after
``node.summary = data.summary`` (no re-fetch — the marker clears in
the surface where the edit happened);
* the level block's OR condition (a stored description OR
``summary_pending``) + the EXACT pending note — neither stored nor
pending stays hidden (the ls rule, unchanged) — and the level's
pending text takes the muted ``kb-summary-pending`` class on render
(the block is REUSED across levels — a stored level clears it);
* the editor's close re-derives the display state from the node (task
05 defect fix): the pending marker's muted class + D4 tooltip
CANNOT survive a save (the flag is cleared first — the in-place
clear, with the stale "next sync" tooltip gone), and a cancel
restores the surface's pending display (each surface passes its
pending copy via ``pendingText`` — the row's ``Summary pending``
marker, the level's D4 note — and its tooltip via ``pendingTitle``,
the row only);
* ``.kb-summary-pending`` in styles.css (the ink-soft muted AA pair —
text + color, never color alone; no font/white-space/italic
overrides, so the row height is unchanged; no new hue in the
phase-97 block).
"""
from __future__ import annotations
import re
from pathlib import Path
FRONTEND = Path(__file__).resolve().parents[2] / "frontend"
ASSETS = FRONTEND / "assets"
SOURCES_JS = ASSETS / "sources.js"
STYLES_CSS = ASSETS / "styles.css"
SHELL_HTML = FRONTEND / "index.html"
def _text(path: Path) -> str:
assert path.is_file(), f"missing frontend file: {path}"
return path.read_text(encoding="utf-8")
def _js() -> str:
return _text(SOURCES_JS)
def _rag_view(html: str) -> str:
"""The RAG view section of the shell (view-scoped scope — the shell
carries many views, so whole-file matches hit the wrong view)."""
i = html.find('<section class="view" id="view-rag"')
assert i != -1, "the RAG view section must be in the shell"
j = html.find('<section class="view" id="view-git-sources"', i)
assert j != -1, "the Sources view section must follow the RAG view"
return html[i:j]
def _css_block(css_text: str, start_marker: str, end_marker: str) -> str:
"""The CSS between two phase markers (comments included — the no-new
hue check wants to see that none was added in prose either)."""
i = css_text.find(start_marker)
assert i != -1, f"missing CSS marker: {start_marker!r}"
j = css_text.find(end_marker, i)
assert j != -1, f"missing CSS end marker: {end_marker!r}"
return css_text[i:j]
def _fn(js: str, name: str) -> str:
"""The source of a (possibly async, possibly nested) function via
balanced-brace counting (module-level and mount-scoped alike). The
brace count starts AFTER the parameter list — a destructured
parameter (wireDescriptionEdit's target object) may carry braces of
its own."""
for prefix in ("async function ", "function "):
start = js.find(f"{prefix}{name}(")
if start != -1:
# Skip the parameter list (balanced parens).
depth = 0
i = js.find("(", start)
close = i
while i < len(js):
if js[i] == "(":
depth += 1
elif js[i] == ")":
depth -= 1
if depth == 0:
close = i
break
i += 1
# Then brace-count the body.
brace = js.find("{", close)
depth = 0
for j in range(brace, len(js)):
if js[j] == "{":
depth += 1
elif js[j] == "}":
depth -= 1
if depth == 0:
return js[start : j + 1]
raise AssertionError(f"unbalanced braces in {name}()")
raise AssertionError(f"{name}() must exist in sources.js")
# ---------- the shell's RAG view: the static tree skeleton ----------
def test_rag_view_ships_the_tree_skeleton_in_order() -> None:
"""The house no-JS-safe skeleton convention: the tree surfaces ship
in the static HTML (after the stat cards, before the file table)
and ship HIDDEN — assets/sources.js fills them with createElement.
``#kb-crumb`` is the location nav; ``#kb-level`` is the level block
(title + description); ``#folders-wrap`` hosts the ONE
folders/sources table (``.table-wrap`` card, Folder | Documents |
Updated | Description — phase 106 (task 08, D8), visually-hidden
caption, the ``.docs-table`` language + ``.kb-folders-table``).
The file table's head is the 6-column contract (Source | Path |
Title | Chunks | Created | Indexed — phase 106 (task 08, D8))."""
view = _rag_view(_text(SHELL_HTML))
for fragment in (
'<nav id="kb-crumb" class="kb-crumb" aria-label="Catalog location" hidden></nav>',
'<section id="kb-level" class="kb-level" aria-labelledby="kb-level-title" hidden>',
'<h2 id="kb-level-title"></h2>',
'<p id="kb-level-summary"></p>',
'<div id="folders-wrap" class="table-wrap" role="region" '
'aria-label="Folders" tabindex="0" hidden>',
'<table class="docs-table kb-folders-table" id="folders-table">',
'<caption class="visually-hidden">Catalog sources and folders</caption>',
'<tbody id="folders-tbody"></tbody>',
):
assert fragment in view, f"the RAG view must ship {fragment!r}"
head = re.search(r"<thead>(.*?)</thead>", view[view.find('id="folders-table"'):], re.S)
assert head, "#folders-table must keep a static thead"
for column in (
"<th scope=\"col\">Folder</th>",
"<th scope=\"col\">Documents</th>",
"<th scope=\"col\">Updated</th>", # phase 106 (task 08, D8)
"<th scope=\"col\">Description</th>",
):
assert column in head.group(1), f"#folders-table head must carry {column!r}"
# The deliberate placement: after the stat cards, before the file
# table (the file table keeps its own wrap + head, unchanged).
assert (
view.find('id="stat-cards"')
< view.find('id="kb-crumb"')
< view.find('id="kb-level"')
< view.find('id="folders-wrap"')
< view.find('id="docs-table"')
), "the skeleton must sit between the stat cards and the file table"
# The file table's head (the 6-column contract — phase 106 (task
# 08, D8) added Created BEFORE Indexed; makeRow's home).
doc_head = re.search(
r'<table class="docs-table" id="docs-table">.*?<thead>(.*?)</thead>', view, re.S
)
assert doc_head, "the file table must keep its static thead"
for column in ("Source", "Path", "Title", "Chunks", "Created", "Indexed"):
assert f">{column}</th>" in doc_head.group(1), f"#docs-table head must keep {column!r}"
# ---------- the tree load: the ONE fetch + the race token ----------
def test_sources_js_fetches_the_tree_endpoint_only() -> None:
"""The catalog load is `loadTree()`: ONE fetch of the tree endpoint.
The flat `GET /api/docs` fetch is GONE from the view module (the
endpoint itself stays — the API surface is untouched, task 02)."""
js = _js()
assert 'fetch("/api/docs/tree")' in js, "the load must fetch /api/docs/tree"
assert js.count('fetch("/api/docs/tree")') == 1, (
"the tree is the view's SINGLE catalog fetch (zero per-level fetches)"
)
assert 'fetch("/api/docs")' not in js, "the flat /api/docs fetch must be gone"
def test_load_tree_is_race_tokened_and_resets_before_rendering() -> None:
"""Phase 79 carries over: the monotonic loadSeq token — only the
newest load may touch the DOM after its await. On success the load
stores the tree in the module-scoped `kbTree`, runs the never-stale
reset, and renders (in that order — the reset must see the NEW
tree and happen BEFORE the render)."""
js = _js()
assert "async function loadTree()" in js, "loadTree must exist"
body = js[js.find("async function loadTree()") :]
body = body[: body.find("\n }")]
assert "const my = ++loadSeq;" in body, "the race token stays (phase 79)"
assert "if (my !== loadSeq) return;" in body, "stale loads must not touch the DOM"
store_i = body.find("kbTree = tree && Array.isArray(tree.sources) ? tree : { sources: [] };")
reset_i = body.find("resetVanishedLocation();")
render_i = body.find("renderLevel();")
assert -1 < store_i < reset_i < render_i, (
"store the tree → reset a vanished location → render (in order)"
)
# ---------- the drill state + the navigation transitions ----------
def test_drill_state_and_module_tree_are_scoped_to_the_mount() -> None:
"""`current` ({ source, folder } — null/null = the top level,
folder "" = the source root) and `kbTree` are mount-scoped module
state (the router mounts a view ONCE — the state survives every
switch, as the sync state machine does)."""
js = _js()
assert "let kbTree = { sources: [] };" in js
assert "let current = { source: null, folder: null };" in js, (
"the drill state starts at the top level"
)
def test_source_row_click_drills_into_the_source_root() -> None:
"""Top-level row = a SOURCE (the ls() equivalence): the name cell
is a `.folder-link` whose click sets `{ source, "" }` (the source
ROOT) and re-renders — client-side only (no fetch, no URL change).
The row carries the recursive count and the stored (source, "")
description — textContent only."""
js = _js()
# Slice to the next function (the row builders grew with phase 106's
# Updated cell — a fixed window would drift out of the function).
body = js[js.find("function makeSourceRow(") : js.find("function makeFolderRow(")]
assert 'link.className = "folder-link"' in body, "the source row uses the row drill link"
assert "link.textContent = s.name" in body
assert "goTo({ source: s.name, folder: \"\" })" in body, (
"a source row drills to the source ROOT (folder \"\")"
)
assert "countTd.textContent = String(s.documents)" in body, "the recursive count"
assert 'makeDescCell(s, s.name, "", s.name)' in body, (
"the Description cell (task 05): the stored (source, \"\") "
"description + the ALWAYS-present Edit button — folder_path \"\" "
"for the source root"
)
assert "link.href = \"#\"" in body, "client-side drill — no URL change"
def test_folder_row_click_drills_into_the_folder() -> None:
"""A level's subfolder row: the folder's LAST path segment as the
label (the full source-relative path rides the title), the
recursive count, the stored description — and the click sets
`{ source, folder: f.path }` (the source-relative folder path)."""
js = _js()
start = js.find("function makeFolderRow(")
body = js[start : js.find("function renderLevel(")]
assert 'link.className = "folder-link"' in body
assert "link.textContent = f.path.split(\"/\").pop()" in body, (
"the label is the last segment (the breadcrumb language)"
)
assert "link.title = current.source + \"/\" + f.path" in body, "full path on hover"
assert "goTo({ source: current.source, folder: f.path })" in body, (
"a folder row drills to that folder"
)
assert "countTd.textContent = String(f.documents)" in body
assert (
'makeDescCell(f, current.source, f.path, current.source + "/" + f.path)'
in body
), "the Description cell (task 05): the stored row + the ALWAYS-present Edit"
def test_breadcrumb_links_go_up_and_the_top_reset() -> None:
"""The breadcrumb: hidden at the top level; when drilled in, one
link per ancestor — the TOP level (→ `{ null, null }` — the reset),
the source (→ `{ source, "" }`), then the folder chain (→ each
ancestor folder) — the LAST segment a span with
aria-current=\"page\" (not a link)."""
js = _js()
start = js.find("function renderCrumb(")
body = js[start : js.find("function crumbSegment(")]
assert "if (current.source === null) {" in body
assert "crumbEl.hidden = true" in body, "the breadcrumb is hidden at the top level"
assert "crumbSegment(\"Knowledge base\", { source: null, folder: null })" in body, (
"the top-level segment goes back to the sources list (the reset)"
)
assert "crumbSegment(current.source, { source: current.source, folder: \"\" })" in body, (
"the source segment goes up to the source root"
)
assert "crumbSegment(part, { source: current.source, folder: acc })" in body, (
"each folder-ancestor segment goes up to that folder"
)
assert "crumbCurrent(part)" in body, "the last segment is the current span"
seg = js[js.find("function crumbSegment(") : js.find("function crumbCurrent(")]
assert 'a.className = "kb-crumb-link"' in seg
assert "a.textContent = label" in seg, "segment labels are textContent"
cur = js[js.find("function crumbCurrent(") : js.find("function goTo(")]
assert 'span.className = "kb-crumb-current"' in cur
assert 'span.setAttribute("aria-current", "page")' in cur, (
"the last segment is aria-current (a span, never a link)"
)
assert "span.textContent = label" in cur
def test_drill_navigation_pushes_state_only_entries() -> None:
"""Phase 99 (task 02, D2): every user-initiated drill is a history
entry — ``goTo`` is the push variant of ``applyTarget``: when the
target DIFFERS from the current level (a re-click of the current
row/segment pushes NO duplicate entry), it FIRST records a
STATE-ONLY entry — ``history.pushState({ view: "rag", kb: target },
"")`` — the second arg is ``""`` and there is no third (the URL
stays the shell's pathname: no new route, the phase-76 deep-link
surface is untouched) — then sets ``current`` + re-renders. The
drill still never fetches (the tree is in memory) and the module
never ``replaceState``s."""
js = _js()
start = js.find("function applyTarget(")
assert start != -1, "applyTarget must exist (the two-step drill)"
body = js[start : js.find("function makeSourceRow(")]
gate_i = body.find(
"if (push && (target.source !== current.source || target.folder !== current.folder)) {"
)
push_i = body.find('history.pushState({ view: "rag", kb: target }, "")')
set_i = body.find("current = { source: target.source, folder: target.folder };")
render_i = body.find("renderLevel();")
assert -1 < gate_i < push_i < set_i < render_i, (
"push gate (target differs from current) → pushState → set current → render"
)
# STATE-ONLY: exactly ONE pushState call site — the "" second arg,
# no third (no URL change).
assert _code(body).count("pushState") == 1, (
"ONE pushState call site — the state-only drill entry"
)
assert "replaceState" not in _code(js), "no replaceState anywhere in the module"
# goTo is the push variant — every drill call site keeps calling it.
go = body[body.find("function goTo(") :]
assert "applyTarget(target, true)" in go, "goTo pushes (the user-initiated drill)"
assert "fetch(" not in body, "the drill never fetches (client-side only)"
# ---------- the level rendering ----------
def test_top_level_lists_sources_and_hides_the_file_table() -> None:
"""Top level: the breadcrumb + level block are hidden; the folders
table lists the SOURCES themselves (makeSourceRow for every source)
and the file table is ALWAYS hidden (files are seen per source —
`ls()` shows none at the top)."""
js = _js()
start = js.find("function renderLevel(")
body = js[start : js.find("function renderEmpty(")]
top = body[body.find("if (current.source === null) {") :]
assert "levelEl.hidden = true;" in top, "the level block is hidden at the top level"
assert "for (const s of kbTree.sources) foldersTbody.appendChild(makeSourceRow(s));" in top, (
"ONE table for every level: at the top the rows are the sources"
)
assert "foldersWrap.hidden = false;" in top
assert "if (tableWrap) tableWrap.hidden = true;" in top, (
"the file table is always hidden at the top level"
)
def test_level_block_shows_the_stored_description_or_the_pending_note() -> None:
"""Inside a source/folder: the level block shows the CURRENT
level's stored description — the title is the full source-relative
path (e.g. `alpha/two`; the source root shows the source name) —
or, since Phase 98 (task 04, D4), the PENDING note when the level
is summary_pending (no stored description yet — the next sync's
gap-fill will generate it, or the owner writes one via the block's
Edit button). HIDDEN only when NEITHER is stored nor pending (the
ls rule: count only, no placeholder — unchanged)."""
js = _js()
start = js.find("function renderLevel(")
body = js[start : js.find("function renderEmpty(")]
cond = "if (node.summary || node.summary_pending) {"
assert cond in body, "the block shows for a stored OR a pending level"
shown = body[body.find(cond) : body.find("} else {", body.find(cond))]
assert "levelTitleEl.textContent = current.folder" in shown
assert "current.source + \"/\" + current.folder" in shown, (
"the folder title is the full source-relative path"
)
assert ": current.source;" in shown, "the source root title is the source name"
assert "levelSummaryEl.textContent = node.summary ||" in shown, (
"the stored text first, the pending note as the fall-through"
)
assert (
"No description stored yet — the next sync will generate one. (You can write one yourself.)"
in shown
), "the EXACT D4 pending note"
assert (
'levelSummaryEl.className = node.summary ? "" : "kb-summary-pending";'
in shown
), (
"the pending text is the muted marker style — and a stored level CLEARS a "
"previous pending level's class (the block is reused, task-05 defect fix)"
)
assert "levelEl.hidden = false;" in shown
hidden = body[body.find("} else {", body.find(cond)) :]
assert "levelEl.hidden = true;" in hidden[:200], (
"neither stored nor pending → the block is hidden (the ls rule)"
)
def test_level_lists_subfolders_and_direct_files_only() -> None:
"""A level's folders table = its DIRECT subfolders (hidden when
none); its file table = its DIRECT files (hidden when none) —
makeRow UNCHANGED (the file node carries no source: the row object
restores the flat /api/docs shape makeRow reads, so the phase-10/26
pins on makeRow stay green)."""
js = _js()
start = js.find("function renderLevel(")
body = js[start : js.find("function renderEmpty(")]
assert 'children.filter((c) => c.kind === "folder")' in body
assert 'children.filter((c) => c.kind === "file")' in body
assert "for (const f of subfolders) foldersTbody.appendChild(makeFolderRow(f));" in body
assert "foldersWrap.hidden = subfolders.length === 0;" in body, (
"#folders-wrap hidden when the level has no subfolders"
)
assert "for (const f of files) {" in body
assert "source: current.source," in body, "the row object restores the source"
assert "makeRow({" in body, "makeRow is unchanged (the phase-10/26 pins)"
assert "if (tableWrap) tableWrap.hidden = files.length === 0;" in body, (
"the file table hides when the level has no direct files"
)
# makeRow itself is untouched — the standing pins on its body.
assert "link.href = documentUrl(d.source, d.path)" in js
assert "openDocumentModal(d.source, d.path, link)" in js
def test_render_clears_both_row_containers_before_filling() -> None:
"""Re-entrancy (the phase-77 pattern, extended to the second
container): the render clears BOTH tbodies at the top — a refresh
from a populated level into a sparser one leaves no ghost rows."""
js = _js()
start = js.find("function renderLevel(")
body = js[start : js.find("\n }", start)]
clear_folders = body.find("if (foldersTbody) foldersTbody.replaceChildren();")
clear_docs = body.find("if (tbody) tbody.replaceChildren();")
assert 0 <= clear_folders < clear_docs, "both containers clear at the top"
for append in ("foldersTbody.appendChild", "tbody.appendChild"):
assert clear_docs < body.find(append), f"{append} must come after the clears"
# ---------- the stat cards: the whole-tree walk ----------
def test_stat_cards_walk_the_whole_tree() -> None:
"""The KB-wide stat cards walk the WHOLE tree (every file node at
any depth): document count, chunks sum, max indexed_at (fmtDate
reuse) — the values identical to the former flat walk. A
0-document registered source contributes nothing (and still lists
its row — the ls invariant)."""
js = _js()
body = js[js.find("function treeStats()") : js.find("function renderCrumb(")]
assert "docs += 1;" in body
assert "totalChunks += child.chunks;" in body
assert "if (child.indexed_at > last) last = child.indexed_at;" in body
assert "walk(child);" in body, "the walk recurses into subfolders"
assert "for (const s of kbTree.sources) walk(s);" in body, "every source is walked"
render = js[js.find("function renderLevel(") : js.find("function renderEmpty(")]
assert "const st = treeStats();" in render
assert "statDocs.textContent = String(st.docs);" in render
assert "statChunks.textContent = String(st.totalChunks);" in render
assert 'st.last ? fmtDate(st.last) : "–"' in render, "the fmtDate reuse"
# ---------- the empty state: the zero-sources semantic ----------
def test_empty_state_is_zero_sources_only() -> None:
"""The deliberate phase-97 semantic change (module docstring):
#sources-empty shows ONLY when the tree has zero sources — a
registered 0-document source renders its row instead (the ls
invariant). A failed tree fetch renders the same no-data state
(the former loadDocs failure behavior, unchanged in kind)."""
js = _js()
render = js[js.find("function renderLevel(") : js.find("function renderEmpty(")]
assert "if (!kbTree.sources.length) {" in render, (
"the empty state fires on ZERO SOURCES (not zero documents)"
)
assert "renderEmpty();" in render[render.find("if (!kbTree.sources.length) {"):]
empty_start = js.find("function renderEmpty(")
empty = js[empty_start : js.find("\n }", empty_start)]
assert 'statDocs.textContent = "0";' in empty
assert 'statChunks.textContent = "0";' in empty
assert 'statLast.textContent = "–";' in empty
assert "if (foldersWrap) foldersWrap.hidden = true;" in empty, "both wraps hidden"
assert "if (tableWrap) tableWrap.hidden = true;" in empty
assert "emptyEl.hidden = false;" in empty, "#sources-empty shows"
# The failure paths of the load land in the same no-data state.
load = js[js.find("async function loadTree()") : js.find("async function loadTree()") + 900]
assert load.count("renderEmpty();") >= 2, "a failed fetch renders the no-data state"
def test_module_docstring_documents_the_semantic_change() -> None:
"""The house module-docstring convention: the phase-97 section
records the tree load, the drill state, and the deliberate
0-document-source semantic change."""
js = _js()
header = js[: js.find("import { fetchIsAdmin }")]
assert "Phase 97 (task 04)" in header
assert "zero SOURCES" in header, "the empty-state semantic change is documented"
assert "0-document source renders its row" in header, (
"the registered 0-document source exception is documented"
)
assert "loadTree()" in header
# ---------- the never-stale reset-to-top fallback ----------
def test_reset_to_top_when_the_location_vanished() -> None:
"""PLAN §7.4: after a re-fetch, if the current source is no longer
in the tree (unregistered/pruned) OR the current folder no longer
exists under it, `current` RESETS to the top level BEFORE rendering
— no stale breadcrumb, no stale block. The folder existence check
mirrors the phase-94 rule: some indexed path starts with
`folder + "/"` (the source root — \"\" — always exists)."""
js = _js()
body = js[js.find("function resetVanishedLocation()") : js.find("function folderExistsIn(")]
assert "if (current.source === null) return;" in body, "the top level never vanishes"
assert "kbTree.sources.find((s) => s.name === current.source)" in body
assert body.count("current = { source: null, folder: null };") == 2, (
"both vanishing arms reset to the top level"
)
exists = js[js.find("function folderExistsIn(") : js.find("function currentLevelNode(")]
assert 'if (folderPath === "") return true;' in exists, "the source root always exists"
assert "p.startsWith(folderPath + \"/\")" in exists, "the phase-94 existence rule"
# The reset runs on the success path of the load, before the render
# (pinned in the load-order test — here: it is defined and called).
assert "resetVanishedLocation();" in js[js.find("async function loadTree()"):]
# ---------- the refresh wirings: loadTree at the three points ----------
def test_load_tree_wired_at_exactly_the_three_refresh_points_plus_boot() -> None:
"""loadDocs's call sites become loadTree's (same points, same
semantics): the boot load, the bor:view-refresh listener (admin
branch), applySyncSuccess (the KB just changed), and the
upload-success branch of startSyncPolling — and the old name is
gone. Exactly 5 occurrences of `loadTree()`: the definition +
those 4 calls (no other call site exists)."""
js = _js()
assert "loadDocs" not in js, "the old load name is gone from the module"
assert _code(js).count("loadTree()") == 5, (
"the definition + the 4 call sites, no others (code, not comments)"
)
# 1. the view-refresh listener, armed in the admin branch — since
# Phase 99 (task 02, D2) its body is the ALIGNMENT step (a
# kb-carrying history.state entry keeps the drill — the
# active-link re-click; a kb-less entry starts at the top — a
# fresh nav visit) BEFORE the re-fetch.
listener = js.find('addEventListener("bor:view-refresh"')
assert listener != -1
gate = js.find("const admin = await fetchIsAdmin();")
assert -1 < gate < listener, "armed after the whoami gate (the phase-77 pin)"
listener_body = js[listener : js.find('window.addEventListener("popstate"')]
state_i = listener_body.find("history.state && history.state.kb")
adopt_i = listener_body.find("applyTarget(kb, false)")
reset_i = listener_body.find("applyTarget({ source: null, folder: null }, false)")
load_i = listener_body.find("loadTree();")
assert -1 < state_i < adopt_i < reset_i < load_i, (
"align with history.state (adopt kb / reset to top) BEFORE the re-fetch"
)
assert "pushState" not in _code(listener_body), ("the alignment adopts only — no push")
# 2. the boot load — the last statement of mount, right after the
# listeners are armed (the mount's own load is the first fetch).
pop = js.find('window.addEventListener("popstate"')
boot = js[js.find("});", pop) + 3 :] # past the popstate listener's close
assert boot.strip().startswith("loadTree();"), (
"the boot load follows the listeners (no pushState on boot)"
)
# 3. applySyncSuccess (the sync's terminal — the KB just changed).
success = js[js.find("function applySyncSuccess(") : js.find("function applySyncFailure(")]
assert "loadTree();" in success
# 4. the upload-success branch of the tick (settle + refresh).
tick = js[js.find("function startSyncPolling(") : js.find("function startSync()")]
up_ok = tick.find('uploadStatus && uploadStatus.state === "success"')
up_fail = tick.find('uploadStatus && uploadStatus.state === "failed"')
assert -1 < up_ok < up_fail
assert "loadTree();" in tick[up_ok:up_fail], "the upload success refreshes the tree"
def test_anonymous_branch_never_fetches_the_tree() -> None:
"""The phase-16 soft rule (unchanged in kind): the anonymous branch
gates the catalog in / out with NO catalog fetch — no /api/docs/
tree request at all, and the tree surfaces stay hidden (they ship
hidden and never fill)."""
js = _js()
start = js.find("if (!admin) {")
end = js.find("if (gateEl) gateEl.hidden = true;")
assert -1 < start < end, "the anonymous branch must exist"
branch = js[start:end]
assert "fetch(" not in branch, "anonymous never fetches the catalog"
assert "loadTree" not in branch, "anonymous never starts a tree load"
assert "if (foldersWrap) foldersWrap.hidden = true;" in branch, (
"the folders wrap is part of the anonymous hide"
)
assert "if (tableWrap) tableWrap.hidden = true;" in branch
assert "if (gateEl) gateEl.hidden = false;" in branch
# ---------- the textContent contract ----------
def _code(js: str) -> str:
"""sources.js with comments stripped (a comment may legally carry
any string — the count pins below must only see code)."""
no_block = re.sub(r"/\*.*?\*/", "", js, flags=re.S)
return re.sub(r"//.*", "", no_block)
def test_tree_cells_never_use_innerhtml_with_derived_data() -> None:
"""The module's standing rule: document-derived text goes through
textContent only. The ONE innerHTML left in the module CODE is the
static (non-derived) sync-error modal skeleton — unchanged since
phase 41."""
js = _js()
code = _code(js)
assert code.count("innerHTML") == 1, (
"only the static sync-modal skeleton may use innerHTML (code, not comments)"
)
modal = code.find("function createSyncModal()")
assert modal != -1
assert code.find("innerHTML") > modal, "the single innerHTML is the sync-modal creation"
# The tree's derived-text sinks are all textContent (spot the
# contract across the builders + the level block).
for sink in (
"link.textContent = s.name",
"link.textContent = f.path.split(\"/\").pop()",
"text.textContent = node.summary",
'text.textContent = "Summary pending"',
"levelSummaryEl.textContent = node.summary ||",
"a.textContent = label",
"span.textContent = label",
"countTd.textContent = String(s.documents)",
"countTd.textContent = String(f.documents)",
):
assert sink in js, f"the derived-text contract: {sink!r}"
# ---------- the file-table wrap lookup (the new class collision) ----------
def test_file_wrap_lookup_avoids_the_new_folders_wrap() -> None:
"""#folders-wrap ALSO carries the shared .table-wrap class (the
card language) — the FILE table's wrap is looked up through its
own table, not the class (a class lookup would hit #folders-wrap
first in document order and hide/show the wrong card)."""
js = _js()
assert 'root.querySelector("#docs-table").parentElement' in js
assert 'root.querySelector(".table-wrap")' not in js, (
"the bare class lookup must not survive (it would hit #folders-wrap)"
)
# ---------- the styles: the tree's classes, no new hue ----------
def test_styles_carry_the_tree_classes_with_no_new_hue() -> None:
"""The new classes exist (.kb-crumb + link/current/separator,
.kb-level, .kb-folders-table, .folder-link) and the phase-97 block
introduces NO new hue (the phase-92 monochrome invariant — every
color via a var(); no hex/rgb literal)."""
css = _text(STYLES_CSS)
for selector in (
".kb-crumb",
".kb-crumb-link",
".kb-crumb-current",
".kb-crumb-sep",
".kb-level",
".kb-folders-table",
".folder-link",
):
assert re.search(r"(?<![\w-])" + re.escape(selector) + r"\s*\{", css), (
f"styles.css must define {selector}"
)
block = _css_block(css, "KB drill-down tree (phase 97", "Git sources page (phase 35)")
assert not re.search(r"#[0-9a-fA-F]{3,8}\b", block), "no new hue (phase-92 invariant)"
assert not re.search(r"rgba?\(", block), "no new hue (phase-92 invariant)"
# The drill link keeps the AA brand-ink pair + the hover/focus
# affordance (text + color, never color alone).
link = block[block.find(".folder-link {") : block.find(".folder-link:hover")]
assert "var(--brand-ink)" in link
assert ".folder-link:hover, .folder-link:focus-visible" in block, (
"the drill affordance covers hover AND focus"
)
# ---------- the folder-description editor (phase 97, task 05) ----------
#
# The phase-57 edit affordance, mirrored: Edit → inline textarea
# (prefilled) → Save/Cancel → live-region status, wired to
# PATCH /api/folders/summary (task 03). The browser behavior is
# E2E-covered by the phase's dedicated suite (task 06); this module
# pins the source-level contract (the house pattern of
# tests/unit/test_summary_edit_ui.py for the phase-57 analog).
def test_level_block_ships_the_static_edit_button() -> None:
"""The level block ships the phase-57 Edit button in the static
HTML: a real type=button with the .kb-summary-edit class and the
"Edit" label, INSIDE #kb-level after p#kb-level-summary — in the
.kb-level-body (the editor swaps inside it, the <h2> stays put)."""
view = _rag_view(_text(SHELL_HTML))
assert '<div class="kb-level-body">' in view, (
"the level's description UI ships in the .kb-level-body"
)
assert (
'<button type="button" class="kb-summary-edit" id="kb-level-edit">Edit</button>'
in view
), "the static level Edit button (phase-57 label, real button)"
body = view[view.find('id="kb-level"') : view.find('id="folders-wrap"')]
assert (
body.find('<h2 id="kb-level-title"></h2>')
< body.find('<p id="kb-level-summary"></p>')
< body.find('id="kb-level-edit"')
), "title → description → Edit button, in order"
def test_row_description_cell_builds_text_and_always_present_edit() -> None:
"""makeDescCell (the shared Description-cell builder, called from
BOTH row builders): the description text as a text node
(textContent — never innerHTML) + the Edit button built
UNCONDITIONALLY — a description can be CREATED where none is
stored (a < 2-document folder, the generator's fail-soft miss), so
the button sits OUTSIDE the three-state text branch (Phase 98:
the text gates on the node state, the button never does). The
button is a real type=button with a human aria-label, and the
shared editor is wired with a CONSTANT target { node, source,
folder }. Phase 99 (task 01, D1): text + button live in ONE
`div.kb-desc-cell` flex wrapper (the <td> holds only the wrapper),
and the editor's container IS the wrapper (the open/close swaps
fill it — the cell layout survives the editor swap)."""
body = _fn(_js(), "makeDescCell")
# The Edit button is built unconditionally — the whole button
# block (from its creation to the append) carries no `if` gate on
# the stored description or the pending flag.
btn_block = body[
body.find("const btn = document.createElement") : body.find("wrap.append(text, btn)")
]
assert "if (" not in btn_block, (
"always present: no gate on the stored description"
)
assert 'btn.type = "button"' in body
assert 'btn.className = "kb-summary-edit"' in body
assert 'btn.textContent = "Edit"' in body
assert 'btn.setAttribute("aria-label", `Edit description: ${label}`)' in body
assert 'wrap.className = "kb-desc-cell"' in body, "the ONE flex wrapper"
assert "wrap.append(text, btn)" in body, "text + button, in order, in the wrapper"
assert "td.append(wrap)" in body, "the <td> holds the wrapper (no new td class)"
assert "container: wrap" in body, (
"the editor's container is the WRAPPER (Phase 99 — the swap fills it)"
)
assert "getTarget: () => ({ node, source, folder })" in body, (
"a row's target is a constant (its own node)"
)
# The level block's editor is UNTOUCHED — its container is still
# .kb-level-body (the D1 escape hatch keeps its full text).
js = _js()
wire = js[js.find("levelEditor = wireDescriptionEdit({") : js.find("/* ---------- view boot")]
assert "container: levelBody" in wire
def test_desc_cell_pending_marker_branch() -> None:
"""Phase 98 (task 04, D4): makeDescCell's THREE text states, in
order — (1) a stored summary → the stored text, NEVER the
marker; (2) no stored summary AND node.summary_pending → the
marker: the kb-summary-pending class on the EXISTING text span +
the exact "Summary pending" copy + the exact D4 title
(textContent/title only — the house rule, no innerHTML); (3)
neither stored nor pending → the empty cell (the ls rule,
unchanged)."""
body = _fn(_js(), "makeDescCell")
stored_i = body.find("if (node && node.summary) {")
pending_i = body.find("else if (node && node.summary_pending) {")
empty_i = body.find("} else {")
assert -1 < stored_i < pending_i < empty_i, ("stored → pending → empty, in order")
stored = body[stored_i:pending_i]
assert "text.textContent = node.summary;" in stored, "stored → the stored text"
assert "kb-summary-pending" not in stored, "a stored summary is NEVER the marker"
assert "Summary pending" not in stored
pending = body[pending_i:empty_i]
assert 'text.classList.add("kb-summary-pending")' in pending, (
"Phase 99: the marker TOGGLES onto the .kb-desc-text base class"
)
assert 'text.className = "kb-summary-pending"' not in body, (
"a bare className re-assignment would drop the clamp's base class"
)
assert 'text.textContent = "Summary pending"' in pending, "the D4 marker copy"
assert (
'text.title = "No stored description yet — the next sync will generate one."'
in pending
), "the D4 title"
empty = body[empty_i : empty_i + 160]
assert 'text.textContent = ""' in empty, "neither stored nor pending → the empty cell"
# The marker is textContent/title only — the house rule (no
# innerHTML anywhere in the cell builder).
code = re.sub(r"//.*?$|/\*.*?\*/", "", body, flags=re.S | re.M)
assert "innerHTML" not in code
def test_save_success_clears_the_pending_flag_in_place() -> None:
"""Phase 98 (task 04, D4): a successful save syncs the in-memory
node IN PLACE — node.summary = data.summary AND, immediately
after, node.summary_pending = false (a created/updated description
is no longer pending: the marker clears in the surface where the
edit happened, NO re-fetch). The flag clear sits between the
summary sync and the announcement, and nothing after it re-fetches
the tree (the re-fetch stays the safety net, not the in-place
clear)."""
body = _fn(_js(), "wireDescriptionEdit")
save = body[body.find("async function saveDescription()") :]
sync_i = save.find("node.summary = data.summary")
clear_i = save.find("node.summary_pending = false")
announce_i = save.find(
'closeEditor(data.summary === null ? "Description cleared." : "Description updated.")'
)
assert -1 < sync_i < clear_i < announce_i, (
"summary sync → pending-flag clear → announce (in place)"
)
tail = save[clear_i:]
assert "loadTree()" not in tail and 'fetch("' not in tail, (
"the in-place clear is the whole of it — no re-fetch"
)
def test_close_editor_rederives_the_pending_display() -> None:
"""Phase 98 (task 05 defect fix, D4): closeEditor re-renders the
display state from the node with the SAME three-state rule the
surfaces use — the pending marker's muted class + D4 tooltip
CANNOT survive a save (the success path cleared
``node.summary_pending`` first, so a stale "next sync" tooltip
under a just-created description is impossible), and a cancel
RESTORES the surface's pending display. Each surface passes its
pending copy via ``pendingText`` (the row's ``Summary pending``
marker, the level's D4 note) and its tooltip via ``pendingTitle``
(the row only — D4's title is the row cell's, the level's <p>
carries none). textContent/class/title only — the house rule."""
body = _fn(_js(), "wireDescriptionEdit")
close = body[body.find("function closeEditor(") : body.find("function openEditor()")]
flag_i = close.find('const pending = node !== null && stored === "" && node.summary_pending;')
value_i = close.find("const value = pending && pendingText ? pendingText : stored;")
class_i = close.find('textEl.classList.toggle("kb-summary-pending", pending);')
title_i = close.find("if (pendingTitle) textEl.title = pendingTitle;")
hover_i = close.find('} else if (stored && textEl.classList.contains("kb-desc-text")) {')
full_i = close.find("textEl.title = stored;")
clear_title_i = close.find('textEl.removeAttribute("title")')
render_i = close.find("textEl.textContent = value")
assert -1 < flag_i < value_i < class_i < title_i < hover_i < full_i, (
"flag → value → class toggle → title (pending / hover-full), in order"
)
assert full_i < clear_title_i < render_i, "title-set → title-clear → text, in order"
# Phase 99 (task 01): the class line is a TOGGLE (the base classes
# survive — the row's span keeps .kb-desc-text, the clamp; the
# level's <p> keeps none), and the CLAMPED row span's title is
# re-derived from the stored text (the D1 hover escape hatch — a
# stale pre-edit title cannot survive a save; the unclamped
# level's <p> is gated out by the kb-desc-text check).
assert 'textEl.className' not in close, "no bare className re-assignment"
# The row passes its marker copy + the EXACT D4 tooltip…
cell = _fn(_js(), "makeDescCell")
assert 'pendingText: "Summary pending"' in cell
assert (
'pendingTitle: "No stored description yet — the next sync will generate one."'
in cell
), "the D4 tooltip is the row's"
# …and the level passes the D4 note WITHOUT a tooltip.
js = _js()
wire = js[js.find("levelEditor = wireDescriptionEdit({") : js.find("/* ---------- view boot")]
assert 'pendingText:' in wire and "pendingTitle" not in wire, (
"the level's <p> carries the note, never a tooltip (D4)"
)
assert (
"No description stored yet — the next sync will generate one. (You can write one yourself.)"
in wire
), "the level's pending copy is the D4 note"
def test_editor_swap_builds_textarea_save_cancel_and_live_region() -> None:
"""Edit swaps the description UI for the inline editor: a
<textarea class="kb-summary-editor"> prefilled via .value (NEVER
innerHTML — the XSS contract), the Save / Cancel real type=buttons,
and the <p class="kb-summary-status" role="status"
aria-live="polite"> live region (the shared parts are built once).
The Edit button hides while the editor is open; hide → swap →
focus the textarea, in order."""
body = _fn(_js(), "wireDescriptionEdit")
open_body = body[
body.find("function openEditor()") : body.find("async function saveDescription()")
]
assert 'editor.className = "kb-summary-editor"' in open_body
assert (
'editor.value = typeof target.node.summary === "string" ? target.node.summary : ""'
in open_body
), "prefill via .value — value, not innerHTML"
assert "container.replaceChildren(editor, actions, status)" in open_body
hide = open_body.find("editBtn.hidden = true")
swap = open_body.find("container.replaceChildren(editor, actions, status)")
focus = open_body.find("editor.focus()")
assert -1 < hide < swap < focus, "hide Edit → swap → focus the textarea"
# The shared parts (built once at wiring).
assert 'actions.className = "kb-summary-actions"' in body
assert 'mkBtn("kb-summary-save", "Save")' in body
assert 'mkBtn("kb-summary-cancel", "Cancel")' in body
assert 'status.className = "kb-summary-status"' in body
assert 'status.setAttribute("role", "status")' in body
assert 'status.setAttribute("aria-live", "polite")' in body
# The bindings.
assert 'editBtn.addEventListener("click", openEditor)' in body
assert 'cancelBtn.addEventListener("click", () => closeEditor(""))' in body
# The textarea is rebuilt on every open (fresh prefill each time).
assert "editor = document.createElement(\"textarea\")" in open_body
assert "let editor = null;" in body
# XSS contract: the whole affordance is textContent/.value only.
code = re.sub(r"//.*?$|/\*.*?\*/", "", body, flags=re.S | re.M)
assert "innerHTML" not in code, "XSS contract: no innerHTML in the wiring"
def test_save_patches_the_exact_endpoint_with_the_folder_body() -> None:
"""Save → PATCH /api/folders/summary (the task-03 endpoint) with
the EXACT body { source, folder_path, summary } — folder_path ""
for the source root (makeSourceRow passes ""), the
source-relative folder path otherwise (f.path), JSON content
type. Exactly ONE call site in the module (the shared editor)."""
js = _js()
assert js.count('fetch("/api/folders/summary"') == 1, (
"exactly one PATCH call site (the shared editor)"
)
body = _fn(js, "wireDescriptionEdit")
save = body[body.find("async function saveDescription()") :]
assert 'method: "PATCH"' in save
assert '"Content-Type": "application/json"' in save
assert "JSON.stringify({ source, folder_path: folder, summary: value })" in save, (
"the exact body shape: { source, folder_path, summary }"
)
# folder_path "" for the source root; the source-relative path for
# folder rows.
src_row = _fn(js, "makeSourceRow")
assert 'makeDescCell(s, s.name, "", s.name)' in src_row, "root: folder_path \"\""
f_row = _fn(js, "makeFolderRow")
assert (
"makeDescCell(f, current.source, f.path, current.source + \"/\" + f.path)" in f_row
), "folder rows: the source-relative folder path"
def test_save_success_updates_kbtree_in_place_and_announces() -> None:
"""A 200 syncs the in-memory kbTree node IN PLACE (node.summary =
data.summary — no re-fetch: the tree state stays coherent, the
re-fetch is the safety net), then re-renders the text node via
textContent only, with the EXACT status copies: "Description
updated." for a saved text, "Description cleared." for the null
(cleared) echo the server returns."""
body = _fn(_js(), "wireDescriptionEdit")
save = body[body.find("async function saveDescription()") :]
ok_i = save.find("if (!res.ok)")
json_i = save.find("await res.json()")
sync_i = save.find("node.summary = data.summary")
announce_i = save.find(
'closeEditor(data.summary === null ? "Description cleared." : "Description updated.")'
)
assert -1 < ok_i < json_i < sync_i < announce_i, (
"non-ok checked first → JSON → in-place node sync → announce"
)
close = body[body.find("function closeEditor(") : body.find("function openEditor()")]
assert "textEl.textContent = value" in close, (
"the display state re-renders the text node (textContent only)"
)
assert 'status.textContent = message' in close
def test_empty_save_clears_hides_the_level_or_empties_the_cell() -> None:
"""An empty save (the server echoes summary null) empties the text
(row cell) and, for the level block, hides the WHOLE block (the ls
rule) via onCleared — with the phase-57 announcement beat first
(setTimeout) so the role=status "Description cleared." is still
readable, GUARDED so a navigate-away first (or a re-create within
the beat) can't get the block yanked out from under a newer level.
A row cell passes NO onCleared: it just goes empty, its
always-present Edit button stays (a description can be
re-created)."""
js = _js()
body = _fn(js, "wireDescriptionEdit")
close = body[body.find("function closeEditor(") : body.find("function openEditor()")]
clear_i = close.find('if (value.trim() === "" && onCleared) onCleared();')
focus_i = close.find("editBtn.focus()")
assert -1 < clear_i < focus_i, "cleared → onCleared; otherwise focus the opener"
# The level wiring passes the guarded delayed hide.
wire = js[
js.find("levelEditor = wireDescriptionEdit({") : js.find("/* ---------- view boot")
]
assert "onCleared" in wire
assert "setTimeout" in wire, "the phase-57 announcement beat before the hide"
assert "levelEl.hidden = true" in wire
guard = wire[wire.find("setTimeout") :]
assert "currentLevelNode()" in guard and "!n.summary" in guard, (
"the hide is guarded: only if the current node still has none"
)
# The row cells pass NO onCleared (makeDescCell's call omits it).
cell = _fn(js, "makeDescCell")
assert "onCleared" not in cell, "a row cell never hides — the button stays"
def test_cancel_restores_the_text_node() -> None:
"""Cancel → closeEditor(""): the display state restored (the text
element re-rendered from the node — the stored text was NEVER
mutated: only the editor held the draft), the (empty) live region
kept, the Edit button un-hidden + re-focused. Cancel never
fetches (no PATCH was sent)."""
body = _fn(_js(), "wireDescriptionEdit")
assert 'cancelBtn.addEventListener("click", () => closeEditor(""))' in body
close = body[body.find("function closeEditor(") : body.find("function openEditor()")]
render_i = close.find("textEl.textContent = value")
unhide_i = close.find("editBtn.hidden = false")
restore_i = close.find("container.replaceChildren(textEl, editBtn, status)")
assert -1 < render_i < unhide_i < restore_i, (
"re-render → unhide → restore (focus follows for a non-clear)"
)
assert "fetch(" not in close, "cancel never fetches"
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 node mutation) 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 (the phase-55 pin style: the failure slices carry
neither a close nor a stored-text touch)."""
body = _fn(_js(), "wireDescriptionEdit")
save = body[body.find("async function saveDescription()") :]
nonok = save.find("if (!res.ok)")
neutral = save.find("Couldn't save the description — try again.")
assert -1 < nonok < neutral, "the non-ok branch lands on the neutral copy"
catch_i = save.find("} catch {")
reachable = save.find("Couldn't save the description — 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 never close the editor or touch the node
# — the editor stays open with the user's text.
nonok_slice = save[nonok : save.find("await res.json()")]
assert "closeEditor" not in nonok_slice
assert "node.summary" not in nonok_slice
catch_slice = save[catch_i : save.find("finally")]
assert "closeEditor" not in catch_slice
assert "node.summary" 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). The reset() teardown re-enables too (a torn-down editor's
Save must be usable again after the row is rebuilt… the button
ships disabled-ready for the next open)."""
body = _fn(_js(), "wireDescriptionEdit")
save = body[body.find("async function saveDescription()") :]
disable_i = save.find("saveBtn.disabled = true")
fetch_i = save.find('fetch("/api/folders/summary"')
finally_i = save.find("finally")
enable_i = save.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") == 2, (
"the save finally + the reset() teardown"
)
def test_level_editor_uses_a_target_getter_and_resets_on_every_rerender() -> None:
"""The level block is PERSISTENT (reused across levels as you
drill) — its target is a GETTER reading `current` +
currentLevelNode() at open/save time (a row's is a constant),
with the PATCH pair from the drill state (folder "" at the source
root). The handle's reset() is called on EVERY re-render
(renderLevel + renderEmpty) so a navigate-away / refresh never
leaves a stale open editor (PLAN §7.4); reset tears down only an
OPEN editor, with no message, and the display state it restores
is text + button (a stale status line must not survive)."""
js = _js()
wire = js[
js.find("levelEditor = wireDescriptionEdit({") : js.find("/* ---------- view boot")
]
assert "getTarget: () => {" in wire, "the level's target is a getter"
assert "currentLevelNode()" in wire
assert "{ node, source: current.source, folder: current.folder }" in wire, (
"the PATCH pair from the drill state (folder \"\" at the source root)"
)
# reset() runs on both re-render paths.
rl = js.find("function renderLevel(")
render = js[rl : js.find("\n }", rl)]
assert "if (levelEditor) levelEditor.reset();" in render
re_ = js.find("function renderEmpty(")
empty = js[re_ : js.find("\n }", re_)]
assert "if (levelEditor) levelEditor.reset();" in empty
# The handle: reset is a no-op for a closed editor, and the display
# state is text + button (the status line is dropped).
body = _fn(js, "wireDescriptionEdit")
reset = body[body.find("reset() {") :]
assert "if (!isOpen) return;" in reset
assert "container.replaceChildren(textEl, editBtn);" in reset, (
"the display state is text + button (the status line is dropped)"
)
def test_editor_wiring_never_uses_innerhtml() -> None:
"""The whole task-05 wiring (mkBtn, wireDescriptionEdit,
makeDescCell) is createElement + textContent/.value only — the
module's single innerHTML stays the static sync-modal skeleton
(the phase-41 shape, unchanged)."""
js = _js()
code = _code(js)
assert code.count("innerHTML") == 1, (
"only the static sync-modal skeleton may use innerHTML (code, not comments)"
)
for name in ("mkBtn", "wireDescriptionEdit", "makeDescCell"):
fn_code = re.sub(r"//.*?$|/\*.*?\*/", "", _fn(js, name), flags=re.S | re.M)
assert "innerHTML" not in fn_code, f"{name} must be textContent/.value only"
def test_module_docstring_documents_the_edit_section() -> None:
"""The house per-phase module-note convention: the phase-97
task-05 section records the shared editor, the PATCH body
(folder_path "" for the root), the exact status copies, the
in-place kbTree update, and the reset contract."""
js = _js()
header = js[: js.find("import { fetchIsAdmin }")]
assert "Phase 97 (task 05)" in header
assert "wireDescriptionEdit()" in header
assert "PATCH /api/folders/summary" in header
assert '"Description updated."' in header
assert '"Description cleared."' in header
assert "IN PLACE" in header
def test_styles_carry_the_folder_editor_classes() -> None:
"""styles.css carries the five task-named .kb-summary-* classes on
the house dark-tech palette: the 24px+ edit target with the
[hidden] override, the 8rem-min editor (the phase-57 spec), the
solid brand Save pill (--bg on --brand, borderless), the ink-soft
Cancel + status (AA), :focus-visible via the global rule, and NO
new hue in the phase-97 block (the phase-92 invariant — the
task-05 block sits inside it)."""
css = _text(STYLES_CSS)
for cls in (
".kb-summary-edit",
".kb-summary-editor",
".kb-summary-save",
".kb-summary-cancel",
".kb-summary-status",
):
assert re.search(r"(?<![\w-])" + re.escape(cls) + r"\s*\{", css), (
f"styles.css must define {cls}"
)
block = _css_block(css, "KB drill-down tree (phase 97", "Git sources page (phase 35)")
assert "kb-summary-editor" in block, "the task-05 block lives in the phase-97 region"
assert not re.search(r"#[0-9a-fA-F]{3,8}\b", block), "no new hue (phase-92 invariant)"
assert not re.search(r"rgba?\(", block), "no new hue (phase-92 invariant)"
edit = css[css.find(".kb-summary-edit {") :]
edit = edit[: edit.find("\n}")]
assert "min-height: 24px" in edit, "the 24px+ edit target"
assert "var(--line)" in edit and "var(--ink-soft)" in edit
hidden = css.find(".kb-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(".kb-summary-editor {") :]
editor = editor[: editor.find("\n}")]
for prop in ("display: block", "width: 100%", "min-height: 8rem", "resize: vertical"):
assert prop in editor, f".kb-summary-editor must keep {prop}"
save = css[css.find(".kb-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(".kb-summary-status {") :]
status = status[: status.find("\n}")]
assert "var(--ink-soft)" in status, "AA status copy (5.1:1)"
# :focus-visible via the GLOBAL 3px outline rule (no local
# suppression for these controls).
assert ":focus-visible {" in css
assert "outline: 3px solid var(--brand)" in css
# ---------- the "Summary pending" markers (phase 98, task 04) ----------
def test_styles_carry_the_pending_marker_class() -> None:
"""Phase 98 (task 04, D4): .kb-summary-pending mutes the row-cell
marker via the ink-soft token (5.1:1 on --surface — AA; text +
color, never color alone — B5). The class sits on the EXISTING
description text span, so the row cell's font-size/line-height
apply — no font/white-space/italic overrides (the marker must not
change row height), and the phase-97 block carries no new hue
(the phase-92 monochrome invariant)."""
css = _text(STYLES_CSS)
m = re.search(r"(?<![\w-])\.kb-summary-pending\s*\{([^}]*)\}", css)
assert m, "styles.css must define .kb-summary-pending"
rule = m.group(1)
assert "color: var(--ink-soft)" in rule, "the muted AA pair (5.1:1 on --surface)"
for prop in ("font-style", "font-size", "line-height", "white-space"):
assert prop not in rule, f"the marker keeps the row's {prop} (row height unchanged)"
block = _css_block(css, "KB drill-down tree (phase 97", "Git sources page (phase 35)")
assert ".kb-summary-pending" in block, "the marker class lives in the phase-97 region"
assert not re.search(r"#[0-9a-fA-F]{3,8}\b", block), "no new hue (phase-92 invariant)"
assert not re.search(r"rgba?\(", block), "no new hue (phase-92 invariant)"
def test_module_docstring_documents_the_pending_markers() -> None:
"""The house per-phase module-note convention: the phase-98
task-04 section records the marker's surfaces — the row cell's
D4 copy + title, the level block's pending note — and the
in-place clear on the editor's success path."""
js = _js()
header = js[: js.find("import { fetchIsAdmin }")]
assert "Phase 98 (task 04)" in header
assert '"Summary pending"' in header, "the D4 marker copy"
assert "No stored description yet — the next sync will generate one." in header, (
"the D4 row-cell title"
)
assert (
"No description stored yet — the next sync will generate one. (You can write one yourself.)"
in header
), "the level block's pending note"
assert "node.summary_pending = false" in header, "the in-place clear"
# ---------- the one-line Description clamp (phase 99, task 01) ----------
# D1: the clamp is VISUAL only — the row's Description cell is ONE flex
# row (text flexes + ellipsizes, the Edit button stays fixed), the full
# text stays in the DOM + on the span's hover title, and the level
# block keeps the full unclamped description. The measured-height proof
# lands in the phase's E2E (task 03); this module pins the structure
# and the CSS.
def _css_rule(css_text: str, selector: str) -> str:
"""The declarations of the rule with exactly this selector (the
task-named classes own their base rule — no compound matches)."""
m = re.search(r"(?<![\w-])" + re.escape(selector) + r"\s*\{([^}]*)\}", css_text)
assert m, f"styles.css must define {selector}"
return m.group(1)
def test_desc_cell_hover_title_covers_real_description_text_only() -> None:
"""Phase 99 (task 01, D1): the text span (`.kb-desc-text` — the
base class, set UNCONDITIONALLY before the three-state branch) AL-
WAYS carries `title` = the FULL description text for real
(non-empty) stored text — the hover escape hatch of the visual
clamp; the phase-98 marker keeps its OWN D4 title (never over-
ridden by the hover rule); the empty cell carries no title at all.
textContent/title only — the house rule (no innerHTML)."""
body = _fn(_js(), "makeDescCell")
base_i = body.find('text.className = "kb-desc-text"')
stored_i = body.find("if (node && node.summary) {")
pending_i = body.find("else if (node && node.summary_pending) {")
empty_i = body.find("} else {")
assert -1 < base_i < stored_i, "the base clamp class is set before the state branch"
stored = body[stored_i:pending_i]
assert "text.title = node.summary;" in stored, "stored → title = the full text (hover)"
assert "Summary pending" not in stored, "the stored branch never touches the marker"
pending = body[pending_i:empty_i]
assert (
'text.title = "No stored description yet — the next sync will generate one."'
in pending
), "the marker keeps its OWN D4 title"
empty = body[empty_i : empty_i + 200]
assert "text.title" not in empty, "the empty cell carries no title"
code = re.sub(r"//.*?$|/\*.*?\*/", "", body, flags=re.S | re.M)
assert "innerHTML" not in code, "the house rule: no innerHTML in the cell builder"
def test_close_editor_rederives_the_hover_title_from_the_stored_text() -> None:
"""Phase 99 (task 01, D1): the hover title is the escape hatch for
the CLAMPED text, so a Save/Cancel must re-derive it — a stale
pre-edit title cannot survive a save (the span is re-inserted
into the wrapper by the swap, attributes included). The re-
derivation is gated on the kb-desc-text class: only the clamped
row span gets the full-text title; the unclamped level's <p>
(full text visible) keeps its no-title behavior, and an emptied
(cleared) cell loses its title."""
body = _fn(_js(), "wireDescriptionEdit")
close = body[body.find("function closeEditor(") : body.find("function openEditor()")]
gate_i = close.find('} else if (stored && textEl.classList.contains("kb-desc-text")) {')
set_i = close.find("textEl.title = stored;")
clear_i = close.find('textEl.removeAttribute("title")')
assert -1 < gate_i < set_i < clear_i, "gated set (stored + clamped span) → clear, in order"
assert close.count("textEl.title =") == 2, (
"exactly: pendingTitle (the marker's own) + stored (the hover full text) — nothing else"
)
def test_styles_carry_the_one_line_clamp() -> None:
"""Phase 99 (task 01, D1): the Description cell is one flex row —
.kb-desc-cell (flex + center + the 0.4rem gap that replaces the
button's old margin-left + min-width: 0) and .kb-desc-text (flex:
1 1 auto + min-width: 0 — the flex item may shrink — + the
ellipsis triad). The column itself is white-space: nowrap (was
normal — the column width is the clamp's budget; the min/max width
+ font-size stay). The button's margin-left rule is GONE, the
level block's margin-top rule is untouched, and the open editor
fills the flex wrapper (flex: 1 1 auto + min-width: 0). .kb-level
p is UNTOUCHED (the full unclamped description — D1), and the
phase-97 block carries no new hue (the phase-92 invariant)."""
css = _text(STYLES_CSS)
cell = _css_rule(css, ".kb-desc-cell")
for prop in ("display: flex", "align-items: center", "gap: 0.4rem", "min-width: 0"):
assert prop in cell, f".kb-desc-cell must carry {prop!r}"
text = _css_rule(css, ".kb-desc-text")
for prop in (
"flex: 1 1 auto",
"min-width: 0",
"overflow: hidden",
"text-overflow: ellipsis",
"white-space: nowrap",
):
assert prop in text, f".kb-desc-text must carry {prop!r}"
# Phase 106 (task 08): the clamp follows the Description cell, which
# moved to the 4th column (Updated took 3rd).
col_i = css.find(".kb-folders-table td:nth-child(4) {")
assert col_i != -1
col = css[col_i : col_i + 400]
col = col[: col.find("\n}")]
assert "white-space: nowrap" in col, "the column is one line (was: normal)"
assert "white-space: normal" not in col
for prop in ("min-width: 18rem", "max-width: 44rem", "font-size: 0.88rem"):
assert prop in col, f"the column keeps {prop!r} (the clamp's budget)"
assert ".kb-folders-table .kb-summary-edit" not in css, (
"the button's margin-left is gone — the wrapper's gap replaces it"
)
lvl_i = css.find(".kb-level-body .kb-summary-edit")
assert lvl_i != -1
assert "margin-top: 0.5rem" in css[lvl_i : lvl_i + 80], (
"the level block's spacing rule is untouched"
)
fill_i = css.find(".kb-desc-cell .kb-summary-editor")
assert fill_i != -1
fill = css[fill_i : fill_i + 80]
assert "flex: 1 1 auto" in fill and "min-width: 0" in fill, (
"the open editor fills the flex wrapper"
)
# The level block keeps the FULL unclamped description (D1) — its
# rule is unchanged (no white-space/ellipsis additions).
m = re.search(r"(?<![\w-])\.kb-level p\s*\{([^}]*)\}", css)
assert m, "the level block's <p> rule must exist"
assert "white-space" not in m.group(1) and "text-overflow" not in m.group(1)
block = _css_block(css, "KB drill-down tree (phase 97", "Git sources page (phase 35)")
assert ".kb-desc-cell" in block and ".kb-desc-text" in block, (
"the clamp lives in the phase-97 tree region"
)
assert not re.search(r"#[0-9a-fA-F]{3,8}\b", block), "no new hue (phase-92 invariant)"
assert not re.search(r"rgba?\(", block), "no new hue (phase-92 invariant)"
def test_module_docstring_documents_the_one_line_clamp() -> None:
"""The house per-phase module-note convention: the phase-99
task-01 section records the flex wrapper structure (text flexes +
ellipsizes, button fixed), the VISUAL-only clamp (full text in the
DOM + on the hover title), the marker's own title, and the wrapper
as the editor's container (the level block's container stays
.kb-level-body)."""
js = _js()
header = js[: js.find("import { fetchIsAdmin }")]
assert "Phase 99 (task 01, D1)" in header
assert "kb-desc-cell" in header and "kb-desc-text" in header
assert "VISUAL only" in header, "the clamp is documented as visual only"
assert "title" in header, "the hover escape hatch is documented"
assert "kb-level-body" in header, "the level block's container is documented as kept"
# ---------- the history integration (phase 99, task 02, D2) ----------
# The drill state IS the history state: every drill pushes a state-
# only entry (no URL change), Back/Forward adopt the entry's kb (or
# reset to the top), the re-show aligns before the re-fetch, and the
# anonymous gate installs no listeners. The browser proof lands in
# the phase's E2E (task 03); this module pins the wiring.
def test_popstate_adopts_the_entry_kb_or_resets_to_top() -> None:
"""Phase 99 (task 02, D2): the window popstate — armed exactly
ONCE, in the ADMIN branch (after the whoami gate, next to the
refresh listener) — ADOPTS the popped entry's ``event.state.kb``
via the no-push variant (the browser owns its entries —
adopt/reset never push), and an entry WITHOUT a kb (the boot
entry, the router's view entries, any foreign state) RESETS the
drill to the TOP level. Rendering a currently-hidden view is
harmless: the router's own popstate (registered earlier, at shell
boot) owns the view switch for foreign entries; rag-internal
entries never change the pathname, so this listener's render is
the visible one."""
js = _js()
assert js.count('window.addEventListener("popstate"') == 1, (
"armed exactly once (mount-once — the router mounts a view ONCE)"
)
pop = js.find('window.addEventListener("popstate"')
gate = js.find("const admin = await fetchIsAdmin();")
refresh = js.find('root.addEventListener("bor:view-refresh"')
assert -1 < gate < refresh < pop, (
"armed in the admin branch, after the gate, next to the refresh listener"
)
body = js[pop : js.find("});", pop)]
kb_i = body.find("event.state && event.state.kb")
adopt_i = body.find("applyTarget(kb, false)")
reset_i = body.find("applyTarget({ source: null, folder: null }, false)")
assert -1 < kb_i < adopt_i < reset_i, (
"read event.state.kb → adopt (no push) / reset to the top"
)
assert "pushState" not in _code(body), ("adopt/reset never push (the browser owns its entries)")
def test_refresh_alignment_adopts_history_state_before_the_refetch() -> None:
"""Phase 99 (task 02, D2): the bor:view-refresh listener (the
phase-77 re-show) aligns ``current`` with ``history.state``
BEFORE ``loadTree()`` — a kb-carrying top entry (the active-link
re-click pushed NOTHING — the drilled entry is still on top)
KEEPS the drill (``applyTarget(kb, false)``); a kb-less entry (a
fresh nav visit) RESETS to the top level. The alignment adopts —
it never pushes."""
js = _js()
listener = js.find('root.addEventListener("bor:view-refresh"')
assert listener != -1
body = js[listener : js.find("});", listener)]
state_i = body.find("history.state && history.state.kb")
adopt_i = body.find("applyTarget(kb, false)")
reset_i = body.find("applyTarget({ source: null, folder: null }, false)")
load_i = body.find("loadTree();")
assert -1 < state_i < adopt_i < reset_i < load_i, (
"align (adopt kb / reset to top) BEFORE the re-fetch"
)
assert "pushState" not in _code(body), "the alignment never pushes"
def test_anonymous_branch_installs_no_navigation_listeners() -> None:
"""Phase 99 (task 02): the anonymous gate installs NO listeners at
all — no popstate, no refresh (the phase-16 rule: anonymous shows
the gate, fetches nothing, and owns no drill state — both
listeners are armed only in the admin branch)."""
js = _js()
start = js.find("if (!admin) {")
end = js.find("if (gateEl) gateEl.hidden = true;")
assert -1 < start < end, "the anonymous branch must exist"
branch = js[start:end]
assert "addEventListener" not in branch, (
"the anonymous branch arms no listeners (no popstate, no refresh)"
)
def test_module_docstring_documents_the_history_integration() -> None:
"""The house per-phase module-note convention: the phase-99
task-02 section records the push/adopt/reset rules — the
STATE-ONLY push (the URL STAYS PUT, no new route — the phase-76
deep-link surface untouched), the duplicate-push skip, the
popstate adopt/reset, the refresh alignment before the re-fetch,
the no-boot-push contract, the UNCHANGED (no-push)
resetVanishedLocation, and the UNTOUCHED router module."""
js = _js()
header = js[: js.find("import { fetchIsAdmin }")]
assert "Phase 99 (task 02, D2)" in header
assert "STATE-ONLY" in header, "the state-only push is documented"
assert "STAYS PUT" in header, "the URL-stays-put note"
assert "no duplicate history" in header, "the duplicate-push skip"
assert "ADOPTS" in header and "popstate" in header, "the popstate adopt"
assert "RESETS the drill to the TOP level" in header, "the reset branch"
assert "BEFORE the re-fetch" in header, "the refresh alignment"
assert "Boot pushes NOTHING" in header, "no pushState on boot"
assert "resetVanishedLocation is UNCHANGED" in header
assert "module is UNTOUCHED" in header, "the router module is untouched"