gabriel / muse public
test_cmd_read_commit.py python
439 lines 18.1 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 143 days ago
1 """Comprehensive tests for ``muse read-commit``.
2
3 Coverage tiers
4 --------------
5 - Unit: _ALL_FIELDS completeness
6 - Integration: JSON/text format, prefix resolution, --fields filter, parent chain
7 - Security: ANSI in branch/author/message stripped in text mode
8 - Stress: 200 sequential reads, --fields on large schema
9 """
10 from __future__ import annotations
11
12 import datetime
13 import json
14 import pathlib
15
16 from muse.core.errors import ExitCode
17 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
18 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
19 from tests.cli_test_helper import CliRunner, InvokeResult
20 from muse.core._types import long_id
21
22 runner = CliRunner()
23
24 # Module-level constants so every test uses the same deterministic inputs.
25 _SNAP_ID: str = compute_snapshot_id({})
26 _COMMITTED_AT: datetime.datetime = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
27
28
29 # ---------------------------------------------------------------------------
30 # Helpers
31 # ---------------------------------------------------------------------------
32
33 def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path:
34 repo = tmp_path / "repo"
35 muse = repo / ".muse"
36 for sub in ("objects", "commits", "snapshots", "refs/heads"):
37 (muse / sub).mkdir(parents=True)
38 (muse / "HEAD").write_text("ref: refs/heads/main")
39 (muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo", "domain": "code"}))
40 return repo
41
42
43 def _commit(
44 repo: pathlib.Path,
45 *,
46 branch: str = "main",
47 message: str = "test commit",
48 author: str = "tester",
49 parent: str | None = None,
50 agent_id: str = "",
51 model_id: str = "",
52 ) -> str:
53 """Write a commit with a real content-addressed ID; return the commit_id."""
54 parent_ids: list[str] = [parent] if parent else []
55 commit_id = compute_commit_id(parent_ids, _SNAP_ID, message, _COMMITTED_AT.isoformat())
56 write_snapshot(repo, SnapshotRecord(
57 snapshot_id=_SNAP_ID,
58 manifest={},
59 created_at=_COMMITTED_AT,
60 ))
61 rec = CommitRecord(
62 commit_id=commit_id,
63 repo_id="test-repo",
64 branch=branch,
65 snapshot_id=_SNAP_ID,
66 message=message,
67 committed_at=_COMMITTED_AT,
68 author=author,
69 parent_commit_id=parent,
70 agent_id=agent_id,
71 model_id=model_id,
72 )
73 write_commit(repo, rec)
74 return commit_id
75
76
77 def _rc(repo: pathlib.Path, *args: str) -> InvokeResult:
78 from muse.cli.app import main as cli
79 return runner.invoke(
80 cli,
81 ["read-commit", *args],
82 env={"MUSE_REPO_ROOT": str(repo)},
83 )
84
85
86 # ---------------------------------------------------------------------------
87 # Unit — _ALL_FIELDS
88 # ---------------------------------------------------------------------------
89
90
91 class TestAllFields:
92 def test_all_fields_matches_commitdict_annotations(self) -> None:
93 """_ALL_FIELDS must be exactly the keys in CommitDict.__annotations__."""
94 from muse.cli.commands.read_commit import _ALL_FIELDS
95 from muse.core.store import CommitDict
96 assert _ALL_FIELDS == frozenset(CommitDict.__annotations__.keys())
97
98 def test_required_fields_present(self) -> None:
99 from muse.cli.commands.read_commit import _ALL_FIELDS
100 for field in ("commit_id", "branch", "message", "committed_at",
101 "agent_id", "model_id", "format_version", "reviewed_by"):
102 assert field in _ALL_FIELDS
103
104
105 # ---------------------------------------------------------------------------
106 # Integration — JSON format
107 # ---------------------------------------------------------------------------
108
109
110 class TestJsonFormat:
111 def test_full_schema_returned(self, tmp_path: pathlib.Path) -> None:
112 repo = _make_repo(tmp_path)
113 cid = _commit(repo, message="hello world")
114 result = _rc(repo, cid)
115 assert result.exit_code == 0
116 data = json.loads(result.output)
117 assert data["commit_id"] == cid
118 assert data["message"] == "hello world"
119 assert data["branch"] == "main"
120 assert "format_version" in data
121
122 def test_json_flag_shorthand(self, tmp_path: pathlib.Path) -> None:
123 repo = _make_repo(tmp_path)
124 cid = _commit(repo, message="shorthand test")
125 result = _rc(repo, "--json", cid)
126 assert result.exit_code == 0
127 assert "commit_id" in json.loads(result.output)
128
129 def test_agent_provenance_fields_present(self, tmp_path: pathlib.Path) -> None:
130 repo = _make_repo(tmp_path)
131 cid = _commit(repo, agent_id="my-agent", model_id="claude-4")
132 data = json.loads(_rc(repo, cid).output)
133 assert data["agent_id"] == "my-agent"
134 assert data["model_id"] == "claude-4"
135
136 def test_parent_commit_id_null_for_root(self, tmp_path: pathlib.Path) -> None:
137 repo = _make_repo(tmp_path)
138 cid = _commit(repo, message="root commit")
139 data = json.loads(_rc(repo, cid).output)
140 assert data["parent_commit_id"] is None
141
142 def test_parent_commit_id_set_for_child(self, tmp_path: pathlib.Path) -> None:
143 repo = _make_repo(tmp_path)
144 parent = _commit(repo, message="parent commit")
145 child = _commit(repo, message="child commit", parent=parent)
146 data = json.loads(_rc(repo, child).output)
147 assert data["parent_commit_id"] == parent
148
149 def test_committed_at_is_iso8601(self, tmp_path: pathlib.Path) -> None:
150 repo = _make_repo(tmp_path)
151 cid = _commit(repo, message="iso date test")
152 data = json.loads(_rc(repo, cid).output)
153 # Should parse without error
154 datetime.datetime.fromisoformat(data["committed_at"])
155
156 def test_snapshot_id_in_output(self, tmp_path: pathlib.Path) -> None:
157 repo = _make_repo(tmp_path)
158 cid = _commit(repo, message="snapshot test")
159 data = json.loads(_rc(repo, cid).output)
160 import re
161 assert re.fullmatch(r"sha256:[0-9a-f]{64}", data["snapshot_id"])
162
163
164 # ---------------------------------------------------------------------------
165 # Integration — text format
166 # ---------------------------------------------------------------------------
167
168
169 class TestTextFormat:
170 def test_text_format_contains_commit_prefix(self, tmp_path: pathlib.Path) -> None:
171 repo = _make_repo(tmp_path)
172 cid = _commit(repo, message="text test")
173 result = _rc(repo, "--format", "text", cid)
174 assert result.exit_code == 0
175 line = result.output.strip()
176 assert cid[:12] in line
177
178 def test_text_format_contains_branch(self, tmp_path: pathlib.Path) -> None:
179 repo = _make_repo(tmp_path)
180 cid = _commit(repo, branch="main", message="branch test")
181 result = _rc(repo, "--format", "text", cid)
182 assert "main" in result.output
183
184 def test_text_format_contains_message(self, tmp_path: pathlib.Path) -> None:
185 repo = _make_repo(tmp_path)
186 cid = _commit(repo, message="my commit message")
187 result = _rc(repo, "--format", "text", cid)
188 assert "my commit message" in result.output
189
190 def test_text_multiline_message_flattened(self, tmp_path: pathlib.Path) -> None:
191 repo = _make_repo(tmp_path)
192 cid = _commit(repo, message="line one\nline two")
193 result = _rc(repo, "--format", "text", cid)
194 # Newline replaced with space — output stays on one line
195 assert "\n" not in result.output.strip()
196 assert "line one" in result.output
197
198
199 # ---------------------------------------------------------------------------
200 # Integration — prefix resolution
201 # ---------------------------------------------------------------------------
202
203
204 class TestPrefixResolution:
205 def test_sha256_short_prefix_resolves(self, tmp_path: pathlib.Path) -> None:
206 """sha256:<8-hex> prefix form resolves to the full commit."""
207 repo = _make_repo(tmp_path)
208 cid = _commit(repo, message="prefix resolve test")
209 # cid is "sha256:<64-hex>"; take long_id(first 8 hex chars = 15 chars)
210 short_ref = cid[:15]
211 result = _rc(repo, short_ref)
212 assert result.exit_code == 0
213 assert json.loads(result.output)["commit_id"] == cid
214
215 def test_sha256_full_id_resolves(self, tmp_path: pathlib.Path) -> None:
216 """Full sha256:<64-hex> canonical form resolves directly."""
217 repo = _make_repo(tmp_path)
218 cid = _commit(repo, message="full id resolve test")
219 result = _rc(repo, cid)
220 assert result.exit_code == 0
221 assert json.loads(result.output)["commit_id"] == cid
222
223 def test_ambiguous_prefix_errors(self, tmp_path: pathlib.Path) -> None:
224 repo = _make_repo(tmp_path)
225 # "msg 121" and "msg 127" produce commit IDs sharing the "3f47" 4-char
226 # hex prefix when hashed with an empty manifest and 2026-01-01T00:00:00+00:00.
227 # Verified by precomputation; changing _SNAP_ID or _COMMITTED_AT requires
228 # updating these message strings.
229 cid1 = _commit(repo, message="msg 121")
230 cid2 = _commit(repo, message="msg 127")
231 result = _rc(repo, "sha256:3f47")
232 assert result.exit_code == ExitCode.USER_ERROR
233 data = json.loads(result.output)
234 assert "ambiguous" in data["error"]
235 assert set(data["candidates"]) == {cid1, cid2}
236
237 def test_missing_commit_errors(self, tmp_path: pathlib.Path) -> None:
238 repo = _make_repo(tmp_path)
239 # Valid canonical ID that doesn't exist in the store
240 result = _rc(repo, long_id("dead" + "beef" * 15))
241 assert result.exit_code == ExitCode.USER_ERROR
242 data = json.loads(result.output)
243 assert "not found" in data["error"]
244
245 def test_bare_hex_rejected(self, tmp_path: pathlib.Path) -> None:
246 """Bare hex without sha256: prefix is rejected with a clear error."""
247 repo = _make_repo(tmp_path)
248 result = _rc(repo, "a" * 64)
249 assert result.exit_code == ExitCode.USER_ERROR
250 data = json.loads(result.output)
251 assert "sha256:" in data["error"]
252
253 def test_bare_short_hex_rejected(self, tmp_path: pathlib.Path) -> None:
254 """Short bare hex prefix is rejected — sha256:<hex> form required."""
255 repo = _make_repo(tmp_path)
256 result = _rc(repo, "deadbeef")
257 assert result.exit_code == ExitCode.USER_ERROR
258 data = json.loads(result.output)
259 assert "sha256:" in data["error"]
260
261 def test_invalid_commit_id_errors(self, tmp_path: pathlib.Path) -> None:
262 repo = _make_repo(tmp_path)
263 result = _rc(repo, "ZZZZ" + "a" * 60)
264 assert result.exit_code == ExitCode.USER_ERROR
265
266
267 class TestSymbolicRefResolution:
268 def test_head_resolves(self, tmp_path: pathlib.Path) -> None:
269 """HEAD resolves to the tip of the current branch."""
270 repo = _make_repo(tmp_path)
271 cid = _commit(repo, branch="main", message="head test")
272 # Write branch ref so HEAD resolves
273 (repo / ".muse" / "refs" / "heads" / "main").write_text(cid, encoding="utf-8")
274 result = _rc(repo, "HEAD")
275 assert result.exit_code == 0
276 assert json.loads(result.output)["commit_id"] == cid
277
278 def test_branch_name_resolves(self, tmp_path: pathlib.Path) -> None:
279 """A branch name resolves to the tip commit of that branch."""
280 repo = _make_repo(tmp_path)
281 cid = _commit(repo, branch="dev", message="branch ref test")
282 (repo / ".muse" / "refs" / "heads" / "dev").write_text(cid, encoding="utf-8")
283 result = _rc(repo, "dev")
284 assert result.exit_code == 0
285 assert json.loads(result.output)["commit_id"] == cid
286
287 def test_tilde_notation_resolves(self, tmp_path: pathlib.Path) -> None:
288 """HEAD~1 resolves to the parent of the HEAD commit."""
289 repo = _make_repo(tmp_path)
290 parent_cid = _commit(repo, branch="main", message="parent")
291 child_cid = _commit(repo, branch="main", message="child", parent=parent_cid)
292 (repo / ".muse" / "refs" / "heads" / "main").write_text(child_cid, encoding="utf-8")
293 result = _rc(repo, "HEAD~1")
294 assert result.exit_code == 0
295 assert json.loads(result.output)["commit_id"] == parent_cid
296
297
298 # ---------------------------------------------------------------------------
299 # Integration — --fields filter
300 # ---------------------------------------------------------------------------
301
302
303 class TestFieldsFilter:
304 def test_single_field(self, tmp_path: pathlib.Path) -> None:
305 repo = _make_repo(tmp_path)
306 cid = _commit(repo, message="filtered")
307 result = _rc(repo, "--fields", "message", cid)
308 assert result.exit_code == 0
309 data = json.loads(result.output)
310 # duration_ms and exit_code are always-present metadata fields — not commit fields.
311 commit_keys = set(data.keys()) - {"duration_ms", "exit_code"}
312 assert commit_keys == {"message"}
313 assert data["message"] == "filtered"
314
315 def test_multiple_fields(self, tmp_path: pathlib.Path) -> None:
316 repo = _make_repo(tmp_path)
317 cid = _commit(repo, branch="dev", message="multi field test")
318 result = _rc(repo, "--fields", "commit_id,branch,message", cid)
319 data = json.loads(result.output)
320 commit_keys = set(data.keys()) - {"duration_ms", "exit_code"}
321 assert commit_keys == {"commit_id", "branch", "message"}
322 assert data["commit_id"] == cid
323 assert data["branch"] == "dev"
324
325 def test_agent_fields_filter(self, tmp_path: pathlib.Path) -> None:
326 """Agents extracting provenance fields should get exactly what they asked for."""
327 repo = _make_repo(tmp_path)
328 cid = _commit(repo, agent_id="audit-bot", model_id="claude-4")
329 result = _rc(repo, "--fields", "agent_id,model_id,format_version", cid)
330 data = json.loads(result.output)
331 commit_keys = set(data.keys()) - {"duration_ms", "exit_code"}
332 assert commit_keys == {"agent_id", "model_id", "format_version"}
333 assert data["agent_id"] == "audit-bot"
334 assert data["model_id"] == "claude-4"
335
336 def test_unknown_field_errors(self, tmp_path: pathlib.Path) -> None:
337 repo = _make_repo(tmp_path)
338 cid = _commit(repo, message="unknown field test")
339 result = _rc(repo, "--fields", "nonexistent_field", cid)
340 assert result.exit_code == ExitCode.USER_ERROR
341
342 def test_fields_with_text_format_errors(self, tmp_path: pathlib.Path) -> None:
343 repo = _make_repo(tmp_path)
344 cid = _commit(repo, message="fields text error test")
345 result = _rc(repo, "--fields", "commit_id", "--format", "text", cid)
346 assert result.exit_code == ExitCode.USER_ERROR
347
348 def test_fields_whitespace_trimmed(self, tmp_path: pathlib.Path) -> None:
349 repo = _make_repo(tmp_path)
350 cid = _commit(repo, message="whitespace trim test")
351 result = _rc(repo, "--fields", " commit_id , message ", cid)
352 assert result.exit_code == 0
353 data = json.loads(result.output)
354 assert "commit_id" in data
355 assert "message" in data
356
357
358 # ---------------------------------------------------------------------------
359 # Security
360 # ---------------------------------------------------------------------------
361
362
363 class TestSecurity:
364 def test_ansi_in_branch_stripped_in_text(self, tmp_path: pathlib.Path) -> None:
365 repo = _make_repo(tmp_path)
366 _commit(repo, branch="main")
367 # Write an evil commit directly, bypassing the normal helper.
368 # The commit_id must be a real hash of the stored fields for read_commit
369 # to pass content-hash verification.
370 from muse.core.store import write_commit as _wc
371 evil_message = "test"
372 evil_cid = compute_commit_id([], _SNAP_ID, evil_message, _COMMITTED_AT.isoformat())
373 evil_rec = CommitRecord(
374 commit_id=evil_cid,
375 repo_id="test-repo",
376 branch="\x1b[31mevil\x1b[0m",
377 snapshot_id=_SNAP_ID,
378 message=evil_message,
379 committed_at=_COMMITTED_AT,
380 )
381 _wc(repo, evil_rec)
382 result = _rc(repo, "--format", "text", evil_cid)
383 assert result.exit_code == 0
384 assert "\x1b" not in result.output
385
386 def test_ansi_in_message_stripped_in_text(self, tmp_path: pathlib.Path) -> None:
387 repo = _make_repo(tmp_path)
388 # snapshot_id does not need to be a valid hex string for read_commit to
389 # succeed — _verify_commit_id uses it as an opaque string in the hash.
390 evil_snap_id = "s" * 64
391 evil_message = "\x1b[31mmalicious\x1b[0m"
392 evil_committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
393 evil_cid = compute_commit_id([], evil_snap_id, evil_message, evil_committed_at.isoformat())
394 evil_rec = CommitRecord(
395 commit_id=evil_cid,
396 repo_id="test-repo",
397 branch="main",
398 snapshot_id=evil_snap_id,
399 message=evil_message,
400 committed_at=evil_committed_at,
401 )
402 write_commit(repo, evil_rec)
403 result = _rc(repo, "--format", "text", evil_cid)
404 assert result.exit_code == 0
405 assert "\x1b" not in result.output
406
407 def test_ansi_in_commit_id_rejected(self, tmp_path: pathlib.Path) -> None:
408 repo = _make_repo(tmp_path)
409 result = _rc(repo, "\x1b[31m" + "a" * 64)
410 assert result.exit_code == ExitCode.USER_ERROR
411
412 def test_no_traceback_on_bad_input(self, tmp_path: pathlib.Path) -> None:
413 repo = _make_repo(tmp_path)
414 result = _rc(repo, "not-valid")
415 assert "Traceback" not in result.output
416
417
418 # ---------------------------------------------------------------------------
419 # Stress
420 # ---------------------------------------------------------------------------
421
422
423 class TestStress:
424 def test_200_sequential_reads(self, tmp_path: pathlib.Path) -> None:
425 repo = _make_repo(tmp_path)
426 cid = _commit(repo, message="stable")
427 for i in range(200):
428 result = _rc(repo, cid)
429 assert result.exit_code == 0, f"failed at iteration {i}"
430 assert json.loads(result.output)["message"] == "stable"
431
432 def test_fields_filter_200_iterations(self, tmp_path: pathlib.Path) -> None:
433 repo = _make_repo(tmp_path)
434 cid = _commit(repo, agent_id="bot")
435 for i in range(200):
436 result = _rc(repo, "--fields", "commit_id,agent_id", cid)
437 assert result.exit_code == 0, f"failed at iteration {i}"
438 data = json.loads(result.output)
439 assert data["agent_id"] == "bot"
File History 2 commits
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 143 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 146 days ago