package llm
import (
"encoding/json"
"strings"
"testing"
)
func TestContentBlockMarshalsOnlyItsOwnFields(t *testing.T) {
tests := []struct {
name string
block ContentBlock
want string
}{
{
name: "text carries no stray thinking key",
block: TextBlock("hello"),
want: `{"text":"hello","type":"text"}`,
},
{
name: "tool_use keeps input as raw JSON",
block: ContentBlock{Type: BlockToolUse, ID: "tu_1", Name: "read_file", Input: json.RawMessage(`{"path":"main.go"}`)},
want: `{"id":"tu_1","input":{"path":"main.go"},"name":"read_file","type":"tool_use"}`,
},
{
name: "tool_use with no input sends an empty object",
block: ContentBlock{Type: BlockToolUse, ID: "tu_2", Name: "pwd"},
want: `{"id":"tu_2","input":{},"name":"pwd","type":"tool_use"}`,
},
{
name: "successful tool_result omits is_error",
block: ToolResultBlock("tu_1", "ok", false),
want: `{"content":"ok","tool_use_id":"tu_1","type":"tool_result"}`,
},
{
name: "failed tool_result sets is_error",
block: ToolResultBlock("tu_1", "no such file", true),
want: `{"content":"no such file","is_error":true,"tool_use_id":"tu_1","type":"tool_result"}`,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := json.Marshal(tc.block)
if err != nil {
t.Fatalf("marshal: %v", err)
}
if string(got) != tc.want {
t.Errorf("got %s\nwant %s", got, tc.want)
}
})
}
}
// With display omitted (the default on Opus 5) a thinking block has empty text
// but a signature that must go back unchanged, so the field cannot be omitempty.
func TestThinkingBlockRoundTripsEmptyTextAndSignature(t *testing.T) {
const wire = `{"type":"thinking","thinking":"","signature":"sig-abc123"}`
var b ContentBlock
if err := json.Unmarshal([]byte(wire), &b); err != nil {
t.Fatalf("unmarshal: %v", err)
}
out, err := json.Marshal(b)
if err != nil {
t.Fatalf("marshal: %v", err)
}
if !strings.Contains(string(out), `"thinking":""`) {
t.Errorf("empty thinking text must survive the round trip, got %s", out)
}
if !strings.Contains(string(out), `"signature":"sig-abc123"`) {
t.Errorf("signature must survive the round trip, got %s", out)
}
}
func TestUnknownBlockTypeRoundTripsVerbatim(t *testing.T) {
const wire = `{"type":"server_tool_use_future","id":"x","weird":{"nested":[1,2]}}`
var b ContentBlock
if err := json.Unmarshal([]byte(wire), &b); err != nil {
t.Fatalf("unmarshal: %v", err)
}
out, err := json.Marshal(b)
if err != nil {
t.Fatalf("marshal: %v", err)
}
if string(out) != wire {
t.Errorf("unmodelled block should be echoed verbatim\ngot %s\nwant %s", out, wire)
}
}
func TestEmptyToolResultGetsPlaceholder(t *testing.T) {
if got := ToolResultBlock("tu_1", "", false).Content; got != "(no output)" {
t.Errorf("content = %q, want a placeholder: the API rejects empty tool_result content", got)
}
}
func TestResponseAccessors(t *testing.T) {
const wire = `{
"id":"msg_1","type":"message","role":"assistant","model":"claude-opus-5",
"content":[
{"type":"thinking","thinking":"","signature":"s"},
{"type":"text","text":"Let me look. "},
{"type":"tool_use","id":"tu_1","name":"read_file","input":{"path":"a.go"}},
{"type":"text","text":"One moment."}
],
"stop_reason":"tool_use","stop_details":null,
"usage":{"input_tokens":10,"output_tokens":20,"cache_read_input_tokens":5,"cache_creation_input_tokens":1}
}`
var r Response
if err := json.Unmarshal([]byte(wire), &r); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if got := r.Text(); got != "Let me look. One moment." {
t.Errorf("Text() = %q, want the text blocks concatenated", got)
}
uses := r.ToolUses()
if len(uses) != 1 || uses[0].ID != "tu_1" || uses[0].Name != "read_file" {
t.Errorf("ToolUses() = %+v, want the single read_file call", uses)
}
if r.Usage.CacheReadInputTokens != 5 {
t.Errorf("cache_read_input_tokens = %d, want 5", r.Usage.CacheReadInputTokens)
}
if msg := r.Message(); msg.Role != RoleAssistant || len(msg.Content) != 4 {
t.Errorf("Message() = %+v, want all 4 blocks under the assistant role", msg)
}
}
// The effort levels are a closed set, and both the -effort flag and the TUI's
// /effort command answer out of it rather than keeping their own copy.
func TestValidEffort(t *testing.T) {
for _, level := range EffortLevels {
if !ValidEffort(level) {
t.Errorf("ValidEffort(%q) = false for a level we advertise", level)
}
}
for _, level := range []string{"", "HIGH", "higher", "none"} {
if ValidEffort(level) {
t.Errorf("ValidEffort(%q) = true, want false", level)
}
}
}