mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-04-25 11:18:22 +00:00
* Refactor tests for SKILL.md parser Updated tests for SKILL.md parser to handle quoted names and descriptions correctly. Added new tests for parsing plain and single-quoted names, and ensured multi-line descriptions are processed properly. * Implement tool name validation and deduplication Add tool name mismatch warning and deduplication logic * Refactor skill file parsing and error handling * Add tests for tool name deduplication Added tests for tool name deduplication in get_available_tools(). Ensured that duplicates are not returned, the first occurrence is kept, and warnings are logged for skipped duplicates. * Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Update minimal config to include tools list * Update test for nonexistent skill file Ensure the test for nonexistent files checks for None. * Refactor tool loading and add skill management support Refactor tool loading logic to include skill management tools based on configuration and clean up comments. * Enhance code comments for tool loading logic Added comments to clarify the purpose of various code sections related to tool loading and configuration. * Fix assertion for duplicate tool name warning * Fix indentation issues in tools.py * Fix the lint error of test_tool_deduplication * Fix the lint error of tools.py * Fix the lint error * Fix the lint error * make format --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
132 lines
5.0 KiB
Python
132 lines
5.0 KiB
Python
"""Tests for the SKILL.md parser regression introduced in issue #1803.
|
|
|
|
The previous hand-rolled YAML parser stored quoted string values with their
|
|
surrounding quotes intact (e.g. ``name: "my-skill"`` → ``'"my-skill"'``).
|
|
This caused a mismatch with ``_validate_skill_frontmatter`` (which uses
|
|
``yaml.safe_load``) and broke skill lookup after installation.
|
|
|
|
The parser now uses ``yaml.safe_load`` consistently with ``validation.py``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from deerflow.skills.parser import parse_skill_file
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _write_skill(tmp_path: Path, front_matter: str, body: str = "# My Skill\n") -> Path:
|
|
"""Write a minimal SKILL.md and return the path."""
|
|
skill_dir = tmp_path / "my-skill"
|
|
skill_dir.mkdir()
|
|
skill_file = skill_dir / "SKILL.md"
|
|
skill_file.write_text(f"---\n{front_matter}\n---\n{body}", encoding="utf-8")
|
|
return skill_file
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Basic parsing
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_parse_plain_name(tmp_path):
|
|
"""Unquoted name is parsed correctly."""
|
|
skill_file = _write_skill(tmp_path, "name: my-skill\ndescription: A test skill")
|
|
skill = parse_skill_file(skill_file, category="custom")
|
|
assert skill is not None
|
|
assert skill.name == "my-skill"
|
|
|
|
|
|
def test_parse_quoted_name_no_quotes_in_result(tmp_path):
|
|
"""Quoted name (YAML string) must not include surrounding quotes in result.
|
|
|
|
Regression: the old hand-rolled parser stored ``'"my-skill"'`` instead of
|
|
``'my-skill'`` when the YAML value was wrapped in double-quotes.
|
|
"""
|
|
skill_file = _write_skill(tmp_path, 'name: "my-skill"\ndescription: A test skill')
|
|
skill = parse_skill_file(skill_file, category="custom")
|
|
assert skill is not None
|
|
assert skill.name == "my-skill", f"Expected 'my-skill', got {skill.name!r}"
|
|
|
|
|
|
def test_parse_single_quoted_name(tmp_path):
|
|
"""Single-quoted YAML strings are also handled correctly."""
|
|
skill_file = _write_skill(tmp_path, "name: 'my-skill'\ndescription: A test skill")
|
|
skill = parse_skill_file(skill_file, category="custom")
|
|
assert skill is not None
|
|
assert skill.name == "my-skill"
|
|
|
|
|
|
def test_parse_description_returned(tmp_path):
|
|
"""Description field is correctly extracted."""
|
|
skill_file = _write_skill(tmp_path, "name: my-skill\ndescription: Does amazing things")
|
|
skill = parse_skill_file(skill_file, category="custom")
|
|
assert skill is not None
|
|
assert skill.description == "Does amazing things"
|
|
|
|
|
|
def test_parse_multiline_description(tmp_path):
|
|
"""Multi-line YAML descriptions are collapsed correctly by yaml.safe_load."""
|
|
front_matter = "name: my-skill\ndescription: >\n A folded\n description"
|
|
skill_file = _write_skill(tmp_path, front_matter)
|
|
skill = parse_skill_file(skill_file, category="custom")
|
|
assert skill is not None
|
|
assert "folded" in skill.description
|
|
|
|
|
|
def test_parse_license_field(tmp_path):
|
|
"""Optional license field is captured when present."""
|
|
skill_file = _write_skill(tmp_path, "name: my-skill\ndescription: Test\nlicense: MIT")
|
|
skill = parse_skill_file(skill_file, category="custom")
|
|
assert skill is not None
|
|
assert skill.license == "MIT"
|
|
|
|
|
|
def test_parse_missing_name_returns_none(tmp_path):
|
|
"""Skills missing a name field are rejected."""
|
|
skill_file = _write_skill(tmp_path, "description: A test skill")
|
|
skill = parse_skill_file(skill_file, category="custom")
|
|
assert skill is None
|
|
|
|
|
|
def test_parse_missing_description_returns_none(tmp_path):
|
|
"""Skills missing a description field are rejected."""
|
|
skill_file = _write_skill(tmp_path, "name: my-skill")
|
|
skill = parse_skill_file(skill_file, category="custom")
|
|
assert skill is None
|
|
|
|
|
|
def test_parse_no_front_matter_returns_none(tmp_path):
|
|
"""Files without YAML front-matter delimiters return None."""
|
|
skill_dir = tmp_path / "no-fm"
|
|
skill_dir.mkdir()
|
|
skill_file = skill_dir / "SKILL.md"
|
|
skill_file.write_text("# No front matter here\n", encoding="utf-8")
|
|
skill = parse_skill_file(skill_file, category="public")
|
|
assert skill is None
|
|
|
|
|
|
def test_parse_invalid_yaml_returns_none(tmp_path):
|
|
"""Malformed YAML front-matter is handled gracefully (returns None)."""
|
|
skill_file = _write_skill(tmp_path, "name: [unclosed")
|
|
skill = parse_skill_file(skill_file, category="custom")
|
|
assert skill is None
|
|
|
|
|
|
def test_parse_category_stored(tmp_path):
|
|
"""Category is propagated into the returned Skill object."""
|
|
skill_file = _write_skill(tmp_path, "name: my-skill\ndescription: Test")
|
|
skill = parse_skill_file(skill_file, category="public")
|
|
assert skill is not None
|
|
assert skill.category == "public"
|
|
|
|
|
|
def test_parse_nonexistent_file_returns_none(tmp_path):
|
|
"""Non-existent files are handled gracefully."""
|
|
skill = parse_skill_file(tmp_path / "ghost" / "SKILL.md", category="custom")
|
|
assert skill is None
|