github.com/anthropics/anthropic-sdk-go@v1.70.1/paramunmarshal_test.go (about)

     1  package anthropic_test
     2  
     3  // Union variant selection for request params: the variant whose
     4  // constants match wins; otherwise the closest structural fit.
     5  
     6  import (
     7  	"encoding/json"
     8  	"reflect"
     9  	"strings"
    10  	"testing"
    11  
    12  	"github.com/anthropics/anthropic-sdk-go"
    13  )
    14  
    15  func TestParamUnmarshalNullAnyFieldDecodesToNil(t *testing.T) {
    16  	raw := `{"model": "m", "max_tokens": 1,
    17  		"messages": [{"role": "assistant", "content": [{"type": "tool_use", "id": "t1", "name": "f", "input": null}]}]}`
    18  	var p anthropic.MessageNewParams
    19  	if err := json.Unmarshal([]byte(raw), &p); err != nil {
    20  		t.Fatalf("unexpected error: %v", err)
    21  	}
    22  	tu := p.Messages[0].Content[0].OfToolUse
    23  	if tu == nil {
    24  		t.Fatalf("tool_use variant not selected, got %#v", p.Messages[0].Content[0])
    25  	}
    26  	if tu.Input != nil {
    27  		t.Errorf("input must decode to nil as encoding/json does, got %#v", tu.Input)
    28  	}
    29  }
    30  
    31  // Unknown keys are dropped and never touch SetExtraFields.
    32  func TestParamUnmarshalUnknownFieldsIgnored(t *testing.T) {
    33  	raw := `{"model": "m", "max_tokens": 1, "some_future_field": {"a": 1},
    34  		"messages": [{"role": "user", "content": [{"type": "text", "text": "hi", "x_meta": 1}]}]}`
    35  	var p anthropic.MessageNewParams
    36  	if err := json.Unmarshal([]byte(raw), &p); err != nil {
    37  		t.Fatalf("unexpected error: %v", err)
    38  	}
    39  	if p.Messages[0].Content[0].OfText == nil || p.Messages[0].Content[0].OfText.Text != "hi" {
    40  		t.Errorf("text block not decoded alongside unknown field, got %#v", p.Messages[0].Content[0])
    41  	}
    42  	if len(p.ExtraFields()) != 0 || len(p.Messages[0].Content[0].OfText.ExtraFields()) != 0 {
    43  		t.Errorf("unknown keys must not populate the SetExtraFields escape hatch")
    44  	}
    45  	out, err := json.Marshal(p)
    46  	if err != nil {
    47  		t.Fatalf("marshal error: %v", err)
    48  	}
    49  	for _, dropped := range []string{"x_meta", "some_future_field"} {
    50  		if strings.Contains(string(out), dropped) {
    51  			t.Errorf("unknown key %s must be dropped on re-marshal, got %s", dropped, out)
    52  		}
    53  	}
    54  }
    55  
    56  func TestParamUnmarshalToolUnionVariantIdentity(t *testing.T) {
    57  	cases := map[string]struct {
    58  		raw    string
    59  		picked func(u anthropic.ToolUnionParam) bool
    60  	}{
    61  		"web_search_20250305": {
    62  			`{"type": "web_search_20250305", "name": "web_search", "max_uses": 3}`,
    63  			func(u anthropic.ToolUnionParam) bool {
    64  				return u.OfWebSearchTool20250305 != nil && u.OfWebSearchTool20250305.MaxUses.Value == 3
    65  			},
    66  		},
    67  		"text_editor_20250429": {
    68  			`{"type": "text_editor_20250429", "name": "str_replace_based_edit_tool"}`,
    69  			func(u anthropic.ToolUnionParam) bool { return u.OfTextEditor20250429 != nil },
    70  		},
    71  		"code_execution_20260120": {
    72  			`{"type": "code_execution_20260120", "name": "code_execution"}`,
    73  			func(u anthropic.ToolUnionParam) bool { return u.OfCodeExecutionTool20260120 != nil },
    74  		},
    75  		"web_fetch_20250910": {
    76  			`{"type": "web_fetch_20250910", "name": "web_fetch"}`,
    77  			func(u anthropic.ToolUnionParam) bool { return u.OfWebFetchTool20250910 != nil },
    78  		},
    79  	}
    80  	for name, tc := range cases {
    81  		t.Run(name, func(t *testing.T) {
    82  			var u anthropic.ToolUnionParam
    83  			if err := json.Unmarshal([]byte(tc.raw), &u); err != nil {
    84  				t.Fatalf("unexpected error: %v", err)
    85  			}
    86  			if !tc.picked(u) {
    87  				t.Fatalf("wrong variant selected for %s: %#v", tc.raw, u)
    88  			}
    89  		})
    90  	}
    91  }
    92  
    93  // An unknown newer version lands in the closest sibling, so its
    94  // config survives the round trip.
    95  func TestParamUnmarshalUnknownToolVersionPreservesConfig(t *testing.T) {
    96  	raw := `{"type":"web_search_20990101","name":"web_search","max_uses":5,"allowed_domains":["example.com"]}`
    97  	var u anthropic.ToolUnionParam
    98  	if err := json.Unmarshal([]byte(raw), &u); err != nil {
    99  		t.Fatalf("unexpected error: %v", err)
   100  	}
   101  	if u.OfTool != nil {
   102  		t.Fatalf("must not fall through to the custom-tool variant (drops config): %#v", u)
   103  	}
   104  	out, err := json.Marshal(u)
   105  	if err != nil {
   106  		t.Fatalf("marshal error: %v", err)
   107  	}
   108  	assertJSONEqual(t, raw, string(out))
   109  }
   110  
   111  func TestParamUnmarshalCustomToolPicksOfTool(t *testing.T) {
   112  	for _, raw := range []string{
   113  		`{"name":"my_tool","input_schema":{"type":"object"}}`,
   114  		`{"type":"custom","name":"my_tool","input_schema":{"type":"object"}}`,
   115  	} {
   116  		var u anthropic.ToolUnionParam
   117  		if err := json.Unmarshal([]byte(raw), &u); err != nil {
   118  			t.Fatalf("unexpected error: %v", err)
   119  		}
   120  		if u.OfTool == nil {
   121  			t.Fatalf("expected the custom-tool variant for %s, got %#v", raw, u)
   122  		}
   123  	}
   124  }
   125  
   126  func assertJSONEqual(t *testing.T, want, got string) {
   127  	t.Helper()
   128  	var a, b any
   129  	if err := json.Unmarshal([]byte(want), &a); err != nil {
   130  		t.Fatalf("bad want json: %v", err)
   131  	}
   132  	if err := json.Unmarshal([]byte(got), &b); err != nil {
   133  		t.Fatalf("bad got json: %v", err)
   134  	}
   135  	if !reflect.DeepEqual(a, b) {
   136  		t.Fatalf("round trip lost data\n want %s\n  got %s", want, got)
   137  	}
   138  }
   139  
   140  // Bare-string content promotes to a one-element text-block slice.
   141  func TestParamUnmarshalStringPromotion(t *testing.T) {
   142  	var p anthropic.MessageNewParams
   143  	if err := json.Unmarshal([]byte(`{"model":"m","max_tokens":1,"system":"be brief"}`), &p); err != nil {
   144  		t.Fatalf("unexpected error: %v", err)
   145  	}
   146  	if len(p.System) != 1 || p.System[0].Text != "be brief" {
   147  		t.Errorf("system string not promoted, got %#v", p.System)
   148  	}
   149  	var bm anthropic.BetaMessageParam
   150  	if err := json.Unmarshal([]byte(`{"role":"user","content":"hello"}`), &bm); err != nil {
   151  		t.Fatalf("unexpected error: %v", err)
   152  	}
   153  	if len(bm.Content) != 1 || bm.Content[0].OfText == nil || bm.Content[0].OfText.Text != "hello" {
   154  		t.Errorf("beta content string not promoted, got %#v", bm.Content)
   155  	}
   156  }
   157