@@ -1,185 +1,37 @@ |
| 1 | -"""Sprint 26 X1 — `dlm export --emit-sway-json` test coverage. | 1 | +"""Sprint 26 X1 — CLI-flag wiring for `dlm export --emit-sway-json`. |
| 2 | - | 2 | + |
| 3 | -Two scopes here, both unit-level: | 3 | +The helper-module unit tests (``write_sway_json`` round-trip + every |
| 4 | - | 4 | +error path) live at ``tests/unit/export/test_sway_json.py`` so the |
| 5 | -1. The helper module (``dlm.export.sway_json.write_sway_json``) | 5 | +``Coverage gate — src/dlm/export = 100%`` job (which runs only |
| 6 | - round-trips a synthetic ``.dlm`` document into a ``sway.yaml`` on | 6 | +``tests/unit/export``) sees them. This file owns the CLI-surface |
| 7 | - disk. Stubs the dlm-sway dependency via ``sys.modules`` injection | 7 | +half: the flag is registered, shows up in ``--help``, and carries |
| 8 | - so the test runs without the real ``[sway]`` extra installed. | 8 | +the sprint-specified text. |
| 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 | """ | 9 | """ |
| 20 | | 10 | |
| 21 | from __future__ import annotations | 11 | from __future__ import annotations |
| 22 | | 12 | |
| 23 | -import sys | 13 | +import re |
| 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 | | 14 | |
| 88 | - assert out == export_dir / "sway.yaml" | 15 | +from typer.testing import CliRunner |
| 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 | | 16 | |
| 97 | - def test_creates_export_dir_if_missing( | 17 | +from dlm.cli.app import app |
| 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 | | 18 | |
| 160 | | 19 | |
| 161 | class TestExportCliFlagWiring: | 20 | 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: | 21 | def test_flag_present_in_export_help(self) -> None: |
| 166 | - from typer.testing import CliRunner | 22 | + """``--emit-sway-json`` flag appears in ``dlm export --help``.""" |
| 167 | - | | |
| 168 | - from dlm.cli.app import app | | |
| 169 | - | | |
| 170 | # Force a wide terminal so typer/Rich don't wrap the long | 23 | # Force a wide terminal so typer/Rich don't wrap the long |
| 171 | # ``--emit-sway-json`` flag across lines (CI's runner has a | 24 | # ``--emit-sway-json`` flag across lines (CI's runner has a |
| 172 | # narrow default that breaks substring asserts). | 25 | # narrow default that breaks substring asserts). |
| 173 | runner = CliRunner(env={"COLUMNS": "200", "TERM": "dumb"}) | 26 | runner = CliRunner(env={"COLUMNS": "200", "TERM": "dumb"}) |
| 174 | result = runner.invoke(app, ["export", "--help"]) | 27 | result = runner.invoke(app, ["export", "--help"]) |
| 175 | assert result.exit_code == 0, result.output | 28 | assert result.exit_code == 0, result.output |
| 176 | - # Strip any ANSI escapes so a substring match is robust to | | |
| 177 | - # color codes inserted at arbitrary positions. | | |
| 178 | - import re | | |
| 179 | | 29 | |
| | 30 | + # Strip ANSI escapes + collapse whitespace so substring asserts |
| | 31 | + # are robust to color codes and any wrap that COLUMNS=200 still |
| | 32 | + # leaves in place. |
| 180 | plain = re.sub(r"\x1b\[[0-9;]*m", "", result.output) | 33 | plain = re.sub(r"\x1b\[[0-9;]*m", "", result.output) |
| 181 | - # Collapse whitespace so wrap-induced line breaks within the | | |
| 182 | - # flag name still match — belt + braces with the COLUMNS env. | | |
| 183 | plain = re.sub(r"\s+", " ", plain) | 34 | plain = re.sub(r"\s+", " ", plain) |
| | 35 | + |
| 184 | assert "--emit-sway-json" in plain, plain | 36 | assert "--emit-sway-json" in plain, plain |
| 185 | assert "sway.yaml" in plain, plain | 37 | assert "sway.yaml" in plain, plain |