tenseleyflow/sway / c032725

Browse files

tests/unit: prove-the-value — HTML from committed history fixture loads offline, no external URLs

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
c032725b396e4c59c36af58d4d0a695d66323872
Parents
f7a9fdd
Tree
04c6511

1 changed file

StatusFile+-
A tests/unit/test_report_html_offline.py 88 0
tests/unit/test_report_html_offline.pyadded
@@ -0,0 +1,88 @@
1
+"""S12 prove-the-value (§F6): HTML report loads offline from a real run.
2
+
3
+Uses the committed `tests/fixtures/sway-history/02-2026-01-22.json`
4
+(the mid-run baseline from S11) as a realistic input: four probe
5
+kinds, healthy-band composite, full evidence dicts for SIS and
6
+ablation. Emits the HTML to a tmp dir and asserts:
7
+
8
+1. The file is a well-formed HTML document.
9
+2. No external ``<script src="http...">`` or ``<link rel=stylesheet>``
10
+   references — every byte loads from the file itself.
11
+3. All four interactive panels (gauge / category / ablation / scatter)
12
+   render; the SIS panel skips because the fixture's
13
+   ``section_internalization`` evidence doesn't carry a ``per_section``
14
+   array (it's a real terminal-rendered history, not a full export).
15
+4. Every probe name from the fixture appears in the probe table.
16
+
17
+Closure notes should record the produced file size and the total
18
+probe count for the record.
19
+"""
20
+
21
+from __future__ import annotations
22
+
23
+import json
24
+import re
25
+from html.parser import HTMLParser
26
+from pathlib import Path
27
+
28
+import pytest
29
+
30
+from dlm_sway.suite import report, report_html
31
+
32
+pytest.importorskip("plotly")
33
+
34
+FIXTURE = Path(__file__).parent.parent / "fixtures" / "sway-history" / "02-2026-01-22.json"
35
+
36
+
37
+class _Parser(HTMLParser):
38
+    def error(self, message: str) -> None:  # pragma: no cover
39
+        raise AssertionError(message)
40
+
41
+
42
+def _parse_ok(text: str) -> None:
43
+    parser = _Parser(convert_charrefs=True)
44
+    parser.feed(text)
45
+    parser.close()
46
+
47
+
48
+def test_html_from_real_history_loads_offline(tmp_path: Path) -> None:
49
+    raw = json.loads(FIXTURE.read_text(encoding="utf-8"))
50
+    suite, score = report.from_json(raw)
51
+
52
+    html_text = report_html.to_html(suite, score)
53
+    target = tmp_path / "report.html"
54
+    target.write_text(html_text, encoding="utf-8")
55
+
56
+    # 1. Well-formed.
57
+    _parse_ok(target.read_text(encoding="utf-8"))
58
+
59
+    # 2. No external script / stylesheet references — fully self-contained.
60
+    disk = target.read_text(encoding="utf-8")
61
+    external_scripts = re.findall(r'<script[^>]*\bsrc\s*=\s*["\'](https?:[^"\']+)', disk)
62
+    external_links = re.findall(r'<link[^>]*\bhref\s*=\s*["\'](https?:[^"\']+)', disk)
63
+    assert external_scripts == [], f"external scripts: {external_scripts}"
64
+    assert external_links == [], f"external stylesheets: {external_links}"
65
+
66
+    # 3. The three always-present panels render.
67
+    for required in ("sway-gauge", "sway-category", "sway-scatter"):
68
+        assert f'id="{required}"' in disk, f"panel {required!r} missing"
69
+    # Evidence-dependent panels: the committed history fixture carries
70
+    # terminal-message metadata but not the full evidence dicts. Both
71
+    # panels correctly opt out, confirming the renderer handles partial
72
+    # inputs without crashing.
73
+    assert 'id="sway-sis"' not in disk
74
+    assert 'id="sway-ablation"' not in disk
75
+
76
+    # 4. All four probe names appear in the probe table.
77
+    for probe_name in (
78
+        "delta_kl",
79
+        "section_internalization",
80
+        "calibration_drift",
81
+        "adapter_ablation",
82
+    ):
83
+        assert probe_name in disk, f"probe {probe_name!r} not in HTML body"
84
+
85
+    # Sanity: file is in the expected size range (1-10 MB; Plotly 6.x
86
+    # bundles ~4.8 MB of JS).
87
+    size = target.stat().st_size
88
+    assert 1_000_000 < size < 10_000_000, f"unexpected HTML size: {size:,} bytes"