gabriel / muse public
test_cmd_reset_hardening.py python
625 lines 27.5 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
1 """Hardening tests for ``muse reset`` — security, schema, error routing, ordering.
2
3 These tests cover the issues fixed in the security/correctness/agent-UX audit.
4 They are intentionally distinct from the existing test_cmd_reset_revert.py and
5 test_cli_reset_revert.py suites, which cover the core reset algorithm.
6
7 Coverage tiers
8 --------------
9 Unit — parser flags, dead-code removal.
10 Integration — error routing to stderr, JSON schema, --dry-run, ordering safety.
11 End-to-end — full CLI: security, branch-name sanitization.
12 Security — ANSI injection, ref sanitization, exc sanitization.
13 Stress — large repos, concurrent repos, reset-and-verify cycles.
14 """
15
16 from __future__ import annotations
17
18 import json
19 import os
20 import pathlib
21 import subprocess
22 import threading
23 import time
24
25 import pytest
26
27 from tests.cli_test_helper import CliRunner, InvokeResult
28 from muse.core.store import get_head_commit_id, read_current_branch, snapshot_path
29 from muse.core._types import split_id
30
31 runner = CliRunner()
32
33 # ──────────────────────────────────────────────────────────────────────────────
34 # Helpers
35 # ──────────────────────────────────────────────────────────────────────────────
36
37 JSON_REQUIRED_KEYS = {
38 "branch", "ref", "old_commit_id", "new_commit_id", "snapshot_id", "mode", "dry_run",
39 }
40
41
42 def _invoke(repo: pathlib.Path, args: list[str]) -> InvokeResult:
43 saved = os.getcwd()
44 try:
45 os.chdir(repo)
46 return runner.invoke(None, args)
47 finally:
48 os.chdir(saved)
49
50
51 def _reset(repo: pathlib.Path, *extra: str) -> InvokeResult:
52 return _invoke(repo, ["reset", *extra])
53
54
55 def _commit(repo: pathlib.Path, message: str) -> str:
56 """Commit current working tree and return the (prefix of) commit ID."""
57 import re
58
59 result = _invoke(repo, ["commit", "-m", message])
60 m = re.search(r'\[(?:main|[^ ]+) ([0-9a-f]{8,})', result.output)
61 return m.group(1) if m else ""
62
63
64 @pytest.fixture()
65 def repo(tmp_path: pathlib.Path) -> pathlib.Path:
66 """Initialised repo with two commits on ``main``."""
67 saved = os.getcwd()
68 try:
69 os.chdir(tmp_path)
70 runner.invoke(None, ["init"])
71 finally:
72 os.chdir(saved)
73 (tmp_path / "a.py").write_text("x = 1\n")
74 _commit(tmp_path, "initial")
75 (tmp_path / "b.py").write_text("y = 2\n")
76 _commit(tmp_path, "add b")
77 return tmp_path
78
79
80 @pytest.fixture()
81 def c1_id(repo: pathlib.Path) -> str:
82 """Full commit ID of the first commit (HEAD~1)."""
83 from muse.core.store import read_commit
84
85 head_id = get_head_commit_id(repo, "main") or ""
86 head = read_commit(repo, head_id)
87 return (head.parent_commit_id or "") if head else ""
88
89
90 # ──────────────────────────────────────────────────────────────────────────────
91 # Unit — parser flags
92 # ──────────────────────────────────────────────────────────────────────────────
93
94
95 class TestRegisterFlags:
96 def _parse(self, *args: str) -> "object":
97 import argparse
98 from muse.cli.commands.reset import register
99
100 p = argparse.ArgumentParser()
101 sub = p.add_subparsers()
102 register(sub)
103 return p.parse_args(["reset", *args])
104
105 def test_default_json_out_is_false(self) -> None:
106 ns = self._parse("HEAD~1")
107 assert ns.json_out is False
108
109 def test_json_flag_sets_json_out(self) -> None:
110 ns = self._parse("HEAD~1", "--json")
111 assert ns.json_out is True
112
113 def test_j_shorthand_sets_json_out(self) -> None:
114 ns = self._parse("HEAD~1", "-j")
115 assert ns.json_out is True
116
117 def test_dry_run_default_false(self) -> None:
118 import argparse
119 from muse.cli.commands.reset import register
120
121 p = argparse.ArgumentParser()
122 sub = p.add_subparsers()
123 register(sub)
124 ns = p.parse_args(["reset", "HEAD~1"])
125 assert ns.dry_run is False
126
127 def test_dry_run_flag(self) -> None:
128 import argparse
129 from muse.cli.commands.reset import register
130
131 p = argparse.ArgumentParser()
132 sub = p.add_subparsers()
133 register(sub)
134 ns = p.parse_args(["reset", "HEAD~1", "--dry-run"])
135 assert ns.dry_run is True
136
137 def test_hard_default_false(self) -> None:
138 import argparse
139 from muse.cli.commands.reset import register
140
141 p = argparse.ArgumentParser()
142 sub = p.add_subparsers()
143 register(sub)
144 ns = p.parse_args(["reset", "HEAD~1"])
145 assert ns.hard is False
146
147 def test_hard_flag(self) -> None:
148 import argparse
149 from muse.cli.commands.reset import register
150
151 p = argparse.ArgumentParser()
152 sub = p.add_subparsers()
153 register(sub)
154 ns = p.parse_args(["reset", "HEAD~1", "--hard"])
155 assert ns.hard is True
156
157 def test_force_default_false(self) -> None:
158 import argparse
159 from muse.cli.commands.reset import register
160
161 p = argparse.ArgumentParser()
162 sub = p.add_subparsers()
163 register(sub)
164 ns = p.parse_args(["reset", "HEAD~1"])
165 assert ns.force is False
166
167
168 # ──────────────────────────────────────────────────────────────────────────────
169 # Unit — dead-code removal
170 # ──────────────────────────────────────────────────────────────────────────────
171
172
173 class TestDeadCodeRemoved:
174 def test_read_branch_wrapper_removed(self) -> None:
175 import muse.cli.commands.reset as m
176
177 assert not hasattr(m, "_read_branch"), (
178 "_read_branch was a dead one-liner wrapper and must be deleted"
179 )
180
181
182 # ──────────────────────────────────────────────────────────────────────────────
183 # Integration — error routing to stderr
184 # ──────────────────────────────────────────────────────────────────────────────
185
186
187 class TestErrorRouting:
188 def test_unknown_ref_error_to_stderr(self, repo: pathlib.Path) -> None:
189 result = _reset(repo, "bogus-ref")
190 assert result.exit_code == 1
191 assert "not found" in (result.stderr or "").lower()
192 assert "not found" not in result.output.replace(result.stderr or "", "")
193
194 def test_unknown_flag_exits_nonzero(self, repo: pathlib.Path) -> None:
195 result = _reset(repo, "HEAD~1", "--format", "xml")
196 assert result.exit_code != 0
197
198 def test_missing_snapshot_error_to_stderr(self, repo: pathlib.Path, c1_id: str) -> None:
199 """When --hard reset target snapshot is missing, error goes to stderr."""
200 from muse.core.store import read_commit
201
202 commit = read_commit(repo, c1_id)
203 if commit is None:
204 pytest.skip("Could not read c1 commit")
205 snap_id = commit.snapshot_id
206 snap_path = snapshot_path(repo, snap_id)
207 snap_path.unlink(missing_ok=True)
208
209 result = _reset(repo, c1_id, "--hard")
210 assert result.exit_code != 0
211 assert "not found" in (result.stderr or "").lower() or "snapshot" in (result.stderr or "").lower()
212
213 def test_snapshot_pre_validated_before_branch_ref_written(
214 self, repo: pathlib.Path, c1_id: str
215 ) -> None:
216 """Critical ordering fix: branch ref must NOT advance when snapshot is missing.
217
218 Before the fix, write_branch_ref() was called BEFORE read_snapshot(),
219 so a missing snapshot would leave the branch pointer at the new commit
220 with an unrestored working tree — an inconsistent, unrecoverable state.
221 """
222 from muse.core.store import read_commit
223
224 commit = read_commit(repo, c1_id)
225 if commit is None:
226 pytest.skip("Could not read c1 commit")
227 snap_id = commit.snapshot_id
228 snap_path = snapshot_path(repo, snap_id)
229 snap_path.unlink(missing_ok=True)
230
231 before_head = get_head_commit_id(repo, "main")
232 _reset(repo, c1_id, "--hard")
233 after_head = get_head_commit_id(repo, "main")
234
235 # Branch ref must remain at the original commit — not advanced to c1.
236 assert before_head == after_head, (
237 "Branch ref was advanced even though snapshot was missing — "
238 "this is the pre-fix ordering bug"
239 )
240
241 def test_snapshot_source_in_run_before_write_branch_ref(self) -> None:
242 """Source inspection: read_snapshot must appear before write_branch_ref.
243
244 We skip comment lines (those starting with #) to avoid false matches
245 from documentation comments that reference function names.
246 """
247 import inspect
248 from muse.cli.commands.reset import run
249
250 src = inspect.getsource(run)
251 # Only consider non-comment executable lines.
252 code_lines = [
253 (i, l) for i, l in enumerate(src.split("\n"))
254 if not l.lstrip().startswith("#")
255 ]
256 snap_lineno = next((i for i, l in code_lines if "read_snapshot(" in l), -1)
257 write_lineno = next((i for i, l in code_lines if "write_branch_ref(" in l), -1)
258 assert snap_lineno != -1, "read_snapshot not found in run()"
259 assert write_lineno != -1, "write_branch_ref not found in run()"
260 assert snap_lineno < write_lineno, (
261 f"read_snapshot (line {snap_lineno}) must appear before "
262 f"write_branch_ref (line {write_lineno}) in run() — "
263 "this is the critical ordering fix that prevents orphaned branch refs"
264 )
265
266
267 # ──────────────────────────────────────────────────────────────────────────────
268 # Integration — JSON schema stability
269 # ──────────────────────────────────────────────────────────────────────────────
270
271
272 class TestJsonSchema:
273 def test_soft_reset_has_all_keys(self, repo: pathlib.Path, c1_id: str) -> None:
274 result = _reset(repo, c1_id, "--json")
275 assert result.exit_code == 0
276 data = json.loads(result.output)
277 missing = JSON_REQUIRED_KEYS - set(data)
278 assert not missing, f"Missing keys in soft reset JSON: {missing}"
279
280 def test_hard_reset_has_all_keys(self, repo: pathlib.Path, c1_id: str) -> None:
281 result = _reset(repo, c1_id, "--hard", "--json")
282 assert result.exit_code == 0
283 data = json.loads(result.output)
284 missing = JSON_REQUIRED_KEYS - set(data)
285 assert not missing, f"Missing keys in hard reset JSON: {missing}"
286
287 def test_dry_run_has_all_keys(self, repo: pathlib.Path, c1_id: str) -> None:
288 result = _reset(repo, c1_id, "--dry-run", "--json")
289 assert result.exit_code == 0
290 data = json.loads(result.output)
291 missing = JSON_REQUIRED_KEYS - set(data)
292 assert not missing, f"Missing keys in dry-run JSON: {missing}"
293
294 def test_soft_mode_is_soft(self, repo: pathlib.Path, c1_id: str) -> None:
295 result = _reset(repo, c1_id, "--json")
296 data = json.loads(result.output)
297 assert data["mode"] == "soft"
298
299 def test_hard_mode_is_hard(self, repo: pathlib.Path, c1_id: str) -> None:
300 result = _reset(repo, c1_id, "--hard", "--json")
301 data = json.loads(result.output)
302 assert data["mode"] == "hard"
303
304 def test_dry_run_flag_is_true(self, repo: pathlib.Path, c1_id: str) -> None:
305 result = _reset(repo, c1_id, "--dry-run", "--json")
306 data = json.loads(result.output)
307 assert data["dry_run"] is True
308
309 def test_live_reset_dry_run_flag_is_false(self, repo: pathlib.Path, c1_id: str) -> None:
310 result = _reset(repo, c1_id, "--json")
311 data = json.loads(result.output)
312 assert data["dry_run"] is False
313
314 def test_ref_field_matches_input(self, repo: pathlib.Path, c1_id: str) -> None:
315 result = _reset(repo, c1_id, "--json")
316 data = json.loads(result.output)
317 assert data["ref"] == c1_id
318
319 def test_snapshot_id_is_sha256(self, repo: pathlib.Path, c1_id: str) -> None:
320 result = _reset(repo, c1_id, "--json")
321 data = json.loads(result.output)
322 sid = data["snapshot_id"]
323 _, hex_part = split_id(sid)
324 assert len(hex_part) == 64, f"Expected 64-char hex after prefix, got {len(hex_part)}: {sid!r}"
325 assert all(c in "0123456789abcdef" for c in hex_part)
326
327 def test_new_commit_id_is_sha256(self, repo: pathlib.Path, c1_id: str) -> None:
328 result = _reset(repo, c1_id, "--json")
329 data = json.loads(result.output)
330 nid = data["new_commit_id"]
331 _, hex_part = split_id(nid)
332 assert len(hex_part) == 64, f"Expected 64-char hex after prefix, got {len(hex_part)}: {nid!r}"
333
334 def test_old_commit_id_was_head(self, repo: pathlib.Path, c1_id: str) -> None:
335 head_before = get_head_commit_id(repo, "main")
336 result = _reset(repo, c1_id, "--json")
337 data = json.loads(result.output)
338 assert data["old_commit_id"] == head_before
339
340 def test_branch_field_is_current_branch(self, repo: pathlib.Path, c1_id: str) -> None:
341 result = _reset(repo, c1_id, "--json")
342 data = json.loads(result.output)
343 assert data["branch"] == "main"
344
345
346 # ──────────────────────────────────────────────────────────────────────────────
347 # Integration — --dry-run
348 # ──────────────────────────────────────────────────────────────────────────────
349
350
351 class TestDryRun:
352 def test_dry_run_does_not_advance_branch(self, repo: pathlib.Path, c1_id: str) -> None:
353 before = get_head_commit_id(repo, "main")
354 _reset(repo, c1_id, "--dry-run")
355 after = get_head_commit_id(repo, "main")
356 assert before == after
357
358 def test_dry_run_does_not_modify_workdir(self, repo: pathlib.Path, c1_id: str) -> None:
359 b_content = (repo / "b.py").read_text()
360 _reset(repo, c1_id, "--dry-run", "--hard")
361 assert (repo / "b.py").read_text() == b_content
362
363 def test_dry_run_exit_code_zero(self, repo: pathlib.Path, c1_id: str) -> None:
364 result = _reset(repo, c1_id, "--dry-run")
365 assert result.exit_code == 0
366
367 def test_dry_run_invalid_ref_exits_1(self, repo: pathlib.Path) -> None:
368 result = _reset(repo, "nonexistent-ref", "--dry-run")
369 assert result.exit_code == 1
370
371 def test_dry_run_json_shows_would_be_commit(self, repo: pathlib.Path, c1_id: str) -> None:
372 result = _reset(repo, c1_id, "--dry-run", "--json")
373 data = json.loads(result.output)
374 assert data["new_commit_id"] == c1_id or data["new_commit_id"].startswith(c1_id)
375
376 def test_dry_run_text_mentions_would(self, repo: pathlib.Path, c1_id: str) -> None:
377 result = _reset(repo, c1_id, "--dry-run")
378 assert "dry-run" in result.output.lower() or "would" in result.output.lower()
379
380 def test_dry_run_does_not_write_reflog(self, repo: pathlib.Path, c1_id: str) -> None:
381 from muse.core.reflog import read_reflog
382
383 before_count = len(read_reflog(repo, "main"))
384 _reset(repo, c1_id, "--dry-run")
385 after_count = len(read_reflog(repo, "main"))
386 assert before_count == after_count
387
388
389 # ──────────────────────────────────────────────────────────────────────────────
390 # Integration — soft reset
391 # ──────────────────────────────────────────────────────────────────────────────
392
393
394 class TestSoftReset:
395 def test_soft_advances_branch_to_target(self, repo: pathlib.Path, c1_id: str) -> None:
396 _reset(repo, c1_id)
397 head = get_head_commit_id(repo, "main")
398 assert head is not None and head.startswith(c1_id)
399
400 def test_soft_preserves_working_tree(self, repo: pathlib.Path, c1_id: str) -> None:
401 before = (repo / "b.py").read_text()
402 _reset(repo, c1_id)
403 assert (repo / "b.py").read_text() == before
404
405 def test_soft_reflog_entry_written(self, repo: pathlib.Path, c1_id: str) -> None:
406 from muse.core.reflog import read_reflog
407
408 before_count = len(read_reflog(repo, "main"))
409 _reset(repo, c1_id)
410 after_count = len(read_reflog(repo, "main"))
411 assert after_count > before_count
412
413 def test_soft_text_output_has_commit_id(self, repo: pathlib.Path, c1_id: str) -> None:
414 result = _reset(repo, c1_id)
415 assert c1_id[:8] in result.output
416
417
418 # ──────────────────────────────────────────────────────────────────────────────
419 # Integration — hard reset
420 # ──────────────────────────────────────────────────────────────────────────────
421
422
423 class TestHardReset:
424 def test_hard_advances_branch_to_target(self, repo: pathlib.Path, c1_id: str) -> None:
425 result = _reset(repo, c1_id, "--hard")
426 assert result.exit_code == 0
427 head = get_head_commit_id(repo, "main")
428 assert head is not None and head.startswith(c1_id)
429
430 def test_hard_restores_workdir(self, repo: pathlib.Path, c1_id: str) -> None:
431 assert (repo / "b.py").exists()
432 result = _reset(repo, c1_id, "--hard")
433 assert result.exit_code == 0
434 # b.py was added in the second commit; resetting to c1 removes it
435 assert not (repo / "b.py").exists()
436
437 def test_hard_text_output_shows_head_is_now(self, repo: pathlib.Path, c1_id: str) -> None:
438 result = _reset(repo, c1_id, "--hard")
439 assert "HEAD is now at" in result.output or c1_id[:8] in result.output
440
441 def test_hard_uses_message_first_line(self, repo: pathlib.Path, c1_id: str) -> None:
442 """Text output shows only the first line of a multiline commit message."""
443 (repo / "x.py").write_text("x=1\n")
444 multiline_id = _commit(repo, "first line\n\nmore detail here")
445 # Go back to c1 so we can reset to the multiline commit
446 _reset(repo, c1_id)
447 result = _reset(repo, multiline_id, "--hard")
448 assert "more detail here" not in result.output
449
450
451 # ──────────────────────────────────────────────────────────────────────────────
452 # Security — ANSI injection
453 # ──────────────────────────────────────────────────────────────────────────────
454
455
456 class TestSecurityAnsi:
457 ESC = "\x1b["
458
459 def test_unknown_ref_sanitized_in_stderr(self, repo: pathlib.Path) -> None:
460 evil_ref = f"{self.ESC}31mevil{self.ESC}0m"
461 result = _reset(repo, evil_ref)
462 assert self.ESC not in (result.stderr or "")
463
464 def test_unknown_flag_with_ansi_exits_nonzero(self, repo: pathlib.Path) -> None:
465 evil_fmt = f"{self.ESC}31mxml{self.ESC}0m"
466 result = _reset(repo, "HEAD~1", "--format", evil_fmt)
467 assert result.exit_code != 0
468
469 def test_no_ansi_in_stdout_on_error(self, repo: pathlib.Path) -> None:
470 evil_ref = f"{self.ESC}31mevil{self.ESC}0m"
471 result = _reset(repo, evil_ref)
472 # stdout must be clean — errors go to stderr
473 stdout_only = result.output.replace(result.stderr or "", "")
474 assert self.ESC not in stdout_only
475
476 def test_exc_sanitized_in_branch_validation(self, repo: pathlib.Path) -> None:
477 """sanitize_display(str(exc)) must be used, not bare f'{exc}'."""
478 import inspect
479 from muse.cli.commands.reset import run
480
481 src = inspect.getsource(run)
482 # Confirm the pattern sanitize_display(str(exc)) is used, not bare {exc}
483 assert "sanitize_display(str(exc))" in src
484
485 def test_ref_sanitized_in_not_found_message(self) -> None:
486 """sanitize_display(ref) must be used in the not-found error message."""
487 import inspect
488 from muse.cli.commands.reset import run
489
490 src = inspect.getsource(run)
491 assert "sanitize_display(ref)" in src
492
493 def test_soft_text_output_sanitizes_branch(self, repo: pathlib.Path, c1_id: str) -> None:
494 result = _reset(repo, c1_id)
495 assert self.ESC not in result.output
496
497 def test_hard_text_output_sanitizes_message(self, repo: pathlib.Path, c1_id: str) -> None:
498 result = _reset(repo, c1_id, "--hard")
499 assert self.ESC not in result.output
500
501
502 # ──────────────────────────────────────────────────────────────────────────────
503 # Integration — get_head_commit_id replaces ref_file.read_text()
504 # ──────────────────────────────────────────────────────────────────────────────
505
506
507 class TestRefAbstraction:
508 def test_no_direct_ref_file_read(self) -> None:
509 """run() must use get_head_commit_id(), not read ref_file directly."""
510 import inspect
511 from muse.cli.commands.reset import run
512
513 src = inspect.getsource(run)
514 assert "ref_file.read_text" not in src, (
515 "Direct ref_file.read_text() bypasses the get_head_commit_id "
516 "abstraction layer and is a TOCTOU vulnerability"
517 )
518 assert "get_head_commit_id" in src
519
520 def test_old_commit_id_correct_on_first_commit(self, repo: pathlib.Path) -> None:
521 """old_commit_id in JSON must match HEAD before the reset."""
522 head = get_head_commit_id(repo, "main")
523 result = _reset(repo, "HEAD~1", "--json")
524 data = json.loads(result.output)
525 assert data["old_commit_id"] == head
526
527
528 # ──────────────────────────────────────────────────────────────────────────────
529 # Stress
530 # ──────────────────────────────────────────────────────────────────────────────
531
532
533 @pytest.mark.slow
534 class TestStress:
535 def test_soft_reset_across_50_commits(self, repo: pathlib.Path) -> None:
536 """Soft-reset across 50 commits must complete under 5s."""
537 # Add 48 more commits (we already have 2)
538 for i in range(48):
539 (repo / f"f{i:03d}.py").write_text(f"x={i}\n")
540 _commit(repo, f"commit {i}")
541
542 from muse.core.store import read_commit
543
544 # Walk to the 10th commit from the end
545 current_id = get_head_commit_id(repo, "main") or ""
546 target_id = current_id
547 for _ in range(10):
548 c = read_commit(repo, target_id)
549 if c and c.parent_commit_id:
550 target_id = c.parent_commit_id
551
552 t0 = time.perf_counter()
553 result = _reset(repo, target_id)
554 elapsed = (time.perf_counter() - t0) * 1000
555 assert result.exit_code == 0
556 assert elapsed < 5000, f"50-commit soft reset took {elapsed:.0f}ms (limit 5s)"
557
558 def test_hard_reset_with_100_files(self, repo: pathlib.Path, c1_id: str) -> None:
559 """Hard-reset restoring 100 files must complete under 5s."""
560 for i in range(100):
561 (repo / f"g{i:03d}.py").write_text(f"y={i}\n")
562 _commit(repo, "add 100 files")
563 head_id = get_head_commit_id(repo, "main") or ""
564
565 # Reset back to c1 (removes 101 files) then back to head (restores them)
566 t0 = time.perf_counter()
567 r1 = _reset(repo, c1_id, "--hard")
568 r2 = _reset(repo, head_id, "--hard")
569 elapsed = (time.perf_counter() - t0) * 1000
570 assert r1.exit_code == 0
571 assert r2.exit_code == 0
572 assert elapsed < 5000, f"100-file hard reset cycle took {elapsed:.0f}ms"
573
574 def test_dry_run_50_commits_fast(self, repo: pathlib.Path) -> None:
575 """Dry-run across 50 commits must complete under 2s."""
576 for i in range(48):
577 (repo / f"h{i:03d}.py").write_text(f"z={i}\n")
578 _commit(repo, f"commit {i}")
579
580 t0 = time.perf_counter()
581 result = _reset(repo, "HEAD~1", "--dry-run")
582 elapsed = (time.perf_counter() - t0) * 1000
583 assert result.exit_code == 0
584 assert elapsed < 2000, f"dry-run took {elapsed:.0f}ms (limit 2s)"
585
586 def test_concurrent_resets_separate_repos(self, tmp_path: pathlib.Path) -> None:
587 """Multiple repos resetting concurrently must not interfere."""
588 errors: list[str] = []
589
590 def do_reset(idx: int) -> None:
591 repo_dir = tmp_path / f"repo_{idx}"
592 repo_dir.mkdir()
593 subprocess.run(["muse", "init"], cwd=str(repo_dir), capture_output=True)
594 (repo_dir / "a.py").write_text(f"x={idx}\n")
595 subprocess.run(
596 ["muse", "commit", "-m", f"c1_{idx}"],
597 cwd=str(repo_dir), capture_output=True,
598 )
599 (repo_dir / "b.py").write_text(f"y={idx}\n")
600 subprocess.run(
601 ["muse", "commit", "-m", f"c2_{idx}"],
602 cwd=str(repo_dir), capture_output=True,
603 )
604 r = subprocess.run(
605 ["muse", "reset", "HEAD~1", "--json"],
606 cwd=str(repo_dir), capture_output=True, text=True,
607 )
608 if r.returncode != 0:
609 errors.append(f"repo_{idx}: exit={r.returncode}, err={r.stderr[:60]}")
610 return
611 try:
612 data = json.loads(r.stdout)
613 if data["mode"] != "soft":
614 errors.append(f"repo_{idx}: unexpected mode {data['mode']}")
615 if data["dry_run"] is not False:
616 errors.append(f"repo_{idx}: dry_run not False")
617 except Exception as e:
618 errors.append(f"repo_{idx}: parse error {e}")
619
620 threads = [threading.Thread(target=do_reset, args=(i,)) for i in range(6)]
621 for t in threads:
622 t.start()
623 for t in threads:
624 t.join()
625 assert not errors, "Concurrent reset errors:\n" + "\n".join(errors)
File History 3 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 137 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 140 days ago