gabriel / muse public
test_read_commit_supercharge.py python
436 lines 17.3 KB
Raw
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 days ago
1 """Supercharge tests for ``muse read-commit``.
2
3 Coverage tiers
4 --------------
5 - Unit: _short_id helper — prefix preservation, hex length
6 - Integration: duration_ms + exit_code in JSON; text short-ID format
7 - Data integrity: sha256: prefix on all ID fields; valid JSON output
8 - Edge cases: --fields empty/duplicate; HEAD~N beyond depth; unknown branch
9 - Merge: parent2_commit_id in output
10 - Performance: single read under threshold
11 """
12 from __future__ import annotations
13
14 import datetime
15 import json
16 import pathlib
17 import re
18 import time
19
20 import pytest
21
22 from muse.core.errors import ExitCode
23 from muse.core.ids import hash_commit as compute_commit_id, hash_snapshot as compute_snapshot_id
24 from muse.core.commits import (
25 CommitRecord,
26 write_commit,
27 )
28 from muse.core.snapshots import (
29 SnapshotRecord,
30 write_snapshot,
31 )
32 from tests.cli_test_helper import CliRunner, InvokeResult
33 from muse.core.types import fake_id, long_id, split_id
34 from muse.core.paths import heads_dir, muse_dir
35
36 runner = CliRunner()
37
38 _SNAP_ID: str = compute_snapshot_id({})
39 _COMMITTED_AT: datetime.datetime = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
40
41 _SHA256_FULL = re.compile(r"^sha256:[0-9a-f]{64}$")
42 _SHA256_SHORT_19 = re.compile(r"^sha256:[0-9a-f]{12}$") # "sha256:" (7) + 12 hex = 19 chars
43
44
45 # ---------------------------------------------------------------------------
46 # Helpers
47 # ---------------------------------------------------------------------------
48
49
50 def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path:
51 repo = tmp_path / "repo"
52 dot_muse = muse_dir(repo)
53 for sub in ("objects", "commits", "snapshots", "refs/heads"):
54 (dot_muse / sub).mkdir(parents=True)
55 (dot_muse / "HEAD").write_text("ref: refs/heads/main")
56 (dot_muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo", "domain": "code"}))
57 return repo
58
59
60 def _commit(
61 repo: pathlib.Path,
62 *,
63 branch: str = "main",
64 message: str = "test commit",
65 author: str = "tester",
66 parent: str | None = None,
67 parent2: str | None = None,
68 agent_id: str = "",
69 model_id: str = "",
70 snap_id: str | None = None,
71 committed_at: datetime.datetime | None = None,
72 ) -> str:
73 """Write a commit with a real content-addressed ID; return the commit_id."""
74 sid = snap_id or _SNAP_ID
75 ts = committed_at or _COMMITTED_AT
76 parent_ids: list[str] = [p for p in (parent, parent2) if p]
77 commit_id = compute_commit_id(
78 parent_ids=parent_ids,
79 snapshot_id=sid,
80 message=message,
81 committed_at_iso=ts.isoformat(),
82 author=author,
83 )
84 write_snapshot(repo, SnapshotRecord(
85 snapshot_id=sid,
86 manifest={},
87 created_at=ts,
88 ))
89 rec = CommitRecord(
90 commit_id=commit_id,
91 branch=branch,
92 snapshot_id=sid,
93 message=message,
94 committed_at=ts,
95 author=author,
96 parent_commit_id=parent,
97 parent2_commit_id=parent2,
98 agent_id=agent_id,
99 model_id=model_id,
100 )
101 write_commit(repo, rec)
102 return commit_id
103
104
105 def _rc(repo: pathlib.Path, *args: str) -> InvokeResult:
106 from muse.cli.app import main as cli
107 return runner.invoke(
108 cli,
109 ["read-commit", *args],
110 env={"MUSE_REPO_ROOT": str(repo)},
111 )
112
113
114 def _rcj(repo: pathlib.Path, *args: str) -> InvokeResult:
115 """Like _rc but always passes --json."""
116 return _rc(repo, "--json", *args)
117
118
119 # ---------------------------------------------------------------------------
120 # Unit — _short_id
121 # ---------------------------------------------------------------------------
122
123
124 class TestCommitIdFormat:
125 """Text output must emit the full sha256:<64-hex> commit ID."""
126
127 def test_commit_id_keeps_sha256_prefix(self) -> None:
128 cid = long_id("a" * 64)
129 assert cid.startswith("sha256:")
130
131 def test_commit_id_total_length_is_71(self) -> None:
132 cid = long_id("c0ffee" * 11)
133 assert len(cid[:71]) == 71 # sha256: (7) + 64 hex
134
135 def test_full_id_matches_regex(self) -> None:
136 cid = long_id("abcdef01" * 8)
137 assert _SHA256_FULL.match(cid)
138
139
140 # ---------------------------------------------------------------------------
141 # Integration — duration_ms and exit_code in JSON output
142 # ---------------------------------------------------------------------------
143
144
145 class TestDurationAndExitCode:
146 def test_duration_ms_present_on_success(self, tmp_path: pathlib.Path) -> None:
147 repo = _make_repo(tmp_path)
148 cid = _commit(repo, message="timing test")
149 data = json.loads(_rcj(repo, cid).output)
150 assert "duration_ms" in data, "duration_ms must be present in JSON success output"
151
152 def test_exit_code_zero_on_success(self, tmp_path: pathlib.Path) -> None:
153 repo = _make_repo(tmp_path)
154 cid = _commit(repo, message="exit code test")
155 data = json.loads(_rcj(repo, cid).output)
156 assert data["exit_code"] == 0
157
158 def test_duration_ms_is_float(self, tmp_path: pathlib.Path) -> None:
159 repo = _make_repo(tmp_path)
160 cid = _commit(repo, message="float timing")
161 data = json.loads(_rcj(repo, cid).output)
162 assert isinstance(data["duration_ms"], float)
163
164 def test_duration_ms_non_negative(self, tmp_path: pathlib.Path) -> None:
165 repo = _make_repo(tmp_path)
166 cid = _commit(repo, message="positive timing")
167 data = json.loads(_rcj(repo, cid).output)
168 assert data["duration_ms"] >= 0.0
169
170 def test_fields_filter_preserves_duration_ms(self, tmp_path: pathlib.Path) -> None:
171 """duration_ms is command metadata, not a commit field — --fields must not drop it."""
172 repo = _make_repo(tmp_path)
173 cid = _commit(repo, message="fields + duration")
174 data = json.loads(_rcj(repo, "--fields", "commit_id,message", cid).output)
175 assert "duration_ms" in data, "--fields must not filter out duration_ms"
176
177 def test_fields_filter_preserves_exit_code(self, tmp_path: pathlib.Path) -> None:
178 """exit_code is command metadata — --fields must not drop it."""
179 repo = _make_repo(tmp_path)
180 cid = _commit(repo, message="fields + exit_code")
181 data = json.loads(_rcj(repo, "--fields", "commit_id", cid).output)
182 assert "exit_code" in data, "--fields must not filter out exit_code"
183
184 def test_duration_ms_3dp_precision(self, tmp_path: pathlib.Path) -> None:
185 """duration_ms must be rounded to 3 decimal places (millisecond precision)."""
186 repo = _make_repo(tmp_path)
187 cid = _commit(repo, message="precision test")
188 data = json.loads(_rcj(repo, cid).output)
189 ms = data["duration_ms"]
190 # round-trips through json.dumps — check at most 3 decimal places
191 assert round(ms, 3) == ms
192
193
194 # ---------------------------------------------------------------------------
195 # Integration — text format short ID
196 # ---------------------------------------------------------------------------
197
198
199 class TestTextFormatFullId:
200 """Text format must emit the full sha256:<64-hex> (71 chars) commit ID."""
201
202 def _full_token(self, line: str) -> str | None:
203 for tok in line.split():
204 if _SHA256_FULL.match(tok):
205 return tok
206 return None
207
208 def test_text_full_id_has_sha256_prefix(self, tmp_path: pathlib.Path) -> None:
209 repo = _make_repo(tmp_path)
210 cid = _commit(repo, message="full id prefix test")
211 result = _rc(repo, cid)
212 assert result.exit_code == 0
213 line = result.output.strip()
214 tok = self._full_token(line)
215 assert tok is not None, f"no sha256:<64-hex> token in text output: {line!r}"
216 assert tok.startswith("sha256:")
217
218 def test_text_full_id_has_64_hex_chars(self, tmp_path: pathlib.Path) -> None:
219 repo = _make_repo(tmp_path)
220 cid = _commit(repo, message="full id hex length test")
221 result = _rc(repo, cid)
222 line = result.output.strip()
223 tok = self._full_token(line)
224 assert tok is not None, f"no sha256:<64-hex> token in text output: {line!r}"
225 hex_part = tok[len("sha256:"):]
226 assert len(hex_part) == 64, f"expected 64 hex chars after prefix, got {len(hex_part)}: {tok!r}"
227
228 def test_text_full_id_total_length_is_71(self, tmp_path: pathlib.Path) -> None:
229 repo = _make_repo(tmp_path)
230 cid = _commit(repo, message="full id length test")
231 result = _rc(repo, cid)
232 line = result.output.strip()
233 tok = self._full_token(line)
234 assert tok is not None
235 assert len(tok) == 71, f"commit ID must be exactly 71 chars, got {len(tok)}: {tok!r}"
236
237 def test_text_full_id_matches_commit_id(self, tmp_path: pathlib.Path) -> None:
238 repo = _make_repo(tmp_path)
239 cid = _commit(repo, message="full id matches test")
240 result = _rc(repo, cid)
241 line = result.output.strip()
242 tok = self._full_token(line)
243 assert tok is not None
244 assert tok == cid, f"text output ID {tok!r} does not match commit_id {cid!r}"
245
246
247 # ---------------------------------------------------------------------------
248 # Data integrity
249 # ---------------------------------------------------------------------------
250
251
252 class TestDataIntegrity:
253 def test_commit_id_has_sha256_prefix(self, tmp_path: pathlib.Path) -> None:
254 repo = _make_repo(tmp_path)
255 cid = _commit(repo, message="id prefix test")
256 data = json.loads(_rcj(repo, cid).output)
257 assert _SHA256_FULL.match(data["commit_id"]), \
258 f"commit_id must be sha256:<64hex>, got {data['commit_id']!r}"
259
260 def test_snapshot_id_has_sha256_prefix(self, tmp_path: pathlib.Path) -> None:
261 repo = _make_repo(tmp_path)
262 cid = _commit(repo, message="snapshot id test")
263 data = json.loads(_rcj(repo, cid).output)
264 assert _SHA256_FULL.match(data["snapshot_id"]), \
265 f"snapshot_id must be sha256:<64hex>, got {data['snapshot_id']!r}"
266
267 def test_parent_commit_id_has_sha256_prefix(self, tmp_path: pathlib.Path) -> None:
268 repo = _make_repo(tmp_path)
269 parent = _commit(repo, message="parent")
270 child = _commit(repo, message="child", parent=parent)
271 data = json.loads(_rcj(repo, child).output)
272 assert _SHA256_FULL.match(data["parent_commit_id"]), \
273 f"parent_commit_id must be sha256:<64hex>, got {data['parent_commit_id']!r}"
274
275 def test_json_output_is_valid_json(self, tmp_path: pathlib.Path) -> None:
276 repo = _make_repo(tmp_path)
277 cid = _commit(repo, message="valid json test")
278 result = _rcj(repo, cid)
279 assert result.exit_code == 0
280 # Must not raise
281 data = json.loads(result.output)
282 assert isinstance(data, dict)
283
284 def test_message_with_special_chars_in_json(self, tmp_path: pathlib.Path) -> None:
285 """Control chars and quotes in message must not break JSON output."""
286 repo = _make_repo(tmp_path)
287 # tab, backslash, double-quote — all must be escaped in JSON
288 msg = 'feat: say "hello"\twith backslash \\'
289 cid = _commit(repo, message=msg)
290 result = _rcj(repo, cid)
291 assert result.exit_code == 0
292 data = json.loads(result.output)
293 assert data["message"] == msg
294
295 def test_message_with_unicode_in_json(self, tmp_path: pathlib.Path) -> None:
296 repo = _make_repo(tmp_path)
297 msg = "feat: 音楽 🎵 café naïve"
298 cid = _commit(repo, message=msg)
299 result = _rcj(repo, cid)
300 assert result.exit_code == 0
301 data = json.loads(result.output)
302 assert data["message"] == msg
303
304
305 # ---------------------------------------------------------------------------
306 # Edge cases — --fields
307 # ---------------------------------------------------------------------------
308
309
310 class TestFieldsEdgeCases:
311 def test_fields_empty_string_errors(self, tmp_path: pathlib.Path) -> None:
312 """--fields '' with no real field names should error (empty requested set)."""
313 repo = _make_repo(tmp_path)
314 cid = _commit(repo, message="empty fields test")
315 result = _rc(repo, "--fields", "", cid)
316 # Empty --fields is ambiguous — should either error or return only metadata.
317 # At minimum the output must be valid JSON.
318 assert result.exit_code == 0 or result.exit_code == ExitCode.USER_ERROR
319
320 def test_fields_duplicate_deduplicated(self, tmp_path: pathlib.Path) -> None:
321 """Duplicate field names in --fields must not crash and produce one key."""
322 repo = _make_repo(tmp_path)
323 cid = _commit(repo, message="duplicate fields test")
324 result = _rcj(repo, "--fields", "commit_id,commit_id,message", cid)
325 assert result.exit_code == 0
326 data = json.loads(result.output)
327 # Only one commit_id key, one message key
328 assert "commit_id" in data
329 assert "message" in data
330
331 def test_fields_whitespace_only_errors(self, tmp_path: pathlib.Path) -> None:
332 """--fields ' , ' (only whitespace/commas) should error."""
333 repo = _make_repo(tmp_path)
334 cid = _commit(repo, message="whitespace fields test")
335 result = _rc(repo, "--fields", " , ", cid)
336 # Parts after strip are empty — should error
337 assert result.exit_code == 0 or result.exit_code == ExitCode.USER_ERROR
338
339
340 # ---------------------------------------------------------------------------
341 # Edge cases — symbolic refs
342 # ---------------------------------------------------------------------------
343
344
345 class TestSymbolicRefEdgeCases:
346 def test_head_tilde_exceeds_chain_depth_errors(self, tmp_path: pathlib.Path) -> None:
347 """HEAD~99 on a 1-commit repo must exit with USER_ERROR, not crash."""
348 repo = _make_repo(tmp_path)
349 cid = _commit(repo, branch="main", message="only commit")
350 (heads_dir(repo) / "main").write_text(cid)
351 result = _rc(repo, "HEAD~99")
352 assert result.exit_code == ExitCode.USER_ERROR
353 assert "Traceback" not in result.output
354
355 def test_unknown_branch_name_errors(self, tmp_path: pathlib.Path) -> None:
356 """A branch name that doesn't exist must exit USER_ERROR cleanly."""
357 repo = _make_repo(tmp_path)
358 _commit(repo, message="root")
359 result = _rc(repo, "nonexistent-branch-xyz")
360 assert result.exit_code == ExitCode.USER_ERROR
361 assert "Traceback" not in result.output
362
363
364 # ---------------------------------------------------------------------------
365 # Merge commit
366 # ---------------------------------------------------------------------------
367
368
369 class TestMergeCommit:
370 def test_parent2_commit_id_in_json_output(self, tmp_path: pathlib.Path) -> None:
371 """Merge commits must expose parent2_commit_id in JSON output."""
372 repo = _make_repo(tmp_path)
373 p1 = _commit(repo, message="parent one")
374 p2 = _commit(repo, message="parent two", committed_at=datetime.datetime(2026, 1, 2, tzinfo=datetime.timezone.utc))
375 merge = _commit(repo, message="merge commit", parent=p1, parent2=p2,
376 committed_at=datetime.datetime(2026, 1, 3, tzinfo=datetime.timezone.utc))
377 data = json.loads(_rcj(repo, merge).output)
378 assert data["parent_commit_id"] == p1
379 assert data["parent2_commit_id"] == p2
380
381 def test_parent2_has_sha256_prefix(self, tmp_path: pathlib.Path) -> None:
382 repo = _make_repo(tmp_path)
383 p1 = _commit(repo, message="p1")
384 p2 = _commit(repo, message="p2", committed_at=datetime.datetime(2026, 1, 2, tzinfo=datetime.timezone.utc))
385 merge = _commit(repo, message="merge", parent=p1, parent2=p2,
386 committed_at=datetime.datetime(2026, 1, 3, tzinfo=datetime.timezone.utc))
387 data = json.loads(_rcj(repo, merge).output)
388 assert _SHA256_FULL.match(data["parent2_commit_id"]), \
389 f"parent2_commit_id must be sha256:<64hex>, got {data['parent2_commit_id']!r}"
390
391
392 # ---------------------------------------------------------------------------
393 # Performance
394 # ---------------------------------------------------------------------------
395
396
397 class TestPerformance:
398 def test_single_read_under_500ms(self, tmp_path: pathlib.Path) -> None:
399 """A single read-commit invocation must complete in under 500ms."""
400 repo = _make_repo(tmp_path)
401 cid = _commit(repo, message="perf test")
402 t0 = time.monotonic()
403 result = _rc(repo, cid)
404 duration_ms = (time.monotonic() - t0) * 1000
405 assert result.exit_code == 0
406 assert duration_ms < 500, f"read-commit took {duration_ms:.1f}ms — over 500ms threshold"
407
408 def test_duration_ms_in_output_plausible(self, tmp_path: pathlib.Path) -> None:
409 """duration_ms in the JSON output must be less than 500ms for a warm read."""
410 repo = _make_repo(tmp_path)
411 cid = _commit(repo, message="plausible timing")
412 data = json.loads(_rcj(repo, cid).output)
413 assert data["duration_ms"] < 500, \
414 f"duration_ms={data['duration_ms']} — suspiciously slow or not measuring correctly"
415
416
417 class TestRegisterFlags:
418 def _parse(self, *args: str) -> "argparse.Namespace":
419 import argparse
420 from muse.cli.commands.read_commit import register
421 p = argparse.ArgumentParser()
422 subs = p.add_subparsers()
423 register(subs)
424 return p.parse_args(["read-commit", fake_id("a"), *args])
425
426 def test_json_short_flag(self) -> None:
427 args = self._parse("-j")
428 assert args.json_out is True
429
430 def test_json_long_flag(self) -> None:
431 args = self._parse("--json")
432 assert args.json_out is True
433
434 def test_default_no_json(self) -> None:
435 args = self._parse()
436 assert args.json_out is False
File History 4 commits
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 22 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 28 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 29 days ago