Python · 7772 bytes Raw Blame History
1 """Focused early-branch coverage for `dlm export`."""
2
3 from __future__ import annotations
4
5 from pathlib import Path
6 from types import SimpleNamespace
7
8 import pytest
9 from typer.testing import CliRunner
10
11 from dlm.base_models import BaseModelSpec
12 from dlm.base_models.errors import GatedModelError
13 from dlm.cli.app import app
14 from dlm.export.errors import ExportError
15
16
17 def _joined_output(result: object) -> str:
18 text = getattr(result, "output", "") + getattr(result, "stderr", "")
19 return " ".join(text.split())
20
21
22 def _scaffold_doc(tmp_path: Path) -> Path:
23 doc = tmp_path / "doc.dlm"
24 runner = CliRunner()
25 result = runner.invoke(
26 app,
27 [
28 "--home",
29 str(tmp_path / "home"),
30 "init",
31 str(doc),
32 "--base",
33 "smollm2-135m",
34 ],
35 )
36 assert result.exit_code == 0, result.output
37 return doc
38
39
40 def _spec(*, key: str = "demo-1b", modality: str = "text") -> BaseModelSpec:
41 payload: dict[str, object] = {
42 "key": key,
43 "hf_id": f"org/{key}",
44 "revision": "0123456789abcdef0123456789abcdef01234567",
45 "architecture": "DemoForCausalLM",
46 "params": 1_000_000_000,
47 "target_modules": ["q_proj", "v_proj"],
48 "template": "chatml",
49 "gguf_arch": "demo",
50 "tokenizer_pre": "demo",
51 "license_spdx": "Apache-2.0",
52 "license_url": None,
53 "requires_acceptance": False,
54 "redistributable": True,
55 "size_gb_fp16": 2.0,
56 "context_length": 4096,
57 "recommended_seq_len": 2048,
58 "modality": modality,
59 }
60 if modality == "vision-language":
61 payload["vl_preprocessor_plan"] = {
62 "target_size": [224, 224],
63 "image_token": "<image>",
64 "num_image_tokens": 256,
65 }
66 elif modality == "audio-language":
67 payload["audio_preprocessor_plan"] = {
68 "sample_rate": 16000,
69 "audio_token": "<audio>",
70 "num_audio_tokens": 64,
71 "max_length_seconds": 30.0,
72 }
73 return BaseModelSpec.model_validate(payload)
74
75
76 def _patch_export_runtime(
77 monkeypatch: pytest.MonkeyPatch,
78 *,
79 spec: BaseModelSpec | None = None,
80 dispatch: object | None = None,
81 ) -> None:
82 monkeypatch.setattr(
83 "dlm.base_models.resolve",
84 lambda *args, **kwargs: spec or _spec(),
85 )
86 monkeypatch.setattr(
87 "dlm.modality.modality_for",
88 lambda model_spec: (
89 dispatch
90 or SimpleNamespace(
91 accepts_images=model_spec.modality == "vision-language",
92 accepts_audio=model_spec.modality == "audio-language",
93 )
94 ),
95 )
96
97
98 class TestExportEdgePaths:
99 def test_gate_fallback_banner_prints_before_gated_base_refusal(
100 self,
101 tmp_path: Path,
102 monkeypatch: pytest.MonkeyPatch,
103 ) -> None:
104 doc = _scaffold_doc(tmp_path)
105 runner = CliRunner()
106
107 monkeypatch.setattr(
108 "dlm.export.gate_fallback.resolve_and_announce",
109 lambda store, parsed: SimpleNamespace(
110 entries=[("knowledge", 0.7), ("tone", 0.3)],
111 banner_lines=["[yellow]gate:[/yellow] using learned adapter prior"],
112 ),
113 )
114 monkeypatch.setattr(
115 "dlm.base_models.resolve",
116 lambda *args, **kwargs: (_ for _ in ()).throw(
117 GatedModelError("org/gated-base", "https://example.test/license")
118 ),
119 )
120
121 result = runner.invoke(
122 app,
123 ["--home", str(tmp_path / "home"), "export", str(doc)],
124 )
125
126 assert result.exit_code == 1, result.output
127 text = _joined_output(result)
128 assert "using learned adapter prior" in text
129 assert "review the license at: https://example.test/license" in text
130 assert "accept via `dlm train --i-accept-license` before exporting." in text
131
132 @pytest.mark.parametrize(
133 ("target", "modality", "needle"),
134 [
135 (
136 "vllm",
137 "audio-language",
138 "--target vllm is not wired for audio-language documents yet",
139 ),
140 (
141 "mlx-serve",
142 "audio-language",
143 "--target mlx-serve is not wired for audio-language documents yet",
144 ),
145 (
146 "vllm",
147 "vision-language",
148 "--target vllm is not wired for vision-language documents yet",
149 ),
150 (
151 "mlx-serve",
152 "vision-language",
153 "--target mlx-serve is not wired for vision-language documents yet",
154 ),
155 ],
156 )
157 def test_runtime_targets_refuse_unsupported_modalities(
158 self,
159 tmp_path: Path,
160 monkeypatch: pytest.MonkeyPatch,
161 target: str,
162 modality: str,
163 needle: str,
164 ) -> None:
165 doc = _scaffold_doc(tmp_path)
166 runner = CliRunner()
167
168 _patch_export_runtime(
169 monkeypatch, spec=_spec(key=f"{target}-{modality}", modality=modality)
170 )
171
172 result = runner.invoke(
173 app,
174 ["--home", str(tmp_path / "home"), "export", str(doc), "--target", target],
175 )
176
177 assert result.exit_code == 2, result.output
178 assert needle in _joined_output(result)
179
180 def test_audio_dispatch_export_error_maps_to_exit_1(
181 self,
182 tmp_path: Path,
183 monkeypatch: pytest.MonkeyPatch,
184 ) -> None:
185 doc = _scaffold_doc(tmp_path)
186 runner = CliRunner()
187
188 class _AudioDispatch:
189 accepts_images = False
190 accepts_audio = True
191
192 def dispatch_export(self, **kwargs: object) -> object:
193 raise ExportError("audio snapshot failed")
194
195 _patch_export_runtime(
196 monkeypatch,
197 spec=_spec(key="audio-demo", modality="audio-language"),
198 dispatch=_AudioDispatch(),
199 )
200
201 result = runner.invoke(
202 app,
203 ["--home", str(tmp_path / "home"), "export", str(doc)],
204 )
205
206 assert result.exit_code == 1, result.output
207 assert "audio snapshot failed" in _joined_output(result)
208
209 def test_vl_dispatch_export_error_maps_to_exit_1(
210 self,
211 tmp_path: Path,
212 monkeypatch: pytest.MonkeyPatch,
213 ) -> None:
214 doc = _scaffold_doc(tmp_path)
215 runner = CliRunner()
216
217 class _VlDispatch:
218 accepts_images = True
219 accepts_audio = False
220
221 def dispatch_export(self, **kwargs: object) -> object:
222 raise ExportError("vl snapshot failed")
223
224 _patch_export_runtime(
225 monkeypatch,
226 spec=_spec(key="vl-demo", modality="vision-language"),
227 dispatch=_VlDispatch(),
228 )
229
230 result = runner.invoke(
231 app,
232 ["--home", str(tmp_path / "home"), "export", str(doc)],
233 )
234
235 assert result.exit_code == 1, result.output
236 assert "vl snapshot failed" in _joined_output(result)
237
238 def test_invalid_export_plan_value_exits_2(
239 self,
240 tmp_path: Path,
241 monkeypatch: pytest.MonkeyPatch,
242 ) -> None:
243 doc = _scaffold_doc(tmp_path)
244 runner = CliRunner()
245
246 _patch_export_runtime(monkeypatch)
247 monkeypatch.setattr(
248 "dlm.export.resolve_export_plan",
249 lambda **kwargs: (_ for _ in ()).throw(ValueError("bad export plan")),
250 )
251
252 result = runner.invoke(
253 app,
254 ["--home", str(tmp_path / "home"), "export", str(doc)],
255 )
256
257 assert result.exit_code == 2, result.output
258 assert "bad export plan" in _joined_output(result)