Files
YiemAgent/test/toolTest.jl
2026-08-15 16:50:28 +07:00

368 lines
13 KiB
Julia

using Test
using Dates
using YiemAgent
using YiemAgent.toolRegistry
using YiemAgent.type
using YiemAgent.agentCore
@testset "register_all_tools with toolStore" begin
# ------------------------------------------------------------------ #
# 1. register_all_tools registers all static tools + listTools #
# ------------------------------------------------------------------ #
store = toolStore(name="test1")
loaded = register_all_tools(store)
@test !isempty(loaded)
@test length(loaded) == 4 # getWeather + getTime + writeTool + listTools
names = [k for k in keys(loaded)]
@test "getTime" in names
@test "getWeather" in names
@test "writeTool" in names
@test "listTools" in names
# ------------------------------------------------------------------ #
# 2. register_all_tools returns tools in registration order #
# ------------------------------------------------------------------ #
@test collect(keys(loaded))[1] == "getWeather"
@test collect(keys(loaded))[2] == "getTime"
@test collect(keys(loaded))[3] == "writeTool"
@test collect(keys(loaded))[4] == "listTools"
# ------------------------------------------------------------------ #
# 3. Verify loaded tool fields are correct #
# ------------------------------------------------------------------ #
# getTime
time_tool = loaded["getTime"]
@test time_tool.name == "getTime"
@test time_tool.label == "Time Lookup"
@test time_tool.validateRequiredArgs !== nothing
@test time_tool.parallelToolExecute == false
@test time_tool.inputSchema["required"] == Any[]
# getWeather
weather = loaded["getWeather"]
@test weather.name == "getWeather"
@test weather.label == "Weather Lookup"
@test weather.execute !== nothing
@test weather.parallelToolExecute == false
@test weather.inputSchema["required"] == ["city"]
# writeTool
wt = loaded["writeTool"]
@test wt.name == "writeTool"
@test wt.label == "Create Tool"
@test wt.execute !== nothing
@test "name" in wt.inputSchema["required"]
@test "executeCode" in wt.inputSchema["required"]
# ------------------------------------------------------------------ #
# 4. Tool execution returns valid results #
# ------------------------------------------------------------------ #
sig = nothing
op = x -> x # no-op partial result callback
# execute getTime
result_t = time_tool.execute("call-1", Dict{String,Any}("city" => "Tokyo"), sig, op)
@test result_t isa agentToolResult
@test result_t.content[1] isa textContent
@test occursin("Tokyo", result_t.content[1].text)
# execute getTime with timezone
result_tz = time_tool.execute("call-2", Dict{String,Any}("timezone" => "America/New_York"), sig, op)
@test result_tz isa agentToolResult
@test occursin("America/New_York", result_tz.content[1].text)
# execute getWeather
result_w = weather.execute("call-3", Dict{String,Any}("city" => "Bangkok"), sig, op)
@test result_w isa agentToolResult
@test result_w.content[1] isa textContent
@test occursin("Bangkok", result_w.content[1].text)
# execute getWeather with units
result_w2 = weather.execute("call-4", Dict{String,Any}("city" => "London", "units" => "fahrenheit"), sig, op)
@test occursin("72\u00b0F", result_w2.content[1].text)
# ------------------------------------------------------------------ #
# 5. getTools / registerTool / clearTools (per-store isolation) #
# ------------------------------------------------------------------ #
store3 = toolStore(name="test3")
registry_tools = getTools(store3)
@test isempty(registry_tools)
# Register tools manually
registerTool(store3, loaded["getTime"])
registerTool(store3, loaded["getWeather"])
registerTool(store3, loaded["writeTool"])
reg = getTools(store3)
@test !isempty(reg)
@test "getTime" in keys(reg)
@test "getWeather" in keys(reg)
@test "writeTool" in keys(reg)
@test collect(keys(reg))[1] == "getTime"
@test collect(keys(reg))[2] == "getWeather"
@test collect(keys(reg))[3] == "writeTool"
clearTools(store3)
@test isempty(getTools(store3))
test_tool = agentTool(
name = "manualTool",
label = "Manual Tool",
description = "Registered manually",
inputSchema = Dict{String,Any}("type" => "object", "properties" => Dict{String,Any}(), "required" => Any[]),
execute = (toolCallId, args, signal, onPartialResult) ->
agentToolResult([textContent("manual")], Dict{Any,Any}(), nothing, false),
prepareArguments = nothing,
validateRequiredArgs = nothing,
parallelToolExecute = true
)
registerTool(store3, test_tool)
reg = getTools(store3)
@test haskey(reg, "manualTool")
@test length(reg) == 1
@test reg["manualTool"].parallelToolExecute == true
# ------------------------------------------------------------------ #
# 6. getTools returns direct reference (mutations affect registry) #
# ------------------------------------------------------------------ #
copy1 = getTools(store3)
copy2 = getTools(store3)
@test copy1 === copy2 # same reference, not a deep copy
empty!(copy1)
@test isempty(getTools(store3)) # mutation propagates
# ------------------------------------------------------------------ #
# 7. Per-store isolation — two stores don't share tools #
# ------------------------------------------------------------------ #
storeA = toolStore(name="isolationA")
storeB = toolStore(name="isolationB")
registerTool(storeA, loaded["getTime"])
registerTool(storeB, loaded["getWeather"])
regA = getTools(storeA)
regB = getTools(storeB)
@test "getTime" in keys(regA)
@test "getWeather" keys(regA)
@test "getWeather" in keys(regB)
@test "getTime" keys(regB)
clearTools(storeA)
@test isempty(getTools(storeA))
@test !isempty(getTools(storeB)) # storeB unaffected
end
@testset "listTool" begin
store = toolStore(name="test_list")
register_all_tools(store) # auto-registers getWeather, getTime, writeTool + listTools
# register_all_tools auto-registers listTool
@test "listTools" in keys(store.tools)
# listTool returns an agentTool, not a string or array
list_t = listTool(store)
@test list_t isa agentTool
@test list_t.name == "listTools"
@test list_t.label == "List Tools"
@test isempty(list_t.inputSchema["required"])
# Verify all tools appear (3 loaded + listTools = 4)
result = list_t.execute("call-1", Dict{String,Any}(), nothing, x -> x)
@test result isa agentToolResult
@test result.content[1] isa textContent
@test occursin("listTools", result.content[1].text)
@test occursin("getWeather", result.content[1].text)
@test occursin("getTime", result.content[1].text)
@test occursin("writeTool", result.content[1].text)
@test result.details["count"] == 4
# Each listTool call creates an independent closure
storeB = toolStore(name="test_listB")
registerTool(storeB, store.tools["getWeather"])
list_tB = listTool(storeB)
resultA = list_t.execute("call-3", Dict{String,Any}(), nothing, x -> x)
resultB = list_tB.execute("call-4", Dict{String,Any}(), nothing, x -> x)
@test occursin("getWeather", resultA.content[1].text)
@test occursin("getWeather", resultB.content[1].text)
@test occursin("getTime", resultA.content[1].text)
@test occursin("getTime", resultB.content[1].text) == false # storeB only has getWeather
end
@testset "executePreparedToolCall with static tools" begin
# Tests executePreparedToolCall with statically loaded tools.
# The world-age issue is resolved because tool.execute comes from
# a statically included module, not a dynamically created one.
store = toolStore(name="test_static")
register_all_tools(store)
weather_tool = store.tools["getWeather"]
# Create a preparedToolCall that mimics what prepareToolCall() returns
tool_call = agentToolCall(
"function", "call-static-1", "getWeather",
Dict{String,Any}("city" => "San Francisco")
)
prep = preparedToolCall(
weather_tool, tool_call, Dict{String,Any}("city" => "San Francisco")
)
sig = abortSignal(false)
# This call goes through: executePreparedToolCall -> prep.tool.execute(...)
result = executePreparedToolCall(
prep, sig, x -> nothing
)
@test result isa executedOutcome
@test result.isError == false
@test result.result.content[1] isa textContent
@test occursin("San Francisco", result.result.content[1].text)
end
@testset "executePreparedToolCall with validation (static tools)" begin
# Tests executePreparedToolCall with a tool that has custom validation hooks.
# This exercises the full tool execution path including validation.
store = toolStore(name="test_static_validate")
register_all_tools(store)
time_tool = store.tools["getTime"]
tool_call = agentToolCall(
"function", "call-static-2", "getTime",
Dict{String,Any}("timezone" => "America/New_York")
)
prep = preparedToolCall(
time_tool, tool_call, Dict{String,Any}("timezone" => "America/New_York")
)
sig = abortSignal(false)
result = executePreparedToolCall(
prep, sig, x -> nothing
)
@test result isa executedOutcome
@test result.isError == false
@test result.result.content[1] isa textContent
@test occursin("America/New_York", result.result.content[1].text)
end
@testset "executeToolCallsSequential with static tools (full pipeline)" begin
# Tests the full tool execution pipeline: executeToolCallsSequential
# which calls prepareToolCall -> executePreparedToolCall -> finalizeExecutedToolCall
# with statically loaded tools.
store = toolStore(name="test_full_pipeline")
register_all_tools(store)
# Build agentContext from the store's tools
tools = getTools(store)
ctx = agentContext(
"test system prompt",
agentMessage[],
tools
)
# Create an assistant message containing tool calls
assistant_msg = assistantMessage(
role="assistant",
content=Vector{messageContent}(),
api="openai",
provider="test",
model="test-model",
usage=llmUsage(0, 0),
stopReason="tool_calls",
errorMessage=nothing,
timestamp=now()
)
# Create tool calls for multiple statically loaded tools
tool_calls = [
agentToolCall(
"function", "call-seq-1", "getWeather",
Dict{String,Any}("city" => "Tokyo")
),
agentToolCall(
"function", "call-seq-2", "getTime",
Dict{String,Any}("timezone" => "Europe/London")
),
]
config = agentLoopConfig(
nothing, nothing, "sequential"
)
sig = abortSignal(false)
# Execute the full pipeline
batch = executeToolCallsSequential(
ctx, assistant_msg, tool_calls, config, sig, x -> nothing
)
@test batch.messages isa Vector{toolResultMessage}
@test length(batch.messages) == 2
@test batch.messages[1].toolName == "getWeather"
@test batch.messages[1].isError == false
@test occursin("Tokyo", batch.messages[1].content[1].text)
@test batch.messages[2].toolName == "getTime"
@test batch.messages[2].isError == false
@test occursin("Europe/London", batch.messages[2].content[1].text)
end
@testset "executeToolCallsParallel with static tools (full pipeline)" begin
# Same as above but tests parallel execution path.
store = toolStore(name="test_parallel")
register_all_tools(store)
tools = getTools(store)
ctx = agentContext(
"test system prompt",
agentMessage[],
tools
)
assistant_msg = assistantMessage(
role="assistant",
content=Vector{messageContent}(),
api="openai",
provider="test",
model="test-model",
usage=llmUsage(0, 0),
stopReason="tool_calls",
errorMessage=nothing,
timestamp=now()
)
tool_calls = [
agentToolCall(
"function", "call-par-1", "getWeather",
Dict{String,Any}("city" => "Paris")
),
agentToolCall(
"function", "call-par-2", "getTime",
Dict{String,Any}("city" => "Sydney")
),
]
config = agentLoopConfig(
nothing, nothing, "parallel"
)
sig = abortSignal(false)
batch = executeToolCallsParallel(
ctx, assistant_msg, tool_calls, config, sig, x -> nothing
)
@test batch.messages isa Vector{toolResultMessage}
@test length(batch.messages) == 2
@test batch.messages[1].toolName == "getWeather"
@test batch.messages[1].isError == false
@test batch.messages[2].toolName == "getTime"
@test batch.messages[2].isError == false
end