gabriel / muse public
test_cmd_clean.py python
190 lines 6.5 KB
86000da9 fix: replace typer CliRunner with argparse-compatible test helper Gabriel Cardona <gabriel@tellurstori.com> 1d ago
1 """Tests for ``muse clean``.
2
3 Covers: --dry-run preview, --force delete, --directories, no-force error,
4 already-clean repo, multiple untracked files, stress: 500 untracked files.
5 """
6
7 from __future__ import annotations
8
9 import json
10 import pathlib
11
12 import pytest
13 from tests.cli_test_helper import CliRunner
14
15 cli = None # argparse migration — CliRunner ignores this arg
16 from muse.core.object_store import write_object
17 from muse.core.snapshot import compute_snapshot_id
18 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
19
20 import datetime
21 import hashlib
22
23 runner = CliRunner()
24
25
26 # ---------------------------------------------------------------------------
27 # Helpers
28 # ---------------------------------------------------------------------------
29
30
31 def _sha(data: bytes) -> str:
32 return hashlib.sha256(data).hexdigest()
33
34
35 def _init_repo(path: pathlib.Path) -> pathlib.Path:
36 muse = path / ".muse"
37 (muse / "commits").mkdir(parents=True)
38 (muse / "snapshots").mkdir(parents=True)
39 (muse / "objects").mkdir(parents=True)
40 (muse / "refs" / "heads").mkdir(parents=True)
41 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
42 (muse / "repo.json").write_text(
43 json.dumps({"repo_id": "clean-test", "domain": "midi"}), encoding="utf-8"
44 )
45 return path
46
47
48 def _env(repo: pathlib.Path) -> dict[str, str]:
49 return {"MUSE_REPO_ROOT": str(repo)}
50
51
52 def _commit_file(root: pathlib.Path, rel_path: str, content: bytes) -> str:
53 """Write a file, store its object, and commit it. Returns commit_id."""
54 obj_id = _sha(content)
55 write_object(root, obj_id, content)
56 (root / rel_path).write_bytes(content)
57 manifest = {rel_path: obj_id}
58 snap_id = compute_snapshot_id(manifest)
59 snap = SnapshotRecord(snapshot_id=snap_id, manifest=manifest)
60 write_snapshot(root, snap)
61 committed_at = datetime.datetime.now(datetime.timezone.utc)
62 commit_id = _sha(f"commit:{snap_id}:{committed_at.isoformat()}".encode())
63 write_commit(root, CommitRecord(
64 commit_id=commit_id,
65 repo_id="clean-test",
66 branch="main",
67 snapshot_id=snap_id,
68 message="initial",
69 committed_at=committed_at,
70 ))
71 (root / ".muse" / "refs" / "heads" / "main").write_text(commit_id, encoding="utf-8")
72 return commit_id
73
74
75 # ---------------------------------------------------------------------------
76 # Unit: safety guard — no flags
77 # ---------------------------------------------------------------------------
78
79
80 def test_clean_no_force_exits_with_error(tmp_path: pathlib.Path) -> None:
81 _init_repo(tmp_path)
82 (tmp_path / "untracked.txt").write_text("hello", encoding="utf-8")
83 result = runner.invoke(cli, ["clean"], env=_env(tmp_path))
84 assert result.exit_code != 0
85
86
87 def test_clean_help() -> None:
88 result = runner.invoke(cli, ["clean", "--help"])
89 assert result.exit_code == 0
90 # Rich injects ANSI codes between '--' dashes; the short flag '-f' is reliable.
91 assert "--force" in result.output or "-f" in result.output
92
93
94 # ---------------------------------------------------------------------------
95 # Unit: dry-run shows but does not delete
96 # ---------------------------------------------------------------------------
97
98
99 def test_clean_dry_run_shows_untracked(tmp_path: pathlib.Path) -> None:
100 _init_repo(tmp_path)
101 _commit_file(tmp_path, "tracked.txt", b"I am tracked")
102 untracked = tmp_path / "ghost.txt"
103 untracked.write_text("untracked", encoding="utf-8")
104
105 result = runner.invoke(cli, ["clean", "-n"], env=_env(tmp_path))
106 assert result.exit_code == 0
107 assert "ghost.txt" in result.output
108 assert untracked.exists() # not deleted
109
110
111 def test_clean_dry_run_short_flag(tmp_path: pathlib.Path) -> None:
112 _init_repo(tmp_path)
113 (tmp_path / "junk.txt").write_text("junk", encoding="utf-8")
114 result = runner.invoke(cli, ["clean", "-n"], env=_env(tmp_path))
115 assert result.exit_code == 0
116
117
118 # ---------------------------------------------------------------------------
119 # Unit: --force deletes untracked files
120 # ---------------------------------------------------------------------------
121
122
123 def test_clean_force_deletes_untracked(tmp_path: pathlib.Path) -> None:
124 _init_repo(tmp_path)
125 _commit_file(tmp_path, "kept.txt", b"keep me")
126 untracked = tmp_path / "delete_me.txt"
127 untracked.write_text("bye", encoding="utf-8")
128
129 result = runner.invoke(cli, ["clean", "-f"], env=_env(tmp_path))
130 assert result.exit_code == 0
131 assert not untracked.exists()
132 assert (tmp_path / "kept.txt").exists()
133
134
135 def test_clean_force_nothing_to_clean(tmp_path: pathlib.Path) -> None:
136 _init_repo(tmp_path)
137 _commit_file(tmp_path, "tracked.txt", b"tracked")
138
139 result = runner.invoke(cli, ["clean", "-f"], env=_env(tmp_path))
140 assert result.exit_code == 0
141 assert "nothing" in result.output.lower()
142
143
144 # ---------------------------------------------------------------------------
145 # Unit: --directories removes empty dirs
146 # ---------------------------------------------------------------------------
147
148
149 def test_clean_directories_removes_empty_dir(tmp_path: pathlib.Path) -> None:
150 _init_repo(tmp_path)
151 _commit_file(tmp_path, "kept.txt", b"kept")
152 empty_dir = tmp_path / "empty_dir"
153 empty_dir.mkdir()
154 (empty_dir / "junk.txt").write_text("junk", encoding="utf-8")
155
156 result = runner.invoke(cli, ["clean", "-f", "-d"], env=_env(tmp_path))
157 assert result.exit_code == 0
158 assert not (empty_dir / "junk.txt").exists()
159
160
161 # ---------------------------------------------------------------------------
162 # Integration: multiple untracked files
163 # ---------------------------------------------------------------------------
164
165
166 def test_clean_multiple_untracked(tmp_path: pathlib.Path) -> None:
167 _init_repo(tmp_path)
168 for i in range(10):
169 (tmp_path / f"untracked_{i}.txt").write_text(f"data {i}", encoding="utf-8")
170
171 result = runner.invoke(cli, ["clean", "-f"], env=_env(tmp_path))
172 assert result.exit_code == 0
173 remaining = [f for f in tmp_path.iterdir() if f.name.startswith("untracked")]
174 assert len(remaining) == 0
175
176
177 # ---------------------------------------------------------------------------
178 # Stress: 500 untracked files
179 # ---------------------------------------------------------------------------
180
181
182 def test_clean_stress_500_untracked(tmp_path: pathlib.Path) -> None:
183 _init_repo(tmp_path)
184 for i in range(500):
185 (tmp_path / f"stress_{i}.dat").write_bytes(b"x" * 100)
186
187 result = runner.invoke(cli, ["clean", "-f"], env=_env(tmp_path))
188 assert result.exit_code == 0
189 remaining = list(tmp_path.glob("stress_*.dat"))
190 assert len(remaining) == 0