package autoassignment import ( "context" "fmt" "strings" "testing" "github.com/alicebob/miniredis/v2" "github.com/gochat/gochat/internal/channel" "github.com/gochat/gochat/internal/model" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) func setupFullAADB_Cov9(t *testing.T) (*gorm.DB, *redis.Client) { t.Helper() db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", strings.ReplaceAll(t.Name(), "/", "_"))), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) require.NoError(t, err) require.NoError(t, db.AutoMigrate( &model.Account{}, &model.User{}, &model.AccountUser{}, &model.Inbox{}, &model.InboxMember{}, &model.Contact{}, &model.ContactInbox{}, &model.Conversation{}, &model.Message{}, &AssignmentPolicy{}, &InboxAssignmentPolicy{}, &model.AgentCapacityPolicy{}, &model.InboxCapacityLimit{}, )) mr := miniredis.RunT(t) rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()}) t.Cleanup(func() { _ = rdb.Close() }) return db, rdb } func seedAssignableConversation_Cov9(t *testing.T, db *gorm.DB) (*model.Account, *model.User, *model.Inbox, *model.Conversation) { t.Helper() acc := &model.Account{Name: "cov9-account"} require.NoError(t, db.Create(acc).Error) agent := &model.User{AccountID: acc.ID, Name: "agent", Email: "agent-cov9@example.com", Password: "p", Active: true, Available: true} require.NoError(t, db.Create(agent).Error) require.NoError(t, db.Create(&model.AccountUser{AccountID: acc.ID, UserID: agent.ID, Role: "agent"}).Error) inbox := &model.Inbox{AccountID: acc.ID, Name: "inbox", ChannelType: "web_widget", EnableAutoAssignment: true} require.NoError(t, db.Create(inbox).Error) require.NoError(t, db.Create(&model.InboxMember{InboxID: inbox.ID, UserID: agent.ID, Role: "agent", AvailabilityStatus: "online"}).Error) contact := &model.Contact{AccountID: acc.ID, Name: "contact", Email: "contact-cov9@example.com"} require.NoError(t, db.Create(contact).Error) conv := &model.Conversation{AccountID: acc.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen)} require.NoError(t, db.Create(conv).Error) return acc, agent, inbox, conv } func TestEffectivePolicyLimitWindow_ActiveInactive_Cov9(t *testing.T) { activeAccount := &AssignmentPolicy{Policy: PolicyLongestWaiting, FairDistributionLimit: 11, FairDistributionWindow: 111, Active: true} inactiveAccount := &AssignmentPolicy{Policy: PolicyLowestLoad, FairDistributionLimit: 22, FairDistributionWindow: 222, Active: false} activeInbox := &InboxAssignmentPolicy{Policy: PolicyLowestLoad, FairDistributionLimit: 33, FairDistributionWindow: 333, Active: true} inactiveInbox := &InboxAssignmentPolicy{Policy: PolicyLowestLoad, FairDistributionLimit: 44, FairDistributionWindow: 444, Active: false} require.Equal(t, PolicyRoundRobin, EffectivePolicy(nil, nil)) require.Equal(t, PolicyLongestWaiting, EffectivePolicy(activeAccount, nil)) require.Equal(t, PolicyRoundRobin, EffectivePolicy(inactiveAccount, nil)) require.Equal(t, PolicyLowestLoad, EffectivePolicy(activeAccount, activeInbox)) require.Equal(t, PolicyLongestWaiting, EffectivePolicy(activeAccount, inactiveInbox)) require.Equal(t, 5, EffectiveLimit(nil, nil)) require.Equal(t, 11, EffectiveLimit(activeAccount, nil)) require.Equal(t, 22, EffectiveLimit(inactiveAccount, nil)) require.Equal(t, 33, EffectiveLimit(activeAccount, activeInbox)) require.Equal(t, 11, EffectiveLimit(activeAccount, inactiveInbox)) require.Equal(t, 300, EffectiveWindow(nil, nil)) require.Equal(t, 111, EffectiveWindow(activeAccount, nil)) require.Equal(t, 222, EffectiveWindow(inactiveAccount, nil)) require.Equal(t, 333, EffectiveWindow(activeAccount, activeInbox)) require.Equal(t, 111, EffectiveWindow(activeAccount, inactiveInbox)) } func TestAssignmentService_AssignConversation_Success_Cov9(t *testing.T) { db, rdb := setupFullAADB_Cov9(t) acc, agent, inbox, conv := seedAssignableConversation_Cov9(t, db) svc := NewAssignmentService(db, rdb) agentID, err := svc.AssignConversation(context.Background(), conv.ID, inbox.ID, acc.ID) require.NoError(t, err) require.Equal(t, agent.ID, agentID) var updated model.Conversation require.NoError(t, db.First(&updated, conv.ID).Error) require.NotNil(t, updated.AssigneeID) require.Equal(t, agent.ID, *updated.AssigneeID) } func TestAssignmentService_AssignUnassignedConversations_Success_Cov9(t *testing.T) { db, rdb := setupFullAADB_Cov9(t) acc, _, inbox, conv := seedAssignableConversation_Cov9(t, db) svc := NewAssignmentService(db, rdb) ids, err := svc.AssignUnassignedConversations(context.Background(), inbox.ID, acc.ID) require.NoError(t, err) require.Contains(t, ids, conv.ID) } func TestAssignmentService_AssignConversation_NoEligibleAgents_Cov9(t *testing.T) { db, rdb := setupFullAADB_Cov9(t) acc, agent, inbox, conv := seedAssignableConversation_Cov9(t, db) require.NoError(t, db.Model(agent).Update("available", false).Error) svc := NewAssignmentService(db, rdb) agentID, err := svc.AssignConversation(context.Background(), conv.ID, inbox.ID, acc.ID) require.NoError(t, err) require.Zero(t, agentID) } func TestAssignmentService_GetAndPolicyHelpers_Cov9(t *testing.T) { db, rdb := setupFullAADB_Cov9(t) acc, _, inbox, conv := seedAssignableConversation_Cov9(t, db) svc := NewAssignmentService(db, rdb) policy := &AssignmentPolicy{AccountID: acc.ID, Policy: PolicyLowestLoad, FairDistributionLimit: 7, FairDistributionWindow: 77, Active: true} require.NoError(t, db.Create(policy).Error) inboxPolicy := &InboxAssignmentPolicy{AccountID: acc.ID, InboxID: inbox.ID, Policy: PolicyLongestWaiting, FairDistributionLimit: 8, FairDistributionWindow: 88, Active: true} require.NoError(t, db.Create(inboxPolicy).Error) loadedInbox, err := svc.getInbox(context.Background(), inbox.ID) require.NoError(t, err) require.Equal(t, inbox.ID, loadedInbox.ID) convs, err := svc.findUnassignedConversations(context.Background(), inbox.ID, acc.ID) require.NoError(t, err) require.NotEmpty(t, convs) _ = conv require.Equal(t, PolicyLowestLoad, svc.getAccountPolicy(context.Background(), acc.ID).Policy) require.Equal(t, PolicyLongestWaiting, svc.getInboxPolicy(context.Background(), inbox.ID).Policy) } func TestAssignmentService_AgentHasInboxCapacity_Limited_Cov9(t *testing.T) { db, rdb := setupFullAADB_Cov9(t) acc, agent, inbox, conv := seedAssignableConversation_Cov9(t, db) policy := &model.AgentCapacityPolicy{AccountID: acc.ID, Name: "cap"} require.NoError(t, db.Create(policy).Error) require.NoError(t, db.Create(&model.InboxCapacityLimit{AgentCapacityPolicyID: policy.ID, InboxID: inbox.ID, ConversationLimit: 1}).Error) require.NoError(t, db.Model(&model.AccountUser{}).Where("account_id = ? AND user_id = ?", acc.ID, agent.ID).Update("agent_capacity_policy_id", policy.ID).Error) agentID := agent.ID require.NoError(t, db.Model(&model.Conversation{}).Where("id = ?", conv.ID).Update("assignee_id", agentID).Error) svc := NewAssignmentService(db, rdb) ok, err := svc.agentHasInboxCapacity(context.Background(), acc.ID, inbox.ID, agent.ID, 0) require.NoError(t, err) require.False(t, ok) ok, err = svc.agentHasInboxCapacity(context.Background(), acc.ID, inbox.ID, agent.ID, conv.ID) require.NoError(t, err) require.True(t, ok) } func TestRoundRobinAndLowestLoad_Cov9(t *testing.T) { db, rdb := setupFullAADB_Cov9(t) _, agent, inbox, _ := seedAssignableConversation_Cov9(t, db) rr := NewRoundRobinSelector(rdb) rr.SyncQueue(context.Background(), inbox.ID, []uint{agent.ID, agent.ID + 1}) next, err := rr.Next(context.Background(), inbox.ID) require.NoError(t, err) require.NotZero(t, next) ll := NewLowestLoadSelector(db) selected, err := ll.Select(context.Background(), inbox.ID, []uint{agent.ID, agent.ID + 1}) require.NoError(t, err) require.NotZero(t, selected) } func TestAutoAssignmentListener_OnEventPaths_Cov9(t *testing.T) { db, rdb := setupFullAADB_Cov9(t) acc, _, inbox, conv := seedAssignableConversation_Cov9(t, db) listener := NewAutoAssignmentListener(db, rdb) data := map[string]any{"conversation_id": float64(conv.ID), "inbox_id": float64(inbox.ID), "account_id": float64(acc.ID)} require.NoError(t, listener.OnEvent(context.Background(), &channel.ChannelEvent{Type: channel.EventConversationCreated, Data: data})) // Reset assignee to exercise opened/unassigned paths. require.NoError(t, db.Model(&model.Conversation{}).Where("id = ?", conv.ID).Update("assignee_id", nil).Error) require.NoError(t, listener.OnEvent(context.Background(), &channel.ChannelEvent{Type: channel.EventConversationOpened, Data: data})) require.NoError(t, listener.OnEvent(context.Background(), &channel.ChannelEvent{Type: channel.EventConversationUnassigned, Data: data})) require.NoError(t, listener.OnEvent(context.Background(), &channel.ChannelEvent{Type: "ignored", Data: data})) } func TestEventNames_Cov9(t *testing.T) { names := EventNames() require.Contains(t, names, string(channel.EventConversationCreated)) require.Contains(t, names, string(channel.EventConversationOpened)) require.Contains(t, names, string(channel.EventConversationUnassigned)) } func TestConversationQueryModel_TableName_Cov9(t *testing.T) { require.Equal(t, "conversations", ConversationQueryModel{}.TableName()) }