Files
brain-of-reese/tests/unit/test_document_viewer.py
T
ducoterra 6ba46e735a
Build and Push Containers / build-and-push-app (push) Successful in 2m3s
Build and Push Containers / build-and-push-db (push) Successful in 21s
phase: 163_pdf_strip_additive_selfheal
All gates verified. Final report:

**Phase 163 — final verification pass (all 5 tasks already complete; verification-only run)**

- Verified D1–D4 in code: `page_strip_mode` column + migration 0027, `DocContent` absent-never-null rule keyed off `page_count`; `_persist_pdf_pages` render → atomic tmp+`os.replace` writes → stale-tail-last + `strip_class` stamp; `_regate_pdf_strip` on both unchanged paths (mirror gate, heal, repair, fail-soft, one INFO line only when acting); viewer mode branch + Text/Pages toggle (Text default, `aria-pressed`, no innerHTML, lazy imgs).
- `uv run pytest --cov=app --cov-report=term-missing` → **3796 tests, exit 0, coverage 99%** (>90%)
- `uv run pytest tests/e2e/test_pdf_strip_additive_selfheal.py -v --no-cov` → **5 passed** in isolation
- `test_pdf_pages_in_sources + test_pictorial_pdf + test_garbage_pdf` → 10 passed; `test_document_viewer(_context) + test_image_documents` → 20 passed
- `uv run ruff check .` clean; `uv run pyright` → 0 errors; alembic **0027 downgrade→upgrade CLI round-trip clean** (+ integration pins)
- Completion criteria: additive view w/ toggle ✅ · primary DOM byte-identity ✅ · write-first order/mid-write-failure/atomic pins ✅ · heal + repair E2E ✅ · zero vision/LLM on fast path (mock-captured) ✅ · non-qualifying payload byte-identity ✅ · docs pass ✅
- **Documentation**: README.md + AGENTS.md reviewed in full — the phase's updates are accurate; all config-table defaults cross-checked against `app/config.py` (no drift), every page route, script, and E2E command referenced exists, every `.env.example` var documented — **verified consistent, no changes needed**.
- Notable: live `Letumdei` repair is owner-runtime post-deploy per D5 (next production Sync self-heals; no live changes made); the modified `.agents/screenshots/doc_modal_scrollbar_1280.png` is an E2E-regenerated artifact left for the pipeline commit.
- **Next pending phase:** `164_chat_error_honesty`
2026-10-07 13:57:30 -04:00

