Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions message/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,14 @@ type serializedFunctionResultContent struct {
Type contentKind
}

type serializedFunctionResultContentForUnmarshal struct {
ContentHeader

CallID string
Error string `json:",omitempty"`
Result json.RawMessage `json:",omitempty"`
}

// FunctionResultContent represents the result of a function call.
type FunctionResultContent struct {
ContentHeader
Expand All @@ -397,15 +405,31 @@ func (t *FunctionResultContent) MarshalJSON() ([]byte, error) {
}

func (t *FunctionResultContent) UnmarshalJSON(data []byte) error {
var tmp serializedFunctionResultContent
var tmp serializedFunctionResultContentForUnmarshal
if err := json.Unmarshal(data, &tmp); err != nil {
return err
}
t.ContentHeader = tmp.ContentHeader
t.CallID = tmp.CallID
t.Result = tmp.Result
// Decode the result with a number-preserving decoder so numeric leaves become
// json.Number rather than float64. This keeps large integers exact (values
// above 2^53 would otherwise be corrupted) and makes Marshal/Unmarshal
Comment on lines 412 to +416

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in c7d6580: UnmarshalJSON now resets t.Result to nil when tmp.Result is empty and t.Error to nil when tmp.Error is empty, so stale values on a non-zero receiver no longer stick. Added TestFunctionResultContentUnmarshalResetsStaleFields to lock the behavior.

// idempotent, matching .NET's JsonElement preservation.
if len(tmp.Result) > 0 {
dec := json.NewDecoder(bytes.NewReader(tmp.Result))
dec.UseNumber()
var r any
if err := dec.Decode(&r); err != nil {
return err
}
t.Result = r
} else {
t.Result = nil
}
if tmp.Error != "" {
t.Error = errors.New(tmp.Error)
} else {
t.Error = nil
}
return nil
}
Expand Down
62 changes: 62 additions & 0 deletions message/content_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
package message_test

import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"reflect"
"testing"

Expand Down Expand Up @@ -183,6 +185,66 @@ func TestContentEncoding_Roundtrip(t *testing.T) {
}
}

func TestFunctionResultContentRoundtripPreservesResult(t *testing.T) {
cases := []struct {
name string
result any
want string // fmt.Sprint of the round-tripped result
}{
{"large-int", int64(9007199254740993), "9007199254740993"},
{"small-int", int64(42), "42"},
{"string", "hello", "hello"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
original := &message.FunctionResultContent{CallID: "call-1", Result: tc.result}
first, err := json.Marshal(original)
if err != nil {
t.Fatal(err)
}
var decoded message.FunctionResultContent
if err := json.Unmarshal(first, &decoded); err != nil {
t.Fatal(err)
}
if got := fmt.Sprint(decoded.Result); got != tc.want {
t.Fatalf("round-tripped result = %q, want %q", got, tc.want)
}
// Re-marshaling the decoded value must produce identical bytes,
// i.e. Marshal/Unmarshal is idempotent for the result payload.
second, err := json.Marshal(&decoded)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(first, second) {
t.Fatalf("re-marshaled JSON differs:\n first = %s\nsecond = %s", first, second)
}
})
}
}

func TestFunctionResultContentUnmarshalResetsStaleFields(t *testing.T) {
// Unmarshaling into a non-zero receiver must clear previously set Result
// and Error when the incoming JSON omits them, matching standard JSON
// unmarshal semantics.
decoded := message.FunctionResultContent{
CallID: "old",
Result: "stale",
Error: errors.New("stale error"),
}
if err := json.Unmarshal([]byte(`{"Type":"functionResult","CallID":"new"}`), &decoded); err != nil {
t.Fatal(err)
}
if decoded.Result != nil {
t.Fatalf("Result = %v, want nil", decoded.Result)
}
if decoded.Error != nil {
t.Fatalf("Error = %v, want nil", decoded.Error)
}
if decoded.CallID != "new" {
t.Fatalf("CallID = %q, want %q", decoded.CallID, "new")
}
}

func TestDataContentUnmarshalDefaultsMissingMediaType(t *testing.T) {
var content message.DataContent
if err := json.Unmarshal([]byte(`{"Type":"data","URI":"data:,hello%20world+literal"}`), &content); err != nil {
Expand Down
Loading