From e6d720082c9037d405ef2e76b14b68cdd25f7d38 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 16 Jul 2025 10:28:39 +0530 Subject: [PATCH] test: captain tools --- .../tools/add_contact_note_tool_spec.rb | 116 ++++++++++++++++ .../add_label_to_conversation_tool_spec.rb | 125 ++++++++++++++++++ .../tools/add_private_note_tool_spec.rb | 124 +++++++++++++++++ .../tools/update_priority_tool_spec.rb | 117 ++++++++++++++++ 4 files changed, 482 insertions(+) create mode 100644 spec/enterprise/lib/captain/tools/add_contact_note_tool_spec.rb create mode 100644 spec/enterprise/lib/captain/tools/add_label_to_conversation_tool_spec.rb create mode 100644 spec/enterprise/lib/captain/tools/add_private_note_tool_spec.rb create mode 100644 spec/enterprise/lib/captain/tools/update_priority_tool_spec.rb diff --git a/spec/enterprise/lib/captain/tools/add_contact_note_tool_spec.rb b/spec/enterprise/lib/captain/tools/add_contact_note_tool_spec.rb new file mode 100644 index 000000000..c087242dc --- /dev/null +++ b/spec/enterprise/lib/captain/tools/add_contact_note_tool_spec.rb @@ -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 diff --git a/spec/enterprise/lib/captain/tools/add_label_to_conversation_tool_spec.rb b/spec/enterprise/lib/captain/tools/add_label_to_conversation_tool_spec.rb new file mode 100644 index 000000000..38e4dc7c6 --- /dev/null +++ b/spec/enterprise/lib/captain/tools/add_label_to_conversation_tool_spec.rb @@ -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 diff --git a/spec/enterprise/lib/captain/tools/add_private_note_tool_spec.rb b/spec/enterprise/lib/captain/tools/add_private_note_tool_spec.rb new file mode 100644 index 000000000..cfce1a7d1 --- /dev/null +++ b/spec/enterprise/lib/captain/tools/add_private_note_tool_spec.rb @@ -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 diff --git a/spec/enterprise/lib/captain/tools/update_priority_tool_spec.rb b/spec/enterprise/lib/captain/tools/update_priority_tool_spec.rb new file mode 100644 index 000000000..9aa858593 --- /dev/null +++ b/spec/enterprise/lib/captain/tools/update_priority_tool_spec.rb @@ -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