| | """ |
| | Test Skill Loader |
| | """ |
| |
|
| | import tempfile |
| | from pathlib import Path |
| |
|
| | import pytest |
| |
|
| | from mini_agent.tools.skill_loader import Skill, SkillLoader |
| |
|
| |
|
| | def create_test_skill(skill_dir: Path, name: str, description: str, content: str): |
| | """Create a test skill""" |
| | skill_file = skill_dir / "SKILL.md" |
| | skill_content = f"""--- |
| | name: {name} |
| | description: {description} |
| | --- |
| | |
| | {content} |
| | """ |
| | skill_file.write_text(skill_content, encoding="utf-8") |
| |
|
| |
|
| | def test_load_valid_skill(): |
| | """Test loading a valid skill""" |
| | with tempfile.TemporaryDirectory() as tmpdir: |
| | skill_dir = Path(tmpdir) / "test-skill" |
| | skill_dir.mkdir() |
| |
|
| | create_test_skill( |
| | skill_dir, |
| | "test-skill", |
| | "A test skill", |
| | "This is a test skill content.", |
| | ) |
| |
|
| | loader = SkillLoader(tmpdir) |
| | skill = loader.load_skill(skill_dir / "SKILL.md") |
| |
|
| | assert skill is not None |
| | assert skill.name == "test-skill" |
| | assert skill.description == "A test skill" |
| | assert "This is a test skill content" in skill.content |
| |
|
| |
|
| | def test_load_skill_with_metadata(): |
| | """Test loading a skill with metadata""" |
| | with tempfile.TemporaryDirectory() as tmpdir: |
| | skill_dir = Path(tmpdir) / "test-skill" |
| | skill_dir.mkdir() |
| |
|
| | skill_file = skill_dir / "SKILL.md" |
| | skill_content = """--- |
| | name: test-skill |
| | description: A test skill |
| | license: MIT |
| | allowed-tools: |
| | - read_file |
| | - write_file |
| | metadata: |
| | author: Test Author |
| | version: "1.0" |
| | --- |
| | |
| | Skill content here. |
| | """ |
| | skill_file.write_text(skill_content, encoding="utf-8") |
| |
|
| | loader = SkillLoader(tmpdir) |
| | skill = loader.load_skill(skill_file) |
| |
|
| | assert skill is not None |
| | assert skill.name == "test-skill" |
| | assert skill.license == "MIT" |
| | assert skill.allowed_tools == ["read_file", "write_file"] |
| | assert skill.metadata["author"] == "Test Author" |
| | assert skill.metadata["version"] == "1.0" |
| |
|
| |
|
| | def test_load_invalid_skill(): |
| | """Test loading an invalid skill (missing frontmatter)""" |
| | with tempfile.TemporaryDirectory() as tmpdir: |
| | skill_dir = Path(tmpdir) / "invalid-skill" |
| | skill_dir.mkdir() |
| |
|
| | skill_file = skill_dir / "SKILL.md" |
| | skill_file.write_text("No frontmatter here!", encoding="utf-8") |
| |
|
| | loader = SkillLoader(tmpdir) |
| | skill = loader.load_skill(skill_file) |
| |
|
| | assert skill is None |
| |
|
| |
|
| | def test_discover_skills(): |
| | """Test discovering multiple skills""" |
| | with tempfile.TemporaryDirectory() as tmpdir: |
| | |
| | for i in range(3): |
| | skill_dir = Path(tmpdir) / f"skill-{i}" |
| | skill_dir.mkdir() |
| | create_test_skill( |
| | skill_dir, f"skill-{i}", f"Test skill {i}", f"Content {i}" |
| | ) |
| |
|
| | loader = SkillLoader(tmpdir) |
| | skills = loader.discover_skills() |
| |
|
| | assert len(skills) == 3 |
| | assert len(loader.list_skills()) == 3 |
| |
|
| |
|
| | def test_get_skill(): |
| | """Test getting a loaded skill""" |
| | with tempfile.TemporaryDirectory() as tmpdir: |
| | skill_dir = Path(tmpdir) / "test-skill" |
| | skill_dir.mkdir() |
| | create_test_skill(skill_dir, "test-skill", "Test", "Content") |
| |
|
| | loader = SkillLoader(tmpdir) |
| | loader.discover_skills() |
| |
|
| | skill = loader.get_skill("test-skill") |
| | assert skill is not None |
| | assert skill.name == "test-skill" |
| |
|
| | |
| | assert loader.get_skill("nonexistent") is None |
| |
|
| |
|
| |
|
| | def test_get_skills_metadata_prompt(): |
| | """Test generating metadata-only prompt (Progressive Disclosure Level 1)""" |
| | with tempfile.TemporaryDirectory() as tmpdir: |
| | |
| | |
| | long_content = """ |
| | # Detailed Skill Content |
| | |
| | This is a comprehensive skill guide with lots of detailed instructions. |
| | |
| | ## Section 1 |
| | Here are detailed instructions for using this skill. |
| | |
| | ## Section 2 |
| | More detailed content and examples. |
| | |
| | ## Section 3 |
| | Even more content to make this realistic. |
| | """ * 3 |
| |
|
| | skills_data = [ |
| | ("pdf", "PDF manipulation toolkit", long_content), |
| | ("docx", "Document creation tool", long_content), |
| | ("canvas-design", "Canvas design tool", long_content), |
| | ] |
| |
|
| | for name, desc, content in skills_data: |
| | skill_dir = Path(tmpdir) / name |
| | skill_dir.mkdir() |
| | create_test_skill(skill_dir, name, desc, content) |
| |
|
| | loader = SkillLoader(tmpdir) |
| | loader.discover_skills() |
| |
|
| | |
| | metadata_prompt = loader.get_skills_metadata_prompt() |
| |
|
| | |
| | assert "pdf" in metadata_prompt |
| | assert "docx" in metadata_prompt |
| | assert "canvas-design" in metadata_prompt |
| | assert "PDF manipulation toolkit" in metadata_prompt |
| | assert "Document creation tool" in metadata_prompt |
| |
|
| | |
| | assert "Available Skills" in metadata_prompt |
| |
|
| | |
| | assert "Detailed Skill Content" not in metadata_prompt |
| | assert "Section 1" not in metadata_prompt |
| | assert "Section 2" not in metadata_prompt |
| |
|
| |
|
| | def test_nested_document_path_processing(): |
| | """Test processing of nested document references (Level 3+)""" |
| | with tempfile.TemporaryDirectory() as tmpdir: |
| | skill_dir = Path(tmpdir) / "test-skill" |
| | skill_dir.mkdir() |
| |
|
| | |
| | (skill_dir / "reference.md").write_text("Reference content", encoding="utf-8") |
| | (skill_dir / "forms.md").write_text("Forms content", encoding="utf-8") |
| |
|
| | |
| | skill_content = """--- |
| | name: test-skill |
| | description: Test skill with nested docs |
| | --- |
| | |
| | For advanced features, see reference.md. |
| | If you need forms, read forms.md and follow instructions. |
| | """ |
| | (skill_dir / "SKILL.md").write_text(skill_content, encoding="utf-8") |
| |
|
| | loader = SkillLoader(tmpdir) |
| | skill = loader.load_skill(skill_dir / "SKILL.md") |
| |
|
| | assert skill is not None |
| |
|
| | |
| | assert str(skill_dir / "reference.md") in skill.content |
| | assert str(skill_dir / "forms.md") in skill.content |
| | assert "use read_file" in skill.content.lower() |
| |
|
| |
|
| | def test_script_path_processing(): |
| | """Test processing of script paths in skills""" |
| | with tempfile.TemporaryDirectory() as tmpdir: |
| | skill_dir = Path(tmpdir) / "test-skill" |
| | skill_dir.mkdir() |
| |
|
| | |
| | scripts_dir = skill_dir / "scripts" |
| | scripts_dir.mkdir() |
| | (scripts_dir / "test_script.py").write_text("# Python script", encoding="utf-8") |
| |
|
| | |
| | skill_content = """--- |
| | name: test-skill |
| | description: Test skill with scripts |
| | --- |
| | |
| | Run the script: python scripts/test_script.py |
| | """ |
| | (skill_dir / "SKILL.md").write_text(skill_content, encoding="utf-8") |
| |
|
| | loader = SkillLoader(tmpdir) |
| | skill = loader.load_skill(skill_dir / "SKILL.md") |
| |
|
| | assert skill is not None |
| |
|
| | |
| | assert str(skill_dir / "scripts" / "test_script.py") in skill.content |
| |
|
| |
|
| | def test_skill_to_prompt_includes_root_directory(): |
| | """Test that to_prompt includes skill root directory path""" |
| | with tempfile.TemporaryDirectory() as tmpdir: |
| | skill_dir = Path(tmpdir) / "test-skill" |
| | skill_dir.mkdir() |
| |
|
| | skill_file = skill_dir / "SKILL.md" |
| | skill_content = """--- |
| | name: test-skill |
| | description: A test skill |
| | --- |
| | |
| | Skill content here. |
| | """ |
| | skill_file.write_text(skill_content, encoding="utf-8") |
| |
|
| | loader = SkillLoader(tmpdir) |
| | skill = loader.load_skill(skill_file) |
| |
|
| | assert skill is not None |
| |
|
| | |
| | prompt = skill.to_prompt() |
| | assert "Skill Root Directory" in prompt |
| | assert str(skill_dir) in prompt |
| | assert "All files and references in this skill are relative to this directory" in prompt |
| |
|