213 lines
8.6 KiB
Julia
213 lines
8.6 KiB
Julia
using Test
|
|
using YiemAgent
|
|
using YiemAgent.toolRegistry
|
|
using YiemAgent.type
|
|
|
|
# Path to the real tools directory
|
|
TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
|
|
|
@testset "loadTools with toolStore" begin
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# 1. loadTools throws on non-existent directory #
|
|
# ------------------------------------------------------------------ #
|
|
store = toolStore(name="test1")
|
|
@test_throws ArgumentError loadTools(store, "/nonexistent/dir/that/does/not/exist")
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# 2. loadTools throws if a .jl file does not define getTool() #
|
|
# Must run BEFORE any other loadTools call (getTool binding #
|
|
# persists in module scope after include()). #
|
|
# ------------------------------------------------------------------ #
|
|
bad_dir = mktempdir()
|
|
write(joinpath(bad_dir, "noTool.jl"), "x = 42\n")
|
|
@test_throws ArgumentError loadTools(store, bad_dir)
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# 3. loadTools loads actual tool files from src/tools/ #
|
|
# ------------------------------------------------------------------ #
|
|
store2 = toolStore(name="test2")
|
|
loaded = loadTools(store2, TOOLS_DIR)
|
|
@test !isempty(loaded)
|
|
@test length(loaded) == 4 # 3 files + auto-registered listTools
|
|
|
|
names = [k for k in keys(loaded)]
|
|
@test "getTime" in names
|
|
@test "getWeather" in names
|
|
@test "writeTool" in names
|
|
@test "listTools" in names
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# 4. loadTools returns tools sorted alphabetically by filename #
|
|
# (getTime.jl < getWeather.jl < writeTool.jl) + listTools at end #
|
|
# ------------------------------------------------------------------ #
|
|
@test collect(keys(loaded))[1] == "getTime"
|
|
@test collect(keys(loaded))[2] == "getWeather"
|
|
@test collect(keys(loaded))[3] == "writeTool"
|
|
@test collect(keys(loaded))[4] == "listTools"
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# 5. 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"]
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# 6. 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°F", result_w2.content[1].text)
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# 7. getTools / registerTool / clearTools (per-store isolation) #
|
|
# ------------------------------------------------------------------ #
|
|
store3 = toolStore(name="test3")
|
|
registry_tools = getTools(store3)
|
|
@test isempty(registry_tools)
|
|
|
|
# listTool is not auto-registered anymore — each store starts empty
|
|
# 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
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# 8. 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
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# 9. 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")
|
|
loaded = loadTools(store, TOOLS_DIR) # auto-registers getWeather, getTime, writeTool + listTools
|
|
|
|
# loadTools auto-registers listTool
|
|
@test "listTools" in keys(loaded)
|
|
|
|
# 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, loaded["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
|