Files
brain-of-reese/tests/unit/test_document_viewer.py
T
ducoterra 32a3ec4f5e
Build and Push Containers / build-and-push-app (push) Successful in 2m23s
Build and Push Containers / build-and-push-db (push) Successful in 13s
phase: 145_git_source_links
**Phase 145 final verification pass — all green, no code changes needed**

- **Verified (no defects found):** resolver `origin_for_source` + `_url_basename` (scp/`.git`/trailing-slash, sanitize, ambiguity WARNING); `origin:`/`branch:` lines in all four read shapes via shared `_origin_lines`; HIGH `<tools>` origin rule (verbatim + only-byte-change pins); `DocContent.origin_url/origin_branch` (absent-never-null serializer); `renderDocument` "Original" anchor (copyable verbatim URL, `_blank`/`noopener`, branch in title); mock scripted origin-link flow (disjoint trigger — grep-verified vs all other E2E files)
- **Tests:** `uv run pytest --cov=app --cov-report=term-missing` → 3402 passed, `app/` coverage **99%** (>90% gate); `uv run pytest tests/e2e/test_git_source_links.py -v --no-cov` → **4 passed** in isolation; `uv run ruff check .` → clean; `uv run pyright` → 0 errors, 0 warnings
- **Completion criteria:** (1) git read results carry origin/branch, local byte-identical — ✅ (unit pins + E2E control); (2) HIGH `<tools>` rule pinned, LOW byte-identical — ✅; (3) modal AND /document.html show Original link, branch in label when pinned, nothing for local — ✅; (4) full suite green + coverage + E2E + lint/types — ✅; (5) no completed-phase behavior change (read header line 1, mock prefix, done frame/SourceRef untouched, LOW sha) — ✅
- **Extra regression check:** re-ran shared-mock E2E suites `test_read_line_ranges.py` (2 passed) and `test_document_viewer.py` (8 passed) — no cross-suite regressions from the `mock_llm.py` addition
- **Documentation:** README.md reviewed in full (env table ⊇ `.env.example`, pages/routes, config defaults, scripts, 17-color palette, 300 s guard, 2 s sync poll, phase-145 provenance section matches implementation) and AGENTS.md (72rem container, no-CDN test, smoke test, gitignore, gates) — **verified consistent, no changes needed**
- **Notable:** phase-116 screenshot PNG was regenerated during this phase's E2E runs (byte re-encode only, visually identical, correct no-anchor local-source meta row) — left in tree for the harness commit
- **Next pending phase:** `146_search_tool_scope` (then 147, 148, 149)
2026-10-01 11:13:43 -04:00

851 lines
38 KiB
Python

