Files
chatwoot/spec/enterprise/lib/captain/base_task_service_spec.rb
Aakash BakhleandGitHub 45f4b423ae fix: captain usage for BYOK OpenAI tasks (#14587)
# 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
2026-06-08 13:37:21 +05:30

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