| 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) |
| } |
| }) |
| } |
|
|