182 lines
6.6 KiB
Python
182 lines
6.6 KiB
Python
"""SkillLoader v7 provenance + 危险能力告警单元测试"""
|
||
|
||
import os
|
||
import tempfile
|
||
from unittest.mock import patch
|
||
|
||
import yaml
|
||
|
||
from agentkit.skills.base import Skill, SkillConfig
|
||
from agentkit.skills.loader import SkillLoader
|
||
from agentkit.skills.registry import SkillRegistry
|
||
|
||
|
||
def _write_yaml(directory: str, filename: str, data: dict) -> str:
|
||
path = os.path.join(directory, filename)
|
||
with open(path, "w", encoding="utf-8") as f:
|
||
yaml.dump(data, f, allow_unicode=True)
|
||
return path
|
||
|
||
|
||
class _FakeEntryPoint:
|
||
"""模拟 importlib.metadata.EntryPoint"""
|
||
|
||
def __init__(self, name: str, skill: Skill):
|
||
self.name = name
|
||
self._skill = skill
|
||
|
||
def load(self):
|
||
return self._skill
|
||
|
||
|
||
def _make_skill(name: str = "ep_skill", capabilities=None, tools=None) -> Skill:
|
||
config = SkillConfig(
|
||
name=name,
|
||
agent_type="test",
|
||
task_mode="llm_generate",
|
||
prompt={"identity": "test"},
|
||
capabilities=capabilities,
|
||
tools=tools,
|
||
)
|
||
return Skill(config)
|
||
|
||
|
||
class TestSkillLoaderProvenance:
|
||
def test_load_from_file_sets_yaml_provenance(self):
|
||
registry = SkillRegistry()
|
||
loader = SkillLoader(skill_registry=registry)
|
||
with tempfile.TemporaryDirectory() as tmpdir:
|
||
path = _write_yaml(
|
||
tmpdir,
|
||
"s.yaml",
|
||
{
|
||
"name": "s",
|
||
"agent_type": "t",
|
||
"task_mode": "llm_generate",
|
||
"prompt": {"identity": "x"},
|
||
},
|
||
)
|
||
skill = loader.load_from_file(path)
|
||
assert skill.config.provenance == f"yaml:{path}"
|
||
|
||
def test_load_from_skill_md_sets_provenance(self):
|
||
registry = SkillRegistry()
|
||
loader = SkillLoader(skill_registry=registry)
|
||
skill_md = """\
|
||
---
|
||
name: md-skill
|
||
description: "test"
|
||
agent_type: test
|
||
execution_mode: react
|
||
---
|
||
|
||
# Trigger
|
||
- test
|
||
|
||
# Steps
|
||
1. step
|
||
|
||
# Pitfalls
|
||
- none
|
||
|
||
# Verification
|
||
- ok
|
||
"""
|
||
with tempfile.TemporaryDirectory() as tmpdir:
|
||
path = os.path.join(tmpdir, "SKILL.md")
|
||
with open(path, "w", encoding="utf-8") as f:
|
||
f.write(skill_md)
|
||
skill = loader.load_from_skill_md(path)
|
||
assert skill.config.provenance == f"skill_md:{path}"
|
||
|
||
def test_load_from_entry_points_sets_provenance(self):
|
||
registry = SkillRegistry()
|
||
loader = SkillLoader(skill_registry=registry)
|
||
fake_ep = _FakeEntryPoint("my_ep", _make_skill("ep_skill"))
|
||
with patch("agentkit.skills.loader.sys.version_info", (3, 12, 0)):
|
||
with patch("importlib.metadata.entry_points", return_value=[fake_ep]):
|
||
skills = loader.load_from_entry_points()
|
||
assert len(skills) == 1
|
||
assert skills[0].config.provenance == "entry_point:my_ep"
|
||
|
||
def test_entry_points_dangerous_capability_warning(self, caplog):
|
||
"""entry_points 加载声明 shell 能力的 Skill 时触发 warning"""
|
||
import logging
|
||
|
||
registry = SkillRegistry()
|
||
loader = SkillLoader(skill_registry=registry)
|
||
dangerous_skill = _make_skill(
|
||
"dangerous_skill", capabilities=[{"tag": "shell"}, {"tag": "code_execution"}]
|
||
)
|
||
fake_ep = _FakeEntryPoint("dangerous_ep", dangerous_skill)
|
||
with patch("agentkit.skills.loader.sys.version_info", (3, 12, 0)):
|
||
with patch("importlib.metadata.entry_points", return_value=[fake_ep]):
|
||
with caplog.at_level(logging.WARNING):
|
||
skills = loader.load_from_entry_points()
|
||
assert len(skills) == 1
|
||
assert skills[0].config.provenance == "entry_point:dangerous_ep"
|
||
# warning 包含 skill 名与危险能力
|
||
warnings = [r for r in caplog.records if r.levelno == logging.WARNING]
|
||
assert any(
|
||
"dangerous_skill" in r.getMessage() and "shell" in r.getMessage() for r in warnings
|
||
)
|
||
|
||
def test_entry_points_dangerous_tools_warning(self, caplog):
|
||
"""entry_points 加载绑定 shell 工具但未声明 capabilities 的 Skill 时触发 warning"""
|
||
import logging
|
||
|
||
registry = SkillRegistry()
|
||
loader = SkillLoader(skill_registry=registry)
|
||
# 有危险 tools 但无 capabilities 声明——旧逻辑会漏检
|
||
dangerous_skill = _make_skill("stealthy_skill", capabilities=None, tools=["shell"])
|
||
fake_ep = _FakeEntryPoint("stealthy_ep", dangerous_skill)
|
||
with patch("agentkit.skills.loader.sys.version_info", (3, 12, 0)):
|
||
with patch("importlib.metadata.entry_points", return_value=[fake_ep]):
|
||
with caplog.at_level(logging.WARNING):
|
||
skills = loader.load_from_entry_points()
|
||
assert len(skills) == 1
|
||
warnings = [r for r in caplog.records if r.levelno == logging.WARNING]
|
||
assert any(
|
||
"stealthy_skill" in r.getMessage() and "shell" in r.getMessage() for r in warnings
|
||
)
|
||
|
||
def test_entry_points_no_capabilities_no_warning(self, caplog):
|
||
import logging
|
||
|
||
registry = SkillRegistry()
|
||
loader = SkillLoader(skill_registry=registry)
|
||
safe_skill = _make_skill("safe_skill", capabilities=None)
|
||
fake_ep = _FakeEntryPoint("safe_ep", safe_skill)
|
||
with patch("agentkit.skills.loader.sys.version_info", (3, 12, 0)):
|
||
with patch("importlib.metadata.entry_points", return_value=[fake_ep]):
|
||
with caplog.at_level(logging.WARNING):
|
||
skills = loader.load_from_entry_points()
|
||
assert len(skills) == 1
|
||
# 不应有危险能力 warning(只可能有其他 warning)
|
||
dangerous_warnings = [
|
||
r
|
||
for r in caplog.records
|
||
if r.levelno == logging.WARNING and "dangerous capabilities" in r.getMessage()
|
||
]
|
||
assert dangerous_warnings == []
|
||
|
||
def test_yaml_provenance_overridden_by_loader(self):
|
||
"""YAML 中已有 provenance 字段时,加载路径覆盖它(加载路径是权威来源)"""
|
||
registry = SkillRegistry()
|
||
loader = SkillLoader(skill_registry=registry)
|
||
with tempfile.TemporaryDirectory() as tmpdir:
|
||
path = _write_yaml(
|
||
tmpdir,
|
||
"s.yaml",
|
||
{
|
||
"name": "s",
|
||
"agent_type": "t",
|
||
"task_mode": "llm_generate",
|
||
"prompt": {"identity": "x"},
|
||
"provenance": "user_supplied:should_be_overridden",
|
||
},
|
||
)
|
||
skill = loader.load_from_file(path)
|
||
assert skill.config.provenance == f"yaml:{path}"
|
||
assert "user_supplied" not in skill.config.provenance
|