"""Supercharge tests for ``muse code languages`` — agent-usability gaps. The existing test_code_language_config.py covers the language detection machinery (AST parser, CodeConfig, adapter routing) but has zero CLI tests. This file covers the CLI command end-to-end: Coverage matrix --------------- - --json / -j: -j alias (was missing — caused argparse error) - exit_code: both JSON paths (snapshot + diff) include exit_code = 0 - duration_ms: both JSON paths include non-negative float duration_ms - TypedDicts: _SnapshotOutputJson and _DiffOutputJson carry all envelope fields - Docstrings: run() docstring mentions exit_code and duration_ms - Snapshot JSON: shape, required keys, language entry structure - Diff JSON: shape, required keys, diff entry structure - --sort: name / files / symbols all accepted; output ordering correct - --include-imports: import pseudo-symbols added to counts - --commit: historical snapshot accepted; bad ref exits cleanly - --diff: diff mode activates; bad ref exits cleanly - ANSI: no escape codes in JSON output - Performance: duration_ms < 5000 ms on a small repo """ 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 — two-commit repo with Python + Markdown files # --------------------------------------------------------------------------- @pytest.fixture() def lang_repo( tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> pathlib.Path: """Repo with Python and Markdown files across two commits. Commit 1: billing.py (Invoice class + validate_amount fn) Commit 2: auth.py (AuthError class + verify_token fn) + README.md """ 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("""\ import decimal class Invoice: def compute_total(self, items): return sum(items) def validate_amount(amount): return amount > 0 """)) r = _run(tmp_path, "code", "add", ".") assert r.exit_code == 0, r.output r = _run(tmp_path, "commit", "-m", "first commit") assert r.exit_code == 0, r.output (tmp_path / "auth.py").write_text(textwrap.dedent("""\ class AuthError(Exception): pass def verify_token(token): return bool(token) """)) (tmp_path / "README.md").write_text("# Test Repo\n\nA test repo.\n") r = _run(tmp_path, "code", "add", ".") assert r.exit_code == 0, r.output r = _run(tmp_path, "commit", "-m", "second commit") assert r.exit_code == 0, r.output return tmp_path # --------------------------------------------------------------------------- # TestJsonAlias — -j alias must work # --------------------------------------------------------------------------- class TestJsonAlias: """-j must be accepted and produce identical output to --json.""" def test_j_alias_exits_zero(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "-j") assert r.exit_code == 0, r.output def test_j_alias_valid_json(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "-j") json.loads(r.output) def test_j_alias_same_keys_as_json_flag(self, lang_repo: pathlib.Path) -> None: r1 = _run(lang_repo, "code", "languages", "--json") r2 = _run(lang_repo, "code", "languages", "-j") d1, d2 = json.loads(r1.output), 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_diff_mode(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--diff", "HEAD~1", "-j") assert r.exit_code == 0, r.output json.loads(r.output) def test_j_alias_no_ansi(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "-j") assert "\x1b" not in r.output # --------------------------------------------------------------------------- # TestSnapshotJson — snapshot mode JSON envelope # --------------------------------------------------------------------------- class TestSnapshotJson: """Snapshot mode JSON must include exit_code, duration_ms, and correct shape.""" def test_has_exit_code(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--json") assert "exit_code" in json.loads(r.output) def test_exit_code_zero(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--json") assert r.exit_code == 0 assert json.loads(r.output)["exit_code"] == 0 def test_exit_code_mirrors_process_exit(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--json") assert json.loads(r.output)["exit_code"] == r.exit_code def test_has_duration_ms(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--json") assert "duration_ms" in json.loads(r.output) def test_duration_ms_is_float(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--json") assert isinstance(json.loads(r.output)["duration_ms"], float) def test_duration_ms_nonnegative(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--json") assert json.loads(r.output)["duration_ms"] >= 0 def test_has_languages_key(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--json") assert "languages" in json.loads(r.output) def test_languages_is_list(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--json") assert isinstance(json.loads(r.output)["languages"], list) def test_has_commit_key(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--json") assert "commit" in json.loads(r.output) def test_has_include_imports_key(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--json") assert "include_imports" in json.loads(r.output) def test_include_imports_false_by_default(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--json") assert json.loads(r.output)["include_imports"] is False def test_python_present_in_languages(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--json") langs = {e["language"] for e in json.loads(r.output)["languages"]} assert "Python" in langs def test_language_entry_has_required_keys(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--json") for entry in json.loads(r.output)["languages"]: assert "language" in entry assert "files" in entry assert "symbols" in entry assert "kinds" in entry def test_files_and_symbols_are_ints(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--json") for entry in json.loads(r.output)["languages"]: assert isinstance(entry["files"], int) assert isinstance(entry["symbols"], int) def test_no_ansi_in_json(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--json") assert "\x1b" not in r.output # --------------------------------------------------------------------------- # TestDiffJson — diff mode JSON envelope # --------------------------------------------------------------------------- class TestDiffJson: """Diff mode JSON must include exit_code, duration_ms, and correct shape.""" def test_diff_has_exit_code(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--diff", "HEAD~1", "--json") assert r.exit_code == 0, r.output assert "exit_code" in json.loads(r.output) def test_diff_exit_code_zero(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--diff", "HEAD~1", "--json") assert json.loads(r.output)["exit_code"] == 0 def test_diff_has_duration_ms(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--diff", "HEAD~1", "--json") assert "duration_ms" in json.loads(r.output) def test_diff_duration_ms_is_float(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--diff", "HEAD~1", "--json") assert isinstance(json.loads(r.output)["duration_ms"], float) def test_diff_has_from_and_to(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--diff", "HEAD~1", "--json") d = json.loads(r.output) assert "from_commit" in d assert "to_commit" in d def test_diff_has_diff_key(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--diff", "HEAD~1", "--json") assert "diff" in json.loads(r.output) def test_diff_entries_have_required_keys(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--diff", "HEAD~1", "--json") for entry in json.loads(r.output)["diff"]: assert "language" in entry assert "delta_files" in entry assert "delta_symbols" in entry assert "status" in entry def test_diff_python_added_symbols(self, lang_repo: pathlib.Path) -> None: """Second commit added auth.py — Python should show positive delta.""" r = _run(lang_repo, "code", "languages", "--diff", "HEAD~1", "--json") entries = {e["language"]: e for e in json.loads(r.output)["diff"]} assert "Python" in entries assert entries["Python"]["delta_files"] >= 0 assert entries["Python"]["delta_symbols"] >= 0 def test_diff_status_values_valid(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--diff", "HEAD~1", "--json") valid = {"added", "removed", "changed", "unchanged"} for entry in json.loads(r.output)["diff"]: assert entry["status"] in valid def test_diff_no_ansi(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--diff", "HEAD~1", "--json") assert "\x1b" not in r.output # --------------------------------------------------------------------------- # TestSortFlag — --sort ordering # --------------------------------------------------------------------------- class TestSortFlag: """--sort name / files / symbols must produce correctly ordered output.""" def test_sort_name_alphabetical(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--sort", "name", "--json") langs = [e["language"] for e in json.loads(r.output)["languages"]] assert langs == sorted(langs) def test_sort_files_descending(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--sort", "files", "--json") counts = [e["files"] for e in json.loads(r.output)["languages"]] assert counts == sorted(counts, reverse=True) def test_sort_symbols_descending(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--sort", "symbols", "--json") counts = [e["symbols"] for e in json.loads(r.output)["languages"]] assert counts == sorted(counts, reverse=True) def test_invalid_sort_exits_nonzero(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--sort", "bogus", "--json") assert r.exit_code != 0 # --------------------------------------------------------------------------- # TestIncludeImports — import pseudo-symbols # --------------------------------------------------------------------------- class TestIncludeImports: """--include-imports must add import pseudo-symbols to counts.""" def test_include_imports_flag_in_json(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--include-imports", "--json") assert json.loads(r.output)["include_imports"] is True def test_symbols_higher_with_imports(self, lang_repo: pathlib.Path) -> None: r_no = _run(lang_repo, "code", "languages", "--json") r_yes = _run(lang_repo, "code", "languages", "--include-imports", "--json") py_no = next(e for e in json.loads(r_no.output)["languages"] if e["language"] == "Python") py_yes = next(e for e in json.loads(r_yes.output)["languages"] if e["language"] == "Python") assert py_yes["symbols"] >= py_no["symbols"] def test_import_kind_present_with_flag(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--include-imports", "--json") py = next(e for e in json.loads(r.output)["languages"] if e["language"] == "Python") assert "import" in py["kinds"] def test_import_kind_absent_without_flag(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--json") py = next(e for e in json.loads(r.output)["languages"] if e["language"] == "Python") assert "import" not in py["kinds"] # --------------------------------------------------------------------------- # TestHistoricalCommit — --commit flag # --------------------------------------------------------------------------- class TestHistoricalCommit: """--commit must accept branch names and commit IDs; bad refs exit cleanly.""" def test_commit_head_tilde_1_accepted(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--commit", "HEAD~1", "--json") assert r.exit_code == 0, r.output def test_commit_head_tilde_1_has_envelope(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--commit", "HEAD~1", "--json") d = json.loads(r.output) assert "exit_code" in d assert "duration_ms" in d def test_commit_head_tilde_1_fewer_languages(self, lang_repo: pathlib.Path) -> None: """First commit has no README.md, so Markdown absent or zero.""" r_old = _run(lang_repo, "code", "languages", "--commit", "HEAD~1", "--json") r_new = _run(lang_repo, "code", "languages", "--json") old_langs = {e["language"] for e in json.loads(r_old.output)["languages"] if e["files"] > 0} new_langs = {e["language"] for e in json.loads(r_new.output)["languages"] if e["files"] > 0} # HEAD~1 should have fewer or equal active languages assert len(old_langs) <= len(new_langs) def test_bad_commit_ref_exits_nonzero(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--commit", "nonexistent_branch", "--json") assert r.exit_code != 0 def test_bad_commit_no_json_on_error(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--commit", "nonexistent_branch", "--json") assert r.exit_code != 0 # Should not emit JSON on error with pytest.raises(Exception): json.loads(r.output) # --------------------------------------------------------------------------- # TestTypedDicts — TypedDicts carry envelope fields # --------------------------------------------------------------------------- class TestTypedDicts: """_SnapshotOutputJson and _DiffOutputJson must carry exit_code and duration_ms.""" def test_snapshot_typeddict_exists(self) -> None: from muse.cli.commands.languages import _SnapshotOutputJson # noqa: F401 def test_snapshot_has_exit_code_annotation(self) -> None: from muse.cli.commands.languages import _SnapshotOutputJson assert "exit_code" in _SnapshotOutputJson.__annotations__ def test_snapshot_has_duration_ms_annotation(self) -> None: from muse.cli.commands.languages import _SnapshotOutputJson assert "duration_ms" in _SnapshotOutputJson.__annotations__ def test_snapshot_has_languages_annotation(self) -> None: from muse.cli.commands.languages import _SnapshotOutputJson assert "languages" in _SnapshotOutputJson.__annotations__ def test_diff_typeddict_exists(self) -> None: from muse.cli.commands.languages import _DiffOutputJson # noqa: F401 def test_diff_has_exit_code_annotation(self) -> None: from muse.cli.commands.languages import _DiffOutputJson assert "exit_code" in _DiffOutputJson.__annotations__ def test_diff_has_duration_ms_annotation(self) -> None: from muse.cli.commands.languages import _DiffOutputJson assert "duration_ms" in _DiffOutputJson.__annotations__ def test_diff_has_diff_annotation(self) -> None: from muse.cli.commands.languages import _DiffOutputJson assert "diff" in _DiffOutputJson.__annotations__ # --------------------------------------------------------------------------- # TestDocstrings # --------------------------------------------------------------------------- class TestDocstrings: """run() must document exit_code.""" def test_run_mentions_exit_code(self) -> None: from muse.cli.commands.languages import run assert run.__doc__ is not None assert "exit_code" in run.__doc__ # --------------------------------------------------------------------------- # TestPerformance # --------------------------------------------------------------------------- class TestPerformance: """duration_ms must be present and reasonable on a small repo.""" def test_snapshot_duration_under_5000ms(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--json") assert json.loads(r.output)["duration_ms"] < 5000 def test_diff_duration_under_5000ms(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--diff", "HEAD~1", "--json") assert json.loads(r.output)["duration_ms"] < 5000 def test_duration_ms_is_float_not_int(self, lang_repo: pathlib.Path) -> None: r = _run(lang_repo, "code", "languages", "--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.languages 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(["languages", "--json"]) assert ns.json_out is True def test_j_alias(self) -> None: ns = self._make_parser().parse_args(["languages", "-j"]) assert ns.json_out is True def test_default_is_text(self) -> None: ns = self._make_parser().parse_args(["languages"]) assert ns.json_out is False def test_dest_is_json_out(self) -> None: ns = self._make_parser().parse_args(["languages", "-j"]) assert hasattr(ns, "json_out") assert not hasattr(ns, "fmt")