310 lines
11 KiB
Python
310 lines
11 KiB
Python
"""Tests for Skill Management API routes"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from agentkit.llm.gateway import LLMGateway
|
|
from agentkit.server.app import create_app
|
|
from agentkit.server.config import ServerConfig
|
|
from agentkit.skills.base import Skill, SkillConfig
|
|
from agentkit.skills.registry import SkillRegistry
|
|
from agentkit.tools.registry import ToolRegistry
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_llm_gateway():
|
|
return LLMGateway()
|
|
|
|
|
|
@pytest.fixture
|
|
def skill_registry():
|
|
return SkillRegistry()
|
|
|
|
|
|
@pytest.fixture
|
|
def tool_registry():
|
|
return ToolRegistry()
|
|
|
|
|
|
@pytest.fixture
|
|
def app(mock_llm_gateway, skill_registry, tool_registry):
|
|
return create_app(
|
|
llm_gateway=mock_llm_gateway,
|
|
skill_registry=skill_registry,
|
|
tool_registry=tool_registry,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def client(app):
|
|
return TestClient(app)
|
|
|
|
|
|
def _register_skill(registry: SkillRegistry, name: str = "test_skill", **kwargs):
|
|
"""Helper to register a skill with sensible defaults."""
|
|
config = SkillConfig(
|
|
name=name,
|
|
agent_type="test_type",
|
|
task_mode="llm_generate",
|
|
prompt={"identity": "Test Skill", "instructions": "Handle test"},
|
|
intent={"keywords": ["test"], "description": "A test skill"},
|
|
**kwargs,
|
|
)
|
|
skill = Skill(config=config)
|
|
registry.register(skill)
|
|
return skill
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GET /skill-management/skills
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestListSkills:
|
|
def test_list_skills_empty(self, client):
|
|
response = client.get("/api/v1/skill-management/skills")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "skills" in data
|
|
assert isinstance(data["skills"], list)
|
|
assert "total" in data
|
|
assert "page" in data
|
|
assert "size" in data
|
|
|
|
def test_list_skills_with_registered(self, client, skill_registry):
|
|
_register_skill(skill_registry, "skill_a")
|
|
_register_skill(skill_registry, "skill_b")
|
|
|
|
response = client.get("/api/v1/skill-management/skills")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["total"] >= 2
|
|
assert len(data["skills"]) >= 2
|
|
|
|
def test_list_skills_pagination(self, client, skill_registry):
|
|
for i in range(5):
|
|
_register_skill(skill_registry, f"page_skill_{i}")
|
|
|
|
response = client.get("/api/v1/skill-management/skills?page=1&size=2")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["size"] == 2
|
|
assert len(data["skills"]) <= 2
|
|
assert data["total"] >= 5
|
|
|
|
def test_list_skills_skill_structure(self, client, skill_registry):
|
|
_register_skill(skill_registry, "struct_skill")
|
|
|
|
response = client.get("/api/v1/skill-management/skills")
|
|
data = response.json()
|
|
if data["skills"]:
|
|
skill = data["skills"][0]
|
|
assert "name" in skill
|
|
assert "version" in skill
|
|
assert "description" in skill
|
|
assert "capabilities" in skill
|
|
assert "dependencies" in skill
|
|
assert "status" in skill
|
|
|
|
def test_category_derived_from_name_suffix(self, client, skill_registry):
|
|
"""category field distinguishes agent templates from business skills.
|
|
|
|
*_agent -> "agent_template"
|
|
others -> "business_skill"
|
|
Also verifies agent_type/execution_mode/task_mode are exposed.
|
|
"""
|
|
_register_skill(skill_registry, "react_agent", execution_mode="react")
|
|
_register_skill(skill_registry, "geo_optimizer")
|
|
|
|
response = client.get("/api/v1/skill-management/skills")
|
|
data = response.json()
|
|
by_name = {s["name"]: s for s in data["skills"]}
|
|
|
|
agent = by_name["react_agent"]
|
|
assert agent["category"] == "agent_template"
|
|
assert agent["execution_mode"] == "react"
|
|
assert agent["agent_type"] == "test_type"
|
|
assert agent["task_mode"] == "llm_generate"
|
|
|
|
biz = by_name["geo_optimizer"]
|
|
assert biz["category"] == "business_skill"
|
|
|
|
def test_category_no_orphans(self, client, skill_registry):
|
|
"""Every skill must fall into exactly one category — no orphans."""
|
|
_register_skill(skill_registry, "react_agent")
|
|
_register_skill(skill_registry, "geo_optimizer")
|
|
_register_skill(skill_registry, "trend_agent")
|
|
|
|
response = client.get("/api/v1/skill-management/skills")
|
|
data = response.json()
|
|
valid = {"agent_template", "business_skill"}
|
|
assert all(s["category"] in valid for s in data["skills"])
|
|
|
|
def test_trend_agent_classified_as_business_skill(self, client, skill_registry):
|
|
"""trend_agent has _agent suffix but is a business-domain skill, not an engine."""
|
|
_register_skill(skill_registry, "trend_agent")
|
|
|
|
response = client.get("/api/v1/skill-management/skills")
|
|
data = response.json()
|
|
skill = next(s for s in data["skills"] if s["name"] == "trend_agent")
|
|
assert skill["category"] == "business_skill"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GET /skill-management/skills/{skill_name}
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestGetSkillDetail:
|
|
def test_get_skill_detail(self, client, skill_registry):
|
|
_register_skill(skill_registry, "detail_skill")
|
|
|
|
response = client.get("/api/v1/skill-management/skills/detail_skill")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["name"] == "detail_skill"
|
|
assert "version" in data
|
|
assert "description" in data
|
|
assert "capabilities" in data
|
|
assert "dependencies" in data
|
|
assert "config" in data
|
|
assert "health_status" in data
|
|
|
|
def test_get_skill_detail_not_found(self, client):
|
|
response = client.get("/api/v1/skill-management/skills/nonexistent")
|
|
assert response.status_code == 404
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GET /skill-management/skills/{skill_name}/health
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSkillHealth:
|
|
def test_skill_health(self, client, skill_registry):
|
|
_register_skill(skill_registry, "health_skill")
|
|
|
|
response = client.get("/api/v1/skill-management/skills/health_skill/health")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["skill_name"] == "health_skill"
|
|
assert data["status"] == "healthy"
|
|
|
|
def test_skill_health_not_found(self, client):
|
|
response = client.get("/api/v1/skill-management/skills/nonexistent/health")
|
|
assert response.status_code == 404
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GET /skill-management/capabilities
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestListCapabilities:
|
|
def test_list_capabilities_empty(self, client):
|
|
response = client.get("/api/v1/skill-management/capabilities")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "capabilities" in data
|
|
assert isinstance(data["capabilities"], list)
|
|
|
|
def test_list_capabilities_with_skills(self, client, skill_registry):
|
|
_register_skill(skill_registry, "cap_skill", capabilities=["chat"])
|
|
|
|
response = client.get("/api/v1/skill-management/capabilities")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data["capabilities"]) >= 1
|
|
# Each capability should have name, display_name, skill_count
|
|
for cap in data["capabilities"]:
|
|
assert "name" in cap
|
|
assert "display_name" in cap
|
|
assert "skill_count" in cap
|
|
|
|
def test_list_capabilities_structure(self, client, skill_registry):
|
|
_register_skill(skill_registry, "multi_cap_skill", capabilities=["chat", "search"])
|
|
|
|
response = client.get("/api/v1/skill-management/capabilities")
|
|
data = response.json()
|
|
cap_names = [c["name"] for c in data["capabilities"]]
|
|
assert "chat" in cap_names
|
|
assert "search" in cap_names
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# POST /skill-management/skills/{skill_name}/reload
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestReloadSkill:
|
|
def test_reload_skill(self, client, skill_registry, tmp_path):
|
|
# Write a valid YAML file for "reload_skill" to a temp skills dir.
|
|
skills_dir = tmp_path / "skills"
|
|
skills_dir.mkdir()
|
|
yaml_content = (
|
|
"name: reload_skill\n"
|
|
'agent_type: "test_type"\n'
|
|
'version: "1.0.0"\n'
|
|
'description: "Skill for reload testing"\n'
|
|
"task_mode: llm_generate\n"
|
|
"execution_mode: direct\n"
|
|
"max_steps: 1\n"
|
|
"intent:\n"
|
|
' keywords: ["reload"]\n'
|
|
' description: "reload test"\n'
|
|
"prompt:\n"
|
|
' identity: "reload tester"\n'
|
|
' instructions: "handle reload"\n'
|
|
"tools: []\n"
|
|
)
|
|
(skills_dir / "reload_skill.yaml").write_text(yaml_content, encoding="utf-8")
|
|
|
|
# Point the app at the temp skills dir and register the skill so the
|
|
# 404 guard in the route passes.
|
|
client.app.state.server_config = ServerConfig(skill_paths=[str(skills_dir)])
|
|
_register_skill(skill_registry, "reload_skill")
|
|
|
|
response = client.post("/api/v1/skill-management/skills/reload_skill/reload")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["skill_name"] == "reload_skill"
|
|
assert data["status"] == "reloaded"
|
|
|
|
def test_reload_skill_not_found(self, client):
|
|
response = client.post("/api/v1/skill-management/skills/nonexistent/reload")
|
|
assert response.status_code == 404
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Capability filtering
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSkillCapabilityFilter:
|
|
def test_filter_by_capability(self, client, skill_registry):
|
|
_register_skill(skill_registry, "chat_only_skill", capabilities=["chat"])
|
|
_register_skill(skill_registry, "search_only_skill", capabilities=["search"])
|
|
|
|
response = client.get("/api/v1/skill-management/skills?capability=chat")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
# All returned skills should have "chat" capability
|
|
for skill in data["skills"]:
|
|
assert "chat" in skill["capabilities"]
|
|
|
|
def test_filter_by_nonexistent_capability(self, client, skill_registry):
|
|
_register_skill(skill_registry, "some_skill")
|
|
|
|
response = client.get("/api/v1/skill-management/skills?capability=nonexistent_cap")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["total"] == 0
|
|
assert len(data["skills"]) == 0
|