"""Supercharge tests for ``muse code gravity`` — agent-usability gaps. There are NO existing gravity tests (confirmed: test_cmd_gravity.py is empty, no other gravity test files exist). This file targets both correctness gaps and agent-usability gaps: Coverage matrix --------------- - --json / -j: -j alias works identically to --json (both modes) - exit_code: JSON output includes exit_code = 0 on success (both modes) - duration_ms: JSON output includes non-negative float duration_ms (both) - TypedDicts: _JsonOut and _GravityExplainJson carry exit_code/duration_ms - Docstrings: run() docstring mentions exit_code and duration_ms - ANSI: JSON output never contains terminal escape sequences - Performance: duration_ms stays under 5000 ms for a small repo - Schema: leaderboard JSON has required top-level keys - Explain schema: --explain JSON has required fields - args.as_json: --json flag uses dest="as_json" (idiomatic) Two JSON modes exercised ------------------------ 1. Leaderboard mode: --json / -j → _JsonOut envelope 2. Explain mode: --explain ADDR --json → _GravityExplainJson envelope """ from __future__ import annotations from collections.abc import Mapping import argparse 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 — small Python repo with call-graph structure # --------------------------------------------------------------------------- @pytest.fixture() def gravity_repo( tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> pathlib.Path: """Repo with a simple Python call graph for gravity analysis. core.py: def read_object() ← foundation, called by everything def validate(x) ← helper, called by process service.py: def process(x) ← calls validate, read_object def publish(x) ← calls process api.py: def handle(req) ← calls publish, read_object This gives: read_object: high gravity (called by process, publish, handle) validate: medium gravity (called by process → publish → handle) process: medium gravity (called by publish → handle) publish: lower gravity (called by handle) handle: zero gravity (nobody calls it) """ monkeypatch.chdir(tmp_path) r = _run(tmp_path, "init", "--domain", "code") assert r.exit_code == 0, r.output (tmp_path / "core.py").write_text(textwrap.dedent("""\ def read_object(obj_id): \"\"\"Load an object by ID.\"\"\" return {"id": obj_id} def validate(x): \"\"\"Validate input.\"\"\" if x is None: raise ValueError("x must not be None") return x """)) (tmp_path / "service.py").write_text(textwrap.dedent("""\ from core import read_object, validate def process(x): \"\"\"Process with validation.\"\"\" v = validate(x) obj = read_object(v) return obj def publish(x): \"\"\"Publish a processed result.\"\"\" return process(x) """)) (tmp_path / "api.py").write_text(textwrap.dedent("""\ from service import publish from core import read_object def handle(req): \"\"\"Handle an incoming request.\"\"\" read_object(req) return publish(req) """)) r = _run(tmp_path, "code", "add", ".") assert r.exit_code == 0, r.output r = _run(tmp_path, "commit", "-m", "seed gravity repo") assert r.exit_code == 0, r.output return tmp_path # --------------------------------------------------------------------------- # TestJsonAlias — -j works identically to --json (leaderboard mode) # --------------------------------------------------------------------------- class TestJsonAlias: """-j shorthand must behave identically to --json in leaderboard mode.""" def test_j_alias_exits_zero(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "-j") assert r.exit_code == 0, r.output def test_j_alias_valid_json(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "-j") json.loads(r.output) # must not raise def test_j_alias_has_symbols_key(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "-j") assert "symbols" in json.loads(r.output) def test_j_alias_has_ref_key(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "-j") assert "ref" in json.loads(r.output) def test_j_alias_has_filters_key(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "-j") assert "filters" in json.loads(r.output) def test_j_alias_same_top_level_keys_as_json_flag( self, gravity_repo: pathlib.Path ) -> None: r1 = _run(gravity_repo, "code", "gravity", "--json") r2 = _run(gravity_repo, "code", "gravity", "-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_symbol_count_matches_json_flag( self, gravity_repo: pathlib.Path ) -> None: r1 = _run(gravity_repo, "code", "gravity", "--json") r2 = _run(gravity_repo, "code", "gravity", "-j") assert len(json.loads(r1.output)["symbols"]) == len( json.loads(r2.output)["symbols"] ) def test_j_alias_with_top_filter(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "-j", "--top", "2") assert r.exit_code == 0, r.output assert len(json.loads(r.output)["symbols"]) <= 2 def test_j_alias_with_min_gravity(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "-j", "--min-gravity", "0") assert r.exit_code == 0, r.output data = json.loads(r.output) assert "symbols" in data # --------------------------------------------------------------------------- # TestDurationMs — JSON output must include duration_ms in both modes # --------------------------------------------------------------------------- class TestDurationMs: """Every JSON path must include a non-negative float duration_ms.""" def test_json_has_duration_ms_leaderboard(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json") assert "duration_ms" in json.loads(r.output) def test_json_duration_ms_nonnegative(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json") assert json.loads(r.output)["duration_ms"] >= 0 def test_json_duration_ms_is_float(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json") assert isinstance(json.loads(r.output)["duration_ms"], float) def test_j_alias_duration_ms_present(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "-j") assert "duration_ms" in json.loads(r.output) def test_duration_ms_with_top_filter(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json", "--top", "2") data = json.loads(r.output) assert "duration_ms" in data assert data["duration_ms"] >= 0 def test_duration_ms_explain_mode(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json", "--explain", "core.py::read_object") assert r.exit_code == 0, r.output data = json.loads(r.output) assert "duration_ms" in data assert isinstance(data["duration_ms"], float) assert data["duration_ms"] >= 0 def test_j_alias_duration_ms_explain(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "-j", "--explain", "core.py::read_object") assert r.exit_code == 0, r.output assert "duration_ms" in json.loads(r.output) # --------------------------------------------------------------------------- # TestExitCode — JSON includes exit_code = 0 on success (both modes) # --------------------------------------------------------------------------- class TestExitCode: """JSON exit_code must be 0 on success in both leaderboard and explain modes.""" def test_json_has_exit_code_leaderboard(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json") assert "exit_code" in json.loads(r.output) def test_json_exit_code_zero_leaderboard(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json") assert r.exit_code == 0 assert json.loads(r.output)["exit_code"] == 0 def test_json_exit_code_is_int_leaderboard(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json") assert isinstance(json.loads(r.output)["exit_code"], int) def test_j_alias_exit_code_present(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "-j") assert "exit_code" in json.loads(r.output) def test_exit_code_mirrors_process_exit(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json") assert json.loads(r.output)["exit_code"] == r.exit_code def test_json_has_exit_code_explain(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json", "--explain", "core.py::read_object") assert r.exit_code == 0, r.output assert "exit_code" in json.loads(r.output) def test_json_exit_code_zero_explain(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json", "--explain", "core.py::read_object") assert r.exit_code == 0 assert json.loads(r.output)["exit_code"] == 0 def test_exit_code_is_int_explain(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json", "--explain", "core.py::read_object") assert isinstance(json.loads(r.output)["exit_code"], int) def test_exit_code_mirrors_process_exit_explain( self, gravity_repo: pathlib.Path ) -> None: r = _run(gravity_repo, "code", "gravity", "--json", "--explain", "core.py::read_object") assert json.loads(r.output)["exit_code"] == r.exit_code # --------------------------------------------------------------------------- # TestTypedDicts — TypedDicts carry exit_code and duration_ms # --------------------------------------------------------------------------- class TestTypedDicts: """_JsonOut and _GravityExplainJson must carry exit_code and duration_ms.""" def test_json_out_typeddict_exists(self) -> None: from muse.cli.commands.gravity import _JsonOut # noqa: F401 def test_json_out_has_exit_code_annotation(self) -> None: from muse.cli.commands.gravity import _JsonOut assert "exit_code" in _JsonOut.__annotations__ def test_json_out_has_duration_ms_annotation(self) -> None: from muse.cli.commands.gravity import _JsonOut assert "duration_ms" in _JsonOut.__annotations__ def test_json_out_retains_symbols_annotation(self) -> None: from muse.cli.commands.gravity import _JsonOut assert "symbols" in _JsonOut.__annotations__ def test_json_out_retains_filters_annotation(self) -> None: from muse.cli.commands.gravity import _JsonOut assert "filters" in _JsonOut.__annotations__ def test_gravity_explain_json_exists(self) -> None: from muse.cli.commands.gravity import _GravityExplainJson # noqa: F401 def test_gravity_explain_json_has_exit_code(self) -> None: from muse.cli.commands.gravity import _GravityExplainJson assert "exit_code" in _GravityExplainJson.__annotations__ def test_gravity_explain_json_has_duration_ms(self) -> None: from muse.cli.commands.gravity import _GravityExplainJson assert "duration_ms" in _GravityExplainJson.__annotations__ def test_gravity_explain_json_has_address(self) -> None: from muse.cli.commands.gravity import _GravityExplainJson assert "address" in _GravityExplainJson.__annotations__ def test_gravity_explain_json_has_gravity_pct(self) -> None: from muse.cli.commands.gravity import _GravityExplainJson assert "gravity_pct" in _GravityExplainJson.__annotations__ # --------------------------------------------------------------------------- # TestAnsiSanitization — no escape codes in JSON output # --------------------------------------------------------------------------- class TestAnsiSanitization: """No ANSI escape sequences anywhere in the JSON output.""" def test_json_output_no_ansi_leaderboard(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json") assert "\x1b" not in r.output def test_j_alias_output_no_ansi(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "-j") assert "\x1b" not in r.output def test_json_output_no_ansi_explain(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json", "--explain", "core.py::read_object") assert "\x1b" not in r.output # --------------------------------------------------------------------------- # TestLeaderboardSchema — JSON shape for leaderboard mode # --------------------------------------------------------------------------- class TestLeaderboardSchema: """Leaderboard JSON must carry the documented top-level keys.""" def test_has_ref_key(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json") assert "ref" in json.loads(r.output) def test_has_snapshot_id_key(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json") assert "snapshot_id" in json.loads(r.output) def test_has_total_production_symbols_key(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json") assert "total_production_symbols" in json.loads(r.output) def test_has_include_tests_key(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json") assert "include_tests" in json.loads(r.output) def test_include_tests_is_false_by_default(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json") assert json.loads(r.output)["include_tests"] is False def test_symbols_is_list(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json") assert isinstance(json.loads(r.output)["symbols"], list) def test_symbol_entries_have_gravity_pct(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json") data = json.loads(r.output) for sym in data["symbols"]: assert "gravity_pct" in sym def test_symbol_entries_have_address(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json") data = json.loads(r.output) for sym in data["symbols"]: assert "address" in sym def test_top_filter_bounds_symbols(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json", "--top", "2") data = json.loads(r.output) assert len(data["symbols"]) <= 2 # --------------------------------------------------------------------------- # TestExplainSchema — JSON shape for --explain mode # --------------------------------------------------------------------------- class TestExplainSchema: """Explain JSON must carry the documented fields.""" def test_explain_has_address(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json", "--explain", "core.py::read_object") assert r.exit_code == 0, r.output assert "address" in json.loads(r.output) def test_explain_has_gravity_pct(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json", "--explain", "core.py::read_object") data = json.loads(r.output) assert "gravity_pct" in data assert isinstance(data["gravity_pct"], float) def test_explain_has_direct_dependents(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json", "--explain", "core.py::read_object") assert "direct_dependents" in json.loads(r.output) def test_explain_has_depth_distribution(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json", "--explain", "core.py::read_object") assert "depth_distribution" in json.loads(r.output) def test_explain_address_matches_flag(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json", "--explain", "core.py::validate") assert r.exit_code == 0, r.output data = json.loads(r.output) assert data["address"] == "core.py::validate" # --------------------------------------------------------------------------- # TestPerformance — duration_ms under 5000 ms for a small repo # --------------------------------------------------------------------------- class TestPerformance: """duration_ms must stay under 5000 ms for small repos (AST parse overhead).""" def test_leaderboard_duration_under_5000ms(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json") assert json.loads(r.output)["duration_ms"] < 5000 def test_explain_duration_under_5000ms(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json", "--explain", "core.py::read_object") assert json.loads(r.output)["duration_ms"] < 5000 def test_duration_ms_is_float_not_int(self, gravity_repo: pathlib.Path) -> None: r = _run(gravity_repo, "code", "gravity", "--json") assert isinstance(json.loads(r.output)["duration_ms"], float) # --------------------------------------------------------------------------- # TestRegisterFlags — argparse-level verification # --------------------------------------------------------------------------- class TestRegisterFlags: """Verify that register() wires --json / -j correctly.""" def _make_parser(self) -> "argparse.ArgumentParser": import argparse from muse.cli.commands.gravity import register ap = argparse.ArgumentParser() subs = ap.add_subparsers() register(subs) return ap def test_json_flag_long(self) -> None: ns = self._make_parser().parse_args(["gravity", "--json"]) assert ns.json_out is True def test_j_alias(self) -> None: ns = self._make_parser().parse_args(["gravity", "-j"]) assert ns.json_out is True def test_default_is_text(self) -> None: ns = self._make_parser().parse_args(["gravity"]) assert ns.json_out is False def test_dest_is_json_out(self) -> None: ns = self._make_parser().parse_args(["gravity", "-j"]) assert hasattr(ns, "json_out") assert not hasattr(ns, "fmt")