internal/util/sanitize_test.go
Rama main · réplica de lectura del repositorio original.
package util
import (
"testing"
"github.com/MoeForge/moego/ojson"
)
func TestSanitizeClaudeFunctionName(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"Normal", "valid_name", "valid_name"},
{"With Dots", "name.with.dots", "name_with_dots"},
{"With Colons", "name:with:colons", "name_with_colons"},
{"With Dashes", "name-with-dashes", "name-with-dashes"},
{"MCP Tool with Dots and Colons", "mcp.server.special:get_time", "mcp_server_special_get_time"},
{"With Slashes", "server/action", "server_action"},
{"Invalid Characters", "name!with@invalid#chars", "name_with_invalid_chars"},
{"Spaces", "name with spaces", "name_with_spaces"},
{"Non-ASCII", "name_with_你好_chars", "name_with____chars"},
{"Empty", "", ""},
{"Single character valid", "a", "a"},
{"Single character invalid", "@", "_"},
{"Starts with digit", "123name", "123name"},
{"Starts with dash", "-name", "-name"},
{"Exactly 64 chars", "this_is_a_very_long_name_that_exactly_reaches_sixty_four_charact", "this_is_a_very_long_name_that_exactly_reaches_sixty_four_charact"},
{"Too long (65 chars)", "this_is_a_very_long_name_that_exactly_reaches_sixty_four_charactX", "this_is_a_very_long_name_that_exactly_reaches_sixty_four_charact"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := SanitizeClaudeFunctionName(tt.input)
if got != tt.expected {
t.Errorf("SanitizeClaudeFunctionName(%q) = %q, want %q", tt.input, got, tt.expected)
}
if len(got) > 64 {
t.Errorf("SanitizeClaudeFunctionName(%q) result too long: %d", tt.input, len(got))
}
})
}
}
func TestSanitizeFunctionName(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"Normal", "valid_name", "valid_name"},
{"With Dots", "name.with.dots", "name.with.dots"},
{"With Colons", "name:with:colons", "name:with:colons"},
{"With Dashes", "name-with-dashes", "name-with-dashes"},
{"Mixed Allowed", "name.with_dots:colons-dashes", "name.with_dots:colons-dashes"},
{"Invalid Characters", "name!with@invalid#chars", "name_with_invalid_chars"},
{"Spaces", "name with spaces", "name_with_spaces"},
{"Non-ASCII", "name_with_你好_chars", "name_with____chars"},
{"Starts with digit", "123name", "_123name"},
{"Starts with dot", ".name", "_.name"},
{"Starts with colon", ":name", "_:name"},
{"Starts with dash", "-name", "_-name"},
{"Starts with invalid char", "!name", "_name"},
{"Exactly 64 chars", "this_is_a_very_long_name_that_exactly_reaches_sixty_four_charact", "this_is_a_very_long_name_that_exactly_reaches_sixty_four_charact"},
{"Too long (65 chars)", "this_is_a_very_long_name_that_exactly_reaches_sixty_four_charactX", "this_is_a_very_long_name_that_exactly_reaches_sixty_four_charact"},
{"Very long", "this_is_a_very_long_name_that_exceeds_the_sixty_four_character_limit_for_function_names", "this_is_a_very_long_name_that_exceeds_the_sixty_four_character_l"},
{"Starts with digit (64 chars total)", "1234567890123456789012345678901234567890123456789012345678901234", "_123456789012345678901234567890123456789012345678901234567890123"},
{"Starts with invalid char (64 chars total)", "!234567890123456789012345678901234567890123456789012345678901234", "_234567890123456789012345678901234567890123456789012345678901234"},
{"Empty", "", ""},
{"Single character invalid", "@", "_"},
{"Single character valid", "a", "a"},
{"Single character digit", "1", "_1"},
{"Single character underscore", "_", "_"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := SanitizeFunctionName(tt.input)
if got != tt.expected {
t.Errorf("SanitizeFunctionName(%q) = %v, want %v", tt.input, got, tt.expected)
}
// Verify Gemini compliance
if len(got) > 64 {
t.Errorf("SanitizeFunctionName(%q) result too long: %d", tt.input, len(got))
}
if len(got) > 0 {
first := got[0]
if !((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z') || first == '_') {
t.Errorf("SanitizeFunctionName(%q) result starts with invalid char: %c", tt.input, first)
}
}
})
}
}
func TestSanitizedToolNameMap(t *testing.T) {
t.Run("returns map for tools needing sanitization", func(t *testing.T) {
raw := []byte(`{"tools":[
{"name":"valid_tool","input_schema":{}},
{"name":"mcp/server/read","input_schema":{}},
{"name":"tool@v2","input_schema":{}}
]}`)
m := SanitizedToolNameMap(raw)
if m == nil {
t.Fatal("expected non-nil map")
}
if m["mcp_server_read"] != "mcp/server/read" {
t.Errorf("expected mcp_server_read → mcp/server/read, got %q", m["mcp_server_read"])
}
if m["tool_v2"] != "tool@v2" {
t.Errorf("expected tool_v2 → tool@v2, got %q", m["tool_v2"])
}
if _, exists := m["valid_tool"]; exists {
t.Error("valid_tool should not be in the map (no sanitization needed)")
}
})
t.Run("returns nil when no tools need sanitization", func(t *testing.T) {
raw := []byte(`{"tools":[{"name":"Read","input_schema":{}},{"name":"Write","input_schema":{}}]}`)
m := SanitizedToolNameMap(raw)
if m != nil {
t.Errorf("expected nil, got %v", m)
}
})
t.Run("returns nil for empty/missing tools", func(t *testing.T) {
if m := SanitizedToolNameMap([]byte(`{}`)); m != nil {
t.Error("expected nil for no tools")
}
if m := SanitizedToolNameMap(nil); m != nil {
t.Error("expected nil for nil input")
}
})
t.Run("legacy map ignores nested OpenAI tools", func(t *testing.T) {
raw := []byte(`{"tools":[
{"type":"function","function":{"name":"web/search"}},
{"type":"web_search","name":"web_search"}
]}`)
if m := SanitizedToolNameMap(raw); m != nil {
t.Fatalf("legacy map = %v, want nil", m)
}
})
t.Run("collision keeps first legacy mapping", func(t *testing.T) {
raw := []byte(`{"tools":[
{"name":"read/file","input_schema":{}},
{"name":"read@file","input_schema":{}}
]}`)
m := SanitizedToolNameMap(raw)
if m == nil {
t.Fatal("expected non-nil map")
}
if got := m["read_file"]; got != "read/file" {
t.Errorf("legacy collision mapping = %q, want read/file", got)
}
})
}
func TestSanitizedFunctionNameMapDisambiguatesCollisions(t *testing.T) {
first := "mcp__plugin_cloudflare_cloudflare-builds__workers_builds_get_build"
second := "mcp__plugin_cloudflare_cloudflare-builds__workers_builds_get_build_logs"
raw := []byte(`{"tools":[
{"name":"` + first + `"},
{"name":"` + first + `"},
{"name":"` + second + `"}
]}`)
forward := SanitizedFunctionNameMap(raw)
firstMapped := forward[first]
secondMapped := forward[second]
if firstMapped == "" || secondMapped == "" || secondMapped == firstMapped {
t.Fatalf("mapped names = %q and %q, want distinct non-empty names", firstMapped, secondMapped)
}
if len(firstMapped) > 64 || len(secondMapped) > 64 {
t.Fatalf("mapped name lengths = %d and %d, want <= 64", len(firstMapped), len(secondMapped))
}
reversed := []byte(`{"tools":[{"name":"` + second + `"},{"name":"` + first + `"}]}`)
reversedForward := SanitizedFunctionNameMap(reversed)
if reversedForward[first] != firstMapped || reversedForward[second] != secondMapped {
t.Fatalf("mapping changed with declaration order: forward=%v reversed=%v", forward, reversedForward)
}
reverse := DisambiguatedToolNameMap(raw)
if got := reverse[firstMapped]; got != first {
t.Fatalf("reverse[%q] = %q, want %q", firstMapped, got, first)
}
if got := reverse[secondMapped]; got != second {
t.Fatalf("reverse[%q] = %q, want %q", secondMapped, got, second)
}
}
func TestSanitizedFunctionNameMapReadsSupportedToolShapes(t *testing.T) {
raw := []byte(`{"tools":[
{"type":"function","function":{"name":"nested/name"}},
{
"functionDeclarations":[{"name":"camel@name"}],
"function_declarations":[{"name":"snake name"}]
}
]}`)
forward := SanitizedFunctionNameMap(raw)
for original, want := range map[string]string{
"nested/name": "nested_name",
"camel@name": "camel_name",
"snake name": "snake_name",
} {
if got := forward[original]; got != want {
t.Errorf("forward[%q] = %q, want %q", original, got, want)
}
}
}
func TestDeduplicateFunctionDeclarations(t *testing.T) {
raw := []byte(`[
{"name":"lookup","description":"first"},
{"name":"other"},
{"name":"lookup","description":"second"}
]`)
deduped := DeduplicateFunctionDeclarations(raw)
declarations := ojson.ParseBytes(deduped).Array()
if len(declarations) != 2 {
t.Fatalf("declaration count = %d, want 2: %s", len(declarations), deduped)
}
if got := declarations[0].Get("description").String(); got != "first" {
t.Fatalf("first duplicate description = %q, want first", got)
}
if got := declarations[1].Get("name").String(); got != "other" {
t.Fatalf("second declaration name = %q, want other", got)
}
}
func TestRestoreSanitizedToolName(t *testing.T) {
m := map[string]string{
"mcp_server_read": "mcp/server/read",
"tool_v2": "tool@v2",
}
if got := RestoreSanitizedToolName(m, "mcp_server_read"); got != "mcp/server/read" {
t.Errorf("expected mcp/server/read, got %q", got)
}
if got := RestoreSanitizedToolName(m, "unknown"); got != "unknown" {
t.Errorf("expected passthrough for unknown, got %q", got)
}
if got := RestoreSanitizedToolName(nil, "name"); got != "name" {
t.Errorf("expected passthrough for nil map, got %q", got)
}
if got := RestoreSanitizedToolName(m, ""); got != "" {
t.Errorf("expected empty for empty name, got %q", got)
}
}
func TestCleanClaudeToolArguments(t *testing.T) {
origRequest := []byte(`{
"tools": [
{"name": "EnterPlanMode", "input_schema": {"type": "object", "properties": {}}},
{"name": "CronList", "input_schema": {"type": "object"}},
{"name": "Bash", "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
{"name": "CustomToolWithReason", "input_schema": {"type": "object", "properties": {"reason": {"type": "string"}}}},
{"name": "DynamicTool", "input_schema": {"type": "object", "properties": {"name": {"type": "string"}}, "additionalProperties": true}},
{"name": "UnionTool", "input_schema": {"anyOf": [{"type": "object", "properties": {"reason": {"type": "string"}}}]}}
]
}`)
dynamicRequest := []byte(`{
"tools": [
{"name": "MapTool", "input_schema": {"type": "object", "additionalProperties": true}},
{"name": "TypedMapTool", "input_schema": {"type": "object", "properties": {}, "additionalProperties": {"type": "string"}}}
]
}`)
caseCollisionRequest := []byte(`{
"tools": [
{"name": "Read", "input_schema": {"type": "object", "properties": {}}},
{"name": "read", "input_schema": {"type": "object", "properties": {"path": {"type": "string"}}}}
]
}`)
composedRequest := []byte(`{
"tools": [
{"name": "AllOfEmpty", "input_schema": {"type": "object", "allOf": [{"type": "object", "properties": {}}]}},
{"name": "AllOfArgs", "input_schema": {"type": "object", "allOf": [{"type": "object", "properties": {"path": {"type": "string"}}}]}},
{"name": "RefEmpty", "input_schema": {"$ref": "#/$defs/empty", "$defs": {"empty": {"type": "object", "properties": {}}}}},
{"name": "NestedTool", "input_schema": {"type": "object", "properties": {"opts": {"type": "object", "properties": {}}, "cfg": {"type": "object", "properties": {"mode": {"type": "string"}}}}}}
]
}`)
transferredRequest := []byte(`{
"model": "gemini-3.8-flash-high",
"request": {"tools": [{"functionDeclarations": [{"name": "Bash", "parametersJsonSchema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}}]}]}
}`)
translatedReasonRequired := []byte(`{
"model": "gemini-3.8-flash-high",
"request": {"tools": [{"functionDeclarations": [{"name": "Bash", "parametersJsonSchema": {"type": "object", "properties": {"command": {"type": "string"}, "reason": {"type": "string"}}, "required": ["command", "reason"]}}]}]}
}`)
uniqueItemsRequest := []byte(`{
"tools": [
{"name": "BatchUnique", "input_schema": {"type": "object", "properties": {"rows": {"type": "array", "uniqueItems": true, "items": {"type": "object", "properties": {"tag": {"type": "string"}}}}}, "required": ["rows"], "additionalProperties": false}}
]
}`)
translatedUniqueItems := []byte(`{
"model": "claude-sonnet-4-6",
"request": {"tools": [{"functionDeclarations": [
{"name": "BatchUnique", "parametersJsonSchema": {"type": "object", "properties": {"rows": {"type": "array", "items": {"type": "object", "properties": {"tag": {"type": "string"}, "_": {"type": "boolean"}}, "required": ["_"]}}}, "required": ["rows"]}}
]}]}
}`)
minPropsRequest := []byte(`{
"tools": [
{"name": "StoreMetadata", "input_schema": {"type": "object", "properties": {}, "minProperties": 1}}
]
}`)
translatedMinProps := []byte(`{
"model": "claude-sonnet-4-6",
"request": {"tools": [{"functionDeclarations": [
{"name": "StoreMetadata", "parametersJsonSchema": {"type": "object", "properties": {"reason": {"type": "string"}}, "required": ["reason"], "minProperties": 1}}
]}]}
}`)
siblingAllOfRequest := []byte(`{
"tools": [
{"name": "ConfigureSibling", "input_schema": {"type": "object", "required": ["cfg"], "anyOf": [{"type": "object", "properties": {"cfg": {"type": "object", "properties": {}}}, "required": ["cfg"], "additionalProperties": false}, {"type": "object", "properties": {"other": {"type": "string"}}, "required": ["other"], "additionalProperties": false}], "allOf": [{"properties": {"cfg": {"type": "object", "properties": {"reason": {"type": "string"}}, "required": ["reason"]}}}]}}
]
}`)
translatedSiblingAllOf := []byte(`{
"model": "gemini-2.5-pro",
"tools": [{"functionDeclarations": [
{"name": "ConfigureSibling", "parametersJsonSchema": {"type": "object", "properties": {"cfg": {"type": "object", "properties": {"reason": {"type": "string"}}, "required": ["reason"]}}, "required": ["cfg"]}}
]}]
}`)
arrayContainsRequest := []byte(`{
"tools": [
{"name": "BatchContains", "input_schema": {"type": "object", "properties": {"rows": {"type": "array", "items": {"type": "object", "properties": {}}, "contains": {"type": "object", "properties": {"reason": {"type": "string"}}, "required": ["reason"]}}}, "required": ["rows"], "additionalProperties": false}}
]
}`)
translatedArrayContains := []byte(`{
"model": "claude-sonnet-4-6",
"request": {"tools": [{"functionDeclarations": [
{"name": "BatchContains", "parametersJsonSchema": {"type": "object", "properties": {"rows": {"type": "array", "items": {"type": "object", "properties": {"reason": {"type": "string"}}, "required": ["reason"]}}}, "required": ["rows"]}}
]}]}
}`)
arrayEnumRequest := []byte(`{
"tools": [
{"name": "BatchEnum", "input_schema": {"type": "object", "properties": {"rows": {"type": "array", "items": {"type": "object", "properties": {}}, "enum": [[{"reason": "business"}]]}}, "required": ["rows"], "additionalProperties": false}}
]
}`)
translatedArrayEnum := []byte(`{
"model": "claude-sonnet-4-6",
"request": {"tools": [{"functionDeclarations": [
{"name": "BatchEnum", "parametersJsonSchema": {"type": "object", "properties": {"rows": {"type": "array", "items": {"type": "object", "properties": {"reason": {"type": "string"}}, "required": ["reason"]}}}, "required": ["rows"]}}
]}]}
}`)
enumUnionRequest := []byte(`{
"tools": [
{"name": "ExplainFixed", "input_schema": {"type": "object", "enum": [{"reason": "business"}], "anyOf": [{"type": "object", "properties": {}}]}}
]
}`)
enumRequest := []byte(`{
"tools": [
{"name": "ExplainFixed", "input_schema": {"type": "object", "enum": [{"reason": "business"}]}},
{"name": "ConstTool", "input_schema": {"type": "object", "const": {"reason": "fixed"}}}
]
}`)
translatedEnum := []byte(`{
"model": "claude-sonnet-4-6",
"request": {"tools": [{"functionDeclarations": [
{"name": "ExplainFixed", "parametersJsonSchema": {"type": "object", "properties": {"reason": {"type": "string"}}, "required": ["reason"]}},
{"name": "ConstTool", "parametersJsonSchema": {"type": "object", "properties": {"reason": {"type": "string"}}, "required": ["reason"]}}
]}]}
}`)
translatedCollisionDynamic := func() []byte {
collisionSeed := []byte(`{"tools":[{"name":"1"},{"name":"_1"}]}`)
sentName := SanitizedFunctionNameMap(collisionSeed)["1"]
return []byte(`{"model":"claude-sonnet-4-6","request":{"tools":[{"functionDeclarations":[{"name":"` + sentName + `","parametersJsonSchema":{"type":"object","properties":{"reason":{"type":"string"}},"required":["reason"]}}]}]}}`)
}()
collisionRequest := []byte(`{
"tools": [
{"name": "1", "input_schema": {"type": "object", "properties": {}, "required": ["reason"]}},
{"name": "_1", "input_schema": {"type": "object", "properties": {}}}
]
}`)
round15Request := []byte(`{
"tools": [
{"name": "Batch", "input_schema": {"type": "object", "anyOf": [{"type": "object", "properties": {"rows": {"type": "array", "items": {"type": "object", "properties": {}, "additionalProperties": false}}}, "required": ["rows"], "additionalProperties": false}, {"type": "object", "properties": {"rows": {}}, "required": ["rows"], "additionalProperties": false}]}},
{"name": "BatchStrict", "input_schema": {"type": "object", "properties": {"rows": {"type": "array", "items": {"type": "object", "properties": {}, "additionalProperties": false}}}, "required": ["rows"]}}
]
}`)
translatedRound15 := []byte(`{
"model": "gemini-2.5-pro",
"tools": [{"functionDeclarations": [
{"name": "Batch", "parametersJsonSchema": {"type": "object", "anyOf": [{"type": "object", "properties": {"rows": {"type": "array", "items": {"type": "object", "properties": {}}}}, "required": ["rows"]}, {"type": "object", "properties": {"rows": {}}, "required": ["rows"]}]}},
{"name": "BatchStrict", "parametersJsonSchema": {"type": "object", "properties": {"rows": {"type": "array", "items": {"type": "object", "properties": {"reason": {"type": "string"}}, "required": ["reason"]}}}, "required": ["rows"]}}
]}]
}`)
round14Request := []byte(`{
"tools": [
{"name": "ConfigureAltFree", "input_schema": {"type": "object", "anyOf": [{"type": "object", "properties": {"cfg": {"type": "object", "properties": {}, "additionalProperties": false}}, "required": ["cfg"], "additionalProperties": false}, {"type": "object", "properties": {"cfg": {}}, "required": ["cfg"], "additionalProperties": false}]}}
]
}`)
translatedRound14 := []byte(`{
"model": "gemini-2.5-pro",
"tools": [{"functionDeclarations": [
{"name": "ConfigureAltFree", "parametersJsonSchema": {"type": "object", "anyOf": [{"type": "object", "properties": {"cfg": {"type": "object", "properties": {}}}, "required": ["cfg"]}, {"type": "object", "properties": {"cfg": {}}, "required": ["cfg"]}]}}
]}]
}`)
round13Request := []byte(`{
"tools": [
{"name": "DeepAlt", "input_schema": {"type": "object", "properties": {"choice": {"anyOf": [{"type": "object", "properties": {"cfg": {"type": "object", "properties": {"opts": {"type": "object", "properties": {"mode": {"type": "string"}}, "additionalProperties": false}}, "required": ["opts"], "additionalProperties": false}}, "required": ["cfg"], "additionalProperties": false}, {"type": "object", "properties": {"cfg": {"type": "object", "properties": {"tag": {"type": "string"}}, "required": ["tag"]}}, "required": ["cfg"], "additionalProperties": false}]}}, "required": ["choice"], "additionalProperties": false}}
]
}`)
translatedRound13 := []byte(`{
"model": "gemini-3-pro-high",
"request": {"tools": [{"functionDeclarations": [
{"name": "DeepAlt", "parametersJsonSchema": {"type": "object", "properties": {"choice": {"type": "object", "properties": {"cfg": {"type": "object", "properties": {"opts": {"type": "object", "properties": {"mode": {"type": "string"}, "_": {"type": "boolean"}}, "required": ["_"]}}, "required": ["opts"]}}, "required": ["cfg"]}}, "required": ["choice"]}}
]}]}
}`)
round12Request := []byte(`{
"tools": [
{"name": "ConfigureAlt", "input_schema": {"type": "object", "properties": {"choice": {"anyOf": [{"type": "object", "properties": {"cfg": {"type": "object", "properties": {}, "additionalProperties": false}}, "required": ["cfg"], "additionalProperties": false}, {"type": "object", "properties": {"cfg": {"type": "object", "properties": {"mode": {"type": "string"}}, "required": ["mode"]}}, "required": ["cfg"], "additionalProperties": false}]}}, "required": ["choice"], "additionalProperties": false}}
]
}`)
translatedRound12 := []byte(`{
"model": "gemini-3-pro-high",
"request": {"tools": [{"functionDeclarations": [
{"name": "ConfigureAlt", "parametersJsonSchema": {"type": "object", "properties": {"choice": {"anyOf": [{"type": "object", "properties": {"cfg": {"type": "object", "properties": {"reason": {"type": "string"}}, "required": ["reason"]}}, "required": ["cfg"]}, {"type": "object", "properties": {"cfg": {"type": "object", "properties": {"mode": {"type": "string"}}, "required": ["mode"]}}, "required": ["cfg"]}]}}, "required": ["choice"]}}
]}]}
}`)
round11Request := []byte(`{
"tools": [
{"name": "ChooseImplicit", "input_schema": {"type": "object", "properties": {"choice": {"anyOf": [{"type": "object", "properties": {}, "additionalProperties": false}, {"type": "object", "properties": {"tag": {"type": "string"}}, "required": ["tag"]}]}}, "required": ["choice"]}}
]
}`)
translatedRound11 := []byte(`{
"model": "gemini-3-pro-high",
"request": {"tools": [{"functionDeclarations": [
{"name": "ChooseImplicit", "parametersJsonSchema": {"type": "object", "properties": {"choice": {"anyOf": [{"type": "object", "properties": {"reason": {"type": "string"}}, "required": ["reason"]}, {"type": "object", "properties": {"tag": {"type": "string"}}, "required": ["tag"]}]}}, "required": ["choice"]}}
]}]}
}`)
round10Request := []byte(`{
"tools": [
{"name": "ImplicitBranch", "input_schema": {"type": "object", "properties": {"choice": {"anyOf": [{"type": "object", "properties": {"cfg": {"type": "object", "properties": {"mode": {"type": "string"}}, "additionalProperties": false}}, "required": ["cfg"], "additionalProperties": false}, {"type": "object", "properties": {"tag": {"type": "string"}}, "required": ["tag"]}]}}, "required": ["choice"], "additionalProperties": false}}
]
}`)
translatedRound10 := []byte(`{
"model": "gemini-3.1-pro",
"request": {"tools": [{"functionDeclarations": [
{"name": "ImplicitBranch", "parametersJsonSchema": {"type": "object", "properties": {"choice": {"type": "object", "properties": {"cfg": {"type": "object", "properties": {"mode": {"type": "string"}, "_": {"type": "boolean"}}, "required": ["_"]}}, "required": ["cfg"]}}, "required": ["choice"]}}
]}]}
}`)
round9Request := []byte(`{
"tools": [
{"name": "ConfigureNested", "input_schema": {"type": "object", "properties": {"choice": {"anyOf": [{"type": "object", "properties": {"cfg": {"type": "object", "properties": {"mode": {"type": "string"}}, "additionalProperties": false}}, "required": ["cfg"], "additionalProperties": false}, {"type": "object", "patternProperties": {"^cfg$": {"type": "object", "additionalProperties": true}}, "additionalProperties": false}]}}, "required": ["choice"]}}
]
}`)
translatedRound9 := []byte(`{
"model": "gemini-3.1-pro",
"request": {"tools": [{"functionDeclarations": [
{"name": "ConfigureNested", "parametersJsonSchema": {"type": "object", "properties": {"choice": {"type": "object", "properties": {"cfg": {"type": "object", "properties": {"mode": {"type": "string"}, "_": {"type": "boolean"}}, "required": ["_"]}}, "required": ["cfg"]}}, "required": ["choice"]}}
]}]}
}`)
round8Request := []byte(`{
"tools": [
{"name": "ExplainPattern", "input_schema": {"type": "object", "properties": {}, "patternProperties": {"^reason$": {"type": "string"}}, "additionalProperties": false}},
{"name": "PatternData", "input_schema": {"type": "object", "properties": {}, "patternProperties": {"^x-": {"type": "string"}}, "additionalProperties": false}}
]
}`)
translatedRound8 := []byte(`{
"model": "gemini-3.1-pro",
"request": {"tools": [{"functionDeclarations": [
{"name": "ExplainPattern", "parametersJsonSchema": {"type": "object", "properties": {"reason": {"type": "string"}}, "required": ["reason"]}},
{"name": "PatternData", "parametersJsonSchema": {"type": "object", "properties": {"reason": {"type": "string"}}, "required": ["reason"]}}
]}]}
}`)
round7Request := []byte(`{
"tools": [
{"name": "Configure", "input_schema": {"type": "object", "properties": {"cfg": {"anyOf": [{"type": "object", "properties": {}, "additionalProperties": false}, {"type": "object", "additionalProperties": true}]}}, "required": ["cfg"]}}
]
}`)
translatedRound7 := []byte(`{
"model": "gemini-3.1-pro",
"request": {"tools": [{"functionDeclarations": [
{"name": "Configure", "parametersJsonSchema": {"type": "object", "properties": {"cfg": {"anyOf": [{"type": "object", "properties": {"reason": {"type": "string"}}, "required": ["reason"]}, {"type": "object", "properties": {"reason": {"type": "string"}}, "required": ["reason"]}]}}, "required": ["cfg"]}}
]}]}
}`)
round6Request := []byte(`{
"tools": [
{"name": "PlainOptions", "input_schema": {"type": "object", "properties": {"flag": {"type": "string"}}}},
{"name": "StrictOptions", "input_schema": {"type": "object", "properties": {"flag": {"type": "string"}}, "additionalProperties": false}},
{"name": "BranchChoice", "input_schema": {"type": "object", "properties": {"choice": {"anyOf": [{"type": "object", "properties": {"cfg": {"type": "object", "additionalProperties": true}}, "required": ["cfg"]}, {"type": "object", "properties": {"cfg": {"type": "object", "properties": {}, "additionalProperties": false}}, "required": ["cfg"]}]}}, "required": ["choice"]}}
]
}`)
translatedRound6 := []byte(`{
"model": "gemini-2.5-pro",
"tools": [{"functionDeclarations": [
{"name": "PlainOptions", "parametersJsonSchema": {"type": "object", "properties": {"flag": {"type": "string"}}}},
{"name": "StrictOptions", "parametersJsonSchema": {"type": "object", "properties": {"flag": {"type": "string"}}}},
{"name": "BranchChoice", "parametersJsonSchema": {"type": "object", "properties": {"choice": {"anyOf": [{"type": "object", "properties": {"cfg": {"type": "object", "additionalProperties": true}}, "required": ["cfg"]}, {"type": "object", "properties": {"cfg": {"type": "object", "properties": {}}}, "required": ["cfg"]}]}}, "required": ["choice"]}}
]}]
}`)
round5Request := []byte(`{
"tools": [
{"name": "Explain", "input_schema": {"type": "object", "required": ["reason"], "additionalProperties": true}},
{"name": "Choose", "input_schema": {"type": "object", "properties": {"choice": {"anyOf": [{"type": "object", "properties": {"opts": {"type": "object", "properties": {}, "additionalProperties": false}}, "required": ["opts"]}, {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}]}}, "required": ["choice"]}},
{"name": "Options", "input_schema": {"type": "object", "properties": {"flag": {"type": "string"}}, "additionalProperties": true}}
]
}`)
translatedRound5 := []byte(`{
"model": "gemini-3.8-flash-high",
"tools": [{"functionDeclarations": [
{"name": "Explain", "parametersJsonSchema": {"type": "object", "properties": {"reason": {"type": "string"}}, "required": ["reason"]}},
{"name": "Choose", "parametersJsonSchema": {"type": "object", "properties": {"choice": {"type": "object", "properties": {"opts": {"type": "object", "properties": {"reason": {"type": "string"}}, "required": ["reason"]}}, "required": ["opts"]}}, "required": ["choice"]}},
{"name": "Options", "parametersJsonSchema": {"type": "object", "properties": {"flag": {"type": "string"}}, "additionalProperties": true}}
]}]
}`)
round4Request := []byte(`{
"tools": [
{"name": "UnionChoice", "input_schema": {"type": "object", "properties": {"choice": {"anyOf": [{"type": "object", "properties": {"reason": {"type": "string"}}, "required": ["reason"]}, {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}]}}, "required": ["choice"]}},
{"name": "AllOfChildMerge", "input_schema": {"type": "object", "properties": {"cfg": {"type": "object", "properties": {"mode": {"type": "string"}}}}, "allOf": [{"properties": {"cfg": {"type": "object", "properties": {}}}}]}},
{"name": "NestedRefTool", "input_schema": {"type": "object", "properties": {"opts": {"$ref": "#/$defs/Empty"}}, "$defs": {"Empty": {"type": "object", "properties": {}, "additionalProperties": false}}}},
{"name": "WrappedPlaceholder", "input_schema": {"type": "object", "properties": {"flag": {"type": "string"}}, "additionalProperties": false}}
]
}`)
translatedRound4 := []byte(`{
"model": "gemini-3.8-flash-high",
"request": {"tools": [{"functionDeclarations": [
{"name": "UnionChoice", "parametersJsonSchema": {"type": "object", "properties": {"choice": {"anyOf": [{"type": "object", "properties": {"reason": {"type": "string"}}, "required": ["reason"]}, {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}]}}, "required": ["choice"]}},
{"name": "AllOfChildMerge", "parametersJsonSchema": {"type": "object", "properties": {"cfg": {"type": "object", "properties": {"mode": {"type": "string"}}}}}},
{"name": "NestedRefTool", "parametersJsonSchema": {"type": "object", "properties": {"opts": {"type": "object", "properties": {"reason": {"type": "string"}}, "required": ["reason"]}}}},
{"name": "WrappedPlaceholder", "parametersJsonSchema": {"type": "object", "properties": {"flag": {"type": "string"}}}}
]}]}
}`)
extraRequest := []byte(`{
"tools": [
{"name": "AllOfDynamicConflict", "input_schema": {"type": "object", "properties": {}, "additionalProperties": false, "allOf": [{"additionalProperties": true}]}},
{"name": "MultiUnionEmpty", "input_schema": {"anyOf": [{"type": "object", "properties": {}, "additionalProperties": false}, {"type": "object", "properties": {"x": {"type": "string"}}, "required": ["x"], "additionalProperties": false}]}},
{"name": "DupTool", "input_schema": {"type": "object", "properties": {}}},
{"name": "DupTool", "input_schema": {"type": "object", "properties": {}}},
{"name": "ConflictingDup", "input_schema": {"type": "object", "properties": {}}},
{"name": "ConflictingDup", "input_schema": {"type": "object", "properties": {"path": {"type": "string"}}}},
{"name": "MatrixTool", "input_schema": {"type": "object", "properties": {"rows": {"type": "array", "items": {"type": "array", "items": {"type": "object", "properties": {}}}}}}}
]
}`)
translatedExtra := []byte(`{
"model": "gemini-3.8-flash-high",
"request": {"tools": [{"functionDeclarations": [
{"name": "AllOfDynamicConflict", "parametersJsonSchema": {"type": "object", "properties": {"reason": {"type": "string"}}, "required": ["reason"]}},
{"name": "MultiUnionEmpty", "parametersJsonSchema": {"anyOf": [{"type": "object", "properties": {"reason": {"type": "string"}}, "required": ["reason"]}, {"type": "object", "properties": {"x": {"type": "string"}}, "required": ["x"]}]}},
{"name": "MatrixTool", "parametersJsonSchema": {"type": "object", "properties": {"rows": {"type": "array", "items": {"type": "array", "items": {"type": "object", "properties": {"reason": {"type": "string"}}, "required": ["reason"]}}}}}}
]}]}
}`)
deepRequest := []byte(`{
"tools": [
{"name": "FreeFormNested", "input_schema": {"type": "object", "properties": {"payload": {}}, "required": ["payload"]}},
{"name": "AllOfMerge", "input_schema": {"type": "object", "properties": {"cfg": {"type": "object", "properties": {"mode": {"type": "string"}}}}, "allOf": [{"type": "object", "properties": {"cfg": {"type": "object", "properties": {"reason": {"type": "string"}}}}}]}},
{"name": "SingleUnionEmpty", "input_schema": {"anyOf": [{"type": "object", "properties": {}}]}},
{"name": "DynamicParent", "input_schema": {"type": "object", "properties": {"opts": {"type": "object", "properties": {}}}, "additionalProperties": true}},
{"name": "RefArray", "input_schema": {"type": "object", "properties": {"items": {"$ref": "#/$defs/list"}}, "$defs": {"list": {"type": "array", "items": {"type": "object", "properties": {"v": {"type": "string"}}}}}}},
{"name": "PropRef", "input_schema": {"type": "object", "properties": {"template": {"type": "object", "properties": {}}, "copy": {"$ref": "#/properties/template"}}}}
]
}`)
tests := []struct {
name string
rawArgs string
toolName string
origReq []byte
want string
transferred []byte
}{
{"parameterless empty properties", `{"reason":"Entering plan mode"}`, "EnterPlanMode", origRequest, "{}", nil},
{"parameterless missing properties", `{"reason":"List jobs"}`, "CronList", origRequest, "{}", nil},
{"declared args preserved", `{"command":"ls -la"}`, "Bash", origRequest, `{"command":"ls -la"}`, nil},
{"undeclared reason stripped", `{"command":"ls -la","reason":"why"}`, "Bash", origRequest, `{"command":"ls -la"}`, nil},
{"reason kept when translated schema lacks placeholder", `{"command":"ls -la","reason":"why"}`, "Bash", origRequest, `{"command":"ls -la","reason":"why"}`, transferredRequest},
{"reason stripped when translated schema marks placeholder", `{"command":"ls -la","reason":"why"}`, "Bash", origRequest, `{"command":"ls -la"}`, translatedReasonRequired},
{"undeclared underscore stripped", `{"command":"ls -la","_":true}`, "Bash", origRequest, `{"command":"ls -la"}`, nil},
{"declared reason preserved", `{"reason":"valid"}`, "CustomToolWithReason", origRequest, `{"reason":"valid"}`, nil},
{"additional properties preserved", `{"name":"x","reason":"dynamic"}`, "DynamicTool", origRequest, `{"name":"x","reason":"dynamic"}`, nil},
{"union schema passthrough", `{"reason":"x"}`, "UnionTool", origRequest, `{"reason":"x"}`, nil},
{"unknown tool passthrough", `{"reason":"x"}`, "EnterPlanMode", nil, `{"reason":"x"}`, nil},
{"empty args", "", "EnterPlanMode", origRequest, "{}", nil},
{"empty object args", "{}", "EnterPlanMode", origRequest, "{}", nil},
{"dynamic map tool preserved", `{"key":"value"}`, "MapTool", dynamicRequest, `{"key":"value"}`, nil},
{"typed map tool preserved", `{"key":"value"}`, "TypedMapTool", dynamicRequest, `{"key":"value"}`, nil},
{"case collision exact match keeps args", `{"path":"/tmp/a"}`, "read", caseCollisionRequest, `{"path":"/tmp/a"}`, nil},
{"case collision exact match parameterless", `{"reason":"x"}`, "Read", caseCollisionRequest, "{}", nil},
{"allOf empty normalized", `{"reason":"x"}`, "AllOfEmpty", composedRequest, "{}", nil},
{"allOf declared args preserved", `{"path":"/tmp/a"}`, "AllOfArgs", composedRequest, `{"path":"/tmp/a"}`, nil},
{"ref to empty normalized", `{"reason":"x"}`, "RefEmpty", composedRequest, "{}", nil},
{"nested placeholders stripped", `{"opts":{"reason":"x"},"cfg":{"mode":"fast","_":true}}`, "NestedTool", composedRequest, `{"opts":{},"cfg":{"mode":"fast"}}`, nil},
{"free-form nested preserved", `{"payload":{"value":1}}`, "FreeFormNested", deepRequest, `{"payload":{"value":1}}`, nil},
{"allOf deep merge keeps declared reason", `{"cfg":{"mode":"a","reason":"b"}}`, "AllOfMerge", deepRequest, `{"cfg":{"mode":"a","reason":"b"}}`, nil},
{"single union empty normalized", `{"reason":"x"}`, "SingleUnionEmpty", deepRequest, "{}", nil},
{"dynamic parent still cleans declared child", `{"extra":1,"opts":{"reason":"x"}}`, "DynamicParent", deepRequest, `{"extra":1,"opts":{}}`, nil},
{"ref array items cleaned", `{"items":[{"v":"a","_":true}]}`, "RefArray", deepRequest, `{"items":[{"v":"a"}]}`, nil},
{"property ref cleaned", `{"copy":{"reason":"x"},"template":{}}`, "PropRef", deepRequest, `{"copy":{},"template":{}}`, nil},
{"allOf dynamic conflict uses translated schema", `{"reason":"x"}`, "AllOfDynamicConflict", extraRequest, "{}", translatedExtra},
{"multi-branch union uses translated schema", `{"reason":"x"}`, "MultiUnionEmpty", extraRequest, "{}", translatedExtra},
{"identical duplicates cleaned", `{"reason":"x"}`, "DupTool", extraRequest, "{}", nil},
{"conflicting duplicates passthrough", `{"path":"/tmp/a"}`, "ConflictingDup", extraRequest, `{"path":"/tmp/a"}`, nil},
{"conflicting duplicates keep first-declared cleaning", `{"reason":"x"}`, "ConflictingDup", extraRequest, `{"reason":"x"}`, nil},
{"matrix nested arrays cleaned", `{"rows":[[{"reason":"x"}]]}`, "MatrixTool", extraRequest, `{"rows":[[{}]]}`, translatedExtra},
{"union declared required reason preserved", `{"choice":{"reason":"maintenance"}}`, "UnionChoice", round4Request, `{"choice":{"reason":"maintenance"}}`, translatedRound4},
{"allOf child merge keeps mode", `{"cfg":{"mode":"fast"}}`, "AllOfChildMerge", round4Request, `{"cfg":{"mode":"fast"}}`, translatedRound4},
{"nested ref placeholder cleaned", `{"opts":{"reason":"initialize"}}`, "NestedRefTool", round4Request, `{"opts":{}}`, translatedRound4},
{"required-only reason preserved", `{"reason":"maintenance"}`, "Explain", round5Request, `{"reason":"maintenance"}`, translatedRound5},
{"union branch nested placeholder cleaned", `{"choice":{"opts":{"reason":"initialize"}}}`, "Choose", round5Request, `{"choice":{"opts":{}}}`, translatedRound5},
{"gemini route keeps dynamic underscore", `{"flag":"x","_":true}`, "Options", round5Request, `{"flag":"x","_":true}`, translatedRound5},
{"implicit additional properties keeps underscore", `{"flag":"x","_":true}`, "PlainOptions", round6Request, `{"flag":"x","_":true}`, translatedRound6},
{"closed object still strips executor underscore", `{"flag":"x","_":true}`, "StrictOptions", round6Request, `{"flag":"x"}`, translatedRound6},
{"alternative branch dynamic data preserved", `{"choice":{"cfg":{"value":1}}}`, "BranchChoice", round6Request, `{"choice":{"cfg":{"value":1}}}`, translatedRound6},
{"alternative branch dynamic reason preserved", `{"cfg":{"reason":"business"}}`, "Configure", round7Request, `{"cfg":{"reason":"business"}}`, translatedRound7},
{"pattern property reason preserved", `{"reason":"business"}`, "ExplainPattern", round8Request, `{"reason":"business"}`, translatedRound8},
{"pattern property non-placeholder data preserved", `{"x-note":"keep"}`, "PatternData", round8Request, `{"x-note":"keep"}`, translatedRound8},
{"union branch pattern child preserved", `{"choice":{"cfg":{"mode":"fast","_":true}}}`, "ConfigureNested", round9Request, `{"choice":{"cfg":{"mode":"fast","_":true}}}`, translatedRound9},
{"implicit open branch child preserved", `{"choice":{"tag":"keep","cfg":{"mode":"fast","_":true}}}`, "ImplicitBranch", round10Request, `{"choice":{"tag":"keep","cfg":{"mode":"fast","_":true}}}`, translatedRound10},
{"implicit open branch field preserved", `{"choice":{"tag":"keep","reason":"business"}}`, "ChooseImplicit", round11Request, `{"choice":{"tag":"keep","reason":"business"}}`, translatedRound11},
{"implicit open branch non-matching object cleaned", `{"choice":{"reason":"x"}}`, "ChooseImplicit", round11Request, `{"choice":{}}`, translatedRound11},
{"alternative child candidates keep matched data", `{"choice":{"cfg":{"mode":"fast","reason":"business"}}}`, "ConfigureAlt", round12Request, `{"choice":{"cfg":{"mode":"fast","reason":"business"}}}`, translatedRound12},
{"alternative child candidates clean unmatched placeholder", `{"choice":{"cfg":{"reason":"x"}}}`, "ConfigureAlt", round12Request, `{"choice":{"cfg":{}}}`, translatedRound12},
{"deep alternative candidates keep matched subtree", `{"choice":{"cfg":{"tag":"keep","opts":{"mode":"fast","_":true}}}}`, "DeepAlt", round13Request, `{"choice":{"cfg":{"tag":"keep","opts":{"mode":"fast","_":true}}}}`, translatedRound13},
{"deep alternative candidates clean unmatched subtree", `{"choice":{"cfg":{"opts":{"mode":"fast","_":true}}}}`, "DeepAlt", round13Request, `{"choice":{"cfg":{"opts":{"mode":"fast"}}}}`, translatedRound13},
{"free-form alternative child preserved", `{"cfg":{"reason":"business","value":1}}`, "ConfigureAltFree", round14Request, `{"cfg":{"reason":"business","value":1}}`, translatedRound14},
{"free-form array candidate preserved", `{"rows":[{"value":1}]}`, "Batch", round15Request, `{"rows":[{"value":1}]}`, translatedRound15},
{"strict array placeholder cleaned", `{"rows":[{"reason":"x"}]}`, "BatchStrict", round15Request, `{"rows":[{}]}`, translatedRound15},
{"fallback keeps required-only reason", `{"reason":"business"}`, "1", collisionRequest, `{"reason":"business"}`, nil},
{"object enum value preserved", `{"reason":"business"}`, "ExplainFixed", enumRequest, `{"reason":"business"}`, translatedEnum},
{"object enum with union preserved", `{"reason":"business"}`, "ExplainFixed", enumUnionRequest, `{"reason":"business"}`, translatedEnum},
{"array enum elements preserved", `{"rows":[{"reason":"business"}]}`, "BatchEnum", arrayEnumRequest, `{"rows":[{"reason":"business"}]}`, translatedArrayEnum},
{"array enum elements preserved fallback", `{"rows":[{"reason":"business"}]}`, "BatchEnum", arrayEnumRequest, `{"rows":[{"reason":"business"}]}`, nil},
{"array contains elements preserved", `{"rows":[{"reason":"business"}]}`, "BatchContains", arrayContainsRequest, `{"rows":[{"reason":"business"}]}`, translatedArrayContains},
{"array contains elements preserved fallback", `{"rows":[{"reason":"business"}]}`, "BatchContains", arrayContainsRequest, `{"rows":[{"reason":"business"}]}`, nil},
{"sibling allOf declaration preserved", `{"cfg":{"reason":"business"}}`, "ConfigureSibling", siblingAllOfRequest, `{"cfg":{"reason":"business"}}`, translatedSiblingAllOf},
{"minProperties object preserved", `{"reason":"store","label":"keep"}`, "StoreMetadata", minPropsRequest, `{"reason":"store","label":"keep"}`, translatedMinProps},
{"minProperties object preserved fallback", `{"reason":"store","label":"keep"}`, "StoreMetadata", minPropsRequest, `{"reason":"store","label":"keep"}`, nil},
{"uniqueItems elements preserved", `{"rows":[{"tag":"same","_":true},{"tag":"same","_":false}]}`, "BatchUnique", uniqueItemsRequest, `{"rows":[{"tag":"same","_":true},{"tag":"same","_":false}]}`, translatedUniqueItems},
{"uniqueItems elements preserved fallback", `{"rows":[{"tag":"same","_":true},{"tag":"same","_":false}]}`, "BatchUnique", uniqueItemsRequest, `{"rows":[{"tag":"same","_":true},{"tag":"same","_":false}]}`, nil},
{"object enum value preserved fallback", `{"reason":"business"}`, "ExplainFixed", enumRequest, `{"reason":"business"}`, nil},
{"object const value preserved", `{"reason":"fixed"}`, "ConstTool", enumRequest, `{"reason":"fixed"}`, translatedEnum},
{"disambiguated name uses translated schema", `{"reason":"business"}`, "1", collisionRequest, `{"reason":"business"}`, translatedCollisionDynamic},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := CleanClaudeToolArgumentsWithTranslated(tt.rawArgs, tt.toolName, tt.origReq, tt.transferred); got != tt.want {
t.Errorf("CleanClaudeToolArguments(%q, %q) = %q, want %q", tt.rawArgs, tt.toolName, got, tt.want)
}
})
}
}
func TestCleanClaudeToolArguments_ImplicitOpenObjectPreservesArguments(t *testing.T) {
for _, tc := range []struct {
name string
schema string
want string
wantNoArgs bool
}{
{"additionalProperties_omitted", `{"type":"object","properties":{}}`, `{"user":"v"}`, false},
{"additionalProperties_omitted_no_properties", `{"type":"object"}`, `{"user":"v"}`, false},
{"additionalProperties_true", `{"type":"object","properties":{},"additionalProperties":true}`, `{"user":"v"}`, false},
{"additionalProperties_false", `{"type":"object","properties":{},"additionalProperties":false}`, `{}`, true},
{"additionalProperties_false_no_properties", `{"type":"object","additionalProperties":false}`, `{}`, true},
} {
t.Run(tc.name, func(t *testing.T) {
original := []byte(`{"tools":[{"name":"echo","input_schema":` + tc.schema + `}]}`)
cleanedSchema := CleanJSONSchemaForGeminiJSONSchema(tc.schema)
translated := []byte(`{"tools":[{"functionDeclarations":[{"name":"echo","parametersJsonSchema":` + cleanedSchema + `}]}]}`)
args := `{"user":"v"}`
direct := CleanClaudeToolArguments(args, "echo", original)
withTranslated := CleanClaudeToolArgumentsWithTranslated(args, "echo", original, translated)
noArgs := ClaudeToolIsParameterless("echo", original)
if direct != tc.want || withTranslated != tc.want || noArgs != tc.wantNoArgs {
t.Errorf("want direct/translated=%s parameterless=%v; got direct=%s translated=%s parameterless=%v",
tc.want, tc.wantNoArgs, direct, withTranslated, noArgs)
}
})
}
t.Run("implicit_open_strips_injected_placeholder_but_keeps_user_arg", func(t *testing.T) {
schema := `{"type":"object","properties":{}}`
original := []byte(`{"tools":[{"name":"echo","input_schema":` + schema + `}]}`)
antigravitySchema := CleanJSONSchemaForAntigravity(schema)
translated := []byte(`{"tools":[{"functionDeclarations":[{"name":"echo","parametersJsonSchema":` + antigravitySchema + `}]}]}`)
args := `{"user":"v","reason":"Brief explanation"}`
if got := CleanClaudeToolArguments(args, "echo", original); got != `{"user":"v"}` {
t.Errorf("CleanClaudeToolArguments = %s, want {\"user\":\"v\"}", got)
}
if got := CleanClaudeToolArgumentsWithTranslated(args, "echo", original, translated); got != `{"user":"v"}` {
t.Errorf("CleanClaudeToolArgumentsWithTranslated = %s, want {\"user\":\"v\"}", got)
}
})
for _, tc := range []struct {
name string
schema string
args string
want string
wantNoArgs bool
}{
{
"nested_implicit_open_preserves_user_arg",
`{"type":"object","properties":{"opts":{"type":"object","properties":{}}}}`,
`{"opts":{"user":"v"}}`,
`{"opts":{"user":"v"}}`,
false,
},
{
"nested_explicit_closed_erases_user_arg",
`{"type":"object","properties":{"opts":{"type":"object","properties":{},"additionalProperties":false}}}`,
`{"opts":{"user":"v"}}`,
`{"opts":{}}`,
false,
},
{
"array_item_implicit_open_preserves_user_arg",
`{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{}}}}}`,
`{"items":[{"user":"v"}]}`,
`{"items":[{"user":"v"}]}`,
false,
},
{
"array_item_explicit_closed_erases_user_arg",
`{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{},"additionalProperties":false}}}}`,
`{"items":[{"user":"v"}]}`,
`{"items":[{}]}`,
false,
},
{
"allOf_implicit_open_preserves_user_arg",
`{"type":"object","allOf":[{"type":"object","properties":{}}]}`,
`{"user":"v"}`,
`{"user":"v"}`,
false,
},
{
"allOf_explicit_closed_erases_user_arg",
`{"type":"object","allOf":[{"type":"object","properties":{},"additionalProperties":false}]}`,
`{"user":"v"}`,
`{}`,
true,
},
{
"local_ref_implicit_open_preserves_user_arg",
`{"$ref":"#/$defs/open","$defs":{"open":{"type":"object","properties":{}}}}`,
`{"user":"v"}`,
`{"user":"v"}`,
false,
},
{
"local_ref_explicit_closed_erases_user_arg",
`{"$ref":"#/$defs/closed","$defs":{"closed":{"type":"object","properties":{},"additionalProperties":false}}}`,
`{"user":"v"}`,
`{}`,
true,
},
} {
t.Run(tc.name, func(t *testing.T) {
original := []byte(`{"tools":[{"name":"echo","input_schema":` + tc.schema + `}]}`)
cleanedSchema := CleanJSONSchemaForGeminiJSONSchema(tc.schema)
translated := []byte(`{"tools":[{"functionDeclarations":[{"name":"echo","parametersJsonSchema":` + cleanedSchema + `}]}]}`)
direct := CleanClaudeToolArguments(tc.args, "echo", original)
withTranslated := CleanClaudeToolArgumentsWithTranslated(tc.args, "echo", original, translated)
noArgs := ClaudeToolIsParameterless("echo", original)
if direct != tc.want || withTranslated != tc.want || noArgs != tc.wantNoArgs {
t.Errorf("want direct/translated=%s parameterless=%v; got direct=%s translated=%s parameterless=%v",
tc.want, tc.wantNoArgs, direct, withTranslated, noArgs)
}
})
}
}
func TestToolNameMapsDoNotAliasInputBytes(t *testing.T) {
raw := []byte(`{"tools":[{"name":"mcp__alpha__run/tool"},{"type":"namespace","name":"ns","tools":[{"type":"custom","name":"patch/tool"}]}],"messages":[{"role":"system","content":[{"type":"tool_addition","tool":{"type":"tool_definition","definition":{"name":"Inline/Tool"}}}]}]}`)
claudeMap := ToolNameMapFromClaudeRequest(raw)
unambiguousMap := UnambiguousToolNameMapFromClaudeRequest(raw)
sanitizedToolMap := SanitizedToolNameMap(raw)
sanitizedFuncMap := SanitizedFunctionNameMap(raw)
disambiguatedMap := DisambiguatedToolNameMap(raw)
reverseIdentityMap := ResponsesToolReverseIdentityMap(raw)
for i := range raw {
raw[i] = 'X'
}
if got := claudeMap[CanonicalToolName("mcp__alpha__run/tool")]; got != "mcp__alpha__run/tool" {
t.Fatalf("ToolNameMapFromClaudeRequest aliased input buffer: got %q", got)
}
if got := claudeMap[CanonicalToolName("Inline/Tool")]; got != "Inline/Tool" {
t.Fatalf("ToolNameMapFromClaudeRequest inline tool_addition aliased input buffer: got %q", got)
}
if got := unambiguousMap[CanonicalToolName("mcp__alpha__run/tool")]; got != "mcp__alpha__run/tool" {
t.Fatalf("UnambiguousToolNameMapFromClaudeRequest aliased input buffer: got %q", got)
}
if got := sanitizedToolMap["mcp__alpha__run_tool"]; got != "mcp__alpha__run/tool" {
t.Fatalf("SanitizedToolNameMap aliased input buffer: got %q", got)
}
if got := sanitizedFuncMap["mcp__alpha__run/tool"]; got != "mcp__alpha__run_tool" {
t.Fatalf("SanitizedFunctionNameMap aliased input buffer: got %q", got)
}
if got := disambiguatedMap["mcp__alpha__run_tool"]; got != "mcp__alpha__run/tool" {
t.Fatalf("DisambiguatedToolNameMap aliased input buffer: got %q", got)
}
if got := reverseIdentityMap["ns__patch_tool"]; got.Name != "patch/tool" || got.Namespace != "ns" {
t.Fatalf("ResponsesToolReverseIdentityMap aliased input buffer: got %+v", got)
}
}