gabriel / muse public
test_mv_supercharge.py python
411 lines 15.6 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 135 days ago
1 """Supercharge tests for ``muse mv``.
2
3 Coverage tiers
4 --------------
5 - JSON envelope: exit_code and duration_ms present on success and dry_run
6 - Error payload: errors go to stdout as JSON in --json mode, no dual stderr prose
7 - Data integrity: sha256: OID prefix preserved; source/dest echoed verbatim
8 - TypedDicts: _MvResultJson and _MvErrorJson with required annotations
9 - Docstring: module docstring covers exit_code and duration_ms
10 - No-prose pollution: JSON stdout is valid on all non-error paths
11 - Stress: 50-file move-all with --json, correct stage state
12 """
13 from __future__ import annotations
14 from collections.abc import Mapping
15
16 import datetime
17 import json
18 import pathlib
19 from typing import get_type_hints
20
21 from muse.core.object_store import write_object
22 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
23 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
24 from muse.core._types import Manifest, blob_id
25 from muse.plugins.code.stage import make_entry, read_stage, write_stage
26 from tests.cli_test_helper import CliRunner
27
28 runner = CliRunner()
29
30 _REPO_ID = "mv-supercharge"
31 _DT = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
32
33
34 # ---------------------------------------------------------------------------
35 # Helpers
36 # ---------------------------------------------------------------------------
37
38
39 def _sha(data: bytes) -> str:
40 return blob_id(data)
41
42
43 def _init_repo(tmp_path: pathlib.Path) -> pathlib.Path:
44 muse = tmp_path / ".muse"
45 for d in ("commits", "snapshots", "objects", "refs/heads", "code"):
46 (muse / d).mkdir(parents=True, exist_ok=True)
47 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
48 (muse / "repo.json").write_text(
49 json.dumps({"repo_id": _REPO_ID, "domain": "code"}), encoding="utf-8"
50 )
51 return tmp_path
52
53
54 def _env(root: pathlib.Path) -> Mapping[str, str]:
55 return {"MUSE_REPO_ROOT": str(root)}
56
57
58 def _commit(root: pathlib.Path, files: Mapping[str, bytes]) -> str:
59 manifest: Manifest = {}
60 for rel, content in files.items():
61 oid = _sha(content)
62 write_object(root, oid, content)
63 manifest[rel] = oid
64 abs_p = root / rel
65 abs_p.parent.mkdir(parents=True, exist_ok=True)
66 abs_p.write_bytes(content)
67 snap_id = compute_snapshot_id(manifest)
68 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest, created_at=_DT))
69 cid = compute_commit_id(
70 repo_id=_REPO_ID,
71 parent_ids=[],
72 snapshot_id=snap_id,
73 message="test",
74 committed_at_iso=_DT.isoformat(),
75 )
76 write_commit(root, CommitRecord(
77 commit_id=cid, repo_id=_REPO_ID, created_on_branch="main",
78 snapshot_id=snap_id, message="test", committed_at=_DT,
79 ))
80 (root / ".muse" / "refs" / "heads" / "main").write_text(cid, encoding="utf-8")
81 return cid
82
83
84 def _mv(root: pathlib.Path, *args: str):
85 from muse.cli.app import main as cli
86 return runner.invoke(cli, ["mv", *args], env=_env(root))
87
88
89 # ---------------------------------------------------------------------------
90 # JSON envelope — exit_code
91 # ---------------------------------------------------------------------------
92
93
94 class TestJsonEnvelopeExitCode:
95 def test_success_has_exit_code_zero(self, tmp_path: pathlib.Path) -> None:
96 root = _init_repo(tmp_path)
97 _commit(root, {"a.py": b"# a\n"})
98 r = _mv(root, "--json", "a.py", "b.py")
99 assert r.exit_code == 0
100 d = json.loads(r.output)
101 assert "exit_code" in d, "exit_code missing from success envelope"
102 assert d["exit_code"] == 0
103
104 def test_dry_run_has_exit_code_zero(self, tmp_path: pathlib.Path) -> None:
105 root = _init_repo(tmp_path)
106 _commit(root, {"a.py": b"# a\n"})
107 r = _mv(root, "--json", "--dry-run", "a.py", "b.py")
108 assert r.exit_code == 0
109 d = json.loads(r.output)
110 assert "exit_code" in d, "exit_code missing from dry_run envelope"
111 assert d["exit_code"] == 0
112
113 def test_error_payload_has_exit_code_nonzero(self, tmp_path: pathlib.Path) -> None:
114 root = _init_repo(tmp_path)
115 _commit(root, {"anchor.py": b"# a\n"})
116 r = _mv(root, "--json", "ghost.py", "dest.py")
117 d = json.loads(r.output)
118 assert "exit_code" in d
119 assert d["exit_code"] != 0
120
121
122 # ---------------------------------------------------------------------------
123 # JSON envelope — duration_ms
124 # ---------------------------------------------------------------------------
125
126
127 class TestJsonEnvelopeDurationMs:
128 def test_success_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
129 root = _init_repo(tmp_path)
130 _commit(root, {"a.py": b"# a\n"})
131 r = _mv(root, "--json", "a.py", "b.py")
132 d = json.loads(r.output)
133 assert "duration_ms" in d, "duration_ms missing from success envelope"
134 assert isinstance(d["duration_ms"], float)
135 assert d["duration_ms"] >= 0.0
136
137 def test_dry_run_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
138 root = _init_repo(tmp_path)
139 _commit(root, {"a.py": b"# a\n"})
140 r = _mv(root, "--json", "--dry-run", "a.py", "b.py")
141 d = json.loads(r.output)
142 assert "duration_ms" in d, "duration_ms missing from dry_run envelope"
143 assert isinstance(d["duration_ms"], float)
144
145 def test_force_move_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
146 root = _init_repo(tmp_path)
147 _commit(root, {"src.py": b"# s\n", "dst.py": b"# d\n"})
148 r = _mv(root, "--json", "--force", "src.py", "dst.py")
149 d = json.loads(r.output)
150 assert "duration_ms" in d
151
152
153 # ---------------------------------------------------------------------------
154 # Error payload — errors route to stdout as JSON in --json mode
155 # ---------------------------------------------------------------------------
156
157
158 class TestErrorPayload:
159 def test_untracked_source_error_goes_to_stdout(self, tmp_path: pathlib.Path) -> None:
160 root = _init_repo(tmp_path)
161 _commit(root, {"anchor.py": b"# a\n"})
162 (root / "ghost.py").write_text("# untracked\n")
163 r = _mv(root, "--json", "ghost.py", "dest.py")
164 assert r.exit_code != 0
165 assert not r.stderr.strip(), f"unexpected stderr: {r.stderr!r}"
166 d = json.loads(r.output)
167 assert d["status"] == "error"
168
169 def test_missing_disk_source_error_goes_to_stdout(self, tmp_path: pathlib.Path) -> None:
170 root = _init_repo(tmp_path)
171 _commit(root, {"a.py": b"# a\n"})
172 (root / "a.py").unlink()
173 r = _mv(root, "--json", "a.py", "b.py")
174 assert r.exit_code != 0
175 assert not r.stderr.strip(), f"unexpected stderr: {r.stderr!r}"
176 d = json.loads(r.output)
177 assert d["status"] == "error"
178
179 def test_dest_already_tracked_error_goes_to_stdout(self, tmp_path: pathlib.Path) -> None:
180 root = _init_repo(tmp_path)
181 _commit(root, {"src.py": b"# s\n", "dst.py": b"# d\n"})
182 r = _mv(root, "--json", "src.py", "dst.py")
183 assert r.exit_code != 0
184 assert not r.stderr.strip(), f"unexpected stderr: {r.stderr!r}"
185 d = json.loads(r.output)
186 assert d["status"] == "error"
187
188 def test_dest_exists_on_disk_error_goes_to_stdout(self, tmp_path: pathlib.Path) -> None:
189 root = _init_repo(tmp_path)
190 _commit(root, {"src.py": b"# s\n"})
191 (root / "dst.py").write_text("# existing\n")
192 r = _mv(root, "--json", "src.py", "dst.py")
193 assert r.exit_code != 0
194 assert not r.stderr.strip(), f"unexpected stderr: {r.stderr!r}"
195 d = json.loads(r.output)
196 assert d["status"] == "error"
197
198 def test_path_traversal_error_goes_to_stderr_in_text_mode(self, tmp_path: pathlib.Path) -> None:
199 """Path traversal in text mode is caught before --json is parsed — stderr is OK."""
200 root = _init_repo(tmp_path)
201 _commit(root, {"anchor.py": b"# a\n"})
202 r = _mv(root, "../../../etc/passwd", "dest.py")
203 assert r.exit_code != 0
204
205 def test_error_payload_has_status_error(self, tmp_path: pathlib.Path) -> None:
206 root = _init_repo(tmp_path)
207 _commit(root, {"anchor.py": b"# a\n"})
208 r = _mv(root, "--json", "ghost.py", "dest.py")
209 d = json.loads(r.output)
210 assert d["status"] == "error"
211
212 def test_error_payload_has_error_field(self, tmp_path: pathlib.Path) -> None:
213 root = _init_repo(tmp_path)
214 _commit(root, {"anchor.py": b"# a\n"})
215 r = _mv(root, "--json", "ghost.py", "dest.py")
216 d = json.loads(r.output)
217 assert "error" in d
218 assert d["error"]
219
220 def test_no_emoji_on_stderr_in_json_mode(self, tmp_path: pathlib.Path) -> None:
221 root = _init_repo(tmp_path)
222 _commit(root, {"anchor.py": b"# a\n"})
223 r = _mv(root, "--json", "ghost.py", "dest.py")
224 assert "❌" not in r.stderr
225
226
227 # ---------------------------------------------------------------------------
228 # Data integrity
229 # ---------------------------------------------------------------------------
230
231
232 class TestDataIntegrity:
233 def test_object_id_is_sha256_prefixed(self, tmp_path: pathlib.Path) -> None:
234 """object_id in JSON response must carry sha256: prefix."""
235 root = _init_repo(tmp_path)
236 content = b"# content\n"
237 _commit(root, {"a.py": content})
238 r = _mv(root, "--json", "a.py", "b.py")
239 d = json.loads(r.output)
240 assert d["object_id"].startswith("sha256:"), (
241 f"object_id missing sha256: prefix: {d['object_id']!r}"
242 )
243
244 def test_source_dest_echoed_verbatim(self, tmp_path: pathlib.Path) -> None:
245 root = _init_repo(tmp_path)
246 _commit(root, {"src/module.py": b"# m\n"})
247 r = _mv(root, "--json", "src/module.py", "lib/module.py")
248 d = json.loads(r.output)
249 assert d["source"] == "src/module.py"
250 assert d["dest"] == "lib/module.py"
251
252 def test_object_id_matches_staged_entry(self, tmp_path: pathlib.Path) -> None:
253 """object_id in JSON must match what was written to the stage."""
254 root = _init_repo(tmp_path)
255 content = b"# exact\n"
256 _commit(root, {"x.py": content})
257 r = _mv(root, "--json", "x.py", "y.py")
258 d = json.loads(r.output)
259 stage = read_stage(root)
260 assert stage["y.py"]["object_id"] == d["object_id"]
261
262 def test_staged_only_move_object_id_preserved(self, tmp_path: pathlib.Path) -> None:
263 """Staged-only file move must echo the correct object_id."""
264 root = _init_repo(tmp_path)
265 _commit(root, {"anchor.py": b"# anchor\n"})
266 content = b"# staged only\n"
267 oid = _sha(content)
268 write_object(root, oid, content)
269 (root / "new.py").write_bytes(content)
270 stage = read_stage(root)
271 stage["new.py"] = make_entry(oid, "A")
272 write_stage(root, stage)
273 r = _mv(root, "--json", "new.py", "renamed.py")
274 d = json.loads(r.output)
275 assert d["object_id"] == oid
276
277
278 # ---------------------------------------------------------------------------
279 # No-prose pollution
280 # ---------------------------------------------------------------------------
281
282
283 class TestNoProsePollution:
284 def test_success_stdout_is_valid_json(self, tmp_path: pathlib.Path) -> None:
285 root = _init_repo(tmp_path)
286 _commit(root, {"a.py": b"# a\n"})
287 r = _mv(root, "--json", "a.py", "b.py")
288 json.loads(r.output) # must not raise
289
290 def test_dry_run_stdout_is_valid_json(self, tmp_path: pathlib.Path) -> None:
291 root = _init_repo(tmp_path)
292 _commit(root, {"a.py": b"# a\n"})
293 r = _mv(root, "--json", "--dry-run", "a.py", "b.py")
294 json.loads(r.output) # must not raise
295
296 def test_no_emoji_in_success_json(self, tmp_path: pathlib.Path) -> None:
297 root = _init_repo(tmp_path)
298 _commit(root, {"a.py": b"# a\n"})
299 r = _mv(root, "--json", "a.py", "b.py")
300 assert "✅" not in r.output
301 assert "❌" not in r.output
302
303
304 # ---------------------------------------------------------------------------
305 # TypedDicts
306 # ---------------------------------------------------------------------------
307
308
309 class TestTypedDicts:
310 def test_mv_result_json_typeddict_exists(self) -> None:
311 from muse.cli.commands.mv import _MvResultJson
312 assert _MvResultJson is not None
313
314 def test_mv_error_json_typeddict_exists(self) -> None:
315 from muse.cli.commands.mv import _MvErrorJson
316 assert _MvErrorJson is not None
317
318 def test_mv_result_json_has_exit_code_annotation(self) -> None:
319 from muse.cli.commands.mv import _MvResultJson
320 hints = get_type_hints(_MvResultJson)
321 assert "exit_code" in hints
322
323 def test_mv_result_json_has_duration_ms_annotation(self) -> None:
324 from muse.cli.commands.mv import _MvResultJson
325 hints = get_type_hints(_MvResultJson)
326 assert "duration_ms" in hints
327
328 def test_mv_error_json_has_required_fields(self) -> None:
329 from muse.cli.commands.mv import _MvErrorJson
330 hints = get_type_hints(_MvErrorJson)
331 for field in ("status", "error", "exit_code"):
332 assert field in hints, f"Missing annotation: {field!r}"
333
334
335 # ---------------------------------------------------------------------------
336 # Docstring coverage
337 # ---------------------------------------------------------------------------
338
339
340 class TestDocstring:
341 def _doc(self) -> str:
342 import muse.cli.commands.mv as mod
343 return mod.__doc__ or ""
344
345 def test_docstring_documents_exit_code(self) -> None:
346 assert "exit_code" in self._doc()
347
348 def test_docstring_documents_duration_ms(self) -> None:
349 assert "duration_ms" in self._doc()
350
351
352 # ---------------------------------------------------------------------------
353 # Stress
354 # ---------------------------------------------------------------------------
355
356
357 class TestStress:
358 def test_50_files_move_all_json(self, tmp_path: pathlib.Path) -> None:
359 """Move all 50 tracked files with --json; all envelopes must be valid."""
360 root = _init_repo(tmp_path)
361 n = 50
362 files = {f"f{i:03d}.py": f"# {i}\n".encode() for i in range(n)}
363 _commit(root, files)
364
365 for i in range(n):
366 r = _mv(root, "--json", f"f{i:03d}.py", f"moved/f{i:03d}.py")
367 assert r.exit_code == 0, f"move {i} failed: {r.output}"
368 d = json.loads(r.output)
369 assert d["exit_code"] == 0
370 assert "duration_ms" in d
371 assert d["source"] == f"f{i:03d}.py"
372 assert d["dest"] == f"moved/f{i:03d}.py"
373
374 stage = read_stage(root)
375 for i in range(n):
376 assert stage[f"f{i:03d}.py"]["mode"] == "D"
377 assert stage[f"moved/f{i:03d}.py"]["mode"] == "A"
378
379
380 # ---------------------------------------------------------------------------
381 # TestRegisterFlags — argparse-level verification
382 # ---------------------------------------------------------------------------
383
384
385 class TestRegisterFlags:
386 """Verify that register() wires --json / -j correctly."""
387
388 def _make_parser(self):
389 import argparse
390 from muse.cli.commands.mv import register
391 ap = argparse.ArgumentParser()
392 subs = ap.add_subparsers()
393 register(subs)
394 return ap
395
396 def test_json_flag_long(self):
397 ns = self._make_parser().parse_args(["mv", "src.py", "dst.py", "--json"])
398 assert ns.json_out is True
399
400 def test_j_alias(self):
401 ns = self._make_parser().parse_args(["mv", "src.py", "dst.py", "-j"])
402 assert ns.json_out is True
403
404 def test_default_is_text(self):
405 ns = self._make_parser().parse_args(["mv", "src.py", "dst.py"])
406 assert ns.json_out is False
407
408 def test_dest_is_json_out(self):
409 ns = self._make_parser().parse_args(["mv", "src.py", "dst.py", "-j"])
410 assert hasattr(ns, "json_out")
411 assert not hasattr(ns, "fmt")
File History 2 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 135 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 141 days ago