Files
erp-ass/backend/tests/test_ai_engine.py
dazhuang acd73431ae feat: implement ERP AI Assistant Phase 1
Backend (FastAPI + SQLAlchemy + Claude API + RAG):
- Config management with Pydantic v2
- Database engine with connection pooling and SQL injection prevention
- AI engine with Claude API integration (support custom base URL)
- RAG engine with ChromaDB and sentence-transformers
- Requirement analysis service
- Config generation service
- Executor engine with SQL validation
- REST API endpoints: /analyze, /generate, /execute

Frontend (Vue 3 + Element Plus + Pinia):
- Complete 3-step workflow: analyze → generate → execute
- Step indicator with progress visualization
- Analysis result display with field table
- SQL preview with monospace font
- Execute confirmation dialog with safety warning
- Execution result display
- State management with Pinia
- API service integration

Security:
- SQL injection prevention with parameterized queries
- Dangerous SQL operation blocking
- Database password URL encoding
- Transaction auto-rollback
- Pydantic config validation

Features:
- Natural language requirement analysis
- Automated SQL configuration generation
- Safe execution with human review
- LAN access support
- Custom Claude API endpoint support

Documentation:
- README with quick start guide
- Quick start guide
- LAN access configuration
- Dependency fixes guide
- Claude API configuration
- Git operation guide
- Implementation report

Dependencies fixed:
- numpy<2.0.0 for chromadb compatibility
- sentence-transformers==2.7.0 for huggingface_hub compatibility

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 14:23:20 +00:00

149 lines
4.9 KiB
Python

import pytest
from app.core.ai_engine import ClaudeEngine
@pytest.fixture
def mock_settings(mocker):
"""Mock settings for test isolation."""
mock_settings = mocker.MagicMock()
mock_settings.ANTHROPIC_API_KEY = "test-key"
mock_settings.CLAUDE_MODEL = "claude-sonnet-4-6"
mock_settings.CLAUDE_MAX_TOKENS = 1024
mock_settings.CLAUDE_TEMPERATURE = 0.7
mocker.patch('app.core.ai_engine.get_settings', return_value=mock_settings)
return mock_settings
@pytest.fixture
def mock_anthropic_client(mocker):
"""Mock Anthropic async client."""
mock_client = mocker.AsyncMock()
mocker.patch('app.core.ai_engine.anthropic.AsyncAnthropic', return_value=mock_client)
return mock_client
def test_claude_engine_init(mocker, mock_settings):
"""测试 Claude 引擎初始化"""
engine = ClaudeEngine()
assert engine.client is not None
assert engine.model == "claude-sonnet-4-6"
assert engine.max_tokens == 1024
assert engine.temperature == 0.7
def test_parse_json_response(mocker, mock_settings):
"""测试 JSON 解析"""
engine = ClaudeEngine()
# 测试纯 JSON
json_str = '{"name": "test", "value": 123}'
result = engine.parse_json_response(json_str)
assert result["name"] == "test"
assert result["value"] == 123
# 测试 markdown 代码块
md_str = '```json\n{"name": "test"}\n```'
result = engine.parse_json_response(md_str)
assert result["name"] == "test"
def test_parse_json_response_empty_content(mocker, mock_settings):
"""测试空内容错误处理"""
engine = ClaudeEngine()
with pytest.raises(ValueError, match="Empty content provided"):
engine.parse_json_response("")
with pytest.raises(ValueError, match="Empty content provided"):
engine.parse_json_response(" ")
def test_parse_json_response_invalid_json(mocker, mock_settings):
"""测试无效 JSON 错误处理"""
engine = ClaudeEngine()
# 无效 JSON 且无法提取任何代码块
invalid_str = "This is not JSON at all"
with pytest.raises(ValueError, match="无法解析 Claude 返回的 JSON"):
engine.parse_json_response(invalid_str)
# 无效的 JSON 代码块
invalid_json_block = '```json\n{invalid json}\n```'
with pytest.raises(ValueError, match="无法解析 Claude 返回的 JSON"):
engine.parse_json_response(invalid_json_block)
def test_parse_json_response_code_block(mocker, mock_settings):
"""测试代码块 JSON 解析"""
engine = ClaudeEngine()
# 普通代码块(无 json 标签)
code_block = '```\n{"status": "ok"}\n```'
result = engine.parse_json_response(code_block)
assert result["status"] == "ok"
def test_parse_json_response_nested_json(mocker, mock_settings):
"""测试嵌套 JSON 解析"""
engine = ClaudeEngine()
# 带有一些额外文本的 JSON
text_with_json = 'Some text before {"key": "value"} and after'
result = engine.parse_json_response(text_with_json)
assert result["key"] == "value"
# 嵌套 JSON
nested_json = '{"outer": {"inner": "value"}}'
result = engine.parse_json_response(nested_json)
assert result["outer"]["inner"] == "value"
@pytest.mark.asyncio
async def test_call_claude(mocker, mock_settings, mock_anthropic_client):
"""测试 call_claude 方法"""
# 设置 mock 响应
mock_response = mocker.MagicMock()
mock_response.content = [mocker.MagicMock(text="Hello, I am Claude")]
mock_anthropic_client.messages.create.return_value = mock_response
engine = ClaudeEngine()
messages = [{"role": "user", "content": "Hello"}]
result = await engine.call_claude(messages)
assert result == "Hello, I am Claude"
mock_anthropic_client.messages.create.assert_called_once_with(
model="claude-sonnet-4-6",
max_tokens=1024,
temperature=0.7,
messages=messages
)
@pytest.mark.asyncio
async def test_call_claude_with_temperature(mocker, mock_settings, mock_anthropic_client):
"""测试 call_claude 带温度参数"""
mock_response = mocker.MagicMock()
mock_response.content = [mocker.MagicMock(text="Response")]
mock_anthropic_client.messages.create.return_value = mock_response
engine = ClaudeEngine()
messages = [{"role": "user", "content": "Hello"}]
result = await engine.call_claude(messages, temperature=1.5)
assert result == "Response"
call_args = mock_anthropic_client.messages.create.call_args
assert call_args.kwargs["temperature"] == 1.5
@pytest.mark.asyncio
async def test_call_claude_error(mocker, mock_settings, mock_anthropic_client):
"""测试 call_claude 错误处理"""
# 设置 mock 抛出异常
mock_anthropic_client.messages.create.side_effect = Exception("API Error")
engine = ClaudeEngine()
messages = [{"role": "user", "content": "Hello"}]
with pytest.raises(Exception, match="API Error"):
await engine.call_claude(messages)