gabriel / muse public
test_plumbing_commit_tree.py python
334 lines 12.0 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
1 """Comprehensive tests for ``muse plumbing commit-tree``.
2
3 Coverage tiers
4 --------------
5 - Unit: _FORMAT_CHOICES
6 - Integration: basic creation, --parent chain, merge commit, text format,
7 --agent-id/--model-id/--toolchain-id provenance, --branch override
8 - Security: >2 parents silently rejected, errors to stderr, no traceback
9 - Stress: 200 sequential commit-tree calls
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, read_commit, write_commit, write_snapshot
20 from muse.core._types import Manifest
21 from tests.cli_test_helper import CliRunner, InvokeResult
22
23 runner = CliRunner()
24
25
26 # ---------------------------------------------------------------------------
27 # Helpers
28 # ---------------------------------------------------------------------------
29
30 def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path:
31 repo = tmp_path / "repo"
32 muse = repo / ".muse"
33 for sub in ("objects", "commits", "snapshots", "refs/heads"):
34 (muse / sub).mkdir(parents=True)
35 (muse / "HEAD").write_text("ref: refs/heads/main")
36 (muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo", "domain": "code"}))
37 return repo
38
39
40 def _snap(repo: pathlib.Path) -> str:
41 manifest: Manifest = {}
42 sid = compute_snapshot_id(manifest)
43 write_snapshot(repo, SnapshotRecord(
44 snapshot_id=sid,
45 manifest=manifest,
46 created_at=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
47 ))
48 return sid
49
50
51 def _commit(
52 repo: pathlib.Path,
53 snap_id: str,
54 parent: str | None = None,
55 message: str = "parent",
56 ) -> str:
57 committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
58 parent_ids: list[str] = [parent] if parent else []
59 commit_id = compute_commit_id(parent_ids, snap_id, message, committed_at.isoformat())
60 write_commit(repo, CommitRecord(
61 commit_id=commit_id,
62 repo_id="test-repo",
63 branch="main",
64 snapshot_id=snap_id,
65 message=message,
66 committed_at=committed_at,
67 parent_commit_id=parent,
68 ))
69 return commit_id
70
71
72 def _ct(repo: pathlib.Path, *args: str) -> InvokeResult:
73 from muse.cli.app import main as cli
74 return runner.invoke(
75 cli,
76 ["commit-tree", *args],
77 env={"MUSE_REPO_ROOT": str(repo)},
78 )
79
80
81 # ---------------------------------------------------------------------------
82 # Unit
83 # ---------------------------------------------------------------------------
84
85
86 class TestUnit:
87 def test_format_choices(self) -> None:
88 from muse.cli.commands.plumbing.commit_tree import _FORMAT_CHOICES
89 assert "json" in _FORMAT_CHOICES
90 assert "text" in _FORMAT_CHOICES
91
92
93 # ---------------------------------------------------------------------------
94 # Integration — basic creation
95 # ---------------------------------------------------------------------------
96
97
98 class TestBasicCreation:
99 def test_creates_commit(self, tmp_path: pathlib.Path) -> None:
100 repo = _make_repo(tmp_path)
101 sid = _snap(repo)
102 result = _ct(repo, "--snapshot", sid, "--message", "first commit")
103 assert result.exit_code == 0
104 data = json.loads(result.output)
105 assert "commit_id" in data
106 assert len(data["commit_id"]) == 64
107
108 def test_commit_persisted_in_store(self, tmp_path: pathlib.Path) -> None:
109 repo = _make_repo(tmp_path)
110 sid = _snap(repo)
111 data = json.loads(_ct(repo, "--snapshot", sid).output)
112 cid = data["commit_id"]
113 rec = read_commit(repo, cid)
114 assert rec is not None
115 assert rec.snapshot_id == sid
116
117 def test_json_flag_shorthand(self, tmp_path: pathlib.Path) -> None:
118 repo = _make_repo(tmp_path)
119 sid = _snap(repo)
120 result = _ct(repo, "--json", "--snapshot", sid)
121 assert result.exit_code == 0
122 assert "commit_id" in json.loads(result.output)
123
124 def test_text_format_bare_commit_id(self, tmp_path: pathlib.Path) -> None:
125 repo = _make_repo(tmp_path)
126 sid = _snap(repo)
127 result = _ct(repo, "--format", "text", "--snapshot", sid)
128 assert result.exit_code == 0
129 line = result.output.strip()
130 assert len(line) == 64
131
132 def test_message_stored(self, tmp_path: pathlib.Path) -> None:
133 repo = _make_repo(tmp_path)
134 sid = _snap(repo)
135 data = json.loads(_ct(repo, "--snapshot", sid, "--message", "my msg").output)
136 rec = read_commit(repo, data["commit_id"])
137 assert rec is not None
138 assert rec.message == "my msg"
139
140 def test_author_stored(self, tmp_path: pathlib.Path) -> None:
141 repo = _make_repo(tmp_path)
142 sid = _snap(repo)
143 data = json.loads(_ct(repo, "--snapshot", sid, "--author", "gabriel").output)
144 rec = read_commit(repo, data["commit_id"])
145 assert rec is not None
146 assert rec.author == "gabriel"
147
148
149 # ---------------------------------------------------------------------------
150 # Integration — parent chain
151 # ---------------------------------------------------------------------------
152
153
154 class TestParentChain:
155 def test_single_parent_stored(self, tmp_path: pathlib.Path) -> None:
156 repo = _make_repo(tmp_path)
157 sid = _snap(repo)
158 p1_id = _commit(repo, sid)
159 data = json.loads(_ct(repo, "--snapshot", sid, "--parent", p1_id).output)
160 rec = read_commit(repo, data["commit_id"])
161 assert rec is not None
162 assert rec.parent_commit_id == p1_id
163 assert rec.parent2_commit_id is None
164
165 def test_merge_commit_two_parents(self, tmp_path: pathlib.Path) -> None:
166 repo = _make_repo(tmp_path)
167 sid = _snap(repo)
168 p1 = _commit(repo, sid, message="parent1")
169 p2 = _commit(repo, sid, message="parent2")
170 data = json.loads(
171 _ct(repo, "--snapshot", sid, "--parent", p1, "--parent", p2).output
172 )
173 rec = read_commit(repo, data["commit_id"])
174 assert rec is not None
175 assert rec.parent_commit_id == p1
176 assert rec.parent2_commit_id == p2
177
178 def test_three_parents_rejected(self, tmp_path: pathlib.Path) -> None:
179 repo = _make_repo(tmp_path)
180 sid = _snap(repo)
181 p = _commit(repo, sid)
182 result = _ct(
183 repo, "--snapshot", sid,
184 "--parent", p, "--parent", p, "--parent", p,
185 )
186 assert result.exit_code == ExitCode.USER_ERROR
187
188 def test_missing_parent_errors(self, tmp_path: pathlib.Path) -> None:
189 repo = _make_repo(tmp_path)
190 sid = _snap(repo)
191 result = _ct(repo, "--snapshot", sid, "--parent", "dead" + "beef" * 15)
192 assert result.exit_code == ExitCode.USER_ERROR
193
194
195 # ---------------------------------------------------------------------------
196 # Integration — agent provenance flags
197 # ---------------------------------------------------------------------------
198
199
200 class TestAgentProvenance:
201 def test_agent_id_stored(self, tmp_path: pathlib.Path) -> None:
202 repo = _make_repo(tmp_path)
203 sid = _snap(repo)
204 data = json.loads(
205 _ct(repo, "--snapshot", sid, "--agent-id", "my-bot").output
206 )
207 rec = read_commit(repo, data["commit_id"])
208 assert rec is not None
209 assert rec.agent_id == "my-bot"
210
211 def test_model_id_stored(self, tmp_path: pathlib.Path) -> None:
212 repo = _make_repo(tmp_path)
213 sid = _snap(repo)
214 data = json.loads(
215 _ct(repo, "--snapshot", sid, "--model-id", "claude-opus-4").output
216 )
217 rec = read_commit(repo, data["commit_id"])
218 assert rec is not None
219 assert rec.model_id == "claude-opus-4"
220
221 def test_toolchain_id_stored(self, tmp_path: pathlib.Path) -> None:
222 repo = _make_repo(tmp_path)
223 sid = _snap(repo)
224 data = json.loads(
225 _ct(repo, "--snapshot", sid, "--toolchain-id", "cursor-agent-v2").output
226 )
227 rec = read_commit(repo, data["commit_id"])
228 assert rec is not None
229 assert rec.toolchain_id == "cursor-agent-v2"
230
231 def test_full_provenance_round_trip(self, tmp_path: pathlib.Path) -> None:
232 repo = _make_repo(tmp_path)
233 sid = _snap(repo)
234 data = json.loads(_ct(
235 repo,
236 "--snapshot", sid,
237 "--agent-id", "audit-bot",
238 "--model-id", "claude-4",
239 "--toolchain-id", "muse-agent-v1",
240 ).output)
241 rec = read_commit(repo, data["commit_id"])
242 assert rec is not None
243 assert rec.agent_id == "audit-bot"
244 assert rec.model_id == "claude-4"
245 assert rec.toolchain_id == "muse-agent-v1"
246
247 def test_defaults_to_empty_strings(self, tmp_path: pathlib.Path) -> None:
248 repo = _make_repo(tmp_path)
249 sid = _snap(repo)
250 data = json.loads(_ct(repo, "--snapshot", sid).output)
251 rec = read_commit(repo, data["commit_id"])
252 assert rec is not None
253 assert rec.agent_id == ""
254 assert rec.model_id == ""
255 assert rec.toolchain_id == ""
256
257
258 # ---------------------------------------------------------------------------
259 # Integration — --branch override
260 # ---------------------------------------------------------------------------
261
262
263 class TestBranchOverride:
264 def test_branch_override_stored(self, tmp_path: pathlib.Path) -> None:
265 repo = _make_repo(tmp_path)
266 sid = _snap(repo)
267 data = json.loads(_ct(repo, "--snapshot", sid, "--branch", "feat/x").output)
268 rec = read_commit(repo, data["commit_id"])
269 assert rec is not None
270 assert rec.branch == "feat/x"
271
272
273 # ---------------------------------------------------------------------------
274 # Error cases
275 # ---------------------------------------------------------------------------
276
277
278 class TestErrors:
279 def test_missing_snapshot_errors(self, tmp_path: pathlib.Path) -> None:
280 repo = _make_repo(tmp_path)
281 result = _ct(repo, "--snapshot", "dead" + "beef" * 15)
282 assert result.exit_code == ExitCode.USER_ERROR
283
284 def test_invalid_snapshot_id_errors(self, tmp_path: pathlib.Path) -> None:
285 repo = _make_repo(tmp_path)
286 result = _ct(repo, "--snapshot", "not-hex")
287 assert result.exit_code == ExitCode.USER_ERROR
288
289 def test_invalid_parent_id_errors(self, tmp_path: pathlib.Path) -> None:
290 repo = _make_repo(tmp_path)
291 sid = _snap(repo)
292 result = _ct(repo, "--snapshot", sid, "--parent", "bad-hex")
293 assert result.exit_code == ExitCode.USER_ERROR
294
295 def test_unknown_format_errors(self, tmp_path: pathlib.Path) -> None:
296 repo = _make_repo(tmp_path)
297 sid = _snap(repo)
298 result = _ct(repo, "--snapshot", sid, "--format", "msgpack")
299 assert result.exit_code == ExitCode.USER_ERROR
300
301
302 # ---------------------------------------------------------------------------
303 # Security
304 # ---------------------------------------------------------------------------
305
306
307 class TestSecurity:
308 def test_no_traceback_on_bad_snapshot(self, tmp_path: pathlib.Path) -> None:
309 repo = _make_repo(tmp_path)
310 result = _ct(repo, "--snapshot", "bad")
311 assert "Traceback" not in result.output
312
313 def test_no_traceback_on_too_many_parents(self, tmp_path: pathlib.Path) -> None:
314 repo = _make_repo(tmp_path)
315 sid = _snap(repo)
316 p = _commit(repo, sid)
317 result = _ct(repo, "--snapshot", sid, "--parent", p, "--parent", p, "--parent", p)
318 assert "Traceback" not in result.output
319
320
321 # ---------------------------------------------------------------------------
322 # Stress
323 # ---------------------------------------------------------------------------
324
325
326 class TestStress:
327 def test_200_sequential_commits(self, tmp_path: pathlib.Path) -> None:
328 repo = _make_repo(tmp_path)
329 sid = _snap(repo)
330 for i in range(200):
331 result = _ct(repo, "--snapshot", sid, "--message", f"commit {i}")
332 assert result.exit_code == 0, f"failed at iteration {i}"
333 data = json.loads(result.output)
334 assert len(data["commit_id"]) == 64
File History 3 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 26 days ago
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa feat: Muse — version control for the agent era Human 100 days ago