gabriel / muse public
test_plumbing_verify_object.py python
185 lines 7.1 KB
86000da9 fix: replace typer CliRunner with argparse-compatible test helper Gabriel Cardona <gabriel@tellurstori.com> 1d ago
1 """Tests for ``muse plumbing verify-object``.
2
3 Verifies streaming integrity checking, detection of missing objects, detection
4 of corrupted objects (hash mismatch), batch mode (multiple IDs), quiet-mode
5 exit codes, and text-format output.
6 """
7
8 from __future__ import annotations
9
10 import hashlib
11 import json
12 import pathlib
13
14 from tests.cli_test_helper import CliRunner
15
16 cli = None # argparse migration — CliRunner ignores this arg
17 from muse.core.errors import ExitCode
18 from muse.core.object_store import object_path, write_object
19
20 runner = CliRunner()
21
22
23 # ---------------------------------------------------------------------------
24 # Helpers
25 # ---------------------------------------------------------------------------
26
27
28 def _sha(data: bytes) -> str:
29 return hashlib.sha256(data).hexdigest()
30
31
32 def _init_repo(path: pathlib.Path) -> pathlib.Path:
33 muse = path / ".muse"
34 (muse / "commits").mkdir(parents=True)
35 (muse / "snapshots").mkdir(parents=True)
36 (muse / "objects").mkdir(parents=True)
37 (muse / "refs" / "heads").mkdir(parents=True)
38 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
39 (muse / "repo.json").write_text(
40 json.dumps({"repo_id": "test-repo", "domain": "midi"}), encoding="utf-8"
41 )
42 return path
43
44
45 def _env(repo: pathlib.Path) -> dict[str, str]:
46 return {"MUSE_REPO_ROOT": str(repo)}
47
48
49 def _obj(repo: pathlib.Path, content: bytes) -> str:
50 oid = _sha(content)
51 write_object(repo, oid, content)
52 return oid
53
54
55 def _fake_id(tag: str) -> str:
56 return hashlib.sha256(tag.encode()).hexdigest()
57
58
59 # ---------------------------------------------------------------------------
60 # Tests
61 # ---------------------------------------------------------------------------
62
63
64 class TestVerifyObject:
65 def test_valid_object_passes_verification(self, tmp_path: pathlib.Path) -> None:
66 repo = _init_repo(tmp_path)
67 content = b"test object content"
68 oid = _obj(repo, content)
69 result = runner.invoke(cli, ["plumbing", "verify-object", oid], env=_env(repo))
70 assert result.exit_code == 0, result.output
71 data = json.loads(result.stdout)
72 assert data["all_ok"] is True
73 assert data["checked"] == 1
74 assert data["failed"] == 0
75 assert data["results"][0]["ok"] is True
76 assert data["results"][0]["size_bytes"] == len(content)
77 assert data["results"][0]["error"] is None
78
79 def test_missing_object_reported_as_failure(self, tmp_path: pathlib.Path) -> None:
80 repo = _init_repo(tmp_path)
81 missing = _fake_id("not-stored")
82 result = runner.invoke(cli, ["plumbing", "verify-object", missing], env=_env(repo))
83 assert result.exit_code == ExitCode.USER_ERROR
84 data = json.loads(result.stdout)
85 assert data["all_ok"] is False
86 assert "not found" in data["results"][0]["error"]
87
88 def test_corrupted_object_detected_as_hash_mismatch(self, tmp_path: pathlib.Path) -> None:
89 repo = _init_repo(tmp_path)
90 oid = _obj(repo, b"original content")
91 # Overwrite on disk with different bytes — same path, different content.
92 object_path(repo, oid).write_bytes(b"corrupted bytes that do not match the sha256 id")
93 result = runner.invoke(cli, ["plumbing", "verify-object", oid], env=_env(repo))
94 assert result.exit_code == ExitCode.USER_ERROR
95 data = json.loads(result.stdout)
96 assert data["all_ok"] is False
97 assert "mismatch" in data["results"][0]["error"]
98
99 def test_batch_all_valid_exits_zero(self, tmp_path: pathlib.Path) -> None:
100 repo = _init_repo(tmp_path)
101 oid1 = _obj(repo, b"first")
102 oid2 = _obj(repo, b"second")
103 oid3 = _obj(repo, b"third")
104 result = runner.invoke(
105 cli, ["plumbing", "verify-object", oid1, oid2, oid3], env=_env(repo)
106 )
107 assert result.exit_code == 0, result.output
108 data = json.loads(result.stdout)
109 assert data["all_ok"] is True
110 assert data["checked"] == 3
111 assert data["failed"] == 0
112
113 def test_batch_mixed_reports_partial_failure(self, tmp_path: pathlib.Path) -> None:
114 repo = _init_repo(tmp_path)
115 good = _obj(repo, b"good content")
116 bad = _fake_id("missing-id")
117 result = runner.invoke(
118 cli, ["plumbing", "verify-object", good, bad], env=_env(repo)
119 )
120 assert result.exit_code == ExitCode.USER_ERROR
121 data = json.loads(result.stdout)
122 assert data["all_ok"] is False
123 assert data["checked"] == 2
124 assert data["failed"] == 1
125
126 def test_quiet_all_valid_exits_zero_no_output(self, tmp_path: pathlib.Path) -> None:
127 repo = _init_repo(tmp_path)
128 oid = _obj(repo, b"quiet test")
129 result = runner.invoke(
130 cli, ["plumbing", "verify-object", "--quiet", oid], env=_env(repo)
131 )
132 assert result.exit_code == 0
133 assert result.stdout.strip() == ""
134
135 def test_quiet_any_invalid_exits_user_error(self, tmp_path: pathlib.Path) -> None:
136 repo = _init_repo(tmp_path)
137 bad = _fake_id("nonexistent")
138 result = runner.invoke(
139 cli, ["plumbing", "verify-object", "--quiet", bad], env=_env(repo)
140 )
141 assert result.exit_code == ExitCode.USER_ERROR
142
143 def test_text_format_shows_ok_status(self, tmp_path: pathlib.Path) -> None:
144 repo = _init_repo(tmp_path)
145 oid = _obj(repo, b"text format test")
146 result = runner.invoke(
147 cli, ["plumbing", "verify-object", "--format", "text", oid], env=_env(repo)
148 )
149 assert result.exit_code == 0, result.output
150 assert "OK" in result.stdout
151 assert oid in result.stdout
152
153 def test_text_format_shows_fail_status(self, tmp_path: pathlib.Path) -> None:
154 repo = _init_repo(tmp_path)
155 bad = _fake_id("not-there")
156 result = runner.invoke(
157 cli, ["plumbing", "verify-object", "--format", "text", bad], env=_env(repo)
158 )
159 assert result.exit_code == ExitCode.USER_ERROR
160 assert "FAIL" in result.stdout
161
162 def test_invalid_sha256_format_reported_without_crash(self, tmp_path: pathlib.Path) -> None:
163 repo = _init_repo(tmp_path)
164 result = runner.invoke(
165 cli, ["plumbing", "verify-object", "not-a-hex-string"], env=_env(repo)
166 )
167 assert result.exit_code == ExitCode.USER_ERROR
168 data = json.loads(result.stdout)
169 assert data["results"][0]["ok"] is False
170
171 def test_bad_format_flag_exits_user_error(self, tmp_path: pathlib.Path) -> None:
172 repo = _init_repo(tmp_path)
173 oid = _obj(repo, b"data")
174 result = runner.invoke(
175 cli, ["plumbing", "verify-object", "--format", "csv", oid], env=_env(repo)
176 )
177 assert result.exit_code == ExitCode.USER_ERROR
178
179 def test_size_bytes_reported_correctly(self, tmp_path: pathlib.Path) -> None:
180 repo = _init_repo(tmp_path)
181 content = b"x" * 1024
182 oid = _obj(repo, content)
183 result = runner.invoke(cli, ["plumbing", "verify-object", oid], env=_env(repo))
184 assert result.exit_code == 0, result.output
185 assert json.loads(result.stdout)["results"][0]["size_bytes"] == 1024