gabriel / muse public
test_release_supercharge.py python
465 lines 18.9 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 142 days ago
1 """Supercharge tests for ``muse release``.
2
3 Coverage tiers
4 --------------
5 - Unit: _short_id helper — sha256:-prefixed input
6 - Integration: duration_ms + exit_code on every JSON path
7 (add, list, read, push --dry-run, delete --dry-run, suggest)
8 - Data: short commit IDs in text output (not bare truncated hex)
9 - Data: list JSON wraps in {total, releases} envelope
10 - Security: no tracebacks; ANSI stripped in text
11 - Performance: suggest on 50 commits under 500ms
12 """
13 from __future__ import annotations
14
15 import datetime
16 import json
17 import pathlib
18 import time
19 import uuid
20
21 from muse.core.errors import ExitCode
22 from tests.cli_test_helper import CliRunner, InvokeResult
23 from muse.core._types import long_id, short_id as _short_id
24
25 runner = CliRunner()
26
27 _SHA256_SHORT_19 = __import__("re").compile(r"^sha256:[0-9a-f]{12}$")
28
29
30 # ---------------------------------------------------------------------------
31 # Repo + commit helpers (mirrors test_release.py)
32 # ---------------------------------------------------------------------------
33
34
35 def _make_repo(tmp_path: pathlib.Path) -> tuple[pathlib.Path, str]:
36 muse_dir = tmp_path / ".muse"
37 muse_dir.mkdir()
38 repo_id = str(uuid.uuid4())
39 (muse_dir / "repo.json").write_text(
40 json.dumps({"repo_id": repo_id, "domain": "code",
41 "default_branch": "main",
42 "created_at": "2025-01-01T00:00:00+00:00"}),
43 encoding="utf-8",
44 )
45 (muse_dir / "HEAD").write_text("ref: refs/heads/main\n", encoding="utf-8")
46 (muse_dir / "refs" / "heads").mkdir(parents=True)
47 (muse_dir / "snapshots").mkdir()
48 (muse_dir / "commits").mkdir()
49 (muse_dir / "objects").mkdir()
50 return tmp_path, repo_id
51
52
53 def _commit(
54 root: pathlib.Path,
55 repo_id: str,
56 *,
57 message: str = "feat: add something",
58 sem_ver_bump: str = "minor",
59 agent_id: str = "claude-code",
60 model_id: str = "claude-sonnet-4-6",
61 ) -> str:
62 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
63 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
64 from muse.domain import SemVerBump
65
66 branch = "main"
67 ref_file = root / ".muse" / "refs" / "heads" / branch
68 raw_parent = ref_file.read_text().strip() if ref_file.exists() else ""
69 parent_id: str | None = raw_parent if raw_parent else None
70 snap_id = compute_snapshot_id({})
71 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest={}))
72 now = datetime.datetime.now(datetime.timezone.utc)
73 parent_ids = [parent_id] if parent_id else []
74 commit_id = compute_commit_id(parent_ids, snap_id, message, now.isoformat())
75 bump: SemVerBump = sem_ver_bump # type: ignore[assignment]
76 write_commit(root, CommitRecord(
77 commit_id=commit_id, repo_id=repo_id, branch=branch,
78 snapshot_id=snap_id, message=message, committed_at=now,
79 parent_commit_id=parent_id, sem_ver_bump=bump,
80 breaking_changes=[], agent_id=agent_id, model_id=model_id,
81 ))
82 ref_file.write_text(commit_id, encoding="utf-8")
83 return commit_id
84
85
86 def _invoke(root: pathlib.Path, *args: str) -> InvokeResult:
87 from muse.cli.app import main as cli
88 return runner.invoke(cli, ["release", *args],
89 env={"MUSE_REPO_ROOT": str(root)})
90
91
92 def _add(root: pathlib.Path, tag: str = "v0.1.0", **kwargs: str) -> InvokeResult:
93 extra = []
94 for k, v in kwargs.items():
95 extra.extend([f"--{k}", v])
96 return _invoke(root, "add", tag, "--json", *extra)
97
98
99 # ---------------------------------------------------------------------------
100 # Unit — _short_id
101 # ---------------------------------------------------------------------------
102
103
104 class TestShortId:
105 """_short_id normalises sha256:-prefixed commit IDs to sha256:<12-hex>."""
106
107 def test_sha256_prefixed_returns_sha256_short(self) -> None:
108
109 cid = long_id("a" * 64)
110 assert _short_id(cid) == long_id("a" * 12)
111
112 def test_result_is_19_chars(self) -> None:
113
114 assert len(_short_id(long_id("b" * 64))) == 19
115
116 def test_bare_hex_also_handled(self) -> None:
117 # bare hex in → first 12 chars out, no prefix added
118 result = _short_id("c" * 64)
119 assert result == "c" * 12
120
121 def test_matches_short_regex(self) -> None:
122
123 assert _SHA256_SHORT_19.match(_short_id(long_id("d" * 64)))
124
125 def test_idempotent_on_short_input(self) -> None:
126 """Already-short sha256:<12-hex> passes through unchanged."""
127
128 short = long_id("e" * 12)
129 assert _short_id(short) == short
130
131
132 # ---------------------------------------------------------------------------
133 # Integration — duration_ms and exit_code on every JSON path
134 # ---------------------------------------------------------------------------
135
136
137 class TestDurationAndExitCode:
138 """Every subcommand's JSON output must carry duration_ms and exit_code."""
139
140 def test_add_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
141 root, repo_id = _make_repo(tmp_path)
142 _commit(root, repo_id)
143 data = json.loads(_add(root).output)
144 assert "duration_ms" in data
145
146 def test_add_json_exit_code_zero(self, tmp_path: pathlib.Path) -> None:
147 root, repo_id = _make_repo(tmp_path)
148 _commit(root, repo_id)
149 data = json.loads(_add(root).output)
150 assert data["exit_code"] == 0
151
152 def test_add_duration_ms_is_float(self, tmp_path: pathlib.Path) -> None:
153 root, repo_id = _make_repo(tmp_path)
154 _commit(root, repo_id)
155 assert isinstance(json.loads(_add(root).output)["duration_ms"], float)
156
157 def test_list_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
158 root, repo_id = _make_repo(tmp_path)
159 _commit(root, repo_id)
160 _add(root)
161 data = json.loads(_invoke(root, "list", "--json").output)
162 assert "duration_ms" in data
163
164 def test_list_json_exit_code_zero(self, tmp_path: pathlib.Path) -> None:
165 root, repo_id = _make_repo(tmp_path)
166 data = json.loads(_invoke(root, "list", "--json").output)
167 assert data["exit_code"] == 0
168
169 def test_list_empty_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
170 """Even with zero releases, metadata fields must be present."""
171 root, repo_id = _make_repo(tmp_path)
172 data = json.loads(_invoke(root, "list", "--json").output)
173 assert "duration_ms" in data
174
175 def test_read_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
176 root, repo_id = _make_repo(tmp_path)
177 _commit(root, repo_id)
178 _add(root)
179 data = json.loads(_invoke(root, "read", "v0.1.0", "--json").output)
180 assert "duration_ms" in data
181
182 def test_read_json_exit_code_zero(self, tmp_path: pathlib.Path) -> None:
183 root, repo_id = _make_repo(tmp_path)
184 _commit(root, repo_id)
185 _add(root)
186 data = json.loads(_invoke(root, "read", "v0.1.0", "--json").output)
187 assert data["exit_code"] == 0
188
189 def test_push_dry_run_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
190 root, repo_id = _make_repo(tmp_path)
191 _commit(root, repo_id)
192 _add(root)
193 data = json.loads(
194 _invoke(root, "push", "v0.1.0", "--dry-run", "--json").output
195 )
196 assert "duration_ms" in data
197
198 def test_push_dry_run_json_exit_code_zero(self, tmp_path: pathlib.Path) -> None:
199 root, repo_id = _make_repo(tmp_path)
200 _commit(root, repo_id)
201 _add(root)
202 data = json.loads(
203 _invoke(root, "push", "v0.1.0", "--dry-run", "--json").output
204 )
205 assert data["exit_code"] == 0
206
207 def test_delete_dry_run_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
208 root, repo_id = _make_repo(tmp_path)
209 _commit(root, repo_id)
210 _add(root)
211 data = json.loads(
212 _invoke(root, "delete", "v0.1.0", "--dry-run", "--json").output
213 )
214 assert "duration_ms" in data
215
216 def test_delete_dry_run_json_exit_code_zero(self, tmp_path: pathlib.Path) -> None:
217 root, repo_id = _make_repo(tmp_path)
218 _commit(root, repo_id)
219 _add(root)
220 data = json.loads(
221 _invoke(root, "delete", "v0.1.0", "--dry-run", "--json").output
222 )
223 assert data["exit_code"] == 0
224
225 def test_suggest_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
226 root, repo_id = _make_repo(tmp_path)
227 _commit(root, repo_id, sem_ver_bump="minor")
228 data = json.loads(_invoke(root, "suggest", "--json").output)
229 assert "duration_ms" in data
230
231 def test_suggest_json_exit_code_zero(self, tmp_path: pathlib.Path) -> None:
232 root, repo_id = _make_repo(tmp_path)
233 _commit(root, repo_id, sem_ver_bump="minor")
234 data = json.loads(_invoke(root, "suggest", "--json").output)
235 assert data["exit_code"] == 0
236
237 def test_duration_ms_non_negative(self, tmp_path: pathlib.Path) -> None:
238 root, repo_id = _make_repo(tmp_path)
239 _commit(root, repo_id)
240 _add(root)
241 assert json.loads(_add(root, "v0.2.0").output)["duration_ms"] >= 0.0
242
243 def test_duration_ms_3dp_precision(self, tmp_path: pathlib.Path) -> None:
244 root, repo_id = _make_repo(tmp_path)
245 _commit(root, repo_id)
246 ms = json.loads(_add(root).output)["duration_ms"]
247 assert round(ms, 3) == ms
248
249
250 # ---------------------------------------------------------------------------
251 # Data integrity — text format short IDs
252 # ---------------------------------------------------------------------------
253
254
255 class TestTextFormatShortId:
256 """Text output must show sha256:<12-hex> commit IDs, not bare [:8] slices."""
257
258 def _short_tokens(self, text: str) -> list[str]:
259 return [tok for tok in text.split() if _SHA256_SHORT_19.match(tok)]
260
261 def test_read_text_shows_sha256_short_commit(self, tmp_path: pathlib.Path) -> None:
262 root, repo_id = _make_repo(tmp_path)
263 cid = _commit(root, repo_id)
264 _invoke(root, "add", "v0.1.0")
265 result = _invoke(root, "read", "v0.1.0")
266 assert result.exit_code == 0
267 tokens = self._short_tokens(result.output)
268 assert tokens, f"no sha256:<12-hex> token in read text:\n{result.output}"
269
270 def test_list_text_shows_sha256_short_commit(self, tmp_path: pathlib.Path) -> None:
271 root, repo_id = _make_repo(tmp_path)
272 _commit(root, repo_id)
273 _invoke(root, "add", "v0.1.0")
274 result = _invoke(root, "list")
275 assert result.exit_code == 0
276 tokens = self._short_tokens(result.output)
277 assert tokens, f"no sha256:<12-hex> token in list text:\n{result.output}"
278
279 def test_suggest_text_shows_sha256_short_in_driver(self, tmp_path: pathlib.Path) -> None:
280 root, repo_id = _make_repo(tmp_path)
281 _commit(root, repo_id, sem_ver_bump="minor")
282 result = _invoke(root, "suggest")
283 assert result.exit_code == 0
284 tokens = self._short_tokens(result.output)
285 assert tokens, f"no sha256:<12-hex> token in suggest text:\n{result.output}"
286
287 def test_read_text_commit_not_bare_hex_prefix_only(self, tmp_path: pathlib.Path) -> None:
288 """Regression: commit_id[:8] on sha256:-prefixed ID yields 'sha256:a' — wrong."""
289 root, repo_id = _make_repo(tmp_path)
290 _commit(root, repo_id)
291 _invoke(root, "add", "v0.1.0")
292 result = _invoke(root, "read", "v0.1.0")
293 # "sha256:a" with no further hex after colon would indicate the bug
294 assert "sha256:a\n" not in result.output
295 assert "sha256:b\n" not in result.output
296
297 def test_changelog_entries_show_sha256_short(self, tmp_path: pathlib.Path) -> None:
298 root, repo_id = _make_repo(tmp_path)
299 _commit(root, repo_id, message="feat: first", sem_ver_bump="minor")
300 _commit(root, repo_id, message="feat: second", sem_ver_bump="minor")
301 _invoke(root, "add", "v0.1.0")
302 result = _invoke(root, "read", "v0.1.0")
303 tokens = self._short_tokens(result.output)
304 # changelog has 2 entries, each with a short commit ID
305 assert len(tokens) >= 2, f"expected ≥2 sha256:<12-hex> tokens:\n{result.output}"
306
307
308 # ---------------------------------------------------------------------------
309 # Data integrity — list JSON envelope
310 # ---------------------------------------------------------------------------
311
312
313 class TestListJsonEnvelope:
314 """list --json emits {total, releases, duration_ms, exit_code} — not a bare array."""
315
316 def test_list_json_top_level_is_object(self, tmp_path: pathlib.Path) -> None:
317 root, repo_id = _make_repo(tmp_path)
318 data = json.loads(_invoke(root, "list", "--json").output)
319 assert isinstance(data, dict)
320
321 def test_list_json_has_total_key(self, tmp_path: pathlib.Path) -> None:
322 root, repo_id = _make_repo(tmp_path)
323 data = json.loads(_invoke(root, "list", "--json").output)
324 assert "total" in data
325
326 def test_list_json_has_releases_key(self, tmp_path: pathlib.Path) -> None:
327 root, repo_id = _make_repo(tmp_path)
328 data = json.loads(_invoke(root, "list", "--json").output)
329 assert "releases" in data
330
331 def test_list_json_releases_is_array(self, tmp_path: pathlib.Path) -> None:
332 root, repo_id = _make_repo(tmp_path)
333 data = json.loads(_invoke(root, "list", "--json").output)
334 assert isinstance(data["releases"], list)
335
336 def test_list_json_total_matches_releases_length(self, tmp_path: pathlib.Path) -> None:
337 root, repo_id = _make_repo(tmp_path)
338 _commit(root, repo_id)
339 _add(root, "v0.1.0")
340 _commit(root, repo_id)
341 _add(root, "v0.2.0")
342 data = json.loads(_invoke(root, "list", "--json").output)
343 assert data["total"] == len(data["releases"]) == 2
344
345 def test_list_json_empty_total_is_zero(self, tmp_path: pathlib.Path) -> None:
346 root, repo_id = _make_repo(tmp_path)
347 data = json.loads(_invoke(root, "list", "--json").output)
348 assert data["total"] == 0
349 assert data["releases"] == []
350
351 def test_bare_release_json_flag_uses_envelope(self, tmp_path: pathlib.Path) -> None:
352 """`muse release --json` (no subcommand) also uses the envelope."""
353 root, repo_id = _make_repo(tmp_path)
354 data = json.loads(_invoke(root, "--json").output)
355 assert "releases" in data
356 assert "total" in data
357
358
359 # ---------------------------------------------------------------------------
360 # Security
361 # ---------------------------------------------------------------------------
362
363
364 class TestSecuritySupercharge:
365 def test_no_traceback_on_bad_semver(self, tmp_path: pathlib.Path) -> None:
366 root, _ = _make_repo(tmp_path)
367 result = _invoke(root, "add", "not-semver", "--json")
368 assert result.exit_code == ExitCode.USER_ERROR
369 assert "Traceback" not in result.output
370
371 def test_no_traceback_on_missing_read(self, tmp_path: pathlib.Path) -> None:
372 root, _ = _make_repo(tmp_path)
373 result = _invoke(root, "read", "v9.9.9", "--json")
374 assert result.exit_code == ExitCode.NOT_FOUND
375 assert "Traceback" not in result.output
376
377 def test_no_traceback_on_suggest_no_commits(self, tmp_path: pathlib.Path) -> None:
378 root, _ = _make_repo(tmp_path)
379 result = _invoke(root, "suggest", "--json")
380 # Either no commits (exit 1) or 0 unreleased — must not traceback
381 assert "Traceback" not in result.output
382
383 def test_add_json_error_on_duplicate(self, tmp_path: pathlib.Path) -> None:
384 root, repo_id = _make_repo(tmp_path)
385 _commit(root, repo_id)
386 _add(root)
387 result = _invoke(root, "add", "v0.1.0", "--json")
388 assert result.exit_code == ExitCode.USER_ERROR
389 # JSON error is on stdout (first line); ❌ message goes to stderr
390 first_line = result.output.splitlines()[0]
391 data = json.loads(first_line)
392 assert data["error"] == "already_exists"
393
394
395 # ---------------------------------------------------------------------------
396 # Performance
397 # ---------------------------------------------------------------------------
398
399
400 class TestPerformanceSupercharge:
401 def test_list_50_releases_under_500ms(self, tmp_path: pathlib.Path) -> None:
402 root, repo_id = _make_repo(tmp_path)
403 for i in range(50):
404 _commit(root, repo_id, message=f"feat: thing {i}")
405 _invoke(root, "add", f"v0.{i}.0")
406 t0 = time.monotonic()
407 result = _invoke(root, "list", "--json")
408 duration_ms = (time.monotonic() - t0) * 1000
409 assert result.exit_code == 0
410 assert duration_ms < 500
411
412 def test_suggest_50_commits_under_500ms(self, tmp_path: pathlib.Path) -> None:
413 root, repo_id = _make_repo(tmp_path)
414 for i in range(50):
415 _commit(root, repo_id, sem_ver_bump="patch")
416 t0 = time.monotonic()
417 result = _invoke(root, "suggest", "--json")
418 duration_ms = (time.monotonic() - t0) * 1000
419 assert result.exit_code == 0
420 assert duration_ms < 500
421
422 def test_duration_ms_plausible(self, tmp_path: pathlib.Path) -> None:
423 root, repo_id = _make_repo(tmp_path)
424 _commit(root, repo_id)
425 ms = json.loads(_add(root).output)["duration_ms"]
426 assert 0.0 <= ms < 500
427
428
429 # ---------------------------------------------------------------------------
430 # Content-addressed release_id
431 # ---------------------------------------------------------------------------
432
433
434 class TestReleaseIdContentAddressed:
435 """release_id must be sha256: of genesis content, not a UUID."""
436
437 def test_release_id_is_sha256_prefixed(self, tmp_path: pathlib.Path) -> None:
438 root, repo_id = _make_repo(tmp_path)
439 _commit(root, repo_id)
440 data = json.loads(_add(root, "v1.0.0").output)
441 assert data["release_id"].startswith("sha256:"), f"Expected sha256: prefix, got {data['release_id']!r}"
442 assert len(data["release_id"]) == 71
443
444 def test_release_id_not_uuid(self, tmp_path: pathlib.Path) -> None:
445 import re
446 root, repo_id = _make_repo(tmp_path)
447 _commit(root, repo_id)
448 data = json.loads(_add(root, "v1.0.0").output)
449 uuid_re = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
450 assert not uuid_re.match(data["release_id"])
451
452 def test_release_id_is_deterministic(self, tmp_path: pathlib.Path) -> None:
453 """Same repo + tag + commit → same release_id."""
454 from muse.core.store import compute_release_id
455 release_id = compute_release_id(repo_id="test-repo", tag="v1.0.0", commit_id=long_id("a" * 64))
456 assert release_id == compute_release_id(repo_id="test-repo", tag="v1.0.0", commit_id=long_id("a" * 64))
457
458 def test_release_id_differs_by_tag(self, tmp_path: pathlib.Path) -> None:
459 from muse.core.store import compute_release_id
460 cid = long_id("a" * 64)
461 assert compute_release_id("repo", "v1.0.0", cid) != compute_release_id("repo", "v2.0.0", cid)
462
463 def test_release_id_differs_by_commit(self, tmp_path: pathlib.Path) -> None:
464 from muse.core.store import compute_release_id
465 assert compute_release_id("repo", "v1.0.0", long_id("a" * 64)) != compute_release_id("repo", "v1.0.0", long_id("b" * 64))
File History 1 commit
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 142 days ago