fix(captain): integracao do delay do inbox com resposta humanizada e remocao de race conditions
This commit is contained in:
parent
ccc1bdf35f
commit
f12e1d6545
@ -17,6 +17,12 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
|
|||||||
# Cancel if there are newer messages after the provided message
|
# Cancel if there are newer messages after the provided message
|
||||||
return if debounce_requested?(message)
|
return if debounce_requested?(message)
|
||||||
|
|
||||||
|
# Simulate reading the message before starting to type
|
||||||
|
delay_before_typing(message)
|
||||||
|
|
||||||
|
# Re-check debounce to avoid race condition where another message arrived during reading sleep
|
||||||
|
return if debounce_requested?(message)
|
||||||
|
|
||||||
# Trigger typing on before processing
|
# Trigger typing on before processing
|
||||||
simulate_typing('typing_on')
|
simulate_typing('typing_on')
|
||||||
@start_time = Time.zone.now
|
@start_time = Time.zone.now
|
||||||
@ -53,6 +59,28 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
|
|||||||
last_incoming.present? && last_incoming.id != message.id
|
last_incoming.present? && last_incoming.id != message.id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def delay_before_typing(message)
|
||||||
|
return if message.blank? || message.content.blank?
|
||||||
|
|
||||||
|
chars_count = message.content.to_s.length
|
||||||
|
configured_delay = @inbox.typing_delay.to_i
|
||||||
|
|
||||||
|
# Simulate reading time proportional to configured delay. Max reading is ~40% of configured delay.
|
||||||
|
max_reading = configured_delay.positive? ? (configured_delay * 0.4) : 4.0
|
||||||
|
min_reading = [1.0, max_reading].min
|
||||||
|
reading_time = (chars_count / 25.0).clamp(min_reading, max_reading)
|
||||||
|
|
||||||
|
# Add jitter (randomness between 85% and 120%)
|
||||||
|
jitter = 0.85 + (rand * 0.35)
|
||||||
|
delay = reading_time * jitter
|
||||||
|
|
||||||
|
Rails.logger.info(
|
||||||
|
"[CAPTAIN][ResponseBuilderJob] Simulating reading delay of #{delay.round(2)}s " \
|
||||||
|
"for message of #{chars_count} chars"
|
||||||
|
)
|
||||||
|
sleep(delay)
|
||||||
|
end
|
||||||
|
|
||||||
def simulate_typing(status)
|
def simulate_typing(status)
|
||||||
# Trigger ActionCable for the Chatwoot dashboard
|
# Trigger ActionCable for the Chatwoot dashboard
|
||||||
cable_status = status == 'typing_on' ? 'on' : 'off'
|
cable_status = status == 'typing_on' ? 'on' : 'off'
|
||||||
@ -101,23 +129,33 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
|
|||||||
return if response_text.blank?
|
return if response_text.blank?
|
||||||
|
|
||||||
text = response_text.to_s
|
text = response_text.to_s
|
||||||
words_count = text.scan(/\b[\p{L}\p{N}]+\b/u).size
|
|
||||||
chars_count = text.length
|
chars_count = text.length
|
||||||
punctuation_pauses = text.count(',.!?;:')
|
punctuation_pauses = text.count(',.!?;:')
|
||||||
|
configured_delay = @inbox.typing_delay.to_i
|
||||||
|
|
||||||
# Modela tempo de digitação de forma mais humana:
|
# Modela tempo de digitação de forma mais humana no WhatsApp:
|
||||||
# - base por palavra (mais estável para textos longos),
|
# - Velocidade média de digitação: ~15 a 20 caracteres por segundo
|
||||||
# - ajuste por tamanho,
|
# - Pequenas pausas por pontuação
|
||||||
# - pequenas pausas por pontuação,
|
base_time = (chars_count / 15.0) + (punctuation_pauses * 0.25)
|
||||||
# - jitter para não repetir sempre o mesmo tempo.
|
|
||||||
base_time = (words_count * 0.32) + (chars_count * 0.01) + (punctuation_pauses * 0.18)
|
# Se configurado, max_typing é o valor escolhido, senão, 12s
|
||||||
|
max_typing = configured_delay.positive? ? configured_delay.to_f : 12.0
|
||||||
|
min_delay = [2.0, max_typing].min
|
||||||
|
|
||||||
|
# Adicionando uma ligeira variação humana
|
||||||
jitter = 0.85 + (rand * 0.35)
|
jitter = 0.85 + (rand * 0.35)
|
||||||
target_delay = (base_time * jitter).clamp(1.8, 18.0)
|
target_delay = (base_time * jitter).clamp(min_delay, max_typing)
|
||||||
|
|
||||||
elapsed_time = Time.zone.now - @start_time
|
elapsed_time = Time.zone.now - @start_time
|
||||||
remaining_delay = target_delay - elapsed_time
|
remaining_delay = target_delay - elapsed_time
|
||||||
|
|
||||||
sleep(remaining_delay) if remaining_delay.positive?
|
return unless remaining_delay.positive?
|
||||||
|
|
||||||
|
Rails.logger.info(
|
||||||
|
"[CAPTAIN][ResponseBuilderJob] Simulating typing delay of #{remaining_delay.round(2)}s " \
|
||||||
|
"(target: #{target_delay.round(2)}s, total elapsed: #{elapsed_time.round(2)}s, configured_max: #{max_typing}s)"
|
||||||
|
)
|
||||||
|
sleep(remaining_delay)
|
||||||
end
|
end
|
||||||
|
|
||||||
def collect_previous_messages
|
def collect_previous_messages
|
||||||
|
|||||||
@ -31,16 +31,11 @@ module Enterprise::MessageTemplates::HookExecutionService
|
|||||||
|
|
||||||
def schedule_captain_response
|
def schedule_captain_response
|
||||||
job_args = [conversation, conversation.inbox.captain_assistant, message]
|
job_args = [conversation, conversation.inbox.captain_assistant, message]
|
||||||
delay = conversation.inbox.typing_delay.to_i
|
|
||||||
|
|
||||||
if message.attachments.blank?
|
if message.attachments.blank?
|
||||||
if delay.positive?
|
Captain::Conversation::ResponseBuilderJob.perform_later(*job_args)
|
||||||
Captain::Conversation::ResponseBuilderJob.set(wait: delay.seconds).perform_later(*job_args)
|
|
||||||
else
|
|
||||||
Captain::Conversation::ResponseBuilderJob.perform_later(*job_args)
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
wait_time = calculate_attachment_wait_time + delay.seconds
|
wait_time = calculate_attachment_wait_time
|
||||||
Captain::Conversation::ResponseBuilderJob.set(wait: wait_time).perform_later(*job_args)
|
Captain::Conversation::ResponseBuilderJob.set(wait: wait_time).perform_later(*job_args)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
module Captain::Conversation::ReactionPolicy
|
module Captain::Conversation::ReactionPolicy
|
||||||
REACTION_SAMPLE_RATE = 5
|
REACTION_SAMPLE_RATE = 20
|
||||||
|
REACTION_SAMPLE_THRESHOLD = 7
|
||||||
GREETING_PATTERNS = [
|
GREETING_PATTERNS = [
|
||||||
/\boi\b/,
|
/\boi\b/,
|
||||||
/\bola\b/,
|
/\bola\b/,
|
||||||
@ -11,6 +12,17 @@ module Captain::Conversation::ReactionPolicy
|
|||||||
/\bhi\b/,
|
/\bhi\b/,
|
||||||
/\bhey\b/
|
/\bhey\b/
|
||||||
].freeze
|
].freeze
|
||||||
|
GRATITUDE_PATTERNS = [
|
||||||
|
/\bobrigad[oa]\b/,
|
||||||
|
/\bobg\b/,
|
||||||
|
/\bvaleu\b/,
|
||||||
|
/\bagradeco\b/,
|
||||||
|
/\bagradecid[oa]\b/,
|
||||||
|
/\bthanks\b/,
|
||||||
|
/\bthank you\b/,
|
||||||
|
/\bthx\b/,
|
||||||
|
/\bty\b/
|
||||||
|
].freeze
|
||||||
FAREWELL_PATTERNS = [
|
FAREWELL_PATTERNS = [
|
||||||
/\btchau\b/,
|
/\btchau\b/,
|
||||||
/\bate mais\b/,
|
/\bate mais\b/,
|
||||||
@ -27,19 +39,20 @@ module Captain::Conversation::ReactionPolicy
|
|||||||
def should_send_reaction_for?(target_message)
|
def should_send_reaction_for?(target_message)
|
||||||
return false if @response['reaction_emoji'].blank?
|
return false if @response['reaction_emoji'].blank?
|
||||||
return false if target_message.blank?
|
return false if target_message.blank?
|
||||||
return true if greeting_or_farewell?(target_message.content)
|
return true if greeting_farewell_or_gratitude?(target_message.content)
|
||||||
|
|
||||||
sampled_reaction_slot?(target_message)
|
sampled_reaction_slot?(target_message)
|
||||||
end
|
end
|
||||||
|
|
||||||
def sampled_reaction_slot?(target_message)
|
def sampled_reaction_slot?(target_message)
|
||||||
(target_message.id % REACTION_SAMPLE_RATE).zero?
|
(target_message.id % REACTION_SAMPLE_RATE) < REACTION_SAMPLE_THRESHOLD
|
||||||
end
|
end
|
||||||
|
|
||||||
def greeting_or_farewell?(text)
|
def greeting_farewell_or_gratitude?(text)
|
||||||
normalized = normalize_reaction_text(text)
|
normalized = normalize_reaction_text(text)
|
||||||
GREETING_PATTERNS.any? { |pattern| normalized.match?(pattern) } ||
|
GREETING_PATTERNS.any? { |pattern| normalized.match?(pattern) } ||
|
||||||
FAREWELL_PATTERNS.any? { |pattern| normalized.match?(pattern) }
|
FAREWELL_PATTERNS.any? { |pattern| normalized.match?(pattern) } ||
|
||||||
|
GRATITUDE_PATTERNS.any? { |pattern| normalized.match?(pattern) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def normalize_reaction_text(text)
|
def normalize_reaction_text(text)
|
||||||
|
|||||||
@ -49,8 +49,8 @@ CRITICAL: Do NOT react to every single message! This makes the interaction feel
|
|||||||
- AVOID reacting to serious complaints or basic continuous questions if the tone doesn't fit.
|
- AVOID reacting to serious complaints or basic continuous questions if the tone doesn't fit.
|
||||||
- If you just sent an emoji in the previous turn, try to hold off on sending another right away. When in doubt, leave `reaction_emoji` empty.
|
- If you just sent an emoji in the previous turn, try to hold off on sending another right away. When in doubt, leave `reaction_emoji` empty.
|
||||||
- Frequency policy:
|
- Frequency policy:
|
||||||
- Prefer reaction on greeting/farewell messages.
|
- Always react on greeting, farewell, and thank-you/appreciation messages when tone is positive.
|
||||||
- For regular conversation, react only occasionally (roughly 20% of turns or less).
|
- For regular conversation, react only occasionally (roughly 35% of turns).
|
||||||
- If uncertain, keep `reaction_emoji` empty.
|
- If uncertain, keep `reaction_emoji` empty.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
24
progresso/humanized_typing_delay.md
Normal file
24
progresso/humanized_typing_delay.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Melhorias no Delay Humanizado (Wuzapi/Captain)
|
||||||
|
|
||||||
|
**Objetivo:** Integrar o delay humanizado (leitura e digitação simuladas) com a configuração de atraso base controlada pelo front-end (`typing_delay` da Inbox), evitando atrasos excessivos e resolvendo uma *Race Condition* de mensagens concorrentes. Ocultar scripts temporários de teste da raiz do repositório.
|
||||||
|
|
||||||
|
**Contexto:**
|
||||||
|
- O delay fixo adicionado anteriormente bloqueava os testes e fugia do controle do administrador. Se o admin configurasse o delay no painel para "5", o sistema adicionava os passos humanizados de leitura e digitação *em cima* desses 5 segundos originais na fila do Sidekiq.
|
||||||
|
- Risco técnico (Race Condition): um `sleep(4)` para simulação de leitura permitia a chegada de uma nova mensagem, que ficaria ignorada ou atropelada.
|
||||||
|
- Sujeira no repositório: arquivos `.rb` na raiz do projeto poluindo o tracking do git.
|
||||||
|
|
||||||
|
**Passos:**
|
||||||
|
1. **Limpeza do Tracking:** Movidos os scripts descartáveis `test_wuzapi_parser.rb`, `test_wuzapi_service.rb`, `test_wuzapi_service2.rb` e `test_wuzapi_service3.rb` para `scripts/dev/`.
|
||||||
|
2. **Transferência do Delay Total:** Removemos o atraso cego na fila pelo Sidekiq (antes em `HookExecutionService` via `wait: delay.seconds`). O job engatilha agora imediatamente para a mensagem começar o fluxo visivelmente.
|
||||||
|
3. **Escalonamento do Delay Dinâmico:** Atualizado `ResponseBuilderJob` para usar `@inbox.typing_delay.to_i`. Agora, se `typing_delay` for maior que zero, ele é utilizado como *peso máximo* para a leitura e digitação, combinando a variação humana (jitter) mas respeitando a escala do front-end.
|
||||||
|
4. **Fechamento de Race Condition:** Adicionado novamente um check `return if debounce_requested?(message)` no final da pausa de "leitura", certificando que nenhuma mensagem invadiu nos segundos intermediários do `sleep`.
|
||||||
|
|
||||||
|
**Principais Códigos Alterados:**
|
||||||
|
- `enterprise/app/services/enterprise/message_templates/hook_execution_service.rb`
|
||||||
|
- `enterprise/app/jobs/captain/conversation/response_builder_job.rb`
|
||||||
|
|
||||||
|
**Como Validar:**
|
||||||
|
No frontend, altere o valor de "Delay before responding" para um número baixo (ex: 2 segundos) e envie mensagens variadas no whatsapp. A resposta de leitura e digitação não vai exceder este tempo máximo. Depois aumente o número livremente. Teste enviar duas mensagens curtas em menos de 2 segundos para validar se a resposta não está atropelando o raciocínio.
|
||||||
|
|
||||||
|
**Como Reverter:**
|
||||||
|
As alterações em `HookExecutionService` podem ser revertidas readicionando `wait: delay.seconds` no `.set()` de enqueue do job. Em `ResponseBuilderJob`, as chamadas das lógicas de escala de delays com `.clamp(1.0, max...)` podem retornar ao padrão duro das branchs anteriores.
|
||||||
21
scripts/dev/test_wuzapi_parser.rb
Normal file
21
scripts/dev/test_wuzapi_parser.rb
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
require_relative 'config/environment'
|
||||||
|
payload = {
|
||||||
|
"event" => {
|
||||||
|
"Info" => {
|
||||||
|
"Type" => "text",
|
||||||
|
"Sender" => "556182098580@s.whatsapp.net",
|
||||||
|
"PushName" => "😅‼️",
|
||||||
|
"Chat" => "556182098580@s.whatsapp.net"
|
||||||
|
},
|
||||||
|
"Message" => {
|
||||||
|
"conversation" => "teste4000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type" => "Message",
|
||||||
|
"phone_number" => "556133712229"
|
||||||
|
}
|
||||||
|
parser = Whatsapp::Providers::Wuzapi::PayloadParser.new(payload)
|
||||||
|
puts "Message Type: #{parser.message_type}"
|
||||||
|
puts "Text: #{parser.text_content}"
|
||||||
|
puts "From Me: #{parser.from_me?}"
|
||||||
|
puts "Sender: #{parser.sender_phone_number}"
|
||||||
32
scripts/dev/test_wuzapi_service.rb
Normal file
32
scripts/dev/test_wuzapi_service.rb
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
require_relative 'config/environment'
|
||||||
|
payload = {
|
||||||
|
"event" => {
|
||||||
|
"Info" => {
|
||||||
|
"Type" => "text",
|
||||||
|
"Sender" => "556182098580@s.whatsapp.net",
|
||||||
|
"PushName" => "😅‼️",
|
||||||
|
"Chat" => "556182098580@s.whatsapp.net"
|
||||||
|
},
|
||||||
|
"Message" => {
|
||||||
|
"conversation" => "teste4000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type" => "Message",
|
||||||
|
"phone_number" => "556133712229",
|
||||||
|
"instanceName" => "Chatwoot_61 33712229"
|
||||||
|
}
|
||||||
|
|
||||||
|
channel = Channel::Whatsapp.find_by(phone_number: "556133712229")
|
||||||
|
if channel
|
||||||
|
puts "Channel found: #{channel.id} / #{channel.phone_number}"
|
||||||
|
service = Whatsapp::IncomingMessageWuzapiService.new(inbox: channel.inbox, params: payload)
|
||||||
|
begin
|
||||||
|
service.perform
|
||||||
|
puts "Service performed successfully."
|
||||||
|
rescue StandardError => e
|
||||||
|
puts "ERROR: #{e.message}"
|
||||||
|
puts e.backtrace.first(10)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
puts "Channel not found for 556133712229"
|
||||||
|
end
|
||||||
39
scripts/dev/test_wuzapi_service2.rb
Normal file
39
scripts/dev/test_wuzapi_service2.rb
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
require_relative 'config/environment'
|
||||||
|
payload = {
|
||||||
|
"event" => {
|
||||||
|
"Info" => {
|
||||||
|
"Type" => "text",
|
||||||
|
"Sender" => "556182098580@s.whatsapp.net",
|
||||||
|
"PushName" => "😅‼️",
|
||||||
|
"Chat" => "556182098580@s.whatsapp.net"
|
||||||
|
},
|
||||||
|
"Message" => {
|
||||||
|
"conversation" => "teste4000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type" => "Message",
|
||||||
|
"phone_number" => "556133712229",
|
||||||
|
"instanceName" => "Chatwoot_61 33712229"
|
||||||
|
}
|
||||||
|
|
||||||
|
phone = "556133712229"
|
||||||
|
channel = Channel::Whatsapp.find_by(phone_number: phone) ||
|
||||||
|
Channel::Whatsapp.find_by(phone_number: "+#{phone}") ||
|
||||||
|
Channel::Whatsapp.where("regexp_replace(phone_number, '[^0-9]', '', 'g') = ?", phone).first
|
||||||
|
|
||||||
|
if channel
|
||||||
|
puts "Channel found: #{channel.id} / #{channel.phone_number}"
|
||||||
|
service = Whatsapp::IncomingMessageWuzapiService.new(inbox: channel.inbox, params: payload)
|
||||||
|
begin
|
||||||
|
service.perform
|
||||||
|
puts "Service performed successfully."
|
||||||
|
rescue StandardError => e
|
||||||
|
puts "ERROR: #{e.message}"
|
||||||
|
puts e.backtrace.first(10)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
puts "Channel still not found for #{phone}. Let's list all whatsapp channels:"
|
||||||
|
Channel::Whatsapp.all.each do |c|
|
||||||
|
puts "- ID: #{c.id}, Phone: #{c.phone_number}, Provider: #{c.provider}"
|
||||||
|
end
|
||||||
|
end
|
||||||
38
scripts/dev/test_wuzapi_service3.rb
Normal file
38
scripts/dev/test_wuzapi_service3.rb
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
require_relative 'config/environment'
|
||||||
|
payload = {
|
||||||
|
"event" => {
|
||||||
|
"Info" => {
|
||||||
|
"Type" => "text",
|
||||||
|
"Sender" => "556182098580@s.whatsapp.net",
|
||||||
|
"PushName" => "😅‼️",
|
||||||
|
"Chat" => "556182098580@s.whatsapp.net"
|
||||||
|
},
|
||||||
|
"Message" => {
|
||||||
|
"conversation" => "teste4000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type" => "Message",
|
||||||
|
"phone_number" => "556191544165",
|
||||||
|
"instanceName" => "Chatwoot_556191544165"
|
||||||
|
}
|
||||||
|
|
||||||
|
phone = "556191544165"
|
||||||
|
channel = Channel::Whatsapp.find_by(phone_number: phone) ||
|
||||||
|
Channel::Whatsapp.find_by(phone_number: "+#{phone}") ||
|
||||||
|
Channel::Whatsapp.where("regexp_replace(phone_number, '[^0-9]', '', 'g') = ?", phone).first
|
||||||
|
|
||||||
|
if channel
|
||||||
|
puts "Channel found: #{channel.id} / #{channel.phone_number}"
|
||||||
|
service = Whatsapp::IncomingMessageWuzapiService.new(inbox: channel.inbox, params: payload)
|
||||||
|
begin
|
||||||
|
service.perform
|
||||||
|
puts "Service performed successfully."
|
||||||
|
puts "Last conversation: #{channel.inbox.conversations.last&.id}"
|
||||||
|
puts "Last message: #{channel.inbox.messages.last&.content}"
|
||||||
|
rescue StandardError => e
|
||||||
|
puts "ERROR: #{e.message}"
|
||||||
|
puts e.backtrace.first(10)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
puts "Channel still not found for #{phone}."
|
||||||
|
end
|
||||||
@ -239,7 +239,7 @@ RSpec.describe Captain::Conversation::ResponseBuilderJob, type: :job do
|
|||||||
job.instance_variable_set(:@response, { 'reaction_emoji' => '👍' })
|
job.instance_variable_set(:@response, { 'reaction_emoji' => '👍' })
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_incoming_until_slot(modulo_target:)
|
def create_incoming_until_slot(sample_target:)
|
||||||
loop do
|
loop do
|
||||||
message = create(
|
message = create(
|
||||||
:message,
|
:message,
|
||||||
@ -248,7 +248,7 @@ RSpec.describe Captain::Conversation::ResponseBuilderJob, type: :job do
|
|||||||
message_type: :incoming,
|
message_type: :incoming,
|
||||||
content: 'quero saber disponibilidade'
|
content: 'quero saber disponibilidade'
|
||||||
)
|
)
|
||||||
return message if (message.id % described_class::REACTION_SAMPLE_RATE).zero? == modulo_target
|
return message if job.send(:sampled_reaction_slot?, message) == sample_target
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -264,14 +264,26 @@ RSpec.describe Captain::Conversation::ResponseBuilderJob, type: :job do
|
|||||||
expect(job.send(:should_send_reaction_for?, message)).to be(true)
|
expect(job.send(:should_send_reaction_for?, message)).to be(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'blocks reaction for regular messages outside the 20% sample' do
|
it 'always allows reaction for gratitude messages' do
|
||||||
message = create_incoming_until_slot(modulo_target: false)
|
message = create(
|
||||||
|
:message,
|
||||||
|
conversation: conversation,
|
||||||
|
inbox: inbox,
|
||||||
|
message_type: :incoming,
|
||||||
|
content: 'Obrigado pelo atendimento'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(job.send(:should_send_reaction_for?, message)).to be(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'blocks reaction for regular messages outside the 35% sample' do
|
||||||
|
message = create_incoming_until_slot(sample_target: false)
|
||||||
|
|
||||||
expect(job.send(:should_send_reaction_for?, message)).to be(false)
|
expect(job.send(:should_send_reaction_for?, message)).to be(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'allows reaction for regular messages inside the 20% sample' do
|
it 'allows reaction for regular messages inside the 35% sample' do
|
||||||
message = create_incoming_until_slot(modulo_target: true)
|
message = create_incoming_until_slot(sample_target: true)
|
||||||
|
|
||||||
expect(job.send(:should_send_reaction_for?, message)).to be(true)
|
expect(job.send(:should_send_reaction_for?, message)).to be(true)
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user