cgcardona / muse public
test_core_worktree.py python
213 lines 6.7 KB
59a915a4 refactor: repo root is the working tree — remove state/ subdirectory Gabriel Cardona <gabriel@tellurstori.com> 6h ago
1 """Tests for muse/core/worktree.py — multiple simultaneous branch checkouts."""
2
3 from __future__ import annotations
4
5 import json
6 import pathlib
7
8 import pytest
9
10 from muse.core.worktree import (
11 WorktreeInfo,
12 add_worktree,
13 list_worktrees,
14 prune_worktrees,
15 remove_worktree,
16 )
17
18
19 # ---------------------------------------------------------------------------
20 # Helpers
21 # ---------------------------------------------------------------------------
22
23
24 def _make_repo(tmp_path: pathlib.Path, branch: str = "main") -> pathlib.Path:
25 """Create a minimal Muse repo inside a named subdirectory."""
26 repo = tmp_path / "myproject"
27 muse = repo / ".muse"
28 for d in ("objects", "commits", "snapshots", "refs/heads"):
29 (muse / d).mkdir(parents=True, exist_ok=True)
30 (muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo"}))
31 (muse / "HEAD").write_text(f"refs/heads/{branch}\n")
32 (muse / "refs" / "heads" / branch).write_text("0" * 64)
33 return repo
34
35
36 def _add_branch(repo: pathlib.Path, branch: str) -> None:
37 ref = repo / ".muse" / "refs" / "heads" / branch
38 ref.parent.mkdir(parents=True, exist_ok=True)
39 ref.write_text("0" * 64)
40
41
42 # ---------------------------------------------------------------------------
43 # add_worktree
44 # ---------------------------------------------------------------------------
45
46
47 def test_add_worktree_creates_directory(tmp_path: pathlib.Path) -> None:
48 repo = _make_repo(tmp_path)
49 _add_branch(repo, "feat/audio")
50 wt_path = add_worktree(repo, "feat-audio", "feat/audio")
51 assert wt_path.exists()
52 assert (wt_path).exists()
53
54
55 def test_add_worktree_creates_metadata(tmp_path: pathlib.Path) -> None:
56 repo = _make_repo(tmp_path)
57 _add_branch(repo, "dev")
58 add_worktree(repo, "mydev", "dev")
59 meta = repo / ".muse" / "worktrees" / "mydev.json"
60 assert meta.exists()
61 data = json.loads(meta.read_text())
62 assert data["name"] == "mydev"
63 assert data["branch"] == "dev"
64
65
66 def test_add_worktree_creates_head_file(tmp_path: pathlib.Path) -> None:
67 repo = _make_repo(tmp_path)
68 _add_branch(repo, "dev")
69 add_worktree(repo, "mydev", "dev")
70 head = repo / ".muse" / "worktrees" / "mydev.HEAD"
71 assert head.exists()
72 assert "refs/heads/dev" in head.read_text()
73
74
75 def test_add_worktree_rejects_unknown_branch(tmp_path: pathlib.Path) -> None:
76 repo = _make_repo(tmp_path)
77 with pytest.raises(ValueError, match="does not exist"):
78 add_worktree(repo, "bad", "no-such-branch")
79
80
81 def test_add_worktree_rejects_duplicate_name(tmp_path: pathlib.Path) -> None:
82 repo = _make_repo(tmp_path)
83 _add_branch(repo, "dev")
84 add_worktree(repo, "mydev", "dev")
85 with pytest.raises(ValueError, match="already exists"):
86 add_worktree(repo, "mydev", "dev")
87
88
89 def test_add_worktree_rejects_invalid_name(tmp_path: pathlib.Path) -> None:
90 repo = _make_repo(tmp_path)
91 _add_branch(repo, "dev")
92 with pytest.raises(ValueError):
93 add_worktree(repo, "..", "dev")
94
95
96 def test_add_worktree_directory_name_includes_repo_name(tmp_path: pathlib.Path) -> None:
97 repo = _make_repo(tmp_path)
98 _add_branch(repo, "dev")
99 wt_path = add_worktree(repo, "dev", "dev")
100 assert "myproject" in wt_path.name
101
102
103 # ---------------------------------------------------------------------------
104 # list_worktrees
105 # ---------------------------------------------------------------------------
106
107
108 def test_list_worktrees_includes_main(tmp_path: pathlib.Path) -> None:
109 repo = _make_repo(tmp_path)
110 worktrees = list_worktrees(repo)
111 assert any(wt.is_main for wt in worktrees)
112
113
114 def test_list_worktrees_includes_linked(tmp_path: pathlib.Path) -> None:
115 repo = _make_repo(tmp_path)
116 _add_branch(repo, "dev")
117 add_worktree(repo, "dev", "dev")
118 worktrees = list_worktrees(repo)
119 names = [wt.name for wt in worktrees]
120 assert "dev" in names
121
122
123 def test_list_worktrees_empty_repo(tmp_path: pathlib.Path) -> None:
124 repo = _make_repo(tmp_path)
125 worktrees = list_worktrees(repo)
126 assert len(worktrees) == 1 # only main
127 assert worktrees[0].is_main
128
129
130 # ---------------------------------------------------------------------------
131 # remove_worktree
132 # ---------------------------------------------------------------------------
133
134
135 def test_remove_worktree_removes_directory(tmp_path: pathlib.Path) -> None:
136 repo = _make_repo(tmp_path)
137 _add_branch(repo, "dev")
138 wt_path = add_worktree(repo, "dev", "dev")
139 assert wt_path.exists()
140 remove_worktree(repo, "dev")
141 assert not wt_path.exists()
142
143
144 def test_remove_worktree_removes_metadata(tmp_path: pathlib.Path) -> None:
145 repo = _make_repo(tmp_path)
146 _add_branch(repo, "dev")
147 add_worktree(repo, "dev", "dev")
148 remove_worktree(repo, "dev")
149 meta = repo / ".muse" / "worktrees" / "dev.json"
150 assert not meta.exists()
151
152
153 def test_remove_worktree_not_found_raises(tmp_path: pathlib.Path) -> None:
154 repo = _make_repo(tmp_path)
155 with pytest.raises(ValueError, match="does not exist"):
156 remove_worktree(repo, "nonexistent")
157
158
159 def test_remove_worktree_not_in_list_after_removal(tmp_path: pathlib.Path) -> None:
160 repo = _make_repo(tmp_path)
161 _add_branch(repo, "dev")
162 add_worktree(repo, "dev", "dev")
163 remove_worktree(repo, "dev")
164 worktrees = list_worktrees(repo)
165 names = [wt.name for wt in worktrees]
166 assert "dev" not in names
167
168
169 # ---------------------------------------------------------------------------
170 # prune_worktrees
171 # ---------------------------------------------------------------------------
172
173
174 def test_prune_removes_stale_metadata(tmp_path: pathlib.Path) -> None:
175 repo = _make_repo(tmp_path)
176 _add_branch(repo, "dev")
177 wt_path = add_worktree(repo, "dev", "dev")
178 # Manually delete the worktree directory to simulate external removal.
179 import shutil
180 shutil.rmtree(wt_path)
181 pruned = prune_worktrees(repo)
182 assert "dev" in pruned
183
184
185 def test_prune_does_nothing_when_all_present(tmp_path: pathlib.Path) -> None:
186 repo = _make_repo(tmp_path)
187 _add_branch(repo, "dev")
188 add_worktree(repo, "dev", "dev")
189 pruned = prune_worktrees(repo)
190 assert pruned == []
191
192
193 def test_prune_empty_repo(tmp_path: pathlib.Path) -> None:
194 repo = _make_repo(tmp_path)
195 assert prune_worktrees(repo) == []
196
197
198 # ---------------------------------------------------------------------------
199 # Stress: multiple worktrees
200 # ---------------------------------------------------------------------------
201
202
203 def test_stress_many_worktrees(tmp_path: pathlib.Path) -> None:
204 """Creating 10 worktrees should all succeed and be listed."""
205 repo = _make_repo(tmp_path)
206 for i in range(10):
207 branch = f"feat-{i}"
208 _add_branch(repo, branch)
209 add_worktree(repo, f"wt{i}", branch)
210
211 worktrees = list_worktrees(repo)
212 linked = [wt for wt in worktrees if not wt.is_main]
213 assert len(linked) == 10