mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-04-14 15:02:37 +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
168 lines
6.0 KiB
Python
168 lines
6.0 KiB
Python
"""Tests for skills/parser.py."""
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from crewai.skills.models import INSTRUCTIONS, METADATA, RESOURCES
|
|
from crewai.skills.parser import (
|
|
SkillParseError,
|
|
load_skill_instructions,
|
|
load_skill_metadata,
|
|
load_skill_resources,
|
|
parse_frontmatter,
|
|
parse_skill_md,
|
|
)
|
|
|
|
|
|
class TestParseFrontmatter:
|
|
"""Tests for parse_frontmatter."""
|
|
|
|
def test_valid_frontmatter_and_body(self) -> None:
|
|
content = "---\nname: test\ndescription: A test\n---\n\nBody text here."
|
|
fm, body = parse_frontmatter(content)
|
|
assert fm["name"] == "test"
|
|
assert fm["description"] == "A test"
|
|
assert body == "Body text here."
|
|
|
|
def test_empty_body(self) -> None:
|
|
content = "---\nname: test\ndescription: A test\n---"
|
|
fm, body = parse_frontmatter(content)
|
|
assert fm["name"] == "test"
|
|
assert body == ""
|
|
|
|
def test_missing_opening_delimiter(self) -> None:
|
|
with pytest.raises(SkillParseError, match="must start with"):
|
|
parse_frontmatter("name: test\n---\nBody")
|
|
|
|
def test_missing_closing_delimiter(self) -> None:
|
|
with pytest.raises(SkillParseError, match="missing closing"):
|
|
parse_frontmatter("---\nname: test\n")
|
|
|
|
def test_invalid_yaml(self) -> None:
|
|
with pytest.raises(SkillParseError, match="Invalid YAML"):
|
|
parse_frontmatter("---\n: :\n bad: [yaml\n---\nBody")
|
|
|
|
def test_triple_dash_in_body(self) -> None:
|
|
content = "---\nname: test\ndescription: desc\n---\n\nBody with --- inside."
|
|
fm, body = parse_frontmatter(content)
|
|
assert "---" in body
|
|
|
|
def test_inline_triple_dash_in_yaml_value(self) -> None:
|
|
content = '---\nname: test\ndescription: "Use---carefully"\n---\n\nBody.'
|
|
fm, body = parse_frontmatter(content)
|
|
assert fm["description"] == "Use---carefully"
|
|
assert body == "Body."
|
|
|
|
def test_unicode_content(self) -> None:
|
|
content = "---\nname: test\ndescription: Beschreibung\n---\n\nUnicode: \u00e4\u00f6\u00fc\u00df"
|
|
fm, body = parse_frontmatter(content)
|
|
assert fm["description"] == "Beschreibung"
|
|
assert "\u00e4\u00f6\u00fc\u00df" in body
|
|
|
|
def test_non_mapping_frontmatter(self) -> None:
|
|
with pytest.raises(SkillParseError, match="must be a YAML mapping"):
|
|
parse_frontmatter("---\n- item1\n- item2\n---\nBody")
|
|
|
|
|
|
class TestParseSkillMd:
|
|
"""Tests for parse_skill_md."""
|
|
|
|
def test_valid_file(self, tmp_path: Path) -> None:
|
|
skill_md = tmp_path / "SKILL.md"
|
|
skill_md.write_text(
|
|
"---\nname: my-skill\ndescription: desc\n---\nInstructions here."
|
|
)
|
|
fm, body = parse_skill_md(skill_md)
|
|
assert fm.name == "my-skill"
|
|
assert body == "Instructions here."
|
|
|
|
def test_file_not_found(self, tmp_path: Path) -> None:
|
|
with pytest.raises(FileNotFoundError):
|
|
parse_skill_md(tmp_path / "nonexistent" / "SKILL.md")
|
|
|
|
|
|
class TestLoadSkillMetadata:
|
|
"""Tests for load_skill_metadata."""
|
|
|
|
def test_valid_skill(self, tmp_path: Path) -> None:
|
|
skill_dir = tmp_path / "my-skill"
|
|
skill_dir.mkdir()
|
|
(skill_dir / "SKILL.md").write_text(
|
|
"---\nname: my-skill\ndescription: Test skill\n---\nBody"
|
|
)
|
|
skill = load_skill_metadata(skill_dir)
|
|
assert skill.name == "my-skill"
|
|
assert skill.disclosure_level == METADATA
|
|
assert skill.instructions is None
|
|
|
|
def test_directory_name_mismatch(self, tmp_path: Path) -> None:
|
|
skill_dir = tmp_path / "wrong-name"
|
|
skill_dir.mkdir()
|
|
(skill_dir / "SKILL.md").write_text(
|
|
"---\nname: my-skill\ndescription: Test skill\n---\n"
|
|
)
|
|
with pytest.raises(ValueError, match="does not match"):
|
|
load_skill_metadata(skill_dir)
|
|
|
|
|
|
class TestLoadSkillInstructions:
|
|
"""Tests for load_skill_instructions."""
|
|
|
|
def test_promotes_to_instructions(self, tmp_path: Path) -> None:
|
|
skill_dir = tmp_path / "my-skill"
|
|
skill_dir.mkdir()
|
|
(skill_dir / "SKILL.md").write_text(
|
|
"---\nname: my-skill\ndescription: Test\n---\nFull body."
|
|
)
|
|
skill = load_skill_metadata(skill_dir)
|
|
promoted = load_skill_instructions(skill)
|
|
assert promoted.disclosure_level == INSTRUCTIONS
|
|
assert promoted.instructions == "Full body."
|
|
|
|
def test_idempotent(self, tmp_path: Path) -> None:
|
|
skill_dir = tmp_path / "my-skill"
|
|
skill_dir.mkdir()
|
|
(skill_dir / "SKILL.md").write_text(
|
|
"---\nname: my-skill\ndescription: Test\n---\nBody."
|
|
)
|
|
skill = load_skill_metadata(skill_dir)
|
|
promoted = load_skill_instructions(skill)
|
|
again = load_skill_instructions(promoted)
|
|
assert again is promoted
|
|
|
|
|
|
class TestLoadSkillResources:
|
|
"""Tests for load_skill_resources."""
|
|
|
|
def test_catalogs_resources(self, tmp_path: Path) -> None:
|
|
skill_dir = tmp_path / "my-skill"
|
|
skill_dir.mkdir()
|
|
(skill_dir / "SKILL.md").write_text(
|
|
"---\nname: my-skill\ndescription: Test\n---\nBody."
|
|
)
|
|
(skill_dir / "scripts").mkdir()
|
|
(skill_dir / "scripts" / "run.sh").write_text("#!/bin/bash")
|
|
(skill_dir / "assets").mkdir()
|
|
(skill_dir / "assets" / "data.json").write_text("{}")
|
|
|
|
skill = load_skill_metadata(skill_dir)
|
|
full = load_skill_resources(skill)
|
|
assert full.disclosure_level == RESOURCES
|
|
assert full.instructions == "Body."
|
|
assert full.resource_files is not None
|
|
assert "scripts" in full.resource_files
|
|
assert "run.sh" in full.resource_files["scripts"]
|
|
assert "assets" in full.resource_files
|
|
assert "data.json" in full.resource_files["assets"]
|
|
|
|
def test_no_resource_dirs(self, tmp_path: Path) -> None:
|
|
skill_dir = tmp_path / "my-skill"
|
|
skill_dir.mkdir()
|
|
(skill_dir / "SKILL.md").write_text(
|
|
"---\nname: my-skill\ndescription: Test\n---\nBody."
|
|
)
|
|
skill = load_skill_metadata(skill_dir)
|
|
full = load_skill_resources(skill)
|
|
assert full.resource_files == {}
|