gabriel / muse public
test_cli_plugin_dispatch.py python
470 lines 17.7 KB
Raw
sha256:c32f229e6f8e5aaf7b1010869b5a0a14e75768f407af9773848932d13d4b74f4 updating more failing tests Human 14 days ago
1 """Integration tests verifying CLI commands dispatch through the domain plugin.
2
3 Each test confirms that the relevant plugin method is called when the CLI
4 command runs, and that the command's output matches the plugin's semantics.
5 These tests use unittest.mock.patch to intercept plugin calls and also
6 perform end-to-end output assertions.
7 """
8
9 import pathlib
10 from unittest.mock import MagicMock, patch
11
12 import pytest
13 from tests.cli_test_helper import CliRunner
14
15 cli = None # argparse migration — CliRunner ignores this arg
16 from muse.domain import (
17 DeleteOp,
18 DriftReport,
19 InsertOp,
20 LiveState,
21 MergeResult,
22 MuseDomainPlugin,
23 SnapshotManifest,
24 StateSnapshot,
25 StructuredDelta,
26 )
27 from muse.plugins.midi.plugin import MidiPlugin
28
29 runner = CliRunner()
30
31
32 @pytest.fixture
33 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
34 """Initialise a fresh Muse repo in tmp_path and set it as cwd."""
35 monkeypatch.chdir(tmp_path)
36 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
37 result = runner.invoke(cli, ["init"])
38 assert result.exit_code == 0, result.output
39 return tmp_path
40
41
42 def _write(repo: pathlib.Path, filename: str, content: str = "data") -> None:
43 (repo / filename).write_text(content)
44
45
46 def _commit(msg: str = "initial") -> None:
47 result = runner.invoke(cli, ["commit", "-m", msg])
48 assert result.exit_code == 0, result.output
49
50
51 # ---------------------------------------------------------------------------
52 # commit
53 # ---------------------------------------------------------------------------
54
55
56 class TestCommitDispatch:
57 def test_commit_calls_plugin_snapshot(self, repo: pathlib.Path) -> None:
58 _write(repo, "beat.mid", "drums")
59 with patch("muse.cli.commands.commit.resolve_plugin") as mock_resolve:
60 real_plugin = MidiPlugin()
61 mock_plugin = MagicMock(spec=MuseDomainPlugin)
62 mock_plugin.snapshot.side_effect = real_plugin.snapshot
63 mock_plugin.diff.side_effect = real_plugin.diff
64 mock_resolve.return_value = mock_plugin
65
66 result = runner.invoke(cli, ["commit", "-m", "test"])
67 assert result.exit_code == 0, result.output
68 mock_plugin.snapshot.assert_called_once()
69
70 def test_commit_snapshot_argument_is_workdir_path(self, repo: pathlib.Path) -> None:
71 _write(repo, "beat.mid", "drums")
72 captured_args: list[LiveState] = []
73
74 with patch("muse.cli.commands.commit.resolve_plugin") as mock_resolve:
75 real_plugin = MidiPlugin()
76 mock_plugin = MagicMock(spec=MuseDomainPlugin)
77
78 def capture_snapshot(live_state: LiveState) -> SnapshotManifest:
79 captured_args.append(live_state)
80 return real_plugin.snapshot(live_state)
81
82 mock_plugin.snapshot.side_effect = capture_snapshot
83 mock_resolve.return_value = mock_plugin
84
85 runner.invoke(cli, ["commit", "-m", "test"])
86 assert len(captured_args) == 1
87 assert isinstance(captured_args[0], pathlib.Path)
88 # snapshot() receives the repository root (the working tree), not a subdirectory
89 assert (captured_args[0] / ".muse").exists()
90
91 def test_commit_uses_snapshot_files_for_manifest(self, repo: pathlib.Path) -> None:
92 _write(repo, "track.mid", "content")
93 result = runner.invoke(cli, ["commit", "-m", "via plugin"])
94 assert result.exit_code == 0
95 assert "via plugin" in result.output
96
97
98 # ---------------------------------------------------------------------------
99 # status
100 # ---------------------------------------------------------------------------
101
102
103 class TestStatusDispatch:
104 def test_status_calls_plugin_drift(self, repo: pathlib.Path) -> None:
105 _write(repo, "beat.mid")
106 _commit()
107 _write(repo, "new.mid", "extra")
108
109 with patch("muse.cli.commands.status.resolve_plugin") as mock_resolve:
110 real_plugin = MidiPlugin()
111 mock_plugin = MagicMock(spec=MuseDomainPlugin)
112 mock_plugin.drift.side_effect = real_plugin.drift
113 mock_resolve.return_value = mock_plugin
114
115 result = runner.invoke(cli, ["status"])
116 assert result.exit_code == 0
117 mock_plugin.drift.assert_called_once()
118
119 def test_status_clean_tree_via_drift(self, repo: pathlib.Path) -> None:
120 _write(repo, "beat.mid")
121 _commit()
122 result = runner.invoke(cli, ["status"])
123 assert result.exit_code == 0
124 assert "clean" in result.output
125
126 def test_status_shows_new_file(self, repo: pathlib.Path) -> None:
127 _write(repo, "beat.mid")
128 _commit()
129 _write(repo, "new.mid", "extra")
130 result = runner.invoke(cli, ["status"])
131 assert result.exit_code == 0
132 assert "new.mid" in result.output
133
134 def test_status_shows_deleted_file(self, repo: pathlib.Path) -> None:
135 _write(repo, "beat.mid")
136 _commit()
137 (repo / "beat.mid").unlink()
138 result = runner.invoke(cli, ["status"])
139 assert result.exit_code == 0
140 assert "beat.mid" in result.output
141
142 def test_status_drift_report_drives_output(self, repo: pathlib.Path) -> None:
143 """Patch drift() to return a controlled DriftReport and verify CLI echoes it."""
144 _write(repo, "beat.mid")
145 _commit()
146
147 fake_delta = StructuredDelta(
148 domain="midi",
149 ops=[InsertOp(op="insert", address="injected.mid", position=None,
150 content_id="abc123", content_summary="new file: injected.mid")],
151 summary="1 file added",
152 )
153 fake_report = DriftReport(has_drift=True, summary="1 added", delta=fake_delta)
154
155 with patch("muse.cli.commands.status.resolve_plugin") as mock_resolve:
156 mock_plugin = MagicMock(spec=MuseDomainPlugin)
157 mock_plugin.drift.return_value = fake_report
158 mock_resolve.return_value = mock_plugin
159
160 result = runner.invoke(cli, ["status"])
161 assert "injected.mid" in result.output
162
163
164 # ---------------------------------------------------------------------------
165 # diff
166 # ---------------------------------------------------------------------------
167
168
169 class TestDiffDispatch:
170 def test_diff_calls_plugin_diff(self, repo: pathlib.Path) -> None:
171 _write(repo, "beat.mid")
172 _commit()
173 _write(repo, "lead.mid", "solo")
174
175 with patch("muse.cli.commands.diff.resolve_plugin") as mock_resolve:
176 real_plugin = MidiPlugin()
177 mock_plugin = MagicMock(spec=MuseDomainPlugin)
178 mock_plugin.snapshot.side_effect = real_plugin.snapshot
179 mock_plugin.diff.side_effect = real_plugin.diff
180 mock_resolve.return_value = mock_plugin
181
182 result = runner.invoke(cli, ["diff"])
183 assert result.exit_code == 0
184 mock_plugin.snapshot.assert_called_once()
185 mock_plugin.diff.assert_called_once()
186
187 def test_diff_calls_plugin_snapshot_for_workdir(self, repo: pathlib.Path) -> None:
188 _write(repo, "beat.mid")
189 _commit()
190 _write(repo, "extra.mid", "new")
191
192 captured: list[LiveState] = []
193 with patch("muse.cli.commands.diff.resolve_plugin") as mock_resolve:
194 real_plugin = MidiPlugin()
195 mock_plugin = MagicMock(spec=MuseDomainPlugin)
196
197 def cap_snapshot(ls: LiveState) -> SnapshotManifest:
198 captured.append(ls)
199 return real_plugin.snapshot(ls)
200
201 mock_plugin.snapshot.side_effect = cap_snapshot
202 mock_plugin.diff.side_effect = real_plugin.diff
203 mock_resolve.return_value = mock_plugin
204
205 runner.invoke(cli, ["diff"])
206 assert any(isinstance(a, pathlib.Path) for a in captured)
207
208 def test_diff_shows_added_file(self, repo: pathlib.Path) -> None:
209 _write(repo, "beat.mid")
210 _commit()
211 _write(repo, "new.mid", "extra")
212 result = runner.invoke(cli, ["diff"])
213 assert result.exit_code == 0
214 assert "new.mid" in result.output
215
216 def test_diff_no_differences(self, repo: pathlib.Path) -> None:
217 _write(repo, "beat.mid")
218 _commit()
219 result = runner.invoke(cli, ["diff"])
220 assert result.exit_code == 0
221 assert "No differences" in result.output
222
223 def test_diff_delta_drives_output(self, repo: pathlib.Path) -> None:
224 """Patch plugin.diff() to return a controlled delta and verify CLI output."""
225 _write(repo, "beat.mid")
226 _commit()
227
228 fake_delta = StructuredDelta(
229 domain="midi",
230 ops=[
231 InsertOp(op="insert", address="injected.mid", position=None,
232 content_id="abc123", content_summary="new file: injected.mid"),
233 DeleteOp(op="delete", address="gone.mid", position=None,
234 content_id="def456", content_summary="deleted: gone.mid"),
235 ],
236 summary="1 file added, 1 file removed",
237 )
238 with patch("muse.cli.commands.diff.resolve_plugin") as mock_resolve:
239 real_plugin = MidiPlugin()
240 mock_plugin = MagicMock(spec=MuseDomainPlugin)
241 mock_plugin.snapshot.side_effect = real_plugin.snapshot
242 mock_plugin.diff.return_value = fake_delta
243 mock_resolve.return_value = mock_plugin
244
245 result = runner.invoke(cli, ["diff"])
246 assert "injected.mid" in result.output
247 assert "gone.mid" in result.output
248
249
250 # ---------------------------------------------------------------------------
251 # merge
252 # ---------------------------------------------------------------------------
253
254
255 class TestMergeDispatch:
256 def test_merge_calls_plugin_merge(self, repo: pathlib.Path) -> None:
257 _write(repo, "beat.mid", "v1")
258 _commit("base")
259
260 runner.invoke(cli, ["branch", "feature"])
261 runner.invoke(cli, ["checkout", "feature"])
262 _write(repo, "lead.mid", "solo")
263 _commit("add lead")
264
265 runner.invoke(cli, ["checkout", "main"])
266 # Add a commit on main so both branches have diverged — forces a real merge.
267 _write(repo, "bass.mid", "bass line")
268 _commit("add bass on main")
269
270 with patch("muse.cli.commands.merge.resolve_plugin") as mock_resolve:
271 real_plugin = MidiPlugin()
272 mock_plugin = MagicMock(spec=MuseDomainPlugin)
273 mock_plugin.merge.side_effect = real_plugin.merge
274 mock_resolve.return_value = mock_plugin
275
276 result = runner.invoke(cli, ["merge", "feature"])
277 assert result.exit_code == 0
278 mock_plugin.merge.assert_called_once()
279
280 def test_merge_plugin_merge_result_drives_outcome(self, repo: pathlib.Path) -> None:
281 from muse.core.object_store import write_object
282 from muse.core.types import blob_id
283
284 _write(repo, "beat.mid", "v1")
285 _commit("base")
286
287 runner.invoke(cli, ["branch", "feature"])
288 runner.invoke(cli, ["checkout", "feature"])
289 _write(repo, "lead.mid", "solo")
290 _commit("add lead")
291
292 runner.invoke(cli, ["checkout", "main"])
293 _write(repo, "bass.mid", "bass line")
294 _commit("add bass on main")
295
296 injected_content = b"injected midi content"
297 injected_oid = blob_id(injected_content)
298 write_object(repo, injected_oid, injected_content)
299
300 fake_result = MergeResult(
301 merged=SnapshotManifest(files={"injected.mid": injected_oid}, domain="midi"),
302 conflicts=[],
303 )
304 with patch("muse.cli.commands.merge.resolve_plugin") as mock_resolve:
305 mock_plugin = MagicMock(spec=MuseDomainPlugin)
306 mock_plugin.merge.return_value = fake_result
307 mock_resolve.return_value = mock_plugin
308
309 result = runner.invoke(cli, ["merge", "feature"])
310 assert result.exit_code == 0
311 assert "Merge" in result.output
312
313 def test_merge_conflict_uses_plugin_conflict_paths(self, repo: pathlib.Path) -> None:
314 _write(repo, "beat.mid", "original")
315 _commit("base")
316
317 runner.invoke(cli, ["branch", "feature"])
318 runner.invoke(cli, ["checkout", "feature"])
319 _write(repo, "beat.mid", "feature-version")
320 _commit("feature changes beat")
321
322 runner.invoke(cli, ["checkout", "main"])
323 _write(repo, "beat.mid", "main-version")
324 _commit("main changes beat")
325
326 result = runner.invoke(cli, ["merge", "feature"])
327 assert result.exit_code != 0
328 assert "beat.mid" in result.stderr
329
330 def test_merge_conflict_paths_come_from_plugin(self, repo: pathlib.Path) -> None:
331 _write(repo, "beat.mid", "original")
332 _commit("base")
333 runner.invoke(cli, ["branch", "feature"])
334 runner.invoke(cli, ["checkout", "feature"])
335 _write(repo, "beat.mid", "feature-version")
336 _commit("feature")
337 runner.invoke(cli, ["checkout", "main"])
338 _write(repo, "beat.mid", "main-version")
339 _commit("main")
340
341 fake_result = MergeResult(
342 merged=SnapshotManifest(files={}, domain="midi"),
343 conflicts=["plugin-conflict.mid"],
344 )
345 with patch("muse.cli.commands.merge.resolve_plugin") as mock_resolve:
346 mock_plugin = MagicMock(spec=MuseDomainPlugin)
347 mock_plugin.merge.return_value = fake_result
348 mock_resolve.return_value = mock_plugin
349
350 result = runner.invoke(cli, ["merge", "feature"])
351 assert result.exit_code != 0
352 assert "plugin-conflict.mid" in result.stderr
353
354
355 # ---------------------------------------------------------------------------
356 # cherry-pick
357 # ---------------------------------------------------------------------------
358
359
360 class TestCherryPickDispatch:
361 def test_cherry_pick_calls_plugin_merge(self, repo: pathlib.Path) -> None:
362 _write(repo, "beat.mid", "v1")
363 _commit("initial")
364
365 runner.invoke(cli, ["branch", "feature"])
366 runner.invoke(cli, ["checkout", "feature"])
367 _write(repo, "lead.mid", "solo")
368 _commit("add lead on feature")
369
370 from muse.core.refs import get_head_commit_id
371 from muse.core.repo import require_repo
372 import os
373 os.chdir(repo)
374 feature_tip = get_head_commit_id(repo, "feature")
375 assert feature_tip is not None
376
377 runner.invoke(cli, ["checkout", "main"])
378
379 with patch("muse.cli.commands.cherry_pick.resolve_plugin") as mock_resolve:
380 real_plugin = MidiPlugin()
381 mock_plugin = MagicMock(spec=MuseDomainPlugin)
382 mock_plugin.merge.side_effect = real_plugin.merge
383 mock_resolve.return_value = mock_plugin
384
385 result = runner.invoke(cli, ["cherry-pick", feature_tip])
386 assert result.exit_code == 0, result.output
387 mock_plugin.merge.assert_called_once()
388
389 def test_cherry_pick_three_way_args_are_snapshot_manifests(
390 self, repo: pathlib.Path
391 ) -> None:
392 _write(repo, "beat.mid", "v1")
393 _commit("initial")
394
395 runner.invoke(cli, ["branch", "feature"])
396 runner.invoke(cli, ["checkout", "feature"])
397 _write(repo, "lead.mid", "solo")
398 _commit("add lead")
399
400 import os
401 os.chdir(repo)
402 from muse.core.refs import get_head_commit_id
403 feature_tip = get_head_commit_id(repo, "feature")
404 assert feature_tip is not None
405
406 runner.invoke(cli, ["checkout", "main"])
407
408 captured_args: list[tuple[StateSnapshot, StateSnapshot, StateSnapshot]] = []
409 with patch("muse.cli.commands.cherry_pick.resolve_plugin") as mock_resolve:
410 real_plugin = MidiPlugin()
411 mock_plugin = MagicMock(spec=MuseDomainPlugin)
412
413 def cap_merge(
414 base: StateSnapshot, left: StateSnapshot, right: StateSnapshot
415 ) -> MergeResult:
416 captured_args.append((base, left, right))
417 return real_plugin.merge(base, left, right)
418
419 mock_plugin.merge.side_effect = cap_merge
420 mock_resolve.return_value = mock_plugin
421
422 runner.invoke(cli, ["cherry-pick", feature_tip])
423 assert len(captured_args) == 1
424 base, left, right = captured_args[0]
425 assert isinstance(base, dict) and "files" in base
426 assert isinstance(left, dict) and "files" in left
427 assert isinstance(right, dict) and "files" in right
428
429
430 # ---------------------------------------------------------------------------
431 # shelf
432 # ---------------------------------------------------------------------------
433
434
435 class TestShelfDispatch:
436 def test_shelf_calls_plugin_snapshot(self, repo: pathlib.Path) -> None:
437 _write(repo, "beat.mid")
438 _commit()
439 _write(repo, "unsaved.mid", "wip")
440
441 with patch("muse.cli.commands.shelf.resolve_plugin") as mock_resolve:
442 real_plugin = MidiPlugin()
443 mock_plugin = MagicMock(spec=MuseDomainPlugin)
444 mock_plugin.snapshot.side_effect = real_plugin.snapshot
445 mock_resolve.return_value = mock_plugin
446
447 result = runner.invoke(cli, ["shelf", "save"])
448 assert result.exit_code == 0
449 mock_plugin.snapshot.assert_called_once()
450
451 def test_shelf_snapshot_argument_is_workdir_path(self, repo: pathlib.Path) -> None:
452 _write(repo, "beat.mid")
453 _commit()
454 _write(repo, "unsaved.mid", "wip")
455
456 captured: list[LiveState] = []
457 with patch("muse.cli.commands.shelf.resolve_plugin") as mock_resolve:
458 real_plugin = MidiPlugin()
459 mock_plugin = MagicMock(spec=MuseDomainPlugin)
460
461 def cap_snapshot(ls: LiveState) -> SnapshotManifest:
462 captured.append(ls)
463 return real_plugin.snapshot(ls)
464
465 mock_plugin.snapshot.side_effect = cap_snapshot
466 mock_resolve.return_value = mock_plugin
467
468 runner.invoke(cli, ["shelf", "save"])
469 assert len(captured) == 1
470 assert isinstance(captured[0], pathlib.Path)
File History 1 commit
sha256:c32f229e6f8e5aaf7b1010869b5a0a14e75768f407af9773848932d13d4b74f4 updating more failing tests Human 14 days ago