"""Tests for collection.py - Collection definitions and management.""" import tempfile from pathlib import Path from unittest.mock import patch, MagicMock import pytest import yaml from cmdforge.collection import ( Collection, list_collections, get_collection, COLLECTIONS_DIR, classify_tool_reference, resolve_tool_references, ToolResolutionResult ) @pytest.fixture def temp_collections_dir(tmp_path): """Create a temporary collections directory.""" with patch('cmdforge.collection.COLLECTIONS_DIR', tmp_path): yield tmp_path class TestCollection: """Tests for Collection dataclass.""" def test_create_basic(self): coll = Collection(name="my-tools", display_name="My Tools") assert coll.name == "my-tools" assert coll.display_name == "My Tools" assert coll.description == "" assert coll.tools == [] assert coll.pinned == {} assert coll.tags == [] assert coll.published is False def test_create_full(self): coll = Collection( name="dev-tools", display_name="Developer Tools", description="Essential tools for developers", maintainer="rob", tools=["summarize", "official/code-review"], pinned={"official/code-review": "^1.0.0"}, tags=["dev", "productivity"], published=True, registry_name="dev-tools" ) assert coll.name == "dev-tools" assert coll.maintainer == "rob" assert len(coll.tools) == 2 assert coll.pinned["official/code-review"] == "^1.0.0" assert coll.published is True def test_to_dict(self): coll = Collection( name="test-coll", display_name="Test Collection", description="Test", tools=["tool1"], tags=["test"] ) d = coll.to_dict() assert d["name"] == "test-coll" assert d["display_name"] == "Test Collection" assert d["tools"] == ["tool1"] assert d["tags"] == ["test"] assert d["published"] is False def test_save_and_load(self, temp_collections_dir): coll = Collection( name="my-coll", display_name="My Collection", description="A test collection", tools=["tool1", "owner/tool2"], pinned={"owner/tool2": "1.0.0"}, tags=["test"] ) coll.save() # Verify file exists path = temp_collections_dir / "my-coll.yaml" assert path.exists() # Load and verify loaded = Collection.load("my-coll") assert loaded is not None assert loaded.name == "my-coll" assert loaded.display_name == "My Collection" assert loaded.tools == ["tool1", "owner/tool2"] assert loaded.pinned == {"owner/tool2": "1.0.0"} def test_load_nonexistent(self, temp_collections_dir): loaded = Collection.load("nonexistent") assert loaded is None def test_load_empty_yaml(self, temp_collections_dir): # Create empty YAML file path = temp_collections_dir / "empty.yaml" path.write_text("") loaded = Collection.load("empty") assert loaded is not None assert loaded.name == "empty" def test_delete(self, temp_collections_dir): coll = Collection(name="to-delete", display_name="To Delete") coll.save() path = temp_collections_dir / "to-delete.yaml" assert path.exists() coll.delete() assert not path.exists() def test_get_registry_url(self): with patch('cmdforge.collection.get_registry_url', return_value="https://cmdforge.brrd.tech/api/v1"): coll = Collection(name="my-coll", display_name="My Coll") url = coll.get_registry_url() assert url == "https://cmdforge.brrd.tech/collections/my-coll" def test_get_registry_url_custom_base(self): with patch('cmdforge.collection.get_registry_url', return_value="https://custom.example.com/api/v1"): coll = Collection(name="my-coll", display_name="My Coll") url = coll.get_registry_url() assert url == "https://custom.example.com/collections/my-coll" def test_get_registry_url_no_api_suffix(self): with patch('cmdforge.collection.get_registry_url', return_value="https://cmdforge.brrd.tech"): coll = Collection(name="my-coll", display_name="My Coll") url = coll.get_registry_url() assert url == "https://cmdforge.brrd.tech/collections/my-coll" def test_get_registry_url_uses_registry_name(self): with patch('cmdforge.collection.get_registry_url', return_value="https://cmdforge.brrd.tech/api/v1"): coll = Collection(name="local-name", display_name="My Coll", registry_name="published-name") url = coll.get_registry_url() assert url == "https://cmdforge.brrd.tech/collections/published-name" class TestListCollections: """Tests for list_collections function.""" def test_empty_dir(self, temp_collections_dir): result = list_collections() assert result == [] def test_with_collections(self, temp_collections_dir): # Create some collections Collection(name="alpha", display_name="Alpha").save() Collection(name="beta", display_name="Beta").save() Collection(name="gamma", display_name="Gamma").save() result = list_collections() assert result == ["alpha", "beta", "gamma"] # Sorted def test_ignores_non_yaml_files(self, temp_collections_dir): Collection(name="valid", display_name="Valid").save() (temp_collections_dir / "not-yaml.txt").write_text("ignored") result = list_collections() assert result == ["valid"] class TestGetCollection: """Tests for get_collection function.""" def test_existing(self, temp_collections_dir): Collection(name="test", display_name="Test", description="Desc").save() coll = get_collection("test") assert coll is not None assert coll.name == "test" assert coll.description == "Desc" def test_nonexistent(self, temp_collections_dir): coll = get_collection("nonexistent") assert coll is None class TestClassifyToolReference: """Tests for classify_tool_reference function.""" def test_local_tool(self): owner, name, is_local = classify_tool_reference("summarize") assert owner == "" assert name == "summarize" assert is_local is True def test_registry_tool(self): owner, name, is_local = classify_tool_reference("official/summarize") assert owner == "official" assert name == "summarize" assert is_local is False def test_registry_tool_nested_name(self): owner, name, is_local = classify_tool_reference("user/my-tool") assert owner == "user" assert name == "my-tool" assert is_local is False class TestResolveToolReferences: """Tests for resolve_tool_references function.""" @pytest.fixture def mock_client(self): client = MagicMock() client.get_my_tool_status.side_effect = lambda name: {"status": "approved"} client.has_approved_public_tool.return_value = True return client def test_all_registry_tools(self, mock_client): tools = ["official/tool1", "user/tool2"] pinned = {"official/tool1": "^1.0.0"} result = resolve_tool_references(tools, pinned, "myuser", mock_client) assert result.registry_refs == ["official/tool1", "user/tool2"] assert result.transformed_pinned == {"official/tool1": "^1.0.0"} assert result.local_unpublished == [] def test_local_tool_not_published(self, mock_client): from cmdforge.registry_client import RegistryError mock_client.get_my_tool_status.side_effect = RegistryError( code="TOOL_NOT_FOUND", message="Not found" ) with patch('cmdforge.collection._get_tool_visibility_from_yaml', return_value='public'): tools = ["my-local-tool"] result = resolve_tool_references(tools, {}, "myuser", mock_client) assert "myuser/my-local-tool" in result.registry_refs assert "my-local-tool" in result.local_unpublished def test_local_tool_published_approved(self, mock_client): mock_client.get_my_tool_status.return_value = {"status": "approved"} with patch('cmdforge.collection._get_tool_visibility_from_yaml', return_value='public'): tools = ["my-tool"] result = resolve_tool_references(tools, {}, "myuser", mock_client) assert "myuser/my-tool" in result.registry_refs assert result.local_unpublished == [] assert ("my-tool", "approved", True) in result.local_published def test_transforms_pinned_for_local_tools(self, mock_client): mock_client.get_my_tool_status.return_value = {"status": "approved"} with patch('cmdforge.collection._get_tool_visibility_from_yaml', return_value='public'): tools = ["my-tool"] pinned = {"my-tool": "1.0.0"} result = resolve_tool_references(tools, pinned, "myuser", mock_client) assert "myuser/my-tool" in result.transformed_pinned assert result.transformed_pinned["myuser/my-tool"] == "1.0.0" def test_visibility_issues_detected(self, mock_client): with patch('cmdforge.collection._get_tool_visibility_from_yaml', return_value='private'): tools = ["private-tool"] result = resolve_tool_references(tools, {}, "myuser", mock_client) assert ("private-tool", "private") in result.visibility_issues def test_registry_tool_not_approved(self, mock_client): mock_client.has_approved_public_tool.return_value = False tools = ["official/unapproved"] result = resolve_tool_references(tools, {}, "myuser", mock_client) assert ("official/unapproved", "no approved public version") in result.registry_tool_issues class TestToolResolutionResult: """Tests for ToolResolutionResult dataclass.""" def test_empty_result(self): result = ToolResolutionResult( registry_refs=[], transformed_pinned={}, local_unpublished=[], local_published=[], visibility_issues=[], registry_tool_issues=[] ) assert len(result.registry_refs) == 0 assert len(result.local_unpublished) == 0 def test_with_data(self): result = ToolResolutionResult( registry_refs=["user/tool1", "user/tool2"], transformed_pinned={"user/tool1": "^1.0"}, local_unpublished=["tool3"], local_published=[("tool1", "approved", True)], visibility_issues=[("tool4", "private")], registry_tool_issues=[("other/tool", "not found")] ) assert len(result.registry_refs) == 2 assert len(result.local_unpublished) == 1 assert len(result.visibility_issues) == 1