226 lines
7.6 KiB
Python
226 lines
7.6 KiB
Python
"""F16 Skill System 单元测试
|
||
|
||
按 test-plan-v2.6.md §F16:
|
||
- T1: 注册/查询(P0)
|
||
- T2: 三层自由度(P0)
|
||
- T3: 技能匹配(P0)
|
||
- T4: 模板变量替换(P1)
|
||
"""
|
||
|
||
import json
|
||
import pytest
|
||
|
||
pytestmark = pytest.mark.unit
|
||
from pathlib import Path
|
||
|
||
from src.daemon.skill_system import (
|
||
Skill,
|
||
SkillExecutor,
|
||
SkillFreedom,
|
||
SkillRegistry,
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Fixtures
|
||
# ---------------------------------------------------------------------------
|
||
|
||
@pytest.fixture
|
||
def registry():
|
||
return SkillRegistry()
|
||
|
||
|
||
@pytest.fixture
|
||
def registry_with_skills(registry):
|
||
registry.register(Skill(
|
||
id="code-review",
|
||
name="Code Review",
|
||
description="Review code quality and suggest improvements",
|
||
freedom=SkillFreedom.HIGH.value,
|
||
tags=["coding", "review"],
|
||
))
|
||
registry.register(Skill(
|
||
id="test-template",
|
||
name="Test Generator",
|
||
description="Generate test cases from code",
|
||
freedom=SkillFreedom.MEDIUM.value,
|
||
prompt_template="Write tests for {{module}} focusing on {{focus}}",
|
||
tags=["testing", "coding"],
|
||
))
|
||
registry.register(Skill(
|
||
id="deploy-script",
|
||
name="Deploy Script",
|
||
description="Run deployment script",
|
||
freedom=SkillFreedom.LOW.value,
|
||
script_path="/usr/local/bin/deploy.sh",
|
||
tags=["deployment"],
|
||
))
|
||
return registry
|
||
|
||
|
||
@pytest.fixture
|
||
def executor(registry_with_skills):
|
||
return SkillExecutor(registry=registry_with_skills)
|
||
|
||
|
||
@pytest.fixture
|
||
def skills_dir(tmp_path):
|
||
d = tmp_path / "skills"
|
||
d.mkdir()
|
||
(d / "skill1.json").write_text(json.dumps({
|
||
"id": "loaded-skill",
|
||
"name": "Loaded Skill",
|
||
"description": "Loaded from file",
|
||
"freedom": "high",
|
||
}))
|
||
(d / "skill2.json").write_text(json.dumps({
|
||
"id": "another",
|
||
"name": "Another",
|
||
"description": "Another skill",
|
||
"freedom": "medium",
|
||
"prompt_template": "Do {{action}}",
|
||
}))
|
||
return d
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# T1: 注册/查询
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestRegistry:
|
||
def test_register_and_get(self, registry):
|
||
skill = Skill(id="s1", name="Test", description="A test skill")
|
||
registry.register(skill)
|
||
assert registry.get("s1") is not None
|
||
assert registry.get("s1").name == "Test"
|
||
|
||
def test_unregister(self, registry):
|
||
registry.register(Skill(id="s1", name="T", description="D"))
|
||
assert registry.unregister("s1") is True
|
||
assert registry.get("s1") is None
|
||
|
||
def test_unregister_nonexistent(self, registry):
|
||
assert registry.unregister("nope") is False
|
||
|
||
def test_list_skills(self, registry_with_skills):
|
||
skills = registry_with_skills.list_skills()
|
||
assert len(skills) == 3
|
||
|
||
def test_list_by_freedom(self, registry_with_skills):
|
||
high = registry_with_skills.list_skills(freedom=SkillFreedom.HIGH.value)
|
||
assert len(high) == 1
|
||
assert high[0].id == "code-review"
|
||
|
||
def test_list_by_tags(self, registry_with_skills):
|
||
coding = registry_with_skills.list_skills(tags=["coding"])
|
||
assert len(coding) == 2
|
||
|
||
def test_list_enabled_only(self, registry):
|
||
registry.register(Skill(id="s1", name="On", description="D", enabled=True))
|
||
registry.register(Skill(id="s2", name="Off", description="D", enabled=False))
|
||
assert len(registry.list_skills()) == 1
|
||
|
||
def test_load_from_dir(self, skills_dir):
|
||
reg = SkillRegistry(skills_dir=skills_dir)
|
||
assert reg.count() == 2
|
||
assert reg.get("loaded-skill") is not None
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# T2: 三层自由度
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestFreedomLevels:
|
||
def test_high_freedom_prompt(self, executor):
|
||
result = executor.build_prompt("code-review")
|
||
assert "Skill: Code Review" in result
|
||
assert "Principle" in result
|
||
|
||
def test_medium_freedom_template(self, executor):
|
||
result = executor.build_prompt("test-template",
|
||
variables={"module": "auth.py", "focus": "edge cases"})
|
||
assert "auth.py" in result
|
||
assert "edge cases" in result
|
||
|
||
def test_low_freedom_script(self, executor):
|
||
result = executor.build_prompt("deploy-script")
|
||
assert "deploy.sh" in result
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# T3: 技能匹配
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestMatching:
|
||
def test_match_by_name(self, registry_with_skills):
|
||
results = registry_with_skills.match("review")
|
||
assert len(results) >= 1
|
||
assert results[0][0].id == "code-review"
|
||
assert results[0][1] > 0 # score > 0
|
||
|
||
def test_match_by_description(self, registry_with_skills):
|
||
results = registry_with_skills.match("deployment")
|
||
assert len(results) >= 1
|
||
|
||
def test_match_with_task_type(self, registry_with_skills):
|
||
results = registry_with_skills.match("test", task_type="testing")
|
||
assert len(results) >= 1
|
||
|
||
def test_match_no_results(self, registry_with_skills):
|
||
results = registry_with_skills.match("nonexistent_xyz_abc")
|
||
assert len(results) == 0
|
||
|
||
def test_match_limit(self, registry_with_skills):
|
||
results = registry_with_skills.match("code", limit=1)
|
||
assert len(results) <= 1
|
||
|
||
def test_match_excludes_disabled(self, registry):
|
||
registry.register(Skill(id="s1", name="Disabled Review",
|
||
description="review", enabled=False))
|
||
results = registry.match("review")
|
||
assert all(s.enabled for s, _ in results)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# T4: 模板变量替换 + 执行
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestExecution:
|
||
def test_execute_high_freedom(self, executor):
|
||
result = executor.execute("code-review")
|
||
assert result["status"] == "success"
|
||
assert result["prompt"] is not None
|
||
|
||
def test_execute_medium_with_vars(self, executor):
|
||
result = executor.execute("test-template",
|
||
context={"module": "api", "focus": "errors"})
|
||
assert result["status"] == "success"
|
||
assert "api" in result["prompt"]
|
||
|
||
def test_execute_low_blocked(self, executor):
|
||
"""默认不允许脚本执行"""
|
||
result = executor.execute("deploy-script")
|
||
assert result["status"] == "error"
|
||
assert "not allowed" in result["error"]
|
||
|
||
def test_execute_low_allowed(self, registry_with_skills):
|
||
executor = SkillExecutor(registry=registry_with_skills, allow_scripts=True)
|
||
result = executor.execute("deploy-script")
|
||
assert result["status"] == "success"
|
||
|
||
def test_execute_not_found(self, executor):
|
||
result = executor.execute("nonexistent")
|
||
assert result["status"] == "error"
|
||
|
||
def test_execute_disabled(self, registry):
|
||
registry.register(Skill(id="s1", name="T", description="D", enabled=False))
|
||
executor = SkillExecutor(registry=registry)
|
||
result = executor.execute("s1")
|
||
assert result["status"] == "error"
|
||
assert "disabled" in result["error"]
|
||
|
||
def test_execution_log(self, executor):
|
||
executor.execute("code-review")
|
||
assert len(executor.execution_log) == 1
|
||
assert executor.execution_log[0]["skill_id"] == "code-review"
|