"""muse#53 item 1 -- `muse symlog hotspots` and `muse symlog coupling`. Analytics commands reading purely from the already-written symlog journal, with no commit-history scan. """ from __future__ import annotations import json import os import pathlib import textwrap from tests.cli_test_helper import CliRunner runner = CliRunner() def _invoke(repo: pathlib.Path, args: list[str]): saved = os.getcwd() try: os.chdir(repo) return runner.invoke(None, args) finally: os.chdir(saved) def _init_repo(repo: pathlib.Path) -> None: repo.mkdir(parents=True, exist_ok=True) _invoke(repo, ["init"]) def _write_file(repo: pathlib.Path, rel: str, content: str) -> None: p = repo / rel p.parent.mkdir(parents=True, exist_ok=True) p.write_text(textwrap.dedent(content)) def _commit(repo: pathlib.Path, msg: str) -> str: _invoke(repo, ["code", "add", "."]) result = _invoke(repo, [ "commit", "-m", msg, "--agent-id", "claude-code", "--model-id", "claude-sonnet-4-6", "--author", "claude-code", "--json", ]) data = json.loads(result.stdout) return data["commit_id"] def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path: _init_repo(tmp_path) return tmp_path def _symlog(repo: pathlib.Path, *args: str): return _invoke(repo, ["symlog", *args]) class TestSymlogHotspots: def test_ranks_by_modification_count(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _write_file(repo, "src/billing.py", ( "def compute_total(items):\n return sum(items)\n\n" "def apply_discount(total, pct):\n return total * (1 - pct)\n" )) _commit(repo, "add two functions") # Modify compute_total twice, apply_discount once. _write_file(repo, "src/billing.py", ( "def compute_total(items):\n return sum(items) + 1\n\n" "def apply_discount(total, pct):\n return total * (1 - pct) - 1\n" )) _commit(repo, "modify both") _write_file(repo, "src/billing.py", ( "def compute_total(items):\n return sum(items) + 2\n\n" "def apply_discount(total, pct):\n return total * (1 - pct) - 1\n" )) _commit(repo, "modify compute_total again") result = _symlog(repo, "hotspots", "--json") assert result.exit_code == 0, result.stdout data = json.loads(result.stdout) assert data["count"] == 2 assert data["hotspots"][0]["symbol"] == "src/billing.py::compute_total" assert data["hotspots"][0]["modifications"] == 2 assert data["hotspots"][1]["symbol"] == "src/billing.py::apply_discount" assert data["hotspots"][1]["modifications"] == 1 def test_creation_only_excluded(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _write_file(repo, "src/billing.py", "def compute_total(items):\n return sum(items)\n") _commit(repo, "add compute_total") result = _symlog(repo, "hotspots", "--json") data = json.loads(result.stdout) assert data["count"] == 0 def test_file_scoping(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _write_file(repo, "src/billing.py", "def compute_total(items):\n return sum(items)\n") _write_file(repo, "src/other.py", "def unrelated():\n pass\n") _commit(repo, "add two files") _write_file(repo, "src/billing.py", "def compute_total(items):\n return sum(items) + 1\n") _write_file(repo, "src/other.py", "def unrelated():\n return 1\n") _commit(repo, "modify both") result = _symlog(repo, "hotspots", "--file", "src/billing.py", "--json") data = json.loads(result.stdout) assert data["count"] == 1 assert data["hotspots"][0]["symbol"] == "src/billing.py::compute_total" def test_top_limits_results(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _write_file(repo, "src/billing.py", ( "def a():\n return 1\n\ndef b():\n return 2\n" )) _commit(repo, "add") _write_file(repo, "src/billing.py", ( "def a():\n return 11\n\ndef b():\n return 22\n" )) _commit(repo, "modify both") result = _symlog(repo, "hotspots", "--top", "1", "--json") data = json.loads(result.stdout) assert len(data["hotspots"]) == 1 class TestSymlogCoupling: def test_finds_symbols_that_change_together(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _write_file(repo, "src/billing.py", ( "def compute_total(items):\n return sum(items)\n\n" "def apply_discount(total, pct):\n return total * (1 - pct)\n" )) _commit(repo, "add two functions") _write_file(repo, "src/billing.py", ( "def compute_total(items):\n return sum(items) + 1\n\n" "def apply_discount(total, pct):\n return total * (1 - pct) - 1\n" )) _commit(repo, "modify both together") result = _symlog(repo, "coupling", "--json") assert result.exit_code == 0, result.stdout data = json.loads(result.stdout) assert data["count"] == 1 pair = data["pairs"][0] assert {pair["symbol_a"], pair["symbol_b"]} == { "src/billing.py::compute_total", "src/billing.py::apply_discount", } assert pair["co_changes"] == 2 # both created + both modified together def test_below_min_threshold_excluded(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _write_file(repo, "src/billing.py", ( "def compute_total(items):\n return sum(items)\n\n" "def apply_discount(total, pct):\n return total * (1 - pct)\n" )) _commit(repo, "add two functions") # Only one shared commit (the creation) -- below the default min of 2. result = _symlog(repo, "coupling", "--json") data = json.loads(result.stdout) assert data["count"] == 0 def test_min_flag_lowers_threshold(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _write_file(repo, "src/billing.py", ( "def compute_total(items):\n return sum(items)\n\n" "def apply_discount(total, pct):\n return total * (1 - pct)\n" )) _commit(repo, "add two functions") result = _symlog(repo, "coupling", "--min", "1", "--json") data = json.loads(result.stdout) assert data["count"] == 1 def test_no_coupling_in_empty_repo(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) result = _symlog(repo, "coupling", "--json") data = json.loads(result.stdout) assert data["count"] == 0