"""Supercharge tests for ``muse code compare`` — agent-usability gaps. The existing TestCompare suite in test_code_commands.py covers correctness, JSON schema, all filters (--kind, --file, --language), --stat, --semver, and invalid-ref error paths. This file targets only the gaps those tests leave open: Coverage matrix --------------- - --json / -j: -j alias works identically to --json - exit_code: JSON output includes exit_code = 0 on success - duration_ms: JSON output includes non-negative float duration_ms - TypedDicts: _CompareJson gains exit_code/duration_ms annotations - Docstrings: run() docstring mentions exit_code and duration_ms - ANSI: JSON output never contains terminal escape sequences - Performance: duration_ms stays under 2000 ms for a small repo """ from __future__ import annotations from collections.abc import Mapping import json import pathlib import textwrap import pytest from tests.cli_test_helper import CliRunner, InvokeResult runner = CliRunner() # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _env(root: pathlib.Path) -> Mapping[str, str]: return {"MUSE_REPO_ROOT": str(root)} def _run(root: pathlib.Path, *args: str) -> InvokeResult: return runner.invoke(None, list(args), env=_env(root)) # --------------------------------------------------------------------------- # Fixture — two-commit repo with a semantic change between them # --------------------------------------------------------------------------- @pytest.fixture() def compare_repo( tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> tuple[pathlib.Path, str, str]: """Repo with two commits. Commit A — alpha.py defines alpha_fn(). Commit B — alpha.py also defines beta_fn() (added symbol). Returns (path, commit_id_a, commit_id_b). """ monkeypatch.chdir(tmp_path) r = _run(tmp_path, "init", "--domain", "code") assert r.exit_code == 0, r.output (tmp_path / "alpha.py").write_text(textwrap.dedent("""\ def alpha_fn(): return 1 """)) r = _run(tmp_path, "code", "add", ".") assert r.exit_code == 0, r.output r = _run(tmp_path, "commit", "-m", "add alpha_fn") assert r.exit_code == 0, r.output from muse.core.refs import ( get_head_commit_id, read_current_branch, ) branch = read_current_branch(tmp_path) commit_a = get_head_commit_id(tmp_path, branch) (tmp_path / "alpha.py").write_text(textwrap.dedent("""\ def alpha_fn(): return 1 def beta_fn(): return 2 """)) r = _run(tmp_path, "code", "add", ".") assert r.exit_code == 0, r.output r = _run(tmp_path, "commit", "-m", "add beta_fn") assert r.exit_code == 0, r.output commit_b = get_head_commit_id(tmp_path, branch) assert commit_a is not None assert commit_b is not None return tmp_path, commit_a, commit_b # --------------------------------------------------------------------------- # TestJsonAlias — -j works identically to --json # --------------------------------------------------------------------------- class TestJsonAlias: """-j shorthand must behave identically to --json.""" def test_j_alias_exits_zero( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, b = compare_repo r = _run(root, "code", "compare", a, b, "-j") assert r.exit_code == 0, r.output def test_j_alias_valid_json( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, b = compare_repo r = _run(root, "code", "compare", a, b, "-j") json.loads(r.output) # must not raise def test_j_alias_has_from_key( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, b = compare_repo r = _run(root, "code", "compare", a, b, "-j") data = json.loads(r.output) assert "from" in data def test_j_alias_has_ops_key( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, b = compare_repo r = _run(root, "code", "compare", a, b, "-j") data = json.loads(r.output) assert "ops" in data def test_j_alias_same_top_level_keys_as_json_flag( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, b = compare_repo r1 = _run(root, "code", "compare", a, b, "--json") r2 = _run(root, "code", "compare", a, b, "-j") d1 = json.loads(r1.output) d2 = json.loads(r2.output) d1.pop("duration_ms", None) d2.pop("duration_ms", None) assert set(d1.keys()) == set(d2.keys()) def test_j_alias_op_count_matches_json_flag( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, b = compare_repo r1 = _run(root, "code", "compare", a, b, "--json") r2 = _run(root, "code", "compare", a, b, "-j") assert len(json.loads(r1.output)["ops"]) == len(json.loads(r2.output)["ops"]) def test_j_alias_same_ref_empty_ops( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, _ = compare_repo r = _run(root, "code", "compare", a, a, "-j") assert r.exit_code == 0, r.output assert json.loads(r.output)["ops"] == [] def test_j_alias_with_language_filter( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, b = compare_repo r = _run(root, "code", "compare", a, b, "-j", "--language", "Python") assert r.exit_code == 0, r.output data = json.loads(r.output) assert data["filters"]["language"] == "Python" # --------------------------------------------------------------------------- # TestDurationMs — JSON output must include duration_ms # --------------------------------------------------------------------------- class TestDurationMs: """JSON output must include a non-negative float duration_ms.""" def test_json_has_duration_ms( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, b = compare_repo r = _run(root, "code", "compare", a, b, "--json") data = json.loads(r.output) assert "duration_ms" in data def test_json_duration_ms_nonnegative( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, b = compare_repo r = _run(root, "code", "compare", a, b, "--json") assert json.loads(r.output)["duration_ms"] >= 0 def test_json_duration_ms_is_float( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, b = compare_repo r = _run(root, "code", "compare", a, b, "--json") assert isinstance(json.loads(r.output)["duration_ms"], float) def test_j_alias_duration_ms_present( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, b = compare_repo r = _run(root, "code", "compare", a, b, "-j") assert "duration_ms" in json.loads(r.output) def test_duration_ms_same_ref( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: """duration_ms is present even when there are no changes.""" root, a, _ = compare_repo r = _run(root, "code", "compare", a, a, "--json") data = json.loads(r.output) assert "duration_ms" in data assert data["duration_ms"] >= 0 def test_duration_ms_with_kind_filter( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, b = compare_repo r = _run(root, "code", "compare", a, b, "--json", "--kind", "function") data = json.loads(r.output) assert "duration_ms" in data assert data["duration_ms"] >= 0 # --------------------------------------------------------------------------- # TestExitCode — JSON includes exit_code = 0 on success # --------------------------------------------------------------------------- class TestExitCode: """JSON exit_code must be 0 on success.""" def test_json_has_exit_code( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, b = compare_repo r = _run(root, "code", "compare", a, b, "--json") assert "exit_code" in json.loads(r.output) def test_json_exit_code_zero_with_changes( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, b = compare_repo r = _run(root, "code", "compare", a, b, "--json") assert r.exit_code == 0 assert json.loads(r.output)["exit_code"] == 0 def test_json_exit_code_zero_no_changes( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, _ = compare_repo r = _run(root, "code", "compare", a, a, "--json") assert r.exit_code == 0 assert json.loads(r.output)["exit_code"] == 0 def test_json_exit_code_is_int( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, b = compare_repo r = _run(root, "code", "compare", a, b, "--json") assert isinstance(json.loads(r.output)["exit_code"], int) def test_j_alias_exit_code_present( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, b = compare_repo r = _run(root, "code", "compare", a, b, "-j") assert "exit_code" in json.loads(r.output) def test_exit_code_mirrors_process_exit( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, b = compare_repo r = _run(root, "code", "compare", a, b, "--json") data = json.loads(r.output) assert data["exit_code"] == r.exit_code def test_exit_code_zero_with_filters( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, b = compare_repo r = _run(root, "code", "compare", a, b, "--json", "--kind", "function") assert r.exit_code == 0 assert json.loads(r.output)["exit_code"] == 0 # --------------------------------------------------------------------------- # TestTypedDicts — _CompareJson carries the new fields # --------------------------------------------------------------------------- class TestTypedDicts: """_CompareJson must carry exit_code and duration_ms annotations.""" def test_compare_json_typeddict_exists(self) -> None: from muse.cli.commands.compare import _CompareJson # noqa: F401 def test_has_exit_code_annotation(self) -> None: from muse.cli.commands.compare import _CompareJson assert "exit_code" in _CompareJson.__annotations__ def test_has_duration_ms_annotation(self) -> None: from muse.cli.commands.compare import _CompareJson assert "duration_ms" in _CompareJson.__annotations__ def test_retains_from_annotation(self) -> None: from muse.cli.commands.compare import _CompareJson assert "from" in _CompareJson.__annotations__ def test_retains_to_annotation(self) -> None: from muse.cli.commands.compare import _CompareJson assert "to" in _CompareJson.__annotations__ def test_retains_stat_annotation(self) -> None: from muse.cli.commands.compare import _CompareJson assert "stat" in _CompareJson.__annotations__ def test_retains_ops_annotation(self) -> None: from muse.cli.commands.compare import _CompareJson assert "ops" in _CompareJson.__annotations__ def test_retains_filters_annotation(self) -> None: from muse.cli.commands.compare import _CompareJson assert "filters" in _CompareJson.__annotations__ # --------------------------------------------------------------------------- # TestDocstrings — run() docstring documents new fields # --------------------------------------------------------------------------- class TestDocstrings: """run() must document exit_code.""" def test_run_docstring_documents_fields(self) -> None: from muse.cli.commands.compare import run assert "exit_code" in run.__doc__ # --------------------------------------------------------------------------- # TestAnsiSanitization — no escape codes in JSON output # --------------------------------------------------------------------------- class TestAnsiSanitization: """No ANSI escape sequences anywhere in the JSON output.""" def test_json_output_no_ansi_with_changes( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, b = compare_repo r = _run(root, "code", "compare", a, b, "--json") assert "\x1b" not in r.output def test_j_alias_output_no_ansi( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, b = compare_repo r = _run(root, "code", "compare", a, b, "-j") assert "\x1b" not in r.output def test_json_output_no_ansi_no_changes( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, _ = compare_repo r = _run(root, "code", "compare", a, a, "--json") assert "\x1b" not in r.output # --------------------------------------------------------------------------- # TestPerformance — duration_ms under 2000 ms for a small repo # --------------------------------------------------------------------------- class TestPerformance: """duration_ms must stay under 2000 ms for small repos.""" def test_json_duration_under_2000ms( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, b = compare_repo r = _run(root, "code", "compare", a, b, "--json") assert json.loads(r.output)["duration_ms"] < 2000 def test_j_alias_duration_under_2000ms( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, b = compare_repo r = _run(root, "code", "compare", a, b, "-j") assert json.loads(r.output)["duration_ms"] < 2000 def test_duration_ms_is_float_not_int( self, compare_repo: tuple[pathlib.Path, str, str] ) -> None: root, a, b = compare_repo r = _run(root, "code", "compare", a, b, "--json") assert isinstance(json.loads(r.output)["duration_ms"], float) # --------------------------------------------------------------------------- # Flag registration tests # --------------------------------------------------------------------------- import argparse as _argparse from muse.cli.commands.compare import register as _register_compare def _parse_compare(*args: str) -> _argparse.Namespace: root_p = _argparse.ArgumentParser() subs = root_p.add_subparsers(dest="cmd") _register_compare(subs) return root_p.parse_args(["compare", *args]) class TestRegisterFlags: def test_default_json_out_is_false(self) -> None: ns = _parse_compare("HEAD~1", "HEAD") assert ns.json_out is False def test_json_flag_sets_json_out(self) -> None: ns = _parse_compare("HEAD~1", "HEAD", "--json") assert ns.json_out is True def test_j_shorthand_sets_json_out(self) -> None: ns = _parse_compare("HEAD~1", "HEAD", "-j") assert ns.json_out is True