- 新增 embedding_service(语义检索)、knowledge_service(RAG)、text_chunker、document_parser - 新增 tool_registry(自定义工具注册表)并完善工具市场 API(CRUD + code/http 执行) - 新增 agent_vector_memory / knowledge_base 模型及对应数据库表 - 实现 SSE 流式响应与 Agent 预算控制 - AgentChat.vue 集成 MainLayout 导航布局 - 完善测试体系:7 个新测试文件共 110 个测试覆盖 - 修复 conftest.py SQLite 内存数据库连接隔离问题 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
264 lines
10 KiB
Python
264 lines
10 KiB
Python
"""
|
|
工具市场 API 集成测试
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import pytest
|
|
from unittest.mock import patch, AsyncMock
|
|
|
|
|
|
class TestToolsAPI:
|
|
"""工具市场 API 测试"""
|
|
|
|
@pytest.mark.api
|
|
def test_list_public_tools(self, authenticated_client):
|
|
resp = authenticated_client.get("/api/v1/tools")
|
|
assert resp.status_code == 200
|
|
assert isinstance(resp.json(), list)
|
|
|
|
@pytest.mark.api
|
|
def test_list_categories(self, authenticated_client):
|
|
resp = authenticated_client.get("/api/v1/tools/categories")
|
|
assert resp.status_code == 200
|
|
cats = resp.json()
|
|
assert isinstance(cats, list)
|
|
# 应包含默认分类
|
|
assert "数据处理" in cats or "网络请求" in cats
|
|
|
|
@pytest.mark.api
|
|
def test_list_without_auth(self, client):
|
|
resp = client.get("/api/v1/tools")
|
|
# 未认证时默认 scope=public 应返回 401 或公开工具
|
|
assert resp.status_code == 401
|
|
|
|
@pytest.mark.api
|
|
def test_list_builtin(self, authenticated_client):
|
|
resp = authenticated_client.get("/api/v1/tools/builtin")
|
|
assert resp.status_code == 200
|
|
tools = resp.json()
|
|
assert isinstance(tools, list)
|
|
assert len(tools) >= 10 # 期待至少 10 个内置工具
|
|
|
|
@pytest.mark.api
|
|
def test_create_and_get_tool(self, authenticated_client):
|
|
# 创建 HTTP 工具
|
|
create_resp = authenticated_client.post("/api/v1/tools", json={
|
|
"name": "echo_test",
|
|
"description": "Echo test tool",
|
|
"category": "network",
|
|
"function_schema": {
|
|
"name": "echo_test",
|
|
"description": "Returns the input",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {"msg": {"type": "string"}},
|
|
"required": ["msg"],
|
|
},
|
|
},
|
|
"implementation_type": "http",
|
|
"implementation_config": {
|
|
"url": "https://httpbin.org/post",
|
|
"method": "POST",
|
|
"headers": {},
|
|
"timeout": 10,
|
|
},
|
|
"is_public": True,
|
|
})
|
|
assert create_resp.status_code == 201
|
|
data = create_resp.json()
|
|
assert data["name"] == "echo_test"
|
|
assert data["implementation_type"] == "http"
|
|
tool_id = data["id"]
|
|
|
|
# 获取工具详情
|
|
get_resp = authenticated_client.get(f"/api/v1/tools/{tool_id}")
|
|
assert get_resp.status_code == 200
|
|
assert get_resp.json()["name"] == "echo_test"
|
|
|
|
@pytest.mark.api
|
|
def test_create_code_tool(self, authenticated_client):
|
|
create_resp = authenticated_client.post("/api/v1/tools", json={
|
|
"name": "double_test",
|
|
"description": "Double a number",
|
|
"category": "math",
|
|
"function_schema": {
|
|
"name": "double_test",
|
|
"description": "Doubles a number",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {"n": {"type": "number"}},
|
|
"required": ["n"],
|
|
},
|
|
},
|
|
"implementation_type": "code",
|
|
"implementation_config": {
|
|
"source": "def run(args):\n n = args.get('n', 0)\n return {'result': n * 2}",
|
|
"language": "python",
|
|
},
|
|
"is_public": True,
|
|
})
|
|
assert create_resp.status_code == 201
|
|
data = create_resp.json()
|
|
assert data["name"] == "double_test"
|
|
assert data["implementation_type"] == "code"
|
|
|
|
@pytest.mark.api
|
|
def test_create_duplicate_name(self, authenticated_client):
|
|
# 先创建
|
|
authenticated_client.post("/api/v1/tools", json={
|
|
"name": "dup_tool",
|
|
"description": "Dup",
|
|
"function_schema": {"name": "dup_tool", "parameters": {"type": "object", "properties": {}}},
|
|
"implementation_type": "code",
|
|
"implementation_config": {"source": "def run(args):\n return {}"},
|
|
"is_public": False,
|
|
})
|
|
# 重复创建应报错
|
|
resp = authenticated_client.post("/api/v1/tools", json={
|
|
"name": "dup_tool",
|
|
"description": "Dup again",
|
|
"function_schema": {"name": "dup_tool", "parameters": {"type": "object", "properties": {}}},
|
|
"implementation_type": "code",
|
|
"implementation_config": {"source": "def run(args):\n return {}"},
|
|
"is_public": False,
|
|
})
|
|
assert resp.status_code == 400
|
|
assert "已存在" in resp.json().get("detail", "")
|
|
|
|
@pytest.mark.api
|
|
def test_invalid_implementation_type(self, authenticated_client):
|
|
resp = authenticated_client.post("/api/v1/tools", json={
|
|
"name": "bad_tool",
|
|
"description": "Bad",
|
|
"function_schema": {"name": "bad_tool", "parameters": {"type": "object", "properties": {}}},
|
|
"implementation_type": "invalid_type",
|
|
"is_public": False,
|
|
})
|
|
assert resp.status_code == 400
|
|
|
|
@pytest.mark.api
|
|
def test_mine_scope(self, authenticated_client):
|
|
resp = authenticated_client.get("/api/v1/tools?scope=mine")
|
|
assert resp.status_code == 200
|
|
|
|
@pytest.mark.api
|
|
def test_get_nonexistent_tool(self, authenticated_client):
|
|
resp = authenticated_client.get("/api/v1/tools/nonexistent-id")
|
|
assert resp.status_code == 404
|
|
|
|
@pytest.mark.api
|
|
@pytest.mark.asyncio
|
|
async def test_test_http_endpoint(self, authenticated_client):
|
|
"""测试 HTTP 工具的测试端点"""
|
|
with patch("httpx.AsyncClient.request", new=AsyncMock()) as mock_request:
|
|
mock_response = AsyncMock()
|
|
mock_response.status_code = 200
|
|
mock_response.text = '{"origin": "1.2.3.4"}'
|
|
mock_request.return_value = mock_response
|
|
|
|
resp = authenticated_client.post("/api/v1/tools/test/http", json={
|
|
"url": "https://httpbin.org/get",
|
|
"method": "GET",
|
|
"headers": {},
|
|
"body": None,
|
|
"args": {"ip": "8.8.8.8"},
|
|
"timeout": 5,
|
|
})
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["success"] is True
|
|
|
|
@pytest.mark.api
|
|
def test_test_code_endpoint(self, authenticated_client):
|
|
resp = authenticated_client.post("/api/v1/tools/test/code", json={
|
|
"source": "def run(args):\n return args.get('x', 0) + args.get('y', 0)",
|
|
"args": {"x": 10, "y": 20},
|
|
})
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["success"] is True
|
|
assert data["result"] == 30
|
|
|
|
@pytest.mark.api
|
|
def test_test_code_compile_error(self, authenticated_client):
|
|
resp = authenticated_client.post("/api/v1/tools/test/code", json={
|
|
"source": "invalid python {{{",
|
|
"args": {},
|
|
})
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["success"] is False
|
|
assert "error" in data
|
|
|
|
@pytest.mark.api
|
|
def test_use_count(self, authenticated_client):
|
|
# 先创建工具
|
|
create_resp = authenticated_client.post("/api/v1/tools", json={
|
|
"name": "count_test",
|
|
"description": "Count test",
|
|
"function_schema": {"name": "count_test", "parameters": {"type": "object", "properties": {}}},
|
|
"implementation_type": "code",
|
|
"implementation_config": {"source": "def run(args):\n return {}"},
|
|
"is_public": True,
|
|
})
|
|
assert create_resp.status_code == 201
|
|
tool_id = create_resp.json()["id"]
|
|
|
|
# 使用计数
|
|
use_resp = authenticated_client.post(f"/api/v1/tools/{tool_id}/use")
|
|
assert use_resp.status_code == 200
|
|
assert use_resp.json()["use_count"] == 1
|
|
|
|
# 再次使用
|
|
use_resp2 = authenticated_client.post(f"/api/v1/tools/{tool_id}/use")
|
|
assert use_resp2.status_code == 200
|
|
assert use_resp2.json()["use_count"] == 2
|
|
|
|
@pytest.mark.api
|
|
def test_delete_tool(self, authenticated_client):
|
|
# 创建
|
|
create_resp = authenticated_client.post("/api/v1/tools", json={
|
|
"name": "del_test",
|
|
"description": "Delete test",
|
|
"function_schema": {"name": "del_test", "parameters": {"type": "object", "properties": {}}},
|
|
"implementation_type": "code",
|
|
"implementation_config": {"source": "def run(args):\n return {}"},
|
|
"is_public": False,
|
|
})
|
|
assert create_resp.status_code == 201
|
|
tool_id = create_resp.json()["id"]
|
|
|
|
# 删除
|
|
del_resp = authenticated_client.delete(f"/api/v1/tools/{tool_id}")
|
|
assert del_resp.status_code == 200
|
|
|
|
# 确认已删除
|
|
get_resp = authenticated_client.get(f"/api/v1/tools/{tool_id}")
|
|
assert get_resp.status_code == 404
|
|
|
|
@pytest.mark.api
|
|
def test_update_tool(self, authenticated_client):
|
|
create_resp = authenticated_client.post("/api/v1/tools", json={
|
|
"name": "update_test",
|
|
"description": "Original desc",
|
|
"function_schema": {"name": "update_test", "parameters": {"type": "object", "properties": {}}},
|
|
"implementation_type": "code",
|
|
"implementation_config": {"source": "def run(args):\n return {}"},
|
|
"is_public": False,
|
|
})
|
|
assert create_resp.status_code == 201
|
|
tool_id = create_resp.json()["id"]
|
|
|
|
update_resp = authenticated_client.put(f"/api/v1/tools/{tool_id}", json={
|
|
"name": "update_test",
|
|
"description": "Updated desc",
|
|
"function_schema": {"name": "update_test", "parameters": {"type": "object", "properties": {}}},
|
|
"implementation_type": "code",
|
|
"implementation_config": {"source": "def run(args):\n return {'updated': True}"},
|
|
"is_public": True,
|
|
})
|
|
assert update_resp.status_code == 200
|
|
assert update_resp.json()["description"] == "Updated desc"
|
|
assert update_resp.json()["is_public"] is True
|