# Pull Request Template ## Description Fixes: https://linear.app/chatwoot/issue/CW-7167/label-suggestions-bad-ux ## Type of change Please delete options that are not relevant. - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. ## Checklist: - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules
265 lines
8.6 KiB
Ruby
265 lines
8.6 KiB
Ruby
require 'rails_helper'
|
|
|
|
RSpec.describe Captain::BaseTaskService, type: :model do
|
|
let(:account) { create(:account) }
|
|
let(:inbox) { create(:inbox, account: account) }
|
|
let(:conversation) { create(:conversation, account: account, inbox: inbox) }
|
|
let(:perform_result) { { message: 'Test response' } }
|
|
|
|
# Create a concrete test service class with enterprise module prepended
|
|
let(:test_service_class) do
|
|
result = perform_result
|
|
klass = Class.new(described_class) do
|
|
define_method(:perform) { result }
|
|
|
|
def event_name
|
|
'test_event'
|
|
end
|
|
end
|
|
# Manually prepend enterprise module to test class
|
|
klass.prepend(Enterprise::Captain::BaseTaskService)
|
|
klass
|
|
end
|
|
|
|
let(:service) { test_service_class.new(account: account, conversation_display_id: conversation.display_id) }
|
|
|
|
before do
|
|
create(:installation_config, name: 'CAPTAIN_OPEN_AI_API_KEY', value: 'test-key')
|
|
end
|
|
|
|
describe '#perform with enterprise usage tracking' do
|
|
# Ensure captain is enabled by default for tests unless explicitly testing disabled state
|
|
before do
|
|
allow(account).to receive(:feature_enabled?).and_call_original
|
|
allow(account).to receive(:feature_enabled?).with('captain_tasks').and_return(true)
|
|
allow(Integrations::Openai::KeyValidator).to receive(:valid?).and_return(true)
|
|
end
|
|
|
|
context 'when usage limit is exceeded' do
|
|
before do
|
|
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
|
|
allow(account).to receive(:usage_limits).and_return({
|
|
captain: { responses: { current_available: 0 } }
|
|
})
|
|
end
|
|
|
|
it 'returns usage limit exceeded error' do
|
|
result = service.perform
|
|
expect(result[:error]).to eq(I18n.t('captain.copilot_limit'))
|
|
expect(result[:error_code]).to eq(429)
|
|
end
|
|
|
|
it 'does not increment usage' do
|
|
expect(account).not_to receive(:increment_response_usage)
|
|
service.perform
|
|
end
|
|
end
|
|
|
|
it 'increments response usage on successful execution' do
|
|
expect(account).to receive(:increment_response_usage)
|
|
service.perform
|
|
end
|
|
|
|
context 'when result has an error' do
|
|
let(:perform_result) { { error: 'API Error' } }
|
|
|
|
it 'does not increment usage' do
|
|
expect(account).not_to receive(:increment_response_usage)
|
|
service.perform
|
|
end
|
|
end
|
|
|
|
context 'when result is nil' do
|
|
let(:perform_result) { nil }
|
|
|
|
it 'does not increment usage' do
|
|
expect(account).not_to receive(:increment_response_usage)
|
|
service.perform
|
|
end
|
|
end
|
|
|
|
context 'when result is empty hash' do
|
|
let(:perform_result) { {} }
|
|
|
|
it 'does not increment usage' do
|
|
expect(account).not_to receive(:increment_response_usage)
|
|
service.perform
|
|
end
|
|
end
|
|
|
|
context 'when result has blank message' do
|
|
let(:perform_result) { { message: '' } }
|
|
|
|
it 'does not increment usage' do
|
|
expect(account).not_to receive(:increment_response_usage)
|
|
service.perform
|
|
end
|
|
end
|
|
|
|
context 'when result has nil message' do
|
|
let(:perform_result) { { message: nil } }
|
|
|
|
it 'does not increment usage' do
|
|
expect(account).not_to receive(:increment_response_usage)
|
|
service.perform
|
|
end
|
|
end
|
|
|
|
it 'actually increments the usage counter in custom_attributes' do
|
|
expect do
|
|
service.perform
|
|
account.reload
|
|
end.to change { account.custom_attributes['captain_responses_usage'].to_i }.by(1)
|
|
end
|
|
|
|
context 'when account has its own OpenAI hook key' do
|
|
before do
|
|
create(:integrations_hook, :openai, account: account, settings: { 'api_key' => 'customer-own-key' })
|
|
end
|
|
|
|
it 'still increments usage for services that do not opt into BYOK' do
|
|
expect(account).to receive(:increment_response_usage)
|
|
service.perform
|
|
end
|
|
|
|
context 'when the captain_responses quota is exhausted on Cloud' do
|
|
before do
|
|
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
|
|
allow(account).to receive(:usage_limits).and_return({
|
|
captain: { responses: { current_available: 0 } }
|
|
})
|
|
end
|
|
|
|
it 'returns usage limit exceeded error for services that do not opt into BYOK' do
|
|
result = service.perform
|
|
expect(result[:error]).to eq(I18n.t('captain.copilot_limit'))
|
|
expect(result[:error_code]).to eq(429)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when subclass opts into account OpenAI hook usage' do
|
|
let(:test_service_class) do
|
|
result = perform_result
|
|
klass = Class.new(described_class) do
|
|
define_method(:perform) { result }
|
|
define_method(:event_name) { 'test_event' }
|
|
define_method(:use_account_openai_hook?) { true }
|
|
end
|
|
klass.prepend(Enterprise::Captain::BaseTaskService)
|
|
klass
|
|
end
|
|
|
|
before do
|
|
create(:integrations_hook, :openai, account: account, settings: { 'api_key' => 'customer-own-key' })
|
|
end
|
|
|
|
it 'does not increment usage on a successful result' do
|
|
expect(account).not_to receive(:increment_response_usage)
|
|
service.perform
|
|
end
|
|
|
|
context 'when the captain_responses quota is exhausted on Cloud' do
|
|
before do
|
|
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
|
|
allow(account).to receive(:usage_limits).and_return({
|
|
captain: { responses: { current_available: 0 } }
|
|
})
|
|
end
|
|
|
|
it 'bypasses the 429 gate and returns the underlying result' do
|
|
result = service.perform
|
|
expect(result).to eq(perform_result)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when captain is disabled' do
|
|
before do
|
|
allow(account).to receive(:feature_enabled?).with('captain_tasks').and_return(false)
|
|
end
|
|
|
|
context 'when on Chatwoot Cloud' do
|
|
before do
|
|
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
|
|
end
|
|
|
|
it 'returns upgrade error message' do
|
|
result = service.perform
|
|
expect(result[:error]).to eq(I18n.t('captain.upgrade'))
|
|
end
|
|
|
|
it 'does not increment usage' do
|
|
expect(account).not_to receive(:increment_response_usage)
|
|
service.perform
|
|
end
|
|
end
|
|
|
|
context 'when self-hosted' do
|
|
before do
|
|
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(false)
|
|
end
|
|
|
|
it 'returns disabled error message' do
|
|
result = service.perform
|
|
expect(result[:error]).to eq(I18n.t('captain.disabled'))
|
|
end
|
|
|
|
it 'does not increment usage' do
|
|
expect(account).not_to receive(:increment_response_usage)
|
|
service.perform
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when captain is enabled' do
|
|
before do
|
|
allow(account).to receive(:feature_enabled?).with('captain_tasks').and_return(true)
|
|
end
|
|
|
|
it 'proceeds with the task' do
|
|
result = service.perform
|
|
expect(result[:message]).to eq('Test response')
|
|
expect(result[:error]).to be_nil
|
|
end
|
|
|
|
it 'increments usage' do
|
|
expect(account).to receive(:increment_response_usage)
|
|
service.perform
|
|
end
|
|
end
|
|
|
|
context 'when subclass opts out via counts_toward_usage?' do
|
|
let(:test_service_class) do
|
|
result = perform_result
|
|
klass = Class.new(described_class) do
|
|
define_method(:perform) { result }
|
|
define_method(:event_name) { 'test_event' }
|
|
define_method(:counts_toward_usage?) { false }
|
|
end
|
|
klass.prepend(Enterprise::Captain::BaseTaskService)
|
|
klass
|
|
end
|
|
|
|
it 'does not increment usage even on a successful result' do
|
|
expect(account).not_to receive(:increment_response_usage)
|
|
service.perform
|
|
end
|
|
|
|
context 'when the captain_responses quota is exhausted on Cloud' do
|
|
before do
|
|
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
|
|
allow(account).to receive(:usage_limits).and_return({
|
|
captain: { responses: { current_available: 0 } }
|
|
})
|
|
end
|
|
|
|
it 'bypasses the 429 gate and returns the underlying result' do
|
|
result = service.perform
|
|
expect(result).to eq(perform_result)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|