gabriel / muse public
test_cmd_sparse_checkout.py python
387 lines 14.6 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 132 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 from collections.abc import Mapping
16
17 import datetime
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, blob_id, load_json_file
28
29 runner = CliRunner()
30
31 _REPO_ID = "sparse-checkout-test"
32
33
34 # ---------------------------------------------------------------------------
35 # Helpers
36 # ---------------------------------------------------------------------------
37
38
39
40 def _init_repo(path: pathlib.Path) -> pathlib.Path:
41 muse = path / ".muse"
42 for d in ("commits", "snapshots", "objects", "refs/heads", "code"):
43 (muse / d).mkdir(parents=True, exist_ok=True)
44 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
45 (muse / "repo.json").write_text(
46 json.dumps({"repo_id": _REPO_ID, "domain": "code"}), encoding="utf-8"
47 )
48 return path
49
50
51 def _env(repo: pathlib.Path) -> Mapping[str, str]:
52 return {"MUSE_REPO_ROOT": str(repo)}
53
54
55 def _write_files(root: pathlib.Path, files: Mapping[str, bytes]) -> Manifest:
56 manifest: Manifest = {}
57 for rel_path, content in files.items():
58 obj_id = blob_id(content)
59 write_object(root, obj_id, content)
60 manifest[rel_path] = obj_id
61 abs_path = root / rel_path
62 abs_path.parent.mkdir(parents=True, exist_ok=True)
63 abs_path.write_bytes(content)
64 return manifest
65
66
67 def _invoke(args: list[str], repo: pathlib.Path):
68 """Invoke muse with MUSE_REPO_ROOT set; return (exit_code, stdout, stderr)."""
69 result = runner.invoke(None, args, env=_env(repo))
70 return result.exit_code, result.stdout, result.stderr
71
72
73 def _sparse_config(repo: pathlib.Path) -> pathlib.Path:
74 return repo / ".muse" / "sparse-checkout"
75
76
77 # ---------------------------------------------------------------------------
78 # Unit — init
79 # ---------------------------------------------------------------------------
80
81
82 class TestInit:
83 def test_init_creates_config_file(self, tmp_path):
84 repo = _init_repo(tmp_path / "repo")
85 rc, out, err = _invoke(["sparse-checkout", "init"], repo)
86 assert rc == 0
87 assert _sparse_config(repo).exists()
88
89 def test_init_default_mode_is_cone(self, tmp_path):
90 repo = _init_repo(tmp_path / "repo")
91 _invoke(["sparse-checkout", "init"], repo)
92 cfg = load_json_file(_sparse_config(repo))
93 assert cfg["mode"] == "cone"
94
95 def test_init_no_cone_sets_pattern_mode(self, tmp_path):
96 repo = _init_repo(tmp_path / "repo")
97 rc, out, err = _invoke(["sparse-checkout", "init", "--no-cone"], repo)
98 assert rc == 0
99 cfg = load_json_file(_sparse_config(repo))
100 assert cfg["mode"] == "pattern"
101
102 def test_init_idempotent(self, tmp_path):
103 repo = _init_repo(tmp_path / "repo")
104 _invoke(["sparse-checkout", "init"], repo)
105 _invoke(["sparse-checkout", "set", "src/"], repo)
106 rc, out, err = _invoke(["sparse-checkout", "init"], repo)
107 assert rc == 0
108 assert _sparse_config(repo).exists()
109
110
111 # ---------------------------------------------------------------------------
112 # Unit — set
113 # ---------------------------------------------------------------------------
114
115
116 class TestSet:
117 def test_set_writes_patterns(self, tmp_path):
118 repo = _init_repo(tmp_path / "repo")
119 _invoke(["sparse-checkout", "init"], repo)
120 rc, out, err = _invoke(["sparse-checkout", "set", "src/", "tests/"], repo)
121 assert rc == 0
122 cfg = load_json_file(_sparse_config(repo))
123 assert cfg["patterns"] == ["src/", "tests/"]
124
125 def test_set_replaces_existing(self, tmp_path):
126 repo = _init_repo(tmp_path / "repo")
127 _invoke(["sparse-checkout", "init"], repo)
128 _invoke(["sparse-checkout", "set", "old/"], repo)
129 _invoke(["sparse-checkout", "set", "new/"], repo)
130 cfg = load_json_file(_sparse_config(repo))
131 assert cfg["patterns"] == ["new/"]
132
133 def test_set_without_init_fails(self, tmp_path):
134 repo = _init_repo(tmp_path / "repo")
135 rc, out, err = _invoke(["sparse-checkout", "set", "src/"], repo)
136 assert rc != 0
137
138 def test_set_ansi_injection_rejected(self, tmp_path):
139 repo = _init_repo(tmp_path / "repo")
140 _invoke(["sparse-checkout", "init"], repo)
141 rc, out, err = _invoke(["sparse-checkout", "set", "\x1b[31mbad/\x1b[0m"], repo)
142 assert rc != 0
143
144
145 # ---------------------------------------------------------------------------
146 # Unit — add
147 # ---------------------------------------------------------------------------
148
149
150 class TestAdd:
151 def test_add_appends_patterns(self, tmp_path):
152 repo = _init_repo(tmp_path / "repo")
153 _invoke(["sparse-checkout", "init"], repo)
154 _invoke(["sparse-checkout", "set", "src/"], repo)
155 rc, out, err = _invoke(["sparse-checkout", "add", "tests/"], repo)
156 assert rc == 0
157 cfg = load_json_file(_sparse_config(repo))
158 assert "src/" in cfg["patterns"]
159 assert "tests/" in cfg["patterns"]
160
161 def test_add_deduplicates(self, tmp_path):
162 repo = _init_repo(tmp_path / "repo")
163 _invoke(["sparse-checkout", "init"], repo)
164 _invoke(["sparse-checkout", "set", "src/"], repo)
165 _invoke(["sparse-checkout", "add", "src/"], repo)
166 cfg = load_json_file(_sparse_config(repo))
167 assert cfg["patterns"].count("src/") == 1
168
169 def test_add_without_init_fails(self, tmp_path):
170 repo = _init_repo(tmp_path / "repo")
171 rc, out, err = _invoke(["sparse-checkout", "add", "src/"], repo)
172 assert rc != 0
173
174
175 # ---------------------------------------------------------------------------
176 # Unit — list
177 # ---------------------------------------------------------------------------
178
179
180 class TestList:
181 def test_list_shows_patterns_text(self, tmp_path):
182 repo = _init_repo(tmp_path / "repo")
183 _invoke(["sparse-checkout", "init"], repo)
184 _invoke(["sparse-checkout", "set", "src/", "docs/"], repo)
185 rc, out, err = _invoke(["sparse-checkout", "list"], repo)
186 assert rc == 0
187 assert "src/" in out
188 assert "docs/" in out
189
190 def test_list_json(self, tmp_path):
191 repo = _init_repo(tmp_path / "repo")
192 _invoke(["sparse-checkout", "init"], repo)
193 _invoke(["sparse-checkout", "set", "src/"], repo)
194 rc, out, err = _invoke(["sparse-checkout", "list", "--json"], repo)
195 assert rc == 0
196 data = json.loads(out)
197 assert data["mode"] == "cone"
198 assert "src/" in data["patterns"]
199
200 def test_list_when_disabled(self, tmp_path):
201 repo = _init_repo(tmp_path / "repo")
202 rc, out, err = _invoke(["sparse-checkout", "list"], repo)
203 assert rc == 0
204 assert "disabled" in out.lower()
205
206
207 # ---------------------------------------------------------------------------
208 # Unit — disable
209 # ---------------------------------------------------------------------------
210
211
212 class TestDisable:
213 def test_disable_removes_config(self, tmp_path):
214 repo = _init_repo(tmp_path / "repo")
215 _invoke(["sparse-checkout", "init"], repo)
216 _invoke(["sparse-checkout", "set", "src/"], repo)
217 rc, out, err = _invoke(["sparse-checkout", "disable"], repo)
218 assert rc == 0
219 assert not _sparse_config(repo).exists()
220
221 def test_disable_when_not_active_is_noop(self, tmp_path):
222 repo = _init_repo(tmp_path / "repo")
223 rc, out, err = _invoke(["sparse-checkout", "disable"], repo)
224 assert rc == 0
225
226
227 # ---------------------------------------------------------------------------
228 # Unit — core filter logic
229 # ---------------------------------------------------------------------------
230
231
232 class TestFilterLogic:
233 def test_cone_includes_root_files(self):
234 from muse.core.sparse import matches_sparse
235 assert matches_sparse("README.md", ["src/"], mode="cone")
236 assert matches_sparse("Makefile", ["src/"], mode="cone")
237
238 def test_cone_includes_files_in_pattern_dir(self):
239 from muse.core.sparse import matches_sparse
240 assert matches_sparse("src/foo.py", ["src/"], mode="cone")
241 assert matches_sparse("src/bar/baz.py", ["src/"], mode="cone")
242
243 def test_cone_excludes_other_dirs(self):
244 from muse.core.sparse import matches_sparse
245 assert not matches_sparse("tests/test_foo.py", ["src/"], mode="cone")
246 assert not matches_sparse("docs/readme.md", ["src/"], mode="cone")
247
248 def test_cone_multiple_dirs(self):
249 from muse.core.sparse import matches_sparse
250 assert matches_sparse("src/foo.py", ["src/", "tests/"], mode="cone")
251 assert matches_sparse("tests/test_foo.py", ["src/", "tests/"], mode="cone")
252 assert not matches_sparse("docs/guide.md", ["src/", "tests/"], mode="cone")
253
254 def test_pattern_mode_glob(self):
255 from muse.core.sparse import matches_sparse
256 assert matches_sparse("src/foo.py", ["src/**"], mode="pattern")
257 assert not matches_sparse("tests/foo.py", ["src/**"], mode="pattern")
258
259 def test_pattern_mode_extension(self):
260 from muse.core.sparse import matches_sparse
261 assert matches_sparse("foo.py", ["*.py"], mode="pattern")
262 assert not matches_sparse("foo.txt", ["*.py"], mode="pattern")
263
264 def test_filter_manifest_sparse_cone(self):
265 from muse.core.sparse import filter_manifest_sparse
266 manifest = {
267 "README.md": "aaa",
268 "src/foo.py": "bbb",
269 "tests/test_foo.py": "ccc",
270 "docs/guide.md": "ddd",
271 }
272 result = filter_manifest_sparse(manifest, ["src/"], mode="cone")
273 assert "README.md" in result
274 assert "src/foo.py" in result
275 assert "tests/test_foo.py" not in result
276 assert "docs/guide.md" not in result
277
278 def test_filter_manifest_sparse_pattern(self):
279 from muse.core.sparse import filter_manifest_sparse
280 manifest = {
281 "src/foo.py": "aaa",
282 "src/bar.txt": "bbb",
283 "tests/test_foo.py": "ccc",
284 }
285 result = filter_manifest_sparse(manifest, ["**/*.py"], mode="pattern")
286 assert "src/foo.py" in result
287 assert "tests/test_foo.py" in result
288 assert "src/bar.txt" not in result
289
290
291 # ---------------------------------------------------------------------------
292 # Integration — apply_manifest auto-reads sparse config
293 # ---------------------------------------------------------------------------
294
295
296 class TestApplyManifestSparse:
297 def test_apply_manifest_respects_sparse_config(self, tmp_path):
298 """apply_manifest should only materialize files matching sparse patterns."""
299 from muse.core.workdir import apply_manifest
300 repo = _init_repo(tmp_path / "repo")
301 _sparse_config(repo).write_text(
302 json.dumps({"mode": "cone", "patterns": ["src/"]}), encoding="utf-8"
303 )
304 manifest = {
305 "README.md": blob_id(b"readme"),
306 "src/foo.py": blob_id(b"foo"),
307 "tests/test_foo.py": blob_id(b"test"),
308 }
309 write_object(repo, blob_id(b"readme"), b"readme")
310 write_object(repo, blob_id(b"foo"), b"foo")
311 write_object(repo, blob_id(b"test"), b"test")
312
313 apply_manifest(repo, {}, manifest)
314
315 assert (repo / "README.md").exists()
316 assert (repo / "src" / "foo.py").exists()
317 assert not (repo / "tests" / "test_foo.py").exists()
318
319 def test_apply_manifest_no_sparse_config_writes_all(self, tmp_path):
320 """Without sparse config, apply_manifest writes everything."""
321 from muse.core.workdir import apply_manifest
322 repo = _init_repo(tmp_path / "repo")
323 manifest = {
324 "README.md": blob_id(b"readme"),
325 "src/foo.py": blob_id(b"foo"),
326 "tests/test_foo.py": blob_id(b"test"),
327 }
328 write_object(repo, blob_id(b"readme"), b"readme")
329 write_object(repo, blob_id(b"foo"), b"foo")
330 write_object(repo, blob_id(b"test"), b"test")
331
332 apply_manifest(repo, {}, manifest)
333
334 assert (repo / "README.md").exists()
335 assert (repo / "src" / "foo.py").exists()
336 assert (repo / "tests" / "test_foo.py").exists()
337
338
339 # ---------------------------------------------------------------------------
340 # Stress — 200-file manifest filtered to cone
341 # ---------------------------------------------------------------------------
342
343
344 class TestStress:
345 def test_200_file_manifest_cone_filter(self):
346 from muse.core.sparse import filter_manifest_sparse
347 manifest: Manifest = {}
348 for i in range(100):
349 manifest[f"src/file_{i:03d}.py"] = blob_id(f"src-{i}".encode())
350 for i in range(100):
351 manifest[f"other/file_{i:03d}.py"] = blob_id(f"other-{i}".encode())
352 manifest["README.md"] = blob_id(b"readme")
353
354 result = filter_manifest_sparse(manifest, ["src/"], mode="cone")
355 # src/ files + root files
356 assert len(result) == 101 # 100 src + README
357 assert all(
358 k.startswith("src/") or "/" not in k for k in result
359 )
360
361
362 # ---------------------------------------------------------------------------
363 # Flag registration tests
364 # ---------------------------------------------------------------------------
365
366
367 class TestRegisterFlags:
368 def _parser(self):
369 import argparse
370 from muse.cli.commands.sparse_checkout import register
371
372 p = argparse.ArgumentParser()
373 subs = p.add_subparsers()
374 register(subs)
375 return p
376
377 def test_default_json_out_is_false(self):
378 args = self._parser().parse_args(["sparse-checkout", "init"])
379 assert args.json_out is False
380
381 def test_json_flag_sets_json_out(self):
382 args = self._parser().parse_args(["sparse-checkout", "init", "--json"])
383 assert args.json_out is True
384
385 def test_j_shorthand_sets_json_out(self):
386 args = self._parser().parse_args(["sparse-checkout", "init", "-j"])
387 assert args.json_out is True
File History 3 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 132 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 141 days ago