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