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