CallToolResult currently decodes structuredContent through an any field, which converts JSON numbers to float64 and silently rounds integers outside the IEEE-754 safe range. This change decodes structuredContent separately with UseNumber, preserving wire numbers as json.Number while retaining the existing decoded object, array, and primitive shapes. The field documentation now describes that representation. Users who depend on the previous float64 behavior can temporarily restore it with MCPGODEBUG=structuredcontentfloat64=1. An end-to-end in-memory client/server regression covers 9007199254740993 from a low-level Server.AddTool result. Additional coverage verifies the compatibility option restores the legacy rounded float64 value. Verification: - gofmt -l . - go test ./... - go vet ./... Fixes #1236 --------- Co-authored-by: Guglielmo Colombo <guglielmoc@google.com>
1374 lines
37 KiB
Go
1374 lines
37 KiB
Go
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
|
|
// Use of this source code is governed by an MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package mcp
|
|
|
|
import (
|
|
"encoding/json"
|
|
"maps"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
)
|
|
|
|
func TestParamsMeta(t *testing.T) {
|
|
// Verify some properties of the Meta field of Params structs.
|
|
// We use CallToolParams for the test, but the Meta setup of all params types
|
|
// is identical so they should all behave the same.
|
|
|
|
toJSON := func(x any) string {
|
|
data, err := json.Marshal(x)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return string(data)
|
|
}
|
|
|
|
meta := map[string]any{"m": 1}
|
|
|
|
// You can set the embedded Meta field to a literal map.
|
|
p := &CallToolParams{
|
|
Meta: meta,
|
|
Name: "name",
|
|
}
|
|
|
|
// The Meta field marshals properly when it's present.
|
|
if g, w := toJSON(p), `{"_meta":{"m":1},"name":"name"}`; g != w {
|
|
t.Errorf("got %s, want %s", g, w)
|
|
}
|
|
// ... and when it's absent.
|
|
p2 := &CallToolParams{Name: "n"}
|
|
if g, w := toJSON(p2), `{"name":"n"}`; g != w {
|
|
t.Errorf("got %s, want %s", g, w)
|
|
}
|
|
|
|
// The GetMeta and SetMeta functions work as expected.
|
|
if g := p.GetMeta(); !maps.Equal(g, meta) {
|
|
t.Errorf("got %+v, want %+v", g, meta)
|
|
}
|
|
|
|
meta2 := map[string]any{"x": 2}
|
|
p.SetMeta(meta2)
|
|
if g := p.GetMeta(); !maps.Equal(g, meta2) {
|
|
t.Errorf("got %+v, want %+v", g, meta2)
|
|
}
|
|
|
|
// The GetProgressToken and SetProgressToken methods work as expected.
|
|
if g := p.GetProgressToken(); g != nil {
|
|
t.Errorf("got %v, want nil", g)
|
|
}
|
|
|
|
p.SetProgressToken("t")
|
|
if g := p.GetProgressToken(); g != "t" {
|
|
t.Errorf("got %v, want `t`", g)
|
|
}
|
|
|
|
// The GetProgressToken and SetProgressToken methods work on a params struct that doesn't have a Meta field.
|
|
if g := p2.GetProgressToken(); g != nil {
|
|
t.Errorf("got %v, want nil", g)
|
|
}
|
|
|
|
p2.SetProgressToken("t")
|
|
if g := p2.GetProgressToken(); g != "t" {
|
|
t.Errorf("got %v, want `t`", g)
|
|
}
|
|
|
|
// You can set a progress token to an int, int32 or int64.
|
|
p.SetProgressToken(int(1))
|
|
p.SetProgressToken(int32(1))
|
|
p.SetProgressToken(int64(1))
|
|
}
|
|
|
|
func TestCompleteReference(t *testing.T) {
|
|
marshalTests := []struct {
|
|
name string
|
|
in CompleteReference // The Go struct to marshal
|
|
want string // The expected JSON string output
|
|
wantErr bool // True if json.Marshal is expected to return an error
|
|
}{
|
|
{
|
|
name: "ValidPrompt",
|
|
in: CompleteReference{Type: "ref/prompt", Name: "my_prompt"},
|
|
want: `{"type":"ref/prompt","name":"my_prompt"}`,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "ValidResource",
|
|
in: CompleteReference{Type: "ref/resource", URI: "file:///path/to/resource.txt"},
|
|
want: `{"type":"ref/resource","uri":"file:///path/to/resource.txt"}`,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "ValidPromptEmptyName",
|
|
in: CompleteReference{Type: "ref/prompt", Name: ""},
|
|
want: `{"type":"ref/prompt"}`,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "ValidResourceEmptyURI",
|
|
in: CompleteReference{Type: "ref/resource", URI: ""},
|
|
want: `{"type":"ref/resource"}`,
|
|
wantErr: false,
|
|
},
|
|
// Error cases for MarshalJSON
|
|
{
|
|
name: "InvalidType",
|
|
in: CompleteReference{Type: "ref/unknown", Name: "something"},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "PromptWithURI",
|
|
in: CompleteReference{Type: "ref/prompt", Name: "my_prompt", URI: "unexpected_uri"},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "ResourceWithName",
|
|
in: CompleteReference{Type: "ref/resource", URI: "my_uri", Name: "unexpected_name"},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "MissingTypeField",
|
|
in: CompleteReference{Name: "missing"}, // Type is ""
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
// Define test cases specifically for Unmarshalling
|
|
unmarshalTests := []struct {
|
|
name string
|
|
in string // The JSON string input
|
|
want CompleteReference // The expected Go struct output
|
|
wantErr bool // True if json.Unmarshal is expected to return an error
|
|
}{
|
|
{
|
|
name: "ValidPrompt",
|
|
in: `{"type":"ref/prompt","name":"my_prompt"}`,
|
|
want: CompleteReference{Type: "ref/prompt", Name: "my_prompt"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "ValidResource",
|
|
in: `{"type":"ref/resource","uri":"file:///path/to/resource.txt"}`,
|
|
want: CompleteReference{Type: "ref/resource", URI: "file:///path/to/resource.txt"},
|
|
wantErr: false,
|
|
},
|
|
// Error cases for UnmarshalJSON
|
|
{
|
|
name: "UnrecognizedType",
|
|
in: `{"type":"ref/unknown","name":"something"}`,
|
|
want: CompleteReference{}, // placeholder, as unmarshal will fail
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "PromptWithURI",
|
|
in: `{"type":"ref/prompt","name":"my_prompt","uri":"unexpected_uri"}`,
|
|
want: CompleteReference{}, // placeholder
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "ResourceWithName",
|
|
in: `{"type":"ref/resource","uri":"my_uri","name":"unexpected_name"}`,
|
|
want: CompleteReference{}, // placeholder
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "MissingType",
|
|
in: `{"name":"missing"}`,
|
|
want: CompleteReference{}, // placeholder
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "InvalidJSON",
|
|
in: `invalid json`,
|
|
want: CompleteReference{}, // placeholder
|
|
wantErr: true, // json.Unmarshal will fail natively
|
|
},
|
|
}
|
|
|
|
// Run Marshal Tests
|
|
for _, test := range marshalTests {
|
|
t.Run("Marshal/"+test.name, func(t *testing.T) {
|
|
gotBytes, err := json.Marshal(&test.in)
|
|
if (err != nil) != test.wantErr {
|
|
t.Errorf("json.Marshal(%v) got error %v (want error %t)", test.in, err, test.wantErr)
|
|
}
|
|
if !test.wantErr { // Only check JSON output if marshal was expected to succeed
|
|
if diff := cmp.Diff(test.want, string(gotBytes)); diff != "" {
|
|
t.Errorf("json.Marshal(%v) mismatch (-want +got):\n%s", test.in, diff)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Run Unmarshal Tests
|
|
for _, test := range unmarshalTests {
|
|
t.Run("Unmarshal/"+test.name, func(t *testing.T) {
|
|
var got CompleteReference
|
|
err := json.Unmarshal([]byte(test.in), &got)
|
|
|
|
if (err != nil) != test.wantErr {
|
|
t.Errorf("json.Unmarshal(%q) got error %v (want error %t)", test.in, err, test.wantErr)
|
|
}
|
|
if !test.wantErr { // Only check content if unmarshal was expected to succeed
|
|
if diff := cmp.Diff(test.want, got); diff != "" {
|
|
t.Errorf("json.Unmarshal(%q) mismatch (-want +got):\n%s", test.in, diff)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompleteParams(t *testing.T) {
|
|
// Define test cases specifically for Marshalling
|
|
marshalTests := []struct {
|
|
name string
|
|
in CompleteParams
|
|
want string // Expected JSON output
|
|
}{
|
|
{
|
|
name: "BasicPromptCompletion",
|
|
in: CompleteParams{
|
|
Ref: &CompleteReference{
|
|
Type: "ref/prompt",
|
|
Name: "my_prompt",
|
|
},
|
|
Argument: CompleteParamsArgument{
|
|
Name: "language",
|
|
Value: "go",
|
|
},
|
|
},
|
|
want: `{"argument":{"name":"language","value":"go"},"ref":{"type":"ref/prompt","name":"my_prompt"}}`,
|
|
},
|
|
{
|
|
name: "ResourceCompletionRequest",
|
|
in: CompleteParams{
|
|
Ref: &CompleteReference{
|
|
Type: "ref/resource",
|
|
URI: "file:///src/main.java",
|
|
},
|
|
Argument: CompleteParamsArgument{
|
|
Name: "class",
|
|
Value: "MyClas",
|
|
},
|
|
},
|
|
want: `{"argument":{"name":"class","value":"MyClas"},"ref":{"type":"ref/resource","uri":"file:///src/main.java"}}`,
|
|
},
|
|
{
|
|
name: "PromptCompletionEmptyArgumentValue",
|
|
in: CompleteParams{
|
|
Ref: &CompleteReference{
|
|
Type: "ref/prompt",
|
|
Name: "another_prompt",
|
|
},
|
|
Argument: CompleteParamsArgument{
|
|
Name: "query",
|
|
Value: "",
|
|
},
|
|
},
|
|
want: `{"argument":{"name":"query","value":""},"ref":{"type":"ref/prompt","name":"another_prompt"}}`,
|
|
},
|
|
{
|
|
name: "PromptCompletionWithContext",
|
|
in: CompleteParams{
|
|
Ref: &CompleteReference{
|
|
Type: "ref/prompt",
|
|
Name: "my_prompt",
|
|
},
|
|
Argument: CompleteParamsArgument{
|
|
Name: "language",
|
|
Value: "go",
|
|
},
|
|
Context: &CompleteContext{
|
|
Arguments: map[string]string{
|
|
"framework": "mcp",
|
|
"language": "python",
|
|
},
|
|
},
|
|
},
|
|
want: `{"argument":{"name":"language","value":"go"},"context":{"arguments":{"framework":"mcp","language":"python"}},"ref":{"type":"ref/prompt","name":"my_prompt"}}`,
|
|
},
|
|
{
|
|
name: "PromptCompletionEmptyContextArguments",
|
|
in: CompleteParams{
|
|
Ref: &CompleteReference{
|
|
Type: "ref/prompt",
|
|
Name: "my_prompt",
|
|
},
|
|
Argument: CompleteParamsArgument{
|
|
Name: "language",
|
|
Value: "go",
|
|
},
|
|
Context: &CompleteContext{
|
|
Arguments: map[string]string{},
|
|
},
|
|
},
|
|
want: `{"argument":{"name":"language","value":"go"},"context":{},"ref":{"type":"ref/prompt","name":"my_prompt"}}`,
|
|
},
|
|
}
|
|
|
|
// Define test cases specifically for Unmarshalling
|
|
unmarshalTests := []struct {
|
|
name string
|
|
in string // JSON string input
|
|
want CompleteParams // Expected Go struct output
|
|
}{
|
|
{
|
|
name: "BasicPromptCompletion",
|
|
in: `{"argument":{"name":"language","value":"go"},"ref":{"type":"ref/prompt","name":"my_prompt"}}`,
|
|
want: CompleteParams{
|
|
Ref: &CompleteReference{Type: "ref/prompt", Name: "my_prompt"},
|
|
Argument: CompleteParamsArgument{Name: "language", Value: "go"},
|
|
},
|
|
},
|
|
{
|
|
name: "ResourceCompletionRequest",
|
|
in: `{"argument":{"name":"class","value":"MyClas"},"ref":{"type":"ref/resource","uri":"file:///src/main.java"}}`,
|
|
want: CompleteParams{
|
|
Ref: &CompleteReference{Type: "ref/resource", URI: "file:///src/main.java"},
|
|
Argument: CompleteParamsArgument{Name: "class", Value: "MyClas"},
|
|
},
|
|
},
|
|
{
|
|
name: "PromptCompletionWithContext",
|
|
in: `{"argument":{"name":"language","value":"go"},"context":{"arguments":{"framework":"mcp","language":"python"}},"ref":{"type":"ref/prompt","name":"my_prompt"}}`,
|
|
want: CompleteParams{
|
|
Ref: &CompleteReference{Type: "ref/prompt", Name: "my_prompt"},
|
|
Argument: CompleteParamsArgument{Name: "language", Value: "go"},
|
|
Context: &CompleteContext{Arguments: map[string]string{
|
|
"framework": "mcp",
|
|
"language": "python",
|
|
}},
|
|
},
|
|
},
|
|
{
|
|
name: "PromptCompletionEmptyContextArguments",
|
|
in: `{"argument":{"name":"language","value":"go"},"context":{"arguments":{}},"ref":{"type":"ref/prompt","name":"my_prompt"}}`,
|
|
want: CompleteParams{
|
|
Ref: &CompleteReference{Type: "ref/prompt", Name: "my_prompt"},
|
|
Argument: CompleteParamsArgument{Name: "language", Value: "go"},
|
|
Context: &CompleteContext{Arguments: map[string]string{}},
|
|
},
|
|
},
|
|
{
|
|
name: "PromptCompletionNilContext", // JSON `null` for context
|
|
in: `{"argument":{"name":"language","value":"go"},"context":null,"ref":{"type":"ref/prompt","name":"my_prompt"}}`,
|
|
want: CompleteParams{
|
|
Ref: &CompleteReference{Type: "ref/prompt", Name: "my_prompt"},
|
|
Argument: CompleteParamsArgument{Name: "language", Value: "go"},
|
|
Context: nil, // Should unmarshal to nil pointer
|
|
},
|
|
},
|
|
}
|
|
|
|
// Run Marshal Tests
|
|
for _, test := range marshalTests {
|
|
t.Run("Marshal/"+test.name, func(t *testing.T) {
|
|
got, err := json.Marshal(&test.in) // Marshal takes a pointer
|
|
if err != nil {
|
|
t.Fatalf("json.Marshal(CompleteParams) failed: %v", err)
|
|
}
|
|
if diff := cmp.Diff(test.want, string(got)); diff != "" {
|
|
t.Errorf("CompleteParams marshal mismatch (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Run Unmarshal Tests
|
|
for _, test := range unmarshalTests {
|
|
t.Run("Unmarshal/"+test.name, func(t *testing.T) {
|
|
var got CompleteParams
|
|
if err := json.Unmarshal([]byte(test.in), &got); err != nil {
|
|
t.Fatalf("json.Unmarshal(CompleteParams) failed: %v", err)
|
|
}
|
|
if diff := cmp.Diff(test.want, got); diff != "" {
|
|
t.Errorf("CompleteParams unmarshal mismatch (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompleteResult(t *testing.T) {
|
|
// Define test cases specifically for Marshalling
|
|
marshalTests := []struct {
|
|
name string
|
|
in CompleteResult
|
|
want string // Expected JSON output
|
|
}{
|
|
{
|
|
name: "BasicCompletionResult",
|
|
in: CompleteResult{
|
|
Completion: CompletionResultDetails{
|
|
Values: []string{"golang", "google", "goroutine"},
|
|
Total: 10,
|
|
HasMore: true,
|
|
},
|
|
},
|
|
want: `{"completion":{"hasMore":true,"total":10,"values":["golang","google","goroutine"]}}`,
|
|
},
|
|
{
|
|
name: "CompletionResultNoTotalNoHasMore",
|
|
in: CompleteResult{
|
|
Completion: CompletionResultDetails{
|
|
Values: []string{"only"},
|
|
HasMore: false,
|
|
Total: 0,
|
|
},
|
|
},
|
|
want: `{"completion":{"values":["only"]}}`,
|
|
},
|
|
{
|
|
name: "CompletionResultEmptyValues",
|
|
in: CompleteResult{
|
|
Completion: CompletionResultDetails{
|
|
Values: []string{},
|
|
Total: 0,
|
|
HasMore: false,
|
|
},
|
|
},
|
|
want: `{"completion":{"values":[]}}`,
|
|
},
|
|
}
|
|
|
|
// Define test cases specifically for Unmarshalling
|
|
unmarshalTests := []struct {
|
|
name string
|
|
in string // JSON string input
|
|
want CompleteResult // Expected Go struct output
|
|
}{
|
|
{
|
|
name: "BasicCompletionResult",
|
|
in: `{"completion":{"hasMore":true,"total":10,"values":["golang","google","goroutine"]}}`,
|
|
want: CompleteResult{
|
|
Completion: CompletionResultDetails{
|
|
Values: []string{"golang", "google", "goroutine"},
|
|
Total: 10,
|
|
HasMore: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "CompletionResultNoTotalNoHasMore",
|
|
in: `{"completion":{"values":["only"]}}`,
|
|
want: CompleteResult{
|
|
Completion: CompletionResultDetails{
|
|
Values: []string{"only"},
|
|
HasMore: false,
|
|
Total: 0,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "CompletionResultEmptyValues",
|
|
in: `{"completion":{"hasMore":false,"total":0,"values":[]}}`,
|
|
want: CompleteResult{
|
|
Completion: CompletionResultDetails{
|
|
Values: []string{},
|
|
Total: 0,
|
|
HasMore: false,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Run Marshal Tests
|
|
for _, test := range marshalTests {
|
|
t.Run("Marshal/"+test.name, func(t *testing.T) {
|
|
got, err := json.Marshal(&test.in) // Marshal takes a pointer
|
|
if err != nil {
|
|
t.Fatalf("json.Marshal(CompleteResult) failed: %v", err)
|
|
}
|
|
if diff := cmp.Diff(test.want, string(got)); diff != "" {
|
|
t.Errorf("CompleteResult marshal mismatch (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Run Unmarshal Tests
|
|
for _, test := range unmarshalTests {
|
|
t.Run("Unmarshal/"+test.name, func(t *testing.T) {
|
|
var got CompleteResult
|
|
if err := json.Unmarshal([]byte(test.in), &got); err != nil {
|
|
t.Fatalf("json.Unmarshal(CompleteResult) failed: %v", err)
|
|
}
|
|
if diff := cmp.Diff(test.want, got, cmpopts.IgnoreUnexported(CompleteResult{})); diff != "" {
|
|
t.Errorf("CompleteResult unmarshal mismatch (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TODO: merge the following 4 tests into content_test.go.
|
|
func TestToolUseContent_MarshalJSON(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
content *ToolUseContent
|
|
want string
|
|
}{
|
|
{
|
|
name: "basic tool use",
|
|
content: &ToolUseContent{
|
|
ID: "tool_123",
|
|
Name: "calculator",
|
|
Input: map[string]any{
|
|
"operation": "add",
|
|
"x": 1.0,
|
|
"y": 2.0,
|
|
},
|
|
},
|
|
want: `{"type":"tool_use","id":"tool_123","name":"calculator","input":{"operation":"add","x":1,"y":2}}`,
|
|
},
|
|
{
|
|
name: "nil input marshals as empty object",
|
|
content: &ToolUseContent{
|
|
ID: "tool_456",
|
|
Name: "no_args_tool",
|
|
Input: nil,
|
|
},
|
|
want: `{"type":"tool_use","id":"tool_456","name":"no_args_tool","input":{}}`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := tt.content.MarshalJSON()
|
|
if err != nil {
|
|
t.Fatalf("MarshalJSON() error = %v", err)
|
|
}
|
|
if diff := cmp.Diff(tt.want, string(got)); diff != "" {
|
|
t.Errorf("mismatch (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestToolUseContent_UnmarshalJSON(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
json string
|
|
want *ToolUseContent
|
|
}{
|
|
{
|
|
name: "basic tool use",
|
|
json: `{"type":"tool_use","id":"tool_123","name":"calculator","input":{"x":1,"y":2}}`,
|
|
want: &ToolUseContent{
|
|
ID: "tool_123",
|
|
Name: "calculator",
|
|
Input: map[string]any{
|
|
"x": 1.0,
|
|
"y": 2.0,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "with meta",
|
|
json: `{"type":"tool_use","id":"t1","name":"calc","input":{"x":1},"_meta":{"requestId":"req-123"}}`,
|
|
want: &ToolUseContent{
|
|
ID: "t1",
|
|
Name: "calc",
|
|
Input: map[string]any{"x": 1.0},
|
|
Meta: Meta{"requestId": "req-123"},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
wire := &wireContent{}
|
|
if err := json.Unmarshal([]byte(tt.json), wire); err != nil {
|
|
t.Fatalf("Unmarshal() error = %v", err)
|
|
}
|
|
got, err := contentFromWire(wire, map[string]bool{"tool_use": true})
|
|
if err != nil {
|
|
t.Fatalf("contentFromWire() error = %v", err)
|
|
}
|
|
if diff := cmp.Diff(tt.want, got); diff != "" {
|
|
t.Errorf("mismatch (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestToolResultContent_MarshalJSON(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
content *ToolResultContent
|
|
want string
|
|
}{
|
|
{
|
|
name: "basic tool result",
|
|
content: &ToolResultContent{
|
|
ToolUseID: "tool_123",
|
|
Content: []Content{&TextContent{Text: "42"}},
|
|
},
|
|
want: `{"type":"tool_result","toolUseId":"tool_123","content":[{"type":"text","text":"42"}]}`,
|
|
},
|
|
{
|
|
name: "tool result with error",
|
|
content: &ToolResultContent{
|
|
ToolUseID: "tool_456",
|
|
Content: []Content{&TextContent{Text: "division by zero"}},
|
|
IsError: true,
|
|
},
|
|
want: `{"type":"tool_result","toolUseId":"tool_456","content":[{"type":"text","text":"division by zero"}],"isError":true}`,
|
|
},
|
|
{
|
|
name: "tool result with structured content",
|
|
content: &ToolResultContent{
|
|
ToolUseID: "tool_789",
|
|
Content: []Content{&TextContent{Text: `{"result": 42}`}},
|
|
StructuredContent: map[string]any{"result": 42.0},
|
|
},
|
|
want: `{"type":"tool_result","toolUseId":"tool_789","content":[{"type":"text","text":"{\"result\": 42}"}],"structuredContent":{"result":42}}`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := tt.content.MarshalJSON()
|
|
if err != nil {
|
|
t.Fatalf("MarshalJSON() error = %v", err)
|
|
}
|
|
if diff := cmp.Diff(tt.want, string(got)); diff != "" {
|
|
t.Errorf("mismatch (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestToolResultContent_UnmarshalJSON(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
json string
|
|
want *ToolResultContent
|
|
}{
|
|
{
|
|
name: "basic tool result",
|
|
json: `{"type":"tool_result","toolUseId":"tool_123","content":[{"type":"text","text":"42"}],"isError":false}`,
|
|
want: &ToolResultContent{
|
|
ToolUseID: "tool_123",
|
|
Content: []Content{&TextContent{Text: "42"}},
|
|
IsError: false,
|
|
},
|
|
},
|
|
{
|
|
name: "image nested content",
|
|
json: `{"type":"tool_result","toolUseId":"t1","content":[{"type":"image","mimeType":"image/png","data":"YWJj"}]}`,
|
|
want: &ToolResultContent{
|
|
ToolUseID: "t1",
|
|
Content: []Content{&ImageContent{MIMEType: "image/png", Data: []byte("abc")}},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
wire := &wireContent{}
|
|
if err := json.Unmarshal([]byte(tt.json), wire); err != nil {
|
|
t.Fatalf("Unmarshal() error = %v", err)
|
|
}
|
|
got, err := contentFromWire(wire, map[string]bool{"tool_result": true})
|
|
if err != nil {
|
|
t.Fatalf("contentFromWire() error = %v", err)
|
|
}
|
|
if diff := cmp.Diff(tt.want, got); diff != "" {
|
|
t.Errorf("mismatch (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSamplingMessage_UnmarshalJSON(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
json string
|
|
want *SamplingMessage
|
|
}{
|
|
{
|
|
name: "tool_use content",
|
|
json: `{"content":{"type":"tool_use","id":"tool_1","name":"calc","input":{}},"role":"assistant"}`,
|
|
want: &SamplingMessage{
|
|
Role: "assistant",
|
|
Content: &ToolUseContent{
|
|
ID: "tool_1",
|
|
Name: "calc",
|
|
Input: map[string]any{},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "tool_result content",
|
|
json: `{"content":{"type":"tool_result","toolUseId":"tool_1","content":[{"type":"text","text":"42"}]},"role":"user"}`,
|
|
want: &SamplingMessage{
|
|
Role: "user",
|
|
Content: &ToolResultContent{
|
|
ToolUseID: "tool_1",
|
|
Content: []Content{&TextContent{Text: "42"}},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var got SamplingMessage
|
|
if err := json.Unmarshal([]byte(tt.json), &got); err != nil {
|
|
t.Fatalf("Unmarshal() error = %v", err)
|
|
}
|
|
if diff := cmp.Diff(tt.want, &got); diff != "" {
|
|
t.Errorf("mismatch (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSamplingCapabilities_MarshalUnmarshalJSON(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
caps *SamplingCapabilities
|
|
json string
|
|
}{
|
|
{
|
|
name: "WithCapabilities",
|
|
caps: &SamplingCapabilities{
|
|
Tools: &SamplingToolsCapabilities{},
|
|
Context: &SamplingContextCapabilities{},
|
|
},
|
|
json: `{"context":{},"tools":{}}`,
|
|
},
|
|
{
|
|
name: "EmptyCapabilities",
|
|
caps: &SamplingCapabilities{},
|
|
json: `{}`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
gotJson, err := json.Marshal(tt.caps)
|
|
if err != nil {
|
|
t.Fatalf("Marshal() error = %v", err)
|
|
}
|
|
if diff := cmp.Diff(tt.json, string(gotJson)); diff != "" {
|
|
t.Errorf("mismatch (-want +got):\n%s", diff)
|
|
}
|
|
var gotCaps SamplingCapabilities
|
|
if err := json.Unmarshal([]byte(tt.json), &gotCaps); err != nil {
|
|
t.Fatalf("Unmarshal() error = %v", err)
|
|
}
|
|
if diff := cmp.Diff(tt.caps, &gotCaps); diff != "" {
|
|
t.Errorf("mismatch (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCreateMessageWithToolsParams_MarshalUnmarshalJSON(t *testing.T) {
|
|
params := &CreateMessageWithToolsParams{
|
|
MaxTokens: 1000,
|
|
Messages: []*SamplingMessageV2{
|
|
{
|
|
Role: "user",
|
|
Content: []Content{&TextContent{Text: "Calculate 1+1"}},
|
|
},
|
|
},
|
|
Tools: []*Tool{
|
|
{
|
|
Name: "calculator",
|
|
Description: "A calculator tool",
|
|
InputSchema: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"x": map[string]any{"type": "number"},
|
|
"y": map[string]any{"type": "number"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
ToolChoice: &ToolChoice{Mode: "auto"},
|
|
}
|
|
|
|
data, err := json.Marshal(params)
|
|
if err != nil {
|
|
t.Fatalf("Marshal() error = %v", err)
|
|
}
|
|
|
|
var got CreateMessageWithToolsParams
|
|
if err := json.Unmarshal(data, &got); err != nil {
|
|
t.Fatalf("Unmarshal() error = %v", err)
|
|
}
|
|
|
|
if diff := cmp.Diff(params, &got); diff != "" {
|
|
t.Errorf("mismatch (-want +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
func TestToolChoice_MarshalUnmarshalJSON(t *testing.T) {
|
|
choices := []ToolChoice{
|
|
{Mode: "auto"},
|
|
{Mode: "required"},
|
|
{Mode: "none"},
|
|
}
|
|
|
|
for _, tc := range choices {
|
|
data, err := json.Marshal(tc)
|
|
if err != nil {
|
|
t.Fatalf("Marshal(%v) error = %v", tc.Mode, err)
|
|
}
|
|
var got ToolChoice
|
|
if err := json.Unmarshal(data, &got); err != nil {
|
|
t.Fatalf("Unmarshal() error = %v", err)
|
|
}
|
|
if diff := cmp.Diff(tc, got); diff != "" {
|
|
t.Errorf("mismatch (-want +got):\n%s", diff)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCreateMessageWithToolsResult_MarshalJSON(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
result *CreateMessageWithToolsResult
|
|
want string
|
|
}{
|
|
{
|
|
name: "single element content",
|
|
result: &CreateMessageWithToolsResult{
|
|
Model: "test",
|
|
Role: "assistant",
|
|
Content: []Content{&TextContent{Text: "hello"}},
|
|
},
|
|
// Single-element Content marshals as object (not array) for backward compat.
|
|
want: `{"content":{"type":"text","text":"hello"},"model":"test","role":"assistant"}`,
|
|
},
|
|
{
|
|
name: "multiple elements content",
|
|
result: &CreateMessageWithToolsResult{
|
|
Model: "test",
|
|
Role: "assistant",
|
|
Content: []Content{
|
|
&TextContent{Text: "thinking..."},
|
|
&ToolUseContent{
|
|
ID: "call_1",
|
|
Name: "calculator",
|
|
Input: map[string]any{
|
|
"a": 1.0,
|
|
"b": 2.0,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
// Multiple elements marshal as array.
|
|
want: `{"content":[{"type":"text","text":"thinking..."},{"type":"tool_use","id":"call_1","name":"calculator","input":{"a":1,"b":2}}],"model":"test","role":"assistant"}`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := json.Marshal(tt.result)
|
|
if err != nil {
|
|
t.Fatalf("Marshal() error = %v", err)
|
|
}
|
|
if diff := cmp.Diff(tt.want, string(got)); diff != "" {
|
|
t.Errorf("mismatch (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCreateMessageWithToolsResult_UnmarshalJSON(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
json string
|
|
want *CreateMessageWithToolsResult
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "single tool_use content",
|
|
json: `{"content":{"type":"tool_use","id":"tool_1","name":"calculator","input":{"x":1}},"model":"test-model","role":"assistant","stopReason":"toolUse"}`,
|
|
want: &CreateMessageWithToolsResult{
|
|
Model: "test-model",
|
|
Role: "assistant",
|
|
StopReason: "toolUse",
|
|
Content: []Content{
|
|
&ToolUseContent{
|
|
ID: "tool_1",
|
|
Name: "calculator",
|
|
Input: map[string]any{
|
|
"x": 1.0,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "array of tool_use content",
|
|
json: `{"content":[{"type":"tool_use","id":"t1","name":"calc","input":{"x":1}},{"type":"tool_use","id":"t2","name":"search","input":{"q":"hi"}}],"model":"test","role":"assistant","stopReason":"toolUse"}`,
|
|
want: &CreateMessageWithToolsResult{
|
|
Model: "test",
|
|
Role: "assistant",
|
|
StopReason: "toolUse",
|
|
Content: []Content{
|
|
&ToolUseContent{
|
|
ID: "t1",
|
|
Name: "calc",
|
|
Input: map[string]any{"x": 1.0},
|
|
},
|
|
&ToolUseContent{
|
|
ID: "t2",
|
|
Name: "search",
|
|
Input: map[string]any{"q": "hi"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "empty array",
|
|
json: `{"content":[],"model":"m","role":"assistant"}`,
|
|
want: &CreateMessageWithToolsResult{
|
|
Model: "m",
|
|
Role: "assistant",
|
|
Content: []Content{},
|
|
},
|
|
},
|
|
{
|
|
name: "null content",
|
|
json: `{"content":null,"model":"m","role":"assistant"}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "rejects tool_result",
|
|
json: `{"content":{"type":"tool_result","toolUseId":"t1","content":[]},"model":"m","role":"assistant"}`,
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var got CreateMessageWithToolsResult
|
|
err := json.Unmarshal([]byte(tt.json), &got)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Fatalf("Unmarshal() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
if tt.wantErr {
|
|
return
|
|
}
|
|
if diff := cmp.Diff(tt.want, &got); diff != "" {
|
|
t.Errorf("mismatch (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSamplingMessageV2_MarshalJSON(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
msg *SamplingMessageV2
|
|
want string
|
|
}{
|
|
{
|
|
name: "empty content",
|
|
msg: &SamplingMessageV2{
|
|
Role: "user",
|
|
Content: []Content{},
|
|
},
|
|
want: `{"content":[],"role":"user"}`,
|
|
},
|
|
{
|
|
name: "single content",
|
|
msg: &SamplingMessageV2{
|
|
Role: "assistant",
|
|
Content: []Content{&TextContent{Text: "hello"}},
|
|
},
|
|
want: `{"content":{"type":"text","text":"hello"},"role":"assistant"}`,
|
|
},
|
|
{
|
|
name: "multiple content",
|
|
msg: &SamplingMessageV2{
|
|
Role: "assistant",
|
|
// Text + tool_use in the same message (valid per spec for assistant).
|
|
Content: []Content{
|
|
&TextContent{Text: "checking weather"},
|
|
&ToolUseContent{ID: "c1", Name: "weather", Input: map[string]any{"city": "SF"}},
|
|
},
|
|
},
|
|
want: `{"content":[{"type":"text","text":"checking weather"},{"type":"tool_use","id":"c1","name":"weather","input":{"city":"SF"}}],"role":"assistant"}`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := json.Marshal(tt.msg)
|
|
if err != nil {
|
|
t.Fatalf("Marshal() error = %v", err)
|
|
}
|
|
if diff := cmp.Diff(tt.want, string(got)); diff != "" {
|
|
t.Errorf("mismatch (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSamplingMessageV2_UnmarshalJSON(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
json string
|
|
want *SamplingMessageV2
|
|
}{
|
|
{
|
|
name: "single content object",
|
|
json: `{"role":"user","content":{"type":"text","text":"hello"}}`,
|
|
want: &SamplingMessageV2{
|
|
Role: "user",
|
|
Content: []Content{
|
|
&TextContent{Text: "hello"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "multiple content",
|
|
json: `{"role":"assistant","content":[{"type":"text","text":"Let me check the weather."},{"type":"tool_use","id":"c1","name":"weather","input":{"city":"SF"}}]}`,
|
|
want: &SamplingMessageV2{
|
|
Role: "assistant",
|
|
// Text + tool_use in the same message (valid per spec for assistant).
|
|
Content: []Content{
|
|
&TextContent{Text: "Let me check the weather."},
|
|
&ToolUseContent{ID: "c1", Name: "weather", Input: map[string]any{"city": "SF"}},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var got SamplingMessageV2
|
|
if err := json.Unmarshal([]byte(tt.json), &got); err != nil {
|
|
t.Fatalf("Unmarshal() error = %v", err)
|
|
}
|
|
if diff := cmp.Diff(tt.want, &got); diff != "" {
|
|
t.Errorf("mismatch (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestToBase_Conversion(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
params *CreateMessageWithToolsParams
|
|
want *CreateMessageParams
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "Single content",
|
|
params: &CreateMessageWithToolsParams{
|
|
MaxTokens: 1000,
|
|
Messages: []*SamplingMessageV2{
|
|
{Role: "user", Content: []Content{&TextContent{Text: "hello"}}},
|
|
},
|
|
Tools: []*Tool{{Name: "calc"}},
|
|
ToolChoice: &ToolChoice{Mode: "auto"},
|
|
},
|
|
want: &CreateMessageParams{
|
|
MaxTokens: 1000,
|
|
Messages: []*SamplingMessage{
|
|
{Role: "user", Content: &TextContent{Text: "hello"}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "Multi content",
|
|
params: &CreateMessageWithToolsParams{
|
|
MaxTokens: 1000,
|
|
Messages: []*SamplingMessageV2{
|
|
{Role: "assistant", Content: []Content{
|
|
&ToolUseContent{ID: "c1", Name: "calc", Input: map[string]any{}},
|
|
&ToolUseContent{ID: "c2", Name: "search", Input: map[string]any{}},
|
|
}},
|
|
},
|
|
},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := tt.params.toBase()
|
|
if (err != nil) != tt.wantErr {
|
|
t.Fatalf("toBase() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
if tt.wantErr {
|
|
return
|
|
}
|
|
if diff := cmp.Diff(tt.want, got); diff != "" {
|
|
t.Errorf("mismatch (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestToWithTools_Conversion(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
result *CreateMessageResult
|
|
want *CreateMessageWithToolsResult
|
|
}{
|
|
{
|
|
name: "with content",
|
|
result: &CreateMessageResult{
|
|
Model: "test",
|
|
Role: "assistant",
|
|
Content: &TextContent{Text: "hello"},
|
|
StopReason: "endTurn",
|
|
},
|
|
want: &CreateMessageWithToolsResult{
|
|
Model: "test",
|
|
Role: "assistant",
|
|
Content: []Content{&TextContent{Text: "hello"}},
|
|
StopReason: "endTurn",
|
|
},
|
|
},
|
|
{
|
|
name: "nil content",
|
|
result: &CreateMessageResult{
|
|
Model: "test",
|
|
Role: "assistant",
|
|
},
|
|
want: &CreateMessageWithToolsResult{
|
|
Model: "test",
|
|
Role: "assistant",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
wt := tt.result.toWithTools()
|
|
if diff := cmp.Diff(tt.want, wt); diff != "" {
|
|
t.Errorf("mismatch (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInputRequestMapJSON(t *testing.T) {
|
|
t.Run("nil is omitted from JSON", func(t *testing.T) {
|
|
result := CallToolResult{Content: []Content{&TextContent{Text: "ok"}}}
|
|
data, err := json.Marshal(&result)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var raw map[string]json.RawMessage
|
|
if err := json.Unmarshal(data, &raw); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := raw["inputRequests"]; ok {
|
|
t.Errorf("nil InputRequests should be omitted, got %s", raw["inputRequests"])
|
|
}
|
|
})
|
|
|
|
t.Run("non-nil empty round-trips", func(t *testing.T) {
|
|
result := CallToolResult{
|
|
Content: []Content{&TextContent{Text: "ok"}},
|
|
InputRequests: InputRequestMap{},
|
|
}
|
|
data, err := json.Marshal(&result)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var raw map[string]json.RawMessage
|
|
if err := json.Unmarshal(data, &raw); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(raw["inputRequests"]) != "{}" {
|
|
t.Errorf("empty InputRequests should marshal to {}, got %s", raw["inputRequests"])
|
|
}
|
|
var got CallToolResult
|
|
if err := json.Unmarshal(data, &got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.InputRequests == nil {
|
|
t.Error("empty InputRequests should round-trip as non-nil")
|
|
}
|
|
})
|
|
|
|
t.Run("populated round-trips", func(t *testing.T) {
|
|
result := CallToolResult{
|
|
Content: []Content{&TextContent{Text: "ok"}},
|
|
InputRequests: InputRequestMap{
|
|
"r1": &ElicitParams{Message: "confirm?"},
|
|
},
|
|
}
|
|
data, err := json.Marshal(&result)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var got CallToolResult
|
|
if err := json.Unmarshal(data, &got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.InputRequests == nil {
|
|
t.Fatal("InputRequests should not be nil after round-trip")
|
|
}
|
|
if _, ok := got.InputRequests["r1"]; !ok {
|
|
t.Error("expected key r1 in InputRequests")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestInputResponseMapJSON(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value InputResponse
|
|
check func(t *testing.T, got InputResponse)
|
|
}{
|
|
{
|
|
name: "elicit",
|
|
value: &ElicitResult{Action: "accept", Content: map[string]any{"ok": true}},
|
|
},
|
|
{
|
|
name: "sampling",
|
|
value: &CreateMessageWithToolsResult{Role: "assistant", Model: "test-model", Content: []Content{&TextContent{Text: "hello"}}},
|
|
},
|
|
{
|
|
name: "list-roots",
|
|
value: &ListRootsResult{Roots: []*Root{{URI: "file:///tmp", Name: "tmp"}}},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
key := tt.name
|
|
data, err := json.Marshal(InputResponseMap{key: tt.value})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var got InputResponseMap
|
|
if err := json.Unmarshal(data, &got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if diff := cmp.Diff(tt.value, got[key], ctrCmpOpts...); diff != "" {
|
|
t.Errorf("mismatch (-want, +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestContentUnmarshal(t *testing.T) {
|
|
// Verify that types with a Content field round-trip properly.
|
|
roundtrip := func(in, out any) {
|
|
t.Helper()
|
|
data, err := json.Marshal(in)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := json.Unmarshal(data, out); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if diff := cmp.Diff(in, out, ctrCmpOpts...); diff != "" {
|
|
t.Errorf("mismatch (-want, +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
content := []Content{&TextContent{Text: "t"}}
|
|
|
|
ctr := &CallToolResult{
|
|
Meta: Meta{"m": true},
|
|
Content: content,
|
|
IsError: true,
|
|
StructuredContent: map[string]any{"s": "x"},
|
|
}
|
|
var got CallToolResult
|
|
roundtrip(ctr, &got)
|
|
|
|
ctrf := &CallToolResult{
|
|
Meta: Meta{"m": true},
|
|
Content: content,
|
|
IsError: true,
|
|
StructuredContent: json.Number("3"),
|
|
}
|
|
var gotf CallToolResult
|
|
roundtrip(ctrf, &gotf)
|
|
|
|
pm := &PromptMessage{
|
|
Content: content[0],
|
|
Role: "",
|
|
}
|
|
var gotpm PromptMessage
|
|
roundtrip(pm, &gotpm)
|
|
}
|
|
|
|
func TestCallToolResultStructuredContentFloat64Compatibility(t *testing.T) {
|
|
prev := structuredcontentfloat64
|
|
structuredcontentfloat64 = "1"
|
|
t.Cleanup(func() { structuredcontentfloat64 = prev })
|
|
|
|
var got CallToolResult
|
|
if err := json.Unmarshal([]byte(`{"content":[],"structuredContent":{"id":9007199254740993}}`), &got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
structured, ok := got.StructuredContent.(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("StructuredContent type = %T, want map[string]any", got.StructuredContent)
|
|
}
|
|
if got := structured["id"]; got != float64(9007199254740992) {
|
|
t.Errorf("id = %v (%T), want rounded float64 value", got, got)
|
|
}
|
|
}
|
|
|
|
func TestToolAnnotations_MarshalJSON(t *testing.T) {
|
|
boolPtr := func(b bool) *bool { return &b }
|
|
|
|
tests := []struct {
|
|
name string
|
|
in ToolAnnotations
|
|
want string
|
|
}{
|
|
{
|
|
name: "ZeroValue",
|
|
in: ToolAnnotations{},
|
|
want: `{"idempotentHint":false,"readOnlyHint":false}`,
|
|
},
|
|
{
|
|
name: "AllFalse",
|
|
in: ToolAnnotations{
|
|
DestructiveHint: boolPtr(false),
|
|
IdempotentHint: false,
|
|
OpenWorldHint: boolPtr(false),
|
|
ReadOnlyHint: false,
|
|
},
|
|
want: `{"destructiveHint":false,"idempotentHint":false,"openWorldHint":false,"readOnlyHint":false}`,
|
|
},
|
|
{
|
|
name: "AllTrue",
|
|
in: ToolAnnotations{
|
|
DestructiveHint: boolPtr(true),
|
|
IdempotentHint: true,
|
|
OpenWorldHint: boolPtr(true),
|
|
ReadOnlyHint: true,
|
|
Title: "my tool",
|
|
},
|
|
want: `{"destructiveHint":true,"idempotentHint":true,"openWorldHint":true,"readOnlyHint":true,"title":"my tool"}`,
|
|
},
|
|
{
|
|
name: "MixedValues",
|
|
in: ToolAnnotations{
|
|
ReadOnlyHint: true,
|
|
Title: "read tool",
|
|
},
|
|
want: `{"idempotentHint":false,"readOnlyHint":true,"title":"read tool"}`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := json.Marshal(tt.in)
|
|
if err != nil {
|
|
t.Fatalf("json.Marshal(%v) failed: %v", tt.in, err)
|
|
}
|
|
if diff := cmp.Diff(tt.want, string(got)); diff != "" {
|
|
t.Errorf("json.Marshal() mismatch (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|