gabriel / muse public
test_cmd_gc.py python
248 lines 9.4 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 136 days ago
1 """Comprehensive tests for ``muse gc``.
2
3 Covers:
4 - Unit: run_gc core logic (reachable vs unreachable objects)
5 - Integration: gc cleans up orphaned objects after commits
6 - E2E: full CLI via CliRunner (--dry-run, --verbose, --format json)
7 - Security: only objects dir affected, no path traversal
8 - Stress: gc with many orphaned objects
9 """
10
11 from __future__ import annotations
12
13 import datetime
14 import json
15 import pathlib
16
17 import pytest
18 from tests.cli_test_helper import CliRunner
19 from muse.core._types import blob_id, fake_id, short_id
20 from muse.core.object_store import object_path
21
22 cli = None # argparse migration — CliRunner ignores this arg
23
24 runner = CliRunner()
25
26
27 # ---------------------------------------------------------------------------
28 # Helpers
29 # ---------------------------------------------------------------------------
30
31 def _env(root: pathlib.Path) -> Manifest:
32 return {"MUSE_REPO_ROOT": str(root)}
33
34
35 def _init_repo(tmp_path: pathlib.Path) -> tuple[pathlib.Path, str]:
36 muse_dir = tmp_path / ".muse"
37 muse_dir.mkdir()
38 repo_id = fake_id("repo")
39 (muse_dir / "repo.json").write_text(json.dumps({
40 "repo_id": repo_id,
41 "domain": "midi",
42 "default_branch": "main",
43 "created_at": "2025-01-01T00:00:00+00:00",
44 }), encoding="utf-8")
45 (muse_dir / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
46 (muse_dir / "refs" / "heads").mkdir(parents=True)
47 (muse_dir / "snapshots").mkdir()
48 (muse_dir / "commits").mkdir()
49 (muse_dir / "objects" / "sha256").mkdir(parents=True)
50 return tmp_path, repo_id
51
52
53 def _write_object(root: pathlib.Path, content: bytes) -> str:
54 oid = blob_id(content)
55 p = object_path(root, oid)
56 p.parent.mkdir(parents=True, exist_ok=True)
57 p.write_bytes(content)
58 return oid
59
60
61 def _make_commit(root: pathlib.Path, repo_id: str, message: str = "init") -> str:
62 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
63 from muse.core.snapshot import compute_snapshot_id, compute_commit_id
64
65 ref_file = root / ".muse" / "refs" / "heads" / "main"
66 parent_id = ref_file.read_text().strip() if ref_file.exists() else None
67 manifest: Manifest = {}
68 snap_id = compute_snapshot_id(manifest)
69 committed_at = datetime.datetime.now(datetime.timezone.utc)
70 commit_id = compute_commit_id(
71 repo_id=repo_id,
72 parent_ids=[parent_id] if parent_id else [],
73 snapshot_id=snap_id,
74 message=message,
75 committed_at_iso=committed_at.isoformat(),
76 )
77 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
78 write_commit(root, CommitRecord(
79 commit_id=commit_id, repo_id=repo_id, created_on_branch="main",
80 snapshot_id=snap_id, message=message, committed_at=committed_at,
81 parent_commit_id=parent_id,
82 ))
83 ref_file.parent.mkdir(parents=True, exist_ok=True)
84 ref_file.write_text(commit_id, encoding="utf-8")
85 return commit_id
86
87
88 # ---------------------------------------------------------------------------
89 # Unit tests
90 # ---------------------------------------------------------------------------
91
92
93 class TestRegisterFlags:
94 def _parse(self, *args: str) -> "argparse.Namespace":
95 import argparse
96 from muse.cli.commands.gc import register
97 p = argparse.ArgumentParser()
98 sub = p.add_subparsers()
99 register(sub)
100 return p.parse_args(["gc", *args])
101
102 def test_default_json_out_is_false(self) -> None:
103 ns = self._parse()
104 assert ns.json_out is False
105
106 def test_json_flag_sets_json_out(self) -> None:
107 ns = self._parse("--json")
108 assert ns.json_out is True
109
110 def test_j_shorthand_sets_json_out(self) -> None:
111 ns = self._parse("-j")
112 assert ns.json_out is True
113
114
115 class TestGcUnit:
116 def test_run_gc_empty_repo(self, tmp_path: pathlib.Path) -> None:
117 root, _ = _init_repo(tmp_path)
118 from muse.core.gc import run_gc
119 result = run_gc(root, dry_run=False)
120 assert result.collected_count == 0
121
122 def test_run_gc_dry_run_does_not_delete(self, tmp_path: pathlib.Path) -> None:
123 root, _ = _init_repo(tmp_path)
124 orphan_id = _write_object(root, b"orphaned content")
125 from muse.core.gc import run_gc
126 result = run_gc(root, dry_run=True, grace_period_seconds=0)
127 assert object_path(root, orphan_id).exists()
128 assert result.collected_count >= 1
129
130 def test_run_gc_collects_unreachable_objects(self, tmp_path: pathlib.Path) -> None:
131 root, repo_id = _init_repo(tmp_path)
132 _make_commit(root, repo_id, message="committed")
133 orphan_id = _write_object(root, b"never committed content")
134 from muse.core.gc import run_gc
135 result = run_gc(root, dry_run=False, grace_period_seconds=0)
136 assert not object_path(root, orphan_id).exists()
137 assert orphan_id in result.collected_ids
138
139
140 # ---------------------------------------------------------------------------
141 # Integration (CLI) tests
142 # ---------------------------------------------------------------------------
143
144 class TestGcIntegration:
145 def test_gc_default_clean_repo(self, tmp_path: pathlib.Path) -> None:
146 root, repo_id = _init_repo(tmp_path)
147 _make_commit(root, repo_id)
148 result = runner.invoke(cli, ["gc"], env=_env(root), catch_exceptions=False)
149 assert result.exit_code == 0
150
151 def test_gc_dry_run_reports_orphans(self, tmp_path: pathlib.Path) -> None:
152 root, repo_id = _init_repo(tmp_path)
153 _make_commit(root, repo_id)
154 _write_object(root, b"orphan1")
155 _write_object(root, b"orphan2")
156 result = runner.invoke(
157 cli, ["gc", "--dry-run", "--grace-period", "0"],
158 env=_env(root), catch_exceptions=False,
159 )
160 assert result.exit_code == 0
161 assert "2" in result.output or "collect" in result.output.lower()
162
163 def test_gc_verbose_shows_ids(self, tmp_path: pathlib.Path) -> None:
164 root, repo_id = _init_repo(tmp_path)
165 _make_commit(root, repo_id)
166 orphan_id = _write_object(root, b"verbose orphan")
167 result = runner.invoke(
168 cli, ["gc", "--verbose", "--grace-period", "0"],
169 env=_env(root), catch_exceptions=False,
170 )
171 assert result.exit_code == 0
172 assert short_id(orphan_id, strip=True) in result.output
173
174 def test_gc_output_includes_count(self, tmp_path: pathlib.Path) -> None:
175 root, repo_id = _init_repo(tmp_path)
176 _write_object(root, b"orphan for count test")
177 result = runner.invoke(
178 cli, ["gc", "--grace-period", "0"],
179 env=_env(root), catch_exceptions=False,
180 )
181 assert result.exit_code == 0
182 assert "Removed" in result.output or "object" in result.output
183
184 def test_gc_keeps_referenced_objects(self, tmp_path: pathlib.Path) -> None:
185 root, repo_id = _init_repo(tmp_path)
186 content = b"referenced file content"
187 obj_id = _write_object(root, content)
188
189 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
190 from muse.core.snapshot import compute_snapshot_id, compute_commit_id
191
192 manifest = {"file.mid": obj_id}
193 snap_id = compute_snapshot_id(manifest)
194 committed_at = datetime.datetime.now(datetime.timezone.utc)
195 commit_id = compute_commit_id(
196 repo_id=repo_id,
197 parent_ids=[],
198 snapshot_id=snap_id,
199 message="with file",
200 committed_at_iso=committed_at.isoformat(),
201 )
202 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
203 write_commit(root, CommitRecord(
204 commit_id=commit_id, repo_id=repo_id, created_on_branch="main",
205 snapshot_id=snap_id, message="with file",
206 committed_at=committed_at, parent_commit_id=None,
207 ))
208 (root / ".muse" / "refs" / "heads" / "main").write_text(commit_id)
209
210 runner.invoke(cli, ["gc", "--grace-period", "0"], env=_env(root), catch_exceptions=False)
211 assert object_path(root, obj_id).exists()
212
213 def test_gc_short_flags(self, tmp_path: pathlib.Path) -> None:
214 root, repo_id = _init_repo(tmp_path)
215 _make_commit(root, repo_id)
216 _write_object(root, b"short flag orphan")
217 result = runner.invoke(
218 cli, ["gc", "-n", "-v", "--grace-period", "0"],
219 env=_env(root), catch_exceptions=False,
220 )
221 assert result.exit_code == 0
222
223
224 # ---------------------------------------------------------------------------
225 # Stress tests
226 # ---------------------------------------------------------------------------
227
228 class TestGcStress:
229 def test_gc_many_orphaned_objects(self, tmp_path: pathlib.Path) -> None:
230 root, repo_id = _init_repo(tmp_path)
231 _make_commit(root, repo_id)
232 orphan_ids = [_write_object(root, f"orphan {i}".encode()) for i in range(100)]
233
234 result = runner.invoke(
235 cli, ["gc", "--grace-period", "0"], env=_env(root), catch_exceptions=False,
236 )
237 assert result.exit_code == 0
238 assert "100" in result.output
239
240 for oid in orphan_ids:
241 assert not object_path(root, oid).exists()
242
243 def test_gc_repeated_runs_idempotent(self, tmp_path: pathlib.Path) -> None:
244 root, repo_id = _init_repo(tmp_path)
245 _make_commit(root, repo_id)
246 for _ in range(3):
247 result = runner.invoke(cli, ["gc"], env=_env(root), catch_exceptions=False)
248 assert result.exit_code == 0
File History 3 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 136 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 142 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 145 days ago