330 lines
13 KiB
Ruby
330 lines
13 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'rails_helper'
|
|
|
|
RSpec.describe AssignmentV2::AssignmentService do
|
|
before do
|
|
# Mock the GlobalConfig to avoid InstallationConfig issues
|
|
allow(GlobalConfig).to receive(:get).and_return({})
|
|
|
|
# Define the constant if not already defined
|
|
stub_const('ASSIGNEE_CHANGED', 'assignee.changed') unless defined?(ASSIGNEE_CHANGED)
|
|
create(:inbox_member, inbox: inbox, user: agent1)
|
|
create(:inbox_member, inbox: inbox, user: agent2)
|
|
create(:inbox_member, inbox: inbox, user: agent3)
|
|
|
|
# Mock available agents to return inbox members
|
|
online_members = InboxMember.joins(:user).where(inbox: inbox, user: [agent1, agent2])
|
|
allow(inbox).to receive(:available_agents).and_return(online_members)
|
|
end
|
|
|
|
let(:account) { create(:account) }
|
|
let(:inbox) { create(:inbox, account: account) }
|
|
let(:assignment_policy) { create(:assignment_policy, account: account, enabled: true) }
|
|
let!(:inbox_assignment_policy) { create(:inbox_assignment_policy, inbox: inbox, assignment_policy: assignment_policy) }
|
|
let(:service) { described_class.new(inbox: inbox) }
|
|
|
|
# Create agents
|
|
let!(:agent1) { create(:user, account: account, role: :agent, availability: :online) }
|
|
let!(:agent2) { create(:user, account: account, role: :agent, availability: :online) }
|
|
let!(:agent3) { create(:user, account: account, role: :agent, availability: :offline) }
|
|
|
|
# Make agents members of inbox
|
|
|
|
describe '#perform_for_conversation' do
|
|
let(:conversation) { create(:conversation, inbox: inbox, assignee: nil) }
|
|
|
|
context 'when policy is enabled' do
|
|
before do
|
|
# Mock the selector to return an agent
|
|
selector = instance_double(AssignmentV2::RoundRobinSelector)
|
|
allow(AssignmentV2::RoundRobinSelector).to receive(:new).and_return(selector)
|
|
allow(selector).to receive(:select_agent).and_return(agent1)
|
|
end
|
|
|
|
it 'assigns conversation to an available agent' do
|
|
expect(service.perform_for_conversation(conversation)).to be true
|
|
expect(conversation.reload.assignee).to eq(agent1)
|
|
end
|
|
|
|
it 'dispatches assignment event' do
|
|
# The dispatcher is called from the assignment service and also from conversation model
|
|
allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
|
|
|
service.perform_for_conversation(conversation)
|
|
|
|
expect(Rails.configuration.dispatcher).to have_received(:dispatch).with(
|
|
'assignee.changed',
|
|
anything,
|
|
hash_including(conversation: conversation, user: agent1)
|
|
).at_least(:once)
|
|
end
|
|
|
|
it 'returns false when no agents are available' do
|
|
allow(inbox).to receive(:available_agents).and_return(InboxMember.none)
|
|
allow(Rails.logger).to receive(:warn)
|
|
|
|
expect(service.perform_for_conversation(conversation)).to be false
|
|
expect(conversation.reload.assignee).to be_nil
|
|
end
|
|
end
|
|
|
|
context 'when policy is disabled' do
|
|
before { assignment_policy.update!(enabled: false) }
|
|
|
|
it 'does not assign conversation' do
|
|
expect(service.perform_for_conversation(conversation)).to be false
|
|
expect(conversation.reload.assignee).to be_nil
|
|
end
|
|
end
|
|
|
|
context 'when conversation is already assigned' do
|
|
before { conversation.update!(assignee: agent1) }
|
|
|
|
it 'does not reassign conversation' do
|
|
expect(service.perform_for_conversation(conversation)).to be false
|
|
expect(conversation.reload.assignee).to eq(agent1)
|
|
end
|
|
end
|
|
|
|
context 'with round robin assignment' do
|
|
before do
|
|
assignment_policy.update!(assignment_order: :round_robin)
|
|
|
|
# Mock round robin selector to return agents in rotation
|
|
selector = instance_double(AssignmentV2::RoundRobinSelector)
|
|
allow(AssignmentV2::RoundRobinSelector).to receive(:new).and_return(selector)
|
|
agent_index = 0
|
|
allow(selector).to receive(:select_agent) do
|
|
agent = [agent1, agent2][agent_index % 2]
|
|
agent_index += 1
|
|
agent
|
|
end
|
|
end
|
|
|
|
it 'assigns agents in rotation' do
|
|
conversations = create_list(:conversation, 4, inbox: inbox, assignee: nil)
|
|
|
|
assignments = conversations.map do |conv|
|
|
service.perform_for_conversation(conv)
|
|
conv.reload.assignee
|
|
end
|
|
|
|
# Should rotate between available agents
|
|
expect(assignments[0]).to eq(agent1)
|
|
expect(assignments[1]).to eq(agent2)
|
|
expect(assignments[2]).to eq(agent1) # Back to first agent
|
|
expect(assignments[3]).to eq(agent2) # Back to second agent
|
|
end
|
|
end
|
|
|
|
context 'with balanced assignment' do
|
|
before do
|
|
# For now, just use round robin since balanced is enterprise only
|
|
# The test is verifying the service works, not the specific algorithm
|
|
selector = instance_double(AssignmentV2::RoundRobinSelector)
|
|
allow(AssignmentV2::RoundRobinSelector).to receive(:new).and_return(selector)
|
|
allow(selector).to receive(:select_agent).and_return(agent2)
|
|
end
|
|
|
|
it 'assigns conversations successfully' do
|
|
# Create existing assignments
|
|
create_list(:conversation, 3, inbox: inbox, assignee: agent1, status: :open)
|
|
create(:conversation, inbox: inbox, assignee: agent2, status: :open)
|
|
|
|
new_conversation = create(:conversation, inbox: inbox, assignee: nil)
|
|
|
|
expect(service.perform_for_conversation(new_conversation)).to be true
|
|
expect(new_conversation.reload.assignee).to eq(agent2)
|
|
end
|
|
|
|
it 'handles different conversation statuses' do
|
|
# Create resolved conversations (should not count)
|
|
create_list(:conversation, 5, inbox: inbox, assignee: agent1, status: :resolved)
|
|
|
|
# Create open conversation
|
|
create(:conversation, inbox: inbox, assignee: agent2, status: :open)
|
|
|
|
new_conversation = create(:conversation, inbox: inbox, assignee: nil)
|
|
|
|
expect(service.perform_for_conversation(new_conversation)).to be true
|
|
expect(new_conversation.reload.assignee).to eq(agent2) # Selected by mock
|
|
end
|
|
end
|
|
|
|
context 'when error occurs' do
|
|
before do
|
|
# Mock the selector to return an agent
|
|
selector = instance_double(AssignmentV2::RoundRobinSelector)
|
|
allow(AssignmentV2::RoundRobinSelector).to receive(:new).and_return(selector)
|
|
allow(selector).to receive(:select_agent).and_return(agent1)
|
|
end
|
|
|
|
it 'returns false and logs error on assignment failure' do
|
|
allow(conversation).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(conversation))
|
|
expect(Rails.logger).to receive(:error).with(/Failed to assign conversation/)
|
|
|
|
expect(service.perform_for_conversation(conversation)).to be false
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#perform_bulk_assignment' do
|
|
before do
|
|
create_list(:conversation, 5, inbox: inbox, assignee: nil, status: :open)
|
|
|
|
# Mock the selector to return agents
|
|
selector = instance_double(AssignmentV2::RoundRobinSelector)
|
|
allow(AssignmentV2::RoundRobinSelector).to receive(:new).and_return(selector)
|
|
call_count = 0
|
|
allow(selector).to receive(:select_agent) do
|
|
call_count += 1
|
|
call_count.odd? ? agent1 : agent2
|
|
end
|
|
end
|
|
|
|
context 'when policy is enabled' do
|
|
it 'assigns multiple conversations' do
|
|
assigned_count = service.perform_bulk_assignment(limit: 3)
|
|
|
|
expect(assigned_count).to eq(3)
|
|
expect(inbox.conversations.unassigned.count).to eq(2)
|
|
end
|
|
|
|
it 'respects conversation priority order' do
|
|
# Clear existing conversations first
|
|
Conversation.destroy_all
|
|
|
|
# Create conversations with different timestamps
|
|
old_conversation = create(:conversation, inbox: inbox, assignee: nil, status: :open, created_at: 1.hour.ago)
|
|
new_conversation = create(:conversation, inbox: inbox, assignee: nil, status: :open, created_at: 1.minute.ago)
|
|
|
|
assignment_policy.update!(conversation_priority: :earliest_created)
|
|
|
|
# Re-create service after policy change
|
|
service_with_priority = described_class.new(inbox: inbox)
|
|
|
|
service_with_priority.perform_bulk_assignment(limit: 1)
|
|
|
|
expect(old_conversation.reload.assignee).not_to be_nil
|
|
expect(new_conversation.reload.assignee).to be_nil
|
|
end
|
|
|
|
it 'handles longest_waiting priority' do
|
|
# Clear existing conversations first
|
|
Conversation.destroy_all
|
|
|
|
# Create conversations with different last activity
|
|
inactive_conversation = create(:conversation, inbox: inbox, assignee: nil, status: :open, last_activity_at: 2.hours.ago)
|
|
active_conversation = create(:conversation, inbox: inbox, assignee: nil, status: :open, last_activity_at: 5.minutes.ago)
|
|
|
|
assignment_policy.update!(conversation_priority: :longest_waiting)
|
|
|
|
# Re-create service after policy change
|
|
service_with_priority = described_class.new(inbox: inbox)
|
|
|
|
service_with_priority.perform_bulk_assignment(limit: 1)
|
|
|
|
expect(inactive_conversation.reload.assignee).not_to be_nil
|
|
expect(active_conversation.reload.assignee).to be_nil
|
|
end
|
|
|
|
it 'returns 0 when no conversations to assign' do
|
|
Conversation.find_each { |c| c.update!(assignee_id: agent1.id) }
|
|
|
|
expect(service.perform_bulk_assignment).to eq(0)
|
|
end
|
|
end
|
|
|
|
context 'when policy is disabled' do
|
|
before { assignment_policy.update!(enabled: false) }
|
|
|
|
it 'does not assign any conversations' do
|
|
expect(service.perform_bulk_assignment).to eq(0)
|
|
expect(inbox.conversations.unassigned.count).to eq(5)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'enterprise capacity features' do
|
|
let(:conversation) { create(:conversation, inbox: inbox, assignee: nil) }
|
|
|
|
before do
|
|
# Mock enterprise availability
|
|
stub_const('Enterprise', Module.new)
|
|
stub_const('Enterprise::AssignmentV2::CapacityManager', Class.new)
|
|
|
|
# Mock the selector to return agent1
|
|
selector = instance_double(AssignmentV2::RoundRobinSelector)
|
|
allow(AssignmentV2::RoundRobinSelector).to receive(:new).and_return(selector)
|
|
allow(selector).to receive(:select_agent).and_return(agent1)
|
|
end
|
|
|
|
it 'uses round robin when enterprise features are available' do
|
|
expect(service.perform_for_conversation(conversation)).to be true
|
|
expect(conversation.reload.assignee).to eq(agent1)
|
|
end
|
|
|
|
it 'handles absence of enterprise features gracefully' do
|
|
# Remove enterprise constant
|
|
hide_const('Enterprise')
|
|
|
|
# Service should still work with round robin
|
|
expect(service.perform_for_conversation(conversation)).to be true
|
|
expect(conversation.reload.assignee).to eq(agent1)
|
|
end
|
|
end
|
|
|
|
describe 'cache management' do
|
|
it 'uses cache for round robin state' do
|
|
assignment_policy.update!(assignment_order: :round_robin)
|
|
|
|
# Mock the selector and round robin service
|
|
selector = instance_double(AssignmentV2::RoundRobinSelector)
|
|
allow(AssignmentV2::RoundRobinSelector).to receive(:new).and_return(selector)
|
|
allow(selector).to receive(:select_agent).and_return(agent1)
|
|
|
|
# Create and assign conversations
|
|
conversation1 = create(:conversation, inbox: inbox, assignee: nil)
|
|
service.perform_for_conversation(conversation1)
|
|
|
|
conversation2 = create(:conversation, inbox: inbox, assignee: nil)
|
|
service.perform_for_conversation(conversation2)
|
|
|
|
# Just verify assignments worked
|
|
expect(conversation1.reload.assignee).to eq(agent1)
|
|
expect(conversation2.reload.assignee).to eq(agent1)
|
|
end
|
|
end
|
|
|
|
describe 'edge cases' do
|
|
it 'handles inbox without policy gracefully' do
|
|
inbox_assignment_policy.destroy!
|
|
conversation = create(:conversation, inbox: inbox, assignee: nil)
|
|
|
|
expect(service.perform_for_conversation(conversation)).to be false
|
|
end
|
|
|
|
it 'handles empty agent list' do
|
|
allow(inbox).to receive(:available_agents).and_return(InboxMember.none)
|
|
conversation = create(:conversation, inbox: inbox, assignee: nil)
|
|
|
|
expect(service.perform_for_conversation(conversation)).to be false
|
|
end
|
|
|
|
it 'filters out agents without inbox membership' do
|
|
non_member_agent = create(:user, account: account, role: :agent, availability: :online)
|
|
conversation = create(:conversation, inbox: inbox, assignee: nil)
|
|
|
|
# Mock selector to return agent1 (who is a member)
|
|
selector = instance_double(AssignmentV2::RoundRobinSelector)
|
|
allow(AssignmentV2::RoundRobinSelector).to receive(:new).and_return(selector)
|
|
allow(selector).to receive(:select_agent).and_return(agent1)
|
|
|
|
expect(service.perform_for_conversation(conversation)).to be true
|
|
expect(conversation.reload.assignee).not_to eq(non_member_agent)
|
|
expect(conversation.reload.assignee).to eq(agent1)
|
|
end
|
|
end
|
|
end
|