gabriel / muse public
test_plumbing_rev_parse.py python
325 lines 12.1 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 rev-parse``.
2
3 Coverage tiers
4 --------------
5 - Integration: branch, HEAD, SHA prefix, full SHA, --abbrev-ref, --format text
6 - Edge cases: empty repo (no commits), empty ref, ambiguous prefix, HEAD→branch
7 - Security: ANSI/control chars in ref → JSON-escaped, empty ref clean error
8 - Stress: 200 rapid resolves
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.object_store import write_object
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 muse.core._types import Manifest
21 from tests.cli_test_helper import CliRunner, InvokeResult
22
23 runner = CliRunner()
24
25 # ---------------------------------------------------------------------------
26 # Helpers
27 # ---------------------------------------------------------------------------
28
29 def _make_repo(tmp_path: pathlib.Path, branch: str = "main") -> pathlib.Path:
30 repo = tmp_path / "repo"
31 muse = repo / ".muse"
32 for sub in ("objects", "commits", "snapshots", "refs/heads"):
33 (muse / sub).mkdir(parents=True)
34 (muse / "HEAD").write_text(f"ref: refs/heads/{branch}")
35 (muse / "repo.json").write_text(json.dumps({"repo_id": "test", "domain": "code"}))
36 return repo
37
38
39 _TS = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
40
41
42 def _store_snap(repo: pathlib.Path, manifest: Manifest | None = None) -> str:
43 sid = compute_snapshot_id(manifest or {})
44 write_snapshot(repo, SnapshotRecord(
45 snapshot_id=sid,
46 manifest=manifest or {},
47 created_at=_TS,
48 ))
49 return sid
50
51
52 def _make_commit(
53 repo: pathlib.Path,
54 snapshot_id: str,
55 *,
56 branch: str = "main",
57 parent: str | None = None,
58 message: str = "test",
59 ) -> str:
60 parents = [parent] if parent else []
61 cid = compute_commit_id(parents, snapshot_id, message, _TS.isoformat())
62 rec = CommitRecord(
63 commit_id=cid,
64 repo_id="test-repo-id",
65 branch=branch,
66 snapshot_id=snapshot_id,
67 message=message,
68 committed_at=_TS,
69 author="tester",
70 parent_commit_id=parent,
71 )
72 write_commit(repo, rec)
73 return cid
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
81
82 def _rev(repo: pathlib.Path, *args: str) -> InvokeResult:
83 from muse.cli.app import main as cli
84 return runner.invoke(
85 cli,
86 ["rev-parse", *args],
87 env={"MUSE_REPO_ROOT": str(repo)},
88 )
89
90
91 def _populated_repo(tmp_path: pathlib.Path) -> tuple[pathlib.Path, str]:
92 """Return (repo, commit_id) with one commit on main, using real content-addressed IDs."""
93 repo = _make_repo(tmp_path)
94 sid = _store_snap(repo)
95 cid = _make_commit(repo, sid)
96 _set_head(repo, "main", cid)
97 return repo, cid
98
99
100 # ---------------------------------------------------------------------------
101 # Integration — branch resolution
102 # ---------------------------------------------------------------------------
103
104
105 class TestBranchResolution:
106 def test_resolve_branch_json(self, tmp_path: pathlib.Path) -> None:
107 repo, cid = _populated_repo(tmp_path)
108 result = _rev(repo, "main")
109 assert result.exit_code == 0
110 data = json.loads(result.output)
111 assert data["commit_id"] == cid
112 assert data["ref"] == "main"
113
114 def test_resolve_branch_text(self, tmp_path: pathlib.Path) -> None:
115 repo, cid = _populated_repo(tmp_path)
116 result = _rev(repo, "--format", "text", "main")
117 assert result.exit_code == 0
118 assert result.output.strip() == cid
119
120 def test_json_flag_shorthand(self, tmp_path: pathlib.Path) -> None:
121 repo, cid = _populated_repo(tmp_path)
122 result = _rev(repo, "--json", "main")
123 assert result.exit_code == 0
124 data = json.loads(result.output)
125 assert data["commit_id"] == cid
126
127 def test_unknown_branch_not_found(self, tmp_path: pathlib.Path) -> None:
128 repo = _make_repo(tmp_path)
129 result = _rev(repo, "nonexistent-branch")
130 assert result.exit_code == ExitCode.USER_ERROR
131 data = json.loads(result.output)
132 assert data["commit_id"] is None
133 assert data["error"] == "not found"
134
135
136 # ---------------------------------------------------------------------------
137 # Integration — HEAD resolution
138 # ---------------------------------------------------------------------------
139
140
141 class TestHeadResolution:
142 def test_resolve_head(self, tmp_path: pathlib.Path) -> None:
143 repo, cid = _populated_repo(tmp_path)
144 result = _rev(repo, "HEAD")
145 assert result.exit_code == 0
146 data = json.loads(result.output)
147 assert data["commit_id"] == cid
148
149 def test_head_lowercase_also_resolves(self, tmp_path: pathlib.Path) -> None:
150 """HEAD resolution is case-insensitive (matches git behaviour)."""
151 repo, cid = _populated_repo(tmp_path)
152 result = _rev(repo, "head")
153 assert result.exit_code == 0
154 data = json.loads(result.output)
155 assert data["commit_id"] == cid
156
157 def test_head_on_empty_repo_errors(self, tmp_path: pathlib.Path) -> None:
158 """HEAD on a repo with no commits should error cleanly."""
159 repo = _make_repo(tmp_path)
160 result = _rev(repo, "HEAD")
161 assert result.exit_code == ExitCode.USER_ERROR
162 data = json.loads(result.output)
163 assert data["commit_id"] is None
164 assert "no commits" in data["error"]
165
166
167 # ---------------------------------------------------------------------------
168 # Integration — SHA prefix resolution
169 # ---------------------------------------------------------------------------
170
171
172 class TestShaResolution:
173 def test_resolve_full_sha(self, tmp_path: pathlib.Path) -> None:
174 repo, cid = _populated_repo(tmp_path)
175 result = _rev(repo, cid)
176 assert result.exit_code == 0
177 data = json.loads(result.output)
178 assert data["commit_id"] == cid
179
180 def test_resolve_8char_prefix(self, tmp_path: pathlib.Path) -> None:
181 repo, cid = _populated_repo(tmp_path)
182 result = _rev(repo, cid[:8])
183 assert result.exit_code == 0
184 data = json.loads(result.output)
185 assert data["commit_id"] == cid
186
187 def test_ambiguous_prefix_returns_candidates(self, tmp_path: pathlib.Path) -> None:
188 """Two commits sharing a prefix → error with candidates list."""
189 # Messages "commit-search-165" and "commit-search-106" produce IDs
190 # sharing the 4-char prefix "9f7c" (same snapshot, same timestamp).
191 _AMBIG_MSG_1 = "commit-search-165"
192 _AMBIG_MSG_2 = "commit-search-106"
193 _AMBIG_PREFIX = "9f7c"
194
195 repo = _make_repo(tmp_path)
196 sid = _store_snap(repo)
197 cid1 = _make_commit(repo, sid, branch="main", message=_AMBIG_MSG_1)
198 cid2 = _make_commit(repo, sid, branch="dev", message=_AMBIG_MSG_2)
199 assert cid1[:4] == cid2[:4] == _AMBIG_PREFIX
200 _set_head(repo, "main", cid1)
201 _set_head(repo, "dev", cid2)
202
203 result = _rev(repo, _AMBIG_PREFIX)
204 assert result.exit_code == ExitCode.USER_ERROR
205 data = json.loads(result.output)
206 assert data["error"] == "ambiguous"
207 assert set(data["candidates"]) == {cid1, cid2}
208
209 def test_nonexistent_full_sha_not_found(self, tmp_path: pathlib.Path) -> None:
210 repo = _make_repo(tmp_path)
211 result = _rev(repo, "f" * 64)
212 assert result.exit_code == ExitCode.USER_ERROR
213 data = json.loads(result.output)
214 assert data["error"] == "not found"
215
216
217 # ---------------------------------------------------------------------------
218 # Integration — --abbrev-ref
219 # ---------------------------------------------------------------------------
220
221
222 class TestAbbrevRef:
223 def test_abbrev_ref_head_returns_branch_name(self, tmp_path: pathlib.Path) -> None:
224 """The canonical agent UX: what branch am I on?"""
225 repo = _make_repo(tmp_path, branch="feat/my-feature")
226 result = _rev(repo, "--abbrev-ref", "HEAD")
227 assert result.exit_code == 0
228 data = json.loads(result.output)
229 assert data["branch"] == "feat/my-feature"
230 assert data["ref"] == "HEAD"
231
232 def test_abbrev_ref_text_format(self, tmp_path: pathlib.Path) -> None:
233 repo = _make_repo(tmp_path, branch="dev")
234 result = _rev(repo, "--abbrev-ref", "--format", "text", "HEAD")
235 assert result.exit_code == 0
236 assert result.output.strip() == "dev"
237
238 def test_abbrev_ref_main(self, tmp_path: pathlib.Path) -> None:
239 repo = _make_repo(tmp_path, branch="main")
240 result = _rev(repo, "--abbrev-ref", "HEAD")
241 assert result.exit_code == 0
242 assert json.loads(result.output)["branch"] == "main"
243
244
245 # ---------------------------------------------------------------------------
246 # Edge cases
247 # ---------------------------------------------------------------------------
248
249
250 class TestEdgeCases:
251 def test_empty_ref_clean_error(self, tmp_path: pathlib.Path) -> None:
252 """Empty string ref must give a clear 'ref must not be empty' error."""
253 repo = _make_repo(tmp_path)
254 result = _rev(repo, "")
255 assert result.exit_code == ExitCode.USER_ERROR
256 data = json.loads(result.output)
257 assert "empty" in data["error"]
258
259 def test_invalid_format_errors(self, tmp_path: pathlib.Path) -> None:
260 repo, _ = _populated_repo(tmp_path)
261 result = _rev(repo, "--format", "xml", "main")
262 assert result.exit_code == ExitCode.USER_ERROR
263
264 def test_branch_with_slash_resolves(self, tmp_path: pathlib.Path) -> None:
265 repo = _make_repo(tmp_path, branch="feat/my-feature")
266 sid = _store_snap(repo)
267 cid = _make_commit(repo, sid, branch="feat/my-feature", message="feat-init")
268 _set_head(repo, "feat/my-feature", cid)
269 result = _rev(repo, "feat/my-feature")
270 assert result.exit_code == 0
271 assert json.loads(result.output)["commit_id"] == cid
272
273
274 # ---------------------------------------------------------------------------
275 # Security
276 # ---------------------------------------------------------------------------
277
278
279 class TestSecurity:
280 def test_ansi_in_ref_is_json_escaped(self, tmp_path: pathlib.Path) -> None:
281 """ANSI escape in ref is safely JSON-encoded, never echoed raw."""
282 repo = _make_repo(tmp_path)
283 evil = "\x1b[31mevil\x1b[0m"
284 result = _rev(repo, evil)
285 assert result.exit_code == ExitCode.USER_ERROR
286 # Output is JSON — ANSI must be encoded as \u001b, not emitted raw
287 assert "\x1b" not in result.output
288 data = json.loads(result.output)
289 assert data["error"] == "not found"
290
291 def test_path_traversal_ref_gives_not_found(self, tmp_path: pathlib.Path) -> None:
292 repo = _make_repo(tmp_path)
293 result = _rev(repo, "../../../etc/passwd")
294 assert result.exit_code == ExitCode.USER_ERROR
295
296 def test_null_byte_in_ref(self, tmp_path: pathlib.Path) -> None:
297 repo = _make_repo(tmp_path)
298 result = _rev(repo, "branch\x00null")
299 assert result.exit_code == ExitCode.USER_ERROR
300
301 def test_no_traceback_on_bad_input(self, tmp_path: pathlib.Path) -> None:
302 repo = _make_repo(tmp_path)
303 result = _rev(repo, "")
304 assert "Traceback" not in result.output
305
306
307 # ---------------------------------------------------------------------------
308 # Stress
309 # ---------------------------------------------------------------------------
310
311
312 class TestStress:
313 def test_200_rapid_resolves(self, tmp_path: pathlib.Path) -> None:
314 repo, cid = _populated_repo(tmp_path)
315 for i in range(200):
316 result = _rev(repo, "main")
317 assert result.exit_code == 0, f"failed at iteration {i}"
318 assert json.loads(result.output)["commit_id"] == cid
319
320 def test_200_abbrev_ref_resolves(self, tmp_path: pathlib.Path) -> None:
321 repo = _make_repo(tmp_path, branch="dev")
322 for i in range(200):
323 result = _rev(repo, "--abbrev-ref", "HEAD")
324 assert result.exit_code == 0, f"failed at iteration {i}"
325 assert json.loads(result.output)["branch"] == "dev"
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