test_cmd_label.py
python
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