"""Unit: the phase-106 date COLUMNS in the UI (task 08, D8) + the viewer's Created badge (display only — the admin date EDITOR is task 09). The owner asked for the date everywhere it is read: "For files, include a date/timestamp before the 'indexed' column in the UI"; "I would also like to see a last updated dates/timestamps on folders before the description column but after the documents column in the UI"; "The UI must also show a date for every document at the top of that document when the user clicks it." The values ride the task-05 APIs (``created_at`` on the tree's file nodes + ``/api/docs`` + ``/api/documents/content``; the derived subtree-max ``updated_at`` on tree folders/sources, D9 — ``null`` for a 0-document source). The browser behavior itself is E2E-gated by the phase's dedicated suite (``tests/e2e/test_document_dates.py``, task 10); like the other frontend-adjacent unit files (the ``test_source_ignore_paths.py`` / ``test_kb_tree_ui.py`` house pattern), this module pins the source-level contract a silent regression would break: * ``frontend/index.html`` — the header cell ORDER, pinned as the exact ```` sequence in the RAG view: ``Source | Path | Title | Chunks | Created | Indexed`` (Created BETWEEN Chunks and Indexed — D8 verbatim) and ``Folder | Documents | Updated | Description`` (Updated BETWEEN Documents and Description — D8 verbatim); * ``frontend/assets/sources.js`` — ``makeRow``'s cell order (``created_at`` BEFORE ``indexed_at``; the Created cell built explicitly — ``textContent`` = ``fmtDate(d.created_at)`` AND ``title`` = the full ISO value, the path-cell hover idiom the E2E asserts on — never innerHTML), the file-row object fed from the tree's file nodes carries ``created_at``, and the ``updatedTd`` null → ``"–"`` branch (the statLast idiom) is present in BOTH row builders (makeSourceRow + makeFolderRow); * ``frontend/assets/document.js`` — the ONE shared core's ``.doc-meta`` badge row: the ``doc-created`` badge BEFORE ``doc-indexed`` (modal + ``/document.html`` through the same core — no per-surface copy), the ``Created `` label + ``fmtDate(doc.created_at)`` template, and the full ISO timestamp on the badge's ``title`` (the ``titleEl`` ellipsis-precision idiom); * ``frontend/assets/styles.css`` — the ``.doc-created`` rule with the phase-106 D8 provenance comment + the recorded WCAG pair (5.1:1 — the same family as the Indexed badge), and the Description one-line clamp moved to ``td:nth-child(4)`` (the Updated column took 3rd — no hard-coded column count left behind). """ from __future__ import annotations import re from pathlib import Path FRONTEND = Path(__file__).resolve().parents[2] / "frontend" SHELL_HTML = FRONTEND / "index.html" SOURCES_JS = FRONTEND / "assets" / "sources.js" DOCUMENT_JS = FRONTEND / "assets" / "document.js" STYLES_CSS = FRONTEND / "assets" / "styles.css" 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 _doc_js() -> str: return _text(DOCUMENT_JS) def _css() -> str: return _text(STYLES_CSS) def _rag_view(html: str) -> str: """The RAG view section of the shell (view-scoped — the shell carries many views, so whole-file matches hit the wrong one).""" i = html.find('
str: """The source of a (possibly async, possibly nested) function via balanced-brace counting (the test_kb_tree_ui.py helper). The brace count starts AFTER the parameter list — a destructured parameter (renderDocument'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") def _header_columns(html: str, table_id: str) -> list[str]: """The table's ```` cells, IN DOCUMENT ORDER (the ORDER is the pin — D8's verbatim positions).""" i = html.find(f'id="{table_id}"') assert i != -1, f"#{table_id} must be in the RAG view" head = re.search(r"(.*?)", html[i :], re.S) assert head, f"#{table_id} must keep a static thead" return [m.group(1) for m in re.finditer(r"([^<]*)", head.group(1))] def _css_rule(css: str, selector: str) -> str: """The declarations of a simple rule (comments stripped first — a house comment may legally carry braces).""" clean = re.sub(r"/\*.*?\*/", "", css, flags=re.S) start = clean.find(f"{selector} {{") assert start != -1, f"missing rule {selector} in styles.css" brace = clean.find("{", start) depth = 0 for i in range(brace, len(clean)): if clean[i] == "{": depth += 1 elif clean[i] == "}": depth -= 1 if depth == 0: return clean[start : i + 1] raise AssertionError(f"unbalanced braces in {selector}") # ---------- index.html: the two header rows (ORDER pinned) ---------- def test_file_table_header_order_is_source_path_title_chunks_created_indexed() -> None: """D8 verbatim: the file table's ```` sequence is EXACTLY Source | Path | Title | Chunks | Created | Indexed — Created BETWEEN Chunks and Indexed (a set-membership pin would let a regression move the column; the sequence pins the position).""" view = _rag_view(_text(SHELL_HTML)) assert _header_columns(view, "docs-table") == [ "Source", "Path", "Title", "Chunks", "Created", "Indexed", ], "the file table's column order (Created BEFORE Indexed, D8)" def test_folder_table_header_order_is_folder_documents_updated_description() -> None: """D8 verbatim: the folder/source table's ```` sequence is EXACTLY Folder | Documents | Updated | Description — Updated BETWEEN Documents and Description.""" view = _rag_view(_text(SHELL_HTML)) assert _header_columns(view, "folders-table") == [ "Folder", "Documents", "Updated", "Description", ], "the folder table's column order (Updated BETWEEN the two, D8)" def test_inserted_headers_carry_the_phase_106_comment() -> None: """House comment style: each inserted is annotated with a phase-106 provenance comment (D8 for the position; D9 for the derived Updated value) — a bare with no comment is the regression this guards against.""" view = _rag_view(_text(SHELL_HTML)) for col in ("Created", "Updated"): i = view.find(f"{col}") assert i != -1, f"the {col} header must be in the RAG view" comment = view.rfind("", comment) assert comment > -1 and "phase 106" in view[comment:end].lower(), ( f"a phase-106 comment must sit above the {col} header" ) assert "D8" in view[comment:end], f"the {col} comment cites D8" # ---------- sources.js: makeRow's Created cell (before Indexed) ---------- def test_make_row_cell_order_is_created_before_indexed() -> None: """makeRow (the file table's row builder): the cell order is [title, chunks, created, indexed] — the Created cell lands BEFORE the Indexed one (D8 verbatim). Both date cells are built EXPLICITLY (the plain-td loop can't carry per-cell titles): the Created cell has ``textContent = fmtDate(d.created_at)`` AND ``title = d.created_at`` — the full ISO value on hover (the path-cell idiom; the E2E asserts on the locale-stable title, not on the toLocaleString output).""" make = _fn(_js(), "makeRow") # The loop keeps the two plain cells (title, chunks). assert "for (const value of [d.title, String(d.chunks)]) {" in make # The Created cell: explicit, locale date + the ISO title. created_i = make.find("const createdTd = document.createElement(\"td\");") assert created_i != -1, "the Created cell is built explicitly" assert "createdTd.textContent = fmtDate(d.created_at);" in make assert "createdTd.title = d.created_at;" in make, ( "the full ISO value on hover (the E2E's locale-stable pin)" ) # The value-list ORDER: created_at before indexed_at (text AND # append — both orderings pinned, a regression can't swap them). created_fmt = make.find("fmtDate(d.created_at)") indexed_fmt = make.find("fmtDate(d.indexed_at)") assert -1 < created_fmt < indexed_fmt, "created_at BEFORE indexed_at (D8)" assert ( make.find("tr.appendChild(createdTd)") < make.find("tr.appendChild(indexedTd)") ), "the Created cell appends BEFORE the Indexed one" # textContent only — the XSS contract (never innerHTML with # document-derived data). assert "createdTd.innerHTML" not in make and "indexedTd.innerHTML" not in make def test_tree_file_row_object_carries_created_at() -> None: """renderLevel's file loop feeds makeRow the FLAT row shape — the tree's file node carries created_at (task 05's tree shape), and the row object restores it BEFORE indexed_at (the order the reader gets matches makeRow's cell order).""" js = _js() render = js[js.find("function renderLevel(") : js.find("function renderEmpty(")] assert "makeRow({" in render, "renderLevel still feeds makeRow" created_i = render.find("created_at: f.created_at,") indexed_i = render.find("indexed_at: f.indexed_at,") assert -1 < created_i < indexed_i, ( "the row object carries created_at (before indexed_at — task 05's tree shape)" ) # ---------- sources.js: the Updated cell in BOTH row builders ---------- UPDATED_TAIL = "makeDescCell" # the Description cell follows Updated def test_source_row_has_updated_cell_between_count_and_description() -> None: """makeSourceRow (top level — the rows ARE the sources): ONE new td BETWEEN the count td and the Description cell — the ``updatedTd`` null → ``"–"`` branch (the statLast idiom: D9's ``None`` for a 0-document source), the locale date otherwise (fmtDate), and the ISO value on the cell's title (hover precision — the makeRow path-cell idiom).""" make = _fn(_js(), "makeSourceRow") count_i = make.find("countTd.textContent = String(s.documents);") tail_i = make.find(UPDATED_TAIL, count_i) updated_i = make.find("const updatedTd = document.createElement(\"td\");", count_i) assert -1 < count_i < updated_i < tail_i, ( "the Updated cell sits BETWEEN the count and the Description (D8)" ) assert 'updatedTd.textContent = s.updated_at ? fmtDate(s.updated_at) : "–";' in make, ( "the null → '–' branch (the statLast idiom — D9's None for a 0-document source)" ) assert "if (s.updated_at) updatedTd.title = s.updated_at;" in make, ( "the ISO value on hover (only when there is one)" ) append_i = make.find("tr.appendChild(updatedTd)", count_i) desc_i = make.find(UPDATED_TAIL, append_i) assert -1 < append_i < desc_i, "appended before the Description cell" assert "updatedTd.innerHTML" not in make, "textContent only (XSS contract)" def test_folder_row_has_updated_cell_between_count_and_description() -> None: """makeFolderRow (a level's subfolders): the SAME one td BETWEEN the count td and the Description cell — the ``updatedTd`` null → ``"–"`` branch present in BOTH row builders (the phase pins it in each — a source row and a folder row are separate code paths).""" make = _fn(_js(), "makeFolderRow") count_i = make.find("countTd.textContent = String(f.documents);") tail_i = make.find(UPDATED_TAIL, count_i) updated_i = make.find("const updatedTd = document.createElement(\"td\");", count_i) assert -1 < count_i < updated_i < tail_i, ( "the Updated cell sits BETWEEN the count and the Description (D8)" ) assert 'updatedTd.textContent = f.updated_at ? fmtDate(f.updated_at) : "–";' in make, ( "the null → '–' branch in the FOLDER builder too (both builders pinned)" ) assert "if (f.updated_at) updatedTd.title = f.updated_at;" in make, ( "the ISO value on hover (only when there is one)" ) append_i = make.find("tr.appendChild(updatedTd)", count_i) desc_i = make.find(UPDATED_TAIL, append_i) assert -1 < append_i < desc_i, "appended before the Description cell" assert "updatedTd.innerHTML" not in make, "textContent only (XSS contract)" def test_module_docstring_carries_the_phase_106_contract() -> None: """The house module-docstring convention: the phase-106 section records the Created column (before Indexed), the Updated column (between Documents and Description), and the untouched stat cards (the indexed_at 'last indexed' semantics stay).""" doc = _js()[: _js().find("import { fetchIsAdmin }")] for frag in ( "Phase 106 (task 08, D8)", "Created column BEFORE Indexed", "BETWEEN\n * Documents and Description", "D9", "UNTOUCHED", ): assert frag in doc, f"the module docstring lost: {frag!r}" # ---------- document.js: the shared core's Created badge ---------- def test_meta_row_badge_order_is_created_before_indexed() -> None: """renderDocument (the ONE shared core — the modal AND /document.html render through it, no per-surface copy): the .doc-meta badge row carries the ``doc-created`` badge BEFORE the ``doc-indexed`` one (D8 verbatim — the date at the top of a clicked document).""" render = _fn(_doc_js(), "renderDocument") block = render[render.find("metaEl.replaceChildren(") :] created_i = block.find('metaBadge("doc-created"') indexed_i = block.find('metaBadge("doc-indexed"') chunks_i = block.find('metaBadge("doc-chunks"') assert -1 < created_i < indexed_i < chunks_i, ( "the Created badge BEFORE Indexed, both before Chunks (D8)" ) # The label + the locale-date template (the Indexed idiom). assert "Created ${fmtDate(doc.created_at)}" in block, ( "the 'Created ' label + fmtDate(doc.created_at) template" ) assert "Indexed ${fmtDate(doc.indexed_at)}" in block, ( "the Indexed badge is unchanged (the idiom the Created one copies)" ) def test_created_badge_carries_the_full_iso_title() -> None: """The badge's ``title`` attribute carries the FULL ISO timestamp (the titleEl ellipsis-precision idiom — the meta row may clip, the exact value stays reachable): the created badge passes ``doc.created_at`` as metaBadge's title argument, and metaBadge sets it via setAttribute (only when provided — the other badges keep the two-argument shape, byte-identical).""" js = _doc_js() render = _fn(js, "renderDocument") call_start = render.find("metaEl.replaceChildren(") # The call ends at the first `);` AFTER the last badge (the # doc-chunks one) — the comment above the created badge may carry # parentheses, so slicing from the call top would be brittle. chunks_i = render.find('metaBadge("doc-chunks"', call_start) block = render[call_start : render.find(");", chunks_i) + 2] created_i = block.find('metaBadge("doc-created"') created_call = block[created_i : block.find("),", created_i) + 1] assert created_call.endswith(", doc.created_at)"), ( "the created badge passes doc.created_at as its title" ) badge = _fn(js, "metaBadge") assert "function metaBadge(cls, text, title)" in badge, ( "metaBadge's optional title parameter" ) assert 'el.setAttribute("title", title)' in badge assert "title !== undefined" in badge, ( "the guard keeps the other badges' two-argument shape" ) # No other badge passes a title (the pre-phase badges keep the # two-argument shape — exactly one comma in the call). for cls_ in ("doc-source-badge", "format-badge", "doc-indexed", "doc-chunks"): i = block.find(f'metaBadge("{cls_}"') assert i != -1, f"the {cls_} badge must stay in the meta row" call = block[i : block.find("),", i) + 1] assert call.count(",") == 1, f"{cls_} keeps the two-argument shape" def test_core_docstring_and_comment_cite_phase_106_d8() -> None: """House comment style: the module docstring + the badge-row comment record the phase-106 (task 08, D8) created-before-indexed position and the ONE-shared-core guarantee (modal + page).""" js = _doc_js() doc = js.split("*/", 1)[0] assert "Phase 106 (task 08, D8)" in doc assert "BEFORE the Indexed one" in doc assert "task 05" in doc, "the created_at payload provenance (task 05)" render = _fn(js, "renderDocument") assert "Phase 106 (task 08, D8)" in render, "the inline comment at the insertion site" # ---------- styles.css: the .doc-created rule + the clamp move ---------- def test_doc_created_rule_present_with_provenance_and_contrast() -> None: """styles.css carries the ``.doc-created`` rule (the doc-indexed badge family — the meta row's ink-soft text) with the phase-106 D8 provenance comment + the recorded WCAG pair (5.1:1 on --surface — the same pair the Indexed badge inherits via .doc-meta / .doc-modal-meta).""" css = _css() rule = _css_rule(css, ".doc-created") assert "color: var(--ink-soft)" in rule, "the meta-row family (the Indexed look)" # The provenance comment sits DIRECTLY above the rule (the # house style: phase + decision + the verified contrast pair). rule_i = css.find(".doc-created {") comment_start = css.rfind("/*", 0, rule_i) comment_end = css.find("*/", comment_start) assert -1 < comment_start < rule_i and comment_end < rule_i, ( "a comment block must sit directly above the rule" ) header = css[comment_start:comment_end] assert "phase 106" in header.lower() and "D8" in header, ( "the provenance comment cites phase 106 + D8" ) assert "5.1:1" in header, "the verified contrast pair is recorded (house style)" # The rule sits next to the meta-row badge family (after # .doc-chunks, before the .doc-shell block). assert ( css.find(".doc-chunks {") < rule_i < css.find(".doc-shell {") ), "next to the existing meta-row badge rules" def test_folders_description_clamp_follows_the_moved_cell() -> None: """The column-count change must not break the table's rules: the Description one-line clamp (phase 99) follows the cell, which moved to the 4th (the Updated column took 3rd) — no stale ``td:nth-child(3)`` folders rule may remain, and the new Updated cell's ink pair (the table ink on --surface, 13.8:1) is recorded in the house comment.""" css = _css() assert ".kb-folders-table td:nth-child(4) {" in css, ( "the Description clamp moved with the cell (4th column)" ) assert ".kb-folders-table td:nth-child(3)" not in css, ( "no stale 3rd-column folders rule (the Updated cell is there now)" ) i = css.find(".kb-folders-table td:nth-child(4)") comment = css.rfind("/*", 0, i) end = css.find("*/", comment) assert comment > -1 and "phase 106" in css[comment:end].lower(), ( "a phase-106 comment explains the Updated column + the clamp move" ) assert "13.8:1" in css[comment:end], ( "the Updated cell's ink pair is recorded (table ink on --surface)" )