Python · 5517 bytes Raw Blame History
1 """CLI tests for ``sway report --format html`` (S12)."""
2
3 from __future__ import annotations
4
5 import builtins
6 import json
7 from pathlib import Path
8
9 import pytest
10 from typer.testing import CliRunner
11
12 from dlm_sway.cli.app import app
13
14 # Plotly is shipped via the optional [viz] extra. The
15 # ``test_missing_plotly_surfaces_install_hint`` test monkeypatches it
16 # away, so it runs unconditionally; every other test needs plotly loaded.
17 pytest.importorskip("plotly")
18
19
20 def _write_sample_result(path: Path) -> None:
21 payload = {
22 "schema_version": 1,
23 "sway_version": "0.1.0.dev0",
24 "base_model_id": "base",
25 "adapter_id": "adp",
26 "started_at": "2026-01-01T12:00:00+00:00",
27 "finished_at": "2026-01-01T12:00:02+00:00",
28 "score": {
29 "overall": 0.77,
30 "band": "healthy",
31 "components": {
32 "adherence": 0.8,
33 "attribution": 0.7,
34 "calibration": 0.8,
35 "ablation": 0.7,
36 },
37 "weights": {
38 "adherence": 0.30,
39 "attribution": 0.35,
40 "calibration": 0.20,
41 "ablation": 0.15,
42 },
43 "findings": [],
44 },
45 "probes": [
46 {
47 "name": "dk",
48 "kind": "delta_kl",
49 "verdict": "pass",
50 "score": 0.8,
51 "raw": 0.4,
52 "z_score": 4.0,
53 "message": "ok",
54 },
55 {
56 "name": "abl",
57 "kind": "adapter_ablation",
58 "verdict": "pass",
59 "score": 0.7,
60 "raw": 0.9,
61 "message": "R²=0.9",
62 "evidence": {
63 "lambdas": [0.0, 0.5, 1.0],
64 "mean_divergence_per_lambda": [0.0, 0.1, 0.2],
65 "saturation_lambda": 0.75,
66 },
67 },
68 ],
69 }
70 path.write_text(json.dumps(payload), encoding="utf-8")
71
72
73 class TestReportHtmlCli:
74 def test_writes_file(self, tmp_path: Path) -> None:
75 result_json = tmp_path / "result.json"
76 out_html = tmp_path / "report.html"
77 _write_sample_result(result_json)
78 invocation = CliRunner().invoke(
79 app, ["report", str(result_json), "--format", "html", "--out", str(out_html)]
80 )
81 assert invocation.exit_code == 0, invocation.stdout + invocation.stderr
82 assert out_html.is_file()
83 content = out_html.read_text(encoding="utf-8")
84 # Structural markers — the wrapper + panel div ids + probe names.
85 assert content.startswith("<!doctype html>")
86 assert 'id="sway-gauge"' in content
87 assert 'id="sway-scatter"' in content
88 assert "dk" in content
89 # Stderr carries the "wrote HTML → ..." confirmation.
90 assert "wrote HTML" in invocation.stderr
91
92 def test_requires_out_flag(self, tmp_path: Path) -> None:
93 result_json = tmp_path / "result.json"
94 _write_sample_result(result_json)
95 invocation = CliRunner().invoke(app, ["report", str(result_json), "--format", "html"])
96 # Exit 2 — the 3 MB JS bundle has no business on stdout by default.
97 assert invocation.exit_code == 2
98 assert "requires --out" in invocation.stderr
99
100 def test_terminal_with_out_errors(self, tmp_path: Path) -> None:
101 """``--out`` is only for file-producing formats. Terminal stays
102 in the console."""
103 result_json = tmp_path / "result.json"
104 target = tmp_path / "bogus.txt"
105 _write_sample_result(result_json)
106 invocation = CliRunner().invoke(
107 app,
108 ["report", str(result_json), "--format", "terminal", "--out", str(target)],
109 )
110 assert invocation.exit_code == 2
111 assert "does not support --out" in invocation.stderr
112
113 def test_markdown_with_out_writes_file(self, tmp_path: Path) -> None:
114 """The ``--out`` flag works for ``--format md`` too (any file format)."""
115 result_json = tmp_path / "result.json"
116 md_out = tmp_path / "report.md"
117 _write_sample_result(result_json)
118 invocation = CliRunner().invoke(
119 app,
120 ["report", str(result_json), "--format", "md", "--out", str(md_out)],
121 )
122 assert invocation.exit_code == 0, invocation.stdout + invocation.stderr
123 assert md_out.is_file()
124 assert "# " in md_out.read_text(encoding="utf-8")
125
126 def test_missing_plotly_surfaces_install_hint(
127 self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
128 ) -> None:
129 """When plotly is unavailable, the CLI exits 2 with the extras hint."""
130 result_json = tmp_path / "result.json"
131 out_html = tmp_path / "report.html"
132 _write_sample_result(result_json)
133
134 real_import = builtins.__import__
135
136 def fake_import(name, *args, **kwargs): # type: ignore[no-untyped-def]
137 if name.startswith("plotly"):
138 raise ImportError("simulated missing plotly")
139 return real_import(name, *args, **kwargs)
140
141 monkeypatch.setattr(builtins, "__import__", fake_import)
142 invocation = CliRunner().invoke(
143 app, ["report", str(result_json), "--format", "html", "--out", str(out_html)]
144 )
145 assert invocation.exit_code == 2
146 assert "viz" in invocation.stderr
147 # File must not have been written on the error path.
148 assert not out_html.exists()