gabriel / muse public
test_cmd_commit.py python
173 lines 6.8 KB
86000da9 fix: replace typer CliRunner with argparse-compatible test helper Gabriel Cardona <gabriel@tellurstori.com> 1d 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 tests.cli_test_helper import CliRunner
21
22 cli = None # argparse migration — CliRunner ignores this arg
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