- 新增: test_knowledge_base_backend.py, test_skills_middleware.py - 更新: 所有现有测试适配 db_id→kb_id, get_subagents_from_names→slugs 等接口变更
134 lines
4.5 KiB
Python
134 lines
4.5 KiB
Python
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
from langchain_core.messages import ToolMessage
|
|
from langgraph.types import Command
|
|
|
|
import yuxi.agents.middlewares.skills_middleware as skills_middleware
|
|
from yuxi.agents.middlewares.skills_middleware import SkillsMiddleware, resolve_runtime_skills_for_context
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_resolve_runtime_skills_derives_prompt_and_readable_closure(monkeypatch):
|
|
async def fake_get_dependency_map(db=None):
|
|
del db
|
|
return {
|
|
"alpha": {"tools": [], "mcps": [], "skills": ["beta"]},
|
|
"beta": {"tools": [], "mcps": [], "skills": []},
|
|
}
|
|
|
|
monkeypatch.setattr(skills_middleware, "get_dependency_map", fake_get_dependency_map)
|
|
|
|
context = SimpleNamespace(skills=["alpha", "missing"])
|
|
|
|
scope = await resolve_runtime_skills_for_context(context)
|
|
|
|
assert scope == {
|
|
"context_skills": ["alpha"],
|
|
"prompt_skills": ["alpha", "beta"],
|
|
"readable_skills": ["alpha", "beta"],
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_skills_prompt_uses_prepared_prompt_skills(monkeypatch):
|
|
async def fake_get_prompt_metadata(db=None):
|
|
del db
|
|
return {
|
|
"alpha": {
|
|
"name": "Alpha",
|
|
"description": "alpha desc",
|
|
"path": "/home/gem/skills/alpha/SKILL.md",
|
|
},
|
|
"configured-only": {
|
|
"name": "Configured Only",
|
|
"description": "should not appear",
|
|
"path": "/home/gem/skills/configured-only/SKILL.md",
|
|
},
|
|
}
|
|
|
|
monkeypatch.setattr(skills_middleware, "get_prompt_metadata", fake_get_prompt_metadata)
|
|
|
|
context = SimpleNamespace(
|
|
system_prompt="base",
|
|
skills=["configured-only"],
|
|
_prompt_skills=["alpha"],
|
|
)
|
|
|
|
await SkillsMiddleware().abefore_agent({}, SimpleNamespace(context=context))
|
|
|
|
assert "base" in context.system_prompt
|
|
assert "Alpha" in context.system_prompt
|
|
assert "Configured Only" not in context.system_prompt
|
|
assert getattr(context, "_skills_prompt_injected") is True
|
|
assert not hasattr(context, "_visible_skills")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_awrap_model_call_mounts_dependencies_only_for_readable_activated_skills(monkeypatch):
|
|
async def fake_get_dependency_map(db=None):
|
|
del db
|
|
return {
|
|
"alpha": {"tools": ["tool-a"], "mcps": [], "skills": []},
|
|
"beta": {"tools": ["tool-b"], "mcps": [], "skills": []},
|
|
}
|
|
|
|
monkeypatch.setattr(skills_middleware, "get_dependency_map", fake_get_dependency_map)
|
|
monkeypatch.setattr(
|
|
skills_middleware,
|
|
"get_all_tool_instances",
|
|
lambda: [SimpleNamespace(name="tool-a"), SimpleNamespace(name="tool-b")],
|
|
)
|
|
|
|
class FakeRequest:
|
|
def __init__(self, tools=None):
|
|
self.runtime = SimpleNamespace(context=SimpleNamespace(_readable_skills=["alpha"], mcps=[]))
|
|
self.state = {"activated_skills": ["alpha", "beta"]}
|
|
self.tools = tools or []
|
|
|
|
def override(self, *, tools):
|
|
new_request = FakeRequest(tools=tools)
|
|
new_request.runtime = self.runtime
|
|
new_request.state = self.state
|
|
return new_request
|
|
|
|
captured = {}
|
|
|
|
async def handler(request):
|
|
captured["tools"] = [tool.name for tool in request.tools]
|
|
return "ok"
|
|
|
|
result = await SkillsMiddleware().awrap_model_call(FakeRequest(), handler)
|
|
|
|
assert result == "ok"
|
|
assert captured["tools"] == ["tool-a"]
|
|
|
|
|
|
def test_read_file_activates_only_readable_skill() -> None:
|
|
middleware = SkillsMiddleware()
|
|
result = ToolMessage(content="ok", tool_call_id="tool-1", name="read_file")
|
|
request = SimpleNamespace(
|
|
runtime=SimpleNamespace(context=SimpleNamespace(_readable_skills=["alpha"])),
|
|
tool_call={"name": "read_file", "args": {"file_path": "/home/gem/skills/alpha/SKILL.md"}},
|
|
)
|
|
|
|
updated = middleware._process_tool_call_result(result, request)
|
|
|
|
assert isinstance(updated, Command)
|
|
assert updated.update["activated_skills"] == ["alpha"]
|
|
|
|
|
|
def test_read_file_denies_skill_outside_readable_scope() -> None:
|
|
middleware = SkillsMiddleware()
|
|
result = ToolMessage(content="ok", tool_call_id="tool-1", name="read_file")
|
|
request = SimpleNamespace(
|
|
runtime=SimpleNamespace(context=SimpleNamespace(_readable_skills=["alpha"])),
|
|
tool_call={"name": "read_file", "args": {"file_path": "/home/gem/skills/beta/SKILL.md"}},
|
|
)
|
|
|
|
updated = middleware._process_tool_call_result(result, request)
|
|
|
|
assert updated is result
|