feat: 添加项目规则、环境配置示例及开发文档
This commit is contained in:
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""MineNASAI 测试套件"""
|
||||
1
tests/agent/__init__.py
Normal file
1
tests/agent/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Agent 测试"""
|
||||
22
tests/conftest.py
Normal file
22
tests/conftest.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""pytest 配置和共享 fixtures"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from minenasai.core import reset_settings
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_global_settings():
|
||||
"""每个测试后重置全局配置"""
|
||||
yield
|
||||
reset_settings()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_config(tmp_path):
|
||||
"""创建临时配置目录"""
|
||||
config_dir = tmp_path / ".config" / "minenasai"
|
||||
config_dir.mkdir(parents=True)
|
||||
return config_dir
|
||||
1
tests/gateway/__init__.py
Normal file
1
tests/gateway/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Gateway 测试"""
|
||||
124
tests/test_core.py
Normal file
124
tests/test_core.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""核心模块测试"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from minenasai.core import Settings, get_settings, load_config, reset_settings
|
||||
from minenasai.core.config import expand_path
|
||||
|
||||
|
||||
class TestConfig:
|
||||
"""配置模块测试"""
|
||||
|
||||
def test_default_settings(self):
|
||||
"""测试默认配置"""
|
||||
settings = Settings()
|
||||
|
||||
assert settings.app.name == "MineNASAI"
|
||||
assert settings.app.version == "0.1.0"
|
||||
assert settings.gateway.port == 8000
|
||||
assert settings.webtui.port == 8080
|
||||
|
||||
def test_expand_path(self):
|
||||
"""测试路径展开"""
|
||||
import os
|
||||
|
||||
home = os.path.expanduser("~")
|
||||
path = expand_path("~/test")
|
||||
|
||||
assert str(path).startswith(home)
|
||||
|
||||
def test_load_config_no_file(self, tmp_path):
|
||||
"""测试配置文件不存在时使用默认值"""
|
||||
config_path = tmp_path / "nonexistent.json5"
|
||||
settings = load_config(config_path)
|
||||
|
||||
assert settings.app.name == "MineNASAI"
|
||||
|
||||
def test_load_config_with_file(self, tmp_path):
|
||||
"""测试从文件加载配置"""
|
||||
import json5
|
||||
|
||||
config_path = tmp_path / "config.json5"
|
||||
config_data = {
|
||||
"app": {"name": "TestApp", "debug": True},
|
||||
"gateway": {"port": 9000},
|
||||
}
|
||||
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
json5.dump(config_data, f)
|
||||
|
||||
settings = load_config(config_path)
|
||||
|
||||
assert settings.app.name == "TestApp"
|
||||
assert settings.app.debug is True
|
||||
assert settings.gateway.port == 9000
|
||||
|
||||
def test_get_settings_singleton(self):
|
||||
"""测试全局配置单例"""
|
||||
reset_settings()
|
||||
|
||||
settings1 = get_settings()
|
||||
settings2 = get_settings()
|
||||
|
||||
assert settings1 is settings2
|
||||
|
||||
def test_env_override(self, monkeypatch):
|
||||
"""测试环境变量覆盖"""
|
||||
# Settings 使用 MINENASAI_ 前缀
|
||||
monkeypatch.setenv("MINENASAI_ANTHROPIC_API_KEY", "test-key")
|
||||
|
||||
reset_settings()
|
||||
settings = Settings()
|
||||
|
||||
assert settings.anthropic_api_key == "test-key"
|
||||
|
||||
|
||||
class TestLogging:
|
||||
"""日志模块测试"""
|
||||
|
||||
def test_setup_logging(self):
|
||||
"""测试日志初始化"""
|
||||
from minenasai.core import setup_logging
|
||||
from minenasai.core.config import LoggingConfig
|
||||
|
||||
config = LoggingConfig(level="DEBUG", format="console", file=False)
|
||||
setup_logging(config)
|
||||
|
||||
# 应该不抛出异常
|
||||
|
||||
def test_get_logger(self):
|
||||
"""测试获取日志记录器"""
|
||||
from minenasai.core import get_logger
|
||||
|
||||
logger = get_logger("test")
|
||||
|
||||
assert logger is not None
|
||||
|
||||
def test_audit_logger(self, tmp_path):
|
||||
"""测试审计日志"""
|
||||
from minenasai.core.logging import AuditLogger
|
||||
|
||||
audit_path = tmp_path / "audit.jsonl"
|
||||
audit = AuditLogger(audit_path)
|
||||
|
||||
audit.log_tool_call(
|
||||
agent_id="test-agent",
|
||||
tool_name="read",
|
||||
params={"path": "/test"},
|
||||
danger_level="safe",
|
||||
result="success",
|
||||
duration_ms=100,
|
||||
)
|
||||
|
||||
assert audit_path.exists()
|
||||
|
||||
with open(audit_path, encoding="utf-8") as f:
|
||||
record = json.loads(f.readline())
|
||||
|
||||
assert record["agent_id"] == "test-agent"
|
||||
assert record["tool_name"] == "read"
|
||||
assert record["danger_level"] == "safe"
|
||||
144
tests/test_gateway.py
Normal file
144
tests/test_gateway.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""Gateway 模块测试"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from minenasai.gateway.protocol import (
|
||||
ChatMessage,
|
||||
MessageType,
|
||||
TaskComplexity,
|
||||
parse_message,
|
||||
)
|
||||
from minenasai.gateway.router import SmartRouter
|
||||
|
||||
|
||||
class TestProtocol:
|
||||
"""协议测试"""
|
||||
|
||||
def test_chat_message(self):
|
||||
"""测试聊天消息"""
|
||||
msg = ChatMessage(content="你好")
|
||||
|
||||
assert msg.type == MessageType.CHAT
|
||||
assert msg.content == "你好"
|
||||
assert msg.id is not None
|
||||
|
||||
def test_parse_message(self):
|
||||
"""测试消息解析"""
|
||||
data = {
|
||||
"type": "chat",
|
||||
"content": "测试消息",
|
||||
}
|
||||
msg = parse_message(data)
|
||||
|
||||
assert isinstance(msg, ChatMessage)
|
||||
assert msg.content == "测试消息"
|
||||
|
||||
def test_parse_invalid_type(self):
|
||||
"""测试无效消息类型"""
|
||||
data = {"type": "invalid"}
|
||||
|
||||
with pytest.raises(ValueError, match="未知的消息类型"):
|
||||
parse_message(data)
|
||||
|
||||
def test_parse_missing_type(self):
|
||||
"""测试缺少类型"""
|
||||
data = {"content": "test"}
|
||||
|
||||
with pytest.raises(ValueError, match="缺少 type"):
|
||||
parse_message(data)
|
||||
|
||||
|
||||
class TestRouter:
|
||||
"""智能路由测试"""
|
||||
|
||||
def setup_method(self):
|
||||
"""初始化路由器"""
|
||||
self.router = SmartRouter()
|
||||
|
||||
def test_simple_question(self):
|
||||
"""测试简单问题"""
|
||||
result = self.router.evaluate("今天天气怎么样?")
|
||||
|
||||
assert result["complexity"] == TaskComplexity.SIMPLE
|
||||
assert result["suggested_handler"] == "quick_response"
|
||||
|
||||
def test_complex_task(self):
|
||||
"""测试复杂任务"""
|
||||
# 包含多个复杂关键词:重构、实现、优化
|
||||
result = self.router.evaluate(
|
||||
"请帮我重构这个项目的数据库模块,实现异步操作支持,"
|
||||
"优化连接池管理,同时要保持向后兼容。这是一个架构设计任务。"
|
||||
)
|
||||
|
||||
# 复杂任务应该识别为 COMPLEX 或 MEDIUM
|
||||
assert result["complexity"] in [TaskComplexity.COMPLEX, TaskComplexity.MEDIUM]
|
||||
|
||||
def test_medium_task(self):
|
||||
"""测试中等任务"""
|
||||
result = self.router.evaluate("查看当前目录下的文件列表")
|
||||
|
||||
assert result["complexity"] in [TaskComplexity.SIMPLE, TaskComplexity.MEDIUM]
|
||||
|
||||
def test_command_override_simple(self):
|
||||
"""测试命令覆盖 - 简单"""
|
||||
result = self.router.evaluate("/快速 帮我重构整个项目")
|
||||
|
||||
assert result["complexity"] == TaskComplexity.SIMPLE
|
||||
assert result["confidence"] == 1.0
|
||||
assert result["content"] == "帮我重构整个项目"
|
||||
|
||||
def test_command_override_complex(self):
|
||||
"""测试命令覆盖 - 复杂"""
|
||||
result = self.router.evaluate("/深度 你好")
|
||||
|
||||
assert result["complexity"] == TaskComplexity.COMPLEX
|
||||
assert result["confidence"] == 1.0
|
||||
|
||||
def test_code_detection(self):
|
||||
"""测试代码检测"""
|
||||
result = self.router.evaluate(
|
||||
"请帮我实现这个函数:\n```python\ndef hello():\n pass\n```"
|
||||
)
|
||||
|
||||
assert result["complexity"] == TaskComplexity.COMPLEX
|
||||
|
||||
def test_multi_step_detection(self):
|
||||
"""测试多步骤检测"""
|
||||
# 使用英文 step 模式和更多内容
|
||||
result = self.router.evaluate(
|
||||
"Step 1: 创建数据库表结构\n"
|
||||
"Step 2: 实现数据导入功能\n"
|
||||
"Step 3: 开发验证脚本\n"
|
||||
"Step 4: 部署到生产环境"
|
||||
)
|
||||
|
||||
# 多步骤任务应该识别为复杂任务
|
||||
assert result["complexity"] in [TaskComplexity.COMPLEX, TaskComplexity.MEDIUM]
|
||||
|
||||
|
||||
class TestRouterEdgeCases:
|
||||
"""路由器边界情况测试"""
|
||||
|
||||
def setup_method(self):
|
||||
self.router = SmartRouter()
|
||||
|
||||
def test_empty_content(self):
|
||||
"""测试空内容"""
|
||||
result = self.router.evaluate("")
|
||||
|
||||
assert result["complexity"] == TaskComplexity.SIMPLE
|
||||
|
||||
def test_very_long_content(self):
|
||||
"""测试超长内容"""
|
||||
long_content = "请帮我分析 " + "这段代码 " * 200
|
||||
result = self.router.evaluate(long_content)
|
||||
|
||||
assert result["complexity"] == TaskComplexity.COMPLEX
|
||||
|
||||
def test_special_characters(self):
|
||||
"""测试特殊字符"""
|
||||
result = self.router.evaluate("查看 /tmp/test.txt 文件内容")
|
||||
|
||||
assert result["complexity"] in [TaskComplexity.SIMPLE, TaskComplexity.MEDIUM]
|
||||
135
tests/test_llm.py
Normal file
135
tests/test_llm.py
Normal file
@@ -0,0 +1,135 @@
|
||||
"""LLM 模块测试"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from minenasai.llm.base import Message, Provider, ToolCall, ToolDefinition
|
||||
|
||||
|
||||
class TestProvider:
|
||||
"""Provider 枚举测试"""
|
||||
|
||||
def test_is_overseas(self):
|
||||
"""测试境外服务识别"""
|
||||
assert Provider.ANTHROPIC.is_overseas is True
|
||||
assert Provider.OPENAI.is_overseas is True
|
||||
assert Provider.GEMINI.is_overseas is True
|
||||
|
||||
assert Provider.DEEPSEEK.is_overseas is False
|
||||
assert Provider.ZHIPU.is_overseas is False
|
||||
assert Provider.MINIMAX.is_overseas is False
|
||||
assert Provider.MOONSHOT.is_overseas is False
|
||||
|
||||
def test_display_name(self):
|
||||
"""测试显示名称"""
|
||||
assert "Claude" in Provider.ANTHROPIC.display_name
|
||||
assert "GPT" in Provider.OPENAI.display_name
|
||||
assert "DeepSeek" in Provider.DEEPSEEK.display_name
|
||||
assert "GLM" in Provider.ZHIPU.display_name
|
||||
assert "Kimi" in Provider.MOONSHOT.display_name
|
||||
|
||||
|
||||
class TestMessage:
|
||||
"""消息测试"""
|
||||
|
||||
def test_basic_message(self):
|
||||
"""测试基本消息"""
|
||||
msg = Message(role="user", content="Hello")
|
||||
assert msg.role == "user"
|
||||
assert msg.content == "Hello"
|
||||
assert msg.tool_calls is None
|
||||
|
||||
def test_message_with_tool_call(self):
|
||||
"""测试带工具调用的消息"""
|
||||
tool_call = ToolCall(
|
||||
id="tc_123",
|
||||
name="read_file",
|
||||
arguments={"path": "/test.txt"},
|
||||
)
|
||||
msg = Message(
|
||||
role="assistant",
|
||||
content="Let me read that file.",
|
||||
tool_calls=[tool_call],
|
||||
)
|
||||
assert len(msg.tool_calls) == 1
|
||||
assert msg.tool_calls[0].name == "read_file"
|
||||
|
||||
|
||||
class TestToolDefinition:
|
||||
"""工具定义测试"""
|
||||
|
||||
def test_tool_definition(self):
|
||||
"""测试工具定义"""
|
||||
tool = ToolDefinition(
|
||||
name="read_file",
|
||||
description="Read a file",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string"},
|
||||
},
|
||||
"required": ["path"],
|
||||
},
|
||||
)
|
||||
assert tool.name == "read_file"
|
||||
assert "path" in tool.parameters["properties"]
|
||||
|
||||
|
||||
class TestClientImports:
|
||||
"""客户端导入测试"""
|
||||
|
||||
def test_import_all_clients(self):
|
||||
"""测试导入所有客户端"""
|
||||
from minenasai.llm.clients import (
|
||||
AnthropicClient,
|
||||
DeepSeekClient,
|
||||
GeminiClient,
|
||||
MiniMaxClient,
|
||||
MoonshotClient,
|
||||
OpenAICompatClient,
|
||||
ZhipuClient,
|
||||
)
|
||||
|
||||
assert AnthropicClient.provider == Provider.ANTHROPIC
|
||||
assert OpenAICompatClient.provider == Provider.OPENAI
|
||||
assert DeepSeekClient.provider == Provider.DEEPSEEK
|
||||
assert ZhipuClient.provider == Provider.ZHIPU
|
||||
assert MiniMaxClient.provider == Provider.MINIMAX
|
||||
assert MoonshotClient.provider == Provider.MOONSHOT
|
||||
assert GeminiClient.provider == Provider.GEMINI
|
||||
|
||||
def test_client_models(self):
|
||||
"""测试客户端模型列表"""
|
||||
from minenasai.llm.clients import (
|
||||
AnthropicClient,
|
||||
DeepSeekClient,
|
||||
ZhipuClient,
|
||||
)
|
||||
|
||||
assert "claude-sonnet-4-20250514" in AnthropicClient.MODELS
|
||||
assert "deepseek-chat" in DeepSeekClient.MODELS
|
||||
assert "glm-4-plus" in ZhipuClient.MODELS
|
||||
|
||||
|
||||
class TestLLMManager:
|
||||
"""LLM 管理器测试"""
|
||||
|
||||
def test_import_manager(self):
|
||||
"""测试导入管理器"""
|
||||
from minenasai.llm import LLMManager, get_llm_manager
|
||||
|
||||
manager = get_llm_manager()
|
||||
assert isinstance(manager, LLMManager)
|
||||
|
||||
def test_no_api_keys(self):
|
||||
"""测试无 API Key 时的行为"""
|
||||
from minenasai.llm import LLMManager
|
||||
|
||||
manager = LLMManager()
|
||||
manager.initialize()
|
||||
|
||||
# 没有配置 API Key,应该没有可用的提供商
|
||||
providers = manager.get_available_providers()
|
||||
# 可能为空,取决于环境变量
|
||||
assert isinstance(providers, list)
|
||||
227
tests/test_permissions.py
Normal file
227
tests/test_permissions.py
Normal file
@@ -0,0 +1,227 @@
|
||||
"""权限模块测试"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from minenasai.agent.permissions import (
|
||||
ConfirmationStatus,
|
||||
DangerLevel,
|
||||
PermissionManager,
|
||||
ToolPermission,
|
||||
)
|
||||
|
||||
|
||||
class TestDangerLevel:
|
||||
"""DangerLevel 枚举测试"""
|
||||
|
||||
def test_danger_levels(self):
|
||||
"""测试危险等级"""
|
||||
assert DangerLevel.SAFE.value == "safe"
|
||||
assert DangerLevel.CRITICAL.value == "critical"
|
||||
|
||||
|
||||
class TestToolPermission:
|
||||
"""ToolPermission 测试"""
|
||||
|
||||
def test_default_permission(self):
|
||||
"""测试默认权限"""
|
||||
perm = ToolPermission(
|
||||
name="test_tool",
|
||||
danger_level=DangerLevel.SAFE,
|
||||
)
|
||||
|
||||
assert perm.requires_confirmation is False
|
||||
assert perm.rate_limit is None
|
||||
|
||||
|
||||
class TestPermissionManager:
|
||||
"""PermissionManager 测试"""
|
||||
|
||||
def setup_method(self):
|
||||
"""初始化"""
|
||||
self.manager = PermissionManager()
|
||||
|
||||
def test_get_default_permission(self):
|
||||
"""测试获取默认权限"""
|
||||
perm = self.manager.get_permission("read_file")
|
||||
|
||||
assert perm is not None
|
||||
assert perm.danger_level == DangerLevel.SAFE
|
||||
|
||||
def test_register_tool(self):
|
||||
"""测试注册工具权限"""
|
||||
perm = ToolPermission(
|
||||
name="custom_tool",
|
||||
danger_level=DangerLevel.MEDIUM,
|
||||
description="自定义工具",
|
||||
)
|
||||
self.manager.register_tool(perm)
|
||||
|
||||
result = self.manager.get_permission("custom_tool")
|
||||
assert result is not None
|
||||
assert result.danger_level == DangerLevel.MEDIUM
|
||||
|
||||
def test_check_permission_allowed(self):
|
||||
"""测试权限检查 - 允许"""
|
||||
allowed, reason = self.manager.check_permission("read_file")
|
||||
|
||||
assert allowed is True
|
||||
|
||||
def test_check_permission_unknown_tool(self):
|
||||
"""测试权限检查 - 未知工具"""
|
||||
allowed, reason = self.manager.check_permission("unknown_tool")
|
||||
|
||||
assert allowed is False
|
||||
assert "未知工具" in reason
|
||||
|
||||
def test_check_permission_denied_path(self):
|
||||
"""测试权限检查 - 禁止路径"""
|
||||
perm = ToolPermission(
|
||||
name="restricted_read",
|
||||
danger_level=DangerLevel.SAFE,
|
||||
denied_paths=["/etc/", "/root/"],
|
||||
)
|
||||
self.manager.register_tool(perm)
|
||||
|
||||
allowed, reason = self.manager.check_permission(
|
||||
"restricted_read",
|
||||
params={"path": "/etc/passwd"},
|
||||
)
|
||||
|
||||
assert allowed is False
|
||||
assert "禁止访问" in reason
|
||||
|
||||
def test_requires_confirmation_by_level(self):
|
||||
"""测试确认要求 - 按等级"""
|
||||
# HIGH 级别需要确认
|
||||
assert self.manager.requires_confirmation("delete_file") is True
|
||||
|
||||
# SAFE 级别不需要确认
|
||||
assert self.manager.requires_confirmation("read_file") is False
|
||||
|
||||
def test_requires_confirmation_explicit(self):
|
||||
"""测试确认要求 - 显式设置"""
|
||||
perm = ToolPermission(
|
||||
name="explicit_confirm",
|
||||
danger_level=DangerLevel.LOW,
|
||||
requires_confirmation=True,
|
||||
)
|
||||
self.manager.register_tool(perm)
|
||||
|
||||
assert self.manager.requires_confirmation("explicit_confirm") is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_confirmation_flow(self):
|
||||
"""测试确认流程"""
|
||||
request = await self.manager.request_confirmation(
|
||||
request_id="req-1",
|
||||
tool_name="delete_file",
|
||||
params={"path": "/test.txt"},
|
||||
)
|
||||
|
||||
assert request.status == ConfirmationStatus.PENDING
|
||||
|
||||
# 批准
|
||||
self.manager.approve_confirmation("req-1")
|
||||
assert request.status == ConfirmationStatus.APPROVED
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_confirmation_deny(self):
|
||||
"""测试拒绝确认"""
|
||||
request = await self.manager.request_confirmation(
|
||||
request_id="req-2",
|
||||
tool_name="delete_file",
|
||||
params={"path": "/test.txt"},
|
||||
)
|
||||
|
||||
self.manager.deny_confirmation("req-2")
|
||||
assert request.status == ConfirmationStatus.DENIED
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_pending_confirmations(self):
|
||||
"""测试获取待处理确认"""
|
||||
await self.manager.request_confirmation(
|
||||
request_id="req-3",
|
||||
tool_name="test",
|
||||
params={},
|
||||
)
|
||||
|
||||
pending = self.manager.get_pending_confirmations()
|
||||
assert len(pending) >= 1
|
||||
|
||||
|
||||
class TestToolRegistry:
|
||||
"""ToolRegistry 测试"""
|
||||
|
||||
def test_import_registry(self):
|
||||
"""测试导入注册中心"""
|
||||
from minenasai.agent import ToolRegistry, get_tool_registry
|
||||
|
||||
registry = get_tool_registry()
|
||||
assert isinstance(registry, ToolRegistry)
|
||||
|
||||
def test_register_builtin_tools(self):
|
||||
"""测试注册内置工具"""
|
||||
from minenasai.agent import get_tool_registry, register_builtin_tools
|
||||
|
||||
registry = get_tool_registry()
|
||||
initial_count = len(registry.list_tools())
|
||||
|
||||
register_builtin_tools()
|
||||
|
||||
# 应该有更多工具
|
||||
new_count = len(registry.list_tools())
|
||||
assert new_count >= initial_count
|
||||
|
||||
def test_tool_decorator(self):
|
||||
"""测试工具装饰器"""
|
||||
from minenasai.agent import tool, get_tool_registry
|
||||
|
||||
@tool(name="decorated_tool", description="装饰器测试")
|
||||
async def decorated_tool(param: str) -> dict:
|
||||
return {"result": param}
|
||||
|
||||
registry = get_tool_registry()
|
||||
tool_obj = registry.get("decorated_tool")
|
||||
|
||||
assert tool_obj is not None
|
||||
assert tool_obj.description == "装饰器测试"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_tool(self):
|
||||
"""测试执行工具"""
|
||||
from minenasai.agent import get_tool_registry, DangerLevel
|
||||
|
||||
registry = get_tool_registry()
|
||||
|
||||
# 注册测试工具
|
||||
async def echo(message: str) -> dict:
|
||||
return {"echo": message}
|
||||
|
||||
registry.register(
|
||||
name="echo",
|
||||
description="回显消息",
|
||||
func=echo,
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {"message": {"type": "string"}},
|
||||
"required": ["message"],
|
||||
},
|
||||
danger_level=DangerLevel.SAFE,
|
||||
)
|
||||
|
||||
result = await registry.execute("echo", {"message": "hello"})
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["result"]["echo"] == "hello"
|
||||
|
||||
def test_get_stats(self):
|
||||
"""测试获取统计"""
|
||||
from minenasai.agent import get_tool_registry
|
||||
|
||||
registry = get_tool_registry()
|
||||
stats = registry.get_stats()
|
||||
|
||||
assert "total_tools" in stats
|
||||
assert "categories" in stats
|
||||
165
tests/test_scheduler.py
Normal file
165
tests/test_scheduler.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""调度器模块测试"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from minenasai.scheduler.cron import CronJob, CronParser, CronScheduler, JobStatus
|
||||
|
||||
|
||||
class TestCronParser:
|
||||
"""Cron 解析器测试"""
|
||||
|
||||
def test_parse_all_stars(self):
|
||||
"""测试全星号表达式"""
|
||||
result = CronParser.parse("* * * * *")
|
||||
|
||||
assert len(result["minute"]) == 60
|
||||
assert len(result["hour"]) == 24
|
||||
assert len(result["day"]) == 31
|
||||
assert len(result["month"]) == 12
|
||||
assert len(result["weekday"]) == 7
|
||||
|
||||
def test_parse_specific_values(self):
|
||||
"""测试具体值"""
|
||||
result = CronParser.parse("30 8 * * *")
|
||||
|
||||
assert result["minute"] == {30}
|
||||
assert result["hour"] == {8}
|
||||
|
||||
def test_parse_range(self):
|
||||
"""测试范围"""
|
||||
result = CronParser.parse("0 9-17 * * *")
|
||||
|
||||
assert result["hour"] == {9, 10, 11, 12, 13, 14, 15, 16, 17}
|
||||
|
||||
def test_parse_step(self):
|
||||
"""测试步进"""
|
||||
result = CronParser.parse("*/15 * * * *")
|
||||
|
||||
assert result["minute"] == {0, 15, 30, 45}
|
||||
|
||||
def test_parse_list(self):
|
||||
"""测试列表"""
|
||||
result = CronParser.parse("0 8,12,18 * * *")
|
||||
|
||||
assert result["hour"] == {8, 12, 18}
|
||||
|
||||
def test_parse_preset_daily(self):
|
||||
"""测试预定义表达式 @daily"""
|
||||
result = CronParser.parse("@daily")
|
||||
|
||||
assert result["minute"] == {0}
|
||||
assert result["hour"] == {0}
|
||||
|
||||
def test_parse_preset_hourly(self):
|
||||
"""测试预定义表达式 @hourly"""
|
||||
result = CronParser.parse("@hourly")
|
||||
|
||||
assert result["minute"] == {0}
|
||||
assert len(result["hour"]) == 24
|
||||
|
||||
def test_parse_invalid_expression(self):
|
||||
"""测试无效表达式"""
|
||||
with pytest.raises(ValueError, match="无效的 cron"):
|
||||
CronParser.parse("invalid")
|
||||
|
||||
def test_get_next_run(self):
|
||||
"""测试计算下次运行时间"""
|
||||
# 每小时整点
|
||||
next_run = CronParser.get_next_run(
|
||||
"0 * * * *",
|
||||
after=datetime(2026, 1, 1, 10, 30)
|
||||
)
|
||||
|
||||
assert next_run.minute == 0
|
||||
assert next_run.hour == 11
|
||||
|
||||
|
||||
class TestCronJob:
|
||||
"""CronJob 测试"""
|
||||
|
||||
def test_job_creation(self):
|
||||
"""测试创建任务"""
|
||||
job = CronJob(
|
||||
id="test-job",
|
||||
name="测试任务",
|
||||
schedule="*/5 * * * *",
|
||||
task="测试",
|
||||
)
|
||||
|
||||
assert job.id == "test-job"
|
||||
assert job.enabled is True
|
||||
assert job.last_status == JobStatus.PENDING
|
||||
|
||||
|
||||
class TestCronScheduler:
|
||||
"""CronScheduler 测试"""
|
||||
|
||||
def setup_method(self):
|
||||
"""初始化"""
|
||||
self.scheduler = CronScheduler()
|
||||
|
||||
def test_add_job(self):
|
||||
"""测试添加任务"""
|
||||
async def task():
|
||||
pass
|
||||
|
||||
job = self.scheduler.add_job(
|
||||
job_id="test-1",
|
||||
name="测试任务",
|
||||
schedule="*/5 * * * *",
|
||||
callback=task,
|
||||
)
|
||||
|
||||
assert job.id == "test-1"
|
||||
assert job.next_run is not None
|
||||
|
||||
def test_remove_job(self):
|
||||
"""测试移除任务"""
|
||||
async def task():
|
||||
pass
|
||||
|
||||
self.scheduler.add_job("test-1", "测试", "* * * * *", task)
|
||||
|
||||
assert self.scheduler.remove_job("test-1") is True
|
||||
assert self.scheduler.get_job("test-1") is None
|
||||
|
||||
def test_enable_disable_job(self):
|
||||
"""测试启用/禁用任务"""
|
||||
async def task():
|
||||
pass
|
||||
|
||||
self.scheduler.add_job("test-1", "测试", "* * * * *", task)
|
||||
|
||||
assert self.scheduler.disable_job("test-1") is True
|
||||
job = self.scheduler.get_job("test-1")
|
||||
assert job.enabled is False
|
||||
assert job.last_status == JobStatus.DISABLED
|
||||
|
||||
assert self.scheduler.enable_job("test-1") is True
|
||||
assert job.enabled is True
|
||||
|
||||
def test_list_jobs(self):
|
||||
"""测试列出任务"""
|
||||
async def task():
|
||||
pass
|
||||
|
||||
self.scheduler.add_job("test-1", "任务1", "* * * * *", task)
|
||||
self.scheduler.add_job("test-2", "任务2", "*/5 * * * *", task)
|
||||
|
||||
jobs = self.scheduler.list_jobs()
|
||||
assert len(jobs) == 2
|
||||
|
||||
def test_get_stats(self):
|
||||
"""测试获取统计"""
|
||||
async def task():
|
||||
pass
|
||||
|
||||
self.scheduler.add_job("test-1", "任务1", "* * * * *", task)
|
||||
|
||||
stats = self.scheduler.get_stats()
|
||||
assert stats["total_jobs"] == 1
|
||||
assert stats["enabled_jobs"] == 1
|
||||
150
tests/test_tools.py
Normal file
150
tests/test_tools.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""工具模块测试"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from minenasai.agent.tools.basic import (
|
||||
list_directory_tool,
|
||||
python_eval_tool,
|
||||
read_file_tool,
|
||||
)
|
||||
|
||||
|
||||
class TestReadFileTool:
|
||||
"""读取文件工具测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_existing_file(self, tmp_path):
|
||||
"""测试读取存在的文件"""
|
||||
test_file = tmp_path / "test.txt"
|
||||
test_file.write_text("Hello, World!\nLine 2\nLine 3")
|
||||
|
||||
result = await read_file_tool(str(test_file))
|
||||
|
||||
assert "error" not in result
|
||||
assert "Hello, World!" in result["content"]
|
||||
assert result["lines"] == 3
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_nonexistent_file(self):
|
||||
"""测试读取不存在的文件"""
|
||||
result = await read_file_tool("/nonexistent/file.txt")
|
||||
|
||||
assert "error" in result
|
||||
assert "不存在" in result["error"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_with_max_lines(self, tmp_path):
|
||||
"""测试最大行数限制"""
|
||||
test_file = tmp_path / "long.txt"
|
||||
test_file.write_text("\n".join([f"Line {i}" for i in range(100)]))
|
||||
|
||||
result = await read_file_tool(str(test_file), max_lines=10)
|
||||
|
||||
assert "error" not in result
|
||||
assert "截断" in result["content"]
|
||||
|
||||
|
||||
class TestListDirectoryTool:
|
||||
"""列出目录工具测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_directory(self, tmp_path):
|
||||
"""测试列出目录"""
|
||||
# 创建测试文件
|
||||
(tmp_path / "file1.txt").touch()
|
||||
(tmp_path / "file2.py").touch()
|
||||
(tmp_path / "subdir").mkdir()
|
||||
|
||||
result = await list_directory_tool(str(tmp_path))
|
||||
|
||||
assert "error" not in result
|
||||
assert result["count"] == 3
|
||||
names = [item["name"] for item in result["items"]]
|
||||
assert "file1.txt" in names
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_with_pattern(self, tmp_path):
|
||||
"""测试模式匹配"""
|
||||
(tmp_path / "test.py").touch()
|
||||
(tmp_path / "test.txt").touch()
|
||||
|
||||
result = await list_directory_tool(str(tmp_path), pattern="*.py")
|
||||
|
||||
assert "error" not in result
|
||||
assert result["count"] == 1
|
||||
assert result["items"][0]["name"] == "test.py"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_nonexistent_directory(self):
|
||||
"""测试列出不存在的目录"""
|
||||
result = await list_directory_tool("/nonexistent/dir")
|
||||
|
||||
assert "error" in result
|
||||
|
||||
|
||||
class TestPythonEvalTool:
|
||||
"""Python 执行工具测试"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_simple_math(self):
|
||||
"""测试简单数学计算"""
|
||||
result = await python_eval_tool("1 + 2 * 3")
|
||||
|
||||
assert "error" not in result
|
||||
assert result["result"] == 7
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_math_functions(self):
|
||||
"""测试数学函数"""
|
||||
result = await python_eval_tool("math.sqrt(16)")
|
||||
|
||||
assert "error" not in result
|
||||
assert result["result"] == 4.0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_operations(self):
|
||||
"""测试列表操作"""
|
||||
result = await python_eval_tool("sum([1, 2, 3, 4, 5])")
|
||||
|
||||
assert "error" not in result
|
||||
assert result["result"] == 15
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_blocked_import(self):
|
||||
"""测试阻止 import"""
|
||||
result = await python_eval_tool("__import__('os')")
|
||||
|
||||
assert "error" in result
|
||||
assert "不允许" in result["error"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_blocked_exec(self):
|
||||
"""测试阻止 exec"""
|
||||
result = await python_eval_tool("exec('print(1)')")
|
||||
|
||||
assert "error" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_blocked_open(self):
|
||||
"""测试阻止 open"""
|
||||
result = await python_eval_tool("open('/etc/passwd')")
|
||||
|
||||
assert "error" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_syntax_error(self):
|
||||
"""测试语法错误"""
|
||||
result = await python_eval_tool("1 +")
|
||||
|
||||
assert "error" in result
|
||||
assert "语法错误" in result["error"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_error(self):
|
||||
"""测试运行时错误"""
|
||||
result = await python_eval_tool("1 / 0")
|
||||
|
||||
assert "error" in result
|
||||
assert "division by zero" in result["error"]
|
||||
158
tests/test_webtui.py
Normal file
158
tests/test_webtui.py
Normal file
@@ -0,0 +1,158 @@
|
||||
"""Web TUI 模块测试"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from minenasai.webtui.auth import AuthManager, AuthToken
|
||||
|
||||
|
||||
class TestAuthToken:
|
||||
"""AuthToken 测试"""
|
||||
|
||||
def test_token_not_expired(self):
|
||||
"""测试未过期令牌"""
|
||||
token = AuthToken(
|
||||
token="test",
|
||||
user_id="user1",
|
||||
created_at=time.time(),
|
||||
expires_at=time.time() + 3600,
|
||||
)
|
||||
assert not token.is_expired
|
||||
assert token.remaining_time > 0
|
||||
|
||||
def test_token_expired(self):
|
||||
"""测试已过期令牌"""
|
||||
token = AuthToken(
|
||||
token="test",
|
||||
user_id="user1",
|
||||
created_at=time.time() - 7200,
|
||||
expires_at=time.time() - 3600,
|
||||
)
|
||||
assert token.is_expired
|
||||
assert token.remaining_time == 0
|
||||
|
||||
|
||||
class TestAuthManager:
|
||||
"""AuthManager 测试"""
|
||||
|
||||
def setup_method(self):
|
||||
"""初始化"""
|
||||
self.manager = AuthManager(secret_key="test-secret-key")
|
||||
|
||||
def test_generate_token(self):
|
||||
"""测试生成令牌"""
|
||||
token = self.manager.generate_token("user1")
|
||||
|
||||
assert token is not None
|
||||
assert len(token) > 20
|
||||
|
||||
def test_verify_token(self):
|
||||
"""测试验证令牌"""
|
||||
token = self.manager.generate_token("user1")
|
||||
auth_token = self.manager.verify_token(token)
|
||||
|
||||
assert auth_token is not None
|
||||
assert auth_token.user_id == "user1"
|
||||
|
||||
def test_verify_invalid_token(self):
|
||||
"""测试验证无效令牌"""
|
||||
auth_token = self.manager.verify_token("invalid-token")
|
||||
assert auth_token is None
|
||||
|
||||
def test_verify_expired_token(self):
|
||||
"""测试验证过期令牌"""
|
||||
token = self.manager.generate_token("user1", expires_in=0)
|
||||
|
||||
# 等待过期
|
||||
time.sleep(0.1)
|
||||
|
||||
auth_token = self.manager.verify_token(token)
|
||||
assert auth_token is None
|
||||
|
||||
def test_revoke_token(self):
|
||||
"""测试撤销令牌"""
|
||||
token = self.manager.generate_token("user1")
|
||||
|
||||
assert self.manager.revoke_token(token) is True
|
||||
assert self.manager.verify_token(token) is None
|
||||
|
||||
def test_revoke_nonexistent_token(self):
|
||||
"""测试撤销不存在的令牌"""
|
||||
assert self.manager.revoke_token("nonexistent") is False
|
||||
|
||||
def test_revoke_user_tokens(self):
|
||||
"""测试撤销用户所有令牌"""
|
||||
self.manager.generate_token("user1")
|
||||
self.manager.generate_token("user1")
|
||||
self.manager.generate_token("user2")
|
||||
|
||||
count = self.manager.revoke_user_tokens("user1")
|
||||
|
||||
assert count == 2
|
||||
assert self.manager.get_stats()["total_tokens"] == 1
|
||||
|
||||
def test_refresh_token(self):
|
||||
"""测试刷新令牌"""
|
||||
old_token = self.manager.generate_token("user1")
|
||||
new_token = self.manager.refresh_token(old_token)
|
||||
|
||||
assert new_token is not None
|
||||
assert new_token != old_token
|
||||
assert self.manager.verify_token(old_token) is None
|
||||
assert self.manager.verify_token(new_token) is not None
|
||||
|
||||
def test_token_metadata(self):
|
||||
"""测试令牌元数据"""
|
||||
metadata = {"channel": "wework", "task_id": "123"}
|
||||
token = self.manager.generate_token("user1", metadata=metadata)
|
||||
|
||||
auth_token = self.manager.verify_token(token)
|
||||
|
||||
assert auth_token is not None
|
||||
assert auth_token.metadata == metadata
|
||||
|
||||
def test_cleanup_expired(self):
|
||||
"""测试清理过期令牌"""
|
||||
self.manager.generate_token("user1", expires_in=0)
|
||||
self.manager.generate_token("user2", expires_in=3600)
|
||||
|
||||
time.sleep(0.1)
|
||||
count = self.manager.cleanup_expired()
|
||||
|
||||
assert count == 1
|
||||
assert self.manager.get_stats()["total_tokens"] == 1
|
||||
|
||||
|
||||
class TestSSHManager:
|
||||
"""SSHManager 测试(不实际连接)"""
|
||||
|
||||
def test_import_ssh_manager(self):
|
||||
"""测试导入 SSH 管理器"""
|
||||
from minenasai.webtui import SSHManager, get_ssh_manager
|
||||
|
||||
manager = get_ssh_manager()
|
||||
assert isinstance(manager, SSHManager)
|
||||
|
||||
def test_ssh_manager_stats(self):
|
||||
"""测试 SSH 管理器统计"""
|
||||
from minenasai.webtui import SSHManager
|
||||
|
||||
manager = SSHManager()
|
||||
stats = manager.get_stats()
|
||||
|
||||
assert "active_sessions" in stats
|
||||
assert stats["active_sessions"] == 0
|
||||
|
||||
|
||||
class TestWebTUIServer:
|
||||
"""Web TUI 服务器测试"""
|
||||
|
||||
def test_import_server(self):
|
||||
"""测试导入服务器"""
|
||||
from minenasai.webtui.server import app
|
||||
|
||||
assert app is not None
|
||||
assert app.title == "MineNASAI Web TUI"
|
||||
1
tests/webtui/__init__.py
Normal file
1
tests/webtui/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Web TUI 测试"""
|
||||
Reference in New Issue
Block a user