Python · 3329 bytes Raw Blame History
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"