mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-04-14 06:53:25 +00:00
introduce the agent skills standard for packaging reusable instructions that agents can discover and activate at runtime. - skills defined via SKILL.md with yaml frontmatter and markdown body - three-level progressive disclosure: metadata, instructions, resources - filesystem discovery with directory name validation - skill lifecycle events (discovery, loaded, activated, failed) - crew-level skills resolved once and shared across agents - skill context injected into both task execution and standalone kickoff
162 lines
5.6 KiB
Python
162 lines
5.6 KiB
Python
"""Tests for skills/loader.py."""
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from crewai.skills.loader import (
|
|
activate_skill,
|
|
discover_skills,
|
|
format_skill_context,
|
|
load_resources,
|
|
)
|
|
from crewai.skills.models import INSTRUCTIONS, METADATA, RESOURCES, Skill, SkillFrontmatter
|
|
from crewai.skills.parser import load_skill_metadata
|
|
|
|
|
|
def _create_skill_dir(parent: Path, name: str, body: str = "Body.") -> Path:
|
|
"""Helper to create a skill directory with SKILL.md."""
|
|
skill_dir = parent / name
|
|
skill_dir.mkdir()
|
|
(skill_dir / "SKILL.md").write_text(
|
|
f"---\nname: {name}\ndescription: Skill {name}\n---\n{body}"
|
|
)
|
|
return skill_dir
|
|
|
|
|
|
class TestDiscoverSkills:
|
|
"""Tests for discover_skills."""
|
|
|
|
def test_finds_valid_skills(self, tmp_path: Path) -> None:
|
|
_create_skill_dir(tmp_path, "alpha")
|
|
_create_skill_dir(tmp_path, "beta")
|
|
skills = discover_skills(tmp_path)
|
|
names = {s.name for s in skills}
|
|
assert names == {"alpha", "beta"}
|
|
|
|
def test_skips_dirs_without_skill_md(self, tmp_path: Path) -> None:
|
|
_create_skill_dir(tmp_path, "valid")
|
|
(tmp_path / "no-skill").mkdir()
|
|
skills = discover_skills(tmp_path)
|
|
assert len(skills) == 1
|
|
assert skills[0].name == "valid"
|
|
|
|
def test_skips_invalid_skills(self, tmp_path: Path) -> None:
|
|
_create_skill_dir(tmp_path, "good-skill")
|
|
bad_dir = tmp_path / "bad-skill"
|
|
bad_dir.mkdir()
|
|
(bad_dir / "SKILL.md").write_text(
|
|
"---\nname: Wrong-Name\ndescription: bad\n---\n"
|
|
)
|
|
skills = discover_skills(tmp_path)
|
|
assert len(skills) == 1
|
|
|
|
def test_empty_directory(self, tmp_path: Path) -> None:
|
|
skills = discover_skills(tmp_path)
|
|
assert skills == []
|
|
|
|
def test_nonexistent_path(self, tmp_path: Path) -> None:
|
|
with pytest.raises(FileNotFoundError):
|
|
discover_skills(tmp_path / "nonexistent")
|
|
|
|
def test_sorted_by_name(self, tmp_path: Path) -> None:
|
|
_create_skill_dir(tmp_path, "zebra")
|
|
_create_skill_dir(tmp_path, "alpha")
|
|
skills = discover_skills(tmp_path)
|
|
assert [s.name for s in skills] == ["alpha", "zebra"]
|
|
|
|
|
|
class TestActivateSkill:
|
|
"""Tests for activate_skill."""
|
|
|
|
def test_promotes_to_instructions(self, tmp_path: Path) -> None:
|
|
_create_skill_dir(tmp_path, "my-skill", body="Instructions.")
|
|
skill = load_skill_metadata(tmp_path / "my-skill")
|
|
activated = activate_skill(skill)
|
|
assert activated.disclosure_level == INSTRUCTIONS
|
|
assert activated.instructions == "Instructions."
|
|
|
|
def test_idempotent(self, tmp_path: Path) -> None:
|
|
_create_skill_dir(tmp_path, "my-skill")
|
|
skill = load_skill_metadata(tmp_path / "my-skill")
|
|
activated = activate_skill(skill)
|
|
again = activate_skill(activated)
|
|
assert again is activated
|
|
|
|
|
|
class TestLoadResources:
|
|
"""Tests for load_resources."""
|
|
|
|
def test_promotes_to_resources(self, tmp_path: Path) -> None:
|
|
skill_dir = _create_skill_dir(tmp_path, "my-skill")
|
|
(skill_dir / "scripts").mkdir()
|
|
(skill_dir / "scripts" / "run.sh").write_text("#!/bin/bash")
|
|
skill = load_skill_metadata(skill_dir)
|
|
full = load_resources(skill)
|
|
assert full.disclosure_level == RESOURCES
|
|
|
|
|
|
class TestFormatSkillContext:
|
|
"""Tests for format_skill_context."""
|
|
|
|
def test_metadata_level(self, tmp_path: Path) -> None:
|
|
fm = SkillFrontmatter(name="test-skill", description="A skill")
|
|
skill = Skill(
|
|
frontmatter=fm, path=tmp_path, disclosure_level=METADATA
|
|
)
|
|
ctx = format_skill_context(skill)
|
|
assert "## Skill: test-skill" in ctx
|
|
assert "A skill" in ctx
|
|
|
|
def test_instructions_level(self, tmp_path: Path) -> None:
|
|
fm = SkillFrontmatter(name="test-skill", description="A skill")
|
|
skill = Skill(
|
|
frontmatter=fm,
|
|
path=tmp_path,
|
|
disclosure_level=INSTRUCTIONS,
|
|
instructions="Do these things.",
|
|
)
|
|
ctx = format_skill_context(skill)
|
|
assert "## Skill: test-skill" in ctx
|
|
assert "Do these things." in ctx
|
|
|
|
def test_no_instructions_at_instructions_level(self, tmp_path: Path) -> None:
|
|
fm = SkillFrontmatter(name="test-skill", description="A skill")
|
|
skill = Skill(
|
|
frontmatter=fm,
|
|
path=tmp_path,
|
|
disclosure_level=INSTRUCTIONS,
|
|
instructions=None,
|
|
)
|
|
ctx = format_skill_context(skill)
|
|
assert ctx == "## Skill: test-skill\nA skill"
|
|
|
|
def test_resources_level(self, tmp_path: Path) -> None:
|
|
fm = SkillFrontmatter(name="test-skill", description="A skill")
|
|
skill = Skill(
|
|
frontmatter=fm,
|
|
path=tmp_path,
|
|
disclosure_level=RESOURCES,
|
|
instructions="Do things.",
|
|
resource_files={
|
|
"scripts": ["run.sh"],
|
|
"assets": ["data.json", "config.yaml"],
|
|
},
|
|
)
|
|
ctx = format_skill_context(skill)
|
|
assert "### Available Resources" in ctx
|
|
assert "**assets/**: data.json, config.yaml" in ctx
|
|
assert "**scripts/**: run.sh" in ctx
|
|
|
|
def test_resources_level_empty_files(self, tmp_path: Path) -> None:
|
|
fm = SkillFrontmatter(name="test-skill", description="A skill")
|
|
skill = Skill(
|
|
frontmatter=fm,
|
|
path=tmp_path,
|
|
disclosure_level=RESOURCES,
|
|
instructions="Do things.",
|
|
resource_files={},
|
|
)
|
|
ctx = format_skill_context(skill)
|
|
assert "### Available Resources" not in ctx
|