package service import ( "context" "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/gochat/gochat/internal/llm" ) // ========== Streaming Tests (M12) ========== func TestCaptainTaskService_ReplySuggestionStream_成功(t *testing.T) { db, mockProvider, svc := setupCaptainTaskService(t) account := createTestAccount(t, db) assistant := createTestAssistant(t, db, account.ID, "客服助手") conv := createTestConversationWithMessages(t, db, account.ID, []struct{ sender, content string }{ {"contact", "你们的产品怎么使用?"}, {"agent", "请问是哪个产品?"}, }) mockProvider.embeddingResponse = makeEmbeddingResponse() mockProvider.streamChunks = []llm.StreamChunk{ {Choices: []llm.StreamChoice{{Delta: llm.StreamDelta{Content: "建议一"}, FinishReason: ""}}}, {Choices: []llm.StreamChoice{{Delta: llm.StreamDelta{Content: "建议二"}, FinishReason: "stop"}}}, } var collected []string err := svc.ReplySuggestionStream(context.Background(), account.ID, &TaskReplySuggestionRequest{ AssistantID: assistant.ID, ConversationID: conv.ID, Tone: "friendly", }, func(chunk llm.StreamChunk) error { if len(chunk.Choices) > 0 && chunk.Choices[0].Delta.Content != "" { collected = append(collected, chunk.Choices[0].Delta.Content) } return nil }) require.NoError(t, err) assert.Len(t, collected, 2) assert.Equal(t, "建议一", collected[0]) assert.Equal(t, "建议二", collected[1]) } func TestCaptainTaskService_ReplySuggestionStream_助手不存在(t *testing.T) { _, _, svc := setupCaptainTaskService(t) err := svc.ReplySuggestionStream(context.Background(), 1, &TaskReplySuggestionRequest{ AssistantID: 99999, ConversationID: 1, }, func(chunk llm.StreamChunk) error { return nil }) assert.Error(t, err) assert.Contains(t, err.Error(), "assistant not found") } func TestCaptainTaskService_ReplySuggestionStream_LLM流错误(t *testing.T) { db, mockProvider, svc := setupCaptainTaskService(t) account := createTestAccount(t, db) assistant := createTestAssistant(t, db, account.ID, "助手") conv := createTestConversationWithMessages(t, db, account.ID, []struct{ sender, content string }{ {"contact", "hello"}, }) mockProvider.embeddingResponse = makeEmbeddingResponse() mockProvider.streamError = fmt.Errorf("LLM stream 调用超时") err := svc.ReplySuggestionStream(context.Background(), account.ID, &TaskReplySuggestionRequest{ AssistantID: assistant.ID, ConversationID: conv.ID, }, func(chunk llm.StreamChunk) error { return nil }) assert.Error(t, err) assert.Contains(t, err.Error(), "LLM stream") } func TestCaptainTaskService_SummarizeStream_成功(t *testing.T) { db, mockProvider, svc := setupCaptainTaskService(t) account := createTestAccount(t, db) conv := createTestConversationWithMessages(t, db, account.ID, []struct{ sender, content string }{ {"contact", "你好,我的订单还没收到"}, {"agent", "我来帮您查询一下"}, }) mockProvider.streamChunks = []llm.StreamChunk{ {Choices: []llm.StreamChoice{{Delta: llm.StreamDelta{Content: "客户咨询订单"}, FinishReason: ""}}}, {Choices: []llm.StreamChoice{{Delta: llm.StreamDelta{Content: "状态查询"}, FinishReason: "stop"}}}, } var collected []string err := svc.SummarizeStream(context.Background(), account.ID, &TaskSummarizeRequest{ ConversationID: conv.ID, Language: "zh-CN", }, func(chunk llm.StreamChunk) error { if len(chunk.Choices) > 0 && chunk.Choices[0].Delta.Content != "" { collected = append(collected, chunk.Choices[0].Delta.Content) } return nil }) require.NoError(t, err) assert.Len(t, collected, 2) } func TestCaptainTaskService_SummarizeStream_无消息(t *testing.T) { db, mockProvider, svc := setupCaptainTaskService(t) account := createTestAccount(t, db) conv := createTestConversationWithMessages(t, db, account.ID, nil) // When no messages, SummarizeStream still proceeds with empty context // It won't return an error, but the LLM will receive minimal context mockProvider.streamChunks = []llm.StreamChunk{ {Choices: []llm.StreamChoice{{Delta: llm.StreamDelta{Content: "no content"}}}}, } var collected []string err := svc.SummarizeStream(context.Background(), account.ID, &TaskSummarizeRequest{ ConversationID: conv.ID, }, func(chunk llm.StreamChunk) error { if len(chunk.Choices) > 0 && chunk.Choices[0].Delta.Content != "" { collected = append(collected, chunk.Choices[0].Delta.Content) } return nil }) require.NoError(t, err) assert.NotEmpty(t, collected) } func TestCaptainTaskService_SummarizeStream_LLM流错误(t *testing.T) { db, mockProvider, svc := setupCaptainTaskService(t) account := createTestAccount(t, db) conv := createTestConversationWithMessages(t, db, account.ID, []struct{ sender, content string }{ {"contact", "test message"}, }) mockProvider.streamError = fmt.Errorf("LLM stream 服务不可用") err := svc.SummarizeStream(context.Background(), account.ID, &TaskSummarizeRequest{ ConversationID: conv.ID, }, func(chunk llm.StreamChunk) error { return nil }) assert.Error(t, err) } func TestCaptainTaskService_RewriteStream_成功(t *testing.T) { _, mockProvider, svc := setupCaptainTaskService(t) mockProvider.streamChunks = []llm.StreamChunk{ {Choices: []llm.StreamChoice{{Delta: llm.StreamDelta{Content: "感谢您的"}, FinishReason: ""}}}, {Choices: []llm.StreamChoice{{Delta: llm.StreamDelta{Content: "来信"}, FinishReason: "stop"}}}, } var collected []string err := svc.RewriteStream(context.Background(), 1, &TaskRewriteRequest{ Content: "来信收到了", Tone: "professional", Language: "zh-CN", }, func(chunk llm.StreamChunk) error { if len(chunk.Choices) > 0 && chunk.Choices[0].Delta.Content != "" { collected = append(collected, chunk.Choices[0].Delta.Content) } return nil }) require.NoError(t, err) assert.Len(t, collected, 2) assert.Equal(t, "感谢您的", collected[0]) } func TestCaptainTaskService_RewriteStream_默认参数(t *testing.T) { _, mockProvider, svc := setupCaptainTaskService(t) mockProvider.streamChunks = []llm.StreamChunk{ {Choices: []llm.StreamChoice{{Delta: llm.StreamDelta{Content: "重写文本"}, FinishReason: "stop"}}}, } err := svc.RewriteStream(context.Background(), 1, &TaskRewriteRequest{ Content: "原始内容", }, func(chunk llm.StreamChunk) error { return nil }) require.NoError(t, err) require.NotNil(t, mockProvider.lastStreamReq) // 默认 tone 为 professional, language 为 zh-CN assert.Contains(t, mockProvider.lastStreamReq.Messages[0].Content, "zh-CN") } func TestCaptainTaskService_RewriteStream_LLM流错误(t *testing.T) { _, mockProvider, svc := setupCaptainTaskService(t) mockProvider.streamError = fmt.Errorf("LLM stream 不可用") err := svc.RewriteStream(context.Background(), 1, &TaskRewriteRequest{ Content: "test", }, func(chunk llm.StreamChunk) error { return nil }) assert.Error(t, err) } func TestCaptainTaskService_ReplySuggestionStream_回调中断(t *testing.T) { db, mockProvider, svc := setupCaptainTaskService(t) account := createTestAccount(t, db) assistant := createTestAssistant(t, db, account.ID, "助手") conv := createTestConversationWithMessages(t, db, account.ID, []struct{ sender, content string }{ {"contact", "hello"}, }) mockProvider.embeddingResponse = makeEmbeddingResponse() mockProvider.streamChunks = []llm.StreamChunk{ {Choices: []llm.StreamChoice{{Delta: llm.StreamDelta{Content: "chunk1"}, FinishReason: ""}}}, {Choices: []llm.StreamChoice{{Delta: llm.StreamDelta{Content: "chunk2"}, FinishReason: ""}}}, {Choices: []llm.StreamChoice{{Delta: llm.StreamDelta{Content: "chunk3"}, FinishReason: "stop"}}}, } callCount := 0 err := svc.ReplySuggestionStream(context.Background(), account.ID, &TaskReplySuggestionRequest{ AssistantID: assistant.ID, ConversationID: conv.ID, }, func(chunk llm.StreamChunk) error { callCount++ if callCount == 2 { return fmt.Errorf("callback interrupted") } return nil }) assert.Error(t, err) assert.Contains(t, err.Error(), "callback interrupted") assert.Equal(t, 2, callCount) }