210 lines
8.3 KiB
Ruby
210 lines
8.3 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# Test: Balanced Selector Strategy (Enterprise)
|
|
# Run with: bundle exec rails runner script/assignment_v2/test_balanced_selector.rb
|
|
#
|
|
# Tests:
|
|
# - Balanced selector assigns to agent with least open conversations
|
|
# - When agents have equal workload, any can be chosen
|
|
# - Workload is recalculated for each assignment (not cached)
|
|
# - Balanced selector produces different distribution than round-robin
|
|
#
|
|
# Key Implementation Details:
|
|
# - AssignmentPolicy has assignment_order enum: { round_robin: 0, balanced: 1 } (Enterprise)
|
|
# - Enterprise::AutoAssignment::AssignmentService uses balanced_selector when policy.balanced?
|
|
# - BalancedSelector.select_agent(agents) returns agent with min open conversation count
|
|
# - Counts are fetched fresh for each assignment: inbox.conversations.open.where(assignee_id: user_ids).count
|
|
# - This enables real-time workload balancing within a single assignment batch
|
|
|
|
require_relative 'test_helpers'
|
|
|
|
class TestBalancedSelector
|
|
include AssignmentV2TestHelpers
|
|
|
|
def run
|
|
skip_if_not_enterprise
|
|
|
|
section('TEST: Balanced Selector Strategy (Enterprise)')
|
|
|
|
account = get_test_account
|
|
inbox = nil
|
|
|
|
begin
|
|
# Setup test inbox with balanced policy
|
|
inbox = create_test_inbox(account, name: 'Balanced Selector Test Inbox')
|
|
|
|
# Create policy with balanced assignment order
|
|
policy = create_test_policy(
|
|
account,
|
|
name: 'Balanced Test Policy',
|
|
assignment_order: 'balanced' # Enterprise: balanced selector
|
|
)
|
|
link_policy_to_inbox(inbox, policy)
|
|
|
|
info('Created policy with assignment_order: balanced')
|
|
|
|
# Test 1: Initial balanced distribution
|
|
section('Test 1: Balanced Distribution from Zero')
|
|
|
|
# Create 3 agents
|
|
agent_1 = create_test_agent(account, inbox, name: 'Agent 1', online: true)
|
|
agent_2 = create_test_agent(account, inbox, name: 'Agent 2', online: true)
|
|
agent_3 = create_test_agent(account, inbox, name: 'Agent 3', online: true)
|
|
|
|
# Create 9 conversations (3 per agent if perfectly balanced)
|
|
create_bulk_conversations(inbox, count: 9)
|
|
|
|
# Run assignment
|
|
assigned_count = run_assignment(inbox)
|
|
|
|
# Verify balanced distribution
|
|
log('Verifying initial balanced distribution...', color: :blue)
|
|
|
|
# All 9 should be assigned
|
|
all_assigned = assert_equal(
|
|
assigned_count,
|
|
9,
|
|
'All 9 conversations assigned'
|
|
)
|
|
|
|
# Check distribution - should be 3 each (or as close as possible)
|
|
agent_1_count = inbox.conversations.where(assignee: agent_1).count
|
|
agent_2_count = inbox.conversations.where(assignee: agent_2).count
|
|
agent_3_count = inbox.conversations.where(assignee: agent_3).count
|
|
|
|
# All agents should have 3 conversations (perfect balance)
|
|
balanced_ok = assert(
|
|
agent_1_count == 3 && agent_2_count == 3 && agent_3_count == 3,
|
|
"Balanced distribution: #{agent_1.name}=3, #{agent_2.name}=3, #{agent_3.name}=3",
|
|
"Unbalanced: #{agent_1.name}=#{agent_1_count}, #{agent_2.name}=#{agent_2_count}, #{agent_3.name}=#{agent_3_count}"
|
|
)
|
|
|
|
test1_ok = all_assigned && balanced_ok
|
|
|
|
# Test 2: Rebalancing when agents have unequal workloads
|
|
section('Test 2: Rebalancing Unequal Workloads')
|
|
|
|
# Manually assign 5 more conversations to agent_1 (simulating prior workload)
|
|
manual_conversations = create_bulk_conversations(inbox, count: 5)
|
|
manual_conversations.each { |conv| conv.update!(assignee: agent_1) }
|
|
info("Manually assigned 5 conversations to #{agent_1.name}")
|
|
|
|
# Current state: agent_1 = 8, agent_2 = 3, agent_3 = 3
|
|
|
|
# Create 6 more conversations to assign
|
|
create_bulk_conversations(inbox, count: 6)
|
|
|
|
# Run assignment - balanced selector should favor agent_2 and agent_3
|
|
assigned_count_2 = run_assignment(inbox)
|
|
|
|
# Verify rebalancing behavior
|
|
log('Verifying rebalancing...', color: :blue)
|
|
|
|
# All 6 should be assigned
|
|
all_assigned_2 = assert_equal(
|
|
assigned_count_2,
|
|
6,
|
|
'All 6 new conversations assigned'
|
|
)
|
|
|
|
# Check new distribution
|
|
agent_1_new = inbox.conversations.where(assignee: agent_1, status: 'open').count
|
|
agent_2_new = inbox.conversations.where(assignee: agent_2, status: 'open').count
|
|
agent_3_new = inbox.conversations.where(assignee: agent_3, status: 'open').count
|
|
|
|
info("After rebalancing: #{agent_1.name}=#{agent_1_new}, #{agent_2.name}=#{agent_2_new}, #{agent_3.name}=#{agent_3_new}")
|
|
|
|
# agent_1 should still have 8 (no new assignments because already overloaded)
|
|
# agent_2 and agent_3 should have gotten the 6 new ones (3 each)
|
|
rebalanced_ok = assert(
|
|
agent_1_new == 8 && agent_2_new == 6 && agent_3_new == 6,
|
|
'Balanced selector avoided overloaded agent',
|
|
"Unexpected distribution: #{agent_1.name}=#{agent_1_new}, #{agent_2.name}=#{agent_2_new}, #{agent_3.name}=#{agent_3_new}"
|
|
)
|
|
|
|
test2_ok = all_assigned_2 && rebalanced_ok
|
|
|
|
# Test 3: Compare with round-robin behavior
|
|
section('Test 3: Balanced vs Round-Robin Comparison')
|
|
|
|
# Create a second inbox with round-robin policy for comparison
|
|
inbox_rr = create_test_inbox(account, name: 'Round-Robin Comparison Inbox')
|
|
policy_rr = create_test_policy(
|
|
account,
|
|
name: 'Round-Robin Policy',
|
|
assignment_order: 'round_robin'
|
|
)
|
|
link_policy_to_inbox(inbox_rr, policy_rr)
|
|
|
|
# Add same 3 agents to this inbox
|
|
inbox_rr.inbox_members.create!(user: agent_1)
|
|
inbox_rr.inbox_members.create!(user: agent_2)
|
|
inbox_rr.inbox_members.create!(user: agent_3)
|
|
|
|
# Give agent_1 a head start (5 conversations in round-robin inbox)
|
|
rr_manual = create_bulk_conversations(inbox_rr, count: 5)
|
|
rr_manual.each { |conv| conv.update!(assignee: agent_1) }
|
|
info("Round-robin inbox: Manually assigned 5 to #{agent_1.name}")
|
|
|
|
# Create 6 more conversations in round-robin inbox
|
|
create_bulk_conversations(inbox_rr, count: 6)
|
|
|
|
# Run assignment with round-robin
|
|
run_assignment(inbox_rr)
|
|
|
|
# Check round-robin distribution
|
|
agent_1_rr = inbox_rr.conversations.where(assignee: agent_1, status: 'open').count
|
|
agent_2_rr = inbox_rr.conversations.where(assignee: agent_2, status: 'open').count
|
|
agent_3_rr = inbox_rr.conversations.where(assignee: agent_3, status: 'open').count
|
|
|
|
info("Round-robin distribution: #{agent_1.name}=#{agent_1_rr}, #{agent_2.name}=#{agent_2_rr}, #{agent_3.name}=#{agent_3_rr}")
|
|
|
|
# Round-robin will assign 2 to each agent (6 ÷ 3 = 2 each), resulting in:
|
|
# agent_1 = 7, agent_2 = 2, agent_3 = 2 (unbalanced!)
|
|
rr_unbalanced = assert(
|
|
agent_1_rr > agent_2_rr && agent_1_rr > agent_3_rr,
|
|
'Round-robin creates unbalanced distribution when starting unequal',
|
|
'Round-robin distribution was balanced (unexpected)'
|
|
)
|
|
|
|
# Compare: balanced inbox should be more even
|
|
balanced_diff = (agent_1_new - agent_2_new).abs + (agent_2_new - agent_3_new).abs + (agent_1_new - agent_3_new).abs
|
|
rr_diff = (agent_1_rr - agent_2_rr).abs + (agent_2_rr - agent_3_rr).abs + (agent_1_rr - agent_3_rr).abs
|
|
|
|
better_balance = assert(
|
|
balanced_diff < rr_diff,
|
|
"Balanced selector (diff=#{balanced_diff}) is more even than round-robin (diff=#{rr_diff})",
|
|
"Balanced selector (diff=#{balanced_diff}) not better than round-robin (diff=#{rr_diff})"
|
|
)
|
|
|
|
test3_ok = rr_unbalanced && better_balance
|
|
|
|
# Show final comparison
|
|
log('Final Comparison:', color: :blue)
|
|
puts " Balanced Inbox: #{agent_1.name}=#{agent_1_new}, #{agent_2.name}=#{agent_2_new}, #{agent_3.name}=#{agent_3_new}"
|
|
puts " Round-Robin Inbox: #{agent_1.name}=#{agent_1_rr}, #{agent_2.name}=#{agent_2_rr}, #{agent_3.name}=#{agent_3_rr}"
|
|
|
|
# Cleanup round-robin inbox
|
|
cleanup_test_data(account, inbox: inbox_rr)
|
|
|
|
# Final result
|
|
if test1_ok && test2_ok && test3_ok
|
|
section('✓ ALL TESTS PASSED')
|
|
true
|
|
else
|
|
section('✗ SOME TESTS FAILED')
|
|
false
|
|
end
|
|
rescue StandardError => e
|
|
error("Test failed with error: #{e.message}")
|
|
puts e.backtrace.first(5).join("\n")
|
|
false
|
|
ensure
|
|
cleanup_test_data(account, inbox: inbox) if inbox
|
|
end
|
|
end
|
|
end
|
|
|
|
# Run test and exit with appropriate code
|
|
exit(TestBalancedSelector.new.run ? 0 : 1)
|