Files
chatwoot/script/assignment_v2/test_balanced_selector.rb
T

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)