gabriel / muse public
test_sparse_checkout_supercharge.py python
697 lines 27.2 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 138 days ago
1 """Supercharge tests for ``muse sparse-checkout``.
2
3 Covers gaps from the baseline test suite:
4
5 - JSON output with ``duration_ms`` / ``exit_code`` on ALL subcommands
6 - Exact JSON schema for every subcommand
7 - ``stats`` subcommand (matching_files, excluded_files, efficiency, total_files)
8 - Security: path traversal patterns (``../``), null bytes
9 - Mode switching via ``init --no-cone`` on an already-initialised cone repo
10 - Config corruption graceful recovery
11 - Pattern edge cases: whitespace, very long patterns, unicode
12 - Integration: stats against a real HEAD snapshot
13 - Data integrity: set replaces cleanly; add deduplicates exactly
14 - Stress: 500-pattern list, 1 000-file manifest stats
15 """
16
17 from __future__ import annotations
18
19 import datetime
20 import json
21 import pathlib
22
23 import pytest
24
25 from muse.core._types import Manifest, blob_id
26 from muse.core.object_store import write_object
27 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
28 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
29 from tests.cli_test_helper import CliRunner
30
31 runner = CliRunner()
32 cli = None
33
34 _REPO_ID = "sparse-supercharge-test"
35
36
37 # ---------------------------------------------------------------------------
38 # Helpers
39 # ---------------------------------------------------------------------------
40
41
42 def _init_repo(path: pathlib.Path) -> pathlib.Path:
43 muse = path / ".muse"
44 for d in ("commits", "snapshots", "objects", "refs/heads", "code"):
45 (muse / d).mkdir(parents=True, exist_ok=True)
46 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
47 (muse / "repo.json").write_text(
48 json.dumps({"repo_id": _REPO_ID, "domain": "code"}), encoding="utf-8"
49 )
50 return path
51
52
53 def _env(repo: pathlib.Path) -> dict[str, str]:
54 return {"MUSE_REPO_ROOT": str(repo)}
55
56
57 def _invoke(args: list[str], repo: pathlib.Path):
58 result = runner.invoke(cli, args, env=_env(repo))
59 return result.exit_code, result.stdout, result.stderr
60
61
62 def _sparse_config(repo: pathlib.Path) -> pathlib.Path:
63 return repo / ".muse" / "sparse-checkout"
64
65
66 def _obj(repo: pathlib.Path, content: bytes) -> str:
67 oid = blob_id(content)
68 write_object(repo, oid, content)
69 return oid
70
71
72 def _snap(repo: pathlib.Path, manifest: Manifest) -> str:
73 sid = compute_snapshot_id(manifest)
74 write_snapshot(
75 repo,
76 SnapshotRecord(
77 snapshot_id=sid,
78 manifest=manifest,
79 created_at=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
80 ),
81 )
82 return sid
83
84
85 def _commit(repo: pathlib.Path, sid: str, branch: str = "main") -> str:
86 committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
87 cid = compute_commit_id([], sid, "test", committed_at.isoformat())
88 write_commit(
89 repo,
90 CommitRecord(
91 commit_id=cid,
92 repo_id=_REPO_ID,
93 branch=branch,
94 snapshot_id=sid,
95 message="test",
96 committed_at=committed_at,
97 author="gabriel",
98 parent_commit_id=None,
99 ),
100 )
101 ref = repo / ".muse" / "refs" / "heads" / branch
102 ref.write_text(cid, encoding="utf-8")
103 return cid
104
105
106 def _make_repo_with_snapshot(tmp_path: pathlib.Path) -> tuple[pathlib.Path, str]:
107 """Return (repo, snapshot_id) with 10 files: 5 under src/, 5 under tests/."""
108 repo = _init_repo(tmp_path)
109 manifest: Manifest = {}
110 for i in range(5):
111 oid = _obj(repo, f"src content {i}".encode())
112 manifest[f"src/module_{i}.py"] = oid
113 for i in range(5):
114 oid = _obj(repo, f"test content {i}".encode())
115 manifest[f"tests/test_{i}.py"] = oid
116 oid_root = _obj(repo, b"readme")
117 manifest["README.md"] = oid_root
118 sid = _snap(repo, manifest)
119 _commit(repo, sid)
120 return repo, sid
121
122
123 # ---------------------------------------------------------------------------
124 # TestJsonEnvelopeAllSubcommands
125 # ---------------------------------------------------------------------------
126
127
128 class TestJsonEnvelopeAllSubcommands:
129 """Every subcommand must emit duration_ms and exit_code when --json is passed."""
130
131 def test_init_json_has_duration_ms(self, tmp_path):
132 repo = _init_repo(tmp_path)
133 rc, out, _ = _invoke(["sparse-checkout", "init", "--json"], repo)
134 assert rc == 0
135 data = json.loads(out)
136 assert "duration_ms" in data, "init --json must include duration_ms"
137 assert isinstance(data["duration_ms"], (int, float))
138
139 def test_init_json_has_exit_code(self, tmp_path):
140 repo = _init_repo(tmp_path)
141 rc, out, _ = _invoke(["sparse-checkout", "init", "--json"], repo)
142 assert rc == 0
143 data = json.loads(out)
144 assert data["exit_code"] == 0
145
146 def test_set_json_has_duration_ms(self, tmp_path):
147 repo = _init_repo(tmp_path)
148 _invoke(["sparse-checkout", "init"], repo)
149 rc, out, _ = _invoke(["sparse-checkout", "set", "src/", "--json"], repo)
150 assert rc == 0
151 data = json.loads(out)
152 assert "duration_ms" in data
153
154 def test_set_json_has_exit_code(self, tmp_path):
155 repo = _init_repo(tmp_path)
156 _invoke(["sparse-checkout", "init"], repo)
157 rc, out, _ = _invoke(["sparse-checkout", "set", "src/", "--json"], repo)
158 assert rc == 0
159 data = json.loads(out)
160 assert data["exit_code"] == 0
161
162 def test_add_json_has_duration_ms(self, tmp_path):
163 repo = _init_repo(tmp_path)
164 _invoke(["sparse-checkout", "init"], repo)
165 rc, out, _ = _invoke(["sparse-checkout", "add", "src/", "--json"], repo)
166 assert rc == 0
167 data = json.loads(out)
168 assert "duration_ms" in data
169
170 def test_add_json_has_exit_code(self, tmp_path):
171 repo = _init_repo(tmp_path)
172 _invoke(["sparse-checkout", "init"], repo)
173 rc, out, _ = _invoke(["sparse-checkout", "add", "src/", "--json"], repo)
174 assert rc == 0
175 data = json.loads(out)
176 assert data["exit_code"] == 0
177
178 def test_disable_json_has_duration_ms(self, tmp_path):
179 repo = _init_repo(tmp_path)
180 _invoke(["sparse-checkout", "init"], repo)
181 rc, out, _ = _invoke(["sparse-checkout", "disable", "--json"], repo)
182 assert rc == 0
183 data = json.loads(out)
184 assert "duration_ms" in data
185
186 def test_disable_json_has_exit_code(self, tmp_path):
187 repo = _init_repo(tmp_path)
188 _invoke(["sparse-checkout", "init"], repo)
189 rc, out, _ = _invoke(["sparse-checkout", "disable", "--json"], repo)
190 assert rc == 0
191 data = json.loads(out)
192 assert data["exit_code"] == 0
193
194 def test_list_json_has_duration_ms(self, tmp_path):
195 repo = _init_repo(tmp_path)
196 _invoke(["sparse-checkout", "init"], repo)
197 rc, out, _ = _invoke(["sparse-checkout", "list", "--json"], repo)
198 assert rc == 0
199 data = json.loads(out)
200 assert "duration_ms" in data
201
202 def test_list_json_has_exit_code(self, tmp_path):
203 repo = _init_repo(tmp_path)
204 _invoke(["sparse-checkout", "init"], repo)
205 rc, out, _ = _invoke(["sparse-checkout", "list", "--json"], repo)
206 assert rc == 0
207 data = json.loads(out)
208 assert data["exit_code"] == 0
209
210
211 # ---------------------------------------------------------------------------
212 # TestInitJsonSchema
213 # ---------------------------------------------------------------------------
214
215
216 class TestInitJsonSchema:
217 """init --json must emit the correct schema in every scenario."""
218
219 def test_fresh_init_cone_mode(self, tmp_path):
220 repo = _init_repo(tmp_path)
221 rc, out, _ = _invoke(["sparse-checkout", "init", "--json"], repo)
222 assert rc == 0
223 data = json.loads(out)
224 assert data["mode"] == "cone"
225 assert data["switched"] is False
226 assert data["exit_code"] == 0
227
228 def test_fresh_init_pattern_mode(self, tmp_path):
229 repo = _init_repo(tmp_path)
230 rc, out, _ = _invoke(["sparse-checkout", "init", "--no-cone", "--json"], repo)
231 assert rc == 0
232 data = json.loads(out)
233 assert data["mode"] == "pattern"
234 assert data["switched"] is False
235 assert data["exit_code"] == 0
236
237 def test_idempotent_init_no_switch(self, tmp_path):
238 repo = _init_repo(tmp_path)
239 _invoke(["sparse-checkout", "init"], repo)
240 rc, out, _ = _invoke(["sparse-checkout", "init", "--json"], repo)
241 assert rc == 0
242 data = json.loads(out)
243 assert data["switched"] is False
244 assert data["mode"] == "cone"
245
246 def test_mode_switch_cone_to_pattern(self, tmp_path):
247 """init --no-cone on existing cone repo must switch mode and report it."""
248 repo = _init_repo(tmp_path)
249 _invoke(["sparse-checkout", "init"], repo)
250 rc, out, _ = _invoke(["sparse-checkout", "init", "--no-cone", "--json"], repo)
251 assert rc == 0
252 data = json.loads(out)
253 assert data["switched"] is True
254 assert data["mode"] == "pattern"
255 assert data["previous_mode"] == "cone"
256
257 def test_mode_switch_pattern_to_cone(self, tmp_path):
258 """init (cone default) on existing pattern repo must switch and report."""
259 repo = _init_repo(tmp_path)
260 _invoke(["sparse-checkout", "init", "--no-cone"], repo)
261 rc, out, _ = _invoke(["sparse-checkout", "init", "--json"], repo)
262 assert rc == 0
263 data = json.loads(out)
264 assert data["switched"] is True
265 assert data["mode"] == "cone"
266 assert data["previous_mode"] == "pattern"
267
268 def test_mode_switch_preserves_patterns(self, tmp_path):
269 """Mode switch must keep existing patterns."""
270 repo = _init_repo(tmp_path)
271 _invoke(["sparse-checkout", "init"], repo)
272 _invoke(["sparse-checkout", "set", "src/", "tests/"], repo)
273 _invoke(["sparse-checkout", "init", "--no-cone"], repo)
274 cfg = json.loads(_sparse_config(repo).read_text())
275 assert "src/" in cfg["patterns"]
276 assert "tests/" in cfg["patterns"]
277
278
279 # ---------------------------------------------------------------------------
280 # TestSetJsonSchema
281 # ---------------------------------------------------------------------------
282
283
284 class TestSetJsonSchema:
285 """set --json must emit patterns, total, duration_ms, exit_code."""
286
287 def test_set_json_patterns_array(self, tmp_path):
288 repo = _init_repo(tmp_path)
289 _invoke(["sparse-checkout", "init"], repo)
290 rc, out, _ = _invoke(["sparse-checkout", "set", "src/", "tests/", "--json"], repo)
291 assert rc == 0
292 data = json.loads(out)
293 assert data["patterns"] == ["src/", "tests/"]
294
295 def test_set_json_total_count(self, tmp_path):
296 repo = _init_repo(tmp_path)
297 _invoke(["sparse-checkout", "init"], repo)
298 rc, out, _ = _invoke(["sparse-checkout", "set", "src/", "tests/", "--json"], repo)
299 assert rc == 0
300 data = json.loads(out)
301 assert data["total"] == 2
302
303 def test_set_json_replaces_previous(self, tmp_path):
304 repo = _init_repo(tmp_path)
305 _invoke(["sparse-checkout", "init"], repo)
306 _invoke(["sparse-checkout", "set", "old/", "--json"], repo)
307 rc, out, _ = _invoke(["sparse-checkout", "set", "new/", "--json"], repo)
308 assert rc == 0
309 data = json.loads(out)
310 assert data["patterns"] == ["new/"]
311 assert data["total"] == 1
312
313 def test_set_without_init_fails(self, tmp_path):
314 repo = _init_repo(tmp_path)
315 rc, _, _ = _invoke(["sparse-checkout", "set", "src/", "--json"], repo)
316 assert rc != 0
317
318
319 # ---------------------------------------------------------------------------
320 # TestAddJsonSchema
321 # ---------------------------------------------------------------------------
322
323
324 class TestAddJsonSchema:
325 """add --json must emit added, skipped, patterns, total, duration_ms, exit_code."""
326
327 def test_add_json_added_count(self, tmp_path):
328 repo = _init_repo(tmp_path)
329 _invoke(["sparse-checkout", "init"], repo)
330 rc, out, _ = _invoke(["sparse-checkout", "add", "src/", "tests/", "--json"], repo)
331 assert rc == 0
332 data = json.loads(out)
333 assert data["added"] == 2
334 assert data["skipped"] == 0
335
336 def test_add_json_skipped_count(self, tmp_path):
337 repo = _init_repo(tmp_path)
338 _invoke(["sparse-checkout", "init"], repo)
339 _invoke(["sparse-checkout", "add", "src/", "--json"], repo)
340 rc, out, _ = _invoke(["sparse-checkout", "add", "src/", "docs/", "--json"], repo)
341 assert rc == 0
342 data = json.loads(out)
343 assert data["added"] == 1
344 assert data["skipped"] == 1
345
346 def test_add_json_patterns_array(self, tmp_path):
347 repo = _init_repo(tmp_path)
348 _invoke(["sparse-checkout", "init"], repo)
349 _invoke(["sparse-checkout", "add", "src/", "--json"], repo)
350 rc, out, _ = _invoke(["sparse-checkout", "add", "tests/", "--json"], repo)
351 assert rc == 0
352 data = json.loads(out)
353 assert "src/" in data["patterns"]
354 assert "tests/" in data["patterns"]
355
356 def test_add_json_total_is_cumulative(self, tmp_path):
357 repo = _init_repo(tmp_path)
358 _invoke(["sparse-checkout", "init"], repo)
359 _invoke(["sparse-checkout", "add", "src/", "tests/", "--json"], repo)
360 rc, out, _ = _invoke(["sparse-checkout", "add", "docs/", "--json"], repo)
361 assert rc == 0
362 data = json.loads(out)
363 assert data["total"] == 3
364
365
366 # ---------------------------------------------------------------------------
367 # TestDisableJsonSchema
368 # ---------------------------------------------------------------------------
369
370
371 class TestDisableJsonSchema:
372 """disable --json must emit was_enabled, duration_ms, exit_code."""
373
374 def test_disable_json_was_enabled_true(self, tmp_path):
375 repo = _init_repo(tmp_path)
376 _invoke(["sparse-checkout", "init"], repo)
377 rc, out, _ = _invoke(["sparse-checkout", "disable", "--json"], repo)
378 assert rc == 0
379 data = json.loads(out)
380 assert data["was_enabled"] is True
381 assert data["exit_code"] == 0
382
383 def test_disable_json_was_enabled_false_when_already_disabled(self, tmp_path):
384 repo = _init_repo(tmp_path)
385 rc, out, _ = _invoke(["sparse-checkout", "disable", "--json"], repo)
386 assert rc == 0
387 data = json.loads(out)
388 assert data["was_enabled"] is False
389 assert data["exit_code"] == 0
390
391 def test_disable_removes_config(self, tmp_path):
392 repo = _init_repo(tmp_path)
393 _invoke(["sparse-checkout", "init"], repo)
394 _invoke(["sparse-checkout", "disable", "--json"], repo)
395 assert not _sparse_config(repo).exists()
396
397
398 # ---------------------------------------------------------------------------
399 # TestStatsSubcommand
400 # ---------------------------------------------------------------------------
401
402
403 class TestStatsSubcommand:
404 """stats subcommand reports matching/excluded file counts from HEAD snapshot."""
405
406 def test_stats_with_cone_filter(self, tmp_path):
407 repo, _ = _make_repo_with_snapshot(tmp_path)
408 _invoke(["sparse-checkout", "init"], repo)
409 _invoke(["sparse-checkout", "set", "src/"], repo)
410 rc, out, _ = _invoke(["sparse-checkout", "stats", "--json"], repo)
411 assert rc == 0, out
412 data = json.loads(out)
413 # 5 src/ files + 1 README.md (root file in cone) = 6 matching
414 assert data["matching_files"] == 6
415 assert data["excluded_files"] == 5 # tests/ excluded
416 assert data["total_files"] == 11
417
418 def test_stats_total_files(self, tmp_path):
419 repo, _ = _make_repo_with_snapshot(tmp_path)
420 _invoke(["sparse-checkout", "init"], repo)
421 _invoke(["sparse-checkout", "set", "src/"], repo)
422 rc, out, _ = _invoke(["sparse-checkout", "stats", "--json"], repo)
423 assert rc == 0
424 data = json.loads(out)
425 assert data["total_files"] == data["matching_files"] + data["excluded_files"]
426
427 def test_stats_efficiency_ratio(self, tmp_path):
428 repo, _ = _make_repo_with_snapshot(tmp_path)
429 _invoke(["sparse-checkout", "init"], repo)
430 _invoke(["sparse-checkout", "set", "src/"], repo)
431 rc, out, _ = _invoke(["sparse-checkout", "stats", "--json"], repo)
432 assert rc == 0
433 data = json.loads(out)
434 expected = data["matching_files"] / data["total_files"]
435 assert abs(data["efficiency"] - expected) < 0.001
436
437 def test_stats_disabled_means_all_match(self, tmp_path):
438 """When sparse-checkout is disabled, all files match."""
439 repo, _ = _make_repo_with_snapshot(tmp_path)
440 rc, out, _ = _invoke(["sparse-checkout", "stats", "--json"], repo)
441 assert rc == 0
442 data = json.loads(out)
443 assert data["enabled"] is False
444 assert data["matching_files"] == data["total_files"]
445 assert data["excluded_files"] == 0
446 assert data["efficiency"] == 1.0
447
448 def test_stats_has_duration_ms(self, tmp_path):
449 repo, _ = _make_repo_with_snapshot(tmp_path)
450 rc, out, _ = _invoke(["sparse-checkout", "stats", "--json"], repo)
451 assert rc == 0
452 data = json.loads(out)
453 assert "duration_ms" in data
454 assert isinstance(data["duration_ms"], (int, float))
455
456 def test_stats_has_exit_code(self, tmp_path):
457 repo, _ = _make_repo_with_snapshot(tmp_path)
458 rc, out, _ = _invoke(["sparse-checkout", "stats", "--json"], repo)
459 assert rc == 0
460 data = json.loads(out)
461 assert data["exit_code"] == 0
462
463 def test_stats_no_commits_exits_cleanly(self, tmp_path):
464 """Stats on a repo with no commits must exit 0 with zeros."""
465 repo = _init_repo(tmp_path)
466 _invoke(["sparse-checkout", "init"], repo)
467 rc, out, _ = _invoke(["sparse-checkout", "stats", "--json"], repo)
468 assert rc == 0
469 data = json.loads(out)
470 assert data["total_files"] == 0
471 assert data["matching_files"] == 0
472
473 def test_stats_pattern_mode(self, tmp_path):
474 repo, _ = _make_repo_with_snapshot(tmp_path)
475 _invoke(["sparse-checkout", "init", "--no-cone"], repo)
476 _invoke(["sparse-checkout", "set", "src/**"], repo)
477 rc, out, _ = _invoke(["sparse-checkout", "stats", "--json"], repo)
478 assert rc == 0
479 data = json.loads(out)
480 # pattern mode: only src/** matches; README.md excluded
481 assert data["matching_files"] == 5
482 assert data["excluded_files"] == 6
483
484
485 # ---------------------------------------------------------------------------
486 # TestSecurityValidation
487 # ---------------------------------------------------------------------------
488
489
490 class TestSecurityValidation:
491 """Dangerous patterns must be rejected before being written to disk."""
492
493 def test_path_traversal_rejected_set(self, tmp_path):
494 repo = _init_repo(tmp_path)
495 _invoke(["sparse-checkout", "init"], repo)
496 rc, _, err = _invoke(["sparse-checkout", "set", "../etc/passwd"], repo)
497 assert rc != 0, "path traversal pattern must be rejected"
498
499 def test_path_traversal_rejected_add(self, tmp_path):
500 repo = _init_repo(tmp_path)
501 _invoke(["sparse-checkout", "init"], repo)
502 rc, _, err = _invoke(["sparse-checkout", "add", "../secret"], repo)
503 assert rc != 0, "path traversal in add must be rejected"
504
505 def test_path_traversal_nested_rejected(self, tmp_path):
506 repo = _init_repo(tmp_path)
507 _invoke(["sparse-checkout", "init"], repo)
508 rc, _, _ = _invoke(["sparse-checkout", "set", "src/../../etc"], repo)
509 assert rc != 0, "nested path traversal must be rejected"
510
511 def test_null_byte_pattern_rejected_set(self, tmp_path):
512 repo = _init_repo(tmp_path)
513 _invoke(["sparse-checkout", "init"], repo)
514 rc, _, _ = _invoke(["sparse-checkout", "set", "src/\x00evil"], repo)
515 assert rc != 0, "null byte in pattern must be rejected"
516
517 def test_null_byte_pattern_rejected_add(self, tmp_path):
518 repo = _init_repo(tmp_path)
519 _invoke(["sparse-checkout", "init"], repo)
520 rc, _, _ = _invoke(["sparse-checkout", "add", "foo\x00bar"], repo)
521 assert rc != 0, "null byte in add pattern must be rejected"
522
523 def test_ansi_still_rejected(self, tmp_path):
524 repo = _init_repo(tmp_path)
525 _invoke(["sparse-checkout", "init"], repo)
526 rc, _, _ = _invoke(["sparse-checkout", "set", "src/\x1b[31mevil"], repo)
527 assert rc != 0, "ANSI escape in pattern must still be rejected"
528
529 def test_safe_pattern_not_rejected(self, tmp_path):
530 repo = _init_repo(tmp_path)
531 _invoke(["sparse-checkout", "init"], repo)
532 rc, _, _ = _invoke(["sparse-checkout", "set", "src/"], repo)
533 assert rc == 0, "normal safe pattern must not be rejected"
534
535 def test_error_message_mentions_traversal(self, tmp_path):
536 repo = _init_repo(tmp_path)
537 _invoke(["sparse-checkout", "init"], repo)
538 rc, out, err = _invoke(["sparse-checkout", "set", "../bad"], repo)
539 assert rc != 0
540 combined = (out + err).lower()
541 assert "traversal" in combined or ".." in combined, (
542 "error message must describe the path traversal issue"
543 )
544
545
546 # ---------------------------------------------------------------------------
547 # TestConfigCorruption
548 # ---------------------------------------------------------------------------
549
550
551 class TestConfigCorruption:
552 """Malformed sparse-checkout config must produce a clear error, not a traceback."""
553
554 def test_malformed_json_exits_nonzero(self, tmp_path):
555 repo = _init_repo(tmp_path)
556 _sparse_config(repo).write_text("{invalid json", encoding="utf-8")
557 rc, _, _ = _invoke(["sparse-checkout", "list", "--json"], repo)
558 assert rc != 0, "malformed config must exit non-zero"
559
560 def test_malformed_json_error_message(self, tmp_path):
561 repo = _init_repo(tmp_path)
562 _sparse_config(repo).write_text("{invalid json", encoding="utf-8")
563 rc, out, err = _invoke(["sparse-checkout", "list", "--json"], repo)
564 assert rc != 0
565 combined = out + err
566 assert len(combined.strip()) > 0, "must emit an error message"
567
568 def test_missing_mode_key(self, tmp_path):
569 repo = _init_repo(tmp_path)
570 _sparse_config(repo).write_text(json.dumps({"patterns": []}), encoding="utf-8")
571 rc, _, _ = _invoke(["sparse-checkout", "list"], repo)
572 assert rc != 0, "config missing 'mode' key must exit non-zero"
573
574 def test_missing_patterns_key(self, tmp_path):
575 repo = _init_repo(tmp_path)
576 _sparse_config(repo).write_text(json.dumps({"mode": "cone"}), encoding="utf-8")
577 rc, _, _ = _invoke(["sparse-checkout", "list"], repo)
578 assert rc != 0, "config missing 'patterns' key must exit non-zero"
579
580
581 # ---------------------------------------------------------------------------
582 # TestModeSwitch
583 # ---------------------------------------------------------------------------
584
585
586 class TestModeSwitch:
587 """Mode switching via re-init must work cleanly."""
588
589 def test_switch_updates_config_file(self, tmp_path):
590 repo = _init_repo(tmp_path)
591 _invoke(["sparse-checkout", "init"], repo)
592 _invoke(["sparse-checkout", "init", "--no-cone"], repo)
593 cfg = json.loads(_sparse_config(repo).read_text())
594 assert cfg["mode"] == "pattern"
595
596 def test_same_mode_reinit_is_not_switch(self, tmp_path):
597 repo = _init_repo(tmp_path)
598 _invoke(["sparse-checkout", "init"], repo)
599 rc, out, _ = _invoke(["sparse-checkout", "init", "--json"], repo)
600 assert rc == 0
601 data = json.loads(out)
602 assert data["switched"] is False
603
604 def test_switch_preserves_patterns_count(self, tmp_path):
605 repo = _init_repo(tmp_path)
606 _invoke(["sparse-checkout", "init"], repo)
607 _invoke(["sparse-checkout", "set", "a/", "b/", "c/"], repo)
608 _invoke(["sparse-checkout", "init", "--no-cone"], repo)
609 cfg = json.loads(_sparse_config(repo).read_text())
610 assert len(cfg["patterns"]) == 3
611
612
613 # ---------------------------------------------------------------------------
614 # TestPatternEdgeCases
615 # ---------------------------------------------------------------------------
616
617
618 class TestPatternEdgeCases:
619 """Edge case patterns that might slip through validation."""
620
621 def test_whitespace_only_pattern_rejected(self, tmp_path):
622 repo = _init_repo(tmp_path)
623 _invoke(["sparse-checkout", "init"], repo)
624 rc, _, _ = _invoke(["sparse-checkout", "set", " "], repo)
625 assert rc != 0, "whitespace-only pattern must be rejected"
626
627 def test_empty_string_pattern_rejected(self, tmp_path):
628 repo = _init_repo(tmp_path)
629 _invoke(["sparse-checkout", "init"], repo)
630 # argparse nargs="+" won't pass empty string, so simulate via direct config write
631 _sparse_config(repo).write_text(
632 json.dumps({"mode": "cone", "patterns": [""]}), encoding="utf-8"
633 )
634 rc, out, _ = _invoke(["sparse-checkout", "list", "--json"], repo)
635 # Reading an empty pattern is fine; it won't match anything useful
636 assert rc == 0 # list itself doesn't re-validate stored patterns
637
638 def test_very_long_pattern_accepted(self, tmp_path):
639 """Patterns up to 1024 chars must be accepted (no arbitrary length limit)."""
640 repo = _init_repo(tmp_path)
641 _invoke(["sparse-checkout", "init"], repo)
642 long_pat = "a/" * 200 # 400 chars
643 rc, _, _ = _invoke(["sparse-checkout", "set", long_pat], repo)
644 assert rc == 0, "long but valid pattern must be accepted"
645
646 def test_unicode_pattern_accepted(self, tmp_path):
647 """Unicode patterns are valid (e.g. internationalised path names)."""
648 repo = _init_repo(tmp_path)
649 _invoke(["sparse-checkout", "init"], repo)
650 rc, _, _ = _invoke(["sparse-checkout", "set", "música/", "--json"], repo)
651 assert rc == 0, "unicode path pattern must be accepted"
652
653
654 # ---------------------------------------------------------------------------
655 # TestStressLargeManifest
656 # ---------------------------------------------------------------------------
657
658
659 class TestStressLargeManifest:
660 """stats must handle large manifests without degrading."""
661
662 def test_stats_1000_file_manifest(self, tmp_path):
663 repo = _init_repo(tmp_path)
664 manifest: Manifest = {}
665 for i in range(500):
666 oid = _obj(repo, f"src {i}".encode())
667 manifest[f"src/file_{i}.py"] = oid
668 for i in range(500):
669 oid = _obj(repo, f"other {i}".encode())
670 manifest[f"other/file_{i}.py"] = oid
671 sid = _snap(repo, manifest)
672 _commit(repo, sid)
673 _invoke(["sparse-checkout", "init"], repo)
674 _invoke(["sparse-checkout", "set", "src/"], repo)
675 rc, out, _ = _invoke(["sparse-checkout", "stats", "--json"], repo)
676 assert rc == 0
677 data = json.loads(out)
678 assert data["total_files"] == 1000
679 assert data["matching_files"] == 500
680 assert data["excluded_files"] == 500
681
682 def test_stats_500_patterns(self, tmp_path):
683 """500-pattern list must not degrade stats computation."""
684 repo = _init_repo(tmp_path)
685 manifest: Manifest = {}
686 for i in range(10):
687 oid = _obj(repo, f"content {i}".encode())
688 manifest[f"dir_{i}/file.py"] = oid
689 sid = _snap(repo, manifest)
690 _commit(repo, sid)
691 _invoke(["sparse-checkout", "init", "--no-cone"], repo)
692 patterns = [f"dir_{i}/**" for i in range(500)]
693 _invoke(["sparse-checkout", "set"] + patterns, repo)
694 rc, out, _ = _invoke(["sparse-checkout", "stats", "--json"], repo)
695 assert rc == 0
696 data = json.loads(out)
697 assert data["matching_files"] == 10
File History 1 commit
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 138 days ago