tenseleyflow/documentlanguagemodel / ec6f384

Browse files

tests/unit/cli: 5 tests for write_sway_json + --emit-sway-json wiring (S26 X1-P5)

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
ec6f384c841784087ea83f59486b2432a6e5d748
Parents
a939c5c
Tree
ce4b170

1 changed file

StatusFile+-
A tests/unit/cli/test_export_sway_json.py 174 0
tests/unit/cli/test_export_sway_json.pyadded
@@ -0,0 +1,174 @@
1
+"""Sprint 26 X1 — `dlm export --emit-sway-json` test coverage.
2
+
3
+Two scopes here, both unit-level:
4
+
5
+1. The helper module (``dlm.export.sway_json.write_sway_json``)
6
+   round-trips a synthetic ``.dlm`` document into a ``sway.yaml`` on
7
+   disk. Stubs the dlm-sway dependency via ``sys.modules`` injection
8
+   so the test runs without the real ``[sway]`` extra installed.
9
+
10
+2. The CLI flag (``--emit-sway-json``) is wired into ``dlm export``
11
+   with the right help text, and the typed
12
+   :class:`SwayJsonExportError` surfaces a clear message when
13
+   dlm-sway isn't installed.
14
+
15
+End-to-end against the real ``dlm-sway`` package (running ``sway run``
16
+on the emitted yaml) lives in the sway repo as
17
+``tests/integration/test_dlm_sway_json_export.py`` once both PRs are
18
+mergeable — see the sprint file's coordination notes.
19
+"""
20
+
21
+from __future__ import annotations
22
+
23
+import sys
24
+import types
25
+from pathlib import Path
26
+
27
+import pytest
28
+
29
+from dlm.export.sway_json import SwayJsonExportError, write_sway_json
30
+
31
+
32
+def _install_fake_dlm_sway(monkeypatch: pytest.MonkeyPatch) -> None:
33
+    """Inject minimal `dlm_sway.integrations.dlm.{autogen,resolver}`
34
+    modules so write_sway_json runs without the real extra installed.
35
+
36
+    The fakes return shapes the real autogen produces so the helper
37
+    can write a syntactically-valid YAML.
38
+    """
39
+    dlm_sway = types.ModuleType("dlm_sway")
40
+    integrations = types.ModuleType("dlm_sway.integrations")
41
+    integrations_dlm = types.ModuleType("dlm_sway.integrations.dlm")
42
+    autogen = types.ModuleType("dlm_sway.integrations.dlm.autogen")
43
+    resolver = types.ModuleType("dlm_sway.integrations.dlm.resolver")
44
+
45
+    class _FakeHandle:
46
+        dlm_id = "01TEST"
47
+
48
+    def _resolve_dlm(_path: Path) -> _FakeHandle:
49
+        return _FakeHandle()
50
+
51
+    def _build_spec_dict(_handle: _FakeHandle, *, dlm_source: str) -> dict[str, object]:
52
+        return {
53
+            "version": 1,
54
+            "models": {
55
+                "base": {"kind": "hf", "base": "smollm2-135m"},
56
+                "ft": {"kind": "hf", "base": "smollm2-135m"},
57
+            },
58
+            "defaults": {"seed": 0},
59
+            "suite": [
60
+                {"name": "dk", "kind": "delta_kl", "prompts": ["x"]},
61
+            ],
62
+            "dlm_source": dlm_source,
63
+        }
64
+
65
+    resolver.resolve_dlm = _resolve_dlm  # type: ignore[attr-defined]
66
+    autogen.build_spec_dict = _build_spec_dict  # type: ignore[attr-defined]
67
+
68
+    monkeypatch.setitem(sys.modules, "dlm_sway", dlm_sway)
69
+    monkeypatch.setitem(sys.modules, "dlm_sway.integrations", integrations)
70
+    monkeypatch.setitem(sys.modules, "dlm_sway.integrations.dlm", integrations_dlm)
71
+    monkeypatch.setitem(sys.modules, "dlm_sway.integrations.dlm.autogen", autogen)
72
+    monkeypatch.setitem(sys.modules, "dlm_sway.integrations.dlm.resolver", resolver)
73
+
74
+
75
+class TestWriteSwayJson:
76
+    def test_writes_yaml_in_export_dir(
77
+        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
78
+    ) -> None:
79
+        """Helper writes ``<export_dir>/sway.yaml`` and returns the path."""
80
+        _install_fake_dlm_sway(monkeypatch)
81
+        dlm_path = tmp_path / "doc.dlm"
82
+        dlm_path.write_text("---\ndlm_id: 01TEST\n---\nbody\n", encoding="utf-8")
83
+        export_dir = tmp_path / "export"
84
+        export_dir.mkdir()
85
+
86
+        out = write_sway_json(dlm_path, export_dir)
87
+
88
+        assert out == export_dir / "sway.yaml"
89
+        assert out.exists()
90
+        # Structural check on the emitted YAML — the test's fake
91
+        # ``build_spec_dict`` returned a delta_kl probe.
92
+        content = out.read_text(encoding="utf-8")
93
+        assert "version: 1" in content
94
+        assert "delta_kl" in content
95
+        assert "dlm_source:" in content
96
+
97
+    def test_creates_export_dir_if_missing(
98
+        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
99
+    ) -> None:
100
+        """Caller may pass a non-existent export_dir — helper mkdirs."""
101
+        _install_fake_dlm_sway(monkeypatch)
102
+        dlm_path = tmp_path / "doc.dlm"
103
+        dlm_path.write_text("---\ndlm_id: 01\n---\n", encoding="utf-8")
104
+        export_dir = tmp_path / "fresh" / "nested" / "export"
105
+        # NOT mkdir'd — helper should create.
106
+
107
+        out = write_sway_json(dlm_path, export_dir)
108
+        assert out.exists()
109
+        assert export_dir.is_dir()
110
+
111
+    def test_dlm_sway_missing_raises_typed_error(
112
+        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
113
+    ) -> None:
114
+        """ImportError on dlm_sway → SwayJsonExportError with install hint."""
115
+        # Wipe any cached dlm_sway modules so the import lookup re-fires.
116
+        for mod in list(sys.modules):
117
+            if mod == "dlm_sway" or mod.startswith("dlm_sway."):
118
+                monkeypatch.delitem(sys.modules, mod, raising=False)
119
+
120
+        # Block the import so the lazy import inside write_sway_json fails.
121
+        import builtins
122
+
123
+        real_import = builtins.__import__
124
+
125
+        def fake_import(name: str, *args: object, **kwargs: object) -> object:
126
+            if name.startswith("dlm_sway"):
127
+                raise ImportError("dlm-sway not installed (test stub)")
128
+            return real_import(name, *args, **kwargs)
129
+
130
+        monkeypatch.setattr(builtins, "__import__", fake_import)
131
+
132
+        dlm_path = tmp_path / "doc.dlm"
133
+        dlm_path.write_text("---\ndlm_id: 01\n---\n", encoding="utf-8")
134
+
135
+        with pytest.raises(SwayJsonExportError, match="pip install 'dlm\\[sway\\]'"):
136
+            write_sway_json(dlm_path, tmp_path / "export")
137
+
138
+    def test_autogen_failure_wrapped_in_typed_error(
139
+        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
140
+    ) -> None:
141
+        """A sway-side parse error during build_spec_dict wraps to
142
+        SwayJsonExportError so the dlm CLI sees a familiar exception
143
+        family."""
144
+        # Install a dlm_sway whose build_spec_dict explodes.
145
+        _install_fake_dlm_sway(monkeypatch)
146
+        from dlm_sway.integrations.dlm import (
147
+            autogen as fake_autogen,  # type: ignore[import-not-found]
148
+        )
149
+
150
+        def _raise(*_a: object, **_kw: object) -> dict[str, object]:
151
+            raise RuntimeError("intentional autogen blowup for test")
152
+
153
+        monkeypatch.setattr(fake_autogen, "build_spec_dict", _raise)
154
+
155
+        dlm_path = tmp_path / "doc.dlm"
156
+        dlm_path.write_text("---\ndlm_id: 01\n---\n", encoding="utf-8")
157
+        with pytest.raises(SwayJsonExportError, match="intentional autogen blowup"):
158
+            write_sway_json(dlm_path, tmp_path / "export")
159
+
160
+
161
+class TestExportCliFlagWiring:
162
+    """The ``--emit-sway-json`` flag is registered on the CLI with the
163
+    sprint-specified help text. Smoke-level — flag presence + help."""
164
+
165
+    def test_flag_present_in_export_help(self) -> None:
166
+        from typer.testing import CliRunner
167
+
168
+        from dlm.cli.app import app
169
+
170
+        runner = CliRunner()
171
+        result = runner.invoke(app, ["export", "--help"])
172
+        assert result.exit_code == 0, result.output
173
+        assert "--emit-sway-json" in result.output
174
+        assert "sway.yaml" in result.output