"""Unit: document viewer (phase 10).
Python side:
* ``doc_format`` — format from the path suffix (incl. ``.markdown`` and the
no-suffix fallback) + the phase-102 extensionless name-token rule
(``Dockerfile`` → ``dockerfile`` with a configured token, suffixes still
unconditional, no-arg calls byte-identical to the suffix-only rule);
* the content endpoint's 200/404 mapping — tested WITHOUT a database by
stubbing the session via FastAPI's dependency override (unknown pairs and
traversal-style paths map to 404 ``{detail: "document not found"}``;
known pairs map to the full ``DocContent`` shape).
Frontend side:
* the viewer URL builder — its real query-encoding behavior (paths with
spaces/slashes) executed under node when available, plus source pins that
run everywhere;
* the shared-renderer extraction — ``markdown.js`` holds the renderer,
loaded by the pages that use it via a relative ``<script src>`` before
the module scripts;
* phase 26 — the shared ``renderDocument`` export in ``document.js``, the
import-safe page guard, the modal skeleton on chat + Sources, and the
modal module's close/focus/URL contract.
Phase 145 (task 03) — the content payload's git origin fields + the
``renderDocument`` "Original" anchor (modal AND /document.html, one
site):
* the ``DocContent`` omission rule — both origin keys ABSENT on the
wire when ``None`` (never ``null``; non-git payload byte-identical);
* the content endpoint (stubbed session) — a matching ``kind='git'``
row adds ``origin_url`` (+ ``origin_branch`` for a pinned row), a
legacy embedded-credential row is served sanitized, and local /
ambiguous sources keep the pre-phase key set;
* the anchor itself (source pins + a node behavioral check over a
minimal fake DOM) — ``createElement("a")``, the full URL verbatim as
``href`` AND visible text (the copyable-link contract),
``target="_blank" rel="noopener"``, the ``title`` = full URL (+
`` — branch: <name>`` when pinned), inserted AFTER the path code and
BEFORE the Created badge (absent → the pre-phase six children), no
innerHTML/outerHTML (the module's ONE innerHTML survives), and the
``.doc-origin`` CSS mirroring the ``.doc-path`` ellipsis idiom with
the ``.doc-link`` affordance (+ the ≤640px squeeze).
"""
from __future__ import annotations
import json
import re
import shutil
import subprocess
from datetime import UTC, datetime
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from app.api.docs import doc_format
from app.db import get_db
from app.main import create_app
from app.models import Document, GitSource
from app.schemas import DocContent
from tests.conftest import ADMIN_PASSWORD
FRONTEND = Path(__file__).resolve().parents[2] / "frontend"
APP_JS = FRONTEND / "assets" / "app.js"
SOURCES_JS = FRONTEND / "assets" / "sources.js"
DOCUMENT_JS = FRONTEND / "assets" / "document.js"
MARKDOWN_JS = FRONTEND / "assets" / "markdown.js"
MODAL_JS = FRONTEND / "assets" / "document-modal.js" # phase 26: the modal owner
INDEX_HTML = FRONTEND / "index.html" # the ONE-document shell (phase 76: sources.html folded in)
HAVE_NODE = shutil.which("node") is not None
def _read(path: Path) -> str:
return path.read_text(encoding="utf-8")
# ---------------------------------------------------------------------------
# doc_format — format-from-suffix (phase 10, PLAN §4)
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
("path", "expected"),
[
("kubernetes.md", "md"),
("notes/sub/deep.markdown", "markdown"),
("NOTES/ARCHIVE.MD", "md"),
("homelab/container_gitlab/gitlab-compose.yaml", "yaml"),
("homelab/networking/static-dns.json", "json"),
("homelab/scripts/uptime_probe.py", "py"),
("homelab/ssh/ssh_aliases.txt", "txt"),
("noext", "text"), # no suffix → fallback
("a/b", "text"), # no suffix → fallback
],
)
def test_doc_format_from_suffix(path: str, expected: str) -> None:
assert doc_format(path) == expected
def test_doc_format_no_args_extensionless_falls_back_to_text() -> None:
"""The no-arg contract: default ``extensions=frozenset()`` keeps the
pre-phase-102 result for every path — an extensionless name that LOOKS
like a configured token still badges ``text`` without the token set."""
assert doc_format("Dockerfile") == "text"
assert doc_format("README") == "text"
#: A token set in the dotted form ``import_extension_set`` passes it.
_TOKEN_EXTS = frozenset({".md", ".dev", ".dockerfile", ".containerfile"})
@pytest.mark.parametrize(
("path", "extensions", "expected"),
[
# Name-token branch (phase 102): suffix-less name in the set.
("services/api/Dockerfile", _TOKEN_EXTS, "dockerfile"),
("DOCKERFILE", _TOKEN_EXTS, "dockerfile"), # case-insensitive name
("Containerfile", _TOKEN_EXTS, "containerfile"),
# Suffix precedence: display never depends on the import list.
("Dockerfile.dev", _TOKEN_EXTS, "dev"), # suffixed → its suffix rules
("readme.rst", _TOKEN_EXTS, "rst"), # out-of-scope suffix still badges
("notes/README.dev", _TOKEN_EXTS, "dev"),
("dockerfile.bak", _TOKEN_EXTS, "bak"), # lookalike → suffix, not name
("kubernetes.md", frozenset(), "md"), # empty set: suffix unconditional
# Extensionless names NOT in the set fall back to text — exact
# name only, no partial names.
("README", _TOKEN_EXTS, "text"),
("mydockerfile", _TOKEN_EXTS, "text"),
],
)
def test_doc_format_with_token_set(
path: str, extensions: frozenset[str], expected: str
) -> None:
assert doc_format(path, extensions) == expected
# ---------------------------------------------------------------------------
# Content endpoint mapping — stubbed session, no database required
# ---------------------------------------------------------------------------
class _FakeResult:
def __init__(self, row: object) -> None:
self._row = row
def first(self) -> object:
return self._row
class _FakeScalars:
"""The resolver's ``db.scalars(...).all()`` — only the git rows
ever reach the caller (the ``kind = 'git'`` WHERE clause is
emulated, the task-01 ``_GitOnlySession`` pattern)."""
def __init__(self, rows: tuple[GitSource, ...]) -> None:
self._rows = [row for row in rows if row.kind == "git"]
def all(self) -> list[GitSource]:
return self._rows
class _FakeSession:
def __init__(self, row: object, git_rows: tuple[GitSource, ...] = ()) -> None:
self._row = row
self._git_rows = git_rows
def execute(self, _stmt: object) -> _FakeResult:
return _FakeResult(self._row)
def scalars(self, _stmt: object) -> _FakeScalars:
return _FakeScalars(self._git_rows)
def _client_with_row(row: object, git_rows: tuple[GitSource, ...] = ()) -> TestClient:
"""Fresh app whose ``get_db`` dependency is a stub returning ``row``
(``None`` → no matching document row) and, for the phase-145
origin lookup, *git_rows* (default: no rows — the non-git corner).
Phase 79: the endpoint is user-gated, so the client signs in as
the admin first — these tests pin the CONTENT mapping (200/404/
422), not the auth contract (which ``test_auth_api.py`` pins)."""
app = create_app()
app.dependency_overrides[get_db] = lambda: _FakeSession(row, git_rows)
client = TestClient(app)
r = client.post("/api/login", json={"password": ADMIN_PASSWORD})
assert r.status_code == 204, f"admin login failed: {r.status_code} {r.text}"
return client
def test_content_unknown_pair_maps_to_404() -> None:
with _client_with_row(None) as client:
r = client.get(
"/api/documents/content",
params={"source": "Homelab", "path": "nope.md"},
)
assert r.status_code == 404
assert r.json() == {"detail": "document not found"}
def test_content_traversal_style_path_maps_to_404() -> None:
"""``../``-style values are just non-existent rows → 404, never a
file read (DB-only endpoint, no filesystem access)."""
with _client_with_row(None) as client:
r = client.get(
"/api/documents/content",
params={"source": "Homelab", "path": "../../etc/passwd"},
)
assert r.status_code == 404
assert r.json() == {"detail": "document not found"}
assert "root:" not in r.text # nothing leaked
def test_content_known_pair_maps_to_doc_content() -> None:
doc = Document(
source="Homelab",
path="notes/deep mark.md",
full_path="/tmp/deep mark.md",
title="Deep Mark",
content="# Deep Mark\n\nbody",
content_hash="f" * 64,
is_image=False, # phase 122: the stub session never applies defaults
)
doc.indexed_at = datetime(2026, 8, 22, 1, 2, 3, tzinfo=UTC)
doc.created_at = datetime(2026, 8, 20, 9, 0, 0, tzinfo=UTC) # phase 106
with _client_with_row((doc, 3)) as client:
r = client.get(
"/api/documents/content",
params={"source": "Homelab", "path": "notes/deep mark.md"},
)
assert r.status_code == 200
body = r.json()
# Wire-additive (phase 106, task 05): the pre-date keys are all
# still there, joined by ``created_at`` — and (phase 122, task 04)
# by ``is_image`` (ALWAYS present; text docs: false) while
# ``image_url`` is ABSENT (never null — the omission rule).
assert set(body) == {
"source", "path", "title", "format", "summary", "created_at",
"content", "indexed_at", "chunks", "is_image",
}
assert body["is_image"] is False
assert "image_url" not in body # absent — never null (text doc)
assert body["source"] == "Homelab"
assert body["path"] == "notes/deep mark.md"
assert body["title"] == "Deep Mark"
assert body["format"] == "md"
assert body["summary"] is None # markdown doc → no summary (phase 36)
assert body["content"] == "# Deep Mark\n\nbody"
assert body["created_at"] == "2026-08-20T09:00:00+00:00" # phase 106
assert body["indexed_at"] == "2026-08-22T01:02:03+00:00"
assert body["chunks"] == 3
def test_content_requires_both_params() -> None:
with _client_with_row(None) as client:
assert client.get("/api/documents/content").status_code == 422
assert client.get("/api/documents/content", params={"source": "Homelab"}).status_code == 422
# ---------------------------------------------------------------------------
# Phase 145 (task 03) — the content payload's git origin fields + the
# renderDocument "Original" anchor (modal AND /document.html, one site)
# ---------------------------------------------------------------------------
def _text_doc() -> Document:
"""The content-endpoint stub's document row (a markdown doc — no
summary, the pre-phase shape)."""
doc = Document(
source="Homelab",
path="notes/deep mark.md",
full_path="/tmp/deep mark.md",
title="Deep Mark",
content="# Deep Mark\n\nbody",
content_hash="f" * 64,
is_image=False, # phase 122: the stub session never applies defaults
)
doc.indexed_at = datetime(2026, 8, 22, 1, 2, 3, tzinfo=UTC)
doc.created_at = datetime(2026, 8, 20, 9, 0, 0, tzinfo=UTC)
return doc
def _text_doc_content(
origin_url: str | None = None, origin_branch: str | None = None
) -> DocContent:
"""One text doc's content payload (the pre-phase shape) with the
phase-145 origin fields optionally set."""
return DocContent(
source="S",
path="a.md",
title="A",
format="md",
created_at="2026-01-01T00:00:00+00:00",
content="body",
indexed_at="2026-01-01T00:00:00+00:00",
chunks=1,
origin_url=origin_url,
origin_branch=origin_branch,
)
def test_doc_content_origin_fields_absent_on_the_wire_when_unset() -> None:
"""The model-level omission rule (the :func:`_drop_absent_share_url`
precedent, the ``image_url`` shape): a non-git document (no
``origin_url``) serializes with BOTH origin keys ABSENT — never
``null`` — so the payload is byte-identical to pre-phase. A
sanitized-URL-only payload (an unpinned row) drops
``origin_branch``; a pinned row keeps both keys."""
dumped = _text_doc_content().model_dump()
assert "origin_url" not in dumped # absent — never null
assert "origin_branch" not in dumped # absent — never null
dumped = _text_doc_content(origin_url="https://host/x.git").model_dump()
assert dumped["origin_url"] == "https://host/x.git"
assert "origin_branch" not in dumped # NULL = the default branch — nothing to show
dumped = _text_doc_content(
origin_url="https://host/x.git", origin_branch="docs"
).model_dump()
assert dumped["origin_branch"] == "docs"
def test_content_origin_url_carried_for_a_matching_git_row() -> None:
"""The endpoint (stubbed session, no database): a ``kind='git'``
row whose URL basename matches the document's source adds
``origin_url`` — the sanitized clone URL — to the payload, and
every pre-phase key stands untouched. The row pins no branch →
``origin_branch`` is ABSENT (never ``null``)."""
row = GitSource(url="https://github.com/reese/Homelab.git", kind="git")
with _client_with_row((_text_doc(), 3), git_rows=(row,)) as client:
r = client.get(
"/api/documents/content",
params={"source": "Homelab", "path": "notes/deep mark.md"},
)
assert r.status_code == 200
body = r.json()
assert set(body) == {
"source", "path", "title", "format", "summary", "created_at",
"content", "indexed_at", "chunks", "is_image", "origin_url",
}
assert body["origin_url"] == "https://github.com/reese/Homelab.git"
assert "origin_branch" not in body # absent — never null (unpinned row)
def test_content_origin_branch_carried_for_a_pinned_row() -> None:
"""A pinned row (phase 135 ``branch``) also carries
``origin_branch`` — the branch the title attribute will show."""
row = GitSource(
url="https://github.com/reese/Homelab.git", kind="git", branch="docs"
)
with _client_with_row((_text_doc(), 3), git_rows=(row,)) as client:
r = client.get(
"/api/documents/content",
params={"source": "Homelab", "path": "notes/deep mark.md"},
)
body = r.json()
assert body["origin_url"] == "https://github.com/reese/Homelab.git"
assert body["origin_branch"] == "docs"
def test_content_origin_url_sanitizes_a_legacy_embedded_credential_row() -> None:
"""End-to-end (stubbed): a legacy row whose credential is still
embedded in the stored URL never leaks it — the payload carries
the phase-121 sanitized URL (the resolver's guarantee, one mapping
for the whole app)."""
row = GitSource(url="https://user:tok@example.com/reese/Homelab.git", kind="git")
with _client_with_row((_text_doc(), 3), git_rows=(row,)) as client:
r = client.get(
"/api/documents/content",
params={"source": "Homelab", "path": "notes/deep mark.md"},
)
body = r.json()
assert body["origin_url"] == "https://example.com/reese/Homelab.git"
assert "user:tok" not in r.text # the credential stays out of the wire
def test_content_origin_fields_absent_for_local_and_ambiguous_sources() -> None:
"""No git row (a plain local source) OR two rows sharing the
basename (ambiguous — never guess) → both origin keys ABSENT from
the JSON (not ``null`` — the omission pinned on the wire; the
payload is byte-identical to pre-phase, the ``test_content_known_
pair_maps_to_doc_content`` key-set pin)."""
local_only = (_text_doc(), 3)
with _client_with_row(local_only) as client: # no git rows at all
r = client.get(
"/api/documents/content",
params={"source": "Homelab", "path": "notes/deep mark.md"},
)
assert r.status_code == 200
assert "origin_url" not in r.json()
assert "origin_branch" not in r.json()
ambiguous = (
GitSource(url="https://a.example/org/Homelab.git", kind="git"),
GitSource(url="git@b.example:other/Homelab.git", kind="git"),
)
with _client_with_row(local_only, git_rows=ambiguous) as client:
r = client.get(
"/api/documents/content",
params={"source": "Homelab", "path": "notes/deep mark.md"},
)
assert r.status_code == 200
assert "origin_url" not in r.json()
assert "origin_branch" not in r.json()
# ---------- the renderDocument "Original" anchor (the shared core) ----------
def _fn(js: str, name: str) -> str:
"""The source of a top-level function via balanced-brace counting
(the ``test_date_editor.py`` pattern)."""
start = js.find(f"function {name}(")
assert start != -1, f"{name}() must exist in document.js"
i = js.find("(", start)
depth = 0
close = i
while i < len(js):
if js[i] == "(":
depth += 1
elif js[i] == ")":
depth -= 1
if depth == 0:
close = i
break
i += 1
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}()")
def test_origin_anchor_is_a_copyable_external_link() -> None:
"""The anchor (house-style source contract): ``createElement("a")``
(the never-build-HTML rule), the ``doc-origin`` class, ``href =
doc.origin_url``, ``target="_blank" rel="noopener"`` (the modal's
"Full page" escape-hatch idiom), the full URL VERBATIM as the
visible text (the copyable-link contract — no decorative label),
and the ``title`` = the full URL, plus `` — branch: <name>`` when
the payload pins a branch (the pathCode precision idiom)."""
body = _fn(_read(DOCUMENT_JS), "originAnchor")
assert 'document.createElement("a")' in body
assert 'a.className = "doc-origin"' in body
assert "a.href = doc.origin_url" in body
assert 'a.target = "_blank"' in body
assert 'a.rel = "noopener"' in body
assert "a.textContent = doc.origin_url" in body, (
"the visible text is the full URL verbatim (copy-pasteable)"
)
branch_title = 'doc.origin_url + " — branch: " + doc.origin_branch'
assert 'doc.origin_branch ? ' + branch_title + " : doc.origin_url" in body, (
"the title carries the full URL (+ the pinned branch when present)"
)
# The never-build-HTML shape: no innerHTML/outerHTML in the new code.
code = re.sub(r"//.*?$|/\*.*?\*/", "", body, flags=re.S | re.M)
assert "innerHTML" not in code and "outerHTML" not in code
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_origin_anchor_behaves_for_branch_and_no_branch_variants() -> None:
"""Behavioral check (node, minimal fake DOM) of the real anchor
builder: the exact ``href``/``target``/``rel``/``textContent``/
``title`` for a git doc, branch and no-branch variants."""
fn = _fn(_read(DOCUMENT_JS), "originAnchor")
fake_dom = (
"globalThis.document = { createElement: (tag) => {"
' const el = { tag, className: "", href: "", target: "", rel: "",'
' textContent: "", setAttribute(k, v) { this[k] = v; } };'
" return el; } };"
)
out = _run_node(
fake_dom
+ "\n" + fn
+ '\nconst g1 = originAnchor({ origin_url: "https://github.com/reese/skills.git",'
' origin_branch: "main" });' + "\n"
+ '\nconst g2 = originAnchor({ origin_url: "https://github.com/reese/skills.git" });' + "\n"
+ "console.log(JSON.stringify(g1));\nconsole.log(JSON.stringify(g2));"
)
j1, j2 = (json.loads(line) for line in out.splitlines())
for j in (j1, j2):
assert j["tag"] == "a"
assert j["className"] == "doc-origin"
assert j["href"] == "https://github.com/reese/skills.git"
assert j["target"] == "_blank"
assert j["rel"] == "noopener"
# The full URL VERBATIM as the visible text (copy-pasteable).
assert j["textContent"] == "https://github.com/reese/skills.git"
# The title: the full URL, + the pinned branch when present.
assert j1["title"] == "https://github.com/reese/skills.git — branch: main"
assert j2["title"] == "https://github.com/reese/skills.git"
def test_origin_anchor_sits_after_the_path_code_before_the_created_badge() -> None:
"""renderDocument (the ONE shared core — modal AND /document.html):
the anchor joins the meta row AFTER the path ``code`` and BEFORE
the Created badge — and ONLY when the payload carries
``origin_url``. Absent (a non-git document — the field simply
missing on the wire) → the gate builds the empty array, the
spread adds nothing, and the meta row's children are EXACTLY the
pre-phase six (the existing badge-order pins pass unchanged)."""
render = _fn(_read(DOCUMENT_JS), "renderDocument")
gate_i = render.find("const originItems = doc.origin_url ? [originAnchor(doc)] : [];")
call_i = render.find("metaEl.replaceChildren(")
path_i = render.find("pathCode,", call_i)
spread_i = render.find("...originItems,", call_i)
created_i = render.find('metaBadge("doc-created"', call_i)
assert -1 < gate_i < call_i < path_i < spread_i < created_i, (
"the anchor gate precedes the row build and lands AFTER the path code, "
"BEFORE the Created badge"
)
# The gate: present only on a truthy origin_url — an ABSENT field
# (undefined) is falsy → [] → the pre-phase children stand.
assert "doc.origin_url ? [originAnchor(doc)] : []" in render
# The spread is the meta row's ONLY conditional child (the six
# pre-phase children are the fixed ones).
assert render[call_i:].count("...") == 1
def test_origin_anchor_keeps_the_module_xss_contract() -> None:
"""The module's ONE innerHTML (the escape-first markdown render)
survives — the new anchor adds no HTML-building of its own
(createElement + properties/textContent only)."""
code = re.sub(r"//.*", "", re.sub(r"/\*.*?\*/", "", _read(DOCUMENT_JS), flags=re.S))
assert code.count("innerHTML") == 1
assert "outerHTML" not in code
def test_doc_origin_css_mirrors_the_doc_path_ellipsis_idiom() -> None:
"""styles.css: the ``.doc-origin`` rule sits next to the
``.doc-path`` family (mono, 0.75rem, the same 26rem ellipsis
budget + 6rem floor) with the ``.doc-link`` affordance
(brand-ink, underline on hover/focus — the existing link
tokens), provenance-cited, and the ≤640px block squeezes it like
the path (16rem — the row's own wrap/clip rules are unchanged:
the anchor is one more inline item)."""
css = _read(FRONTEND / "assets" / "styles.css")
rule_i = css.find(".doc-origin {")
assert rule_i != -1, "styles.css must style .doc-origin"
# Provenance comment directly above the rule (house style).
comment_start = css.rfind("/*", 0, rule_i)
assert -1 < comment_start < rule_i and "Phase 145 (task 03)" in css[comment_start:rule_i]
# Next to the .doc-path family (before .doc-chunks).
assert css.find(".doc-path {") < rule_i < css.find(".doc-chunks {")
rule = css[rule_i : css.find("}", rule_i)]
for decl in (
"font-family: var(--mono)",
"font-size: 0.75rem",
"max-width: 26rem",
"white-space: nowrap",
"overflow: hidden",
"text-overflow: ellipsis",
"min-width: 6rem",
"color: var(--brand-ink)",
"text-decoration: none",
):
assert decl in rule, f".doc-origin must carry {decl!r}"
# The .doc-link hover/focus affordance (the existing link idiom).
assert ".doc-origin:hover, .doc-origin:focus-visible { text-decoration: underline; }" in css
# The ≤640px squeeze mirrors .doc-path's 16rem (same media block).
mobile = css.find("@media (max-width: 640px)")
path_sq = css.find(".doc-path { max-width: 16rem; }", mobile)
origin_sq = css.find(".doc-origin { max-width: 16rem; }", mobile)
close_640 = css.find("@media", mobile + 1)
assert -1 < path_sq < origin_sq < (close_640 if close_640 != -1 else len(css)), (
"the mobile block squeezes the anchor like the path"
)
assert "url(http" not in css and "@import url(" not in css, (
"no CDN (AGENTS.md rule 6)"
)
# ---------------------------------------------------------------------------
# Viewer URL builder — encoded query (spaces/slashes in real paths)
# ---------------------------------------------------------------------------
def test_viewer_url_builder_present_in_chat_and_sources() -> None:
"""Both entry points (chat chips, Sources rows) build the same
encoded viewer URL — kept as each link's ``href`` (no-JS /
context-menu escape hatch to the dedicated viewer).
Phase 13: the chat builder additionally carries ``back=/`` (encoded
%2F) so the viewer's back button returns to the chat; Sources links
intentionally omit the param (the viewer's /sources.html default).
Phase 26: the left-click no longer opens a new tab — it is
intercepted (preventDefault) and routed to openDocumentModal from
the shared modal module; no ``target="_blank"" survives on either
entry point."""
for name, js in (("app.js", _read(APP_JS)), ("sources.js", _read(SOURCES_JS))):
assert '"/document.html?source=" + encodeURIComponent(' in js, name
assert '"&path=" + encodeURIComponent(' in js, name
assert 'target = "_blank"' not in js, f"{name}: phase 26 — no new tabs"
app_js = _read(APP_JS)
# Chat: 3-arg builder with back defaulting to the chat page.
assert 'function documentUrl(source, path, back = "/")' in app_js
assert '"&back=" + encodeURIComponent(back)' in app_js
assert 'chip.href = documentUrl(s.source, s.path, "/")' in app_js
assert 'chip.addEventListener("click"' in app_js
assert "e.preventDefault()" in app_js
assert "openDocumentModal(s.source, s.path, chip)" in app_js
sources_js = _read(SOURCES_JS)
# Sources: unchanged 2-arg builder — no back param in the URL.
assert "function documentUrl(source, path)" in sources_js
assert 'link.className = "doc-link"' in sources_js
assert "link.href = documentUrl(d.source, d.path)" in sources_js
assert 'link.addEventListener("click"' in sources_js
assert "openDocumentModal(d.source, d.path, link)" in sources_js
# The full path stays the hover name on the ellipsized cell AND the link.
assert "pathTd.title = d.path" in sources_js
assert "link.title = d.path" in sources_js
def _run_node(script: str) -> str:
proc = subprocess.run(["node", "-e", script], capture_output=True, text=True, timeout=60)
assert proc.returncode == 0, f"node failed: {proc.stderr}"
return proc.stdout
def _extract_function(js: str, name: str) -> str:
match = re.search(
rf"(?:export )?function {name}\(source, path(?:, back = \"/\")?\) \{{.*?\n\}}", js, re.S
)
assert match, f"{name}(source, path) not found"
return match.group(0).replace("export ", "", 1)
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_viewer_url_builder_encodes_spaces_and_slashes() -> None:
"""Behavioral check of the real builder (app.js) under node: slashes
and spaces in source/path values must come out percent-encoded, and
the back target is appended + encoded (phase 13)."""
fn = _extract_function(_read(APP_JS), "documentUrl")
out = _run_node(
f"{fn}\n"
"console.log(documentUrl('Homelab', 'kubernetes.md'));\n"
"console.log(documentUrl('Homelab', 'notes/my file.yaml'));\n"
"console.log(documentUrl('H omelab', 'a/b.md'));\n"
"console.log(documentUrl('Homelab', 'kubernetes.md', '/sources.html'));"
)
assert out.splitlines() == [
"/document.html?source=Homelab&path=kubernetes.md&back=%2F",
"/document.html?source=Homelab&path=notes%2Fmy%20file.yaml&back=%2F",
"/document.html?source=H%20omelab&path=a%2Fb.md&back=%2F",
"/document.html?source=Homelab&path=kubernetes.md&back=%2Fsources.html",
]
# ---------------------------------------------------------------------------
# Shared renderer extraction (phase 10 step 2)
# ---------------------------------------------------------------------------
def test_renderer_extracted_to_shared_markdown_js() -> None:
"""The renderer moved to assets/markdown.js (not duplicated in app.js)
and BOTH pages load it via a relative <script src> before their module
scripts — so the globals exist when app.js/document.js run."""
md = _read(MARKDOWN_JS)
assert "function renderMarkdown(md)" in md
assert "function escapeHtml(s)" in md
app_js = _read(APP_JS)
assert "function renderMarkdown" not in app_js, "renderer must live in markdown.js"
assert "function escapeHtml" not in app_js
for page in ("index.html", "document.html"):
html = _read(FRONTEND / page)
assert re.search(r'<script src="assets/markdown\.js"></script>', html), (
f"{page} must load markdown.js via a relative <script src>"
)
assert html.index('src="assets/markdown.js"') < html.index('type="module"'), (
f"{page}: markdown.js must load before the module script"
)
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_markdown_renderer_stays_xss_safe_and_unchanged() -> None:
"""Behavioral check (node) that the extracted renderer still escapes
first: hostile content never becomes live HTML; basic transforms work."""
js = _read(MARKDOWN_JS)
out = _run_node(
js
+ "\nconsole.log(renderMarkdown('# Title\\n\\n<script>alert(1)</script>"
+ "\\n\\n**bold** and `code`'));"
)
html = out.strip()
assert "<script>" not in html # never live HTML
assert "&lt;script&gt;alert(1)&lt;/script&gt;" in html
assert "<strong>bold</strong>" in html
assert "<code>code</code>" in html
assert "<h3>Title</h3>" in html
# ---------------------------------------------------------------------------
# Phase 26 — shared renderDocument + the document modal wiring
# ---------------------------------------------------------------------------
def test_render_document_exported_and_modal_imports_it() -> None:
"""Phase 26: document.js EXPORTS renderDocument(doc, { … }) — the
exact renderer the standalone page and the modal share (no drift).
The modal module imports it relatively, and BOTH view scripts
(app.js — the chat view at shell boot — and sources.js — the RAG
view module) import the modal module relatively — no direct
<script> tag (the header.js single-evaluation design: esbuild
inlines it into the bundle, one module instance per document).
"""
doc_js = _read(DOCUMENT_JS)
# Task 03 signature: the page passes its #doc-title / #doc-meta /
# #doc-content elements under exactly these names.
assert "export function renderDocument(doc, { titleEl, metaEl, contentEl })" in doc_js, (
"document.js must export renderDocument(doc, { titleEl, metaEl, contentEl })"
)
# The standalone page renders through the SAME shared function with its
# own page elements (no second renderer copy).
assert "renderDocument(doc, { titleEl, metaEl, contentEl })" in doc_js
modal_js = _read(MODAL_JS)
assert 'from "./document.js"' in modal_js
assert "export function openDocumentModal(" in modal_js
assert "export function closeDocumentModal(" in modal_js
for name, js in (("app.js", _read(APP_JS)), ("sources.js", _read(SOURCES_JS))):
assert 'from "./document-modal.js"' in js, (
f"{name}: must import the modal module relatively"
)
text = _read(INDEX_HTML)
assert not re.search(r"<script[^>]*document-modal\.js", text), (
"index.html: no direct document-modal.js <script> tag "
"(single-evaluation design — the view scripts import it)"
)
def test_document_js_page_init_is_import_safe() -> None:
"""Phase 26: the /document.html-specific init (back-link resolution,
whoami, content load) runs ONLY when #doc-title exists — the modal
module's `import { renderDocument } from "./document.js"` on the
chat/sources pages must have no side effects."""
js = _read(DOCUMENT_JS)
guard = js.find('querySelector("#doc-title")')
back_href = js.find("backLink.href = backTarget")
load_call = js.rfind("load();")
assert 0 < guard < back_href < load_call, (
"the viewer-page init must sit inside the #doc-title guard "
"(after it, and load() must be the guarded entry point)"
)
def test_shell_carries_the_single_modal_skeleton() -> None:
"""Phase 26 + phase 76 (task 02) dedup pin: the shell ships the
modal skeleton EXACTLY ONCE (the chat's, body level) — the RAG
view's second copy was dropped in the fold; BOTH view scripts
(app.js chat chips, sources.js RAG row links) open documents
through openDocumentModal(...) against that single instance
(document-modal.js resolves it by document-level querySelector at
import). The a11y frame is included: role=dialog + aria-modal, a
labelled close control, a focusable content target (tabindex=-1),
and a role=status announcer. Hidden by default — inert until JS
opens it."""
text = _read(INDEX_HTML)
assert text.count('id="doc-modal"') == 1, (
"the shell must carry exactly ONE modal skeleton (the fold dedup)"
)
assert '<div class="doc-modal" id="doc-modal" hidden>' in text
assert 'id="doc-modal-backdrop"' in text
assert 'id="doc-modal-panel"' in text
assert 'role="dialog"' in text and 'aria-modal="true"' in text
assert 'id="doc-modal-title"' in text
assert 'id="doc-modal-meta"' in text
assert 'id="doc-modal-desc"' in text
assert 'id="doc-modal-open"' in text
assert re.search(r'id="doc-modal-content"[^>]*tabindex="-1"', text)
assert re.search(r'id="doc-modal-close"[^>]*aria-label="Close document"', text)
def test_shell_loads_markdown_before_its_modules() -> None:
"""Phase 26 + phase 76 (task 02): the modal renders md documents
in the RAG view too — so the shell loads the classic markdown.js
(global renderMarkdown) via a relative <script src> BEFORE its
module scripts (app.js — the chat view — and the lazy view
modules' modal imports), exactly as the old sources.html did."""
html = _read(INDEX_HTML)
assert re.search(r'<script src="assets/markdown\.js"></script>', html)
assert html.index('src="assets/markdown.js"') < html.index('type="module"'), (
"index.html: markdown.js must load before the module scripts"
)
def test_modal_close_contract_pins() -> None:
"""Phase 26: the modal closes on the close button, on backdrop
click, and on Escape (captured document-level, so it works from any
focus position); focus returns to the triggering control
(best-effort); the fetch goes to the stateless content endpoint with
the same percent-encoding the page uses, and success renders through
the shared renderDocument; the "Full page" link is rebuilt on open."""
js = _read(MODAL_JS)
assert 'e.key === "Escape"' in js
assert 'addEventListener("keydown"' in js
assert "backdropEl.addEventListener(\"click\", closeDocumentModal)" in js
assert "closeEl.addEventListener(\"click\", closeDocumentModal)" in js
assert "triggerEl.focus" in js # best-effort focus restore
assert '"/api/documents/content?source=" + encodeURIComponent(' in js
assert '"&path=" + encodeURIComponent(' in js
# The shared renderer (not a copy), called with the modal's own
# #doc-modal-title / #doc-modal-meta / #doc-modal-content elements.
assert "renderDocument(doc, { titleEl, metaEl, contentEl })" in js
assert 'openEl.href = fullPageUrl(source, path)' in js
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_modal_url_builders_encode_like_the_page() -> None:
"""Behavioral check (node) of the modal's own URL builders: the
content fetch and the "Full page" href must come out percent-encoded
exactly like the page's builders (slashes/spaces in real paths)."""
js = _read(MODAL_JS)
content_fn = _extract_function(js, "contentUrl")
full_fn = _extract_function(js, "fullPageUrl")
out = _run_node(
content_fn
+ full_fn
+ "\nconsole.log(contentUrl('Homelab', 'notes/my file.yaml'));\n"
+ "console.log(fullPageUrl('H omelab', 'a/b.md'));"
)
assert out.splitlines() == [
"/api/documents/content?source=Homelab&path=notes%2Fmy%20file.yaml",
"/document.html?source=H%20omelab&path=a%2Fb.md",
]
def test_viewer_js_rendering_contracts() -> None:
"""document.js: raw formats go in via textContent (never parsed as
HTML), markdown via the shared renderer, 404 → designed not-found
state, and (phase 13) the back link resolves the ``back`` param —
same-origin relative URLs only, /sources.html default, no browser
history heuristics (both entry points are fresh tabs)."""
js = _read(DOCUMENT_JS)
assert "pre.textContent = doc.content" in js # raw formats: text node
assert "renderMarkdown(doc.content)" in js # md/markdown: shared renderer
assert "showNotFound" in js
# Phase 13: deterministic back-target resolution, no history heuristics.
assert "history.length" not in js
assert "window.history.back" not in js
assert 'backParam.startsWith("/")' in js # same-origin relative only…
assert 'backParam.startsWith("//")' in js # …and not protocol-relative
assert 'backLink.href = backTarget' in js # deterministic anchor navigation
assert '"/sources.html"' in js # default target + no-JS fallback value
assert '"Chat"' in js and '"Sources"' in js # labels for the two entry points
assert "encodeURIComponent" in js # content fetch uses the same encoding