"""Supercharge tests for ``muse code checkout-symbol`` — agent-usability gaps. The existing test_cmd_checkout_symbol.py already covers correctness, JSON schema, E2E round-trips, stress, and post-write verification. This file targets only the gaps those tests leave open: Coverage matrix --------------- - --json / -j: -j alias works identically to --json - exit_code: all three JSON paths (no-op, dry-run, write) include exit_code = 0 - duration_ms: all three JSON paths include non-negative float duration_ms - TypedDicts: _CheckoutSymbolOutputJson 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 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 — repo with two commits so checkout-symbol has history to restore # --------------------------------------------------------------------------- @pytest.fixture() def cs_repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path: """Code-domain repo with two commits on billing.py. Commit 1 (v1): compute_total returns sum(items) Commit 2 (v2): compute_total returns round(sum(items), 2) checkout-symbol --commit restores the original body. The current working tree matches commit 2. """ monkeypatch.chdir(tmp_path) r = _run(tmp_path, "init", "--domain", "code") assert r.exit_code == 0, r.output (tmp_path / "billing.py").write_text(textwrap.dedent("""\ class Invoice: def compute_total(self, items): return sum(items) def add_tax(self, rate): return rate """)) r1 = _run(tmp_path, "code", "add", "billing.py") assert r1.exit_code == 0, r1.output r2 = _run(tmp_path, "commit", "-m", "v1 billing") assert r2.exit_code == 0, r2.output (tmp_path / "billing.py").write_text(textwrap.dedent("""\ class Invoice: def compute_total(self, items): return round(sum(items), 2) def add_tax(self, rate): return rate """)) r3 = _run(tmp_path, "code", "add", "billing.py") assert r3.exit_code == 0, r3.output r4 = _run(tmp_path, "commit", "-m", "v2 billing") assert r4.exit_code == 0, r4.output return tmp_path def _v1_commit_id(root: pathlib.Path) -> str: """Return the full commit_id of the first (v1) commit.""" r = runner.invoke(None, ["log", "--json"], env=_env(root)) commits = json.loads(r.output)["commits"] # commits are newest-first; v1 is last return commits[-1]["commit_id"] ADDRESS = "billing.py::Invoice.compute_total" # --------------------------------------------------------------------------- # TestJsonAlias — -j works identically to --json # --------------------------------------------------------------------------- class TestJsonAlias: """-j shorthand must behave identically to --json.""" def test_j_alias_dry_run_exits_zero(self, cs_repo: pathlib.Path) -> None: v1 = _v1_commit_id(cs_repo) r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "-j") assert r.exit_code == 0, r.output def test_j_alias_dry_run_valid_json(self, cs_repo: pathlib.Path) -> None: v1 = _v1_commit_id(cs_repo) r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "-j") json.loads(r.output) # must not raise def test_j_alias_has_address_key(self, cs_repo: pathlib.Path) -> None: v1 = _v1_commit_id(cs_repo) r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "-j") data = json.loads(r.output) assert "address" in data def test_j_alias_has_changed_key(self, cs_repo: pathlib.Path) -> None: v1 = _v1_commit_id(cs_repo) r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "-j") data = json.loads(r.output) assert "changed" in data def test_j_alias_same_top_level_keys_as_json_flag(self, cs_repo: pathlib.Path) -> None: v1 = _v1_commit_id(cs_repo) r1 = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "--json") r2 = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "-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_write_path_exits_zero(self, cs_repo: pathlib.Path) -> None: v1 = _v1_commit_id(cs_repo) r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "-j") assert r.exit_code == 0, r.output def test_j_alias_write_path_valid_json(self, cs_repo: pathlib.Path) -> None: v1 = _v1_commit_id(cs_repo) r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "-j") json.loads(r.output) # must not raise # --------------------------------------------------------------------------- # TestDurationMs — all three JSON paths include duration_ms # --------------------------------------------------------------------------- class TestDurationMs: """Every JSON code path must emit a non-negative float duration_ms.""" def test_dry_run_has_duration_ms(self, cs_repo: pathlib.Path) -> None: v1 = _v1_commit_id(cs_repo) r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "--json") data = json.loads(r.output) assert "duration_ms" in data def test_dry_run_duration_ms_nonnegative(self, cs_repo: pathlib.Path) -> None: v1 = _v1_commit_id(cs_repo) r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "--json") data = json.loads(r.output) assert data["duration_ms"] >= 0 def test_dry_run_duration_ms_is_float(self, cs_repo: pathlib.Path) -> None: v1 = _v1_commit_id(cs_repo) r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "--json") data = json.loads(r.output) assert isinstance(data["duration_ms"], float) def test_write_path_has_duration_ms(self, cs_repo: pathlib.Path) -> None: v1 = _v1_commit_id(cs_repo) r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--json") data = json.loads(r.output) assert "duration_ms" in data def test_write_path_duration_ms_nonnegative(self, cs_repo: pathlib.Path) -> None: v1 = _v1_commit_id(cs_repo) r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--json") data = json.loads(r.output) assert data["duration_ms"] >= 0 def test_noop_path_has_duration_ms(self, cs_repo: pathlib.Path) -> None: """No-op: restore HEAD commit → symbol already matches.""" r_log = runner.invoke(None, ["log", "--json"], env=_env(cs_repo)) head_id = json.loads(r_log.output)["commits"][0]["commit_id"] r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", head_id, "--json") data = json.loads(r.output) assert "duration_ms" in data assert data["duration_ms"] >= 0 def test_j_alias_dry_run_duration_ms_present(self, cs_repo: pathlib.Path) -> None: v1 = _v1_commit_id(cs_repo) r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "-j") data = json.loads(r.output) assert "duration_ms" in data # --------------------------------------------------------------------------- # TestExitCode — all three JSON paths include exit_code = 0 # --------------------------------------------------------------------------- class TestExitCode: """All JSON paths must carry exit_code = 0 (errors exit before JSON is emitted).""" def test_dry_run_has_exit_code(self, cs_repo: pathlib.Path) -> None: v1 = _v1_commit_id(cs_repo) r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "--json") data = json.loads(r.output) assert "exit_code" in data def test_dry_run_exit_code_zero(self, cs_repo: pathlib.Path) -> None: v1 = _v1_commit_id(cs_repo) r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "--json") assert r.exit_code == 0 data = json.loads(r.output) assert data["exit_code"] == 0 def test_dry_run_exit_code_is_int(self, cs_repo: pathlib.Path) -> None: v1 = _v1_commit_id(cs_repo) r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "--json") data = json.loads(r.output) assert isinstance(data["exit_code"], int) def test_write_path_has_exit_code(self, cs_repo: pathlib.Path) -> None: v1 = _v1_commit_id(cs_repo) r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--json") data = json.loads(r.output) assert "exit_code" in data def test_write_path_exit_code_zero(self, cs_repo: pathlib.Path) -> None: v1 = _v1_commit_id(cs_repo) r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--json") assert r.exit_code == 0 data = json.loads(r.output) assert data["exit_code"] == 0 def test_noop_path_has_exit_code(self, cs_repo: pathlib.Path) -> None: r_log = runner.invoke(None, ["log", "--json"], env=_env(cs_repo)) head_id = json.loads(r_log.output)["commits"][0]["commit_id"] r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", head_id, "--json") data = json.loads(r.output) assert "exit_code" in data def test_noop_path_exit_code_zero(self, cs_repo: pathlib.Path) -> None: r_log = runner.invoke(None, ["log", "--json"], env=_env(cs_repo)) head_id = json.loads(r_log.output)["commits"][0]["commit_id"] r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", head_id, "--json") assert r.exit_code == 0 data = json.loads(r.output) assert data["exit_code"] == 0 def test_exit_code_mirrors_process_exit_dry_run(self, cs_repo: pathlib.Path) -> None: v1 = _v1_commit_id(cs_repo) r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "--json") data = json.loads(r.output) assert data["exit_code"] == r.exit_code def test_exit_code_mirrors_process_exit_write(self, cs_repo: pathlib.Path) -> None: v1 = _v1_commit_id(cs_repo) r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--json") data = json.loads(r.output) assert data["exit_code"] == r.exit_code # --------------------------------------------------------------------------- # TestTypedDicts — _CheckoutSymbolOutputJson carries the new fields # --------------------------------------------------------------------------- class TestTypedDicts: """_CheckoutSymbolOutputJson must carry exit_code and duration_ms.""" def test_checkout_symbol_output_json_exists(self) -> None: from muse.cli.commands.checkout_symbol import _CheckoutSymbolOutputJson # noqa: F401 def test_has_exit_code_annotation(self) -> None: from muse.cli.commands.checkout_symbol import _CheckoutSymbolOutputJson assert "exit_code" in _CheckoutSymbolOutputJson.__annotations__ def test_has_duration_ms_annotation(self) -> None: from muse.cli.commands.checkout_symbol import _CheckoutSymbolOutputJson assert "duration_ms" in _CheckoutSymbolOutputJson.__annotations__ def test_retains_address_annotation(self) -> None: from muse.cli.commands.checkout_symbol import _CheckoutSymbolOutputJson assert "address" in _CheckoutSymbolOutputJson.__annotations__ def test_retains_changed_annotation(self) -> None: from muse.cli.commands.checkout_symbol import _CheckoutSymbolOutputJson assert "changed" in _CheckoutSymbolOutputJson.__annotations__ def test_retains_dry_run_annotation(self) -> None: from muse.cli.commands.checkout_symbol import _CheckoutSymbolOutputJson assert "dry_run" in _CheckoutSymbolOutputJson.__annotations__ def test_retains_verified_annotation(self) -> None: from muse.cli.commands.checkout_symbol import _CheckoutSymbolOutputJson assert "verified" in _CheckoutSymbolOutputJson.__annotations__ # --------------------------------------------------------------------------- # TestDocstrings — run() docstring documents new fields # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # TestAnsiSanitization — no escape codes in JSON output # --------------------------------------------------------------------------- class TestAnsiSanitization: """No ANSI escape sequences anywhere in the JSON output.""" def test_dry_run_json_no_ansi(self, cs_repo: pathlib.Path) -> None: v1 = _v1_commit_id(cs_repo) r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "--json") assert "\x1b" not in r.output def test_write_json_no_ansi(self, cs_repo: pathlib.Path) -> None: v1 = _v1_commit_id(cs_repo) r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--json") assert "\x1b" not in r.output def test_noop_json_no_ansi(self, cs_repo: pathlib.Path) -> None: r_log = runner.invoke(None, ["log", "--json"], env=_env(cs_repo)) head_id = json.loads(r_log.output)["commits"][0]["commit_id"] r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", head_id, "--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_dry_run_duration_under_2000ms(self, cs_repo: pathlib.Path) -> None: v1 = _v1_commit_id(cs_repo) r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "--json") data = json.loads(r.output) assert data["duration_ms"] < 2000 def test_write_duration_under_2000ms(self, cs_repo: pathlib.Path) -> None: v1 = _v1_commit_id(cs_repo) r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--json") data = json.loads(r.output) assert data["duration_ms"] < 2000 def test_duration_ms_is_float_not_int(self, cs_repo: pathlib.Path) -> None: v1 = _v1_commit_id(cs_repo) r = _run(cs_repo, "code", "checkout-symbol", ADDRESS, "--commit", v1, "--dry-run", "--json") data = json.loads(r.output) assert isinstance(data["duration_ms"], float)