"""ToolCalls Integration Tests"""
import asyncio
import time
import pytest
from secafs_sdk import ToolCalls
@pytest.mark.asyncio
class TestToolCallsBasicOperations:
"""Basic ToolCalls operations"""
async def test_start_tool_call_returns_id(self, db):
"""Should start a tool call and return an ID"""
tools = await ToolCalls.from_database(db)
call_id = await tools.start("test_tool", {"arg1": "value1"})
assert call_id > 0
async def test_start_without_parameters(self, db):
"""Should start a tool call without parameters"""
tools = await ToolCalls.from_database(db)
call_id = await tools.start("simple_tool")
assert call_id > 0
tool_call = await tools.get(call_id)
assert tool_call is not None
assert tool_call.name == "simple_tool"
assert tool_call.parameters is None
assert tool_call.status == "pending"
async def test_mark_call_as_successful(self, db):
"""Should mark a tool call as successful"""
tools = await ToolCalls.from_database(db)
call_id = await tools.start("test_tool", {"input": "test"})
await tools.success(call_id, {"output": "result"})
tool_call = await tools.get(call_id)
assert tool_call is not None
assert tool_call.status == "success"
assert tool_call.result == {"output": "result"}
assert tool_call.completed_at is not None and tool_call.completed_at > 0
assert tool_call.duration_ms is not None and tool_call.duration_ms >= 0
async def test_mark_successful_without_result(self, db):
"""Should mark a tool call as successful without result"""
tools = await ToolCalls.from_database(db)
call_id = await tools.start("test_tool", {"input": "test"})
await tools.success(call_id)
tool_call = await tools.get(call_id)
assert tool_call is not None
assert tool_call.status == "success"
assert tool_call.result is None
async def test_mark_call_as_failed(self, db):
"""Should mark a tool call as failed"""
tools = await ToolCalls.from_database(db)
call_id = await tools.start("test_tool", {"input": "test"})
await tools.error(call_id, "Something went wrong")
tool_call = await tools.get(call_id)
assert tool_call is not None
assert tool_call.status == "error"
assert tool_call.error == "Something went wrong"
assert tool_call.completed_at is not None and tool_call.completed_at > 0
assert tool_call.duration_ms is not None and tool_call.duration_ms >= 0
async def test_get_tool_call_by_id(self, db):
"""Should get a tool call by ID"""
tools = await ToolCalls.from_database(db)
call_id = await tools.start("test_tool", {"arg": "value"})
tool_call = await tools.get(call_id)
assert tool_call is not None
assert tool_call.id == call_id
assert tool_call.name == "test_tool"
assert tool_call.parameters == {"arg": "value"}
assert tool_call.status == "pending"
assert tool_call.started_at > 0
async def test_get_nonexistent_id(self, db):
"""Should return None for non-existent ID"""
tools = await ToolCalls.from_database(db)
tool_call = await tools.get(99999)
assert tool_call is None
@pytest.mark.asyncio
class TestToolCallsQueryOperations:
"""ToolCalls query operations"""
async def test_get_by_name(self, db):
"""Should get tool calls by name"""
tools = await ToolCalls.from_database(db)
await tools.start("tool_a", {"test": 1})
await tools.start("tool_b", {"test": 2})
await tools.start("tool_a", {"test": 3})
results = await tools.get_by_name("tool_a")
assert len(results) == 2
assert all(tc.name == "tool_a" for tc in results)
async def test_limit_results_by_name(self, db):
"""Should limit results when querying by name"""
tools = await ToolCalls.from_database(db)
await tools.start("tool_a", {"test": 1})
await tools.start("tool_a", {"test": 2})
await tools.start("tool_a", {"test": 3})
results = await tools.get_by_name("tool_a", limit=2)
assert len(results) == 2
async def test_get_recent_calls(self, db):
"""Should get recent tool calls"""
tools = await ToolCalls.from_database(db)
await tools.start("tool_old")
await asyncio.sleep(1.1)
midpoint = int(time.time())
await asyncio.sleep(1.1)
await tools.start("tool_new")
results = await tools.get_recent(midpoint)
assert len(results) >= 1
assert all(tc.started_at >= midpoint for tc in results)
async def test_limit_recent_calls(self, db):
"""Should limit recent tool calls"""
tools = await ToolCalls.from_database(db)
now = int(time.time())
await tools.start("tool_1")
await tools.start("tool_2")
await tools.start("tool_3")
results = await tools.get_recent(now - 10, limit=2)
assert len(results) <= 2
async def test_empty_results_for_nonexistent_name(self, db):
"""Should return empty array when no matching tool calls by name"""
tools = await ToolCalls.from_database(db)
results = await tools.get_by_name("non_existent_tool")
assert results == []
@pytest.mark.asyncio
class TestToolCallsStatistics:
"""ToolCalls statistics"""
async def test_calculate_statistics(self, db):
"""Should calculate tool call statistics"""
tools = await ToolCalls.from_database(db)
id1 = await tools.start("tool_a", {"test": 1})
await tools.success(id1, {"result": "ok"})
id2 = await tools.start("tool_a", {"test": 2})
await tools.error(id2, "failed")
id3 = await tools.start("tool_a", {"test": 3})
await tools.success(id3, {"result": "ok"})
id4 = await tools.start("tool_b", {"test": 4})
await tools.success(id4, {"result": "ok"})
stats = await tools.get_stats()
assert len(stats) == 2
tool_a_stats = next((s for s in stats if s.name == "tool_a"), None)
assert tool_a_stats is not None
assert tool_a_stats.total_calls == 3
assert tool_a_stats.successful == 2
assert tool_a_stats.failed == 1
assert tool_a_stats.avg_duration_ms >= 0
tool_b_stats = next((s for s in stats if s.name == "tool_b"), None)
assert tool_b_stats is not None
assert tool_b_stats.total_calls == 1
assert tool_b_stats.successful == 1
assert tool_b_stats.failed == 0
async def test_exclude_pending_from_stats(self, db):
"""Should exclude pending calls from statistics"""
tools = await ToolCalls.from_database(db)
id1 = await tools.start("tool_a", {"test": 1})
await tools.success(id1, {"result": "ok"})
await tools.start("tool_a", {"test": 2})
stats = await tools.get_stats()
tool_a_stats = next((s for s in stats if s.name == "tool_a"), None)
assert tool_a_stats is not None
assert tool_a_stats.total_calls == 1
async def test_empty_stats_no_completed_calls(self, db):
"""Should return empty array when no completed calls"""
tools = await ToolCalls.from_database(db)
await tools.start("tool_a", {"test": 1})
stats = await tools.get_stats()
assert stats == []
@pytest.mark.asyncio
class TestToolCallsComplexData:
"""ToolCalls complex parameters and results"""
async def test_complex_nested_parameters(self, db):
"""Should handle complex nested parameters"""
tools = await ToolCalls.from_database(db)
complex_params = {
"user": {"id": 123, "name": "Test User"},
"options": {"timeout": 5000, "retry": True},
"data": [1, 2, 3, 4, 5],
}
call_id = await tools.start("complex_tool", complex_params)
tool_call = await tools.get(call_id)
assert tool_call is not None
assert tool_call.parameters == complex_params
async def test_complex_nested_results(self, db):
"""Should handle complex nested results"""
tools = await ToolCalls.from_database(db)
complex_result = {
"data": {"items": [{"id": 1, "value": "a"}, {"id": 2, "value": "b"}]},
"metadata": {"count": 2, "hasMore": False},
}
call_id = await tools.start("complex_tool")
await tools.success(call_id, complex_result)
tool_call = await tools.get(call_id)
assert tool_call is not None
assert tool_call.result == complex_result
async def test_large_parameters(self, db):
"""Should handle large parameters"""
tools = await ToolCalls.from_database(db)
large_params = {"items": [{"id": i, "data": f"Data for item {i}"} for i in range(100)]}
call_id = await tools.start("large_tool", large_params)
tool_call = await tools.get(call_id)
assert tool_call is not None
assert tool_call.parameters == large_params
@pytest.mark.asyncio
class TestToolCallsPersistence:
"""ToolCalls persistence"""
async def test_persist_across_instances(self, db):
"""Should persist tool calls across instances"""
tools = await ToolCalls.from_database(db)
call_id = await tools.start("persist_tool", {"test": "value"})
await tools.success(call_id, {"result": "ok"})
new_tools = await ToolCalls.from_database(db)
tool_call = await new_tools.get(call_id)
assert tool_call is not None
assert tool_call.name == "persist_tool"
assert tool_call.status == "success"
@pytest.mark.asyncio
class TestToolCallsOrdering:
"""ToolCalls ordering"""
async def test_order_by_started_at_desc(self, db):
"""Should return tool calls ordered by started_at desc"""
tools = await ToolCalls.from_database(db)
await tools.start("tool_first")
await asyncio.sleep(0.1)
await tools.start("tool_second")
await asyncio.sleep(0.1)
await tools.start("tool_third")
recent = await tools.get_recent(0)
assert len(recent) >= 3
for i in range(len(recent) - 1):
assert recent[i].started_at >= recent[i + 1].started_at
@pytest.mark.asyncio
class TestToolCallsRecord:
"""ToolCalls record method"""
async def test_record_completed_call(self, db):
"""Should record a completed tool call"""
tools = await ToolCalls.from_database(db)
start = int(time.time())
end = start + 1
call_id = await tools.record(
"test_tool",
start,
end,
parameters={"param": "value"},
result={"result": "success"},
)
assert call_id > 0
call = await tools.get(call_id)
assert call is not None
assert call.name == "test_tool"
assert call.parameters == {"param": "value"}
assert call.result == {"result": "success"}
assert call.status == "success"
async def test_record_failed_call(self, db):
"""Should record a failed tool call"""
tools = await ToolCalls.from_database(db)
start = int(time.time())
end = start + 1
call_id = await tools.record(
"test_tool", start, end, parameters={"param": "value"}, error="Failed"
)
assert call_id > 0
call = await tools.get(call_id)
assert call is not None
assert call.name == "test_tool"
assert call.error == "Failed"
assert call.status == "error"