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