Files
brain-of-reese/tests/unit/test_kb_tree_ui.py
T
ducoterra 2ac37455f0
Build and Push Containers / build-and-push-app (push) Successful in 1m45s
Build and Push Containers / build-and-push-db (push) Successful in 14s
phase: 167_api_payload_diet
All gates green. Final report:

**Verified (phase 167 final pass — all 6 tasks already in `complete/`)**
- Live re-measure of the headline criterion on the **1,000-doc live-replica corpus** (restored `data/bor_live_kb_replica.dump.sql` into a scratch DB `bor_audit`, alembic 0011→0027, dropped after; the shared dev DB was never touched): `GET /api/docs/tree` default depth=1 = **8,474 identity → 1,128 B with `Content-Encoding: gzip`** (< 40 KB bar; ~2 MB defect fixed), repeat load **304 / 0 bytes**; depth=8 265,147→23,515; node drill 20,844→1,065; `/api/docs` 246,366→45,104; `/api/documents/content` 892,124→60,776 (`LOOK`, accepted-with-why). Reproduces `AUDIT.md` to within 1 byte.
- Transport contracts proven live: `POST /api/chat` **no** `Content-Encoding` + frames arriving live + budget line `… enc=identity duration_ms=7380 stream=1`; HTML still `no-cache` with the `?v=` rewrite (and gzipped); A20 headers on 200 **and** 304; ETag identical across identity/gzip; `depth=0|9|abc`→422; anonymous tree/node→403; unknown folder/source→404 `unknown node`.

