+ editorRoot.value?.classList.contains('popover-prosemirror-menu') ?? false
+);
+
const handleCopilotAction = actionKey => {
if (actionKey === 'improve_selection' && editorView?.state) {
const { from, to } = editorView.state.selection;
@@ -211,7 +216,7 @@ const handleCopilotAction = actionKey => {
emit('executeCopilotAction', 'improve', selectedText);
}
} else {
- emit('executeCopilotAction', actionKey);
+ emit('executeCopilotAction', actionKey, props.modelValue);
}
showSelectionMenu.value = false;
@@ -484,6 +489,7 @@ function setToolbarPosition() {
function setMenubarPosition({ selection } = {}) {
const wrapper = editorRoot.value;
if (!selection || !wrapper) return;
+ if (!isEditorMenuPopover.value) return;
const rect = wrapper.getBoundingClientRect();
const isRtl = getComputedStyle(wrapper).direction === 'rtl';
@@ -866,8 +872,12 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
v-if="showSelectionMenu"
v-on-click-outside="handleClickOutside"
:has-selection="isTextSelected"
+ :is-editor-menu-popover="isEditorMenuPopover"
+ :editor-content="modelValue"
+ :conversation-id="conversationId"
:show-selection-menu="showSelectionMenu"
:show-general-menu="false"
+ class="copilot-editor-menu"
@execute-copilot-action="handleCopilotAction"
/>
.copilot-editor-menu {
+ top: 1.5rem !important;
+
+ [dir='rtl'] & {
+ left: auto !important;
+ right: 0 !important;
+ }
+}
+
// Float editor menu
.popover-prosemirror-menu {
position: relative;
diff --git a/app/javascript/dashboard/components/widgets/WootWriter/ReplyTopPanel.vue b/app/javascript/dashboard/components/widgets/WootWriter/ReplyTopPanel.vue
index ae49145dc..210741481 100644
--- a/app/javascript/dashboard/components/widgets/WootWriter/ReplyTopPanel.vue
+++ b/app/javascript/dashboard/components/widgets/WootWriter/ReplyTopPanel.vue
@@ -49,6 +49,10 @@ export default {
type: Number,
default: () => 0,
},
+ editorContent: {
+ type: String,
+ default: undefined,
+ },
},
emits: ['setReplyMode', 'togglePopout', 'executeCopilotAction'],
setup(props, { emit }) {
@@ -73,8 +77,8 @@ export default {
const { captainTasksEnabled } = useCaptain();
const showCopilotMenu = ref(false);
- const handleCopilotAction = actionKey => {
- emit('executeCopilotAction', actionKey);
+ const handleCopilotAction = (actionKey, data) => {
+ emit('executeCopilotAction', actionKey, data || props.editorContent);
showCopilotMenu.value = false;
};
@@ -174,6 +178,8 @@ export default {
v-if="showCopilotMenu"
v-on-click-outside="handleClickOutside"
:has-selection="false"
+ :editor-content="editorContent"
+ :conversation-id="conversationId"
class="ltr:right-0 rtl:left-0 bottom-full mb-2"
@execute-copilot-action="handleCopilotAction"
/>
diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
index 23b4d89a8..81c424aec 100644
--- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
@@ -1245,6 +1245,7 @@ export default {
:is-editor-disabled="isEditorDisabled"
:is-message-length-reaching-threshold="isMessageLengthReachingThreshold"
:characters-remaining="charactersRemaining"
+ :editor-content="message"
:popout-reply-box="popOutReplyBox"
@set-reply-mode="setReplyMode"
@toggle-popout="togglePopout"
diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json
index 0d9f79984..803cd66cb 100644
--- a/app/javascript/dashboard/i18n/locale/en/contact.json
+++ b/app/javascript/dashboard/i18n/locale/en/contact.json
@@ -613,7 +613,7 @@
"NO_INBOX_ALERT": "There are no available inboxes to start a conversation with this contact.",
"CONTACT_SELECTOR": {
"LABEL": "To:",
- "TAG_INPUT_PLACEHOLDER": "Search for a contact with name, email or phone number",
+ "TAG_INPUT_PLACEHOLDER": "Enter at least 2 characters to search by name, email, or phone number",
"CONTACT_CREATING": "Creating contact..."
},
"INBOX_SELECTOR": {
@@ -624,9 +624,9 @@
"SUBJECT_LABEL": "Subject :",
"SUBJECT_PLACEHOLDER": "Enter your email subject here",
"CC_LABEL": "Cc:",
- "CC_PLACEHOLDER": "Search for a contact with their email address",
+ "CC_PLACEHOLDER": "Enter at least 2 characters to search by email",
"BCC_LABEL": "Bcc:",
- "BCC_PLACEHOLDER": "Search for a contact with their email address",
+ "BCC_PLACEHOLDER": "Enter at least 2 characters to search by email",
"BCC_BUTTON": "Bcc"
},
"MESSAGE_EDITOR": {
diff --git a/app/javascript/dashboard/modules/search/components/SearchContactAgentSelector.vue b/app/javascript/dashboard/modules/search/components/SearchContactAgentSelector.vue
index 17edc33a5..a177296c0 100644
--- a/app/javascript/dashboard/modules/search/components/SearchContactAgentSelector.vue
+++ b/app/javascript/dashboard/modules/search/components/SearchContactAgentSelector.vue
@@ -5,7 +5,7 @@ import { useToggle } from '@vueuse/core';
import { vOnClickOutside } from '@vueuse/components';
import { debounce } from '@chatwoot/utils';
import { useMapGetter } from 'dashboard/composables/store.js';
-import { searchContacts } from 'dashboard/components-next/NewConversation/helpers/composeConversationHelper';
+import { createContactSearcher } from 'dashboard/components-next/NewConversation/helpers/composeConversationHelper';
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
import { fetchContactDetails } from '../helpers/searchHelper';
@@ -18,6 +18,8 @@ const props = defineProps({
const emit = defineEmits(['change']);
+const searchContacts = createContactSearcher();
+
const FROM_TYPE = {
CONTACT: 'contact',
AGENT: 'agent',
@@ -119,7 +121,10 @@ const debouncedSearch = debounce(async query => {
}
try {
- const contacts = await searchContacts(query);
+ const contacts = await searchContacts(query, { skipMinLength: true });
+
+ // null means the request was aborted (a newer search is in-flight),
+ if (contacts === null) return;
// Add selected contact to top if not already in results
const allContacts = selectedContact.value
@@ -130,9 +135,8 @@ const debouncedSearch = debounce(async query => {
: contacts;
searchedContacts.value = allContacts;
+ isSearching.value = false;
} catch {
- // Ignore error
- } finally {
isSearching.value = false;
}
}, 300);
diff --git a/app/jobs/agent_bots/webhook_job.rb b/app/jobs/agent_bots/webhook_job.rb
index b3a3d6cc1..2786ce70e 100644
--- a/app/jobs/agent_bots/webhook_job.rb
+++ b/app/jobs/agent_bots/webhook_job.rb
@@ -1,7 +1,14 @@
class AgentBots::WebhookJob < WebhookJob
queue_as :high
+ retry_on RestClient::TooManyRequests, RestClient::InternalServerError, wait: 3.seconds, attempts: 3 do |job, error|
+ url, payload, webhook_type = job.arguments
+ Webhooks::Trigger.new(url, payload, webhook_type || :agent_bot_webhook).handle_failure(error)
+ end
def perform(url, payload, webhook_type = :agent_bot_webhook)
super(url, payload, webhook_type)
+ rescue RestClient::TooManyRequests, RestClient::InternalServerError => e
+ Rails.logger.warn("[AgentBots::WebhookJob] attempt #{executions} failed #{e.class.name}")
+ raise
end
end
diff --git a/app/models/account.rb b/app/models/account.rb
index 4816494fb..eabaa5c26 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -41,6 +41,7 @@ class Account < ApplicationRecord
'audio_transcriptions': { 'type': %w[boolean null] },
'auto_resolve_label': { 'type': %w[string null] },
'keep_pending_on_bot_failure': { 'type': %w[boolean null] },
+ 'captain_disable_auto_resolve': { 'type': %w[boolean null] },
'conversation_required_attributes': {
'type': %w[array null],
'items': { 'type': 'string' }
@@ -90,6 +91,7 @@ class Account < ApplicationRecord
store_accessor :settings, :audio_transcriptions, :auto_resolve_label
store_accessor :settings, :captain_models, :captain_features
store_accessor :settings, :keep_pending_on_bot_failure
+ store_accessor :settings, :captain_disable_auto_resolve
has_many :account_users, dependent: :destroy_async
has_many :agent_bot_inboxes, dependent: :destroy_async
diff --git a/app/services/facebook/send_on_facebook_service.rb b/app/services/facebook/send_on_facebook_service.rb
index ed3b7e4ab..baf72ef6e 100644
--- a/app/services/facebook/send_on_facebook_service.rb
+++ b/app/services/facebook/send_on_facebook_service.rb
@@ -49,7 +49,7 @@ class Facebook::SendOnFacebookService < Base::SendOnChannelService
recipient: { id: contact.get_source_id(inbox.id) },
message: fb_text_message_payload,
messaging_type: 'MESSAGE_TAG',
- tag: 'ACCOUNT_UPDATE'
+ tag: message_tag
}
end
@@ -90,10 +90,14 @@ class Facebook::SendOnFacebookService < Base::SendOnChannelService
}
},
messaging_type: 'MESSAGE_TAG',
- tag: 'ACCOUNT_UPDATE'
+ tag: message_tag
}
end
+ def message_tag
+ @message_tag ||= GlobalConfigService.load('ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT', nil) ? 'HUMAN_AGENT' : 'ACCOUNT_UPDATE'
+ end
+
def attachment_type(attachment)
return attachment.file_type if %w[image audio video file].include? attachment.file_type
diff --git a/config/features.yml b/config/features.yml
index 65b3c6194..eed6a9da1 100644
--- a/config/features.yml
+++ b/config/features.yml
@@ -74,10 +74,9 @@
- name: voice_recorder
display_name: Voice Recorder
enabled: true
-- name: mobile_v2
- display_name: Mobile App V2
+- name: report_rollup
+ display_name: Report Rollup
enabled: false
- deprecated: true
- name: channel_website
display_name: Website Channel
enabled: true
diff --git a/db/migrate/20260226153427_disable_report_rollup_for_all_accounts.rb b/db/migrate/20260226153427_disable_report_rollup_for_all_accounts.rb
new file mode 100644
index 000000000..60a8f4604
--- /dev/null
+++ b/db/migrate/20260226153427_disable_report_rollup_for_all_accounts.rb
@@ -0,0 +1,8 @@
+class DisableReportRollupForAllAccounts < ActiveRecord::Migration[7.1]
+ def up
+ Account.feature_report_rollup.find_each(batch_size: 100) do |account|
+ account.disable_features(:report_rollup)
+ account.save!(validate: false)
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 8a450e734..4bb0ca3af 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.1].define(version: 2026_02_26_084618) do
+ActiveRecord::Schema[7.1].define(version: 2026_02_26_153427) do
# These extensions should be enabled to support this database
enable_extension "pg_stat_statements"
enable_extension "pg_trgm"
diff --git a/enterprise/app/jobs/captain/inbox_pending_conversations_resolution_job.rb b/enterprise/app/jobs/captain/inbox_pending_conversations_resolution_job.rb
index d3f1f5d96..ab9ca2ab1 100644
--- a/enterprise/app/jobs/captain/inbox_pending_conversations_resolution_job.rb
+++ b/enterprise/app/jobs/captain/inbox_pending_conversations_resolution_job.rb
@@ -2,6 +2,8 @@ class Captain::InboxPendingConversationsResolutionJob < ApplicationJob
queue_as :low
def perform(inbox)
+ return if inbox.account.captain_disable_auto_resolve
+
Current.executed_by = inbox.captain_assistant
resolvable_conversations = inbox.conversations.pending.where('last_activity_at < ? ', Time.now.utc - 1.hour).limit(Limits::BULK_ACTIONS_LIMIT)
diff --git a/enterprise/app/jobs/enterprise/account/conversations_resolution_scheduler_job.rb b/enterprise/app/jobs/enterprise/account/conversations_resolution_scheduler_job.rb
index 599dee96a..8b6527c93 100644
--- a/enterprise/app/jobs/enterprise/account/conversations_resolution_scheduler_job.rb
+++ b/enterprise/app/jobs/enterprise/account/conversations_resolution_scheduler_job.rb
@@ -12,6 +12,7 @@ module Enterprise::Account::ConversationsResolutionSchedulerJob
inbox = captain_inbox.inbox
next if inbox.email?
+ next if inbox.account.captain_disable_auto_resolve
Captain::InboxPendingConversationsResolutionJob.perform_later(
inbox
diff --git a/enterprise/lib/captain/tools/resolve_conversation_tool.rb b/enterprise/lib/captain/tools/resolve_conversation_tool.rb
index 0d2563a8b..5d96d3af1 100644
--- a/enterprise/lib/captain/tools/resolve_conversation_tool.rb
+++ b/enterprise/lib/captain/tools/resolve_conversation_tool.rb
@@ -6,6 +6,7 @@ class Captain::Tools::ResolveConversationTool < Captain::Tools::BasePublicTool
conversation = find_conversation(tool_context.state)
return 'Conversation not found' unless conversation
return "Conversation ##{conversation.display_id} is already resolved" if conversation.resolved?
+ return 'Auto-resolve is disabled for this account' if conversation.account.captain_disable_auto_resolve
log_tool_usage('resolve_conversation', { conversation_id: conversation.id, reason: reason })
diff --git a/lib/webhooks/trigger.rb b/lib/webhooks/trigger.rb
index 456e186ca..7cb15c836 100644
--- a/lib/webhooks/trigger.rb
+++ b/lib/webhooks/trigger.rb
@@ -15,9 +15,17 @@ class Webhooks::Trigger
def execute
perform_request
+ rescue RestClient::TooManyRequests, RestClient::InternalServerError => e
+ raise if @webhook_type == :agent_bot_webhook
+
+ handle_failure(e)
rescue StandardError => e
- handle_error(e)
- Rails.logger.warn "Exception: Invalid webhook URL #{@url} : #{e.message}"
+ handle_failure(e)
+ end
+
+ def handle_failure(error)
+ handle_error(error)
+ Rails.logger.warn "Exception: Invalid webhook URL #{@url} : #{error.message}"
end
private
diff --git a/spec/enterprise/jobs/captain/inbox_pending_conversations_resolution_job_spec.rb b/spec/enterprise/jobs/captain/inbox_pending_conversations_resolution_job_spec.rb
index 1a8a5a342..ab8f0296c 100644
--- a/spec/enterprise/jobs/captain/inbox_pending_conversations_resolution_job_spec.rb
+++ b/spec/enterprise/jobs/captain/inbox_pending_conversations_resolution_job_spec.rb
@@ -64,4 +64,15 @@ RSpec.describe Captain::InboxPendingConversationsResolutionJob, type: :job do
}
)
end
+
+ it 'does not resolve conversations when auto-resolve is disabled at execution time' do
+ inbox.account.update!(captain_disable_auto_resolve: true)
+
+ expect do
+ described_class.perform_now(inbox)
+ end.not_to(change { resolvable_pending_conversation.reload.status })
+
+ expect(resolvable_pending_conversation.reload.status).to eq('pending')
+ expect(resolvable_pending_conversation.messages.outgoing).to be_empty
+ end
end
diff --git a/spec/enterprise/jobs/enterprise/account/conversations_resolution_scheduler_job_spec.rb b/spec/enterprise/jobs/enterprise/account/conversations_resolution_scheduler_job_spec.rb
index b67877412..343100a50 100644
--- a/spec/enterprise/jobs/enterprise/account/conversations_resolution_scheduler_job_spec.rb
+++ b/spec/enterprise/jobs/enterprise/account/conversations_resolution_scheduler_job_spec.rb
@@ -30,6 +30,22 @@ RSpec.describe Account::ConversationsResolutionSchedulerJob, type: :job do
end
end
+ context 'when account has captain_disable_auto_resolve enabled' do
+ let!(:regular_inbox) { create(:inbox, account: account) }
+
+ before do
+ create(:captain_inbox, captain_assistant: assistant, inbox: regular_inbox)
+ account.update!(captain_disable_auto_resolve: true)
+ end
+
+ it 'does not enqueue resolution jobs' do
+ expect do
+ described_class.perform_now
+ end.not_to have_enqueued_job(Captain::InboxPendingConversationsResolutionJob)
+ .with(regular_inbox)
+ end
+ end
+
context 'when inbox has no captain enabled' do
let!(:inbox_without_captain) { create(:inbox, account: create(:account)) }
diff --git a/spec/enterprise/lib/captain/tools/resolve_conversation_tool_spec.rb b/spec/enterprise/lib/captain/tools/resolve_conversation_tool_spec.rb
index f91f430e8..d5792cf78 100644
--- a/spec/enterprise/lib/captain/tools/resolve_conversation_tool_spec.rb
+++ b/spec/enterprise/lib/captain/tools/resolve_conversation_tool_spec.rb
@@ -36,6 +36,17 @@ RSpec.describe Captain::Tools::ResolveConversationTool do
end
end
+ describe 'when auto-resolve is disabled for the account' do
+ before { account.update!(captain_disable_auto_resolve: true) }
+
+ it 'does not resolve and returns a disabled message' do
+ result = tool.perform(tool_context, reason: 'Possible spam')
+
+ expect(result).to eq('Auto-resolve is disabled for this account')
+ expect(conversation.reload).not_to be_resolved
+ end
+ end
+
describe 'resolving an already resolved conversation' do
let(:conversation) { create(:conversation, account: account, inbox: inbox, status: :resolved) }
diff --git a/spec/jobs/agent_bots/webhook_job_spec.rb b/spec/jobs/agent_bots/webhook_job_spec.rb
index 346d85e83..c14c46cb3 100644
--- a/spec/jobs/agent_bots/webhook_job_spec.rb
+++ b/spec/jobs/agent_bots/webhook_job_spec.rb
@@ -8,6 +8,16 @@ RSpec.describe AgentBots::WebhookJob do
let(:url) { 'https://test.com' }
let(:payload) { { name: 'test' } }
let(:webhook_type) { :agent_bot_webhook }
+ let(:retryable_error) { RestClient::InternalServerError.new(nil, 500) }
+
+ before do
+ ActiveJob::Base.queue_adapter = :test
+ end
+
+ after do
+ clear_enqueued_jobs
+ clear_performed_jobs
+ end
it 'queues the job' do
expect { job }.to have_enqueued_job(described_class)
@@ -19,4 +29,23 @@ RSpec.describe AgentBots::WebhookJob do
expect(Webhooks::Trigger).to receive(:execute).with(url, payload, webhook_type, secret: nil, delivery_id: nil)
perform_enqueued_jobs { job }
end
+
+ it 'configures retry handlers for 429 and 500 errors' do
+ handlers = described_class.rescue_handlers.map(&:first)
+
+ expect(handlers).to include('RestClient::TooManyRequests', 'RestClient::InternalServerError')
+ end
+
+ it 'retries 3 times and handles failure after retries are exhausted' do
+ allow(Webhooks::Trigger).to receive(:execute).and_raise(retryable_error)
+ trigger_instance = instance_double(Webhooks::Trigger, handle_failure: true)
+ allow(Webhooks::Trigger).to receive(:new).and_return(trigger_instance)
+ allow(Rails.logger).to receive(:warn)
+
+ expect(Webhooks::Trigger).to receive(:execute).exactly(3).times
+ expect(trigger_instance).to receive(:handle_failure).with(instance_of(RestClient::InternalServerError)).once
+ expect(Rails.logger).to receive(:warn).with(/AgentBots::WebhookJob/).exactly(3).times
+
+ perform_enqueued_jobs { job }
+ end
end
diff --git a/spec/lib/webhooks/trigger_spec.rb b/spec/lib/webhooks/trigger_spec.rb
index 1e047b557..90d1ce7f8 100644
--- a/spec/lib/webhooks/trigger_spec.rb
+++ b/spec/lib/webhooks/trigger_spec.rb
@@ -77,6 +77,40 @@ describe Webhooks::Trigger do
let!(:pending_conversation) { create(:conversation, inbox: inbox, status: :pending, account: account) }
let!(:pending_message) { create(:message, account: account, inbox: inbox, conversation: pending_conversation) }
+ it 'raises 500 errors for retry and does not reopen conversation immediately' do
+ payload = { event: 'message_created', id: pending_message.id }
+
+ expect(RestClient::Request).to receive(:execute)
+ .with(
+ method: :post,
+ url: url,
+ payload: payload.to_json,
+ headers: { content_type: :json, accept: :json },
+ timeout: webhook_timeout
+ ).and_raise(RestClient::InternalServerError.new(nil, 500)).once
+
+ expect { trigger.execute(url, payload, webhook_type) }.to raise_error(RestClient::InternalServerError)
+ expect(pending_conversation.reload.status).to eq('pending')
+ expect(Conversations::ActivityMessageJob).not_to have_been_enqueued
+ end
+
+ it 'raises 429 errors for retry and does not reopen conversation immediately' do
+ payload = { event: 'message_created', id: pending_message.id }
+
+ expect(RestClient::Request).to receive(:execute)
+ .with(
+ method: :post,
+ url: url,
+ payload: payload.to_json,
+ headers: { content_type: :json, accept: :json },
+ timeout: webhook_timeout
+ ).and_raise(RestClient::TooManyRequests.new(nil, 429)).once
+
+ expect { trigger.execute(url, payload, webhook_type) }.to raise_error(RestClient::TooManyRequests)
+ expect(pending_conversation.reload.status).to eq('pending')
+ expect(Conversations::ActivityMessageJob).not_to have_been_enqueued
+ end
+
it 'reopens conversation and enqueues activity message if pending' do
payload = { event: 'message_created', id: pending_message.id }
@@ -166,6 +200,22 @@ describe Webhooks::Trigger do
expect(activity_message.content).to eq(agent_bot_error_content)
end
end
+
+ it 'handles 500 without raising for non-agent webhooks' do
+ payload = { event: 'message_created', conversation: { id: conversation.id }, id: message.id }
+
+ expect(RestClient::Request).to receive(:execute)
+ .with(
+ method: :post,
+ url: url,
+ payload: payload.to_json,
+ headers: { content_type: :json, accept: :json },
+ timeout: webhook_timeout
+ ).and_raise(RestClient::InternalServerError.new(nil, 500)).once
+
+ expect { trigger.execute(url, payload, webhook_type) }.not_to raise_error
+ expect(message.reload.status).to eq('failed')
+ end
end
describe 'request headers' do
diff --git a/spec/services/facebook/send_on_facebook_service_spec.rb b/spec/services/facebook/send_on_facebook_service_spec.rb
index 4d5f9babd..f99b1c469 100644
--- a/spec/services/facebook/send_on_facebook_service_spec.rb
+++ b/spec/services/facebook/send_on_facebook_service_spec.rb
@@ -7,6 +7,7 @@ describe Facebook::SendOnFacebookService do
allow(Facebook::Messenger::Subscriptions).to receive(:subscribe).and_return(true)
allow(bot).to receive(:deliver).and_return({ recipient_id: '1008372609250235', message_id: 'mid.1456970487936:c34767dfe57ee6e339' }.to_json)
create(:message, message_type: :incoming, inbox: facebook_inbox, account: account, conversation: conversation)
+ GlobalConfig.clear_cache
end
let!(:account) { create(:account) }
@@ -90,6 +91,17 @@ describe Facebook::SendOnFacebookService do
}, { page_id: facebook_channel.page_id })
end
+ it 'sends with HUMAN_AGENT tag when ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT is enabled' do
+ with_modified_env ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT: 'true' do
+ message = create(:message, message_type: 'outgoing', inbox: facebook_inbox, account: account, conversation: conversation)
+ described_class.new(message: message).perform
+ expect(bot).to have_received(:deliver).with(
+ hash_including(tag: 'HUMAN_AGENT'),
+ { page_id: facebook_channel.page_id }
+ )
+ end
+ end
+
it 'if message is sent with multiple attachments' do
message = build(:message, content: nil, message_type: 'outgoing', inbox: facebook_inbox, account: account, conversation: conversation)
avatar = message.attachments.new(account_id: message.account_id, file_type: :image)