gabriel / muse public
test_cmd_commit.py python
173 lines 6.8 KB
95b86799 feat: add --format json to all porcelain commands for agent-first output Gabriel Cardona <gabriel@tellurstori.com> 2d ago
1 """Comprehensive tests for ``muse commit``.
2
3 Covers:
4 - Unit: snapshot creation, commit record written
5 - Integration: commit updates HEAD ref, snapshot manifest reflects files
6 - E2E: CLI flags (--message / -m, --author, --format json)
7 - Security: sanitized output for conflict paths
8 - Stress: many sequential commits on one branch
9 """
10
11 from __future__ import annotations
12
13 import datetime
14 import hashlib
15 import json
16 import pathlib
17 import uuid
18
19 import pytest
20 from typer.testing import CliRunner
21
22 from muse.cli.app import cli
23
24 runner = CliRunner()
25
26
27 # ---------------------------------------------------------------------------
28 # Shared helpers
29 # ---------------------------------------------------------------------------
30
31 def _env(root: pathlib.Path) -> dict[str, str]:
32 return {"MUSE_REPO_ROOT": str(root)}
33
34
35 def _init_repo(tmp_path: pathlib.Path) -> tuple[pathlib.Path, str]:
36 muse_dir = tmp_path / ".muse"
37 muse_dir.mkdir()
38 repo_id = str(uuid.uuid4())
39 (muse_dir / "repo.json").write_text(json.dumps({
40 "repo_id": repo_id,
41 "domain": "midi",
42 "default_branch": "main",
43 "created_at": "2025-01-01T00:00:00+00:00",
44 }), encoding="utf-8")
45 (muse_dir / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
46 (muse_dir / "refs" / "heads").mkdir(parents=True)
47 (muse_dir / "snapshots").mkdir()
48 (muse_dir / "commits").mkdir()
49 (muse_dir / "objects").mkdir()
50 return tmp_path, repo_id
51
52
53 def _add_file(root: pathlib.Path, filename: str, content: bytes) -> str:
54 """Write a file to the workspace and return its SHA-256."""
55 (root / filename).write_bytes(content)
56 return hashlib.sha256(content).hexdigest()
57
58
59 # ---------------------------------------------------------------------------
60 # E2E CLI tests
61 # ---------------------------------------------------------------------------
62
63 class TestCommitCLI:
64 def test_commit_creates_commit_record(self, tmp_path: pathlib.Path) -> None:
65 root, _ = _init_repo(tmp_path)
66 _add_file(root, "song.mid", b"MIDI data")
67 result = runner.invoke(
68 cli, ["commit", "-m", "first commit"], env=_env(root), catch_exceptions=False
69 )
70 assert result.exit_code == 0
71 commits_dir = root / ".muse" / "commits"
72 assert any(commits_dir.iterdir())
73
74 def test_commit_short_message_flag(self, tmp_path: pathlib.Path) -> None:
75 root, _ = _init_repo(tmp_path)
76 _add_file(root, "song.mid", b"MIDI data 2")
77 result = runner.invoke(
78 cli, ["commit", "-m", "short flag"], env=_env(root), catch_exceptions=False
79 )
80 assert result.exit_code == 0
81
82 def test_commit_long_message_flag(self, tmp_path: pathlib.Path) -> None:
83 root, _ = _init_repo(tmp_path)
84 _add_file(root, "song.mid", b"MIDI data 3")
85 result = runner.invoke(
86 cli, ["commit", "--message", "long flag"], env=_env(root), catch_exceptions=False
87 )
88 assert result.exit_code == 0
89
90 def test_commit_updates_head(self, tmp_path: pathlib.Path) -> None:
91 root, _ = _init_repo(tmp_path)
92 _add_file(root, "track.mid", b"data")
93 runner.invoke(cli, ["commit", "-m", "first"], env=_env(root), catch_exceptions=False)
94 ref_file = root / ".muse" / "refs" / "heads" / "main"
95 assert ref_file.exists()
96 commit_id = ref_file.read_text().strip()
97 assert len(commit_id) == 64
98
99 def test_commit_message_in_record(self, tmp_path: pathlib.Path) -> None:
100 root, _ = _init_repo(tmp_path)
101 _add_file(root, "msg_test.mid", b"message track data")
102 runner.invoke(cli, ["commit", "-m", "my message"], env=_env(root), catch_exceptions=False)
103 from muse.core.store import get_all_commits
104 commits = get_all_commits(root)
105 assert any("my message" in c.message for c in commits)
106
107 def test_commit_output_contains_hash(self, tmp_path: pathlib.Path) -> None:
108 import re
109 root, _ = _init_repo(tmp_path)
110 _add_file(root, "hash_test.mid", b"hash test MIDI data")
111 result = runner.invoke(
112 cli, ["commit", "-m", "hash check"], env=_env(root), catch_exceptions=False
113 )
114 assert result.exit_code == 0
115 assert re.search(r"[0-9a-f]{6,}", result.output)
116
117 def test_commit_with_author(self, tmp_path: pathlib.Path) -> None:
118 root, _ = _init_repo(tmp_path)
119 _add_file(root, "authored.mid", b"authored MIDI")
120 result = runner.invoke(
121 cli, ["commit", "-m", "authored", "--author", "Alice"],
122 env=_env(root), catch_exceptions=False
123 )
124 assert result.exit_code == 0
125
126 def test_commit_allow_empty_flag(self, tmp_path: pathlib.Path) -> None:
127 """Committing with --allow-empty should succeed even with no tracked files."""
128 root, _ = _init_repo(tmp_path)
129 result = runner.invoke(
130 cli, ["commit", "-m", "empty", "--allow-empty"], env=_env(root), catch_exceptions=False
131 )
132 assert result.exit_code == 0
133
134 def test_commit_no_files_exits_user_error(self, tmp_path: pathlib.Path) -> None:
135 """Committing with no tracked files (no --allow-empty) should fail."""
136 root, _ = _init_repo(tmp_path)
137 result = runner.invoke(cli, ["commit", "-m", "empty"], env=_env(root))
138 assert result.exit_code != 0
139
140 def test_second_commit_has_parent(self, tmp_path: pathlib.Path) -> None:
141 root, _ = _init_repo(tmp_path)
142 _add_file(root, "track1.mid", b"first track")
143 runner.invoke(cli, ["commit", "-m", "first"], env=_env(root), catch_exceptions=False)
144 _add_file(root, "track2.mid", b"second track")
145 runner.invoke(cli, ["commit", "-m", "second"], env=_env(root), catch_exceptions=False)
146 from muse.core.store import get_all_commits
147 commits = get_all_commits(root)
148 assert len(commits) == 2
149 # At least one commit should have a parent
150 assert any(c.parent_commit_id is not None for c in commits)
151
152
153 class TestCommitStress:
154 def test_many_sequential_commits(self, tmp_path: pathlib.Path) -> None:
155 root, _ = _init_repo(tmp_path)
156 for i in range(25):
157 _add_file(root, f"track_{i:03d}.mid", f"unique data {i} xyz".encode())
158 result = runner.invoke(
159 cli, ["commit", "-m", f"commit {i}"], env=_env(root), catch_exceptions=False
160 )
161 assert result.exit_code == 0
162 from muse.core.store import get_all_commits
163 commits = get_all_commits(root)
164 assert len(commits) == 25
165
166 def test_commit_with_many_files(self, tmp_path: pathlib.Path) -> None:
167 root, _ = _init_repo(tmp_path)
168 for i in range(50):
169 _add_file(root, f"track_{i:03d}.mid", f"data {i}".encode())
170 result = runner.invoke(
171 cli, ["commit", "-m", "many files"], env=_env(root), catch_exceptions=False
172 )
173 assert result.exit_code == 0