299 lines
11 KiB
Python
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
|