diff --git a/spec/jobs/assignment_v2/continuous_assignment_job_spec.rb b/spec/jobs/assignment_v2/continuous_assignment_job_spec.rb deleted file mode 100644 index 8873f39b5..000000000 --- a/spec/jobs/assignment_v2/continuous_assignment_job_spec.rb +++ /dev/null @@ -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 diff --git a/spec/services/assignment_v2/assignment_orchestrator_spec.rb b/spec/services/assignment_v2/assignment_orchestrator_spec.rb deleted file mode 100644 index 6d784a995..000000000 --- a/spec/services/assignment_v2/assignment_orchestrator_spec.rb +++ /dev/null @@ -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 diff --git a/spec/services/enterprise/assignment_v2/capacity_manager_spec.rb b/spec/services/enterprise/assignment_v2/capacity_manager_spec.rb deleted file mode 100644 index 0acc03e0a..000000000 --- a/spec/services/enterprise/assignment_v2/capacity_manager_spec.rb +++ /dev/null @@ -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