package anthropic import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "nex/backend/internal/protocol/openai" ) func TestStreamConverter_MessageStart(t *testing.T) { converter := NewStreamConverter("msg_123", "claude-3-opus") chunk := &openai.StreamChunk{ ID: "chatcmpl-123", Choices: []openai.StreamChoice{{Index: 0, Delta: openai.Delta{}}}, } events, err := converter.ConvertChunk(chunk) require.NoError(t, err) require.NotEmpty(t, events) // 第一个事件应该是 message_start assert.Equal(t, "message_start", events[0].Type) require.NotNil(t, events[0].Message) assert.Equal(t, "msg_123", events[0].Message.ID) assert.Equal(t, "message", events[0].Message.Type) assert.Equal(t, "assistant", events[0].Message.Role) assert.Equal(t, "claude-3-opus", events[0].Message.Model) } func TestStreamConverter_TextDelta(t *testing.T) { converter := NewStreamConverter("msg_123", "claude-3-opus") // 先发送一个空块以触发 message_start chunk1 := &openai.StreamChunk{ Choices: []openai.StreamChoice{ {Delta: openai.Delta{Content: "Hello"}}, }, } events1, err := converter.ConvertChunk(chunk1) require.NoError(t, err) // 应有 message_start + content_block_start + text delta assert.GreaterOrEqual(t, len(events1), 3) // 第二个文本块不应再发送 message_start 和 content_block_start chunk2 := &openai.StreamChunk{ Choices: []openai.StreamChoice{ {Delta: openai.Delta{Content: " world"}}, }, } events2, err := converter.ConvertChunk(chunk2) require.NoError(t, err) // 只有 text delta assert.Len(t, events2, 1) assert.Equal(t, "content_block_delta", events2[0].Type) assert.Equal(t, "text_delta", events2[0].Delta.Type) assert.Equal(t, " world", events2[0].Delta.Text) } func TestStreamConverter_FinishReason(t *testing.T) { converter := NewStreamConverter("msg_123", "claude-3-opus") chunk := &openai.StreamChunk{ Choices: []openai.StreamChoice{ {Delta: openai.Delta{Content: "Hello"}, FinishReason: "stop"}, }, } events, err := converter.ConvertChunk(chunk) require.NoError(t, err) // 查找 message_delta 事件 var messageDelta *StreamEvent for _, e := range events { if e.Type == "message_delta" { messageDelta = &e break } } require.NotNil(t, messageDelta) assert.Equal(t, "end_turn", messageDelta.Delta.StopReason) // 查找 message_stop 事件 var messageStop *StreamEvent for _, e := range events { if e.Type == "message_stop" { messageStop = &e break } } assert.NotNil(t, messageStop) } func TestStreamConverter_FinishReasonToolCalls(t *testing.T) { converter := NewStreamConverter("msg_123", "claude-3-opus") chunk := &openai.StreamChunk{ Choices: []openai.StreamChoice{ {Delta: openai.Delta{}, FinishReason: "tool_calls"}, }, } events, err := converter.ConvertChunk(chunk) require.NoError(t, err) var messageDelta *StreamEvent for _, e := range events { if e.Type == "message_delta" { messageDelta = &e break } } require.NotNil(t, messageDelta) assert.Equal(t, "tool_use", messageDelta.Delta.StopReason) } func TestStreamConverter_FinishReasonLength(t *testing.T) { converter := NewStreamConverter("msg_123", "claude-3-opus") chunk := &openai.StreamChunk{ Choices: []openai.StreamChoice{ {Delta: openai.Delta{}, FinishReason: "length"}, }, } events, err := converter.ConvertChunk(chunk) require.NoError(t, err) var messageDelta *StreamEvent for _, e := range events { if e.Type == "message_delta" { messageDelta = &e break } } require.NotNil(t, messageDelta) assert.Equal(t, "max_tokens", messageDelta.Delta.StopReason) } func TestStreamConverter_ToolCalls(t *testing.T) { converter := NewStreamConverter("msg_123", "claude-3-opus") chunk := &openai.StreamChunk{ Choices: []openai.StreamChoice{ { Delta: openai.Delta{ ToolCalls: []openai.ToolCall{ { ID: "call_123", Type: "function", Function: openai.FunctionCall{ Name: "get_weather", Arguments: `{"city": "Beijing"}`, }, }, }, }, }, }, } events, err := converter.ConvertChunk(chunk) require.NoError(t, err) // 应包含 content_block_start (tool_use) + content_block_delta (input_json_delta) hasBlockStart := false hasInputDelta := false for _, e := range events { if e.Type == "content_block_start" && e.ContentBlock != nil && e.ContentBlock.Type == "tool_use" { hasBlockStart = true assert.Equal(t, "call_123", e.ContentBlock.ID) assert.Equal(t, "get_weather", e.ContentBlock.Name) } if e.Type == "content_block_delta" && e.Delta != nil && e.Delta.Type == "input_json_delta" { hasInputDelta = true assert.Equal(t, `{"city": "Beijing"}`, e.Delta.Input) } } assert.True(t, hasBlockStart, "应有 tool_use content_block_start") assert.True(t, hasInputDelta, "应有 input_json_delta") } func TestSerializeEvent(t *testing.T) { event := StreamEvent{ Type: "message_start", Message: &MessagesResponse{ ID: "msg_123", Type: "message", Role: "assistant", }, } result, err := SerializeEvent(event) require.NoError(t, err) assert.Contains(t, result, "event: message_start") assert.Contains(t, result, "data: ") assert.Contains(t, result, "msg_123") } func TestSerializeEvent_InvalidJSON(t *testing.T) { event := StreamEvent{ Type: "test", } // 这个应该能正常序列化 result, err := SerializeEvent(event) require.NoError(t, err) assert.Contains(t, result, "event: test") } func TestContentBlock_ParseInputJSON(t *testing.T) { t.Run("字符串输入", func(t *testing.T) { cb := &ContentBlock{Input: `{"key": "value"}`} result, err := cb.ParseInputJSON() require.NoError(t, err) assert.Equal(t, "value", result["key"]) }) t.Run("对象输入", func(t *testing.T) { cb := &ContentBlock{Input: map[string]interface{}{"key": "value"}} result, err := cb.ParseInputJSON() require.NoError(t, err) assert.Equal(t, "value", result["key"]) }) t.Run("无效类型", func(t *testing.T) { cb := &ContentBlock{Input: 42} _, err := cb.ParseInputJSON() assert.Error(t, err) }) }