gabriel / muse public
test_cmd_branch.py python
227 lines 9.2 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 branch``.
2
3 Covers:
4 - Unit: _list_branches helper behaviour
5 - Integration: create, delete, list with committed repo
6 - E2E: full CLI round-trips via CliRunner
7 - Security: invalid branch names rejected, no path traversal
8 - Stress: many branches listed and deleted efficiently
9 """
10
11 from __future__ import annotations
12
13 import json
14 import pathlib
15 import uuid
16
17 import pytest
18 from typer.testing import CliRunner
19
20 from muse.cli.app import cli
21
22 runner = CliRunner()
23
24
25 # ---------------------------------------------------------------------------
26 # Helpers
27 # ---------------------------------------------------------------------------
28
29 def _env(root: pathlib.Path) -> dict[str, str]:
30 return {"MUSE_REPO_ROOT": str(root)}
31
32
33 def _init_repo(tmp_path: pathlib.Path) -> pathlib.Path:
34 muse_dir = tmp_path / ".muse"
35 muse_dir.mkdir()
36 repo_id = str(uuid.uuid4())
37 (muse_dir / "repo.json").write_text(json.dumps({
38 "repo_id": repo_id,
39 "domain": "midi",
40 "default_branch": "main",
41 "created_at": "2025-01-01T00:00:00+00:00",
42 }), encoding="utf-8")
43 (muse_dir / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
44 refs_dir = muse_dir / "refs" / "heads"
45 refs_dir.mkdir(parents=True)
46 (muse_dir / "snapshots").mkdir()
47 (muse_dir / "commits").mkdir()
48 (muse_dir / "objects").mkdir()
49 return tmp_path
50
51
52 def _make_commit(root: pathlib.Path, branch: str = "main", message: str = "init") -> str:
53 import datetime
54 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
55 from muse.core.snapshot import compute_snapshot_id, compute_commit_id
56
57 repo_id = json.loads((root / ".muse" / "repo.json").read_text())["repo_id"]
58 ref_file = root / ".muse" / "refs" / "heads" / branch
59 parent_id = ref_file.read_text().strip() if ref_file.exists() else None
60 manifest: dict[str, str] = {}
61 snap_id = compute_snapshot_id(manifest)
62 committed_at = datetime.datetime.now(datetime.timezone.utc)
63 commit_id = compute_commit_id(
64 parent_ids=[parent_id] if parent_id else [],
65 snapshot_id=snap_id,
66 message=message,
67 committed_at_iso=committed_at.isoformat(),
68 )
69 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
70 write_commit(root, CommitRecord(
71 commit_id=commit_id, repo_id=repo_id, branch=branch,
72 snapshot_id=snap_id, message=message, committed_at=committed_at,
73 parent_commit_id=parent_id,
74 ))
75 ref_file.parent.mkdir(parents=True, exist_ok=True)
76 ref_file.write_text(commit_id, encoding="utf-8")
77 return commit_id
78
79
80 # ---------------------------------------------------------------------------
81 # Unit tests
82 # ---------------------------------------------------------------------------
83
84 class TestBranchUnit:
85 def test_list_branches_empty_repo(self, tmp_path: pathlib.Path) -> None:
86 root = _init_repo(tmp_path)
87 from muse.cli.commands.branch import _list_branches
88 assert _list_branches(root) == []
89
90 def test_list_branches_after_create(self, tmp_path: pathlib.Path) -> None:
91 root = _init_repo(tmp_path)
92 refs_dir = root / ".muse" / "refs" / "heads"
93 (refs_dir / "main").write_text("a" * 64, encoding="utf-8")
94 (refs_dir / "dev").write_text("b" * 64, encoding="utf-8")
95 from muse.cli.commands.branch import _list_branches
96 assert _list_branches(root) == ["dev", "main"]
97
98
99 # ---------------------------------------------------------------------------
100 # Integration tests
101 # ---------------------------------------------------------------------------
102
103 class TestBranchIntegration:
104 def test_create_and_list_branch(self, tmp_path: pathlib.Path) -> None:
105 root = _init_repo(tmp_path)
106 _make_commit(root, "main")
107 result = runner.invoke(cli, ["branch", "feat/test"], env=_env(root), catch_exceptions=False)
108 assert result.exit_code == 0
109 assert "Created branch" in result.output
110
111 result2 = runner.invoke(cli, ["branch"], env=_env(root), catch_exceptions=False)
112 assert "feat/test" in result2.output
113 assert "main" in result2.output
114
115 def test_list_marks_current_branch(self, tmp_path: pathlib.Path) -> None:
116 root = _init_repo(tmp_path)
117 _make_commit(root, "main")
118 result = runner.invoke(cli, ["branch"], env=_env(root), catch_exceptions=False)
119 assert "* main" in result.output
120
121 def test_verbose_shows_commit_sha(self, tmp_path: pathlib.Path) -> None:
122 root = _init_repo(tmp_path)
123 commit_id = _make_commit(root, "main")
124 result = runner.invoke(cli, ["branch", "--verbose"], env=_env(root), catch_exceptions=False)
125 assert commit_id[:8] in result.output
126
127 def test_delete_branch(self, tmp_path: pathlib.Path) -> None:
128 root = _init_repo(tmp_path)
129 _make_commit(root, "main")
130 runner.invoke(cli, ["branch", "to-delete"], env=_env(root), catch_exceptions=False)
131 result = runner.invoke(cli, ["branch", "--delete", "to-delete"], env=_env(root), catch_exceptions=False)
132 assert result.exit_code == 0
133 assert "Deleted" in result.output
134
135 result2 = runner.invoke(cli, ["branch"], env=_env(root), catch_exceptions=False)
136 assert "to-delete" not in result2.output
137
138 def test_delete_current_branch_rejected(self, tmp_path: pathlib.Path) -> None:
139 root = _init_repo(tmp_path)
140 _make_commit(root, "main")
141 result = runner.invoke(cli, ["branch", "--delete", "main"], env=_env(root))
142 assert result.exit_code != 0
143 assert "Cannot delete" in result.output
144
145 def test_duplicate_branch_rejected(self, tmp_path: pathlib.Path) -> None:
146 root = _init_repo(tmp_path)
147 _make_commit(root, "main")
148 runner.invoke(cli, ["branch", "dup"], env=_env(root), catch_exceptions=False)
149 result = runner.invoke(cli, ["branch", "dup"], env=_env(root))
150 assert result.exit_code != 0
151 assert "already exists" in result.output
152
153 def test_short_flag_delete(self, tmp_path: pathlib.Path) -> None:
154 root = _init_repo(tmp_path)
155 _make_commit(root, "main")
156 runner.invoke(cli, ["branch", "shortflag"], env=_env(root), catch_exceptions=False)
157 result = runner.invoke(cli, ["branch", "-d", "shortflag"], env=_env(root), catch_exceptions=False)
158 assert result.exit_code == 0
159
160 def test_short_flag_verbose(self, tmp_path: pathlib.Path) -> None:
161 root = _init_repo(tmp_path)
162 commit_id = _make_commit(root, "main")
163 result = runner.invoke(cli, ["branch", "-v"], env=_env(root), catch_exceptions=False)
164 assert commit_id[:8] in result.output
165
166
167 # ---------------------------------------------------------------------------
168 # Security tests
169 # ---------------------------------------------------------------------------
170
171 class TestBranchSecurity:
172 def test_invalid_branch_name_double_dot(self, tmp_path: pathlib.Path) -> None:
173 root = _init_repo(tmp_path)
174 result = runner.invoke(cli, ["branch", "../evil"], env=_env(root))
175 assert result.exit_code != 0
176
177 def test_invalid_branch_name_slash_prefix(self, tmp_path: pathlib.Path) -> None:
178 root = _init_repo(tmp_path)
179 result = runner.invoke(cli, ["branch", "/etc/passwd"], env=_env(root))
180 assert result.exit_code != 0
181
182 def test_invalid_delete_name(self, tmp_path: pathlib.Path) -> None:
183 root = _init_repo(tmp_path)
184 result = runner.invoke(cli, ["branch", "--delete", "../../../etc"], env=_env(root))
185 assert result.exit_code != 0
186
187 def test_delete_nonexistent_branch(self, tmp_path: pathlib.Path) -> None:
188 root = _init_repo(tmp_path)
189 result = runner.invoke(cli, ["branch", "--delete", "ghost"], env=_env(root))
190 assert result.exit_code != 0
191 assert "not found" in result.output
192
193
194 # ---------------------------------------------------------------------------
195 # Stress tests
196 # ---------------------------------------------------------------------------
197
198 class TestBranchStress:
199 def test_many_branches_list_performance(self, tmp_path: pathlib.Path) -> None:
200 root = _init_repo(tmp_path)
201 _make_commit(root, "main")
202 refs_dir = root / ".muse" / "refs" / "heads"
203 commit_id = (refs_dir / "main").read_text().strip()
204
205 for i in range(100):
206 (refs_dir / f"feat-{i:03d}").write_text(commit_id, encoding="utf-8")
207
208 result = runner.invoke(cli, ["branch"], env=_env(root), catch_exceptions=False)
209 assert result.exit_code == 0
210 assert "feat-000" in result.output
211 assert "feat-099" in result.output
212
213 def test_delete_many_branches(self, tmp_path: pathlib.Path) -> None:
214 root = _init_repo(tmp_path)
215 _make_commit(root, "main")
216 refs_dir = root / ".muse" / "refs" / "heads"
217 commit_id = (refs_dir / "main").read_text().strip()
218
219 for i in range(20):
220 (refs_dir / f"temp-{i}").write_text(commit_id, encoding="utf-8")
221
222 for i in range(20):
223 result = runner.invoke(cli, ["branch", "--delete", f"temp-{i}"], env=_env(root), catch_exceptions=False)
224 assert result.exit_code == 0
225
226 result = runner.invoke(cli, ["branch"], env=_env(root), catch_exceptions=False)
227 assert "temp-" not in result.output