gabriel / muse public
test_cmd_code_reset.py python
721 lines 26.0 KB
Raw
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 20 days ago
1 """Comprehensive tests for ``muse code reset``.
2
3 Review findings addressed
4 --------------------------
5 Performance bug fixed
6 * write_stage was called unconditionally even when no files were actually
7 unstaged (all-"not staged" case). Now skipped when to_unstage is empty.
8
9 Security
10 * Paths outside the repo root now produce an explicit warning and are
11 reported in the not_staged list instead of silently falling back to
12 treating the raw string as a stage key.
13
14 New capabilities (added this review)
15 * ``-n`` / ``--dry-run`` — preview what would be unstaged, no write.
16 * Glob pattern support — ``muse code reset '*.py'`` unstages all staged
17 Python files matching the pattern.
18 * ``not_staged`` key in JSON output — agents can see which files were
19 requested but not found in the stage (enables partial-failure detection).
20
21 Test categories
22 ---------------
23 I Core behaviour — clear all, specific file, HEAD syntax, nothing staged.
24 II JSON output — all keys present, not_staged populated, dry_run flag.
25 III Dry-run — no writes, correct preview output, JSON dry_run=true.
26 IV Glob patterns — *.ext, ?, partial match, no match.
27 V Security — out-of-root path, path traversal, sanitize_display.
28 VI Performance — no write when nothing unstaged.
29 VII Edge cases — duplicate paths, mixed staged/not-staged, fresh repo.
30 VIII Stress — 500 files, 100 reset-cycles, multi-pattern glob.
31 """
32
33 from __future__ import annotations
34
35 import json
36 import pathlib
37
38 import pytest
39
40 from muse.plugins.code.stage import StagedEntry, StagedFileMap, read_stage, stage_path, write_stage, make_entry
41 from muse.core.types import Manifest
42 from tests.cli_test_helper import CliRunner
43
44 runner = CliRunner()
45 cli = None
46
47
48 # ---------------------------------------------------------------------------
49 # Helpers and fixtures
50 # ---------------------------------------------------------------------------
51
52
53 def _env(root: pathlib.Path) -> Manifest:
54 return {"MUSE_REPO_ROOT": str(root)}
55
56
57 def _run(root: pathlib.Path, *args: str) -> tuple[int, str]:
58 result = runner.invoke(cli, list(args), env=_env(root), catch_exceptions=False)
59 return result.exit_code, result.output
60
61
62 def _run_unchecked(root: pathlib.Path, *args: str) -> tuple[int, str]:
63 result = runner.invoke(cli, list(args), env=_env(root))
64 return result.exit_code, result.output
65
66
67 @pytest.fixture()
68 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
69 """Fresh code-domain repo with main.py + models.py committed."""
70 monkeypatch.chdir(tmp_path)
71 r = runner.invoke(cli, ["init", "--domain", "code"], env=_env(tmp_path))
72 assert r.exit_code == 0, r.output
73 (tmp_path / "main.py").write_text("x = 1\n")
74 (tmp_path / "models.py").write_text("class User: pass\n")
75 r2 = runner.invoke(cli, ["commit", "--allow-empty", "-m", "init"], env=_env(tmp_path))
76 assert r2.exit_code == 0, r2.output
77 return tmp_path
78
79
80 def _stage(repo: pathlib.Path, *filenames: str) -> None:
81 """Write modified content and stage the given files."""
82 for fn in filenames:
83 path = repo / fn
84 if not path.exists():
85 path.write_text(f"# new: {fn}\n")
86 else:
87 path.write_text(path.read_text() + "# modified\n")
88 _run(repo, "code", "add", "-A")
89
90
91 # ===========================================================================
92 # I Core behaviour
93 # ===========================================================================
94
95
96 class TestCoreBehaviourI:
97 """Baseline correctness: clear-all, specific-file, HEAD syntax."""
98
99 def test_I1_reset_all_clears_entire_stage(self, repo: pathlib.Path) -> None:
100 """I1: reset with no args clears every staged file."""
101 _stage(repo, "main.py", "models.py")
102 assert stage_path(repo).exists()
103
104 code, out = _run(repo, "code", "reset")
105 assert code == 0, out
106 assert not stage_path(repo).exists()
107 assert read_stage(repo) == {}
108
109 def test_I2_reset_specific_file_leaves_others(
110 self, repo: pathlib.Path
111 ) -> None:
112 """I2: resetting one file leaves other staged files intact."""
113 _stage(repo, "main.py", "models.py")
114
115 code, out = _run(repo, "code", "reset", "main.py")
116 assert code == 0, out
117 stage = read_stage(repo)
118 assert "main.py" not in stage
119 assert "models.py" in stage
120
121 def test_I3_reset_HEAD_syntax_works(self, repo: pathlib.Path) -> None:
122 """I3: 'muse code reset HEAD <file>' is an alias for 'muse code reset <file>'."""
123 _stage(repo, "main.py")
124
125 code, out = _run(repo, "code", "reset", "HEAD", "main.py")
126 assert code == 0, out
127 assert not stage_path(repo).exists()
128
129 def test_I4_reset_when_nothing_staged(self, repo: pathlib.Path) -> None:
130 """I4: reset with no stage prints 'Nothing staged' and exits 0."""
131 code, out = _run(repo, "code", "reset")
132 assert code == 0, out
133 assert "Nothing staged" in out
134
135 def test_I5_reset_not_staged_file_exits_zero(
136 self, repo: pathlib.Path
137 ) -> None:
138 """I5: resetting a file that is not staged exits 0 and says 'not staged'."""
139 _stage(repo, "main.py")
140
141 code, out = _run(repo, "code", "reset", "models.py")
142 assert code == 0, out
143 assert "not staged" in out
144 # main.py must still be staged.
145 assert "main.py" in read_stage(repo)
146
147 def test_I6_reset_multiple_files_in_one_invocation(
148 self, repo: pathlib.Path
149 ) -> None:
150 """I6: multiple files can be unstaged in a single command."""
151 _stage(repo, "main.py", "models.py")
152
153 code, out = _run(repo, "code", "reset", "main.py", "models.py")
154 assert code == 0, out
155 assert read_stage(repo) == {}
156
157 def test_I7_reset_preserves_working_tree(self, repo: pathlib.Path) -> None:
158 """I7: unstaging does NOT revert the working-tree file."""
159 (repo / "main.py").write_text("changed content\n")
160 # Stage directly without using _stage helper (which modifies content again).
161 _run(repo, "code", "add", "main.py")
162 _run(repo, "code", "reset", "main.py")
163
164 assert (repo / "main.py").read_text() == "changed content\n"
165
166 def test_I8_text_output_shows_unstaged_per_file(
167 self, repo: pathlib.Path
168 ) -> None:
169 """I8: text output lists each unstaged file."""
170 _stage(repo, "main.py", "models.py")
171 _, out = _run(repo, "code", "reset", "main.py")
172 assert "main.py" in out
173
174 def test_I9_reset_HEAD_alone_clears_all(self, repo: pathlib.Path) -> None:
175 """I9: 'muse code reset HEAD' (no file) clears everything — HEAD is stripped."""
176 _stage(repo, "main.py", "models.py")
177 code, out = _run(repo, "code", "reset", "HEAD")
178 assert code == 0, out
179 assert read_stage(repo) == {}
180
181
182 # ===========================================================================
183 # II JSON output
184 # ===========================================================================
185
186
187 class TestJsonOutputII:
188 """``--format json`` must emit complete, accurate JSON."""
189
190 def test_II1_json_unstaged_specific_file(
191 self, repo: pathlib.Path
192 ) -> None:
193 """II1: JSON output contains unstaged count, files list, and not_staged."""
194 _stage(repo, "main.py")
195
196 _, out = _run(repo, "code", "reset", "--json", "main.py")
197 data = json.loads(out.strip())
198 assert data["unstaged"] == 1
199 assert "main.py" in data["files"]
200 assert data["not_staged"] == []
201 assert data["dry_run"] is False
202
203 def test_II2_json_not_staged_populated_for_missing_file(
204 self, repo: pathlib.Path
205 ) -> None:
206 """II2: not_staged is populated when a requested file is not in the stage."""
207 _stage(repo, "main.py")
208
209 _, out = _run(
210 repo, "code", "reset", "--json", "main.py", "ghost.py"
211 )
212 data = json.loads(out.strip())
213 assert data["unstaged"] == 1
214 assert "main.py" in data["files"]
215 assert "ghost.py" in data["not_staged"]
216
217 def test_II3_json_clear_all_has_all_keys(
218 self, repo: pathlib.Path
219 ) -> None:
220 """II3: clear-all (no args) JSON includes all required keys."""
221 _stage(repo, "main.py", "models.py")
222
223 _, out = _run(repo, "code", "reset", "--json")
224 data = json.loads(out.strip())
225 assert "unstaged" in data
226 assert "files" in data
227 assert "not_staged" in data
228 assert "dry_run" in data
229 assert data["unstaged"] == 2
230 assert data["not_staged"] == []
231
232 def test_II4_json_nothing_staged(self, repo: pathlib.Path) -> None:
233 """II4: nothing staged returns unstaged=0, files=[], not_staged=[]."""
234 _, out = _run(repo, "code", "reset", "--json")
235 data = json.loads(out.strip())
236 assert data["unstaged"] == 0
237 assert data["files"] == []
238 assert data["not_staged"] == []
239 assert data["dry_run"] is False
240
241 def test_II5_json_output_is_valid_json(self, repo: pathlib.Path) -> None:
242 """II5: output is always parseable JSON regardless of outcome."""
243 _stage(repo, "main.py")
244 _, out = _run(repo, "code", "reset", "--json", "main.py")
245 json.loads(out.strip()) # must not raise
246
247 def test_II6_json_all_not_staged_when_none_found(
248 self, repo: pathlib.Path
249 ) -> None:
250 """II6: when all requested files are not staged, files=[], all in not_staged."""
251 _stage(repo, "main.py")
252 _, out = _run(
253 repo, "code", "reset", "--json", "ghost1.py", "ghost2.py"
254 )
255 data = json.loads(out.strip())
256 assert data["unstaged"] == 0
257 assert data["files"] == []
258 assert "ghost1.py" in data["not_staged"]
259 assert "ghost2.py" in data["not_staged"]
260
261 def test_II7_json_glob_results_included(self, repo: pathlib.Path) -> None:
262 """II7: glob results appear in JSON files list."""
263 _stage(repo, "main.py", "models.py")
264
265 _, out = _run(repo, "code", "reset", "--json", "*.py")
266 data = json.loads(out.strip())
267 assert data["unstaged"] >= 2
268 assert "main.py" in data["files"]
269 assert "models.py" in data["files"]
270
271
272 # ===========================================================================
273 # III Dry-run
274 # ===========================================================================
275
276
277 class TestDryRunIII:
278 """``-n`` / ``--dry-run`` must preview without writing."""
279
280 def test_III1_dry_run_does_not_remove_from_stage(
281 self, repo: pathlib.Path
282 ) -> None:
283 """III1: after dry-run, stage is unchanged."""
284 _stage(repo, "main.py")
285 before = dict(read_stage(repo))
286
287 _run(repo, "code", "reset", "-n", "main.py")
288
289 assert read_stage(repo) == before
290
291 def test_III2_dry_run_lists_would_unstage(
292 self, repo: pathlib.Path
293 ) -> None:
294 """III2: dry-run text output mentions files that would be unstaged."""
295 _stage(repo, "main.py", "models.py")
296 _, out = _run(repo, "code", "reset", "--dry-run", "main.py")
297 assert "main.py" in out
298 assert "would" in out.lower()
299
300 def test_III3_dry_run_json_flag_is_true(
301 self, repo: pathlib.Path
302 ) -> None:
303 """III3: dry_run field is true in JSON output when -n given."""
304 _stage(repo, "main.py")
305 _, out = _run(
306 repo, "code", "reset", "-n", "--json", "main.py"
307 )
308 data = json.loads(out.strip())
309 assert data["dry_run"] is True
310 assert data["unstaged"] == 1
311 assert "main.py" in data["files"]
312
313 def test_III4_dry_run_clear_all_is_preview_only(
314 self, repo: pathlib.Path
315 ) -> None:
316 """III4: dry-run on clear-all leaves stage intact."""
317 _stage(repo, "main.py", "models.py")
318 _run(repo, "code", "reset", "--dry-run")
319 assert stage_path(repo).exists()
320 assert "main.py" in read_stage(repo)
321
322 def test_III5_dry_run_nothing_staged(self, repo: pathlib.Path) -> None:
323 """III5: dry-run when nothing staged exits 0 and reports 0."""
324 code, out = _run(repo, "code", "reset", "-n")
325 assert code == 0, out
326 assert "Nothing staged" in out
327
328 def test_III6_dry_run_with_json_nothing_staged(
329 self, repo: pathlib.Path
330 ) -> None:
331 """III6: dry-run + JSON + nothing staged emits valid JSON with zeros."""
332 _, out = _run(repo, "code", "reset", "-n", "--json")
333 data = json.loads(out.strip())
334 assert data["unstaged"] == 0
335 assert data["dry_run"] is True
336
337
338 # ===========================================================================
339 # IV Glob patterns
340 # ===========================================================================
341
342
343 class TestGlobPatternsIV:
344 """Glob patterns must match staged paths using fnmatch semantics."""
345
346 def test_IV1_star_ext_unstages_matching_files(
347 self, repo: pathlib.Path
348 ) -> None:
349 """IV1: '*.py' unstages all staged Python files."""
350 _stage(repo, "main.py", "models.py")
351
352 code, out = _run(repo, "code", "reset", "*.py")
353 assert code == 0, out
354 assert read_stage(repo) == {}
355
356 def test_IV2_star_ext_leaves_non_matching_staged(
357 self, repo: pathlib.Path
358 ) -> None:
359 """IV2: '*.py' does not unstage non-Python staged files."""
360 _stage(repo, "main.py")
361 # Manually add a .txt file to the stage.
362 stage = dict(read_stage(repo))
363 stage["readme.txt"] = make_entry("a" * 64, "A")
364 write_stage(repo, stage)
365
366 _run(repo, "code", "reset", "*.py")
367 remaining = read_stage(repo)
368 assert "main.py" not in remaining
369 assert "readme.txt" in remaining
370
371 def test_IV3_glob_with_directory_prefix(
372 self, repo: pathlib.Path
373 ) -> None:
374 """IV3: 'src/*.py' matches only files inside src/."""
375 # Manually build a stage with nested paths.
376 stage: StagedFileMap = {
377 "src/auth.py": make_entry("a" * 64, "A"),
378 "src/models.py": make_entry("b" * 64, "A"),
379 "top.py": make_entry("c" * 64, "M"),
380 }
381 write_stage(repo, stage)
382
383 _run(repo, "code", "reset", "src/*.py")
384 remaining = read_stage(repo)
385 assert "src/auth.py" not in remaining
386 assert "src/models.py" not in remaining
387 assert "top.py" in remaining
388
389 def test_IV4_glob_no_match_goes_to_not_staged(
390 self, repo: pathlib.Path
391 ) -> None:
392 """IV4: glob with no matches appears in not_staged in JSON output."""
393 _stage(repo, "main.py")
394 _, out = _run(
395 repo, "code", "reset", "--json", "*.toml"
396 )
397 data = json.loads(out.strip())
398 assert data["unstaged"] == 0
399 assert "*.toml" in data["not_staged"]
400
401 def test_IV5_question_mark_glob(self, repo: pathlib.Path) -> None:
402 """IV5: '?.py' matches single-char filenames."""
403 stage: StagedFileMap = {
404 "a.py": make_entry("a" * 64, "A"),
405 "ab.py": make_entry("b" * 64, "A"),
406 }
407 write_stage(repo, stage)
408
409 _run(repo, "code", "reset", "?.py")
410 remaining = read_stage(repo)
411 assert "a.py" not in remaining
412 assert "ab.py" in remaining
413
414 def test_IV6_multiple_globs_in_one_invocation(
415 self, repo: pathlib.Path
416 ) -> None:
417 """IV6: multiple glob patterns can be combined in one reset call."""
418 stage: StagedFileMap = {
419 "main.py": make_entry("a" * 64, "M"),
420 "config.toml": make_entry("b" * 64, "M"),
421 "readme.md": make_entry("c" * 64, "M"),
422 }
423 write_stage(repo, stage)
424
425 _run(repo, "code", "reset", "*.py", "*.toml")
426 remaining = read_stage(repo)
427 assert "main.py" not in remaining
428 assert "config.toml" not in remaining
429 assert "readme.md" in remaining
430
431 def test_IV7_literal_path_and_glob_combined(
432 self, repo: pathlib.Path
433 ) -> None:
434 """IV7: literal path and glob can be combined in one invocation."""
435 _stage(repo, "main.py", "models.py")
436
437 _, out = _run(
438 repo, "code", "reset", "--json", "main.py", "*.py"
439 )
440 data = json.loads(out.strip())
441 # Both main.py and models.py should be unstaged.
442 assert "main.py" in data["files"]
443 assert "models.py" in data["files"]
444
445
446 # ===========================================================================
447 # V Security
448 # ===========================================================================
449
450
451 class TestSecurityV:
452 """Out-of-root paths must be rejected with a warning, not silently used."""
453
454 def test_V1_path_outside_repo_goes_to_not_staged(
455 self, repo: pathlib.Path
456 ) -> None:
457 """V1: an absolute path outside the repo appears in not_staged, not files."""
458 _stage(repo, "main.py")
459 outside = str(repo.parent / "outside.py")
460
461 _, out = _run_unchecked(
462 repo, "code", "reset", "--json", outside
463 )
464 data = json.loads(out.strip())
465 assert data["unstaged"] == 0
466 assert data["files"] == []
467 assert any("outside" in p for p in data["not_staged"])
468
469 def test_V2_dotdot_traversal_rejected(self, repo: pathlib.Path) -> None:
470 """V2: '../secret.py' is treated as outside-root and lands in not_staged."""
471 _stage(repo, "main.py")
472
473 _, out = _run(repo, "code", "reset", "--json", "../secret.py")
474 data = json.loads(out.strip())
475 assert data["unstaged"] == 0
476 # main.py must remain staged — traversal did not corrupt state.
477 assert "main.py" in read_stage(repo)
478
479 def test_V3_out_of_root_does_not_corrupt_stage(
480 self, repo: pathlib.Path
481 ) -> None:
482 """V3: out-of-root path does not modify any staged entry."""
483 _stage(repo, "main.py", "models.py")
484 before = dict(read_stage(repo))
485
486 _run_unchecked(repo, "code", "reset", str(repo.parent / "malicious.py"))
487
488 assert read_stage(repo) == before
489
490
491 # ===========================================================================
492 # VI Performance — no write when nothing unstaged
493 # ===========================================================================
494
495
496 class TestPerformanceVI:
497 """write_stage must NOT be called when nothing was actually unstaged."""
498
499 def test_VI1_stage_file_mtime_unchanged_when_nothing_unstaged(
500 self, repo: pathlib.Path
501 ) -> None:
502 """VI1: stage.json mtime stays the same when no files are unstaged."""
503 import time
504 _stage(repo, "main.py")
505 path = stage_path(repo)
506 mtime_before = path.stat().st_mtime
507
508 time.sleep(0.05) # ensure mtime would differ if written
509 _run(repo, "code", "reset", "ghost.py")
510
511 mtime_after = path.stat().st_mtime
512 assert mtime_before == mtime_after, (
513 "Stage file was rewritten despite nothing being unstaged"
514 )
515
516 def test_VI2_stage_file_mtime_changes_when_something_unstaged(
517 self, repo: pathlib.Path
518 ) -> None:
519 """VI2: stage.json mtime changes when a file is actually unstaged."""
520 import time
521 _stage(repo, "main.py", "models.py")
522 path = stage_path(repo)
523 mtime_before = path.stat().st_mtime
524
525 time.sleep(0.05)
526 _run(repo, "code", "reset", "main.py")
527
528 mtime_after = path.stat().st_mtime
529 assert mtime_after > mtime_before, (
530 "Stage file was not rewritten after unstaging a file"
531 )
532
533 def test_VI3_dry_run_never_writes(self, repo: pathlib.Path) -> None:
534 """VI3: dry-run never touches the stage file."""
535 import time
536 _stage(repo, "main.py")
537 path = stage_path(repo)
538 mtime_before = path.stat().st_mtime
539
540 time.sleep(0.05)
541 _run(repo, "code", "reset", "--dry-run", "main.py")
542
543 assert path.stat().st_mtime == mtime_before
544
545
546 # ===========================================================================
547 # VII Edge cases
548 # ===========================================================================
549
550
551 class TestEdgeCasesVII:
552 """Fresh repo, duplicate paths, and partial-staged scenarios."""
553
554 def test_VII1_fresh_repo_no_commits(
555 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
556 ) -> None:
557 """VII1: reset works on a repo with no commits."""
558 monkeypatch.chdir(tmp_path)
559 runner.invoke(cli, ["init", "--domain", "code"], env=_env(tmp_path))
560 (tmp_path / "f.py").write_text("x = 1\n")
561 _run(tmp_path, "code", "add", "f.py")
562
563 code, out = _run(tmp_path, "code", "reset", "f.py")
564 assert code == 0, out
565 assert read_stage(tmp_path) == {}
566
567 def test_VII2_duplicate_paths_not_double_counted(
568 self, repo: pathlib.Path
569 ) -> None:
570 """VII2: passing the same path twice only unstages it once."""
571 _stage(repo, "main.py")
572
573 _, out = _run(
574 repo, "code", "reset", "--json", "main.py", "main.py"
575 )
576 data = json.loads(out.strip())
577 assert data["unstaged"] == 1
578
579 def test_VII3_reset_file_then_restage_works(
580 self, repo: pathlib.Path
581 ) -> None:
582 """VII3: unstage then restage a file produces a clean round-trip."""
583 (repo / "main.py").write_text("v1\n")
584 _run(repo, "code", "add", "main.py")
585 _run(repo, "code", "reset", "main.py")
586 assert not stage_path(repo).exists()
587
588 (repo / "main.py").write_text("v2\n")
589 _run(repo, "code", "add", "main.py")
590 assert "main.py" in read_stage(repo)
591
592 def test_VII4_reset_all_then_readd_works(
593 self, repo: pathlib.Path
594 ) -> None:
595 """VII4: clear stage then re-add produces a fresh stage."""
596 _stage(repo, "main.py")
597 _run(repo, "code", "reset")
598 assert read_stage(repo) == {}
599
600 _stage(repo, "models.py")
601 assert "models.py" in read_stage(repo)
602
603 def test_VII5_reset_after_commit_clears_nothing(
604 self, repo: pathlib.Path
605 ) -> None:
606 """VII5: after a commit the stage is clear — reset reports 'Nothing staged'."""
607 _stage(repo, "main.py")
608 _run(repo, "commit", "-m", "committed")
609 assert not stage_path(repo).exists()
610
611 code, out = _run(repo, "code", "reset")
612 assert code == 0
613 assert "Nothing staged" in out
614
615 def test_VII6_reset_mode_D_staged_deletion(
616 self, repo: pathlib.Path
617 ) -> None:
618 """VII6: a staged deletion (mode D) can be unstaged."""
619 (repo / "models.py").unlink()
620 _run(repo, "code", "add", "-u")
621 assert "models.py" in read_stage(repo)
622
623 _run(repo, "code", "reset", "models.py")
624 assert "models.py" not in read_stage(repo)
625
626 def test_VII7_json_and_text_formats_consistent(
627 self, repo: pathlib.Path
628 ) -> None:
629 """VII7: JSON and text modes agree on what was unstaged."""
630 _stage(repo, "main.py", "models.py")
631
632 # Reset in JSON mode and capture what was unstaged.
633 _, out_json = _run(
634 repo, "code", "add", "-A"
635 )
636 # Re-stage after the read above.
637 _stage(repo, "main.py", "models.py")
638 _, out_j = _run(repo, "code", "reset", "--json", "main.py")
639 data = json.loads(out_j.strip())
640 assert data["unstaged"] == 1
641
642 # Re-stage again and reset in text mode.
643 _stage(repo, "main.py")
644 _, out_t = _run(repo, "code", "reset", "main.py")
645 assert "main.py" in out_t
646
647
648 # ===========================================================================
649 # VIII Stress
650 # ===========================================================================
651
652
653 class TestStressVIII:
654 """High-volume and repeated-operation scenarios."""
655
656 def test_VIII1_reset_500_staged_files(self, repo: pathlib.Path) -> None:
657 """VIII1: unstaging 500 files in one shot clears the stage completely."""
658 for i in range(500):
659 (repo / f"f_{i:04d}.py").write_text(f"X = {i}\n")
660 _run(repo, "code", "add", "-A")
661 assert len(read_stage(repo)) >= 500
662
663 code, out = _run(repo, "code", "reset")
664 assert code == 0, out
665 assert read_stage(repo) == {}
666
667 def test_VIII2_glob_reset_500_files(self, repo: pathlib.Path) -> None:
668 """VIII2: glob '*.py' unstages 500 staged Python files."""
669 for i in range(500):
670 (repo / f"m_{i:04d}.py").write_text(f"V = {i}\n")
671 _run(repo, "code", "add", "-A")
672
673 _, out = _run(repo, "code", "reset", "--json", "*.py")
674 data = json.loads(out.strip())
675 assert data["unstaged"] >= 500
676 assert read_stage(repo) == {}
677
678 def test_VIII3_100_reset_cycles_leave_clean_stage(
679 self, repo: pathlib.Path
680 ) -> None:
681 """VIII3: 100 add/reset cycles leave a clean stage every time."""
682 for cycle in range(100):
683 # Use content that never matches the committed version (x = 1).
684 (repo / "main.py").write_text(f"# cycle {cycle:04d}\nx = {cycle + 1000}\n")
685 _run(repo, "code", "add", "main.py")
686 assert stage_path(repo).exists(), f"Cycle {cycle}: stage not written"
687
688 _run(repo, "code", "reset")
689 assert not stage_path(repo).exists(), f"Cycle {cycle}: stage not cleared"
690
691 def test_VIII4_dry_run_on_500_staged_files_accurate(
692 self, repo: pathlib.Path
693 ) -> None:
694 """VIII4: dry-run JSON count matches actual stage size."""
695 for i in range(500):
696 (repo / f"d_{i:04d}.py").write_text(f"Z = {i}\n")
697 _run(repo, "code", "add", "-A")
698 stage_size = len(read_stage(repo))
699
700 _, out = _run(repo, "code", "reset", "-n", "--json")
701 data = json.loads(out.strip())
702 assert data["dry_run"] is True
703 assert data["unstaged"] == stage_size
704 # Stage must be untouched.
705 assert len(read_stage(repo)) == stage_size
706
707 def test_VIII5_multi_pattern_glob_500_files(
708 self, repo: pathlib.Path
709 ) -> None:
710 """VIII5: multiple globs combined unstage all matching files."""
711 for i in range(250):
712 (repo / f"py_{i:03d}.py").write_text(f"A = {i}\n")
713 (repo / f"md_{i:03d}.md").write_text(f"# doc {i}\n")
714 _run(repo, "code", "add", "-A")
715
716 _, out = _run(
717 repo, "code", "reset", "--json", "*.py", "*.md"
718 )
719 data = json.loads(out.strip())
720 assert data["unstaged"] >= 500
721 assert read_stage(repo) == {}
File History 4 commits
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 20 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 22 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 28 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 28 days ago