gabriel / muse public
test_cmd_commit_graph.py python
693 lines 25.7 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 138 days ago
1 """Comprehensive tests for ``muse commit-graph``.
2
3 Coverage tiers
4 --------------
5 - Unit: _CommitNode schema, _DEFAULT_MAX
6 - Integration: linear chain, --tip, --max, --count, --first-parent, --stop-at,
7 --ancestry-path, text format, json shorthand
8 - Security: errors to stderr, no traceback on bad tip
9 - Stress: 50-commit chain traversal
10 """
11 from __future__ import annotations
12
13 import datetime
14 import json
15 import pathlib
16
17 from muse.core.errors import ExitCode
18 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
19 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
20 from tests.cli_test_helper import CliRunner, InvokeResult
21
22 runner = CliRunner()
23
24 _DT = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
25
26
27 # ---------------------------------------------------------------------------
28 # Helpers
29 # ---------------------------------------------------------------------------
30
31 def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path:
32 repo = tmp_path / "repo"
33 muse = repo / ".muse"
34 for sub in ("objects", "commits", "snapshots", "refs/heads"):
35 (muse / sub).mkdir(parents=True)
36 (muse / "HEAD").write_text("ref: refs/heads/main")
37 (muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo", "domain": "code"}))
38 return repo
39
40
41 def _snap(repo: pathlib.Path) -> str:
42 """Write a snapshot with an empty manifest; return its content-addressed ID."""
43 sid = compute_snapshot_id({})
44 write_snapshot(repo, SnapshotRecord(
45 snapshot_id=sid,
46 manifest={},
47 created_at=_DT,
48 ))
49 return sid
50
51
52 def _commit(
53 repo: pathlib.Path,
54 snap_id: str,
55 *,
56 parent: str | None = None,
57 parent2: str | None = None,
58 message: str = "test",
59 ) -> str:
60 """Write a commit with a real content-addressed ID; return the commit ID."""
61 parent_ids = [p for p in [parent, parent2] if p is not None]
62 commit_id = compute_commit_id(parent_ids, snap_id, message, _DT.isoformat())
63 write_commit(repo, CommitRecord(
64 commit_id=commit_id,
65 repo_id="test-repo",
66 branch="main",
67 snapshot_id=snap_id,
68 message=message,
69 committed_at=_DT,
70 parent_commit_id=parent,
71 parent2_commit_id=parent2,
72 ))
73 return commit_id
74
75
76 def _set_head(repo: pathlib.Path, branch: str, commit_id: str) -> None:
77 ref = repo / ".muse" / "refs" / "heads" / branch
78 ref.parent.mkdir(parents=True, exist_ok=True)
79 ref.write_text(commit_id)
80 (repo / ".muse" / "HEAD").write_text(f"ref: refs/heads/{branch}")
81
82
83 def _cg(repo: pathlib.Path, *args: str) -> InvokeResult:
84 from muse.cli.app import main as cli
85 return runner.invoke(
86 cli,
87 ["commit-graph", *args],
88 env={"MUSE_REPO_ROOT": str(repo)},
89 )
90
91
92 # ---------------------------------------------------------------------------
93 # Unit
94 # ---------------------------------------------------------------------------
95
96
97 class TestUnit:
98 def test_commit_node_fields(self) -> None:
99 from muse.cli.commands.commit_graph import _CommitNode
100 fields = set(_CommitNode.__annotations__.keys())
101 assert "commit_id" in fields
102 assert "parent_commit_id" in fields
103 assert "parent2_commit_id" in fields
104 assert "message" in fields
105 assert "snapshot_id" in fields
106 assert "branch" in fields
107 assert "committed_at" in fields
108 assert "author" in fields
109
110 def test_default_max(self) -> None:
111 from muse.cli.commands.commit_graph import _DEFAULT_MAX
112 assert _DEFAULT_MAX >= 1000
113
114 def test_ancestors_of_single_commit(self, tmp_path: pathlib.Path) -> None:
115 from muse.cli.commands.commit_graph import _ancestors_of
116 repo = _make_repo(tmp_path)
117 snap_id = _snap(repo)
118 cid = _commit(repo, snap_id)
119 result = _ancestors_of(repo, cid)
120 assert cid in result
121
122 def test_ancestors_of_linear_chain(self, tmp_path: pathlib.Path) -> None:
123 from muse.cli.commands.commit_graph import _ancestors_of
124 repo = _make_repo(tmp_path)
125 snap_id = _snap(repo)
126 c1 = _commit(repo, snap_id, message="c1")
127 c2 = _commit(repo, snap_id, parent=c1, message="c2")
128 c3 = _commit(repo, snap_id, parent=c2, message="c3")
129 result = _ancestors_of(repo, c3)
130 assert c1 in result
131 assert c2 in result
132 assert c3 in result
133
134 def test_ancestors_of_merge_commit_follows_both_parents(self, tmp_path: pathlib.Path) -> None:
135 from muse.cli.commands.commit_graph import _ancestors_of
136 repo = _make_repo(tmp_path)
137 snap_id = _snap(repo)
138 base = _commit(repo, snap_id, message="base")
139 left = _commit(repo, snap_id, parent=base, message="left")
140 right = _commit(repo, snap_id, parent=base, message="right")
141 merge = _commit(repo, snap_id, parent=left, parent2=right, message="merge")
142 result = _ancestors_of(repo, merge)
143 assert base in result
144 assert left in result
145 assert right in result
146 assert merge in result
147
148 def test_ancestors_of_missing_commit_returns_empty(self, tmp_path: pathlib.Path) -> None:
149 from muse.cli.commands.commit_graph import _ancestors_of
150 from muse.core._types import blob_id
151 repo = _make_repo(tmp_path)
152 # Use blob_id to produce a well-formed sha256: ID that doesn't exist as a commit.
153 missing = blob_id(b"this commit does not exist")
154 # Missing commit: read_commit returns None, so it is skipped → empty set.
155 result = _ancestors_of(repo, missing)
156 assert missing not in result
157 assert len(result) == 0
158
159
160 # ---------------------------------------------------------------------------
161 # Integration — JSON format
162 # ---------------------------------------------------------------------------
163
164
165 class TestJsonFormat:
166 def test_linear_two_commits(self, tmp_path: pathlib.Path) -> None:
167 repo = _make_repo(tmp_path)
168 snap_id = _snap(repo)
169 c1 = _commit(repo, snap_id, message="c1")
170 c2 = _commit(repo, snap_id, parent=c1, message="c2")
171 _set_head(repo, "main", c2)
172 result = _cg(repo)
173 assert result.exit_code == 0
174 data = json.loads(result.output)
175 assert data["count"] == 2
176 ids = {c["commit_id"] for c in data["commits"]}
177 assert {c1, c2} == ids
178
179 def test_tip_is_present_in_output(self, tmp_path: pathlib.Path) -> None:
180 repo = _make_repo(tmp_path)
181 snap_id = _snap(repo)
182 cid = _commit(repo, snap_id)
183 _set_head(repo, "main", cid)
184 data = json.loads(_cg(repo).output)
185 assert data["tip"] == cid
186
187 def test_explicit_tip(self, tmp_path: pathlib.Path) -> None:
188 repo = _make_repo(tmp_path)
189 snap_id = _snap(repo)
190 cid = _commit(repo, snap_id)
191 result = _cg(repo, "--tip", cid)
192 assert result.exit_code == 0
193 data = json.loads(result.output)
194 assert data["tip"] == cid
195
196 def test_json_shorthand(self, tmp_path: pathlib.Path) -> None:
197 repo = _make_repo(tmp_path)
198 snap_id = _snap(repo)
199 cid = _commit(repo, snap_id)
200 _set_head(repo, "main", cid)
201 result = _cg(repo, "--json")
202 assert result.exit_code == 0
203 assert "commits" in json.loads(result.output)
204
205 def test_truncated_flag_when_limited(self, tmp_path: pathlib.Path) -> None:
206 repo = _make_repo(tmp_path)
207 snap_id = _snap(repo)
208 c1 = _commit(repo, snap_id, message="c1")
209 c2 = _commit(repo, snap_id, parent=c1, message="c2")
210 _set_head(repo, "main", c2)
211 data = json.loads(_cg(repo, "--max", "1").output)
212 assert data["truncated"] is True
213
214
215 # ---------------------------------------------------------------------------
216 # Integration — --count
217 # ---------------------------------------------------------------------------
218
219
220 class TestCountOnly:
221 def test_count_returns_integer(self, tmp_path: pathlib.Path) -> None:
222 repo = _make_repo(tmp_path)
223 snap_id = _snap(repo)
224 cid = _commit(repo, snap_id)
225 _set_head(repo, "main", cid)
226 data = json.loads(_cg(repo, "--count").output)
227 assert data["count"] == 1
228 assert "commits" not in data
229
230 def test_count_reflects_chain_length(self, tmp_path: pathlib.Path) -> None:
231 repo = _make_repo(tmp_path)
232 snap_id = _snap(repo)
233 c1 = _commit(repo, snap_id, message="c1")
234 c2 = _commit(repo, snap_id, parent=c1, message="c2")
235 c3 = _commit(repo, snap_id, parent=c2, message="c3")
236 _set_head(repo, "main", c3)
237 data = json.loads(_cg(repo, "--count").output)
238 assert data["count"] == 3
239
240
241 # ---------------------------------------------------------------------------
242 # Integration — --first-parent
243 # ---------------------------------------------------------------------------
244
245
246 class TestFirstParent:
247 def test_first_parent_skips_merge_parent(self, tmp_path: pathlib.Path) -> None:
248 repo = _make_repo(tmp_path)
249 snap_id = _snap(repo)
250 p1 = _commit(repo, snap_id, message="p1")
251 p2 = _commit(repo, snap_id, message="p2")
252 merge = _commit(repo, snap_id, parent=p1, parent2=p2, message="merge")
253 _set_head(repo, "main", merge)
254 data = json.loads(_cg(repo, "--first-parent").output)
255 ids = {c["commit_id"] for c in data["commits"]}
256 assert p2 not in ids
257 assert p1 in ids
258 assert merge in ids
259
260
261 # ---------------------------------------------------------------------------
262 # Integration — --stop-at
263 # ---------------------------------------------------------------------------
264
265
266 class TestStopAt:
267 def test_stop_at_excludes_old_commits(self, tmp_path: pathlib.Path) -> None:
268 repo = _make_repo(tmp_path)
269 snap_id = _snap(repo)
270 c1 = _commit(repo, snap_id, message="c1")
271 c2 = _commit(repo, snap_id, parent=c1, message="c2")
272 c3 = _commit(repo, snap_id, parent=c2, message="c3")
273 _set_head(repo, "main", c3)
274 data = json.loads(_cg(repo, "--stop-at", c2).output)
275 ids = {c["commit_id"] for c in data["commits"]}
276 assert c2 not in ids
277 assert c1 not in ids
278 assert c3 in ids
279
280
281 # ---------------------------------------------------------------------------
282 # Integration — text format
283 # ---------------------------------------------------------------------------
284
285
286 class TestTextFormat:
287 def test_text_one_id_per_line(self, tmp_path: pathlib.Path) -> None:
288 repo = _make_repo(tmp_path)
289 snap_id = _snap(repo)
290 cid = _commit(repo, snap_id)
291 _set_head(repo, "main", cid)
292 result = _cg(repo, "--format", "text")
293 assert result.exit_code == 0
294 assert cid in result.output
295
296
297 # ---------------------------------------------------------------------------
298 # Error cases
299 # ---------------------------------------------------------------------------
300
301
302 class TestErrors:
303 def test_no_commits_errors(self, tmp_path: pathlib.Path) -> None:
304 repo = _make_repo(tmp_path)
305 result = _cg(repo)
306 assert result.exit_code == ExitCode.USER_ERROR
307
308 def test_tip_not_found_errors(self, tmp_path: pathlib.Path) -> None:
309 repo = _make_repo(tmp_path)
310 result = _cg(repo, "--tip", "dead" + "beef" * 15)
311 assert result.exit_code == ExitCode.USER_ERROR
312
313 def test_ancestry_path_without_stop_at_errors(self, tmp_path: pathlib.Path) -> None:
314 repo = _make_repo(tmp_path)
315 snap_id = _snap(repo)
316 cid = _commit(repo, snap_id)
317 _set_head(repo, "main", cid)
318 result = _cg(repo, "--ancestry-path")
319 assert result.exit_code == ExitCode.USER_ERROR
320
321 def test_no_traceback_on_bad_tip(self, tmp_path: pathlib.Path) -> None:
322 repo = _make_repo(tmp_path)
323 result = _cg(repo, "--tip", "bad")
324 assert "Traceback" not in result.output
325
326
327 # ---------------------------------------------------------------------------
328 # Stress
329 # ---------------------------------------------------------------------------
330
331
332 class TestSecurity:
333 def test_format_error_to_stderr(self, tmp_path: pathlib.Path) -> None:
334 repo = _make_repo(tmp_path)
335 r = _cg(repo, "--format", "xml")
336 assert r.exit_code != 0
337 assert r.stdout_bytes == b""
338 assert "error" in r.stderr.lower()
339
340 def test_no_traceback_on_bad_format(self, tmp_path: pathlib.Path) -> None:
341 repo = _make_repo(tmp_path)
342 r = _cg(repo, "--format", "bad")
343 assert "Traceback" not in r.output
344 assert "Traceback" not in r.stderr
345
346 def test_ansi_in_tip_rejected_gracefully(self, tmp_path: pathlib.Path) -> None:
347 """An ANSI-injected tip ID must not crash; it's not a valid commit."""
348 repo = _make_repo(tmp_path)
349 r = _cg(repo, "--tip", "\x1b[31mbad\x1b[0m")
350 assert "Traceback" not in r.output
351 assert "Traceback" not in r.stderr
352
353 def test_json_shorthand_flag(self, tmp_path: pathlib.Path) -> None:
354 repo = _make_repo(tmp_path)
355 snap_id = _snap(repo)
356 cid = _commit(repo, snap_id)
357 _set_head(repo, "main", cid)
358 r = _cg(repo, "--json")
359 assert r.exit_code == 0
360 d = json.loads(r.output)
361 assert "commits" in d
362
363
364 class TestStress:
365 def test_50_commit_linear_chain(self, tmp_path: pathlib.Path) -> None:
366 repo = _make_repo(tmp_path)
367 snap_id = _snap(repo)
368 prev: str | None = None
369 for i in range(50):
370 prev = _commit(repo, snap_id, parent=prev, message=f"commit {i}")
371 assert prev is not None
372 _set_head(repo, "main", prev)
373 data = json.loads(_cg(repo, "--count").output)
374 assert data["count"] == 50
375
376 def test_branching_dag_100_commits(self, tmp_path: pathlib.Path) -> None:
377 """10-branch DAG — --ancestry-path + --first-parent should complete."""
378 repo = _make_repo(tmp_path)
379 snap_id = _snap(repo)
380 base = _commit(repo, snap_id, message="base")
381 tips: list[str] = []
382 for i in range(10):
383 tip = _commit(repo, snap_id, parent=base, message=f"branch {i}")
384 tips.append(tip)
385 merge = _commit(repo, snap_id, parent=tips[-1], parent2=tips[-2], message="merge")
386 _set_head(repo, "main", merge)
387 r = _cg(repo, "--json")
388 assert r.exit_code == 0
389 d = json.loads(r.output)
390 commit_ids = {c["commit_id"] for c in d["commits"]}
391 assert merge in commit_ids
392 assert base in commit_ids
393
394 def test_200_sequential_calls(self, tmp_path: pathlib.Path) -> None:
395 repo = _make_repo(tmp_path)
396 snap_id = _snap(repo)
397 cid = _commit(repo, snap_id)
398 _set_head(repo, "main", cid)
399 for i in range(200):
400 r = _cg(repo)
401 assert r.exit_code == 0, f"failed at {i}"
402
403
404 # ---------------------------------------------------------------------------
405 # Supercharge — duration_ms, exit_code, agent provenance in nodes
406 # ---------------------------------------------------------------------------
407
408 _FULL_TOP_KEYS = frozenset({"tip", "count", "truncated", "commits",
409 "duration_ms", "exit_code"})
410 _FULL_COUNT_KEYS = frozenset({"tip", "count", "truncated",
411 "duration_ms", "exit_code"})
412 _FULL_NODE_KEYS = frozenset({
413 "commit_id", "parent_commit_id", "parent2_commit_id",
414 "message", "branch", "committed_at", "snapshot_id", "author",
415 "agent_id", "model_id", "sem_ver_bump", "breaking_changes",
416 })
417
418
419 def _commit_with_provenance(
420 repo: pathlib.Path,
421 snap_id: str,
422 *,
423 parent: str | None = None,
424 message: str = "test",
425 agent_id: str = "claude-code",
426 model_id: str = "claude-sonnet-4-6",
427 sem_ver_bump: str = "minor",
428 ) -> str:
429 """Write a commit with full agent provenance; return the commit ID."""
430 parent_ids = [parent] if parent else []
431 commit_id = compute_commit_id(parent_ids, snap_id, message, _DT.isoformat())
432 write_commit(repo, CommitRecord(
433 commit_id=commit_id,
434 repo_id="test-repo",
435 branch="main",
436 snapshot_id=snap_id,
437 message=message,
438 committed_at=_DT,
439 parent_commit_id=parent,
440 agent_id=agent_id,
441 model_id=model_id,
442 sem_ver_bump=sem_ver_bump,
443 breaking_changes=[],
444 ))
445 return commit_id
446
447
448 class TestElapsed:
449 """Every JSON output path must include ``duration_ms`` as a float."""
450
451 def test_full_json_has_elapsed(self, tmp_path: pathlib.Path) -> None:
452 repo = _make_repo(tmp_path)
453 snap_id = _snap(repo)
454 cid = _commit(repo, snap_id)
455 _set_head(repo, "main", cid)
456 r = _cg(repo)
457 assert r.exit_code == 0
458 data = json.loads(r.output)
459 assert "duration_ms" in data, "duration_ms missing from full JSON"
460 assert isinstance(data["duration_ms"], float)
461 assert data["duration_ms"] >= 0.0
462
463 def test_count_only_has_elapsed(self, tmp_path: pathlib.Path) -> None:
464 repo = _make_repo(tmp_path)
465 snap_id = _snap(repo)
466 cid = _commit(repo, snap_id)
467 _set_head(repo, "main", cid)
468 r = _cg(repo, "--count")
469 assert r.exit_code == 0
470 data = json.loads(r.output)
471 assert "duration_ms" in data, "duration_ms missing from --count JSON"
472 assert isinstance(data["duration_ms"], float)
473
474 def test_elapsed_is_non_negative(self, tmp_path: pathlib.Path) -> None:
475 repo = _make_repo(tmp_path)
476 snap_id = _snap(repo)
477 cid = _commit(repo, snap_id)
478 _set_head(repo, "main", cid)
479 r = _cg(repo)
480 data = json.loads(r.output)
481 assert data["duration_ms"] >= 0.0
482
483
484 class TestExitCode:
485 """Every JSON output path must include ``exit_code`` mirroring the process exit."""
486
487 def test_full_json_exit_code_0(self, tmp_path: pathlib.Path) -> None:
488 repo = _make_repo(tmp_path)
489 snap_id = _snap(repo)
490 cid = _commit(repo, snap_id)
491 _set_head(repo, "main", cid)
492 r = _cg(repo)
493 assert r.exit_code == 0
494 data = json.loads(r.output)
495 assert data["exit_code"] == 0
496
497 def test_count_only_exit_code_0(self, tmp_path: pathlib.Path) -> None:
498 repo = _make_repo(tmp_path)
499 snap_id = _snap(repo)
500 cid = _commit(repo, snap_id)
501 _set_head(repo, "main", cid)
502 r = _cg(repo, "--count")
503 assert r.exit_code == 0
504 data = json.loads(r.output)
505 assert data["exit_code"] == 0
506
507
508 class TestJsonSchemaComplete:
509 """Full key-set present in both top-level and node objects."""
510
511 def test_top_level_keys_complete(self, tmp_path: pathlib.Path) -> None:
512 repo = _make_repo(tmp_path)
513 snap_id = _snap(repo)
514 cid = _commit(repo, snap_id)
515 _set_head(repo, "main", cid)
516 r = _cg(repo)
517 data = json.loads(r.output)
518 missing = _FULL_TOP_KEYS - data.keys()
519 assert not missing, f"Top-level JSON missing keys: {missing}"
520
521 def test_count_keys_complete(self, tmp_path: pathlib.Path) -> None:
522 repo = _make_repo(tmp_path)
523 snap_id = _snap(repo)
524 cid = _commit(repo, snap_id)
525 _set_head(repo, "main", cid)
526 r = _cg(repo, "--count")
527 data = json.loads(r.output)
528 missing = _FULL_COUNT_KEYS - data.keys()
529 assert not missing, f"--count JSON missing keys: {missing}"
530
531 def test_node_provenance_keys_complete(self, tmp_path: pathlib.Path) -> None:
532 """Each commit node must expose agent_id, model_id, sem_ver_bump, breaking_changes."""
533 repo = _make_repo(tmp_path)
534 snap_id = _snap(repo)
535 cid = _commit_with_provenance(repo, snap_id)
536 _set_head(repo, "main", cid)
537 r = _cg(repo)
538 data = json.loads(r.output)
539 assert data["commits"], "Expected at least one node"
540 node = data["commits"][0]
541 missing = _FULL_NODE_KEYS - node.keys()
542 assert not missing, f"Node missing keys: {missing}"
543
544
545 class TestNodeProvenance:
546 """agent_id, model_id, sem_ver_bump, breaking_changes are surfaced per node."""
547
548 def test_agent_id_in_node(self, tmp_path: pathlib.Path) -> None:
549 repo = _make_repo(tmp_path)
550 snap_id = _snap(repo)
551 cid = _commit_with_provenance(repo, snap_id, agent_id="agentception/worker")
552 _set_head(repo, "main", cid)
553 r = _cg(repo)
554 node = json.loads(r.output)["commits"][0]
555 assert node["agent_id"] == "agentception/worker"
556
557 def test_model_id_in_node(self, tmp_path: pathlib.Path) -> None:
558 repo = _make_repo(tmp_path)
559 snap_id = _snap(repo)
560 cid = _commit_with_provenance(repo, snap_id, model_id="claude-opus-4-6")
561 _set_head(repo, "main", cid)
562 r = _cg(repo)
563 node = json.loads(r.output)["commits"][0]
564 assert node["model_id"] == "claude-opus-4-6"
565
566 def test_sem_ver_bump_in_node(self, tmp_path: pathlib.Path) -> None:
567 repo = _make_repo(tmp_path)
568 snap_id = _snap(repo)
569 cid = _commit_with_provenance(repo, snap_id, sem_ver_bump="major")
570 _set_head(repo, "main", cid)
571 r = _cg(repo)
572 node = json.loads(r.output)["commits"][0]
573 assert node["sem_ver_bump"] == "major"
574
575 def test_breaking_changes_is_list(self, tmp_path: pathlib.Path) -> None:
576 repo = _make_repo(tmp_path)
577 snap_id = _snap(repo)
578 cid = _commit_with_provenance(repo, snap_id)
579 _set_head(repo, "main", cid)
580 r = _cg(repo)
581 node = json.loads(r.output)["commits"][0]
582 assert isinstance(node["breaking_changes"], list)
583
584 def test_human_commit_has_empty_agent_id(self, tmp_path: pathlib.Path) -> None:
585 """A commit without agent provenance must still have the key, value empty string."""
586 repo = _make_repo(tmp_path)
587 snap_id = _snap(repo)
588 cid = _commit(repo, snap_id, message="human commit")
589 _set_head(repo, "main", cid)
590 r = _cg(repo)
591 node = json.loads(r.output)["commits"][0]
592 assert "agent_id" in node
593 assert node["agent_id"] == ""
594
595 def test_chain_preserves_provenance_per_node(self, tmp_path: pathlib.Path) -> None:
596 """Each node in a multi-commit chain retains its own provenance."""
597 repo = _make_repo(tmp_path)
598 snap_id = _snap(repo)
599 c1 = _commit_with_provenance(repo, snap_id, agent_id="bot-a", model_id="m-a")
600 c2 = _commit_with_provenance(repo, snap_id, parent=c1, agent_id="bot-b", model_id="m-b")
601 _set_head(repo, "main", c2)
602 r = _cg(repo)
603 nodes = {n["commit_id"]: n for n in json.loads(r.output)["commits"]}
604 assert nodes[c1]["agent_id"] == "bot-a"
605 assert nodes[c1]["model_id"] == "m-a"
606 assert nodes[c2]["agent_id"] == "bot-b"
607 assert nodes[c2]["model_id"] == "m-b"
608
609
610 class TestDataIntegrity:
611 """Nodes returned by commit-graph must match CommitRecords on disk."""
612
613 def test_commit_id_matches_disk(self, tmp_path: pathlib.Path) -> None:
614 from muse.core.store import read_commit
615 repo = _make_repo(tmp_path)
616 snap_id = _snap(repo)
617 cid = _commit(repo, snap_id)
618 _set_head(repo, "main", cid)
619 r = _cg(repo)
620 node = json.loads(r.output)["commits"][0]
621 record = read_commit(repo, node["commit_id"])
622 assert record is not None
623 assert record.commit_id == node["commit_id"]
624
625 def test_snapshot_id_matches_disk(self, tmp_path: pathlib.Path) -> None:
626 from muse.core.store import read_commit
627 repo = _make_repo(tmp_path)
628 snap_id = _snap(repo)
629 cid = _commit(repo, snap_id)
630 _set_head(repo, "main", cid)
631 r = _cg(repo)
632 node = json.loads(r.output)["commits"][0]
633 record = read_commit(repo, node["commit_id"])
634 assert record is not None
635 assert record.snapshot_id == node["snapshot_id"]
636
637 def test_parent_chain_matches_disk(self, tmp_path: pathlib.Path) -> None:
638 from muse.core.store import read_commit
639 repo = _make_repo(tmp_path)
640 snap_id = _snap(repo)
641 c1 = _commit(repo, snap_id, message="root")
642 c2 = _commit(repo, snap_id, parent=c1, message="child")
643 _set_head(repo, "main", c2)
644 r = _cg(repo)
645 nodes = {n["commit_id"]: n for n in json.loads(r.output)["commits"]}
646 disk_c2 = read_commit(repo, c2)
647 assert disk_c2 is not None
648 assert nodes[c2]["parent_commit_id"] == disk_c2.parent_commit_id
649
650 def test_count_matches_node_list_length(self, tmp_path: pathlib.Path) -> None:
651 repo = _make_repo(tmp_path)
652 snap_id = _snap(repo)
653 c1 = _commit(repo, snap_id)
654 c2 = _commit(repo, snap_id, parent=c1)
655 c3 = _commit(repo, snap_id, parent=c2)
656 _set_head(repo, "main", c3)
657 r = _cg(repo)
658 data = json.loads(r.output)
659 assert data["count"] == len(data["commits"]) == 3
660
661
662 class TestPerformance:
663 """Large graph walks must complete within acceptable time."""
664
665 def test_1000_commit_chain_under_5s(self, tmp_path: pathlib.Path) -> None:
666 import time
667 repo = _make_repo(tmp_path)
668 snap_id = _snap(repo)
669 parent: str | None = None
670 for i in range(1000):
671 parent = _commit(repo, snap_id, parent=parent, message=f"c{i}")
672 _set_head(repo, "main", parent) # type: ignore[arg-type]
673 start = time.monotonic()
674 r = _cg(repo)
675 elapsed = time.monotonic() - start
676 assert r.exit_code == 0
677 data = json.loads(r.output)
678 assert data["count"] == 1000
679 assert elapsed < 5.0, f"1000-commit walk took {elapsed:.2f}s"
680
681 def test_duration_ms_plausible(self, tmp_path: pathlib.Path) -> None:
682 import time
683 repo = _make_repo(tmp_path)
684 snap_id = _snap(repo)
685 parent: str | None = None
686 for i in range(20):
687 parent = _commit(repo, snap_id, parent=parent, message=f"c{i}")
688 _set_head(repo, "main", parent) # type: ignore[arg-type]
689 start = time.monotonic()
690 r = _cg(repo)
691 wall = time.monotonic() - start
692 data = json.loads(r.output)
693 assert data["duration_ms"] <= wall + 0.5
File History 2 commits
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 138 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 141 days ago