gabriel / muse public
test_worktree_supercharge.py python
397 lines 15.9 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 worktree`` — 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 - list envelope: {worktrees, exit_code, duration_ms} — not a bare array
8 - TypedDicts: verify new fields exist in class annotations
9 - Docstrings: every handler docstring mentions exit_code and duration_ms
10 - Performance: duration_ms is a non-negative float within bounds
11 """
12
13 from __future__ import annotations
14
15 import json
16 import pathlib
17
18 import pytest
19
20 from tests.cli_test_helper import CliRunner, InvokeResult
21 from muse.cli.commands.worktree import (
22 _WorktreeAddJson,
23 _WorktreeListEntryJson,
24 _WorktreeRemoveJson,
25 _WorktreePruneJson,
26 run_worktree_add,
27 run_worktree_list,
28 run_worktree_remove,
29 run_worktree_prune,
30 run_worktree_repair,
31 run_worktree_status,
32 )
33
34 runner = CliRunner()
35
36
37 # ---------------------------------------------------------------------------
38 # Helpers
39 # ---------------------------------------------------------------------------
40
41
42 def _make_repo(tmp_path: pathlib.Path, branch: str = "main") -> pathlib.Path:
43 repo = tmp_path / "myproject"
44 muse = repo / ".muse"
45 for d in ("objects", "commits", "snapshots", "refs/heads"):
46 (muse / d).mkdir(parents=True, exist_ok=True)
47 (muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo"}))
48 (muse / "HEAD").write_text(f"ref: refs/heads/{branch}\n")
49 (muse / "refs" / "heads" / branch).write_text("0" * 64)
50 return repo
51
52
53 def _add_branch(repo: pathlib.Path, branch: str) -> None:
54 ref = repo / ".muse" / "refs" / "heads" / branch
55 ref.parent.mkdir(parents=True, exist_ok=True)
56 ref.write_text("0" * 64)
57
58
59 def _invoke(repo: pathlib.Path, args: list[str]) -> InvokeResult:
60 return runner.invoke(None, args, env={"MUSE_REPO_ROOT": str(repo)})
61
62
63 def _add_worktree(repo: pathlib.Path, name: str = "wt1", branch: str = "main") -> None:
64 _invoke(repo, ["worktree", "add", name, branch])
65
66
67 # ---------------------------------------------------------------------------
68 # duration_ms — every JSON subcommand must include it
69 # ---------------------------------------------------------------------------
70
71
72 class TestDurationMs:
73
74 def test_add_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
75 repo = _make_repo(tmp_path)
76 r = _invoke(repo, ["worktree", "add", "wt1", "main", "--json"])
77 assert r.exit_code == 0
78 assert "duration_ms" in json.loads(r.output)
79
80 def test_list_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
81 repo = _make_repo(tmp_path)
82 r = _invoke(repo, ["worktree", "list", "--json"])
83 assert r.exit_code == 0
84 assert "duration_ms" in json.loads(r.output)
85
86 def test_status_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
87 repo = _make_repo(tmp_path)
88 r = _invoke(repo, ["worktree", "status", "main", "--json"])
89 assert r.exit_code == 0
90 assert "duration_ms" in json.loads(r.output)
91
92 def test_remove_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
93 repo = _make_repo(tmp_path)
94 _add_worktree(repo)
95 r = _invoke(repo, ["worktree", "remove", "wt1", "--json"])
96 assert r.exit_code == 0
97 assert "duration_ms" in json.loads(r.output)
98
99 def test_prune_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
100 repo = _make_repo(tmp_path)
101 r = _invoke(repo, ["worktree", "prune", "--json"])
102 assert r.exit_code == 0
103 assert "duration_ms" in json.loads(r.output)
104
105 def test_repair_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
106 repo = _make_repo(tmp_path)
107 r = _invoke(repo, ["worktree", "repair", "--json"])
108 assert r.exit_code == 0
109 assert "duration_ms" in json.loads(r.output)
110
111 def test_duration_ms_is_numeric(self, tmp_path: pathlib.Path) -> None:
112 repo = _make_repo(tmp_path)
113 r = _invoke(repo, ["worktree", "list", "--json"])
114 data = json.loads(r.output)
115 assert isinstance(data["duration_ms"], (int, float))
116
117 def test_duration_ms_is_non_negative(self, tmp_path: pathlib.Path) -> None:
118 repo = _make_repo(tmp_path)
119 r = _invoke(repo, ["worktree", "list", "--json"])
120 data = json.loads(r.output)
121 assert data["duration_ms"] >= 0
122
123
124 # ---------------------------------------------------------------------------
125 # exit_code — every JSON subcommand must include it
126 # ---------------------------------------------------------------------------
127
128
129 class TestExitCode:
130
131 def test_add_json_has_exit_code(self, tmp_path: pathlib.Path) -> None:
132 repo = _make_repo(tmp_path)
133 r = _invoke(repo, ["worktree", "add", "wt1", "main", "--json"])
134 assert r.exit_code == 0
135 assert "exit_code" in json.loads(r.output)
136
137 def test_list_json_has_exit_code(self, tmp_path: pathlib.Path) -> None:
138 repo = _make_repo(tmp_path)
139 r = _invoke(repo, ["worktree", "list", "--json"])
140 assert r.exit_code == 0
141 assert "exit_code" in json.loads(r.output)
142
143 def test_status_json_has_exit_code(self, tmp_path: pathlib.Path) -> None:
144 repo = _make_repo(tmp_path)
145 r = _invoke(repo, ["worktree", "status", "main", "--json"])
146 assert r.exit_code == 0
147 assert "exit_code" in json.loads(r.output)
148
149 def test_remove_json_has_exit_code(self, tmp_path: pathlib.Path) -> None:
150 repo = _make_repo(tmp_path)
151 _add_worktree(repo)
152 r = _invoke(repo, ["worktree", "remove", "wt1", "--json"])
153 assert r.exit_code == 0
154 assert "exit_code" in json.loads(r.output)
155
156 def test_prune_json_has_exit_code(self, tmp_path: pathlib.Path) -> None:
157 repo = _make_repo(tmp_path)
158 r = _invoke(repo, ["worktree", "prune", "--json"])
159 assert r.exit_code == 0
160 assert "exit_code" in json.loads(r.output)
161
162 def test_repair_json_has_exit_code(self, tmp_path: pathlib.Path) -> None:
163 repo = _make_repo(tmp_path)
164 r = _invoke(repo, ["worktree", "repair", "--json"])
165 assert r.exit_code == 0
166 assert "exit_code" in json.loads(r.output)
167
168 def test_add_exit_code_zero_on_success(self, tmp_path: pathlib.Path) -> None:
169 repo = _make_repo(tmp_path)
170 r = _invoke(repo, ["worktree", "add", "wt1", "main", "--json"])
171 assert json.loads(r.output)["exit_code"] == 0
172
173 def test_list_exit_code_zero(self, tmp_path: pathlib.Path) -> None:
174 repo = _make_repo(tmp_path)
175 r = _invoke(repo, ["worktree", "list", "--json"])
176 assert json.loads(r.output)["exit_code"] == 0
177
178 def test_prune_exit_code_zero_nothing_to_prune(self, tmp_path: pathlib.Path) -> None:
179 repo = _make_repo(tmp_path)
180 r = _invoke(repo, ["worktree", "prune", "--json"])
181 assert json.loads(r.output)["exit_code"] == 0
182
183 def test_exit_code_is_int(self, tmp_path: pathlib.Path) -> None:
184 repo = _make_repo(tmp_path)
185 r = _invoke(repo, ["worktree", "list", "--json"])
186 assert isinstance(json.loads(r.output)["exit_code"], int)
187
188 def test_exit_code_mirrors_process_exit(self, tmp_path: pathlib.Path) -> None:
189 """exit_code in JSON must equal the process exit code on success."""
190 repo = _make_repo(tmp_path)
191 r = _invoke(repo, ["worktree", "add", "wt1", "main", "--json"])
192 data = json.loads(r.output)
193 assert data["exit_code"] == r.exit_code
194
195
196 # ---------------------------------------------------------------------------
197 # list envelope — {worktrees, exit_code, duration_ms}, not a bare array
198 # ---------------------------------------------------------------------------
199
200
201 class TestListEnvelope:
202
203 def test_list_json_is_dict_not_array(self, tmp_path: pathlib.Path) -> None:
204 repo = _make_repo(tmp_path)
205 r = _invoke(repo, ["worktree", "list", "--json"])
206 data = json.loads(r.output)
207 assert isinstance(data, dict), "list --json must return an envelope dict, not a bare array"
208
209 def test_list_json_has_worktrees_key(self, tmp_path: pathlib.Path) -> None:
210 repo = _make_repo(tmp_path)
211 r = _invoke(repo, ["worktree", "list", "--json"])
212 data = json.loads(r.output)
213 assert "worktrees" in data
214
215 def test_list_json_worktrees_is_array(self, tmp_path: pathlib.Path) -> None:
216 repo = _make_repo(tmp_path)
217 r = _invoke(repo, ["worktree", "list", "--json"])
218 data = json.loads(r.output)
219 assert isinstance(data["worktrees"], list)
220
221 def test_list_json_includes_main_in_worktrees(self, tmp_path: pathlib.Path) -> None:
222 repo = _make_repo(tmp_path)
223 r = _invoke(repo, ["worktree", "list", "--json"])
224 data = json.loads(r.output)
225 assert any(w["is_main"] for w in data["worktrees"])
226
227 def test_list_json_entry_fields_intact(self, tmp_path: pathlib.Path) -> None:
228 repo = _make_repo(tmp_path)
229 r = _invoke(repo, ["worktree", "list", "--json"])
230 entry = json.loads(r.output)["worktrees"][0]
231 for key in ("name", "branch", "path", "head_commit", "is_main"):
232 assert key in entry, f"worktree entry missing key: {key}"
233
234 def test_list_json_count_after_add(self, tmp_path: pathlib.Path) -> None:
235 repo = _make_repo(tmp_path)
236 _add_worktree(repo, "wt1")
237 r = _invoke(repo, ["worktree", "list", "--json"])
238 data = json.loads(r.output)
239 # main + wt1
240 assert len(data["worktrees"]) == 2
241
242 def test_list_j_alias_envelope(self, tmp_path: pathlib.Path) -> None:
243 """-j and --json produce the same envelope structure."""
244 repo = _make_repo(tmp_path)
245 r1 = _invoke(repo, ["worktree", "list", "--json"])
246 r2 = _invoke(repo, ["worktree", "list", "-j"])
247 d1 = json.loads(r1.output); d1.pop("duration_ms", None); d1.pop("timestamp", None)
248 d2 = json.loads(r2.output); d2.pop("duration_ms", None); d2.pop("timestamp", None)
249 assert d1 == d2
250
251
252 # ---------------------------------------------------------------------------
253 # TypedDicts — verify annotations carry the new fields
254 # ---------------------------------------------------------------------------
255
256
257 class TestTypedDicts:
258
259 def test_worktree_add_json_has_duration_ms_annotation(self) -> None:
260 assert "duration_ms" in _WorktreeAddJson.__annotations__
261
262 def test_worktree_add_json_has_exit_code_annotation(self) -> None:
263 assert "exit_code" in _WorktreeAddJson.__annotations__
264
265 def test_worktree_list_entry_json_unchanged(self) -> None:
266 """List entry TypedDict keeps its 5 data fields."""
267 for field in ("name", "branch", "path", "head_commit", "is_main"):
268 assert field in _WorktreeListEntryJson.__annotations__
269
270 def test_worktree_remove_json_has_duration_ms_annotation(self) -> None:
271 assert "duration_ms" in _WorktreeRemoveJson.__annotations__
272
273 def test_worktree_remove_json_has_exit_code_annotation(self) -> None:
274 assert "exit_code" in _WorktreeRemoveJson.__annotations__
275
276 def test_worktree_prune_json_has_duration_ms_annotation(self) -> None:
277 assert "duration_ms" in _WorktreePruneJson.__annotations__
278
279 def test_worktree_prune_json_has_exit_code_annotation(self) -> None:
280 assert "exit_code" in _WorktreePruneJson.__annotations__
281
282 def test_worktree_list_json_typeddict_exists(self) -> None:
283 """_WorktreeListJson envelope TypedDict must exist."""
284 from muse.cli.commands.worktree import _WorktreeListJson
285 assert "worktrees" in _WorktreeListJson.__annotations__
286 assert "exit_code" in _WorktreeListJson.__annotations__
287 assert "duration_ms" in _WorktreeListJson.__annotations__
288
289 def test_worktree_status_json_typeddict_exists(self) -> None:
290 """_WorktreeStatusJson TypedDict must exist with exit_code/duration_ms."""
291 from muse.cli.commands.worktree import _WorktreeStatusJson
292 assert "exit_code" in _WorktreeStatusJson.__annotations__
293 assert "duration_ms" in _WorktreeStatusJson.__annotations__
294
295 def test_worktree_repair_json_typeddict_exists(self) -> None:
296 """_WorktreeRepairJson TypedDict must exist with exit_code/duration_ms."""
297 from muse.cli.commands.worktree import _WorktreeRepairJson
298 assert "repaired" in _WorktreeRepairJson.__annotations__
299 assert "exit_code" in _WorktreeRepairJson.__annotations__
300 assert "duration_ms" in _WorktreeRepairJson.__annotations__
301
302
303 # ---------------------------------------------------------------------------
304 # Docstrings — handlers must document exit_code and duration_ms
305 # ---------------------------------------------------------------------------
306
307
308 class TestDocstrings:
309
310 def test_add_docstring_mentions_exit_code(self) -> None:
311 assert "exit_code" in (run_worktree_add.__doc__ or "")
312
313 def test_add_docstring_mentions_duration_ms(self) -> None:
314 assert "duration_ms" in (run_worktree_add.__doc__ or "")
315
316 def test_list_docstring_mentions_exit_code(self) -> None:
317 assert "exit_code" in (run_worktree_list.__doc__ or "")
318
319 def test_list_docstring_mentions_duration_ms(self) -> None:
320 assert "duration_ms" in (run_worktree_list.__doc__ or "")
321
322 def test_status_docstring_mentions_exit_code(self) -> None:
323 assert "exit_code" in (run_worktree_status.__doc__ or "")
324
325 def test_status_docstring_mentions_duration_ms(self) -> None:
326 assert "duration_ms" in (run_worktree_status.__doc__ or "")
327
328 def test_remove_docstring_mentions_exit_code(self) -> None:
329 assert "exit_code" in (run_worktree_remove.__doc__ or "")
330
331 def test_remove_docstring_mentions_duration_ms(self) -> None:
332 assert "duration_ms" in (run_worktree_remove.__doc__ or "")
333
334 def test_prune_docstring_mentions_exit_code(self) -> None:
335 assert "exit_code" in (run_worktree_prune.__doc__ or "")
336
337 def test_prune_docstring_mentions_duration_ms(self) -> None:
338 assert "duration_ms" in (run_worktree_prune.__doc__ or "")
339
340 def test_repair_docstring_mentions_exit_code(self) -> None:
341 assert "exit_code" in (run_worktree_repair.__doc__ or "")
342
343 def test_repair_docstring_mentions_duration_ms(self) -> None:
344 assert "duration_ms" in (run_worktree_repair.__doc__ or "")
345
346
347 # ---------------------------------------------------------------------------
348 # Performance — duration_ms stays within reason
349 # ---------------------------------------------------------------------------
350
351
352 class TestPerformance:
353
354 def test_list_duration_ms_under_1000(self, tmp_path: pathlib.Path) -> None:
355 repo = _make_repo(tmp_path)
356 r = _invoke(repo, ["worktree", "list", "--json"])
357 assert r.exit_code == 0
358 assert json.loads(r.output)["duration_ms"] < 1000
359
360 def test_add_duration_ms_under_1000(self, tmp_path: pathlib.Path) -> None:
361 repo = _make_repo(tmp_path)
362 r = _invoke(repo, ["worktree", "add", "wt1", "main", "--json"])
363 assert r.exit_code == 0
364 assert json.loads(r.output)["duration_ms"] < 1000
365
366 def test_prune_duration_ms_under_1000(self, tmp_path: pathlib.Path) -> None:
367 repo = _make_repo(tmp_path)
368 r = _invoke(repo, ["worktree", "prune", "--json"])
369 assert r.exit_code == 0
370 assert json.loads(r.output)["duration_ms"] < 1000
371
372
373 # ---------------------------------------------------------------------------
374 # Flag registration
375 # ---------------------------------------------------------------------------
376
377
378 class TestRegisterFlags:
379 def _parse(self, *args: str):
380 import argparse
381 from muse.cli.commands.worktree import register
382 p = argparse.ArgumentParser()
383 sub = p.add_subparsers()
384 register(sub)
385 return p.parse_args(["worktree", *args])
386
387 def test_default_json_out_is_false_list(self) -> None:
388 ns = self._parse("list")
389 assert ns.json_out is False
390
391 def test_json_flag_sets_json_out_list(self) -> None:
392 ns = self._parse("list", "--json")
393 assert ns.json_out is True
394
395 def test_j_shorthand_sets_json_out_list(self) -> None:
396 ns = self._parse("list", "-j")
397 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