Files
chatwoot/spec/services/assignment_v2/assignment_service_spec.rb
T
2025-07-31 14:29:54 +05:30

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