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:
@@ -0,0 +1,101 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Tools::Copilot::GetArticleService do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:service) { described_class.new(assistant, user: user) }
|
||||
|
||||
describe '#name' do
|
||||
it 'returns the correct service name' do
|
||||
expect(service.name).to eq('get_article')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#description' do
|
||||
it 'returns the service description' do
|
||||
expect(service.description).to eq('Get details of an article including its content and metadata')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parameters' do
|
||||
it 'defines article_id parameter' do
|
||||
expect(service.parameters.keys).to contain_exactly(:article_id)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#active?' do
|
||||
context 'when user is an admin' do
|
||||
let(:user) { create(:user, :administrator, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
|
||||
it 'returns true' do
|
||||
expect(service.active?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has custom role with knowledge_base_manage permission' do
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:custom_role) { create(:custom_role, account: account, permissions: ['knowledge_base_manage']) }
|
||||
|
||||
before do
|
||||
account_user = AccountUser.find_by(user: user, account: account)
|
||||
account_user.update(role: :agent, custom_role: custom_role)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(service.active?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has custom role without knowledge_base_manage permission' do
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:custom_role) { create(:custom_role, account: account, permissions: []) }
|
||||
|
||||
before do
|
||||
account_user = AccountUser.find_by(user: user, account: account)
|
||||
account_user.update(role: :agent, custom_role: custom_role)
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(service.active?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
context 'when article_id is blank' do
|
||||
it 'returns error message' do
|
||||
expect(service.execute(article_id: nil)).to eq('Article not found')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when article is not found' do
|
||||
it 'returns not found message' do
|
||||
expect(service.execute(article_id: 999)).to eq('Article not found')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when article exists' do
|
||||
let(:portal) { create(:portal, account: account) }
|
||||
let(:article) { create(:article, account: account, portal: portal, author: user, title: 'Test Article', content: 'Content') }
|
||||
|
||||
it 'returns the article in llm text format' do
|
||||
result = service.execute(article_id: article.id)
|
||||
expect(result).to eq(article.to_llm_text)
|
||||
end
|
||||
|
||||
context 'when article belongs to different account' do
|
||||
let(:other_account) { create(:account) }
|
||||
let(:other_portal) { create(:portal, account: other_account) }
|
||||
let(:other_article) { create(:article, account: other_account, portal: other_portal, author: user, title: 'Other Article') }
|
||||
|
||||
it 'returns not found message' do
|
||||
expect(service.execute(article_id: other_article.id)).to eq('Article not found')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,99 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Tools::Copilot::GetContactService do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:service) { described_class.new(assistant, user: user) }
|
||||
|
||||
describe '#name' do
|
||||
it 'returns the correct service name' do
|
||||
expect(service.name).to eq('get_contact')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#description' do
|
||||
it 'returns the service description' do
|
||||
expect(service.description).to eq('Get details of a contact including their profile information')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parameters' do
|
||||
it 'defines contact_id parameter' do
|
||||
expect(service.parameters.keys).to contain_exactly(:contact_id)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#active?' do
|
||||
context 'when user is an admin' do
|
||||
let(:user) { create(:user, :administrator, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
|
||||
it 'returns true' do
|
||||
expect(service.active?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has custom role with contact_manage permission' do
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:custom_role) { create(:custom_role, account: account, permissions: ['contact_manage']) }
|
||||
|
||||
before do
|
||||
account_user = AccountUser.find_by(user: user, account: account)
|
||||
account_user.update(role: :agent, custom_role: custom_role)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(service.active?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has custom role without contact_manage permission' do
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:custom_role) { create(:custom_role, account: account, permissions: []) }
|
||||
|
||||
before do
|
||||
account_user = AccountUser.find_by(user: user, account: account)
|
||||
account_user.update(role: :agent, custom_role: custom_role)
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(service.active?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
context 'when contact_id is blank' do
|
||||
it 'returns not found message' do
|
||||
expect(service.execute(contact_id: nil)).to eq('Contact not found')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when contact is not found' do
|
||||
it 'returns not found message' do
|
||||
expect(service.execute(contact_id: 999)).to eq('Contact not found')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when contact exists' do
|
||||
let(:contact) { create(:contact, account: account) }
|
||||
|
||||
it 'returns the contact in llm text format' do
|
||||
result = service.execute(contact_id: contact.id)
|
||||
expect(result).to eq(contact.to_llm_text)
|
||||
end
|
||||
|
||||
context 'when contact belongs to different account' do
|
||||
let(:other_account) { create(:account) }
|
||||
let(:other_contact) { create(:contact, account: other_account) }
|
||||
|
||||
it 'returns not found message' do
|
||||
expect(service.execute(contact_id: other_contact.id)).to eq('Contact not found')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,148 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Tools::Copilot::GetConversationService do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:service) { described_class.new(assistant, user: user) }
|
||||
|
||||
describe '#name' do
|
||||
it 'returns the correct service name' do
|
||||
expect(service.name).to eq('get_conversation')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#description' do
|
||||
it 'returns the service description' do
|
||||
expect(service.description).to eq('Get details of a conversation including messages and contact information')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parameters' do
|
||||
it 'defines conversation_id parameter' do
|
||||
expect(service.parameters.keys).to contain_exactly(:conversation_id)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#active?' do
|
||||
context 'when user is an admin' do
|
||||
let(:user) { create(:user, :administrator, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
|
||||
it 'returns true' do
|
||||
expect(service.active?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has custom role with conversation_manage permission' do
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:custom_role) { create(:custom_role, account: account, permissions: ['conversation_manage']) }
|
||||
|
||||
before do
|
||||
account_user = AccountUser.find_by(user: user, account: account)
|
||||
account_user.update(role: :agent, custom_role: custom_role)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(service.active?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has custom role with conversation_unassigned_manage permission' do
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:custom_role) { create(:custom_role, account: account, permissions: ['conversation_unassigned_manage']) }
|
||||
|
||||
before do
|
||||
account_user = AccountUser.find_by(user: user, account: account)
|
||||
account_user.update(role: :agent, custom_role: custom_role)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(service.active?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has custom role with conversation_participating_manage permission' do
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:custom_role) { create(:custom_role, account: account, permissions: ['conversation_participating_manage']) }
|
||||
|
||||
before do
|
||||
account_user = AccountUser.find_by(user: user, account: account)
|
||||
account_user.update(role: :agent, custom_role: custom_role)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(service.active?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has custom role without any conversation permissions' do
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:custom_role) { create(:custom_role, account: account, permissions: []) }
|
||||
|
||||
before do
|
||||
account_user = AccountUser.find_by(user: user, account: account)
|
||||
account_user.update(role: :agent, custom_role: custom_role)
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(service.active?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
context 'when conversation is not found' do
|
||||
it 'returns not found message' do
|
||||
expect(service.execute(conversation_id: 999)).to eq('Conversation not found')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation exists' do
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:conversation) { create(:conversation, account: account, inbox: inbox) }
|
||||
|
||||
it 'returns the conversation in llm text format' do
|
||||
result = service.execute(conversation_id: conversation.display_id)
|
||||
expect(result).to eq(conversation.to_llm_text)
|
||||
end
|
||||
|
||||
it 'includes private messages in the llm text format' do
|
||||
# Create a regular message
|
||||
create(:message,
|
||||
conversation: conversation,
|
||||
message_type: 'outgoing',
|
||||
content: 'Regular message',
|
||||
private: false)
|
||||
|
||||
# Create a private message
|
||||
create(:message,
|
||||
conversation: conversation,
|
||||
message_type: 'outgoing',
|
||||
content: 'Private note content',
|
||||
private: true)
|
||||
|
||||
result = service.execute(conversation_id: conversation.display_id)
|
||||
|
||||
# Verify that the result includes both regular and private messages
|
||||
expect(result).to include('Regular message')
|
||||
expect(result).to include('Private note content')
|
||||
expect(result).to include('[Private Note]')
|
||||
end
|
||||
|
||||
context 'when conversation belongs to different account' do
|
||||
let(:other_account) { create(:account) }
|
||||
let(:other_inbox) { create(:inbox, account: other_account) }
|
||||
let(:other_conversation) { create(:conversation, account: other_account, inbox: other_inbox) }
|
||||
|
||||
it 'returns not found message' do
|
||||
expect(service.execute(conversation_id: other_conversation.display_id)).to eq('Conversation not found')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,115 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Tools::Copilot::SearchArticlesService do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:service) { described_class.new(assistant, user: user) }
|
||||
|
||||
describe '#name' do
|
||||
it 'returns the correct service name' do
|
||||
expect(service.name).to eq('search_articles')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#description' do
|
||||
it 'returns the service description' do
|
||||
expect(service.description).to eq('Search articles based on parameters')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#active?' do
|
||||
context 'when user is an admin' do
|
||||
let(:user) { create(:user, :administrator, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
|
||||
it 'returns true' do
|
||||
expect(service.active?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is an agent' do
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
|
||||
it 'returns true' do
|
||||
expect(service.active?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has custom role with knowledge_base_manage permission' do
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:custom_role) { create(:custom_role, account: account, permissions: ['knowledge_base_manage']) }
|
||||
|
||||
before do
|
||||
account_user = AccountUser.find_by(user: user, account: account)
|
||||
account_user.update(role: :agent, custom_role: custom_role)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(service.active?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has custom role without knowledge_base_manage permission' do
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:custom_role) { create(:custom_role, account: account, permissions: []) }
|
||||
|
||||
before do
|
||||
account_user = AccountUser.find_by(user: user, account: account)
|
||||
account_user.update(role: :agent, custom_role: custom_role)
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(service.active?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
context 'when no articles are found' do
|
||||
it 'returns no articles found message' do
|
||||
expect(service.execute(query: 'test', category_id: nil, status: nil)).to eq('No articles found')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when articles are found' do
|
||||
let(:portal) { create(:portal, account: account) }
|
||||
let!(:article1) { create(:article, account: account, portal: portal, author: user, title: 'Test Article 1', content: 'Content 1') }
|
||||
let!(:article2) { create(:article, account: account, portal: portal, author: user, title: 'Test Article 2', content: 'Content 2') }
|
||||
|
||||
it 'returns formatted articles with count' do
|
||||
result = service.execute(query: 'Test', category_id: nil, status: nil)
|
||||
expect(result).to include('Total number of articles: 2')
|
||||
expect(result).to include(article1.to_llm_text)
|
||||
expect(result).to include(article2.to_llm_text)
|
||||
end
|
||||
|
||||
context 'when filtered by category' do
|
||||
let(:category) { create(:category, slug: 'test-category', portal: portal, account: account) }
|
||||
let!(:article3) { create(:article, account: account, portal: portal, author: user, category: category, title: 'Test Article 3') }
|
||||
|
||||
it 'returns only articles from the specified category' do
|
||||
result = service.execute(query: 'Test', category_id: category.id, status: nil)
|
||||
expect(result).to include('Total number of articles: 1')
|
||||
expect(result).to include(article3.to_llm_text)
|
||||
expect(result).not_to include(article1.to_llm_text)
|
||||
expect(result).not_to include(article2.to_llm_text)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtered by status' do
|
||||
let!(:article3) { create(:article, account: account, portal: portal, author: user, title: 'Test Article 3', status: 'published') }
|
||||
let!(:article4) { create(:article, account: account, portal: portal, author: user, title: 'Test Article 4', status: 'draft') }
|
||||
|
||||
it 'returns only articles with the specified status' do
|
||||
result = service.execute(query: 'Test', category_id: nil, status: 'published')
|
||||
expect(result).to include(article3.to_llm_text)
|
||||
expect(result).not_to include(article4.to_llm_text)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,94 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Tools::Copilot::SearchContactsService do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:service) { described_class.new(assistant, user: user) }
|
||||
|
||||
describe '#name' do
|
||||
it 'returns the correct service name' do
|
||||
expect(service.name).to eq('search_contacts')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#description' do
|
||||
it 'returns the service description' do
|
||||
expect(service.description).to eq('Search contacts based on query parameters')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parameters' do
|
||||
it 'defines email, phone_number, and name parameters' do
|
||||
expect(service.parameters.keys).to contain_exactly(:email, :phone_number, :name)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#active?' do
|
||||
context 'when user has contact_manage permission' do
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:custom_role) { create(:custom_role, account: account, permissions: ['contact_manage']) }
|
||||
|
||||
before do
|
||||
account_user = AccountUser.find_by(user: user, account: account)
|
||||
account_user.update(role: :agent, custom_role: custom_role)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(service.active?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have contact_manage permission' do
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:custom_role) { create(:custom_role, account: account, permissions: []) }
|
||||
|
||||
before do
|
||||
account_user = AccountUser.find_by(user: user, account: account)
|
||||
account_user.update(role: :agent, custom_role: custom_role)
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(service.active?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
context 'when contacts are found' do
|
||||
let(:contact1) { create(:contact, account: account, email: 'test1@example.com', name: 'Test Contact 1', phone_number: '+1234567890') }
|
||||
let(:contact2) { create(:contact, account: account, email: 'test2@example.com', name: 'Test Contact 2', phone_number: '+1234567891') }
|
||||
|
||||
before do
|
||||
contact1
|
||||
contact2
|
||||
end
|
||||
|
||||
it 'returns contacts when filtered by email' do
|
||||
result = service.execute(email: 'test1@example.com')
|
||||
expect(result).to include(contact1.to_llm_text)
|
||||
expect(result).not_to include(contact2.to_llm_text)
|
||||
end
|
||||
|
||||
it 'returns contacts when filtered by phone number' do
|
||||
result = service.execute(phone_number: '+1234567890')
|
||||
expect(result).to include(contact1.to_llm_text)
|
||||
expect(result).not_to include(contact2.to_llm_text)
|
||||
end
|
||||
|
||||
it 'returns contacts when filtered by name' do
|
||||
result = service.execute(name: 'Contact 1')
|
||||
expect(result).to include(contact1.to_llm_text)
|
||||
expect(result).not_to include(contact2.to_llm_text)
|
||||
end
|
||||
|
||||
it 'returns all matching contacts when no filters are provided' do
|
||||
result = service.execute
|
||||
expect(result).to include(contact1.to_llm_text)
|
||||
expect(result).to include(contact2.to_llm_text)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,160 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Tools::Copilot::SearchConversationsService do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, role: 'administrator', account: account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:service) { described_class.new(assistant, user: user) }
|
||||
|
||||
describe '#name' do
|
||||
it 'returns the correct service name' do
|
||||
expect(service.name).to eq('search_conversation')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#description' do
|
||||
it 'returns the service description' do
|
||||
expect(service.description).to eq('Search conversations based on parameters')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parameters' do
|
||||
it 'defines the expected parameters' do
|
||||
expect(service.parameters.keys).to contain_exactly(:status, :contact_id, :priority, :labels)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#active?' do
|
||||
context 'when user has conversation_manage permission' do
|
||||
let(:custom_role) { create(:custom_role, account: account, permissions: ['conversation_manage']) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
|
||||
before do
|
||||
account_user = AccountUser.find_by(user: user, account: account)
|
||||
account_user.update(role: :agent, custom_role: custom_role)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(service.active?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has conversation_unassigned_manage permission' do
|
||||
let(:custom_role) { create(:custom_role, account: account, permissions: ['conversation_unassigned_manage']) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
|
||||
before do
|
||||
account_user = AccountUser.find_by(user: user, account: account)
|
||||
account_user.update(role: :agent, custom_role: custom_role)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(service.active?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has conversation_participating_manage permission' do
|
||||
let(:custom_role) { create(:custom_role, account: account, permissions: ['conversation_participating_manage']) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
|
||||
before do
|
||||
account_user = AccountUser.find_by(user: user, account: account)
|
||||
account_user.update(role: :agent, custom_role: custom_role)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(service.active?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has no relevant conversation permissions' do
|
||||
let(:custom_role) { create(:custom_role, account: account, permissions: []) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
|
||||
before do
|
||||
account_user = AccountUser.find_by(user: user, account: account)
|
||||
account_user.update(role: :agent, custom_role: custom_role)
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(service.active?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
let(:contact) { create(:contact, account: account) }
|
||||
let!(:open_conversation) { create(:conversation, account: account, contact: contact, status: 'open', priority: 'high') }
|
||||
let!(:resolved_conversation) { create(:conversation, account: account, status: 'resolved', priority: 'low') }
|
||||
|
||||
it 'returns all conversations when no filters are applied' do
|
||||
result = service.execute
|
||||
expect(result).to include('Total number of conversations: 2')
|
||||
expect(result).to include(open_conversation.to_llm_text(include_contact_details: true))
|
||||
expect(result).to include(resolved_conversation.to_llm_text(include_contact_details: true))
|
||||
end
|
||||
|
||||
it 'filters conversations by status' do
|
||||
result = service.execute(status: 'open')
|
||||
expect(result).to include('Total number of conversations: 1')
|
||||
expect(result).to include(open_conversation.to_llm_text(include_contact_details: true))
|
||||
expect(result).not_to include(resolved_conversation.to_llm_text(include_contact_details: true))
|
||||
end
|
||||
|
||||
it 'filters conversations by contact_id' do
|
||||
result = service.execute(contact_id: contact.id)
|
||||
expect(result).to include('Total number of conversations: 1')
|
||||
expect(result).to include(open_conversation.to_llm_text(include_contact_details: true))
|
||||
expect(result).not_to include(resolved_conversation.to_llm_text(include_contact_details: true))
|
||||
end
|
||||
|
||||
it 'filters conversations by priority' do
|
||||
result = service.execute(priority: 'high')
|
||||
expect(result).to include('Total number of conversations: 1')
|
||||
expect(result).to include(open_conversation.to_llm_text(include_contact_details: true))
|
||||
expect(result).not_to include(resolved_conversation.to_llm_text(include_contact_details: true))
|
||||
end
|
||||
|
||||
it 'returns appropriate message when no conversations are found' do
|
||||
result = service.execute(status: 'snoozed')
|
||||
expect(result).to eq('No conversations found')
|
||||
end
|
||||
|
||||
context 'when invalid status is provided' do
|
||||
it 'ignores invalid status and returns all conversations' do
|
||||
result = service.execute(status: 'all')
|
||||
expect(result).to include('Total number of conversations: 2')
|
||||
expect(result).to include(open_conversation.to_llm_text(include_contact_details: true))
|
||||
expect(result).to include(resolved_conversation.to_llm_text(include_contact_details: true))
|
||||
end
|
||||
|
||||
it 'ignores random invalid status values' do
|
||||
result = service.execute(status: 'invalid_status')
|
||||
expect(result).to include('Total number of conversations: 2')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when invalid priority is provided' do
|
||||
it 'ignores invalid priority and returns all conversations' do
|
||||
result = service.execute(priority: 'all')
|
||||
expect(result).to include('Total number of conversations: 2')
|
||||
expect(result).to include(open_conversation.to_llm_text(include_contact_details: true))
|
||||
expect(result).to include(resolved_conversation.to_llm_text(include_contact_details: true))
|
||||
end
|
||||
|
||||
it 'ignores random invalid priority values' do
|
||||
result = service.execute(priority: 'invalid_priority')
|
||||
expect(result).to include('Total number of conversations: 2')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when combining valid and invalid parameters' do
|
||||
it 'applies valid filters and ignores invalid ones' do
|
||||
result = service.execute(status: 'all', contact_id: contact.id)
|
||||
expect(result).to include('Total number of conversations: 1')
|
||||
expect(result).to include(open_conversation.to_llm_text(include_contact_details: true))
|
||||
expect(result).not_to include(resolved_conversation.to_llm_text(include_contact_details: true))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,139 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Tools::Copilot::SearchLinearIssuesService do
|
||||
let(:account) { create(:account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:service) { described_class.new(assistant, user: user) }
|
||||
|
||||
describe '#name' do
|
||||
it 'returns the correct service name' do
|
||||
expect(service.name).to eq('search_linear_issues')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#description' do
|
||||
it 'returns the service description' do
|
||||
expect(service.description).to eq('Search Linear issues based on a search term')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parameters' do
|
||||
it 'defines term parameter' do
|
||||
expect(service.parameters.keys).to contain_exactly(:term)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#active?' do
|
||||
context 'when Linear integration is enabled' do
|
||||
before do
|
||||
create(:integrations_hook, :linear, account: account)
|
||||
end
|
||||
|
||||
context 'when user is present' do
|
||||
it 'returns true' do
|
||||
expect(service.active?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not present' do
|
||||
let(:service) { described_class.new(assistant) }
|
||||
|
||||
it 'returns false' do
|
||||
expect(service.active?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when Linear integration is not enabled' do
|
||||
context 'when user is present' do
|
||||
it 'returns false' do
|
||||
expect(service.active?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not present' do
|
||||
let(:service) { described_class.new(assistant) }
|
||||
|
||||
it 'returns false' do
|
||||
expect(service.active?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
context 'when Linear integration is not enabled' do
|
||||
it 'returns error message' do
|
||||
expect(service.execute(term: 'test')).to eq('Linear integration is not enabled')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when Linear integration is enabled' do
|
||||
let(:linear_service) { instance_double(Integrations::Linear::ProcessorService) }
|
||||
|
||||
before do
|
||||
create(:integrations_hook, :linear, account: account)
|
||||
allow(Integrations::Linear::ProcessorService).to receive(:new).and_return(linear_service)
|
||||
end
|
||||
|
||||
context 'when term is blank' do
|
||||
before do
|
||||
allow(linear_service).to receive(:search_issue).with('').and_return({ data: [] })
|
||||
end
|
||||
|
||||
it 'returns no issues found message' do
|
||||
expect(service.execute(term: '')).to eq('No issues found, I should try another similar search term')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when search returns error' do
|
||||
before do
|
||||
allow(linear_service).to receive(:search_issue).and_return({ error: 'API Error' })
|
||||
end
|
||||
|
||||
it 'returns the error message' do
|
||||
expect(service.execute(term: 'test')).to eq('API Error')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when search returns no issues' do
|
||||
before do
|
||||
allow(linear_service).to receive(:search_issue).and_return({ data: [] })
|
||||
end
|
||||
|
||||
it 'returns no issues found message' do
|
||||
expect(service.execute(term: 'test')).to eq('No issues found, I should try another similar search term')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when search returns issues' do
|
||||
let(:issues) do
|
||||
[{
|
||||
'title' => 'Test Issue',
|
||||
'id' => 'TEST-123',
|
||||
'state' => { 'name' => 'In Progress' },
|
||||
'priority' => 4,
|
||||
'assignee' => { 'name' => 'John Doe' },
|
||||
'description' => 'Test description'
|
||||
}]
|
||||
end
|
||||
|
||||
before do
|
||||
allow(linear_service).to receive(:search_issue).and_return({ data: issues })
|
||||
end
|
||||
|
||||
it 'returns formatted issues' do
|
||||
result = service.execute(term: 'test')
|
||||
expect(result).to include('Total number of issues: 1')
|
||||
expect(result).to include('Title: Test Issue')
|
||||
expect(result).to include('ID: TEST-123')
|
||||
expect(result).to include('State: In Progress')
|
||||
expect(result).to include('Priority: Low')
|
||||
expect(result).to include('Assignee: John Doe')
|
||||
expect(result).to include('Description: Test description')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
144
spec/enterprise/services/captain/tools/firecrawl_service_spec.rb
Normal file
144
spec/enterprise/services/captain/tools/firecrawl_service_spec.rb
Normal file
@@ -0,0 +1,144 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Tools::FirecrawlService do
|
||||
let(:api_key) { 'test-api-key' }
|
||||
let(:url) { 'https://example.com' }
|
||||
let(:webhook_url) { 'https://webhook.example.com/callback' }
|
||||
let(:crawl_limit) { 15 }
|
||||
|
||||
before do
|
||||
create(:installation_config, name: 'CAPTAIN_FIRECRAWL_API_KEY', value: api_key)
|
||||
end
|
||||
|
||||
describe '#initialize' do
|
||||
context 'when API key is configured' do
|
||||
it 'initializes successfully' do
|
||||
expect { described_class.new }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when API key is missing' do
|
||||
before do
|
||||
InstallationConfig.find_by(name: 'CAPTAIN_FIRECRAWL_API_KEY').destroy
|
||||
end
|
||||
|
||||
it 'raises an error' do
|
||||
expect { described_class.new }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when API key is nil' do
|
||||
before do
|
||||
InstallationConfig.find_by(name: 'CAPTAIN_FIRECRAWL_API_KEY').update(value: nil)
|
||||
end
|
||||
|
||||
it 'raises an error' do
|
||||
expect { described_class.new }.to raise_error(NoMethodError)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when API key is empty' do
|
||||
before do
|
||||
InstallationConfig.find_by(name: 'CAPTAIN_FIRECRAWL_API_KEY').update(value: '')
|
||||
end
|
||||
|
||||
it 'raises an error' do
|
||||
expect { described_class.new }.to raise_error('Missing API key')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
let(:service) { described_class.new }
|
||||
let(:expected_payload) do
|
||||
{
|
||||
url: url,
|
||||
maxDepth: 50,
|
||||
ignoreSitemap: false,
|
||||
limit: crawl_limit,
|
||||
webhook: webhook_url,
|
||||
scrapeOptions: {
|
||||
onlyMainContent: false,
|
||||
formats: ['markdown'],
|
||||
excludeTags: ['iframe']
|
||||
}
|
||||
}.to_json
|
||||
end
|
||||
|
||||
let(:expected_headers) do
|
||||
{
|
||||
'Authorization' => "Bearer #{api_key}",
|
||||
'Content-Type' => 'application/json'
|
||||
}
|
||||
end
|
||||
|
||||
context 'when the API call is successful' do
|
||||
before do
|
||||
stub_request(:post, 'https://api.firecrawl.dev/v1/crawl')
|
||||
.with(
|
||||
body: expected_payload,
|
||||
headers: expected_headers
|
||||
)
|
||||
.to_return(status: 200, body: '{"status": "success"}')
|
||||
end
|
||||
|
||||
it 'makes a POST request with correct parameters' do
|
||||
service.perform(url, webhook_url, crawl_limit)
|
||||
|
||||
expect(WebMock).to have_requested(:post, 'https://api.firecrawl.dev/v1/crawl')
|
||||
.with(
|
||||
body: expected_payload,
|
||||
headers: expected_headers
|
||||
)
|
||||
end
|
||||
|
||||
it 'uses default crawl limit when not specified' do
|
||||
default_payload = expected_payload.gsub(crawl_limit.to_s, '10')
|
||||
|
||||
stub_request(:post, 'https://api.firecrawl.dev/v1/crawl')
|
||||
.with(
|
||||
body: default_payload,
|
||||
headers: expected_headers
|
||||
)
|
||||
.to_return(status: 200, body: '{"status": "success"}')
|
||||
|
||||
service.perform(url, webhook_url)
|
||||
|
||||
expect(WebMock).to have_requested(:post, 'https://api.firecrawl.dev/v1/crawl')
|
||||
.with(
|
||||
body: default_payload,
|
||||
headers: expected_headers
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the API call fails' do
|
||||
before do
|
||||
stub_request(:post, 'https://api.firecrawl.dev/v1/crawl')
|
||||
.to_raise(StandardError.new('Connection failed'))
|
||||
end
|
||||
|
||||
it 'raises an error with the failure message' do
|
||||
expect { service.perform(url, webhook_url, crawl_limit) }
|
||||
.to raise_error('Failed to crawl URL: Connection failed')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the API returns an error response' do
|
||||
before do
|
||||
stub_request(:post, 'https://api.firecrawl.dev/v1/crawl')
|
||||
.to_return(status: 422, body: '{"error": "Invalid URL"}')
|
||||
end
|
||||
|
||||
it 'makes the request but does not raise an error' do
|
||||
expect { service.perform(url, webhook_url, crawl_limit) }.not_to raise_error
|
||||
|
||||
expect(WebMock).to have_requested(:post, 'https://api.firecrawl.dev/v1/crawl')
|
||||
.with(
|
||||
body: expected_payload,
|
||||
headers: expected_headers
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,66 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Tools::SearchDocumentationService do
|
||||
let(:assistant) { create(:captain_assistant) }
|
||||
let(:service) { described_class.new(assistant) }
|
||||
let(:question) { 'How to create a new account?' }
|
||||
let(:answer) { 'You can create a new account by clicking on the Sign Up button.' }
|
||||
let(:external_link) { 'https://example.com/docs/create-account' }
|
||||
|
||||
describe '#name' do
|
||||
it 'returns the correct service name' do
|
||||
expect(service.name).to eq('search_documentation')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#description' do
|
||||
it 'returns the service description' do
|
||||
expect(service.description).to eq('Search and retrieve documentation from knowledge base')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parameters' do
|
||||
it 'defines query parameter' do
|
||||
expect(service.parameters.keys).to contain_exactly(:query)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
let!(:response) do
|
||||
create(
|
||||
:captain_assistant_response,
|
||||
assistant: assistant,
|
||||
question: question,
|
||||
answer: answer,
|
||||
status: 'approved'
|
||||
)
|
||||
end
|
||||
|
||||
let(:documentable) { create(:captain_document, external_link: external_link) }
|
||||
|
||||
context 'when matching responses exist' do
|
||||
before do
|
||||
response.update(documentable: documentable)
|
||||
allow(Captain::AssistantResponse).to receive(:search).with(question).and_return([response])
|
||||
end
|
||||
|
||||
it 'returns formatted responses for the search query' do
|
||||
result = service.execute(query: question)
|
||||
|
||||
expect(result).to include(question)
|
||||
expect(result).to include(answer)
|
||||
expect(result).to include(external_link)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no matching responses exist' do
|
||||
before do
|
||||
allow(Captain::AssistantResponse).to receive(:search).with(question).and_return([])
|
||||
end
|
||||
|
||||
it 'returns an empty string' do
|
||||
expect(service.execute(query: question)).to eq('No FAQs found for the given query')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,187 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Tools::SimplePageCrawlService do
|
||||
let(:base_url) { 'https://example.com' }
|
||||
let(:service) { described_class.new(base_url) }
|
||||
|
||||
before do
|
||||
WebMock.disable_net_connect!
|
||||
end
|
||||
|
||||
after do
|
||||
WebMock.allow_net_connect!
|
||||
end
|
||||
|
||||
describe '#page_title' do
|
||||
context 'when title exists' do
|
||||
before do
|
||||
stub_request(:get, base_url)
|
||||
.to_return(body: '<html><head><title>Example Page</title></head></html>')
|
||||
end
|
||||
|
||||
it 'returns the page title' do
|
||||
expect(service.page_title).to eq('Example Page')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when title does not exist' do
|
||||
before do
|
||||
stub_request(:get, base_url)
|
||||
.to_return(body: '<html><head></head></html>')
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(service.page_title).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#page_links' do
|
||||
context 'with HTML page' do
|
||||
let(:html_content) do
|
||||
<<~HTML
|
||||
<html>
|
||||
<body>
|
||||
<a href="/relative">Relative Link</a>
|
||||
<a href="https://external.com">External Link</a>
|
||||
<a href="#anchor">Anchor Link</a>
|
||||
</body>
|
||||
</html>
|
||||
HTML
|
||||
end
|
||||
|
||||
before do
|
||||
stub_request(:get, base_url).to_return(body: html_content)
|
||||
end
|
||||
|
||||
it 'extracts and absolutizes all links' do
|
||||
links = service.page_links
|
||||
expect(links).to include(
|
||||
'https://example.com/relative',
|
||||
'https://external.com',
|
||||
'https://example.com#anchor'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with sitemap XML' do
|
||||
let(:sitemap_url) { 'https://example.com/sitemap.xml' }
|
||||
let(:sitemap_service) { described_class.new(sitemap_url) }
|
||||
let(:sitemap_content) do
|
||||
<<~XML
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://example.com/page1</loc>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://example.com/page2</loc>
|
||||
</url>
|
||||
</urlset>
|
||||
XML
|
||||
end
|
||||
|
||||
before do
|
||||
stub_request(:get, sitemap_url).to_return(body: sitemap_content)
|
||||
end
|
||||
|
||||
it 'extracts links from sitemap' do
|
||||
links = sitemap_service.page_links
|
||||
expect(links).to contain_exactly(
|
||||
'https://example.com/page1',
|
||||
'https://example.com/page2'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#body_text_content' do
|
||||
let(:html_content) do
|
||||
<<~HTML
|
||||
<html>
|
||||
<body>
|
||||
<h1>Main Title</h1>
|
||||
<p>Some <strong>formatted</strong> content.</p>
|
||||
<ul>
|
||||
<li>List item 1</li>
|
||||
<li>List item 2</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
HTML
|
||||
end
|
||||
|
||||
before do
|
||||
stub_request(:get, base_url).to_return(body: html_content)
|
||||
allow(ReverseMarkdown).to receive(:convert).and_return("# Main Title\n\nConverted markdown")
|
||||
end
|
||||
|
||||
it 'converts body content to markdown' do
|
||||
expect(service.body_text_content).to eq("# Main Title\n\nConverted markdown")
|
||||
expect(ReverseMarkdown).to have_received(:convert).with(
|
||||
kind_of(Nokogiri::XML::Element),
|
||||
unknown_tags: :bypass,
|
||||
github_flavored: true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#meta_description' do
|
||||
context 'when meta description exists' do
|
||||
before do
|
||||
stub_request(:get, base_url)
|
||||
.to_return(body: '<html><head><meta name="description" content="This is a test page description"></head></html>')
|
||||
end
|
||||
|
||||
it 'returns the meta description content' do
|
||||
expect(service.meta_description).to eq('This is a test page description')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when meta description does not exist' do
|
||||
before do
|
||||
stub_request(:get, base_url)
|
||||
.to_return(body: '<html><head><title>Test</title></head></html>')
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(service.meta_description).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#favicon_url' do
|
||||
context 'when favicon exists with relative URL' do
|
||||
before do
|
||||
stub_request(:get, base_url)
|
||||
.to_return(body: '<html><head><link rel="icon" href="/favicon.ico"></head></html>')
|
||||
end
|
||||
|
||||
it 'returns the resolved absolute favicon URL' do
|
||||
expect(service.favicon_url).to eq('https://example.com/favicon.ico')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when favicon exists with absolute URL' do
|
||||
before do
|
||||
stub_request(:get, base_url)
|
||||
.to_return(body: '<html><head><link rel="icon" href="https://cdn.example.com/favicon.ico"></head></html>')
|
||||
end
|
||||
|
||||
it 'returns the absolute favicon URL' do
|
||||
expect(service.favicon_url).to eq('https://cdn.example.com/favicon.ico')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when favicon does not exist' do
|
||||
before do
|
||||
stub_request(:get, base_url)
|
||||
.to_return(body: '<html><head><title>Test</title></head></html>')
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(service.favicon_url).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user