CmdForge/tests/test_collection.py

299 lines
11 KiB
Python

"""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