gabriel / muse public
test_cmd_clean.py python
189 lines 6.4 KB
1ba7f7b1 feat(porcelain): implement 9 gap-fill porcelain commands with full test… Gabriel Cardona <gabriel@tellurstori.com> 2d 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 typer.testing import CliRunner
14
15 from muse.cli.app import cli
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 assert "--force" in result.output
91
92
93 # ---------------------------------------------------------------------------
94 # Unit: dry-run shows but does not delete
95 # ---------------------------------------------------------------------------
96
97
98 def test_clean_dry_run_shows_untracked(tmp_path: pathlib.Path) -> None:
99 _init_repo(tmp_path)
100 _commit_file(tmp_path, "tracked.txt", b"I am tracked")
101 untracked = tmp_path / "ghost.txt"
102 untracked.write_text("untracked", encoding="utf-8")
103
104 result = runner.invoke(cli, ["clean", "-n"], env=_env(tmp_path))
105 assert result.exit_code == 0
106 assert "ghost.txt" in result.output
107 assert untracked.exists() # not deleted
108
109
110 def test_clean_dry_run_short_flag(tmp_path: pathlib.Path) -> None:
111 _init_repo(tmp_path)
112 (tmp_path / "junk.txt").write_text("junk", encoding="utf-8")
113 result = runner.invoke(cli, ["clean", "-n"], env=_env(tmp_path))
114 assert result.exit_code == 0
115
116
117 # ---------------------------------------------------------------------------
118 # Unit: --force deletes untracked files
119 # ---------------------------------------------------------------------------
120
121
122 def test_clean_force_deletes_untracked(tmp_path: pathlib.Path) -> None:
123 _init_repo(tmp_path)
124 _commit_file(tmp_path, "kept.txt", b"keep me")
125 untracked = tmp_path / "delete_me.txt"
126 untracked.write_text("bye", encoding="utf-8")
127
128 result = runner.invoke(cli, ["clean", "-f"], env=_env(tmp_path))
129 assert result.exit_code == 0
130 assert not untracked.exists()
131 assert (tmp_path / "kept.txt").exists()
132
133
134 def test_clean_force_nothing_to_clean(tmp_path: pathlib.Path) -> None:
135 _init_repo(tmp_path)
136 _commit_file(tmp_path, "tracked.txt", b"tracked")
137
138 result = runner.invoke(cli, ["clean", "-f"], env=_env(tmp_path))
139 assert result.exit_code == 0
140 assert "nothing" in result.output.lower()
141
142
143 # ---------------------------------------------------------------------------
144 # Unit: --directories removes empty dirs
145 # ---------------------------------------------------------------------------
146
147
148 def test_clean_directories_removes_empty_dir(tmp_path: pathlib.Path) -> None:
149 _init_repo(tmp_path)
150 _commit_file(tmp_path, "kept.txt", b"kept")
151 empty_dir = tmp_path / "empty_dir"
152 empty_dir.mkdir()
153 (empty_dir / "junk.txt").write_text("junk", encoding="utf-8")
154
155 result = runner.invoke(cli, ["clean", "-f", "-d"], env=_env(tmp_path))
156 assert result.exit_code == 0
157 assert not (empty_dir / "junk.txt").exists()
158
159
160 # ---------------------------------------------------------------------------
161 # Integration: multiple untracked files
162 # ---------------------------------------------------------------------------
163
164
165 def test_clean_multiple_untracked(tmp_path: pathlib.Path) -> None:
166 _init_repo(tmp_path)
167 for i in range(10):
168 (tmp_path / f"untracked_{i}.txt").write_text(f"data {i}", encoding="utf-8")
169
170 result = runner.invoke(cli, ["clean", "-f"], env=_env(tmp_path))
171 assert result.exit_code == 0
172 remaining = [f for f in tmp_path.iterdir() if f.name.startswith("untracked")]
173 assert len(remaining) == 0
174
175
176 # ---------------------------------------------------------------------------
177 # Stress: 500 untracked files
178 # ---------------------------------------------------------------------------
179
180
181 def test_clean_stress_500_untracked(tmp_path: pathlib.Path) -> None:
182 _init_repo(tmp_path)
183 for i in range(500):
184 (tmp_path / f"stress_{i}.dat").write_bytes(b"x" * 100)
185
186 result = runner.invoke(cli, ["clean", "-f"], env=_env(tmp_path))
187 assert result.exit_code == 0
188 remaining = list(tmp_path.glob("stress_*.dat"))
189 assert len(remaining) == 0