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