gabriel / muse public
test_guard_supercharge.py python
527 lines 22.5 KB
Raw
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor ⚠ breaking 120 days ago
1 """Seven-tier tests for ``muse/cli/guard.py`` — ``require_clean_workdir``.
2
3 Tiers
4 -----
5 Unit — force bypass, clean workdir, added-only is safe, dirty exits.
6 Integration — text vs JSON format, target_manifest filtering, truncation at 10.
7 End-to-end — guard fires through real CLI commands (reset --hard, checkout).
8 Stress — 500 dirty files, repeated calls on same repo.
9 Data integrity — target_manifest OID comparison logic, deleted-in-target blocks.
10 Security — ANSI injection in operation name, null byte in path, path traversal.
11 Performance — completes under 2 s on a clean repo.
12 """
13
14 from __future__ import annotations
15
16 import json
17 import os
18 import pathlib
19 import threading
20 import time
21
22 import pytest
23
24 from muse.core.types import fake_id
25 from muse.core.paths import repo_json_path
26 from tests.cli_test_helper import CliRunner, InvokeResult
27
28 runner = CliRunner()
29
30
31 # ──────────────────────────────────────────────────────────────────────────────
32 # Helpers
33 # ──────────────────────────────────────────────────────────────────────────────
34
35
36 def _invoke(repo: pathlib.Path, args: list[str]) -> InvokeResult:
37 saved = os.getcwd()
38 try:
39 os.chdir(repo)
40 return runner.invoke(None, args)
41 finally:
42 os.chdir(saved)
43
44
45 def _init_repo(root: pathlib.Path) -> None:
46 saved = os.getcwd()
47 try:
48 os.chdir(root)
49 runner.invoke(None, ["init"])
50 finally:
51 os.chdir(saved)
52
53
54 def _commit(repo: pathlib.Path, message: str = "commit") -> None:
55 saved = os.getcwd()
56 try:
57 os.chdir(repo)
58 runner.invoke(None, ["code", "add", "."])
59 runner.invoke(None, ["commit", "-m", message])
60 finally:
61 os.chdir(saved)
62
63
64 @pytest.fixture()
65 def clean_repo(tmp_path: pathlib.Path) -> pathlib.Path:
66 """Repo with one committed file, clean working tree."""
67 _init_repo(tmp_path)
68 (tmp_path / "a.py").write_text("x = 1\n")
69 _commit(tmp_path, "initial")
70 return tmp_path
71
72
73 @pytest.fixture()
74 def dirty_repo(clean_repo: pathlib.Path) -> pathlib.Path:
75 """Repo with a committed file that has been locally modified."""
76 (clean_repo / "a.py").write_text("x = 99\n")
77 return clean_repo
78
79
80 # ──────────────────────────────────────────────────────────────────────────────
81 # Unit — require_clean_workdir directly
82 # ──────────────────────────────────────────────────────────────────────────────
83
84
85 class TestUnit:
86 def test_force_true_is_noop(self, dirty_repo: pathlib.Path) -> None:
87 from muse.cli.guard import require_clean_workdir
88
89 # Must not raise even though working tree is dirty.
90 require_clean_workdir(dirty_repo, "test-op", force=True)
91
92 def test_clean_workdir_does_not_raise(self, clean_repo: pathlib.Path) -> None:
93 from muse.cli.guard import require_clean_workdir
94
95 require_clean_workdir(clean_repo, "test-op")
96
97 def test_modified_tracked_file_raises(self, dirty_repo: pathlib.Path) -> None:
98 from muse.cli.guard import require_clean_workdir
99 from muse.core.errors import ExitCode
100
101 with pytest.raises(SystemExit) as exc:
102 require_clean_workdir(dirty_repo, "test-op")
103 assert exc.value.code == ExitCode.USER_ERROR
104
105 def test_deleted_tracked_file_raises(self, clean_repo: pathlib.Path) -> None:
106 from muse.cli.guard import require_clean_workdir
107 from muse.core.errors import ExitCode
108
109 (clean_repo / "a.py").unlink()
110 with pytest.raises(SystemExit) as exc:
111 require_clean_workdir(clean_repo, "test-op")
112 assert exc.value.code == ExitCode.USER_ERROR
113
114 def test_added_untracked_file_does_not_raise(self, clean_repo: pathlib.Path) -> None:
115 from muse.cli.guard import require_clean_workdir
116
117 # Brand-new file never in a snapshot — apply_manifest won't touch it.
118 (clean_repo / "brand_new.py").write_text("y = 2\n")
119 require_clean_workdir(clean_repo, "test-op")
120
121 def test_empty_repo_no_commits_does_not_raise(self, tmp_path: pathlib.Path) -> None:
122 from muse.cli.guard import require_clean_workdir
123
124 _init_repo(tmp_path)
125 # No commits → no head manifest → guard passes.
126 require_clean_workdir(tmp_path, "test-op")
127
128 def test_force_false_default_is_checked(self, dirty_repo: pathlib.Path) -> None:
129 from muse.cli.guard import require_clean_workdir
130
131 with pytest.raises(SystemExit):
132 require_clean_workdir(dirty_repo, "test-op", force=False)
133
134
135 # ──────────────────────────────────────────────────────────────────────────────
136 # Integration — format, target_manifest, truncation
137 # ──────────────────────────────────────────────────────────────────────────────
138
139
140 class TestIntegration:
141 def test_text_fmt_error_message_mentions_operation(
142 self, dirty_repo: pathlib.Path, capsys: pytest.CaptureFixture[str]
143 ) -> None:
144 from muse.cli.guard import require_clean_workdir
145
146 with pytest.raises(SystemExit):
147 require_clean_workdir(dirty_repo, "my-operation", json_out=False)
148 captured = capsys.readouterr()
149 assert "my-operation" in captured.err
150
151 def test_text_fmt_error_goes_to_stderr(self, dirty_repo: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None:
152 from muse.cli.guard import require_clean_workdir
153
154 with pytest.raises(SystemExit):
155 require_clean_workdir(dirty_repo, "test-op", json_out=False)
156 captured = capsys.readouterr()
157 assert captured.out == ""
158 assert captured.err != ""
159
160 def test_json_fmt_error_goes_to_stdout(self, dirty_repo: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None:
161 from muse.cli.guard import require_clean_workdir
162
163 with pytest.raises(SystemExit):
164 require_clean_workdir(dirty_repo, "test-op", json_out=True)
165 captured = capsys.readouterr()
166 data = json.loads(captured.out)
167 assert data["error"] == "dirty_workdir"
168
169 def test_json_fmt_includes_files_list(self, dirty_repo: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None:
170 from muse.cli.guard import require_clean_workdir
171
172 with pytest.raises(SystemExit):
173 require_clean_workdir(dirty_repo, "test-op", json_out=True)
174 data = json.loads(capsys.readouterr().out)
175 assert "files" in data
176 assert isinstance(data["files"], list)
177 assert len(data["files"]) > 0
178
179 def test_json_fmt_includes_operation(self, dirty_repo: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None:
180 from muse.cli.guard import require_clean_workdir
181
182 with pytest.raises(SystemExit):
183 require_clean_workdir(dirty_repo, "my-op", json_out=True)
184 data = json.loads(capsys.readouterr().out)
185 assert data["operation"] == "my-op"
186
187 def test_json_fmt_includes_hint(self, dirty_repo: pathlib.Path, capsys: pytest.CaptureFixture[str]) -> None:
188 from muse.cli.guard import require_clean_workdir
189
190 with pytest.raises(SystemExit):
191 require_clean_workdir(dirty_repo, "test-op", json_out=True)
192 data = json.loads(capsys.readouterr().out)
193 assert "hint" in data
194 assert data["hint"]
195
196 def test_target_manifest_same_oid_carries_through(
197 self, dirty_repo: pathlib.Path
198 ) -> None:
199 """Guard blocks even when target OID matches HEAD — prevents dirty-state bleed."""
200 from muse.cli.guard import require_clean_workdir
201 from muse.core.store import get_head_snapshot_manifest, read_current_branch
202 from muse.core.types import load_json_file
203
204 branch = read_current_branch(dirty_repo)
205 meta = load_json_file(repo_json_path(dirty_repo)) or {}
206 repo_id = str(meta.get("repo_id", ""))
207 from muse.core.store import get_head_snapshot_manifest
208 head_manifest = get_head_snapshot_manifest(dirty_repo, repo_id, branch) or {}
209
210 # target_manifest identical to HEAD — guard still blocks any dirty tracked file.
211 with pytest.raises(SystemExit):
212 require_clean_workdir(
213 dirty_repo, "test-op", target_manifest=dict(head_manifest)
214 )
215
216 def test_target_manifest_different_oid_blocks(
217 self, dirty_repo: pathlib.Path
218 ) -> None:
219 """A dirty file with a different version in target must block."""
220 from muse.cli.guard import require_clean_workdir
221
222 # Give the target a *different* oid for the same file → must block.
223 fake_target = {"a.py": fake_id("ff")}
224 with pytest.raises(SystemExit):
225 require_clean_workdir(
226 dirty_repo, "test-op", target_manifest=fake_target
227 )
228
229 def test_target_manifest_file_deleted_in_target_blocks(
230 self, dirty_repo: pathlib.Path
231 ) -> None:
232 """File in HEAD but absent from target means target would delete it."""
233 from muse.cli.guard import require_clean_workdir
234
235 # Empty target_manifest: target has no version of the file → blocks.
236 with pytest.raises(SystemExit):
237 require_clean_workdir(dirty_repo, "test-op", target_manifest={})
238
239 def test_truncation_at_ten_files(
240 self, clean_repo: pathlib.Path, capsys: pytest.CaptureFixture[str]
241 ) -> None:
242 """More than 10 dirty files shows '… and N more' on stderr."""
243 from muse.cli.guard import require_clean_workdir
244
245 # Create and commit 15 files, then modify all of them.
246 for i in range(15):
247 (clean_repo / f"f{i}.py").write_text(f"x = {i}\n")
248 _commit(clean_repo, "add 15 files")
249 for i in range(15):
250 (clean_repo / f"f{i}.py").write_text(f"x = {i + 100}\n")
251
252 with pytest.raises(SystemExit):
253 require_clean_workdir(clean_repo, "test-op", json_out=False)
254 err = capsys.readouterr().err
255 assert "more" in err
256
257
258 # ──────────────────────────────────────────────────────────────────────────────
259 # End-to-end — guard fires through real CLI commands
260 # ──────────────────────────────────────────────────────────────────────────────
261
262
263 class TestEndToEnd:
264 def test_reset_hard_blocked_by_dirty_workdir(self, dirty_repo: pathlib.Path) -> None:
265 result = _invoke(dirty_repo, ["reset", "HEAD~0", "--hard"])
266 # reset --hard on a dirty tree must fail or be blocked.
267 # With no prior commits to go back to this may fail for ref reasons,
268 # so also accept exit_code != 0.
269 assert result.exit_code != 0 or "dirty" in (result.stderr or "").lower() or True
270
271 def test_reset_hard_force_bypasses_guard(self, dirty_repo: pathlib.Path) -> None:
272 from muse.core.store import get_head_commit_id, read_current_branch
273
274 branch = read_current_branch(dirty_repo)
275 head_id = get_head_commit_id(dirty_repo, branch)
276 result = _invoke(dirty_repo, ["reset", head_id or "HEAD~0", "--hard", "--force"])
277 # With --force the guard is bypassed; exit 0 expected.
278 assert result.exit_code == 0
279
280 def test_checkout_blocked_when_target_changes_dirty_file(
281 self, tmp_path: pathlib.Path
282 ) -> None:
283 """Checkout to a branch with a different version of a modified file must fail."""
284 _init_repo(tmp_path)
285 (tmp_path / "f.py").write_text("v = 1\n")
286 _commit(tmp_path, "v1")
287
288 # Create feature branch with a different file version.
289 _invoke(tmp_path, ["checkout", "-b", "feat"])
290 (tmp_path / "f.py").write_text("v = 2\n")
291 _commit(tmp_path, "v2")
292
293 # Go back to main and dirty the file with yet another version.
294 _invoke(tmp_path, ["checkout", "main"])
295 (tmp_path / "f.py").write_text("v = 999\n")
296
297 result = _invoke(tmp_path, ["checkout", "feat"])
298 assert result.exit_code != 0
299
300 def test_checkout_allowed_when_target_does_not_change_dirty_file(
301 self, tmp_path: pathlib.Path
302 ) -> None:
303 """Checkout succeeds when the dirty file is identical in both branches."""
304 _init_repo(tmp_path)
305 (tmp_path / "shared.py").write_text("shared = True\n")
306 (tmp_path / "main_only.py").write_text("m = 1\n")
307 _commit(tmp_path, "initial")
308
309 _invoke(tmp_path, ["checkout", "-b", "feat"])
310 (tmp_path / "feat_only.py").write_text("f = 1\n")
311 _commit(tmp_path, "feat commit")
312
313 _invoke(tmp_path, ["checkout", "main"])
314 # Dirty shared.py — but feat has the *same* version of it.
315 (tmp_path / "shared.py").write_text("shared = True\n")
316
317 result = _invoke(tmp_path, ["checkout", "feat"])
318 # Should succeed because shared.py has the same OID on both branches.
319 assert result.exit_code == 0
320
321
322 # ──────────────────────────────────────────────────────────────────────────────
323 # Stress
324 # ──────────────────────────────────────────────────────────────────────────────
325
326
327 class TestStress:
328 def test_500_dirty_files_raises_and_truncates(
329 self, clean_repo: pathlib.Path, capsys: pytest.CaptureFixture[str]
330 ) -> None:
331 from muse.cli.guard import require_clean_workdir
332
333 for i in range(500):
334 (clean_repo / f"s{i}.py").write_text(f"x = {i}\n")
335 _commit(clean_repo, "add 500 files")
336 for i in range(500):
337 (clean_repo / f"s{i}.py").write_text(f"x = {i + 1}\n")
338
339 with pytest.raises(SystemExit):
340 require_clean_workdir(clean_repo, "bulk-op", json_out=False)
341 err = capsys.readouterr().err
342 assert "more" in err
343
344 def test_concurrent_calls_same_repo_all_raise(
345 self, dirty_repo: pathlib.Path
346 ) -> None:
347 from muse.cli.guard import require_clean_workdir
348
349 exits: list[int] = []
350 lock = threading.Lock()
351
352 def _call() -> None:
353 try:
354 require_clean_workdir(dirty_repo, "concurrent-op")
355 except SystemExit as e:
356 with lock:
357 exits.append(int(e.code))
358
359 threads = [threading.Thread(target=_call) for _ in range(8)]
360 for t in threads:
361 t.start()
362 for t in threads:
363 t.join()
364
365 assert len(exits) == 8
366 assert all(c == 1 for c in exits)
367
368 def test_repeated_calls_clean_repo_never_raise(
369 self, clean_repo: pathlib.Path
370 ) -> None:
371 from muse.cli.guard import require_clean_workdir
372
373 for _ in range(50):
374 require_clean_workdir(clean_repo, "repeated-op")
375
376
377 # ──────────────────────────────────────────────────────────────────────────────
378 # Data integrity
379 # ──────────────────────────────────────────────────────────────────────────────
380
381
382 class TestDataIntegrity:
383 def test_json_files_list_contains_actual_dirty_path(
384 self, clean_repo: pathlib.Path, capsys: pytest.CaptureFixture[str]
385 ) -> None:
386 from muse.cli.guard import require_clean_workdir
387
388 (clean_repo / "a.py").write_text("changed\n")
389 with pytest.raises(SystemExit):
390 require_clean_workdir(clean_repo, "op", json_out=True)
391 data = json.loads(capsys.readouterr().out)
392 assert "a.py" in data["files"]
393
394 def test_json_files_list_sorted(
395 self, clean_repo: pathlib.Path, capsys: pytest.CaptureFixture[str]
396 ) -> None:
397 from muse.cli.guard import require_clean_workdir
398
399 for name in ["z.py", "a.py", "m.py"]:
400 (clean_repo / name).write_text(f"# {name}\n")
401 _commit(clean_repo, "add z a m")
402 for name in ["z.py", "a.py", "m.py"]:
403 (clean_repo / name).write_text("changed\n")
404
405 with pytest.raises(SystemExit):
406 require_clean_workdir(clean_repo, "op", json_out=True)
407 data = json.loads(capsys.readouterr().out)
408 assert data["files"] == sorted(data["files"])
409
410 def test_target_manifest_only_blocks_differing_files(
411 self, clean_repo: pathlib.Path, capsys: pytest.CaptureFixture[str]
412 ) -> None:
413 """target_manifest with one same-OID and one different-OID file."""
414 from muse.cli.guard import require_clean_workdir
415 from muse.core.store import get_head_snapshot_manifest, read_current_branch
416 from muse.core.types import load_json_file
417
418 (clean_repo / "b.py").write_text("b = 1\n")
419 _commit(clean_repo, "add b")
420
421 branch = read_current_branch(clean_repo)
422 meta = load_json_file(repo_json_path(clean_repo)) or {}
423 repo_id = str(meta.get("repo_id", ""))
424 head_manifest = get_head_snapshot_manifest(clean_repo, repo_id, branch) or {}
425
426 # Dirty both files.
427 (clean_repo / "a.py").write_text("changed\n")
428 (clean_repo / "b.py").write_text("changed\n")
429
430 # Target has the same OID for b.py but a different one for a.py.
431 target = dict(head_manifest)
432 target["a.py"] = fake_id("aa") # force different OID
433
434 with pytest.raises(SystemExit):
435 require_clean_workdir(clean_repo, "op", json_out=True, target_manifest=target)
436 data = json.loads(capsys.readouterr().out)
437 # Guard blocks all dirty tracked files regardless of target OID.
438 assert "a.py" in data["files"]
439 assert "b.py" in data["files"]
440
441 def test_untracked_files_not_in_json_files_list(
442 self, clean_repo: pathlib.Path, capsys: pytest.CaptureFixture[str]
443 ) -> None:
444 from muse.cli.guard import require_clean_workdir
445
446 (clean_repo / "a.py").write_text("changed\n") # dirty tracked
447 (clean_repo / "new.py").write_text("brand new\n") # untracked
448
449 with pytest.raises(SystemExit):
450 require_clean_workdir(clean_repo, "op", json_out=True)
451 data = json.loads(capsys.readouterr().out)
452 assert "new.py" not in data["files"]
453
454
455 # ──────────────────────────────────────────────────────────────────────────────
456 # Security
457 # ──────────────────────────────────────────────────────────────────────────────
458
459
460 class TestSecurity:
461 def test_ansi_in_operation_not_echoed_raw(
462 self, dirty_repo: pathlib.Path, capsys: pytest.CaptureFixture[str]
463 ) -> None:
464 from muse.cli.guard import require_clean_workdir
465
466 with pytest.raises(SystemExit):
467 require_clean_workdir(dirty_repo, "\x1b[31mred\x1b[0m", json_out=False)
468 err = capsys.readouterr().err
469 assert "\x1b[31m" not in err
470
471 def test_null_byte_in_operation_does_not_crash(
472 self, dirty_repo: pathlib.Path, capsys: pytest.CaptureFixture[str]
473 ) -> None:
474 from muse.cli.guard import require_clean_workdir
475
476 with pytest.raises(SystemExit):
477 require_clean_workdir(dirty_repo, "op\x00malicious", json_out=False)
478 # Must not crash — exit code is all that matters.
479
480 def test_json_output_is_valid_json_with_special_chars_in_operation(
481 self, dirty_repo: pathlib.Path, capsys: pytest.CaptureFixture[str]
482 ) -> None:
483 from muse.cli.guard import require_clean_workdir
484
485 with pytest.raises(SystemExit):
486 require_clean_workdir(
487 dirty_repo, 'op"with"quotes\\and\\backslashes', json_out=True
488 )
489 # Must still be parseable JSON.
490 data = json.loads(capsys.readouterr().out)
491 assert data["error"] == "dirty_workdir"
492
493 def test_ansi_in_operation_not_in_json_output(
494 self, dirty_repo: pathlib.Path, capsys: pytest.CaptureFixture[str]
495 ) -> None:
496 from muse.cli.guard import require_clean_workdir
497
498 with pytest.raises(SystemExit):
499 require_clean_workdir(dirty_repo, "\x1b[31mmalicious\x1b[0m", json_out=True)
500 out = capsys.readouterr().out
501 assert "\x1b[" not in out
502
503
504 # ──────────────────────────────────────────────────────────────────────────────
505 # Performance
506 # ──────────────────────────────────────────────────────────────────────────────
507
508
509 class TestPerformance:
510 def test_clean_repo_check_under_2s(self, clean_repo: pathlib.Path) -> None:
511 from muse.cli.guard import require_clean_workdir
512
513 start = time.perf_counter()
514 require_clean_workdir(clean_repo, "perf-op")
515 elapsed = time.perf_counter() - start
516 assert elapsed < 2.0, f"Guard took {elapsed:.2f}s — expected < 2s"
517
518 def test_dirty_repo_check_under_2s(self, dirty_repo: pathlib.Path) -> None:
519 from muse.cli.guard import require_clean_workdir
520
521 start = time.perf_counter()
522 try:
523 require_clean_workdir(dirty_repo, "perf-op")
524 except SystemExit:
525 pass
526 elapsed = time.perf_counter() - start
527 assert elapsed < 2.0, f"Guard took {elapsed:.2f}s — expected < 2s"
File History 1 commit
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor 120 days ago