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