1
0
Files
nex/backend/internal/conversion/anthropic/stream_decoder_test.go
lanyuanxiaoyao 1dac347d3b refactor: 实现 ConversionEngine 协议转换引擎,替代旧 protocol 包
引入 Canonical Model 和 ProtocolAdapter 架构,支持 OpenAI/Anthropic 协议间
无缝转换,统一 ProxyHandler 替代分散的 OpenAI/Anthropic Handler,简化
ProviderClient 为协议无关的 HTTP 发送器,Provider 新增 protocol 字段。
2026-04-20 00:36:27 +08:00

275 lines
7.0 KiB
Go

package anthropic
import (
"encoding/json"
"fmt"
"testing"
"nex/backend/internal/conversion/canonical"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func makeAnthropicEvent(eventType string, data any) []byte {
dataBytes, _ := json.Marshal(data)
return []byte(fmt.Sprintf("event: %s\ndata: %s\n\n", eventType, string(dataBytes)))
}
func TestStreamDecoder_MessageStart(t *testing.T) {
d := NewStreamDecoder()
payload := map[string]any{
"type": "message_start",
"message": map[string]any{
"id": "msg_1",
"model": "claude-3",
"usage": map[string]any{"input_tokens": 10, "output_tokens": 0},
},
}
raw := makeAnthropicEvent("message_start", payload)
events := d.ProcessChunk(raw)
require.NotEmpty(t, events)
assert.Equal(t, canonical.EventMessageStart, events[0].Type)
assert.Equal(t, "msg_1", events[0].Message.ID)
assert.Equal(t, "claude-3", events[0].Message.Model)
}
func TestStreamDecoder_ContentBlockDelta(t *testing.T) {
d := NewStreamDecoder()
tests := []struct {
name string
deltaType string
deltaData map[string]any
checkField string
checkValue string
}{
{
name: "text_delta",
deltaType: "text_delta",
deltaData: map[string]any{"type": "text_delta", "text": "你好"},
checkField: "text",
checkValue: "你好",
},
{
name: "input_json_delta",
deltaType: "input_json_delta",
deltaData: map[string]any{"type": "input_json_delta", "partial_json": "{\"key\":"},
checkField: "partial_json",
checkValue: "{\"key\":",
},
{
name: "thinking_delta",
deltaType: "thinking_delta",
deltaData: map[string]any{"type": "thinking_delta", "thinking": "思考中"},
checkField: "thinking",
checkValue: "思考中",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
payload := map[string]any{
"type": "content_block_delta",
"index": 0,
"delta": tt.deltaData,
}
raw := makeAnthropicEvent("content_block_delta", payload)
events := d.ProcessChunk(raw)
require.NotEmpty(t, events)
assert.Equal(t, canonical.EventContentBlockDelta, events[0].Type)
assert.NotNil(t, events[0].Delta)
switch tt.checkField {
case "text":
assert.Equal(t, tt.checkValue, events[0].Delta.Text)
case "partial_json":
assert.Equal(t, tt.checkValue, events[0].Delta.PartialJSON)
case "thinking":
assert.Equal(t, tt.checkValue, events[0].Delta.Thinking)
}
})
}
}
func TestStreamDecoder_RedactedThinking(t *testing.T) {
d := NewStreamDecoder()
// redacted_thinking block start 应被抑制
payload := map[string]any{
"type": "content_block_start",
"index": 1,
"content_block": map[string]any{
"type": "redacted_thinking",
"data": "redacted-data",
},
}
raw := makeAnthropicEvent("content_block_start", payload)
events := d.ProcessChunk(raw)
assert.Empty(t, events)
assert.True(t, d.redactedBlocks[1])
}
func TestStreamDecoder_RedactedBlockStopSuppressed(t *testing.T) {
d := NewStreamDecoder()
d.redactedBlocks[2] = true
// content_block_stop 对 redacted block 返回 nil
payload := map[string]any{
"type": "content_block_stop",
"index": 2,
}
raw := makeAnthropicEvent("content_block_stop", payload)
events := d.ProcessChunk(raw)
assert.Empty(t, events)
// 应清理 redactedBlocks
_, exists := d.redactedBlocks[2]
assert.False(t, exists)
}
func TestStreamDecoder_ContentBlockStart(t *testing.T) {
d := NewStreamDecoder()
payload := map[string]any{
"type": "content_block_start",
"index": 0,
"content_block": map[string]any{
"type": "text",
"text": "",
},
}
raw := makeAnthropicEvent("content_block_start", payload)
events := d.ProcessChunk(raw)
require.Len(t, events, 1)
assert.Equal(t, canonical.EventContentBlockStart, events[0].Type)
require.NotNil(t, events[0].ContentBlock)
assert.Equal(t, "text", events[0].ContentBlock.Type)
require.NotNil(t, events[0].Index)
assert.Equal(t, 0, *events[0].Index)
}
func TestStreamDecoder_ContentBlockStart_ToolUse(t *testing.T) {
d := NewStreamDecoder()
payload := map[string]any{
"type": "content_block_start",
"index": 1,
"content_block": map[string]any{
"type": "tool_use",
"id": "toolu_1",
"name": "search",
},
}
raw := makeAnthropicEvent("content_block_start", payload)
events := d.ProcessChunk(raw)
require.Len(t, events, 1)
assert.Equal(t, canonical.EventContentBlockStart, events[0].Type)
require.NotNil(t, events[0].ContentBlock)
assert.Equal(t, "tool_use", events[0].ContentBlock.Type)
assert.Equal(t, "toolu_1", events[0].ContentBlock.ID)
assert.Equal(t, "search", events[0].ContentBlock.Name)
}
func TestStreamDecoder_ContentBlockStop(t *testing.T) {
d := NewStreamDecoder()
payload := map[string]any{
"type": "content_block_stop",
"index": 0,
}
raw := makeAnthropicEvent("content_block_stop", payload)
events := d.ProcessChunk(raw)
require.Len(t, events, 1)
assert.Equal(t, canonical.EventContentBlockStop, events[0].Type)
require.NotNil(t, events[0].Index)
assert.Equal(t, 0, *events[0].Index)
}
func TestStreamDecoder_MessageDelta(t *testing.T) {
d := NewStreamDecoder()
payload := map[string]any{
"type": "message_delta",
"delta": map[string]any{
"stop_reason": "end_turn",
},
"usage": map[string]any{
"output_tokens": 42,
},
}
raw := makeAnthropicEvent("message_delta", payload)
events := d.ProcessChunk(raw)
require.Len(t, events, 1)
assert.Equal(t, canonical.EventMessageDelta, events[0].Type)
require.NotNil(t, events[0].StopReason)
assert.Equal(t, canonical.StopReasonEndTurn, *events[0].StopReason)
require.NotNil(t, events[0].Usage)
assert.Equal(t, 42, events[0].Usage.OutputTokens)
}
func TestStreamDecoder_MessageStop(t *testing.T) {
d := NewStreamDecoder()
raw := makeAnthropicEvent("message_stop", map[string]any{"type": "message_stop"})
events := d.ProcessChunk(raw)
require.Len(t, events, 1)
assert.Equal(t, canonical.EventMessageStop, events[0].Type)
}
func TestStreamDecoder_Ping(t *testing.T) {
d := NewStreamDecoder()
raw := makeAnthropicEvent("ping", map[string]any{"type": "ping"})
events := d.ProcessChunk(raw)
require.Len(t, events, 1)
assert.Equal(t, canonical.EventPing, events[0].Type)
}
func TestStreamDecoder_Error(t *testing.T) {
d := NewStreamDecoder()
payload := map[string]any{
"type": "error",
"error": map[string]any{
"type": "overloaded_error",
"message": "服务过载",
},
}
raw := makeAnthropicEvent("error", payload)
events := d.ProcessChunk(raw)
require.Len(t, events, 1)
assert.Equal(t, canonical.EventError, events[0].Type)
require.NotNil(t, events[0].Error)
assert.Equal(t, "overloaded_error", events[0].Error.Type)
assert.Equal(t, "服务过载", events[0].Error.Message)
}
func TestStreamDecoder_RedactedDeltaSuppressed(t *testing.T) {
d := NewStreamDecoder()
d.redactedBlocks[1] = true
payload := map[string]any{
"type": "content_block_delta",
"index": 1,
"delta": map[string]any{
"type": "text_delta",
"text": "被抑制的内容",
},
}
raw := makeAnthropicEvent("content_block_delta", payload)
events := d.ProcessChunk(raw)
assert.Empty(t, events)
}