cgcardona / muse public
test_plugin_apply_and_checkout.py python
306 lines 11.7 KB
cc9bbc18 feat: complete MuseDomainPlugin integration — apply(), incremental checkout Gabriel Cardona <gabriel@tellurstori.com> 3d ago
1 """Tests for plugin.apply() and the incremental checkout that wires it in.
2
3 Covers:
4 - MusicPlugin.apply() with a workdir path (files already updated on disk)
5 - MusicPlugin.apply() with a snapshot dict (in-memory removals)
6 - checkout incremental delta: only changed files are touched
7 - revert reuses parent snapshot_id (no re-scan)
8 - cherry-pick uses merged_manifest directly (no re-scan)
9 """
10 from __future__ import annotations
11
12 import pathlib
13
14 import pytest
15 from typer.testing import CliRunner
16
17 from muse.cli.app import cli
18 from muse.core.store import get_head_commit_id, read_commit, read_snapshot
19 from muse.domain import DeltaManifest, SnapshotManifest
20 from muse.plugins.music.plugin import MusicPlugin
21
22 runner = CliRunner()
23 plugin = MusicPlugin()
24
25
26 # ---------------------------------------------------------------------------
27 # Fixtures
28 # ---------------------------------------------------------------------------
29
30
31 @pytest.fixture
32 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
33 monkeypatch.chdir(tmp_path)
34 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
35 result = runner.invoke(cli, ["init"])
36 assert result.exit_code == 0, result.output
37 return tmp_path
38
39
40 def _write(repo: pathlib.Path, filename: str, content: str = "data") -> None:
41 (repo / "muse-work" / filename).write_text(content)
42
43
44 def _commit(msg: str = "commit") -> None:
45 result = runner.invoke(cli, ["commit", "-m", msg])
46 assert result.exit_code == 0, result.output
47
48
49 def _head_id(repo: pathlib.Path) -> str:
50 cid = get_head_commit_id(repo, "main")
51 assert cid is not None
52 return cid
53
54
55 # ---------------------------------------------------------------------------
56 # MusicPlugin.apply() — unit tests
57 # ---------------------------------------------------------------------------
58
59
60 class TestMusicPluginApplyPath:
61 """apply() with a workdir path rescans the directory for ground truth."""
62
63 def test_apply_returns_snapshot_of_workdir(self, tmp_path: pathlib.Path) -> None:
64 workdir = tmp_path / "work"
65 workdir.mkdir()
66 (workdir / "a.mid").write_bytes(b"midi-a")
67 (workdir / "b.mid").write_bytes(b"midi-b")
68
69 snap_a = plugin.snapshot(workdir)
70 # Simulate remove b.mid physically
71 (workdir / "b.mid").unlink()
72
73 delta = DeltaManifest(domain="music", added=[], removed=["b.mid"], modified=[])
74 result = plugin.apply(delta, workdir)
75
76 assert "b.mid" not in result["files"]
77 assert "a.mid" in result["files"]
78
79 def test_apply_picks_up_added_file(self, tmp_path: pathlib.Path) -> None:
80 workdir = tmp_path / "work"
81 workdir.mkdir()
82 (workdir / "a.mid").write_bytes(b"original")
83
84 delta = DeltaManifest(domain="music", added=["new.mid"], removed=[], modified=[])
85 # Add file physically before calling apply
86 (workdir / "new.mid").write_bytes(b"new content")
87 result = plugin.apply(delta, workdir)
88
89 assert "new.mid" in result["files"]
90
91 def test_apply_picks_up_modified_content(self, tmp_path: pathlib.Path) -> None:
92 workdir = tmp_path / "work"
93 workdir.mkdir()
94 (workdir / "a.mid").write_bytes(b"v1")
95
96 snap_v1 = plugin.snapshot(workdir)
97
98 # Modify physically
99 (workdir / "a.mid").write_bytes(b"v2")
100 delta = DeltaManifest(domain="music", added=[], removed=[], modified=["a.mid"])
101 result = plugin.apply(delta, workdir)
102
103 assert result["files"]["a.mid"] != snap_v1["files"]["a.mid"]
104
105 def test_apply_empty_delta_returns_current_state(self, tmp_path: pathlib.Path) -> None:
106 workdir = tmp_path / "work"
107 workdir.mkdir()
108 (workdir / "a.mid").write_bytes(b"data")
109
110 delta = DeltaManifest(domain="music", added=[], removed=[], modified=[])
111 result = plugin.apply(delta, workdir)
112 expected = plugin.snapshot(workdir)
113 assert result["files"] == expected["files"]
114
115
116 class TestMusicPluginApplyDict:
117 """apply() with a snapshot dict applies removals in-memory."""
118
119 def test_apply_removes_deleted_paths(self) -> None:
120 snap = SnapshotManifest(files={"a.mid": "aaa", "b.mid": "bbb"}, domain="music")
121 delta = DeltaManifest(domain="music", added=[], removed=["b.mid"], modified=[])
122 result = plugin.apply(delta, snap)
123 assert "b.mid" not in result["files"]
124 assert "a.mid" in result["files"]
125
126 def test_apply_removes_multiple_paths(self) -> None:
127 snap = SnapshotManifest(files={"a.mid": "aaa", "b.mid": "bbb", "c.mid": "ccc"}, domain="music")
128 delta = DeltaManifest(domain="music", added=[], removed=["a.mid", "c.mid"], modified=[])
129 result = plugin.apply(delta, snap)
130 assert result["files"] == {"b.mid": "bbb"}
131
132 def test_apply_nonexistent_remove_is_noop(self) -> None:
133 snap = SnapshotManifest(files={"a.mid": "aaa"}, domain="music")
134 delta = DeltaManifest(domain="music", added=[], removed=["ghost.mid"], modified=[])
135 result = plugin.apply(delta, snap)
136 assert result["files"] == {"a.mid": "aaa"}
137
138 def test_apply_preserves_domain(self) -> None:
139 snap = SnapshotManifest(files={}, domain="music")
140 delta = DeltaManifest(domain="music", added=[], removed=[], modified=[])
141 result = plugin.apply(delta, snap)
142 assert result["domain"] == "music"
143
144
145 # ---------------------------------------------------------------------------
146 # Incremental checkout via plugin.apply()
147 # ---------------------------------------------------------------------------
148
149
150 class TestIncrementalCheckout:
151 def test_checkout_branch_only_changes_delta(self, repo: pathlib.Path) -> None:
152 """Files unchanged between branches are not touched on disk."""
153 _write(repo, "shared.mid", "shared")
154 _write(repo, "main-only.mid", "main")
155 _commit("main initial")
156
157 runner.invoke(cli, ["branch", "feature"])
158 runner.invoke(cli, ["checkout", "feature"])
159 _write(repo, "feature-only.mid", "feature")
160 _commit("feature commit")
161
162 runner.invoke(cli, ["checkout", "main"])
163 assert (repo / "muse-work" / "shared.mid").exists()
164 assert (repo / "muse-work" / "main-only.mid").exists()
165 assert not (repo / "muse-work" / "feature-only.mid").exists()
166
167 def test_checkout_restores_correct_content(self, repo: pathlib.Path) -> None:
168 """Modified files get the correct content after checkout."""
169 _write(repo, "beat.mid", "v1")
170 _commit("v1")
171
172 runner.invoke(cli, ["branch", "feature"])
173 runner.invoke(cli, ["checkout", "feature"])
174 _write(repo, "beat.mid", "v2")
175 _commit("v2 on feature")
176
177 runner.invoke(cli, ["checkout", "main"])
178 assert (repo / "muse-work" / "beat.mid").read_text() == "v1"
179
180 def test_checkout_commit_id_incremental(self, repo: pathlib.Path) -> None:
181 """Detached HEAD checkout also uses incremental apply."""
182 _write(repo, "a.mid", "original")
183 _commit("first")
184 first_id = _head_id(repo)
185
186 _write(repo, "b.mid", "new")
187 _commit("second")
188
189 runner.invoke(cli, ["checkout", first_id])
190 assert (repo / "muse-work" / "a.mid").exists()
191 assert not (repo / "muse-work" / "b.mid").exists()
192
193 def test_checkout_to_new_branch_keeps_workdir(self, repo: pathlib.Path) -> None:
194 """Switching to a brand-new (no-commit) branch keeps the current workdir intact."""
195 _write(repo, "beat.mid")
196 _commit("some work")
197
198 runner.invoke(cli, ["branch", "empty-branch"])
199 runner.invoke(cli, ["checkout", "empty-branch"])
200 # workdir is preserved — new branch inherits from where we branched
201 assert (repo / "muse-work" / "beat.mid").exists()
202
203
204 # ---------------------------------------------------------------------------
205 # Revert reuses parent snapshot (no re-scan)
206 # ---------------------------------------------------------------------------
207
208
209 class TestRevertSnapshotReuse:
210 def test_revert_commit_points_to_parent_snapshot(self, repo: pathlib.Path) -> None:
211 """The revert commit's snapshot_id is the same as the reverted commit's parent."""
212 _write(repo, "beat.mid", "base")
213 _commit("base")
214 base_id = _head_id(repo)
215 base_commit = read_commit(repo, base_id)
216 assert base_commit is not None
217 parent_snapshot_id = base_commit.snapshot_id
218
219 _write(repo, "lead.mid", "new")
220 _commit("add lead")
221 lead_id = _head_id(repo)
222
223 runner.invoke(cli, ["revert", lead_id])
224 revert_id = _head_id(repo)
225 revert_commit = read_commit(repo, revert_id)
226 assert revert_commit is not None
227 # The revert commit points to the same snapshot as "base" — no re-scan.
228 assert revert_commit.snapshot_id == parent_snapshot_id
229
230 def test_revert_snapshot_already_in_store(self, repo: pathlib.Path) -> None:
231 """After revert, the referenced snapshot is already in the store (not re-created)."""
232 _write(repo, "beat.mid", "base")
233 _commit("base")
234 base_id = _head_id(repo)
235 base_commit = read_commit(repo, base_id)
236 assert base_commit is not None
237
238 _write(repo, "lead.mid", "new")
239 _commit("add lead")
240 lead_id = _head_id(repo)
241
242 runner.invoke(cli, ["revert", lead_id])
243 revert_id = _head_id(repo)
244 revert_commit = read_commit(repo, revert_id)
245 assert revert_commit is not None
246
247 snap = read_snapshot(repo, revert_commit.snapshot_id)
248 assert snap is not None
249 assert "beat.mid" in snap.manifest
250 assert "lead.mid" not in snap.manifest
251
252
253 # ---------------------------------------------------------------------------
254 # Cherry-pick uses merged_manifest (no re-scan)
255 # ---------------------------------------------------------------------------
256
257
258 class TestCherryPickManifestReuse:
259 def test_cherry_pick_commit_has_correct_snapshot(self, repo: pathlib.Path) -> None:
260 """Cherry-picked commit's snapshot contains only the right files."""
261 _write(repo, "base.mid", "base")
262 _commit("base")
263
264 runner.invoke(cli, ["branch", "feature"])
265 runner.invoke(cli, ["checkout", "feature"])
266 _write(repo, "feature.mid", "feature content")
267 _commit("feature addition")
268 feature_id = get_head_commit_id(repo, "feature")
269 assert feature_id is not None
270
271 runner.invoke(cli, ["checkout", "main"])
272 result = runner.invoke(cli, ["cherry-pick", feature_id])
273 assert result.exit_code == 0, result.output
274
275 picked_id = _head_id(repo)
276 picked_commit = read_commit(repo, picked_id)
277 assert picked_commit is not None
278 snap = read_snapshot(repo, picked_commit.snapshot_id)
279 assert snap is not None
280 assert "feature.mid" in snap.manifest
281 assert "base.mid" in snap.manifest
282
283 def test_cherry_pick_snapshot_objects_in_store(self, repo: pathlib.Path) -> None:
284 """Objects in the cherry-pick snapshot are already in the store."""
285 _write(repo, "base.mid", "base")
286 _commit("base")
287
288 runner.invoke(cli, ["branch", "feature"])
289 runner.invoke(cli, ["checkout", "feature"])
290 _write(repo, "extra.mid", "extra")
291 _commit("feature extra")
292 feature_id = get_head_commit_id(repo, "feature")
293 assert feature_id is not None
294
295 runner.invoke(cli, ["checkout", "main"])
296 runner.invoke(cli, ["cherry-pick", feature_id])
297
298 picked_id = _head_id(repo)
299 picked_commit = read_commit(repo, picked_id)
300 assert picked_commit is not None
301 snap = read_snapshot(repo, picked_commit.snapshot_id)
302 assert snap is not None
303
304 from muse.core.object_store import has_object
305 for oid in snap.manifest.values():
306 assert has_object(repo, oid), f"Object {oid[:8]} missing from store"