"""CLI tests for ``sway report --format html`` (S12)."""
from __future__ import annotations
import builtins
import json
from pathlib import Path
import pytest
from typer.testing import CliRunner
from dlm_sway.cli.app import app
# Plotly is shipped via the optional [viz] extra. The
# ``test_missing_plotly_surfaces_install_hint`` test monkeypatches it
# away, so it runs unconditionally; every other test needs plotly loaded.
pytest.importorskip("plotly")
def _write_sample_result(path: Path) -> None:
payload = {
"schema_version": 1,
"sway_version": "0.1.0.dev0",
"base_model_id": "base",
"adapter_id": "adp",
"started_at": "2026-01-01T12:00:00+00:00",
"finished_at": "2026-01-01T12:00:02+00:00",
"score": {
"overall": 0.77,
"band": "healthy",
"components": {
"adherence": 0.8,
"attribution": 0.7,
"calibration": 0.8,
"ablation": 0.7,
},
"weights": {
"adherence": 0.30,
"attribution": 0.35,
"calibration": 0.20,
"ablation": 0.15,
},
"findings": [],
},
"probes": [
{
"name": "dk",
"kind": "delta_kl",
"verdict": "pass",
"score": 0.8,
"raw": 0.4,
"z_score": 4.0,
"message": "ok",
},
{
"name": "abl",
"kind": "adapter_ablation",
"verdict": "pass",
"score": 0.7,
"raw": 0.9,
"message": "R²=0.9",
"evidence": {
"lambdas": [0.0, 0.5, 1.0],
"mean_divergence_per_lambda": [0.0, 0.1, 0.2],
"saturation_lambda": 0.75,
},
},
],
}
path.write_text(json.dumps(payload), encoding="utf-8")
class TestReportHtmlCli:
def test_writes_file(self, tmp_path: Path) -> None:
result_json = tmp_path / "result.json"
out_html = tmp_path / "report.html"
_write_sample_result(result_json)
invocation = CliRunner().invoke(
app, ["report", str(result_json), "--format", "html", "--out", str(out_html)]
)
assert invocation.exit_code == 0, invocation.stdout + invocation.stderr
assert out_html.is_file()
content = out_html.read_text(encoding="utf-8")
# Structural markers — the wrapper + panel div ids + probe names.
assert content.startswith("")
assert 'id="sway-gauge"' in content
assert 'id="sway-scatter"' in content
assert "dk" in content
# Stderr carries the "wrote HTML → ..." confirmation.
assert "wrote HTML" in invocation.stderr
def test_requires_out_flag(self, tmp_path: Path) -> None:
result_json = tmp_path / "result.json"
_write_sample_result(result_json)
invocation = CliRunner().invoke(app, ["report", str(result_json), "--format", "html"])
# Exit 2 — the 3 MB JS bundle has no business on stdout by default.
assert invocation.exit_code == 2
assert "requires --out" in invocation.stderr
def test_terminal_with_out_errors(self, tmp_path: Path) -> None:
"""``--out`` is only for file-producing formats. Terminal stays
in the console."""
result_json = tmp_path / "result.json"
target = tmp_path / "bogus.txt"
_write_sample_result(result_json)
invocation = CliRunner().invoke(
app,
["report", str(result_json), "--format", "terminal", "--out", str(target)],
)
assert invocation.exit_code == 2
assert "does not support --out" in invocation.stderr
def test_markdown_with_out_writes_file(self, tmp_path: Path) -> None:
"""The ``--out`` flag works for ``--format md`` too (any file format)."""
result_json = tmp_path / "result.json"
md_out = tmp_path / "report.md"
_write_sample_result(result_json)
invocation = CliRunner().invoke(
app,
["report", str(result_json), "--format", "md", "--out", str(md_out)],
)
assert invocation.exit_code == 0, invocation.stdout + invocation.stderr
assert md_out.is_file()
assert "# " in md_out.read_text(encoding="utf-8")
def test_missing_plotly_surfaces_install_hint(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""When plotly is unavailable, the CLI exits 2 with the extras hint."""
result_json = tmp_path / "result.json"
out_html = tmp_path / "report.html"
_write_sample_result(result_json)
real_import = builtins.__import__
def fake_import(name, *args, **kwargs): # type: ignore[no-untyped-def]
if name.startswith("plotly"):
raise ImportError("simulated missing plotly")
return real_import(name, *args, **kwargs)
monkeypatch.setattr(builtins, "__import__", fake_import)
invocation = CliRunner().invoke(
app, ["report", str(result_json), "--format", "html", "--out", str(out_html)]
)
assert invocation.exit_code == 2
assert "viz" in invocation.stderr
# File must not have been written on the error path.
assert not out_html.exists()