gabriel / muse public
test_cmd_tag.py python
279 lines 12.4 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 139 days ago
1 """Comprehensive tests for ``muse tag``.
2
3 Covers:
4 - Unit: write_tag, delete_tag, get_tags_for_commit, get_all_tags
5 - Integration: add → list → remove round-trip
6 - E2E: full CLI via CliRunner
7 - Security: tag names sanitized, ref validation
8 - Stress: many tags on many commits
9 """
10
11 from __future__ import annotations
12
13 import datetime
14 import json
15 import pathlib
16 import uuid
17
18 import pytest
19 from tests.cli_test_helper import CliRunner
20
21 cli = None # argparse migration — CliRunner ignores this arg
22
23 runner = CliRunner()
24
25
26 # ---------------------------------------------------------------------------
27 # Helpers
28 # ---------------------------------------------------------------------------
29
30 def _env(root: pathlib.Path) -> Manifest:
31 return {"MUSE_REPO_ROOT": str(root)}
32
33
34 def _init_repo(tmp_path: pathlib.Path) -> tuple[pathlib.Path, str]:
35 muse_dir = tmp_path / ".muse"
36 muse_dir.mkdir()
37 repo_id = str(uuid.uuid4())
38 (muse_dir / "repo.json").write_text(json.dumps({
39 "repo_id": repo_id,
40 "domain": "midi",
41 "default_branch": "main",
42 "created_at": "2025-01-01T00:00:00+00:00",
43 }), encoding="utf-8")
44 (muse_dir / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
45 (muse_dir / "refs" / "heads").mkdir(parents=True)
46 (muse_dir / "snapshots").mkdir()
47 (muse_dir / "commits").mkdir()
48 (muse_dir / "objects").mkdir()
49 return tmp_path, repo_id
50
51
52 def _make_commit(
53 root: pathlib.Path, repo_id: str, branch: str = "main", message: str = "test"
54 ) -> str:
55 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
56 from muse.core.snapshot import compute_snapshot_id, compute_commit_id
57
58 ref_file = root / ".muse" / "refs" / "heads" / branch
59 parent_id = ref_file.read_text().strip() if ref_file.exists() else None
60 manifest: Manifest = {}
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, message=message,
66 committed_at_iso=committed_at.isoformat(),
67 )
68 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
69 write_commit(root, CommitRecord(
70 commit_id=commit_id, repo_id=repo_id, branch=branch,
71 snapshot_id=snap_id, message=message, committed_at=committed_at,
72 parent_commit_id=parent_id,
73 ))
74 ref_file.parent.mkdir(parents=True, exist_ok=True)
75 ref_file.write_text(commit_id, encoding="utf-8")
76 return commit_id
77
78
79 # ---------------------------------------------------------------------------
80 # Unit tests
81 # ---------------------------------------------------------------------------
82
83 class TestTagUnit:
84 def test_write_and_read_tag(self, tmp_path: pathlib.Path) -> None:
85 root, repo_id = _init_repo(tmp_path)
86 commit_id = _make_commit(root, repo_id)
87 from muse.core.store import TagRecord, write_tag, get_tags_for_commit
88 tag = TagRecord(tag_id=str(uuid.uuid4()), repo_id=repo_id,
89 commit_id=commit_id, tag="emotion:joyful")
90 write_tag(root, tag)
91 tags = get_tags_for_commit(root, repo_id, commit_id)
92 assert len(tags) == 1
93 assert tags[0].tag == "emotion:joyful"
94
95 def test_delete_tag(self, tmp_path: pathlib.Path) -> None:
96 root, repo_id = _init_repo(tmp_path)
97 commit_id = _make_commit(root, repo_id)
98 from muse.core.store import TagRecord, write_tag, get_tags_for_commit, delete_tag
99 tag_id = str(uuid.uuid4())
100 write_tag(root, TagRecord(tag_id=tag_id, repo_id=repo_id,
101 commit_id=commit_id, tag="section:chorus"))
102 assert len(get_tags_for_commit(root, repo_id, commit_id)) == 1
103 assert delete_tag(root, repo_id, tag_id) is True
104 assert get_tags_for_commit(root, repo_id, commit_id) == []
105
106 def test_delete_nonexistent_tag_returns_false(self, tmp_path: pathlib.Path) -> None:
107 root, repo_id = _init_repo(tmp_path)
108 from muse.core.store import delete_tag
109 assert delete_tag(root, repo_id, str(uuid.uuid4())) is False
110
111 def test_get_all_tags_empty(self, tmp_path: pathlib.Path) -> None:
112 root, repo_id = _init_repo(tmp_path)
113 from muse.core.store import get_all_tags
114 assert get_all_tags(root, repo_id) == []
115
116
117 # ---------------------------------------------------------------------------
118 # Content-addressed tag_id
119 # ---------------------------------------------------------------------------
120
121 class TestTagIdContentAddressed:
122 """tag_id must be sha256: of genesis content, not a UUID."""
123
124 def test_tag_id_is_sha256_prefixed(self, tmp_path: pathlib.Path) -> None:
125 root, repo_id = _init_repo(tmp_path)
126 commit_id = _make_commit(root, repo_id)
127 from muse.core.store import compute_tag_id
128 tag_id = compute_tag_id(repo_id=repo_id, commit_id=commit_id, tag="emotion:joyful")
129 assert tag_id.startswith("sha256:"), f"Expected sha256: prefix, got {tag_id!r}"
130 assert len(tag_id) == 71
131
132 def test_tag_id_is_deterministic(self, tmp_path: pathlib.Path) -> None:
133 root, repo_id = _init_repo(tmp_path)
134 commit_id = _make_commit(root, repo_id)
135 from muse.core.store import compute_tag_id
136 id1 = compute_tag_id(repo_id=repo_id, commit_id=commit_id, tag="v1.0")
137 id2 = compute_tag_id(repo_id=repo_id, commit_id=commit_id, tag="v1.0")
138 assert id1 == id2
139
140 def test_tag_id_differs_by_tag_name(self, tmp_path: pathlib.Path) -> None:
141 root, repo_id = _init_repo(tmp_path)
142 commit_id = _make_commit(root, repo_id)
143 from muse.core.store import compute_tag_id
144 assert compute_tag_id(repo_id, commit_id, "v1.0") != compute_tag_id(repo_id, commit_id, "v2.0")
145
146 def test_tag_id_differs_by_commit(self, tmp_path: pathlib.Path) -> None:
147 root, repo_id = _init_repo(tmp_path)
148 c1 = _make_commit(root, repo_id, message="first")
149 c2 = _make_commit(root, repo_id, message="second")
150 from muse.core.store import compute_tag_id
151 assert compute_tag_id(repo_id, c1, "v1.0") != compute_tag_id(repo_id, c2, "v1.0")
152
153 def test_tag_id_not_uuid(self, tmp_path: pathlib.Path) -> None:
154 import re
155 root, repo_id = _init_repo(tmp_path)
156 commit_id = _make_commit(root, repo_id)
157 from muse.core.store import compute_tag_id
158 tag_id = compute_tag_id(repo_id, commit_id, "release:1.0")
159 uuid_re = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
160 assert not uuid_re.match(tag_id)
161
162 def test_cli_tag_add_returns_content_addressed_id(self, tmp_path: pathlib.Path) -> None:
163 root, repo_id = _init_repo(tmp_path)
164 commit_id = _make_commit(root, repo_id)
165 result = runner.invoke(cli, ["tag", "add", "v1.0", commit_id, "--json"], env=_env(root))
166 assert result.exit_code == 0
167 data = json.loads(result.output)
168 tag_id = data["tag_id"]
169 assert tag_id.startswith("sha256:"), f"Expected sha256: prefix, got {tag_id!r}"
170 assert len(tag_id) == 71
171
172 def test_write_tag_uses_content_addressed_id(self, tmp_path: pathlib.Path) -> None:
173 root, repo_id = _init_repo(tmp_path)
174 commit_id = _make_commit(root, repo_id)
175 from muse.core.store import TagRecord, write_tag, get_tags_for_commit, compute_tag_id
176 expected_id = compute_tag_id(repo_id, commit_id, "emotion:sad")
177 tag = TagRecord(tag_id=expected_id, repo_id=repo_id, commit_id=commit_id, tag="emotion:sad")
178 write_tag(root, tag)
179 tags = get_tags_for_commit(root, repo_id, commit_id)
180 assert tags[0].tag_id == expected_id
181
182
183 # ---------------------------------------------------------------------------
184 # Integration tests
185 # ---------------------------------------------------------------------------
186
187 class TestTagIntegration:
188 def test_add_and_list_tag(self, tmp_path: pathlib.Path) -> None:
189 root, repo_id = _init_repo(tmp_path)
190 _make_commit(root, repo_id)
191 result = runner.invoke(cli, ["tag", "add", "emotion:joyful"], env=_env(root), catch_exceptions=False)
192 assert result.exit_code == 0
193 assert "Tagged" in result.output
194
195 result2 = runner.invoke(cli, ["tag", "list"], env=_env(root), catch_exceptions=False)
196 assert "emotion:joyful" in result2.output
197
198 def test_list_tags_for_specific_commit(self, tmp_path: pathlib.Path) -> None:
199 root, repo_id = _init_repo(tmp_path)
200 commit_id = _make_commit(root, repo_id)
201 runner.invoke(cli, ["tag", "add", "section:verse"], env=_env(root), catch_exceptions=False)
202 result = runner.invoke(cli, ["tag", "list", commit_id[:12]], env=_env(root), catch_exceptions=False)
203 assert "section:verse" in result.output
204
205 def test_remove_tag(self, tmp_path: pathlib.Path) -> None:
206 root, repo_id = _init_repo(tmp_path)
207 _make_commit(root, repo_id)
208 runner.invoke(cli, ["tag", "add", "emotion:tense"], env=_env(root), catch_exceptions=False)
209 result = runner.invoke(cli, ["tag", "remove", "emotion:tense"], env=_env(root), catch_exceptions=False)
210 assert result.exit_code == 0
211 assert "Removed" in result.output
212 result2 = runner.invoke(cli, ["tag", "list"], env=_env(root), catch_exceptions=False)
213 assert "emotion:tense" not in result2.output
214
215 def test_remove_nonexistent_tag_is_idempotent(self, tmp_path: pathlib.Path) -> None:
216 """Removing a tag that doesn't exist exits 0 (idempotent) with not_found status."""
217 root, repo_id = _init_repo(tmp_path)
218 _make_commit(root, repo_id)
219 result = runner.invoke(cli, ["tag", "remove", "ghost:tag", "--json"], env=_env(root))
220 assert result.exit_code == 0
221 import json as _json
222 d = _json.loads(result.output)
223 assert d["status"] == "not_found"
224 assert d["removed_count"] == 0
225
226 def test_add_multiple_tags_same_commit(self, tmp_path: pathlib.Path) -> None:
227 root, repo_id = _init_repo(tmp_path)
228 _make_commit(root, repo_id)
229 runner.invoke(cli, ["tag", "add", "key:Am"], env=_env(root), catch_exceptions=False)
230 runner.invoke(cli, ["tag", "add", "tempo:120bpm"], env=_env(root), catch_exceptions=False)
231 result = runner.invoke(cli, ["tag", "list"], env=_env(root), catch_exceptions=False)
232 assert "key:Am" in result.output
233 assert "tempo:120bpm" in result.output
234
235 def test_tag_on_invalid_ref_fails(self, tmp_path: pathlib.Path) -> None:
236 root, repo_id = _init_repo(tmp_path)
237 _make_commit(root, repo_id)
238 result = runner.invoke(cli, ["tag", "add", "emotion:sad", "deadbeef" * 8], env=_env(root))
239 assert result.exit_code != 0
240
241
242 # ---------------------------------------------------------------------------
243 # Security tests
244 # ---------------------------------------------------------------------------
245
246 class TestTagSecurity:
247 def test_tag_with_control_characters_sanitized_in_output(
248 self, tmp_path: pathlib.Path
249 ) -> None:
250 root, repo_id = _init_repo(tmp_path)
251 _make_commit(root, repo_id)
252 malicious = "emotion:\x1b[31mred\x1b[0m"
253 runner.invoke(cli, ["tag", "add", malicious], env=_env(root), catch_exceptions=False)
254 result = runner.invoke(cli, ["tag", "list"], env=_env(root), catch_exceptions=False)
255 assert result.exit_code == 0
256 assert "\x1b" not in result.output
257
258
259 # ---------------------------------------------------------------------------
260 # Stress tests
261 # ---------------------------------------------------------------------------
262
263 class TestTagStress:
264 def test_many_tags_on_many_commits(self, tmp_path: pathlib.Path) -> None:
265 root, repo_id = _init_repo(tmp_path)
266 commit_ids = [_make_commit(root, repo_id, message=f"commit {i}") for i in range(30)]
267 from muse.core.store import TagRecord, write_tag, get_all_tags
268 tag_types = ["emotion:joyful", "section:chorus", "key:Am", "tempo:120bpm", "stage:master"]
269 for i, cid in enumerate(commit_ids):
270 write_tag(root, TagRecord(
271 tag_id=str(uuid.uuid4()), repo_id=repo_id,
272 commit_id=cid, tag=tag_types[i % len(tag_types)],
273 ))
274 all_tags = get_all_tags(root, repo_id)
275 assert len(all_tags) == 30
276 result = runner.invoke(cli, ["tag", "list"], env=_env(root), catch_exceptions=False)
277 assert result.exit_code == 0
278 for tag_type in tag_types:
279 assert tag_type in result.output
File History 2 commits
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 139 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 141 days ago