mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-28 14:07:55 +00:00
199 lines
6.7 KiB
Python
199 lines
6.7 KiB
Python
"""Tests for tools/_stubs.py — ToolSpec, BaseTool, ToolExecutor."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from openjarvis.core.events import EventBus, EventType
|
|
from openjarvis.core.types import ToolCall, ToolResult
|
|
from openjarvis.tools._stubs import BaseTool, ToolExecutor, ToolSpec
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class _EchoTool(BaseTool):
|
|
"""Minimal tool that echoes its input."""
|
|
|
|
tool_id = "echo"
|
|
|
|
@property
|
|
def spec(self) -> ToolSpec:
|
|
return ToolSpec(
|
|
name="echo",
|
|
description="Echoes input back.",
|
|
parameters={
|
|
"type": "object",
|
|
"properties": {"text": {"type": "string"}},
|
|
"required": ["text"],
|
|
},
|
|
category="testing",
|
|
)
|
|
|
|
def execute(self, **params) -> ToolResult:
|
|
return ToolResult(
|
|
tool_name="echo",
|
|
content=params.get("text", ""),
|
|
success=True,
|
|
)
|
|
|
|
|
|
class _ErrorTool(BaseTool):
|
|
"""Tool that always raises."""
|
|
|
|
tool_id = "error"
|
|
|
|
@property
|
|
def spec(self) -> ToolSpec:
|
|
return ToolSpec(name="error", description="Always errors.")
|
|
|
|
def execute(self, **params) -> ToolResult:
|
|
raise RuntimeError("boom")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ToolSpec tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestToolSpec:
|
|
def test_defaults(self):
|
|
s = ToolSpec(name="test", description="A test tool.")
|
|
assert s.name == "test"
|
|
assert s.description == "A test tool."
|
|
assert s.parameters == {}
|
|
assert s.category == ""
|
|
assert s.cost_estimate == 0.0
|
|
assert s.requires_confirmation is False
|
|
|
|
def test_full_spec(self):
|
|
s = ToolSpec(
|
|
name="calc",
|
|
description="Calculate things.",
|
|
parameters={"type": "object"},
|
|
category="math",
|
|
cost_estimate=0.01,
|
|
latency_estimate=0.5,
|
|
requires_confirmation=True,
|
|
metadata={"version": "1.0"},
|
|
)
|
|
assert s.category == "math"
|
|
assert s.metadata["version"] == "1.0"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# BaseTool tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestBaseTool:
|
|
def test_echo_tool_spec(self):
|
|
tool = _EchoTool()
|
|
assert tool.spec.name == "echo"
|
|
assert tool.tool_id == "echo"
|
|
|
|
def test_echo_tool_execute(self):
|
|
tool = _EchoTool()
|
|
result = tool.execute(text="hello")
|
|
assert result.content == "hello"
|
|
assert result.success is True
|
|
|
|
def test_to_openai_function(self):
|
|
tool = _EchoTool()
|
|
fn = tool.to_openai_function()
|
|
assert fn["type"] == "function"
|
|
assert fn["function"]["name"] == "echo"
|
|
assert fn["function"]["description"] == "Echoes input back."
|
|
assert "properties" in fn["function"]["parameters"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ToolExecutor tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestToolExecutor:
|
|
def test_execute_success(self):
|
|
executor = ToolExecutor([_EchoTool()])
|
|
call = ToolCall(id="1", name="echo", arguments='{"text":"hi"}')
|
|
result = executor.execute(call)
|
|
assert result.success is True
|
|
assert result.content == "hi"
|
|
assert result.latency_seconds > 0
|
|
|
|
def test_execute_unknown_tool(self):
|
|
executor = ToolExecutor([_EchoTool()])
|
|
call = ToolCall(id="1", name="nonexistent", arguments="{}")
|
|
result = executor.execute(call)
|
|
assert result.success is False
|
|
assert "Unknown tool" in result.content
|
|
|
|
def test_execute_invalid_json(self):
|
|
executor = ToolExecutor([_EchoTool()])
|
|
call = ToolCall(id="1", name="echo", arguments="not json")
|
|
result = executor.execute(call)
|
|
assert result.success is False
|
|
assert "Invalid arguments JSON" in result.content
|
|
|
|
def test_execute_empty_arguments(self):
|
|
executor = ToolExecutor([_EchoTool()])
|
|
call = ToolCall(id="1", name="echo", arguments="")
|
|
result = executor.execute(call)
|
|
assert result.success is True
|
|
assert result.content == ""
|
|
|
|
def test_execute_tool_error(self):
|
|
executor = ToolExecutor([_ErrorTool()])
|
|
call = ToolCall(id="1", name="error", arguments="{}")
|
|
result = executor.execute(call)
|
|
assert result.success is False
|
|
assert "boom" in result.content
|
|
|
|
def test_available_tools(self):
|
|
executor = ToolExecutor([_EchoTool(), _ErrorTool()])
|
|
specs = executor.available_tools()
|
|
assert len(specs) == 2
|
|
names = {s.name for s in specs}
|
|
assert names == {"echo", "error"}
|
|
|
|
def test_get_openai_tools(self):
|
|
executor = ToolExecutor([_EchoTool()])
|
|
tools = executor.get_openai_tools()
|
|
assert len(tools) == 1
|
|
assert tools[0]["type"] == "function"
|
|
assert tools[0]["function"]["name"] == "echo"
|
|
|
|
def test_event_bus_integration(self):
|
|
bus = EventBus(record_history=True)
|
|
executor = ToolExecutor([_EchoTool()], bus=bus)
|
|
call = ToolCall(id="1", name="echo", arguments='{"text":"ping"}')
|
|
executor.execute(call)
|
|
events = bus.history
|
|
types = [e.event_type for e in events]
|
|
assert EventType.TOOL_CALL_START in types
|
|
assert EventType.TOOL_CALL_END in types
|
|
# Check start event data
|
|
start = [e for e in events if e.event_type == EventType.TOOL_CALL_START][0]
|
|
assert start.data["tool"] == "echo"
|
|
# Check end event data
|
|
end = [e for e in events if e.event_type == EventType.TOOL_CALL_END][0]
|
|
assert end.data["success"] is True
|
|
|
|
def test_event_bus_on_error(self):
|
|
bus = EventBus(record_history=True)
|
|
executor = ToolExecutor([_ErrorTool()], bus=bus)
|
|
call = ToolCall(id="1", name="error", arguments="{}")
|
|
executor.execute(call)
|
|
end = [e for e in bus.history if e.event_type == EventType.TOOL_CALL_END][0]
|
|
assert end.data["success"] is False
|
|
|
|
def test_no_bus_works(self):
|
|
executor = ToolExecutor([_EchoTool()])
|
|
call = ToolCall(id="1", name="echo", arguments='{"text":"ok"}')
|
|
result = executor.execute(call)
|
|
assert result.success is True
|
|
|
|
def test_empty_executor(self):
|
|
executor = ToolExecutor([])
|
|
assert executor.available_tools() == []
|
|
assert executor.get_openai_tools() == []
|