gabriel / muse public
test_hash_object_canonical.py python
333 lines 13.1 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
1 """hash-object: canonical sha256: prefix and agent-ready JSON schema.
2
3 Every object ID emitted by ``muse hash-object`` must carry the ``sha256:``
4 prefix. Bare hex is only acceptable at the disk boundary (the filename
5 on disk). This test suite enforces that invariant and covers the new
6 agent-ready JSON fields.
7
8 Test categories
9 ---------------
10 TestCanonicalPrefix — object_id always starts with 'sha256:'
11 TestStdinWriteFixed — stdin + --write was broken (bare hex bug); now fixed
12 TestAgentFields — duration_ms, exit_code, size_bytes in JSON output
13 TestTextOutputPrefix — text format also carries the prefix
14 TestCrossCheck — file and stdin produce identical canonical IDs
15 """
16
17 from __future__ import annotations
18
19 import hashlib
20 import json
21 import pathlib
22
23 from muse.core.errors import ExitCode
24 from tests.cli_test_helper import CliRunner
25 from muse.core._types import blob_id
26 from muse.core.object_store import object_path
27
28 runner = CliRunner()
29
30
31 # ---------------------------------------------------------------------------
32 # Helpers
33 # ---------------------------------------------------------------------------
34
35 def _run(*args: str, stdin: bytes | None = None, repo: pathlib.Path | None = None):
36 from muse.cli.app import main as cli
37 env = {"MUSE_REPO_ROOT": str(repo)} if repo else {}
38 return runner.invoke(cli, ["hash-object", *args], input=stdin, env=env)
39
40
41 def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path:
42 repo = tmp_path / "repo"
43 muse = repo / ".muse"
44 for sub in ("objects", "commits", "snapshots", "refs/heads"):
45 (muse / sub).mkdir(parents=True)
46 (muse / "HEAD").write_text("ref: refs/heads/main")
47 (muse / "repo.json").write_text(
48 json.dumps({"repo_id": "test", "domain": "code"})
49 )
50 return repo
51
52
53 def _sha256(data: bytes) -> str:
54 return blob_id(data)
55
56
57 # ---------------------------------------------------------------------------
58 # TestCanonicalPrefix
59 # ---------------------------------------------------------------------------
60
61 class TestCanonicalPrefix:
62 """object_id in JSON output must always start with 'sha256:'."""
63
64 def test_file_json_object_id_has_prefix(self, tmp_path: pathlib.Path) -> None:
65 f = tmp_path / "f.txt"
66 f.write_bytes(b"hello")
67 result = _run("--json", str(f))
68 assert result.exit_code == 0
69 data = json.loads(result.output)
70 assert data["object_id"].startswith("sha256:"), (
71 f"object_id must start with 'sha256:' — got {data['object_id']!r}"
72 )
73
74 def test_file_json_object_id_correct_length(self, tmp_path: pathlib.Path) -> None:
75 """sha256: (7 chars) + 64 hex = 71 total."""
76 f = tmp_path / "f.txt"
77 f.write_bytes(b"hello")
78 result = _run("--json", str(f))
79 data = json.loads(result.output)
80 assert len(data["object_id"]) == 71
81
82 def test_file_json_object_id_matches_canonical(self, tmp_path: pathlib.Path) -> None:
83 content = b"canonical check"
84 f = tmp_path / "f.txt"
85 f.write_bytes(content)
86 result = _run("--json", str(f))
87 data = json.loads(result.output)
88 assert data["object_id"] == _sha256(content)
89
90 def test_stdin_json_object_id_has_prefix(self, tmp_path: pathlib.Path) -> None:
91 result = _run("--json", "--stdin", stdin=b"from stdin")
92 assert result.exit_code == 0
93 data = json.loads(result.output)
94 assert data["object_id"].startswith("sha256:")
95
96 def test_stdin_json_object_id_matches_canonical(self, tmp_path: pathlib.Path) -> None:
97 content = b"piped data"
98 result = _run("--json", "--stdin", stdin=content)
99 data = json.loads(result.output)
100 assert data["object_id"] == _sha256(content)
101
102 def test_empty_file_has_prefix(self, tmp_path: pathlib.Path) -> None:
103 f = tmp_path / "empty.txt"
104 f.write_bytes(b"")
105 result = _run("--json", str(f))
106 data = json.loads(result.output)
107 assert data["object_id"] == _sha256(b"")
108
109 def test_empty_stdin_has_prefix(self, tmp_path: pathlib.Path) -> None:
110 result = _run("--json", "--stdin", stdin=b"")
111 data = json.loads(result.output)
112 assert data["object_id"] == _sha256(b"")
113
114 def test_no_bare_hex_in_json_output(self, tmp_path: pathlib.Path) -> None:
115 """The raw 64-char hex without prefix must not appear as object_id."""
116 content = b"no bare hex"
117 f = tmp_path / "f.txt"
118 f.write_bytes(content)
119 result = _run("--json", str(f))
120 data = json.loads(result.output)
121 bare_hex = hashlib.sha256(content).hexdigest()
122 assert data["object_id"] != bare_hex, (
123 "object_id must be 'sha256:<hex>', not bare hex"
124 )
125
126
127 # ---------------------------------------------------------------------------
128 # TestStdinWriteFixed
129 # ---------------------------------------------------------------------------
130
131 class TestStdinWriteFixed:
132 """stdin + --write was broken (passed bare hex to write_object). Now fixed."""
133
134 def test_stdin_write_exits_zero(self, tmp_path: pathlib.Path) -> None:
135 repo = _make_repo(tmp_path)
136 result = _run("--stdin", "--write", stdin=b"store me", repo=repo)
137 assert result.exit_code == 0, f"exit {result.exit_code}: {result.output}"
138
139 def test_stdin_write_stored_true(self, tmp_path: pathlib.Path) -> None:
140 repo = _make_repo(tmp_path)
141 result = _run("--json", "--stdin", "--write", stdin=b"store me", repo=repo)
142 assert json.loads(result.output)["stored"] is True
143
144 def test_stdin_write_object_file_exists(self, tmp_path: pathlib.Path) -> None:
145 repo = _make_repo(tmp_path)
146 content = b"stdin stored content"
147 result = _run("--json", "--stdin", "--write", stdin=content, repo=repo)
148 oid = json.loads(result.output)["object_id"]
149 obj_file = object_path(repo, oid)
150 assert obj_file.exists(), f"object file not found at {obj_file}"
151 assert obj_file.read_bytes() == content
152
153 def test_stdin_write_object_id_canonical(self, tmp_path: pathlib.Path) -> None:
154 repo = _make_repo(tmp_path)
155 content = b"canonical write"
156 result = _run("--json", "--stdin", "--write", stdin=content, repo=repo)
157 data = json.loads(result.output)
158 assert data["object_id"] == _sha256(content)
159
160 def test_stdin_write_idempotent(self, tmp_path: pathlib.Path) -> None:
161 repo = _make_repo(tmp_path)
162 content = b"write twice"
163 _run("--stdin", "--write", stdin=content, repo=repo)
164 result2 = _run("--json", "--stdin", "--write", stdin=content, repo=repo)
165 assert result2.exit_code == 0
166 assert json.loads(result2.output)["stored"] is False
167
168
169 # ---------------------------------------------------------------------------
170 # TestAgentFields
171 # ---------------------------------------------------------------------------
172
173 class TestAgentFields:
174 """JSON output must include duration_ms, exit_code, size_bytes."""
175
176 def test_duration_ms_present(self, tmp_path: pathlib.Path) -> None:
177 f = tmp_path / "f.txt"
178 f.write_bytes(b"timing")
179 data = json.loads(_run("--json", str(f)).output)
180 assert "duration_ms" in data, "JSON must include duration_ms"
181
182 def test_duration_ms_non_negative(self, tmp_path: pathlib.Path) -> None:
183 f = tmp_path / "f.txt"
184 f.write_bytes(b"timing")
185 data = json.loads(_run("--json", str(f)).output)
186 assert data["duration_ms"] >= 0
187
188 def test_exit_code_present(self, tmp_path: pathlib.Path) -> None:
189 f = tmp_path / "f.txt"
190 f.write_bytes(b"x")
191 data = json.loads(_run("--json", str(f)).output)
192 assert "exit_code" in data, "JSON must include exit_code"
193
194 def test_exit_code_zero_on_success(self, tmp_path: pathlib.Path) -> None:
195 f = tmp_path / "f.txt"
196 f.write_bytes(b"x")
197 data = json.loads(_run("--json", str(f)).output)
198 assert data["exit_code"] == 0
199
200 def test_size_bytes_present(self, tmp_path: pathlib.Path) -> None:
201 f = tmp_path / "f.txt"
202 f.write_bytes(b"twelve bytes")
203 data = json.loads(_run("--json", str(f)).output)
204 assert "size_bytes" in data, "JSON must include size_bytes"
205
206 def test_size_bytes_correct_for_file(self, tmp_path: pathlib.Path) -> None:
207 content = b"twelve bytes"
208 f = tmp_path / "f.txt"
209 f.write_bytes(content)
210 data = json.loads(_run("--json", str(f)).output)
211 assert data["size_bytes"] == len(content)
212
213 def test_size_bytes_correct_for_stdin(self, tmp_path: pathlib.Path) -> None:
214 content = b"stdin payload"
215 data = json.loads(_run("--json", "--stdin", stdin=content).output)
216 assert data["size_bytes"] == len(content)
217
218 def test_size_bytes_zero_for_empty(self, tmp_path: pathlib.Path) -> None:
219 f = tmp_path / "empty.txt"
220 f.write_bytes(b"")
221 data = json.loads(_run("--json", str(f)).output)
222 assert data["size_bytes"] == 0
223
224 def test_stdin_duration_ms_present(self, tmp_path: pathlib.Path) -> None:
225 data = json.loads(_run("--json", "--stdin", stdin=b"x").output)
226 assert "duration_ms" in data
227
228 def test_stdin_exit_code_present(self, tmp_path: pathlib.Path) -> None:
229 data = json.loads(_run("--json", "--stdin", stdin=b"x").output)
230 assert "exit_code" in data
231
232
233 # ---------------------------------------------------------------------------
234 # TestTextOutputPrefix
235 # ---------------------------------------------------------------------------
236
237 class TestTextOutputPrefix:
238 """Text format must also emit the sha256: prefix."""
239
240 def test_text_file_has_prefix(self, tmp_path: pathlib.Path) -> None:
241 f = tmp_path / "f.txt"
242 f.write_bytes(b"text output")
243 result = _run(str(f))
244 assert result.exit_code == 0
245 assert result.output.strip().startswith("sha256:")
246
247 def test_text_stdin_has_prefix(self, tmp_path: pathlib.Path) -> None:
248 result = _run("--stdin", stdin=b"text stdin")
249 assert result.output.strip().startswith("sha256:")
250
251 def test_text_output_is_correct_canonical_id(self, tmp_path: pathlib.Path) -> None:
252 content = b"text canonical"
253 f = tmp_path / "f.txt"
254 f.write_bytes(content)
255 result = _run(str(f))
256 assert result.output.strip() == _sha256(content)
257
258 def test_text_length_is_71(self, tmp_path: pathlib.Path) -> None:
259 """sha256: (7) + 64 hex = 71 characters."""
260 f = tmp_path / "f.txt"
261 f.write_bytes(b"length check")
262 result = _run(str(f))
263 assert len(result.output.strip()) == 71
264
265
266 # ---------------------------------------------------------------------------
267 # TestCrossCheck
268 # ---------------------------------------------------------------------------
269
270 class TestCrossCheck:
271 """File and stdin paths produce identical canonical IDs for the same bytes."""
272
273 def test_file_and_stdin_same_id(self, tmp_path: pathlib.Path) -> None:
274 content = b"cross check content"
275 f = tmp_path / "f.txt"
276 f.write_bytes(content)
277 file_id = json.loads(_run("--json", str(f)).output)["object_id"]
278 stdin_id = json.loads(_run("--json", "--stdin", stdin=content).output)["object_id"]
279 assert file_id == stdin_id
280
281 def test_write_file_and_stdin_same_id(self, tmp_path: pathlib.Path) -> None:
282 repo = _make_repo(tmp_path)
283 content = b"write cross check"
284 f = repo / "f.txt"
285 f.write_bytes(content)
286 file_id = json.loads(_run("--json", "--write", str(f), repo=repo).output)["object_id"]
287 stdin_id = json.loads(
288 _run("--json", "--stdin", "--write", stdin=content, repo=repo).output
289 )["object_id"]
290 assert file_id == stdin_id
291
292 def test_hash_bytes_returns_canonical(self) -> None:
293 """_hash_bytes must return sha256:-prefixed ID, not bare hex."""
294 from muse.cli.commands.hash_object import _hash_bytes
295 result = _hash_bytes(b"test data")
296 assert result.startswith("sha256:"), (
297 f"_hash_bytes must return 'sha256:<hex>', got {result!r}"
298 )
299 assert len(result) == 71
300
301
302 # ---------------------------------------------------------------------------
303 # TestRegisterFlags — argparse-level verification
304 # ---------------------------------------------------------------------------
305
306
307 class TestRegisterFlags:
308 """Verify that register() wires --json / -j correctly."""
309
310 def _make_parser(self):
311 import argparse
312 from muse.cli.commands.hash_object import register
313 ap = argparse.ArgumentParser()
314 subs = ap.add_subparsers()
315 register(subs)
316 return ap
317
318 def test_json_flag_long(self):
319 ns = self._make_parser().parse_args(["hash-object", "--stdin", "--json"])
320 assert ns.json_out is True
321
322 def test_j_alias(self):
323 ns = self._make_parser().parse_args(["hash-object", "--stdin", "-j"])
324 assert ns.json_out is True
325
326 def test_default_is_text(self):
327 ns = self._make_parser().parse_args(["hash-object", "--stdin"])
328 assert ns.json_out is False
329
330 def test_dest_is_json_out(self):
331 ns = self._make_parser().parse_args(["hash-object", "--stdin", "-j"])
332 assert hasattr(ns, "json_out")
333 assert not hasattr(ns, "fmt")
File History 2 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 138 days ago