first
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from apipilot.adapters.openai import OpenAIAdapter
|
||||
from apipilot.errors import ToolNotFoundError
|
||||
from apipilot.tool import CompositeTool
|
||||
|
||||
from .conftest import build_sample_app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def pilot():
|
||||
_, pilot = build_sample_app(add_composite=True, add_deny=True)
|
||||
return pilot
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tools_for_returns_known_tools(pilot):
|
||||
tools = await pilot.tools_for()
|
||||
names = {t["name"] for t in tools}
|
||||
assert "list_items" in names
|
||||
assert "get_item" in names
|
||||
assert "create_item" in names
|
||||
assert "find_cheapest_item" in names
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tools_for_respects_allow_tool(pilot):
|
||||
user = {"id": "bob", "role": "user"}
|
||||
tools = await pilot.tools_for(user)
|
||||
names = {t["name"] for t in tools}
|
||||
assert "list_items" in names
|
||||
assert "create_item" not in names
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_endpoint_query_params(pilot):
|
||||
res = await pilot.execute(name="list_items", arguments={"category": "active"})
|
||||
assert res["success"] is True
|
||||
data = res["data"]
|
||||
assert data["total"] == 2
|
||||
assert {it["id"] for it in data["items"]} == {1, 2}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_endpoint_path_param(pilot):
|
||||
res = await pilot.execute(name="get_item", arguments={"item_id": 1})
|
||||
assert res["success"] is True
|
||||
assert res["data"]["name"] == "Item A"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_endpoint_path_param_not_found(pilot):
|
||||
res = await pilot.execute(name="get_item", arguments={"item_id": 999})
|
||||
assert res["success"] is False
|
||||
err = res["error"]
|
||||
assert err["code"] == "EXECUTION_ERROR"
|
||||
assert err["status_code"] == 404
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_endpoint_body_params(pilot):
|
||||
res = await pilot.execute(
|
||||
name="create_item",
|
||||
arguments={"name": "New Item", "category": "active", "price": 42},
|
||||
)
|
||||
assert res["success"] is True
|
||||
assert res["data"]["name"] == "New Item"
|
||||
assert res["data"]["price"] == 42
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_unknown_tool_raises(pilot):
|
||||
with pytest.raises(ToolNotFoundError):
|
||||
await pilot.execute(name="does_not_exist", arguments={})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_invalid_arguments_returns_error(pilot):
|
||||
res = await pilot.execute(name="get_item", arguments={"item_id": "not-a-number"})
|
||||
assert res["success"] is False
|
||||
assert res["error"]["code"] == "INVALID_ARGUMENTS"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_denied_tool_user_not_allowed(pilot):
|
||||
user = {"id": "bob", "role": "user"}
|
||||
with pytest.raises(ToolNotFoundError):
|
||||
await pilot.execute(user=user, name="create_item", arguments={"name": "x"})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_timeout(pilot):
|
||||
res = await pilot.execute(name="get_slow_item", arguments={"item_id": 1})
|
||||
assert res["success"] is False
|
||||
assert res["error"]["code"] in {"TIMEOUT", "EXECUTION_ERROR"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_huge_response_truncates(pilot):
|
||||
res = await pilot.execute(name="get_huge_items", arguments={})
|
||||
assert res["success"] is True
|
||||
assert res["meta"].get("truncated") is True or res["data"].get("_truncated") is True
|
||||
import json
|
||||
|
||||
serialized = json.dumps(res["data"])
|
||||
assert len(serialized) <= 1024 + 2048
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_composite_tool(pilot):
|
||||
res = await pilot.execute(
|
||||
name="find_cheapest_item", arguments={"category": "active"}
|
||||
)
|
||||
assert res["success"] is True
|
||||
assert res["data"]["cheapest"]["id"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_empty_result_is_none(pilot):
|
||||
res = await pilot.execute(name="list_items", arguments={"category": "nothing-here"})
|
||||
assert res["success"] is True
|
||||
assert res["data"]["total"] == 0
|
||||
assert res["data"]["items"] == []
|
||||
|
||||
|
||||
def test_openai_adapter_tools_format():
|
||||
definitions = [
|
||||
{
|
||||
"name": "list_items",
|
||||
"description": "Elenca items",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"category": {"type": "string"}},
|
||||
"required": [],
|
||||
},
|
||||
}
|
||||
]
|
||||
tools = OpenAIAdapter.to_tools(definitions)
|
||||
assert tools[0]["type"] == "function"
|
||||
assert tools[0]["function"]["name"] == "list_items"
|
||||
assert tools[0]["function"]["parameters"]["properties"]["category"]["type"] == "string"
|
||||
|
||||
|
||||
def test_openai_adapter_parse_tool_call():
|
||||
tc = {
|
||||
"id": "call_123",
|
||||
"type": "function",
|
||||
"function": {"name": "list_items", "arguments": '{"category":"active"}'},
|
||||
}
|
||||
tc_id, name, args = OpenAIAdapter.parse_tool_call(tc)
|
||||
assert tc_id == "call_123"
|
||||
assert name == "list_items"
|
||||
assert args == {"category": "active"}
|
||||
|
||||
|
||||
def test_openai_adapter_make_tool_result_success():
|
||||
from apipilot.types import ExecuteResponse
|
||||
|
||||
resp: ExecuteResponse = {
|
||||
"success": True,
|
||||
"data": {"items": [1, 2, 3]},
|
||||
"error": None,
|
||||
"meta": {"tool": "list_items"},
|
||||
}
|
||||
tr = OpenAIAdapter.make_tool_result("call_1", "list_items", resp)
|
||||
assert tr["role"] == "tool"
|
||||
assert tr["tool_call_id"] == "call_1"
|
||||
assert tr["name"] == "list_items"
|
||||
import json
|
||||
|
||||
assert json.loads(tr["content"]) == {"items": [1, 2, 3]}
|
||||
|
||||
|
||||
def test_openai_adapter_make_tool_result_error():
|
||||
from apipilot.types import ExecuteResponse
|
||||
|
||||
resp: ExecuteResponse = {
|
||||
"success": False,
|
||||
"data": None,
|
||||
"error": {"code": "X", "message": "fail"},
|
||||
"meta": {"tool": "x"},
|
||||
}
|
||||
tr = OpenAIAdapter.make_tool_result("call_2", "x", resp)
|
||||
import json
|
||||
|
||||
assert json.loads(tr["content"]) == {"error": {"code": "X", "message": "fail"}}
|
||||
Reference in New Issue
Block a user