@@ -0,0 +1,88 @@ |
| 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}" |