gabriel / muse public
test_cmd_verify.py python
492 lines 19.6 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
1 """Tests for ``muse verify`` and ``muse/core/verify.py``.
2
3 Covers: empty repo, healthy repo, missing commit, missing snapshot,
4 missing object, corrupted object (hash mismatch), --no-objects flag,
5 --quiet flag, --format json, stress: 100-commit chain.
6 """
7
8 from __future__ import annotations
9
10 import datetime
11 import json
12 import pathlib
13
14 import pytest
15 from tests.cli_test_helper import CliRunner
16
17 cli = None # argparse migration — CliRunner ignores this arg
18 import os
19
20 from muse.core.object_store import object_path, write_object
21 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
22 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
23 from muse.core.verify import run_verify
24 from muse.core._types import Manifest, blob_id, long_id, fake_id as _fake_id
25
26 runner = CliRunner()
27
28 _REPO_ID = "verify-test"
29
30
31 # ---------------------------------------------------------------------------
32 # Helpers
33 # ---------------------------------------------------------------------------
34
35
36 def _sha(data: bytes) -> str:
37 return blob_id(data)
38
39
40 def _init_repo(path: pathlib.Path) -> pathlib.Path:
41 muse = path / ".muse"
42 for d in ("commits", "snapshots", "objects", "refs/heads"):
43 (muse / d).mkdir(parents=True, exist_ok=True)
44 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
45 (muse / "repo.json").write_text(
46 json.dumps({"repo_id": _REPO_ID, "domain": "midi"}), encoding="utf-8"
47 )
48 return path
49
50
51 def _env(repo: pathlib.Path) -> Manifest:
52 return {"MUSE_REPO_ROOT": str(repo)}
53
54
55 def _make_commit(
56 root: pathlib.Path,
57 parent_id: str | None = None,
58 content: bytes = b"data",
59 branch: str = "main",
60 idx: int = 0,
61 ) -> str:
62 raw = content + str(idx).encode()
63 obj_id = _sha(raw)
64 write_object(root, obj_id, raw)
65 manifest = {f"file_{idx}.txt": obj_id}
66 snap_id = compute_snapshot_id(manifest)
67 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
68 committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc) + datetime.timedelta(hours=idx)
69 parent_ids = [parent_id] if parent_id else []
70 commit_id = compute_commit_id(
71 repo_id=_REPO_ID,
72 parent_ids=parent_ids,
73 snapshot_id=snap_id,
74 message=f"commit {idx}",
75 committed_at_iso=committed_at.isoformat(),
76 )
77 write_commit(root, CommitRecord(
78 commit_id=commit_id,
79 repo_id=_REPO_ID,
80 created_on_branch=branch,
81 snapshot_id=snap_id,
82 message=f"commit {idx}",
83 committed_at=committed_at,
84 parent_commit_id=parent_id,
85 ))
86 (root / ".muse" / "refs" / "heads" / branch).write_text(commit_id, encoding="utf-8")
87 return commit_id
88
89
90 # ---------------------------------------------------------------------------
91 # Unit: core run_verify
92 # ---------------------------------------------------------------------------
93
94
95 def test_verify_empty_repo_no_failures(tmp_path: pathlib.Path) -> None:
96 _init_repo(tmp_path)
97 result = run_verify(tmp_path)
98 assert result["all_ok"] is True
99 assert result["failures"] == []
100 assert result["nothing_checked"] is True
101
102
103 # ---------------------------------------------------------------------------
104 # Supercharged verify — snapshot sweep, nothing_checked, zero-byte detection
105 # ---------------------------------------------------------------------------
106
107
108 class TestVerifySupercharged:
109 """Tests for the three supercharged verify capabilities:
110
111 1. Snapshot store sweep — finds missing objects even when branch refs are absent.
112 2. nothing_checked flag — distinguishes "empty repo" from "all healthy".
113 3. Truncated objects are caught by the hash check (check_objects=True);
114 existence-only mode (check_objects=False) does not hash-verify content.
115 """
116
117 def test_nothing_checked_false_when_commits_exist(self, tmp_path: pathlib.Path) -> None:
118 _init_repo(tmp_path)
119 _make_commit(tmp_path, content=b"data", idx=0)
120 result = run_verify(tmp_path)
121 assert result["nothing_checked"] is False
122
123 def test_nothing_checked_true_when_no_refs_and_no_snapshots(self, tmp_path: pathlib.Path) -> None:
124 _init_repo(tmp_path)
125 result = run_verify(tmp_path)
126 assert result["nothing_checked"] is True
127
128 def test_orphan_snapshot_with_missing_object_detected(self, tmp_path: pathlib.Path) -> None:
129 """Snapshot exists in .muse/snapshots/ but no commit or branch ref points to it.
130 Its objects are missing. Verify should catch this via the snapshot store sweep."""
131 _init_repo(tmp_path)
132 obj_id = long_id("a" * 64) # non-existent object
133 manifest = {"orphan.py": obj_id}
134 snap_id = compute_snapshot_id(manifest)
135 write_snapshot(tmp_path, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
136 # No branch ref written, no commit written.
137
138 result = run_verify(tmp_path)
139 assert result["all_ok"] is False
140 assert any(f["kind"] == "object" and f["id"] == obj_id for f in result["failures"])
141 assert result["nothing_checked"] is False # sweep found something to check
142
143 def test_orphan_snapshot_with_present_object_passes(self, tmp_path: pathlib.Path) -> None:
144 """Orphan snapshot whose object IS present should not cause failures."""
145 _init_repo(tmp_path)
146 content = b"orphan content"
147 obj_id = _sha(content)
148 write_object(tmp_path, obj_id, content)
149 manifest = {"file.py": obj_id}
150 snap_id = compute_snapshot_id(manifest)
151 write_snapshot(tmp_path, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
152
153 result = run_verify(tmp_path)
154 assert result["all_ok"] is True
155 assert result["nothing_checked"] is False # sweep found the snapshot
156
157 def test_partial_clone_missing_objects_detected(self, tmp_path: pathlib.Path) -> None:
158 """Simulate a failed clone: commits and snapshots written to store,
159 but the branch ref file was never created and objects are absent.
160 Verify must detect the missing objects via the snapshot sweep."""
161 import datetime
162 _init_repo(tmp_path)
163 obj_id = _sha(b"important file content")
164 manifest = {"src/main.py": obj_id}
165 snap_id = compute_snapshot_id(manifest)
166 write_snapshot(tmp_path, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
167 committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
168 commit_id = compute_commit_id(
169 repo_id=_REPO_ID,
170 parent_ids=[],
171 snapshot_id=snap_id,
172 message="partial clone",
173 committed_at_iso=committed_at.isoformat(),
174 )
175 write_commit(tmp_path, CommitRecord(
176 commit_id=commit_id,
177 repo_id=_REPO_ID,
178 created_on_branch="main",
179 snapshot_id=snap_id,
180 message="partial clone",
181 committed_at=committed_at,
182 ))
183 # Critically: the branch ref file is NOT written (simulates clone crash).
184 # The object is also NOT written (simulates R2 gap).
185
186 result = run_verify(tmp_path)
187 assert result["all_ok"] is False
188 object_failures = [f for f in result["failures"] if f["kind"] == "object"]
189 assert any(f["id"] == obj_id for f in object_failures)
190
191 def test_truncated_object_caught_by_hash_check(self, tmp_path: pathlib.Path) -> None:
192 """An object file truncated to empty bytes is caught as a hash mismatch
193 when check_objects=True. Empty bytes have OID sha256:e3b0c44… which
194 differs from the stored OID unless the file was always empty."""
195 import os as _os
196 _init_repo(tmp_path)
197 content = b"real content here"
198 obj_id = _sha(content)
199 write_object(tmp_path, obj_id, content)
200 manifest = {"real.py": obj_id}
201 snap_id = compute_snapshot_id(manifest)
202 write_snapshot(tmp_path, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
203 committed_at = datetime.datetime(2026, 4, 1, tzinfo=datetime.timezone.utc)
204 commit_id = compute_commit_id(
205 repo_id=_REPO_ID,
206 parent_ids=[],
207 snapshot_id=snap_id,
208 message="truncated test",
209 committed_at_iso=committed_at.isoformat(),
210 )
211 write_commit(tmp_path, CommitRecord(
212 commit_id=commit_id, repo_id=_REPO_ID, created_on_branch="main",
213 snapshot_id=snap_id, message="truncated test", committed_at=committed_at,
214 ))
215 (tmp_path / ".muse" / "refs" / "heads" / "main").write_text(commit_id)
216
217 # Simulate truncation (e.g. R2 serving empty body for a non-empty OID).
218 obj_file = object_path(tmp_path, obj_id)
219 _os.chmod(obj_file, 0o644)
220 obj_file.write_bytes(b"")
221
222 # Hash check catches the mismatch.
223 result = run_verify(tmp_path, check_objects=True)
224 assert result["all_ok"] is False
225 assert any(f["kind"] == "object" and f["id"] == obj_id for f in result["failures"])
226
227 def test_truncated_object_passes_existence_check(self, tmp_path: pathlib.Path) -> None:
228 """check_objects=False only verifies the object file exists — it does not
229 re-hash. A truncated file passes existence-only mode."""
230 import os as _os
231 _init_repo(tmp_path)
232 content = b"real content here"
233 obj_id = _sha(content)
234 write_object(tmp_path, obj_id, content)
235 manifest = {"real.py": obj_id}
236 snap_id = compute_snapshot_id(manifest)
237 write_snapshot(tmp_path, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
238 committed_at = datetime.datetime(2026, 4, 1, tzinfo=datetime.timezone.utc)
239 commit_id = compute_commit_id(
240 repo_id=_REPO_ID,
241 parent_ids=[],
242 snapshot_id=snap_id,
243 message="existence test",
244 committed_at_iso=committed_at.isoformat(),
245 )
246 write_commit(tmp_path, CommitRecord(
247 commit_id=commit_id, repo_id=_REPO_ID, created_on_branch="main",
248 snapshot_id=snap_id, message="existence test", committed_at=committed_at,
249 ))
250 (tmp_path / ".muse" / "refs" / "heads" / "main").write_text(commit_id)
251
252 obj_file = object_path(tmp_path, obj_id)
253 _os.chmod(obj_file, 0o644)
254 obj_file.write_bytes(b"")
255
256 result = run_verify(tmp_path, check_objects=False)
257 assert result["all_ok"] is True
258
259 def test_genuinely_empty_file_passes_hash_check(self, tmp_path: pathlib.Path) -> None:
260 """A file whose content is genuinely empty bytes has OID sha256:e3b0c44…
261 The object file is zero bytes and the hash check must pass — empty is valid."""
262 _init_repo(tmp_path)
263 content = b""
264 obj_id = _sha(content) # sha256:e3b0c44...
265 write_object(tmp_path, obj_id, content)
266 manifest = {"__init__.py": obj_id}
267 snap_id = compute_snapshot_id(manifest)
268 write_snapshot(tmp_path, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
269 committed_at = datetime.datetime(2026, 4, 3, tzinfo=datetime.timezone.utc)
270 commit_id = compute_commit_id(
271 repo_id=_REPO_ID,
272 parent_ids=[],
273 snapshot_id=snap_id,
274 message="empty file test",
275 committed_at_iso=committed_at.isoformat(),
276 )
277 write_commit(tmp_path, CommitRecord(
278 commit_id=commit_id, repo_id=_REPO_ID, created_on_branch="main",
279 snapshot_id=snap_id, message="empty file test", committed_at=committed_at,
280 ))
281 (tmp_path / ".muse" / "refs" / "heads" / "main").write_text(commit_id)
282
283 result = run_verify(tmp_path, check_objects=True)
284 assert result["all_ok"] is True, f"Failures: {result['failures']}"
285
286 def test_truncated_object_reported_exactly_once(self, tmp_path: pathlib.Path) -> None:
287 """A truncated object should appear exactly once in failures — the hash
288 mismatch check, not duplicated by any secondary check."""
289 import os as _os
290 _init_repo(tmp_path)
291 content = b"will be truncated"
292 obj_id = _sha(content)
293 write_object(tmp_path, obj_id, content)
294 manifest = {"f.py": obj_id}
295 snap_id = compute_snapshot_id(manifest)
296 write_snapshot(tmp_path, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
297 committed_at = datetime.datetime(2026, 4, 2, tzinfo=datetime.timezone.utc)
298 commit_id = compute_commit_id(
299 repo_id=_REPO_ID,
300 parent_ids=[],
301 snapshot_id=snap_id,
302 message="dup test",
303 committed_at_iso=committed_at.isoformat(),
304 )
305 write_commit(tmp_path, CommitRecord(
306 commit_id=commit_id, repo_id=_REPO_ID, created_on_branch="main",
307 snapshot_id=snap_id, message="dup test", committed_at=committed_at,
308 ))
309 (tmp_path / ".muse" / "refs" / "heads" / "main").write_text(commit_id)
310
311 obj_file = object_path(tmp_path, obj_id)
312 _os.chmod(obj_file, 0o644)
313 obj_file.write_bytes(b"")
314
315 result = run_verify(tmp_path, check_objects=True)
316 matching = [f for f in result["failures"] if f["id"] == obj_id]
317 assert len(matching) == 1, f"Expected 1 failure for {obj_id[:12]}, got {len(matching)}"
318
319 def test_snapshot_sweep_does_not_recheck_already_verified(self, tmp_path: pathlib.Path) -> None:
320 """Snapshots reachable from branch refs should not be double-counted
321 by the orphan sweep pass."""
322 _init_repo(tmp_path)
323 commit_id = _make_commit(tmp_path, content=b"data", idx=0)
324 result = run_verify(tmp_path)
325 assert result["snapshots_checked"] == 1 # not 2
326
327 def test_json_output_includes_nothing_checked(self, tmp_path: pathlib.Path) -> None:
328 """The --json output must include nothing_checked so scripts can distinguish
329 empty repos from healthy ones."""
330 _init_repo(tmp_path)
331 result = runner.invoke(cli, ["verify", "--json"], env=_env(tmp_path))
332 assert result.exit_code == 0
333 data = json.loads(result.output)
334 assert "nothing_checked" in data
335 assert data["nothing_checked"] is True
336
337
338 def test_verify_healthy_repo(tmp_path: pathlib.Path) -> None:
339 _init_repo(tmp_path)
340 _make_commit(tmp_path, content=b"healthy", idx=0)
341 result = run_verify(tmp_path)
342 assert result["all_ok"] is True
343 assert result["commits_checked"] == 1
344 assert result["objects_checked"] >= 1
345
346
347 def test_verify_missing_commit_fails(tmp_path: pathlib.Path) -> None:
348 _init_repo(tmp_path)
349 # Write a ref pointing to a nonexistent commit.
350 missing_commit = _fake_id("nonexistent-commit")
351 (tmp_path / ".muse" / "refs" / "heads" / "main").write_text(missing_commit, encoding="utf-8")
352 result = run_verify(tmp_path)
353 assert result["all_ok"] is False
354 kinds = [f["kind"] for f in result["failures"]]
355 assert "commit" in kinds
356
357
358 def test_verify_corrupted_object_detected(tmp_path: pathlib.Path) -> None:
359 _init_repo(tmp_path)
360 content = b"original content"
361 obj_id = _sha(content)
362 write_object(tmp_path, obj_id, content)
363 manifest = {"file.txt": obj_id}
364 snap_id = compute_snapshot_id(manifest)
365 write_snapshot(tmp_path, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
366 committed_at = datetime.datetime(2026, 3, 1, tzinfo=datetime.timezone.utc)
367 commit_id = compute_commit_id(
368 repo_id=_REPO_ID,
369 parent_ids=[],
370 snapshot_id=snap_id,
371 message="corrupt test",
372 committed_at_iso=committed_at.isoformat(),
373 )
374 write_commit(tmp_path, CommitRecord(
375 commit_id=commit_id,
376 repo_id=_REPO_ID,
377 created_on_branch="main",
378 snapshot_id=snap_id,
379 message="corrupt test",
380 committed_at=committed_at,
381 ))
382 (tmp_path / ".muse" / "refs" / "heads" / "main").write_text(commit_id, encoding="utf-8")
383
384 # Object store writes files as 0o444 (immutable) — chmod before corrupting.
385 obj_file = object_path(tmp_path, obj_id)
386 os.chmod(obj_file, 0o644)
387 obj_file.write_bytes(b"tampered data!")
388
389 result = run_verify(tmp_path, check_objects=True)
390 assert result["all_ok"] is False
391 kinds = [f["kind"] for f in result["failures"]]
392 assert "object" in kinds
393
394
395 def test_verify_no_objects_flag_skips_rehash(tmp_path: pathlib.Path) -> None:
396 _init_repo(tmp_path)
397 content = b"clean"
398 obj_id = _sha(content)
399 write_object(tmp_path, obj_id, content)
400 manifest = {"f.txt": obj_id}
401 snap_id = compute_snapshot_id(manifest)
402 write_snapshot(tmp_path, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
403 committed_at = datetime.datetime(2026, 3, 2, tzinfo=datetime.timezone.utc)
404 commit_id = compute_commit_id(
405 repo_id=_REPO_ID,
406 parent_ids=[],
407 snapshot_id=snap_id,
408 message="test",
409 committed_at_iso=committed_at.isoformat(),
410 )
411 write_commit(tmp_path, CommitRecord(
412 commit_id=commit_id, repo_id=_REPO_ID, created_on_branch="main",
413 snapshot_id=snap_id, message="test", committed_at=committed_at,
414 ))
415 (tmp_path / ".muse" / "refs" / "heads" / "main").write_text(commit_id, encoding="utf-8")
416
417 # Object store writes files as 0o444 (immutable) — chmod before corrupting.
418 obj_file = object_path(tmp_path, obj_id)
419 os.chmod(obj_file, 0o644)
420 obj_file.write_bytes(b"corrupted!")
421
422 result = run_verify(tmp_path, check_objects=False)
423 # Should not flag the corruption since we skipped re-hashing.
424 assert result["all_ok"] is True
425
426
427 # ---------------------------------------------------------------------------
428 # CLI: muse verify
429 # ---------------------------------------------------------------------------
430
431
432 def test_verify_cli_help() -> None:
433 result = runner.invoke(cli, ["verify", "--help"])
434 assert result.exit_code == 0
435 # Rich injects ANSI codes between '--' dashes; the short flag '-O' is reliable.
436 assert "--no-objects" in result.output or "-O" in result.output
437
438
439 def test_verify_cli_healthy(tmp_path: pathlib.Path) -> None:
440 _init_repo(tmp_path)
441 _make_commit(tmp_path, content=b"cli healthy", idx=99)
442 result = runner.invoke(cli, ["verify"], env=_env(tmp_path))
443 assert result.exit_code == 0
444 assert "healthy" in result.output.lower()
445
446
447 def test_verify_cli_json(tmp_path: pathlib.Path) -> None:
448 _init_repo(tmp_path)
449 _make_commit(tmp_path, content=b"json verify", idx=88)
450 result = runner.invoke(cli, ["verify", "--json"], env=_env(tmp_path))
451 assert result.exit_code == 0
452 data = json.loads(result.output)
453 assert data["all_ok"] is True
454 assert data["failures"] == []
455
456
457 def test_verify_cli_quiet_exit_zero_when_clean(tmp_path: pathlib.Path) -> None:
458 _init_repo(tmp_path)
459 _make_commit(tmp_path, content=b"quiet clean", idx=77)
460 result = runner.invoke(cli, ["verify", "--quiet"], env=_env(tmp_path))
461 assert result.exit_code == 0
462
463
464 def test_verify_cli_quiet_exit_one_when_broken(tmp_path: pathlib.Path) -> None:
465 _init_repo(tmp_path)
466 fake_id = "b" * 64
467 (tmp_path / ".muse" / "refs" / "heads" / "main").write_text(fake_id, encoding="utf-8")
468 result = runner.invoke(cli, ["verify", "-q"], env=_env(tmp_path))
469 assert result.exit_code != 0
470
471
472 def test_verify_cli_no_objects_flag(tmp_path: pathlib.Path) -> None:
473 _init_repo(tmp_path)
474 _make_commit(tmp_path, content=b"no-obj flag", idx=66)
475 result = runner.invoke(cli, ["verify", "--no-objects"], env=_env(tmp_path))
476 assert result.exit_code == 0
477
478
479 # ---------------------------------------------------------------------------
480 # Stress: 100-commit chain
481 # ---------------------------------------------------------------------------
482
483
484 def test_verify_stress_100_commit_chain(tmp_path: pathlib.Path) -> None:
485 _init_repo(tmp_path)
486 prev: str | None = None
487 for i in range(100):
488 prev = _make_commit(tmp_path, parent_id=prev, content=b"chain", idx=i)
489
490 result = run_verify(tmp_path, check_objects=True)
491 assert result["all_ok"] is True
492 assert result["commits_checked"] == 100
File History 3 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 137 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 140 days ago