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

     1  package anthropic
     2  
     3  import (
     4  	"encoding/json"
     5  	"strings"
     6  	"testing"
     7  
     8  	"github.com/invopop/jsonschema"
     9  	orderedmap "github.com/pb33f/ordered-map/v2"
    10  )
    11  
    12  func ptr[T any](v T) *T { return &v }
    13  
    14  func props(pairs ...any) *orderedmap.OrderedMap[string, *jsonschema.Schema] {
    15  	m := orderedmap.New[string, *jsonschema.Schema]()
    16  	for i := 0; i < len(pairs); i += 2 {
    17  		m.Set(pairs[i].(string), pairs[i+1].(*jsonschema.Schema))
    18  	}
    19  	return m
    20  }
    21  
    22  // normalizeJSON round-trips JSON through any to normalize key ordering,
    23  // since json.Marshal sorts map keys deterministically.
    24  func normalizeJSON(s string) string {
    25  	var v any
    26  	if err := json.Unmarshal([]byte(s), &v); err != nil {
    27  		return s
    28  	}
    29  	b, _ := json.Marshal(v)
    30  	return string(b)
    31  }
    32  
    33  func TestTransformSchema(t *testing.T) {
    34  	tests := []struct {
    35  		name     string
    36  		input    *jsonschema.Schema
    37  		expected string // expected JSON after transform
    38  	}{
    39  		{
    40  			name:     "nil schema is a no-op",
    41  			input:    nil,
    42  			expected: "",
    43  		},
    44  		{
    45  			name: "integer with unsupported constraints moves them to description",
    46  			input: &jsonschema.Schema{
    47  				Type:        "integer",
    48  				Minimum:     "1",
    49  				Maximum:     "10",
    50  				Description: "A number",
    51  			},
    52  			expected: `{"type":"integer","description":"A number\n\n{maximum: 10, minimum: 1}"}`,
    53  		},
    54  		{
    55  			name: "object gets additionalProperties false and recurses into properties",
    56  			input: &jsonschema.Schema{
    57  				Type: "object",
    58  				Properties: props(
    59  					"name", &jsonschema.Schema{Type: "string"},
    60  					"age", &jsonschema.Schema{Type: "integer"},
    61  				),
    62  				Required: []string{"name"},
    63  			},
    64  			expected: `{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"integer"}},"additionalProperties":false,"required":["name"]}`,
    65  		},
    66  		{
    67  			name: "object without properties gets empty properties",
    68  			input: &jsonschema.Schema{
    69  				Type: "object",
    70  			},
    71  			expected: `{"type":"object","properties":{},"additionalProperties":false}`,
    72  		},
    73  		{
    74  			name: "supported string format is preserved",
    75  			input: &jsonschema.Schema{
    76  				Type:   "string",
    77  				Format: "date-time",
    78  			},
    79  			expected: `{"type":"string","format":"date-time"}`,
    80  		},
    81  		{
    82  			name: "unsupported string format moves to description",
    83  			input: &jsonschema.Schema{
    84  				Type:   "string",
    85  				Format: "binary",
    86  			},
    87  			expected: `{"type":"string","description":"{format: binary}"}`,
    88  		},
    89  		{
    90  			name: "array with minItems 1 is preserved",
    91  			input: &jsonschema.Schema{
    92  				Type:     "array",
    93  				Items:    &jsonschema.Schema{Type: "string"},
    94  				MinItems: ptr(uint64(1)),
    95  			},
    96  			expected: `{"type":"array","items":{"type":"string"},"minItems":1}`,
    97  		},
    98  		{
    99  			name: "array with minItems 5 moves to description",
   100  			input: &jsonschema.Schema{
   101  				Type:     "array",
   102  				Items:    &jsonschema.Schema{Type: "string"},
   103  				MinItems: ptr(uint64(5)),
   104  			},
   105  			expected: `{"type":"array","items":{"type":"string"},"description":"{minItems: 5}"}`,
   106  		},
   107  		{
   108  			name: "array recurses into items",
   109  			input: &jsonschema.Schema{
   110  				Type: "array",
   111  				Items: &jsonschema.Schema{
   112  					Type: "object",
   113  					Properties: props(
   114  						"id", &jsonschema.Schema{Type: "integer"},
   115  					),
   116  				},
   117  			},
   118  			expected: `{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"}},"additionalProperties":false}}`,
   119  		},
   120  		{
   121  			name: "$ref strips all other properties",
   122  			input: &jsonschema.Schema{
   123  				Ref:  "#/definitions/Person",
   124  				Type: "object",
   125  			},
   126  			expected: `{"$ref":"#/definitions/Person"}`,
   127  		},
   128  		{
   129  			name: "oneOf converted to anyOf",
   130  			input: &jsonschema.Schema{
   131  				OneOf: []*jsonschema.Schema{
   132  					{Type: "string"},
   133  					{Type: "number"},
   134  				},
   135  			},
   136  			expected: `{"anyOf":[{"type":"string"},{"type":"number"}]}`,
   137  		},
   138  		{
   139  			name: "anyOf variants are recursively transformed",
   140  			input: &jsonschema.Schema{
   141  				AnyOf: []*jsonschema.Schema{
   142  					{Type: "string"},
   143  					{Type: "object", Properties: props("x", &jsonschema.Schema{Type: "integer"})},
   144  				},
   145  			},
   146  			expected: `{"anyOf":[{"type":"string"},{"type":"object","properties":{"x":{"type":"integer"}},"additionalProperties":false}]}`,
   147  		},
   148  		{
   149  			name: "$defs are recursively transformed",
   150  			input: &jsonschema.Schema{
   151  				Type: "object",
   152  				Definitions: jsonschema.Definitions{
   153  					"Person": &jsonschema.Schema{
   154  						Type: "object",
   155  						Properties: props(
   156  							"name", &jsonschema.Schema{Type: "string"},
   157  						),
   158  					},
   159  				},
   160  				Properties: props(
   161  					"user", &jsonschema.Schema{Ref: "#/$defs/Person"},
   162  				),
   163  			},
   164  			expected: `{"type":"object","$defs":{"Person":{"type":"object","properties":{"name":{"type":"string"}},"additionalProperties":false}},"properties":{"user":{"$ref":"#/$defs/Person"}},"additionalProperties":false}`,
   165  		},
   166  		{
   167  			name: "no type and no anyOf clears the schema",
   168  			input: &jsonschema.Schema{
   169  				Description: "orphan",
   170  			},
   171  			// Annotation-only schemas are zeroed; a zero jsonschema.Schema
   172  			// marshals as `true` (the "match everything" schema).
   173  			expected: `true`,
   174  		},
   175  		{
   176  			name: "$schema version is stripped",
   177  			input: &jsonschema.Schema{
   178  				Version: "https://json-schema.org/draft/2020-12/schema",
   179  				Type:    "object",
   180  				Properties: props(
   181  					"name", &jsonschema.Schema{Type: "string"},
   182  				),
   183  			},
   184  			expected: `{"type":"object","properties":{"name":{"type":"string"}},"additionalProperties":false,"description":"{$schema: https://json-schema.org/draft/2020-12/schema}"}`,
   185  		},
   186  		{
   187  			name: "invalid anyOf variants are dropped, not serialized as true",
   188  			input: &jsonschema.Schema{
   189  				AnyOf: []*jsonschema.Schema{
   190  					{Type: "string"},
   191  					{Description: "orphan: no type, no anyOf"},
   192  				},
   193  			},
   194  			expected: `{"anyOf":[{"type":"string"}]}`,
   195  		},
   196  		{
   197  			name: "invalid oneOf variants are dropped during conversion to anyOf",
   198  			input: &jsonschema.Schema{
   199  				OneOf: []*jsonschema.Schema{
   200  					{Type: "number"},
   201  					{Description: "orphan"},
   202  				},
   203  			},
   204  			expected: `{"anyOf":[{"type":"number"}]}`,
   205  		},
   206  		{
   207  			name: "unsupported not field renders as JSON in description",
   208  			input: &jsonschema.Schema{
   209  				Type: "string",
   210  				Not:  &jsonschema.Schema{Type: "number"},
   211  			},
   212  			expected: `{"type":"string","description":"{not: {\"type\":\"number\"}}"}`,
   213  		},
   214  		{
   215  			name: "enum is preserved",
   216  			input: &jsonschema.Schema{
   217  				Type: "string",
   218  				Enum: []any{"red", "green", "blue"},
   219  			},
   220  			expected: `{"type":"string","enum":["red","green","blue"]}`,
   221  		},
   222  		{
   223  			name: "enum without type is preserved",
   224  			input: &jsonschema.Schema{
   225  				Enum: []any{"a", "b"},
   226  			},
   227  			expected: `{"enum":["a","b"]}`,
   228  		},
   229  		{
   230  			name: "const is preserved",
   231  			input: &jsonschema.Schema{
   232  				Type:  "string",
   233  				Const: "fixed",
   234  			},
   235  			expected: `{"type":"string","const":"fixed"}`,
   236  		},
   237  		{
   238  			name: "const without type is preserved",
   239  			input: &jsonschema.Schema{
   240  				Const: 42,
   241  			},
   242  			expected: `{"const":42}`,
   243  		},
   244  		{
   245  			name: "string pattern is preserved",
   246  			input: &jsonschema.Schema{
   247  				Type:    "string",
   248  				Pattern: "^[a-z]+$",
   249  			},
   250  			expected: `{"type":"string","pattern":"^[a-z]+$"}`,
   251  		},
   252  		{
   253  			name: "allOf variants are preserved and recursively transformed",
   254  			input: &jsonschema.Schema{
   255  				AllOf: []*jsonschema.Schema{
   256  					{Type: "object", Properties: props("a", &jsonschema.Schema{Type: "string"})},
   257  					{Type: "object", Properties: props("b", &jsonschema.Schema{Type: "integer"}), Required: []string{"b"}},
   258  				},
   259  			},
   260  			expected: `{"allOf":[{"type":"object","properties":{"a":{"type":"string"}},"additionalProperties":false},{"type":"object","properties":{"b":{"type":"integer"}},"additionalProperties":false,"required":["b"]}]}`,
   261  		},
   262  	}
   263  
   264  	for _, tt := range tests {
   265  		t.Run(tt.name, func(t *testing.T) {
   266  			if tt.input == nil {
   267  				// Just verify no panic
   268  				transformSchema(nil)
   269  				return
   270  			}
   271  			transformSchema(tt.input)
   272  			gotBytes, _ := json.Marshal(tt.input)
   273  			got := normalizeJSON(string(gotBytes))
   274  			want := normalizeJSON(tt.expected)
   275  			if got != want {
   276  				// Pretty print for readable diffs
   277  				var gotAny, wantAny any
   278  				json.Unmarshal([]byte(got), &gotAny)
   279  				json.Unmarshal([]byte(tt.expected), &wantAny)
   280  				gotPretty, _ := json.MarshalIndent(gotAny, "", "  ")
   281  				wantPretty, _ := json.MarshalIndent(wantAny, "", "  ")
   282  				t.Errorf("transformSchema() mismatch:\ngot:\n%s\nwant:\n%s", gotPretty, wantPretty)
   283  			}
   284  		})
   285  	}
   286  }
   287  
   288  func TestTransformSchemaFromReflector(t *testing.T) {
   289  	type OrderItem struct {
   290  		Name     string  `json:"name"`
   291  		Quantity int     `json:"quantity" jsonschema:"minimum=1"`
   292  		Price    float64 `json:"price"`
   293  	}
   294  	type Order struct {
   295  		Items    []OrderItem `json:"items" jsonschema:"description=List of items"`
   296  		Total    float64     `json:"total"`
   297  		Currency string      `json:"currency" jsonschema:"enum=USD,enum=EUR"`
   298  	}
   299  
   300  	reflector := jsonschema.Reflector{DoNotReference: true}
   301  	schema := reflector.Reflect(&Order{})
   302  	transformSchema(schema)
   303  
   304  	result, err := json.Marshal(schema)
   305  	if err != nil {
   306  		t.Fatalf("failed to marshal: %v", err)
   307  	}
   308  
   309  	var m map[string]any
   310  	if err := json.Unmarshal(result, &m); err != nil {
   311  		t.Fatalf("failed to unmarshal: %v", err)
   312  	}
   313  
   314  	// Top-level object must have additionalProperties: false
   315  	if ap, ok := m["additionalProperties"]; !ok || ap != false {
   316  		t.Errorf("expected top-level additionalProperties=false, got %v", m["additionalProperties"])
   317  	}
   318  
   319  	// $schema should be stripped (moved to description)
   320  	if _, ok := m["$schema"]; ok {
   321  		t.Error("expected $schema to be stripped from output")
   322  	}
   323  
   324  	// Properties should exist
   325  	props, ok := m["properties"].(map[string]any)
   326  	if !ok {
   327  		t.Fatal("expected properties map")
   328  	}
   329  	for _, key := range []string{"items", "total", "currency"} {
   330  		if _, ok := props[key]; !ok {
   331  			t.Errorf("expected property %q", key)
   332  		}
   333  	}
   334  
   335  	// Nested items schema should also have additionalProperties: false
   336  	itemsProp, ok := props["items"].(map[string]any)
   337  	if !ok {
   338  		t.Fatal("expected items property to be a map")
   339  	}
   340  	itemsSchema, ok := itemsProp["items"].(map[string]any)
   341  	if !ok {
   342  		t.Fatal("expected items.items (array element schema) to be a map")
   343  	}
   344  	if ap, ok := itemsSchema["additionalProperties"]; !ok || ap != false {
   345  		t.Errorf("expected nested additionalProperties=false, got %v", itemsSchema["additionalProperties"])
   346  	}
   347  
   348  	// Unsupported constraint (minimum=1 on quantity) should be in description
   349  	quantitySchema, ok := itemsSchema["properties"].(map[string]any)["quantity"].(map[string]any)
   350  	if !ok {
   351  		t.Fatal("expected quantity property")
   352  	}
   353  	desc, _ := quantitySchema["description"].(string)
   354  	if desc == "" {
   355  		t.Error("expected quantity to have a description with the minimum constraint")
   356  	}
   357  }
   358  
   359  // Keywords like maxLength and maxItems are pointer-typed on jsonschema.Schema, so
   360  // the description must render the pointed-to value rather than the address.
   361  func TestTransformSchemaFromReflectorPointerKeywords(t *testing.T) {
   362  	type Input struct {
   363  		Code string   `json:"code" jsonschema:"minLength=2,maxLength=8"`
   364  		Tags []string `json:"tags" jsonschema:"maxItems=3"`
   365  	}
   366  
   367  	reflector := jsonschema.Reflector{DoNotReference: true}
   368  	schema := reflector.Reflect(&Input{})
   369  	transformSchema(schema)
   370  
   371  	tests := []struct {
   372  		property string
   373  		want     string
   374  	}{
   375  		{"code", "{maxLength: 8, minLength: 2}"},
   376  		{"tags", "{maxItems: 3}"},
   377  	}
   378  	for _, tt := range tests {
   379  		t.Run(tt.property+" keywords render as values in the description", func(t *testing.T) {
   380  			prop, ok := schema.Properties.Get(tt.property)
   381  			if !ok {
   382  				t.Fatalf("expected property %q", tt.property)
   383  			}
   384  			if strings.Contains(prop.Description, "0x") {
   385  				t.Errorf("description contains a pointer address: %q", prop.Description)
   386  			}
   387  			if prop.Description != tt.want {
   388  				t.Errorf("description = %q, want %q", prop.Description, tt.want)
   389  			}
   390  		})
   391  	}
   392  }
   393  
   394  func TestFormatExtraValue(t *testing.T) {
   395  	tests := []struct {
   396  		name string
   397  		v    any
   398  		want string
   399  	}{
   400  		{"pointer to integer renders the value", ptr(uint64(8)), "8"},
   401  		{"nil pointer renders as null", (*uint64)(nil), "null"},
   402  		{"untyped nil renders as null", nil, "null"},
   403  		{"plain integer", uint64(3), "3"},
   404  		{"string stays unquoted", "date", "date"},
   405  		{"pointer to schema is JSON-marshaled", &jsonschema.Schema{Type: "string"}, `{"type":"string"}`},
   406  	}
   407  	for _, tt := range tests {
   408  		t.Run(tt.name, func(t *testing.T) {
   409  			if got := formatExtraValue(tt.v); got != tt.want {
   410  				t.Errorf("formatExtraValue(%#v) = %q, want %q", tt.v, got, tt.want)
   411  			}
   412  		})
   413  	}
   414  }
   415  
   416  func TestTransformSchemaMap(t *testing.T) {
   417  	tests := []struct {
   418  		name     string
   419  		input    map[string]any
   420  		expected map[string]any
   421  	}{
   422  		{
   423  			name: "basic integer with unsupported properties",
   424  			input: map[string]any{
   425  				"type":        "integer",
   426  				"minimum":     1,
   427  				"maximum":     10,
   428  				"description": "A number",
   429  			},
   430  			expected: map[string]any{
   431  				"type":        "integer",
   432  				"description": "A number\n\n{maximum: 10, minimum: 1}",
   433  			},
   434  		},
   435  		{
   436  			name: "object with properties",
   437  			input: map[string]any{
   438  				"type": "object",
   439  				"properties": map[string]any{
   440  					"name": map[string]any{
   441  						"type": "string",
   442  					},
   443  					"age": map[string]any{
   444  						"type": "integer",
   445  					},
   446  				},
   447  				"required":             []string{"name"},
   448  				"additionalProperties": true,
   449  			},
   450  			expected: map[string]any{
   451  				"type": "object",
   452  				"properties": map[string]any{
   453  					"name": map[string]any{
   454  						"type": "string",
   455  					},
   456  					"age": map[string]any{
   457  						"type": "integer",
   458  					},
   459  				},
   460  				"required":             []string{"name"},
   461  				"additionalProperties": false,
   462  			},
   463  		},
   464  		{
   465  			name: "string with supported format",
   466  			input: map[string]any{
   467  				"type":   "string",
   468  				"format": "date-time",
   469  			},
   470  			expected: map[string]any{
   471  				"type":   "string",
   472  				"format": "date-time",
   473  			},
   474  		},
   475  		{
   476  			name: "string with unsupported format",
   477  			input: map[string]any{
   478  				"type":   "string",
   479  				"format": "binary",
   480  			},
   481  			expected: map[string]any{
   482  				"type":        "string",
   483  				"description": "{format: binary}",
   484  			},
   485  		},
   486  		{
   487  			name: "array with minItems 1",
   488  			input: map[string]any{
   489  				"type": "array",
   490  				"items": map[string]any{
   491  					"type": "string",
   492  				},
   493  				"minItems": 1,
   494  			},
   495  			expected: map[string]any{
   496  				"type": "array",
   497  				"items": map[string]any{
   498  					"type": "string",
   499  				},
   500  				"minItems": 1,
   501  			},
   502  		},
   503  		{
   504  			name: "array with minItems 5",
   505  			input: map[string]any{
   506  				"type": "array",
   507  				"items": map[string]any{
   508  					"type": "string",
   509  				},
   510  				"minItems": 5,
   511  			},
   512  			expected: map[string]any{
   513  				"type": "array",
   514  				"items": map[string]any{
   515  					"type": "string",
   516  				},
   517  				"description": "{minItems: 5}",
   518  			},
   519  		},
   520  		{
   521  			name: "schema with $ref",
   522  			input: map[string]any{
   523  				"$ref": "#/definitions/Person",
   524  				"type": "object",
   525  			},
   526  			expected: map[string]any{
   527  				"$ref": "#/definitions/Person",
   528  			},
   529  		},
   530  		{
   531  			name: "schema with anyOf",
   532  			input: map[string]any{
   533  				"anyOf": []any{
   534  					map[string]any{
   535  						"type": "string",
   536  					},
   537  					map[string]any{
   538  						"type": "number",
   539  					},
   540  				},
   541  			},
   542  			expected: map[string]any{
   543  				"anyOf": []any{
   544  					map[string]any{
   545  						"type": "string",
   546  					},
   547  					map[string]any{
   548  						"type": "number",
   549  					},
   550  				},
   551  			},
   552  		},
   553  		{
   554  			name: "schema with oneOf converted to anyOf",
   555  			input: map[string]any{
   556  				"oneOf": []any{
   557  					map[string]any{
   558  						"type": "string",
   559  					},
   560  					map[string]any{
   561  						"type": "number",
   562  					},
   563  				},
   564  			},
   565  			expected: map[string]any{
   566  				"anyOf": []any{
   567  					map[string]any{
   568  						"type": "string",
   569  					},
   570  					map[string]any{
   571  						"type": "number",
   572  					},
   573  				},
   574  			},
   575  		},
   576  		{
   577  			name: "schema with $defs",
   578  			input: map[string]any{
   579  				"type": "object",
   580  				"$defs": map[string]any{
   581  					"Person": map[string]any{
   582  						"type": "object",
   583  						"properties": map[string]any{
   584  							"name": map[string]any{
   585  								"type": "string",
   586  							},
   587  						},
   588  					},
   589  				},
   590  				"properties": map[string]any{
   591  					"user": map[string]any{
   592  						"$ref": "#/$defs/Person",
   593  					},
   594  				},
   595  			},
   596  			expected: map[string]any{
   597  				"type": "object",
   598  				"$defs": map[string]any{
   599  					"Person": map[string]any{
   600  						"type": "object",
   601  						"properties": map[string]any{
   602  							"name": map[string]any{
   603  								"type": "string",
   604  							},
   605  						},
   606  						"additionalProperties": false,
   607  					},
   608  				},
   609  				"properties": map[string]any{
   610  					"user": map[string]any{
   611  						"$ref": "#/$defs/Person",
   612  					},
   613  				},
   614  				"additionalProperties": false,
   615  			},
   616  		},
   617  		{
   618  			name:     "nil schema returns nil",
   619  			input:    nil,
   620  			expected: nil,
   621  		},
   622  		{
   623  			name: "schema without type returns nil",
   624  			input: map[string]any{
   625  				"description": "A schema without type",
   626  			},
   627  			expected: nil,
   628  		},
   629  		{
   630  			name: "array items false remains false",
   631  			input: map[string]any{
   632  				"type":  "array",
   633  				"items": false,
   634  			},
   635  			expected: map[string]any{
   636  				"type":  "array",
   637  				"items": false,
   638  			},
   639  		},
   640  		{
   641  			name: "object property false remains false",
   642  			input: map[string]any{
   643  				"type": "object",
   644  				"properties": map[string]any{
   645  					"x": false,
   646  				},
   647  			},
   648  			expected: map[string]any{
   649  				"type": "object",
   650  				"properties": map[string]any{
   651  					"x": false,
   652  				},
   653  				"additionalProperties": false,
   654  			},
   655  		},
   656  	}
   657  
   658  	for _, tt := range tests {
   659  		t.Run(tt.name, func(t *testing.T) {
   660  			result := transformSchemaMap(tt.input)
   661  			// Compare via JSON to avoid Go type mismatches (e.g. []string vs []any,
   662  			// int vs float64) that arise from the JSON round-trip in transformSchemaMap.
   663  			resultJSON, _ := json.Marshal(result)
   664  			expectedJSON, _ := json.Marshal(tt.expected)
   665  			if string(resultJSON) != string(expectedJSON) {
   666  				resultPretty, _ := json.MarshalIndent(result, "", "  ")
   667  				expectedPretty, _ := json.MarshalIndent(tt.expected, "", "  ")
   668  				t.Errorf("transformSchemaMap() mismatch:\ngot:\n%s\nwant:\n%s", resultPretty, expectedPretty)
   669  			}
   670  		})
   671  	}
   672  }
   673  
   674  func TestStructuredSchemaHelpersPreserveFalseSubschemas(t *testing.T) {
   675  	tests := []struct {
   676  		name     string
   677  		got      any
   678  		expected string
   679  	}{
   680  		{
   681  			name:     "BetaJSONSchemaOutputFormat preserves false child schemas",
   682  			got:      BetaJSONSchemaOutputFormat(map[string]any{"type": "array", "items": false}).Schema,
   683  			expected: `{"type":"array","items":false}`,
   684  		},
   685  		{
   686  			name:     "BetaToolInputSchema preserves false child schemas",
   687  			got:      BetaToolInputSchema(map[string]any{"type": "object", "properties": map[string]any{"x": false}}).ExtraFields,
   688  			expected: `{"type":"object","properties":{"x":false},"additionalProperties":false}`,
   689  		},
   690  	}
   691  
   692  	for _, tt := range tests {
   693  		t.Run(tt.name, func(t *testing.T) {
   694  			got, _ := json.Marshal(tt.got)
   695  			if normalizeJSON(string(got)) != normalizeJSON(tt.expected) {
   696  				t.Errorf("schema helper mismatch:\ngot:\n%s\nwant:\n%s", got, tt.expected)
   697  			}
   698  		})
   699  	}
   700  }