test_cmd_symlog_prune_orphans.py
python
sha256:8fff16f1bc41d40757bb7488701ddbc520adbed1effb6429da72f06b84631e56
feat(format-patch,rev-list): -N shorthand + A..B range pari…
Sonnet 5
patch
22 minutes ago
| 1 | """muse#53 item 12 -- `muse symlog prune-orphans [--dry-run] [--json]`. |
| 2 | |
| 3 | Finds and removes symlog journals whose symbol no longer exists at HEAD |
| 4 | (file deleted, or symbol removed from a file that still exists). |
| 5 | """ |
| 6 | |
| 7 | from __future__ import annotations |
| 8 | |
| 9 | import json |
| 10 | import os |
| 11 | import pathlib |
| 12 | import textwrap |
| 13 | |
| 14 | from tests.cli_test_helper import CliRunner |
| 15 | |
| 16 | runner = CliRunner() |
| 17 | |
| 18 | |
| 19 | def _invoke(repo: pathlib.Path, args: list[str]): |
| 20 | saved = os.getcwd() |
| 21 | try: |
| 22 | os.chdir(repo) |
| 23 | return runner.invoke(None, args) |
| 24 | finally: |
| 25 | os.chdir(saved) |
| 26 | |
| 27 | |
| 28 | def _init_repo(repo: pathlib.Path) -> None: |
| 29 | repo.mkdir(parents=True, exist_ok=True) |
| 30 | _invoke(repo, ["init"]) |
| 31 | |
| 32 | |
| 33 | def _write_file(repo: pathlib.Path, rel: str, content: str) -> None: |
| 34 | p = repo / rel |
| 35 | p.parent.mkdir(parents=True, exist_ok=True) |
| 36 | p.write_text(textwrap.dedent(content)) |
| 37 | |
| 38 | |
| 39 | def _commit(repo: pathlib.Path, msg: str) -> str: |
| 40 | _invoke(repo, ["code", "add", "."]) |
| 41 | result = _invoke(repo, [ |
| 42 | "commit", "-m", msg, |
| 43 | "--agent-id", "claude-code", |
| 44 | "--model-id", "claude-sonnet-4-6", |
| 45 | "--author", "claude-code", |
| 46 | "--json", |
| 47 | ]) |
| 48 | data = json.loads(result.stdout) |
| 49 | return data["commit_id"] |
| 50 | |
| 51 | |
| 52 | def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path: |
| 53 | _init_repo(tmp_path) |
| 54 | return tmp_path |
| 55 | |
| 56 | |
| 57 | def _symlog(repo: pathlib.Path, *args: str): |
| 58 | return _invoke(repo, ["symlog", *args]) |
| 59 | |
| 60 | |
| 61 | class TestPruneOrphans: |
| 62 | def test_no_orphans_in_clean_repo(self, tmp_path: pathlib.Path) -> None: |
| 63 | repo = _make_repo(tmp_path) |
| 64 | _write_file(repo, "src/billing.py", "def compute_total(items):\n return sum(items)\n") |
| 65 | _commit(repo, "add compute_total") |
| 66 | |
| 67 | result = _symlog(repo, "prune-orphans", "--json") |
| 68 | assert result.exit_code == 0, result.stdout |
| 69 | data = json.loads(result.stdout) |
| 70 | assert data["pruned"] == 0 |
| 71 | assert data["symbols"] == [] |
| 72 | |
| 73 | # The live symbol's journal must still exist afterward. |
| 74 | exists_result = _symlog(repo, "exists", "src/billing.py::compute_total") |
| 75 | assert exists_result.exit_code == 0 |
| 76 | |
| 77 | def test_orphan_from_deleted_symbol_is_pruned(self, tmp_path: pathlib.Path) -> None: |
| 78 | repo = _make_repo(tmp_path) |
| 79 | _write_file(repo, "src/billing.py", ( |
| 80 | "def compute_total(items):\n return sum(items)\n\n" |
| 81 | "def apply_discount(total, pct):\n return total * (1 - pct)\n" |
| 82 | )) |
| 83 | _commit(repo, "add two functions") |
| 84 | # Remove apply_discount entirely. |
| 85 | _write_file(repo, "src/billing.py", "def compute_total(items):\n return sum(items)\n") |
| 86 | _commit(repo, "remove apply_discount") |
| 87 | |
| 88 | result = _symlog(repo, "prune-orphans", "--json") |
| 89 | assert result.exit_code == 0, result.stdout |
| 90 | data = json.loads(result.stdout) |
| 91 | assert data["pruned"] == 1 |
| 92 | assert data["symbols"] == ["src/billing.py::apply_discount"] |
| 93 | |
| 94 | # Surviving symbol's journal must be untouched. |
| 95 | exists_result = _symlog(repo, "exists", "src/billing.py::compute_total") |
| 96 | assert exists_result.exit_code == 0 |
| 97 | # Orphaned symbol's journal must now be gone. |
| 98 | orphan_exists = _symlog(repo, "exists", "src/billing.py::apply_discount") |
| 99 | assert orphan_exists.exit_code == 1 |
| 100 | |
| 101 | def test_orphan_from_deleted_file_is_pruned(self, tmp_path: pathlib.Path) -> None: |
| 102 | repo = _make_repo(tmp_path) |
| 103 | _write_file(repo, "src/billing.py", "def compute_total(items):\n return sum(items)\n") |
| 104 | _commit(repo, "add compute_total") |
| 105 | (repo / "src" / "billing.py").unlink() |
| 106 | _invoke(repo, ["rm", "src/billing.py"]) |
| 107 | _commit(repo, "remove billing.py") |
| 108 | |
| 109 | result = _symlog(repo, "prune-orphans", "--json") |
| 110 | data = json.loads(result.stdout) |
| 111 | assert data["pruned"] == 1 |
| 112 | assert data["symbols"] == ["src/billing.py::compute_total"] |
| 113 | |
| 114 | def test_dry_run_does_not_delete(self, tmp_path: pathlib.Path) -> None: |
| 115 | repo = _make_repo(tmp_path) |
| 116 | _write_file(repo, "src/billing.py", ( |
| 117 | "def compute_total(items):\n return sum(items)\n\n" |
| 118 | "def apply_discount(total, pct):\n return total * (1 - pct)\n" |
| 119 | )) |
| 120 | _commit(repo, "add two functions") |
| 121 | _write_file(repo, "src/billing.py", "def compute_total(items):\n return sum(items)\n") |
| 122 | _commit(repo, "remove apply_discount") |
| 123 | |
| 124 | result = _symlog(repo, "prune-orphans", "--dry-run", "--json") |
| 125 | data = json.loads(result.stdout) |
| 126 | assert data["pruned"] == 1 |
| 127 | assert data["dry_run"] is True |
| 128 | |
| 129 | # Still present after a dry-run. |
| 130 | still_there = _symlog(repo, "exists", "src/billing.py::apply_discount") |
| 131 | assert still_there.exit_code == 0 |
File History
1 commit
sha256:8fff16f1bc41d40757bb7488701ddbc520adbed1effb6429da72f06b84631e56
feat(format-patch,rev-list): -N shorthand + A..B range pari…
Sonnet 5
patch
22 minutes ago