@@ -1,185 +1,37 @@ |
| 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. |
| 1 | +"""Sprint 26 X1 — CLI-flag wiring for `dlm export --emit-sway-json`. |
| 2 | + |
| 3 | +The helper-module unit tests (``write_sway_json`` round-trip + every |
| 4 | +error path) live at ``tests/unit/export/test_sway_json.py`` so the |
| 5 | +``Coverage gate — src/dlm/export = 100%`` job (which runs only |
| 6 | +``tests/unit/export``) sees them. This file owns the CLI-surface |
| 7 | +half: the flag is registered, shows up in ``--help``, and carries |
| 8 | +the sprint-specified text. |
| 19 | 9 | """ |
| 20 | 10 | |
| 21 | 11 | from __future__ import annotations |
| 22 | 12 | |
| 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) |
| 13 | +import re |
| 87 | 14 | |
| 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 |
| 15 | +from typer.testing import CliRunner |
| 96 | 16 | |
| 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") |
| 17 | +from dlm.cli.app import app |
| 159 | 18 | |
| 160 | 19 | |
| 161 | 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 | 21 | def test_flag_present_in_export_help(self) -> None: |
| 166 | | - from typer.testing import CliRunner |
| 167 | | - |
| 168 | | - from dlm.cli.app import app |
| 169 | | - |
| 22 | + """``--emit-sway-json`` flag appears in ``dlm export --help``.""" |
| 170 | 23 | # Force a wide terminal so typer/Rich don't wrap the long |
| 171 | 24 | # ``--emit-sway-json`` flag across lines (CI's runner has a |
| 172 | 25 | # narrow default that breaks substring asserts). |
| 173 | 26 | runner = CliRunner(env={"COLUMNS": "200", "TERM": "dumb"}) |
| 174 | 27 | result = runner.invoke(app, ["export", "--help"]) |
| 175 | 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 | 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 | 34 | plain = re.sub(r"\s+", " ", plain) |
| 35 | + |
| 184 | 36 | assert "--emit-sway-json" in plain, plain |
| 185 | 37 | assert "sway.yaml" in plain, plain |