fix rubcop validations
This commit is contained in:
@@ -1,235 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AssignmentV2::ContinuousAssignmentJob, type: :job do
|
||||
let(:account) { create(:account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:assignment_policy) { create(:assignment_policy, account: account) }
|
||||
|
||||
before do
|
||||
create(:inbox_assignment_policy, inbox: inbox, assignment_policy: assignment_policy)
|
||||
allow(inbox).to receive(:assignment_v2_enabled?).and_return(true)
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'when inbox exists and assignment should run' do
|
||||
before do
|
||||
create(:conversation, inbox: inbox, assignee: nil, status: :open)
|
||||
create(:conversation, inbox: inbox, assignee: nil, status: :open)
|
||||
end
|
||||
|
||||
it 'runs assignment orchestrator' do
|
||||
orchestrator_double = instance_double(AssignmentV2::AssignmentOrchestrator)
|
||||
expect(AssignmentV2::AssignmentOrchestrator).to receive(:new).with(inbox).and_return(orchestrator_double)
|
||||
expect(orchestrator_double).to receive(:assign_conversations).with(limit: 50).and_return(2)
|
||||
|
||||
described_class.new.perform(inbox.id)
|
||||
end
|
||||
|
||||
it 'logs assignment start and completion' do
|
||||
orchestrator = instance_double(AssignmentV2::AssignmentOrchestrator)
|
||||
allow(AssignmentV2::AssignmentOrchestrator).to receive(:new).with(inbox).and_return(orchestrator)
|
||||
allow(orchestrator).to receive(:assign_conversations).and_return(2)
|
||||
|
||||
expect(Rails.logger).to receive(:info).with("Assignment V2: Running continuous assignment for inbox #{inbox.id}")
|
||||
expect(Rails.logger).to receive(:info).with("Assignment V2: Completed continuous assignment for inbox #{inbox.id}, made 2 assignments")
|
||||
|
||||
described_class.new.perform(inbox.id)
|
||||
end
|
||||
|
||||
context 'when more conversations need processing' do
|
||||
it 'schedules next run when batch size equals assignments made' do
|
||||
orchestrator = instance_double(AssignmentV2::AssignmentOrchestrator)
|
||||
allow(AssignmentV2::AssignmentOrchestrator).to receive(:new).with(inbox).and_return(orchestrator)
|
||||
allow(orchestrator).to receive(:assign_conversations).and_return(50)
|
||||
allow(inbox.conversations.unassigned.open).to receive(:exists?).and_return(true)
|
||||
|
||||
expect(described_class).to receive(:set).with(wait: anything).and_return(described_class)
|
||||
expect(described_class).to receive(:perform_later).with(inbox.id)
|
||||
|
||||
described_class.new.perform(inbox.id, batch_size: 50)
|
||||
end
|
||||
|
||||
it 'does not schedule next run when fewer assignments made than batch size' do
|
||||
orchestrator = instance_double(AssignmentV2::AssignmentOrchestrator)
|
||||
allow(AssignmentV2::AssignmentOrchestrator).to receive(:new).with(inbox).and_return(orchestrator)
|
||||
allow(orchestrator).to receive(:assign_conversations).and_return(25)
|
||||
|
||||
expect(described_class).not_to receive(:set)
|
||||
|
||||
described_class.new.perform(inbox.id, batch_size: 50)
|
||||
end
|
||||
|
||||
it 'does not schedule next run when no more unassigned conversations' do
|
||||
orchestrator = instance_double(AssignmentV2::AssignmentOrchestrator)
|
||||
allow(AssignmentV2::AssignmentOrchestrator).to receive(:new).with(inbox).and_return(orchestrator)
|
||||
allow(orchestrator).to receive(:assign_conversations).and_return(50)
|
||||
allow(inbox.conversations.unassigned.open).to receive(:exists?).and_return(false)
|
||||
|
||||
expect(described_class).not_to receive(:set)
|
||||
|
||||
described_class.new.perform(inbox.id, batch_size: 50)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when inbox does not exist' do
|
||||
it 'logs error and does not raise exception' do
|
||||
expect(Rails.logger).to receive(:error).with('Assignment V2: Inbox 999 not found')
|
||||
|
||||
expect { described_class.new.perform(999) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when assignment should not run' do
|
||||
before do
|
||||
allow(inbox).to receive(:assignment_v2_enabled?).and_return(false)
|
||||
end
|
||||
|
||||
it 'returns early without running assignment' do
|
||||
expect(AssignmentV2::AssignmentOrchestrator).not_to receive(:new)
|
||||
|
||||
described_class.new.perform(inbox.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when assignment policy is disabled' do
|
||||
before do
|
||||
assignment_policy.update!(enabled: false)
|
||||
end
|
||||
|
||||
it 'returns early without running assignment' do
|
||||
expect(AssignmentV2::AssignmentOrchestrator).not_to receive(:new)
|
||||
|
||||
described_class.new.perform(inbox.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no unassigned conversations exist' do
|
||||
it 'returns early without running assignment' do
|
||||
expect(AssignmentV2::AssignmentOrchestrator).not_to receive(:new)
|
||||
|
||||
described_class.new.perform(inbox.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when assignment fails with error' do
|
||||
before do
|
||||
create(:conversation, inbox: inbox, assignee: nil, status: :open)
|
||||
orchestrator = instance_double(AssignmentV2::AssignmentOrchestrator)
|
||||
allow(AssignmentV2::AssignmentOrchestrator).to receive(:new).with(inbox).and_return(orchestrator)
|
||||
allow(orchestrator).to receive(:assign_conversations).and_raise(StandardError, 'Assignment failed')
|
||||
end
|
||||
|
||||
it 'logs error and re-raises exception' do
|
||||
expect(Rails.logger).to receive(:error).with("Assignment V2: Continuous assignment failed for inbox #{inbox.id}: Assignment failed")
|
||||
|
||||
expect { described_class.new.perform(inbox.id) }.to raise_error(StandardError, 'Assignment failed')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.trigger' do
|
||||
let(:conversation) { create(:conversation, inbox: inbox) }
|
||||
|
||||
context 'when inbox has assignment v2 enabled' do
|
||||
before do
|
||||
allow(inbox).to receive(:assignment_v2_enabled?).and_return(true)
|
||||
end
|
||||
|
||||
it 'enqueues job for conversation inbox' do
|
||||
expect(described_class).to receive(:perform_later).with(inbox.id, batch_size: 10)
|
||||
|
||||
described_class.trigger(conversation)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when inbox does not have assignment v2 enabled' do
|
||||
before do
|
||||
allow(inbox).to receive(:assignment_v2_enabled?).and_return(false)
|
||||
end
|
||||
|
||||
it 'does not enqueue job' do
|
||||
expect(described_class).not_to receive(:perform_later)
|
||||
|
||||
described_class.trigger(conversation)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.process_all_inboxes' do
|
||||
let(:account2) { create(:account) }
|
||||
let(:inbox2) { create(:inbox, account: account2) }
|
||||
let(:policy2) { create(:assignment_policy, account: account2) }
|
||||
|
||||
before do
|
||||
create(:inbox_assignment_policy, inbox: inbox2, assignment_policy: policy2)
|
||||
allow(inbox).to receive(:assignment_v2_enabled?).and_return(true)
|
||||
allow(inbox2).to receive(:assignment_v2_enabled?).and_return(true)
|
||||
|
||||
create(:conversation, inbox: inbox, assignee: nil, status: :open)
|
||||
create(:conversation, inbox: inbox2, assignee: nil, status: :open)
|
||||
end
|
||||
|
||||
it 'processes all inboxes with enabled assignment policies' do
|
||||
expect(Rails.logger).to receive(:info).with(/Scheduling bulk assignment for inbox #{inbox.id}/)
|
||||
expect(Rails.logger).to receive(:info).with(/Scheduling bulk assignment for inbox #{inbox2.id}/)
|
||||
|
||||
expect(described_class).to receive(:perform_later).with(inbox.id)
|
||||
expect(described_class).to receive(:perform_later).with(inbox2.id)
|
||||
|
||||
described_class.process_all_inboxes
|
||||
end
|
||||
|
||||
it 'skips inboxes without unassigned conversations' do
|
||||
# Remove unassigned conversations
|
||||
Conversation.find_each { |c| c.update!(status: :resolved) }
|
||||
|
||||
expect(described_class).not_to receive(:perform_later)
|
||||
|
||||
described_class.process_all_inboxes
|
||||
end
|
||||
|
||||
it 'skips inboxes without assignment v2 enabled' do
|
||||
allow(inbox).to receive(:assignment_v2_enabled?).and_return(false)
|
||||
|
||||
expect(described_class).to receive(:perform_later).with(inbox2.id)
|
||||
expect(described_class).not_to receive(:perform_later).with(inbox.id)
|
||||
|
||||
described_class.process_all_inboxes
|
||||
end
|
||||
end
|
||||
|
||||
describe 'delay calculation' do
|
||||
let(:job_instance) { described_class.new }
|
||||
|
||||
it 'calculates delay with base time and jitter' do
|
||||
delay = job_instance.send(:calculate_delay, inbox)
|
||||
|
||||
expect(delay).to be >= 30.seconds
|
||||
expect(delay).to be <= 40.seconds
|
||||
end
|
||||
|
||||
it 'adds randomization to prevent thundering herd' do
|
||||
delays = []
|
||||
10.times do
|
||||
delays << job_instance.send(:calculate_delay, inbox)
|
||||
end
|
||||
|
||||
# All delays should be different due to randomization
|
||||
expect(delays.uniq.size).to be > 1
|
||||
end
|
||||
end
|
||||
|
||||
describe 'retry configuration' do
|
||||
it 'has proper retry configuration' do
|
||||
expect(described_class.get_sidekiq_options['retry']).to eq(3)
|
||||
end
|
||||
|
||||
it 'uses exponentially longer wait times' do
|
||||
# This tests the retry_on configuration
|
||||
expect(described_class.instance_variable_get(:@retry_callbacks)).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,369 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AssignmentV2::AssignmentOrchestrator, type: :integration do
|
||||
let(:account) { create(:account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
|
||||
# Create agents with different availability
|
||||
let!(:agent1) { create(:user, account: account, name: 'Agent 1', role: :agent, availability: :online) }
|
||||
let!(:agent2) { create(:user, account: account, name: 'Agent 2', role: :agent, availability: :online) }
|
||||
let!(:agent3) { create(:user, account: account, name: 'Agent 3', role: :agent, availability: :busy) }
|
||||
let!(:agent4) { create(:user, account: account, name: 'Agent 4', role: :agent, availability: :offline) }
|
||||
|
||||
before do
|
||||
# Make agents members of inbox
|
||||
[agent1, agent2, agent3, agent4].each do |agent|
|
||||
create(:inbox_member, inbox: inbox, user: agent)
|
||||
end
|
||||
|
||||
# Clear Redis to ensure clean state
|
||||
Redis::Alfred.flushdb
|
||||
end
|
||||
|
||||
describe 'Round Robin Assignment' do
|
||||
let(:assignment_policy) do
|
||||
create(:assignment_policy,
|
||||
account: account,
|
||||
name: 'Round Robin Policy',
|
||||
assignment_order: :round_robin,
|
||||
conversation_priority: :earliest_created,
|
||||
enabled: true)
|
||||
end
|
||||
|
||||
let(:inbox_assignment_policy) do
|
||||
create(:inbox_assignment_policy, inbox: inbox, assignment_policy: assignment_policy)
|
||||
end
|
||||
|
||||
it 'assigns conversations in round-robin fashion to online agents only' do
|
||||
# Create unassigned conversations
|
||||
conversations = create_list(:conversation, 6, inbox: inbox, assignee: nil, status: :open)
|
||||
|
||||
# Process assignments
|
||||
service = AssignmentV2::AssignmentService.new(inbox)
|
||||
assigned_count = service.assign_conversations
|
||||
|
||||
expect(assigned_count).to eq(6)
|
||||
|
||||
# Verify all conversations are assigned
|
||||
conversations.each(&:reload)
|
||||
expect(conversations.filter_map(&:assignee).count).to eq(6)
|
||||
|
||||
# Verify only online agents received assignments
|
||||
assigned_agents = conversations.map(&:assignee).uniq
|
||||
expect(assigned_agents).to contain_exactly(agent1, agent2)
|
||||
|
||||
# Verify round-robin distribution
|
||||
agent1_count = conversations.count { |c| c.assignee == agent1 }
|
||||
agent2_count = conversations.count { |c| c.assignee == agent2 }
|
||||
expect([agent1_count, agent2_count]).to contain_exactly(3, 3)
|
||||
end
|
||||
|
||||
it 'respects conversation priority order' do
|
||||
# Create conversations with different creation times
|
||||
old_conv = create(:conversation, inbox: inbox, assignee: nil, created_at: 2.hours.ago)
|
||||
mid_conv = create(:conversation, inbox: inbox, assignee: nil, created_at: 1.hour.ago)
|
||||
new_conv = create(:conversation, inbox: inbox, assignee: nil, created_at: 5.minutes.ago)
|
||||
|
||||
# Assign only 2 conversations
|
||||
service = AssignmentV2::AssignmentService.new(inbox)
|
||||
service.assign_conversations(limit: 2)
|
||||
|
||||
# Oldest conversations should be assigned first
|
||||
expect(old_conv.reload.assignee).not_to be_nil
|
||||
expect(mid_conv.reload.assignee).not_to be_nil
|
||||
expect(new_conv.reload.assignee).to be_nil
|
||||
end
|
||||
|
||||
it 'handles agent availability changes mid-assignment' do
|
||||
conversations = create_list(:conversation, 4, inbox: inbox, assignee: nil)
|
||||
|
||||
# Assign first batch
|
||||
service = AssignmentV2::AssignmentService.new(inbox)
|
||||
service.assign_conversations(limit: 2)
|
||||
|
||||
# Make agent1 offline
|
||||
agent1.update!(availability: :offline)
|
||||
|
||||
# Assign remaining conversations
|
||||
service.assign_conversations(limit: 2)
|
||||
|
||||
# All remaining should go to agent2
|
||||
remaining_assignments = conversations.reload.last(2).map(&:assignee)
|
||||
expect(remaining_assignments).to all(eq(agent2))
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Balanced Assignment' do
|
||||
let(:assignment_policy) do
|
||||
create(:assignment_policy,
|
||||
account: account,
|
||||
name: 'Balanced Policy',
|
||||
assignment_order: :balanced,
|
||||
enabled: true)
|
||||
end
|
||||
|
||||
let(:inbox_assignment_policy) do
|
||||
create(:inbox_assignment_policy, inbox: inbox, assignment_policy: assignment_policy)
|
||||
end
|
||||
|
||||
before do
|
||||
# Mock enterprise features
|
||||
allow(account).to receive(:feature_enabled?).with(:enterprise_agent_capacity).and_return(true)
|
||||
end
|
||||
|
||||
it 'assigns to agent with least conversations' do
|
||||
# Create existing load imbalance
|
||||
create_list(:conversation, 5, inbox: inbox, assignee: agent1, status: :open)
|
||||
create_list(:conversation, 2, inbox: inbox, assignee: agent2, status: :open)
|
||||
|
||||
# Create new conversations
|
||||
new_conversations = create_list(:conversation, 3, inbox: inbox, assignee: nil)
|
||||
|
||||
# Process assignments
|
||||
service = AssignmentV2::AssignmentService.new(inbox)
|
||||
service.assign_conversations
|
||||
|
||||
# All should go to agent2 (less loaded)
|
||||
new_conversations.each(&:reload)
|
||||
expect(new_conversations.map(&:assignee)).to all(eq(agent2))
|
||||
|
||||
# Final count should be more balanced
|
||||
expect(agent1.assigned_conversations.open.where(inbox: inbox).count).to eq(5)
|
||||
expect(agent2.assigned_conversations.open.where(inbox: inbox).count).to eq(5)
|
||||
end
|
||||
|
||||
it 'only counts open conversations for balancing' do
|
||||
# Agent1 has many resolved conversations (shouldn't count)
|
||||
create_list(:conversation, 10, inbox: inbox, assignee: agent1, status: :resolved)
|
||||
# Agent1 has 1 open conversation
|
||||
create(:conversation, inbox: inbox, assignee: agent1, status: :open)
|
||||
|
||||
# Agent2 has 3 open conversations
|
||||
create_list(:conversation, 3, inbox: inbox, assignee: agent2, status: :open)
|
||||
|
||||
# New conversation should go to agent1
|
||||
new_conversation = create(:conversation, inbox: inbox, assignee: nil)
|
||||
|
||||
service = AssignmentV2::AssignmentService.new(inbox)
|
||||
service.assign_conversation(new_conversation)
|
||||
|
||||
expect(new_conversation.reload.assignee).to eq(agent1)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Enterprise Capacity Management' do
|
||||
let(:assignment_policy) do
|
||||
create(:assignment_policy,
|
||||
account: account,
|
||||
assignment_order: :balanced,
|
||||
enabled: true)
|
||||
end
|
||||
|
||||
let(:inbox_assignment_policy) do
|
||||
create(:inbox_assignment_policy, inbox: inbox, assignment_policy: assignment_policy)
|
||||
end
|
||||
|
||||
let(:capacity_policy) do
|
||||
create(:enterprise_agent_capacity_policy, account: account, name: 'Limited Capacity')
|
||||
end
|
||||
|
||||
before do
|
||||
# Mock enterprise features
|
||||
stub_const('Enterprise', Module.new)
|
||||
stub_const('Enterprise::AgentCapacityPolicy', Class.new(ApplicationRecord))
|
||||
stub_const('Enterprise::InboxCapacityLimit', Class.new(ApplicationRecord))
|
||||
|
||||
allow(account).to receive(:feature_enabled?).with(:enterprise_agent_capacity).and_return(true)
|
||||
|
||||
# Set up capacity limits
|
||||
agent1.account_users.first.update!(agent_capacity_policy: capacity_policy)
|
||||
agent2.account_users.first.update!(agent_capacity_policy: capacity_policy)
|
||||
|
||||
create(:enterprise_inbox_capacity_limit,
|
||||
agent_capacity_policy: capacity_policy,
|
||||
inbox: inbox,
|
||||
conversation_limit: 3)
|
||||
end
|
||||
|
||||
it 'respects agent capacity limits' do
|
||||
# Fill agent1 to capacity
|
||||
create_list(:conversation, 3, inbox: inbox, assignee: agent1, status: :open)
|
||||
|
||||
# Create new conversations
|
||||
new_conversations = create_list(:conversation, 4, inbox: inbox, assignee: nil)
|
||||
|
||||
# Mock capacity manager
|
||||
capacity_manager = instance_double(Enterprise::AssignmentV2::CapacityManager)
|
||||
allow(Enterprise::AssignmentV2::CapacityManager).to receive(:new).and_return(capacity_manager)
|
||||
|
||||
# Agent1 at capacity, agent2 has room
|
||||
allow(capacity_manager).to receive(:get_agent_capacity).with(agent1, inbox).and_return(
|
||||
{ available_capacity: 0, current_assignments: 3, total_capacity: 3 }
|
||||
)
|
||||
allow(capacity_manager).to receive(:get_agent_capacity).with(agent2, inbox).and_return(
|
||||
{ available_capacity: 3, current_assignments: 0, total_capacity: 3 }
|
||||
)
|
||||
|
||||
# Process assignments
|
||||
service = AssignmentV2::AssignmentService.new(inbox)
|
||||
assigned_count = service.assign_conversations
|
||||
|
||||
# Only 3 should be assigned (agent2's capacity)
|
||||
expect(assigned_count).to eq(3)
|
||||
|
||||
# All should go to agent2
|
||||
assigned_conversations = new_conversations.select { |c| c.reload.assignee.present? }
|
||||
expect(assigned_conversations.map(&:assignee)).to all(eq(agent2))
|
||||
end
|
||||
|
||||
it 'handles capacity policy with exclusion rules' do
|
||||
# Update capacity policy with exclusion rules
|
||||
capacity_policy.update!(
|
||||
exclusion_rules: {
|
||||
'labels' => ['urgent'],
|
||||
'hours_threshold' => 24
|
||||
}
|
||||
)
|
||||
|
||||
# Create urgent label
|
||||
urgent_label = create(:label, account: account, title: 'urgent')
|
||||
|
||||
# Create mixed conversations for agent1
|
||||
create(:conversation, inbox: inbox, assignee: agent1, status: :open)
|
||||
urgent_conv = create(:conversation, inbox: inbox, assignee: agent1, status: :open)
|
||||
create(:conversation_label, conversation: urgent_conv, label: urgent_label)
|
||||
create(:conversation, inbox: inbox, assignee: agent1, status: :open, created_at: 2.days.ago)
|
||||
|
||||
# Mock capacity calculation with exclusions
|
||||
capacity_manager = instance_double(Enterprise::AssignmentV2::CapacityManager)
|
||||
allow(Enterprise::AssignmentV2::CapacityManager).to receive(:new).and_return(capacity_manager)
|
||||
|
||||
# Only regular conversation counts toward capacity
|
||||
allow(capacity_manager).to receive(:get_agent_capacity).with(agent1, inbox).and_return(
|
||||
{ available_capacity: 2, current_assignments: 1, total_capacity: 3 }
|
||||
)
|
||||
|
||||
# New conversation should still be assignable
|
||||
new_conversation = create(:conversation, inbox: inbox, assignee: nil)
|
||||
|
||||
service = AssignmentV2::AssignmentService.new(inbox)
|
||||
expect(service.assign_conversation(new_conversation)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Team-based Assignment' do
|
||||
let(:team) { create(:team, account: account) }
|
||||
let(:assignment_policy) { create(:assignment_policy, account: account, enabled: true) }
|
||||
|
||||
before do
|
||||
create(:inbox_assignment_policy, inbox: inbox, assignment_policy: assignment_policy)
|
||||
create(:team_member, team: team, user: agent1)
|
||||
create(:team_member, team: team, user: agent2)
|
||||
end
|
||||
|
||||
it 'assigns only to team members when conversation has team' do
|
||||
# Create conversation with team
|
||||
conversation = create(:conversation, inbox: inbox, assignee: nil, team: team)
|
||||
|
||||
# Mock team filtering in service
|
||||
service = AssignmentV2::AssignmentService.new(inbox)
|
||||
|
||||
# Should only consider team members
|
||||
100.times do
|
||||
conversation.update!(assignee: nil)
|
||||
service.assign_conversation(conversation)
|
||||
expect(conversation.reload.assignee).to be_in([agent1, agent2])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Feature Flag Control' do
|
||||
let(:assignment_policy) { create(:assignment_policy, account: account, enabled: true) }
|
||||
|
||||
before do
|
||||
create(:inbox_assignment_policy, inbox: inbox, assignment_policy: assignment_policy)
|
||||
allow(inbox).to receive(:assignment_v2_enabled?).and_return(false)
|
||||
end
|
||||
|
||||
it 'falls back to legacy assignment when V2 is disabled' do
|
||||
conversation = create(:conversation, inbox: inbox, assignee: nil)
|
||||
|
||||
# Enable auto assignment
|
||||
inbox.update!(enable_auto_assignment: true)
|
||||
|
||||
# Should use legacy service
|
||||
expect(AutoAssignment::AgentAssignmentService).to receive(:new).and_call_original
|
||||
|
||||
# Trigger assignment through model callback
|
||||
conversation.update!(status: :open)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Concurrent Assignment Handling' do
|
||||
let(:assignment_policy) { create(:assignment_policy, account: account, enabled: true) }
|
||||
|
||||
before { create(:inbox_assignment_policy, inbox: inbox, assignment_policy: assignment_policy) }
|
||||
|
||||
it 'handles multiple simultaneous assignment jobs' do
|
||||
conversations = create_list(:conversation, 10, inbox: inbox, assignee: nil)
|
||||
|
||||
# Simulate concurrent job execution
|
||||
threads = []
|
||||
|
||||
3.times do
|
||||
threads << Thread.new do
|
||||
AssignmentV2::AssignmentJob.new.perform(inbox_id: inbox.id)
|
||||
end
|
||||
end
|
||||
|
||||
threads.each(&:join)
|
||||
|
||||
# All conversations should be assigned without duplicates
|
||||
conversations.each(&:reload)
|
||||
assigned_count = conversations.count { |c| c.assignee.present? }
|
||||
|
||||
expect(assigned_count).to eq(10)
|
||||
|
||||
# No conversation should have been assigned multiple times
|
||||
assignment_counts = conversations.group_by(&:assignee).transform_values(&:count)
|
||||
expect(assignment_counts.values.sum).to eq(10)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Error Recovery' do
|
||||
let(:assignment_policy) { create(:assignment_policy, account: account, enabled: true) }
|
||||
|
||||
before { create(:inbox_assignment_policy, inbox: inbox, assignment_policy: assignment_policy) }
|
||||
|
||||
it 'continues assignment after individual conversation failure' do
|
||||
conversations = create_list(:conversation, 5, inbox: inbox, assignee: nil)
|
||||
|
||||
# Make one conversation invalid
|
||||
conversations[2].update!(status: 'resolved')
|
||||
|
||||
service = AssignmentV2::AssignmentService.new(inbox)
|
||||
assigned_count = service.assign_conversations
|
||||
|
||||
# Should assign 4 out of 5
|
||||
expect(assigned_count).to eq(4)
|
||||
|
||||
# Invalid conversation remains unassigned
|
||||
expect(conversations[2].reload.assignee).to be_nil
|
||||
end
|
||||
|
||||
it 'recovers from Redis failures' do
|
||||
# Simulate Redis connection failure
|
||||
allow(Redis::Alfred).to receive(:lpop).and_raise(Redis::CannotConnectError)
|
||||
|
||||
conversation = create(:conversation, inbox: inbox, assignee: nil)
|
||||
|
||||
service = AssignmentV2::AssignmentService.new(inbox)
|
||||
|
||||
# Should fall back to database-based assignment
|
||||
expect(service.assign_conversation(conversation)).to be true
|
||||
expect(conversation.reload.assignee).not_to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,262 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Enterprise::AssignmentV2::CapacityManager do
|
||||
let(:manager) { described_class.new }
|
||||
let(:account) { create(:account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:capacity_policy) { create(:enterprise_agent_capacity_policy, account: account) }
|
||||
let!(:account_user) { create(:account_user, account: account, user: agent, agent_capacity_policy: capacity_policy) }
|
||||
|
||||
before do
|
||||
# Clear any existing cache
|
||||
Rails.cache.clear
|
||||
end
|
||||
|
||||
describe '#get_agent_capacity' do
|
||||
context 'with capacity policy and limits' do
|
||||
before { create(:enterprise_inbox_capacity_limit, agent_capacity_policy: capacity_policy, inbox: inbox, conversation_limit: 10) }
|
||||
|
||||
it 'returns correct capacity data' do
|
||||
capacity = manager.get_agent_capacity(agent, inbox)
|
||||
|
||||
expect(capacity).to include(
|
||||
total_capacity: 10,
|
||||
current_assignments: 0,
|
||||
available_capacity: 10,
|
||||
policy_id: capacity_policy.id,
|
||||
has_policy: true
|
||||
)
|
||||
end
|
||||
|
||||
it 'counts only open conversations' do
|
||||
# Create conversations with different statuses
|
||||
create_list(:conversation, 3, inbox: inbox, assignee: agent, status: :open)
|
||||
create_list(:conversation, 2, inbox: inbox, assignee: agent, status: :resolved)
|
||||
create(:conversation, inbox: inbox, assignee: agent, status: :snoozed)
|
||||
|
||||
capacity = manager.get_agent_capacity(agent, inbox)
|
||||
|
||||
expect(capacity[:current_assignments]).to eq(3)
|
||||
expect(capacity[:available_capacity]).to eq(7)
|
||||
end
|
||||
|
||||
it 'caches capacity data' do
|
||||
# First call
|
||||
manager.get_agent_capacity(agent, inbox)
|
||||
|
||||
# Second call should use cache
|
||||
expect(Rails.cache).to receive(:fetch).and_call_original
|
||||
manager.get_agent_capacity(agent, inbox)
|
||||
end
|
||||
|
||||
it 'respects cache TTL' do
|
||||
cache_key = "assignment_v2:capacity:#{agent.accounts.first.id}:#{agent.id}:#{inbox.id}"
|
||||
|
||||
manager.get_agent_capacity(agent, inbox)
|
||||
|
||||
# Check cache exists with TTL
|
||||
expect(Rails.cache.exist?(cache_key)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'without capacity policy' do
|
||||
before { account_user.update!(agent_capacity_policy: nil) }
|
||||
|
||||
it 'returns unlimited capacity' do
|
||||
capacity = manager.get_agent_capacity(agent, inbox)
|
||||
|
||||
expect(capacity).to include(
|
||||
total_capacity: 999_999,
|
||||
current_assignments: 0,
|
||||
available_capacity: 999_999,
|
||||
policy_id: nil,
|
||||
has_policy: false
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without inbox limit' do
|
||||
it 'returns unlimited capacity' do
|
||||
capacity = manager.get_agent_capacity(agent, inbox)
|
||||
|
||||
expect(capacity[:has_policy]).to be false
|
||||
expect(capacity[:available_capacity]).to eq(999_999)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with exclusion rules' do
|
||||
let(:excluded_label) { create(:label, account: account, title: 'vip') }
|
||||
let(:capacity_policy) do
|
||||
create(:enterprise_agent_capacity_policy,
|
||||
account: account,
|
||||
exclusion_rules: {
|
||||
'labels' => ['vip'],
|
||||
'hours_threshold' => 24
|
||||
})
|
||||
end
|
||||
|
||||
before { create(:enterprise_inbox_capacity_limit, agent_capacity_policy: capacity_policy, inbox: inbox, conversation_limit: 10) }
|
||||
|
||||
it 'excludes conversations with specified labels' do
|
||||
# Create regular conversations
|
||||
create_list(:conversation, 3, inbox: inbox, assignee: agent, status: :open)
|
||||
|
||||
# Create VIP conversations (should be excluded)
|
||||
vip_conversations = create_list(:conversation, 2, inbox: inbox, assignee: agent, status: :open)
|
||||
vip_conversations.each { |conv| create(:conversation_label, conversation: conv, label: excluded_label) }
|
||||
|
||||
capacity = manager.get_agent_capacity(agent, inbox)
|
||||
|
||||
expect(capacity[:current_assignments]).to eq(3) # Only non-VIP conversations
|
||||
end
|
||||
|
||||
it 'excludes old conversations based on hours threshold' do
|
||||
# Create recent conversations
|
||||
create_list(:conversation, 2, inbox: inbox, assignee: agent, status: :open, created_at: 1.hour.ago)
|
||||
|
||||
# Create old conversations (should be excluded)
|
||||
create_list(:conversation, 3, inbox: inbox, assignee: agent, status: :open, created_at: 2.days.ago)
|
||||
|
||||
capacity = manager.get_agent_capacity(agent, inbox)
|
||||
|
||||
expect(capacity[:current_assignments]).to eq(2) # Only recent conversations
|
||||
end
|
||||
end
|
||||
|
||||
context 'when error occurs' do
|
||||
it 'returns unlimited capacity on error' do
|
||||
allow(Rails.cache).to receive(:fetch).and_raise(StandardError, 'Cache error')
|
||||
expect(Rails.logger).to receive(:error).with(/Capacity manager failed/)
|
||||
|
||||
capacity = manager.get_agent_capacity(agent, inbox)
|
||||
|
||||
expect(capacity[:has_policy]).to be false
|
||||
expect(capacity[:available_capacity]).to eq(999_999)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get_agents_capacity_status' do
|
||||
let(:agent2) { create(:user, account: account, role: :agent) }
|
||||
|
||||
before { create(:account_user, account: account, user: agent2) }
|
||||
|
||||
it 'returns capacity status for multiple agents' do
|
||||
agents = [agent, agent2]
|
||||
|
||||
statuses = manager.get_agents_capacity_status(agents, inbox)
|
||||
|
||||
expect(statuses.length).to eq(2)
|
||||
expect(statuses[0][:agent]).to eq(agent)
|
||||
expect(statuses[1][:agent]).to eq(agent2)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#agent_has_capacity?' do
|
||||
before { create(:enterprise_inbox_capacity_limit, agent_capacity_policy: capacity_policy, inbox: inbox, conversation_limit: 2) }
|
||||
|
||||
context 'when agent has capacity' do
|
||||
it 'returns true' do
|
||||
create(:conversation, inbox: inbox, assignee: agent, status: :open)
|
||||
|
||||
expect(manager.agent_has_capacity?(agent, inbox)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when agent is at capacity' do
|
||||
it 'returns false' do
|
||||
create_list(:conversation, 2, inbox: inbox, assignee: agent, status: :open)
|
||||
|
||||
expect(manager.agent_has_capacity?(agent, inbox)).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get_remaining_capacity' do
|
||||
before { create(:enterprise_inbox_capacity_limit, agent_capacity_policy: capacity_policy, inbox: inbox, conversation_limit: 5) }
|
||||
|
||||
it 'returns correct remaining capacity' do
|
||||
create_list(:conversation, 3, inbox: inbox, assignee: agent, status: :open)
|
||||
|
||||
expect(manager.get_remaining_capacity(agent, inbox)).to eq(2)
|
||||
end
|
||||
|
||||
it 'returns 0 when over capacity' do
|
||||
create_list(:conversation, 6, inbox: inbox, assignee: agent, status: :open)
|
||||
|
||||
expect(manager.get_remaining_capacity(agent, inbox)).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#invalidate_agent_capacity_cache' do
|
||||
it 'clears specific inbox cache when inbox provided' do
|
||||
cache_key = "assignment_v2:capacity:#{agent.accounts.first.id}:#{agent.id}:#{inbox.id}"
|
||||
Rails.cache.write(cache_key, 'test_data')
|
||||
|
||||
manager.invalidate_agent_capacity_cache(agent, inbox)
|
||||
|
||||
expect(Rails.cache.read(cache_key)).to be_nil
|
||||
end
|
||||
|
||||
it 'clears all agent caches when inbox not provided' do
|
||||
cache_key1 = "assignment_v2:capacity:#{agent.id}:inbox1"
|
||||
cache_key2 = "assignment_v2:capacity:#{agent.id}:inbox2"
|
||||
|
||||
Rails.cache.write(cache_key1, 'test_data')
|
||||
Rails.cache.write(cache_key2, 'test_data')
|
||||
|
||||
expect(Rails.cache).to receive(:delete_matched).with("assignment_v2:capacity:#{agent.id}:*")
|
||||
|
||||
manager.invalidate_agent_capacity_cache(agent)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#invalidate_inbox_capacity_cache' do
|
||||
it 'clears all capacity caches for inbox' do
|
||||
expect(Rails.cache).to receive(:delete_matched).with("assignment_v2:capacity:*:#{inbox.id}")
|
||||
|
||||
manager.invalidate_inbox_capacity_cache(inbox)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'edge cases' do
|
||||
it 'handles agent without account correctly' do
|
||||
agent_without_account = create(:user, role: :agent)
|
||||
|
||||
capacity = manager.get_agent_capacity(agent_without_account, inbox)
|
||||
|
||||
expect(capacity[:has_policy]).to be false
|
||||
end
|
||||
|
||||
it 'handles multiple capacity policies gracefully' do
|
||||
# Create another policy and try to assign it
|
||||
another_policy = create(:enterprise_agent_capacity_policy, account: account)
|
||||
|
||||
# Update account user
|
||||
account_user.update!(agent_capacity_policy: another_policy)
|
||||
|
||||
# Should use the new policy
|
||||
capacity = manager.get_agent_capacity(agent, inbox)
|
||||
expect(capacity[:policy_id]).to eq(another_policy.id)
|
||||
end
|
||||
|
||||
it 'handles conversations from different inboxes' do
|
||||
other_inbox = create(:inbox, account: account)
|
||||
create(:enterprise_inbox_capacity_limit,
|
||||
agent_capacity_policy: capacity_policy,
|
||||
inbox: inbox,
|
||||
conversation_limit: 5)
|
||||
|
||||
# Create conversations in different inboxes
|
||||
create_list(:conversation, 3, inbox: inbox, assignee: agent, status: :open)
|
||||
create_list(:conversation, 10, inbox: other_inbox, assignee: agent, status: :open)
|
||||
|
||||
# Should only count conversations from specified inbox
|
||||
capacity = manager.get_agent_capacity(agent, inbox)
|
||||
expect(capacity[:current_assignments]).to eq(3)
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user