claude-os/tests/test_skill_manager.py
2025-12-11 14:07:58 -06:00

802 lines
31 KiB
Python

"""
Tests for skill_manager.py - Skill management functionality.
"""
import json
import pytest
import tempfile
from pathlib import Path
from datetime import datetime
from unittest.mock import patch, MagicMock, AsyncMock
import httpx
from app.core.skill_manager import (
Skill,
SkillTemplate,
SkillManager,
CommunitySkill,
CommunitySkillsManager,
COMMUNITY_SOURCES,
list_skills,
list_skill_templates,
list_community_skills,
install_community_skill,
get_community_manager,
)
class TestSkillDataclass:
"""Tests for Skill dataclass."""
def test_skill_creation(self):
"""Test basic Skill creation."""
skill = Skill(
name="test-skill",
path="/path/to/skill",
description="A test skill",
scope="global",
source="custom"
)
assert skill.name == "test-skill"
assert skill.path == "/path/to/skill"
assert skill.description == "A test skill"
assert skill.scope == "global"
assert skill.source == "custom"
assert skill.content == ""
assert skill.enabled is True
assert skill.category is None
assert skill.tags == []
def test_skill_with_all_fields(self):
"""Test Skill with all fields populated."""
skill = Skill(
name="full-skill",
path="/path/to/skill",
description="Full skill",
scope="project",
source="template",
content="# Skill content",
enabled=False,
category="testing",
tags=["test", "demo"],
created="2025-01-01T00:00:00",
modified="2025-01-02T00:00:00"
)
assert skill.enabled is False
assert skill.category == "testing"
assert skill.tags == ["test", "demo"]
assert skill.created == "2025-01-01T00:00:00"
assert skill.modified == "2025-01-02T00:00:00"
def test_skill_to_dict(self):
"""Test Skill.to_dict() method."""
skill = Skill(
name="test-skill",
path="/path",
description="Test",
scope="global",
source="custom",
category="testing",
tags=["a", "b"]
)
result = skill.to_dict()
assert isinstance(result, dict)
assert result["name"] == "test-skill"
assert result["path"] == "/path"
assert result["description"] == "Test"
assert result["scope"] == "global"
assert result["source"] == "custom"
assert result["category"] == "testing"
assert result["tags"] == ["a", "b"]
class TestSkillTemplateDataclass:
"""Tests for SkillTemplate dataclass."""
def test_template_creation(self):
"""Test basic SkillTemplate creation."""
template = SkillTemplate(
name="template-name",
category="productivity",
description="A template",
path="/path/to/template"
)
assert template.name == "template-name"
assert template.category == "productivity"
assert template.description == "A template"
assert template.path == "/path/to/template"
assert template.tags == []
assert template.version == "1.0.0"
def test_template_to_dict(self):
"""Test SkillTemplate.to_dict() method."""
template = SkillTemplate(
name="test",
category="test-cat",
description="Test template",
path="/path",
tags=["tag1"],
version="2.0.0"
)
result = template.to_dict()
assert result["name"] == "test"
assert result["category"] == "test-cat"
assert result["description"] == "Test template"
assert result["tags"] == ["tag1"]
assert result["version"] == "2.0.0"
class TestCommunitySkillDataclass:
"""Tests for CommunitySkill dataclass."""
def test_community_skill_creation(self):
"""Test CommunitySkill creation."""
skill = CommunitySkill(
name="frontend-design",
description="Design skills",
source="anthropic",
repo="anthropics/skills",
path="skills/frontend-design"
)
assert skill.name == "frontend-design"
assert skill.source == "anthropic"
assert skill.repo == "anthropics/skills"
assert skill.tags == []
assert skill.url == ""
def test_community_skill_to_dict(self):
"""Test CommunitySkill.to_dict() method."""
skill = CommunitySkill(
name="test-skill",
description="Test",
source="superpowers",
repo="obra/superpowers",
path="skills/test",
tags=["testing"],
url="https://github.com/obra/superpowers"
)
result = skill.to_dict()
assert result["name"] == "test-skill"
assert result["source"] == "superpowers"
assert result["repo"] == "obra/superpowers"
assert result["tags"] == ["testing"]
assert result["url"] == "https://github.com/obra/superpowers"
class TestSkillManager:
"""Tests for SkillManager class."""
@pytest.fixture
def temp_dir(self):
"""Create a temporary directory for skills."""
with tempfile.TemporaryDirectory() as tmpdir:
yield Path(tmpdir)
@pytest.fixture
def skill_manager(self, temp_dir):
"""Create SkillManager with mocked paths."""
with patch.object(SkillManager, 'GLOBAL_SKILLS_DIR', temp_dir / "global_skills"):
with patch.object(SkillManager, 'TEMPLATES_DIR', temp_dir / "templates"):
manager = SkillManager(project_path=str(temp_dir / "project"))
manager.GLOBAL_SKILLS_DIR.mkdir(parents=True, exist_ok=True)
yield manager
def test_init_without_project(self, temp_dir):
"""Test SkillManager initialization without project path."""
with patch.object(SkillManager, 'GLOBAL_SKILLS_DIR', temp_dir / "global"):
manager = SkillManager()
assert manager.project_path is None
assert manager.project_skills_dir is None
def test_init_with_project(self, temp_dir):
"""Test SkillManager initialization with project path."""
with patch.object(SkillManager, 'GLOBAL_SKILLS_DIR', temp_dir / "global"):
manager = SkillManager(project_path=str(temp_dir / "myproject"))
assert manager.project_path == temp_dir / "myproject"
assert manager.project_skills_dir == temp_dir / "myproject" / ".claude" / "skills"
def test_list_global_skills_empty(self, skill_manager):
"""Test listing global skills when none exist."""
skills = skill_manager.list_global_skills()
assert skills == []
def test_list_global_skills_with_skills(self, skill_manager):
"""Test listing global skills with existing skills."""
# Create a skill directory
skill_dir = skill_manager.GLOBAL_SKILLS_DIR / "test-skill"
skill_dir.mkdir(parents=True)
# Create skill.md
(skill_dir / "skill.md").write_text("# Test Skill\n\nThis is a test skill.")
skills = skill_manager.list_global_skills()
assert len(skills) == 1
assert skills[0].name == "test-skill"
assert skills[0].scope == "global"
def test_list_global_skills_with_metadata(self, skill_manager):
"""Test listing global skills with metadata.json."""
skill_dir = skill_manager.GLOBAL_SKILLS_DIR / "documented-skill"
skill_dir.mkdir(parents=True)
# Create metadata
metadata = {
"name": "documented-skill",
"description": "A well documented skill",
"category": "productivity",
"tags": ["demo", "test"],
"source": "custom"
}
(skill_dir / "metadata.json").write_text(json.dumps(metadata))
(skill_dir / "skill.md").write_text("Skill content")
skills = skill_manager.list_global_skills()
assert len(skills) == 1
assert skills[0].description == "A well documented skill"
assert skills[0].category == "productivity"
assert skills[0].tags == ["demo", "test"]
def test_list_project_skills_no_project(self, temp_dir):
"""Test listing project skills without project path."""
with patch.object(SkillManager, 'GLOBAL_SKILLS_DIR', temp_dir / "global"):
manager = SkillManager()
skills = manager.list_project_skills()
assert skills == []
def test_list_project_skills_with_skills(self, skill_manager, temp_dir):
"""Test listing project skills with existing skills."""
# Create project skills directory
project_skills_dir = temp_dir / "project" / ".claude" / "skills" / "my-skill"
project_skills_dir.mkdir(parents=True)
(project_skills_dir / "skill.md").write_text("# My Skill")
skills = skill_manager.list_project_skills()
assert len(skills) == 1
assert skills[0].name == "my-skill"
assert skills[0].scope == "project"
def test_list_all_skills(self, skill_manager, temp_dir):
"""Test listing all skills (global and project)."""
# Create global skill
global_skill = skill_manager.GLOBAL_SKILLS_DIR / "global-skill"
global_skill.mkdir(parents=True)
(global_skill / "skill.md").write_text("Global")
# Create project skill
project_skill = temp_dir / "project" / ".claude" / "skills" / "project-skill"
project_skill.mkdir(parents=True)
(project_skill / "skill.md").write_text("Project")
all_skills = skill_manager.list_all_skills()
assert "global" in all_skills
assert "project" in all_skills
assert len(all_skills["global"]) == 1
assert len(all_skills["project"]) == 1
def test_get_skill_global(self, skill_manager):
"""Test getting a global skill by name."""
skill_dir = skill_manager.GLOBAL_SKILLS_DIR / "my-skill"
skill_dir.mkdir(parents=True)
(skill_dir / "skill.md").write_text("# Skill Content\n\nDetails here")
skill = skill_manager.get_skill("my-skill", "global")
assert skill is not None
assert skill.name == "my-skill"
assert "Skill Content" in skill.content
def test_get_skill_not_found(self, skill_manager):
"""Test getting a non-existent skill."""
skill = skill_manager.get_skill("nonexistent", "global")
assert skill is None
def test_get_skill_invalid_scope(self, skill_manager):
"""Test getting skill with invalid scope."""
skill = skill_manager.get_skill("test", "invalid")
assert skill is None
def test_create_skill(self, skill_manager, temp_dir):
"""Test creating a new custom skill."""
# Ensure project skills directory can be created
skill = skill_manager.create_skill(
name="new-skill",
description="A new skill",
content="# New Skill\n\nContent here",
category="productivity",
tags=["new", "test"]
)
assert skill.name == "new-skill"
assert skill.description == "A new skill"
assert skill.category == "productivity"
assert skill.scope == "project"
# Verify files were created
skill_path = temp_dir / "project" / ".claude" / "skills" / "new-skill"
assert (skill_path / "skill.md").exists()
assert (skill_path / "metadata.json").exists()
def test_create_skill_no_project(self, temp_dir):
"""Test creating skill without project path."""
with patch.object(SkillManager, 'GLOBAL_SKILLS_DIR', temp_dir / "global"):
manager = SkillManager()
with pytest.raises(ValueError, match="No project path set"):
manager.create_skill("test", "desc", "content")
def test_create_skill_already_exists(self, skill_manager, temp_dir):
"""Test creating skill that already exists."""
# Create existing skill
skill_path = temp_dir / "project" / ".claude" / "skills" / "existing"
skill_path.mkdir(parents=True)
(skill_path / "skill.md").write_text("Existing")
with pytest.raises(FileExistsError, match="Skill already exists"):
skill_manager.create_skill("existing", "desc", "content")
def test_update_skill(self, skill_manager, temp_dir):
"""Test updating an existing skill."""
# Create skill first
skill_path = temp_dir / "project" / ".claude" / "skills" / "update-me"
skill_path.mkdir(parents=True)
(skill_path / "skill.md").write_text("Original content")
(skill_path / "metadata.json").write_text(json.dumps({"name": "update-me"}))
# Update it
skill = skill_manager.update_skill(
name="update-me",
content="Updated content",
description="New description",
tags=["updated"]
)
assert skill.name == "update-me"
assert "Updated content" in (skill_path / "skill.md").read_text()
def test_update_skill_not_found(self, skill_manager):
"""Test updating non-existent skill."""
with pytest.raises(FileNotFoundError, match="Skill not found"):
skill_manager.update_skill("nonexistent", content="new")
def test_delete_skill(self, skill_manager, temp_dir):
"""Test deleting a skill."""
# Create skill
skill_path = temp_dir / "project" / ".claude" / "skills" / "delete-me"
skill_path.mkdir(parents=True)
(skill_path / "skill.md").write_text("Delete me")
result = skill_manager.delete_skill("delete-me")
assert result is True
assert not skill_path.exists()
def test_delete_skill_not_found(self, skill_manager):
"""Test deleting non-existent skill."""
with pytest.raises(FileNotFoundError, match="Skill not found"):
skill_manager.delete_skill("nonexistent")
def test_list_templates(self, skill_manager, temp_dir):
"""Test listing skill templates."""
# Create template directories
templates_dir = skill_manager.TEMPLATES_DIR
templates_dir.mkdir(parents=True)
category_dir = templates_dir / "productivity"
category_dir.mkdir()
template_dir = category_dir / "time-tracker"
template_dir.mkdir()
(template_dir / "skill.md").write_text("# Time Tracker\n\nTrack your time.")
(template_dir / "metadata.json").write_text(json.dumps({
"name": "time-tracker",
"description": "Track your time"
}))
templates = skill_manager.list_templates()
assert len(templates) == 1
assert templates[0].name == "time-tracker"
assert templates[0].category == "productivity"
def test_list_templates_by_category(self, skill_manager, temp_dir):
"""Test listing templates filtered by category."""
templates_dir = skill_manager.TEMPLATES_DIR
templates_dir.mkdir(parents=True)
# Create two categories
for cat in ["productivity", "testing"]:
cat_dir = templates_dir / cat
cat_dir.mkdir()
(cat_dir / "skill-1").mkdir()
(cat_dir / "skill-1" / "skill.md").write_text(f"# {cat} skill")
# Filter by category
templates = skill_manager.list_templates(category="testing")
assert len(templates) == 1
assert templates[0].category == "testing"
def test_get_template_categories(self, skill_manager, temp_dir):
"""Test getting template categories."""
templates_dir = skill_manager.TEMPLATES_DIR
templates_dir.mkdir(parents=True)
for cat in ["productivity", "testing", "debugging"]:
(templates_dir / cat).mkdir()
categories = skill_manager.get_template_categories()
assert sorted(categories) == ["debugging", "productivity", "testing"]
def test_install_template(self, skill_manager, temp_dir):
"""Test installing a template to project."""
# Create template
templates_dir = skill_manager.TEMPLATES_DIR
templates_dir.mkdir(parents=True)
cat_dir = templates_dir / "testing"
cat_dir.mkdir()
template_dir = cat_dir / "tdd-skill"
template_dir.mkdir()
(template_dir / "skill.md").write_text("# TDD Skill")
(template_dir / "metadata.json").write_text(json.dumps({
"name": "tdd-skill",
"description": "Test-driven development"
}))
# Install it
skill = skill_manager.install_template("tdd-skill")
assert skill.name == "tdd-skill"
assert skill.scope == "project"
# Verify installed
installed_path = temp_dir / "project" / ".claude" / "skills" / "tdd-skill"
assert installed_path.exists()
def test_install_template_with_custom_name(self, skill_manager, temp_dir):
"""Test installing template with custom name."""
templates_dir = skill_manager.TEMPLATES_DIR
templates_dir.mkdir(parents=True)
cat_dir = templates_dir / "productivity"
cat_dir.mkdir()
template_dir = cat_dir / "original-name"
template_dir.mkdir()
(template_dir / "skill.md").write_text("# Skill")
(template_dir / "metadata.json").write_text(json.dumps({"name": "original-name"}))
skill = skill_manager.install_template("original-name", custom_name="my-custom-name")
assert skill.name == "my-custom-name"
def test_install_template_not_found(self, skill_manager):
"""Test installing non-existent template."""
with pytest.raises(ValueError, match="Template not found"):
skill_manager.install_template("nonexistent")
def test_parse_skill_with_symlink(self, skill_manager, temp_dir):
"""Test parsing skill that is a symlink."""
# Create actual skill
real_skill = temp_dir / "real-skill"
real_skill.mkdir()
(real_skill / "skill.md").write_text("# Real Skill")
# Create symlink
link_path = skill_manager.GLOBAL_SKILLS_DIR / "linked-skill"
link_path.symlink_to(real_skill)
skills = skill_manager.list_global_skills()
assert len(skills) == 1
assert skills[0].name == "linked-skill"
def test_parse_skill_extracts_description_from_content(self, skill_manager):
"""Test that description is extracted from content if not in metadata."""
skill_dir = skill_manager.GLOBAL_SKILLS_DIR / "no-meta"
skill_dir.mkdir(parents=True)
(skill_dir / "skill.md").write_text("# Header\n\nThis is the description paragraph.\n\nMore content.")
skill = skill_manager.get_skill("no-meta", "global")
assert skill is not None
assert "This is the description paragraph" in skill.description
def test_core_skills_marked_correctly(self, skill_manager):
"""Test that core skills are marked with correct source."""
# Create a core skill (memory is in CORE_SKILLS)
skill_dir = skill_manager.GLOBAL_SKILLS_DIR / "memory"
skill_dir.mkdir(parents=True)
(skill_dir / "skill.md").write_text("Memory skill")
skills = skill_manager.list_global_skills()
memory_skill = next(s for s in skills if s.name == "memory")
assert memory_skill.source == "claude-os-core"
class TestCommunitySkillsManager:
"""Tests for CommunitySkillsManager class."""
@pytest.fixture
def manager(self):
"""Create CommunitySkillsManager instance."""
return CommunitySkillsManager()
def test_cache_validity(self, manager):
"""Test cache validity checking."""
# Initially cache should be invalid
assert manager._is_cache_valid("test_key") is False
# After setting, should be valid
manager._set_cache("test_key", "value")
assert manager._is_cache_valid("test_key") is True
def test_set_cache(self, manager):
"""Test setting cache values."""
manager._set_cache("key1", {"data": "value"})
assert manager._cache["key1"] == {"data": "value"}
assert "key1" in manager._cache_times
def test_extract_description_from_frontmatter(self, manager):
"""Test extracting description from YAML frontmatter."""
content = """---
description: "This is the skill description"
version: 1.0
---
# Skill Title
Content here."""
description = manager._extract_description(content)
assert description == "This is the skill description"
def test_extract_description_from_paragraph(self, manager):
"""Test extracting description from first paragraph."""
content = """# Skill Title
This is the first paragraph that should be used as description.
More content here."""
description = manager._extract_description(content)
assert description == "This is the first paragraph that should be used as description."
def test_extract_description_truncates_long_text(self, manager):
"""Test that description is truncated to 200 chars."""
long_text = "A" * 500
content = f"# Title\n\n{long_text}"
description = manager._extract_description(content)
assert len(description) == 200
def test_infer_tags_testing(self, manager):
"""Test tag inference for testing-related skills."""
tags = manager._infer_tags("tdd-helper", "Test-driven development workflow")
assert "testing" in tags
def test_infer_tags_frontend(self, manager):
"""Test tag inference for frontend skills."""
tags = manager._infer_tags("react-components", "Build React UI components")
assert "frontend" in tags
def test_infer_tags_multiple(self, manager):
"""Test inferring multiple tags."""
tags = manager._infer_tags("debug-tests", "Debug and fix failing tests")
assert "testing" in tags
assert "debugging" in tags
@pytest.mark.asyncio
async def test_list_community_skills_cached(self, manager):
"""Test that cache is used when valid."""
# The _fetch_skills_from_repo method checks cache internally
# So we need to test that _is_cache_valid returns True
cached_skills = [
CommunitySkill("skill1", "desc", "anthropic", "anthropics/skills", "skills/skill1"),
]
# Set cache directly and verify it's valid
manager._set_cache("skills_anthropic", cached_skills)
assert manager._is_cache_valid("skills_anthropic") is True
# The internal _fetch_skills_from_repo will return cached data
# We verify by checking the cache mechanism works
assert manager._cache["skills_anthropic"] == cached_skills
@pytest.mark.asyncio
async def test_fetch_skills_from_repo_success(self, manager):
"""Test fetching skills from GitHub repo."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = [
{"type": "dir", "name": "skill-one"},
{"type": "dir", "name": "skill-two"},
{"type": "file", "name": "README.md"}, # Should be skipped
]
with patch.object(httpx.AsyncClient, 'get', new_callable=AsyncMock) as mock_get:
mock_get.return_value = mock_response
# Also mock description fetching
with patch.object(manager, '_fetch_skill_description', new_callable=AsyncMock) as mock_desc:
mock_desc.return_value = "Skill description"
skills = await manager._fetch_skills_from_repo(
"anthropic",
{"repo": "anthropics/skills", "skills_path": "skills"}
)
assert len(skills) == 2
assert skills[0].name == "skill-one"
assert skills[1].name == "skill-two"
@pytest.mark.asyncio
async def test_fetch_skills_from_repo_api_error(self, manager):
"""Test handling GitHub API errors."""
mock_response = MagicMock()
mock_response.status_code = 404
with patch.object(httpx.AsyncClient, 'get', new_callable=AsyncMock) as mock_get:
mock_get.return_value = mock_response
skills = await manager._fetch_skills_from_repo(
"anthropic",
{"repo": "anthropics/skills", "skills_path": "skills"}
)
assert skills == []
@pytest.mark.asyncio
async def test_get_skill_content(self, manager):
"""Test fetching skill content from GitHub."""
# Mock directory listing
dir_response = MagicMock()
dir_response.status_code = 200
dir_response.json.return_value = [
{"type": "file", "name": "skill.md"},
{"type": "file", "name": "metadata.json"},
]
# Mock file content
file_response = MagicMock()
file_response.status_code = 200
file_response.text = "File content"
with patch.object(httpx.AsyncClient, 'get', new_callable=AsyncMock) as mock_get:
mock_get.side_effect = [dir_response, file_response, file_response]
contents = await manager.get_skill_content("anthropics/skills", "skills/test")
assert "skill.md" in contents
assert "metadata.json" in contents
@pytest.mark.asyncio
async def test_install_community_skill(self, manager):
"""Test installing a community skill."""
skill = CommunitySkill(
name="test-skill",
description="Test",
source="anthropic",
repo="anthropics/skills",
path="skills/test-skill",
tags=["testing"]
)
with tempfile.TemporaryDirectory() as tmpdir:
# Mock get_skill_content
with patch.object(manager, 'get_skill_content', new_callable=AsyncMock) as mock_content:
mock_content.return_value = {
"SKILL.md": "# Skill Content",
"metadata.json": json.dumps({"name": "test-skill"})
}
installed = await manager.install_community_skill(skill, tmpdir)
assert installed.name == "test-skill"
assert installed.scope == "project"
# Verify files created
skill_path = Path(tmpdir) / ".claude" / "skills" / "test-skill"
assert skill_path.exists()
assert (skill_path / "skill.md").exists() # Normalized from SKILL.md
@pytest.mark.asyncio
async def test_install_community_skill_already_exists(self, manager):
"""Test installing skill that already exists."""
skill = CommunitySkill("existing", "desc", "anthropic", "repo", "path")
with tempfile.TemporaryDirectory() as tmpdir:
# Create existing skill
existing = Path(tmpdir) / ".claude" / "skills" / "existing"
existing.mkdir(parents=True)
# Mock get_skill_content to avoid HTTP request
with patch.object(manager, 'get_skill_content', new_callable=AsyncMock) as mock_content:
mock_content.return_value = {"skill.md": "Content"}
with pytest.raises(FileExistsError, match="Skill already exists"):
await manager.install_community_skill(skill, tmpdir)
@pytest.mark.asyncio
async def test_install_community_skill_empty_content(self, manager):
"""Test installing skill with no content."""
skill = CommunitySkill("empty", "desc", "anthropic", "repo", "path")
with tempfile.TemporaryDirectory() as tmpdir:
with patch.object(manager, 'get_skill_content', new_callable=AsyncMock) as mock_content:
mock_content.return_value = {}
with pytest.raises(ValueError, match="No content found"):
await manager.install_community_skill(skill, tmpdir)
class TestConvenienceFunctions:
"""Tests for module-level convenience functions."""
def test_list_skills(self):
"""Test list_skills function."""
with tempfile.TemporaryDirectory() as tmpdir:
with patch.object(SkillManager, 'GLOBAL_SKILLS_DIR', Path(tmpdir) / "global"):
Path(tmpdir, "global").mkdir()
result = list_skills()
assert "global" in result
assert "project" in result
assert isinstance(result["global"], list)
assert isinstance(result["project"], list)
def test_list_skill_templates(self):
"""Test list_skill_templates function."""
with tempfile.TemporaryDirectory() as tmpdir:
templates_dir = Path(tmpdir) / "templates"
templates_dir.mkdir()
with patch.object(SkillManager, 'TEMPLATES_DIR', templates_dir):
result = list_skill_templates()
assert isinstance(result, list)
def test_get_community_manager_singleton(self):
"""Test that get_community_manager returns singleton."""
manager1 = get_community_manager()
manager2 = get_community_manager()
assert manager1 is manager2
@pytest.mark.asyncio
async def test_list_community_skills_function(self):
"""Test list_community_skills convenience function."""
mock_skills = [CommunitySkill("test", "desc", "anthropic", "repo", "path")]
with patch.object(CommunitySkillsManager, 'list_community_skills', new_callable=AsyncMock) as mock_list:
mock_list.return_value = mock_skills
result = await list_community_skills()
assert len(result) == 1
assert result[0]["name"] == "test"
@pytest.mark.asyncio
async def test_install_community_skill_function(self):
"""Test install_community_skill convenience function."""
mock_skill = CommunitySkill("test-skill", "desc", "anthropic", "repo", "path")
mock_installed = Skill("test-skill", "/path", "desc", "project", "community:anthropic")
with patch.object(CommunitySkillsManager, 'list_community_skills', new_callable=AsyncMock) as mock_list:
mock_list.return_value = [mock_skill]
with patch.object(CommunitySkillsManager, 'install_community_skill', new_callable=AsyncMock) as mock_install:
mock_install.return_value = mock_installed
result = await install_community_skill("anthropic", "test-skill", "/project")
assert result["name"] == "test-skill"
@pytest.mark.asyncio
async def test_install_community_skill_not_found(self):
"""Test install_community_skill with non-existent skill."""
with patch.object(CommunitySkillsManager, 'list_community_skills', new_callable=AsyncMock) as mock_list:
mock_list.return_value = []
with pytest.raises(ValueError, match="Skill not found"):
await install_community_skill("anthropic", "nonexistent", "/project")
class TestCommunitySourcesConfig:
"""Tests for COMMUNITY_SOURCES configuration."""
def test_anthropic_source_config(self):
"""Test Anthropic source is configured correctly."""
assert "anthropic" in COMMUNITY_SOURCES
assert COMMUNITY_SOURCES["anthropic"]["repo"] == "anthropics/skills"
def test_superpowers_source_config(self):
"""Test Superpowers source is configured correctly."""
assert "superpowers" in COMMUNITY_SOURCES
assert COMMUNITY_SOURCES["superpowers"]["repo"] == "obra/superpowers"