Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Tools::AddContactNoteTool, type: :model do
|
||||
let(:account) { create(:account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:tool) { described_class.new(assistant) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:contact) { create(:contact, account: account) }
|
||||
let(:conversation) { create(:conversation, account: account, inbox: inbox, contact: contact) }
|
||||
let(:tool_context) { Struct.new(:state).new({ contact: { id: contact.id } }) }
|
||||
|
||||
describe '#description' do
|
||||
it 'returns the correct description' do
|
||||
expect(tool.description).to eq('Add a note to a contact profile')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parameters' do
|
||||
it 'returns the correct parameters' do
|
||||
expect(tool.parameters).to have_key(:note)
|
||||
expect(tool.parameters[:note].name).to eq(:note)
|
||||
expect(tool.parameters[:note].type).to eq('string')
|
||||
expect(tool.parameters[:note].description).to eq('The note content to add to the contact')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'when contact exists' do
|
||||
context 'with valid note content' do
|
||||
it 'creates a contact note and returns success message' do
|
||||
note_content = 'This is a contact note'
|
||||
|
||||
expect do
|
||||
result = tool.perform(tool_context, note: note_content)
|
||||
expect(result).to eq("Note added successfully to contact #{contact.name} (ID: #{contact.id})")
|
||||
end.to change(Note, :count).by(1)
|
||||
|
||||
created_note = Note.last
|
||||
expect(created_note.content).to eq(note_content)
|
||||
expect(created_note.account).to eq(account)
|
||||
expect(created_note.contact).to eq(contact)
|
||||
expect(created_note.user).to eq(assistant.account.users.first)
|
||||
end
|
||||
|
||||
it 'logs tool usage' do
|
||||
expect(tool).to receive(:log_tool_usage).with(
|
||||
'add_contact_note',
|
||||
{ contact_id: contact.id, note_length: 19 }
|
||||
)
|
||||
|
||||
tool.perform(tool_context, note: 'This is a test note')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with blank note content' do
|
||||
it 'returns error message' do
|
||||
result = tool.perform(tool_context, note: '')
|
||||
expect(result).to eq('Note content is required')
|
||||
end
|
||||
|
||||
it 'does not create a note' do
|
||||
expect do
|
||||
tool.perform(tool_context, note: '')
|
||||
end.not_to change(Note, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with nil note content' do
|
||||
it 'returns error message' do
|
||||
result = tool.perform(tool_context, note: nil)
|
||||
expect(result).to eq('Note content is required')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when contact does not exist' do
|
||||
let(:tool_context) { Struct.new(:state).new({ contact: { id: 999_999 } }) }
|
||||
|
||||
it 'returns error message' do
|
||||
result = tool.perform(tool_context, note: 'Some note')
|
||||
expect(result).to eq('Contact not found')
|
||||
end
|
||||
|
||||
it 'does not create a note' do
|
||||
expect do
|
||||
tool.perform(tool_context, note: 'Some note')
|
||||
end.not_to change(Note, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when contact state is missing' do
|
||||
let(:tool_context) { Struct.new(:state).new({}) }
|
||||
|
||||
it 'returns error message' do
|
||||
result = tool.perform(tool_context, note: 'Some note')
|
||||
expect(result).to eq('Contact not found')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when contact id is nil' do
|
||||
let(:tool_context) { Struct.new(:state).new({ contact: { id: nil } }) }
|
||||
|
||||
it 'returns error message' do
|
||||
result = tool.perform(tool_context, note: 'Some note')
|
||||
expect(result).to eq('Contact not found')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#active?' do
|
||||
it 'returns true for public tools' do
|
||||
expect(tool.active?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,125 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Tools::AddLabelToConversationTool, type: :model do
|
||||
let(:account) { create(:account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:tool) { described_class.new(assistant) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:contact) { create(:contact, account: account) }
|
||||
let(:conversation) { create(:conversation, account: account, inbox: inbox, contact: contact) }
|
||||
let(:label) { create(:label, account: account, title: 'urgent') }
|
||||
let(:tool_context) { Struct.new(:state).new({ conversation: { id: conversation.id } }) }
|
||||
|
||||
describe '#description' do
|
||||
it 'returns the correct description' do
|
||||
expect(tool.description).to eq('Add a label to a conversation')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parameters' do
|
||||
it 'returns the correct parameters' do
|
||||
expect(tool.parameters).to have_key(:label_name)
|
||||
expect(tool.parameters[:label_name].name).to eq(:label_name)
|
||||
expect(tool.parameters[:label_name].type).to eq('string')
|
||||
expect(tool.parameters[:label_name].description).to eq('The name of the label to add')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'when conversation exists' do
|
||||
context 'with valid label that exists' do
|
||||
before { label }
|
||||
|
||||
it 'adds label to conversation and returns success message' do
|
||||
result = tool.perform(tool_context, label_name: 'urgent')
|
||||
expect(result).to eq("Label 'urgent' added to conversation ##{conversation.display_id}")
|
||||
|
||||
expect(conversation.reload.label_list).to include('urgent')
|
||||
end
|
||||
|
||||
it 'logs tool usage' do
|
||||
expect(tool).to receive(:log_tool_usage).with(
|
||||
'added_label',
|
||||
{ conversation_id: conversation.id, label: 'urgent' }
|
||||
)
|
||||
|
||||
tool.perform(tool_context, label_name: 'urgent')
|
||||
end
|
||||
|
||||
it 'handles case insensitive label names' do
|
||||
result = tool.perform(tool_context, label_name: 'URGENT')
|
||||
expect(result).to eq("Label 'urgent' added to conversation ##{conversation.display_id}")
|
||||
end
|
||||
|
||||
it 'strips whitespace from label names' do
|
||||
result = tool.perform(tool_context, label_name: ' urgent ')
|
||||
expect(result).to eq("Label 'urgent' added to conversation ##{conversation.display_id}")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with label that does not exist' do
|
||||
it 'returns error message' do
|
||||
result = tool.perform(tool_context, label_name: 'nonexistent')
|
||||
expect(result).to eq('Label not found')
|
||||
end
|
||||
|
||||
it 'does not add any labels to conversation' do
|
||||
expect do
|
||||
tool.perform(tool_context, label_name: 'nonexistent')
|
||||
end.not_to(change { conversation.reload.labels.count })
|
||||
end
|
||||
end
|
||||
|
||||
context 'with blank label name' do
|
||||
it 'returns error message for empty string' do
|
||||
result = tool.perform(tool_context, label_name: '')
|
||||
expect(result).to eq('Label name is required')
|
||||
end
|
||||
|
||||
it 'returns error message for nil' do
|
||||
result = tool.perform(tool_context, label_name: nil)
|
||||
expect(result).to eq('Label name is required')
|
||||
end
|
||||
|
||||
it 'returns error message for whitespace only' do
|
||||
result = tool.perform(tool_context, label_name: ' ')
|
||||
expect(result).to eq('Label name is required')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation does not exist' do
|
||||
let(:tool_context) { Struct.new(:state).new({ conversation: { id: 999_999 } }) }
|
||||
|
||||
it 'returns error message' do
|
||||
result = tool.perform(tool_context, label_name: 'urgent')
|
||||
expect(result).to eq('Conversation not found')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation state is missing' do
|
||||
let(:tool_context) { Struct.new(:state).new({}) }
|
||||
|
||||
it 'returns error message' do
|
||||
result = tool.perform(tool_context, label_name: 'urgent')
|
||||
expect(result).to eq('Conversation not found')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation id is nil' do
|
||||
let(:tool_context) { Struct.new(:state).new({ conversation: { id: nil } }) }
|
||||
|
||||
it 'returns error message' do
|
||||
result = tool.perform(tool_context, label_name: 'urgent')
|
||||
expect(result).to eq('Conversation not found')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#active?' do
|
||||
it 'returns true for public tools' do
|
||||
expect(tool.active?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,124 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Tools::AddPrivateNoteTool, type: :model do
|
||||
let(:account) { create(:account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:tool) { described_class.new(assistant) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:contact) { create(:contact, account: account) }
|
||||
let(:conversation) { create(:conversation, account: account, inbox: inbox, contact: contact) }
|
||||
let(:tool_context) { Struct.new(:state).new({ conversation: { id: conversation.id } }) }
|
||||
|
||||
describe '#description' do
|
||||
it 'returns the correct description' do
|
||||
expect(tool.description).to eq('Add a private note to a conversation')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parameters' do
|
||||
it 'returns the correct parameters' do
|
||||
expect(tool.parameters).to have_key(:note)
|
||||
expect(tool.parameters[:note].name).to eq(:note)
|
||||
expect(tool.parameters[:note].type).to eq('string')
|
||||
expect(tool.parameters[:note].description).to eq('The private note content')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'when conversation exists' do
|
||||
context 'with valid note content' do
|
||||
it 'creates a private note and returns success message' do
|
||||
note_content = 'This is a private note'
|
||||
|
||||
expect do
|
||||
result = tool.perform(tool_context, note: note_content)
|
||||
expect(result).to eq('Private note added successfully')
|
||||
end.to change(Message, :count).by(1)
|
||||
end
|
||||
|
||||
it 'creates a private note with correct attributes' do
|
||||
note_content = 'This is a private note'
|
||||
|
||||
tool.perform(tool_context, note: note_content)
|
||||
|
||||
created_message = Message.last
|
||||
expect(created_message.content).to eq(note_content)
|
||||
expect(created_message.message_type).to eq('outgoing')
|
||||
expect(created_message.private).to be true
|
||||
expect(created_message.account).to eq(account)
|
||||
expect(created_message.inbox).to eq(inbox)
|
||||
expect(created_message.conversation).to eq(conversation)
|
||||
end
|
||||
|
||||
it 'logs tool usage' do
|
||||
expect(tool).to receive(:log_tool_usage).with(
|
||||
'add_private_note',
|
||||
{ conversation_id: conversation.id, note_length: 19 }
|
||||
)
|
||||
|
||||
tool.perform(tool_context, note: 'This is a test note')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with blank note content' do
|
||||
it 'returns error message' do
|
||||
result = tool.perform(tool_context, note: '')
|
||||
expect(result).to eq('Note content is required')
|
||||
end
|
||||
|
||||
it 'does not create a message' do
|
||||
expect do
|
||||
tool.perform(tool_context, note: '')
|
||||
end.not_to change(Message, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with nil note content' do
|
||||
it 'returns error message' do
|
||||
result = tool.perform(tool_context, note: nil)
|
||||
expect(result).to eq('Note content is required')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation does not exist' do
|
||||
let(:tool_context) { Struct.new(:state).new({ conversation: { id: 999_999 } }) }
|
||||
|
||||
it 'returns error message' do
|
||||
result = tool.perform(tool_context, note: 'Some note')
|
||||
expect(result).to eq('Conversation not found')
|
||||
end
|
||||
|
||||
it 'does not create a message' do
|
||||
expect do
|
||||
tool.perform(tool_context, note: 'Some note')
|
||||
end.not_to change(Message, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation state is missing' do
|
||||
let(:tool_context) { Struct.new(:state).new({}) }
|
||||
|
||||
it 'returns error message' do
|
||||
result = tool.perform(tool_context, note: 'Some note')
|
||||
expect(result).to eq('Conversation not found')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation id is nil' do
|
||||
let(:tool_context) { Struct.new(:state).new({ conversation: { id: nil } }) }
|
||||
|
||||
it 'returns error message' do
|
||||
result = tool.perform(tool_context, note: 'Some note')
|
||||
expect(result).to eq('Conversation not found')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#active?' do
|
||||
it 'returns true for public tools' do
|
||||
expect(tool.active?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,120 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Tools::FaqLookupTool, type: :model do
|
||||
let(:account) { create(:account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:tool) { described_class.new(assistant) }
|
||||
let(:tool_context) { Struct.new(:state).new({}) }
|
||||
|
||||
before do
|
||||
# Create installation config for OpenAI API key to avoid errors
|
||||
create(:installation_config, name: 'CAPTAIN_OPEN_AI_API_KEY', value: 'test-key')
|
||||
|
||||
# Mock embedding service to avoid actual API calls
|
||||
embedding_service = instance_double(Captain::Llm::EmbeddingService)
|
||||
allow(Captain::Llm::EmbeddingService).to receive(:new).and_return(embedding_service)
|
||||
allow(embedding_service).to receive(:get_embedding).and_return(Array.new(1536, 0.1))
|
||||
end
|
||||
|
||||
describe '#description' do
|
||||
it 'returns the correct description' do
|
||||
expect(tool.description).to eq('Search FAQ responses using semantic similarity to find relevant answers')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parameters' do
|
||||
it 'returns the correct parameters' do
|
||||
expect(tool.parameters).to have_key(:query)
|
||||
expect(tool.parameters[:query].name).to eq(:query)
|
||||
expect(tool.parameters[:query].type).to eq('string')
|
||||
expect(tool.parameters[:query].description).to eq('The question or topic to search for in the FAQ database')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'when FAQs exist' do
|
||||
let(:document) { create(:captain_document, assistant: assistant) }
|
||||
let!(:response1) do
|
||||
create(:captain_assistant_response,
|
||||
assistant: assistant,
|
||||
question: 'How to reset password?',
|
||||
answer: 'Click on forgot password link',
|
||||
documentable: document,
|
||||
status: 'approved')
|
||||
end
|
||||
let!(:response2) do
|
||||
create(:captain_assistant_response,
|
||||
assistant: assistant,
|
||||
question: 'How to change email?',
|
||||
answer: 'Go to settings and update email',
|
||||
status: 'approved')
|
||||
end
|
||||
|
||||
before do
|
||||
# Mock nearest_neighbors to return our test responses
|
||||
allow(Captain::AssistantResponse).to receive(:nearest_neighbors).and_return(
|
||||
Captain::AssistantResponse.where(id: [response1.id, response2.id])
|
||||
)
|
||||
end
|
||||
|
||||
it 'searches FAQs and returns formatted responses' do
|
||||
result = tool.perform(tool_context, query: 'password reset')
|
||||
|
||||
expect(result).to include('Question: How to reset password?')
|
||||
expect(result).to include('Answer: Click on forgot password link')
|
||||
expect(result).to include('Question: How to change email?')
|
||||
expect(result).to include('Answer: Go to settings and update email')
|
||||
end
|
||||
|
||||
it 'includes source link when document has external_link' do
|
||||
document.update!(external_link: 'https://help.example.com/password')
|
||||
|
||||
result = tool.perform(tool_context, query: 'password')
|
||||
|
||||
expect(result).to include('Source: https://help.example.com/password')
|
||||
end
|
||||
|
||||
it 'logs tool usage for search' do
|
||||
expect(tool).to receive(:log_tool_usage).with('searching', { query: 'password reset' })
|
||||
expect(tool).to receive(:log_tool_usage).with('found_results', { query: 'password reset', count: 2 })
|
||||
|
||||
tool.perform(tool_context, query: 'password reset')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no FAQs found' do
|
||||
before do
|
||||
# Return empty result set
|
||||
allow(Captain::AssistantResponse).to receive(:nearest_neighbors).and_return(Captain::AssistantResponse.none)
|
||||
end
|
||||
|
||||
it 'returns no results message' do
|
||||
result = tool.perform(tool_context, query: 'nonexistent topic')
|
||||
expect(result).to eq('No relevant FAQs found for: nonexistent topic')
|
||||
end
|
||||
|
||||
it 'logs tool usage for no results' do
|
||||
expect(tool).to receive(:log_tool_usage).with('searching', { query: 'nonexistent topic' })
|
||||
expect(tool).to receive(:log_tool_usage).with('no_results', { query: 'nonexistent topic' })
|
||||
|
||||
tool.perform(tool_context, query: 'nonexistent topic')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with blank query' do
|
||||
it 'handles empty query' do
|
||||
# Return empty result set
|
||||
allow(Captain::AssistantResponse).to receive(:nearest_neighbors).and_return(Captain::AssistantResponse.none)
|
||||
|
||||
result = tool.perform(tool_context, query: '')
|
||||
expect(result).to eq('No relevant FAQs found for: ')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#active?' do
|
||||
it 'returns true for public tools' do
|
||||
expect(tool.active?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,228 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Tools::HandoffTool, type: :model do
|
||||
let(:account) { create(:account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:tool) { described_class.new(assistant) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:contact) { create(:contact, account: account) }
|
||||
let(:conversation) { create(:conversation, account: account, inbox: inbox, contact: contact) }
|
||||
let(:tool_context) { Struct.new(:state).new({ conversation: { id: conversation.id } }) }
|
||||
|
||||
describe '#description' do
|
||||
it 'returns the correct description' do
|
||||
expect(tool.description).to eq('Hand off the conversation to a human agent when unable to assist further')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parameters' do
|
||||
it 'returns the correct parameters' do
|
||||
expect(tool.parameters).to have_key(:reason)
|
||||
expect(tool.parameters[:reason].name).to eq(:reason)
|
||||
expect(tool.parameters[:reason].type).to eq('string')
|
||||
expect(tool.parameters[:reason].description).to eq('The reason why handoff is needed (optional)')
|
||||
expect(tool.parameters[:reason].required).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'when conversation exists' do
|
||||
context 'with reason provided' do
|
||||
it 'creates a private note with reason and hands off conversation' do
|
||||
reason = 'Customer needs specialized support'
|
||||
|
||||
expect do
|
||||
result = tool.perform(tool_context, reason: reason)
|
||||
expect(result).to eq("Conversation handed off to human support team (Reason: #{reason})")
|
||||
end.to change(Message, :count).by(1)
|
||||
end
|
||||
|
||||
it 'creates message with correct attributes' do
|
||||
reason = 'Customer needs specialized support'
|
||||
tool.perform(tool_context, reason: reason)
|
||||
|
||||
created_message = Message.last
|
||||
expect(created_message.content).to eq(reason)
|
||||
expect(created_message.message_type).to eq('outgoing')
|
||||
expect(created_message.private).to be true
|
||||
expect(created_message.sender).to eq(assistant)
|
||||
expect(created_message.account).to eq(account)
|
||||
expect(created_message.inbox).to eq(inbox)
|
||||
expect(created_message.conversation).to eq(conversation)
|
||||
end
|
||||
|
||||
it 'triggers bot handoff on conversation' do
|
||||
# The tool finds the conversation by ID, so we need to mock the found conversation
|
||||
found_conversation = Conversation.find(conversation.id)
|
||||
scoped_conversations = Conversation.where(account_id: assistant.account_id)
|
||||
allow(Conversation).to receive(:where).with(account_id: assistant.account_id).and_return(scoped_conversations)
|
||||
allow(scoped_conversations).to receive(:find_by).with(id: conversation.id).and_return(found_conversation)
|
||||
expect(found_conversation).to receive(:bot_handoff!)
|
||||
|
||||
tool.perform(tool_context, reason: 'Test reason')
|
||||
end
|
||||
|
||||
it 'logs tool usage with reason' do
|
||||
reason = 'Customer needs help'
|
||||
expect(tool).to receive(:log_tool_usage).with(
|
||||
'tool_handoff',
|
||||
{ conversation_id: conversation.id, reason: reason }
|
||||
)
|
||||
|
||||
tool.perform(tool_context, reason: reason)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without reason provided' do
|
||||
it 'creates a private note with nil content and hands off conversation' do
|
||||
expect do
|
||||
result = tool.perform(tool_context)
|
||||
expect(result).to eq('Conversation handed off to human support team')
|
||||
end.to change(Message, :count).by(1)
|
||||
|
||||
created_message = Message.last
|
||||
expect(created_message.content).to be_nil
|
||||
end
|
||||
|
||||
it 'logs tool usage with default reason' do
|
||||
expect(tool).to receive(:log_tool_usage).with(
|
||||
'tool_handoff',
|
||||
{ conversation_id: conversation.id, reason: 'Agent requested handoff' }
|
||||
)
|
||||
|
||||
tool.perform(tool_context)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when handoff fails' do
|
||||
before do
|
||||
# Mock the conversation lookup and handoff failure
|
||||
found_conversation = Conversation.find(conversation.id)
|
||||
scoped_conversations = Conversation.where(account_id: assistant.account_id)
|
||||
allow(Conversation).to receive(:where).with(account_id: assistant.account_id).and_return(scoped_conversations)
|
||||
allow(scoped_conversations).to receive(:find_by).with(id: conversation.id).and_return(found_conversation)
|
||||
allow(found_conversation).to receive(:bot_handoff!).and_raise(StandardError, 'Handoff error')
|
||||
|
||||
exception_tracker = instance_double(ChatwootExceptionTracker)
|
||||
allow(ChatwootExceptionTracker).to receive(:new).and_return(exception_tracker)
|
||||
allow(exception_tracker).to receive(:capture_exception)
|
||||
end
|
||||
|
||||
it 'returns error message' do
|
||||
result = tool.perform(tool_context, reason: 'Test')
|
||||
expect(result).to eq('Failed to handoff conversation')
|
||||
end
|
||||
|
||||
it 'captures exception' do
|
||||
exception_tracker = instance_double(ChatwootExceptionTracker)
|
||||
expect(ChatwootExceptionTracker).to receive(:new).with(instance_of(StandardError)).and_return(exception_tracker)
|
||||
expect(exception_tracker).to receive(:capture_exception)
|
||||
|
||||
tool.perform(tool_context, reason: 'Test')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation does not exist' do
|
||||
let(:tool_context) { Struct.new(:state).new({ conversation: { id: 999_999 } }) }
|
||||
|
||||
it 'returns error message' do
|
||||
result = tool.perform(tool_context, reason: 'Test')
|
||||
expect(result).to eq('Conversation not found')
|
||||
end
|
||||
|
||||
it 'does not create a message' do
|
||||
expect do
|
||||
tool.perform(tool_context, reason: 'Test')
|
||||
end.not_to change(Message, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation state is missing' do
|
||||
let(:tool_context) { Struct.new(:state).new({}) }
|
||||
|
||||
it 'returns error message' do
|
||||
result = tool.perform(tool_context, reason: 'Test')
|
||||
expect(result).to eq('Conversation not found')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation id is nil' do
|
||||
let(:tool_context) { Struct.new(:state).new({ conversation: { id: nil } }) }
|
||||
|
||||
it 'returns error message' do
|
||||
result = tool.perform(tool_context, reason: 'Test')
|
||||
expect(result).to eq('Conversation not found')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#active?' do
|
||||
it 'returns true for public tools' do
|
||||
expect(tool.active?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe 'out of office message after handoff' do
|
||||
context 'when outside business hours' do
|
||||
before do
|
||||
inbox.update!(
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message: 'We are currently closed. Please leave your email.'
|
||||
)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
closed_all_day: true,
|
||||
open_all_day: false
|
||||
)
|
||||
end
|
||||
|
||||
it 'sends out of office message after handoff' do
|
||||
expect do
|
||||
tool.perform(tool_context, reason: 'Customer needs help')
|
||||
end.to change { conversation.messages.template.count }.by(1)
|
||||
|
||||
ooo_message = conversation.messages.template.last
|
||||
expect(ooo_message.content).to eq('We are currently closed. Please leave your email.')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when within business hours' do
|
||||
before do
|
||||
inbox.update!(
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message: 'We are currently closed.'
|
||||
)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
open_all_day: true,
|
||||
closed_all_day: false
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not send out of office message after handoff' do
|
||||
expect do
|
||||
tool.perform(tool_context, reason: 'Customer needs help')
|
||||
end.not_to(change { conversation.messages.template.count })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no out of office message is configured' do
|
||||
before do
|
||||
inbox.update!(
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message: nil
|
||||
)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
closed_all_day: true,
|
||||
open_all_day: false
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not send out of office message' do
|
||||
expect do
|
||||
tool.perform(tool_context, reason: 'Customer needs help')
|
||||
end.not_to(change { conversation.messages.template.count })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,117 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Tools::UpdatePriorityTool, type: :model do
|
||||
let(:account) { create(:account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:tool) { described_class.new(assistant) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:contact) { create(:contact, account: account) }
|
||||
let(:conversation) { create(:conversation, account: account, inbox: inbox, contact: contact) }
|
||||
let(:tool_context) { Struct.new(:state).new({ conversation: { id: conversation.id } }) }
|
||||
|
||||
describe '#description' do
|
||||
it 'returns the correct description' do
|
||||
expect(tool.description).to eq('Update the priority of a conversation')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parameters' do
|
||||
it 'returns the correct parameters' do
|
||||
expect(tool.parameters).to have_key(:priority)
|
||||
expect(tool.parameters[:priority].name).to eq(:priority)
|
||||
expect(tool.parameters[:priority].type).to eq('string')
|
||||
expect(tool.parameters[:priority].description).to eq('The priority level: low, medium, high, urgent, or nil to remove priority')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'when conversation exists' do
|
||||
context 'with valid priority levels' do
|
||||
%w[low medium high urgent].each do |priority|
|
||||
it "updates conversation priority to #{priority}" do
|
||||
result = tool.perform(tool_context, priority: priority)
|
||||
expect(result).to eq("Priority updated to '#{priority}' for conversation ##{conversation.display_id}")
|
||||
|
||||
expect(conversation.reload.priority).to eq(priority)
|
||||
end
|
||||
end
|
||||
|
||||
it 'removes priority when set to nil' do
|
||||
conversation.update!(priority: 'high')
|
||||
|
||||
result = tool.perform(tool_context, priority: 'nil')
|
||||
expect(result).to eq("Priority updated to 'none' for conversation ##{conversation.display_id}")
|
||||
|
||||
expect(conversation.reload.priority).to be_nil
|
||||
end
|
||||
|
||||
it 'removes priority when set to empty string' do
|
||||
conversation.update!(priority: 'high')
|
||||
|
||||
result = tool.perform(tool_context, priority: '')
|
||||
expect(result).to eq("Priority updated to 'none' for conversation ##{conversation.display_id}")
|
||||
|
||||
expect(conversation.reload.priority).to be_nil
|
||||
end
|
||||
|
||||
it 'logs tool usage' do
|
||||
expect(tool).to receive(:log_tool_usage).with(
|
||||
'update_priority',
|
||||
{ conversation_id: conversation.id, priority: 'high' }
|
||||
)
|
||||
|
||||
tool.perform(tool_context, priority: 'high')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid priority levels' do
|
||||
it 'returns error message for invalid priority' do
|
||||
result = tool.perform(tool_context, priority: 'invalid')
|
||||
expect(result).to eq('Invalid priority. Valid options: low, medium, high, urgent, nil')
|
||||
end
|
||||
|
||||
it 'does not update conversation priority' do
|
||||
original_priority = conversation.priority
|
||||
|
||||
tool.perform(tool_context, priority: 'invalid')
|
||||
|
||||
expect(conversation.reload.priority).to eq(original_priority)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation does not exist' do
|
||||
let(:tool_context) { Struct.new(:state).new({ conversation: { id: 999_999 } }) }
|
||||
|
||||
it 'returns error message' do
|
||||
result = tool.perform(tool_context, priority: 'high')
|
||||
expect(result).to eq('Conversation not found')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation state is missing' do
|
||||
let(:tool_context) { Struct.new(:state).new({}) }
|
||||
|
||||
it 'returns error message' do
|
||||
result = tool.perform(tool_context, priority: 'high')
|
||||
expect(result).to eq('Conversation not found')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation id is nil' do
|
||||
let(:tool_context) { Struct.new(:state).new({ conversation: { id: nil } }) }
|
||||
|
||||
it 'returns error message' do
|
||||
result = tool.perform(tool_context, priority: 'high')
|
||||
expect(result).to eq('Conversation not found')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#active?' do
|
||||
it 'returns true for public tools' do
|
||||
expect(tool.active?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user