vibe-proxy/backend/internal/translator/common/claude_messages_test.go
2026-08-24 00:10:41 +02:00

110 lines
4.8 KiB
Go

package common
import (
"testing"
"github.com/tidwall/gjson"
)
func TestClaudeMessageAccumulatorGroupsAndOrdersAssistantParts(t *testing.T) {
accumulator := NewClaudeMessageAccumulator(3)
accumulator.Append([]byte(`{"role":"assistant","content":[{"type":"tool_use","id":"call_1","name":"first","input":{}}]}`))
accumulator.Append([]byte(`{"role":"assistant","content":[{"type":"thinking","thinking":"reason"},{"type":"text","text":"answer"}]}`))
accumulator.Append([]byte(`{"role":"assistant","content":[{"type":"tool_use","id":"call_2","name":"second","input":{}}]}`))
messages := accumulator.Messages()
if len(messages) != 1 {
t.Fatalf("message count = %d, want 1", len(messages))
}
content := gjson.GetBytes(messages[0], "content").Array()
wantTypes := []string{"thinking", "text", "tool_use", "tool_use"}
if len(content) != len(wantTypes) {
t.Fatalf("content count = %d, want %d. Message: %s", len(content), len(wantTypes), string(messages[0]))
}
for i, wantType := range wantTypes {
if got := content[i].Get("type").String(); got != wantType {
t.Fatalf("content[%d].type = %q, want %q", i, got, wantType)
}
}
if got := content[2].Get("id").String(); got != "call_1" {
t.Fatalf("first tool_use id = %q, want call_1", got)
}
if got := content[3].Get("id").String(); got != "call_2" {
t.Fatalf("second tool_use id = %q, want call_2", got)
}
}
func TestClaudeMessageAccumulatorPreservesUserOrderAndRoleBoundaries(t *testing.T) {
accumulator := NewClaudeMessageAccumulator(3)
accumulator.Append([]byte(`{"role":"user","content":[{"type":"tool_result","tool_use_id":"call_1","content":"ok"}]}`))
accumulator.Append([]byte(`{"role":"user","content":[{"type":"text","text":"continue"}]}`))
accumulator.Append([]byte(`{"role":"assistant","content":[{"type":"text","text":"done"}]}`))
messages := accumulator.Messages()
if len(messages) != 2 {
t.Fatalf("message count = %d, want 2", len(messages))
}
if got := gjson.GetBytes(messages[0], "role").String(); got != "user" {
t.Fatalf("messages[0].role = %q, want user", got)
}
if got := gjson.GetBytes(messages[0], "content.0.type").String(); got != "tool_result" {
t.Fatalf("first user block type = %q, want tool_result", got)
}
if got := gjson.GetBytes(messages[0], "content.1.text").String(); got != "continue" {
t.Fatalf("second user block text = %q, want continue", got)
}
if got := gjson.GetBytes(messages[1], "role").String(); got != "assistant" {
t.Fatalf("messages[1].role = %q, want assistant", got)
}
}
func TestClaudeMessageAccumulatorSkipsEmptyMessagesWithoutBreakingTurn(t *testing.T) {
accumulator := NewClaudeMessageAccumulator(3)
accumulator.Append([]byte(`{"role":"assistant","content":[{"type":"text","text":"first"}]}`))
accumulator.Append([]byte(`{"role":"user"}`))
accumulator.Append([]byte(`{"role":"user","content":null}`))
accumulator.Append([]byte(`{"role":"user","content":""}`))
accumulator.Append([]byte(`{"role":"user","content":[]}`))
accumulator.Append([]byte(`{"role":"invalid","content":[{"type":"text","text":"ignored"}]}`))
accumulator.Append([]byte(`{"role":"assistant","content":[{"type":"text","text":"second"}]}`))
messages := accumulator.Messages()
if len(messages) != 1 {
t.Fatalf("message count = %d, want 1", len(messages))
}
if got := gjson.GetBytes(messages[0], "content.#").Int(); got != 2 {
t.Fatalf("assistant content count = %d, want 2. Message: %s", got, string(messages[0]))
}
}
func TestClaudeMessageAccumulatorFlushPreservesExplicitBoundary(t *testing.T) {
accumulator := NewClaudeMessageAccumulator(2)
accumulator.Append([]byte(`{"role":"user","content":"system reminder"}`))
accumulator.Flush()
accumulator.Append([]byte(`{"role":"user","content":[{"type":"text","text":"question"}]}`))
messages := accumulator.Messages()
if len(messages) != 2 {
t.Fatalf("message count = %d, want 2", len(messages))
}
if got := gjson.GetBytes(messages[0], "content.0.text").String(); got != "system reminder" {
t.Fatalf("first message text = %q, want system reminder", got)
}
if got := gjson.GetBytes(messages[1], "content.0.text").String(); got != "question" {
t.Fatalf("second message text = %q, want question", got)
}
}
func TestClaudeMessageAccumulatorPreservesBlockCacheControl(t *testing.T) {
accumulator := NewClaudeMessageAccumulator(2)
accumulator.Append([]byte(`{"role":"user","content":[{"type":"text","text":"cached","cache_control":{"type":"ephemeral"}}]}`))
accumulator.Append([]byte(`{"role":"user","content":[{"type":"text","text":"fresh"}]}`))
messages := accumulator.Messages()
if got := gjson.GetBytes(messages[0], "content.0.cache_control.type").String(); got != "ephemeral" {
t.Fatalf("cache_control.type = %q, want ephemeral", got)
}
if gjson.GetBytes(messages[0], "content.1.cache_control").Exists() {
t.Fatalf("second block should not have cache_control: %s", string(messages[0]))
}
}