gabriel / muse public
test_cmd_sparse_checkout.py python
362 lines 13.7 KB
Raw
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor ⚠ breaking 150 days ago
1 """Tests for ``muse sparse-checkout`` — partial working-tree materialization.
2
3 Coverage tiers:
4 - Unit: init creates config; set replaces patterns; add appends patterns;
5 list shows patterns; disable removes config; cone matching;
6 pattern (glob) matching; filter_manifest_sparse; auto-read from root
7 - Integration: checkout respects sparse config; disable restores full tree;
8 cone mode directory filtering; pattern mode glob filtering;
9 JSON output for list; init --no-cone switches to pattern mode
10 - Security: ANSI injection in pattern name rejected; path traversal in pattern rejected
11 - Stress: 200-file manifest filtered to cone subdirectory (≤ 20 files)
12 """
13
14 from __future__ import annotations
15
16 import datetime
17 import hashlib
18 import json
19 import pathlib
20
21 import pytest
22
23 from tests.cli_test_helper import CliRunner
24 from muse.core.object_store import write_object
25 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
26 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
27 from muse.core._types import Manifest
28
29 runner = CliRunner()
30
31 _REPO_ID = "sparse-checkout-test"
32
33
34 # ---------------------------------------------------------------------------
35 # Helpers
36 # ---------------------------------------------------------------------------
37
38
39 def _sha(data: bytes) -> str:
40 return hashlib.sha256(data).hexdigest()
41
42
43 def _init_repo(path: pathlib.Path) -> pathlib.Path:
44 muse = path / ".muse"
45 for d in ("commits", "snapshots", "objects", "refs/heads", "code"):
46 (muse / d).mkdir(parents=True, exist_ok=True)
47 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
48 (muse / "repo.json").write_text(
49 json.dumps({"repo_id": _REPO_ID, "domain": "code"}), encoding="utf-8"
50 )
51 return path
52
53
54 def _env(repo: pathlib.Path) -> dict[str, str]:
55 return {"MUSE_REPO_ROOT": str(repo)}
56
57
58 def _write_files(root: pathlib.Path, files: dict[str, bytes]) -> Manifest:
59 manifest: Manifest = {}
60 for rel_path, content in files.items():
61 obj_id = _sha(content)
62 write_object(root, obj_id, content)
63 manifest[rel_path] = obj_id
64 abs_path = root / rel_path
65 abs_path.parent.mkdir(parents=True, exist_ok=True)
66 abs_path.write_bytes(content)
67 return manifest
68
69
70 def _invoke(args: list[str], repo: pathlib.Path):
71 """Invoke muse with MUSE_REPO_ROOT set; return (exit_code, stdout, stderr)."""
72 result = runner.invoke(None, args, env=_env(repo))
73 return result.exit_code, result.stdout, result.stderr
74
75
76 def _sparse_config(repo: pathlib.Path) -> pathlib.Path:
77 return repo / ".muse" / "sparse-checkout"
78
79
80 # ---------------------------------------------------------------------------
81 # Unit — init
82 # ---------------------------------------------------------------------------
83
84
85 class TestInit:
86 def test_init_creates_config_file(self, tmp_path):
87 repo = _init_repo(tmp_path / "repo")
88 rc, out, err = _invoke(["sparse-checkout", "init"], repo)
89 assert rc == 0
90 assert _sparse_config(repo).exists()
91
92 def test_init_default_mode_is_cone(self, tmp_path):
93 repo = _init_repo(tmp_path / "repo")
94 _invoke(["sparse-checkout", "init"], repo)
95 cfg = json.loads(_sparse_config(repo).read_text())
96 assert cfg["mode"] == "cone"
97
98 def test_init_no_cone_sets_pattern_mode(self, tmp_path):
99 repo = _init_repo(tmp_path / "repo")
100 rc, out, err = _invoke(["sparse-checkout", "init", "--no-cone"], repo)
101 assert rc == 0
102 cfg = json.loads(_sparse_config(repo).read_text())
103 assert cfg["mode"] == "pattern"
104
105 def test_init_idempotent(self, tmp_path):
106 repo = _init_repo(tmp_path / "repo")
107 _invoke(["sparse-checkout", "init"], repo)
108 _invoke(["sparse-checkout", "set", "src/"], repo)
109 rc, out, err = _invoke(["sparse-checkout", "init"], repo)
110 assert rc == 0
111 assert _sparse_config(repo).exists()
112
113
114 # ---------------------------------------------------------------------------
115 # Unit — set
116 # ---------------------------------------------------------------------------
117
118
119 class TestSet:
120 def test_set_writes_patterns(self, tmp_path):
121 repo = _init_repo(tmp_path / "repo")
122 _invoke(["sparse-checkout", "init"], repo)
123 rc, out, err = _invoke(["sparse-checkout", "set", "src/", "tests/"], repo)
124 assert rc == 0
125 cfg = json.loads(_sparse_config(repo).read_text())
126 assert cfg["patterns"] == ["src/", "tests/"]
127
128 def test_set_replaces_existing(self, tmp_path):
129 repo = _init_repo(tmp_path / "repo")
130 _invoke(["sparse-checkout", "init"], repo)
131 _invoke(["sparse-checkout", "set", "old/"], repo)
132 _invoke(["sparse-checkout", "set", "new/"], repo)
133 cfg = json.loads(_sparse_config(repo).read_text())
134 assert cfg["patterns"] == ["new/"]
135
136 def test_set_without_init_fails(self, tmp_path):
137 repo = _init_repo(tmp_path / "repo")
138 rc, out, err = _invoke(["sparse-checkout", "set", "src/"], repo)
139 assert rc != 0
140
141 def test_set_ansi_injection_rejected(self, tmp_path):
142 repo = _init_repo(tmp_path / "repo")
143 _invoke(["sparse-checkout", "init"], repo)
144 rc, out, err = _invoke(["sparse-checkout", "set", "\x1b[31mbad/\x1b[0m"], repo)
145 assert rc != 0
146
147
148 # ---------------------------------------------------------------------------
149 # Unit — add
150 # ---------------------------------------------------------------------------
151
152
153 class TestAdd:
154 def test_add_appends_patterns(self, tmp_path):
155 repo = _init_repo(tmp_path / "repo")
156 _invoke(["sparse-checkout", "init"], repo)
157 _invoke(["sparse-checkout", "set", "src/"], repo)
158 rc, out, err = _invoke(["sparse-checkout", "add", "tests/"], repo)
159 assert rc == 0
160 cfg = json.loads(_sparse_config(repo).read_text())
161 assert "src/" in cfg["patterns"]
162 assert "tests/" in cfg["patterns"]
163
164 def test_add_deduplicates(self, tmp_path):
165 repo = _init_repo(tmp_path / "repo")
166 _invoke(["sparse-checkout", "init"], repo)
167 _invoke(["sparse-checkout", "set", "src/"], repo)
168 _invoke(["sparse-checkout", "add", "src/"], repo)
169 cfg = json.loads(_sparse_config(repo).read_text())
170 assert cfg["patterns"].count("src/") == 1
171
172 def test_add_without_init_fails(self, tmp_path):
173 repo = _init_repo(tmp_path / "repo")
174 rc, out, err = _invoke(["sparse-checkout", "add", "src/"], repo)
175 assert rc != 0
176
177
178 # ---------------------------------------------------------------------------
179 # Unit — list
180 # ---------------------------------------------------------------------------
181
182
183 class TestList:
184 def test_list_shows_patterns_text(self, tmp_path):
185 repo = _init_repo(tmp_path / "repo")
186 _invoke(["sparse-checkout", "init"], repo)
187 _invoke(["sparse-checkout", "set", "src/", "docs/"], repo)
188 rc, out, err = _invoke(["sparse-checkout", "list"], repo)
189 assert rc == 0
190 assert "src/" in out
191 assert "docs/" in out
192
193 def test_list_json(self, tmp_path):
194 repo = _init_repo(tmp_path / "repo")
195 _invoke(["sparse-checkout", "init"], repo)
196 _invoke(["sparse-checkout", "set", "src/"], repo)
197 rc, out, err = _invoke(["sparse-checkout", "list", "--json"], repo)
198 assert rc == 0
199 data = json.loads(out)
200 assert data["mode"] == "cone"
201 assert "src/" in data["patterns"]
202
203 def test_list_when_disabled(self, tmp_path):
204 repo = _init_repo(tmp_path / "repo")
205 rc, out, err = _invoke(["sparse-checkout", "list"], repo)
206 assert rc == 0
207 assert "disabled" in out.lower()
208
209
210 # ---------------------------------------------------------------------------
211 # Unit — disable
212 # ---------------------------------------------------------------------------
213
214
215 class TestDisable:
216 def test_disable_removes_config(self, tmp_path):
217 repo = _init_repo(tmp_path / "repo")
218 _invoke(["sparse-checkout", "init"], repo)
219 _invoke(["sparse-checkout", "set", "src/"], repo)
220 rc, out, err = _invoke(["sparse-checkout", "disable"], repo)
221 assert rc == 0
222 assert not _sparse_config(repo).exists()
223
224 def test_disable_when_not_active_is_noop(self, tmp_path):
225 repo = _init_repo(tmp_path / "repo")
226 rc, out, err = _invoke(["sparse-checkout", "disable"], repo)
227 assert rc == 0
228
229
230 # ---------------------------------------------------------------------------
231 # Unit — core filter logic
232 # ---------------------------------------------------------------------------
233
234
235 class TestFilterLogic:
236 def test_cone_includes_root_files(self):
237 from muse.core.sparse import matches_sparse
238 assert matches_sparse("README.md", ["src/"], mode="cone")
239 assert matches_sparse("Makefile", ["src/"], mode="cone")
240
241 def test_cone_includes_files_in_pattern_dir(self):
242 from muse.core.sparse import matches_sparse
243 assert matches_sparse("src/foo.py", ["src/"], mode="cone")
244 assert matches_sparse("src/bar/baz.py", ["src/"], mode="cone")
245
246 def test_cone_excludes_other_dirs(self):
247 from muse.core.sparse import matches_sparse
248 assert not matches_sparse("tests/test_foo.py", ["src/"], mode="cone")
249 assert not matches_sparse("docs/readme.md", ["src/"], mode="cone")
250
251 def test_cone_multiple_dirs(self):
252 from muse.core.sparse import matches_sparse
253 assert matches_sparse("src/foo.py", ["src/", "tests/"], mode="cone")
254 assert matches_sparse("tests/test_foo.py", ["src/", "tests/"], mode="cone")
255 assert not matches_sparse("docs/guide.md", ["src/", "tests/"], mode="cone")
256
257 def test_pattern_mode_glob(self):
258 from muse.core.sparse import matches_sparse
259 assert matches_sparse("src/foo.py", ["src/**"], mode="pattern")
260 assert not matches_sparse("tests/foo.py", ["src/**"], mode="pattern")
261
262 def test_pattern_mode_extension(self):
263 from muse.core.sparse import matches_sparse
264 assert matches_sparse("foo.py", ["*.py"], mode="pattern")
265 assert not matches_sparse("foo.txt", ["*.py"], mode="pattern")
266
267 def test_filter_manifest_sparse_cone(self):
268 from muse.core.sparse import filter_manifest_sparse
269 manifest = {
270 "README.md": "aaa",
271 "src/foo.py": "bbb",
272 "tests/test_foo.py": "ccc",
273 "docs/guide.md": "ddd",
274 }
275 result = filter_manifest_sparse(manifest, ["src/"], mode="cone")
276 assert "README.md" in result
277 assert "src/foo.py" in result
278 assert "tests/test_foo.py" not in result
279 assert "docs/guide.md" not in result
280
281 def test_filter_manifest_sparse_pattern(self):
282 from muse.core.sparse import filter_manifest_sparse
283 manifest = {
284 "src/foo.py": "aaa",
285 "src/bar.txt": "bbb",
286 "tests/test_foo.py": "ccc",
287 }
288 result = filter_manifest_sparse(manifest, ["**/*.py"], mode="pattern")
289 assert "src/foo.py" in result
290 assert "tests/test_foo.py" in result
291 assert "src/bar.txt" not in result
292
293
294 # ---------------------------------------------------------------------------
295 # Integration — apply_manifest auto-reads sparse config
296 # ---------------------------------------------------------------------------
297
298
299 class TestApplyManifestSparse:
300 def test_apply_manifest_respects_sparse_config(self, tmp_path):
301 """apply_manifest should only materialize files matching sparse patterns."""
302 from muse.core.workdir import apply_manifest
303 repo = _init_repo(tmp_path / "repo")
304 _sparse_config(repo).write_text(
305 json.dumps({"mode": "cone", "patterns": ["src/"]}), encoding="utf-8"
306 )
307 manifest = {
308 "README.md": _sha(b"readme"),
309 "src/foo.py": _sha(b"foo"),
310 "tests/test_foo.py": _sha(b"test"),
311 }
312 write_object(repo, _sha(b"readme"), b"readme")
313 write_object(repo, _sha(b"foo"), b"foo")
314 write_object(repo, _sha(b"test"), b"test")
315
316 apply_manifest(repo, manifest)
317
318 assert (repo / "README.md").exists()
319 assert (repo / "src" / "foo.py").exists()
320 assert not (repo / "tests" / "test_foo.py").exists()
321
322 def test_apply_manifest_no_sparse_config_writes_all(self, tmp_path):
323 """Without sparse config, apply_manifest writes everything."""
324 from muse.core.workdir import apply_manifest
325 repo = _init_repo(tmp_path / "repo")
326 manifest = {
327 "README.md": _sha(b"readme"),
328 "src/foo.py": _sha(b"foo"),
329 "tests/test_foo.py": _sha(b"test"),
330 }
331 write_object(repo, _sha(b"readme"), b"readme")
332 write_object(repo, _sha(b"foo"), b"foo")
333 write_object(repo, _sha(b"test"), b"test")
334
335 apply_manifest(repo, manifest)
336
337 assert (repo / "README.md").exists()
338 assert (repo / "src" / "foo.py").exists()
339 assert (repo / "tests" / "test_foo.py").exists()
340
341
342 # ---------------------------------------------------------------------------
343 # Stress — 200-file manifest filtered to cone
344 # ---------------------------------------------------------------------------
345
346
347 class TestStress:
348 def test_200_file_manifest_cone_filter(self):
349 from muse.core.sparse import filter_manifest_sparse
350 manifest: Manifest = {}
351 for i in range(100):
352 manifest[f"src/file_{i:03d}.py"] = _sha(f"src-{i}".encode())
353 for i in range(100):
354 manifest[f"other/file_{i:03d}.py"] = _sha(f"other-{i}".encode())
355 manifest["README.md"] = _sha(b"readme")
356
357 result = filter_manifest_sparse(manifest, ["src/"], mode="cone")
358 # src/ files + root files
359 assert len(result) == 101 # 100 src + README
360 assert all(
361 k.startswith("src/") or "/" not in k for k in result
362 )
File History 1 commit
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 150 days ago