"""PLAN A12 guard: the browser must load nothing this process does not serve.
This is the mechanical half of "no external CDNs" (LOCKED decision A12, AGENTS.md §5).
It has two halves on purpose:
1. **A text scan** of every `app/static/**` asset (`.html`, `.css`, `.js`, `.svg`) for the
shapes that reach off-origin: `http(s)://`, protocol-relative `//host/`, hosted-library
mirrors (`jsdelivr`, `unpkg`, …), CSS `@import url(`, protocol-relative stylesheets,
`srcset`, `fetch()` / `import` of an absolute URL, an absolute-URL `WebSocket`, and
`EventSource` (SSE is not this project's transport). A single remote reference is both
an availability and a privacy defect, so the scan is blunt and needs no allow-list —
with one documented exception, the `xmlns` namespace URIs of SVG/XML, which are
identifiers the browser never fetches (A11 ships in-repo SVG art).
2. **An element check** of `index.html`: every `'),
(
"module_src",
'',
),
("stylesheet_link", ''),
("protocol_relative_stylesheet", ''),
("img_src", '
'),
("img_srcset", '
'),
],
)
def test_a_planted_html_reference_fails_the_scan(
static_tree: Path, name: str, content: str
) -> None:
page = static_tree / f"{name}.html"
page.write_text(content, encoding="utf-8")
violations = scan_tree(static_tree)
assert violations, content
assert all(violation.path == page for violation in violations), format_violations(violations)
assert page.name in format_violations(violations)
@pytest.mark.parametrize(
("suffix", "content", "rule"),
[
(
".js",
'import { x } from "https://cdn.example.com/lib.js";',
"import of an off-origin URL",
),
(
".js",
'import "https://cdn.example.com/side-effect.js";',
"bare import of an off-origin URL",
),
(".js", 'export * from "//cdn.example.com/lib.js";', "import of an off-origin URL"),
(".js", 'fetch("https://api.example.com/state");', "fetch() of an off-origin URL"),
(".js", 'fetch("//api.example.com/state");', "fetch() of an off-origin URL"),
(
".js",
'new WebSocket("wss://remote.example.com/ws/ABCDE");',
"WebSocket with an absolute URL",
),
(".js", 'new EventSource("/events");', "EventSource (server-sent events)"),
(".js", 'const u = "https://cdn.example.com/x.js";', "absolute URL"),
(".css", "@import url(https://fonts.example.com/one.css);", "CSS @import url()"),
(".css", "@import url(//fonts.example.com/one.css);", "CSS @import url()"),
(".css", "body { background: url(//cdn.example.com/bg.png); }", "protocol-relative URL"),
(
".svg",
'',
"absolute URL",
),
(".html", 'out', "absolute URL"),
],
)
def test_a_planted_asset_reference_fails_the_scan(
static_tree: Path, suffix: str, content: str, rule: str
) -> None:
planted = static_tree / "js" / f"planted{suffix}"
planted.write_text(content, encoding="utf-8")
violations = scan_tree(static_tree)
assert violations, content
assert rule in {violation.rule for violation in violations}, format_violations(violations)
assert any(violation.path == planted for violation in violations)
assert planted.name in format_violations(violations)
@pytest.mark.parametrize("token", HOSTED_LIBRARY_TOKENS)
@pytest.mark.parametrize("suffix", [".js", ".html"])
def test_a_planted_hosted_library_token_fails_the_scan(
static_tree: Path, token: str, suffix: str
) -> None:
planted = static_tree / f"planted{suffix}"
planted.write_text(f"/* vendored from {token} once */", encoding="utf-8")
violations = [v for v in scan_tree(static_tree) if v.path == planted]
assert violations, token
assert token in format_violations(violations)
# --------------------------------------------------------------------------- what must stay green
@pytest.mark.parametrize(
("suffix", "content"),
[
(
".svg",
'',
),
(
".svg",
'',
),
(".js", "// a comment about docs/PROTOCOL.md and a/b/c paths\nexport const ok = 1;\n"),
(".js", 'const ws = new WebSocket(`${location.origin}/ws/${code}`);\n'),
(".js", 'fetch("/static/api/levels");\n'),
(".css", ".a { background: url(/static/assets/tile.svg); }\n"),
(".html", '
\n'),
],
)
def test_same_origin_and_namespace_references_are_allowed(
static_tree: Path, suffix: str, content: str
) -> None:
"""The guard must not train anyone to delete real code: same-origin stays green."""
(static_tree / "js" / f"fine{suffix}").write_text(content, encoding="utf-8")
assert scan_tree(static_tree) == []
def test_non_asset_files_are_out_of_scope(static_tree: Path) -> None:
"""Notes and images are not client instructions; the scan is about loadable code."""
(static_tree / "NOTES.md").write_text("see https://example.com for details", encoding="utf-8")
(static_tree / "logo.png").write_bytes(b"\x89PNG\r\n")
assert scan_tree(static_tree) == []
def test_an_empty_tree_is_not_a_passing_signal(tmp_path: Path) -> None:
"""Documented explicitly: an empty root scans clean, so the root tests above matter."""
assert scan_tree(tmp_path) == []
assert list(iter_asset_files(tmp_path)) == []