Files
chatwoot/spec/enterprise/lib/captain/base_task_service_spec.rb
Shivam MishraandGitHub 3489298726 feat: add WidgetCreationService for onboarding web widget setup (#14314)
When a new account finishes onboarding we want to land them on a
dashboard with a working web widget already configured, branded, named,
and assigned to them, instead of an empty inbox list. This PR adds the
services that produce that widget. **No user-visible change yet:** the
services are dormant until the trigger and background job are wired up
in the follow-up PR.

## Context

Milestone 1 added `Account::BrandingEnrichmentJob`, which calls
context.dev during signup and stores brand data on
`account.custom_attributes['brand_info']`, plus the new onboarding form
that captures `domain`, `name`, `industry`, etc. Milestone 2 starts
using that data, and the first thing we want is a web widget
materialized automatically. Splitting the service layer from the
orchestration plumbing (Redis key, `onboarding_step` extension,
controller wiring, ActionCable) keeps this diff focused and lets the
LLM/widget logic merge independently.

## How to test

Run against an existing account that already has `brand_info` populated.

```ruby
account = Account.find(<account_id>)
user    = account.administrators.first
inbox   = WidgetCreationService.new(account, user).perform

inbox.channel.widget_color     # color from brand_info, or '#1f93ff'
inbox.channel.welcome_title    # brand_info[:title], or account.name
inbox.channel.welcome_tagline  # LLM tagline (Enterprise + system key set),
                               # else brand_info[:slogan]/[:description]/nil
inbox.inbox_members.pluck(:user_id)
```

Toggle `InstallationConfig['CAPTAIN_OPEN_AI_API_KEY']` to flip between
LLM and brand-text tagline paths. To verify failure isolation, raise
inside `Captain::Llm::WidgetTaglineService#perform` and confirm widget
creation still succeeds with the fallback tagline.
2026-05-11 16:10:48 +05:30

202 lines
6.2 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)
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 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