Files
brain-of-reese/tests/integration/test_containerfile_assets.py
T

304 lines
13 KiB
Python

"""Integration: Containerfile stage-1 (frontend) asset coverage pin (phase 23).
HERMETIC — no podman, no network, no database: this suite parses the
``Containerfile`` and ``frontend/`` as plain text and pins the image-build
coverage that the TODO L6 bug ("Fix Containerfile build not working")
demonstrated can silently rot in two independent ways:
* the stage-1 ``cp`` line only copies the pages that existed when it was
written (``document.html`` / ``login.html`` + their scripts +
``markdown.js`` were all missing from the image), and
* absolute module imports (``import … from "/assets/header.js"``) break
the esbuild bundle, so a "fixed" stage 1 can still ship a broken page.
The pins (each is one test, per the phase-23 task file):
1. every ``frontend/*.html`` page is copied into stage 1's ``/out``
— exactly (a new page without a ``cp`` entry fails; a ``cp`` of a
deleted page also fails);
2. every local ``assets/`` / ``/assets/`` ``src=``/``href=`` reference in
the pages is produced by a stage-1 line (``esbuild … --outfile`` or
``cp``) — the missing-``markdown.js``-style gap cannot reappear;
3. the set of ``type="module"`` page scripts the HTML references equals
the set of inputs esbuild ``--bundle``s in stage 1;
4. ``header.js`` is imported relatively by every page script and loaded
by NO direct ``<script>`` tag (single-evaluation design pin,
owner-confirmed 2026-08-24);
5. ``markdown.js`` is a classic script: stage-1 minify line WITHOUT
``--bundle``, and no top-level ``import``/``export`` in the source
(the source-level assumption that makes that build line safe);
6. the frontend stage pins a concrete ``esbuild@X.Y.Z`` (no floating
version — the exact pinned 0.25.5 is what the diagnosis reproduced
against).
"""
from __future__ import annotations
import re
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
FRONTEND = REPO_ROOT / "frontend"
ASSETS = FRONTEND / "assets"
CONTAINERFILE = REPO_ROOT / "Containerfile"
# A local asset reference: src="assets/…" or src/href="/assets/…"
# (data: URIs, "#main" anchors, and same-origin page links never match).
_ASSET_REF_RE = re.compile(r"""(?:src|href)\s*=\s*["']((?:/)?assets/[^"']+)["']""")
_SCRIPT_TAG_RE = re.compile(r"<script\b[^>]*>", re.IGNORECASE)
_MODULE_ATTR_RE = re.compile(r"""type\s*=\s*["']module["']""", re.IGNORECASE)
_SRC_ATTR_RE = re.compile(r"""src\s*=\s*["']([^"']+)["']""")
_TOP_LEVEL_MODULE_RE = re.compile(r"^\s*(?:import\b|export\b)", re.MULTILINE)
def _stage1_lines() -> list[str]:
"""The physical lines of the ``frontend`` build stage (FROM … up to,
but not including, the next FROM)."""
assert CONTAINERFILE.is_file(), f"missing Containerfile: {CONTAINERFILE}"
lines = CONTAINERFILE.read_text(encoding="utf-8").splitlines()
start = next(
i
for i, ln in enumerate(lines)
if re.match(r"^FROM\s+\S+\s+AS\s+frontend\b", ln, re.IGNORECASE)
)
end = len(lines)
for j in range(start + 1, len(lines)):
if re.match(r"^FROM\s", lines[j], re.IGNORECASE):
end = j
break
return lines[start:end]
def _is_esbuild_invocation(ln: str) -> bool:
"""True if the line actually RUNS esbuild (not just mentions it in a
comment or in the npm install line)."""
stripped = ln.lstrip()
return not stripped.startswith("#") and bool(re.search(r"(?:^|&&)\s*esbuild\b", ln))
def _esbuild_outputs(stage1: list[str]) -> dict[str, bool]:
"""basename of every asset a stage-1 esbuild line writes → was it
``--bundle``d? (Only lines with an ``--outfile`` count.)"""
outputs: dict[str, bool] = {}
for ln in stage1:
if not _is_esbuild_invocation(ln):
continue
m = re.search(r"--outfile=(\S+)", ln)
assert m, f"stage-1 esbuild line without --outfile: {ln.strip()}"
assert m.group(1).startswith("/out/assets/"), (
f"stage-1 asset must be written under /out/assets (the served "
f"/assets/ URL space), got {m.group(1)}"
)
outputs[m.group(1).rsplit("/", 1)[-1]] = "--bundle" in ln
return outputs
def _esbuild_bundle_inputs(stage1: list[str]) -> set[str]:
"""Basenames of the inputs to every stage-1 ``esbuild … --bundle``."""
inputs: set[str] = set()
for ln in stage1:
if _is_esbuild_invocation(ln) and "--bundle" in ln:
m = re.search(r"esbuild\s+(\S+)", ln)
assert m, f"stage-1 esbuild --bundle line without an input: {ln.strip()}"
inputs.add(m.group(1).rsplit("/", 1)[-1])
return inputs
def _cp_produced_basenames(stage1: list[str]) -> set[str]:
"""Basenames written into the image by stage-1 ``cp`` lines (the last
argument is the destination; a ``/``-suffixed one is a directory)."""
produced: set[str] = set()
for ln in stage1:
m = re.search(r"\bcp\s+(.+?)(?:\s*\\)?\s*$", ln)
if not m:
continue
args = m.group(1).split()
assert len(args) >= 2, f"malformed cp line in stage 1: {ln.strip()}"
dest = args[-1]
if dest.endswith("/"):
produced.update(a.rsplit("/", 1)[-1] for a in args[:-1])
else: # single-file rename: the image sees the destination name
produced.add(dest.rsplit("/", 1)[-1])
return produced
def _local_asset_basenames() -> set[str]:
"""Every local assets/ reference (basenames) across all frontend pages."""
refs: set[str] = set()
for html in sorted(FRONTEND.glob("*.html")):
text = html.read_text(encoding="utf-8")
refs.update(m.group(1).rsplit("/", 1)[-1] for m in _ASSET_REF_RE.finditer(text))
return refs
def _module_script_basenames() -> set[str]:
"""Basenames of the ``<script type="module" src=…>`` page scripts."""
names: set[str] = set()
for html in sorted(FRONTEND.glob("*.html")):
for tag in _SCRIPT_TAG_RE.findall(html.read_text(encoding="utf-8")):
if _MODULE_ATTR_RE.search(tag):
m = _SRC_ATTR_RE.search(tag)
assert m, f"{html.name}: module <script> without src: {tag}"
names.add(m.group(1).rsplit("/", 1)[-1])
return names
# ---------- 1. page coverage ----------
def test_every_html_page_is_copied_into_stage1() -> None:
"""Every ``frontend/*.html`` page is copied into stage 1's ``/out`` —
and the ``cp`` set is EXACTLY the pages on disk: a new page without a
matching cp entry (image 404s) and a cp of a deleted page (stale build
step) both fail here."""
stage1 = _stage1_lines()
on_disk = {p.name for p in FRONTEND.glob("*.html")}
assert on_disk, "frontend/ contains no .html pages — test is blind"
copied: set[str] = set()
for ln in stage1:
if ".html" not in ln:
continue
m = re.search(r"\bcp\s+(.+?)(?:\s*\\)?\s*$", ln)
if not m:
continue
args = m.group(1).split()
dest = args[-1]
assert dest.rstrip("/") == "/out", f"pages must be copied into /out, got {dest}"
copied.update(a.rsplit("/", 1)[-1] for a in args[:-1] if a.endswith(".html"))
missing = on_disk - copied
extra = copied - on_disk
assert not missing, (
f"pages missing from stage 1's cp line (404 in the image): {sorted(missing)}"
)
assert not extra, (
f"stage 1 copies pages that no longer exist in frontend/: {sorted(extra)}"
)
# ---------- 2. asset-reference coverage ----------
def test_every_local_asset_reference_is_produced() -> None:
"""Every local assets/… src/href in the pages is produced by a stage-1
line — esbuild ``--outfile=/out/assets/<name>`` or a ``cp`` of it.
This is what catches a missing-markdown.js-style gap: a page that
references an asset stage 1 never builds is a 404 in the image."""
stage1 = _stage1_lines()
produced = set(_esbuild_outputs(stage1)) | _cp_produced_basenames(stage1)
refs = _local_asset_basenames()
assert refs, "no local assets/ references found in the pages — test is blind"
missing = refs - produced
assert not missing, (
f"local assets referenced by the pages but not produced by stage 1 "
f"(404 in the image): {sorted(missing)}"
)
# ---------- 3. page-module ⇄ bundle-input parity ----------
def test_page_module_scripts_are_bundled() -> None:
"""The set of ``type="module"`` page scripts the HTML references
(basenames) EQUALS the set of inputs esbuild ``--bundle``s in stage 1
(today: app.js, sources.js, document.js, login.js). A new page script
without a bundle entry would 404 in the image; a bundle input whose
page no longer references it is dead build weight."""
stage1 = _stage1_lines()
html_modules = _module_script_basenames()
bundled = _esbuild_bundle_inputs(stage1)
assert html_modules, "no module page scripts found in the pages — test is blind"
assert html_modules == bundled, (
f"page module scripts {sorted(html_modules)} != esbuild bundle inputs "
f"{sorted(bundled)} — stage 1 must bundle exactly the pages' modules"
)
# ---------- 4. single-evaluation design pin ----------
def test_header_module_is_imported_not_directly_loaded() -> None:
"""Owner-confirmed design (2026-08-24, A4-2): NO page loads header.js
with a direct ``<script>`` tag — in the image the bundled page script
already contains the header code, so a raw header.js tag would evaluate
the module TWICE (duplicate sign-out listener, double init). Every page
script imports it relatively instead (``from "./header.js"``) — a
hoisted import that guarantees evaluation order in dev AND in the
bundle, and the only form esbuild can resolve."""
htmls = sorted(FRONTEND.glob("*.html"))
assert htmls, "no frontend pages — test is blind"
for html in htmls:
text = html.read_text(encoding="utf-8")
for tag in _SCRIPT_TAG_RE.findall(text):
m = _SRC_ATTR_RE.search(tag)
assert m is None or m.group(1).rsplit("/", 1)[-1] != "header.js", (
f"{html.name}: direct header.js <script> tag — double-evaluation "
f"trap in the image: {tag}"
)
page_scripts = _module_script_basenames()
assert page_scripts, "no module page scripts found — test is blind"
for name in sorted(page_scripts):
js_path = ASSETS / name
assert js_path.is_file(), f"page script missing: {js_path}"
body = js_path.read_text(encoding="utf-8")
assert re.search(r"""from\s+["']\./header\.js["']""", body), (
f"{name}: must import the shared header module relatively "
f'("from \\"./header.js\\"")'
)
assert '"/assets/header.js"' not in body, (
f"{name}: absolute header import breaks the esbuild stage-1 bundle"
)
# ---------- 5. markdown.js is a classic script ----------
def test_markdown_js_is_a_produced_classic_script() -> None:
"""markdown.js ships minified WITHOUT --bundle (it is a classic global
script — window.markdownRender — loaded by index.html and
document.html), and the source has no top-level import/export: that
source-level fact is exactly what makes the no-bundle build line safe.
Pinning both sides keeps the assumption honest."""
stage1 = _stage1_lines()
outputs = _esbuild_outputs(stage1)
assert "markdown.js" in outputs, (
"markdown.js has no stage-1 esbuild --outfile line — index.html and "
"document.html would 404 the renderer in the image"
)
assert outputs["markdown.js"] is False, (
"markdown.js must NOT be --bundled: it is a classic global script, and "
"bundling it would strip the window-level globals the pages rely on"
)
md_path = ASSETS / "markdown.js"
assert md_path.is_file(), f"missing {md_path}"
hits = _TOP_LEVEL_MODULE_RE.findall(md_path.read_text(encoding="utf-8"))
assert not hits, f"markdown.js has top-level module syntax: {hits!r}"
# ---------- 6. pinned esbuild ----------
def test_esbuild_stays_pinned() -> None:
"""The frontend stage installs esbuild at a CONCRETE X.Y.Z. The phase-23
diagnosis was reproduced against the exact pinned 0.25.5; a floating
version (latest, ^, ~, or no spec) would let an upstream release change
the bundle behavior with no diff to see."""
stage1 = _stage1_lines()
install_lines = [ln for ln in stage1 if re.search(r"\bnpm\s+(?:install|i|add)\b", ln)]
assert install_lines, "no npm install line in the frontend stage"
pinned = [
v for ln in install_lines for v in re.findall(r"\besbuild@(\d+\.\d+\.\d+)\b", ln)
]
assert pinned, (
"the frontend stage must pin esbuild@X.Y.Z — a floating esbuild is how "
"the bundle behavior rots silently"
)
for ln in install_lines:
for m in re.finditer(r"\besbuild(@\S*)?", ln):
spec = m.group(1) or ""
assert re.fullmatch(r"@\d+\.\d+\.\d+", spec), (
f"esbuild must be installed with a concrete pin, got "
f"{m.group(0)!r} in: {ln.strip()}"
)