gabriel / muse public
test_cmd_clean.py python
197 lines 6.8 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 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, blob_id
20
21 import datetime
22
23 runner = CliRunner()
24
25
26 # ---------------------------------------------------------------------------
27 # Helpers
28 # ---------------------------------------------------------------------------
29
30
31 def _sha(data: bytes) -> str:
32 """Return the canonical Muse object ID (sha256: prefix + 64 hex chars)."""
33 return blob_id(data)
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(
64 repo_id="clean-test",
65 parent_ids=[],
66 snapshot_id=snap_id,
67 message="initial",
68 committed_at_iso=committed_at.isoformat(),
69 )
70 write_commit(root, CommitRecord(
71 commit_id=commit_id,
72 repo_id="clean-test",
73 created_on_branch="main",
74 snapshot_id=snap_id,
75 message="initial",
76 committed_at=committed_at,
77 ))
78 (root / ".muse" / "refs" / "heads" / "main").write_text(commit_id, encoding="utf-8")
79 return commit_id
80
81
82 # ---------------------------------------------------------------------------
83 # Unit: safety guard — no flags
84 # ---------------------------------------------------------------------------
85
86
87 def test_clean_no_force_exits_with_error(tmp_path: pathlib.Path) -> None:
88 _init_repo(tmp_path)
89 (tmp_path / "untracked.txt").write_text("hello", encoding="utf-8")
90 result = runner.invoke(cli, ["clean"], env=_env(tmp_path))
91 assert result.exit_code != 0
92
93
94 def test_clean_help() -> None:
95 result = runner.invoke(cli, ["clean", "--help"])
96 assert result.exit_code == 0
97 # Rich injects ANSI codes between '--' dashes; the short flag '-f' is reliable.
98 assert "--force" in result.output or "-f" in result.output
99
100
101 # ---------------------------------------------------------------------------
102 # Unit: dry-run shows but does not delete
103 # ---------------------------------------------------------------------------
104
105
106 def test_clean_dry_run_shows_untracked(tmp_path: pathlib.Path) -> None:
107 _init_repo(tmp_path)
108 _commit_file(tmp_path, "tracked.txt", b"I am tracked")
109 untracked = tmp_path / "ghost.txt"
110 untracked.write_text("untracked", encoding="utf-8")
111
112 result = runner.invoke(cli, ["clean", "-n"], env=_env(tmp_path))
113 assert result.exit_code == 0
114 assert "ghost.txt" in result.output
115 assert untracked.exists() # not deleted
116
117
118 def test_clean_dry_run_short_flag(tmp_path: pathlib.Path) -> None:
119 _init_repo(tmp_path)
120 (tmp_path / "junk.txt").write_text("junk", encoding="utf-8")
121 result = runner.invoke(cli, ["clean", "-n"], env=_env(tmp_path))
122 assert result.exit_code == 0
123
124
125 # ---------------------------------------------------------------------------
126 # Unit: --force deletes untracked files
127 # ---------------------------------------------------------------------------
128
129
130 def test_clean_force_deletes_untracked(tmp_path: pathlib.Path) -> None:
131 _init_repo(tmp_path)
132 _commit_file(tmp_path, "kept.txt", b"keep me")
133 untracked = tmp_path / "delete_me.txt"
134 untracked.write_text("bye", encoding="utf-8")
135
136 result = runner.invoke(cli, ["clean", "-f"], env=_env(tmp_path))
137 assert result.exit_code == 0
138 assert not untracked.exists()
139 assert (tmp_path / "kept.txt").exists()
140
141
142 def test_clean_force_nothing_to_clean(tmp_path: pathlib.Path) -> None:
143 _init_repo(tmp_path)
144 _commit_file(tmp_path, "tracked.txt", b"tracked")
145
146 result = runner.invoke(cli, ["clean", "-f"], env=_env(tmp_path))
147 assert result.exit_code == 0
148 assert "nothing" in result.output.lower()
149
150
151 # ---------------------------------------------------------------------------
152 # Unit: --directories removes empty dirs
153 # ---------------------------------------------------------------------------
154
155
156 def test_clean_directories_removes_empty_dir(tmp_path: pathlib.Path) -> None:
157 _init_repo(tmp_path)
158 _commit_file(tmp_path, "kept.txt", b"kept")
159 empty_dir = tmp_path / "empty_dir"
160 empty_dir.mkdir()
161 (empty_dir / "junk.txt").write_text("junk", encoding="utf-8")
162
163 result = runner.invoke(cli, ["clean", "-f", "-d"], env=_env(tmp_path))
164 assert result.exit_code == 0
165 assert not (empty_dir / "junk.txt").exists()
166
167
168 # ---------------------------------------------------------------------------
169 # Integration: multiple untracked files
170 # ---------------------------------------------------------------------------
171
172
173 def test_clean_multiple_untracked(tmp_path: pathlib.Path) -> None:
174 _init_repo(tmp_path)
175 for i in range(10):
176 (tmp_path / f"untracked_{i}.txt").write_text(f"data {i}", encoding="utf-8")
177
178 result = runner.invoke(cli, ["clean", "-f"], env=_env(tmp_path))
179 assert result.exit_code == 0
180 remaining = [f for f in tmp_path.iterdir() if f.name.startswith("untracked")]
181 assert len(remaining) == 0
182
183
184 # ---------------------------------------------------------------------------
185 # Stress: 500 untracked files
186 # ---------------------------------------------------------------------------
187
188
189 def test_clean_stress_500_untracked(tmp_path: pathlib.Path) -> None:
190 _init_repo(tmp_path)
191 for i in range(500):
192 (tmp_path / f"stress_{i}.dat").write_bytes(b"x" * 100)
193
194 result = runner.invoke(cli, ["clean", "-f"], env=_env(tmp_path))
195 assert result.exit_code == 0
196 remaining = list(tmp_path.glob("stress_*.dat"))
197 assert len(remaining) == 0
File History 3 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 138 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 140 days ago