| 1 | package bot |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "strings" |
| 6 | "testing" |
| 7 | ) |
| 8 | |
| 9 | // TestAnthBlockMarshalArglessToolUse pins the echo wire shape: a tool_use the |
| 10 | // model called with NO arguments must round-trip with an explicit empty |
| 11 | // input (the API 400s without it — "tool_use.input: Field required"), while |
| 12 | // other block shapes never grow an input field. |
| 13 | func TestAnthBlockMarshalArglessToolUse(t *testing.T) { |
| 14 | argless, err := json.Marshal(anthBlock{Type: "tool_use", ID: "tu_1", Name: "issues.list"}) |
| 15 | if err != nil { |
| 16 | t.Fatal(err) |
| 17 | } |
| 18 | if !strings.Contains(string(argless), `"input":{}`) { |
| 19 | t.Fatalf("argless tool_use must carry an explicit empty input: %s", argless) |
| 20 | } |
| 21 | |
| 22 | withArgs, _ := json.Marshal(anthBlock{Type: "tool_use", ID: "tu_2", Name: "fetch", |
| 23 | Input: map[string]any{"url": "https://a.com/"}}) |
| 24 | if !strings.Contains(string(withArgs), `"url":"https://a.com/"`) { |
| 25 | t.Fatalf("tool args lost: %s", withArgs) |
| 26 | } |
| 27 | |
| 28 | for _, b := range []anthBlock{ |
| 29 | {Type: "text", Text: "hi"}, |
| 30 | {Type: "thinking", Thinking: "hmm", Signature: "sig"}, |
| 31 | {Type: "tool_result", ToolUseID: "tu_1", Content: "ok"}, |
| 32 | } { |
| 33 | out, _ := json.Marshal(b) |
| 34 | if strings.Contains(string(out), `"input"`) { |
| 35 | t.Fatalf("%s block grew an input field: %s", b.Type, out) |
| 36 | } |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | // TestAnthBlockMarshalEmptyThinking pins the other half of the echo trap: a |
| 41 | // ZERO-LENGTH thinking block (adaptive thinking emits these on continuation |
| 42 | // turns — seen live from the read_app_source loop) must round-trip with an |
| 43 | // explicit empty thinking field, or the echo 400s ("thinking.thinking: Field |
| 44 | // required") and the whole reply goes silent. Non-thinking blocks never grow |
| 45 | // the field. |
| 46 | func TestAnthBlockMarshalEmptyThinking(t *testing.T) { |
| 47 | empty, err := json.Marshal(anthBlock{Type: "thinking", Signature: "sig"}) |
| 48 | if err != nil { |
| 49 | t.Fatal(err) |
| 50 | } |
| 51 | if !strings.Contains(string(empty), `"thinking":""`) { |
| 52 | t.Fatalf("empty thinking block must carry an explicit thinking field: %s", empty) |
| 53 | } |
| 54 | if !strings.Contains(string(empty), `"signature":"sig"`) { |
| 55 | t.Fatalf("signature lost from empty thinking block: %s", empty) |
| 56 | } |
| 57 | |
| 58 | full, _ := json.Marshal(anthBlock{Type: "thinking", Thinking: "hmm", Signature: "sig"}) |
| 59 | if !strings.Contains(string(full), `"thinking":"hmm"`) { |
| 60 | t.Fatalf("thinking text lost: %s", full) |
| 61 | } |
| 62 | |
| 63 | for _, b := range []anthBlock{ |
| 64 | {Type: "text", Text: "hi"}, |
| 65 | {Type: "tool_result", ToolUseID: "tu_1", Content: "ok"}, |
| 66 | } { |
| 67 | out, _ := json.Marshal(b) |
| 68 | if strings.Contains(string(out), `"thinking"`) { |
| 69 | t.Fatalf("%s block grew a thinking field: %s", b.Type, out) |
| 70 | } |
| 71 | } |
| 72 | } |
| 73 | |