package api import ( "encoding/json" "strings" "testing" ) // TestContentBlock_ToolUseAlwaysEmitsInput locks the fix for the MiniMax 400 // regression: tool_use blocks must always serialize an "input" field, even // when the tool was called with no arguments. Without this guard, the default // map[string]any + omitempty combo silently drops `input` from the wire, // triggering "invalid params, invalid function arguments json string" on the // MiniMax Anthropic-compat endpoint. // // TestContentBlock_ToolUseAlwaysEmitsInput 锁住 MiniMax 400 回归修复: // tool_use 块必须永远序列化 "input" 字段, 即使工具无参数. 没这个保险, // 默认 map[string]any + omitempty 组合会从 wire 上默默丢掉 input, 在 MiniMax // Anthropic-compat 端触发 "invalid params, invalid function arguments json string". func TestContentBlock_ToolUseAlwaysEmitsInput(t *testing.T) { cases := []struct { name string in map[string]any }{ {name: "nil input", in: nil}, {name: "empty input", in: map[string]any{}}, {name: "non-empty input", in: map[string]any{"command": "echo hi"}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { b, err := json.Marshal(ContentBlock{ Type: "tool_use", ID: "toolu_01abc", Name: "Bash", Input: tc.in, }) if err != nil { t.Fatalf("marshal failed: %v", err) } s := string(b) if !strings.Contains(s, `"input":`) { t.Errorf("tool_use must always emit \"input\" field, got %s", s) } if tc.in == nil || len(tc.in) == 0 { if !strings.Contains(s, `"input":{}`) { t.Errorf("nil/empty input must serialize as \"input\":{}, got %s", s) } } }) } } // TestContentBlock_NonToolUseRespectsOmitempty guards the regression that a // well-meaning fix could accidentally make text/thinking/tool_result blocks // always emit an empty "input":{} field, which is non-spec for those types. // // TestContentBlock_NonToolUseRespectsOmitempty 防止善意的修复意外让 // text/thinking/tool_result 块也总是输出空 "input":{} 字段, 这对那些类型不合规. func TestContentBlock_NonToolUseRespectsOmitempty(t *testing.T) { cases := []ContentBlock{ {Type: "text", Text: "hello"}, {Type: "thinking", Text: "let me think", Signature: "sig123"}, {Type: "tool_result", ToolUseID: "toolu_01abc", Content: "result"}, } for _, c := range cases { t.Run(c.Type, func(t *testing.T) { b, err := json.Marshal(c) if err != nil { t.Fatalf("marshal failed: %v", err) } s := string(b) if strings.Contains(s, `"input"`) { t.Errorf("non-tool_use block must not emit \"input\", got %s", s) } }) } } // TestContentBlock_ToolResultArrayForm locks the MarshalJSON 3-case decision // for tool_result content field (advisor checkpoint): // 1. ContentItems non-empty → emit "content":[{...},{...}] array // 2. ContentItems empty + Content non-empty → emit "content":"string" // 3. both empty → omit "content" (omitempty default) // Catches the L699/022ea20-class silent-drop regression where adding an // array-form path could accidentally suppress the string-form fallback, // or the omitempty tail could leak a stale empty string. // // TestContentBlock_ToolResultArrayForm 锁 tool_result content 字段的 // MarshalJSON 3 档决策: // 1. ContentItems 非空 → 发 "content":[...] 数组 // 2. ContentItems 空 + Content 非空 → 发 "content":"string" // 3. 两个都空 → 省略 "content" (omitempty 默认) // 防 L699/022ea20 同类静默丢弃回归: 加 array 路径时别误杀 string fallback, // omitempty 尾巴也别泄漏过期空串. func TestContentBlock_ToolResultArrayForm(t *testing.T) { t.Run("array form", func(t *testing.T) { b, err := json.Marshal(ContentBlock{ Type: "tool_result", ToolUseID: "toolu_01", ContentItems: []ContentBlock{ {Type: "text", Text: "screenshot taken"}, { Type: "image", Source: &ImageSource{Type: "base64", MediaType: "image/png", Data: "abc=="}, }, }, }) if err != nil { t.Fatalf("marshal: %v", err) } s := string(b) if !strings.Contains(s, `"content":[`) { t.Errorf("expected array content, got %s", s) } if !strings.Contains(s, `"type":"image"`) { t.Errorf("expected image block in content, got %s", s) } if !strings.Contains(s, `"media_type":"image/png"`) { t.Errorf("expected media_type in source, got %s", s) } }) t.Run("string form fallback", func(t *testing.T) { b, err := json.Marshal(ContentBlock{ Type: "tool_result", ToolUseID: "toolu_01", Content: "plain text result", }) if err != nil { t.Fatalf("marshal: %v", err) } s := string(b) if !strings.Contains(s, `"content":"plain text result"`) { t.Errorf("expected string content, got %s", s) } }) t.Run("both empty omits content", func(t *testing.T) { b, err := json.Marshal(ContentBlock{ Type: "tool_result", ToolUseID: "toolu_01", }) if err != nil { t.Fatalf("marshal: %v", err) } s := string(b) if strings.Contains(s, `"content"`) { t.Errorf("empty content should omit, got %s", s) } }) }