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