gabriel / muse public
test_cmd_tag.py python
280 lines 12.4 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 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
17 import pytest
18 from tests.cli_test_helper import CliRunner
19 from muse.core._types import fake_id, short_id
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 = fake_id("repo")
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 repo_id=repo_id,
65 parent_ids=[parent_id] if parent_id else [],
66 snapshot_id=snap_id, 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, created_on_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 TestTagUnit:
85 def test_write_and_read_tag(self, tmp_path: pathlib.Path) -> None:
86 root, repo_id = _init_repo(tmp_path)
87 commit_id = _make_commit(root, repo_id)
88 from muse.core.store import TagRecord, write_tag, get_tags_for_commit
89 tag = TagRecord(tag_id=fake_id("tag"), repo_id=repo_id,
90 commit_id=commit_id, tag="emotion:joyful")
91 write_tag(root, tag)
92 tags = get_tags_for_commit(root, repo_id, commit_id)
93 assert len(tags) == 1
94 assert tags[0].tag == "emotion:joyful"
95
96 def test_delete_tag(self, tmp_path: pathlib.Path) -> None:
97 root, repo_id = _init_repo(tmp_path)
98 commit_id = _make_commit(root, repo_id)
99 from muse.core.store import TagRecord, write_tag, get_tags_for_commit, delete_tag
100 tag_id = fake_id("tag")
101 write_tag(root, TagRecord(tag_id=tag_id, repo_id=repo_id,
102 commit_id=commit_id, tag="section:chorus"))
103 assert len(get_tags_for_commit(root, repo_id, commit_id)) == 1
104 assert delete_tag(root, repo_id, tag_id) is True
105 assert get_tags_for_commit(root, repo_id, commit_id) == []
106
107 def test_delete_nonexistent_tag_returns_false(self, tmp_path: pathlib.Path) -> None:
108 root, repo_id = _init_repo(tmp_path)
109 from muse.core.store import delete_tag
110 assert delete_tag(root, repo_id, fake_id("tag")) is False
111
112 def test_get_all_tags_empty(self, tmp_path: pathlib.Path) -> None:
113 root, repo_id = _init_repo(tmp_path)
114 from muse.core.store import get_all_tags
115 assert get_all_tags(root, repo_id) == []
116
117
118 # ---------------------------------------------------------------------------
119 # Content-addressed tag_id
120 # ---------------------------------------------------------------------------
121
122 class TestTagIdContentAddressed:
123 """tag_id must be sha256: of genesis content, not a UUID."""
124
125 def test_tag_id_is_sha256_prefixed(self, tmp_path: pathlib.Path) -> None:
126 root, repo_id = _init_repo(tmp_path)
127 commit_id = _make_commit(root, repo_id)
128 from muse.core.store import compute_tag_id
129 tag_id = compute_tag_id(repo_id=repo_id, commit_id=commit_id, tag="emotion:joyful")
130 assert tag_id.startswith("sha256:"), f"Expected sha256: prefix, got {tag_id!r}"
131 assert len(tag_id) == 71
132
133 def test_tag_id_is_deterministic(self, tmp_path: pathlib.Path) -> None:
134 root, repo_id = _init_repo(tmp_path)
135 commit_id = _make_commit(root, repo_id)
136 from muse.core.store import compute_tag_id
137 id1 = compute_tag_id(repo_id=repo_id, commit_id=commit_id, tag="v1.0")
138 id2 = compute_tag_id(repo_id=repo_id, commit_id=commit_id, tag="v1.0")
139 assert id1 == id2
140
141 def test_tag_id_differs_by_tag_name(self, tmp_path: pathlib.Path) -> None:
142 root, repo_id = _init_repo(tmp_path)
143 commit_id = _make_commit(root, repo_id)
144 from muse.core.store import compute_tag_id
145 assert compute_tag_id(repo_id, commit_id, "v1.0") != compute_tag_id(repo_id, commit_id, "v2.0")
146
147 def test_tag_id_differs_by_commit(self, tmp_path: pathlib.Path) -> None:
148 root, repo_id = _init_repo(tmp_path)
149 c1 = _make_commit(root, repo_id, message="first")
150 c2 = _make_commit(root, repo_id, message="second")
151 from muse.core.store import compute_tag_id
152 assert compute_tag_id(repo_id, c1, "v1.0") != compute_tag_id(repo_id, c2, "v1.0")
153
154 def test_tag_id_not_uuid(self, tmp_path: pathlib.Path) -> None:
155 import re
156 root, repo_id = _init_repo(tmp_path)
157 commit_id = _make_commit(root, repo_id)
158 from muse.core.store import compute_tag_id
159 tag_id = compute_tag_id(repo_id, commit_id, "release:1.0")
160 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}$")
161 assert not uuid_re.match(tag_id)
162
163 def test_cli_tag_add_returns_content_addressed_id(self, tmp_path: pathlib.Path) -> None:
164 root, repo_id = _init_repo(tmp_path)
165 commit_id = _make_commit(root, repo_id)
166 result = runner.invoke(cli, ["tag", "add", "v1.0", commit_id, "--json"], env=_env(root))
167 assert result.exit_code == 0
168 data = json.loads(result.output)
169 tag_id = data["tag_id"]
170 assert tag_id.startswith("sha256:"), f"Expected sha256: prefix, got {tag_id!r}"
171 assert len(tag_id) == 71
172
173 def test_write_tag_uses_content_addressed_id(self, tmp_path: pathlib.Path) -> None:
174 root, repo_id = _init_repo(tmp_path)
175 commit_id = _make_commit(root, repo_id)
176 from muse.core.store import TagRecord, write_tag, get_tags_for_commit, compute_tag_id
177 expected_id = compute_tag_id(repo_id, commit_id, "emotion:sad")
178 tag = TagRecord(tag_id=expected_id, repo_id=repo_id, commit_id=commit_id, tag="emotion:sad")
179 write_tag(root, tag)
180 tags = get_tags_for_commit(root, repo_id, commit_id)
181 assert tags[0].tag_id == expected_id
182
183
184 # ---------------------------------------------------------------------------
185 # Integration tests
186 # ---------------------------------------------------------------------------
187
188 class TestTagIntegration:
189 def test_add_and_list_tag(self, tmp_path: pathlib.Path) -> None:
190 root, repo_id = _init_repo(tmp_path)
191 _make_commit(root, repo_id)
192 result = runner.invoke(cli, ["tag", "add", "emotion:joyful"], env=_env(root), catch_exceptions=False)
193 assert result.exit_code == 0
194 assert "Tagged" in result.output
195
196 result2 = runner.invoke(cli, ["tag", "list"], env=_env(root), catch_exceptions=False)
197 assert "emotion:joyful" in result2.output
198
199 def test_list_tags_for_specific_commit(self, tmp_path: pathlib.Path) -> None:
200 root, repo_id = _init_repo(tmp_path)
201 commit_id = _make_commit(root, repo_id)
202 runner.invoke(cli, ["tag", "add", "section:verse"], env=_env(root), catch_exceptions=False)
203 result = runner.invoke(cli, ["tag", "list", short_id(commit_id)], env=_env(root), catch_exceptions=False)
204 assert "section:verse" in result.output
205
206 def test_remove_tag(self, tmp_path: pathlib.Path) -> None:
207 root, repo_id = _init_repo(tmp_path)
208 _make_commit(root, repo_id)
209 runner.invoke(cli, ["tag", "add", "emotion:tense"], env=_env(root), catch_exceptions=False)
210 result = runner.invoke(cli, ["tag", "remove", "emotion:tense"], env=_env(root), catch_exceptions=False)
211 assert result.exit_code == 0
212 assert "Removed" in result.output
213 result2 = runner.invoke(cli, ["tag", "list"], env=_env(root), catch_exceptions=False)
214 assert "emotion:tense" not in result2.output
215
216 def test_remove_nonexistent_tag_is_idempotent(self, tmp_path: pathlib.Path) -> None:
217 """Removing a tag that doesn't exist exits 0 (idempotent) with not_found status."""
218 root, repo_id = _init_repo(tmp_path)
219 _make_commit(root, repo_id)
220 result = runner.invoke(cli, ["tag", "remove", "ghost:tag", "--json"], env=_env(root))
221 assert result.exit_code == 0
222 import json as _json
223 d = _json.loads(result.output)
224 assert d["status"] == "not_found"
225 assert d["removed_count"] == 0
226
227 def test_add_multiple_tags_same_commit(self, tmp_path: pathlib.Path) -> None:
228 root, repo_id = _init_repo(tmp_path)
229 _make_commit(root, repo_id)
230 runner.invoke(cli, ["tag", "add", "key:Am"], env=_env(root), catch_exceptions=False)
231 runner.invoke(cli, ["tag", "add", "tempo:120bpm"], env=_env(root), catch_exceptions=False)
232 result = runner.invoke(cli, ["tag", "list"], env=_env(root), catch_exceptions=False)
233 assert "key:Am" in result.output
234 assert "tempo:120bpm" in result.output
235
236 def test_tag_on_invalid_ref_fails(self, tmp_path: pathlib.Path) -> None:
237 root, repo_id = _init_repo(tmp_path)
238 _make_commit(root, repo_id)
239 result = runner.invoke(cli, ["tag", "add", "emotion:sad", "deadbeef" * 8], env=_env(root))
240 assert result.exit_code != 0
241
242
243 # ---------------------------------------------------------------------------
244 # Security tests
245 # ---------------------------------------------------------------------------
246
247 class TestTagSecurity:
248 def test_tag_with_control_characters_sanitized_in_output(
249 self, tmp_path: pathlib.Path
250 ) -> None:
251 root, repo_id = _init_repo(tmp_path)
252 _make_commit(root, repo_id)
253 malicious = "emotion:\x1b[31mred\x1b[0m"
254 runner.invoke(cli, ["tag", "add", malicious], env=_env(root), catch_exceptions=False)
255 result = runner.invoke(cli, ["tag", "list"], env=_env(root), catch_exceptions=False)
256 assert result.exit_code == 0
257 assert "\x1b" not in result.output
258
259
260 # ---------------------------------------------------------------------------
261 # Stress tests
262 # ---------------------------------------------------------------------------
263
264 class TestTagStress:
265 def test_many_tags_on_many_commits(self, tmp_path: pathlib.Path) -> None:
266 root, repo_id = _init_repo(tmp_path)
267 commit_ids = [_make_commit(root, repo_id, message=f"commit {i}") for i in range(30)]
268 from muse.core.store import TagRecord, write_tag, get_all_tags
269 tag_types = ["emotion:joyful", "section:chorus", "key:Am", "tempo:120bpm", "stage:master"]
270 for i, cid in enumerate(commit_ids):
271 write_tag(root, TagRecord(
272 tag_id=fake_id(f"tag-{i}"), repo_id=repo_id,
273 commit_id=cid, tag=tag_types[i % len(tag_types)],
274 ))
275 all_tags = get_all_tags(root, repo_id)
276 assert len(all_tags) == 30
277 result = runner.invoke(cli, ["tag", "list"], env=_env(root), catch_exceptions=False)
278 assert result.exit_code == 0
279 for tag_type in tag_types:
280 assert tag_type in result.output
File History 3 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 137 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 140 days ago