package service import ( "context" "encoding/json" "fmt" "testing" "github.com/gochat/gochat/internal/llm" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" ) // --- Mock LLM Provider for assistant response tests --- type mockResponseLLM struct { response *llm.ChatResponse err error } func (m *mockResponseLLM) ChatCompletion(_ context.Context, _ llm.ChatRequest) (*llm.ChatResponse, error) { return m.response, m.err } func (m *mockResponseLLM) CreateEmbedding(_ context.Context, _ llm.EmbeddingRequest) (*llm.EmbeddingResponse, error) { return nil, nil } func (m *mockResponseLLM) ChatCompletionStream(_ context.Context, _ llm.ChatRequest, _ func(llm.StreamChunk) error) error { return nil } func setupAssistantResponseTestDB(t *testing.T) *gorm.DB { t.Helper() dbName := fmt.Sprintf("file:%s?mode=memory&cache=private", t.Name()) db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{}) require.NoError(t, err) require.NoError(t, db.AutoMigrate( &model.CaptainPreference{}, &model.CaptainAssistant{}, &model.Conversation{}, &model.Message{}, &model.Inbox{}, &model.Contact{}, &model.Account{}, )) return db } func setupAssistantResponseService(t *testing.T, mockLLM *mockResponseLLM) (*CaptainAssistantResponseService, *gorm.DB) { t.Helper() db := setupAssistantResponseTestDB(t) assistantRepo := repository.NewCaptainAssistantRepo(db) responseRepo := repository.NewCaptainAssistantResponseRepo(db) convRepo := repository.NewConversationRepo(db) msgRepo := repository.NewMessageRepo(db) prefRepo := repository.NewCaptainPreferenceRepo(db) svc := NewCaptainAssistantResponseService(assistantRepo, responseRepo, convRepo, msgRepo, prefRepo, mockLLM) return svc, db } func seedAssistantResponseData(t *testing.T, db *gorm.DB, accountID uint) (*model.CaptainAssistant, *model.Conversation, *model.Message) { t.Helper() inbox := &model.Inbox{AccountID: accountID, Name: "Support Inbox", ChannelType: "web_widget"} require.NoError(t, db.Create(inbox).Error) contact := &model.Contact{AccountID: accountID, Name: "Customer"} require.NoError(t, db.Create(contact).Error) conv := &model.Conversation{AccountID: accountID, InboxID: inbox.ID, Status: "open"} require.NoError(t, db.Create(conv).Error) msg := &model.Message{ ConversationID: conv.ID, AccountID: accountID, InboxID: inbox.ID, SenderType: "contact", Content: "How do I reset my password?", ContentType: "text", MessageType: "incoming", } require.NoError(t, db.Create(msg).Error) // Create assistant with config cfg := &model.AssistantConfig{Instructions: "Be helpful and concise", Temperature: 0.7} cfgJSON, err := json.Marshal(cfg) require.NoError(t, err) assistant := &model.CaptainAssistant{ AccountID: accountID, Name: "Support Bot", Config: cfgJSON, Status: model.AssistantStatusActive, } require.NoError(t, db.Create(assistant).Error) return assistant, conv, msg } func TestCaptainAssistantResponseService_ProcessResponse(t *testing.T) { mockLLM := &mockResponseLLM{ response: &llm.ChatResponse{ Choices: []llm.ChatChoice{ { Message: llm.ChatMessage{ Role: "assistant", Content: "To reset your password, go to Settings > Security > Reset Password.", }, }, }, }, } svc, db := setupAssistantResponseService(t, mockLLM) ctx := context.Background() assistant, conv, _ := seedAssistantResponseData(t, db, 1) req := &ProcessResponseRequest{ ConversationID: conv.ID, AssistantID: assistant.ID, SendMessage: false, } result, err := svc.ProcessResponse(ctx, 1, req) require.NoError(t, err) assert.Equal(t, "To reset your password, go to Settings > Security > Reset Password.", result.Content) assert.Equal(t, conv.ID, result.ConversationID) assert.Equal(t, assistant.ID, result.AssistantID) assert.False(t, result.Sent) // send_message=false assert.Zero(t, result.MessageID) } func TestCaptainAssistantResponseService_ProcessResponse_WithSendMessage(t *testing.T) { mockLLM := &mockResponseLLM{ response: &llm.ChatResponse{ Choices: []llm.ChatChoice{ { Message: llm.ChatMessage{ Role: "assistant", Content: "Here is how to reset your password.", }, }, }, }, } svc, db := setupAssistantResponseService(t, mockLLM) ctx := context.Background() assistant, conv, _ := seedAssistantResponseData(t, db, 1) req := &ProcessResponseRequest{ ConversationID: conv.ID, AssistantID: assistant.ID, SendMessage: true, } result, err := svc.ProcessResponse(ctx, 1, req) require.NoError(t, err) assert.Equal(t, "Here is how to reset your password.", result.Content) assert.True(t, result.Sent) assert.NotZero(t, result.MessageID) } func TestCaptainAssistantResponseService_ProcessResponse_AssistantNotFound(t *testing.T) { mockLLM := &mockResponseLLM{} svc, db := setupAssistantResponseService(t, mockLLM) ctx := context.Background() // Create conversation but no assistant inbox := &model.Inbox{AccountID: 1, Name: "Inbox", ChannelType: "web_widget"} require.NoError(t, db.Create(inbox).Error) conv := &model.Conversation{AccountID: 1, InboxID: inbox.ID, Status: "open"} require.NoError(t, db.Create(conv).Error) req := &ProcessResponseRequest{ ConversationID: conv.ID, AssistantID: 999, // nonexistent SendMessage: false, } _, err := svc.ProcessResponse(ctx, 1, req) assert.Error(t, err) assert.Contains(t, err.Error(), "assistant not found") } func TestCaptainAssistantResponseService_ProcessResponse_LLMError(t *testing.T) { mockLLM := &mockResponseLLM{err: fmt.Errorf("LLM service unavailable")} svc, db := setupAssistantResponseService(t, mockLLM) ctx := context.Background() assistant, conv, _ := seedAssistantResponseData(t, db, 1) req := &ProcessResponseRequest{ ConversationID: conv.ID, AssistantID: assistant.ID, SendMessage: false, } _, err := svc.ProcessResponse(ctx, 1, req) assert.Error(t, err) assert.Contains(t, err.Error(), "generate response") } func TestCaptainAssistantResponseService_ProcessResponse_NoLLMContent(t *testing.T) { mockLLM := &mockResponseLLM{ response: &llm.ChatResponse{Choices: []llm.ChatChoice{}}, // empty choices } svc, db := setupAssistantResponseService(t, mockLLM) ctx := context.Background() assistant, conv, _ := seedAssistantResponseData(t, db, 1) req := &ProcessResponseRequest{ ConversationID: conv.ID, AssistantID: assistant.ID, SendMessage: false, } _, err := svc.ProcessResponse(ctx, 1, req) assert.Error(t, err) assert.Contains(t, err.Error(), "no LLM response") }