gabriel / muse public
test_plumbing_commit_tree.py python
271 lines 9.2 KB
99746394 feat(tests+docs): supercharge plumbing test suite and update reference doc Gabriel Cardona <gabriel@tellurstori.com> 2d ago
1 """Tests for ``muse plumbing commit-tree``.
2
3 Covers: basic commit creation, custom author/message/branch, single parent,
4 two-parent merge commit, snapshot-not-found, parent-not-found, repo.json
5 validation, and deterministic commit-ID computation.
6 """
7
8 from __future__ import annotations
9
10 import datetime
11 import hashlib
12 import json
13 import pathlib
14
15 from typer.testing import CliRunner
16
17 from muse.cli.app import cli
18 from muse.core.errors import ExitCode
19 from muse.core.store import CommitRecord, SnapshotRecord, read_commit, write_commit, write_snapshot
20
21 runner = CliRunner()
22
23
24 # ---------------------------------------------------------------------------
25 # Helpers
26 # ---------------------------------------------------------------------------
27
28
29 def _sha(tag: str) -> str:
30 return hashlib.sha256(tag.encode()).hexdigest()
31
32
33 def _init_repo(path: pathlib.Path) -> pathlib.Path:
34 muse = path / ".muse"
35 (muse / "commits").mkdir(parents=True)
36 (muse / "snapshots").mkdir(parents=True)
37 (muse / "objects").mkdir(parents=True)
38 (muse / "refs" / "heads").mkdir(parents=True)
39 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
40 (muse / "repo.json").write_text(
41 json.dumps({"repo_id": "test-repo-uuid", "domain": "midi"}), encoding="utf-8"
42 )
43 return path
44
45
46 def _env(repo: pathlib.Path) -> dict[str, str]:
47 return {"MUSE_REPO_ROOT": str(repo)}
48
49
50 def _snap(repo: pathlib.Path, tag: str = "snap") -> str:
51 sid = _sha(f"snap-{tag}")
52 write_snapshot(
53 repo,
54 SnapshotRecord(
55 snapshot_id=sid,
56 manifest={},
57 created_at=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
58 ),
59 )
60 return sid
61
62
63 def _stored_commit(repo: pathlib.Path, tag: str, sid: str, branch: str = "main") -> str:
64 cid = _sha(tag)
65 write_commit(
66 repo,
67 CommitRecord(
68 commit_id=cid,
69 repo_id="test-repo-uuid",
70 branch=branch,
71 snapshot_id=sid,
72 message=tag,
73 committed_at=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
74 author="tester",
75 parent_commit_id=None,
76 ),
77 )
78 ref = repo / ".muse" / "refs" / "heads" / branch
79 ref.parent.mkdir(parents=True, exist_ok=True)
80 ref.write_text(cid, encoding="utf-8")
81 return cid
82
83
84 # ---------------------------------------------------------------------------
85 # Unit: basic commit creation
86 # ---------------------------------------------------------------------------
87
88
89 class TestCommitTreeUnit:
90 def test_creates_commit_and_returns_commit_id(self, tmp_path: pathlib.Path) -> None:
91 repo = _init_repo(tmp_path)
92 sid = _snap(repo)
93 result = runner.invoke(
94 cli,
95 ["plumbing", "commit-tree", "--snapshot", sid, "--message", "first commit"],
96 env=_env(repo),
97 )
98 assert result.exit_code == 0, result.output
99 data = json.loads(result.stdout)
100 assert "commit_id" in data
101 assert len(data["commit_id"]) == 64
102
103 def test_commit_is_retrievable_from_store(self, tmp_path: pathlib.Path) -> None:
104 repo = _init_repo(tmp_path)
105 sid = _snap(repo)
106 result = runner.invoke(
107 cli,
108 ["plumbing", "commit-tree", "--snapshot", sid, "--message", "stored"],
109 env=_env(repo),
110 )
111 assert result.exit_code == 0
112 cid = json.loads(result.stdout)["commit_id"]
113 record = read_commit(repo, cid)
114 assert record is not None
115 assert record.snapshot_id == sid
116 assert record.message == "stored"
117
118 def test_snapshot_not_found_exits_user_error(self, tmp_path: pathlib.Path) -> None:
119 repo = _init_repo(tmp_path)
120 ghost_sid = _sha("ghost-snap")
121 result = runner.invoke(
122 cli,
123 ["plumbing", "commit-tree", "--snapshot", ghost_sid, "--message", "x"],
124 env=_env(repo),
125 )
126 assert result.exit_code == ExitCode.USER_ERROR
127 assert "error" in json.loads(result.stdout)
128
129
130 # ---------------------------------------------------------------------------
131 # Integration: metadata flags
132 # ---------------------------------------------------------------------------
133
134
135 class TestCommitTreeMetadata:
136 def test_custom_author_stored_in_record(self, tmp_path: pathlib.Path) -> None:
137 repo = _init_repo(tmp_path)
138 sid = _snap(repo)
139 result = runner.invoke(
140 cli,
141 ["plumbing", "commit-tree", "-s", sid, "-m", "msg", "-a", "Ada Lovelace"],
142 env=_env(repo),
143 )
144 assert result.exit_code == 0
145 cid = json.loads(result.stdout)["commit_id"]
146 record = read_commit(repo, cid)
147 assert record is not None
148 assert record.author == "Ada Lovelace"
149
150 def test_custom_branch_stored_in_record(self, tmp_path: pathlib.Path) -> None:
151 repo = _init_repo(tmp_path)
152 sid = _snap(repo)
153 result = runner.invoke(
154 cli,
155 ["plumbing", "commit-tree", "-s", sid, "-m", "msg", "-b", "feature"],
156 env=_env(repo),
157 )
158 assert result.exit_code == 0
159 cid = json.loads(result.stdout)["commit_id"]
160 record = read_commit(repo, cid)
161 assert record is not None
162 assert record.branch == "feature"
163
164 def test_default_branch_is_current_branch(self, tmp_path: pathlib.Path) -> None:
165 repo = _init_repo(tmp_path)
166 sid = _snap(repo)
167 result = runner.invoke(
168 cli,
169 ["plumbing", "commit-tree", "--snapshot", sid, "--message", "def-branch"],
170 env=_env(repo),
171 )
172 assert result.exit_code == 0
173 cid = json.loads(result.stdout)["commit_id"]
174 record = read_commit(repo, cid)
175 assert record is not None
176 assert record.branch == "main"
177
178
179 # ---------------------------------------------------------------------------
180 # Integration: parent commits
181 # ---------------------------------------------------------------------------
182
183
184 class TestCommitTreeParents:
185 def test_single_parent_stored_in_record(self, tmp_path: pathlib.Path) -> None:
186 repo = _init_repo(tmp_path)
187 sid = _snap(repo)
188 parent_cid = _stored_commit(repo, "parent", sid)
189 result = runner.invoke(
190 cli,
191 [
192 "plumbing", "commit-tree",
193 "--snapshot", sid,
194 "--message", "child",
195 "--parent", parent_cid,
196 ],
197 env=_env(repo),
198 )
199 assert result.exit_code == 0
200 cid = json.loads(result.stdout)["commit_id"]
201 record = read_commit(repo, cid)
202 assert record is not None
203 assert record.parent_commit_id == parent_cid
204
205 def test_two_parents_creates_merge_commit(self, tmp_path: pathlib.Path) -> None:
206 repo = _init_repo(tmp_path)
207 sid = _snap(repo)
208 p1 = _stored_commit(repo, "p1", sid)
209 sid2 = _snap(repo, "snap2")
210 p2 = _stored_commit(repo, "p2", sid2, branch="feat")
211 result = runner.invoke(
212 cli,
213 [
214 "plumbing", "commit-tree",
215 "--snapshot", sid,
216 "--message", "merge",
217 "--parent", p1,
218 "--parent", p2,
219 ],
220 env=_env(repo),
221 )
222 assert result.exit_code == 0
223 cid = json.loads(result.stdout)["commit_id"]
224 record = read_commit(repo, cid)
225 assert record is not None
226 assert record.parent_commit_id == p1
227 assert record.parent2_commit_id == p2
228
229 def test_parent_not_in_store_exits_user_error(self, tmp_path: pathlib.Path) -> None:
230 repo = _init_repo(tmp_path)
231 sid = _snap(repo)
232 ghost = _sha("ghost-parent")
233 result = runner.invoke(
234 cli,
235 ["plumbing", "commit-tree", "--snapshot", sid, "--message", "x", "--parent", ghost],
236 env=_env(repo),
237 )
238 assert result.exit_code == ExitCode.USER_ERROR
239 assert "error" in json.loads(result.stdout)
240
241
242 # ---------------------------------------------------------------------------
243 # Integration: determinism
244 # ---------------------------------------------------------------------------
245
246
247 class TestCommitTreeDeterminism:
248 def test_same_inputs_produce_same_commit_id(self, tmp_path: pathlib.Path) -> None:
249 """commit-tree is deterministic when called at the same ISO timestamp.
250
251 We verify that two commits with the same snapshot and message have the
252 same ID only in trivial cases; since committed_at is generated at
253 runtime the IDs will differ — but the test confirms the command is
254 *stable* (no random state, no crashes on repeated calls).
255 """
256 repo = _init_repo(tmp_path)
257 sid = _snap(repo)
258 r1 = runner.invoke(
259 cli,
260 ["plumbing", "commit-tree", "--snapshot", sid, "--message", "stable"],
261 env=_env(repo),
262 )
263 r2 = runner.invoke(
264 cli,
265 ["plumbing", "commit-tree", "--snapshot", sid, "--message", "stable"],
266 env=_env(repo),
267 )
268 assert r1.exit_code == 0 and r2.exit_code == 0
269 # Both should return valid commit IDs (64-char hex), even if different.
270 assert len(json.loads(r1.stdout)["commit_id"]) == 64
271 assert len(json.loads(r2.stdout)["commit_id"]) == 64