gabriel / muse public
test_cmd_label.py python
353 lines 15.0 KB
Raw
sha256:c0eba5ad689cec79f4a3fcdc4f5da78556cb4b8cb7b330f944634356c10379ed chore: pivot to nightly channel — bump version to 0.2.0.dev… Sonnet 5 patch 12 days ago
1 """Phase 0 — muse label command tests (LB_01–LB_08).
2
3 Covers the ``muse label`` CLI after the rename from old ``muse tag`` label
4 annotations. All label operations live here; version tags live in
5 ``test_cmd_version_tags.py``.
6 """
7
8 from __future__ import annotations
9
10 import datetime
11 import json
12 import pathlib
13
14 import pytest
15 from tests.cli_test_helper import CliRunner
16 from muse.core.types import fake_id
17 from muse.core.paths import muse_dir, ref_path
18
19 cli = None
20 runner = CliRunner()
21
22
23 # ---------------------------------------------------------------------------
24 # Helpers
25 # ---------------------------------------------------------------------------
26
27 def _env(root: pathlib.Path) -> dict:
28 return {"MUSE_REPO_ROOT": str(root)}
29
30
31 def _init_repo(tmp_path: pathlib.Path) -> tuple[pathlib.Path, str]:
32 dot_muse = muse_dir(tmp_path)
33 dot_muse.mkdir()
34 repo_id = fake_id("repo")
35 (dot_muse / "repo.json").write_text(json.dumps({
36 "repo_id": repo_id,
37 "domain": "midi",
38 "default_branch": "main",
39 "created_at": "2025-01-01T00:00:00+00:00",
40 }), encoding="utf-8")
41 (dot_muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
42 (dot_muse / "refs" / "heads").mkdir(parents=True)
43 (dot_muse / "snapshots").mkdir()
44 (dot_muse / "commits").mkdir()
45 (dot_muse / "objects").mkdir()
46 return tmp_path, repo_id
47
48
49 def _make_commit(
50 root: pathlib.Path, repo_id: str, branch: str = "main", message: str = "test"
51 ) -> str:
52 from muse.core.commits import CommitRecord, write_commit
53 from muse.core.snapshots import SnapshotRecord, write_snapshot
54 from muse.core.ids import hash_snapshot, hash_commit
55
56 ref_file = ref_path(root, branch)
57 parent_id = ref_file.read_text().strip() if ref_file.exists() else None
58 manifest: dict = {}
59 snap_id = hash_snapshot(manifest)
60 committed_at = datetime.datetime.now(datetime.timezone.utc)
61 commit_id = hash_commit(
62 parent_ids=[parent_id] if parent_id else [],
63 snapshot_id=snap_id,
64 message=message,
65 committed_at_iso=committed_at.isoformat(),
66 )
67 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
68 write_commit(root, CommitRecord(
69 commit_id=commit_id, branch=branch,
70 snapshot_id=snap_id, message=message, committed_at=committed_at,
71 parent_commit_id=parent_id,
72 ))
73 ref_file.parent.mkdir(parents=True, exist_ok=True)
74 ref_file.write_text(commit_id, encoding="utf-8")
75 return commit_id
76
77
78 # ---------------------------------------------------------------------------
79 # LB_01 — label add returns label_id (not tag_id)
80 # ---------------------------------------------------------------------------
81
82 class TestLB01LabelAddReturnsLabelId:
83 """LB_01: ``muse label add`` JSON output uses ``label_id``, not ``tag_id``."""
84
85 def test_add_json_has_label_id(self, tmp_path: pathlib.Path) -> None:
86 root, _ = _init_repo(tmp_path)
87 _make_commit(root, fake_id("repo"))
88 result = runner.invoke(cli, ["label", "add", "emotion:joyful", "--json"], env=_env(root))
89 assert result.exit_code == 0, result.output
90 data = json.loads(result.output)
91 assert "label_id" in data
92 assert "tag_id" not in data
93
94 def test_add_json_label_id_is_sha256(self, tmp_path: pathlib.Path) -> None:
95 root, _ = _init_repo(tmp_path)
96 _make_commit(root, fake_id("repo"))
97 result = runner.invoke(cli, ["label", "add", "section:chorus", "--json"], env=_env(root))
98 data = json.loads(result.output)
99 assert data["label_id"].startswith("sha256:")
100 assert len(data["label_id"]) == 71
101
102 def test_add_json_label_field_present(self, tmp_path: pathlib.Path) -> None:
103 root, _ = _init_repo(tmp_path)
104 _make_commit(root, fake_id("repo"))
105 result = runner.invoke(cli, ["label", "add", "tempo:120bpm", "--json"], env=_env(root))
106 data = json.loads(result.output)
107 assert "label" in data
108 assert data["label"] == "tempo:120bpm"
109 assert "tag" not in data
110
111
112 # ---------------------------------------------------------------------------
113 # LB_02 — label list returns labels key (not tags)
114 # ---------------------------------------------------------------------------
115
116 class TestLB02LabelListReturnsLabelsKey:
117 """LB_02: ``muse label list`` JSON output uses ``labels`` key, not ``tags``."""
118
119 def test_list_json_has_labels_key(self, tmp_path: pathlib.Path) -> None:
120 root, _ = _init_repo(tmp_path)
121 _make_commit(root, fake_id("repo"))
122 runner.invoke(cli, ["label", "add", "emotion:happy"], env=_env(root))
123 result = runner.invoke(cli, ["label", "list", "--json"], env=_env(root))
124 assert result.exit_code == 0, result.output
125 data = json.loads(result.output)
126 assert "labels" in data
127 assert "tags" not in data
128
129 def test_list_json_no_tags_key_when_empty(self, tmp_path: pathlib.Path) -> None:
130 root, _ = _init_repo(tmp_path)
131 _make_commit(root, fake_id("repo"))
132 result = runner.invoke(cli, ["label", "list", "--json"], env=_env(root))
133 data = json.loads(result.output)
134 assert "labels" in data
135 assert "tags" not in data
136 assert data["labels"] == []
137
138 def test_list_entry_has_label_field(self, tmp_path: pathlib.Path) -> None:
139 root, _ = _init_repo(tmp_path)
140 _make_commit(root, fake_id("repo"))
141 runner.invoke(cli, ["label", "add", "stage:master"], env=_env(root))
142 result = runner.invoke(cli, ["label", "list", "--json"], env=_env(root))
143 data = json.loads(result.output)
144 entry = data["labels"][0]
145 assert "label" in entry
146 assert "tag" not in entry
147 assert "label_id" in entry
148 assert "tag_id" not in entry
149
150
151 # ---------------------------------------------------------------------------
152 # LB_03 — add status is "labelled" / "already_labelled"
153 # ---------------------------------------------------------------------------
154
155 class TestLB03StatusStrings:
156 """LB_03: status strings use label-specific values."""
157
158 def test_first_add_status_is_labelled(self, tmp_path: pathlib.Path) -> None:
159 root, _ = _init_repo(tmp_path)
160 _make_commit(root, fake_id("repo"))
161 result = runner.invoke(cli, ["label", "add", "emotion:tense", "--json"], env=_env(root))
162 data = json.loads(result.output)
163 assert data["status"] == "labelled"
164 assert data["status"] != "tagged"
165
166 def test_duplicate_add_status_is_already_labelled(self, tmp_path: pathlib.Path) -> None:
167 root, _ = _init_repo(tmp_path)
168 _make_commit(root, fake_id("repo"))
169 runner.invoke(cli, ["label", "add", "emotion:tense"], env=_env(root))
170 result = runner.invoke(cli, ["label", "add", "emotion:tense", "--json"], env=_env(root))
171 data = json.loads(result.output)
172 assert data["status"] == "already_labelled"
173 assert data["status"] != "already_tagged"
174
175
176 # ---------------------------------------------------------------------------
177 # LB_04 — remove returns label_ids (not tag_ids)
178 # ---------------------------------------------------------------------------
179
180 class TestLB04RemoveReturnsLabelIds:
181 """LB_04: ``muse label remove`` JSON output uses ``label_ids``, not ``tag_ids``."""
182
183 def test_remove_json_has_label_ids(self, tmp_path: pathlib.Path) -> None:
184 root, _ = _init_repo(tmp_path)
185 _make_commit(root, fake_id("repo"))
186 runner.invoke(cli, ["label", "add", "emotion:joyful"], env=_env(root))
187 result = runner.invoke(cli, ["label", "remove", "emotion:joyful", "--json"], env=_env(root))
188 assert result.exit_code == 0, result.output
189 data = json.loads(result.output)
190 assert "label_ids" in data
191 assert "tag_ids" not in data
192
193 def test_remove_json_label_field(self, tmp_path: pathlib.Path) -> None:
194 root, _ = _init_repo(tmp_path)
195 _make_commit(root, fake_id("repo"))
196 runner.invoke(cli, ["label", "add", "section:verse"], env=_env(root))
197 result = runner.invoke(cli, ["label", "remove", "section:verse", "--json"], env=_env(root))
198 data = json.loads(result.output)
199 assert "label" in data
200 assert data["label"] == "section:verse"
201 assert "tag" not in data
202
203 def test_remove_not_found_has_label_ids(self, tmp_path: pathlib.Path) -> None:
204 root, _ = _init_repo(tmp_path)
205 _make_commit(root, fake_id("repo"))
206 result = runner.invoke(cli, ["label", "remove", "ghost:label", "--json"], env=_env(root))
207 data = json.loads(result.output)
208 assert "label_ids" in data
209 assert data["label_ids"] == []
210 assert "tag_ids" not in data
211
212
213 # ---------------------------------------------------------------------------
214 # LB_05 — muse label is a registered command (not just muse tag)
215 # ---------------------------------------------------------------------------
216
217 class TestLB05LabelCommandRegistered:
218 """LB_05: ``muse label`` is registered as a top-level command."""
219
220 def test_label_add_exits_zero(self, tmp_path: pathlib.Path) -> None:
221 root, _ = _init_repo(tmp_path)
222 _make_commit(root, fake_id("repo"))
223 result = runner.invoke(cli, ["label", "add", "key:Am"], env=_env(root))
224 assert result.exit_code == 0
225
226 def test_label_list_exits_zero(self, tmp_path: pathlib.Path) -> None:
227 root, _ = _init_repo(tmp_path)
228 _make_commit(root, fake_id("repo"))
229 result = runner.invoke(cli, ["label", "list"], env=_env(root))
230 assert result.exit_code == 0
231
232 def test_label_remove_exits_zero(self, tmp_path: pathlib.Path) -> None:
233 root, _ = _init_repo(tmp_path)
234 _make_commit(root, fake_id("repo"))
235 runner.invoke(cli, ["label", "add", "key:Am"], env=_env(root))
236 result = runner.invoke(cli, ["label", "remove", "key:Am"], env=_env(root))
237 assert result.exit_code == 0
238
239
240 # ---------------------------------------------------------------------------
241 # LB_06 — muse tag no longer handles label annotations
242 # ---------------------------------------------------------------------------
243
244 class TestLB06TagNoLongerHandlesLabels:
245 """LB_06: ``muse tag`` subcommands are add/list/read/delete/latest (version tags only)."""
246
247 def test_tag_add_requires_version_format(self, tmp_path: pathlib.Path) -> None:
248 root, _ = _init_repo(tmp_path)
249 _make_commit(root, fake_id("repo"))
250 result = runner.invoke(cli, ["tag", "add", "v1.0.0"], env=_env(root))
251 assert result.exit_code == 0
252
253 def test_tag_add_emotion_label_rejected(self, tmp_path: pathlib.Path) -> None:
254 root, _ = _init_repo(tmp_path)
255 _make_commit(root, fake_id("repo"))
256 result = runner.invoke(cli, ["tag", "add", "emotion:joyful"], env=_env(root))
257 assert result.exit_code != 0
258
259 def test_tag_list_returns_tags_key(self, tmp_path: pathlib.Path) -> None:
260 root, _ = _init_repo(tmp_path)
261 _make_commit(root, fake_id("repo"))
262 result = runner.invoke(cli, ["tag", "list", "--json"], env=_env(root))
263 assert result.exit_code == 0
264 data = json.loads(result.output)
265 assert "tags" in data
266 assert "labels" not in data
267
268
269 # ---------------------------------------------------------------------------
270 # LB_07 — label and tag coexist without interference
271 # ---------------------------------------------------------------------------
272
273 class TestLB07LabelAndTagCoexist:
274 """LB_07: ``muse label`` and ``muse tag`` store data in separate directories."""
275
276 def test_label_storage_separate_from_version_tags(self, tmp_path: pathlib.Path) -> None:
277 root, _ = _init_repo(tmp_path)
278 _make_commit(root, fake_id("repo"))
279 runner.invoke(cli, ["label", "add", "emotion:happy"], env=_env(root))
280 runner.invoke(cli, ["tag", "add", "v1.0.0"], env=_env(root))
281
282 label_result = runner.invoke(cli, ["label", "list", "--json"], env=_env(root))
283 tag_result = runner.invoke(cli, ["tag", "list", "--json"], env=_env(root))
284
285 labels = json.loads(label_result.output)
286 tags = json.loads(tag_result.output)
287
288 assert labels["total"] == 1
289 assert tags["total"] == 1
290
291 assert labels["labels"][0]["label"] == "emotion:happy"
292 assert tags["tags"][0]["tag"] == "v1.0.0"
293
294 def test_label_dir_is_dot_muse_tags(self, tmp_path: pathlib.Path) -> None:
295 root, _ = _init_repo(tmp_path)
296 _make_commit(root, fake_id("repo"))
297 runner.invoke(cli, ["label", "add", "section:chorus"], env=_env(root))
298 tags_dir = root / ".muse" / "tags"
299 assert tags_dir.exists()
300
301 def test_version_tag_dir_is_dot_muse_version_tags(self, tmp_path: pathlib.Path) -> None:
302 root, _ = _init_repo(tmp_path)
303 _make_commit(root, fake_id("repo"))
304 runner.invoke(cli, ["tag", "add", "v2.0.0"], env=_env(root))
305 version_tags_dir = root / ".muse" / "version-tags"
306 assert version_tags_dir.exists()
307
308
309 # ---------------------------------------------------------------------------
310 # LB_08 — label round-trip (add → list → remove)
311 # ---------------------------------------------------------------------------
312
313 class TestLB08LabelRoundTrip:
314 """LB_08: full round-trip for label annotations."""
315
316 def test_round_trip(self, tmp_path: pathlib.Path) -> None:
317 root, _ = _init_repo(tmp_path)
318 _make_commit(root, fake_id("repo"))
319
320 add_result = runner.invoke(cli, ["label", "add", "emotion:joyful", "--json"], env=_env(root))
321 assert add_result.exit_code == 0
322 add_data = json.loads(add_result.output)
323 assert add_data["status"] == "labelled"
324 assert add_data["label_id"].startswith("sha256:")
325
326 list_result = runner.invoke(cli, ["label", "list", "--json"], env=_env(root))
327 list_data = json.loads(list_result.output)
328 assert list_data["total"] == 1
329 assert list_data["labels"][0]["label"] == "emotion:joyful"
330 assert list_data["labels"][0]["label_id"] == add_data["label_id"]
331
332 remove_result = runner.invoke(cli, ["label", "remove", "emotion:joyful", "--json"], env=_env(root))
333 remove_data = json.loads(remove_result.output)
334 assert remove_data["status"] == "removed"
335 assert remove_data["removed_count"] == 1
336 assert add_data["label_id"] in remove_data["label_ids"]
337
338 list_after = runner.invoke(cli, ["label", "list", "--json"], env=_env(root))
339 assert json.loads(list_after.output)["total"] == 0
340
341 def test_multiple_labels_round_trip(self, tmp_path: pathlib.Path) -> None:
342 root, _ = _init_repo(tmp_path)
343 _make_commit(root, fake_id("repo"))
344
345 for label in ["emotion:joyful", "section:chorus", "key:Am", "tempo:120bpm"]:
346 result = runner.invoke(cli, ["label", "add", label, "--json"], env=_env(root))
347 assert json.loads(result.output)["status"] == "labelled"
348
349 list_result = runner.invoke(cli, ["label", "list", "--json"], env=_env(root))
350 data = json.loads(list_result.output)
351 assert data["total"] == 4
352 label_names = {e["label"] for e in data["labels"]}
353 assert label_names == {"emotion:joyful", "section:chorus", "key:Am", "tempo:120bpm"}
File History 2 commits
sha256:c287f599c5429903a139eadf3c5db5d930520e57cb0c3c575d9570e953c3b2d6 chore: bump version to 0.2.0.dev2 — nightly.2 Sonnet 4.6 patch 11 days ago
sha256:8de4334a98c945aace420969d389ad678aa926d4ab4e886b2ac4c4241cb3bf2b revert: keep pyproject.toml in canonical PEP 440 form Sonnet 4.6 patch 14 days ago