gabriel / muse public
test_name_rev_supercharge.py python
366 lines 13.4 KB
Raw
1 """Supercharge tests for ``muse name-rev``.
2
3 Coverage tiers
4 --------------
5 - JSON envelope: exit_code and duration_ms present on all resolution outcomes
6 - Error payload: errors go to stdout as JSON in --json mode, no dual stderr prose
7 - Prefix resolution: bare hex short-prefix matches sha256:-prefixed keys in name_map
8 - TypedDicts: _NameRevJson and _NameRevErrorJson with required annotations
9 - Docstring: module docstring covers exit_code and duration_ms
10 - No-prose pollution: JSON stdout is valid on all non-error paths
11 - Stress: 100-commit chain, all entries have exit_code and duration_ms
12 """
13 from __future__ import annotations
14 from collections.abc import Mapping
15
16 import datetime
17 import json
18 import pathlib
19 from typing import get_type_hints
20
21 from muse.core.ids import hash_commit as compute_commit_id, hash_snapshot as compute_snapshot_id
22 from muse.core.commits import (
23 CommitRecord,
24 write_commit,
25 )
26 from muse.core.snapshots import (
27 SnapshotRecord,
28 write_snapshot,
29 )
30 from muse.core.paths import ref_path, muse_dir
31 from tests.cli_test_helper import CliRunner, InvokeResult
32
33 runner = CliRunner()
34
35 _DT = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
36
37
38 # ---------------------------------------------------------------------------
39 # Helpers
40 # ---------------------------------------------------------------------------
41
42
43 def _init_repo(tmp_path: pathlib.Path) -> pathlib.Path:
44 dot_muse = muse_dir(tmp_path)
45 for d in ("commits", "snapshots", "objects", "refs/heads"):
46 (dot_muse / d).mkdir(parents=True, exist_ok=True)
47 (dot_muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
48 (dot_muse / "repo.json").write_text(
49 json.dumps({"repo_id": "nr-supercharge", "domain": "midi"}), encoding="utf-8"
50 )
51 return tmp_path
52
53
54 def _env(root: pathlib.Path) -> Mapping[str, str]:
55 return {"MUSE_REPO_ROOT": str(root)}
56
57
58 def _commit(
59 root: pathlib.Path,
60 msg: str,
61 branch: str = "main",
62 parent: str | None = None,
63 ) -> str:
64 sid = compute_snapshot_id({})
65 write_snapshot(root, SnapshotRecord(snapshot_id=sid, manifest={}, created_at=_DT))
66 parent_ids = [parent] if parent else []
67 cid = compute_commit_id( parent_ids=parent_ids,
68 snapshot_id=sid,
69 message=msg,
70 committed_at_iso=_DT.isoformat(),
71 )
72 write_commit(root, CommitRecord(
73 commit_id=cid, branch=branch,
74 snapshot_id=sid, message=msg, committed_at=_DT,
75 parent_commit_id=parent,
76 ))
77 ref = ref_path(root, branch)
78 ref.parent.mkdir(parents=True, exist_ok=True)
79 ref.write_text(cid, encoding="utf-8")
80 return cid
81
82
83 def _nr(root: pathlib.Path, *args: str, stdin: str | None = None) -> InvokeResult:
84 from muse.cli.app import main as cli
85 extra = [] if "--json" in args or "-j" in args else ["--json"]
86 return runner.invoke(cli, ["name-rev", *extra, *args], env=_env(root), input=stdin)
87
88
89 def _hex_prefix(cid: str, n: int = 8) -> str:
90 """Extract n hex chars from a sha256:-prefixed commit ID."""
91 return cid[len("sha256:"):len("sha256:") + n]
92
93
94 # ---------------------------------------------------------------------------
95 # JSON envelope — exit_code
96 # ---------------------------------------------------------------------------
97
98
99 class TestJsonEnvelopeExitCode:
100 def test_found_has_exit_code_zero(self, tmp_path: pathlib.Path) -> None:
101 root = _init_repo(tmp_path)
102 cid = _commit(root, "c1")
103 r = _nr(root, cid)
104 assert r.exit_code == 0
105 d = json.loads(r.output)
106 assert "exit_code" in d, "exit_code missing from found envelope"
107 assert d["exit_code"] == 0
108
109 def test_undefined_result_still_exit_code_zero(self, tmp_path: pathlib.Path) -> None:
110 """undefined entries are a valid outcome — exit_code must still be 0."""
111 root = _init_repo(tmp_path)
112 _commit(root, "c1")
113 fake = "a" * 64
114 r = _nr(root, fake)
115 assert r.exit_code == 0
116 d = json.loads(r.output)
117 assert "exit_code" in d
118 assert d["exit_code"] == 0
119
120 def test_mixed_found_and_undefined_exit_code_zero(self, tmp_path: pathlib.Path) -> None:
121 root = _init_repo(tmp_path)
122 cid = _commit(root, "c1")
123 fake = "b" * 64
124 r = _nr(root, cid, fake)
125 assert r.exit_code == 0
126 d = json.loads(r.output)
127 assert d["exit_code"] == 0
128
129
130 # ---------------------------------------------------------------------------
131 # JSON envelope — duration_ms
132 # ---------------------------------------------------------------------------
133
134
135 class TestJsonEnvelopeDurationMs:
136 def test_found_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
137 root = _init_repo(tmp_path)
138 cid = _commit(root, "c1")
139 r = _nr(root, cid)
140 d = json.loads(r.output)
141 assert "duration_ms" in d, "duration_ms missing from found envelope"
142 assert isinstance(d["duration_ms"], float)
143 assert d["duration_ms"] >= 0.0
144
145 def test_undefined_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
146 root = _init_repo(tmp_path)
147 _commit(root, "c1")
148 r = _nr(root, "a" * 64)
149 d = json.loads(r.output)
150 assert "duration_ms" in d
151
152 def test_branches_filter_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
153 root = _init_repo(tmp_path)
154 cid = _commit(root, "c1", "main")
155 r = _nr(root, cid, "--branches", "main")
156 d = json.loads(r.output)
157 assert "duration_ms" in d
158 assert isinstance(d["duration_ms"], float)
159
160
161 # ---------------------------------------------------------------------------
162 # Error payload — errors route to stdout as JSON in --json mode
163 # ---------------------------------------------------------------------------
164
165
166 class TestErrorPayload:
167 def test_no_inputs_error_goes_to_stdout(self, tmp_path: pathlib.Path) -> None:
168 root = _init_repo(tmp_path)
169 r = _nr(root, "--json")
170 assert r.exit_code != 0
171 assert not r.stderr.strip(), f"unexpected stderr: {r.stderr!r}"
172 d = json.loads(r.output)
173 assert d["status"] == "error"
174
175 def test_invalid_hex_error_goes_to_stdout(self, tmp_path: pathlib.Path) -> None:
176 root = _init_repo(tmp_path)
177 r = _nr(root, "--json", "not-hex!")
178 assert r.exit_code != 0
179 assert not r.stderr.strip(), f"unexpected stderr: {r.stderr!r}"
180 d = json.loads(r.output)
181 assert d["status"] == "error"
182
183 def test_bad_max_walk_error_goes_to_stdout(self, tmp_path: pathlib.Path) -> None:
184 root = _init_repo(tmp_path)
185 cid = _commit(root, "c1")
186 r = _nr(root, "--json", cid, "--max-walk", "0")
187 assert r.exit_code != 0
188 assert not r.stderr.strip(), f"unexpected stderr: {r.stderr!r}"
189 d = json.loads(r.output)
190 assert d["status"] == "error"
191
192 def test_error_payload_has_status_error(self, tmp_path: pathlib.Path) -> None:
193 root = _init_repo(tmp_path)
194 r = _nr(root, "--json", "not-valid!")
195 d = json.loads(r.output)
196 assert d["status"] == "error"
197
198 def test_error_payload_has_exit_code(self, tmp_path: pathlib.Path) -> None:
199 root = _init_repo(tmp_path)
200 r = _nr(root, "--json", "not-valid!")
201 d = json.loads(r.output)
202 assert "exit_code" in d
203 assert d["exit_code"] != 0
204
205 def test_error_payload_has_error_field(self, tmp_path: pathlib.Path) -> None:
206 root = _init_repo(tmp_path)
207 r = _nr(root, "--json")
208 d = json.loads(r.output)
209 assert "error" in d
210 assert d["error"]
211
212 def test_no_emoji_on_stderr_in_json_mode(self, tmp_path: pathlib.Path) -> None:
213 root = _init_repo(tmp_path)
214 r = _nr(root, "--json", "not-valid!")
215 assert "❌" not in r.stderr
216
217
218 # ---------------------------------------------------------------------------
219 # Prefix resolution — bare hex prefix against sha256:-prefixed keys
220 # ---------------------------------------------------------------------------
221
222
223 class TestPrefixResolutionSha256:
224 def test_bare_hex_8char_prefix_resolves(self, tmp_path: pathlib.Path) -> None:
225 """Bare 8-char hex prefix must resolve against sha256:-prefixed name_map keys."""
226 root = _init_repo(tmp_path)
227 cid = _commit(root, "c1")
228 prefix = _hex_prefix(cid, 8) # first 8 hex chars, no sha256: prefix
229 r = _nr(root, prefix)
230 assert r.exit_code == 0
231 entry = json.loads(r.output)["results"][0]
232 assert entry["commit_id"] == cid
233 assert entry["undefined"] is False
234
235 def test_bare_hex_4char_prefix_resolves(self, tmp_path: pathlib.Path) -> None:
236 """4-char bare hex prefix must resolve when unambiguous."""
237 root = _init_repo(tmp_path)
238 cid = _commit(root, "unique-c1-msg")
239 prefix = _hex_prefix(cid, 4)
240 r = _nr(root, prefix)
241 assert r.exit_code == 0
242 entry = json.loads(r.output)["results"][0]
243 # Either resolves to cid or is ambiguous — must not crash or error
244 assert entry["input"] == prefix
245 assert r.exit_code == 0
246
247 def test_sha256_prefixed_short_id_resolves(self, tmp_path: pathlib.Path) -> None:
248 """sha256:-prefixed short IDs (e.g. sha256:abcd1234) must also resolve."""
249 root = _init_repo(tmp_path)
250 cid = _commit(root, "c1")
251 short = cid[:len("sha256:") + 8] # keep sha256: + 8 hex chars
252 r = _nr(root, short)
253 assert r.exit_code == 0
254 entry = json.loads(r.output)["results"][0]
255 assert entry["commit_id"] == cid
256
257 def test_input_field_preserves_bare_hex_prefix(self, tmp_path: pathlib.Path) -> None:
258 """input field echoes the caller's original value, not the resolved full ID."""
259 root = _init_repo(tmp_path)
260 cid = _commit(root, "c1")
261 prefix = _hex_prefix(cid, 8)
262 r = _nr(root, prefix)
263 entry = json.loads(r.output)["results"][0]
264 assert entry["input"] == prefix
265
266
267 # ---------------------------------------------------------------------------
268 # No-prose pollution
269 # ---------------------------------------------------------------------------
270
271
272 class TestNoProsePollution:
273 def test_found_stdout_is_valid_json(self, tmp_path: pathlib.Path) -> None:
274 root = _init_repo(tmp_path)
275 cid = _commit(root, "c1")
276 r = _nr(root, cid)
277 json.loads(r.output) # must not raise
278
279 def test_undefined_stdout_is_valid_json(self, tmp_path: pathlib.Path) -> None:
280 root = _init_repo(tmp_path)
281 _commit(root, "c1")
282 json.loads(_nr(root, "a" * 64).output)
283
284 def test_no_emoji_in_success_json(self, tmp_path: pathlib.Path) -> None:
285 root = _init_repo(tmp_path)
286 cid = _commit(root, "c1")
287 r = _nr(root, cid)
288 assert "✅" not in r.output
289 assert "❌" not in r.output
290
291
292 # ---------------------------------------------------------------------------
293 # TypedDicts
294 # ---------------------------------------------------------------------------
295
296
297 class TestTypedDicts:
298 def test_name_rev_json_typeddict_exists(self) -> None:
299 from muse.cli.commands.name_rev import _NameRevJson
300 assert _NameRevJson is not None
301
302 def test_name_rev_error_json_typeddict_exists(self) -> None:
303 from muse.cli.commands.name_rev import _NameRevErrorJson
304 assert _NameRevErrorJson is not None
305
306 def test_name_rev_json_has_exit_code_annotation(self) -> None:
307 from muse.cli.commands.name_rev import _NameRevJson
308 hints = get_type_hints(_NameRevJson)
309 assert "exit_code" in hints
310
311 def test_name_rev_json_has_duration_ms_annotation(self) -> None:
312 from muse.cli.commands.name_rev import _NameRevJson
313 hints = get_type_hints(_NameRevJson)
314 assert "duration_ms" in hints
315
316 def test_name_rev_error_json_has_required_fields(self) -> None:
317 from muse.cli.commands.name_rev import _NameRevErrorJson
318 hints = get_type_hints(_NameRevErrorJson)
319 for field in ("status", "error", "exit_code"):
320 assert field in hints, f"Missing annotation: {field!r}"
321
322
323 # ---------------------------------------------------------------------------
324 # Docstring coverage
325 # ---------------------------------------------------------------------------
326
327
328 class TestDocstring:
329 def _doc(self) -> str:
330 import muse.cli.commands.name_rev as mod
331 return mod.__doc__ or ""
332
333 def test_docstring_documents_exit_code(self) -> None:
334 assert "exit_code" in self._doc()
335
336 def test_docstring_documents_duration_ms(self) -> None:
337 assert "duration_ms" in self._doc()
338
339
340 # ---------------------------------------------------------------------------
341 # Stress
342 # ---------------------------------------------------------------------------
343
344
345 class TestStress:
346 def test_100_commit_chain_all_have_envelope_fields(self, tmp_path: pathlib.Path) -> None:
347 root = _init_repo(tmp_path)
348 parent: str | None = None
349 commits: list[str] = []
350 for i in range(100):
351 cid = _commit(root, f"c{i:03d}", parent=parent)
352 commits.append(cid)
353 parent = cid
354
355 r = _nr(root, commits[0], commits[49], commits[-1])
356 assert r.exit_code == 0
357 d = json.loads(r.output)
358 assert "exit_code" in d
359 assert "duration_ms" in d
360 assert d["exit_code"] == 0
361 assert isinstance(d["duration_ms"], float)
362 # Tip at distance 0, midpoint at 50, root at 99
363 by_id = {e["commit_id"]: e for e in d["results"]}
364 assert by_id[commits[-1]]["distance"] == 0
365 assert by_id[commits[49]]["distance"] == 50
366 assert by_id[commits[0]]["distance"] == 99
File History 1 commit