gabriel / muse public
test_cmd_verify.py python
239 lines 8.4 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 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
96
97 def test_verify_healthy_repo(tmp_path: pathlib.Path) -> None:
98 _init_repo(tmp_path)
99 _make_commit(tmp_path, content=b"healthy", idx=0)
100 result = run_verify(tmp_path)
101 assert result["all_ok"] is True
102 assert result["commits_checked"] == 1
103 assert result["objects_checked"] >= 1
104
105
106 def test_verify_missing_commit_fails(tmp_path: pathlib.Path) -> None:
107 _init_repo(tmp_path)
108 # Write a ref pointing to a nonexistent commit.
109 fake_id = "a" * 64
110 (tmp_path / ".muse" / "refs" / "heads" / "main").write_text(fake_id, encoding="utf-8")
111 result = run_verify(tmp_path)
112 assert result["all_ok"] is False
113 kinds = [f["kind"] for f in result["failures"]]
114 assert "commit" in kinds
115
116
117 def test_verify_corrupted_object_detected(tmp_path: pathlib.Path) -> None:
118 _init_repo(tmp_path)
119 content = b"original content"
120 obj_id = _sha(content)
121 write_object(tmp_path, obj_id, content)
122 manifest = {"file.txt": obj_id}
123 snap_id = compute_snapshot_id(manifest)
124 write_snapshot(tmp_path, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
125 committed_at = datetime.datetime(2026, 3, 1, tzinfo=datetime.timezone.utc)
126 commit_id = compute_commit_id([], snap_id, "corrupt test", committed_at.isoformat())
127 write_commit(tmp_path, CommitRecord(
128 commit_id=commit_id,
129 repo_id=_REPO_ID,
130 branch="main",
131 snapshot_id=snap_id,
132 message="corrupt test",
133 committed_at=committed_at,
134 ))
135 (tmp_path / ".muse" / "refs" / "heads" / "main").write_text(commit_id, encoding="utf-8")
136
137 # Object store writes files as 0o444 (immutable) — chmod before corrupting.
138 obj_file = object_path(tmp_path, obj_id)
139 os.chmod(obj_file, 0o644)
140 obj_file.write_bytes(b"tampered data!")
141
142 result = run_verify(tmp_path, check_objects=True)
143 assert result["all_ok"] is False
144 kinds = [f["kind"] for f in result["failures"]]
145 assert "object" in kinds
146
147
148 def test_verify_no_objects_flag_skips_rehash(tmp_path: pathlib.Path) -> None:
149 _init_repo(tmp_path)
150 content = b"clean"
151 obj_id = _sha(content)
152 write_object(tmp_path, obj_id, content)
153 manifest = {"f.txt": obj_id}
154 snap_id = compute_snapshot_id(manifest)
155 write_snapshot(tmp_path, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
156 committed_at = datetime.datetime(2026, 3, 2, tzinfo=datetime.timezone.utc)
157 commit_id = compute_commit_id([], snap_id, "test", committed_at.isoformat())
158 write_commit(tmp_path, CommitRecord(
159 commit_id=commit_id, repo_id=_REPO_ID, branch="main",
160 snapshot_id=snap_id, message="test", committed_at=committed_at,
161 ))
162 (tmp_path / ".muse" / "refs" / "heads" / "main").write_text(commit_id, encoding="utf-8")
163
164 # Object store writes files as 0o444 (immutable) — chmod before corrupting.
165 obj_file = object_path(tmp_path, obj_id)
166 os.chmod(obj_file, 0o644)
167 obj_file.write_bytes(b"corrupted!")
168
169 result = run_verify(tmp_path, check_objects=False)
170 # Should not flag the corruption since we skipped re-hashing.
171 assert result["all_ok"] is True
172
173
174 # ---------------------------------------------------------------------------
175 # CLI: muse verify
176 # ---------------------------------------------------------------------------
177
178
179 def test_verify_cli_help() -> None:
180 result = runner.invoke(cli, ["verify", "--help"])
181 assert result.exit_code == 0
182 # Rich injects ANSI codes between '--' dashes; the short flag '-O' is reliable.
183 assert "--no-objects" in result.output or "-O" in result.output
184
185
186 def test_verify_cli_healthy(tmp_path: pathlib.Path) -> None:
187 _init_repo(tmp_path)
188 _make_commit(tmp_path, content=b"cli healthy", idx=99)
189 result = runner.invoke(cli, ["verify"], env=_env(tmp_path))
190 assert result.exit_code == 0
191 assert "healthy" in result.output.lower()
192
193
194 def test_verify_cli_json(tmp_path: pathlib.Path) -> None:
195 _init_repo(tmp_path)
196 _make_commit(tmp_path, content=b"json verify", idx=88)
197 result = runner.invoke(cli, ["verify", "--json"], env=_env(tmp_path))
198 assert result.exit_code == 0
199 data = json.loads(result.output)
200 assert data["all_ok"] is True
201 assert data["failures"] == []
202
203
204 def test_verify_cli_quiet_exit_zero_when_clean(tmp_path: pathlib.Path) -> None:
205 _init_repo(tmp_path)
206 _make_commit(tmp_path, content=b"quiet clean", idx=77)
207 result = runner.invoke(cli, ["verify", "--quiet"], env=_env(tmp_path))
208 assert result.exit_code == 0
209
210
211 def test_verify_cli_quiet_exit_one_when_broken(tmp_path: pathlib.Path) -> None:
212 _init_repo(tmp_path)
213 fake_id = "b" * 64
214 (tmp_path / ".muse" / "refs" / "heads" / "main").write_text(fake_id, encoding="utf-8")
215 result = runner.invoke(cli, ["verify", "-q"], env=_env(tmp_path))
216 assert result.exit_code != 0
217
218
219 def test_verify_cli_no_objects_flag(tmp_path: pathlib.Path) -> None:
220 _init_repo(tmp_path)
221 _make_commit(tmp_path, content=b"no-obj flag", idx=66)
222 result = runner.invoke(cli, ["verify", "--no-objects"], env=_env(tmp_path))
223 assert result.exit_code == 0
224
225
226 # ---------------------------------------------------------------------------
227 # Stress: 100-commit chain
228 # ---------------------------------------------------------------------------
229
230
231 def test_verify_stress_100_commit_chain(tmp_path: pathlib.Path) -> None:
232 _init_repo(tmp_path)
233 prev: str | None = None
234 for i in range(100):
235 prev = _make_commit(tmp_path, parent_id=prev, content=b"chain", idx=i)
236
237 result = run_verify(tmp_path, check_objects=True)
238 assert result["all_ok"] is True
239 assert result["commits_checked"] == 100
File History 5 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 26 days ago
sha256:50d413a635f57761c3fce1f70251b957b6423821525bf330747ef4007339222e Merge branch 'dev' into main Human 29 days ago
sha256:fb67fed5a4d3e40de84bdd163de94ef1386570bef1dd1a020a732c8a038962ce Merge branch 'dev' into main Human 48 days ago
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa feat: Muse — version control for the agent era Human 100 days ago