github.com/anthropics/anthropic-sdk-go@v1.70.1/toolrunner/runner_test.go (about) 1 package toolrunner_test 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "sort" 8 "strings" 9 "sync" 10 "sync/atomic" 11 "testing" 12 "time" 13 14 anthropic "github.com/anthropics/anthropic-sdk-go" 15 "github.com/anthropics/anthropic-sdk-go/internal/testutil" 16 "github.com/anthropics/anthropic-sdk-go/option" 17 "github.com/anthropics/anthropic-sdk-go/toolrunner" 18 ) 19 20 // schemaToBytes converts a map schema to JSON bytes for use with NewBetaToolFromBytes. 21 func schemaToBytes(t *testing.T, schema map[string]any) []byte { 22 t.Helper() 23 bytes, err := json.Marshal(schema) 24 if err != nil { 25 t.Fatalf("marshal schema: %v", err) 26 } 27 return bytes 28 } 29 30 // assistantText extracts concatenated assistant text blocks from a BetaMessage. 31 func assistantText(msg *anthropic.BetaMessage) string { 32 var b strings.Builder 33 for _, c := range msg.Content { 34 if tb, ok := c.AsAny().(anthropic.BetaTextBlock); ok { 35 b.WriteString(tb.Text) 36 } 37 } 38 return b.String() 39 } 40 41 // Shared weather tool used by tests 42 type weatherRequest struct { 43 City string `json:"city"` 44 Units string `json:"units,omitempty"` 45 } 46 47 var weatherSchema = map[string]any{ 48 "type": "object", 49 "properties": map[string]any{ 50 "city": map[string]any{"type": "string"}, 51 "units": map[string]any{"type": "string", "enum": []string{"celsius", "fahrenheit"}}, 52 }, 53 "required": []string{"city"}, 54 } 55 56 func weatherTool(t *testing.T) anthropic.BetaTool { 57 t.Helper() 58 tool, err := toolrunner.NewBetaToolFromBytes("get_weather", "Get weather", schemaToBytes(t, weatherSchema), 59 func(ctx context.Context, req weatherRequest) (anthropic.BetaToolResultBlockParamContentUnion, error) { 60 temp := 20 61 if req.Units == "fahrenheit" { 62 temp = 68 63 } 64 return anthropic.BetaToolResultBlockParamContentUnion{ 65 OfText: &anthropic.BetaTextBlockParam{Text: fmt.Sprintf("The weather in %s is %d degrees %s.", req.City, temp, req.Units)}, 66 }, nil 67 }) 68 if err != nil { 69 t.Fatalf("create weather tool: %v", err) 70 } 71 return tool 72 } 73 74 func newClientWithVCR(t *testing.T, cassette string) anthropic.Client { 75 t.Helper() 76 httpClient, _ := testutil.NewVCRHTTPClient(t, cassette) 77 return anthropic.NewClient( 78 option.WithHTTPClient(httpClient), 79 option.WithAPIKey("test-key"), 80 ) 81 } 82 83 // Test All() end-to-end 84 85 func TestToolRunner_All_Basic(t *testing.T) { 86 t.Parallel() 87 client := newClientWithVCR(t, "tool_runner_basic") 88 tool := weatherTool(t) 89 90 runner := client.Beta.Messages.NewToolRunner([]anthropic.BetaTool{tool}, anthropic.BetaToolRunnerParams{ 91 BetaMessageNewParams: anthropic.BetaMessageNewParams{ 92 Model: anthropic.ModelClaudeSonnet4_5, 93 MaxTokens: 512, 94 Messages: []anthropic.BetaMessageParam{ 95 anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("What's the weather in San Francisco? Use fahrenheit.")), 96 }, 97 }, 98 MaxIterations: 5, 99 }) 100 101 ctx := context.Background() 102 var last *anthropic.BetaMessage 103 for msg, err := range runner.All(ctx) { 104 if err != nil { 105 t.Fatalf("runner error: %v", err) 106 } 107 last = msg 108 } 109 if last == nil { 110 t.Fatalf("no final message produced") 111 } 112 113 // Extract assistant text content concisely 114 got := []byte(assistantText(last) + "\n") 115 testutil.CompareGolden(t, "testdata/snapshots/tool_runner_basic.golden", got) 116 } 117 118 func TestToolRunner_RunToCompletion(t *testing.T) { 119 t.Parallel() 120 client := newClientWithVCR(t, "tool_runner_run_to_completion") 121 tool := weatherTool(t) 122 123 runner := client.Beta.Messages.NewToolRunner([]anthropic.BetaTool{tool}, anthropic.BetaToolRunnerParams{ 124 BetaMessageNewParams: anthropic.BetaMessageNewParams{ 125 Model: anthropic.ModelClaudeSonnet4_5, 126 MaxTokens: 512, 127 Messages: []anthropic.BetaMessageParam{ 128 anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("What's the weather in San Francisco? Use fahrenheit.")), 129 }, 130 }, 131 MaxIterations: 5, 132 }) 133 134 ctx := context.Background() 135 last, err := runner.RunToCompletion(ctx) 136 if err != nil { 137 t.Fatalf("RunToCompletion: %v", err) 138 } 139 140 // Extract assistant text content concisely 141 got := []byte(assistantText(last) + "\n") 142 testutil.CompareGolden(t, "testdata/snapshots/tool_runner_run_to_completion.golden", got) 143 } 144 145 // Test NextMessage step-wise, ensuring an intermediate tool_result is appended, then final answer 146 147 func TestToolRunner_NextMessage_Step(t *testing.T) { 148 t.Parallel() 149 client := newClientWithVCR(t, "tool_runner_next_message") 150 tool := weatherTool(t) 151 152 runner := client.Beta.Messages.NewToolRunner([]anthropic.BetaTool{tool}, anthropic.BetaToolRunnerParams{ 153 BetaMessageNewParams: anthropic.BetaMessageNewParams{ 154 Model: anthropic.ModelClaudeSonnet4_5, 155 MaxTokens: 512, 156 Messages: []anthropic.BetaMessageParam{ 157 anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("What's the weather in SF? Use celsius.")), 158 }, 159 }, 160 MaxIterations: 5, 161 }) 162 163 ctx := context.Background() 164 // Turn 1: expect tool_use 165 msg, err := runner.NextMessage(ctx) 166 if err != nil { 167 t.Fatalf("NextMessage 1: %v", err) 168 } 169 if msg == nil { 170 t.Fatalf("expected first message") 171 } 172 173 got := []byte(assistantText(msg) + "\n") 174 testutil.CompareGolden(t, "testdata/snapshots/tool_runner_next_message_step_1.golden", got) 175 // Turn 2: tool results sent, expect final assistant 176 msg, err = runner.NextMessage(ctx) 177 if err != nil { 178 t.Fatalf("NextMessage 2: %v", err) 179 } 180 if msg == nil { 181 t.Fatalf("expected final message") 182 } 183 184 got = []byte(assistantText(msg) + "\n") 185 testutil.CompareGolden(t, "testdata/snapshots/tool_runner_next_message_step_2.golden", got) 186 } 187 188 // Test AllStreaming end-to-end collects final text and compares 189 190 func TestToolRunner_AllStreaming(t *testing.T) { 191 t.Parallel() 192 client := newClientWithVCR(t, "tool_runner_streaming_all") 193 tool := weatherTool(t) 194 195 runner := client.Beta.Messages.NewToolRunnerStreaming([]anthropic.BetaTool{tool}, anthropic.BetaToolRunnerParams{ 196 BetaMessageNewParams: anthropic.BetaMessageNewParams{ 197 Model: anthropic.ModelClaudeSonnet4_5, 198 MaxTokens: 512, 199 Messages: []anthropic.BetaMessageParam{ 200 anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Weather in SF in fahrenheit?")), 201 }, 202 }, 203 MaxIterations: 5, 204 }) 205 206 ctx := context.Background() 207 assistantMessages := []string{} 208 for iterator, err := range runner.AllStreaming(ctx) { 209 if err != nil { 210 t.Fatalf("stream err: %v", err) 211 } 212 stringBuilder := strings.Builder{} 213 for ev := range iterator { 214 switch evVariant := ev.AsAny().(type) { 215 case anthropic.BetaRawContentBlockDeltaEvent: 216 switch deltaVariant := evVariant.Delta.AsAny().(type) { 217 case anthropic.BetaTextDelta: 218 stringBuilder.WriteString(deltaVariant.Text) 219 } 220 } 221 } 222 assistantMessages = append(assistantMessages, stringBuilder.String()) 223 } 224 225 got := []byte(strings.Join(assistantMessages, "\n")) 226 testutil.CompareGolden(t, "testdata/snapshots/tool_runner_streaming_all.golden", got) 227 } 228 229 // Test NextStreaming for a single turn; verify event types set is stable 230 231 func TestToolRunner_NextStreaming_EventTypes(t *testing.T) { 232 t.Parallel() 233 client := newClientWithVCR(t, "tool_runner_next_streaming") 234 tool := weatherTool(t) 235 236 runner := client.Beta.Messages.NewToolRunnerStreaming([]anthropic.BetaTool{tool}, anthropic.BetaToolRunnerParams{ 237 BetaMessageNewParams: anthropic.BetaMessageNewParams{ 238 Model: anthropic.ModelClaudeSonnet4_5, 239 MaxTokens: 512, 240 Messages: []anthropic.BetaMessageParam{ 241 anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Weather in SF?")), 242 }, 243 }, 244 MaxIterations: 1, 245 }) 246 247 ctx := context.Background() 248 events := runner.NextStreaming(ctx) 249 250 eventsTypes := []string{} 251 for ev := range events { 252 eventsTypes = append(eventsTypes, ev.Type) 253 } 254 255 sort.Strings(eventsTypes) 256 257 got := []byte(strings.Join(eventsTypes, "\n") + "\n") 258 testutil.CompareGolden(t, "testdata/snapshots/tool_runner_next_streaming_types.golden", got) 259 } 260 261 // Test that tool error is surfaced as a tool_result with is_error and the flow completes 262 263 func TestToolRunner_ToolCallError_ThenSuccess(t *testing.T) { 264 t.Parallel() 265 client := newClientWithVCR(t, "tool_runner_tool_call_error") 266 called := false 267 tool, err := toolrunner.NewBetaToolFromBytes("get_weather", "Get weather", schemaToBytes(t, weatherSchema), 268 func(ctx context.Context, req weatherRequest) (anthropic.BetaToolResultBlockParamContentUnion, error) { 269 if !called { 270 called = true 271 return anthropic.BetaToolResultBlockParamContentUnion{}, fmt.Errorf("Unexpected error, try again") 272 } 273 return anthropic.BetaToolResultBlockParamContentUnion{OfText: &anthropic.BetaTextBlockParam{Text: "Sunny 68°F"}}, nil 274 }) 275 if err != nil { 276 t.Fatalf("create tool: %v", err) 277 } 278 279 runner := client.Beta.Messages.NewToolRunner([]anthropic.BetaTool{tool}, anthropic.BetaToolRunnerParams{ 280 BetaMessageNewParams: anthropic.BetaMessageNewParams{ 281 Model: anthropic.ModelClaudeSonnet4_5, 282 MaxTokens: 512, 283 Messages: []anthropic.BetaMessageParam{ 284 anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Weather in San Francisco?")), 285 }, 286 }, 287 }) 288 289 ctx := context.Background() 290 msg, err := runner.RunToCompletion(ctx) 291 if err != nil { 292 t.Fatalf("RunToCompletion: %v", err) 293 } 294 295 messages := runner.Messages() 296 if len(messages) == 0 { 297 t.Fatalf("expected messages in runner") 298 } 299 300 // look through all the messages to find the tool results 301 // 1. should be an error 302 // 2. should be a success 303 toolResultBlocks := []*anthropic.BetaToolResultBlockParam{} 304 for _, msg := range messages { 305 for _, c := range msg.Content { 306 if tr := c.OfToolResult; tr != nil { 307 toolResultBlocks = append(toolResultBlocks, tr) 308 } 309 } 310 } 311 312 if len(toolResultBlocks) != 2 { 313 t.Fatalf("expected 2 tool result blocks, got %d", len(toolResultBlocks)) 314 } 315 316 errorToolResultBlock := toolResultBlocks[0] 317 if !errorToolResultBlock.IsError.Value { 318 t.Fatalf("expected first tool result to be an error") 319 } 320 errorText := errorToolResultBlock.Content[0].OfText.Text 321 if !strings.Contains(errorText, "Unexpected error") { 322 t.Fatalf("expected error message in tool result, got: %s", errorText) 323 } 324 325 successToolResultBlock := toolResultBlocks[1] 326 if successToolResultBlock.IsError.Value { 327 t.Fatalf("expected second tool result to be a success") 328 } 329 successText := successToolResultBlock.Content[0].OfText.Text 330 if successText != "Sunny 68°F" { 331 t.Fatalf("expected success message in tool result, got: %s", successText) 332 } 333 334 // Final assistant golden snapshot and iteration count 335 testutil.CompareGolden(t, "testdata/snapshots/tool_runner_tool_call_error_assistant.golden", []byte(assistantText(msg)+"\n")) 336 if runner.IterationCount() != 3 { 337 t.Fatalf("expected 3 iterations, got %d", runner.IterationCount()) 338 } 339 } 340 341 // Test custom handling: intercept tool_use, push our own tool_result, and disable tools for next turn 342 343 func TestToolRunner_CustomHandlingWithPushMessages(t *testing.T) { 344 t.Parallel() 345 client := newClientWithVCR(t, "tool_runner_custom_handling") 346 tool := weatherTool(t) 347 348 runner := client.Beta.Messages.NewToolRunner([]anthropic.BetaTool{tool}, anthropic.BetaToolRunnerParams{ 349 BetaMessageNewParams: anthropic.BetaMessageNewParams{ 350 Model: anthropic.ModelClaudeSonnet4_5, 351 MaxTokens: 512, 352 Messages: []anthropic.BetaMessageParam{ 353 anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("What's the weather in SF in celsius?")), 354 }, 355 }, 356 }) 357 358 ctx := context.Background() 359 // First assistant message with tool_use 360 msg, err := runner.NextMessage(ctx) 361 if err != nil || msg == nil { 362 t.Fatalf("turn 1: %v %v", msg, err) 363 } 364 // Find first tool_use id 365 var toolUseID string 366 for _, c := range msg.Content { 367 if tu, ok := c.AsAny().(anthropic.BetaToolUseBlock); ok { 368 toolUseID = tu.ID 369 break 370 } 371 } 372 if toolUseID == "" { 373 t.Fatalf("expected a tool_use block") 374 } 375 // Build a new runner with our custom tool_result appended to messages to avoid 376 // automatic execution for the prior assistant tool_use turn. 377 msgs := runner.Messages() 378 msgs = append(msgs, anthropic.NewBetaUserMessage( 379 anthropic.BetaContentBlockParamUnion{OfToolResult: &anthropic.BetaToolResultBlockParam{ToolUseID: toolUseID, Content: []anthropic.BetaToolResultBlockParamContentUnion{{OfText: &anthropic.BetaTextBlockParam{Text: "Celsius 20°C"}}}}}, 380 )) 381 382 // No tools so the next turn is just the assistant producing final text 383 runner2 := client.Beta.Messages.NewToolRunner(nil, anthropic.BetaToolRunnerParams{ 384 BetaMessageNewParams: anthropic.BetaMessageNewParams{ 385 Model: anthropic.ModelClaudeSonnet4_5, 386 MaxTokens: 512, 387 Messages: msgs, 388 }, 389 }) 390 391 // Next turn should finalize with assistant text 392 msg, err = runner2.NextMessage(ctx) 393 if err != nil || msg == nil { 394 t.Fatalf("turn 2: %v %v", msg, err) 395 } 396 } 397 398 // Test max iterations stops further calls 399 400 func TestToolRunner_MaxIterations(t *testing.T) { 401 t.Parallel() 402 client := newClientWithVCR(t, "tool_runner_max_iterations") 403 tool := weatherTool(t) 404 405 runner := client.Beta.Messages.NewToolRunner([]anthropic.BetaTool{tool}, anthropic.BetaToolRunnerParams{ 406 BetaMessageNewParams: anthropic.BetaMessageNewParams{ 407 Model: anthropic.ModelClaudeSonnet4_5, 408 MaxTokens: 512, 409 Messages: []anthropic.BetaMessageParam{ 410 anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Check weather in SF and NY, step by step")), 411 }, 412 }, 413 MaxIterations: 2, 414 }) 415 416 ctx := context.Background() 417 for { 418 msg, err := runner.NextMessage(ctx) 419 if msg == nil { 420 if err != nil { 421 t.Fatalf("runner error: %v", err) 422 } 423 break 424 } 425 } 426 if got := runner.IterationCount(); got != 2 { 427 t.Fatalf("expected 2 iterations, got %d", got) 428 } 429 } 430 431 // Test concurrent tool execution (multiple tools in one message) 432 433 func TestToolRunner_ConcurrentToolExecution(t *testing.T) { 434 t.Parallel() 435 client := newClientWithVCR(t, "tool_runner_concurrent") 436 437 // Track execution with timing to verify concurrency 438 var callCount atomic.Int32 439 var executionTimes sync.Map 440 startTime := time.Now() 441 442 weatherTool, err := toolrunner.NewBetaToolFromBytes("get_weather", "Get weather for a city", schemaToBytes(t, weatherSchema), 443 func(ctx context.Context, req weatherRequest) (anthropic.BetaToolResultBlockParamContentUnion, error) { 444 executionTimes.Store(req.City, time.Since(startTime)) 445 callCount.Add(1) 446 // Small delay - if sequential this would take 3x longer 447 time.Sleep(50 * time.Millisecond) 448 return anthropic.BetaToolResultBlockParamContentUnion{ 449 OfText: &anthropic.BetaTextBlockParam{Text: fmt.Sprintf("Weather in %s: Sunny 72°F", req.City)}, 450 }, nil 451 }) 452 if err != nil { 453 t.Fatalf("create weather tool: %v", err) 454 } 455 456 runner := client.Beta.Messages.NewToolRunner([]anthropic.BetaTool{weatherTool}, anthropic.BetaToolRunnerParams{ 457 BetaMessageNewParams: anthropic.BetaMessageNewParams{ 458 Model: anthropic.ModelClaudeSonnet4_5, 459 MaxTokens: 512, 460 Messages: []anthropic.BetaMessageParam{ 461 anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock( 462 "What's the weather in San Francisco, New York, and London? Check all three cities at once.", 463 )), 464 }, 465 }, 466 MaxIterations: 5, 467 }) 468 469 ctx := context.Background() 470 _, err = runner.RunToCompletion(ctx) 471 if err != nil { 472 t.Fatalf("RunToCompletion: %v", err) 473 } 474 475 // Verify multiple tools were called 476 count := callCount.Load() 477 if count < 2 { 478 t.Fatalf("expected at least 2 concurrent tool calls, got %d", count) 479 } 480 481 // Verify tool results are in the messages 482 messages := runner.Messages() 483 toolResultCount := 0 484 for _, msg := range messages { 485 for _, c := range msg.Content { 486 if c.OfToolResult != nil { 487 toolResultCount++ 488 } 489 } 490 } 491 if toolResultCount < 2 { 492 t.Fatalf("expected at least 2 tool results, got %d", toolResultCount) 493 } 494 } 495 496 // Test context cancellation during tool execution 497 498 func TestToolRunner_ContextCancellation(t *testing.T) { 499 t.Parallel() 500 client := newClientWithVCR(t, "tool_runner_context_cancel") 501 502 toolStarted := make(chan struct{}) 503 toolCompleted := make(chan struct{}) 504 505 slowSchema := map[string]any{ 506 "type": "object", 507 "properties": map[string]any{"input": map[string]any{"type": "string"}}, 508 } 509 slowTool, err := toolrunner.NewBetaToolFromBytes("slow_tool", "A slow tool", schemaToBytes(t, slowSchema), 510 func(ctx context.Context, req struct{ Input string }) (anthropic.BetaToolResultBlockParamContentUnion, error) { 511 close(toolStarted) 512 // Wait for context cancellation or timeout 513 select { 514 case <-ctx.Done(): 515 return anthropic.BetaToolResultBlockParamContentUnion{}, ctx.Err() 516 case <-time.After(5 * time.Second): 517 close(toolCompleted) 518 return anthropic.BetaToolResultBlockParamContentUnion{ 519 OfText: &anthropic.BetaTextBlockParam{Text: "completed"}, 520 }, nil 521 } 522 }) 523 if err != nil { 524 t.Fatalf("create slow tool: %v", err) 525 } 526 527 runner := client.Beta.Messages.NewToolRunner([]anthropic.BetaTool{slowTool}, anthropic.BetaToolRunnerParams{ 528 BetaMessageNewParams: anthropic.BetaMessageNewParams{ 529 Model: anthropic.ModelClaudeSonnet4_5, 530 MaxTokens: 512, 531 Messages: []anthropic.BetaMessageParam{ 532 anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Call the slow_tool with input 'test'")), 533 }, 534 }, 535 MaxIterations: 5, 536 }) 537 538 ctx, cancel := context.WithCancel(context.Background()) 539 defer cancel() 540 541 // Start the runner in a goroutine 542 done := make(chan struct{}) 543 go func() { 544 defer close(done) 545 runner.RunToCompletion(ctx) 546 }() 547 548 // Wait for tool to start, then cancel 549 select { 550 case <-toolStarted: 551 cancel() 552 case <-time.After(10 * time.Second): 553 t.Fatal("tool never started") 554 } 555 556 // Verify runner completes quickly after cancellation (not waiting 5 seconds) 557 select { 558 case <-done: 559 // Good - runner completed 560 case <-toolCompleted: 561 t.Fatal("tool completed without cancellation being respected") 562 case <-time.After(2 * time.Second): 563 t.Fatal("runner did not complete promptly after cancellation") 564 } 565 } 566 567 // Test malformed JSON input error handling through Execute 568 569 func TestToolRunner_MalformedJSONInput(t *testing.T) { 570 t.Parallel() 571 572 type StrictInput struct { 573 RequiredField string `json:"required_field"` 574 NumberField int `json:"number_field"` 575 } 576 577 strictSchema := map[string]any{ 578 "type": "object", 579 "properties": map[string]any{ 580 "required_field": map[string]any{"type": "string"}, 581 "number_field": map[string]any{"type": "integer"}, 582 }, 583 "required": []string{"required_field"}, 584 } 585 tool, err := toolrunner.NewBetaToolFromBytes("strict_tool", "A tool with strict input", schemaToBytes(t, strictSchema), 586 func(ctx context.Context, input StrictInput) (anthropic.BetaToolResultBlockParamContentUnion, error) { 587 return anthropic.BetaToolResultBlockParamContentUnion{ 588 OfText: &anthropic.BetaTextBlockParam{Text: "success"}, 589 }, nil 590 }) 591 if err != nil { 592 t.Fatalf("create tool: %v", err) 593 } 594 595 // Test Execute with valid JSON 596 validJSON := json.RawMessage(`{"required_field": "test", "number_field": 42}`) 597 results, err := tool.Execute(context.Background(), validJSON) 598 if err != nil { 599 t.Fatalf("Execute valid JSON failed: %v", err) 600 } 601 if len(results) != 1 || results[0].OfText == nil || results[0].OfText.Text != "success" { 602 t.Fatalf("Execute returned unexpected result: %+v", results) 603 } 604 605 // Test Execute with malformed JSON (invalid syntax) 606 malformedJSON := json.RawMessage(`{"required_field": "test", "number_field": }`) 607 _, err = tool.Execute(context.Background(), malformedJSON) 608 if err == nil { 609 t.Fatal("expected error for malformed JSON, got nil") 610 } 611 612 // Test Execute with type mismatch (string instead of int) 613 typeMismatchJSON := json.RawMessage(`{"required_field": "test", "number_field": "not a number"}`) 614 _, err = tool.Execute(context.Background(), typeMismatchJSON) 615 if err == nil { 616 t.Fatal("expected error for type mismatch, got nil") 617 } 618 619 // Test Execute with invalid JSON propagates error 620 invalidJSON := json.RawMessage(`{invalid json}`) 621 _, err = tool.Execute(context.Background(), invalidJSON) 622 if err == nil { 623 t.Fatal("expected error for invalid JSON in Execute") 624 } 625 }