Initial commit: Add logistics and order_detail message types
Some checks failed
Lock Threads / action (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Run Linux nightly installer / nightly (push) Has been cancelled
Some checks failed
Lock Threads / action (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Run Linux nightly installer / nightly (push) Has been cancelled
- Add Logistics component with progress tracking - Add OrderDetail component for order information - Support data-driven steps and actions - Add blue color scale to widget SCSS - Fix node overflow and progress bar rendering issues - Add English translations for dashboard components Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
371
spec/enterprise/lib/captain/tools/http_tool_spec.rb
Normal file
371
spec/enterprise/lib/captain/tools/http_tool_spec.rb
Normal file
@@ -0,0 +1,371 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Tools::HttpTool, type: :model do
|
||||
let(:account) { create(:account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:custom_tool) { create(:captain_custom_tool, account: account) }
|
||||
let(:tool) { described_class.new(assistant, custom_tool) }
|
||||
let(:tool_context) { Struct.new(:state).new({}) }
|
||||
|
||||
describe '#active?' do
|
||||
it 'returns true when custom tool is enabled' do
|
||||
custom_tool.update!(enabled: true)
|
||||
|
||||
expect(tool.active?).to be true
|
||||
end
|
||||
|
||||
it 'returns false when custom tool is disabled' do
|
||||
custom_tool.update!(enabled: false)
|
||||
|
||||
expect(tool.active?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'with GET request' do
|
||||
before do
|
||||
custom_tool.update!(
|
||||
http_method: 'GET',
|
||||
endpoint_url: 'https://example.com/orders/123',
|
||||
response_template: nil
|
||||
)
|
||||
stub_request(:get, 'https://example.com/orders/123')
|
||||
.to_return(status: 200, body: '{"status": "success"}')
|
||||
end
|
||||
|
||||
it 'executes GET request and returns response body' do
|
||||
result = tool.perform(tool_context)
|
||||
|
||||
expect(result).to eq('{"status": "success"}')
|
||||
expect(WebMock).to have_requested(:get, 'https://example.com/orders/123')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with POST request' do
|
||||
before do
|
||||
custom_tool.update!(
|
||||
http_method: 'POST',
|
||||
endpoint_url: 'https://example.com/orders',
|
||||
request_template: '{"order_id": "{{ order_id }}"}',
|
||||
response_template: nil
|
||||
)
|
||||
stub_request(:post, 'https://example.com/orders')
|
||||
.with(body: '{"order_id": "123"}', headers: { 'Content-Type' => 'application/json' })
|
||||
.to_return(status: 200, body: '{"created": true}')
|
||||
end
|
||||
|
||||
it 'executes POST request with rendered body' do
|
||||
result = tool.perform(tool_context, order_id: '123')
|
||||
|
||||
expect(result).to eq('{"created": true}')
|
||||
expect(WebMock).to have_requested(:post, 'https://example.com/orders')
|
||||
.with(body: '{"order_id": "123"}')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with template variables in URL' do
|
||||
before do
|
||||
custom_tool.update!(
|
||||
endpoint_url: 'https://example.com/orders/{{ order_id }}',
|
||||
response_template: nil
|
||||
)
|
||||
stub_request(:get, 'https://example.com/orders/456')
|
||||
.to_return(status: 200, body: '{"order_id": "456"}')
|
||||
end
|
||||
|
||||
it 'renders URL template with params' do
|
||||
result = tool.perform(tool_context, order_id: '456')
|
||||
|
||||
expect(result).to eq('{"order_id": "456"}')
|
||||
expect(WebMock).to have_requested(:get, 'https://example.com/orders/456')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with bearer token authentication' do
|
||||
before do
|
||||
custom_tool.update!(
|
||||
auth_type: 'bearer',
|
||||
auth_config: { 'token' => 'secret_bearer_token' },
|
||||
endpoint_url: 'https://example.com/data',
|
||||
response_template: nil
|
||||
)
|
||||
stub_request(:get, 'https://example.com/data')
|
||||
.with(headers: { 'Authorization' => 'Bearer secret_bearer_token' })
|
||||
.to_return(status: 200, body: '{"authenticated": true}')
|
||||
end
|
||||
|
||||
it 'adds Authorization header with bearer token' do
|
||||
result = tool.perform(tool_context)
|
||||
|
||||
expect(result).to eq('{"authenticated": true}')
|
||||
expect(WebMock).to have_requested(:get, 'https://example.com/data')
|
||||
.with(headers: { 'Authorization' => 'Bearer secret_bearer_token' })
|
||||
end
|
||||
end
|
||||
|
||||
context 'with basic authentication' do
|
||||
before do
|
||||
custom_tool.update!(
|
||||
auth_type: 'basic',
|
||||
auth_config: { 'username' => 'user123', 'password' => 'pass456' },
|
||||
endpoint_url: 'https://example.com/data',
|
||||
response_template: nil
|
||||
)
|
||||
stub_request(:get, 'https://example.com/data')
|
||||
.with(basic_auth: %w[user123 pass456])
|
||||
.to_return(status: 200, body: '{"authenticated": true}')
|
||||
end
|
||||
|
||||
it 'adds basic auth credentials' do
|
||||
result = tool.perform(tool_context)
|
||||
|
||||
expect(result).to eq('{"authenticated": true}')
|
||||
expect(WebMock).to have_requested(:get, 'https://example.com/data')
|
||||
.with(basic_auth: %w[user123 pass456])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with API key authentication' do
|
||||
before do
|
||||
custom_tool.update!(
|
||||
auth_type: 'api_key',
|
||||
auth_config: { 'key' => 'api_key_123', 'location' => 'header', 'name' => 'X-API-Key' },
|
||||
endpoint_url: 'https://example.com/data',
|
||||
response_template: nil
|
||||
)
|
||||
stub_request(:get, 'https://example.com/data')
|
||||
.with(headers: { 'X-API-Key' => 'api_key_123' })
|
||||
.to_return(status: 200, body: '{"authenticated": true}')
|
||||
end
|
||||
|
||||
it 'adds API key header' do
|
||||
result = tool.perform(tool_context)
|
||||
|
||||
expect(result).to eq('{"authenticated": true}')
|
||||
expect(WebMock).to have_requested(:get, 'https://example.com/data')
|
||||
.with(headers: { 'X-API-Key' => 'api_key_123' })
|
||||
end
|
||||
end
|
||||
|
||||
context 'with response template' do
|
||||
before do
|
||||
custom_tool.update!(
|
||||
endpoint_url: 'https://example.com/orders/123',
|
||||
response_template: 'Order status: {{ response.status }}, ID: {{ response.order_id }}'
|
||||
)
|
||||
stub_request(:get, 'https://example.com/orders/123')
|
||||
.to_return(status: 200, body: '{"status": "shipped", "order_id": "123"}')
|
||||
end
|
||||
|
||||
it 'formats response using template' do
|
||||
result = tool.perform(tool_context)
|
||||
|
||||
expect(result).to eq('Order status: shipped, ID: 123')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when handling errors' do
|
||||
it 'returns generic error message on network failure' do
|
||||
custom_tool.update!(endpoint_url: 'https://example.com/data')
|
||||
stub_request(:get, 'https://example.com/data').to_raise(SocketError.new('Failed to connect'))
|
||||
|
||||
result = tool.perform(tool_context)
|
||||
|
||||
expect(result).to eq('An error occurred while executing the request')
|
||||
end
|
||||
|
||||
it 'returns generic error message on timeout' do
|
||||
custom_tool.update!(endpoint_url: 'https://example.com/data')
|
||||
stub_request(:get, 'https://example.com/data').to_timeout
|
||||
|
||||
result = tool.perform(tool_context)
|
||||
|
||||
expect(result).to eq('An error occurred while executing the request')
|
||||
end
|
||||
|
||||
it 'returns generic error message on HTTP 404' do
|
||||
custom_tool.update!(endpoint_url: 'https://example.com/data')
|
||||
stub_request(:get, 'https://example.com/data').to_return(status: 404, body: 'Not found')
|
||||
|
||||
result = tool.perform(tool_context)
|
||||
|
||||
expect(result).to eq('An error occurred while executing the request')
|
||||
end
|
||||
|
||||
it 'returns generic error message on HTTP 500' do
|
||||
custom_tool.update!(endpoint_url: 'https://example.com/data')
|
||||
stub_request(:get, 'https://example.com/data').to_return(status: 500, body: 'Server error')
|
||||
|
||||
result = tool.perform(tool_context)
|
||||
|
||||
expect(result).to eq('An error occurred while executing the request')
|
||||
end
|
||||
|
||||
it 'logs error details' do
|
||||
custom_tool.update!(endpoint_url: 'https://example.com/data')
|
||||
stub_request(:get, 'https://example.com/data').to_raise(StandardError.new('Test error'))
|
||||
|
||||
expect(Rails.logger).to receive(:error).with(/HttpTool execution error.*Test error/)
|
||||
|
||||
tool.perform(tool_context)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when integrating with Toolable methods' do
|
||||
it 'correctly integrates URL rendering, body rendering, auth, and response formatting' do
|
||||
custom_tool.update!(
|
||||
http_method: 'POST',
|
||||
endpoint_url: 'https://example.com/users/{{ user_id }}/orders',
|
||||
request_template: '{"product": "{{ product }}", "quantity": {{ quantity }}}',
|
||||
auth_type: 'bearer',
|
||||
auth_config: { 'token' => 'integration_token' },
|
||||
response_template: 'Created order #{{ response.order_number }} for {{ response.product }}'
|
||||
)
|
||||
|
||||
stub_request(:post, 'https://example.com/users/42/orders')
|
||||
.with(
|
||||
body: '{"product": "Widget", "quantity": 5}',
|
||||
headers: {
|
||||
'Authorization' => 'Bearer integration_token',
|
||||
'Content-Type' => 'application/json'
|
||||
}
|
||||
)
|
||||
.to_return(status: 200, body: '{"order_number": "ORD-789", "product": "Widget"}')
|
||||
|
||||
result = tool.perform(tool_context, user_id: '42', product: 'Widget', quantity: 5)
|
||||
|
||||
expect(result).to eq('Created order #ORD-789 for Widget')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with metadata headers' do
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
let(:contact) { conversation.contact }
|
||||
let(:tool_context_with_state) do
|
||||
Struct.new(:state).new({
|
||||
account_id: account.id,
|
||||
assistant_id: assistant.id,
|
||||
conversation: {
|
||||
id: conversation.id,
|
||||
display_id: conversation.display_id
|
||||
},
|
||||
contact: {
|
||||
id: contact.id,
|
||||
email: contact.email,
|
||||
phone_number: contact.phone_number
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
before do
|
||||
custom_tool.update!(
|
||||
endpoint_url: 'https://example.com/api/data',
|
||||
response_template: nil
|
||||
)
|
||||
end
|
||||
|
||||
it 'includes metadata headers in GET request' do
|
||||
stub_request(:get, 'https://example.com/api/data')
|
||||
.with(headers: {
|
||||
'X-Chatwoot-Account-Id' => account.id.to_s,
|
||||
'X-Chatwoot-Assistant-Id' => assistant.id.to_s,
|
||||
'X-Chatwoot-Tool-Slug' => custom_tool.slug,
|
||||
'X-Chatwoot-Conversation-Id' => conversation.id.to_s,
|
||||
'X-Chatwoot-Conversation-Display-Id' => conversation.display_id.to_s,
|
||||
'X-Chatwoot-Contact-Id' => contact.id.to_s,
|
||||
'X-Chatwoot-Contact-Email' => contact.email
|
||||
})
|
||||
.to_return(status: 200, body: '{"success": true}')
|
||||
|
||||
tool.perform(tool_context_with_state)
|
||||
|
||||
expect(WebMock).to have_requested(:get, 'https://example.com/api/data')
|
||||
.with(headers: {
|
||||
'X-Chatwoot-Account-Id' => account.id.to_s,
|
||||
'X-Chatwoot-Contact-Email' => contact.email
|
||||
})
|
||||
end
|
||||
|
||||
it 'includes metadata headers in POST request' do
|
||||
custom_tool.update!(http_method: 'POST', request_template: '{"data": "test"}')
|
||||
|
||||
stub_request(:post, 'https://example.com/api/data')
|
||||
.with(
|
||||
body: '{"data": "test"}',
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
'X-Chatwoot-Account-Id' => account.id.to_s,
|
||||
'X-Chatwoot-Tool-Slug' => custom_tool.slug,
|
||||
'X-Chatwoot-Contact-Email' => contact.email
|
||||
}
|
||||
)
|
||||
.to_return(status: 200, body: '{"success": true}')
|
||||
|
||||
tool.perform(tool_context_with_state)
|
||||
|
||||
expect(WebMock).to have_requested(:post, 'https://example.com/api/data')
|
||||
end
|
||||
|
||||
it 'includes metadata headers along with authentication headers' do
|
||||
custom_tool.update!(
|
||||
auth_type: 'bearer',
|
||||
auth_config: { 'token' => 'test_token' }
|
||||
)
|
||||
|
||||
stub_request(:get, 'https://example.com/api/data')
|
||||
.with(headers: {
|
||||
'Authorization' => 'Bearer test_token',
|
||||
'X-Chatwoot-Account-Id' => account.id.to_s,
|
||||
'X-Chatwoot-Contact-Id' => contact.id.to_s
|
||||
})
|
||||
.to_return(status: 200, body: '{"success": true}')
|
||||
|
||||
tool.perform(tool_context_with_state)
|
||||
|
||||
expect(WebMock).to have_requested(:get, 'https://example.com/api/data')
|
||||
.with(headers: {
|
||||
'Authorization' => 'Bearer test_token',
|
||||
'X-Chatwoot-Contact-Id' => contact.id.to_s
|
||||
})
|
||||
end
|
||||
|
||||
it 'handles missing contact in tool context' do
|
||||
tool_context_no_contact = Struct.new(:state).new({
|
||||
account_id: account.id,
|
||||
assistant_id: assistant.id,
|
||||
conversation: {
|
||||
id: conversation.id,
|
||||
display_id: conversation.display_id
|
||||
}
|
||||
})
|
||||
|
||||
stub_request(:get, 'https://example.com/api/data')
|
||||
.with(headers: {
|
||||
'X-Chatwoot-Account-Id' => account.id.to_s,
|
||||
'X-Chatwoot-Conversation-Id' => conversation.id.to_s
|
||||
})
|
||||
.to_return(status: 200, body: '{"success": true}')
|
||||
|
||||
tool.perform(tool_context_no_contact)
|
||||
|
||||
expect(WebMock).to have_requested(:get, 'https://example.com/api/data')
|
||||
end
|
||||
|
||||
it 'includes contact phone when present' do
|
||||
contact.update!(phone_number: '+1234567890')
|
||||
tool_context_with_state.state[:contact][:phone_number] = '+1234567890'
|
||||
|
||||
stub_request(:get, 'https://example.com/api/data')
|
||||
.with(headers: {
|
||||
'X-Chatwoot-Contact-Phone' => '+1234567890'
|
||||
})
|
||||
.to_return(status: 200, body: '{"success": true}')
|
||||
|
||||
tool.perform(tool_context_with_state)
|
||||
|
||||
expect(WebMock).to have_requested(:get, 'https://example.com/api/data')
|
||||
.with(headers: { 'X-Chatwoot-Contact-Phone' => '+1234567890' })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user