gabriel / muse public

test_tag_supercharge.py file-level

at sha256:d · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 πŸ’₯ blast risk
sha256:4 Merge branch 'dev' into main · gabriel · Jun 17, 2026
1 """Supercharge tests for ``muse tag``.
2
3 Covers gaps identified in the review:
4 - duration_ms + exit_code in all JSON outputs (add / list / remove)
5 - JSON errors emitted to stdout (not stderr) when --json is set
6 - _TagJson TypedDict schema completeness with new telemetry fields
7 - Tag ID is sha256-prefixed, not UUID
8 - Docstring correctness (schema in help text)
9
10 Unit
11 ----
12 U1 _TagJson TypedDict has duration_ms and exit_code fields
13 U2 _emit_error helper is present in module (or equivalent pattern)
14 U3 format error in JSON mode goes to stdout
15 U4 tag_id format is sha256 prefix, not UUID
16 U5 commit_id in JSON output is sha256-prefixed
17
18 Integration β€” duration_ms / exit_code
19 -------------------------------------
20 I1 tag add --json includes duration_ms (float β‰₯ 0)
21 I2 tag add --json includes exit_code == 0 on success
22 I3 tag add --json --dry-run includes duration_ms and exit_code == 0
23 I4 tag add already_tagged includes duration_ms and exit_code == 0
24 I5 tag list --json includes duration_ms and exit_code == 0
25 I6 tag list --json empty repo includes duration_ms and exit_code == 0
26 I7 tag remove --json includes duration_ms and exit_code == 0
27 I8 tag remove --json not_found includes duration_ms and exit_code == 0
28
29 Integration β€” JSON errors to stdout
30 -------------------------------------
31 E1 tag add invalid tag name --json β†’ JSON error on stdout, not stderr
32 E2 tag add commit not found --json β†’ JSON error on stdout
33 E3 tag add bad format --json β†’ handled (format validated before JSON flag seen,
34 but error must still exit nonzero)
35 E4 tag list commit not found --json β†’ JSON error on stdout
36 E5 tag remove invalid tag name --json β†’ JSON error on stdout
37 E6 tag remove commit not found --json β†’ JSON error on stdout
38
39 Error JSON schema
40 -----------------
41 S1 add invalid-tag error JSON has "error", "message", "duration_ms", "exit_code"
42 S2 add commit-not-found error JSON has expected keys
43 S3 list commit-not-found error JSON has expected keys
44 S4 remove invalid-tag error JSON has expected keys
45
46 Data integrity
47 --------------
48 D1 tag_id is sha256-prefixed string (not UUID) on add
49 D2 tag_id is sha256-prefixed on list
50 D3 commit_id is sha256-prefixed on add
51 D4 commit_id is sha256-prefixed on list
52 D5 commit_id is sha256-prefixed on remove
53
54 Performance
55 -----------
56 P1 duration_ms is a float (not int, not None)
57 P2 duration_ms is under 5000 ms for a simple add
58 P3 duration_ms values across two sequential calls are both positive
59
60 Concurrent reads
61 ----------------
62 C1 concurrent tag list calls on isolated repos produce correct counts
63 """
64
65 from __future__ import annotations
66 from collections.abc import Mapping
67
68 import json
69 import os
70 import pathlib
71 import threading
72 import time
73
74 import pytest
75
76 from tests.cli_test_helper import CliRunner
77
78 cli = None
79 runner = CliRunner()
80
81 _CHDIR_LOCK = threading.Lock()
82
83
84 def _env(root: pathlib.Path) -> Mapping[str, str]:
85 return {"MUSE_REPO_ROOT": str(root)}
86
87
88 @pytest.fixture()
89 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
90 monkeypatch.chdir(tmp_path)
91 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
92 runner.invoke(cli, ["init"], env=_env(tmp_path), catch_exceptions=False)
93 (tmp_path / "song.mid").write_bytes(b"\x00" * 16)
94 runner.invoke(cli, ["commit", "-m", "base"], env=_env(tmp_path), catch_exceptions=False)
95 return tmp_path
96
97
98 @pytest.fixture()
99 def tagged_repo(repo: pathlib.Path) -> pathlib.Path:
100 runner.invoke(cli, ["tag", "add", "emotion:joyful"], env=_env(repo), catch_exceptions=False)
101 return repo
102
103
104 # ---------------------------------------------------------------------------
105 # Unit
106 # ---------------------------------------------------------------------------
107
108 class TestUnit:
109 def test_u1_typeddict_has_duration_ms(self) -> None:
110 """U1 β€” _TagAddJson TypedDict must declare duration_ms field."""
111 from muse.cli.commands.tag import _TagAddJson
112 import typing
113 hints = typing.get_type_hints(_TagAddJson)
114 assert "duration_ms" in hints, "_TagAddJson missing duration_ms"
115
116 def test_u2_typeddict_has_exit_code(self) -> None:
117 """U2 β€” _TagAddJson TypedDict must declare exit_code field."""
118 from muse.cli.commands.tag import _TagAddJson
119 import typing
120 hints = typing.get_type_hints(_TagAddJson)
121 assert "exit_code" in hints, "_TagAddJson missing exit_code"
122
123 def test_u3_format_error_json_mode(self, repo: pathlib.Path) -> None:
124 """U3 β€” Invalid format: --json is ambiguous (format set before --json), but exit nonzero."""
125 r = runner.invoke(cli, ["tag", "add", "emotion:happy", "--format", "bad"],
126 env=_env(repo))
127 assert r.exit_code != 0
128
129 def test_u4_tag_id_is_sha256(self, repo: pathlib.Path) -> None:
130 """U4 β€” tag_id must use sha256: prefix, not UUID."""
131 r = runner.invoke(cli, ["tag", "add", "emotion:happy", "--json"], env=_env(repo))
132 data = json.loads(r.output)
133 assert data["tag_id"].startswith("sha256:"), f"tag_id not sha256: got {data['tag_id']!r}"
134
135 def test_u5_commit_id_is_sha256(self, repo: pathlib.Path) -> None:
136 """U5 β€” commit_id in add output must use sha256: prefix."""
137 r = runner.invoke(cli, ["tag", "add", "emotion:happy", "--json"], env=_env(repo))
138 data = json.loads(r.output)
139 assert data["commit_id"].startswith("sha256:"), \
140 f"commit_id not sha256: got {data['commit_id']!r}"
141
142
143 # ---------------------------------------------------------------------------
144 # Integration β€” duration_ms / exit_code in every success path
145 # ---------------------------------------------------------------------------
146
147 class TestElapsedMsExitCode:
148 def test_i1_add_has_duration_ms(self, repo: pathlib.Path) -> None:
149 """I1 β€” tag add --json success includes duration_ms."""
150 r = runner.invoke(cli, ["tag", "add", "emotion:happy", "--json"], env=_env(repo))
151 assert r.exit_code == 0
152 data = json.loads(r.output)
153 assert "duration_ms" in data, f"Missing duration_ms in: {data}"
154
155 def test_i2_add_has_exit_code_zero(self, repo: pathlib.Path) -> None:
156 """I2 β€” tag add --json success includes exit_code == 0."""
157 r = runner.invoke(cli, ["tag", "add", "emotion:happy", "--json"], env=_env(repo))
158 data = json.loads(r.output)
159 assert data["exit_code"] == 0
160
161 def test_i3_add_dry_run_has_duration_ms(self, repo: pathlib.Path) -> None:
162 """I3 β€” tag add --dry-run --json includes duration_ms and exit_code."""
163 r = runner.invoke(cli, ["tag", "add", "emotion:happy", "--dry-run", "--json"],
164 env=_env(repo), catch_exceptions=False)
165 data = json.loads(r.output)
166 assert "duration_ms" in data
167 assert data["exit_code"] == 0
168
169 def test_i4_add_already_tagged_has_duration_ms(self, tagged_repo: pathlib.Path) -> None:
170 """I4 β€” tag add already_tagged includes duration_ms and exit_code."""
171 r = runner.invoke(cli, ["tag", "add", "emotion:joyful", "--json"],
172 env=_env(tagged_repo), catch_exceptions=False)
173 data = json.loads(r.output)
174 assert data["status"] == "already_tagged"
175 assert "duration_ms" in data
176 assert data["exit_code"] == 0
177
178 def test_i5_list_has_duration_ms(self, tagged_repo: pathlib.Path) -> None:
179 """I5 β€” tag list --json includes duration_ms and exit_code."""
180 r = runner.invoke(cli, ["tag", "list", "--json"],
181 env=_env(tagged_repo), catch_exceptions=False)
182 data = json.loads(r.output)
183 assert "duration_ms" in data
184 assert data["exit_code"] == 0
185
186 def test_i6_list_empty_has_duration_ms(self, repo: pathlib.Path) -> None:
187 """I6 β€” tag list --json empty repo includes duration_ms."""
188 r = runner.invoke(cli, ["tag", "list", "--json"],
189 env=_env(repo), catch_exceptions=False)
190 data = json.loads(r.output)
191 assert "duration_ms" in data
192 assert data["total"] == 0
193
194 def test_i7_remove_has_duration_ms(self, tagged_repo: pathlib.Path) -> None:
195 """I7 β€” tag remove --json includes duration_ms and exit_code."""
196 r = runner.invoke(cli, ["tag", "remove", "emotion:joyful", "--json"],
197 env=_env(tagged_repo), catch_exceptions=False)
198 data = json.loads(r.output)
199 assert "duration_ms" in data
200 assert data["exit_code"] == 0
201
202 def test_i8_remove_not_found_has_duration_ms(self, repo: pathlib.Path) -> None:
203 """I8 β€” tag remove not_found --json includes duration_ms."""
204 r = runner.invoke(cli, ["tag", "remove", "nonexistent:tag", "--json"],
205 env=_env(repo), catch_exceptions=False)
206 data = json.loads(r.output)
207 assert data["status"] == "not_found"
208 assert "duration_ms" in data
209 assert data["exit_code"] == 0
210
211
212 # ---------------------------------------------------------------------------
213 # Integration β€” JSON errors go to stdout in --json mode
214 # ---------------------------------------------------------------------------
215
216 class TestJsonErrorsToStdout:
217 def test_e1_add_invalid_tag_json_to_stdout(self, repo: pathlib.Path) -> None:
218 """E1 β€” invalid tag name + --json β†’ JSON error on stdout."""
219 r = runner.invoke(cli, ["tag", "add", "bad\x1btag", "--json"], env=_env(repo))
220 assert r.exit_code != 0
221 # stdout must be parseable JSON
222 data = json.loads(r.output)
223 assert "error" in data
224
225 def test_e2_add_commit_not_found_json_to_stdout(self, repo: pathlib.Path) -> None:
226 """E2 β€” commit not found + --json β†’ JSON error on stdout."""
227 r = runner.invoke(cli, ["tag", "add", "emotion:happy", "deadbeef00", "--json"],
228 env=_env(repo))
229 assert r.exit_code != 0
230 data = json.loads(r.output)
231 assert "error" in data
232
233 def test_e3_bad_format_exits_nonzero(self, repo: pathlib.Path) -> None:
234 """E3 β€” bad --format exits nonzero."""
235 r = runner.invoke(cli, ["tag", "add", "emotion:happy", "--format", "bad"],
236 env=_env(repo))
237 assert r.exit_code != 0
238
239 def test_e4_list_commit_not_found_json_to_stdout(self, repo: pathlib.Path) -> None:
240 """E4 β€” tag list commit not found + --json β†’ JSON error on stdout."""
241 r = runner.invoke(cli, ["tag", "list", "deadbeef00", "--json"], env=_env(repo))
242 assert r.exit_code != 0
243 data = json.loads(r.output)
244 assert "error" in data
245
246 def test_e5_remove_invalid_tag_json_to_stdout(self, repo: pathlib.Path) -> None:
247 """E5 β€” tag remove invalid tag name + --json β†’ JSON error on stdout."""
248 r = runner.invoke(cli, ["tag", "remove", "bad\x1btag", "--json"], env=_env(repo))
249 assert r.exit_code != 0
250 data = json.loads(r.output)
251 assert "error" in data
252
253 def test_e6_remove_commit_not_found_json_to_stdout(self, repo: pathlib.Path) -> None:
254 """E6 β€” tag remove commit not found + --json β†’ JSON error on stdout."""
255 r = runner.invoke(cli, ["tag", "remove", "emotion:happy", "deadbeef00", "--json"],
256 env=_env(repo))
257 assert r.exit_code != 0
258 data = json.loads(r.output)
259 assert "error" in data
260
261
262 # ---------------------------------------------------------------------------
263 # Error JSON schema
264 # ---------------------------------------------------------------------------
265
266 class TestErrorJsonSchema:
267 _REQUIRED = {"error", "message", "duration_ms", "exit_code"}
268
269 def test_s1_add_invalid_tag_error_schema(self, repo: pathlib.Path) -> None:
270 """S1 β€” invalid tag error JSON has all required fields."""
271 r = runner.invoke(cli, ["tag", "add", "bad\x1btag", "--json"], env=_env(repo))
272 data = json.loads(r.output)
273 assert self._REQUIRED <= data.keys(), f"Missing keys: {self._REQUIRED - data.keys()}"
274
275 def test_s2_add_commit_not_found_error_schema(self, repo: pathlib.Path) -> None:
276 """S2 β€” commit not found error JSON has all required fields."""
277 r = runner.invoke(cli, ["tag", "add", "emotion:happy", "deadbeef00", "--json"],
278 env=_env(repo))
279 data = json.loads(r.output)
280 assert self._REQUIRED <= data.keys()
281 assert data["exit_code"] == 1
282
283 def test_s3_list_commit_not_found_error_schema(self, repo: pathlib.Path) -> None:
284 """S3 β€” list commit not found error JSON has all required fields."""
285 r = runner.invoke(cli, ["tag", "list", "deadbeef00", "--json"], env=_env(repo))
286 data = json.loads(r.output)
287 assert self._REQUIRED <= data.keys()
288
289 def test_s4_remove_invalid_tag_error_schema(self, repo: pathlib.Path) -> None:
290 """S4 β€” remove invalid tag error JSON has all required fields."""
291 r = runner.invoke(cli, ["tag", "remove", "bad\x1btag", "--json"], env=_env(repo))
292 data = json.loads(r.output)
293 assert self._REQUIRED <= data.keys()
294
295
296 # ---------------------------------------------------------------------------
297 # Data integrity
298 # ---------------------------------------------------------------------------
299
300 class TestDataIntegrity:
301 def test_d1_tag_id_sha256_on_add(self, repo: pathlib.Path) -> None:
302 """D1 β€” tag_id from add is sha256: prefixed (71 chars)."""
303 r = runner.invoke(cli, ["tag", "add", "emotion:happy", "--json"], env=_env(repo))
304 data = json.loads(r.output)
305 assert data["tag_id"].startswith("sha256:")
306 assert len(data["tag_id"]) == 71
307
308 def test_d2_tag_id_sha256_on_list(self, tagged_repo: pathlib.Path) -> None:
309 """D2 β€” tag_id in list entries is sha256: prefixed."""
310 r = runner.invoke(cli, ["tag", "list", "--json"], env=_env(tagged_repo))
311 entry = json.loads(r.output)["tags"][0]
312 assert entry["tag_id"].startswith("sha256:")
313
314 def test_d3_commit_id_sha256_on_add(self, repo: pathlib.Path) -> None:
315 """D3 β€” commit_id in add output is sha256: prefixed."""
316 r = runner.invoke(cli, ["tag", "add", "emotion:happy", "--json"], env=_env(repo))
317 data = json.loads(r.output)
318 assert data["commit_id"].startswith("sha256:")
319 assert len(data["commit_id"]) == 71
320
321 def test_d4_commit_id_sha256_on_list(self, tagged_repo: pathlib.Path) -> None:
322 """D4 β€” commit_id in list entries is sha256: prefixed."""
323 r = runner.invoke(cli, ["tag", "list", "--json"], env=_env(tagged_repo))
324 entry = json.loads(r.output)["tags"][0]
325 assert entry["commit_id"].startswith("sha256:")
326 assert len(entry["commit_id"]) == 71
327
328 def test_d5_commit_id_sha256_on_remove(self, tagged_repo: pathlib.Path) -> None:
329 """D5 β€” commit_id in remove output is sha256: prefixed."""
330 r = runner.invoke(cli, ["tag", "remove", "emotion:joyful", "--json"],
331 env=_env(tagged_repo))
332 data = json.loads(r.output)
333 assert data["commit_id"].startswith("sha256:")
334 assert len(data["commit_id"]) == 71
335
336
337 # ---------------------------------------------------------------------------
338 # Performance
339 # ---------------------------------------------------------------------------
340
341 class TestPerformance:
342 def test_p1_duration_ms_is_float(self, repo: pathlib.Path) -> None:
343 """P1 β€” duration_ms must be a float (not int, not None)."""
344 r = runner.invoke(cli, ["tag", "add", "emotion:happy", "--json"], env=_env(repo))
345 data = json.loads(r.output)
346 assert isinstance(data["duration_ms"], float), \
347 f"duration_ms is {type(data['duration_ms']).__name__}, expected float"
348
349 def test_p2_duration_ms_under_5000(self, repo: pathlib.Path) -> None:
350 """P2 β€” duration_ms must be < 5000 ms for a simple add."""
351 r = runner.invoke(cli, ["tag", "add", "emotion:happy", "--json"], env=_env(repo))
352 data = json.loads(r.output)
353 assert data["duration_ms"] < 5000.0
354
355 def test_p3_duration_ms_is_positive(self, repo: pathlib.Path) -> None:
356 """P3 β€” duration_ms must be β‰₯ 0."""
357 for tag in ["emotion:one", "section:two"]:
358 r = runner.invoke(cli, ["tag", "add", tag, "--json"], env=_env(repo))
359 data = json.loads(r.output)
360 assert data["duration_ms"] >= 0.0
361
362
363 # ---------------------------------------------------------------------------
364 # Concurrent reads
365 # ---------------------------------------------------------------------------
366
367 class TestConcurrent:
368 def test_c1_concurrent_list_isolated_repos(self, tmp_path: pathlib.Path) -> None:
369 """C1 β€” concurrent tag list calls on isolated repos return correct counts."""
370 errors: list[str] = []
371 lock = threading.Lock()
372
373 def _build_and_list(idx: int) -> None:
374 try:
375 repo_dir = tmp_path / f"cr_{idx}"
376 repo_dir.mkdir()
377 with _CHDIR_LOCK:
378 saved = os.getcwd()
379 try:
380 os.chdir(repo_dir)
381 runner.invoke(cli, ["init"], env={"MUSE_REPO_ROOT": str(repo_dir)})
382 (repo_dir / "a.mid").write_bytes(b"\x00" * 4)
383 runner.invoke(cli, ["commit", "-m", "c"],
384 env={"MUSE_REPO_ROOT": str(repo_dir)})
385 finally:
386 os.chdir(saved)
387
388 env = {"MUSE_REPO_ROOT": str(repo_dir)}
389 # Add 3 tags
390 for i in range(3):
391 runner.invoke(cli, ["tag", "add", f"ns:tag{i}"], env=env)
392
393 r = runner.invoke(cli, ["tag", "list", "--json"], env=env)
394 data = json.loads(r.output)
395 if data["total"] != 3:
396 with lock:
397 errors.append(f"repo {idx}: expected 3 tags, got {data['total']}")
398 except Exception as exc:
399 with lock:
400 errors.append(f"repo {idx}: {exc}")
401
402 threads = [threading.Thread(target=_build_and_list, args=(i,)) for i in range(5)]
403 for t in threads:
404 t.start()
405 for t in threads:
406 t.join()
407 assert not errors, f"Concurrent errors: {errors}"