"""Phase 0 — muse label command tests (LB_01–LB_08). Covers the ``muse label`` CLI after the rename from old ``muse tag`` label annotations. All label operations live here; version tags live in ``test_cmd_version_tags.py``. """ from __future__ import annotations import datetime import json import pathlib import pytest from tests.cli_test_helper import CliRunner from muse.core.types import fake_id from muse.core.paths import muse_dir, ref_path cli = None runner = CliRunner() # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _env(root: pathlib.Path) -> dict: return {"MUSE_REPO_ROOT": str(root)} def _init_repo(tmp_path: pathlib.Path) -> tuple[pathlib.Path, str]: dot_muse = muse_dir(tmp_path) dot_muse.mkdir() repo_id = fake_id("repo") (dot_muse / "repo.json").write_text(json.dumps({ "repo_id": repo_id, "domain": "midi", "default_branch": "main", "created_at": "2025-01-01T00:00:00+00:00", }), encoding="utf-8") (dot_muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8") (dot_muse / "refs" / "heads").mkdir(parents=True) (dot_muse / "snapshots").mkdir() (dot_muse / "commits").mkdir() (dot_muse / "objects").mkdir() return tmp_path, repo_id def _make_commit( root: pathlib.Path, repo_id: str, branch: str = "main", message: str = "test" ) -> str: from muse.core.commits import CommitRecord, write_commit from muse.core.snapshots import SnapshotRecord, write_snapshot from muse.core.ids import hash_snapshot, hash_commit ref_file = ref_path(root, branch) parent_id = ref_file.read_text().strip() if ref_file.exists() else None manifest: dict = {} snap_id = hash_snapshot(manifest) committed_at = datetime.datetime.now(datetime.timezone.utc) commit_id = hash_commit( parent_ids=[parent_id] if parent_id else [], snapshot_id=snap_id, message=message, committed_at_iso=committed_at.isoformat(), ) write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest)) write_commit(root, CommitRecord( commit_id=commit_id, branch=branch, snapshot_id=snap_id, message=message, committed_at=committed_at, parent_commit_id=parent_id, )) ref_file.parent.mkdir(parents=True, exist_ok=True) ref_file.write_text(commit_id, encoding="utf-8") return commit_id # --------------------------------------------------------------------------- # LB_01 — label add returns label_id (not tag_id) # --------------------------------------------------------------------------- class TestLB01LabelAddReturnsLabelId: """LB_01: ``muse label add`` JSON output uses ``label_id``, not ``tag_id``.""" def test_add_json_has_label_id(self, tmp_path: pathlib.Path) -> None: root, _ = _init_repo(tmp_path) _make_commit(root, fake_id("repo")) result = runner.invoke(cli, ["label", "add", "emotion:joyful", "--json"], env=_env(root)) assert result.exit_code == 0, result.output data = json.loads(result.output) assert "label_id" in data assert "tag_id" not in data def test_add_json_label_id_is_sha256(self, tmp_path: pathlib.Path) -> None: root, _ = _init_repo(tmp_path) _make_commit(root, fake_id("repo")) result = runner.invoke(cli, ["label", "add", "section:chorus", "--json"], env=_env(root)) data = json.loads(result.output) assert data["label_id"].startswith("sha256:") assert len(data["label_id"]) == 71 def test_add_json_label_field_present(self, tmp_path: pathlib.Path) -> None: root, _ = _init_repo(tmp_path) _make_commit(root, fake_id("repo")) result = runner.invoke(cli, ["label", "add", "tempo:120bpm", "--json"], env=_env(root)) data = json.loads(result.output) assert "label" in data assert data["label"] == "tempo:120bpm" assert "tag" not in data # --------------------------------------------------------------------------- # LB_02 — label list returns labels key (not tags) # --------------------------------------------------------------------------- class TestLB02LabelListReturnsLabelsKey: """LB_02: ``muse label list`` JSON output uses ``labels`` key, not ``tags``.""" def test_list_json_has_labels_key(self, tmp_path: pathlib.Path) -> None: root, _ = _init_repo(tmp_path) _make_commit(root, fake_id("repo")) runner.invoke(cli, ["label", "add", "emotion:happy"], env=_env(root)) result = runner.invoke(cli, ["label", "list", "--json"], env=_env(root)) assert result.exit_code == 0, result.output data = json.loads(result.output) assert "labels" in data assert "tags" not in data def test_list_json_no_tags_key_when_empty(self, tmp_path: pathlib.Path) -> None: root, _ = _init_repo(tmp_path) _make_commit(root, fake_id("repo")) result = runner.invoke(cli, ["label", "list", "--json"], env=_env(root)) data = json.loads(result.output) assert "labels" in data assert "tags" not in data assert data["labels"] == [] def test_list_entry_has_label_field(self, tmp_path: pathlib.Path) -> None: root, _ = _init_repo(tmp_path) _make_commit(root, fake_id("repo")) runner.invoke(cli, ["label", "add", "stage:master"], env=_env(root)) result = runner.invoke(cli, ["label", "list", "--json"], env=_env(root)) data = json.loads(result.output) entry = data["labels"][0] assert "label" in entry assert "tag" not in entry assert "label_id" in entry assert "tag_id" not in entry # --------------------------------------------------------------------------- # LB_03 — add status is "labelled" / "already_labelled" # --------------------------------------------------------------------------- class TestLB03StatusStrings: """LB_03: status strings use label-specific values.""" def test_first_add_status_is_labelled(self, tmp_path: pathlib.Path) -> None: root, _ = _init_repo(tmp_path) _make_commit(root, fake_id("repo")) result = runner.invoke(cli, ["label", "add", "emotion:tense", "--json"], env=_env(root)) data = json.loads(result.output) assert data["status"] == "labelled" assert data["status"] != "tagged" def test_duplicate_add_status_is_already_labelled(self, tmp_path: pathlib.Path) -> None: root, _ = _init_repo(tmp_path) _make_commit(root, fake_id("repo")) runner.invoke(cli, ["label", "add", "emotion:tense"], env=_env(root)) result = runner.invoke(cli, ["label", "add", "emotion:tense", "--json"], env=_env(root)) data = json.loads(result.output) assert data["status"] == "already_labelled" assert data["status"] != "already_tagged" # --------------------------------------------------------------------------- # LB_04 — remove returns label_ids (not tag_ids) # --------------------------------------------------------------------------- class TestLB04RemoveReturnsLabelIds: """LB_04: ``muse label remove`` JSON output uses ``label_ids``, not ``tag_ids``.""" def test_remove_json_has_label_ids(self, tmp_path: pathlib.Path) -> None: root, _ = _init_repo(tmp_path) _make_commit(root, fake_id("repo")) runner.invoke(cli, ["label", "add", "emotion:joyful"], env=_env(root)) result = runner.invoke(cli, ["label", "remove", "emotion:joyful", "--json"], env=_env(root)) assert result.exit_code == 0, result.output data = json.loads(result.output) assert "label_ids" in data assert "tag_ids" not in data def test_remove_json_label_field(self, tmp_path: pathlib.Path) -> None: root, _ = _init_repo(tmp_path) _make_commit(root, fake_id("repo")) runner.invoke(cli, ["label", "add", "section:verse"], env=_env(root)) result = runner.invoke(cli, ["label", "remove", "section:verse", "--json"], env=_env(root)) data = json.loads(result.output) assert "label" in data assert data["label"] == "section:verse" assert "tag" not in data def test_remove_not_found_has_label_ids(self, tmp_path: pathlib.Path) -> None: root, _ = _init_repo(tmp_path) _make_commit(root, fake_id("repo")) result = runner.invoke(cli, ["label", "remove", "ghost:label", "--json"], env=_env(root)) data = json.loads(result.output) assert "label_ids" in data assert data["label_ids"] == [] assert "tag_ids" not in data # --------------------------------------------------------------------------- # LB_05 — muse label is a registered command (not just muse tag) # --------------------------------------------------------------------------- class TestLB05LabelCommandRegistered: """LB_05: ``muse label`` is registered as a top-level command.""" def test_label_add_exits_zero(self, tmp_path: pathlib.Path) -> None: root, _ = _init_repo(tmp_path) _make_commit(root, fake_id("repo")) result = runner.invoke(cli, ["label", "add", "key:Am"], env=_env(root)) assert result.exit_code == 0 def test_label_list_exits_zero(self, tmp_path: pathlib.Path) -> None: root, _ = _init_repo(tmp_path) _make_commit(root, fake_id("repo")) result = runner.invoke(cli, ["label", "list"], env=_env(root)) assert result.exit_code == 0 def test_label_remove_exits_zero(self, tmp_path: pathlib.Path) -> None: root, _ = _init_repo(tmp_path) _make_commit(root, fake_id("repo")) runner.invoke(cli, ["label", "add", "key:Am"], env=_env(root)) result = runner.invoke(cli, ["label", "remove", "key:Am"], env=_env(root)) assert result.exit_code == 0 # --------------------------------------------------------------------------- # LB_06 — muse tag no longer handles label annotations # --------------------------------------------------------------------------- class TestLB06TagNoLongerHandlesLabels: """LB_06: ``muse tag`` subcommands are add/list/read/delete/latest (version tags only).""" def test_tag_add_requires_version_format(self, tmp_path: pathlib.Path) -> None: root, _ = _init_repo(tmp_path) _make_commit(root, fake_id("repo")) result = runner.invoke(cli, ["tag", "add", "v1.0.0"], env=_env(root)) assert result.exit_code == 0 def test_tag_add_emotion_label_rejected(self, tmp_path: pathlib.Path) -> None: root, _ = _init_repo(tmp_path) _make_commit(root, fake_id("repo")) result = runner.invoke(cli, ["tag", "add", "emotion:joyful"], env=_env(root)) assert result.exit_code != 0 def test_tag_list_returns_tags_key(self, tmp_path: pathlib.Path) -> None: root, _ = _init_repo(tmp_path) _make_commit(root, fake_id("repo")) result = runner.invoke(cli, ["tag", "list", "--json"], env=_env(root)) assert result.exit_code == 0 data = json.loads(result.output) assert "tags" in data assert "labels" not in data # --------------------------------------------------------------------------- # LB_07 — label and tag coexist without interference # --------------------------------------------------------------------------- class TestLB07LabelAndTagCoexist: """LB_07: ``muse label`` and ``muse tag`` store data in separate directories.""" def test_label_storage_separate_from_version_tags(self, tmp_path: pathlib.Path) -> None: root, _ = _init_repo(tmp_path) _make_commit(root, fake_id("repo")) runner.invoke(cli, ["label", "add", "emotion:happy"], env=_env(root)) runner.invoke(cli, ["tag", "add", "v1.0.0"], env=_env(root)) label_result = runner.invoke(cli, ["label", "list", "--json"], env=_env(root)) tag_result = runner.invoke(cli, ["tag", "list", "--json"], env=_env(root)) labels = json.loads(label_result.output) tags = json.loads(tag_result.output) assert labels["total"] == 1 assert tags["total"] == 1 assert labels["labels"][0]["label"] == "emotion:happy" assert tags["tags"][0]["tag"] == "v1.0.0" def test_label_dir_is_dot_muse_tags(self, tmp_path: pathlib.Path) -> None: root, _ = _init_repo(tmp_path) _make_commit(root, fake_id("repo")) runner.invoke(cli, ["label", "add", "section:chorus"], env=_env(root)) tags_dir = root / ".muse" / "tags" assert tags_dir.exists() def test_version_tag_dir_is_dot_muse_version_tags(self, tmp_path: pathlib.Path) -> None: root, _ = _init_repo(tmp_path) _make_commit(root, fake_id("repo")) runner.invoke(cli, ["tag", "add", "v2.0.0"], env=_env(root)) version_tags_dir = root / ".muse" / "version-tags" assert version_tags_dir.exists() # --------------------------------------------------------------------------- # LB_08 — label round-trip (add → list → remove) # --------------------------------------------------------------------------- class TestLB08LabelRoundTrip: """LB_08: full round-trip for label annotations.""" def test_round_trip(self, tmp_path: pathlib.Path) -> None: root, _ = _init_repo(tmp_path) _make_commit(root, fake_id("repo")) add_result = runner.invoke(cli, ["label", "add", "emotion:joyful", "--json"], env=_env(root)) assert add_result.exit_code == 0 add_data = json.loads(add_result.output) assert add_data["status"] == "labelled" assert add_data["label_id"].startswith("sha256:") list_result = runner.invoke(cli, ["label", "list", "--json"], env=_env(root)) list_data = json.loads(list_result.output) assert list_data["total"] == 1 assert list_data["labels"][0]["label"] == "emotion:joyful" assert list_data["labels"][0]["label_id"] == add_data["label_id"] remove_result = runner.invoke(cli, ["label", "remove", "emotion:joyful", "--json"], env=_env(root)) remove_data = json.loads(remove_result.output) assert remove_data["status"] == "removed" assert remove_data["removed_count"] == 1 assert add_data["label_id"] in remove_data["label_ids"] list_after = runner.invoke(cli, ["label", "list", "--json"], env=_env(root)) assert json.loads(list_after.output)["total"] == 0 def test_multiple_labels_round_trip(self, tmp_path: pathlib.Path) -> None: root, _ = _init_repo(tmp_path) _make_commit(root, fake_id("repo")) for label in ["emotion:joyful", "section:chorus", "key:Am", "tempo:120bpm"]: result = runner.invoke(cli, ["label", "add", label, "--json"], env=_env(root)) assert json.loads(result.output)["status"] == "labelled" list_result = runner.invoke(cli, ["label", "list", "--json"], env=_env(root)) data = json.loads(list_result.output) assert data["total"] == 4 label_names = {e["label"] for e in data["labels"]} assert label_names == {"emotion:joyful", "section:chorus", "key:Am", "tempo:120bpm"}