mirror of
https://github.com/pocketpaw/pocketpaw.git
synced 2026-05-22 09:45:00 +00:00
168 lines
6.1 KiB
Python
168 lines
6.1 KiB
Python
# Tests for Feature 6: CreateSkillTool
|
|
# Created: 2026-02-06
|
|
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from pocketpaw.tools.builtin.skill_gen import _VALID_SKILL_NAME, CreateSkillTool
|
|
|
|
|
|
@pytest.fixture
|
|
def tool():
|
|
return CreateSkillTool()
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_skills_dir():
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
yield Path(tmpdir)
|
|
|
|
|
|
class TestCreateSkillTool:
|
|
"""Tests for CreateSkillTool."""
|
|
|
|
def test_name(self, tool):
|
|
assert tool.name == "create_skill"
|
|
|
|
def test_trust_level(self, tool):
|
|
assert tool.trust_level == "high"
|
|
|
|
def test_parameters_schema(self, tool):
|
|
params = tool.parameters
|
|
assert "skill_name" in params["properties"]
|
|
assert "description" in params["properties"]
|
|
assert "instructions" in params["properties"]
|
|
assert "allowed_tools" in params["properties"]
|
|
assert "user_invocable" in params["properties"]
|
|
|
|
def test_valid_skill_names(self):
|
|
assert _VALID_SKILL_NAME.match("my-skill")
|
|
assert _VALID_SKILL_NAME.match("summarize_pr")
|
|
assert _VALID_SKILL_NAME.match("a")
|
|
assert _VALID_SKILL_NAME.match("test123")
|
|
|
|
def test_invalid_skill_names(self):
|
|
assert not _VALID_SKILL_NAME.match("")
|
|
assert not _VALID_SKILL_NAME.match("My-Skill") # uppercase
|
|
assert not _VALID_SKILL_NAME.match("123start") # starts with number
|
|
assert not _VALID_SKILL_NAME.match("has space")
|
|
assert not _VALID_SKILL_NAME.match("-starts-with-dash")
|
|
|
|
@patch("pocketpaw.tools.builtin.skill_gen._get_skills_dir")
|
|
async def test_create_skill_success(self, mock_dir, tool, temp_skills_dir):
|
|
mock_dir.return_value = temp_skills_dir
|
|
|
|
with patch("pocketpaw.skills.get_skill_loader", side_effect=ImportError):
|
|
result = await tool.execute(
|
|
skill_name="test-skill",
|
|
description="A test skill",
|
|
instructions="Do the thing.\nStep 1.\nStep 2.",
|
|
)
|
|
|
|
assert "created successfully" in result
|
|
skill_file = temp_skills_dir / "test-skill" / "SKILL.md"
|
|
assert skill_file.exists()
|
|
|
|
content = skill_file.read_text(encoding="utf-8")
|
|
assert "---" in content
|
|
assert "name: test-skill" in content
|
|
assert "description: A test skill" in content
|
|
assert "user-invocable: true" in content
|
|
assert "Do the thing." in content
|
|
|
|
@patch("pocketpaw.tools.builtin.skill_gen._get_skills_dir")
|
|
async def test_create_skill_with_allowed_tools(self, mock_dir, tool, temp_skills_dir):
|
|
mock_dir.return_value = temp_skills_dir
|
|
|
|
with patch("pocketpaw.skills.get_skill_loader", side_effect=ImportError):
|
|
result = await tool.execute(
|
|
skill_name="code-review",
|
|
description="Review code",
|
|
instructions="Review the code changes.",
|
|
allowed_tools=["read_file", "shell"],
|
|
)
|
|
|
|
assert "created successfully" in result
|
|
content = (temp_skills_dir / "code-review" / "SKILL.md").read_text(encoding="utf-8")
|
|
assert "allowed-tools:" in content
|
|
assert " - read_file" in content
|
|
assert " - shell" in content
|
|
|
|
@patch("pocketpaw.tools.builtin.skill_gen._get_skills_dir")
|
|
async def test_create_skill_not_user_invocable(self, mock_dir, tool, temp_skills_dir):
|
|
mock_dir.return_value = temp_skills_dir
|
|
|
|
with patch("pocketpaw.skills.get_skill_loader", side_effect=ImportError):
|
|
result = await tool.execute(
|
|
skill_name="internal-skill",
|
|
description="Internal only",
|
|
instructions="Do internal stuff.",
|
|
user_invocable=False,
|
|
)
|
|
|
|
assert "created successfully" in result
|
|
content = (temp_skills_dir / "internal-skill" / "SKILL.md").read_text(encoding="utf-8")
|
|
assert "user-invocable: false" in content
|
|
|
|
async def test_invalid_skill_name_rejected(self, tool):
|
|
result = await tool.execute(
|
|
skill_name="Invalid Name!",
|
|
description="Bad name",
|
|
instructions="Nope.",
|
|
)
|
|
assert "Error" in result
|
|
assert "Invalid skill name" in result
|
|
|
|
@patch("pocketpaw.tools.builtin.skill_gen._get_skills_dir")
|
|
async def test_overwrite_protection(self, mock_dir, tool, temp_skills_dir):
|
|
mock_dir.return_value = temp_skills_dir
|
|
|
|
# Pre-create the skill
|
|
skill_dir = temp_skills_dir / "existing-skill"
|
|
skill_dir.mkdir()
|
|
(skill_dir / "SKILL.md").write_text("existing content")
|
|
|
|
result = await tool.execute(
|
|
skill_name="existing-skill",
|
|
description="Overwrite attempt",
|
|
instructions="Should fail.",
|
|
)
|
|
|
|
assert "Error" in result
|
|
assert "already exists" in result
|
|
# Original content preserved
|
|
assert (skill_dir / "SKILL.md").read_text(encoding="utf-8") == "existing content"
|
|
|
|
@patch("pocketpaw.tools.builtin.skill_gen._get_skills_dir")
|
|
async def test_skill_loader_reload_called(self, mock_dir, tool, temp_skills_dir):
|
|
mock_dir.return_value = temp_skills_dir
|
|
|
|
mock_loader = MagicMock()
|
|
with patch("pocketpaw.skills.get_skill_loader", return_value=mock_loader):
|
|
await tool.execute(
|
|
skill_name="reloaded-skill",
|
|
description="Test reload",
|
|
instructions="Content.",
|
|
)
|
|
|
|
mock_loader.reload.assert_called_once()
|
|
|
|
@patch("pocketpaw.tools.builtin.skill_gen._get_skills_dir")
|
|
async def test_yaml_frontmatter_format(self, mock_dir, tool, temp_skills_dir):
|
|
mock_dir.return_value = temp_skills_dir
|
|
|
|
with patch("pocketpaw.skills.get_skill_loader", side_effect=ImportError):
|
|
await tool.execute(
|
|
skill_name="fmt-test",
|
|
description="Format test",
|
|
instructions="Instruction body here.",
|
|
)
|
|
|
|
content = (temp_skills_dir / "fmt-test" / "SKILL.md").read_text(encoding="utf-8")
|
|
# Check frontmatter delimiters
|
|
parts = content.split("---")
|
|
assert len(parts) >= 3 # before, frontmatter, after
|