"""muse#53 item 12 -- `muse symlog prune-orphans [--dry-run] [--json]`. Finds and removes symlog journals whose symbol no longer exists at HEAD (file deleted, or symbol removed from a file that still exists). """ 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 TestPruneOrphans: def test_no_orphans_in_clean_repo(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, "prune-orphans", "--json") assert result.exit_code == 0, result.stdout data = json.loads(result.stdout) assert data["pruned"] == 0 assert data["symbols"] == [] # The live symbol's journal must still exist afterward. exists_result = _symlog(repo, "exists", "src/billing.py::compute_total") assert exists_result.exit_code == 0 def test_orphan_from_deleted_symbol_is_pruned(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") # Remove apply_discount entirely. _write_file(repo, "src/billing.py", "def compute_total(items):\n return sum(items)\n") _commit(repo, "remove apply_discount") result = _symlog(repo, "prune-orphans", "--json") assert result.exit_code == 0, result.stdout data = json.loads(result.stdout) assert data["pruned"] == 1 assert data["symbols"] == ["src/billing.py::apply_discount"] # Surviving symbol's journal must be untouched. exists_result = _symlog(repo, "exists", "src/billing.py::compute_total") assert exists_result.exit_code == 0 # Orphaned symbol's journal must now be gone. orphan_exists = _symlog(repo, "exists", "src/billing.py::apply_discount") assert orphan_exists.exit_code == 1 def test_orphan_from_deleted_file_is_pruned(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") (repo / "src" / "billing.py").unlink() _invoke(repo, ["rm", "src/billing.py"]) _commit(repo, "remove billing.py") result = _symlog(repo, "prune-orphans", "--json") data = json.loads(result.stdout) assert data["pruned"] == 1 assert data["symbols"] == ["src/billing.py::compute_total"] def test_dry_run_does_not_delete(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)\n") _commit(repo, "remove apply_discount") result = _symlog(repo, "prune-orphans", "--dry-run", "--json") data = json.loads(result.stdout) assert data["pruned"] == 1 assert data["dry_run"] is True # Still present after a dry-run. still_there = _symlog(repo, "exists", "src/billing.py::apply_discount") assert still_there.exit_code == 0