gabriel / muse public
test_restore_supercharge.py python
532 lines 19.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 restore`` — performance, data integrity,
2 object-store corruption, concurrency, and source+staged combos.
3
4 Coverage tiers added here:
5 - Performance: duration_ms present, non-negative, and reasonable
6 - Data integrity: complete JSON schema, correct types, exit_code field
7 - Error mapping: object store corruption → exit code 3 (INTERNAL_ERROR)
8 - Concurrent: two threads restore independent files without racing
9 - Source+staged: --source --staged restores stage entry from source commit
10 - Text summary: text output includes "Restored N" summary line
11 - Docstring gap: _resolve_source_manifest returns {} for bad ref (not raises)
12 """
13
14 from __future__ import annotations
15 from collections.abc import Mapping
16
17 import json
18 import pathlib
19 import threading
20 import time
21 import datetime
22 import pytest
23
24 from tests.cli_test_helper import CliRunner
25
26 from muse.core.object_store import write_object
27 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
28 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
29 from muse.core._types import Manifest, blob_id
30 from muse.plugins.code.stage import StagedFileMap, make_entry, read_stage, write_stage
31
32 runner = CliRunner()
33
34 _REPO_ID = "restore-supercharge-test"
35 _counter = 1000 # offset to avoid collisions with test_cmd_restore.py
36
37
38 def _sha(data: bytes) -> str:
39 return blob_id(data)
40
41
42 def _init_repo(path: pathlib.Path) -> pathlib.Path:
43 muse = path / ".muse"
44 for d in ("commits", "snapshots", "objects", "refs/heads", "code"):
45 (muse / d).mkdir(parents=True, exist_ok=True)
46 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
47 (muse / "repo.json").write_text(
48 json.dumps({"repo_id": _REPO_ID, "domain": "code"}), encoding="utf-8"
49 )
50 return path
51
52
53 def _env(repo: pathlib.Path) -> Mapping[str, str]:
54 return {"MUSE_REPO_ROOT": str(repo)}
55
56
57 def _commit_files(root: pathlib.Path, files: Mapping[str, bytes], branch: str = "main") -> str:
58 global _counter
59 _counter += 1
60 manifest: Manifest = {}
61 for rel_path, content in files.items():
62 obj_id = _sha(content)
63 write_object(root, obj_id, content)
64 manifest[rel_path] = obj_id
65 abs_path = root / rel_path
66 abs_path.parent.mkdir(parents=True, exist_ok=True)
67 abs_path.write_bytes(content)
68 snap_id = compute_snapshot_id(manifest)
69 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
70 committed_at = datetime.datetime.now(datetime.timezone.utc)
71 commit_id = compute_commit_id(
72 repo_id=_REPO_ID,
73 parent_ids=[],
74 snapshot_id=snap_id,
75 message=f"commit {_counter}",
76 committed_at_iso=committed_at.isoformat(),
77 )
78 write_commit(
79 root,
80 CommitRecord(
81 commit_id=commit_id,
82 repo_id=_REPO_ID,
83 created_on_branch=branch,
84 snapshot_id=snap_id,
85 message=f"commit {_counter}",
86 committed_at=committed_at,
87 ),
88 )
89 (root / ".muse" / "refs" / "heads" / branch).write_text(commit_id, encoding="utf-8")
90 return commit_id
91
92
93 def _invoke(repo: pathlib.Path, *args: str):
94 from muse.cli.app import main as cli
95 return runner.invoke(cli, ["restore", *args], env=_env(repo))
96
97
98 # ---------------------------------------------------------------------------
99 # Performance tier
100 # ---------------------------------------------------------------------------
101
102
103 def test_restore_json_has_duration_ms(tmp_path: pathlib.Path) -> None:
104 """JSON output must include 'duration_ms' as a non-negative float."""
105 root = _init_repo(tmp_path)
106 _commit_files(root, {"a.py": b"# orig\n"})
107 (root / "a.py").write_bytes(b"# dirty\n")
108
109 result = _invoke(root, "--json", "a.py")
110 assert result.exit_code == 0
111 data = json.loads(result.stdout)
112 assert "duration_ms" in data, "JSON must include 'duration_ms'"
113 assert isinstance(data["duration_ms"], (int, float)), "duration_ms must be numeric"
114 assert data["duration_ms"] >= 0, "duration_ms must be non-negative"
115
116
117 def test_restore_duration_ms_is_reasonable(tmp_path: pathlib.Path) -> None:
118 """duration_ms for a single-file restore should be well under 5 seconds."""
119 root = _init_repo(tmp_path)
120 _commit_files(root, {"a.py": b"# orig\n"})
121 (root / "a.py").write_bytes(b"# dirty\n")
122
123 result = _invoke(root, "--json", "a.py")
124 assert result.exit_code == 0
125 data = json.loads(result.stdout)
126 assert data["duration_ms"] < 5_000, f"duration_ms={data['duration_ms']} is suspiciously large"
127
128
129 def test_restore_dry_run_json_has_duration_ms(tmp_path: pathlib.Path) -> None:
130 """duration_ms must be present even in dry-run mode."""
131 root = _init_repo(tmp_path)
132 _commit_files(root, {"a.py": b"# orig\n"})
133 (root / "a.py").write_bytes(b"# dirty\n")
134
135 result = _invoke(root, "--dry-run", "--json", "a.py")
136 assert result.exit_code == 0
137 data = json.loads(result.stdout)
138 assert "duration_ms" in data
139
140
141 # ---------------------------------------------------------------------------
142 # Data integrity tier
143 # ---------------------------------------------------------------------------
144
145
146 def test_restore_json_schema_complete_on_success(tmp_path: pathlib.Path) -> None:
147 """All required JSON fields are present with correct types on success."""
148 root = _init_repo(tmp_path)
149 _commit_files(root, {"s.py": b"# orig\n"})
150 (root / "s.py").write_bytes(b"# dirty\n")
151
152 result = _invoke(root, "--json", "s.py")
153 assert result.exit_code == 0
154 data = json.loads(result.stdout)
155
156 assert isinstance(data["restored"], list)
157 assert isinstance(data["not_found"], list)
158 assert isinstance(data["dry_run"], bool)
159 assert isinstance(data["staged"], bool)
160 assert isinstance(data["worktree"], bool)
161 assert isinstance(data["duration_ms"], (int, float))
162 assert isinstance(data["exit_code"], int)
163
164
165 def test_restore_json_exit_code_zero_on_success(tmp_path: pathlib.Path) -> None:
166 """exit_code in JSON is 0 when all files are restored successfully."""
167 root = _init_repo(tmp_path)
168 _commit_files(root, {"ok.py": b"# orig\n"})
169 (root / "ok.py").write_bytes(b"# dirty\n")
170
171 result = _invoke(root, "--json", "ok.py")
172 assert result.exit_code == 0
173 data = json.loads(result.stdout)
174 assert data["exit_code"] == 0
175
176
177 def test_restore_json_exit_code_one_when_file_not_found(tmp_path: pathlib.Path) -> None:
178 """exit_code in JSON is 1 (USER_ERROR) when a file is not in source."""
179 root = _init_repo(tmp_path)
180 _commit_files(root, {"anchor.py": b"# anchor\n"})
181
182 result = _invoke(root, "--json", "ghost.py")
183 assert result.exit_code != 0
184 data = json.loads(result.stdout)
185 assert data["exit_code"] == 1
186
187
188 def test_restore_json_restored_list_correct(tmp_path: pathlib.Path) -> None:
189 """restored list contains exactly the successfully restored paths."""
190 root = _init_repo(tmp_path)
191 _commit_files(root, {"x.py": b"# x\n", "y.py": b"# y\n"})
192 (root / "x.py").write_bytes(b"# dirty x\n")
193 (root / "y.py").write_bytes(b"# dirty y\n")
194
195 result = _invoke(root, "--json", "x.py", "y.py")
196 data = json.loads(result.stdout)
197 assert sorted(data["restored"]) == ["x.py", "y.py"]
198 assert data["not_found"] == []
199
200
201 def test_restore_json_not_found_list_correct(tmp_path: pathlib.Path) -> None:
202 """not_found list contains paths that were absent from the source manifest."""
203 root = _init_repo(tmp_path)
204 _commit_files(root, {"real.py": b"# real\n"})
205 (root / "real.py").write_bytes(b"# dirty\n")
206
207 result = _invoke(root, "--json", "real.py", "ghost.py")
208 data = json.loads(result.stdout)
209 assert "real.py" in data["restored"]
210 assert "ghost.py" in data["not_found"]
211
212
213 def test_restore_json_staged_and_worktree_flags_reflect_args(tmp_path: pathlib.Path) -> None:
214 """staged/worktree fields in JSON reflect the CLI flags used."""
215 root = _init_repo(tmp_path)
216 _commit_files(root, {"f.py": b"# orig\n"})
217 obj_id = _sha(b"# mod\n")
218 write_object(root, obj_id, b"# mod\n")
219 stage: StagedFileMap = {"f.py": make_entry(obj_id, "M")}
220 write_stage(root, stage)
221
222 result = _invoke(root, "--staged", "--worktree", "--json", "f.py")
223 data = json.loads(result.stdout)
224 assert data["staged"] is True
225 assert data["worktree"] is True
226
227
228 # ---------------------------------------------------------------------------
229 # Error mapping — object store corruption → exit code 3
230 # ---------------------------------------------------------------------------
231
232
233 def test_restore_missing_object_exits_3(tmp_path: pathlib.Path) -> None:
234 """When an object_id is in the manifest but missing from the store, exit code must be 3."""
235 root = _init_repo(tmp_path)
236 content = b"# original\n"
237 obj_id = _sha(content)
238
239 # Build a manifest pointing at an object that is NOT in the store.
240 # We write the commit but deliberately don't call write_object.
241 manifest: Manifest = {"corrupt.py": obj_id}
242 snap_id = compute_snapshot_id(manifest)
243 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
244 committed_at = datetime.datetime.now(datetime.timezone.utc)
245 global _counter
246 _counter += 1
247 commit_id = compute_commit_id(
248 repo_id=_REPO_ID,
249 parent_ids=[],
250 snapshot_id=snap_id,
251 message=f"corrupt commit {_counter}",
252 committed_at_iso=committed_at.isoformat(),
253 )
254 write_commit(
255 root,
256 CommitRecord(
257 commit_id=commit_id,
258 repo_id=_REPO_ID,
259 created_on_branch="main",
260 snapshot_id=snap_id,
261 message=f"corrupt commit {_counter}",
262 committed_at=committed_at,
263 ),
264 )
265 (root / ".muse" / "refs" / "heads" / "main").write_text(commit_id, encoding="utf-8")
266 # Create the file on disk so path resolution doesn't fail
267 (root / "corrupt.py").write_bytes(b"# dirty\n")
268
269 result = _invoke(root, "corrupt.py")
270 assert result.exit_code == 3, (
271 f"Expected exit code 3 (INTERNAL_ERROR) for missing object, got {result.exit_code}"
272 )
273
274
275 def test_restore_missing_object_json_exit_code_3(tmp_path: pathlib.Path) -> None:
276 """JSON exit_code is 3 when the object is missing from the store."""
277 root = _init_repo(tmp_path)
278 content = b"# original\n"
279 obj_id = _sha(content)
280
281 manifest: Manifest = {"corrupt2.py": obj_id}
282 snap_id = compute_snapshot_id(manifest)
283 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
284 committed_at = datetime.datetime.now(datetime.timezone.utc)
285 global _counter
286 _counter += 1
287 commit_id = compute_commit_id(
288 repo_id=_REPO_ID,
289 parent_ids=[],
290 snapshot_id=snap_id,
291 message=f"corrupt2 {_counter}",
292 committed_at_iso=committed_at.isoformat(),
293 )
294 write_commit(
295 root,
296 CommitRecord(
297 commit_id=commit_id,
298 repo_id=_REPO_ID,
299 created_on_branch="main",
300 snapshot_id=snap_id,
301 message=f"corrupt2 {_counter}",
302 committed_at=committed_at,
303 ),
304 )
305 (root / ".muse" / "refs" / "heads" / "main").write_text(commit_id, encoding="utf-8")
306 (root / "corrupt2.py").write_bytes(b"# dirty\n")
307
308 result = _invoke(root, "--json", "corrupt2.py")
309 assert result.exit_code == 3
310 data = json.loads(result.stdout)
311 assert data["exit_code"] == 3
312
313
314 # ---------------------------------------------------------------------------
315 # Concurrent restore
316 # ---------------------------------------------------------------------------
317
318
319 def test_restore_concurrent_independent_files(tmp_path: pathlib.Path) -> None:
320 """Two threads restore independent files without racing or corrupting each other."""
321 root = _init_repo(tmp_path)
322 original_a = b"# thread-a original\n"
323 original_b = b"# thread-b original\n"
324 _commit_files(root, {"ta.py": original_a, "tb.py": original_b})
325 (root / "ta.py").write_bytes(b"# dirty a\n")
326 (root / "tb.py").write_bytes(b"# dirty b\n")
327
328 errors: list[Exception] = []
329
330 def restore_a():
331 try:
332 result = _invoke(root, "ta.py")
333 assert result.exit_code == 0, f"thread-a exit {result.exit_code}"
334 except Exception as exc:
335 errors.append(exc)
336
337 def restore_b():
338 try:
339 result = _invoke(root, "tb.py")
340 assert result.exit_code == 0, f"thread-b exit {result.exit_code}"
341 except Exception as exc:
342 errors.append(exc)
343
344 t1 = threading.Thread(target=restore_a)
345 t2 = threading.Thread(target=restore_b)
346 t1.start()
347 t2.start()
348 t1.join(timeout=10)
349 t2.join(timeout=10)
350
351 assert not errors, f"Concurrent restore errors: {errors}"
352 assert (root / "ta.py").read_bytes() == original_a
353 assert (root / "tb.py").read_bytes() == original_b
354
355
356 # ---------------------------------------------------------------------------
357 # --source --staged combo
358 # ---------------------------------------------------------------------------
359
360
361 def test_restore_source_and_staged_clears_stage_from_source(tmp_path: pathlib.Path) -> None:
362 """--source <ref> --staged clears the stage entry so it matches source."""
363 root = _init_repo(tmp_path)
364 v1_content = b"# v1\n"
365 v1_commit = _commit_files(root, {"versioned.py": v1_content})
366
367 # Update to v2
368 v2_content = b"# v2\n"
369 _commit_files(root, {"versioned.py": v2_content})
370
371 # Stage a modification on top of v2
372 mod_content = b"# staged mod\n"
373 obj_id = _sha(mod_content)
374 write_object(root, obj_id, mod_content)
375 stage: StagedFileMap = {"versioned.py": make_entry(obj_id, "M")}
376 write_stage(root, stage)
377
378 # --source v1_commit --staged should clear the stage entry
379 result = _invoke(root, "--source", v1_commit, "--staged", "versioned.py")
380 assert result.exit_code == 0
381 stage_after = read_stage(root)
382 assert "versioned.py" not in stage_after
383
384
385 def test_restore_source_staged_worktree_restores_from_source(tmp_path: pathlib.Path) -> None:
386 """--source <ref> --staged --worktree restores disk from source, clears stage."""
387 root = _init_repo(tmp_path)
388 v1_content = b"# v1 original\n"
389 v1_commit = _commit_files(root, {"combo.py": v1_content})
390 _commit_files(root, {"combo.py": b"# v2\n"})
391
392 mod_content = b"# staged mod\n"
393 obj_id = _sha(mod_content)
394 write_object(root, obj_id, mod_content)
395 stage: StagedFileMap = {"combo.py": make_entry(obj_id, "M")}
396 write_stage(root, stage)
397 (root / "combo.py").write_bytes(b"# dirty disk\n")
398
399 result = _invoke(root, "--source", v1_commit, "--staged", "--worktree", "combo.py")
400 assert result.exit_code == 0
401 assert (root / "combo.py").read_bytes() == v1_content
402 stage_after = read_stage(root)
403 assert "combo.py" not in stage_after
404
405
406 # ---------------------------------------------------------------------------
407 # Text summary output
408 # ---------------------------------------------------------------------------
409
410
411 def test_restore_text_output_summary_line(tmp_path: pathlib.Path) -> None:
412 """Text output includes a summary line like 'Restored 2 file(s)'."""
413 root = _init_repo(tmp_path)
414 _commit_files(root, {"p.py": b"# p\n", "q.py": b"# q\n"})
415 (root / "p.py").write_bytes(b"# dirty p\n")
416 (root / "q.py").write_bytes(b"# dirty q\n")
417
418 result = _invoke(root, "p.py", "q.py")
419 assert result.exit_code == 0
420 output = result.stdout + (result.stderr or "")
421 assert "2" in output, f"Expected count in output: {output!r}"
422
423
424 def test_restore_text_output_errors_noted(tmp_path: pathlib.Path) -> None:
425 """Text output notes how many errors occurred when some paths fail."""
426 root = _init_repo(tmp_path)
427 _commit_files(root, {"real.py": b"# real\n"})
428 (root / "real.py").write_bytes(b"# dirty\n")
429
430 result = _invoke(root, "real.py", "ghost.py")
431 assert result.exit_code != 0
432 output = (result.stdout or "") + (result.stderr or "")
433 # Should mention the failure somehow
434 assert "ghost" in output or "error" in output.lower() or "not" in output.lower()
435
436
437 # ---------------------------------------------------------------------------
438 # _resolve_source_manifest — docstring gap: bad ref returns {}, never raises
439 # ---------------------------------------------------------------------------
440
441
442 def test_resolve_source_manifest_bad_ref_returns_empty(tmp_path: pathlib.Path) -> None:
443 """_resolve_source_manifest returns {} for a non-existent ref — never raises."""
444 from muse.cli.commands.restore import _resolve_source_manifest
445 root = _init_repo(tmp_path)
446 _commit_files(root, {"a.py": b"# a\n"})
447 result = _resolve_source_manifest(root, source_ref="nonexistent-branch-xyz")
448 assert result == {}
449
450
451 def test_resolve_source_manifest_valid_ref(tmp_path: pathlib.Path) -> None:
452 """_resolve_source_manifest resolves a valid branch name to its manifest."""
453 from muse.cli.commands.restore import _resolve_source_manifest
454 root = _init_repo(tmp_path)
455 content = b"# branch content\n"
456 _commit_files(root, {"b.py": content}, branch="main")
457 manifest = _resolve_source_manifest(root, source_ref="main")
458 assert "b.py" in manifest
459 assert manifest["b.py"] == _sha(content)
460
461
462 # ---------------------------------------------------------------------------
463 # Edge: restore staged-only with --source doesn't require file on disk
464 # ---------------------------------------------------------------------------
465
466
467 def test_restore_staged_only_source_does_not_require_disk_file(tmp_path: pathlib.Path) -> None:
468 """--staged with --source works even when the disk file doesn't exist."""
469 root = _init_repo(tmp_path)
470 v1_commit = _commit_files(root, {"staged_only.py": b"# v1\n"})
471 # Stage a modification
472 obj_id = _sha(b"# mod\n")
473 write_object(root, obj_id, b"# mod\n")
474 stage: StagedFileMap = {"staged_only.py": make_entry(obj_id, "M")}
475 write_stage(root, stage)
476 # Delete disk file
477 (root / "staged_only.py").unlink()
478
479 result = _invoke(root, "--source", v1_commit, "--staged", "staged_only.py")
480 assert result.exit_code == 0
481 stage_after = read_stage(root)
482 assert "staged_only.py" not in stage_after
483
484
485 # ---------------------------------------------------------------------------
486 # Performance: duration_ms for 50-file restore is under 10 seconds
487 # ---------------------------------------------------------------------------
488
489
490 def test_restore_50_files_duration_ms_reasonable(tmp_path: pathlib.Path) -> None:
491 """50-file restore reports duration_ms and completes under 10 seconds."""
492 root = _init_repo(tmp_path)
493 files = {f"perf_{i}.py": f"# orig {i}\n".encode() for i in range(50)}
494 _commit_files(root, files)
495 for name in files:
496 (root / name).write_bytes(b"# dirty\n")
497
498 result = _invoke(root, "--json", *files.keys())
499 assert result.exit_code == 0
500 data = json.loads(result.stdout)
501 assert "duration_ms" in data
502 assert data["duration_ms"] < 10_000
503 assert len(data["restored"]) == 50
504
505
506 class TestRegisterFlags:
507 def test_default_json_out_is_false(self):
508 import argparse
509 from muse.cli.commands.restore import register
510 p = argparse.ArgumentParser()
511 subs = p.add_subparsers()
512 register(subs)
513 args = p.parse_args(["restore", "src/billing.py"])
514 assert args.json_out is False
515
516 def test_json_flag_sets_json_out(self):
517 import argparse
518 from muse.cli.commands.restore import register
519 p = argparse.ArgumentParser()
520 subs = p.add_subparsers()
521 register(subs)
522 args = p.parse_args(["restore", "src/billing.py", "--json"])
523 assert args.json_out is True
524
525 def test_j_shorthand_sets_json_out(self):
526 import argparse
527 from muse.cli.commands.restore import register
528 p = argparse.ArgumentParser()
529 subs = p.add_subparsers()
530 register(subs)
531 args = p.parse_args(["restore", "src/billing.py", "-j"])
532 assert args.json_out is True
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