Vendored deer-flow upstream (bytedance/deer-flow) plus prompt-injection hardening: - New deerflow.security package: content_delimiter, html_cleaner, sanitizer (8 layers — invisible chars, control chars, symbols, NFC, PUA, tag chars, horizontal whitespace collapse with newline/tab preservation, length cap) - New deerflow.community.searx package: web_search, web_fetch, image_search backed by a private SearX instance, every external string sanitized and wrapped in <<<EXTERNAL_UNTRUSTED_CONTENT>>> delimiters - All native community web providers (ddg_search, tavily, exa, firecrawl, jina_ai, infoquest, image_search) replaced with hard-fail stubs that raise NativeWebToolDisabledError at import time, so a misconfigured tool.use path fails loud rather than silently falling back to unsanitized output - Native client back-doors (jina_client.py, infoquest_client.py) stubbed too - Native-tool tests quarantined under tests/_disabled_native/ (collect_ignore_glob via local conftest.py) - Sanitizer Layer 7 fix: only collapse horizontal whitespace, preserve newlines and tabs so list/table structure survives - Hardened runtime config.yaml references only the searx-backed tools - Factory overlay (backend/) kept in sync with deer-flow tree as a reference / source See HARDENING.md for the full audit trail and verification steps.
120 lines
4.7 KiB
Python
120 lines
4.7 KiB
Python
"""Tests for skill file parser."""
|
|
|
|
from pathlib import Path
|
|
|
|
from deerflow.skills.parser import parse_skill_file
|
|
|
|
|
|
def _write_skill(tmp_path: Path, content: str) -> Path:
|
|
"""Write a SKILL.md file and return its path."""
|
|
skill_file = tmp_path / "SKILL.md"
|
|
skill_file.write_text(content, encoding="utf-8")
|
|
return skill_file
|
|
|
|
|
|
class TestParseSkillFile:
|
|
def test_valid_skill_file(self, tmp_path):
|
|
skill_file = _write_skill(
|
|
tmp_path,
|
|
"---\nname: my-skill\ndescription: A test skill\nlicense: MIT\n---\n\n# My Skill\n",
|
|
)
|
|
result = parse_skill_file(skill_file, "public")
|
|
assert result is not None
|
|
assert result.name == "my-skill"
|
|
assert result.description == "A test skill"
|
|
assert result.license == "MIT"
|
|
assert result.category == "public"
|
|
assert result.enabled is True
|
|
assert result.skill_dir == tmp_path
|
|
assert result.skill_file == skill_file
|
|
|
|
def test_missing_name_returns_none(self, tmp_path):
|
|
skill_file = _write_skill(
|
|
tmp_path,
|
|
"---\ndescription: A test skill\n---\n\nBody\n",
|
|
)
|
|
assert parse_skill_file(skill_file, "public") is None
|
|
|
|
def test_missing_description_returns_none(self, tmp_path):
|
|
skill_file = _write_skill(
|
|
tmp_path,
|
|
"---\nname: my-skill\n---\n\nBody\n",
|
|
)
|
|
assert parse_skill_file(skill_file, "public") is None
|
|
|
|
def test_no_front_matter_returns_none(self, tmp_path):
|
|
skill_file = _write_skill(tmp_path, "# Just a markdown file\n\nNo front matter here.\n")
|
|
assert parse_skill_file(skill_file, "public") is None
|
|
|
|
def test_nonexistent_file_returns_none(self, tmp_path):
|
|
skill_file = tmp_path / "SKILL.md"
|
|
assert parse_skill_file(skill_file, "public") is None
|
|
|
|
def test_wrong_filename_returns_none(self, tmp_path):
|
|
wrong_file = tmp_path / "README.md"
|
|
wrong_file.write_text("---\nname: test\ndescription: test\n---\n", encoding="utf-8")
|
|
assert parse_skill_file(wrong_file, "public") is None
|
|
|
|
def test_optional_license_field(self, tmp_path):
|
|
skill_file = _write_skill(
|
|
tmp_path,
|
|
"---\nname: my-skill\ndescription: A test skill\n---\n\nBody\n",
|
|
)
|
|
result = parse_skill_file(skill_file, "custom")
|
|
assert result is not None
|
|
assert result.license is None
|
|
assert result.category == "custom"
|
|
|
|
def test_custom_relative_path(self, tmp_path):
|
|
skill_file = _write_skill(
|
|
tmp_path,
|
|
"---\nname: nested-skill\ndescription: Nested\n---\n\nBody\n",
|
|
)
|
|
rel = Path("group/nested-skill")
|
|
result = parse_skill_file(skill_file, "public", relative_path=rel)
|
|
assert result is not None
|
|
assert result.relative_path == rel
|
|
|
|
def test_default_relative_path_is_parent_name(self, tmp_path):
|
|
skill_file = _write_skill(
|
|
tmp_path,
|
|
"---\nname: my-skill\ndescription: Test\n---\n\nBody\n",
|
|
)
|
|
result = parse_skill_file(skill_file, "public")
|
|
assert result is not None
|
|
assert result.relative_path == Path(tmp_path.name)
|
|
|
|
def test_colons_in_description(self, tmp_path):
|
|
skill_file = _write_skill(
|
|
tmp_path,
|
|
"---\nname: my-skill\ndescription: A skill: does things\n---\n\nBody\n",
|
|
)
|
|
result = parse_skill_file(skill_file, "public")
|
|
assert result is not None
|
|
assert result.description == "A skill: does things"
|
|
|
|
def test_multiline_yaml_folded_description(self, tmp_path):
|
|
skill_file = _write_skill(
|
|
tmp_path,
|
|
"---\nname: multiline-skill\ndescription: >\n This is a multiline\n description for a skill.\n\n It spans multiple lines.\nlicense: MIT\n---\n\nBody\n",
|
|
)
|
|
result = parse_skill_file(skill_file, "public")
|
|
assert result is not None
|
|
assert result.name == "multiline-skill"
|
|
assert result.description == "This is a multiline description for a skill.\n\nIt spans multiple lines."
|
|
assert result.license == "MIT"
|
|
|
|
def test_multiline_yaml_literal_description(self, tmp_path):
|
|
skill_file = _write_skill(
|
|
tmp_path,
|
|
"---\nname: pipe-skill\ndescription: |\n First line.\n Second line.\n---\n\nBody\n",
|
|
)
|
|
result = parse_skill_file(skill_file, "public")
|
|
assert result is not None
|
|
assert result.name == "pipe-skill"
|
|
assert result.description == "First line.\nSecond line."
|
|
|
|
def test_empty_front_matter_returns_none(self, tmp_path):
|
|
skill_file = _write_skill(tmp_path, "---\n\n---\n\nBody\n")
|
|
assert parse_skill_file(skill_file, "public") is None
|