210 lines
6.4 KiB
Python
210 lines
6.4 KiB
Python
"""Tests for agent skill types, loader, registry, and tool."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from app.agents.skills.types import SkillMetadata, Skill, SkillSource
|
|
from app.agents.skills.loader import SkillLoader
|
|
from app.agents.skills.registry import SkillRegistry
|
|
from app.agents.skills.tool import create_skill_tool
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SkillMetadata & Skill
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSkillMetadata:
|
|
def test_creation(self) -> None:
|
|
meta = SkillMetadata(
|
|
name="test-skill",
|
|
description="A test skill",
|
|
path="/skills/test.md",
|
|
source="builtin",
|
|
)
|
|
assert meta.name == "test-skill"
|
|
assert meta.description == "A test skill"
|
|
assert meta.path == "/skills/test.md"
|
|
assert meta.source == "builtin"
|
|
|
|
|
|
class TestSkill:
|
|
def test_creation_with_instructions(self) -> None:
|
|
skill = Skill(
|
|
name="s1",
|
|
description="desc",
|
|
path="/skills/s1.md",
|
|
source="builtin",
|
|
instructions="Do this and that.",
|
|
)
|
|
assert skill.name == "s1"
|
|
assert skill.instructions == "Do this and that."
|
|
assert skill.source == "builtin"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SkillLoader
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_VALID_SKILL_CONTENT = """\
|
|
---
|
|
name: test-skill
|
|
description: A skill for testing
|
|
---
|
|
|
|
## Instructions
|
|
|
|
Follow these steps to perform the test skill.
|
|
|
|
1. Step one
|
|
2. Step two
|
|
"""
|
|
|
|
_NO_FRONTMATTER_CONTENT = """\
|
|
## Instructions
|
|
|
|
This file has no YAML frontmatter.
|
|
"""
|
|
|
|
_EMPTY_FRONTMATTER_CONTENT = """\
|
|
---
|
|
---
|
|
|
|
## Instructions
|
|
|
|
Empty frontmatter.
|
|
"""
|
|
|
|
|
|
class TestSkillLoader:
|
|
def test_parse_valid(self) -> None:
|
|
skill = SkillLoader.parse_skill_file(
|
|
content=_VALID_SKILL_CONTENT,
|
|
path="/skills/test-skill.md",
|
|
source="builtin",
|
|
)
|
|
assert skill.name == "test-skill"
|
|
assert skill.description == "A skill for testing"
|
|
assert "Step one" in skill.instructions
|
|
assert skill.source == "builtin"
|
|
|
|
def test_parse_no_frontmatter(self) -> None:
|
|
"""frontmatter가 없으면 name/description이 빈 문자열."""
|
|
skill = SkillLoader.parse_skill_file(
|
|
content=_NO_FRONTMATTER_CONTENT,
|
|
path="/skills/no-front.md",
|
|
source="builtin",
|
|
)
|
|
assert skill.name == ""
|
|
assert skill.description == ""
|
|
assert "no YAML frontmatter" in skill.instructions
|
|
|
|
def test_parse_empty_frontmatter(self) -> None:
|
|
"""빈 frontmatter도 정상 처리."""
|
|
skill = SkillLoader.parse_skill_file(
|
|
content=_EMPTY_FRONTMATTER_CONTENT,
|
|
path="/skills/empty-front.md",
|
|
source="builtin",
|
|
)
|
|
assert skill.name == ""
|
|
assert skill.description == ""
|
|
|
|
def test_load_from_path(self, tmp_path: Path) -> None:
|
|
skill_file = tmp_path / "SKILL.md"
|
|
skill_file.write_text(_VALID_SKILL_CONTENT, encoding="utf-8")
|
|
|
|
skill = SkillLoader.load_from_path(skill_file, "builtin")
|
|
assert skill.name == "test-skill"
|
|
assert "Step one" in skill.instructions
|
|
|
|
def test_extract_metadata(self, tmp_path: Path) -> None:
|
|
skill_file = tmp_path / "SKILL.md"
|
|
skill_file.write_text(_VALID_SKILL_CONTENT, encoding="utf-8")
|
|
|
|
meta = SkillLoader.extract_metadata(skill_file, "builtin")
|
|
assert isinstance(meta, SkillMetadata)
|
|
assert meta.name == "test-skill"
|
|
assert meta.description == "A skill for testing"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SkillRegistry
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSkillRegistry:
|
|
def setup_method(self) -> None:
|
|
SkillRegistry.clear_cache()
|
|
|
|
def test_discover_finds_builtin_skills(self) -> None:
|
|
"""실제 builtin 디렉토리에서 dcf-kr, kim-jong-bong-strategy 발견."""
|
|
skills = SkillRegistry.discover()
|
|
names = [s.name for s in skills]
|
|
assert "dcf-kr" in names
|
|
assert "kim-jong-bong-strategy" in names
|
|
|
|
def test_get_returns_skill(self) -> None:
|
|
SkillRegistry.discover()
|
|
skill = SkillRegistry.get("dcf-kr")
|
|
assert skill is not None
|
|
assert skill.name == "dcf-kr"
|
|
assert "WACC" in skill.instructions
|
|
|
|
def test_get_returns_none_for_unknown(self) -> None:
|
|
SkillRegistry.discover()
|
|
assert SkillRegistry.get("nonexistent-skill") is None
|
|
|
|
def test_list_skills(self) -> None:
|
|
SkillRegistry.discover()
|
|
skills = SkillRegistry.list_skills()
|
|
assert len(skills) >= 2
|
|
|
|
def test_build_skills_section(self) -> None:
|
|
SkillRegistry.discover()
|
|
section = SkillRegistry.build_skills_section()
|
|
assert "dcf-kr" in section
|
|
assert "kim-jong-bong-strategy" in section
|
|
|
|
def test_build_skills_section_empty(self) -> None:
|
|
"""캐시 비어있으면 빈 문자열 반환."""
|
|
section = SkillRegistry.build_skills_section()
|
|
assert section == ""
|
|
|
|
def test_clear_cache(self) -> None:
|
|
SkillRegistry.discover()
|
|
assert len(SkillRegistry.list_skills()) >= 2
|
|
SkillRegistry.clear_cache()
|
|
assert len(SkillRegistry._cache) == 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# create_skill_tool
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSkillTool:
|
|
def setup_method(self) -> None:
|
|
SkillRegistry.clear_cache()
|
|
SkillRegistry.discover()
|
|
|
|
def test_tool_metadata(self) -> None:
|
|
tool = create_skill_tool()
|
|
assert tool.name == "use_skill"
|
|
assert tool.concurrency_safe is True
|
|
|
|
def test_execute_valid_skill(self) -> None:
|
|
tool = create_skill_tool()
|
|
result = asyncio.run(tool.execute({"skill_name": "dcf-kr"}))
|
|
assert "WACC" in result.data
|
|
|
|
def test_execute_unknown_skill(self) -> None:
|
|
tool = create_skill_tool()
|
|
result = asyncio.run(tool.execute({"skill_name": "no-such-skill"}))
|
|
assert "찾을 수 없습니다" in result.data
|
|
assert "dcf-kr" in result.data
|