fix(captain): keep responding to pending conversations on audience or schedule drift

This commit is contained in:
Pranav
2026-07-16 20:38:27 -07:00
parent 304777e728
commit 5029b25d5c
3 changed files with 102 additions and 23 deletions
@@ -1,11 +1,11 @@
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { createStore } from 'vuex';
import { useRoute } from 'vue-router';
import InboxChannelsDialog from '../../inbox-setup/InboxChannelsDialog.vue';
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: key => key }) }));
vi.mock('dashboard/composables/store', () => ({
useMapGetter: () => ({ value: {} }),
}));
vi.mock('vue-router');
vi.mock('../../inbox-setup/useChannelConnect', () => ({
useChannelConnect: () => ({
connectViaOAuth: vi.fn(),
@@ -13,10 +13,30 @@ vi.mock('../../inbox-setup/useChannelConnect', () => ({
}),
}));
const store = createStore({
modules: {
globalConfig: {
namespaced: true,
getters: {
get: () => ({}),
isOnChatwootCloud: () => false,
},
},
accounts: {
namespaced: true,
getters: {
getAccount: () => () => ({ id: 1, features: {} }),
isFeatureEnabledonAccount: () => () => false,
},
},
},
});
const mountDialog = () =>
mount(InboxChannelsDialog, {
props: { inboxes: [] },
global: {
plugins: [store],
stubs: {
Dialog: {
template: '<div><slot /></div>',
@@ -31,6 +51,10 @@ const mountDialog = () =>
});
describe('InboxChannelsDialog Facebook gating', () => {
beforeEach(() => {
useRoute.mockReturnValue({ params: { accountId: '1' } });
});
afterEach(() => {
delete window.chatwootConfig;
});
@@ -50,7 +50,7 @@ module Enterprise::MessageTemplates::HookExecutionService
end
def should_process_captain_response?
conversation.pending? && message.incoming? && captain_should_engage?
conversation.pending? && message.incoming? && captain_assistant_configured?
end
def perform_handoff
@@ -76,11 +76,10 @@ module Enterprise::MessageTemplates::HookExecutionService
end
def captain_handling_conversation?
conversation.pending? && captain_should_engage?
conversation.pending? && captain_assistant_configured?
end
def captain_should_engage?
assistant = inbox.captain_assistant
assistant.present? && assistant.engages?(conversation.contact, conversation)
def captain_assistant_configured?
inbox.captain_assistant.present?
end
end
@@ -126,21 +126,6 @@ RSpec.describe MessageTemplates::HookExecutionService do
end
end
context 'when the contact is outside the assistant audience' do
before do
assistant.update!(config: assistant.config.merge('audience' => {
'attribute_key' => 'country_code', 'filter_operator' => 'equal_to', 'values' => ['US']
}))
contact.update!(additional_attributes: { 'country_code' => 'CA' })
end
it 'does not schedule captain response job' do
expect(Captain::Conversation::ResponseBuilderJob).not_to receive(:perform_later)
create(:message, conversation: conversation, message_type: :incoming, account: account)
end
end
context 'when the contact is inside the assistant audience' do
before do
assistant.update!(config: assistant.config.merge('audience' => {
@@ -156,6 +141,77 @@ RSpec.describe MessageTemplates::HookExecutionService do
end
end
context 'when the conversation stops matching the audience mid-conversation' do
it 'still schedules captain response job for the pending conversation' do
conversation
assistant.update!(config: assistant.config.merge('audience' => {
'attribute_key' => 'country_code', 'filter_operator' => 'equal_to', 'values' => ['US']
}))
contact.update!(additional_attributes: { 'country_code' => 'CA' })
expect(Captain::Conversation::ResponseBuilderJob).to receive(:perform_later).with(conversation, assistant)
create(:message, conversation: conversation, message_type: :incoming, account: account)
end
end
context 'when the reply schedule stops matching mid-conversation' do
before do
inbox.update!(working_hours_enabled: true)
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
open_all_day: true,
closed_all_day: false
)
end
it 'still schedules captain response job when business hours end after captain took the conversation' do
assistant.update!(config: assistant.config.merge('response_window' => 'business_hours'))
conversation
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
open_all_day: false,
closed_all_day: true
)
expect(Captain::Conversation::ResponseBuilderJob).to receive(:perform_later).with(conversation, assistant)
create(:message, conversation: conversation, message_type: :incoming, account: account)
end
it 'still schedules captain response job when business hours begin after captain took the conversation' do
assistant.update!(config: assistant.config.merge('response_window' => 'outside_business_hours'))
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
open_all_day: false,
closed_all_day: true
)
conversation
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
open_all_day: true,
closed_all_day: false
)
expect(Captain::Conversation::ResponseBuilderJob).to receive(:perform_later).with(conversation, assistant)
create(:message, conversation: conversation, message_type: :incoming, account: account)
end
it 'still schedules captain response job when both audience and schedule stop matching' do
assistant.update!(config: assistant.config.merge('response_window' => 'business_hours'))
conversation
assistant.update!(config: assistant.config.merge('audience' => {
'attribute_key' => 'country_code', 'filter_operator' => 'equal_to', 'values' => ['US']
}))
contact.update!(additional_attributes: { 'country_code' => 'CA' })
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
open_all_day: false,
closed_all_day: true
)
expect(Captain::Conversation::ResponseBuilderJob).to receive(:perform_later).with(conversation, assistant)
create(:message, conversation: conversation, message_type: :incoming, account: account)
end
end
context 'when message is outgoing' do
it 'does not schedule captain response job' do
expect(Captain::Conversation::ResponseBuilderJob).not_to receive(:perform_later)