| package toolcall |
|
|
| import ( |
| "reflect" |
| "testing" |
| ) |
|
|
| func TestNormalizeParsedToolCallsForSchemasCoercesDeclaredStringFieldsRecursively(t *testing.T) { |
| toolsRaw := []any{ |
| map[string]any{ |
| "type": "function", |
| "function": map[string]any{ |
| "name": "TaskUpdate", |
| "parameters": map[string]any{ |
| "type": "object", |
| "properties": map[string]any{ |
| "taskId": map[string]any{"type": "string"}, |
| "payload": map[string]any{ |
| "type": "object", |
| "properties": map[string]any{ |
| "content": map[string]any{"type": "string"}, |
| "tags": map[string]any{ |
| "type": "array", |
| "items": map[string]any{"type": "string"}, |
| }, |
| "count": map[string]any{"type": "number"}, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| } |
| calls := []ParsedToolCall{{ |
| Name: "TaskUpdate", |
| Input: map[string]any{ |
| "taskId": 1, |
| "payload": map[string]any{ |
| "content": map[string]any{"text": "hello"}, |
| "tags": []any{1, true, map[string]any{"k": "v"}}, |
| "count": 2, |
| }, |
| }, |
| }} |
|
|
| got := NormalizeParsedToolCallsForSchemas(calls, toolsRaw) |
| if len(got) != 1 { |
| t.Fatalf("expected one normalized call, got %#v", got) |
| } |
| if got[0].Input["taskId"] != "1" { |
| t.Fatalf("expected taskId coerced to string, got %#v", got[0].Input["taskId"]) |
| } |
| payload, ok := got[0].Input["payload"].(map[string]any) |
| if !ok { |
| t.Fatalf("expected payload object, got %#v", got[0].Input["payload"]) |
| } |
| if payload["content"] != `{"text":"hello"}` { |
| t.Fatalf("expected nested content coerced to json string, got %#v", payload["content"]) |
| } |
| if payload["count"] != 2 { |
| t.Fatalf("expected non-string count unchanged, got %#v", payload["count"]) |
| } |
| tags, ok := payload["tags"].([]any) |
| if !ok { |
| t.Fatalf("expected tags slice, got %#v", payload["tags"]) |
| } |
| wantTags := []any{"1", "true", `{"k":"v"}`} |
| if !reflect.DeepEqual(tags, wantTags) { |
| t.Fatalf("unexpected normalized tags: got %#v want %#v", tags, wantTags) |
| } |
| } |
|
|
| func TestNormalizeParsedToolCallsForSchemasSupportsDirectToolSchemaShape(t *testing.T) { |
| toolsRaw := []any{ |
| map[string]any{ |
| "name": "Write", |
| "input_schema": map[string]any{ |
| "type": "object", |
| "properties": map[string]any{ |
| "content": map[string]any{"type": "string"}, |
| }, |
| }, |
| }, |
| } |
| calls := []ParsedToolCall{{Name: "Write", Input: map[string]any{"content": []any{"a", 1}}}} |
| got := NormalizeParsedToolCallsForSchemas(calls, toolsRaw) |
| if got[0].Input["content"] != `["a",1]` { |
| t.Fatalf("expected direct-schema content coerced to string, got %#v", got[0].Input["content"]) |
| } |
| } |
|
|
| func TestNormalizeParsedToolCallsForSchemasLeavesAmbiguousUnionUnchanged(t *testing.T) { |
| toolsRaw := []any{ |
| map[string]any{ |
| "type": "function", |
| "function": map[string]any{ |
| "name": "TaskUpdate", |
| "parameters": map[string]any{ |
| "type": "object", |
| "properties": map[string]any{ |
| "taskId": map[string]any{"type": []any{"string", "integer"}}, |
| }, |
| }, |
| }, |
| }, |
| } |
| calls := []ParsedToolCall{{Name: "TaskUpdate", Input: map[string]any{"taskId": 1}}} |
| got := NormalizeParsedToolCallsForSchemas(calls, toolsRaw) |
| if got[0].Input["taskId"] != 1 { |
| t.Fatalf("expected ambiguous union to stay unchanged, got %#v", got[0].Input["taskId"]) |
| } |
| } |
|
|
| func TestNormalizeParsedToolCallsForSchemasSupportsCamelCaseInputSchema(t *testing.T) { |
| toolsRaw := []any{ |
| map[string]any{ |
| "name": "Write", |
| "inputSchema": map[string]any{ |
| "type": "object", |
| "properties": map[string]any{ |
| "content": map[string]any{"type": "string"}, |
| }, |
| }, |
| }, |
| } |
| calls := []ParsedToolCall{{Name: "Write", Input: map[string]any{"content": map[string]any{"message": "hi"}}}} |
| got := NormalizeParsedToolCallsForSchemas(calls, toolsRaw) |
| if got[0].Input["content"] != `{"message":"hi"}` { |
| t.Fatalf("expected camelCase inputSchema content coercion, got %#v", got[0].Input["content"]) |
| } |
| } |
|
|
| func TestNormalizeParsedToolCallsForSchemasPreservesArrayWhenSchemaSaysArray(t *testing.T) { |
| toolsRaw := []any{ |
| map[string]any{ |
| "name": "todowrite", |
| "inputSchema": map[string]any{ |
| "type": "object", |
| "properties": map[string]any{ |
| "todos": map[string]any{ |
| "type": "array", |
| "items": map[string]any{ |
| "type": "object", |
| "properties": map[string]any{ |
| "content": map[string]any{"type": "string"}, |
| "status": map[string]any{"type": "string"}, |
| "priority": map[string]any{"type": "string"}, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| } |
| todos := []any{map[string]any{"content": "x", "status": "pending", "priority": "high"}} |
| calls := []ParsedToolCall{{Name: "todowrite", Input: map[string]any{"todos": todos}}} |
| got := NormalizeParsedToolCallsForSchemas(calls, toolsRaw) |
| if !reflect.DeepEqual(got[0].Input["todos"], todos) { |
| t.Fatalf("expected todos array preserved, got %#v want %#v", got[0].Input["todos"], todos) |
| } |
| } |
|
|