Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 139 additions & 1 deletion core/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func TestE2E_Basic(t *testing.T) {
// The Go SDK performs validation inside Invoke, so we check the error there.
_, err := tool.Invoke(context.Background(), map[string]any{})
require.Error(t, err)
assert.Contains(t, err.Error(), "parameter \"num_rows\" is required")
assert.Contains(t, err.Error(), "missing required parameter 'num_rows'")
})

t.Run("test_run_tool_wrong_param_type", func(t *testing.T) {
Expand Down Expand Up @@ -335,3 +335,141 @@ func TestE2E_Auth(t *testing.T) {
assert.Contains(t, err.Error(), "no field named row_data in claims")
})
}

// TestE2E_OptionalParams maps to the TestOptionalParams class
func TestE2E_OptionalParams(t *testing.T) {
// Helper to create a new client
newClient := func(t *testing.T) *core.ToolboxClient {
client, err := core.NewToolboxClient("http://localhost:5000")
require.NoError(t, err, "Failed to create ToolboxClient")
return client
}

// Helper to load the search-rows tool
searchRowsTool := func(t *testing.T, client *core.ToolboxClient) *core.ToolboxTool {
tool, err := client.LoadTool("search-rows", context.Background())
require.NoError(t, err, "Failed to load tool 'search-rows'")
return tool
}

t.Run("test_tool_schema_is_correct", func(t *testing.T) {
client := newClient(t)
tool := searchRowsTool(t, client)
params := tool.Parameters()

// Convert slice to map for easy lookup
paramMap := make(map[string]core.ParameterSchema)
for _, p := range params {
paramMap[p.Name] = p
}

// Check required parameter 'email'
emailParam, ok := paramMap["email"]
require.True(t, ok, "email parameter should exist")
assert.True(t, emailParam.Required, "'email' should be required")
assert.Equal(t, "string", emailParam.Type)

// Check optional parameter 'data'
dataParam, ok := paramMap["data"]
require.True(t, ok, "data parameter should exist")
assert.False(t, dataParam.Required, "'data' should be optional")
assert.Equal(t, "string", dataParam.Type)

// Check optional parameter 'id'
idParam, ok := paramMap["id"]
require.True(t, ok, "id parameter should exist")
assert.False(t, idParam.Required, "'id' should be optional")
assert.Equal(t, "integer", idParam.Type)
})

t.Run("test_run_tool_omitting_optionals", func(t *testing.T) {
client := newClient(t)
tool := searchRowsTool(t, client)

// Test case 1: Optional params are completely omitted
response1, err1 := tool.Invoke(context.Background(), map[string]any{
"email": "[email protected]",
})
require.NoError(t, err1)
respStr1, ok1 := response1.(string)
require.True(t, ok1)
assert.Contains(t, respStr1, `"email":"[email protected]"`)
assert.Contains(t, respStr1, "row2")
assert.NotContains(t, respStr1, "row3")

// Test case 2: Optional params are explicitly nil
// This should produce the same result as omitting them
response2, err2 := tool.Invoke(context.Background(), map[string]any{
"email": "[email protected]",
"data": nil,
"id": nil,
})
require.NoError(t, err2)
respStr2, ok2 := response2.(string)
require.True(t, ok2)
assert.Equal(t, respStr1, respStr2)
})

t.Run("test_run_tool_with_all_params_provided", func(t *testing.T) {
client := newClient(t)
tool := searchRowsTool(t, client)
response, err := tool.Invoke(context.Background(), map[string]any{
"email": "[email protected]",
"data": "row3",
"id": 3,
})
require.NoError(t, err)
respStr, ok := response.(string)
require.True(t, ok)
assert.Contains(t, respStr, `"email":"[email protected]"`)
assert.Contains(t, respStr, `"id":3`)
assert.Contains(t, respStr, "row3")
assert.NotContains(t, respStr, "row2")
})

t.Run("test_run_tool_missing_required_param", func(t *testing.T) {
client := newClient(t)
tool := searchRowsTool(t, client)
_, err := tool.Invoke(context.Background(), map[string]any{
"data": "row5",
"id": 5,
})
require.Error(t, err)
assert.Contains(t, err.Error(), "missing required parameter 'email'")
})

t.Run("test_run_tool_required_param_is_nil", func(t *testing.T) {
client := newClient(t)
tool := searchRowsTool(t, client)
_, err := tool.Invoke(context.Background(), map[string]any{
"email": nil,
"id": 5,
})
require.Error(t, err)
assert.Contains(t, err.Error(), "parameter 'email' is required but received a nil value")
})

// Corresponds to tests that check server-side logic by providing data that doesn't match
t.Run("test_run_tool_with_non_matching_data", func(t *testing.T) {
client := newClient(t)
tool := searchRowsTool(t, client)

// Test with a different email
response, err := tool.Invoke(context.Background(), map[string]any{
"email": "[email protected]",
"id": 3,
"data": "row3",
})
require.NoError(t, err)
assert.Equal(t, "null", response, "Response should be null for non-matching email")

// Test with different data
response, err = tool.Invoke(context.Background(), map[string]any{
"email": "[email protected]",
"id": 3,
"data": "row4", // This data doesn't match the id
})
require.NoError(t, err)
assert.Equal(t, "null", response, "Response should be null for non-matching data")
})
}
6 changes: 5 additions & 1 deletion core/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
type ParameterSchema struct {
Name string `json:"name"`
Type string `json:"type"`
Required bool `json:"required,omitempty"`
Description string `json:"description"`
AuthSources []string `json:"authSources,omitempty"`
Items *ParameterSchema `json:"items,omitempty"`
Expand All @@ -31,7 +32,10 @@ type ParameterSchema struct {
// validateType is a helper for manual type checking.
func (p *ParameterSchema) validateType(value any) error {
if value == nil {
return fmt.Errorf("parameter '%s' received a nil value", p.Name)
if p.Required {
return fmt.Errorf("parameter '%s' is required but received a nil value", p.Name)
}
return nil
}

switch p.Type {
Expand Down
79 changes: 79 additions & 0 deletions core/protocol_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,3 +252,82 @@ func TestParameterSchemaUndefinedType(t *testing.T) {
}

}

func TestOptionalStringParameter(t *testing.T) {
schema := ParameterSchema{
Name: "nickname",
Type: "string",
Description: "An optional nickname",
Required: false, // Explicitly optional
}

t.Run("allows nil value for optional parameter", func(t *testing.T) {
err := schema.validateType(nil)
if err != nil {
t.Errorf("validateType() with nil should not return an error for an optional parameter, but got: %v", err)
}
})

t.Run("allows valid string value", func(t *testing.T) {
err := schema.validateType("my-name")
if err != nil {
t.Errorf("validateType() should not return an error for a valid string, but got: %v", err)
}
})
}

func TestRequiredParameter(t *testing.T) {
schema := ParameterSchema{
Name: "id",
Type: "integer",
Description: "A required ID",
Required: true, // Explicitly required
}

t.Run("rejects nil value for required parameter", func(t *testing.T) {
err := schema.validateType(nil)
if err == nil {
t.Errorf("validateType() with nil should return an error for a required parameter, but it didn't")
}
})

t.Run("allows valid integer value", func(t *testing.T) {
err := schema.validateType(12345)
if err != nil {
t.Errorf("validateType() should not return an error for a valid integer, but got: %v", err)
}
})
}

func TestOptionalArrayParameter(t *testing.T) {
schema := ParameterSchema{
Name: "optional_scores",
Type: "array",
Description: "An optional list of scores",
Required: false,
Items: &ParameterSchema{
Type: "integer",
},
}

t.Run("allows nil value for optional array", func(t *testing.T) {
err := schema.validateType(nil)
if err != nil {
t.Errorf("validateType() with nil should not return an error for an optional array, but got: %v", err)
}
})

t.Run("allows valid integer slice", func(t *testing.T) {
err := schema.validateType([]int{95, 100})
if err != nil {
t.Errorf("validateType() should not return an error for a valid slice, but got: %v", err)
}
})

t.Run("rejects slice with wrong item type", func(t *testing.T) {
err := schema.validateType([]string{"not", "an", "int"})
if err == nil {
t.Errorf("validateType() should have returned an error for a slice with incorrect item types, but it didn't")
}
})
}
14 changes: 13 additions & 1 deletion core/tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,10 +343,22 @@ func (tt *ToolboxTool) validateAndBuildPayload(input map[string]any) (map[string
}
}

for _, param := range tt.parameters {
if param.Required {
// A required parameter must be present in either the user input or as a bound parameter.
_, isProvided := input[param.Name]
_, isBound := tt.boundParams[param.Name]

if !isProvided && !isBound {
return nil, fmt.Errorf("missing required parameter '%s'", param.Name)
}
}
}

// Initialize the final payload with the validated user input.
finalPayload := make(map[string]any, len(input)+len(tt.boundParams))
for k, v := range input {
if _, ok := paramSchema[k]; ok {
if _, ok := paramSchema[k]; ok && v != nil {
finalPayload[k] = v
}
}
Expand Down
Loading