Files
crewAI/lib/crewai/tests/skills/test_parser.py
Greyson LaLonde 555ee462a3 feat: agent skills
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
2026-03-24 19:03:35 +08:00

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 == {}