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