Merge branch 'develop' into feat/reporting-events-rollup

This commit is contained in:
Shivam Mishra
2026-02-23 17:55:15 +05:30
committed by GitHub
228 changed files with 5648 additions and 4818 deletions
@@ -631,6 +631,7 @@ RSpec.describe 'Inboxes API', type: :request do
it 'updates smtp configuration with starttls encryption' do
smtp_connection = double
allow(smtp_connection).to receive(:open_timeout=).and_return(10)
allow(smtp_connection).to receive(:start).and_return(true)
allow(smtp_connection).to receive(:finish).and_return(true)
allow(smtp_connection).to receive(:respond_to?).and_return(true)
@@ -661,6 +662,7 @@ RSpec.describe 'Inboxes API', type: :request do
it 'updates smtp configuration with ssl/tls encryption' do
smtp_connection = double
allow(smtp_connection).to receive(:open_timeout=).and_return(10)
allow(smtp_connection).to receive(:start).and_return(true)
allow(smtp_connection).to receive(:finish).and_return(true)
allow(smtp_connection).to receive(:respond_to?).and_return(true)
@@ -691,6 +693,7 @@ RSpec.describe 'Inboxes API', type: :request do
it 'updates smtp configuration with authentication mechanism' do
smtp_connection = double
allow(smtp_connection).to receive(:open_timeout=).and_return(10)
allow(smtp_connection).to receive(:start).and_return(true)
allow(smtp_connection).to receive(:finish).and_return(true)
allow(smtp_connection).to receive(:respond_to?).and_return(true)
@@ -357,10 +357,32 @@ RSpec.describe 'Enterprise Billing APIs', type: :request do
context 'when it is an admin' do
before do
# Create the installation config for cloud environment
InstallationConfig.where(name: 'DEPLOYMENT_ENV').first_or_create(value: 'cloud')
InstallationConfig.where(name: 'DEPLOYMENT_ENV').first_or_initialize.update!(value: 'cloud')
end
it 'marks the account for deletion when action is delete' do
cancellation_service = instance_double(Enterprise::Billing::CancelCloudSubscriptionsService, perform: true)
allow(Enterprise::Billing::CancelCloudSubscriptionsService).to receive(:new).with(account: account)
.and_return(cancellation_service)
post "/enterprise/api/v1/accounts/#{account.id}/toggle_deletion",
headers: admin.create_new_auth_token,
params: { action_type: 'delete' },
as: :json
expect(response).to have_http_status(:ok)
expect(account.reload.custom_attributes['marked_for_deletion_at']).to be_present
expect(account.custom_attributes['marked_for_deletion_reason']).to eq('manual_deletion')
expect(Enterprise::Billing::CancelCloudSubscriptionsService).to have_received(:new).with(account: account)
expect(cancellation_service).to have_received(:perform)
end
it 'returns success even if stripe cancellation fails' do
cancellation_service = instance_double(Enterprise::Billing::CancelCloudSubscriptionsService)
allow(Enterprise::Billing::CancelCloudSubscriptionsService).to receive(:new).with(account: account)
.and_return(cancellation_service)
allow(cancellation_service).to receive(:perform).and_raise(Stripe::APIError.new('stripe unavailable'))
post "/enterprise/api/v1/accounts/#{account.id}/toggle_deletion",
headers: admin.create_new_auth_token,
params: { action_type: 'delete' },
@@ -0,0 +1,52 @@
require 'rails_helper'
RSpec.describe Captain::Tools::ResolveConversationTool do
let(:account) { create(:account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:inbox) { create(:inbox, account: account) }
let(:conversation) { create(:conversation, account: account, inbox: inbox, status: :open) }
let(:tool) { described_class.new(assistant) }
let(:tool_context) { Struct.new(:state).new({ conversation: { id: conversation.id } }) }
before do
Current.executed_by = assistant
end
after do
Current.reset
end
describe 'resolving a conversation' do
it 'marks resolved and enqueues an activity message with the reason' do
tool.perform(tool_context, reason: 'Possible spam')
expect(conversation.reload).to be_resolved
expect(Conversations::ActivityMessageJob).to have_been_enqueued.with(
conversation,
hash_including(
content: I18n.t('conversations.activity.captain.resolved_by_tool', user_name: assistant.name, reason: 'Possible spam')
)
)
end
it 'clears captain_resolve_reason after execution' do
tool.perform(tool_context, reason: 'Possible spam')
expect(Current.captain_resolve_reason).to be_nil
end
end
describe 'resolving an already resolved conversation' do
let(:conversation) { create(:conversation, account: account, inbox: inbox, status: :resolved) }
it 'does not re-resolve and returns an already resolved message' do
queue_adapter = ActiveJob::Base.queue_adapter
queue_adapter.enqueued_jobs.clear
result = tool.perform(tool_context, reason: 'Possible spam')
expect(result).to include('already resolved')
expect(Conversations::ActivityMessageJob).not_to have_been_enqueued
end
end
end
@@ -278,6 +278,7 @@ RSpec.describe Captain::Assistant::AgentRunnerService do
contact_id: contact.id,
status: conversation.status
)
expect(state[:channel_type]).to eq(inbox.channel_type)
end
it 'includes contact attributes when contact is present' do
@@ -28,6 +28,19 @@ RSpec.describe Captain::Llm::AssistantChatService do
allow(mock_chat).to receive(:messages).and_return([])
end
describe 'instrumentation metadata' do
it 'passes channel_type to the agent session instrumentation' do
service = described_class.new(assistant: assistant, conversation_id: conversation.display_id)
expect(service).to receive(:instrument_agent_session).with(
hash_including(metadata: hash_including(channel_type: conversation.inbox.channel_type))
).and_yield
allow(mock_chat).to receive(:ask).and_return(mock_response)
service.generate_response(message_history: [{ role: 'user', content: 'Hello' }])
end
end
describe 'image analysis' do
context 'when user sends a message with an image attachment' do
let(:message_history) do
@@ -0,0 +1,51 @@
require 'rails_helper'
RSpec.describe Enterprise::Billing::CancelCloudSubscriptionsService do
subject(:service) { described_class.new(account: account) }
let(:account) { create(:account, custom_attributes: custom_attributes) }
let(:custom_attributes) { { 'stripe_customer_id' => 'cus_123' } }
describe '#perform' do
context 'when deployment is not cloud' do
it 'does not call stripe subscriptions api' do
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(false)
allow(Stripe::Subscription).to receive(:list)
service.perform
expect(Stripe::Subscription).not_to have_received(:list)
end
end
context 'when stripe customer id is missing' do
let(:custom_attributes) { {} }
it 'does not call stripe subscriptions api' do
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
allow(Stripe::Subscription).to receive(:list)
service.perform
expect(Stripe::Subscription).not_to have_received(:list)
end
end
context 'when account is cloud with active subscriptions' do
let(:subscription_response) { Struct.new(:data).new([sub_1, sub_2]) }
let(:sub_1) { instance_double(Stripe::Subscription, id: 'sub_1', cancel_at_period_end: false) }
let(:sub_2) { instance_double(Stripe::Subscription, id: 'sub_2', cancel_at_period_end: true) }
it 'marks only active subscriptions that are not yet set to cancel at period end' do
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
allow(Stripe::Subscription).to receive(:list).and_return(subscription_response)
allow(Stripe::Subscription).to receive(:update)
service.perform
expect(Stripe::Subscription).to have_received(:list).with(customer: 'cus_123', status: 'active', limit: 100)
expect(Stripe::Subscription).to have_received(:update).with('sub_1', cancel_at_period_end: true).once
end
end
end
end
@@ -44,17 +44,19 @@ describe Enterprise::Billing::TopupCheckoutService do
end
it 'raises error for invalid credits' do
expect do
service.create_checkout_session(credits: 500)
end.to raise_error(Enterprise::Billing::TopupCheckoutService::Error)
expect { service.create_checkout_session(credits: 500) }.to raise_error do |error|
expect(error.class.name).to eq('Enterprise::Billing::TopupCheckoutService::Error')
expect(error.message).to eq(I18n.t('errors.topup.invalid_option'))
end
end
it 'raises error when account is on free plan' do
account.update!(custom_attributes: { plan_name: 'Hacker', stripe_customer_id: stripe_customer_id })
expect do
service.create_checkout_session(credits: 1000)
end.to raise_error(Enterprise::Billing::TopupCheckoutService::Error)
expect { service.create_checkout_session(credits: 1000) }.to raise_error do |error|
expect(error.class.name).to eq('Enterprise::Billing::TopupCheckoutService::Error')
expect(error.message).to eq(I18n.t('errors.topup.plan_not_eligible'))
end
end
end
end
+26
View File
@@ -190,6 +190,32 @@ describe ConversationFinder do
end
end
context 'with perform_meta_only' do
let(:params) { { assignee_type: 'assigned' } }
it 'returns only count without conversations' do
result = conversation_finder.perform_meta_only
expect(result).to have_key(:count)
expect(result).not_to have_key(:conversations)
end
it 'returns the correct counts' do
result = conversation_finder.perform_meta_only
expect(result[:count]).to eq({
mine_count: 2,
assigned_count: 3,
unassigned_count: 1,
all_count: 4
})
end
it 'returns same counts as perform' do
meta_result = conversation_finder.perform_meta_only
full_result = conversation_finder.perform
expect(meta_result[:count]).to eq(full_result[:count])
end
end
context 'with unattended' do
let(:params) { { status: 'open', assignee_type: 'me', conversation_type: 'unattended' } }
+19 -3
View File
@@ -142,7 +142,24 @@ describe NotificationListener do
expect(first_agent.notifications.first.notification_type).to eq('conversation_mention')
end
it 'will not create duplicate new message notifications for assignment & participation' do
it 'will create a mention notification when a user is mentioned in a private note' do
create(:inbox_member, user: first_agent, inbox: inbox)
message = build(
:message,
conversation: conversation,
account: account,
content: "hey [#{first_agent.name}](mention://user/#{first_agent.id}/#{first_agent.name})",
private: true
)
event = Events::Base.new(event_name, Time.zone.now, message: message)
listener.message_created(event)
expect(first_agent.notifications.count).to eq(1)
expect(first_agent.notifications.first.notification_type).to eq('conversation_mention')
end
it 'will not create new message notifications for private messages without mentions' do
create(:inbox_member, user: first_agent, inbox: inbox)
conversation.update(assignee: first_agent)
# participants is created by async job. so creating it directly for testcase
@@ -160,8 +177,7 @@ describe NotificationListener do
listener.message_created(event)
expect(conversation.conversation_participants.map(&:user)).to include(first_agent)
expect(first_agent.notifications.count).to eq(1)
expect(first_agent.notifications.first.notification_type).to eq('assigned_conversation_new_message')
expect(first_agent.notifications.count).to eq(0)
end
end
@@ -61,7 +61,7 @@ RSpec.describe AutoAssignment::RateLimiter do
it 'still tracks the assignment with default window' do
expected_key = format(Redis::RedisKeys::ASSIGNMENT_KEY, inbox_id: inbox.id, agent_id: agent.id, conversation_id: conversation.id)
expect(Redis::Alfred).to receive(:set).with(expected_key, conversation.id.to_s, ex: 24.hours.to_i)
expect(Redis::Alfred).to receive(:set).with(expected_key, conversation.id.to_s, ex: 5.minutes.to_i)
rate_limiter.track_assignment(conversation)
end
end
@@ -154,12 +154,12 @@ RSpec.describe AutoAssignment::RateLimiter do
allow(inbox).to receive(:assignment_policy).and_return(assignment_policy)
end
it 'uses the default window value of 24 hours' do
it 'uses the default window value of 5 minutes' do
expected_key = format(Redis::RedisKeys::ASSIGNMENT_KEY, inbox_id: inbox.id, agent_id: agent.id, conversation_id: conversation.id)
expect(Redis::Alfred).to receive(:set).with(
expected_key,
conversation.id.to_s,
ex: 86_400
ex: 5.minutes.to_i
)
rate_limiter.track_assignment(conversation)
end
@@ -2,11 +2,17 @@ require 'rails_helper'
describe Messages::NewMessageNotificationService do
context 'when message is not notifiable' do
it 'will not create any notifications' do
it 'will not create any notifications for activity messages' do
message = build(:message, message_type: :activity)
expect(NotificationBuilder).not_to receive(:new)
described_class.new(message: message).perform
end
it 'will not create any notifications for private messages' do
message = build(:message, message_type: :outgoing, private: true)
expect(NotificationBuilder).not_to receive(:new)
described_class.new(message: message).perform
end
end
context 'when message is notifiable' do