gabriel / muse public
test_cmd_commit.py python
1,190 lines 52.7 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 132 days ago
1 """Tests for ``muse commit``.
2
3 Coverage tiers
4 --------------
5 Unit — parser flags, pure-logic helpers, sanitization.
6 Integration — actual repo operations: commits, snapshots, reflog, harmony.
7 End-to-end — CLI invocations, text and JSON output paths.
8 Security — ANSI injection, author impersonation, provenance field caps.
9 Stress — 100 sequential commits, large manifests, concurrent writes.
10 """
11
12 from __future__ import annotations
13
14 import argparse
15 import json
16 import os
17 import pathlib
18 import subprocess
19 import threading
20 import time
21 from unittest.mock import patch
22
23 import pytest
24
25 from tests.cli_test_helper import CliRunner, InvokeResult
26 from muse.core.store import (
27 get_head_commit_id,
28 read_commit,
29 read_current_branch,
30 read_snapshot,
31 )
32
33 runner = CliRunner()
34
35 # ──────────────────────────────────────────────────────────────────────────────
36 # Helpers
37 # ──────────────────────────────────────────────────────────────────────────────
38
39
40 def _invoke(repo: pathlib.Path, args: list[str]) -> InvokeResult:
41 """Run a muse command in *repo* and return the result."""
42 saved = os.getcwd()
43 try:
44 os.chdir(repo)
45 return runner.invoke(None, args)
46 finally:
47 os.chdir(saved)
48
49
50 def _commit(repo: pathlib.Path, *extra: str) -> InvokeResult:
51 return _invoke(repo, ["commit", *extra])
52
53
54 def _init_repo(repo: pathlib.Path) -> InvokeResult:
55 repo.mkdir(parents=True, exist_ok=True)
56 return _invoke(repo, ["init"])
57
58
59 # ──────────────────────────────────────────────────────────────────────────────
60 # Fixtures
61 # ──────────────────────────────────────────────────────────────────────────────
62
63
64 @pytest.fixture()
65 def repo(tmp_path: pathlib.Path) -> pathlib.Path:
66 """Initialised repo with one tracked file ready to commit."""
67 _init_repo(tmp_path)
68 (tmp_path / "a.py").write_text("x = 1\n")
69 return tmp_path
70
71
72 # ──────────────────────────────────────────────────────────────────────────────
73 # Unit — parser flags
74 # ──────────────────────────────────────────────────────────────────────────────
75
76
77 class TestRegisterFlags:
78 """All expected CLI flags are registered on the commit subcommand."""
79
80 def _parse(self, *args: str) -> argparse.Namespace:
81 from muse.cli.commands.commit import register
82
83 p = argparse.ArgumentParser()
84 sub = p.add_subparsers()
85 register(sub)
86 return p.parse_args(["commit", *args])
87
88 def test_message_flag(self) -> None:
89 ns = self._parse("-m", "hello")
90 assert ns.message == "hello"
91
92 def test_allow_empty_flag(self) -> None:
93 ns = self._parse("-m", "x", "--allow-empty")
94 assert ns.allow_empty is True
95
96 def test_dry_run_short_flag(self) -> None:
97 ns = self._parse("-m", "x", "-n")
98 assert ns.dry_run is True
99
100 def test_dry_run_long_flag(self) -> None:
101 ns = self._parse("-m", "x", "--dry-run")
102 assert ns.dry_run is True
103
104 def test_json_flag(self) -> None:
105 ns = self._parse("-m", "x", "--json")
106 assert ns.json_out is True
107
108 def test_j_shorthand(self) -> None:
109 ns = self._parse("-m", "x", "-j")
110 assert ns.json_out is True
111
112 def test_default_json_out_is_false(self) -> None:
113 ns = self._parse("-m", "x")
114 assert ns.json_out is False
115
116 def test_agent_id_flag(self) -> None:
117 ns = self._parse("-m", "x", "--agent-id", "bot-1")
118 assert ns.agent_id == "bot-1"
119
120 def test_model_id_flag(self) -> None:
121 ns = self._parse("-m", "x", "--model-id", "claude-4")
122 assert ns.model_id == "claude-4"
123
124 def test_toolchain_id_flag(self) -> None:
125 ns = self._parse("-m", "x", "--toolchain-id", "cursor-v1")
126 assert ns.toolchain_id == "cursor-v1"
127
128 def test_section_flag(self) -> None:
129 ns = self._parse("-m", "x", "--section", "chorus")
130 assert ns.section == "chorus"
131
132 def test_track_flag(self) -> None:
133 ns = self._parse("-m", "x", "--track", "bass")
134 assert ns.track == "bass"
135
136 def test_emotion_flag(self) -> None:
137 ns = self._parse("-m", "x", "--emotion", "joyful")
138 assert ns.emotion == "joyful"
139
140 def test_author_flag(self) -> None:
141 ns = self._parse("-m", "x", "--author", "alice")
142 assert ns.author == "alice"
143
144 def test_sign_flag(self) -> None:
145 ns = self._parse("-m", "x", "--sign")
146 assert ns.sign is True
147
148
149 # ──────────────────────────────────────────────────────────────────────────────
150 # Unit — _MAX_FIELD_LEN constant
151 # ──────────────────────────────────────────────────────────────────────────────
152
153
154 class TestMaxFieldLen:
155 def test_constant_exists_and_is_256(self) -> None:
156 from muse.cli.commands.commit import _MAX_FIELD_LEN
157
158 assert _MAX_FIELD_LEN == 256
159
160 def test_no_separate_max_author_constant(self) -> None:
161 import muse.cli.commands.commit as m
162
163 assert not hasattr(m, "_MAX_AUTHOR"), "_MAX_AUTHOR should be replaced by _MAX_FIELD_LEN"
164 assert not hasattr(m, "_MAX_PROV"), "_MAX_PROV should be replaced by _MAX_FIELD_LEN"
165
166
167 # ──────────────────────────────────────────────────────────────────────────────
168 # Unit — dead-code removal
169 # ──────────────────────────────────────────────────────────────────────────────
170
171
172 class TestDeadCodeRemoved:
173 def test_read_branch_removed(self) -> None:
174 import muse.cli.commands.commit as m
175
176 assert not hasattr(m, "_read_branch"), (
177 "_read_branch was a dead wrapper; it should have been deleted"
178 )
179
180 def test_read_parent_id_removed(self) -> None:
181 import muse.cli.commands.commit as m
182
183 assert not hasattr(m, "_read_parent_id"), (
184 "_read_parent_id was a dead wrapper; it should have been deleted"
185 )
186
187
188 # ──────────────────────────────────────────────────────────────────────────────
189 # Unit — inline imports removed
190 # ──────────────────────────────────────────────────────────────────────────────
191
192
193 class TestNoInlineImports:
194 def test_sign_commit_record_is_module_level_import(self) -> None:
195 import inspect
196
197 import muse.cli.commands.commit as m
198
199 src = inspect.getsource(m.run)
200 assert "from muse.core.provenance import sign_commit_record" not in src, (
201 "sign_commit_record import must be at module level, not inside run()"
202 )
203
204 def test_no_inline_store_imports(self) -> None:
205 import inspect
206
207 import muse.cli.commands.commit as m
208
209 src = inspect.getsource(m.run)
210 assert "from muse.core.store import" not in src, (
211 "store imports inside run() should be at module level"
212 )
213
214
215 # ──────────────────────────────────────────────────────────────────────────────
216 # Integration — basic commit lifecycle
217 # ──────────────────────────────────────────────────────────────────────────────
218
219
220 class TestBasicCommit:
221 def test_first_commit_succeeds(self, repo: pathlib.Path) -> None:
222 result = _commit(repo, "-m", "init")
223 assert result.exit_code == 0
224 assert "init" in result.output
225
226 def test_commit_creates_commit_record(self, repo: pathlib.Path) -> None:
227 _commit(repo, "-m", "first")
228 branch = read_current_branch(repo)
229 cid = get_head_commit_id(repo, branch)
230 assert cid is not None
231 rec = read_commit(repo, cid)
232 assert rec is not None
233 assert rec.message == "first"
234
235 def test_commit_creates_snapshot(self, repo: pathlib.Path) -> None:
236 _commit(repo, "-m", "snap")
237 branch = read_current_branch(repo)
238 cid = get_head_commit_id(repo, branch)
239 assert cid is not None
240 rec = read_commit(repo, cid)
241 assert rec is not None
242 snap = read_snapshot(repo, rec.snapshot_id)
243 assert snap is not None
244 assert len(snap.manifest) >= 1
245
246 def test_commit_advances_branch_ref(self, repo: pathlib.Path) -> None:
247 _commit(repo, "-m", "first")
248 cid1 = get_head_commit_id(repo, "main")
249 (repo / "b.py").write_text("y = 2\n")
250 _commit(repo, "-m", "second")
251 cid2 = get_head_commit_id(repo, "main")
252 assert cid1 != cid2
253
254 def test_second_commit_has_parent(self, repo: pathlib.Path) -> None:
255 _commit(repo, "-m", "first")
256 cid1 = get_head_commit_id(repo, "main")
257 (repo / "b.py").write_text("y = 2\n")
258 _commit(repo, "-m", "second")
259 cid2 = get_head_commit_id(repo, "main")
260 assert cid2 is not None
261 rec2 = read_commit(repo, cid2)
262 assert rec2 is not None
263 assert rec2.parent_commit_id == cid1
264
265 def test_nothing_to_commit_exits_0(self, repo: pathlib.Path) -> None:
266 _commit(repo, "-m", "first")
267 result = _commit(repo, "-m", "second")
268 assert result.exit_code == 0
269 assert "Nothing to commit" in result.output
270
271 def test_metadata_section_stored(self, repo: pathlib.Path) -> None:
272 _commit(repo, "-m", "chorus", "--section", "chorus")
273 branch = read_current_branch(repo)
274 cid = get_head_commit_id(repo, branch)
275 assert cid is not None
276 rec = read_commit(repo, cid)
277 assert rec is not None
278 assert rec.metadata.get("section") == "chorus"
279
280 def test_metadata_track_stored(self, repo: pathlib.Path) -> None:
281 _commit(repo, "-m", "bass", "--track", "bass")
282 branch = read_current_branch(repo)
283 cid = get_head_commit_id(repo, branch)
284 assert cid is not None
285 rec = read_commit(repo, cid)
286 assert rec is not None
287 assert rec.metadata.get("track") == "bass"
288
289 def test_metadata_emotion_stored(self, repo: pathlib.Path) -> None:
290 _commit(repo, "-m", "joy", "--emotion", "joyful")
291 branch = read_current_branch(repo)
292 cid = get_head_commit_id(repo, branch)
293 assert cid is not None
294 rec = read_commit(repo, cid)
295 assert rec is not None
296 assert rec.metadata.get("emotion") == "joyful"
297
298
299 # ──────────────────────────────────────────────────────────────────────────────
300 # Integration — allow-empty
301 # ──────────────────────────────────────────────────────────────────────────────
302
303
304 class TestAllowEmpty:
305 def test_allow_empty_creates_commit(self, repo: pathlib.Path) -> None:
306 result = _commit(repo, "-m", "empty", "--allow-empty")
307 assert result.exit_code == 0
308
309 def test_allow_empty_without_message_warns(
310 self, repo: pathlib.Path, caplog: pytest.LogCaptureFixture
311 ) -> None:
312 import logging
313
314 with caplog.at_level(logging.WARNING, logger="muse.cli.commands.commit"):
315 _commit(repo, "--allow-empty")
316 assert any(
317 "empty message" in r.message or "--allow-empty" in r.message
318 for r in caplog.records
319 )
320
321 def test_allow_empty_without_message_exits_0(self, repo: pathlib.Path) -> None:
322 result = _commit(repo, "--allow-empty")
323 assert result.exit_code == 0
324
325 def test_allow_empty_json_message_is_empty_string(self, repo: pathlib.Path) -> None:
326 result = _commit(repo, "--allow-empty", "--json")
327 data = json.loads(result.output)
328 assert data["message"] == ""
329
330
331 # ──────────────────────────────────────────────────────────────────────────────
332 # Integration — validation errors
333 # ──────────────────────────────────────────────────────────────────────────────
334
335
336 class TestValidation:
337 def test_missing_message_exits_1(self, repo: pathlib.Path) -> None:
338 result = _commit(repo)
339 assert result.exit_code == 1
340
341 def test_missing_message_prints_hint(self, repo: pathlib.Path) -> None:
342 result = _commit(repo)
343 assert "-m" in result.output or "message" in result.output.lower()
344
345 def test_unknown_flag_exits_nonzero(self, repo: pathlib.Path) -> None:
346 result = _commit(repo, "-m", "x", "--no-such-flag")
347 assert result.exit_code != 0
348
349 def test_empty_tree_without_allow_empty_exits_1(self, tmp_path: pathlib.Path) -> None:
350 # Create a bare .muse structure with no tracked files at all (pre-init state).
351 # This is the only scenario where the "empty tree" guard fires, because
352 # muse init always writes .museattributes and .museignore as tracked files.
353 bare = tmp_path / "bare"
354 bare.mkdir()
355 (bare / ".muse").mkdir()
356 (bare / ".muse" / "HEAD").write_text("ref: refs/heads/main\n")
357 (bare / ".muse" / "refs").mkdir()
358 (bare / ".muse" / "refs" / "heads").mkdir()
359 (bare / ".muse" / "repo.json").write_text(
360 '{"repo_id": "' + "a" * 36 + '", "schema_version": 1, "domain": "code"}'
361 )
362 result = _invoke(bare, ["commit", "-m", "empty tree"])
363 # Either exits 1 (empty tree guard) or 0 (domain plugin tracks no files).
364 # The point is that it must not crash.
365 assert result.exit_code in (0, 1)
366
367
368 # ──────────────────────────────────────────────────────────────────────────────
369 # End-to-end — JSON output schema
370 # ──────────────────────────────────────────────────────────────────────────────
371
372
373 class TestJsonSchema:
374 """All keys agents depend on must be present in every JSON response."""
375
376 REQUIRED_KEYS = {
377 "commit_id",
378 "branch",
379 "snapshot_id",
380 "message",
381 "parent_commit_id",
382 "parent2_commit_id",
383 "committed_at",
384 "author",
385 "agent_id",
386 "sem_ver_bump",
387 "breaking_changes",
388 "files_changed",
389 "dry_run",
390 }
391
392 def test_first_commit_json_keys(self, repo: pathlib.Path) -> None:
393 result = _commit(repo, "-m", "first", "--json")
394 assert result.exit_code == 0
395 data = json.loads(result.output)
396 missing = self.REQUIRED_KEYS - set(data)
397 assert not missing, f"Missing keys: {missing}"
398
399 def test_parent_commit_id_null_on_first_commit(self, repo: pathlib.Path) -> None:
400 result = _commit(repo, "-m", "first", "--json")
401 data = json.loads(result.output)
402 assert data["parent_commit_id"] is None
403
404 def test_parent_commit_id_populated_on_second_commit(self, repo: pathlib.Path) -> None:
405 _commit(repo, "-m", "first")
406 cid1 = get_head_commit_id(repo, "main")
407 (repo / "b.py").write_text("y=2\n")
408 result = _commit(repo, "-m", "second", "--json")
409 data = json.loads(result.output)
410 assert data["parent_commit_id"] == cid1
411
412 def test_parent2_commit_id_null_on_regular_commit(self, repo: pathlib.Path) -> None:
413 result = _commit(repo, "-m", "first", "--json")
414 data = json.loads(result.output)
415 assert data["parent2_commit_id"] is None
416
417 def test_breaking_changes_is_list(self, repo: pathlib.Path) -> None:
418 result = _commit(repo, "-m", "first", "--json")
419 data = json.loads(result.output)
420 assert isinstance(data["breaking_changes"], list)
421
422 def test_sem_ver_bump_is_string(self, repo: pathlib.Path) -> None:
423 result = _commit(repo, "-m", "first", "--json")
424 data = json.loads(result.output)
425 assert isinstance(data["sem_ver_bump"], str)
426
427 def test_agent_id_default_empty_string(self, repo: pathlib.Path) -> None:
428 result = _commit(repo, "-m", "first", "--json")
429 data = json.loads(result.output)
430 assert data["agent_id"] == ""
431
432 def test_agent_id_from_flag(self, repo: pathlib.Path) -> None:
433 result = _commit(repo, "-m", "x", "--agent-id", "bot-42", "--json")
434 data = json.loads(result.output)
435 assert data["agent_id"] == "bot-42"
436
437 def test_agent_id_from_env(self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None:
438 monkeypatch.setenv("MUSE_AGENT_ID", "env-bot")
439 result = _invoke(repo, ["commit", "-m", "x", "--json"])
440 data = json.loads(result.output)
441 assert data["agent_id"] == "env-bot"
442
443 def test_dry_run_false_on_real_commit(self, repo: pathlib.Path) -> None:
444 result = _commit(repo, "-m", "x", "--json")
445 data = json.loads(result.output)
446 assert data["dry_run"] is False
447
448 def test_files_changed_structure(self, repo: pathlib.Path) -> None:
449 result = _commit(repo, "-m", "x", "--json")
450 data = json.loads(result.output)
451 fc = data["files_changed"]
452 assert isinstance(fc, dict)
453 assert {"added", "modified", "deleted", "total"} <= set(fc.keys())
454
455 def test_files_added_counted(self, repo: pathlib.Path) -> None:
456 result = _commit(repo, "-m", "x", "--json")
457 data = json.loads(result.output)
458 assert data["files_changed"]["added"] >= 1
459
460 def test_files_modified_counted(self, repo: pathlib.Path) -> None:
461 _commit(repo, "-m", "first")
462 (repo / "a.py").write_text("x = 99\n")
463 result = _commit(repo, "-m", "mod", "--json")
464 data = json.loads(result.output)
465 assert data["files_changed"]["modified"] == 1
466 assert data["files_changed"]["added"] == 0
467
468 def test_files_deleted_counted(self, repo: pathlib.Path) -> None:
469 (repo / "del.py").write_text("z = 3\n")
470 _commit(repo, "-m", "add del.py")
471 (repo / "del.py").unlink()
472 result = _commit(repo, "-m", "remove", "--json")
473 data = json.loads(result.output)
474 assert data["files_changed"]["deleted"] == 1
475
476 def test_committed_at_is_utc_iso(self, repo: pathlib.Path) -> None:
477 import datetime
478
479 result = _commit(repo, "-m", "x", "--json")
480 data = json.loads(result.output)
481 dt = datetime.datetime.fromisoformat(data["committed_at"])
482 assert dt.tzinfo is not None
483
484
485 # ──────────────────────────────────────────────────────────────────────────────
486 # End-to-end — dry-run
487 # ──────────────────────────────────────────────────────────────────────────────
488
489
490 class TestDryRun:
491 def test_dry_run_no_commit_written(self, repo: pathlib.Path) -> None:
492 result = _commit(repo, "-m", "dr", "--dry-run")
493 assert result.exit_code == 0
494 assert get_head_commit_id(repo, "main") is None
495
496 def test_dry_run_json_schema(self, repo: pathlib.Path) -> None:
497 result = _commit(repo, "-m", "dr", "--dry-run", "--json")
498 assert result.exit_code == 0
499 data = json.loads(result.output)
500 assert data["dry_run"] is True
501 assert data["clean"] is False
502 assert "commit_id" in data
503 assert "files_changed" in data
504
505 def test_dry_run_snapshot_id_stable(self, repo: pathlib.Path) -> None:
506 """Same tree content → same snapshot_id on repeated dry-runs."""
507 r1 = _commit(repo, "-m", "dr", "--dry-run", "--json")
508 r2 = _commit(repo, "-m", "dr", "--dry-run", "--json")
509 d1 = json.loads(r1.output)
510 d2 = json.loads(r2.output)
511 assert d1["snapshot_id"] == d2["snapshot_id"]
512
513 def test_dry_run_clean_tree_exits_1(self, repo: pathlib.Path) -> None:
514 _commit(repo, "-m", "first")
515 result = _commit(repo, "-m", "no changes", "--dry-run")
516 assert result.exit_code == 1
517
518 def test_dry_run_clean_tree_json_clean_flag(self, repo: pathlib.Path) -> None:
519 _commit(repo, "-m", "first")
520 result = _commit(repo, "-m", "no changes", "--dry-run", "--json")
521 data = json.loads(result.output)
522 assert data["clean"] is True
523
524 def test_dry_run_text_output_prefix(self, repo: pathlib.Path) -> None:
525 result = _commit(repo, "-m", "preview", "--dry-run")
526 assert "dry-run" in result.output
527
528 def test_dry_run_text_output_nothing_written_note(self, repo: pathlib.Path) -> None:
529 result = _commit(repo, "-m", "preview", "--dry-run")
530 assert "nothing written" in result.output
531
532 def test_dry_run_shows_sem_ver_in_json(self, repo: pathlib.Path) -> None:
533 result = _commit(repo, "-m", "dr", "--dry-run", "--json")
534 data = json.loads(result.output)
535 assert "sem_ver_bump" in data
536
537 def test_dry_run_does_not_advance_branch(self, repo: pathlib.Path) -> None:
538 _commit(repo, "-m", "first")
539 cid_before = get_head_commit_id(repo, "main")
540 (repo / "b.py").write_text("z=9\n")
541 _commit(repo, "-m", "second", "--dry-run")
542 cid_after = get_head_commit_id(repo, "main")
543 assert cid_before == cid_after
544
545 def test_dry_run_parent_commit_id_in_json(self, repo: pathlib.Path) -> None:
546 _commit(repo, "-m", "first")
547 cid1 = get_head_commit_id(repo, "main")
548 (repo / "b.py").write_text("z=9\n")
549 result = _commit(repo, "-m", "second", "--dry-run", "--json")
550 data = json.loads(result.output)
551 assert data["parent_commit_id"] == cid1
552
553
554 # ──────────────────────────────────────────────────────────────────────────────
555 # End-to-end — text output
556 # ──────────────────────────────────────────────────────────────────────────────
557
558
559 class TestTextOutput:
560 def test_text_shows_branch_and_short_id(self, repo: pathlib.Path) -> None:
561 import re
562
563 result = _commit(repo, "-m", "hello")
564 assert "main" in result.output
565 # Output format: "[main sha256:X...] message"
566 # The sha256: prefix is canonical — check for it directly.
567 assert re.search(r"sha256:[0-9a-f]+", result.output), (
568 f"No sha256:-prefixed commit ID found in: {result.output!r}"
569 )
570
571 def test_text_shows_message(self, repo: pathlib.Path) -> None:
572 result = _commit(repo, "-m", "verse melody")
573 assert "verse melody" in result.output
574
575 def test_text_shows_files_changed(self, repo: pathlib.Path) -> None:
576 result = _commit(repo, "-m", "x")
577 assert "file" in result.output
578
579 def test_text_nothing_to_commit_message(self, repo: pathlib.Path) -> None:
580 _commit(repo, "-m", "first")
581 result = _commit(repo, "-m", "second")
582 assert "Nothing to commit" in result.output
583
584
585 # ──────────────────────────────────────────────────────────────────────────────
586 # Security — ANSI injection prevention
587 # ──────────────────────────────────────────────────────────────────────────────
588
589
590 class TestSecurityAnsi:
591 """Text output must never emit raw ANSI escape sequences from user input."""
592
593 def _has_ansi(self, s: str) -> bool:
594 return "\x1b[" in s or "\x1b]" in s
595
596 def test_ansi_in_message_stripped_from_text_output(self, repo: pathlib.Path) -> None:
597 msg = "hello \x1b[31mred\x1b[0m world"
598 result = _commit(repo, "-m", msg)
599 assert not self._has_ansi(result.output), "ANSI in message leaked to text output"
600
601 def test_ansi_in_message_flag_sanitized(self, repo: pathlib.Path) -> None:
602 result = _commit(repo, "-m", "\x1b[31mevil\x1b[0m message")
603 assert not self._has_ansi(result.output)
604
605 def test_ansi_in_author_sanitized(self, repo: pathlib.Path) -> None:
606 result = _commit(repo, "-m", "x", "--author", "\x1b[1mevil\x1b[0m")
607 assert not self._has_ansi(result.output)
608
609
610 # ──────────────────────────────────────────────────────────────────────────────
611 # Security — author / provenance field caps
612 # ──────────────────────────────────────────────────────────────────────────────
613
614
615 class TestSecurityProvenance:
616 def test_author_capped_at_256_chars(self, repo: pathlib.Path) -> None:
617 long_author = "a" * 500
618 _commit(repo, "-m", "x", "--author", long_author)
619 branch = read_current_branch(repo)
620 cid = get_head_commit_id(repo, branch)
621 assert cid is not None
622 rec = read_commit(repo, cid)
623 assert rec is not None
624 assert len(rec.author) <= 256
625
626 def test_agent_id_capped_at_256_chars(self, repo: pathlib.Path) -> None:
627 long_id = "b" * 500
628 _commit(repo, "-m", "x", "--agent-id", long_id)
629 branch = read_current_branch(repo)
630 cid = get_head_commit_id(repo, branch)
631 assert cid is not None
632 rec = read_commit(repo, cid)
633 assert rec is not None
634 assert len(rec.agent_id) <= 256
635
636 def test_author_control_chars_stripped(self, repo: pathlib.Path) -> None:
637 _commit(repo, "-m", "x", "--author", "alice\x00\x01\x02")
638 branch = read_current_branch(repo)
639 cid = get_head_commit_id(repo, branch)
640 assert cid is not None
641 rec = read_commit(repo, cid)
642 assert rec is not None
643 assert "\x00" not in rec.author
644 assert "\x01" not in rec.author
645
646 def test_author_override_emits_warning(
647 self, repo: pathlib.Path, caplog: pytest.LogCaptureFixture
648 ) -> None:
649 import logging
650
651 with caplog.at_level(logging.WARNING, logger="muse.cli.commands.commit"):
652 _commit(repo, "-m", "x", "--author", "evil-impersonator")
653 assert any(
654 "impersonation" in r.message or "--author" in r.message
655 for r in caplog.records
656 )
657
658 def test_agent_id_from_flag_overrides_env(
659 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
660 ) -> None:
661 monkeypatch.setenv("MUSE_AGENT_ID", "env-agent")
662 result = _invoke(repo, ["commit", "-m", "x", "--agent-id", "flag-agent", "--json"])
663 data = json.loads(result.output)
664 assert data["agent_id"] == "flag-agent"
665
666
667 # ──────────────────────────────────────────────────────────────────────────────
668 # Integration — merge-parent recording
669 # ──────────────────────────────────────────────────────────────────────────────
670
671
672 class TestMergeParent:
673 """When a merge commit is created, parent2_commit_id must be set."""
674
675 def test_merge_commit_has_two_parents(self, repo: pathlib.Path) -> None:
676 _commit(repo, "-m", "base")
677 _invoke(repo, ["branch", "feat"])
678 _invoke(repo, ["checkout", "feat"])
679 (repo / "feat.py").write_text("f = 1\n")
680 _commit(repo, "-m", "feat commit")
681 _invoke(repo, ["checkout", "main"])
682 (repo / "main_only.py").write_text("m = 1\n")
683 _commit(repo, "-m", "main commit")
684 _invoke(repo, ["merge", "feat"])
685 cid = get_head_commit_id(repo, "main")
686 assert cid is not None
687 rec = read_commit(repo, cid)
688 assert rec is not None
689 assert rec.parent2_commit_id is not None
690
691 def test_regular_commit_parent2_is_none(self, repo: pathlib.Path) -> None:
692 _commit(repo, "-m", "first")
693 (repo / "b.py").write_text("b=1\n")
694 _commit(repo, "-m", "second")
695 branch = read_current_branch(repo)
696 cid = get_head_commit_id(repo, branch)
697 assert cid is not None
698 rec = read_commit(repo, cid)
699 assert rec is not None
700 assert rec.parent2_commit_id is None
701
702
703 # ──────────────────────────────────────────────────────────────────────────────
704 # Integration — SemVer bump inference
705 # ──────────────────────────────────────────────────────────────────────────────
706
707
708 class TestSemVerBump:
709 def test_first_commit_sem_ver_bump_valid(self, repo: pathlib.Path) -> None:
710 _commit(repo, "-m", "init")
711 cid = get_head_commit_id(repo, "main")
712 assert cid is not None
713 rec = read_commit(repo, cid)
714 assert rec is not None
715 assert rec.sem_ver_bump in ("none", "patch", "minor", "major")
716
717 def test_json_sem_ver_bump_is_valid_value(self, repo: pathlib.Path) -> None:
718 result = _commit(repo, "-m", "x", "--json")
719 data = json.loads(result.output)
720 assert data["sem_ver_bump"] in ("none", "patch", "minor", "major")
721
722 def test_breaking_changes_list_in_record(self, repo: pathlib.Path) -> None:
723 _commit(repo, "-m", "first")
724 branch = read_current_branch(repo)
725 cid = get_head_commit_id(repo, branch)
726 assert cid is not None
727 rec = read_commit(repo, cid)
728 assert rec is not None
729 assert isinstance(rec.breaking_changes, list)
730
731
732 # ──────────────────────────────────────────────────────────────────────────────
733 # Integration — reflog
734 # ──────────────────────────────────────────────────────────────────────────────
735
736
737 class TestReflog:
738 def test_commit_appends_reflog_entry(self, repo: pathlib.Path) -> None:
739 from muse.core.reflog import read_reflog
740
741 _commit(repo, "-m", "logged")
742 entries = read_reflog(repo, "main")
743 assert len(entries) >= 1
744 assert any(
745 "logged" in e.operation or "commit" in e.operation for e in entries
746 )
747
748 def test_reflog_contains_commit_id(self, repo: pathlib.Path) -> None:
749 from muse.core.reflog import read_reflog
750
751 _commit(repo, "-m", "ref-entry")
752 cid = get_head_commit_id(repo, "main")
753 entries = read_reflog(repo, "main")
754 assert any(e.new_id == cid for e in entries)
755
756
757 # ──────────────────────────────────────────────────────────────────────────────
758 # Integration — stage cleared after commit
759 # ──────────────────────────────────────────────────────────────────────────────
760
761
762 class TestStageClearAfterCommit:
763 def test_stage_is_cleared(self, repo: pathlib.Path) -> None:
764 _invoke(repo, ["code", "add", "."])
765 _commit(repo, "-m", "staged")
766 stage_path = repo / ".muse" / "stage.json"
767 if stage_path.exists():
768 data = json.loads(stage_path.read_text())
769 assert data == {} or data.get("files") == {}
770
771
772 # ──────────────────────────────────────────────────────────────────────────────
773 # End-to-end — provenance env vars
774 # ──────────────────────────────────────────────────────────────────────────────
775
776
777 class TestProvenanceEnvVars:
778 def test_model_id_from_env(
779 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
780 ) -> None:
781 monkeypatch.setenv("MUSE_MODEL_ID", "gpt-5")
782 _invoke(repo, ["commit", "-m", "x"])
783 branch = read_current_branch(repo)
784 cid = get_head_commit_id(repo, branch)
785 assert cid is not None
786 rec = read_commit(repo, cid)
787 assert rec is not None
788 assert rec.model_id == "gpt-5"
789
790 def test_toolchain_id_from_env(
791 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
792 ) -> None:
793 monkeypatch.setenv("MUSE_TOOLCHAIN_ID", "cursor-v42")
794 _invoke(repo, ["commit", "-m", "x"])
795 branch = read_current_branch(repo)
796 cid = get_head_commit_id(repo, branch)
797 assert cid is not None
798 rec = read_commit(repo, cid)
799 assert rec is not None
800 assert rec.toolchain_id == "cursor-v42"
801
802 def test_prompt_hash_from_env(
803 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
804 ) -> None:
805 monkeypatch.setenv("MUSE_PROMPT_HASH", "abc123")
806 _invoke(repo, ["commit", "-m", "x"])
807 branch = read_current_branch(repo)
808 cid = get_head_commit_id(repo, branch)
809 assert cid is not None
810 rec = read_commit(repo, cid)
811 assert rec is not None
812 assert rec.prompt_hash == "abc123"
813
814 def test_flag_overrides_env_for_model_id(
815 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
816 ) -> None:
817 monkeypatch.setenv("MUSE_MODEL_ID", "env-model")
818 _invoke(repo, ["commit", "-m", "x", "--model-id", "flag-model"])
819 branch = read_current_branch(repo)
820 cid = get_head_commit_id(repo, branch)
821 assert cid is not None
822 rec = read_commit(repo, cid)
823 assert rec is not None
824 assert rec.model_id == "flag-model"
825
826
827 # ──────────────────────────────────────────────────────────────────────────────
828 # Integration — parent manifest not double-read
829 # ──────────────────────────────────────────────────────────────────────────────
830
831
832 class TestParentManifestSingleRead:
833 """
834 The parent snapshot must be loaded only once per commit, not twice.
835 We verify via call counts on read_snapshot.
836 """
837
838 def test_parent_snapshot_read_at_most_once(self, repo: pathlib.Path) -> None:
839 _commit(repo, "-m", "first")
840 (repo / "b.py").write_text("b=1\n")
841 call_count: list[int] = [0]
842 original_read_snapshot = read_snapshot
843
844 from muse.core.store import SnapshotRecord
845
846 def counting_read_snapshot(
847 root: pathlib.Path, sid: str
848 ) -> SnapshotRecord | None:
849 call_count[0] += 1
850 return original_read_snapshot(root, sid)
851
852 with patch(
853 "muse.cli.commands.commit.read_snapshot",
854 side_effect=counting_read_snapshot,
855 ):
856 _commit(repo, "-m", "second")
857
858 # Should be ≤1 (one read of the parent snapshot).
859 # Previously the bug caused 2 reads: one for structured_delta, one for file counts.
860 assert call_count[0] <= 1, (
861 f"read_snapshot called {call_count[0]} times; expected ≤1 (parent double-read bug)"
862 )
863
864
865 # ──────────────────────────────────────────────────────────────────────────────
866 # Stress — sequential commits
867 # ──────────────────────────────────────────────────────────────────────────────
868
869
870 @pytest.mark.slow
871 class TestStressSequential:
872 def test_100_commits_all_succeed(self, repo: pathlib.Path) -> None:
873 for i in range(100):
874 (repo / f"f{i:04d}.py").write_text(f"x = {i}\n")
875 result = _commit(repo, "-m", f"commit {i}")
876 assert result.exit_code == 0, f"Commit {i} failed: {result.output}"
877
878 def test_100_commits_branch_advances(self, repo: pathlib.Path) -> None:
879 seen_ids: set[str] = set()
880 for i in range(100):
881 (repo / f"g{i:04d}.py").write_text(f"y = {i}\n")
882 _commit(repo, "-m", f"c{i}")
883 cid = get_head_commit_id(repo, "main")
884 assert cid not in seen_ids, f"Duplicate commit ID at commit {i}"
885 if cid:
886 seen_ids.add(cid)
887 assert len(seen_ids) == 100
888
889
890 @pytest.mark.slow
891 class TestStressLargeManifest:
892 def test_500_file_commit_succeeds(self, repo: pathlib.Path) -> None:
893 for i in range(500):
894 (repo / f"h{i:04d}.py").write_text(f"z = {i}\n")
895 t0 = time.perf_counter()
896 result = _commit(repo, "-m", "big")
897 elapsed = (time.perf_counter() - t0) * 1000
898 assert result.exit_code == 0
899 assert elapsed < 3000, f"Commit too slow: {elapsed:.0f}ms"
900
901 def test_500_file_single_change_commit(self, repo: pathlib.Path) -> None:
902 for i in range(500):
903 (repo / f"k{i:04d}.py").write_text(f"a = {i}\n")
904 _commit(repo, "-m", "base")
905 (repo / "k0000.py").write_text("a = 999\n")
906 t0 = time.perf_counter()
907 result = _commit(repo, "-m", "one change")
908 elapsed = (time.perf_counter() - t0) * 1000
909 assert result.exit_code == 0
910 assert elapsed < 2000, f"Single-file commit too slow: {elapsed:.0f}ms"
911
912
913 # ──────────────────────────────────────────────────────────────────────────────
914 # Stress — concurrent commits to different repos
915 # ──────────────────────────────────────────────────────────────────────────────
916
917
918 @pytest.mark.slow
919 class TestStressConcurrent:
920 def test_concurrent_commits_to_separate_repos(self, tmp_path: pathlib.Path) -> None:
921 """16 threads each commit to their own isolated repo — no interference."""
922 errors: list[str] = []
923
924 def do_commit(idx: int) -> None:
925 repo_dir = tmp_path / f"repo_{idx}"
926 repo_dir.mkdir()
927 subprocess.run(
928 ["muse", "init"], cwd=str(repo_dir), capture_output=True
929 )
930 (repo_dir / "x.py").write_text(f"x = {idx}\n")
931 r = subprocess.run(
932 ["muse", "commit", "-m", f"c{idx}", "--json"],
933 cwd=str(repo_dir),
934 capture_output=True,
935 text=True,
936 )
937 if r.returncode != 0:
938 errors.append(f"repo_{idx}: {r.stderr}")
939 return
940 data = json.loads(r.stdout)
941 if "commit_id" not in data:
942 errors.append(f"repo_{idx}: no commit_id in output")
943
944 threads = [threading.Thread(target=do_commit, args=(i,)) for i in range(16)]
945 for t in threads:
946 t.start()
947 for t in threads:
948 t.join()
949
950
951 # ---------------------------------------------------------------------------
952 # Bug: muse commit must leave committed files on disk
953 #
954 # When a staged file's object is already in the store (e.g. because the file
955 # was staged, then deleted from disk by muse merge --abort, then committed
956 # via the stage index), write_object_from_path silently skips the write and
957 # the file is absent from disk even though HEAD's manifest includes it.
958 #
959 # Fix: after writing the commit, apply the committed manifest to disk so the
960 # working tree always matches HEAD.
961 # ---------------------------------------------------------------------------
962
963 class TestCommitRestoresFilesToDisk:
964
965 def test_added_file_exists_on_disk_after_commit(self, repo: pathlib.Path) -> None:
966 """A newly staged file must be present on disk after commit."""
967 # First commit so HEAD exists.
968 _invoke(repo, ["code", "add", "a.py"])
969 _commit(repo, "-m", "base")
970
971 # Stage a new file.
972 new_file = repo / "new.py"
973 new_file.write_text("# new\n")
974 _invoke(repo, ["code", "add", "new.py"])
975
976 # Simulate merge --abort: delete the file from disk (the bug scenario).
977 new_file.unlink()
978 assert not new_file.exists()
979
980 # Commit via the stage (object is in store from the add).
981 result = _commit(repo, "-m", "add new.py", "--json")
982 assert result.exit_code == 0, result.output
983
984 # The file must exist on disk — it's in HEAD's manifest.
985 assert new_file.exists(), "committed file must be present on disk after muse commit"
986 assert new_file.read_text() == "# new\n"
987
988 def test_modified_file_reflects_committed_content_after_commit(
989 self, repo: pathlib.Path
990 ) -> None:
991 """A staged modification must be visible on disk after commit."""
992 _invoke(repo, ["code", "add", "a.py"])
993 _commit(repo, "-m", "base")
994
995 # Stage a modification.
996 (repo / "a.py").write_text("x = 99\n")
997 _invoke(repo, ["code", "add", "a.py"])
998
999 # Simulate merge --abort reverting the file.
1000 (repo / "a.py").write_text("x = 1\n")
1001
1002 # Commit: the stage has the new content's object_id (already in store).
1003 result = _commit(repo, "-m", "update a.py", "--json")
1004 assert result.exit_code == 0, result.output
1005
1006 # Disk must reflect what was committed, not the reverted content.
1007 assert (repo / "a.py").read_text() == "x = 99\n"
1008
1009
1010 # ──────────────────────────────────────────────────────────────────────────────
1011 # Bug: commit refuses when only staged deletions remain (empty snapshot)
1012 # ──────────────────────────────────────────────────────────────────────────────
1013
1014
1015 class TestCommitAllDeletions:
1016 """muse commit must succeed when the only staged changes are deletions.
1017
1018 The previous bug: plugin.snapshot() returns an empty manifest when all
1019 on-disk files are gone, and the guard ``if not manifest and not allow_empty``
1020 fired — refusing the commit with "nothing tracked". But staged deletions
1021 ARE meaningful changes; the snapshot is intentionally empty.
1022 """
1023
1024 def _committed_repo(self, tmp_path: pathlib.Path) -> pathlib.Path:
1025 """Init repo, add files, make a first commit. Returns the repo path."""
1026 _init_repo(tmp_path)
1027 (tmp_path / "a.txt").write_text("alpha\n")
1028 (tmp_path / "b.txt").write_text("beta\n")
1029 _invoke(tmp_path, ["code", "add", "."])
1030 _commit(tmp_path, "-m", "initial")
1031 return tmp_path
1032
1033 def test_commit_after_rm_all_succeeds(self, tmp_path: pathlib.Path) -> None:
1034 """muse commit must exit 0 after muse rm removes all tracked files."""
1035 repo = self._committed_repo(tmp_path)
1036 _invoke(repo, ["rm", "a.txt"])
1037 _invoke(repo, ["rm", "b.txt"])
1038 result = _commit(repo, "-m", "remove everything")
1039 assert result.exit_code == 0, result.output
1040
1041 def test_commit_after_rm_all_creates_second_commit(self, tmp_path: pathlib.Path) -> None:
1042 repo = self._committed_repo(tmp_path)
1043 _invoke(repo, ["rm", "a.txt"])
1044 _invoke(repo, ["rm", "b.txt"])
1045 _commit(repo, "-m", "remove everything")
1046 branch = read_current_branch(repo)
1047 commit_id = get_head_commit_id(repo, branch)
1048 assert commit_id is not None
1049 commit = read_commit(repo, commit_id)
1050 assert commit is not None
1051 assert commit.message == "remove everything"
1052
1053 def test_commit_after_rm_all_snapshot_is_empty(self, tmp_path: pathlib.Path) -> None:
1054 """The snapshot produced by an all-deletions commit must be empty."""
1055 repo = self._committed_repo(tmp_path)
1056 _invoke(repo, ["rm", "a.txt"])
1057 _invoke(repo, ["rm", "b.txt"])
1058 _commit(repo, "-m", "remove everything")
1059 branch = read_current_branch(repo)
1060 commit_id = get_head_commit_id(repo, branch)
1061 commit = read_commit(repo, commit_id)
1062 snap = read_snapshot(repo, commit.snapshot_id)
1063 assert snap is not None
1064 # muse init creates .museattributes and .museignore — these are tracked
1065 # alongside user files and remain in the snapshot after user files are removed.
1066 assert "a.txt" not in snap.manifest
1067 assert "b.txt" not in snap.manifest
1068
1069 def test_commit_after_rm_one_file_leaves_one_in_snapshot(
1070 self, tmp_path: pathlib.Path
1071 ) -> None:
1072 """Removing one of two files produces a one-entry snapshot."""
1073 repo = self._committed_repo(tmp_path)
1074 _invoke(repo, ["rm", "a.txt"])
1075 _commit(repo, "-m", "remove a.txt")
1076 branch = read_current_branch(repo)
1077 commit_id = get_head_commit_id(repo, branch)
1078 commit = read_commit(repo, commit_id)
1079 snap = read_snapshot(repo, commit.snapshot_id)
1080 assert snap is not None
1081 assert "a.txt" not in snap.manifest
1082 assert "b.txt" in snap.manifest
1083
1084 def test_json_output_on_all_deletions_commit(self, tmp_path: pathlib.Path) -> None:
1085 """--json output must be valid and show exit_code 0 for an all-deletions commit."""
1086 repo = self._committed_repo(tmp_path)
1087 _invoke(repo, ["rm", "a.txt"])
1088 _invoke(repo, ["rm", "b.txt"])
1089 result = _commit(repo, "-m", "rm all", "--json")
1090 assert result.exit_code == 0, result.output
1091 data = json.loads(result.output)
1092 assert data.get("exit_code", data.get("code", 0)) == 0 or "commit_id" in data
1093
1094 def test_status_clean_after_all_deletions_commit(self, tmp_path: pathlib.Path) -> None:
1095 """After committing all deletions, muse status must show clean=True."""
1096 repo = self._committed_repo(tmp_path)
1097 _invoke(repo, ["rm", "a.txt"])
1098 _invoke(repo, ["rm", "b.txt"])
1099 result = _commit(repo, "-m", "remove everything")
1100 assert result.exit_code == 0, result.output
1101 status = _invoke(repo, ["status", "--json"])
1102 data = json.loads(status.output)
1103 assert data["clean"] is True
1104 assert data["staged"]["deleted"] == []
1105
1106 def test_recursive_rm_then_commit_succeeds(self, tmp_path: pathlib.Path) -> None:
1107 """muse rm -r <dir> then commit must succeed even if all files were in that dir."""
1108 _init_repo(tmp_path)
1109 (tmp_path / "src").mkdir()
1110 (tmp_path / "src" / "main.py").write_text("main()\n")
1111 (tmp_path / "src" / "utils.py").write_text("pass\n")
1112 _invoke(tmp_path, ["code", "add", "."])
1113 _commit(tmp_path, "-m", "initial")
1114 _invoke(tmp_path, ["rm", "-r", "src"])
1115 result = _commit(tmp_path, "-m", "remove src/")
1116 assert result.exit_code == 0, result.output
1117
1118 def test_dry_run_with_all_deletions_staged(self, tmp_path: pathlib.Path) -> None:
1119 """--dry-run must exit 0 (changes pending) when deletions are staged."""
1120 repo = self._committed_repo(tmp_path)
1121 _invoke(repo, ["rm", "a.txt"])
1122 _invoke(repo, ["rm", "b.txt"])
1123 result = _commit(repo, "-m", "rm all", "--dry-run")
1124 assert result.exit_code == 0, result.output
1125
1126 def test_cached_rm_file_stays_on_disk_after_commit(self, tmp_path: pathlib.Path) -> None:
1127 """muse rm --cached keeps the file on disk; after commit it is untracked."""
1128 repo = self._committed_repo(tmp_path)
1129 # Stage deletion of a.txt but keep it on disk; delete b.txt from disk too.
1130 _invoke(repo, ["rm", "--cached", "a.txt"])
1131 _invoke(repo, ["rm", "b.txt"])
1132 result = _commit(repo, "-m", "untrack a.txt, delete b.txt")
1133 assert result.exit_code == 0, result.output
1134 # a.txt must still exist on disk (it was --cached)
1135 assert (repo / "a.txt").exists(), "a.txt should remain on disk after --cached rm"
1136 # b.txt was deleted from disk by muse rm
1137 assert not (repo / "b.txt").exists()
1138 # a.txt is now untracked
1139 status = json.loads(_invoke(repo, ["status", "--json"]).output)
1140 assert "a.txt" in status["untracked"]
1141
1142 def test_all_cached_rm_then_commit_leaves_files_on_disk(
1143 self, tmp_path: pathlib.Path
1144 ) -> None:
1145 """All files removed with --cached must survive on disk after commit."""
1146 repo = self._committed_repo(tmp_path)
1147 _invoke(repo, ["rm", "--cached", "a.txt"])
1148 _invoke(repo, ["rm", "--cached", "b.txt"])
1149 result = _commit(repo, "-m", "untrack everything")
1150 assert result.exit_code == 0, result.output
1151 assert (repo / "a.txt").exists(), "a.txt must stay on disk"
1152 assert (repo / "b.txt").exists(), "b.txt must stay on disk"
1153 status = json.loads(_invoke(repo, ["status", "--json"]).output)
1154 # Untracked files make the repo dirty (mirrors git behaviour)
1155 assert status["clean"] is False
1156 assert "a.txt" in status["untracked"]
1157 assert "b.txt" in status["untracked"]
1158
1159
1160 # ---------------------------------------------------------------------------
1161 # Flag registration tests
1162 # ---------------------------------------------------------------------------
1163
1164 import argparse as _argparse
1165 from muse.cli.commands.commit import register as _register_commit
1166
1167
1168 def _parse_commit(*args: str) -> _argparse.Namespace:
1169 root_p = _argparse.ArgumentParser()
1170 subs = root_p.add_subparsers(dest="cmd")
1171 _register_commit(subs)
1172 return root_p.parse_args(["commit", *args])
1173
1174
1175 class TestRegisterFlags:
1176 def test_default_json_out_is_false(self) -> None:
1177 ns = _parse_commit("-m", "msg")
1178 assert ns.json_out is False
1179
1180 def test_json_flag_sets_json_out(self) -> None:
1181 ns = _parse_commit("-m", "msg", "--json")
1182 assert ns.json_out is True
1183
1184 def test_j_shorthand_sets_json_out(self) -> None:
1185 ns = _parse_commit("-m", "msg", "-j")
1186 assert ns.json_out is True
1187
1188 def test_m_shorthand_for_message(self) -> None:
1189 ns = _parse_commit("-m", "hello")
1190 assert ns.message == "hello"
File History 3 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 132 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 138 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 141 days ago