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>
149 lines
4.9 KiB
Python
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)
|