API / sdk /translator /state_test.go
sshinmen's picture
Add shared state utilities for streaming translators
bd749dd
Raw
History Blame Contribute Delete
12.1 kB
package translator
import (
"strings"
"testing"
)
func TestStreamState(t *testing.T) {
t.Run("Reset clears all fields", func(t *testing.T) {
s := StreamState{
ResponseID: "test-id",
CreatedAt: 12345,
Model: "gpt-4",
SequenceNum: 10,
FinishReason: "stop",
NativeFinishReason: "end_turn",
InputTokens: 100,
OutputTokens: 50,
UsageSeen: true,
}
s.Reset()
if s.ResponseID != "" {
t.Errorf("Expected ResponseID to be empty, got %s", s.ResponseID)
}
if s.CreatedAt != 0 {
t.Errorf("Expected CreatedAt to be 0, got %d", s.CreatedAt)
}
if s.Model != "" {
t.Errorf("Expected Model to be empty, got %s", s.Model)
}
if s.SequenceNum != 0 {
t.Errorf("Expected SequenceNum to be 0, got %d", s.SequenceNum)
}
if s.FinishReason != "" {
t.Errorf("Expected FinishReason to be empty, got %s", s.FinishReason)
}
if s.NativeFinishReason != "" {
t.Errorf("Expected NativeFinishReason to be empty, got %s", s.NativeFinishReason)
}
if s.InputTokens != 0 {
t.Errorf("Expected InputTokens to be 0, got %d", s.InputTokens)
}
if s.OutputTokens != 0 {
t.Errorf("Expected OutputTokens to be 0, got %d", s.OutputTokens)
}
if s.UsageSeen {
t.Error("Expected UsageSeen to be false")
}
})
t.Run("NextSequence increments correctly", func(t *testing.T) {
s := StreamState{}
if s.NextSequence() != 1 {
t.Errorf("Expected first sequence to be 1, got %d", s.SequenceNum)
}
if s.NextSequence() != 2 {
t.Errorf("Expected second sequence to be 2, got %d", s.SequenceNum)
}
if s.NextSequence() != 3 {
t.Errorf("Expected third sequence to be 3, got %d", s.SequenceNum)
}
})
t.Run("EnsureTimestamp sets timestamp when zero", func(t *testing.T) {
s := StreamState{}
ts := s.EnsureTimestamp()
if ts == 0 {
t.Error("Expected timestamp to be set")
}
if s.CreatedAt != ts {
t.Errorf("Expected CreatedAt to match returned timestamp, got %d vs %d", s.CreatedAt, ts)
}
})
t.Run("EnsureTimestamp preserves existing timestamp", func(t *testing.T) {
s := StreamState{CreatedAt: 12345}
ts := s.EnsureTimestamp()
if ts != 12345 {
t.Errorf("Expected timestamp to remain 12345, got %d", ts)
}
})
}
func TestToolCallState(t *testing.T) {
t.Run("GetArguments returns empty object when no arguments", func(t *testing.T) {
tc := &ToolCallState{}
args := tc.GetArguments()
if args != "{}" {
t.Errorf("Expected '{}', got %s", args)
}
})
t.Run("GetArguments returns accumulated arguments", func(t *testing.T) {
tc := &ToolCallState{}
tc.Arguments.WriteString(`{"key": "value"}`)
args := tc.GetArguments()
if args != `{"key": "value"}` {
t.Errorf("Expected '{\"key\": \"value\"}', got %s", args)
}
})
t.Run("WriteArguments appends content", func(t *testing.T) {
tc := &ToolCallState{}
tc.WriteArguments(`{"a":`)
tc.WriteArguments(`1}`)
expected := `{"a":1}`
if tc.Arguments.String() != expected {
t.Errorf("Expected '%s', got %s", expected, tc.Arguments.String())
}
})
}
func TestToolCallAccumulator(t *testing.T) {
t.Run("NewToolCallAccumulator creates empty accumulator", func(t *testing.T) {
a := NewToolCallAccumulator()
if a == nil {
t.Fatal("Expected non-nil accumulator")
}
if a.HasAny() {
t.Error("Expected HasAny to be false for new accumulator")
}
if a.Count() != 0 {
t.Errorf("Expected Count to be 0, got %d", a.Count())
}
})
t.Run("Get returns nil for non-existent index", func(t *testing.T) {
a := NewToolCallAccumulator()
if a.Get(0) != nil {
t.Error("Expected Get to return nil for non-existent index")
}
})
t.Run("GetOrCreate creates new tool call", func(t *testing.T) {
a := NewToolCallAccumulator()
tc := a.GetOrCreate(0)
if tc == nil {
t.Fatal("Expected non-nil tool call")
}
if tc.Index != 0 {
t.Errorf("Expected index 0, got %d", tc.Index)
}
if !a.HasAny() {
t.Error("Expected HasAny to be true after creating tool call")
}
if a.Count() != 1 {
t.Errorf("Expected Count to be 1, got %d", a.Count())
}
})
t.Run("GetOrCreate returns existing tool call", func(t *testing.T) {
a := NewToolCallAccumulator()
tc1 := a.GetOrCreate(0)
tc1.Name = "test-function"
tc2 := a.GetOrCreate(0)
if tc2.Name != "test-function" {
t.Error("Expected to get existing tool call")
}
if a.Count() != 1 {
t.Errorf("Expected Count to still be 1, got %d", a.Count())
}
})
t.Run("CreateNext assigns incremental indices", func(t *testing.T) {
a := NewToolCallAccumulator()
tc1 := a.CreateNext("id1", "func1")
tc2 := a.CreateNext("id2", "func2")
if tc1.Index != 0 {
t.Errorf("Expected first index to be 0, got %d", tc1.Index)
}
if tc2.Index != 1 {
t.Errorf("Expected second index to be 1, got %d", tc2.Index)
}
if tc1.ID != "id1" || tc1.Name != "func1" {
t.Error("Expected first tool call to have correct ID and Name")
}
if tc2.ID != "id2" || tc2.Name != "func2" {
t.Error("Expected second tool call to have correct ID and Name")
}
})
t.Run("MarkComplete marks tool call as complete", func(t *testing.T) {
a := NewToolCallAccumulator()
a.CreateNext("id1", "func1")
tc := a.MarkComplete(0)
if tc == nil {
t.Fatal("Expected non-nil tool call")
}
if !tc.Complete {
t.Error("Expected tool call to be marked complete")
}
})
t.Run("MarkComplete returns nil for non-existent index", func(t *testing.T) {
a := NewToolCallAccumulator()
if a.MarkComplete(0) != nil {
t.Error("Expected MarkComplete to return nil for non-existent index")
}
})
t.Run("Remove deletes tool call", func(t *testing.T) {
a := NewToolCallAccumulator()
a.CreateNext("id1", "func1")
a.CreateNext("id2", "func2")
a.Remove(0)
if a.Count() != 1 {
t.Errorf("Expected Count to be 1 after removal, got %d", a.Count())
}
if a.Get(0) != nil {
t.Error("Expected Get(0) to return nil after removal")
}
if a.Get(1) == nil {
t.Error("Expected Get(1) to still exist")
}
})
t.Run("GetOrdered returns tool calls in creation order", func(t *testing.T) {
a := NewToolCallAccumulator()
a.CreateNext("id1", "func1")
a.CreateNext("id2", "func2")
a.CreateNext("id3", "func3")
ordered := a.GetOrdered()
if len(ordered) != 3 {
t.Fatalf("Expected 3 ordered tool calls, got %d", len(ordered))
}
if ordered[0].ID != "id1" || ordered[1].ID != "id2" || ordered[2].ID != "id3" {
t.Error("Expected tool calls in creation order")
}
})
t.Run("Reset clears all state", func(t *testing.T) {
a := NewToolCallAccumulator()
a.CreateNext("id1", "func1")
a.CreateNext("id2", "func2")
a.Reset()
if a.HasAny() {
t.Error("Expected HasAny to be false after reset")
}
if a.Count() != 0 {
t.Errorf("Expected Count to be 0 after reset, got %d", a.Count())
}
})
}
func TestContentBuffer(t *testing.T) {
t.Run("HasContent returns false for empty buffer", func(t *testing.T) {
cb := ContentBuffer{}
if cb.HasContent() {
t.Error("Expected HasContent to be false for empty buffer")
}
})
t.Run("HasContent returns true with text", func(t *testing.T) {
cb := ContentBuffer{}
cb.Text.WriteString("hello")
if !cb.HasContent() {
t.Error("Expected HasContent to be true with text")
}
})
t.Run("HasContent returns true with reasoning", func(t *testing.T) {
cb := ContentBuffer{}
cb.Reasoning.WriteString("thinking...")
if !cb.HasContent() {
t.Error("Expected HasContent to be true with reasoning")
}
})
t.Run("Reset clears both buffers", func(t *testing.T) {
cb := ContentBuffer{}
cb.Text.WriteString("text")
cb.Reasoning.WriteString("reasoning")
cb.Reset()
if cb.Text.Len() != 0 {
t.Error("Expected Text to be empty after reset")
}
if cb.Reasoning.Len() != 0 {
t.Error("Expected Reasoning to be empty after reset")
}
if cb.HasContent() {
t.Error("Expected HasContent to be false after reset")
}
})
}
func TestStreamingState(t *testing.T) {
t.Run("NewStreamingState creates initialized state", func(t *testing.T) {
s := NewStreamingState()
if s == nil {
t.Fatal("Expected non-nil state")
}
if s.ToolCalls == nil {
t.Error("Expected ToolCalls to be initialized")
}
})
t.Run("Reset clears all fields", func(t *testing.T) {
s := NewStreamingState()
s.ResponseID = "test-id"
s.InTextBlock = true
s.InToolBlock = true
s.InReasoningBlock = true
s.CurrentMessageID = "msg-id"
s.CurrentToolCallID = "tool-id"
s.CurrentReasoningID = "reasoning-id"
s.ToolCalls.CreateNext("id", "func")
s.Content.Text.WriteString("text")
s.Reset()
if s.ResponseID != "" {
t.Error("Expected ResponseID to be cleared")
}
if s.InTextBlock || s.InToolBlock || s.InReasoningBlock {
t.Error("Expected block flags to be false")
}
if s.CurrentMessageID != "" || s.CurrentToolCallID != "" || s.CurrentReasoningID != "" {
t.Error("Expected current IDs to be cleared")
}
if s.ToolCalls.HasAny() {
t.Error("Expected ToolCalls to be cleared")
}
if s.Content.HasContent() {
t.Error("Expected Content to be cleared")
}
})
}
func TestFinishReasonMapper(t *testing.T) {
t.Run("ToOpenAI maps known reasons", func(t *testing.T) {
m := NewFinishReasonMapper()
testCases := []struct {
input string
expected string
}{
{"end_turn", "stop"},
{"tool_use", "tool_calls"},
{"max_tokens", "length"},
{"STOP", "stop"},
{"MAX_TOKENS", "length"},
{"SAFETY", "content_filter"},
{"completed", "stop"},
}
for _, tc := range testCases {
result := m.ToOpenAI(tc.input)
if result != tc.expected {
t.Errorf("ToOpenAI(%s) = %s, expected %s", tc.input, result, tc.expected)
}
}
})
t.Run("ToOpenAI returns default for unknown reasons", func(t *testing.T) {
m := NewFinishReasonMapper()
result := m.ToOpenAI("unknown_reason")
if result != "stop" {
t.Errorf("Expected default 'stop', got %s", result)
}
})
t.Run("FromOpenAI maps known reasons", func(t *testing.T) {
m := NewFinishReasonMapper()
testCases := []struct {
input string
expected string
}{
{"stop", "end_turn"},
{"tool_calls", "tool_use"},
{"length", "max_tokens"},
{"content_filter", "SAFETY"},
}
for _, tc := range testCases {
result := m.FromOpenAI(tc.input)
if result != tc.expected {
t.Errorf("FromOpenAI(%s) = %s, expected %s", tc.input, result, tc.expected)
}
}
})
t.Run("FromOpenAI returns default for unknown reasons", func(t *testing.T) {
m := NewFinishReasonMapper()
result := m.FromOpenAI("unknown")
if result != "STOP" {
t.Errorf("Expected default 'STOP', got %s", result)
}
})
t.Run("RegisterMapping adds new mapping", func(t *testing.T) {
m := NewFinishReasonMapper()
m.RegisterMapping("custom_reason", "custom_openai")
if m.ToOpenAI("custom_reason") != "custom_openai" {
t.Error("Expected new mapping to work in ToOpenAI direction")
}
if m.FromOpenAI("custom_openai") != "custom_reason" {
t.Error("Expected new mapping to work in FromOpenAI direction")
}
})
}
func TestGenerateResponseID(t *testing.T) {
t.Run("generates unique IDs", func(t *testing.T) {
id1 := GenerateResponseID("")
id2 := GenerateResponseID("")
if id1 == id2 {
t.Error("Expected unique IDs")
}
})
t.Run("includes prefix when provided", func(t *testing.T) {
id := GenerateResponseID("chatcmpl")
if !strings.HasPrefix(id, "chatcmpl_") {
t.Errorf("Expected ID to start with 'chatcmpl_', got %s", id)
}
})
t.Run("default prefix is resp_", func(t *testing.T) {
id := GenerateResponseID("")
if !strings.HasPrefix(id, "resp_") {
t.Errorf("Expected ID to start with 'resp_', got %s", id)
}
})
}
func TestGenerateFunctionCallID(t *testing.T) {
t.Run("generates unique IDs", func(t *testing.T) {
id1 := GenerateFunctionCallID("")
id2 := GenerateFunctionCallID("")
if id1 == id2 {
t.Error("Expected unique IDs")
}
})
t.Run("includes name when provided", func(t *testing.T) {
id := GenerateFunctionCallID("get_weather")
if !strings.Contains(id, "get_weather") {
t.Errorf("Expected ID to contain 'get_weather', got %s", id)
}
})
}