1813 lines
83 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 148 (task 02) — the content payload's rendered-page routes
(the viewer strip, task 03, reads from them): the ``DocContent``
omission rule — ``page_urls`` ABSENT on the wire when ``None`` (never
``null``; a non-qualifying doc's payload is byte-identical to
pre-phase) — and the content endpoint (stubbed session) — a
page-qualifying PDF row (``page_count`` set) adds ``page_urls`` = the
exact 1-based ``/api/documents/<id>/pages/<i>`` list over
``1..page_count``.
Phase 163 (task 01) — the content payload's STRIP CLASS signal
(``page_strip_mode``: ``"primary"`` = the fallback class, the strip IS
the view; ``"additive"`` = the pictorial class, text + a Text/Pages
toggle): the model-level omission rule (ABSENT when ``None``, never
``null``), and the endpoint (stubbed session) — the key rides
``page_urls`` for a qualifying row, a PRE-PHASE row (``page_count`` set,
mode NULL — the exact live ``Letumdei`` state) serializes it ABSENT while
keeping ``page_urls``, and the whole strip block keys off
``page_count``, so a row with a stray mode and no strip carries neither
key (every non-qualifying payload byte-identical to pre-phase).
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).
Phase 153 (task 03) — unit pins for the URL contract and the
copy-path strings (additions beside the task-01/02 pins; nothing
loosened):
* the phase-13 back-resolution contract, guarded BOTH as a byte-for-byte
source pin of the resolution block AND as a node behavioral run of
that exact extracted block (missing / empty / garbage /
protocol-relative / pseudo-protocol ``back`` → ``/sources.html`` +
the "Sources" label; ``/`` → the "Chat" label; any other same-origin
relative → the "Back" label — the phase-13 pins themselves are
untouched);
* the copy-path composition end-to-end under node — the modal's
``wireCopyPath`` click handler and the page's ``render()`` wiring
each send the EXACT ``source + "/" + path`` bytes to the (fake)
clipboard for a source/path pair with spaces + slashes, and the
rejected-clipboard failure prefills the fallback field with that
same composed string (success / failure strings + the prefill,
byte-for-byte);
* the ``document.html`` Copy-path static nodes — the page's
``header.doc-header`` (the ``.doc-copy-pop`` block's anchor —
absent, the float would fall back to the meta row and grow the
bar, the phase-12/34 breach) + the ``#doc-copy-announcer``
(role=status) + the ``#doc-meta`` row the shared core builds the
control into.
"""
from __future__ import annotations
import json
import re
import shutil
import subprocess
import uuid
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,
page_urls: list[str] | None = None,
page_strip_mode: str | None = None,
) -> DocContent:
"""One text doc's content payload (the pre-phase shape) with the
phase-145 origin fields + the phase-148 ``page_urls`` + the
phase-163 ``page_strip_mode`` 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,
page_urls=page_urls,
page_strip_mode=page_strip_mode,
)
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_doc_content_page_urls_absent_on_the_wire_when_unset() -> None:
"""Phase 148 (task 02) — the model-level omission rule (the
``image_url`` precedent): a non-qualifying document (``page_urls``
``None`` — a text doc, an image, a text-heavy PDF) serializes with
the key ABSENT — never ``null`` — so its payload is byte-identical
to pre-phase. A page-qualifying PDF keeps the exact route list."""
dumped = _text_doc_content().model_dump()
assert "page_urls" not in dumped # absent — never null
urls = ["/api/documents/d/pages/1", "/api/documents/d/pages/2"]
dumped = _text_doc_content(page_urls=urls).model_dump()
assert dumped["page_urls"] == urls # the exact 1-based route list
def test_doc_content_page_strip_mode_absent_on_the_wire_when_unset() -> None:
"""Phase 163 (task 01, LOCKED A2) — the model-level omission rule
(the ``image_url`` / ``page_urls`` precedent): no strip class
(``page_strip_mode`` ``None`` — every non-qualifying doc AND every
pre-phase row) serializes the key ABSENT — never ``null`` — so those
payloads are byte-identical to pre-phase. A stamped row carries the
class verbatim, beside ``page_urls``."""
dumped = _text_doc_content().model_dump()
assert "page_strip_mode" not in dumped # absent — never null
for mode in ("primary", "additive"):
dumped = _text_doc_content(
page_urls=["/api/documents/d/pages/1"], page_strip_mode=mode
).model_dump()
assert dumped["page_strip_mode"] == mode # the class verbatim
def _page_pdf_doc(
page_count: int | None = 2, page_strip_mode: str | None = None
) -> Document:
"""A page-QUALIFYING PDF row (phase 148): ``page_count`` set — the
endpoint derives ``page_urls`` from it (the exact 1-based list).
``page_strip_mode`` (phase 163) is the strip CLASS; ``None`` is the
PRE-PHASE row shape (the column NULL while the strip stands)."""
doc = Document(
id=uuid.uuid4(),
source="Homelab",
path="ikea/besta.pdf",
full_path="/tmp/besta.pdf",
title="BESTA frame (white) assembly",
content="[Page 1 illustration: the parts tray]…",
content_hash="e" * 64,
is_image=False, # phase 122: the stub session never applies defaults
page_count=page_count,
page_strip_mode=page_strip_mode,
)
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 test_content_page_urls_carried_for_a_page_qualifying_pdf() -> None:
"""Phase 148 (task 02) — the endpoint (stubbed session, no
database): a page-qualifying PDF row (``page_count`` set) carries
``page_urls`` = the EXACT 1-based
``/api/documents/<id>/pages/<i>`` list over ``1..page_count``
(the viewer strip's source, task 03 — a per-page render gap is
still listed; the route 404s that N). Every pre-phase key stands
untouched (``is_image`` false, ``image_url`` ABSENT — the PDF is
not an image doc; the document's TEXT stays user-gated, only the
page bytes are public)."""
doc = _page_pdf_doc()
with _client_with_row((doc, 4)) as client:
r = client.get(
"/api/documents/content",
params={"source": "Homelab", "path": "ikea/besta.pdf"},
)
assert r.status_code == 200
body = r.json()
assert body["page_urls"] == [
f"/api/documents/{doc.id}/pages/1",
f"/api/documents/{doc.id}/pages/2",
] # the exact 1-based list, count == page_count
assert body["is_image"] is False
assert "image_url" not in body # absent — never null (not an image doc)
# Phase 163 (task 01): the pre-phase row (mode NULL) keeps the strip
# and serializes the class ABSENT — the viewer treats that as
# "primary", i.e. today's DOM.
assert "page_strip_mode" not in body
@pytest.mark.parametrize("mode", ["primary", "additive"])
def test_content_page_strip_mode_carried_for_a_stamped_strip(mode: str) -> None:
"""Phase 163 (task 01, LOCKED A2/A3) — the endpoint (stubbed
session, no
database): a page-qualifying PDF whose row carries the class
(``"primary"`` = the fallback class, ``"additive"`` = the pictorial
class) carries ``page_strip_mode`` VERBATIM next to ``page_urls`` —
the signal the viewer branches on so an additive strip never hides
the text (task 04). Nothing else in the payload moves."""
doc = _page_pdf_doc(page_strip_mode=mode)
with _client_with_row((doc, 4)) as client:
r = client.get(
"/api/documents/content",
params={"source": "Homelab", "path": "ikea/besta.pdf"},
)
assert r.status_code == 200
body = r.json()
assert body["page_strip_mode"] == mode # the class verbatim
assert body["page_urls"] == [
f"/api/documents/{doc.id}/pages/1",
f"/api/documents/{doc.id}/pages/2",
] # the strip rides along, unchanged
def test_content_strip_block_keys_off_page_count_never_mode_alone() -> None:
"""Phase 163 (task 01) — the class never appears alone: the whole
strip block keys off ``page_count``, so a row whose column somehow
holds a mode while ``page_count`` is NULL serializes NEITHER
``page_strip_mode`` nor ``page_urls`` — the payload is byte-identical
to a plain non-qualifying document (there is no strip to show, and
the viewer must never be told a class for pages that do not exist)."""
doc = _page_pdf_doc(page_count=None, page_strip_mode="additive")
with _client_with_row((doc, 1)) as client:
r = client.get(
"/api/documents/content",
params={"source": "Homelab", "path": "ikea/besta.pdf"},
)
assert r.status_code == 200
body = r.json()
assert "page_strip_mode" not in body # absent — the strip block is off
assert "page_urls" not in body # absent — never null
def test_content_non_qualifying_payload_key_set_is_byte_identical() -> None:
"""Phase 163 (task 01) — the byte-identity pin: a non-qualifying PDF
row (``page_count`` NULL, mode NULL) carries EXACTLY the pre-phase
key set — neither strip key joins it (``is_image`` always present,
``image_url`` / the origin keys / both strip keys absent)."""
doc = _page_pdf_doc(page_count=None)
with _client_with_row((doc, 1)) as client:
r = client.get(
"/api/documents/content",
params={"source": "Homelab", "path": "ikea/besta.pdf"},
)
assert r.status_code == 200
assert set(r.json()) == {
"source",
"path",
"title",
"format",
"summary",
"created_at",
"content",
"indexed_at",
"chunks",
"is_image",
}
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:
pat = rf"(?:export )?function {name}\(source, path(?:, back = (?:\"/\"|null))?\) \{{.*?\n\}}"
match = re.search(pat, 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, currentBack)' 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).
Phase 153 (task 01): the "Full page" builder's optional ``back``
(default null) — a null/absent ``back`` keeps the URL byte-identical
to pre-phase (no ``&back=`` at all — the Sources path), and a truthy
``back`` is appended + percent-encoded (``"/"`` → ``%2F``)."""
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'));\n"
+ "console.log(fullPageUrl('s', 'p'));\n"
+ "console.log(fullPageUrl('s', 'p', '/'));\n"
+ "console.log(fullPageUrl('H omelab', 'a/b.md', '/sources.html'));"
)
assert out.splitlines() == [
"/api/documents/content?source=Homelab&path=notes%2Fmy%20file.yaml",
# pre-phase byte-identical: no ``back`` arg → no ``&back=``
"/document.html?source=H%20omelab&path=a%2Fb.md",
"/document.html?source=s&path=p",
"/document.html?source=s&path=p&back=%2F",
"/document.html?source=H%20omelab&path=a%2Fb.md&back=%2Fsources.html",
]
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
def _stmt_block(js: str, start_marker: str, last_stmt: str) -> str:
"""Extract the source of a statement group: from ``start_marker``
to the balanced close of the ``{…}`` opened by ``last_stmt``
(the brace walk is the house style — no third-party parsers)."""
start = js.find(start_marker)
assert start != -1, f"marker not found: {start_marker!r}"
stmt_i = js.find(last_stmt, start)
assert stmt_i != -1, f"statement not found: {last_stmt!r}"
brace_i = js.find("{", stmt_i)
depth = 0
for j in range(brace_i, 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 from {last_stmt!r}")
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_phase13_back_resolution_guard() -> None:
"""The phase-13 back-resolution contract, guarded in BOTH layers
(task 03; the phase-13 pins in ``test_viewer_js_rendering_
contracts`` are untouched — this runs beside them):
1. SOURCE — the resolution block itself, extracted from the live
``document.js`` and pinned byte-for-byte (the exact statements,
including the two-label ternary);
2. BEHAVIOR — that exact extracted block (not a re-implementation)
run under node over a fake ``params``/``backLink``:
* missing / empty / garbage (absolute cross-origin) /
protocol-relative / pseudo-protocol ``back`` → the
``/sources.html`` default + the "Sources" label;
* ``/`` → the same-origin chat root WINS + the "Chat" label;
* any other same-origin relative → wins verbatim + the
"Back" label (and an explicit ``/sources.html`` keeps the
"Sources" label).
A refactor that re-introduces a browser-history heuristic, an
origin check, or a different label mapping fails the behavior run
(and a reformat of the block fails the byte pin)."""
js = _read(DOCUMENT_JS)
block = _stmt_block(
js,
'const backParam = params.get("back") || "";',
"if (backLabel) {",
)
# 1. the byte-for-byte source pin (the exact statements)
assert 'const backParam = params.get("back") || "";' in block
assert 'backParam.startsWith("/") && !backParam.startsWith("//")' in block
assert "? backParam" in block and ': "/sources.html";' in block
assert "backLink.href = backTarget;" in block
assert 'backTarget === "/"' in block # the label ternary…
assert '"Chat"' in block
assert 'backTarget === "/sources.html"' in block
assert '"Sources"' in block
assert '"Back"' in block # …and the three labels
# 2. the behavior — the EXACT extracted block, run under node
script = (
"const block = " + json.dumps(block) + ";\n"
"function run(v) {\n"
" const params = { get: (k) => (k === 'back' ? v : null) };\n"
" const label = { textContent: '' };\n"
" const backLink = { href: '',"
' querySelector: (s) => (s === "span" ? label : null) };\n'
' const f = new Function("params", "backLink",'
' block + "\\nreturn backTarget;");\n'
" const target = f(params, backLink);\n"
" return [target, label.textContent];\n"
"}\n"
"const cases = [null, '', 'https://evil.com/x', '//evil.com',"
" 'javascript:alert(1)', '/', '/sources.html', '/chat?x=1'];\n"
"for (const v of cases) console.log(JSON.stringify([v, ...run(v)]));\n"
)
out = [json.loads(line) for line in _run_node(script).splitlines()]
assert out == [
[None, "/sources.html", "Sources"], # missing
["", "/sources.html", "Sources"], # empty
["https://evil.com/x", "/sources.html", "Sources"], # garbage (cross-origin)
["//evil.com", "/sources.html", "Sources"], # protocol-relative
["javascript:alert(1)", "/sources.html", "Sources"], # pseudo-protocol
["/", "/", "Chat"], # the chat root wins
["/sources.html", "/sources.html", "Sources"], # explicit, same label
["/chat?x=1", "/chat?x=1", "Back"], # other same-origin relative wins
]
# ---------------------------------------------------------------------------
# Phase 153 (task 02) — "Copy path" (the standalone titlebar meta row +
# the modal meta block — one shared-core control, per-surface wiring)
# ---------------------------------------------------------------------------
DOCUMENT_MODAL_JS = _read(MODAL_JS) # the modal owner (the live asset)
COPY_PATH_STR = 'doc.source + "/" + doc.path' # the copied string (ASSUMPTION A1)
STATUS_TERNARY = (
'status.textContent = ok ? "Path copied." : "Path ready — copy it from the field."'
) # the status wording pair (owner-locked fallback wording, path-adapted)
def _fn_async(js: str, name: str) -> str:
"""Like _fn, but includes a leading ``async `` when the function is
async (the phase-153 helpers are async)."""
start = js.find(f"function {name}(")
assert start != -1, f"{name}() must exist"
if js[max(0, start - 6) : start] == "async ":
start -= 6
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_copy_path_button_built_in_the_shared_core() -> None:
"""renderDocument (the ONE shared core — modal AND /document.html):
the Copy-path control joins the meta row AFTER the path code and
BEFORE the origin anchor (one build site — the surfaces can never
drift). The button is a labeled icon+text control: type=button,
class doc-copy-path, an aria-hidden SVG (createElementNS — the
never-build-HTML rule stands: the module's ONE innerHTML stays the
escape-first markdown render) and the VISIBLE "Copy path" text as
the accessible name (no redundant aria-label)."""
fn = _fn(_read(DOCUMENT_JS), "copyPathButton")
assert 'document.createElement("button")' in fn
assert "b.type = \"button\"" in fn
assert 'b.className = "doc-copy-path"' in fn
assert "document.createElementNS(SVG_NS, \"svg\")" in fn
assert 'icon.setAttribute("aria-hidden", "true")' in fn
assert 'label.textContent = "Copy path"' in fn
assert "innerHTML" not in re.sub(r"//.*?$|/\*.*?\*/", "", fn, flags=re.S | re.M)
rd = _fn(_read(DOCUMENT_JS), "renderDocument")
assert "copyPathButton()" in rd
# meta row order: path code → copy button → origin anchor (spread) →
# created badge (the phase-145 pin's slot, unchanged).
code_i = rd.find("pathCode,")
btn_i = rd.find("copyBtn,")
spread_i = rd.find("...originItems,")
created_i = rd.find("metaBadge(\"doc-created\"")
assert 0 <= code_i < btn_i < spread_i < created_i
def test_page_copy_path_helper_pins() -> None:
"""document.js's page-local helper (the per-page duplication house
style — the modal module keeps its own copy; no shared module):
* the copied string is `doc.source + "/" + doc.path` (the read-tool
form — the same two values the meta row renders);
* navigator.clipboard.writeText in a try (a non-secure http origin
rejecting the clipboard is the fallback trigger);
* the status ternary: "Path copied." (success, visual-only —
aria-hidden, the toast idiom) / "Path ready — copy it from the
field." (failure — the owner-locked fallback wording,
path-adapted);
* a re-click re-attempts (the previous .doc-copy-pop block is
removed first — a failed copy never clears the previous state
into nothing);
* the success status auto-clears ~4s (the block is dropped if it
is still connected);
* the page's #doc-copy-announcer (role=status) reads the outcome
(one a11y announcer — the visible node is aria-hidden);
* the fallback field: input-like (type text, readOnly, class
doc-path-fallback, prefilled with the path, selects its content
on focus, auto-focused with preventScroll — the
.share-link-fallback pattern);
* the page's state floats in a .doc-copy-pop block anchored under
the button (in the sticky header — the meta row never wraps or
grows the bar, the phase-12/34 contract)."""
js = _read(DOCUMENT_JS)
assert "async function copyPathWithFallback(btn, path)" in js
assert "function makePathFallbackField(path)" in js
assert 'document.querySelectorAll(".doc-copy-pop").forEach((el) => el.remove());' in js
assert "await navigator.clipboard.writeText(path);" in js
try_i = js.find("try {\n await navigator.clipboard.writeText(path);\n } catch")
assert try_i != -1, "the writeText call must sit in a try (the rejection fallback)"
assert js.count(STATUS_TERNARY) == 1
assert "status.setAttribute(\"aria-hidden\", \"true\")" in js # visual-only status
assert 'document.querySelector("#doc-copy-announcer")' in js
assert "if (block.isConnected) block.remove();" in js
assert "}, 4000);" in js # the auto-clear beat
assert js.count(COPY_PATH_STR) == 1 # the copied string, wired in render()
assert 'metaEl.querySelector(".doc-copy-path")' in js # the render() wiring
# the fallback field (makePathFallbackField)
f = _fn(js, "makePathFallbackField")
assert 'document.createElement("input")' in f
assert "field.type = \"text\"" in f
assert "field.readOnly = true" in f
assert 'field.className = "doc-path-fallback"' in f
assert "field.value = path" in f
assert 'field.setAttribute("aria-label", "Document path")' in f
assert "field.addEventListener(\"focus\", () => field.select())" in f
# the floating block (page surface)
p = _fn_async(js, "copyPathWithFallback")
assert 'block.className = "doc-copy-pop"' in p
assert 'document.querySelector("header.doc-header")' in p
assert "br.bottom - hr.top + 6" in p # anchored under the button
assert "field.focus({ preventScroll: true })" in p
def test_modal_copy_path_helper_pins() -> None:
"""document-modal.js's page-local helper (the per-page duplication
house style):
* the copied string is `doc.source + "/" + doc.path` (the read-tool
form — the same two values the meta row renders);
* writeText in a try; the status ternary (the same owner-locked
wording as the page);
* the nodes land right after the "Copy path" control
(insertAdjacentElement("afterend") — the meta block wraps, so
they may take a new row);
* a re-click re-attempts (the previous .doc-copy-status /
.doc-path-fallback state is removed first — a failed copy never
clears the previous state into nothing);
* success auto-clears ~4s; #doc-modal-desc (setDesc) reads the
outcome (one a11y announcer — the visible node is aria-hidden);
* the renderDocument call is followed by the wireCopyPath wiring
(the control is live on every open)."""
js = DOCUMENT_MODAL_JS
assert "async function copyPathWithFallback(metaEl, path)" in js
assert "function wireCopyPath(metaEl, doc)" in js
assert "let modalCopyTimer = 0" in js
assert "await navigator.clipboard.writeText(path);" in js
assert js.count(STATUS_TERNARY) == 1
assert 'status.setAttribute("aria-hidden", "true")' in js
assert js.count('".doc-copy-status, .doc-path-fallback"') == 1 # the re-attempt cleanup
assert js.count('insertAdjacentElement("afterend"') == 3 # 1 success + 2 failure
assert 'setDesc("Path copied.")' in js
assert 'setDesc("Path ready — copy it from the field.")' in js
assert "if (status.isConnected) status.remove();" in js
assert "}, 4000);" in js
assert js.count(COPY_PATH_STR) == 1 # wired in wireCopyPath
assert 'metaEl.querySelector(".doc-copy-path")' in js
# renderDocument → wireCopyPath, in openDocumentModal's fetch .then
# (the LAST occurrence — the docstring names the render earlier):
rd_i = js.rfind("renderDocument(doc, { titleEl, metaEl, contentEl });")
wire_i = js.rfind("wireCopyPath(metaEl, doc)")
assert rd_i != -1 and wire_i != -1
assert 0 < wire_i - rd_i < 300 # right after the render
# the shared core builds the button for BOTH surfaces — the modal
# module never builds one itself (no per-surface copy):
assert "copyPathButton" not in js
def test_document_html_carries_the_copy_announcer() -> None:
"""The /document.html page's role=status live region for the copy
outcome — visually hidden (the visible status line is the
aria-hidden floating node; this node is the sole a11y announcer,
the share pattern)."""
html = (FRONTEND / "document.html").read_text(encoding="utf-8")
i = html.find('id="doc-copy-announcer"')
assert i != -1
node = html[i - 200 : i + 200]
assert "visually-hidden" in node
assert "role=\"status\"" in node
assert "aria-live" in node
def test_document_html_carries_the_copy_path_static_nodes() -> None:
"""The ``document.html`` Copy-path static nodes (task 03 — the
Copy-path node on the full page): the floating ``.doc-copy-pop``
block is anchored in the page's ``header.doc-header`` (absent,
the page helper's ``if (header)`` guard would drop the block and
the status would never float — the meta row is never grown, the
phase-12/34 contract); the ``#doc-copy-announcer`` (role=status,
the sole a11y announcer) and the ``#doc-meta`` row (where the
shared core builds the control) must all be present for the
page surface to work."""
html = (FRONTEND / "document.html").read_text(encoding="utf-8")
assert '<header class="doc-header">' in html, (
"the page's sticky header is the copy block's anchor"
)
assert 'id="doc-copy-announcer"' in html, "the announcer node is missing"
assert 'role="status"' in html, "the announcer must be a live region"
assert 'id="doc-meta"' in html, "the meta row (the control's home) is missing"
def test_copy_path_css() -> None:
"""The Copy-path styles (phase 153, task 02):
* .doc-copy-path — the row-action language (inline-flex icon +
label), neutral secondary fill, brand-soft hover pair,
focus-visible outline; the ≤640px block gives it the ≥44px
touch-target floor (PLAN §7.1);
* .doc-copy-status — the visible status (ink-soft on surface —
the meta row's 5.1:1 pair), serving BOTH surfaces;
* .doc-copy-pop — the page's floating state block (position
absolute in the sticky header — never grows the bar), z-index
30 inside the header's z-20 root context;
* .doc-path-fallback — the .share-link-fallback pattern (input
variant): mono, ellipsis, surface fill, --line border."""
css = (FRONTEND / "assets" / "styles.css").read_text(encoding="utf-8")
bp = css.find(".doc-copy-path {")
assert bp != -1
b = css[bp : css.find("}", bp)]
assert "display: inline-flex" in b
assert "gap: 0.3rem" in b
assert "cursor: pointer" in b
assert ".doc-copy-path svg" in css
assert ".doc-copy-path:hover" in css
assert ".doc-copy-path:focus-visible" in css
assert ".doc-copy-path { min-height: 44px; min-width: 44px; }" in css # mobile floor
sp = css.find(".doc-copy-status {")
assert sp != -1
s = css[sp : css.find("}", sp)]
assert "color: var(--ink-soft)" in s
pp = css.find(".doc-copy-pop {")
assert pp != -1
p = css[pp : css.find("}", pp)]
assert "position: absolute" in p
assert "z-index: 30" in p
fp = css.find(".doc-path-fallback {")
assert fp != -1
f = css[fp : css.find("}", fp)]
assert "font-family: var(--mono)" in f
assert "text-overflow: ellipsis" in f
assert ".doc-path-fallback:hover" in css
assert ".doc-path-fallback:focus-visible" in css
def _copy_path_page_fixture() -> str:
"""The shared node fixture: a fake DOM (elements + query registry +
tracked pop blocks) + a fake clipboard (resolves or rejects) + a
fake timer (captures the fn + delay)."""
return """
function makeEl(tag) {
const el = {
tag, children: [], attrs: {}, style: {},
className: "", type: "", readOnly: false, value: "", title: "",
isConnected: true, focusCount: 0, selectCount: 0, listeners: {},
setAttribute(k, v) { this.attrs[k] = v; },
appendChild(c) { this.children.push(c); return c; },
append(...cs) { this.children.push(...cs); },
insertAdjacentElement(pos, node) {
if (pos === "afterend") this.after.unshift(node);
return node;
},
addEventListener(ev, fn) { (this.listeners[ev] = this.listeners[ev] || []).push(fn); },
focus() { this.focusCount += 1; (this.listeners.focus || []).forEach((f) => f()); },
select() { this.selectCount += 1; },
remove() { this.isConnected = false; },
getBoundingClientRect() {
return { top: 10, left: 20, bottom: 44, right: 90, width: 70, height: 34 };
},
};
el.after = [];
return el;
}
const created = [];
const registry = {};
let written = [];
let reject = false;
let timerFn = null;
let timerDelay = null;
globalThis.setTimeout = (fn, delay) => { timerFn = fn; timerDelay = delay; return 1; };
globalThis.clearTimeout = () => {};
// Node 24's globalThis.navigator is GETTER-ONLY (a plain assignment
// silently fails in sloppy mode) — defineProperty is the only way to
// stand up the fake clipboard.
Object.defineProperty(globalThis, "navigator", {
configurable: true,
value: {
clipboard: {
writeText: async (s) => { written.push(s); if (reject) throw new Error("denied"); },
},
},
});
globalThis.document = {
createElement: (t) => { const el = makeEl(t); created.push(el); return el; },
createElementNS: (ns, t) => makeEl(t),
querySelector: (sel) => {
if (!registry[sel]) registry[sel] = makeEl("root:" + sel);
return registry[sel];
},
querySelectorAll: (sel) =>
sel === ".doc-copy-pop"
? created.filter((c) => c.className === "doc-copy-pop" && c.isConnected)
: [],
};
let copyStatusTimer = 0;
"""
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_page_copy_path_behavior_under_node() -> None:
"""The page's copyPathWithFallback under node (a fake DOM + a fake
clipboard):
* success — the clipboard receives the EXACT `source + "/" + path`
string (spaces + slashes intact — the read-tool form,
ASSUMPTION A1), the visible status is "Path copied." (the
aria-hidden node), the announcer reads the same text, the block
is anchored under the button in the header (top/left computed
from the rects), and the auto-clear timer is scheduled at 4000
ms and drops the block when it fires;
* failure — no write, the pre-filled readOnly fallback field
(doc-path-fallback, aria-labelled "Document path") is
auto-focused and selects its content on focus, the status reads
"Path ready — copy it from the field." (the owner-locked
wording, path-adapted) and the announcer reads it too;
* a re-click re-attempts (failure again → exactly one connected
state block; then success → the field is replaced by the
success status — never cleared into nothing)."""
js = _copy_path_page_fixture()
js += _fn(_read(DOCUMENT_JS), "makePathFallbackField")
js += "\n" + _fn_async(_read(DOCUMENT_JS), "copyPathWithFallback")
js += """
async function main() {
let out = {};
const header = document.querySelector("header.doc-header");
const btn = document.createElement("button");
const ok1 = await copyPathWithFallback(btn, "H omelab/a/b c.md");
const pops = () => created.filter((c) => c.className === "doc-copy-pop");
const pop1 = pops()[pops().length - 1];
out = {
ok1,
written: written.slice(),
popClass: pop1.className,
inHeader: header.children.includes(pop1),
top: pop1.style.top,
left: pop1.style.left,
statusText: pop1.children.find((c) => c.className === "doc-copy-status").textContent,
statusAria: pop1.children.find((c) => c.className === "doc-copy-status").attrs["aria-hidden"],
announcer: registry["#doc-copy-announcer"].textContent,
timerDelay,
connected: pops().filter((p) => p.isConnected).length,
};
timerFn(); // the auto-clear fires
out.cleared = !pop1.isConnected;
reject = true;
const ok2 = await copyPathWithFallback(btn, "H omelab/a/b c.md");
const pop2 = pops().filter((p) => p.isConnected)[0];
const field = pop2.children.find((c) => c.tag === "input");
out.ok2 = ok2;
out.fieldValue = field.value;
out.fieldType = field.type;
out.fieldReadOnly = field.readOnly;
out.fieldClass = field.className;
out.fieldAria = field.attrs["aria-label"];
out.fieldTitle = field.title;
out.fieldFocus = field.focusCount;
out.fieldSelect = field.selectCount;
out.failStatus = pop2.children.find((c) => c.className === "doc-copy-status").textContent;
out.failAnnouncer = registry["#doc-copy-announcer"].textContent;
out.connectedAfterFail = pops().filter((p) => p.isConnected).length;
await copyPathWithFallback(btn, "H omelab/a/b c.md"); // re-attempt (fail again)
out.connectedAfterRetry = pops().filter((p) => p.isConnected).length;
reject = false;
const ok3 = await copyPathWithFallback(btn, "x/y.md"); // re-attempt (success)
out.ok3 = ok3;
out.lastWritten = written[written.length - 1];
const pop3 = pops().filter((p) => p.isConnected)[0];
out.successReplacesField = !pop3.children.some((c) => c.tag === "input");
console.log(JSON.stringify(out));
}
main().catch((e) => { console.error("FAIL " + e.message); process.exit(1); });
"""
r = _run_node(js)
out = json.loads(r)
assert out["ok1"] is True
assert out["written"] == ["H omelab/a/b c.md"] # the exact source/path form
assert out["popClass"] == "doc-copy-pop"
assert out["inHeader"] is True
assert out["top"] == "40px" and out["left"] == "0px" # anchored under the button
assert out["statusText"] == "Path copied."
assert out["statusAria"] == "true" # visual only
assert out["announcer"] == "Path copied." # the a11y region
assert out["timerDelay"] == 4000
assert out["connected"] == 1
assert out["cleared"] is True # the auto-clear dropped the block
assert out["ok2"] is False
assert out["fieldValue"] == "H omelab/a/b c.md" # prefilled with the path
assert out["fieldType"] == "text"
assert out["fieldReadOnly"] is True
assert out["fieldClass"] == "doc-path-fallback"
assert out["fieldAria"] == "Document path"
assert out["fieldTitle"] == "Document path — click, then copy (Ctrl/Cmd+C)"
assert out["fieldFocus"] == 1 # auto-focused (selects the path)
assert out["fieldSelect"] == 1 # the focus listener selected the content
assert out["failStatus"] == "Path ready — copy it from the field."
assert out["failAnnouncer"] == "Path ready — copy it from the field."
assert out["connectedAfterFail"] == 1
assert out["connectedAfterRetry"] == 1 # the re-click replaced, never cleared
assert out["ok3"] is True
assert out["lastWritten"] == "x/y.md"
assert out["successReplacesField"] is True # the field was swapped for the status
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_modal_copy_path_behavior_under_node() -> None:
"""The modal's copyPathWithFallback under node (a fake DOM + a
fake clipboard + the setDesc announcer):
* success — the clipboard receives the EXACT `source + "/" + path`
string, the "Path copied." status lands right after the control
(insertAdjacentElement("afterend")), the visible node is
aria-hidden, #doc-modal-desc reads the outcome, and the
auto-clear timer (4000 ms) drops the status when it fires;
* failure — the pre-filled readOnly fallback field + the
"Path ready — copy it from the field." status land in DOM order
[field, status] right after the control (the meta block wraps),
the field is auto-focused and selects its content on focus, and
#doc-modal-desc reads the outcome;
* a re-click re-attempts (the previous state is removed first —
exactly one connected state pair after the retry — a failed
copy never clears the previous state into nothing)."""
js = """
""" + _copy_path_page_fixture()
js += """
const desc = [];
function setDesc(t) { desc.push(t); }
let modalCopyTimer = 0;
"""
js += "\n" + _fn_async(DOCUMENT_MODAL_JS, "copyPathWithFallback")
js += """
async function main() {
let out = {};
const inserted = [];
const btn = makeEl("button");
btn.className = "doc-copy-path";
btn.insertAdjacentElement = function (pos, node) {
if (pos === "afterend") { this.after.unshift(node); inserted.push(node); }
return node;
};
const metaEl = {
appendChild(c) { inserted.push(c); return c; },
append(...cs) { inserted.push(...cs); },
querySelector(sel) { return sel === ".doc-copy-path" ? btn : null; },
querySelectorAll() { return inserted; },
};
const ok1 = await copyPathWithFallback(metaEl, "H omelab/a/b c.md");
const live = () =>
btn.after.filter(
(n) =>
(n.className === "doc-copy-status" || n.className === "doc-path-fallback") &&
n.isConnected,
);
out = {
ok1,
written: written.slice(),
after: live().map((n) => n.className),
statusText: live().find((n) => n.className === "doc-copy-status").textContent,
statusAria: live().find((n) => n.className === "doc-copy-status").attrs["aria-hidden"],
desc: desc.slice(),
timerDelay,
};
timerFn(); // the auto-clear fires
out.cleared = !btn.after[0].isConnected;
reject = true;
const ok2 = await copyPathWithFallback(metaEl, "H omelab/a/b c.md");
const lv = live();
const field = lv.find((n) => n.className === "doc-path-fallback");
out.ok2 = ok2;
out.afterFail = lv.map((n) => n.className);
out.fieldValue = field.value;
out.fieldType = field.type;
out.fieldReadOnly = field.readOnly;
out.fieldAria = field.attrs["aria-label"];
out.fieldFocus = field.focusCount;
out.fieldSelect = field.selectCount;
out.failStatus = lv.find((n) => n.className === "doc-copy-status").textContent;
out.descFail = desc[desc.length - 1];
out.connectedAfterFail = lv.length;
await copyPathWithFallback(metaEl, "H omelab/a/b c.md"); // re-attempt (fail again)
out.connectedAfterRetry = live().length;
console.log(JSON.stringify(out));
}
main().catch((e) => { console.error("FAIL " + e.message); process.exit(1); });
"""
r = _run_node(js)
out = json.loads(r)
assert out["ok1"] is True
assert out["written"] == ["H omelab/a/b c.md"] # the exact source/path form
assert out["after"] == ["doc-copy-status"] # right after the control
assert out["statusText"] == "Path copied."
assert out["statusAria"] == "true" # visual only
assert out["desc"] == ["Path copied."] # the a11y region (setDesc)
assert out["timerDelay"] == 4000
assert out["cleared"] is True # the auto-clear dropped the status
assert out["ok2"] is False
assert out["afterFail"] == ["doc-path-fallback", "doc-copy-status"] # field first, status after
assert out["fieldValue"] == "H omelab/a/b c.md" # prefilled with the path
assert out["fieldType"] == "text"
assert out["fieldReadOnly"] is True
assert out["fieldAria"] == "Document path"
assert out["fieldFocus"] == 1 # auto-focused (selects the path)
assert out["fieldSelect"] == 1 # the focus listener selected the content
assert out["failStatus"] == "Path ready — copy it from the field."
assert out["descFail"] == "Path ready — copy it from the field."
assert out["connectedAfterFail"] == 2 # one field + one status
assert out["connectedAfterRetry"] == 2 # the re-click replaced, never cleared
# ---------------------------------------------------------------------------
# Phase 153 (task 03) — the copy-path composition, end-to-end under node
# (the click handler + render() wiring → the EXACT composed bytes on the
# (fake) clipboard; the pre-existing pins above call the helper with a
# pre-composed string — these run the composition step itself)
# ---------------------------------------------------------------------------
PUMP = "async function pump() { for (let i = 0; i < 300; i++) await Promise.resolve(); }\n"
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_wire_copy_path_click_composes_the_exact_source_path_string() -> None:
"""The modal's ``wireCopyPath`` click handler, executed end-to-end
under node (the REAL ``wireCopyPath`` + the REAL ``
copyPathWithFallback`` from the live ``document-modal.js``, the
DOM/clipboard/timers faked): for ``source = "H omelab"`` (space) +
``path = "a/b c.md"`` (slashes + a space), a click sends the
EXACT ``"H omelab/a/b c.md"`` bytes to the clipboard (byte-for-
byte — a refactor that reorders, re-encodes, or trims the
composition breaks the exact string). The success beat announces
the exact success string; a REJECTED clipboard then prefills the
fallback field with that SAME composed string (the exact prefill,
readOnly, focused + selected — the "never clears the previous
state" re-click stands: the second status replaces the first)."""
modal_js = _read(MODAL_JS)
wire = _fn(modal_js, "wireCopyPath")
helper = _fn_async(modal_js, "copyPathWithFallback")
script = (
_copy_path_page_fixture()
+ "\nlet modalCopyTimer = 0;\n"
+ "const descCalls = [];\n"
+ "function setDesc(t) { descCalls.push(t); }\n"
+ "let createdInput = null;\n"
+ "document.createElement = (t) => { const el = makeEl(t);"
+ " if (t === 'input') createdInput = el; return el; };\n"
+ "const btn = makeEl('button');\n"
+ "const metaEl = { querySelector: (s) => (s === '.doc-copy-path' ? btn : null),"
+ " querySelectorAll: () => [], replaceChildren() {} };\n"
+ helper
+ "\n" + wire
+ "\n" + PUMP
+ "wireCopyPath(metaEl, { source: 'H omelab', path: 'a/b c.md' });\n"
+ "const newestStatus = () => (btn.after.find((e) => e.className === 'doc-copy-status')"
+ " || { textContent: undefined }).textContent;\n"
+ "const click = btn.listeners.click[0];\n"
+ "click(); await pump();\n"
+ "const okOut = { written: written.slice(), status: newestStatus(),"
+ " descCalls: descCalls.slice() };\n"
+ "reject = true; click(); await pump();\n"
+ "const failOut = { input: createdInput && createdInput.value,"
+ " readOnly: createdInput && createdInput.readOnly,"
+ " focusCount: createdInput && createdInput.listeners.focus.length,"
+ " selectCount: createdInput && createdInput.selectCount,"
+ " status: newestStatus() };\n"
+ "console.log(JSON.stringify({ okOut, failOut }));\n"
)
out = json.loads(_run_node(script))
# the exact composed bytes (spaces + slashes) on the clipboard
assert out["okOut"]["written"] == ["H omelab/a/b c.md"], (
"the click handler must send the exact source + '/' + path bytes"
)
assert out["okOut"]["status"] == "Path copied."
assert out["okOut"]["descCalls"] == ["Path copied."]
# the rejected-clipboard failure: the prefill is the SAME composed string
assert out["failOut"]["input"] == "H omelab/a/b c.md", (
"the fallback field must prefill with the exact composed string"
)
assert out["failOut"]["readOnly"] is True
assert out["failOut"]["focusCount"] == 1
assert out["failOut"]["selectCount"] == 1
assert out["failOut"]["status"] == "Path ready — copy it from the field."
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_page_render_wires_the_copy_path_with_the_composed_string() -> None:
"""The page's ``render()`` wiring, executed end-to-end under node
(the REAL ``render`` + the page-local ``copyPathWithFallback`` +
``makePathFallbackField`` from the live ``document.js``, the
DOM/clipboard/timers faked): a click on the shared core's
``.doc-copy-path`` control sends the EXACT ``"H omelab/a/b c.md"``
bytes to the clipboard (the ``doc.source + "/" + doc.path``
composition, byte-for-byte). The success beat floats the status in
the ``header.doc-header`` block (the meta row is never grown —
the phase-12/34 contract); the REJECTED-clipboard failure floats
the fallback field prefilled with that SAME composed string
(readOnly, focused + selected) beside the exact failure status, and
the role=status announcer mirror gets the same text."""
js = _read(DOCUMENT_JS)
render_fn = _fn(js, "render")
helper = _fn_async(js, "copyPathWithFallback")
field_fn = _fn(js, "makePathFallbackField")
script = (
_copy_path_page_fixture()
+ "const brand = () => 'Brain of Reese';\n"
+ "const rendered = [];\n"
+ "function renderDocument(d, els) { rendered.push(d); }\n"
+ "const btn = makeEl('button');\n"
+ "const titleEl = {};\n"
+ "const metaEl = { querySelector: (s) => (s === '.doc-copy-path' ? btn : null),"
+ " replaceChildren() {} };\n"
+ "const contentEl = {};\n"
+ "const header = document.querySelector('header.doc-header');\n"
+ field_fn
+ "\n" + helper
+ "\n" + render_fn
+ "\n" + PUMP
+ "render({ source: 'H omelab', path: 'a/b c.md', title: 'T' });\n"
+ "const click = btn.listeners.click[0];\n"
+ "click(); await pump();\n"
+ "const okBlock = header.children[0];\n"
+ "const okOut = { written: written.slice(), blockChildren: okBlock.children.length,"
+ " status: okBlock.children[0].textContent,"
+ " announcer: document.querySelector('#doc-copy-announcer').textContent };\n"
+ "reject = true; click(); await pump();\n"
+ "const failBlock = header.children[header.children.length - 1];\n"
+ "const field = failBlock.children[0]; const failStatus = failBlock.children[1];\n"
+ "const failOut = { value: field.value, readOnly: field.readOnly,"
+ " focusCount: field.listeners.focus.length, selectCount: field.selectCount,"
+ " status: failStatus.textContent, className: failBlock.className };\n"
+ "console.log(JSON.stringify({ okOut, failOut }));\n"
)
out = json.loads(_run_node(script))
# the exact composed bytes (spaces + slashes) on the clipboard
assert out["okOut"]["written"] == ["H omelab/a/b c.md"], (
"render() must wire the click to the exact source + '/' + path bytes"
)
# the float lives in the header (never the meta row): one status child
assert out["okOut"]["blockChildren"] == 1
assert out["okOut"]["status"] == "Path copied."
assert out["okOut"]["announcer"] == "Path copied." # the a11y mirror
# the rejected-clipboard failure: the prefill is the SAME composed string
assert out["failOut"]["value"] == "H omelab/a/b c.md", (
"the page fallback field must prefill with the exact composed string"
)
assert out["failOut"]["readOnly"] is True
assert out["failOut"]["focusCount"] == 1
assert out["failOut"]["selectCount"] == 1
assert out["failOut"]["status"] == "Path ready — copy it from the field."
assert out["failOut"]["className"] == "doc-copy-pop" # the floating block