"""Supercharge tests for ``muse tag``. Covers gaps identified in the review: - duration_ms + exit_code in all JSON outputs (add / list / remove) - JSON errors emitted to stdout (not stderr) when --json is set - _TagJson TypedDict schema completeness with new telemetry fields - Tag ID is sha256-prefixed, not UUID - Docstring correctness (schema in help text) Unit ---- U1 _TagJson TypedDict has duration_ms and exit_code fields U2 _emit_error helper is present in module (or equivalent pattern) U3 format error in JSON mode goes to stdout U4 tag_id format is sha256 prefix, not UUID U5 commit_id in JSON output is sha256-prefixed Integration — duration_ms / exit_code ------------------------------------- I1 tag add --json includes duration_ms (float ≥ 0) I2 tag add --json includes exit_code == 0 on success I3 tag add --json --dry-run includes duration_ms and exit_code == 0 I4 tag add already_tagged includes duration_ms and exit_code == 0 I5 tag list --json includes duration_ms and exit_code == 0 I6 tag list --json empty repo includes duration_ms and exit_code == 0 I7 tag remove --json includes duration_ms and exit_code == 0 I8 tag remove --json not_found includes duration_ms and exit_code == 0 Integration — JSON errors to stdout ------------------------------------- E1 tag add invalid tag name --json → JSON error on stdout, not stderr E2 tag add commit not found --json → JSON error on stdout E3 tag add bad format --json → handled (format validated before JSON flag seen, but error must still exit nonzero) E4 tag list commit not found --json → JSON error on stdout E5 tag remove invalid tag name --json → JSON error on stdout E6 tag remove commit not found --json → JSON error on stdout Error JSON schema ----------------- S1 add invalid-tag error JSON has "error", "message", "duration_ms", "exit_code" S2 add commit-not-found error JSON has expected keys S3 list commit-not-found error JSON has expected keys S4 remove invalid-tag error JSON has expected keys Data integrity -------------- D1 tag_id is sha256-prefixed string (not UUID) on add D2 tag_id is sha256-prefixed on list D3 commit_id is sha256-prefixed on add D4 commit_id is sha256-prefixed on list D5 commit_id is sha256-prefixed on remove Performance ----------- P1 duration_ms is a float (not int, not None) P2 duration_ms is under 5000 ms for a simple add P3 duration_ms values across two sequential calls are both positive Concurrent reads ---------------- C1 concurrent tag list calls on isolated repos produce correct counts """ from __future__ import annotations from collections.abc import Mapping import json import os import pathlib import threading import time import pytest from tests.cli_test_helper import CliRunner cli = None runner = CliRunner() _CHDIR_LOCK = threading.Lock() def _env(root: pathlib.Path) -> Mapping[str, str]: return {"MUSE_REPO_ROOT": str(root)} @pytest.fixture() def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path: monkeypatch.chdir(tmp_path) monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path)) runner.invoke(cli, ["init"], env=_env(tmp_path), catch_exceptions=False) (tmp_path / "song.mid").write_bytes(b"\x00" * 16) runner.invoke(cli, ["commit", "-m", "base"], env=_env(tmp_path), catch_exceptions=False) return tmp_path @pytest.fixture() def tagged_repo(repo: pathlib.Path) -> pathlib.Path: runner.invoke(cli, ["tag", "add", "emotion:joyful"], env=_env(repo), catch_exceptions=False) return repo # --------------------------------------------------------------------------- # Unit # --------------------------------------------------------------------------- class TestUnit: def test_u1_typeddict_has_duration_ms(self) -> None: """U1 — _TagAddJson TypedDict must declare duration_ms field.""" from muse.cli.commands.tag import _TagAddJson import typing hints = typing.get_type_hints(_TagAddJson) assert "duration_ms" in hints, "_TagAddJson missing duration_ms" def test_u2_typeddict_has_exit_code(self) -> None: """U2 — _TagAddJson TypedDict must declare exit_code field.""" from muse.cli.commands.tag import _TagAddJson import typing hints = typing.get_type_hints(_TagAddJson) assert "exit_code" in hints, "_TagAddJson missing exit_code" def test_u3_format_error_json_mode(self, repo: pathlib.Path) -> None: """U3 — Invalid format: --json is ambiguous (format set before --json), but exit nonzero.""" r = runner.invoke(cli, ["tag", "add", "emotion:happy", "--format", "bad"], env=_env(repo)) assert r.exit_code != 0 def test_u4_tag_id_is_sha256(self, repo: pathlib.Path) -> None: """U4 — tag_id must use sha256: prefix, not UUID.""" r = runner.invoke(cli, ["tag", "add", "emotion:happy", "--json"], env=_env(repo)) data = json.loads(r.output) assert data["tag_id"].startswith("sha256:"), f"tag_id not sha256: got {data['tag_id']!r}" def test_u5_commit_id_is_sha256(self, repo: pathlib.Path) -> None: """U5 — commit_id in add output must use sha256: prefix.""" r = runner.invoke(cli, ["tag", "add", "emotion:happy", "--json"], env=_env(repo)) data = json.loads(r.output) assert data["commit_id"].startswith("sha256:"), \ f"commit_id not sha256: got {data['commit_id']!r}" # --------------------------------------------------------------------------- # Integration — duration_ms / exit_code in every success path # --------------------------------------------------------------------------- class TestElapsedMsExitCode: def test_i1_add_has_duration_ms(self, repo: pathlib.Path) -> None: """I1 — tag add --json success includes duration_ms.""" r = runner.invoke(cli, ["tag", "add", "emotion:happy", "--json"], env=_env(repo)) assert r.exit_code == 0 data = json.loads(r.output) assert "duration_ms" in data, f"Missing duration_ms in: {data}" def test_i2_add_has_exit_code_zero(self, repo: pathlib.Path) -> None: """I2 — tag add --json success includes exit_code == 0.""" r = runner.invoke(cli, ["tag", "add", "emotion:happy", "--json"], env=_env(repo)) data = json.loads(r.output) assert data["exit_code"] == 0 def test_i3_add_dry_run_has_duration_ms(self, repo: pathlib.Path) -> None: """I3 — tag add --dry-run --json includes duration_ms and exit_code.""" r = runner.invoke(cli, ["tag", "add", "emotion:happy", "--dry-run", "--json"], env=_env(repo), catch_exceptions=False) data = json.loads(r.output) assert "duration_ms" in data assert data["exit_code"] == 0 def test_i4_add_already_tagged_has_duration_ms(self, tagged_repo: pathlib.Path) -> None: """I4 — tag add already_tagged includes duration_ms and exit_code.""" r = runner.invoke(cli, ["tag", "add", "emotion:joyful", "--json"], env=_env(tagged_repo), catch_exceptions=False) data = json.loads(r.output) assert data["status"] == "already_tagged" assert "duration_ms" in data assert data["exit_code"] == 0 def test_i5_list_has_duration_ms(self, tagged_repo: pathlib.Path) -> None: """I5 — tag list --json includes duration_ms and exit_code.""" r = runner.invoke(cli, ["tag", "list", "--json"], env=_env(tagged_repo), catch_exceptions=False) data = json.loads(r.output) assert "duration_ms" in data assert data["exit_code"] == 0 def test_i6_list_empty_has_duration_ms(self, repo: pathlib.Path) -> None: """I6 — tag list --json empty repo includes duration_ms.""" r = runner.invoke(cli, ["tag", "list", "--json"], env=_env(repo), catch_exceptions=False) data = json.loads(r.output) assert "duration_ms" in data assert data["total"] == 0 def test_i7_remove_has_duration_ms(self, tagged_repo: pathlib.Path) -> None: """I7 — tag remove --json includes duration_ms and exit_code.""" r = runner.invoke(cli, ["tag", "remove", "emotion:joyful", "--json"], env=_env(tagged_repo), catch_exceptions=False) data = json.loads(r.output) assert "duration_ms" in data assert data["exit_code"] == 0 def test_i8_remove_not_found_has_duration_ms(self, repo: pathlib.Path) -> None: """I8 — tag remove not_found --json includes duration_ms.""" r = runner.invoke(cli, ["tag", "remove", "nonexistent:tag", "--json"], env=_env(repo), catch_exceptions=False) data = json.loads(r.output) assert data["status"] == "not_found" assert "duration_ms" in data assert data["exit_code"] == 0 # --------------------------------------------------------------------------- # Integration — JSON errors go to stdout in --json mode # --------------------------------------------------------------------------- class TestJsonErrorsToStdout: def test_e1_add_invalid_tag_json_to_stdout(self, repo: pathlib.Path) -> None: """E1 — invalid tag name + --json → JSON error on stdout.""" r = runner.invoke(cli, ["tag", "add", "bad\x1btag", "--json"], env=_env(repo)) assert r.exit_code != 0 # stdout must be parseable JSON data = json.loads(r.output) assert "error" in data def test_e2_add_commit_not_found_json_to_stdout(self, repo: pathlib.Path) -> None: """E2 — commit not found + --json → JSON error on stdout.""" r = runner.invoke(cli, ["tag", "add", "emotion:happy", "deadbeef00", "--json"], env=_env(repo)) assert r.exit_code != 0 data = json.loads(r.output) assert "error" in data def test_e3_bad_format_exits_nonzero(self, repo: pathlib.Path) -> None: """E3 — bad --format exits nonzero.""" r = runner.invoke(cli, ["tag", "add", "emotion:happy", "--format", "bad"], env=_env(repo)) assert r.exit_code != 0 def test_e4_list_commit_not_found_json_to_stdout(self, repo: pathlib.Path) -> None: """E4 — tag list commit not found + --json → JSON error on stdout.""" r = runner.invoke(cli, ["tag", "list", "deadbeef00", "--json"], env=_env(repo)) assert r.exit_code != 0 data = json.loads(r.output) assert "error" in data def test_e5_remove_invalid_tag_json_to_stdout(self, repo: pathlib.Path) -> None: """E5 — tag remove invalid tag name + --json → JSON error on stdout.""" r = runner.invoke(cli, ["tag", "remove", "bad\x1btag", "--json"], env=_env(repo)) assert r.exit_code != 0 data = json.loads(r.output) assert "error" in data def test_e6_remove_commit_not_found_json_to_stdout(self, repo: pathlib.Path) -> None: """E6 — tag remove commit not found + --json → JSON error on stdout.""" r = runner.invoke(cli, ["tag", "remove", "emotion:happy", "deadbeef00", "--json"], env=_env(repo)) assert r.exit_code != 0 data = json.loads(r.output) assert "error" in data # --------------------------------------------------------------------------- # Error JSON schema # --------------------------------------------------------------------------- class TestErrorJsonSchema: _REQUIRED = {"error", "message", "duration_ms", "exit_code"} def test_s1_add_invalid_tag_error_schema(self, repo: pathlib.Path) -> None: """S1 — invalid tag error JSON has all required fields.""" r = runner.invoke(cli, ["tag", "add", "bad\x1btag", "--json"], env=_env(repo)) data = json.loads(r.output) assert self._REQUIRED <= data.keys(), f"Missing keys: {self._REQUIRED - data.keys()}" def test_s2_add_commit_not_found_error_schema(self, repo: pathlib.Path) -> None: """S2 — commit not found error JSON has all required fields.""" r = runner.invoke(cli, ["tag", "add", "emotion:happy", "deadbeef00", "--json"], env=_env(repo)) data = json.loads(r.output) assert self._REQUIRED <= data.keys() assert data["exit_code"] == 1 def test_s3_list_commit_not_found_error_schema(self, repo: pathlib.Path) -> None: """S3 — list commit not found error JSON has all required fields.""" r = runner.invoke(cli, ["tag", "list", "deadbeef00", "--json"], env=_env(repo)) data = json.loads(r.output) assert self._REQUIRED <= data.keys() def test_s4_remove_invalid_tag_error_schema(self, repo: pathlib.Path) -> None: """S4 — remove invalid tag error JSON has all required fields.""" r = runner.invoke(cli, ["tag", "remove", "bad\x1btag", "--json"], env=_env(repo)) data = json.loads(r.output) assert self._REQUIRED <= data.keys() # --------------------------------------------------------------------------- # Data integrity # --------------------------------------------------------------------------- class TestDataIntegrity: def test_d1_tag_id_sha256_on_add(self, repo: pathlib.Path) -> None: """D1 — tag_id from add is sha256: prefixed (71 chars).""" r = runner.invoke(cli, ["tag", "add", "emotion:happy", "--json"], env=_env(repo)) data = json.loads(r.output) assert data["tag_id"].startswith("sha256:") assert len(data["tag_id"]) == 71 def test_d2_tag_id_sha256_on_list(self, tagged_repo: pathlib.Path) -> None: """D2 — tag_id in list entries is sha256: prefixed.""" r = runner.invoke(cli, ["tag", "list", "--json"], env=_env(tagged_repo)) entry = json.loads(r.output)["tags"][0] assert entry["tag_id"].startswith("sha256:") def test_d3_commit_id_sha256_on_add(self, repo: pathlib.Path) -> None: """D3 — commit_id in add output is sha256: prefixed.""" r = runner.invoke(cli, ["tag", "add", "emotion:happy", "--json"], env=_env(repo)) data = json.loads(r.output) assert data["commit_id"].startswith("sha256:") assert len(data["commit_id"]) == 71 def test_d4_commit_id_sha256_on_list(self, tagged_repo: pathlib.Path) -> None: """D4 — commit_id in list entries is sha256: prefixed.""" r = runner.invoke(cli, ["tag", "list", "--json"], env=_env(tagged_repo)) entry = json.loads(r.output)["tags"][0] assert entry["commit_id"].startswith("sha256:") assert len(entry["commit_id"]) == 71 def test_d5_commit_id_sha256_on_remove(self, tagged_repo: pathlib.Path) -> None: """D5 — commit_id in remove output is sha256: prefixed.""" r = runner.invoke(cli, ["tag", "remove", "emotion:joyful", "--json"], env=_env(tagged_repo)) data = json.loads(r.output) assert data["commit_id"].startswith("sha256:") assert len(data["commit_id"]) == 71 # --------------------------------------------------------------------------- # Performance # --------------------------------------------------------------------------- class TestPerformance: def test_p1_duration_ms_is_float(self, repo: pathlib.Path) -> None: """P1 — duration_ms must be a float (not int, not None).""" r = runner.invoke(cli, ["tag", "add", "emotion:happy", "--json"], env=_env(repo)) data = json.loads(r.output) assert isinstance(data["duration_ms"], float), \ f"duration_ms is {type(data['duration_ms']).__name__}, expected float" def test_p2_duration_ms_under_5000(self, repo: pathlib.Path) -> None: """P2 — duration_ms must be < 5000 ms for a simple add.""" r = runner.invoke(cli, ["tag", "add", "emotion:happy", "--json"], env=_env(repo)) data = json.loads(r.output) assert data["duration_ms"] < 5000.0 def test_p3_duration_ms_is_positive(self, repo: pathlib.Path) -> None: """P3 — duration_ms must be ≥ 0.""" for tag in ["emotion:one", "section:two"]: r = runner.invoke(cli, ["tag", "add", tag, "--json"], env=_env(repo)) data = json.loads(r.output) assert data["duration_ms"] >= 0.0 # --------------------------------------------------------------------------- # Concurrent reads # --------------------------------------------------------------------------- class TestConcurrent: def test_c1_concurrent_list_isolated_repos(self, tmp_path: pathlib.Path) -> None: """C1 — concurrent tag list calls on isolated repos return correct counts.""" errors: list[str] = [] lock = threading.Lock() def _build_and_list(idx: int) -> None: try: repo_dir = tmp_path / f"cr_{idx}" repo_dir.mkdir() with _CHDIR_LOCK: saved = os.getcwd() try: os.chdir(repo_dir) runner.invoke(cli, ["init"], env={"MUSE_REPO_ROOT": str(repo_dir)}) (repo_dir / "a.mid").write_bytes(b"\x00" * 4) runner.invoke(cli, ["commit", "-m", "c"], env={"MUSE_REPO_ROOT": str(repo_dir)}) finally: os.chdir(saved) env = {"MUSE_REPO_ROOT": str(repo_dir)} # Add 3 tags for i in range(3): runner.invoke(cli, ["tag", "add", f"ns:tag{i}"], env=env) r = runner.invoke(cli, ["tag", "list", "--json"], env=env) data = json.loads(r.output) if data["total"] != 3: with lock: errors.append(f"repo {idx}: expected 3 tags, got {data['total']}") except Exception as exc: with lock: errors.append(f"repo {idx}: {exc}") threads = [threading.Thread(target=_build_and_list, args=(i,)) for i in range(5)] for t in threads: t.start() for t in threads: t.join() assert not errors, f"Concurrent errors: {errors}"