update the spec file paths

This commit is contained in:
Tanmay Deep Sharma
2025-09-04 16:11:06 +07:00
parent a4fab22e28
commit b95944dd4e
9 changed files with 486 additions and 366 deletions
@@ -0,0 +1,95 @@
require 'rails_helper'
RSpec.describe Enterprise::AutoAssignment::BalancedSelector do
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
let(:selector) { described_class.new(inbox: inbox) }
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: :online) }
before do
@member1 = create(:inbox_member, inbox: inbox, user: agent1)
@member2 = create(:inbox_member, inbox: inbox, user: agent2)
@member3 = create(:inbox_member, inbox: inbox, user: agent3)
end
describe '#select_agent' do
context 'when selecting based on workload' do
let(:available_agents) { [@member1, @member2, @member3] }
it 'selects the agent with least open conversations' do
# Agent1 has 3 open conversations
3.times { create(:conversation, inbox: inbox, assignee: agent1, status: 'open') }
# Agent2 has 1 open conversation
create(:conversation, inbox: inbox, assignee: agent2, status: 'open')
# Agent3 has 2 open conversations
2.times { create(:conversation, inbox: inbox, assignee: agent3, status: 'open') }
selected_agent = selector.select_agent(available_agents)
# Should select agent2 as they have the least conversations
expect(selected_agent).to eq(agent2)
end
it 'considers only open conversations' do
# Agent1 has 1 open and 3 resolved conversations
create(:conversation, inbox: inbox, assignee: agent1, status: 'open')
3.times { create(:conversation, inbox: inbox, assignee: agent1, status: 'resolved') }
# Agent2 has 2 open conversations
2.times { create(:conversation, inbox: inbox, assignee: agent2, status: 'open') }
selected_agent = selector.select_agent([member1, @member2])
# Should select agent1 as they have fewer open conversations
expect(selected_agent).to eq(agent1)
end
it 'uses round robin as fallback when agents have equal workload' do
# All agents have same number of conversations
[@member1, @member2, @member3].each do |member|
create(:conversation, inbox: inbox, assignee: member.user, status: 'open')
end
round_robin_service = instance_double(AutoAssignment::InboxRoundRobinService)
allow(AutoAssignment::InboxRoundRobinService).to receive(:new).and_return(round_robin_service)
expect(round_robin_service).to receive(:available_agent).with(
allowed_agent_ids: [agent1.id.to_s, agent2.id.to_s, agent3.id.to_s]
)
selector.select_agent(available_agents)
end
end
context 'when no agents are available' do
it 'returns nil' do
selected_agent = selector.select_agent([])
expect(selected_agent).to be_nil
end
end
context 'when one agent is available' do
it 'returns that agent' do
selected_agent = selector.select_agent([@member1])
expect(selected_agent).to eq(agent1)
end
end
context 'with new agents (no conversations)' do
it 'prioritizes agents with no conversations' do
# Agent1 and 2 have conversations
create(:conversation, inbox: inbox, assignee: agent1, status: 'open')
create(:conversation, inbox: inbox, assignee: agent2, status: 'open')
# Agent3 is new with no conversations
selected_agent = selector.select_agent(available_agents)
expect(selected_agent).to eq(agent3)
end
end
end
end
@@ -0,0 +1,83 @@
require 'rails_helper'
RSpec.describe AutoAssignment::AssignmentJob, type: :job do
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account, enable_auto_assignment: true) }
let(:agent) { create(:user, account: account, role: :agent, availability: :online) }
before do
create(:inbox_member, inbox: inbox, user: agent)
end
describe '#perform' do
context 'when inbox exists' do
context 'when auto assignment is enabled' do
it 'calls the assignment service' do
service = instance_double(AutoAssignment::AssignmentService)
allow(AutoAssignment::AssignmentService).to receive(:new).with(inbox: inbox).and_return(service)
expect(service).to receive(:perform_bulk_assignment).with(limit: 100).and_return(5)
described_class.new.perform(inbox_id: inbox.id)
end
it 'logs the assignment count' do
service = instance_double(AutoAssignment::AssignmentService)
allow(AutoAssignment::AssignmentService).to receive(:new).and_return(service)
allow(service).to receive(:perform_bulk_assignment).and_return(3)
expect(Rails.logger).to receive(:info).with("Assigned 3 conversations for inbox #{inbox.id}")
described_class.new.perform(inbox_id: inbox.id)
end
it 'uses custom bulk limit from environment' do
allow(ENV).to receive(:fetch).with('AUTO_ASSIGNMENT_BULK_LIMIT', 100).and_return('50')
service = instance_double(AutoAssignment::AssignmentService)
allow(AutoAssignment::AssignmentService).to receive(:new).with(inbox: inbox).and_return(service)
expect(service).to receive(:perform_bulk_assignment).with(limit: 50).and_return(2)
described_class.new.perform(inbox_id: inbox.id)
end
end
context 'when auto assignment is disabled' do
before { inbox.update!(enable_auto_assignment: false) }
it 'returns early without processing' do
expect(AutoAssignment::AssignmentService).not_to receive(:new)
described_class.new.perform(inbox_id: inbox.id)
end
end
end
context 'when inbox does not exist' do
it 'returns early without processing' do
expect(AutoAssignment::AssignmentService).not_to receive(:new)
described_class.new.perform(inbox_id: 999999)
end
end
context 'when an error occurs' do
it 'logs the error and re-raises in test environment' do
service = instance_double(AutoAssignment::AssignmentService)
allow(AutoAssignment::AssignmentService).to receive(:new).and_return(service)
allow(service).to receive(:perform_bulk_assignment).and_raise(StandardError, 'Something went wrong')
expect(Rails.logger).to receive(:error).with("Bulk assignment failed for inbox #{inbox.id}: Something went wrong")
expect {
described_class.new.perform(inbox_id: inbox.id)
}.to raise_error(StandardError, 'Something went wrong')
end
end
end
describe 'job configuration' do
it 'is queued in the default queue' do
expect(described_class.queue_name).to eq('default')
end
end
end
@@ -0,0 +1,92 @@
require 'rails_helper'
RSpec.describe AutoAssignment::PeriodicAssignmentJob, type: :job do
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account, enable_auto_assignment: true) }
let(:assignment_policy) { create(:assignment_policy, inbox: inbox) }
let(:agent) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, inbox: inbox, user: agent)
end
describe '#perform' do
context 'when account has assignment_v2 feature enabled' do
before do
allow(account).to receive(:feature_enabled?).with('assignment_v2').and_return(true)
end
context 'when inbox has auto_assignment_v2 enabled' do
before do
allow(inbox).to receive(:auto_assignment_v2_enabled?).and_return(true)
end
it 'queues assignment job for eligible inboxes' do
expect(AutoAssignment::AssignmentJob).to receive(:perform_later).with(inbox_id: inbox.id)
described_class.new.perform
end
it 'processes multiple accounts' do
account2 = create(:account)
inbox2 = create(:inbox, account: account2, enable_auto_assignment: true)
create(:assignment_policy, inbox: inbox2)
allow(account2).to receive(:feature_enabled?).with('assignment_v2').and_return(true)
allow(inbox2).to receive(:auto_assignment_v2_enabled?).and_return(true)
expect(AutoAssignment::AssignmentJob).to receive(:perform_later).with(inbox_id: inbox.id)
expect(AutoAssignment::AssignmentJob).to receive(:perform_later).with(inbox_id: inbox2.id)
described_class.new.perform
end
end
context 'when inbox does not have auto_assignment_v2 enabled' do
before do
allow(inbox).to receive(:auto_assignment_v2_enabled?).and_return(false)
end
it 'does not queue assignment job' do
expect(AutoAssignment::AssignmentJob).not_to receive(:perform_later)
described_class.new.perform
end
end
end
context 'when account does not have assignment_v2 feature enabled' do
before do
allow(account).to receive(:feature_enabled?).with('assignment_v2').and_return(false)
end
it 'does not process the account' do
expect(AutoAssignment::AssignmentJob).not_to receive(:perform_later)
described_class.new.perform
end
end
context 'with batch processing' do
it 'processes accounts in batches' do
# Create multiple accounts
5.times do |i|
acc = create(:account)
inb = create(:inbox, account: acc, enable_auto_assignment: true)
create(:assignment_policy, inbox: inb)
allow(acc).to receive(:feature_enabled?).with('assignment_v2').and_return(true)
allow(inb).to receive(:auto_assignment_v2_enabled?).and_return(true)
end
expect(Account).to receive(:find_in_batches).and_call_original
described_class.new.perform
end
end
end
describe 'job configuration' do
it 'is queued in the scheduled_jobs queue' do
expect(described_class.queue_name).to eq('scheduled_jobs')
end
end
end
@@ -1,82 +0,0 @@
require 'rails_helper'
RSpec.describe AutoAssignment::AssignmentService do
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
let(:agent) { create(:user, account: account, role: :agent, availability: :online) }
let(:service) { described_class.new(inbox: inbox) }
before do
create(:inbox_member, inbox: inbox, user: agent)
allow(OnlineStatusTracker).to receive(:get_available_users)
.and_return({ agent.id.to_s => 'online' })
end
def create_test_conversation(attrs = {})
conversation = build(:conversation, attrs.reverse_merge(inbox: inbox, assignee: nil, status: :open))
allow(conversation).to receive(:run_auto_assignment).and_return(nil)
conversation.save!
conversation
end
describe 'basic assignment' do
context 'when auto assignment is enabled' do
before { inbox.update!(enable_auto_assignment: true) }
it 'assigns an available agent to conversation' do
conversation = create_test_conversation
assigned_count = service.perform_bulk_assignment(limit: 1)
expect(assigned_count).to eq(1)
expect(conversation.reload.assignee).to eq(agent)
end
it 'returns 0 when no agents are online' do
allow(OnlineStatusTracker).to receive(:get_available_users).and_return({})
conversation = create_test_conversation
assigned_count = service.perform_bulk_assignment(limit: 1)
expect(assigned_count).to eq(0)
expect(conversation.reload.assignee).to be_nil
end
end
context 'when auto assignment is disabled' do
before { inbox.update!(enable_auto_assignment: false) }
it 'does not assign any agent' do
conversation = create_test_conversation
assigned_count = service.perform_bulk_assignment(limit: 1)
expect(assigned_count).to eq(0)
expect(conversation.reload.assignee).to be_nil
end
end
end
describe 'assignment conditions' do
before { inbox.update!(enable_auto_assignment: true) }
it 'only assigns to open conversations' do
resolved_conversation = create_test_conversation(status: 'resolved')
assigned_count = service.perform_bulk_assignment(limit: 1)
expect(assigned_count).to eq(0)
expect(resolved_conversation.reload.assignee).to be_nil
end
it 'does not reassign already assigned conversations' do
other_agent = create(:user, account: account, role: :agent)
assigned_conversation = create_test_conversation(assignee: other_agent)
assigned_count = service.perform_bulk_assignment(limit: 1)
expect(assigned_count).to eq(0)
expect(assigned_conversation.reload.assignee).to eq(other_agent)
end
end
end
@@ -1,136 +0,0 @@
require 'rails_helper'
RSpec.describe AutoAssignment::AssignmentService do
let(:account) { create(:account) }
let(:inbox) do
create(:inbox,
account: account,
enable_auto_assignment: true,
auto_assignment_config: {
'fair_distribution_limit' => 2,
'fair_distribution_window' => 3600
})
end
let(:service) { described_class.new(inbox: inbox) }
let(:agent1) { create(:user, account: account, role: :agent, availability: :online) }
let(:agent2) { create(:user, account: account, role: :agent, availability: :online) }
before do
create(:inbox_member, inbox: inbox, user: agent1)
create(:inbox_member, inbox: inbox, user: agent2)
allow(OnlineStatusTracker).to receive(:get_available_users)
.and_return({
agent1.id.to_s => 'online',
agent2.id.to_s => 'online'
})
# Clean up Redis keys for this inbox
clean_redis_keys
end
after do
clean_redis_keys
end
def clean_redis_keys
# Clean up assignment keys for both agents
pattern1 = "assignment:#{inbox.id}:agent:#{agent1.id}:*"
pattern2 = "assignment:#{inbox.id}:agent:#{agent2.id}:*"
Redis::Alfred.scan_each(match: pattern1) { |key| Redis::Alfred.delete(key) }
Redis::Alfred.scan_each(match: pattern2) { |key| Redis::Alfred.delete(key) }
end
def create_test_conversation
conversation = build(:conversation, inbox: inbox, assignee: nil, status: :open)
allow(conversation).to receive(:run_auto_assignment).and_return(nil)
conversation.save!
conversation
end
describe 'with fair distribution enabled' do
it 'respects the assignment limit per agent' do
# Each agent can handle 2 conversations
conversations = Array.new(4) { create_test_conversation }
# Assign first 4 conversations (2 per agent)
assigned_count = service.perform_bulk_assignment(limit: 4)
expect(assigned_count).to eq(4)
# Verify distribution
assigned_to_agent1 = conversations.count { |c| c.reload.assignee == agent1 }
assigned_to_agent2 = conversations.count { |c| c.reload.assignee == agent2 }
expect(assigned_to_agent1).to eq(2)
expect(assigned_to_agent2).to eq(2)
# Fifth conversation should not be assigned (both agents at limit)
fifth_conversation = create_test_conversation
additional_assigned = service.perform_bulk_assignment(limit: 1)
expect(additional_assigned).to eq(0)
expect(fifth_conversation.reload.assignee).to be_nil
end
it 'tracks assignments using individual Redis keys' do
conversation = create_test_conversation
service.perform_bulk_assignment(limit: 1)
# Check that assignment key exists
pattern = "assignment:#{inbox.id}:agent:#{conversation.reload.assignee.id}:*"
count = Redis::Alfred.keys_count(pattern)
expect(count).to eq(1)
end
it 'allows new assignments after window expires' do
# Create 2 conversations and force assignment to agent1
conversations = Array.new(2) { create_test_conversation }
allow(service).to receive(:round_robin_selector).and_return(
instance_double(AutoAssignment::RoundRobinSelector, select_agent: agent1)
)
# Assign both to agent1
assigned_count = service.perform_bulk_assignment(limit: 2)
expect(assigned_count).to eq(2)
conversations.each do |c|
expect(c.reload.assignee).to eq(agent1)
end
# Agent1 is now at limit
rate_limiter = AutoAssignment::RateLimiter.new(inbox: inbox, agent: agent1)
expect(rate_limiter.within_limit?).to be false
# Clear Redis keys to simulate time window expiry
clean_redis_keys
# Agent1 should be available again
expect(rate_limiter.within_limit?).to be true
# New assignment should work
new_conversation = create_test_conversation
allow(service).to receive(:round_robin_selector).and_return(
instance_double(AutoAssignment::RoundRobinSelector, select_agent: agent1)
)
assigned_count = service.perform_bulk_assignment(limit: 1)
expect(assigned_count).to eq(1)
expect(new_conversation.reload.assignee).to eq(agent1)
end
end
describe 'without fair distribution' do
before do
inbox.update!(auto_assignment_config: {})
end
it 'assigns without limits' do
# Create more conversations than would be allowed with limits
conversations = Array.new(5) { create_test_conversation }
assigned_count = service.perform_bulk_assignment(limit: 5)
expect(assigned_count).to eq(5)
conversations.each do |conversation|
expect(conversation.reload.assignee).not_to be_nil
end
end
end
end
@@ -6,38 +6,63 @@ RSpec.describe AutoAssignment::AssignmentService do
let(:service) { described_class.new(inbox: inbox) }
let(:agent) { create(:user, account: account, role: :agent, availability: :online) }
let(:agent2) { create(:user, account: account, role: :agent, availability: :online) }
let(:conversation) { create(:conversation, inbox: inbox, assignee: nil) }
before do
create(:inbox_member, inbox: inbox, user: agent)
end
def create_test_conversation(attrs = {})
conversation = build(:conversation, attrs.reverse_merge(inbox: inbox, assignee: nil))
# Skip the after_save callback to test our service directly
allow(conversation).to receive(:run_auto_assignment).and_return(nil)
conversation.save!
conversation
end
describe 'assignment behavior' do
before do
allow(OnlineStatusTracker).to receive(:get_available_users).and_return({ agent.id.to_s => 'online' })
end
describe '#perform_bulk_assignment' do
context 'when auto assignment is enabled' do
it 'assigns conversation to available agent' do
conversation = create_test_conversation
before do
allow(OnlineStatusTracker).to receive(:get_available_users).and_return({ agent.id.to_s => 'online' })
end
it 'assigns conversations to available agents' do
assigned_count = service.perform_bulk_assignment(limit: 1)
expect(assigned_count).to eq(1)
expect(conversation.reload.assignee).to eq(agent)
end
it 'dispatches assignee changed event' do
conversation = create_test_conversation
allow(Rails.configuration.dispatcher).to receive(:dispatch).and_call_original
it 'returns 0 when no agents are online' do
allow(OnlineStatusTracker).to receive(:get_available_users).and_return({})
assigned_count = service.perform_bulk_assignment(limit: 1)
expect(assigned_count).to eq(0)
expect(conversation.reload.assignee).to be_nil
end
it 'respects the limit parameter' do
3.times { create(:conversation, inbox: inbox, assignee: nil) }
assigned_count = service.perform_bulk_assignment(limit: 2)
expect(assigned_count).to eq(2)
expect(inbox.conversations.unassigned.count).to eq(1)
end
it 'only assigns open conversations' do
resolved_conversation = create(:conversation, inbox: inbox, assignee: nil, status: 'resolved')
assigned_count = service.perform_bulk_assignment(limit: 10)
expect(conversation.reload.assignee).to eq(agent)
expect(resolved_conversation.reload.assignee).to be_nil
end
it 'does not reassign already assigned conversations' do
assigned_conversation = create(:conversation, inbox: inbox, assignee: agent)
unassigned_conversation = create(:conversation, inbox: inbox, assignee: nil)
assigned_count = service.perform_bulk_assignment(limit: 10)
expect(assigned_count).to eq(2) # Original conversation + unassigned_conversation
expect(assigned_conversation.reload.assignee).to eq(agent)
end
it 'dispatches assignee changed event' do
expect(Rails.configuration.dispatcher).to receive(:dispatch).with(
Events::Types::ASSIGNEE_CHANGED,
anything,
@@ -46,155 +71,143 @@ RSpec.describe AutoAssignment::AssignmentService do
service.perform_bulk_assignment(limit: 1)
end
it 'returns 0 when no agents available' do
create_test_conversation
allow(OnlineStatusTracker).to receive(:get_available_users).and_return({})
assigned_count = service.perform_bulk_assignment(limit: 1)
expect(assigned_count).to eq(0)
end
end
context 'when conversation already assigned' do
it 'does not reassign' do
conversation = create_test_conversation(assignee: agent)
context 'when auto assignment is disabled' do
before { inbox.update!(enable_auto_assignment: false) }
assigned_count = service.perform_bulk_assignment(limit: 1)
expect(assigned_count).to eq(0)
expect(conversation.reload.assignee).to eq(agent)
end
end
context 'when conversation is not open' do
it 'does not assign' do
conversation = create_test_conversation(status: 'resolved')
assigned_count = service.perform_bulk_assignment(limit: 1)
it 'returns 0 without processing' do
assigned_count = service.perform_bulk_assignment(limit: 10)
expect(assigned_count).to eq(0)
expect(conversation.reload.assignee).to be_nil
end
end
end
describe '#perform_bulk_assignment' do
before do
allow(OnlineStatusTracker).to receive(:get_available_users).and_return({ agent.id.to_s => 'online' })
3.times { create_test_conversation(status: :open) }
end
it 'assigns multiple conversations' do
assigned_count = service.perform_bulk_assignment(limit: 2)
expect(assigned_count).to eq(2)
end
it 'respects the limit parameter' do
assigned_count = service.perform_bulk_assignment(limit: 1)
expect(assigned_count).to eq(1)
expect(inbox.conversations.unassigned.count).to eq(2)
end
context 'when auto assignment disabled' do
before { inbox.update!(enable_auto_assignment: false) }
it 'returns 0' do
expect(service.perform_bulk_assignment).to eq(0)
end
end
end
describe 'with rate limiting' do
let(:rate_limiter) { instance_double(AutoAssignment::RateLimiter) }
before do
create(:inbox_member, inbox: inbox, user: agent2)
allow(OnlineStatusTracker).to receive(:get_available_users).and_return({
agent.id.to_s => 'online',
agent2.id.to_s => 'online'
})
allow(inbox).to receive(:auto_assignment_config).and_return({
'fair_distribution_limit' => 2,
'fair_distribution_window' => 3600
})
end
it 'filters agents based on rate limits' do
# Agent 1 has reached limit
rate_limiter_agent1 = instance_double(AutoAssignment::RateLimiter, within_limit?: false)
allow(AutoAssignment::RateLimiter).to receive(:new)
.with(inbox: inbox, agent: agent)
.and_return(rate_limiter_agent1)
# Agent 2 is within limit
rate_limiter_agent2 = instance_double(AutoAssignment::RateLimiter, within_limit?: true, track_assignment: true)
allow(AutoAssignment::RateLimiter).to receive(:new)
.with(inbox: inbox, agent: agent2)
.and_return(rate_limiter_agent2)
conversation = create_test_conversation
assigned_count = service.perform_bulk_assignment(limit: 1)
expect(assigned_count).to eq(1)
expect(conversation.reload.assignee).to eq(agent2)
end
it 'tracks assignments in Redis' do
conversation = create_test_conversation
rate_limiter = instance_double(AutoAssignment::RateLimiter, within_limit?: true)
allow(AutoAssignment::RateLimiter).to receive(:new).and_return(rate_limiter)
expect(rate_limiter).to receive(:track_assignment).with(conversation)
service.perform_bulk_assignment(limit: 1)
end
end
describe 'conversation priority' do
before do
allow(OnlineStatusTracker).to receive(:get_available_users).and_return({ agent.id.to_s => 'online' })
end
context 'with longest_waiting priority' do
let!(:old_conversation) do
create_test_conversation(status: :open, created_at: 2.hours.ago, last_activity_at: 2.hours.ago)
end
let!(:new_conversation) do
create_test_conversation(status: :open, created_at: 1.hour.ago, last_activity_at: 1.hour.ago)
end
context 'with conversation priority' do
before do
allow(inbox).to receive(:auto_assignment_config).and_return({
'conversation_priority' => 'longest_waiting'
})
allow(OnlineStatusTracker).to receive(:get_available_users).and_return({ agent.id.to_s => 'online' })
end
it 'assigns oldest conversation first' do
service.perform_bulk_assignment(limit: 1)
context 'when priority is longest_waiting' do
before do
allow(inbox).to receive(:auto_assignment_config).and_return({ 'conversation_priority' => 'longest_waiting' })
end
expect(old_conversation.reload.assignee).to eq(agent)
expect(new_conversation.reload.assignee).to be_nil
it 'assigns conversations with oldest last_activity_at first' do
old_conversation = create(:conversation,
inbox: inbox,
assignee: nil,
created_at: 2.hours.ago,
last_activity_at: 2.hours.ago)
new_conversation = create(:conversation,
inbox: inbox,
assignee: nil,
created_at: 1.hour.ago,
last_activity_at: 1.hour.ago)
service.perform_bulk_assignment(limit: 1)
expect(old_conversation.reload.assignee).to eq(agent)
expect(new_conversation.reload.assignee).to be_nil
end
end
context 'when priority is default' do
it 'assigns conversations by created_at' do
old_conversation = create(:conversation, inbox: inbox, assignee: nil, created_at: 2.hours.ago)
new_conversation = create(:conversation, inbox: inbox, assignee: nil, created_at: 1.hour.ago)
service.perform_bulk_assignment(limit: 1)
expect(old_conversation.reload.assignee).to eq(agent)
expect(new_conversation.reload.assignee).to be_nil
end
end
end
context 'with default priority' do
let!(:first_created) do
create_test_conversation(status: :open, created_at: 2.hours.ago)
end
let!(:second_created) do
create_test_conversation(status: :open, created_at: 1.hour.ago)
context 'with fair distribution' do
before do
create(:inbox_member, inbox: inbox, user: agent2)
allow(OnlineStatusTracker).to receive(:get_available_users).and_return({
agent.id.to_s => 'online',
agent2.id.to_s => 'online'
})
end
it 'assigns by creation time' do
service.perform_bulk_assignment(limit: 1)
context 'when fair distribution is enabled' do
before do
allow(inbox).to receive(:auto_assignment_config).and_return({
'fair_distribution_limit' => 2,
'fair_distribution_window' => 3600
})
end
expect(first_created.reload.assignee).to eq(agent)
expect(second_created.reload.assignee).to be_nil
it 'respects the assignment limit per agent' do
# Mock agent1 at limit
allow_any_instance_of(AutoAssignment::RateLimiter).to receive(:within_limit?) do |limiter|
limiter.agent == agent ? false : true
end
unassigned_conversation = create(:conversation, inbox: inbox, assignee: nil)
service.perform_bulk_assignment(limit: 1)
expect(unassigned_conversation.reload.assignee).to eq(agent2)
end
it 'tracks assignments in Redis' do
rate_limiter = instance_double(AutoAssignment::RateLimiter, within_limit?: true)
allow(AutoAssignment::RateLimiter).to receive(:new).and_return(rate_limiter)
expect(rate_limiter).to receive(:track_assignment).with(conversation)
service.perform_bulk_assignment(limit: 1)
end
it 'allows assignments after window expires' do
# Simulate time passing for rate limit window
Timecop.freeze(Time.current) do
2.times do
convo = create(:conversation, inbox: inbox, assignee: nil)
service.perform_bulk_assignment(limit: 1)
expect(convo.reload.assignee).not_to be_nil
end
end
# Move forward past the window
Timecop.freeze(Time.current + 2.hours) do
new_convo = create(:conversation, inbox: inbox, assignee: nil)
service.perform_bulk_assignment(limit: 1)
expect(new_convo.reload.assignee).not_to be_nil
end
end
end
context 'when fair distribution is disabled' do
it 'assigns without rate limiting' do
5.times { create(:conversation, inbox: inbox, assignee: nil) }
expect(AutoAssignment::RateLimiter).not_to receive(:new)
assigned_count = service.perform_bulk_assignment(limit: 5)
expect(assigned_count).to eq(5)
end
end
context 'with round robin assignment' do
it 'distributes conversations evenly among agents' do
conversations = 4.times.map { create(:conversation, inbox: inbox, assignee: nil) }
service.perform_bulk_assignment(limit: 4)
agent1_count = conversations.count { |c| c.reload.assignee == agent }
agent2_count = conversations.count { |c| c.reload.assignee == agent2 }
# Should be distributed evenly (2 each) or close to even (3 and 1)
expect([agent1_count, agent2_count].sort).to eq([2, 2]).or(eq([1, 3]))
end
end
end
end
end
end
@@ -0,0 +1,55 @@
require 'rails_helper'
RSpec.describe AutoAssignment::RoundRobinSelector do
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
let(:selector) { described_class.new(inbox: inbox) }
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: :online) }
before do
@member1 = create(:inbox_member, inbox: inbox, user: agent1)
@member2 = create(:inbox_member, inbox: inbox, user: agent2)
@member3 = create(:inbox_member, inbox: inbox, user: agent3)
end
describe '#select_agent' do
context 'when agents are available' do
let(:available_agents) { [@member1, @member2, @member3] }
it 'returns an agent from the available list' do
selected_agent = selector.select_agent(available_agents)
expect(selected_agent).not_to be_nil
expect([agent1, agent2, agent3]).to include(selected_agent)
end
it 'uses round robin service for selection' do
round_robin_service = instance_double(AutoAssignment::InboxRoundRobinService)
allow(AutoAssignment::InboxRoundRobinService).to receive(:new).and_return(round_robin_service)
expect(round_robin_service).to receive(:available_agent).with(
allowed_agent_ids: [agent1.id.to_s, agent2.id.to_s, agent3.id.to_s]
).and_return(agent1)
selected_agent = selector.select_agent(available_agents)
expect(selected_agent).to eq(agent1)
end
end
context 'when no agents are available' do
it 'returns nil' do
selected_agent = selector.select_agent([])
expect(selected_agent).to be_nil
end
end
context 'when one agent is available' do
it 'returns that agent' do
selected_agent = selector.select_agent([@member1])
expect(selected_agent).to eq(agent1)
end
end
end
end