gabriel / muse public
test_read_commit_supercharge.py python
414 lines 17.3 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 145 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 long_id
27
28 runner = CliRunner()
29
30 _SNAP_ID: str = compute_snapshot_id({})
31 _COMMITTED_AT: datetime.datetime = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
32
33 _SHA256_FULL = re.compile(r"^sha256:[0-9a-f]{64}$")
34 _SHA256_SHORT_19 = re.compile(r"^sha256:[0-9a-f]{12}$") # "sha256:" (7) + 12 hex = 19 chars
35
36
37 # ---------------------------------------------------------------------------
38 # Helpers
39 # ---------------------------------------------------------------------------
40
41
42 def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path:
43 repo = tmp_path / "repo"
44 muse = repo / ".muse"
45 for sub in ("objects", "commits", "snapshots", "refs/heads"):
46 (muse / sub).mkdir(parents=True)
47 (muse / "HEAD").write_text("ref: refs/heads/main")
48 (muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo", "domain": "code"}))
49 return repo
50
51
52 def _commit(
53 repo: pathlib.Path,
54 *,
55 branch: str = "main",
56 message: str = "test commit",
57 author: str = "tester",
58 parent: str | None = None,
59 parent2: str | None = None,
60 agent_id: str = "",
61 model_id: str = "",
62 snap_id: str | None = None,
63 committed_at: datetime.datetime | None = None,
64 ) -> str:
65 """Write a commit with a real content-addressed ID; return the commit_id."""
66 sid = snap_id or _SNAP_ID
67 ts = committed_at or _COMMITTED_AT
68 parent_ids: list[str] = [p for p in (parent, parent2) if p]
69 commit_id = compute_commit_id(parent_ids, sid, message, ts.isoformat())
70 write_snapshot(repo, SnapshotRecord(
71 snapshot_id=sid,
72 manifest={},
73 created_at=ts,
74 ))
75 rec = CommitRecord(
76 commit_id=commit_id,
77 repo_id="test-repo",
78 branch=branch,
79 snapshot_id=sid,
80 message=message,
81 committed_at=ts,
82 author=author,
83 parent_commit_id=parent,
84 parent2_commit_id=parent2,
85 agent_id=agent_id,
86 model_id=model_id,
87 )
88 write_commit(repo, rec)
89 return commit_id
90
91
92 def _rc(repo: pathlib.Path, *args: str) -> InvokeResult:
93 from muse.cli.app import main as cli
94 return runner.invoke(
95 cli,
96 ["read-commit", *args],
97 env={"MUSE_REPO_ROOT": str(repo)},
98 )
99
100
101 # ---------------------------------------------------------------------------
102 # Unit — _short_id
103 # ---------------------------------------------------------------------------
104
105
106 class TestShortId:
107 """_short_id must keep the sha256: prefix and truncate to exactly 12 hex chars."""
108
109 def test_short_id_keeps_sha256_prefix(self) -> None:
110 from muse.cli.commands.read_commit import _short_id
111 cid = long_id("a" * 64)
112 assert _short_id(cid).startswith("sha256:")
113
114 def test_short_id_12_hex_chars_after_prefix(self) -> None:
115 from muse.cli.commands.read_commit import _short_id
116 cid = long_id("deadbeef" * 8)
117 result = _short_id(cid)
118 assert result == "sha256:deadbeefdeadbeef"[:19] # sha256: + 12 hex
119
120 def test_short_id_total_length_is_19(self) -> None:
121 from muse.cli.commands.read_commit import _short_id
122 cid = long_id("c0ffee" * 11)# 66 hex, take first 64
123 result = _short_id(cid[:71]) # sha256: + 64 hex
124 assert len(result) == 19 # "sha256:" (7) + 12 hex
125
126 def test_short_id_bare_hex_fallback(self) -> None:
127 """Bare hex without sha256: prefix — truncate to 12 chars."""
128 from muse.cli.commands.read_commit import _short_id
129 bare = "a" * 64
130 result = _short_id(bare)
131 assert len(result) == 12
132
133 def test_short_id_matches_regex(self) -> None:
134 from muse.cli.commands.read_commit import _short_id
135 cid = long_id("abcdef01" * 8)
136 assert _SHA256_SHORT_19.match(_short_id(cid))
137
138
139 # ---------------------------------------------------------------------------
140 # Integration — duration_ms and exit_code in JSON output
141 # ---------------------------------------------------------------------------
142
143
144 class TestDurationAndExitCode:
145 def test_duration_ms_present_on_success(self, tmp_path: pathlib.Path) -> None:
146 repo = _make_repo(tmp_path)
147 cid = _commit(repo, message="timing test")
148 data = json.loads(_rc(repo, cid).output)
149 assert "duration_ms" in data, "duration_ms must be present in JSON success output"
150
151 def test_exit_code_zero_on_success(self, tmp_path: pathlib.Path) -> None:
152 repo = _make_repo(tmp_path)
153 cid = _commit(repo, message="exit code test")
154 data = json.loads(_rc(repo, cid).output)
155 assert data["exit_code"] == 0
156
157 def test_duration_ms_is_float(self, tmp_path: pathlib.Path) -> None:
158 repo = _make_repo(tmp_path)
159 cid = _commit(repo, message="float timing")
160 data = json.loads(_rc(repo, cid).output)
161 assert isinstance(data["duration_ms"], float)
162
163 def test_duration_ms_non_negative(self, tmp_path: pathlib.Path) -> None:
164 repo = _make_repo(tmp_path)
165 cid = _commit(repo, message="positive timing")
166 data = json.loads(_rc(repo, cid).output)
167 assert data["duration_ms"] >= 0.0
168
169 def test_fields_filter_preserves_duration_ms(self, tmp_path: pathlib.Path) -> None:
170 """duration_ms is command metadata, not a commit field — --fields must not drop it."""
171 repo = _make_repo(tmp_path)
172 cid = _commit(repo, message="fields + duration")
173 data = json.loads(_rc(repo, "--fields", "commit_id,message", cid).output)
174 assert "duration_ms" in data, "--fields must not filter out duration_ms"
175
176 def test_fields_filter_preserves_exit_code(self, tmp_path: pathlib.Path) -> None:
177 """exit_code is command metadata — --fields must not drop it."""
178 repo = _make_repo(tmp_path)
179 cid = _commit(repo, message="fields + exit_code")
180 data = json.loads(_rc(repo, "--fields", "commit_id", cid).output)
181 assert "exit_code" in data, "--fields must not filter out exit_code"
182
183 def test_duration_ms_3dp_precision(self, tmp_path: pathlib.Path) -> None:
184 """duration_ms must be rounded to 3 decimal places (millisecond precision)."""
185 repo = _make_repo(tmp_path)
186 cid = _commit(repo, message="precision test")
187 data = json.loads(_rc(repo, cid).output)
188 ms = data["duration_ms"]
189 # round-trips through json.dumps — check at most 3 decimal places
190 assert round(ms, 3) == ms
191
192
193 # ---------------------------------------------------------------------------
194 # Integration — text format short ID
195 # ---------------------------------------------------------------------------
196
197
198 class TestTextFormatShortId:
199 """Text format must emit sha256:<12-hex> (19 chars), not the old 12-char bare slice."""
200
201 def _short_token(self, line: str) -> str | None:
202 """Extract the first sha256:... token from a text output line."""
203 for tok in line.split():
204 if _SHA256_SHORT_19.match(tok):
205 return tok
206 return None
207
208 def test_text_short_id_has_sha256_prefix(self, tmp_path: pathlib.Path) -> None:
209 repo = _make_repo(tmp_path)
210 cid = _commit(repo, message="short id prefix test")
211 result = _rc(repo, "--format", "text", cid)
212 assert result.exit_code == 0
213 line = result.output.strip()
214 tok = self._short_token(line)
215 assert tok is not None, f"no sha256:<12-hex> token in text output: {line!r}"
216 assert tok.startswith("sha256:")
217
218 def test_text_short_id_has_12_hex_chars(self, tmp_path: pathlib.Path) -> None:
219 repo = _make_repo(tmp_path)
220 cid = _commit(repo, message="short id hex length test")
221 result = _rc(repo, "--format", "text", cid)
222 line = result.output.strip()
223 tok = self._short_token(line)
224 assert tok is not None, f"no sha256:<12-hex> token in text output: {line!r}"
225 hex_part = tok[len("sha256:"):]
226 assert len(hex_part) == 12, f"expected 12 hex chars after prefix, got {len(hex_part)}: {tok!r}"
227
228 def test_text_short_id_total_length_is_19(self, tmp_path: pathlib.Path) -> None:
229 repo = _make_repo(tmp_path)
230 cid = _commit(repo, message="short id length test")
231 result = _rc(repo, "--format", "text", cid)
232 line = result.output.strip()
233 tok = self._short_token(line)
234 assert tok is not None
235 assert len(tok) == 19, f"short ID must be exactly 19 chars, got {len(tok)}: {tok!r}"
236
237 def test_text_short_id_is_prefix_of_full_id(self, tmp_path: pathlib.Path) -> None:
238 repo = _make_repo(tmp_path)
239 cid = _commit(repo, message="short id is prefix test")
240 result = _rc(repo, "--format", "text", cid)
241 line = result.output.strip()
242 tok = self._short_token(line)
243 assert tok is not None
244 assert cid.startswith(tok), f"{tok!r} is not a prefix of {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(_rc(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(_rc(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(_rc(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 = _rc(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 = _rc(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 = _rc(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 = _rc(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 (repo / ".muse" / "refs" / "heads" / "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(_rc(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(_rc(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(_rc(repo, cid).output)
413 assert data["duration_ms"] < 500, \
414 f"duration_ms={data['duration_ms']} — suspiciously slow or not measuring correctly"
File History 1 commit
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 145 days ago