package service import ( "context" "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 task extended tests --- type mockTaskExtendedLLM struct { response *llm.ChatResponse err error } func (m *mockTaskExtendedLLM) ChatCompletion(_ context.Context, _ llm.ChatRequest) (*llm.ChatResponse, error) { return m.response, m.err } func (m *mockTaskExtendedLLM) CreateEmbedding(_ context.Context, _ llm.EmbeddingRequest) (*llm.EmbeddingResponse, error) { return nil, nil } func (m *mockTaskExtendedLLM) ChatCompletionStream(_ context.Context, _ llm.ChatRequest, _ func(llm.StreamChunk) error) error { return nil } func setupTaskExtendedTestDB(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 setupTaskExtendedService(t *testing.T, mockLLM *mockTaskExtendedLLM) (*CaptainTaskExtendedService, *gorm.DB) { t.Helper() db := setupTaskExtendedTestDB(t) convRepo := repository.NewConversationRepo(db) msgRepo := repository.NewMessageRepo(db) assistantRepo := repository.NewCaptainAssistantRepo(db) prefRepo := repository.NewCaptainPreferenceRepo(db) svc := NewCaptainTaskExtendedService(convRepo, msgRepo, assistantRepo, prefRepo, mockLLM) return svc, db } // --- Helper: seed test data --- func seedTaskExtendedConversation(t *testing.T, db *gorm.DB, accountID uint) (*model.Conversation, *model.Message) { t.Helper() inbox := &model.Inbox{AccountID: accountID, Name: "Test Inbox", ChannelType: "web_widget"} require.NoError(t, db.Create(inbox).Error) contact := &model.Contact{AccountID: accountID, Name: "Test Contact"} 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: "I need help with billing", ContentType: "text", MessageType: "incoming", } require.NoError(t, db.Create(msg).Error) return conv, msg } // --- Label Suggestion Tests --- func TestCaptainTaskExtendedService_SuggestLabels(t *testing.T) { mockLLM := &mockTaskExtendedLLM{ response: &llm.ChatResponse{ Choices: []llm.ChatChoice{ { Message: llm.ChatMessage{ Role: "assistant", Content: `{"labels": ["billing", "urgent"], "priority": "high", "reason": "customer has billing issue"}`, }, }, }, }, } svc, db := setupTaskExtendedService(t, mockLLM) ctx := context.Background() conv, _ := seedTaskExtendedConversation(t, db, 1) query := &LabelSuggestionQuery{ConversationIDs: []uint{conv.ID}} result, err := svc.SuggestLabels(ctx, 1, query) require.NoError(t, err) assert.Len(t, result.Suggestions, 1) assert.Equal(t, conv.ID, result.Suggestions[0].ConversationID) assert.Contains(t, result.Suggestions[0].Labels, "billing") assert.Equal(t, "high", result.Suggestions[0].Priority) } func TestCaptainTaskExtendedService_SuggestLabels_EmptyConversation(t *testing.T) { mockLLM := &mockTaskExtendedLLM{} svc, db := setupTaskExtendedService(t, mockLLM) ctx := context.Background() // Create conversation with no messages inbox := &model.Inbox{AccountID: 1, Name: "Empty 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) query := &LabelSuggestionQuery{ConversationIDs: []uint{conv.ID}} result, err := svc.SuggestLabels(ctx, 1, query) require.NoError(t, err) assert.Empty(t, result.Suggestions) // no messages = skip } func TestCaptainTaskExtendedService_SuggestLabels_LLMError(t *testing.T) { mockLLM := &mockTaskExtendedLLM{err: fmt.Errorf("LLM unavailable")} svc, db := setupTaskExtendedService(t, mockLLM) ctx := context.Background() conv, _ := seedTaskExtendedConversation(t, db, 1) query := &LabelSuggestionQuery{ConversationIDs: []uint{conv.ID}} // Service skips conversations where LLM fails, returns empty suggestions result, err := svc.SuggestLabels(ctx, 1, query) require.NoError(t, err) assert.Empty(t, result.Suggestions) } // --- Follow Up Tests --- func TestCaptainTaskExtendedService_SuggestFollowUp(t *testing.T) { mockLLM := &mockTaskExtendedLLM{ response: &llm.ChatResponse{ Choices: []llm.ChatChoice{ { Message: llm.ChatMessage{ Role: "assistant", Content: `{"title": "Resolve billing", "description": "Follow up on billing issue", "priority": "high", "due_date_hint": "within 24 hours"}`, }, }, }, }, } svc, db := setupTaskExtendedService(t, mockLLM) ctx := context.Background() conv, _ := seedTaskExtendedConversation(t, db, 1) query := &FollowUpQuery{ConversationIDs: []uint{conv.ID}} result, err := svc.SuggestFollowUp(ctx, 1, query) require.NoError(t, err) assert.Len(t, result.FollowUps, 1) assert.Equal(t, conv.ID, result.FollowUps[0].ConversationID) assert.Equal(t, "Resolve billing", result.FollowUps[0].Title) assert.Equal(t, "high", result.FollowUps[0].Priority) }