Python · 3256 bytes Raw Blame History
1 """C7: positively assert that sway works without ``dlm`` installed.
2
3 The architectural promise from the plan: ``dlm-sway`` does not depend
4 on the ``dlm`` package; the bridge under
5 ``src/dlm_sway/integrations/dlm/`` is the only place that imports it,
6 and only when actually invoked.
7
8 This test simulates the dlm-not-installed environment by patching
9 ``sys.modules['dlm'] = None`` (which makes Python's import machinery
10 raise ``ModuleNotFoundError`` on subsequent ``import dlm``). It then:
11
12 1. Builds a small dummy-backend suite end-to-end and confirms it runs
13 to completion with no ImportError.
14 2. Invokes ``sway autogen`` and confirms a clean error pointing at
15 the ``[dlm]`` extra (rather than a stack trace).
16
17 The integration suite cannot actually uninstall dlm at test time, so
18 this is the next-best regression guard — it pins the lazy-import
19 boundary against future refactors that might pull dlm into a
20 top-level ``dlm_sway.*`` import path.
21 """
22
23 from __future__ import annotations
24
25 import sys
26
27 import pytest
28 from typer.testing import CliRunner
29
30 from dlm_sway.backends.dummy import DummyDifferentialBackend, DummyResponses
31 from dlm_sway.cli.app import app
32 from dlm_sway.suite.runner import run as run_suite
33 from dlm_sway.suite.spec import SwaySpec
34
35
36 @pytest.fixture
37 def dlm_unimportable(monkeypatch: pytest.MonkeyPatch):
38 """Make ``import dlm`` raise ModuleNotFoundError for the test."""
39 # Block the parent package and any submodule the bridge imports.
40 for name in (
41 "dlm",
42 "dlm.doc",
43 "dlm.doc.parser",
44 "dlm.base_models",
45 "dlm.store",
46 "dlm.store.paths",
47 "dlm.data",
48 "dlm.data.instruction_parser",
49 "dlm.data.preference_parser",
50 ):
51 monkeypatch.setitem(sys.modules, name, None)
52
53
54 def test_dummy_suite_runs_without_dlm(dlm_unimportable) -> None:
55 """A normal suite run never touches ``dlm`` and must work without it."""
56 backend = DummyDifferentialBackend(base=DummyResponses(), ft=DummyResponses())
57 spec = SwaySpec.model_validate(
58 {
59 "version": 1,
60 "models": {
61 "base": {"base": "b"},
62 "ft": {"base": "b", "adapter": "/tmp/a"},
63 },
64 "suite": [
65 {
66 "name": "dk",
67 "kind": "delta_kl",
68 "prompts": ["q1", "q2"],
69 "assert_mean_gte": 0.0,
70 }
71 ],
72 }
73 )
74 result = run_suite(spec, backend)
75 assert len(result.probes) == 1
76
77
78 def test_autogen_emits_clean_error_without_dlm(dlm_unimportable, tmp_path) -> None:
79 """``sway autogen`` is the one path that needs dlm — it must surface
80 a clean error pointing at the ``[dlm]`` extra, not a stack trace."""
81 fake_dlm = tmp_path / "doesnt-matter.dlm"
82 fake_dlm.write_text("# stub\n")
83 result = CliRunner().invoke(app, ["autogen", str(fake_dlm)])
84 assert result.exit_code == 2, (
85 f"expected exit 2; got {result.exit_code}\nstdout: {result.stdout}\nstderr: {result.stderr if result.stderr_bytes else ''}"
86 )
87 combined = (result.stdout or "") + (result.stderr or "")
88 assert "dlm-sway[dlm]" in combined, f"expected install hint in error output; got {combined!r}"