gabriel / muse public
test_cmd_verify.py python
232 lines 8.0 KB
229172a6 fix(tests): guard help assertions against Rich ANSI codes between '--' dashes Gabriel Cardona <gabriel@tellurstori.com> 2d 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 typer.testing import CliRunner
17
18 from muse.cli.app import cli
19 from muse.core.object_store import object_path, write_object
20 from muse.core.snapshot import compute_snapshot_id
21 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
22 from muse.core.verify import run_verify
23
24 runner = CliRunner()
25
26 _REPO_ID = "verify-test"
27
28
29 # ---------------------------------------------------------------------------
30 # Helpers
31 # ---------------------------------------------------------------------------
32
33
34 def _sha(data: bytes) -> str:
35 return hashlib.sha256(data).hexdigest()
36
37
38 def _init_repo(path: pathlib.Path) -> pathlib.Path:
39 muse = path / ".muse"
40 for d in ("commits", "snapshots", "objects", "refs/heads"):
41 (muse / d).mkdir(parents=True, exist_ok=True)
42 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
43 (muse / "repo.json").write_text(
44 json.dumps({"repo_id": _REPO_ID, "domain": "midi"}), encoding="utf-8"
45 )
46 return path
47
48
49 def _env(repo: pathlib.Path) -> dict[str, str]:
50 return {"MUSE_REPO_ROOT": str(repo)}
51
52
53 def _make_commit(
54 root: pathlib.Path,
55 parent_id: str | None = None,
56 content: bytes = b"data",
57 branch: str = "main",
58 idx: int = 0,
59 ) -> str:
60 obj_id = _sha(content + str(idx).encode())
61 write_object(root, obj_id, content + str(idx).encode())
62 manifest = {f"file_{idx}.txt": obj_id}
63 snap_id = compute_snapshot_id(manifest)
64 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
65 committed_at = datetime.datetime.now(datetime.timezone.utc)
66 commit_id = _sha(f"{idx}:{snap_id}:{committed_at.isoformat()}".encode())
67 write_commit(root, CommitRecord(
68 commit_id=commit_id,
69 repo_id=_REPO_ID,
70 branch=branch,
71 snapshot_id=snap_id,
72 message=f"commit {idx}",
73 committed_at=committed_at,
74 parent_commit_id=parent_id,
75 ))
76 (root / ".muse" / "refs" / "heads" / branch).write_text(commit_id, encoding="utf-8")
77 return commit_id
78
79
80 # ---------------------------------------------------------------------------
81 # Unit: core run_verify
82 # ---------------------------------------------------------------------------
83
84
85 def test_verify_empty_repo_no_failures(tmp_path: pathlib.Path) -> None:
86 _init_repo(tmp_path)
87 result = run_verify(tmp_path)
88 assert result["all_ok"] is True
89 assert result["failures"] == []
90
91
92 def test_verify_healthy_repo(tmp_path: pathlib.Path) -> None:
93 _init_repo(tmp_path)
94 _make_commit(tmp_path, content=b"healthy", idx=0)
95 result = run_verify(tmp_path)
96 assert result["all_ok"] is True
97 assert result["commits_checked"] == 1
98 assert result["objects_checked"] >= 1
99
100
101 def test_verify_missing_commit_fails(tmp_path: pathlib.Path) -> None:
102 _init_repo(tmp_path)
103 # Write a ref pointing to a nonexistent commit.
104 fake_id = "a" * 64
105 (tmp_path / ".muse" / "refs" / "heads" / "main").write_text(fake_id, encoding="utf-8")
106 result = run_verify(tmp_path)
107 assert result["all_ok"] is False
108 kinds = [f["kind"] for f in result["failures"]]
109 assert "commit" in kinds
110
111
112 def test_verify_corrupted_object_detected(tmp_path: pathlib.Path) -> None:
113 _init_repo(tmp_path)
114 content = b"original content"
115 obj_id = _sha(content)
116 write_object(tmp_path, obj_id, content)
117 manifest = {"file.txt": obj_id}
118 snap_id = compute_snapshot_id(manifest)
119 write_snapshot(tmp_path, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
120 committed_at = datetime.datetime.now(datetime.timezone.utc)
121 commit_id = _sha(f"corrupt:{snap_id}:{committed_at.isoformat()}".encode())
122 write_commit(tmp_path, CommitRecord(
123 commit_id=commit_id,
124 repo_id=_REPO_ID,
125 branch="main",
126 snapshot_id=snap_id,
127 message="corrupt test",
128 committed_at=committed_at,
129 ))
130 (tmp_path / ".muse" / "refs" / "heads" / "main").write_text(commit_id, encoding="utf-8")
131
132 # Corrupt the object file.
133 obj_file = object_path(tmp_path, obj_id)
134 obj_file.write_bytes(b"tampered data!")
135
136 result = run_verify(tmp_path, check_objects=True)
137 assert result["all_ok"] is False
138 kinds = [f["kind"] for f in result["failures"]]
139 assert "object" in kinds
140
141
142 def test_verify_no_objects_flag_skips_rehash(tmp_path: pathlib.Path) -> None:
143 _init_repo(tmp_path)
144 content = b"clean"
145 obj_id = _sha(content)
146 write_object(tmp_path, obj_id, content)
147 manifest = {"f.txt": obj_id}
148 snap_id = compute_snapshot_id(manifest)
149 write_snapshot(tmp_path, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
150 committed_at = datetime.datetime.now(datetime.timezone.utc)
151 commit_id = _sha(f"noobj:{snap_id}".encode())
152 write_commit(tmp_path, CommitRecord(
153 commit_id=commit_id, repo_id=_REPO_ID, branch="main",
154 snapshot_id=snap_id, message="test", committed_at=committed_at,
155 ))
156 (tmp_path / ".muse" / "refs" / "heads" / "main").write_text(commit_id, encoding="utf-8")
157
158 # Corrupt object but check_objects=False should not detect it.
159 obj_file = object_path(tmp_path, obj_id)
160 obj_file.write_bytes(b"corrupted!")
161
162 result = run_verify(tmp_path, check_objects=False)
163 # Should not flag the corruption since we skipped re-hashing.
164 assert result["all_ok"] is True
165
166
167 # ---------------------------------------------------------------------------
168 # CLI: muse verify
169 # ---------------------------------------------------------------------------
170
171
172 def test_verify_cli_help() -> None:
173 result = runner.invoke(cli, ["verify", "--help"])
174 assert result.exit_code == 0
175 # Rich injects ANSI codes between '--' dashes; the short flag '-O' is reliable.
176 assert "--no-objects" in result.output or "-O" in result.output
177
178
179 def test_verify_cli_healthy(tmp_path: pathlib.Path) -> None:
180 _init_repo(tmp_path)
181 _make_commit(tmp_path, content=b"cli healthy", idx=99)
182 result = runner.invoke(cli, ["verify"], env=_env(tmp_path))
183 assert result.exit_code == 0
184 assert "healthy" in result.output.lower()
185
186
187 def test_verify_cli_json(tmp_path: pathlib.Path) -> None:
188 _init_repo(tmp_path)
189 _make_commit(tmp_path, content=b"json verify", idx=88)
190 result = runner.invoke(cli, ["verify", "--format", "json"], env=_env(tmp_path))
191 assert result.exit_code == 0
192 data = json.loads(result.output)
193 assert data["all_ok"] is True
194 assert data["failures"] == []
195
196
197 def test_verify_cli_quiet_exit_zero_when_clean(tmp_path: pathlib.Path) -> None:
198 _init_repo(tmp_path)
199 _make_commit(tmp_path, content=b"quiet clean", idx=77)
200 result = runner.invoke(cli, ["verify", "--quiet"], env=_env(tmp_path))
201 assert result.exit_code == 0
202
203
204 def test_verify_cli_quiet_exit_one_when_broken(tmp_path: pathlib.Path) -> None:
205 _init_repo(tmp_path)
206 fake_id = "b" * 64
207 (tmp_path / ".muse" / "refs" / "heads" / "main").write_text(fake_id, encoding="utf-8")
208 result = runner.invoke(cli, ["verify", "-q"], env=_env(tmp_path))
209 assert result.exit_code != 0
210
211
212 def test_verify_cli_no_objects_flag(tmp_path: pathlib.Path) -> None:
213 _init_repo(tmp_path)
214 _make_commit(tmp_path, content=b"no-obj flag", idx=66)
215 result = runner.invoke(cli, ["verify", "--no-objects"], env=_env(tmp_path))
216 assert result.exit_code == 0
217
218
219 # ---------------------------------------------------------------------------
220 # Stress: 100-commit chain
221 # ---------------------------------------------------------------------------
222
223
224 def test_verify_stress_100_commit_chain(tmp_path: pathlib.Path) -> None:
225 _init_repo(tmp_path)
226 prev: str | None = None
227 for i in range(100):
228 prev = _make_commit(tmp_path, parent_id=prev, content=b"chain", idx=i)
229
230 result = run_verify(tmp_path, check_objects=True)
231 assert result["all_ok"] is True
232 assert result["commits_checked"] == 100