**Gates (all 100% green)**
- `uv run pytest` → **3980 passed**; `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99%** (`compression.py`, `http_cache.py`, `response_budget.py` 100%; `api/docs.py` 99%, one defensive dedupe line).
- `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors.
- Phase + regression E2E **in isolation** (`--junitxml`, exit codes): `test_kb_tree_lazy_load` 11, `test_kb_tree` 8, `test_kb_tree_nav` 4, `test_navbar_refresh` 7, `test_document_dates` 6, `test_git_source_dates` 4, `test_sync_summary_visibility` 3, `test_table_ux_polish` 6, `test_sources_midstream_bug` 6, `test_ls_tree_drilldown` 3, `test_smoke` 3 — **0 failures/errors** (the earlier `-q -q` run had simply suppressed pytest's summary line).

**Documentation** — README.md + AGENTS.md reviewed in full (1,436 / 80 lines):
- Fixed (pre-existing, phase-167-exposed): README *Caching & deploys* claimed "One Starlette middleware (`app/core/caching.py`) applies the rule" while the section now describes three transport layers → rewritten to name caching/compression/http_cache, the real outermost→innermost order, and why `bytes=` is uncompressed.
- AGENTS.md rule 5 (UI Structure Check) gained the phase-167 clause: a lazily-drilled level keeps the current level rendered, its in-flight `role="status"` / `role="alert"`+Retry row lives in the same bounded scroll panel, and stat cards read server `totals` (never a walk of a depth-limited tree).
- Cross-checked and consistent: every `BOR_*` in `.env.example` is in the README table, every `Settings` field is documented, all 15 README-cited API routes exist in the app, no stale "one fetch / drill client-side" claim remains anywhere.

**Notable decisions / deviations**
- `compression.py` adds a documented `_RebufferSmallResponses` seam beyond the D1 sketch (the caching layer's `BaseHTTPMiddleware` re-framing otherwise defeats Starlette's `minimum_size` exemption and inflates tiny responses) — integration-pinned, math still Starlette's.
- A node-route **404 = vanished folder → honest reset to top level** (per `00_phase.md` D4/D6), not task-06's "alert + Retry"; 500 shows alert+Retry with the level preserved — both pinned, nothing weakened.
- Retrieval battery not measurable (shared E2E DB holds no corpus), but `git diff --name-only -- app/rag` is empty, so retrieval is unchanged by construction. `AUDIT.md` left byte-identical (it lives under `todo/`, off-limits to this pass); no git index/history commands run.

**Next pending phase:** none — `167_api_payload_diet` is the only entry in `.agents/phases/todo/`; the harness moves it to `complete/` and commits.
2026-10-08 21:23:49 -04:00

1779 lines
89 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 ``loadTree()``: ONE
fetch of ``GET /api/docs/tree?depth=1`` (sources + one level + the
server's ``totals``) plus ONE ``GET /api/docs/tree/node`` fetch per
not-yet-loaded level — phase 167 (task 04, LOCKED A3 — the TODO L5
2 MB payload fix) SUPERSEDED the phase-97 "one full-tree fetch, drill
purely client-side" contract; the drill UX (breadcrumb, no URL change)
is unchanged.
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?depth=1`` load fetch + the single
``/api/docs/tree/node`` drill fetch (the flat ``/api/docs`` fetch is
gone from the view module), the level cache (declared once, cleared
by every load, consulted by ``needsLevelFetch``, written only after a
200), the §7.4 drill states (in-flight ``aria-busy``/``aria-disabled``
+ one ``role="status"`` row, ``role="alert"`` failure with a real
Retry ``<button>``, the node 404 → top-level reset, the race token
extended to the drill), and the ``totals``-first stat cards;
* 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 depth-1 fetch, the node fetch, the token ----------
def test_sources_js_fetches_the_tree_endpoint_only() -> None:
"""Phase 167 (task 04, A3 — supersedes the phase-97 "ONE full-tree
fetch, zero per-level fetches" pin): the catalog load is ONE
`fetch("/api/docs/tree?depth=1")` and the drill has exactly ONE
node-fetch call site (`fetch("/api/docs/tree/node?…`, reached only
through fetchLevel). The flat `GET /api/docs` fetch stays GONE from
the view module (the endpoint itself stays — task 02)."""
js = _js()
code = _code(js)
assert code.count('fetch("/api/docs/tree?depth=1")') == 1, (
"the load is ONE depth=1 request (the 2 MB first-payload fix)"
)
assert code.count('fetch("/api/docs/tree/node?') == 1, (
"exactly ONE per-level fetch call site — every drill routes through it"
)
assert 'fetch("/api/docs/tree")' not in js, "the depth-less load URL is gone"
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: \"\" }, link)" in body, (
"a source row drills to the source ROOT (folder \"\") — the link "
"rides the phase-167 in-flight state"
)
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 }, link)" in body, (
"a folder row drills to that folder — the link rides the "
"phase-167 in-flight state"
)
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 — ``applyTarget(target, push)``: 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 module never
``replaceState``s. Phase 167 (task 04, A3 — supersedes the "the
drill never fetches" pin): ``goTo`` hands the target to
``enterFolder`` with ``push=true``, so the push lands only when the
level is IN HAND (a failed drill records nothing) — ``applyTarget``
and ``goTo`` themselves stay fetch-free; the drill's only fetch
lives in ``fetchLevel`` (pinned below)."""
js = _js()
assert js.find("function applyTarget(") != -1, (
"applyTarget must exist (the two-step drill)"
)
body = _fn(js, "applyTarget")
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(js).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 = _fn(js, "goTo")
assert "enterFolder(target, link, true)" in go, (
"goTo pushes (the user-initiated drill, phase 167 via enterFolder)"
)
assert "fetch(" not in _code(body), "applyTarget stays a pure state transition"
assert "fetch(" not in _code(go), "goTo delegates the fetch to enterFolder only"
# ---------- phase 167 (task 04): the lazy drill (cache, states, reset) ----------
def test_level_cache_declared_cleared_and_consulted() -> None:
"""The level cache (phase 167, D4): ONE module-scoped Map keyed
`source + "/" + folder`; EVERY successful load clears it (a refresh
invalidates every drilled level — the sync/upload/re-show call
sites keep working unchanged) BEFORE the never-stale reset (the
reset re-derives known-ness from the NEW tree); `needsLevelFetch`
consults it (the second visit to a folder is free — zero network);
the write sits behind the ok gate (only a 200 caches, never the
404/failure arms) and follows the merge (children ride the node
before the level is marked loaded)."""
js = _js()
assert js.count("const levelCache = new Map();") == 1, (
"ONE module-scoped level cache"
)
load = _fn(js, "loadTree")
clear_i = load.find("levelCache.clear();")
reset_i = load.find("resetVanishedLocation();")
assert 0 < clear_i < reset_i, "clear the cache BEFORE the reset (new tree)"
needs = _fn(js, "needsLevelFetch")
assert "node.truncated === true" in needs, "only a TRUNCATED node needs a fetch"
assert "levelCache.has(" in needs, "a cached level never refetches"
fetch = _fn(js, "fetchLevel")
merge_i = fetch.find("Object.assign(targetNode, merged);")
set_i = fetch.find('levelCache.set(target.source + "/" + target.folder, true);')
ok_gate = fetch.find("if (response === null || !response.ok) {")
vanish_i = fetch.find("if (vanished) {")
assert -1 < vanish_i < ok_gate < merge_i < set_i, (
"404 arm → ok gate → merge → cache: only a 200 caches, after the merge"
)
def test_drill_race_token_extends_to_the_node_fetch() -> None:
"""Phase 79 carried over (task 04): fetchLevel captures the
loadSeq token (plus a navSeq bumped by every applyTarget) and a
response may only paint while NEITHER a newer load NOR a newer
navigation happened — pinned twice (after the fetch, after the
json read). A new loadTree invalidates the in-flight drill the
moment it starts, and rehydrates the user's own truncated level
through the node route BEFORE it paints (a refresh never shows a
half-empty level)."""
js = _js()
fetch = _fn(js, "fetchLevel")
assert "const my = loadSeq;" in fetch, "the phase-79 token extends to the drill"
assert "const nav = navSeq;" in fetch, "a navigation invalidates the drill too"
assert fetch.count("if (my !== loadSeq || nav !== navSeq) return false;") == 2, (
"a stale drill response never paints (checked after the fetch and the json)"
)
load = _fn(js, "loadTree")
assert load.find("drillPending = null;") < load.find("await fetch"), (
"a new load invalidates the in-flight drill immediately"
)
assert "if (needsLevelFetch(currentLevelNode(), current.source)) {" in load, (
"the user's own truncated level is REHYDRATED through the node route"
)
assert "let navSeq = 0;" in js
# …and the ADOPTED navigation (popstate) rehydrates the same way:
# a forward/back entry can point at a level the client has not
# fetched since the last load (ensureCurrentLevel, after the adopt).
pop = js[js.find('window.addEventListener("popstate"') :]
pop = pop[: pop.find("});")]
adopt_i = pop.find("applyTarget(kb, false)")
ensure_i = pop.find("ensureCurrentLevel();")
assert -1 < adopt_i < ensure_i, "adopt the entry, THEN rehydrate its level"
ensure = _fn(js, "ensureCurrentLevel")
assert "if (drillPending) return;" in ensure
assert "if (needsLevelFetch(currentLevelNode(), current.source)) {" in ensure
def test_drill_pending_guards_and_aria_inflight_state() -> None:
"""§7.4 in-flight: enterFolder early-returns while a drill is
pending (a double click cannot start a second fetch), sets the
drillPending state BEFORE awaiting fetchLevel, and applies the
target only once the level is in hand. fetchLevel marks the
clicked link aria-busy + aria-disabled BEFORE the fetch and clears
both on every settle path (the control never stays disabled — the
failure path re-enables)."""
js = _js()
enter = _fn(js, "enterFolder")
guard_i = enter.find("if (drillPending) return;")
pending_i = enter.find(
"drillPending = { source: target.source, folder: target.folder };"
)
fetch_i = enter.find("await fetchLevel(target, node, link)")
apply_i = enter.find("applyTarget(target, push);")
assert -1 < guard_i < pending_i < fetch_i < apply_i, (
"guard → in-flight state → fetch → apply (only when the level is in hand)"
)
fetch = _fn(js, "fetchLevel")
busy_i = fetch.find('link.setAttribute("aria-busy", "true")')
disabled_i = fetch.find('link.setAttribute("aria-disabled", "true")')
call_i = fetch.find("response = await fetch(")
clear_busy = fetch.find('link.removeAttribute("aria-busy")')
clear_disabled = fetch.find('link.removeAttribute("aria-disabled")')
assert -1 < busy_i < call_i and -1 < disabled_i < call_i, (
"the busy state rides the row link BEFORE the fetch"
)
assert call_i < clear_busy and call_i < clear_disabled, (
"every settle path re-enables the control (§7.4)"
)
def test_drill_inflight_status_and_failure_alert_rows() -> None:
"""The ONE in-flight row of the folders panel (phase 167, D4): the
loading row is a role="status" "Loading <folder>…" line while the
level the user is LEAVING stays rendered; the failure row is a
role="alert" "Couldn't load <folder> — " line with a REAL
<button type="button">Retry</button> whose click re-runs the SAME
enterFolder fetch. Neither builder calls renderLevel (the current
level never blanks); both clear the previous drill row first (ONE
row at a time) and textContent only (the house rule)."""
js = _js()
loading = _fn(js, "showDrillLoading")
assert "clearDrillRow();" in loading
assert 'note.setAttribute("role", "status");' in loading
assert 'note.textContent = "Loading " + name + "…"' in loading
assert 'row.className = "kb-level-loading"' in loading
assert "foldersTbody.appendChild(row)" in loading, "the row lives in the folders panel"
assert "renderLevel" not in _code(loading), "the leaving level stays rendered"
fail = _fn(js, "showDrillFailure")
assert "clearDrillRow();" in fail
assert 'note.setAttribute("role", "alert");' in fail
assert '"Couldn\'t load " + target.folder.split("/").pop() + " — "' in fail
assert 'retry.type = "button";' in fail, "Retry is a REAL button"
assert 'retry.textContent = "Retry";' in fail
assert (
"retry.addEventListener(\"click\", () => enterFolder(target, link, true));" in fail
), "Retry re-runs the SAME drill fetch"
assert "renderLevel" not in _code(fail), "the failure keeps the current level rendered"
clear = _fn(js, "clearDrillRow")
assert "drillRowEl.remove();" in clear and "drillRowEl = null;" in clear
def test_vanished_node_resets_to_the_top_level() -> None:
"""The node route's 404 IS the "the folder vanished" signal (task
04, D4): the drill resets `current` to the TOP level and re-renders
(no stale breadcrumb), caches NOTHING for the failed level, and
never applies the vanished target (enterFolder returns on false).
A network/non-OK failure takes the alert arm instead — the current
level stays rendered (pinned above)."""
js = _js()
fetch = _fn(js, "fetchLevel")
assert "response.status === 404" in fetch, "the 404 is detected"
vanish = fetch[fetch.find("if (vanished) {") : fetch.find(
"if (response === null || !response.ok) {"
)]
assert "current = { source: null, folder: null };" in vanish, (
"the 404 resets to the top level (the phase-97 never-stale contract)"
)
assert "renderLevel();" in vanish, "the reset renders — no stale breadcrumb"
assert "return false;" in vanish, "the vanished target is never applied"
assert "levelCache" not in vanish, "the failing level is NOT cached"
enter = _fn(js, "enterFolder")
assert "if (!loaded) return;" in enter, "enterFolder applies only on a real merge"
def test_module_docstring_documents_the_lazy_drill() -> None:
"""The house per-phase module-note convention: the Phase 167
(task 04) section records the depth-1 load, the per-level node
fetch + cache, the §7.4 drill states, the extended race token, the
404 reset and the totals-first stat cards — citing TODO L5."""
js = _js()
header = js[: js.find("import { fetchIsAdmin }")]
assert "Phase 167 (task 04, D4/A3" in header
assert "TODO L5" in header, "the 2 MB payload defect is documented"
assert "LAZY DRILL" in header
assert "/api/docs/tree?depth=1" in header
assert "levelCache" in header
assert "loadSeq token EXTENDS" in header, "the race-token narrative stays accurate"
assert "folderKnownIn" in header, "the existence-rule change is documented"
# ---------- 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: server totals, walk as the fallback ----------
def test_stat_cards_prefer_the_server_totals() -> None:
"""Phase 167 (task 04, D2/D4): a depth-limited tree CANNOT be
summed client-side, so treeStats() reads the server's `totals`
(documents / chunks / last_indexed) FIRST; the phase-97 recursive
walk stays only as the defensive no-`totals` fallback (a tree
served by a pre-phase process — the no-crash path). The stat-card
rendering (String(st.docs)/String(st.totalChunks)/fmtDate(st.last)
with the "–" null idiom) is UNCHANGED."""
js = _js()
body = _fn(js, "treeStats")
totals_i = body.find("const totals = kbTree.totals;")
guard_i = body.find('if (totals && typeof totals === "object") {')
docs_i = body.find("docs: totals.documents,")
chunks_i = body.find("totalChunks: totals.chunks,")
last_i = body.find('last: totals.last_indexed || "",')
walk_i = body.find("const walk = (node) => {")
assert -1 < totals_i < guard_i < docs_i < chunks_i < last_i < walk_i, (
"totals first, the recursive walk ONLY as the no-totals fallback"
)
render = js[js.find("function renderLevel(") : js.find("function renderEmpty(")]
assert "const st = treeStats();" in render
assert 'st.last ? fmtDate(st.last) : "–"' in render, (
"the exact fmtDate formatting call is unchanged at every level"
)
def test_stat_cards_walk_the_whole_tree() -> None:
"""The no-`totals` fallback walk (phase 97, kept defensive since
phase 167): walk the WHOLE held 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 is no
longer KNOWN under it, `current` RESETS to the top level BEFORE
rendering — no stale breadcrumb, no stale block. Phase 167 (task
04, D4): the phase-94 full-tree path-prefix walk becomes
folderKnownIn — a folder is known iff its WHOLE ancestor chain
exists in the levels the client HOLDS (the depth-1 tree + the
drilled-and-merged levels; a broken chain — gone OR simply not
loaded that deep — resets honestly; the node route's 404 is the
drill path's own vanished-signal, pinned separately)."""
js = _js()
body = _fn(js, "resetVanishedLocation")
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"
)
assert "folderKnownIn(src, current.folder)" in body, (
"the reset consults KNOWN-ness (the held levels), never a full-tree walk"
)
known = _fn(js, "folderKnownIn")
assert 'if (folderPath === "") return true;' in known, "the source root always exists"
assert "if (!next) return false;" in known, "a broken ancestor chain is not known"
assert "collect" not in _code(known), "the recursive path-collection walk is gone"
assert "folderExistsIn" not in js, "the old full-tree existence walker is gone"
# 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-level-loading",
".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
# Phase 156 (task 02, A3): the visible text is the .cell-clip
# span (a squeezed Description cell shows "…" instead of a hard
# cut) — the aria-label names the row, so the full text stays
# reachable.
assert 'clip.className = "cell-clip"' in body, (
"the clip label span (phase 156, A3)"
)
assert 'clip.textContent = "Edit"' in body, "the visible text (never innerHTML)"
assert "btn.append(clip)" in body, "the span is the button's label"
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"