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