Files
go-llm/v2/tool_coerce_test.go
T
steve 5c5d861915
CI / Root Module (push) Failing after 30s
CI / Lint (push) Failing after 3s
CI / V2 Module (push) Successful in 1m54s
fix(v2): coerce string-encoded numbers/bools in tool arguments
LLMs occasionally return numeric or boolean tool-call fields as JSON
strings (e.g. "3" instead of 3, "true" instead of true), which Go's
strict json.Unmarshal rejects. The strict unmarshal stays as the happy
path; on failure we retry with a coercion pass that walks the target
struct (recursing into nested structs, slices, maps, and pointer fields)
and converts strings to the appropriate kind. Returns the original error
if coercion can't recover.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:12:56 +00:00

131 lines
3.5 KiB
Go

package llm
import (
"context"
"testing"
)
func TestExecuteCoercesStringNumbers(t *testing.T) {
type params struct {
Memory string `json:"memory"`
ReplaceMemoryID *uint `json:"replace_memory_id,omitempty"`
RelationshipChange int `json:"relationship_change"`
}
var got params
tool := Define("process", "test",
func(ctx context.Context, p params) (string, error) {
got = p
return "ok", nil
},
)
cases := []struct {
name string
args string
wantInt int
wantUint uint
}{
{"int as string", `{"memory":"x","relationship_change":"3"}`, 3, 0},
{"int as string with plus", `{"memory":"x","relationship_change":"+3"}`, 3, 0},
{"int as string negative", `{"memory":"x","relationship_change":"-2"}`, -2, 0},
{"int as string with whitespace", `{"memory":"x","relationship_change":" 4 "}`, 4, 0},
{"int as string with decimal", `{"memory":"x","relationship_change":"2.7"}`, 2, 0},
{"native int still works", `{"memory":"x","relationship_change":5}`, 5, 0},
{"pointer uint as string", `{"memory":"x","replace_memory_id":"42","relationship_change":0}`, 0, 42},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got = params{}
result, err := tool.Execute(context.Background(), tc.args)
if err != nil {
t.Fatalf("execute failed: %v", err)
}
if result != "ok" {
t.Errorf("expected 'ok', got %q", result)
}
if got.RelationshipChange != tc.wantInt {
t.Errorf("RelationshipChange: want %d, got %d", tc.wantInt, got.RelationshipChange)
}
if tc.wantUint != 0 {
if got.ReplaceMemoryID == nil || *got.ReplaceMemoryID != tc.wantUint {
t.Errorf("ReplaceMemoryID: want %d, got %v", tc.wantUint, got.ReplaceMemoryID)
}
}
})
}
}
func TestExecuteCoercesStringBoolAndFloat(t *testing.T) {
type params struct {
Enabled bool `json:"enabled"`
Ratio float64 `json:"ratio"`
}
var got params
tool := Define("cfg", "test",
func(ctx context.Context, p params) (string, error) {
got = p
return "ok", nil
},
)
if _, err := tool.Execute(context.Background(), `{"enabled":"true","ratio":"0.5"}`); err != nil {
t.Fatalf("execute failed: %v", err)
}
if !got.Enabled {
t.Errorf("expected enabled=true, got false")
}
if got.Ratio != 0.5 {
t.Errorf("expected ratio=0.5, got %v", got.Ratio)
}
}
func TestExecuteCoercesNestedAndSlices(t *testing.T) {
type inner struct {
N int `json:"n"`
}
type params struct {
Items []inner `json:"items"`
Tags []int `json:"tags"`
}
var got params
tool := Define("nest", "test",
func(ctx context.Context, p params) (string, error) {
got = p
return "ok", nil
},
)
args := `{"items":[{"n":"1"},{"n":"2"}],"tags":["10","20"]}`
if _, err := tool.Execute(context.Background(), args); err != nil {
t.Fatalf("execute failed: %v", err)
}
if len(got.Items) != 2 || got.Items[0].N != 1 || got.Items[1].N != 2 {
t.Errorf("nested struct coercion failed: %+v", got.Items)
}
if len(got.Tags) != 2 || got.Tags[0] != 10 || got.Tags[1] != 20 {
t.Errorf("slice element coercion failed: %+v", got.Tags)
}
}
func TestExecuteUnrecoverableArgsErrors(t *testing.T) {
type params struct {
N int `json:"n"`
}
tool := Define("bad", "test",
func(ctx context.Context, p params) (string, error) {
return "ok", nil
},
)
if _, err := tool.Execute(context.Background(), `{"n":"not-a-number"}`); err == nil {
t.Errorf("expected error for unparseable string")
}
if _, err := tool.Execute(context.Background(), `{not json`); err == nil {
t.Errorf("expected error for malformed JSON")
}
}