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