gabriel / muse public
test_core_workspace.py python
311 lines 11.8 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
1 """Tests for muse/core/workspace.py — multi-repository workspace management."""
2
3 from __future__ import annotations
4
5 import inspect
6 import json
7 import pathlib
8 import time
9
10 import pytest
11
12 from muse.core.workspace import (
13 WorkspaceMemberStatus,
14 add_workspace_member,
15 list_workspace_members,
16 remove_workspace_member,
17 )
18
19
20 # ---------------------------------------------------------------------------
21 # Helpers
22 # ---------------------------------------------------------------------------
23
24
25 def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path:
26 muse = tmp_path / ".muse"
27 for d in ("objects", "commits", "snapshots", "refs/heads"):
28 (muse / d).mkdir(parents=True, exist_ok=True)
29 (muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo"}))
30 (muse / "HEAD").write_text("ref: refs/heads/main\n")
31 (muse / "refs" / "heads" / "main").write_text("0" * 64)
32 return tmp_path
33
34
35 # ---------------------------------------------------------------------------
36 # add_workspace_member
37 # ---------------------------------------------------------------------------
38
39
40 def test_add_member_creates_manifest(tmp_path: pathlib.Path) -> None:
41 repo = _make_repo(tmp_path)
42 add_workspace_member(repo, "core", "https://musehub.ai/acme/core")
43 manifest_path = repo / ".muse" / "workspace.toml"
44 assert manifest_path.exists()
45
46
47 def test_add_member_stores_name_and_url(tmp_path: pathlib.Path) -> None:
48 repo = _make_repo(tmp_path)
49 add_workspace_member(repo, "sounds", "https://musehub.ai/acme/sounds")
50 members = list_workspace_members(repo)
51 assert len(members) == 1
52 assert members[0].name == "sounds"
53 assert members[0].url == "https://musehub.ai/acme/sounds"
54
55
56 def test_add_member_default_path(tmp_path: pathlib.Path) -> None:
57 repo = _make_repo(tmp_path)
58 add_workspace_member(repo, "core", "https://example.com/core")
59 members = list_workspace_members(repo)
60 assert "repos/core" in str(members[0].path)
61
62
63 def test_add_member_custom_path(tmp_path: pathlib.Path) -> None:
64 repo = _make_repo(tmp_path)
65 add_workspace_member(repo, "core", "https://example.com/core", path="vendor/core")
66 members = list_workspace_members(repo)
67 assert "vendor/core" in str(members[0].path)
68
69
70 def test_add_member_default_branch_is_main(tmp_path: pathlib.Path) -> None:
71 repo = _make_repo(tmp_path)
72 add_workspace_member(repo, "core", "https://example.com/core")
73 members = list_workspace_members(repo)
74 assert members[0].branch == "main"
75
76
77 def test_add_member_custom_branch(tmp_path: pathlib.Path) -> None:
78 repo = _make_repo(tmp_path)
79 add_workspace_member(repo, "data", "https://example.com/data", branch="v2")
80 members = list_workspace_members(repo)
81 assert members[0].branch == "v2"
82
83
84 def test_add_duplicate_member_raises(tmp_path: pathlib.Path) -> None:
85 repo = _make_repo(tmp_path)
86 add_workspace_member(repo, "core", "https://example.com/core")
87 with pytest.raises(ValueError, match="already exists"):
88 add_workspace_member(repo, "core", "https://example.com/other")
89
90
91 def test_add_multiple_members(tmp_path: pathlib.Path) -> None:
92 repo = _make_repo(tmp_path)
93 add_workspace_member(repo, "core", "https://example.com/core")
94 add_workspace_member(repo, "sounds", "https://example.com/sounds")
95 add_workspace_member(repo, "docs", "https://example.com/docs")
96 members = list_workspace_members(repo)
97 assert len(members) == 3
98
99
100 # ---------------------------------------------------------------------------
101 # remove_workspace_member
102 # ---------------------------------------------------------------------------
103
104
105 def test_remove_member_removes_from_manifest(tmp_path: pathlib.Path) -> None:
106 repo = _make_repo(tmp_path)
107 add_workspace_member(repo, "core", "https://example.com/core")
108 remove_workspace_member(repo, "core")
109 members = list_workspace_members(repo)
110 assert len(members) == 0
111
112
113 def test_remove_nonexistent_member_raises(tmp_path: pathlib.Path) -> None:
114 repo = _make_repo(tmp_path)
115 # First add a member so the manifest exists, then try to remove a nonexistent one.
116 add_workspace_member(repo, "core", "https://example.com/core")
117 with pytest.raises(ValueError, match="not found"):
118 remove_workspace_member(repo, "nonexistent")
119
120
121 def test_remove_no_manifest_raises(tmp_path: pathlib.Path) -> None:
122 repo = _make_repo(tmp_path)
123 with pytest.raises(ValueError, match="No workspace manifest"):
124 remove_workspace_member(repo, "anything")
125
126
127 def test_remove_only_removes_named_member(tmp_path: pathlib.Path) -> None:
128 repo = _make_repo(tmp_path)
129 add_workspace_member(repo, "core", "https://example.com/core")
130 add_workspace_member(repo, "sounds", "https://example.com/sounds")
131 remove_workspace_member(repo, "core")
132 members = list_workspace_members(repo)
133 assert len(members) == 1
134 assert members[0].name == "sounds"
135
136
137 # ---------------------------------------------------------------------------
138 # list_workspace_members
139 # ---------------------------------------------------------------------------
140
141
142 def test_list_returns_empty_when_no_manifest(tmp_path: pathlib.Path) -> None:
143 repo = _make_repo(tmp_path)
144 assert list_workspace_members(repo) == []
145
146
147 def test_list_present_false_when_not_cloned(tmp_path: pathlib.Path) -> None:
148 repo = _make_repo(tmp_path)
149 add_workspace_member(repo, "core", "https://example.com/core")
150 members = list_workspace_members(repo)
151 assert members[0].present is False
152
153
154 def test_list_present_true_when_cloned(tmp_path: pathlib.Path) -> None:
155 repo = _make_repo(tmp_path)
156 add_workspace_member(repo, "local", str(tmp_path / "local_clone"), path="local_clone")
157 # Simulate a cloned repo at the expected path.
158 clone_path = tmp_path / "local_clone"
159 (clone_path / ".muse").mkdir(parents=True, exist_ok=True)
160 members = list_workspace_members(repo)
161 assert members[0].present is True
162
163
164 def test_list_head_none_when_not_cloned(tmp_path: pathlib.Path) -> None:
165 repo = _make_repo(tmp_path)
166 add_workspace_member(repo, "core", "https://example.com/core")
167 members = list_workspace_members(repo)
168 assert members[0].head_commit is None
169
170
171 def test_list_returns_workspace_member_status(tmp_path: pathlib.Path) -> None:
172 repo = _make_repo(tmp_path)
173 add_workspace_member(repo, "core", "https://example.com/core")
174 members = list_workspace_members(repo)
175 assert isinstance(members[0], WorkspaceMemberStatus)
176
177
178 # ---------------------------------------------------------------------------
179 # Performance — parallelism and no-subprocess shelf count
180 # ---------------------------------------------------------------------------
181
182
183 def _make_present_repo(parent: pathlib.Path, name: str) -> pathlib.Path:
184 """Create a minimal present repo inside *parent* and return its path."""
185 repo = parent / name
186 muse = repo / ".muse"
187 for d in ("objects", "commits", "snapshots", "refs/heads"):
188 (muse / d).mkdir(parents=True, exist_ok=True)
189 (muse / "repo.json").write_text(json.dumps({"repo_id": f"test-{name}"}))
190 (muse / "HEAD").write_text("ref: refs/heads/main\n")
191 (muse / "refs" / "heads" / "main").write_text("0" * 64)
192 (muse / "shelf.json").write_text("[]")
193 return repo
194
195
196 def test_list_workspace_members_runs_in_parallel(tmp_path: pathlib.Path) -> None:
197 """list_workspace_members must dispatch per-member work concurrently.
198
199 We verify this structurally: list_workspace_members (or the helper it
200 calls) must use concurrent.futures.ThreadPoolExecutor — a sequential
201 implementation would be unacceptably slow for large workspaces.
202 """
203 import concurrent.futures
204 from muse.core import workspace as ws_module
205
206 source = inspect.getsource(ws_module)
207 assert "ThreadPoolExecutor" in source, (
208 "list_workspace_members must use ThreadPoolExecutor for parallelism"
209 )
210
211
212 def test_shelf_count_does_not_spawn_subprocess(tmp_path: pathlib.Path) -> None:
213 """Shelf count must be read from .muse/shelf.json, not via subprocess.
214
215 A subprocess for every member is the single largest performance cost.
216 The shelf count is a simple list-length read — no subprocess needed.
217 """
218 from muse.core import workspace as ws_module
219
220 source = inspect.getsource(ws_module)
221 # No subprocess spawning for shelf count.
222 assert '"muse", "shelf"' not in source, (
223 "_member_status must not spawn 'muse shelf list' — read shelf.json directly"
224 )
225
226
227 def test_shelf_count_read_from_file(tmp_path: pathlib.Path) -> None:
228 """shelf_count reflects entries in .muse/shelf.json without any subprocess."""
229 workspace = _make_repo(tmp_path)
230 member_path = tmp_path / "member_a"
231 _make_present_repo(tmp_path, "member_a")
232
233 # Write two shelf entries directly to shelf.json.
234 shelf_data = [{"id": "aaa", "intent": "wip1"}, {"id": "bbb", "intent": "wip2"}]
235 (member_path / ".muse" / "shelf.json").write_text(json.dumps(shelf_data))
236
237 add_workspace_member(workspace, "member_a", "https://example.com/a", path="member_a")
238 members = list_workspace_members(workspace)
239 assert members[0].shelf_count == 2, (
240 f"Expected shelf_count=2, got {members[0].shelf_count}"
241 )
242
243
244 def test_shelf_count_zero_when_file_empty(tmp_path: pathlib.Path) -> None:
245 """shelf_count is 0 when shelf.json holds an empty list."""
246 workspace = _make_repo(tmp_path)
247 _make_present_repo(tmp_path, "member_b")
248 add_workspace_member(workspace, "member_b", "https://example.com/b", path="member_b")
249 members = list_workspace_members(workspace)
250 assert members[0].shelf_count == 0
251
252
253 def test_shelf_count_zero_when_file_absent(tmp_path: pathlib.Path) -> None:
254 """shelf_count is 0 when .muse/shelf.json does not exist."""
255 workspace = _make_repo(tmp_path)
256 member_path = _make_present_repo(tmp_path, "member_c")
257 (member_path / ".muse" / "shelf.json").unlink()
258 add_workspace_member(workspace, "member_c", "https://example.com/c", path="member_c")
259 members = list_workspace_members(workspace)
260 assert members[0].shelf_count == 0
261
262
263 def test_list_workspace_members_wall_time(tmp_path: pathlib.Path) -> None:
264 """Seven present members must complete in under 2 s wall time.
265
266 The sequential implementation with two subprocesses per member takes
267 ~3–4 s. Parallelism plus file-I/O shelf counting brings this well
268 under 1 s; 2 s is a generous upper bound that rules out regression
269 without being fragile on slow CI machines.
270 """
271 workspace = _make_repo(tmp_path)
272 for i in range(7):
273 _make_present_repo(tmp_path, f"repo{i}")
274 add_workspace_member(
275 workspace, f"repo{i}", f"https://example.com/repo{i}", path=f"repo{i}"
276 )
277 t0 = time.perf_counter()
278 members = list_workspace_members(workspace)
279 elapsed = time.perf_counter() - t0
280 assert len(members) == 7
281 assert elapsed < 2.0, (
282 f"list_workspace_members took {elapsed:.2f}s for 7 members (limit 2.0s). "
283 "Check that members are processed in parallel and shelf uses file I/O."
284 )
285
286
287 # ---------------------------------------------------------------------------
288 # Stress
289 # ---------------------------------------------------------------------------
290
291
292 def test_stress_50_members(tmp_path: pathlib.Path) -> None:
293 """Adding 50 members should all be preserved and listed correctly."""
294 repo = _make_repo(tmp_path)
295 for i in range(50):
296 add_workspace_member(repo, f"svc{i}", f"https://example.com/svc{i}")
297 members = list_workspace_members(repo)
298 assert len(members) == 50
299 names = {m.name for m in members}
300 for i in range(50):
301 assert f"svc{i}" in names
302
303
304 def test_stress_add_remove_cycle(tmp_path: pathlib.Path) -> None:
305 """Add and remove 20 members; manifest should be empty at the end."""
306 repo = _make_repo(tmp_path)
307 for i in range(20):
308 add_workspace_member(repo, f"repo{i}", f"https://example.com/repo{i}")
309 for i in range(20):
310 remove_workspace_member(repo, f"repo{i}")
311 assert list_workspace_members(repo) == []
File History 3 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 138 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 140 days ago