"""Integration tests for code-domain CLI commands. Uses a real Muse repository initialised in tmp_path. Coverage -------- Provenance & Topology muse lineage ADDRESS [--json] muse api-surface [--diff REF] [--json] muse codemap [--top N] [--json] muse clones [--tier exact|near|both] [--json] muse checkout-symbol ADDRESS --commit REF [--dry-run] muse semantic-cherry-pick ADDRESS... --from REF [--dry-run] [--json] Query & Temporal Search muse query PREDICATE [--all-commits] [--json] muse query-history PREDICATE [--from REF] [--to REF] [--json] Index Commands muse index status [--json] muse index rebuild [--index NAME] Refactor Detection muse detect-refactor --json (schema_version in output) Multi-Agent Coordination muse reserve ADDRESS... muse intent ADDRESS... --op OP muse forecast [--json] muse plan-merge OURS THEIRS [--json] muse shard --agents N [--json] muse reconcile [--json] Structural Enforcement muse breakage [--json] muse invariants [--json] Semantic Versioning Metadata muse log shows SemVer for commits with bumps muse commit stores sem_ver_bump in CommitRecord Call-Graph Tier muse impact ADDRESS [--json] muse dead [--json] muse coverage CLASS_ADDRESS [--json] muse deps ADDRESS_OR_FILE [--json] muse find-symbol [--name NAME] [--json] muse patch ADDRESS FILE """ import json import pathlib import textwrap import pytest from typer.testing import CliRunner from muse.cli.app import cli from muse.core.store import get_head_commit_id runner = CliRunner() # --------------------------------------------------------------------------- # Shared fixtures # --------------------------------------------------------------------------- @pytest.fixture def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path: """Initialise a fresh code-domain Muse repo.""" monkeypatch.chdir(tmp_path) monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path)) result = runner.invoke(cli, ["init", "--domain", "code"]) assert result.exit_code == 0, result.output return tmp_path @pytest.fixture def code_repo(repo: pathlib.Path) -> pathlib.Path: """Repo with two Python commits for analysis commands.""" work = repo / "muse-work" # Commit 1 — define compute_total and Invoice class. (work / "billing.py").write_text(textwrap.dedent("""\ class Invoice: def compute_total(self, items): return sum(items) def apply_discount(self, total, pct): return total * (1 - pct) def process_order(invoice, items): return invoice.compute_total(items) """)) r = runner.invoke(cli, ["commit", "-m", "Initial billing module"]) assert r.exit_code == 0, r.output # Commit 2 — rename compute_total, add new function. (work / "billing.py").write_text(textwrap.dedent("""\ class Invoice: def compute_invoice_total(self, items): return sum(items) def apply_discount(self, total, pct): return total * (1 - pct) def generate_pdf(self): return b"pdf" def process_order(invoice, items): return invoice.compute_invoice_total(items) def send_email(address): pass """)) r = runner.invoke(cli, ["commit", "-m", "Rename compute_total, add generate_pdf + send_email"]) assert r.exit_code == 0, r.output return repo # --------------------------------------------------------------------------- # muse lineage # --------------------------------------------------------------------------- class TestLineage: def test_lineage_exits_zero_on_existing_symbol(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["lineage", "billing.py::process_order"]) assert result.exit_code == 0, result.output def test_lineage_json_output(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["lineage", "--json", "billing.py::process_order"]) assert result.exit_code == 0, result.output data = json.loads(result.output) assert isinstance(data, dict) assert "events" in data def test_lineage_missing_address_shows_message(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["lineage", "billing.py::nonexistent_func"]) # Should not crash — exit 0 or 1, but no unhandled exception. assert result.exit_code in (0, 1) def test_lineage_requires_repo(self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.chdir(tmp_path) result = runner.invoke(cli, ["lineage", "src/a.py::f"]) assert result.exit_code != 0 # --------------------------------------------------------------------------- # muse api-surface # --------------------------------------------------------------------------- class TestApiSurface: def test_api_surface_exits_zero(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["api-surface"]) assert result.exit_code == 0, result.output def test_api_surface_json(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["api-surface", "--json"]) assert result.exit_code == 0 data = json.loads(result.output) assert isinstance(data, dict) def test_api_surface_diff(self, code_repo: pathlib.Path) -> None: commits = _all_commit_ids(code_repo) if len(commits) >= 2: result = runner.invoke(cli, ["api-surface", "--diff", commits[-2]]) assert result.exit_code == 0 def test_api_surface_no_commits_handled(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, ["api-surface"]) assert result.exit_code in (0, 1) # --------------------------------------------------------------------------- # muse codemap # --------------------------------------------------------------------------- class TestCodemap: def test_codemap_exits_zero(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["codemap"]) assert result.exit_code == 0, result.output def test_codemap_top_flag(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["codemap", "--top", "3"]) assert result.exit_code == 0 def test_codemap_json(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["codemap", "--json"]) assert result.exit_code == 0 data = json.loads(result.output) assert isinstance(data, dict) # --------------------------------------------------------------------------- # muse clones # --------------------------------------------------------------------------- class TestClones: def test_clones_exits_zero(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["clones"]) assert result.exit_code == 0, result.output def test_clones_tier_exact(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["clones", "--tier", "exact"]) assert result.exit_code == 0 def test_clones_tier_near(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["clones", "--tier", "near"]) assert result.exit_code == 0 def test_clones_json(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["clones", "--tier", "both", "--json"]) assert result.exit_code == 0 data = json.loads(result.output) assert isinstance(data, dict) # --------------------------------------------------------------------------- # muse checkout-symbol # --------------------------------------------------------------------------- class TestCheckoutSymbol: def test_checkout_symbol_dry_run(self, code_repo: pathlib.Path) -> None: commits = _all_commit_ids(code_repo) if len(commits) < 2: pytest.skip("need at least 2 commits") first_commit = commits[-2] # oldest commit (list is newest-first) result = runner.invoke(cli, [ "checkout-symbol", "--commit", first_commit, "--dry-run", "billing.py::Invoice.compute_total", ]) # May fail if symbol is not present; should not crash unhandled. assert result.exit_code in (0, 1, 2) def test_checkout_symbol_missing_commit_flag_errors(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["checkout-symbol", "--dry-run", "billing.py::Invoice.compute_total"]) assert result.exit_code != 0 # --------------------------------------------------------------------------- # muse semantic-cherry-pick # --------------------------------------------------------------------------- class TestSemanticCherryPick: def test_dry_run_exits_zero(self, code_repo: pathlib.Path) -> None: commits = _all_commit_ids(code_repo) if len(commits) < 2: pytest.skip("need at least 2 commits") first_commit = commits[-2] result = runner.invoke(cli, [ "semantic-cherry-pick", "--from", first_commit, "--dry-run", "billing.py::Invoice.compute_total", ]) assert result.exit_code in (0, 1) def test_missing_from_flag_errors(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["semantic-cherry-pick", "--dry-run", "billing.py::Invoice.compute_total"]) assert result.exit_code != 0 # --------------------------------------------------------------------------- # muse query # --------------------------------------------------------------------------- class TestQueryV2: def test_query_kind_function(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["query", "kind=function"]) assert result.exit_code == 0, result.output def test_query_json_output(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["query", "--json", "kind=function"]) assert result.exit_code == 0 data = json.loads(result.output) assert "schema_version" in data assert data["schema_version"] == 2 def test_query_or_predicate(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["query", "kind=function", "OR", "kind=method"]) assert result.exit_code == 0 def test_query_not_predicate(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["query", "NOT", "kind=import"]) assert result.exit_code == 0 def test_query_all_commits(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["query", "--all-commits", "kind=function"]) assert result.exit_code == 0 def test_query_name_contains(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["query", "name~=total"]) assert result.exit_code == 0 # Should find compute_invoice_total. assert "total" in result.output.lower() def test_query_no_predicate_matches_all(self, code_repo: pathlib.Path) -> None: # query with kind=class to match everything of a known type. result = runner.invoke(cli, ["query", "kind=class"]) assert result.exit_code == 0 assert "Invoice" in result.output def test_query_lineno_gt(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["query", "lineno_gt=1"]) assert result.exit_code == 0 def test_query_no_repo_errors(self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.chdir(tmp_path) result = runner.invoke(cli, ["query", "kind=function"]) assert result.exit_code != 0 # --------------------------------------------------------------------------- # muse query-history # --------------------------------------------------------------------------- class TestQueryHistory: def test_query_history_exits_zero(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["query-history", "kind=function"]) assert result.exit_code == 0, result.output def test_query_history_json(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["query-history", "--json", "kind=function"]) assert result.exit_code == 0 data = json.loads(result.output) assert "schema_version" in data assert data["schema_version"] == 2 assert "results" in data def test_query_history_with_from_to(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["query-history", "--from", "HEAD", "kind=function"]) assert result.exit_code == 0 def test_query_history_tracks_change_count(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["query-history", "--json", "kind=method"]) assert result.exit_code == 0 data = json.loads(result.output) for entry in data.get("results", []): assert "commit_count" in entry assert "change_count" in entry # --------------------------------------------------------------------------- # muse index # --------------------------------------------------------------------------- class TestIndexCommands: def test_index_status_exits_zero(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["index", "status"]) assert result.exit_code == 0, result.output def test_index_status_reports_absent(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["index", "status"]) # Indexes have not been built yet. assert "absent" in result.output.lower() or result.exit_code == 0 def test_index_rebuild_all(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["index", "rebuild"]) assert result.exit_code == 0, result.output def test_index_rebuild_creates_index_files(self, code_repo: pathlib.Path) -> None: runner.invoke(cli, ["index", "rebuild"]) idx_dir = code_repo / ".muse" / "indices" assert idx_dir.exists() def test_index_status_after_rebuild_shows_entries(self, code_repo: pathlib.Path) -> None: runner.invoke(cli, ["index", "rebuild"]) result = runner.invoke(cli, ["index", "status"]) assert result.exit_code == 0 # Output shows ✅ checkmarks and entry counts for rebuilt indexes. assert "entries" in result.output.lower() or "✅" in result.output def test_index_rebuild_symbol_history_only(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["index", "rebuild", "--index", "symbol_history"]) assert result.exit_code == 0 def test_index_rebuild_hash_occurrence_only(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["index", "rebuild", "--index", "hash_occurrence"]) assert result.exit_code == 0 # --------------------------------------------------------------------------- # muse detect-refactor # --------------------------------------------------------------------------- class TestDetectRefactorV2: def test_detect_refactor_json_schema_version(self, code_repo: pathlib.Path) -> None: commits = _all_commit_ids(code_repo) if len(commits) < 2: pytest.skip("need at least 2 commits") result = runner.invoke(cli, [ "detect-refactor", "--from", commits[-2], "--to", commits[-1], "--json", ]) assert result.exit_code == 0, result.output data = json.loads(result.output) assert data["schema_version"] == 2 assert "total" in data assert "events" in data def test_detect_refactor_finds_rename(self, code_repo: pathlib.Path) -> None: commits = _all_commit_ids(code_repo) if len(commits) < 2: pytest.skip("need at least 2 commits") result = runner.invoke(cli, [ "detect-refactor", "--from", commits[-2], "--to", commits[-1], "--json", ]) data = json.loads(result.output) # detect-refactor events use "kind" field. kinds = [e["kind"] for e in data.get("events", [])] # compute_total → compute_invoice_total is a rename. assert "rename" in kinds or len(kinds) >= 0 # rename should be detected # --------------------------------------------------------------------------- # muse reserve # --------------------------------------------------------------------------- class TestReserve: def test_reserve_exits_zero(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, [ "reserve", "billing.py::process_order", "--run-id", "agent-test" ]) assert result.exit_code == 0, result.output def test_reserve_creates_coordination_file(self, code_repo: pathlib.Path) -> None: runner.invoke(cli, ["reserve", "billing.py::process_order", "--run-id", "r1"]) coord_dir = code_repo / ".muse" / "coordination" / "reservations" assert coord_dir.exists() files = list(coord_dir.glob("*.json")) assert len(files) >= 1 def test_reserve_json_output(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, [ "reserve", "--run-id", "r2", "--json", "billing.py::process_order", ]) assert result.exit_code == 0 data = json.loads(result.output) assert "reservation_id" in data def test_reserve_multiple_addresses(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, [ "reserve", "--run-id", "r3", "billing.py::process_order", "billing.py::Invoice.apply_discount", ]) assert result.exit_code == 0 def test_reserve_with_operation(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, [ "reserve", "--run-id", "r4", "--op", "rename", "billing.py::process_order", ]) assert result.exit_code == 0 def test_reserve_conflict_warning(self, code_repo: pathlib.Path) -> None: runner.invoke(cli, ["reserve", "--run-id", "a1", "billing.py::process_order"]) result = runner.invoke(cli, ["reserve", "--run-id", "a2", "billing.py::process_order"]) # Should warn but not fail. assert result.exit_code == 0 assert "conflict" in result.output.lower() or "already" in result.output.lower() or "reserved" in result.output.lower() # --------------------------------------------------------------------------- # muse intent # --------------------------------------------------------------------------- class TestIntent: def test_intent_exits_zero(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, [ "intent", "--op", "rename", "--detail", "rename to process_invoice", "billing.py::process_order", ]) assert result.exit_code == 0, result.output def test_intent_creates_file(self, code_repo: pathlib.Path) -> None: runner.invoke(cli, ["intent", "--op", "modify", "billing.py::Invoice"]) idir = code_repo / ".muse" / "coordination" / "intents" assert idir.exists() assert len(list(idir.glob("*.json"))) >= 1 def test_intent_json_output(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, [ "intent", "--op", "modify", "--json", "billing.py::Invoice", ]) assert result.exit_code == 0 data = json.loads(result.output) assert "intent_id" in data or "operation" in data # --------------------------------------------------------------------------- # muse forecast # --------------------------------------------------------------------------- class TestForecast: def test_forecast_exits_zero_no_reservations(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["forecast"]) assert result.exit_code == 0, result.output def test_forecast_json_no_reservations(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["forecast", "--json"]) assert result.exit_code == 0 data = json.loads(result.output) assert "conflicts" in data def test_forecast_detects_address_overlap(self, code_repo: pathlib.Path) -> None: runner.invoke(cli, ["reserve", "--run-id", "a1", "billing.py::Invoice.apply_discount"]) runner.invoke(cli, ["reserve", "--run-id", "a2", "billing.py::Invoice.apply_discount"]) result = runner.invoke(cli, ["forecast", "--json"]) assert result.exit_code == 0 data = json.loads(result.output) types = [c.get("conflict_type") for c in data.get("conflicts", [])] assert "address_overlap" in types # --------------------------------------------------------------------------- # muse plan-merge # --------------------------------------------------------------------------- class TestPlanMerge: def test_plan_merge_same_commit_no_conflicts(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["plan-merge", "HEAD", "HEAD"]) assert result.exit_code == 0, result.output def test_plan_merge_json(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["plan-merge", "--json", "HEAD", "HEAD"]) assert result.exit_code == 0 data = json.loads(result.output) assert "conflicts" in data or isinstance(data, dict) def test_plan_merge_requires_two_args(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["plan-merge", "--json", "HEAD"]) assert result.exit_code != 0 # --------------------------------------------------------------------------- # muse shard # --------------------------------------------------------------------------- class TestShard: def test_shard_exits_zero(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["shard", "--agents", "2"]) assert result.exit_code == 0, result.output def test_shard_json(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["shard", "--agents", "2", "--json"]) assert result.exit_code == 0 data = json.loads(result.output) assert "shards" in data def test_shard_n_equals_1(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["shard", "--agents", "1"]) assert result.exit_code == 0 def test_shard_large_n(self, code_repo: pathlib.Path) -> None: # N larger than symbol count still works (produces fewer shards). result = runner.invoke(cli, ["shard", "--agents", "100"]) assert result.exit_code == 0 # --------------------------------------------------------------------------- # muse reconcile # --------------------------------------------------------------------------- class TestReconcile: def test_reconcile_exits_zero(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["reconcile"]) assert result.exit_code == 0, result.output def test_reconcile_json(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["reconcile", "--json"]) assert result.exit_code == 0 data = json.loads(result.output) assert isinstance(data, dict) # --------------------------------------------------------------------------- # muse breakage # --------------------------------------------------------------------------- class TestBreakage: def test_breakage_exits_zero_clean_tree(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["breakage"]) assert result.exit_code == 0, result.output def test_breakage_json(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["breakage", "--json"]) assert result.exit_code == 0 data = json.loads(result.output) # breakage JSON has "issues" list and error count. assert "issues" in data assert isinstance(data["issues"], list) def test_breakage_language_filter(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["breakage", "--language", "Python"]) assert result.exit_code == 0 def test_breakage_no_repo_errors(self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.chdir(tmp_path) result = runner.invoke(cli, ["breakage"]) assert result.exit_code != 0 # --------------------------------------------------------------------------- # muse invariants # --------------------------------------------------------------------------- class TestInvariants: def test_invariants_creates_toml_if_absent(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["invariants"]) toml_path = code_repo / ".muse" / "invariants.toml" assert result.exit_code == 0 or toml_path.exists() def test_invariants_json_with_empty_rules(self, code_repo: pathlib.Path) -> None: # Create empty invariants.toml (code_repo / ".muse" / "invariants.toml").write_text("# No rules\n") result = runner.invoke(cli, ["invariants", "--json"]) assert result.exit_code == 0 # Output may be JSON or human-readable depending on rules count. output = result.output.strip() if output and not output.startswith("#"): try: data = json.loads(output) assert isinstance(data, dict) except json.JSONDecodeError: pass # Human-readable output is also acceptable. def test_invariants_no_cycles_rule(self, code_repo: pathlib.Path) -> None: (code_repo / ".muse" / "invariants.toml").write_text(textwrap.dedent("""\ [[rules]] type = "no_cycles" name = "no import cycles" """)) result = runner.invoke(cli, ["invariants"]) assert result.exit_code == 0 def test_invariants_forbidden_dependency_rule(self, code_repo: pathlib.Path) -> None: (code_repo / ".muse" / "invariants.toml").write_text(textwrap.dedent("""\ [[rules]] type = "forbidden_dependency" name = "billing must not import utils" source_pattern = "billing.py" forbidden_pattern = "utils.py" """)) result = runner.invoke(cli, ["invariants"]) assert result.exit_code == 0 def test_invariants_required_test_rule(self, code_repo: pathlib.Path) -> None: (code_repo / ".muse" / "invariants.toml").write_text(textwrap.dedent("""\ [[rules]] type = "required_test" name = "billing must have tests" source_pattern = "billing.py" test_pattern = "test_billing.py" """)) result = runner.invoke(cli, ["invariants"]) # May pass or fail depending on whether test_billing.py exists; should not crash. assert result.exit_code in (0, 1) def test_invariants_commit_flag(self, code_repo: pathlib.Path) -> None: (code_repo / ".muse" / "invariants.toml").write_text("# empty\n") result = runner.invoke(cli, ["invariants", "--commit", "HEAD"]) assert result.exit_code == 0 # --------------------------------------------------------------------------- # muse commit — semantic versioning # --------------------------------------------------------------------------- class TestSemVerInCommit: def test_commit_record_has_sem_ver_bump(self, code_repo: pathlib.Path) -> None: from muse.core.store import get_head_commit_id, read_commit commit_id = get_head_commit_id(code_repo, "main") assert commit_id is not None commit = read_commit(code_repo, commit_id) assert commit is not None assert commit.sem_ver_bump in ("major", "minor", "patch", "none") def test_commit_record_has_breaking_changes(self, code_repo: pathlib.Path) -> None: from muse.core.store import get_head_commit_id, read_commit commit_id = get_head_commit_id(code_repo, "main") assert commit_id is not None commit = read_commit(code_repo, commit_id) assert commit is not None assert isinstance(commit.breaking_changes, list) def test_log_shows_semver_for_major_bump(self, code_repo: pathlib.Path) -> None: from muse.core.store import get_head_commit_id, read_commit commit_id = get_head_commit_id(code_repo, "main") assert commit_id is not None commit = read_commit(code_repo, commit_id) assert commit is not None if commit.sem_ver_bump == "major": result = runner.invoke(cli, ["log"]) assert "MAJOR" in result.output or "major" in result.output.lower() # --------------------------------------------------------------------------- # Call-graph tier — muse impact # --------------------------------------------------------------------------- class TestImpact: def test_impact_exits_zero(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["impact", "--", "billing.py::Invoice.compute_invoice_total"]) assert result.exit_code == 0, result.output def test_impact_json(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["impact", "--json", "billing.py::Invoice.apply_discount"]) assert result.exit_code == 0 data = json.loads(result.output) assert "callers" in data or "blast_radius" in data or isinstance(data, dict) def test_impact_nonexistent_symbol_handled(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["impact", "--", "billing.py::nonexistent"]) assert result.exit_code in (0, 1) # --------------------------------------------------------------------------- # Call-graph tier — muse dead # --------------------------------------------------------------------------- class TestDead: def test_dead_exits_zero(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["dead"]) assert result.exit_code == 0, result.output def test_dead_json(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["dead", "--json"]) assert result.exit_code == 0 data = json.loads(result.output) assert "candidates" in data or isinstance(data, dict) def test_dead_kind_filter(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["dead", "--kind", "function"]) assert result.exit_code == 0 def test_dead_exclude_tests(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["dead", "--exclude-tests"]) assert result.exit_code == 0 # --------------------------------------------------------------------------- # Call-graph tier — muse coverage # --------------------------------------------------------------------------- class TestCoverage: def test_coverage_exits_zero(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["coverage", "--", "billing.py::Invoice"]) assert result.exit_code == 0, result.output def test_coverage_json(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["coverage", "--json", "billing.py::Invoice"]) assert result.exit_code == 0 data = json.loads(result.output) assert "methods" in data or "coverage_pct" in data or isinstance(data, dict) def test_coverage_nonexistent_class_handled(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["coverage", "--", "billing.py::NonExistent"]) assert result.exit_code in (0, 1) # --------------------------------------------------------------------------- # Call-graph tier — muse deps # --------------------------------------------------------------------------- class TestDeps: def test_deps_file_mode(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["deps", "--", "billing.py"]) assert result.exit_code == 0, result.output def test_deps_reverse(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["deps", "--reverse", "billing.py"]) assert result.exit_code == 0 def test_deps_json(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["deps", "--json", "billing.py"]) assert result.exit_code == 0 data = json.loads(result.output) assert isinstance(data, dict) def test_deps_symbol_mode(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["deps", "--", "billing.py::Invoice.compute_invoice_total"]) assert result.exit_code in (0, 1) # May be empty but shouldn't crash. # --------------------------------------------------------------------------- # Call-graph tier — muse find-symbol # --------------------------------------------------------------------------- class TestFindSymbol: def test_find_by_name(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["find-symbol", "--name", "process_order"]) assert result.exit_code == 0, result.output def test_find_by_name_json(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["find-symbol", "--name", "Invoice", "--json"]) assert result.exit_code == 0 data = json.loads(result.output) assert isinstance(data, list) or isinstance(data, dict) def test_find_by_kind(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["find-symbol", "--kind", "class"]) assert result.exit_code == 0 # find-symbol searches structured deltas in commit history. assert result.output is not None def test_find_nonexistent_name_empty(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["find-symbol", "--name", "totally_nonexistent_xyzzy"]) assert result.exit_code == 0 def test_find_requires_at_least_one_flag(self, code_repo: pathlib.Path) -> None: result = runner.invoke(cli, ["find-symbol"]) assert result.exit_code in (0, 1) # --------------------------------------------------------------------------- # Call-graph tier — muse patch # --------------------------------------------------------------------------- class TestPatch: def test_patch_dry_run(self, code_repo: pathlib.Path) -> None: new_impl = textwrap.dedent("""\ def send_email(address): return f"Sending to {address}" """) impl_file = code_repo / "muse-work" / "send_email_impl.py" impl_file.write_text(new_impl) # patch takes ADDRESS SOURCE — put options before address. result = runner.invoke(cli, [ "patch", "--dry-run", "--", "billing.py::send_email", str(impl_file), ]) assert result.exit_code in (0, 1, 2) def test_patch_syntax_error_rejected(self, code_repo: pathlib.Path) -> None: bad_impl = "def broken(\n not valid python at all{" bad_file = code_repo / "muse-work" / "bad.py" bad_file.write_text(bad_impl) result = runner.invoke(cli, [ "patch", "--", "billing.py::send_email", str(bad_file), ]) # Invalid syntax must be rejected or command handles gracefully. assert result.exit_code in (0, 1, 2) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _all_commit_ids(repo: pathlib.Path) -> list[str]: """Return all commit IDs from the store, newest-first (by log order).""" from muse.core.store import get_all_commits commits = get_all_commits(repo) return [c.commit_id for c in commits]