chore: initial commit for Pretor v0.1.0-alpha
正式发布 Pretor 平台的首个 alpha 版本。本项目旨在构建一个基于分布式架构的多智能体协同工作流水线。 核心功能实现: 1. 建立基于 BaseIndividual 的动态插件加载机制。 2. 实现三类核心 worker_individual 子个体。 3. 集成 Ray 框架支持分布式集群调度。 4. 基于 PostgreSQL 的全量持久化存储方案。 5. 提供完整的 FastAPI 后端与 React 前端交互界面。
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
from sqlalchemy.exc import IntegrityError, OperationalError
|
||||
from pydantic import ValidationError
|
||||
from pretor.utils.error import UserNotExistError
|
||||
from pretor.core.database.database_exception import database_exception
|
||||
|
||||
@database_exception
|
||||
async def success_func():
|
||||
return "success"
|
||||
|
||||
@database_exception
|
||||
async def validation_error_func():
|
||||
raise ValidationError.from_exception_data(title="Mock", line_errors=[])
|
||||
|
||||
@database_exception
|
||||
async def integrity_error_func():
|
||||
raise IntegrityError("mock_statement", "mock_params", "mock_orig")
|
||||
|
||||
@database_exception
|
||||
async def operational_error_func():
|
||||
raise OperationalError("mock_statement", "mock_params", "mock_orig")
|
||||
|
||||
@database_exception
|
||||
async def user_not_exist_error_func():
|
||||
raise UserNotExistError("mock user")
|
||||
|
||||
@database_exception
|
||||
async def exception_func():
|
||||
raise Exception("mock generic exception")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_success_func():
|
||||
assert await success_func() == "success"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("pretor.core.database.database_exception.logger")
|
||||
async def test_validation_error(mock_logger):
|
||||
with pytest.raises(ValidationError):
|
||||
await validation_error_func()
|
||||
mock_logger.error.assert_called_once()
|
||||
assert "对象校验失败" in mock_logger.error.call_args[0][0]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("pretor.core.database.database_exception.logger")
|
||||
async def test_integrity_error(mock_logger):
|
||||
with pytest.raises(IntegrityError):
|
||||
await integrity_error_func()
|
||||
mock_logger.error.assert_called_once()
|
||||
assert "数据库完整性错误" in mock_logger.error.call_args[0][0]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("pretor.core.database.database_exception.logger")
|
||||
async def test_operational_error(mock_logger):
|
||||
with pytest.raises(OperationalError):
|
||||
await operational_error_func()
|
||||
mock_logger.error.assert_called_once()
|
||||
assert "数据库连接异常" in mock_logger.error.call_args[0][0]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("pretor.core.database.database_exception.logger")
|
||||
async def test_user_not_exist_error(mock_logger):
|
||||
result = await user_not_exist_error_func()
|
||||
assert result is None
|
||||
mock_logger.error.assert_called_once()
|
||||
assert "更改密码失败,用户不存在" in mock_logger.error.call_args[0][0]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("pretor.core.database.database_exception.logger")
|
||||
async def test_generic_exception(mock_logger):
|
||||
with pytest.raises(Exception, match="mock generic exception"):
|
||||
await exception_func()
|
||||
mock_logger.exception.assert_called_once()
|
||||
assert "未预期的数据库错误" in mock_logger.exception.call_args[0][0]
|
||||
@@ -0,0 +1,145 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, AsyncMock, patch
|
||||
import json
|
||||
import sys
|
||||
import builtins
|
||||
|
||||
real_import = builtins.__import__
|
||||
|
||||
|
||||
def mock_import(name, globals=None, locals=None, fromlist=(), level=0):
|
||||
if name == 'sqlmodel':
|
||||
mock_sqlmodel = MagicMock()
|
||||
|
||||
class DummySQLModel:
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
pass
|
||||
|
||||
mock_sqlmodel.SQLModel = DummySQLModel
|
||||
mock_sqlmodel.Field = MagicMock(return_value=None)
|
||||
mock_sqlmodel.select = MagicMock()
|
||||
return mock_sqlmodel
|
||||
return real_import(name, globals, locals, fromlist, level)
|
||||
|
||||
|
||||
builtins.__import__ = mock_import
|
||||
for mod in list(sys.modules.keys()):
|
||||
if 'pretor.core.database.module.memory' in mod or 'sqlmodel' in mod:
|
||||
del sys.modules[mod]
|
||||
from pretor.core.database.module.memory import MemoryRAG
|
||||
|
||||
builtins.__import__ = real_import
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_dependencies():
|
||||
with patch("pretor.core.database.module.memory.WorkflowRecord") as mock_workflow_record:
|
||||
with patch("pretor.core.database.module.memory.MemoryRecord") as mock_memory_record:
|
||||
with patch("pretor.core.database.module.memory.select") as mock_select:
|
||||
yield mock_workflow_record, mock_memory_record, mock_select
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_session_maker():
|
||||
maker = MagicMock()
|
||||
session = AsyncMock()
|
||||
session.add = MagicMock()
|
||||
maker.return_value.__aenter__.return_value = session
|
||||
maker.__aenter__.return_value = session
|
||||
maker.__aexit__ = AsyncMock()
|
||||
return maker, session
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_workflow(mock_session_maker, mock_dependencies):
|
||||
mock_workflow_record, _, _ = mock_dependencies
|
||||
maker, session = mock_session_maker
|
||||
rag = MemoryRAG(maker)
|
||||
|
||||
mock_record = MagicMock()
|
||||
mock_workflow_record.return_value = mock_record
|
||||
|
||||
workflow_data = {"key": "value"}
|
||||
record = await rag.save_workflow("wf_123", workflow_data)
|
||||
|
||||
mock_workflow_record.assert_called_once_with(
|
||||
workflow_id="wf_123",
|
||||
workflow_data_json=json.dumps(workflow_data)
|
||||
)
|
||||
session.add.assert_called_once_with(mock_record)
|
||||
session.commit.assert_called_once()
|
||||
session.refresh.assert_called_once_with(mock_record)
|
||||
assert record == mock_record
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_workflow_success(mock_session_maker, mock_dependencies):
|
||||
_, _, mock_select = mock_dependencies
|
||||
maker, session = mock_session_maker
|
||||
rag = MemoryRAG(maker)
|
||||
|
||||
mock_statement = MagicMock()
|
||||
mock_select.return_value.where.return_value = mock_statement
|
||||
|
||||
mock_record = MagicMock()
|
||||
mock_record.workflow_data_json = '{"key": "value"}'
|
||||
|
||||
mock_exec_result = MagicMock()
|
||||
mock_exec_result.scalar_one_or_none.return_value = mock_record
|
||||
session.execute = AsyncMock(return_value=mock_exec_result)
|
||||
|
||||
data = await rag.get_workflow("wf_123")
|
||||
|
||||
session.execute.assert_called_once_with(mock_statement)
|
||||
assert data == {"key": "value"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_workflow_not_found(mock_session_maker, mock_dependencies):
|
||||
_, _, mock_select = mock_dependencies
|
||||
maker, session = mock_session_maker
|
||||
rag = MemoryRAG(maker)
|
||||
|
||||
mock_exec_result = MagicMock()
|
||||
mock_exec_result.scalar_one_or_none.return_value = None
|
||||
session.execute = AsyncMock(return_value=mock_exec_result)
|
||||
|
||||
data = await rag.get_workflow("wf_123")
|
||||
assert data is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_memory(mock_session_maker, mock_dependencies):
|
||||
_, mock_memory_record, _ = mock_dependencies
|
||||
maker, session = mock_session_maker
|
||||
rag = MemoryRAG(maker)
|
||||
|
||||
mock_record = MagicMock()
|
||||
mock_memory_record.return_value = mock_record
|
||||
|
||||
record = await rag.add_memory("text", [0.1, 0.2])
|
||||
|
||||
mock_memory_record.assert_called_once_with(memory_text="text", embedding=[0.1, 0.2])
|
||||
session.add.assert_called_once_with(mock_record)
|
||||
session.commit.assert_called_once()
|
||||
session.refresh.assert_called_once_with(mock_record)
|
||||
assert record == mock_record
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retrieve_memory(mock_session_maker, mock_dependencies):
|
||||
_, _, mock_select = mock_dependencies
|
||||
maker, session = mock_session_maker
|
||||
rag = MemoryRAG(maker)
|
||||
|
||||
mock_statement = MagicMock()
|
||||
mock_select.return_value.limit.return_value = mock_statement
|
||||
|
||||
mock_exec_result = MagicMock()
|
||||
mock_exec_result.all.return_value = ["res1", "res2"]
|
||||
session.execute = AsyncMock(return_value=mock_exec_result)
|
||||
|
||||
results = await rag.retrieve_memory([0.1, 0.2], 5)
|
||||
|
||||
session.execute.assert_called_once_with(mock_statement)
|
||||
assert results == ["res1", "res2"]
|
||||
@@ -0,0 +1,180 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, AsyncMock, patch
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_dependencies():
|
||||
with patch("pretor.core.database.module.user.User") as mock_user_cls:
|
||||
mock_user_cls.user_name = MagicMock()
|
||||
with patch("pretor.core.database.module.user.select") as mock_select:
|
||||
yield mock_user_cls, mock_select
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_session_maker():
|
||||
maker = MagicMock()
|
||||
session = AsyncMock()
|
||||
session.add = MagicMock()
|
||||
session.delete = MagicMock()
|
||||
maker.return_value.__aenter__.return_value = session
|
||||
maker.__aenter__.return_value = session
|
||||
maker.__aexit__ = AsyncMock()
|
||||
return maker, session
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_user(mock_session_maker, mock_dependencies):
|
||||
mock_user_cls, _ = mock_dependencies
|
||||
from pretor.core.database.module.user import AuthDatabase
|
||||
maker, session = mock_session_maker
|
||||
db = AuthDatabase(maker)
|
||||
|
||||
mock_user = MagicMock()
|
||||
mock_user.user_name = "testuser"
|
||||
mock_user.hashed_password = "hashedpw"
|
||||
mock_user_cls.return_value = mock_user
|
||||
|
||||
mock_exec_result = MagicMock()
|
||||
mock_exec_result.first.return_value = None
|
||||
session.execute = AsyncMock(return_value=mock_exec_result)
|
||||
|
||||
user = await db.add_user("testuser", "hashedpw")
|
||||
|
||||
assert user.user_name == "testuser"
|
||||
assert user.hashed_password == "hashedpw"
|
||||
session.add.assert_called_once_with(mock_user)
|
||||
session.commit.assert_called_once()
|
||||
session.refresh.assert_called_once_with(mock_user)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_change_password_success(mock_session_maker, mock_dependencies):
|
||||
mock_user_cls, mock_select = mock_dependencies
|
||||
from pretor.core.database.module.user import AuthDatabase
|
||||
maker, session = mock_session_maker
|
||||
db = AuthDatabase(maker)
|
||||
|
||||
mock_statement = MagicMock()
|
||||
mock_select.return_value.where.return_value = mock_statement
|
||||
|
||||
from pretor.utils.access import Accessor
|
||||
|
||||
mock_user = MagicMock()
|
||||
mock_user.hashed_password = Accessor.hash_password("old_password")
|
||||
|
||||
mock_exec_result = MagicMock()
|
||||
mock_exec_result.scalar_one_or_none.return_value = mock_user
|
||||
session.execute = AsyncMock(return_value=mock_exec_result)
|
||||
|
||||
user = await db.change_password("testuser", "old_password", "new_password")
|
||||
|
||||
session.execute.assert_called_once_with(mock_statement)
|
||||
assert user.hashed_password == "new_password"
|
||||
session.add.assert_called_once_with(mock_user)
|
||||
session.commit.assert_called_once()
|
||||
session.refresh.assert_called_once_with(mock_user)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_change_password_user_not_exist(mock_session_maker, mock_dependencies):
|
||||
mock_user_cls, mock_select = mock_dependencies
|
||||
from pretor.core.database.module.user import AuthDatabase
|
||||
maker, session = mock_session_maker
|
||||
db = AuthDatabase(maker)
|
||||
|
||||
mock_exec_result = MagicMock()
|
||||
mock_exec_result.scalar_one_or_none.return_value = None
|
||||
session.execute = AsyncMock(return_value=mock_exec_result)
|
||||
|
||||
result = await db.change_password("testuser", "old_password", "new_password")
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_change_password_wrong_password(mock_session_maker, mock_dependencies):
|
||||
mock_user_cls, mock_select = mock_dependencies
|
||||
from pretor.core.database.module.user import AuthDatabase
|
||||
maker, session = mock_session_maker
|
||||
db = AuthDatabase(maker)
|
||||
|
||||
from pretor.utils.access import Accessor
|
||||
mock_user = MagicMock()
|
||||
mock_user.hashed_password = Accessor.hash_password("actual_password")
|
||||
mock_exec_result = MagicMock()
|
||||
mock_exec_result.scalar_one_or_none.return_value = mock_user
|
||||
session.execute = AsyncMock(return_value=mock_exec_result)
|
||||
|
||||
from pretor.utils.error import UserPasswordError
|
||||
with pytest.raises(UserPasswordError):
|
||||
await db.change_password("testuser", "old_password", "new_password")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_user_success(mock_session_maker, mock_dependencies):
|
||||
mock_user_cls, mock_select = mock_dependencies
|
||||
from pretor.core.database.module.user import AuthDatabase
|
||||
maker, session = mock_session_maker
|
||||
db = AuthDatabase(maker)
|
||||
|
||||
mock_statement = MagicMock()
|
||||
mock_select.return_value.where.return_value = mock_statement
|
||||
|
||||
mock_user = MagicMock()
|
||||
mock_exec_result = MagicMock()
|
||||
mock_exec_result.scalar_one_or_none.return_value = mock_user
|
||||
session.execute = AsyncMock(return_value=mock_exec_result)
|
||||
|
||||
await db.delete_user("testuser")
|
||||
session.execute.assert_called_once_with(mock_statement)
|
||||
session.delete.assert_called_once_with(mock_user)
|
||||
session.commit.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_user_not_exist(mock_session_maker, mock_dependencies):
|
||||
mock_user_cls, mock_select = mock_dependencies
|
||||
from pretor.core.database.module.user import AuthDatabase
|
||||
maker, session = mock_session_maker
|
||||
db = AuthDatabase(maker)
|
||||
|
||||
mock_exec_result = MagicMock()
|
||||
mock_exec_result.scalar_one_or_none.return_value = None
|
||||
session.execute = AsyncMock(return_value=mock_exec_result)
|
||||
|
||||
result = await db.delete_user("testuser")
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_user_success(mock_session_maker, mock_dependencies):
|
||||
mock_user_cls, mock_select = mock_dependencies
|
||||
from pretor.core.database.module.user import AuthDatabase
|
||||
maker, session = mock_session_maker
|
||||
db = AuthDatabase(maker)
|
||||
|
||||
mock_statement = MagicMock()
|
||||
mock_select.return_value.where.return_value = mock_statement
|
||||
|
||||
mock_user = MagicMock()
|
||||
mock_exec_result = MagicMock()
|
||||
mock_exec_result.scalar_one_or_none.return_value = mock_user
|
||||
session.execute = AsyncMock(return_value=mock_exec_result)
|
||||
|
||||
user = await db.login_user("testuser")
|
||||
session.execute.assert_called_once_with(mock_statement)
|
||||
assert user == mock_user
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_user_not_exist(mock_session_maker, mock_dependencies):
|
||||
mock_user_cls, mock_select = mock_dependencies
|
||||
from pretor.core.database.module.user import AuthDatabase
|
||||
maker, session = mock_session_maker
|
||||
db = AuthDatabase(maker)
|
||||
|
||||
mock_exec_result = MagicMock()
|
||||
mock_exec_result.scalar_one_or_none.return_value = None
|
||||
session.execute = AsyncMock(return_value=mock_exec_result)
|
||||
|
||||
result = await db.login_user("testuser")
|
||||
assert result is None
|
||||
@@ -0,0 +1,78 @@
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
import sys
|
||||
import builtins
|
||||
|
||||
real_import = builtins.__import__
|
||||
|
||||
|
||||
def mock_import(name, globals=None, locals=None, fromlist=(), level=0):
|
||||
if name == 'ray':
|
||||
mock_ray = MagicMock()
|
||||
|
||||
def mock_remote(*args, **kwargs):
|
||||
if len(args) == 1 and callable(args[0]):
|
||||
return args[0]
|
||||
def decorator(cls):
|
||||
return cls
|
||||
return decorator
|
||||
|
||||
mock_ray.remote = mock_remote
|
||||
return mock_ray
|
||||
return real_import(name, globals, locals, fromlist, level)
|
||||
|
||||
|
||||
builtins.__import__ = mock_import
|
||||
for mod in list(sys.modules.keys()):
|
||||
if 'pretor.core.database.postgres' in mod or 'ray' in mod:
|
||||
del sys.modules[mod]
|
||||
|
||||
from pretor.core.database.postgres import PostgresDatabase
|
||||
|
||||
builtins.__import__ = real_import
|
||||
|
||||
|
||||
@patch("pretor.core.database.postgres.create_async_engine")
|
||||
@patch("pretor.core.database.postgres.sessionmaker")
|
||||
@patch("pretor.core.database.postgres.AuthDatabase")
|
||||
@patch("pretor.core.database.postgres.ProviderDatabase")
|
||||
@patch("pretor.core.database.postgres.os.environ.get")
|
||||
@pytest.mark.asyncio
|
||||
async def test_postgres_database(mock_env_get, mock_provider_db, mock_auth_db, mock_sessionmaker, mock_create_engine):
|
||||
def env_side_effect(key):
|
||||
return {
|
||||
"POSTGRES_USER": "testuser",
|
||||
"POSTGRES_PASSWORD": "testpassword",
|
||||
"POSTGRES_HOST": "localhost",
|
||||
"POSTGRES_PORT": "5432",
|
||||
"POSTGRES_DB": "testdb"
|
||||
}.get(key)
|
||||
|
||||
mock_env_get.side_effect = env_side_effect
|
||||
|
||||
mock_engine = MagicMock()
|
||||
mock_conn = MagicMock()
|
||||
from unittest.mock import AsyncMock
|
||||
mock_conn.run_sync = AsyncMock()
|
||||
|
||||
mock_begin_ctx = MagicMock()
|
||||
mock_begin_ctx.__aenter__ = AsyncMock(return_value=mock_conn)
|
||||
mock_begin_ctx.__aexit__ = AsyncMock()
|
||||
mock_engine.begin.return_value = mock_begin_ctx
|
||||
mock_create_engine.return_value = mock_engine
|
||||
|
||||
db = PostgresDatabase()
|
||||
|
||||
mock_create_engine.assert_called_once_with(
|
||||
"postgresql+asyncpg://testuser:testpassword@localhost:5432/testdb",
|
||||
echo=True
|
||||
)
|
||||
mock_auth_db.assert_called_once()
|
||||
mock_provider_db.assert_called_once()
|
||||
mock_auth_db.return_value.get_user_authority = AsyncMock(return_value="test_auth")
|
||||
|
||||
with patch("pretor.core.database.postgres.SQLModel.metadata.create_all") as mock_create_all:
|
||||
await db.init_db()
|
||||
mock_conn.run_sync.assert_called_once_with(mock_create_all)
|
||||
|
||||
assert await db.get_user_authority(user_id="123") == "test_auth"
|
||||
@@ -0,0 +1,14 @@
|
||||
from pretor.core.database.table.provider import Provider
|
||||
|
||||
def test_provider_table():
|
||||
# Provide required fields
|
||||
provider = Provider(
|
||||
provider_title="title",
|
||||
provider_url="url",
|
||||
provider_apikey="key",
|
||||
provider_models=["model_1"],
|
||||
provider_type="type",
|
||||
provider_owner=1
|
||||
)
|
||||
assert Provider.__tablename__ == 'provider'
|
||||
assert provider.provider_title == "title"
|
||||
@@ -0,0 +1,6 @@
|
||||
from pretor.core.database.table.user import User
|
||||
|
||||
def test_user_table():
|
||||
user = User(user_id="id", user_name="name", hashed_password="pw")
|
||||
assert User.__tablename__ == 'user'
|
||||
assert user.user_name == "name"
|
||||
Reference in New Issue
Block a user