gabriel / muse public
test_workspace_supercharge.py python
530 lines 19.8 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
1 """Supercharge tests for ``muse workspace`` — agent-usability, coverage gaps.
2
3 Coverage matrix
4 ---------------
5 - duration_ms: every JSON-outputting subcommand includes it
6 - exit_code: every JSON-outputting subcommand includes it
7 - branch_mismatch: boolean flag in member JSON when actual_branch != branch
8 - list/status envelopes: {members, exit_code, duration_ms} — not bare arrays
9 - sync exit_code: reflects error_count > 0
10 - TypedDicts: verify fields exist in class annotations
11 - Docstrings: sync docstring covers JSON envelope fields
12 - Performance: duration_ms reported within reasonable bounds
13 """
14
15 from __future__ import annotations
16
17 import json
18 import pathlib
19 import subprocess
20 import sys
21
22 import pytest
23
24 from muse.cli.commands.workspace import (
25 _WorkspaceAddJson,
26 _WorkspaceMemberJson,
27 _WorkspaceRemoveJson,
28 _WorkspaceSyncJson,
29 _WorkspaceUpdateJson,
30 _member_to_json,
31 run_workspace_sync,
32 )
33 from muse.core.workspace import (
34 WorkspaceMemberStatus,
35 add_workspace_member,
36 )
37
38
39 # ---------------------------------------------------------------------------
40 # Helpers
41 # ---------------------------------------------------------------------------
42
43
44 def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path:
45 muse = tmp_path / ".muse"
46 for d in ("objects", "commits", "snapshots", "refs/heads"):
47 (muse / d).mkdir(parents=True, exist_ok=True)
48 (muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo"}))
49 (muse / "HEAD").write_text("ref: refs/heads/main\n")
50 (muse / "refs" / "heads" / "main").write_text("0" * 64)
51 return tmp_path
52
53
54 def _cli(args: list[str], cwd: pathlib.Path) -> tuple[str, str, int]:
55 result = subprocess.run(
56 [sys.executable, "-m", "muse.cli.app"] + args,
57 capture_output=True,
58 text=True,
59 cwd=str(cwd),
60 )
61 return result.stdout, result.stderr, result.returncode
62
63
64 def _add(repo: pathlib.Path, name: str = "core", url: str = "https://musehub.ai/acme/core") -> None:
65 add_workspace_member(repo, name, url)
66
67
68 def _fake_member(
69 *,
70 present: bool = False,
71 actual_branch: str | None = None,
72 branch: str = "main",
73 ) -> WorkspaceMemberStatus:
74 return WorkspaceMemberStatus(
75 name="core",
76 url="https://musehub.ai/acme/core",
77 path=pathlib.Path("/tmp/core"),
78 branch=branch,
79 present=present,
80 head_commit=None,
81 dirty=False,
82 actual_branch=actual_branch,
83 shelf_count=0,
84 feature_branches=[],
85 )
86
87
88 # ---------------------------------------------------------------------------
89 # duration_ms — every JSON subcommand must include it
90 # ---------------------------------------------------------------------------
91
92
93 class TestDurationMs:
94
95 def test_add_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
96 repo = _make_repo(tmp_path)
97 out, _, rc = _cli(["workspace", "add", "core", "https://musehub.ai/acme/core", "--json"], repo)
98 assert rc == 0
99 data = json.loads(out)
100 assert "duration_ms" in data
101
102 def test_update_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
103 repo = _make_repo(tmp_path)
104 _add(repo)
105 out, _, rc = _cli(["workspace", "update", "core", "--branch", "dev", "--json"], repo)
106 assert rc == 0
107 data = json.loads(out)
108 assert "duration_ms" in data
109
110 def test_remove_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
111 repo = _make_repo(tmp_path)
112 _add(repo)
113 out, _, rc = _cli(["workspace", "remove", "core", "--json"], repo)
114 assert rc == 0
115 data = json.loads(out)
116 assert "duration_ms" in data
117
118 def test_list_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
119 repo = _make_repo(tmp_path)
120 _add(repo)
121 out, _, rc = _cli(["workspace", "list", "--json"], repo)
122 assert rc == 0
123 data = json.loads(out)
124 assert "duration_ms" in data
125
126 def test_status_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
127 repo = _make_repo(tmp_path)
128 _add(repo)
129 out, _, rc = _cli(["workspace", "status", "--json"], repo)
130 assert rc == 0
131 data = json.loads(out)
132 assert "duration_ms" in data
133
134 def test_sync_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
135 repo = _make_repo(tmp_path)
136 _add(repo)
137 out, _, rc = _cli(["workspace", "sync", "--dry-run", "--json"], repo)
138 assert rc == 0
139 data = json.loads(out)
140 assert "duration_ms" in data
141
142 def test_duration_ms_is_float(self, tmp_path: pathlib.Path) -> None:
143 repo = _make_repo(tmp_path)
144 _add(repo)
145 out, _, _ = _cli(["workspace", "list", "--json"], repo)
146 data = json.loads(out)
147 assert isinstance(data["duration_ms"], (int, float))
148
149 def test_duration_ms_is_non_negative(self, tmp_path: pathlib.Path) -> None:
150 repo = _make_repo(tmp_path)
151 _add(repo)
152 out, _, _ = _cli(["workspace", "list", "--json"], repo)
153 data = json.loads(out)
154 assert data["duration_ms"] >= 0
155
156
157 # ---------------------------------------------------------------------------
158 # exit_code — every JSON subcommand must include it
159 # ---------------------------------------------------------------------------
160
161
162 class TestExitCode:
163
164 def test_add_json_has_exit_code(self, tmp_path: pathlib.Path) -> None:
165 repo = _make_repo(tmp_path)
166 out, _, rc = _cli(["workspace", "add", "core", "https://musehub.ai/acme/core", "--json"], repo)
167 assert rc == 0
168 data = json.loads(out)
169 assert "exit_code" in data
170
171 def test_update_json_has_exit_code(self, tmp_path: pathlib.Path) -> None:
172 repo = _make_repo(tmp_path)
173 _add(repo)
174 out, _, rc = _cli(["workspace", "update", "core", "--branch", "dev", "--json"], repo)
175 assert rc == 0
176 data = json.loads(out)
177 assert "exit_code" in data
178
179 def test_remove_json_has_exit_code(self, tmp_path: pathlib.Path) -> None:
180 repo = _make_repo(tmp_path)
181 _add(repo)
182 out, _, rc = _cli(["workspace", "remove", "core", "--json"], repo)
183 assert rc == 0
184 data = json.loads(out)
185 assert "exit_code" in data
186
187 def test_list_json_has_exit_code(self, tmp_path: pathlib.Path) -> None:
188 repo = _make_repo(tmp_path)
189 out, _, rc = _cli(["workspace", "list", "--json"], repo)
190 assert rc == 0
191 data = json.loads(out)
192 assert "exit_code" in data
193
194 def test_status_json_has_exit_code(self, tmp_path: pathlib.Path) -> None:
195 repo = _make_repo(tmp_path)
196 out, _, rc = _cli(["workspace", "status", "--json"], repo)
197 assert rc == 0
198 data = json.loads(out)
199 assert "exit_code" in data
200
201 def test_sync_json_has_exit_code(self, tmp_path: pathlib.Path) -> None:
202 repo = _make_repo(tmp_path)
203 out, _, rc = _cli(["workspace", "sync", "--dry-run", "--json"], repo)
204 assert rc == 0
205 data = json.loads(out)
206 assert "exit_code" in data
207
208 def test_add_exit_code_zero_on_success(self, tmp_path: pathlib.Path) -> None:
209 repo = _make_repo(tmp_path)
210 out, _, rc = _cli(["workspace", "add", "core", "https://musehub.ai/acme/core", "--json"], repo)
211 assert rc == 0
212 assert json.loads(out)["exit_code"] == 0
213
214 def test_add_exit_code_one_on_duplicate(self, tmp_path: pathlib.Path) -> None:
215 repo = _make_repo(tmp_path)
216 _add(repo)
217 out, _, rc = _cli(["workspace", "add", "core", "https://musehub.ai/acme/core", "--json"], repo)
218 assert rc == 1
219 data = json.loads(out)
220 assert data["exit_code"] == 1
221
222 def test_list_exit_code_zero_empty(self, tmp_path: pathlib.Path) -> None:
223 repo = _make_repo(tmp_path)
224 out, _, rc = _cli(["workspace", "list", "--json"], repo)
225 assert rc == 0
226 assert json.loads(out)["exit_code"] == 0
227
228 def test_exit_code_is_int(self, tmp_path: pathlib.Path) -> None:
229 repo = _make_repo(tmp_path)
230 out, _, _ = _cli(["workspace", "list", "--json"], repo)
231 data = json.loads(out)
232 assert isinstance(data["exit_code"], int)
233
234 def test_exit_code_mirrors_process_exit(self, tmp_path: pathlib.Path) -> None:
235 """exit_code in JSON must equal the process exit code."""
236 repo = _make_repo(tmp_path)
237 _add(repo)
238 out, _, rc = _cli(["workspace", "add", "core", "https://x.com/y", "--json"], repo)
239 data = json.loads(out)
240 assert data["exit_code"] == rc
241
242
243 # ---------------------------------------------------------------------------
244 # List/Status envelopes — {members, exit_code, duration_ms}, not bare arrays
245 # ---------------------------------------------------------------------------
246
247
248 class TestListEnvelope:
249
250 def test_list_json_is_dict_not_array(self, tmp_path: pathlib.Path) -> None:
251 repo = _make_repo(tmp_path)
252 out, _, _ = _cli(["workspace", "list", "--json"], repo)
253 data = json.loads(out)
254 assert isinstance(data, dict), "list --json must return an envelope dict, not a bare array"
255
256 def test_list_json_has_members_key(self, tmp_path: pathlib.Path) -> None:
257 repo = _make_repo(tmp_path)
258 _add(repo)
259 out, _, _ = _cli(["workspace", "list", "--json"], repo)
260 data = json.loads(out)
261 assert "members" in data
262
263 def test_list_json_members_is_array(self, tmp_path: pathlib.Path) -> None:
264 repo = _make_repo(tmp_path)
265 _add(repo)
266 out, _, _ = _cli(["workspace", "list", "--json"], repo)
267 data = json.loads(out)
268 assert isinstance(data["members"], list)
269
270 def test_list_json_members_count_matches_registered(self, tmp_path: pathlib.Path) -> None:
271 repo = _make_repo(tmp_path)
272 _add(repo, "core")
273 _add(repo, "sounds", "https://musehub.ai/acme/sounds")
274 out, _, _ = _cli(["workspace", "list", "--json"], repo)
275 data = json.loads(out)
276 assert len(data["members"]) == 2
277
278 def test_list_json_empty_envelope(self, tmp_path: pathlib.Path) -> None:
279 repo = _make_repo(tmp_path)
280 out, _, rc = _cli(["workspace", "list", "--json"], repo)
281 assert rc == 0
282 data = json.loads(out)
283 assert data["members"] == []
284 assert data["exit_code"] == 0
285
286 def test_list_json_member_fields_intact(self, tmp_path: pathlib.Path) -> None:
287 repo = _make_repo(tmp_path)
288 _add(repo)
289 out, _, _ = _cli(["workspace", "list", "--json"], repo)
290 member = json.loads(out)["members"][0]
291 for key in ("name", "url", "path", "branch", "present", "head_commit", "dirty",
292 "actual_branch", "shelf_count", "feature_branches"):
293 assert key in member, f"member missing key: {key}"
294
295
296 class TestStatusEnvelope:
297
298 def test_status_json_is_dict_not_array(self, tmp_path: pathlib.Path) -> None:
299 repo = _make_repo(tmp_path)
300 out, _, _ = _cli(["workspace", "status", "--json"], repo)
301 data = json.loads(out)
302 assert isinstance(data, dict), "status --json must return an envelope dict, not a bare array"
303
304 def test_status_json_has_members_key(self, tmp_path: pathlib.Path) -> None:
305 repo = _make_repo(tmp_path)
306 _add(repo)
307 out, _, _ = _cli(["workspace", "status", "--json"], repo)
308 data = json.loads(out)
309 assert "members" in data
310
311 def test_status_json_members_is_array(self, tmp_path: pathlib.Path) -> None:
312 repo = _make_repo(tmp_path)
313 _add(repo)
314 out, _, _ = _cli(["workspace", "status", "--json"], repo)
315 data = json.loads(out)
316 assert isinstance(data["members"], list)
317
318 def test_status_named_json_is_dict(self, tmp_path: pathlib.Path) -> None:
319 repo = _make_repo(tmp_path)
320 _add(repo)
321 out, _, _ = _cli(["workspace", "status", "core", "--json"], repo)
322 data = json.loads(out)
323 assert isinstance(data, dict)
324 assert "members" in data
325 assert len(data["members"]) == 1
326
327 def test_status_json_empty_envelope(self, tmp_path: pathlib.Path) -> None:
328 repo = _make_repo(tmp_path)
329 out, _, rc = _cli(["workspace", "status", "--json"], repo)
330 assert rc == 0
331 data = json.loads(out)
332 assert data["members"] == []
333 assert data["exit_code"] == 0
334
335
336 # ---------------------------------------------------------------------------
337 # branch_mismatch — bool flag in member JSON
338 # ---------------------------------------------------------------------------
339
340
341 class TestBranchMismatch:
342
343 def test_branch_mismatch_field_in_member_to_json(self) -> None:
344 m = _fake_member(branch="main", actual_branch="main")
345 result = _member_to_json(m)
346 assert "branch_mismatch" in result
347
348 def test_branch_mismatch_false_when_on_tracking_branch(self) -> None:
349 m = _fake_member(branch="main", actual_branch="main")
350 result = _member_to_json(m)
351 assert result["branch_mismatch"] is False
352
353 def test_branch_mismatch_true_when_on_different_branch(self) -> None:
354 m = _fake_member(branch="main", actual_branch="dev")
355 result = _member_to_json(m)
356 assert result["branch_mismatch"] is True
357
358 def test_branch_mismatch_false_when_not_present(self) -> None:
359 """Not-cloned members have no actual branch — no mismatch."""
360 m = _fake_member(present=False, actual_branch=None, branch="main")
361 result = _member_to_json(m)
362 assert result["branch_mismatch"] is False
363
364 def test_branch_mismatch_is_bool(self) -> None:
365 m = _fake_member(branch="main", actual_branch="main")
366 result = _member_to_json(m)
367 assert isinstance(result["branch_mismatch"], bool)
368
369 def test_branch_mismatch_in_list_json(self, tmp_path: pathlib.Path) -> None:
370 repo = _make_repo(tmp_path)
371 _add(repo)
372 out, _, _ = _cli(["workspace", "list", "--json"], repo)
373 member = json.loads(out)["members"][0]
374 assert "branch_mismatch" in member
375 assert isinstance(member["branch_mismatch"], bool)
376
377 def test_branch_mismatch_in_status_json(self, tmp_path: pathlib.Path) -> None:
378 repo = _make_repo(tmp_path)
379 _add(repo)
380 out, _, _ = _cli(["workspace", "status", "--json"], repo)
381 member = json.loads(out)["members"][0]
382 assert "branch_mismatch" in member
383
384
385 # ---------------------------------------------------------------------------
386 # sync exit_code reflects error_count
387 # ---------------------------------------------------------------------------
388
389
390 class TestSyncExitCode:
391
392 def test_sync_dry_run_exit_code_zero(self, tmp_path: pathlib.Path) -> None:
393 repo = _make_repo(tmp_path)
394 _add(repo)
395 out, _, rc = _cli(["workspace", "sync", "--dry-run", "--json"], repo)
396 assert rc == 0
397 data = json.loads(out)
398 assert data["exit_code"] == 0
399
400 def test_sync_empty_manifest_exit_code_zero(self, tmp_path: pathlib.Path) -> None:
401 repo = _make_repo(tmp_path)
402 out, _, rc = _cli(["workspace", "sync", "--dry-run", "--json"], repo)
403 assert rc == 0
404 data = json.loads(out)
405 assert data["exit_code"] == 0
406
407 def test_sync_exit_code_is_int(self, tmp_path: pathlib.Path) -> None:
408 repo = _make_repo(tmp_path)
409 out, _, _ = _cli(["workspace", "sync", "--dry-run", "--json"], repo)
410 data = json.loads(out)
411 assert isinstance(data["exit_code"], int)
412
413
414 # ---------------------------------------------------------------------------
415 # TypedDicts — verify annotations carry the new fields
416 # ---------------------------------------------------------------------------
417
418
419 class TestTypedDicts:
420
421 def test_workspace_add_json_has_duration_ms_annotation(self) -> None:
422 assert "duration_ms" in _WorkspaceAddJson.__annotations__
423
424 def test_workspace_add_json_has_exit_code_annotation(self) -> None:
425 assert "exit_code" in _WorkspaceAddJson.__annotations__
426
427 def test_workspace_update_json_has_duration_ms_annotation(self) -> None:
428 assert "duration_ms" in _WorkspaceUpdateJson.__annotations__
429
430 def test_workspace_update_json_has_exit_code_annotation(self) -> None:
431 assert "exit_code" in _WorkspaceUpdateJson.__annotations__
432
433 def test_workspace_remove_json_has_duration_ms_annotation(self) -> None:
434 assert "duration_ms" in _WorkspaceRemoveJson.__annotations__
435
436 def test_workspace_remove_json_has_exit_code_annotation(self) -> None:
437 assert "exit_code" in _WorkspaceRemoveJson.__annotations__
438
439 def test_workspace_sync_json_has_duration_ms_annotation(self) -> None:
440 assert "duration_ms" in _WorkspaceSyncJson.__annotations__
441
442 def test_workspace_sync_json_has_exit_code_annotation(self) -> None:
443 assert "exit_code" in _WorkspaceSyncJson.__annotations__
444
445 def test_workspace_member_json_has_branch_mismatch_annotation(self) -> None:
446 assert "branch_mismatch" in _WorkspaceMemberJson.__annotations__
447
448
449 # ---------------------------------------------------------------------------
450 # Docstrings
451 # ---------------------------------------------------------------------------
452
453
454 class TestDocstrings:
455
456 def test_sync_docstring_mentions_workers(self) -> None:
457 assert "workers" in (run_workspace_sync.__doc__ or "").lower()
458
459 def test_sync_docstring_mentions_dry_run(self) -> None:
460 assert "dry" in (run_workspace_sync.__doc__ or "").lower()
461
462 def test_sync_docstring_mentions_json_fields(self) -> None:
463 doc = run_workspace_sync.__doc__ or ""
464 assert "exit_code" in doc or "duration_ms" in doc or "json" in doc.lower()
465
466 def test_sync_docstring_mentions_exit_code(self) -> None:
467 assert "exit_code" in (run_workspace_sync.__doc__ or "")
468
469 def test_sync_docstring_mentions_duration_ms(self) -> None:
470 assert "duration_ms" in (run_workspace_sync.__doc__ or "")
471
472
473 # ---------------------------------------------------------------------------
474 # Performance — duration_ms stays within reason
475 # ---------------------------------------------------------------------------
476
477
478 class TestPerformance:
479
480 def test_list_10_members_duration_ms_under_2000(self, tmp_path: pathlib.Path) -> None:
481 repo = _make_repo(tmp_path)
482 for i in range(10):
483 _add(repo, f"member{i}", f"https://musehub.ai/acme/m{i}")
484 out, _, rc = _cli(["workspace", "list", "--json"], repo)
485 assert rc == 0
486 data = json.loads(out)
487 assert data["duration_ms"] < 2000, f"list of 10 took {data['duration_ms']}ms"
488
489 def test_status_10_members_duration_ms_under_2000(self, tmp_path: pathlib.Path) -> None:
490 repo = _make_repo(tmp_path)
491 for i in range(10):
492 _add(repo, f"member{i}", f"https://musehub.ai/acme/m{i}")
493 out, _, rc = _cli(["workspace", "status", "--json"], repo)
494 assert rc == 0
495 data = json.loads(out)
496 assert data["duration_ms"] < 2000
497
498 def test_add_duration_ms_under_500(self, tmp_path: pathlib.Path) -> None:
499 repo = _make_repo(tmp_path)
500 out, _, rc = _cli(["workspace", "add", "core", "https://musehub.ai/acme/core", "--json"], repo)
501 assert rc == 0
502 data = json.loads(out)
503 assert data["duration_ms"] < 500
504
505
506 # ---------------------------------------------------------------------------
507 # Flag registration
508 # ---------------------------------------------------------------------------
509
510
511 class TestRegisterFlags:
512 def _parse(self, *args: str):
513 import argparse
514 from muse.cli.commands.workspace import register
515 p = argparse.ArgumentParser()
516 sub = p.add_subparsers()
517 register(sub)
518 return p.parse_args(["workspace", *args])
519
520 def test_default_json_out_is_false_add(self) -> None:
521 ns = self._parse("add", "myrepo", "https://musehub.ai/x/y")
522 assert ns.json_out is False
523
524 def test_json_flag_sets_json_out_add(self) -> None:
525 ns = self._parse("add", "myrepo", "https://musehub.ai/x/y", "--json")
526 assert ns.json_out is True
527
528 def test_j_shorthand_sets_json_out_add(self) -> None:
529 ns = self._parse("add", "myrepo", "https://musehub.ai/x/y", "-j")
530 assert ns.json_out is True
File History 2 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