feat: 添加项目规则、环境配置示例及开发文档

This commit is contained in:
锦麟 王
2026-02-04 18:49:38 +08:00
commit df76882178
88 changed files with 13150 additions and 0 deletions

1
tests/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""MineNASAI 测试套件"""

1
tests/agent/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Agent 测试"""

22
tests/conftest.py Normal file
View 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

View File

@@ -0,0 +1 @@
"""Gateway 测试"""

124
tests/test_core.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
"""Web TUI 测试"""