Files
probo/pkg/agent/tool_test.go
Sacha Al Himdani 6e7c96732f Add async third-party vetting
Queue vetting on third_parties with PENDING, PROCESSING,
COMPLETED, and FAILED states. Expose enqueue and status through
GraphQL, MCP, CLI, and n8n, validate vet requests, tune the
worker via config, and poll the detail page while vetting runs.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-06-02 11:39:51 +02:00

467 lines
11 KiB
Go

// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package agent_test
import (
"context"
"encoding/json"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.probo.inc/probo/pkg/agent"
)
func TestFunctionTool_Name(t *testing.T) {
t.Parallel()
type Params struct{}
tool := agent.FunctionTool(
"my_tool",
"does things",
func(_ context.Context, _ Params) (agent.ToolResult, error) {
return agent.ToolResult{}, nil
},
)
assert.Equal(t, "my_tool", tool.Name())
}
func TestFunctionTool_Definition(t *testing.T) {
t.Parallel()
t.Run(
"returns name and description",
func(t *testing.T) {
t.Parallel()
type Params struct {
Query string `json:"query"`
}
tool := agent.FunctionTool(
"search",
"Search for items",
func(_ context.Context, _ Params) (agent.ToolResult, error) {
return agent.ToolResult{}, nil
},
)
def := tool.Definition()
assert.Equal(t, "search", def.Name)
assert.Equal(t, "Search for items", def.Description)
},
)
t.Run(
"generates valid JSON schema from params type",
func(t *testing.T) {
t.Parallel()
type Params struct {
Name string `json:"name" jsonschema:"The item name"`
Count int `json:"count"`
}
tool := agent.FunctionTool(
"create",
"Create items",
func(_ context.Context, _ Params) (agent.ToolResult, error) {
return agent.ToolResult{}, nil
},
)
def := tool.Definition()
require.NotNil(t, def.Parameters)
var schema map[string]any
require.NoError(t, json.Unmarshal(def.Parameters, &schema))
assert.Equal(t, "object", schema["type"])
props := schema["properties"].(map[string]any)
assert.Contains(t, props, "name")
assert.Contains(t, props, "count")
nameProp := props["name"].(map[string]any)
assert.Equal(t, "string", nameProp["type"])
assert.Equal(t, "The item name", nameProp["description"])
countProp := props["count"].(map[string]any)
assert.Equal(t, "integer", countProp["type"])
},
)
t.Run(
"empty struct produces object schema with no properties",
func(t *testing.T) {
t.Parallel()
type Params struct{}
tool := agent.FunctionTool(
"noop",
"No-op",
func(_ context.Context, _ Params) (agent.ToolResult, error) {
return agent.ToolResult{}, nil
},
)
var schema map[string]any
require.NoError(t, json.Unmarshal(tool.Definition().Parameters, &schema))
assert.Equal(t, "object", schema["type"])
},
)
t.Run(
"pointer fields are not nullable in schema",
func(t *testing.T) {
t.Parallel()
type Params struct {
Title *string `json:"title,omitempty"`
}
tool := agent.FunctionTool(
"update",
"Update",
func(_ context.Context, _ Params) (agent.ToolResult, error) {
return agent.ToolResult{}, nil
},
)
var schema map[string]any
require.NoError(t, json.Unmarshal(tool.Definition().Parameters, &schema))
props := schema["properties"].(map[string]any)
titleProp := props["title"].(map[string]any)
assert.Equal(t, "string", titleProp["type"])
assert.Nil(t, titleProp["types"])
},
)
}
func TestFunctionTool_Execute(t *testing.T) {
t.Parallel()
t.Run(
"unmarshals params and calls function",
func(t *testing.T) {
t.Parallel()
type Params struct {
X int `json:"x"`
Y int `json:"y"`
}
tool := agent.FunctionTool(
"add",
"Add two numbers",
func(_ context.Context, p Params) (agent.ToolResult, error) {
return agent.ToolResult{Content: "42"}, nil
},
)
result, err := tool.Execute(context.Background(), `{"x": 1, "y": 2}`)
require.NoError(t, err)
assert.Equal(t, "42", result.Content)
assert.False(t, result.IsError)
},
)
t.Run(
"passes received params to function",
func(t *testing.T) {
t.Parallel()
type Params struct {
City string `json:"city"`
}
var received string
tool := agent.FunctionTool(
"weather",
"Get weather",
func(_ context.Context, p Params) (agent.ToolResult, error) {
received = p.City
return agent.ToolResult{Content: "sunny"}, nil
},
)
_, err := tool.Execute(context.Background(), `{"city":"Paris"}`)
require.NoError(t, err)
assert.Equal(t, "Paris", received)
},
)
t.Run(
"invalid JSON returns tool error not Go error",
func(t *testing.T) {
t.Parallel()
type Params struct{}
tool := agent.FunctionTool(
"noop",
"No-op",
func(_ context.Context, _ Params) (agent.ToolResult, error) {
return agent.ToolResult{Content: "ok"}, nil
},
)
result, err := tool.Execute(context.Background(), `{invalid`)
require.NoError(t, err)
assert.True(t, result.IsError)
assert.Contains(t, result.Content, "Invalid parameters")
},
)
t.Run(
"infrastructure error propagated as Go error",
func(t *testing.T) {
t.Parallel()
type Params struct{}
tool := agent.FunctionTool(
"fail",
"Always fails",
func(_ context.Context, _ Params) (agent.ToolResult, error) {
return agent.ToolResult{}, errors.New("db down")
},
)
_, err := tool.Execute(context.Background(), `{}`)
require.Error(t, err)
assert.Contains(t, err.Error(), "db down")
},
)
t.Run(
"context is forwarded to function",
func(t *testing.T) {
t.Parallel()
type ctxKey struct{}
type Params struct{}
tool := agent.FunctionTool(
"ctx_check",
"Check context",
func(ctx context.Context, _ Params) (agent.ToolResult, error) {
val := ctx.Value(ctxKey{}).(string)
return agent.ToolResult{Content: val}, nil
},
)
ctx := context.WithValue(context.Background(), ctxKey{}, "hello")
result, err := tool.Execute(ctx, `{}`)
require.NoError(t, err)
assert.Equal(t, "hello", result.Content)
},
)
t.Run(
"missing single required field returns tool error",
func(t *testing.T) {
t.Parallel()
type Params struct {
City string `json:"city"`
}
tool := agent.FunctionTool(
"weather",
"Get weather",
func(_ context.Context, _ Params) (agent.ToolResult, error) {
t.Fatal("function should not be called")
return agent.ToolResult{}, nil
},
)
result, err := tool.Execute(context.Background(), `{}`)
require.NoError(t, err)
assert.True(t, result.IsError)
assert.Contains(t, result.Content, "city")
},
)
t.Run(
"missing multiple required fields lists all of them",
func(t *testing.T) {
t.Parallel()
type Params struct {
City string `json:"city"`
Country string `json:"country"`
}
tool := agent.FunctionTool(
"weather",
"Get weather",
func(_ context.Context, _ Params) (agent.ToolResult, error) {
t.Fatal("function should not be called")
return agent.ToolResult{}, nil
},
)
result, err := tool.Execute(context.Background(), `{}`)
require.NoError(t, err)
assert.True(t, result.IsError)
assert.Contains(t, result.Content, "city")
assert.Contains(t, result.Content, "country")
},
)
t.Run(
"partially missing required fields detected",
func(t *testing.T) {
t.Parallel()
type Params struct {
City string `json:"city"`
Country string `json:"country"`
}
tool := agent.FunctionTool(
"weather",
"Get weather",
func(_ context.Context, _ Params) (agent.ToolResult, error) {
t.Fatal("function should not be called")
return agent.ToolResult{}, nil
},
)
result, err := tool.Execute(context.Background(), `{"city":"Paris"}`)
require.NoError(t, err)
assert.True(t, result.IsError)
assert.Contains(t, result.Content, "country")
},
)
t.Run(
"optional fields may be empty but must be present",
func(t *testing.T) {
t.Parallel()
type Params struct {
City string `json:"city"`
Units *string `json:"units,omitempty"`
}
tool := agent.FunctionTool(
"weather",
"Get weather",
func(_ context.Context, p Params) (agent.ToolResult, error) {
return agent.ToolResult{Content: "sunny in " + p.City}, nil
},
)
result, err := tool.Execute(context.Background(), `{"city":"Paris","units":""}`)
require.NoError(t, err)
assert.False(t, result.IsError)
assert.Equal(t, "sunny in Paris", result.Content)
},
)
t.Run(
"extra JSON fields are ignored",
func(t *testing.T) {
t.Parallel()
type Params struct {
Name string `json:"name"`
}
tool := agent.FunctionTool(
"greet",
"Greet",
func(_ context.Context, p Params) (agent.ToolResult, error) {
return agent.ToolResult{Content: "hi " + p.Name}, nil
},
)
result, err := tool.Execute(context.Background(), `{"name":"Alice","extra":"ignored"}`)
require.NoError(t, err)
assert.Equal(t, "hi Alice", result.Content)
assert.False(t, result.IsError)
},
)
t.Run(
"empty JSON object works for empty params",
func(t *testing.T) {
t.Parallel()
type Params struct{}
tool := agent.FunctionTool(
"ping",
"Ping",
func(_ context.Context, _ Params) (agent.ToolResult, error) {
return agent.ToolResult{Content: "pong"}, nil
},
)
result, err := tool.Execute(context.Background(), `{}`)
require.NoError(t, err)
assert.Equal(t, "pong", result.Content)
},
)
t.Run(
"function can return IsError true",
func(t *testing.T) {
t.Parallel()
type Params struct{}
tool := agent.FunctionTool(
"validate",
"Validate input",
func(_ context.Context, _ Params) (agent.ToolResult, error) {
return agent.ToolResult{Content: "validation failed", IsError: true}, nil
},
)
result, err := tool.Execute(context.Background(), `{}`)
require.NoError(t, err)
assert.True(t, result.IsError)
assert.Equal(t, "validation failed", result.Content)
},
)
}
func TestFunctionTool_InterfaceSatisfaction(t *testing.T) {
t.Parallel()
type Params struct{}
tool := agent.FunctionTool(
"test",
"test tool",
func(_ context.Context, _ Params) (agent.ToolResult, error) {
return agent.ToolResult{}, nil
},
)
assert.Implements(t, (*agent.Tool)(nil), tool)
assert.Implements(t, (*agent.ToolDescriptor)(nil), tool)
}