package util import "testing" func TestParseToolCalls(t *testing.T) { text := `prefix {"tool_calls":[{"name":"search","input":{"q":"golang"}}]} suffix` calls := ParseToolCalls(text, []string{"search"}) if len(calls) != 1 { t.Fatalf("expected 1 call, got %d", len(calls)) } if calls[0].Name != "search" { t.Fatalf("unexpected tool name: %s", calls[0].Name) } if calls[0].Input["q"] != "golang" { t.Fatalf("unexpected args: %#v", calls[0].Input) } } func TestParseToolCallsFromFencedJSON(t *testing.T) { text := "I will call tools now\n```json\n{\"tool_calls\":[{\"name\":\"search\",\"input\":{\"q\":\"news\"}}]}\n```" calls := ParseToolCalls(text, []string{"search"}) if len(calls) != 0 { t.Fatalf("expected fenced tool_call example to be ignored, got %#v", calls) } } func TestParseToolCallsWithFunctionArgumentsString(t *testing.T) { text := `{"tool_calls":[{"function":{"name":"get_weather","arguments":"{\"city\":\"beijing\"}"}}]}` calls := ParseToolCalls(text, []string{"get_weather"}) if len(calls) != 1 { t.Fatalf("expected 1 call, got %d", len(calls)) } if calls[0].Name != "get_weather" { t.Fatalf("unexpected tool name: %s", calls[0].Name) } if calls[0].Input["city"] != "beijing" { t.Fatalf("unexpected args: %#v", calls[0].Input) } } func TestParseToolCallsRejectsUnknownToolName(t *testing.T) { text := `{"tool_calls":[{"name":"unknown","input":{}}]}` calls := ParseToolCalls(text, []string{"search"}) if len(calls) != 0 { t.Fatalf("expected unknown tool to be rejected, got %#v", calls) } } func TestParseToolCallsAllowsCaseInsensitiveToolNameAndCanonicalizes(t *testing.T) { text := `{"tool_calls":[{"name":"Bash","input":{"command":"ls -al"}}]}` calls := ParseToolCalls(text, []string{"bash"}) if len(calls) != 1 { t.Fatalf("expected 1 call, got %#v", calls) } if calls[0].Name != "bash" { t.Fatalf("expected canonical tool name bash, got %q", calls[0].Name) } } func TestParseToolCallsDetailedMarksPolicyRejection(t *testing.T) { text := `{"tool_calls":[{"name":"unknown","input":{}}]}` res := ParseToolCallsDetailed(text, []string{"search"}) if !res.SawToolCallSyntax { t.Fatalf("expected SawToolCallSyntax=true, got %#v", res) } if !res.RejectedByPolicy { t.Fatalf("expected RejectedByPolicy=true, got %#v", res) } if len(res.Calls) != 0 { t.Fatalf("expected no calls after policy rejection, got %#v", res.Calls) } } func TestParseToolCallsDetailedRejectsWhenAllowListEmpty(t *testing.T) { text := `{"tool_calls":[{"name":"search","input":{"q":"go"}}]}` res := ParseToolCallsDetailed(text, nil) if !res.SawToolCallSyntax { t.Fatalf("expected SawToolCallSyntax=true, got %#v", res) } if !res.RejectedByPolicy { t.Fatalf("expected RejectedByPolicy=true, got %#v", res) } if len(res.Calls) != 0 { t.Fatalf("expected no calls when allow-list is empty, got %#v", res.Calls) } } func TestFormatOpenAIToolCalls(t *testing.T) { formatted := FormatOpenAIToolCalls([]ParsedToolCall{{Name: "search", Input: map[string]any{"q": "x"}}}) if len(formatted) != 1 { t.Fatalf("expected 1, got %d", len(formatted)) } fn, _ := formatted[0]["function"].(map[string]any) if fn["name"] != "search" { t.Fatalf("unexpected function name: %#v", fn) } } func TestParseStandaloneToolCallsOnlyMatchesStandalonePayload(t *testing.T) { mixed := `这里是示例:{"tool_calls":[{"name":"search","input":{"q":"go"}}]}` if calls := ParseStandaloneToolCalls(mixed, []string{"search"}); len(calls) != 0 { t.Fatalf("expected standalone parser to ignore mixed prose, got %#v", calls) } standalone := `{"tool_calls":[{"name":"search","input":{"q":"go"}}]}` calls := ParseStandaloneToolCalls(standalone, []string{"search"}) if len(calls) != 1 { t.Fatalf("expected standalone parser to match, got %#v", calls) } } func TestParseStandaloneToolCallsIgnoresFencedCodeBlock(t *testing.T) { fenced := "```json\n{\"tool_calls\":[{\"name\":\"search\",\"input\":{\"q\":\"go\"}}]}\n```" if calls := ParseStandaloneToolCalls(fenced, []string{"search"}); len(calls) != 0 { t.Fatalf("expected fenced tool_call example to be ignored, got %#v", calls) } } func TestParseToolCallsAllowsQualifiedToolName(t *testing.T) { text := `{"tool_calls":[{"name":"mcp.search_web","input":{"q":"golang"}}]}` calls := ParseToolCalls(text, []string{"search_web"}) if len(calls) != 1 { t.Fatalf("expected 1 call, got %#v", calls) } if calls[0].Name != "search_web" { t.Fatalf("expected canonical tool name search_web, got %q", calls[0].Name) } } func TestParseToolCallsAllowsPunctuationVariantToolName(t *testing.T) { text := `{"tool_calls":[{"name":"read-file","input":{"path":"README.md"}}]}` calls := ParseToolCalls(text, []string{"read_file"}) if len(calls) != 1 { t.Fatalf("expected 1 call, got %#v", calls) } if calls[0].Name != "read_file" { t.Fatalf("expected canonical tool name read_file, got %q", calls[0].Name) } } func TestParseToolCallsSupportsClaudeXMLToolCall(t *testing.T) { text := `Bashpwdshow cwd` calls := ParseToolCalls(text, []string{"bash"}) if len(calls) != 1 { t.Fatalf("expected 1 call, got %#v", calls) } if calls[0].Name != "bash" { t.Fatalf("expected canonical tool name bash, got %q", calls[0].Name) } if calls[0].Input["command"] != "pwd" { t.Fatalf("expected command argument, got %#v", calls[0].Input) } } func TestParseToolCallsDetailedMarksXMLToolCallSyntax(t *testing.T) { text := `Bashpwd` res := ParseToolCallsDetailed(text, []string{"bash"}) if !res.SawToolCallSyntax { t.Fatalf("expected SawToolCallSyntax=true, got %#v", res) } if len(res.Calls) != 1 { t.Fatalf("expected one parsed call, got %#v", res) } } func TestParseToolCallsSupportsClaudeXMLJSONToolCall(t *testing.T) { text := `{"tool":"Bash","params":{"command":"pwd","description":"show cwd"}}` calls := ParseToolCalls(text, []string{"bash"}) if len(calls) != 1 { t.Fatalf("expected 1 call, got %#v", calls) } if calls[0].Name != "bash" { t.Fatalf("expected canonical tool name bash, got %q", calls[0].Name) } if calls[0].Input["command"] != "pwd" { t.Fatalf("expected command argument, got %#v", calls[0].Input) } } func TestParseToolCallsSupportsFunctionCallTagStyle(t *testing.T) { text := `Bashls -lalist` calls := ParseToolCalls(text, []string{"bash"}) if len(calls) != 1 { t.Fatalf("expected 1 call, got %#v", calls) } if calls[0].Name != "bash" { t.Fatalf("expected canonical tool name bash, got %q", calls[0].Name) } if calls[0].Input["command"] != "ls -la" { t.Fatalf("expected command argument, got %#v", calls[0].Input) } } func TestParseToolCallsSupportsAntmlFunctionCallStyle(t *testing.T) { text := `{"command":"pwd","description":"x"}` calls := ParseToolCalls(text, []string{"bash"}) if len(calls) != 1 { t.Fatalf("expected 1 call, got %#v", calls) } if calls[0].Name != "bash" { t.Fatalf("expected canonical tool name bash, got %q", calls[0].Name) } if calls[0].Input["command"] != "pwd" { t.Fatalf("expected command argument, got %#v", calls[0].Input) } } func TestParseToolCallsSupportsAntmlArgumentStyle(t *testing.T) { text := `pwdx` calls := ParseToolCalls(text, []string{"bash"}) if len(calls) != 1 { t.Fatalf("expected 1 call, got %#v", calls) } if calls[0].Name != "bash" { t.Fatalf("expected canonical tool name bash, got %q", calls[0].Name) } if calls[0].Input["command"] != "pwd" { t.Fatalf("expected command argument, got %#v", calls[0].Input) } } func TestParseToolCallsSupportsInvokeFunctionCallStyle(t *testing.T) { text := `pwdd` calls := ParseToolCalls(text, []string{"bash"}) if len(calls) != 1 { t.Fatalf("expected 1 call, got %#v", calls) } if calls[0].Name != "bash" { t.Fatalf("expected canonical tool name bash, got %q", calls[0].Name) } if calls[0].Input["command"] != "pwd" { t.Fatalf("expected command argument, got %#v", calls[0].Input) } } func TestParseToolCallsSupportsNestedToolTagStyle(t *testing.T) { text := `pwdshow cwd` calls := ParseToolCalls(text, []string{"bash"}) if len(calls) != 1 { t.Fatalf("expected 1 call, got %#v", calls) } if calls[0].Name != "bash" { t.Fatalf("expected canonical tool name bash, got %q", calls[0].Name) } if calls[0].Input["command"] != "pwd" { t.Fatalf("expected command argument, got %#v", calls[0].Input) } } func TestParseToolCallsSupportsAntmlFunctionAttributeWithParametersTag(t *testing.T) { text := `{"command":"pwd"}` calls := ParseToolCalls(text, []string{"bash"}) if len(calls) != 1 { t.Fatalf("expected 1 call, got %#v", calls) } if calls[0].Name != "bash" { t.Fatalf("expected canonical tool name bash, got %q", calls[0].Name) } if calls[0].Input["command"] != "pwd" { t.Fatalf("expected command argument, got %#v", calls[0].Input) } } func TestParseToolCallsSupportsMultipleAntmlFunctionCalls(t *testing.T) { text := `{"command":"pwd"}{"file_path":"README.md"}` calls := ParseToolCalls(text, []string{"bash", "read"}) if len(calls) != 2 { t.Fatalf("expected 2 calls, got %#v", calls) } if calls[0].Name != "bash" || calls[1].Name != "read" { t.Fatalf("expected canonical names [bash read], got %#v", calls) } } func TestParseToolCallsDoesNotAcceptMismatchedMarkupTags(t *testing.T) { text := `read_file{"path":"README.md"}` calls := ParseToolCalls(text, []string{"read_file"}) if len(calls) != 0 { t.Fatalf("expected mismatched tags to be rejected, got %#v", calls) } } func TestRepairInvalidJSONBackslashes(t *testing.T) { tests := []struct { input string expected string }{ {`{"path": "C:\Users\name"}`, `{"path": "C:\\Users\name"}`}, {`{"cmd": "cd D:\git_codes"}`, `{"cmd": "cd D:\\git_codes"}`}, {`{"text": "line1\nline2"}`, `{"text": "line1\nline2"}`}, {`{"path": "D:\\back\\slash"}`, `{"path": "D:\\back\\slash"}`}, {`{"unicode": "\u2705"}`, `{"unicode": "\u2705"}`}, {`{"invalid_u": "\u123"}`, `{"invalid_u": "\\u123"}`}, } for _, tt := range tests { got := repairInvalidJSONBackslashes(tt.input) if got != tt.expected { t.Errorf("repairInvalidJSONBackslashes(%s) = %s; want %s", tt.input, got, tt.expected) } } } func TestParseToolCallsWithInvalidBackslashes(t *testing.T) { // DeepSeek sometimes outputs Windows paths with single backslashes in JSON strings text := `好的,执行以下命令:{"name": "execute_command", "input": "{\"command\": \"cd D:\git_codes && dir\"}"}` availableTools := []string{"execute_command"} parsed := ParseToolCalls(text, availableTools) if len(parsed) != 1 { t.Fatalf("expected 1 tool call, got %d", len(parsed)) } cmd, ok := parsed[0].Input["command"].(string) if !ok { t.Fatalf("expected command string in input, got %v", parsed[0].Input) } expected := "cd D:\\git_codes && dir" if cmd != expected { t.Errorf("expected command %q, got %q", expected, cmd) } }