ForcePilot/backend/test/unit/agents/test_context_auth.py
Wenjie Zhang 708ff29515 test: 新增测试文件并更新现有测试适配命名与架构变更
- 新增: test_knowledge_base_backend.py, test_skills_middleware.py
- 更新: 所有现有测试适配 db_id→kb_id, get_subagents_from_names→slugs 等接口变更
2026-05-26 17:40:17 +08:00

336 lines
12 KiB
Python

from __future__ import annotations
import importlib.util
import sys
import types
from dataclasses import dataclass, field
from pathlib import Path
import pytest
def _load_context_module():
context_path = Path(__file__).resolve().parents[3] / "package/yuxi/agents/context.py"
previous_yuxi = sys.modules.get("yuxi")
sys.modules["yuxi"] = types.SimpleNamespace(config=types.SimpleNamespace(default_model="test:model"))
try:
spec = importlib.util.spec_from_file_location("test_yuxi_agents_context", context_path)
module = importlib.util.module_from_spec(spec)
assert spec and spec.loader
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
finally:
if previous_yuxi is None:
sys.modules.pop("yuxi", None)
else:
sys.modules["yuxi"] = previous_yuxi
context_module = _load_context_module()
BaseContext = context_module.BaseContext
filter_config_by_role = context_module.filter_config_by_role
normalize_agent_context_config = context_module.normalize_agent_context_config
@dataclass
class SuperAdminOnlyContext(BaseContext):
secret_setting: str = field(default="hidden", metadata={"name": "Secret", "auth": "superadmin"})
def test_get_configurable_items_filters_admin_fields_for_user():
items = BaseContext.get_configurable_items(user_role="user")
assert "system_prompt" in items
assert "summary_threshold" not in items
def test_get_configurable_items_allows_admin_and_superadmin_fields():
admin_items = BaseContext.get_configurable_items(user_role="admin")
superadmin_items = SuperAdminOnlyContext.get_configurable_items(user_role="superadmin")
assert "summary_threshold" in admin_items
assert "secret_setting" in superadmin_items
def test_filter_config_by_role_removes_unauthorized_context_values():
config_json = {
"context": {
"system_prompt": "visible",
"summary_threshold": 10,
"secret_setting": "nope",
},
"other": {"keep": True},
}
filtered = filter_config_by_role(config_json, "user", context_schema=SuperAdminOnlyContext)
assert filtered == {"context": {"system_prompt": "visible"}, "other": {"keep": True}}
assert config_json["context"]["summary_threshold"] == 10
def test_filter_config_by_role_keeps_admin_context_values_for_admin():
filtered = filter_config_by_role(
{"context": {"summary_threshold": 10, "secret_setting": "nope"}},
"admin",
context_schema=SuperAdminOnlyContext,
)
assert filtered == {"context": {"summary_threshold": 10}}
@pytest.mark.asyncio
async def test_resolve_agent_resource_options_empty_fields_loads_nothing(monkeypatch):
async def fail_if_loaded(*_args, **_kwargs):
raise AssertionError("empty resource_fields should not load resources")
monkeypatch.setitem(
sys.modules,
"yuxi.knowledge",
types.SimpleNamespace(knowledge_base=types.SimpleNamespace(get_databases_by_user=fail_if_loaded)),
)
assert await context_module.resolve_agent_resource_options(set(), db=object(), user=object()) == {}
@pytest.mark.asyncio
async def test_normalize_agent_context_config_expands_null_and_filters_explicit_lists(monkeypatch):
async def fake_get_databases_by_user(_user):
return {"databases": [{"kb_id": "kb-a"}, {"kb_id": "kb-b"}]}
async def fake_get_all_mcp_servers(_db):
return [
types.SimpleNamespace(slug="mcp-a", name="MCP A", description="", enabled=True),
types.SimpleNamespace(slug="mcp-b", name="MCP B", description="", enabled=True),
]
async def fake_list_skills(_db):
return [
types.SimpleNamespace(slug="skill-a", name="Skill A", description=""),
types.SimpleNamespace(slug="skill-b", name="Skill B", description=""),
]
async def fake_get_all_subagents(_db=None):
return [
{"slug": "research-agent", "name": "Research", "description": "", "enabled": True},
{"slug": "critique-agent", "name": "Critique", "description": "", "enabled": True},
]
monkeypatch.setitem(
sys.modules,
"yuxi.services.tool_service",
types.SimpleNamespace(
get_tool_metadata=lambda: [
{"slug": "ask_user_question", "name": "Ask User", "description": ""},
{"slug": "tavily_search", "name": "Tavily", "description": ""},
]
),
)
monkeypatch.setitem(
sys.modules,
"yuxi.knowledge",
types.SimpleNamespace(knowledge_base=types.SimpleNamespace(get_databases_by_user=fake_get_databases_by_user)),
)
monkeypatch.setitem(
sys.modules,
"yuxi.services.mcp_service",
types.SimpleNamespace(get_all_mcp_servers=fake_get_all_mcp_servers),
)
monkeypatch.setitem(
sys.modules,
"yuxi.services.skill_service",
types.SimpleNamespace(list_skills=fake_list_skills),
)
monkeypatch.setitem(
sys.modules,
"yuxi.services.subagent_service",
types.SimpleNamespace(get_all_subagents=fake_get_all_subagents),
)
normalized = await normalize_agent_context_config(
{
"tools": None,
"knowledges": ["kb-b", "missing", "kb-b"],
"mcps": None,
"skills": [],
"subagents": ["research-agent", "missing"],
"summary_threshold": 10,
},
db=object(),
user=types.SimpleNamespace(role="user", uid="u1", department_id=None),
)
assert normalized["tools"] == ["ask_user_question", "tavily_search"]
assert normalized["knowledges"] == ["kb-b"]
assert normalized["mcps"] == ["mcp-a", "mcp-b"]
assert normalized["skills"] == []
assert normalized["subagents"] == ["research-agent"]
assert "summary_threshold" not in normalized
@pytest.mark.asyncio
async def test_prepare_agent_runtime_context_filters_resources_and_derives_runtime_scope(monkeypatch):
async def fake_get_databases_by_user(_user):
return {"databases": [{"kb_id": "kb-a"}, {"kb_id": "kb-b"}]}
async def fake_get_all_mcp_servers(_db):
return [types.SimpleNamespace(slug="mcp-a", name="MCP A", description="", enabled=True)]
async def fake_list_skills(_db):
return [
types.SimpleNamespace(slug="skill-a", name="Skill A", description=""),
types.SimpleNamespace(slug="skill-b", name="Skill B", description=""),
]
async def fake_get_all_subagents(_db=None):
return [{"slug": "sub-a", "name": "Sub A", "description": "", "enabled": True}]
async def fake_resolve_visible_knowledge_bases(context):
assert context.knowledges == ["kb-a"]
context._visible_knowledge_bases = [{"slug": "kb-a", "name": "Docs A"}]
return context._visible_knowledge_bases
async def fake_resolve_runtime_skills_for_context(context, *, db=None):
del db
assert context.skills == ["skill-a"]
return {
"context_skills": ["skill-a"],
"prompt_skills": ["skill-a", "skill-b"],
"readable_skills": ["skill-a", "skill-b"],
}
class FakeSessionContext:
async def __aenter__(self):
return object()
async def __aexit__(self, exc_type, exc, tb):
return None
class FakeUserRepository:
async def get_by_uid_with_db(self, _db, uid):
assert uid == "u1"
return types.SimpleNamespace(role="user", uid="u1")
monkeypatch.setitem(
sys.modules,
"yuxi.agents.backends.knowledge_base_backend",
types.SimpleNamespace(resolve_visible_knowledge_bases_for_context=fake_resolve_visible_knowledge_bases),
)
monkeypatch.setitem(
sys.modules,
"yuxi.agents.middlewares.skills_middleware",
types.SimpleNamespace(resolve_runtime_skills_for_context=fake_resolve_runtime_skills_for_context),
)
monkeypatch.setitem(
sys.modules,
"yuxi.repositories.user_repository",
types.SimpleNamespace(UserRepository=FakeUserRepository),
)
monkeypatch.setitem(
sys.modules,
"yuxi.storage.postgres.manager",
types.SimpleNamespace(pg_manager=types.SimpleNamespace(get_async_session_context=lambda: FakeSessionContext())),
)
monkeypatch.setitem(
sys.modules,
"yuxi.services.tool_service",
types.SimpleNamespace(
get_tool_metadata=lambda: [{"slug": "ask_user_question", "name": "Ask User", "description": ""}]
),
)
monkeypatch.setitem(
sys.modules,
"yuxi.knowledge",
types.SimpleNamespace(knowledge_base=types.SimpleNamespace(get_databases_by_user=fake_get_databases_by_user)),
)
monkeypatch.setitem(
sys.modules,
"yuxi.services.mcp_service",
types.SimpleNamespace(get_all_mcp_servers=fake_get_all_mcp_servers),
)
monkeypatch.setitem(
sys.modules,
"yuxi.services.skill_service",
types.SimpleNamespace(list_skills=fake_list_skills),
)
monkeypatch.setitem(
sys.modules,
"yuxi.services.subagent_service",
types.SimpleNamespace(get_all_subagents=fake_get_all_subagents),
)
context = BaseContext(
uid="u1",
tools=["ask_user_question", "missing"],
knowledges=["kb-a", "missing"],
mcps=None,
skills=["skill-a", "missing"],
subagents=[],
)
prepared = await context_module.prepare_agent_runtime_context(context)
assert prepared.tools == ["ask_user_question"]
assert prepared.knowledges == ["kb-a"]
assert prepared.mcps == ["mcp-a"]
assert prepared.skills == ["skill-a"]
assert prepared.subagents == []
assert prepared._visible_knowledge_bases == [{"slug": "kb-a", "name": "Docs A"}]
assert prepared._prompt_skills == ["skill-a", "skill-b"]
assert prepared._readable_skills == ["skill-a", "skill-b"]
@pytest.mark.asyncio
async def test_prepare_agent_runtime_context_clears_resources_for_missing_user(monkeypatch):
class FakeSessionContext:
async def __aenter__(self):
return object()
async def __aexit__(self, exc_type, exc, tb):
return None
class FakeUserRepository:
async def get_by_uid_with_db(self, _db, _uid):
return None
monkeypatch.setitem(
sys.modules,
"yuxi.agents.backends.knowledge_base_backend",
types.SimpleNamespace(resolve_visible_knowledge_bases_for_context=lambda _context: None),
)
monkeypatch.setitem(
sys.modules,
"yuxi.agents.middlewares.skills_middleware",
types.SimpleNamespace(resolve_runtime_skills_for_context=lambda _context, db=None: None),
)
monkeypatch.setitem(
sys.modules,
"yuxi.repositories.user_repository",
types.SimpleNamespace(UserRepository=FakeUserRepository),
)
monkeypatch.setitem(
sys.modules,
"yuxi.storage.postgres.manager",
types.SimpleNamespace(pg_manager=types.SimpleNamespace(get_async_session_context=lambda: FakeSessionContext())),
)
context = BaseContext(
uid="missing",
tools=["tool"],
knowledges=["kb"],
mcps=["mcp"],
skills=["skill"],
subagents=["agent"],
)
prepared = await context_module.prepare_agent_runtime_context(context)
assert prepared.tools == []
assert prepared.knowledges == []
assert prepared.mcps == []
assert prepared.skills == []
assert prepared.subagents == []
assert prepared._visible_knowledge_bases == []
assert prepared._prompt_skills == []
assert prepared._readable_skills == []