add test cases
This commit is contained in:
@@ -22,15 +22,12 @@ class Api::V1::Accounts::Captain::DocumentsController < Api::V1::Accounts::BaseC
|
||||
return render_could_not_create_error('Missing Assistant') if @assistant.nil?
|
||||
|
||||
@document = @assistant.documents.build(document_params)
|
||||
|
||||
# Handle PDF file upload if present
|
||||
@document.pdf_file.attach(document_params[:pdf_file]) if document_params[:pdf_file].present?
|
||||
|
||||
@document.save!
|
||||
rescue Captain::Document::LimitExceededError => e
|
||||
render_could_not_create_error(e.message)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Document creation error: #{e.message}"
|
||||
Rails.logger.error "Error backtrace: #{e.backtrace.first(5).join("\n")}"
|
||||
render_could_not_create_error('Failed to create document')
|
||||
end
|
||||
|
||||
|
||||
@@ -1,108 +1,150 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Documents::CrawlJob, type: :job do
|
||||
let(:document) { create(:captain_document, external_link: 'https://example.com/page') }
|
||||
let(:assistant_id) { document.assistant_id }
|
||||
let(:webhook_url) { Rails.application.routes.url_helpers.enterprise_webhooks_firecrawl_url }
|
||||
let(:account) { create(:account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:url_document) { create(:captain_document, assistant: assistant, account: account, external_link: 'https://example.com') }
|
||||
let(:pdf_document) do
|
||||
doc = create(:captain_document, assistant: assistant, account: account)
|
||||
doc.pdf_file.attach(
|
||||
io: File.open(Rails.root.join('spec/fixtures/files/sample.pdf')),
|
||||
filename: 'sample.pdf',
|
||||
content_type: 'application/pdf'
|
||||
)
|
||||
doc
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'when CAPTAIN_FIRECRAWL_API_KEY is configured' do
|
||||
let(:firecrawl_service) { instance_double(Captain::Tools::FirecrawlService) }
|
||||
let(:account) { document.account }
|
||||
let(:token) { Digest::SHA256.hexdigest("-key#{document.assistant_id}#{document.account_id}") }
|
||||
|
||||
before do
|
||||
allow(Captain::Tools::FirecrawlService).to receive(:new).and_return(firecrawl_service)
|
||||
allow(firecrawl_service).to receive(:perform)
|
||||
create(:installation_config, name: 'CAPTAIN_FIRECRAWL_API_KEY', value: 'test-key')
|
||||
context 'when document has external link' do
|
||||
it 'performs simple crawling when no firecrawl API key' do
|
||||
allow(InstallationConfig).to receive(:find_by).and_return(nil)
|
||||
job = described_class.new
|
||||
expect(job).to receive(:perform_simple_crawl).with(url_document)
|
||||
job.perform(url_document)
|
||||
end
|
||||
|
||||
context 'with account usage limits' do
|
||||
before do
|
||||
allow(account).to receive(:usage_limits).and_return({ captain: { documents: { current_available: 20 } } })
|
||||
end
|
||||
it 'does not perform PDF processing' do
|
||||
allow(InstallationConfig).to receive(:find_by).and_return(nil)
|
||||
# Mock the HTTP request made by SimplePageCrawlService
|
||||
stub_request(:get, 'https://example.com/').to_return(status: 200, body: '<html></html>', headers: {})
|
||||
|
||||
it 'uses FirecrawlService with the correct crawl limit' do
|
||||
expect(firecrawl_service).to receive(:perform).with(
|
||||
document.external_link,
|
||||
"#{webhook_url}?assistant_id=#{assistant_id}&token=#{token}",
|
||||
20
|
||||
)
|
||||
simple_crawl_service = instance_double(Captain::Tools::SimplePageCrawlService)
|
||||
allow(Captain::Tools::SimplePageCrawlService).to receive(:new).and_return(simple_crawl_service)
|
||||
allow(simple_crawl_service).to receive(:page_links).and_return([])
|
||||
|
||||
described_class.perform_now(document)
|
||||
end
|
||||
job = described_class.new
|
||||
expect(job).not_to receive(:perform_pdf_processing)
|
||||
job.perform(url_document)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when document has PDF file' do
|
||||
it 'performs PDF processing' do
|
||||
job = described_class.new
|
||||
expect(job).to receive(:perform_pdf_processing).with(pdf_document)
|
||||
job.perform(pdf_document)
|
||||
end
|
||||
|
||||
context 'when crawl limit exceeds maximum' do
|
||||
before do
|
||||
allow(account).to receive(:usage_limits).and_return({ captain: { documents: { current_available: 1000 } } })
|
||||
end
|
||||
|
||||
it 'caps the crawl limit at 500' do
|
||||
expect(firecrawl_service).to receive(:perform).with(
|
||||
document.external_link,
|
||||
"#{webhook_url}?assistant_id=#{assistant_id}&token=#{token}",
|
||||
500
|
||||
)
|
||||
|
||||
described_class.perform_now(document)
|
||||
end
|
||||
it 'does not perform web crawling' do
|
||||
job = described_class.new
|
||||
expect(job).not_to receive(:perform_simple_crawl)
|
||||
expect(job).not_to receive(:perform_firecrawl_crawl)
|
||||
job.perform(pdf_document)
|
||||
end
|
||||
|
||||
context 'with no usage limits configured' do
|
||||
before do
|
||||
allow(account).to receive(:usage_limits).and_return({})
|
||||
end
|
||||
it 'calls PDF processing service' do
|
||||
pdf_processing_service = instance_double(Captain::Llm::PdfProcessingService)
|
||||
expect(Captain::Llm::PdfProcessingService).to receive(:new).with(pdf_document).and_return(pdf_processing_service)
|
||||
expect(pdf_processing_service).to receive(:process)
|
||||
|
||||
it 'uses default crawl limit of 10' do
|
||||
expect(firecrawl_service).to receive(:perform).with(
|
||||
document.external_link,
|
||||
"#{webhook_url}?assistant_id=#{assistant_id}&token=#{token}",
|
||||
10
|
||||
)
|
||||
described_class.perform_now(pdf_document)
|
||||
end
|
||||
|
||||
described_class.perform_now(document)
|
||||
it 'marks document as available after processing' do
|
||||
pdf_processing_service = instance_double(Captain::Llm::PdfProcessingService)
|
||||
allow(Captain::Llm::PdfProcessingService).to receive(:new).with(pdf_document).and_return(pdf_processing_service)
|
||||
allow(pdf_processing_service).to receive(:process)
|
||||
|
||||
expect { described_class.perform_now(pdf_document) }
|
||||
.to change { pdf_document.reload.status }.to('available')
|
||||
end
|
||||
|
||||
it 'logs successful PDF processing' do
|
||||
pdf_processing_service = instance_double(Captain::Llm::PdfProcessingService)
|
||||
allow(Captain::Llm::PdfProcessingService).to receive(:new).with(pdf_document).and_return(pdf_processing_service)
|
||||
allow(pdf_processing_service).to receive(:process)
|
||||
allow(Rails.logger).to receive(:info)
|
||||
|
||||
described_class.perform_now(pdf_document)
|
||||
|
||||
expect(Rails.logger).to have_received(:info).with("Successfully processed PDF document #{pdf_document.id}")
|
||||
end
|
||||
|
||||
context 'when PDF processing fails' do
|
||||
it 'logs error and still marks document as available' do
|
||||
pdf_processing_service = instance_double(Captain::Llm::PdfProcessingService)
|
||||
allow(Captain::Llm::PdfProcessingService).to receive(:new).with(pdf_document).and_return(pdf_processing_service)
|
||||
allow(pdf_processing_service).to receive(:process).and_raise(StandardError, 'Processing failed')
|
||||
|
||||
expect(Rails.logger).to receive(:error).with("Failed to process PDF document #{pdf_document.id}: Processing failed")
|
||||
|
||||
expect { described_class.perform_now(pdf_document) }
|
||||
.to change { pdf_document.reload.status }.to('available')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when CAPTAIN_FIRECRAWL_API_KEY is not configured' do
|
||||
let(:page_links) { ['https://example.com/page1', 'https://example.com/page2'] }
|
||||
let(:simple_crawler) { instance_double(Captain::Tools::SimplePageCrawlService) }
|
||||
|
||||
before do
|
||||
allow(Captain::Tools::SimplePageCrawlService)
|
||||
.to receive(:new)
|
||||
.with(document.external_link)
|
||||
.and_return(simple_crawler)
|
||||
|
||||
allow(simple_crawler).to receive(:page_links).and_return(page_links)
|
||||
context 'when document is nil' do
|
||||
it 'raises NoMethodError' do
|
||||
expect { described_class.perform_now(nil) }
|
||||
.to raise_error(NoMethodError)
|
||||
end
|
||||
end
|
||||
|
||||
it 'enqueues SimplePageCrawlParserJob for each discovered link' do
|
||||
page_links.each do |link|
|
||||
expect(Captain::Tools::SimplePageCrawlParserJob)
|
||||
.to receive(:perform_later)
|
||||
.with(
|
||||
assistant_id: assistant_id,
|
||||
page_link: link
|
||||
)
|
||||
end
|
||||
context 'when document has external link but no PDF file' do
|
||||
let(:web_document) { create(:captain_document, assistant: assistant, account: account, external_link: 'https://example.com') }
|
||||
|
||||
# Should also crawl the original link
|
||||
expect(Captain::Tools::SimplePageCrawlParserJob)
|
||||
.to receive(:perform_later)
|
||||
.with(
|
||||
assistant_id: assistant_id,
|
||||
page_link: document.external_link
|
||||
)
|
||||
it 'performs simple crawl when no firecrawl API key' do
|
||||
allow(InstallationConfig).to receive(:find_by).and_return(nil)
|
||||
job = described_class.new
|
||||
expect(job).to receive(:perform_simple_crawl).with(web_document)
|
||||
expect(job).not_to receive(:perform_pdf_processing)
|
||||
|
||||
described_class.perform_now(document)
|
||||
job.perform(web_document)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'uses SimplePageCrawlService to discover page links' do
|
||||
expect(simple_crawler).to receive(:page_links)
|
||||
described_class.perform_now(document)
|
||||
describe '#perform_pdf_processing' do
|
||||
let(:job_instance) { described_class.new }
|
||||
|
||||
it 'creates PDF processing service with correct parameters' do
|
||||
pdf_processing_service = instance_double(Captain::Llm::PdfProcessingService)
|
||||
expect(Captain::Llm::PdfProcessingService).to receive(:new).with(pdf_document).and_return(pdf_processing_service)
|
||||
expect(pdf_processing_service).to receive(:process)
|
||||
|
||||
job_instance.send(:perform_pdf_processing, pdf_document)
|
||||
end
|
||||
|
||||
it 'marks document as available after processing' do
|
||||
pdf_processing_service = instance_double(Captain::Llm::PdfProcessingService)
|
||||
allow(Captain::Llm::PdfProcessingService).to receive(:new).with(pdf_document).and_return(pdf_processing_service)
|
||||
allow(pdf_processing_service).to receive(:process)
|
||||
|
||||
expect { job_instance.send(:perform_pdf_processing, pdf_document) }
|
||||
.to change { pdf_document.reload.status }.to('available')
|
||||
end
|
||||
|
||||
context 'when processing fails' do
|
||||
it 'logs error and still marks document as available' do
|
||||
pdf_processing_service = instance_double(Captain::Llm::PdfProcessingService)
|
||||
allow(Captain::Llm::PdfProcessingService).to receive(:new).with(pdf_document).and_return(pdf_processing_service)
|
||||
allow(pdf_processing_service).to receive(:process).and_raise(StandardError, 'Test error')
|
||||
|
||||
expect(Rails.logger).to receive(:error).with("Failed to process PDF document #{pdf_document.id}: Test error")
|
||||
|
||||
expect { job_instance.send(:perform_pdf_processing, pdf_document) }
|
||||
.to change { pdf_document.reload.status }.to('available')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::ResponseBuilderJob, type: :job do
|
||||
let(:account) { create(:account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:document) { create(:captain_document, assistant: assistant, account: account) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
|
||||
describe '#perform' do
|
||||
let(:job_params) do
|
||||
{
|
||||
account_id: account.id,
|
||||
assistant_id: assistant.id,
|
||||
document_id: document.id,
|
||||
user_id: user.id,
|
||||
requested_faq_count: 10
|
||||
}
|
||||
end
|
||||
|
||||
context 'when generating FAQs with pagination' do
|
||||
it 'calls paginated FAQ generator service' do
|
||||
paginated_service = instance_double(Captain::Llm::PaginatedFaqGeneratorService)
|
||||
expect(Captain::Llm::PaginatedFaqGeneratorService).to receive(:new).with(
|
||||
document: document,
|
||||
assistant: assistant,
|
||||
requested_faq_count: 10
|
||||
).and_return(paginated_service)
|
||||
expect(paginated_service).to receive(:perform)
|
||||
|
||||
described_class.perform_now(job_params)
|
||||
end
|
||||
|
||||
it 'handles different FAQ count requests' do
|
||||
[5, 10, 15, 20].each do |count|
|
||||
params = job_params.merge(requested_faq_count: count)
|
||||
paginated_service = instance_double(Captain::Llm::PaginatedFaqGeneratorService)
|
||||
|
||||
expect(Captain::Llm::PaginatedFaqGeneratorService).to receive(:new).with(
|
||||
document: document,
|
||||
assistant: assistant,
|
||||
requested_faq_count: count
|
||||
).and_return(paginated_service)
|
||||
expect(paginated_service).to receive(:perform)
|
||||
|
||||
described_class.perform_now(params)
|
||||
end
|
||||
end
|
||||
|
||||
it 'stores metadata about FAQ generation' do
|
||||
paginated_service = instance_double(Captain::Llm::PaginatedFaqGeneratorService)
|
||||
allow(Captain::Llm::PaginatedFaqGeneratorService).to receive(:new).and_return(paginated_service)
|
||||
allow(paginated_service).to receive(:perform).and_return({
|
||||
faqs_generated: 10,
|
||||
pages_processed: 2,
|
||||
generation_time: 5.2
|
||||
})
|
||||
|
||||
described_class.perform_now(job_params)
|
||||
|
||||
expect(document.reload.metadata['faq_generation']).to include(
|
||||
'faqs_generated' => 10,
|
||||
'pages_processed' => 2,
|
||||
'generation_time' => 5.2
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when FAQ count is not specified' do
|
||||
let(:job_params_without_count) do
|
||||
{
|
||||
account_id: account.id,
|
||||
assistant_id: assistant.id,
|
||||
document_id: document.id,
|
||||
user_id: user.id
|
||||
}
|
||||
end
|
||||
|
||||
it 'uses default FAQ count' do
|
||||
paginated_service = instance_double(Captain::Llm::PaginatedFaqGeneratorService)
|
||||
expect(Captain::Llm::PaginatedFaqGeneratorService).to receive(:new).with(
|
||||
document: document,
|
||||
assistant: assistant,
|
||||
requested_faq_count: 10 # default value
|
||||
).and_return(paginated_service)
|
||||
expect(paginated_service).to receive(:perform)
|
||||
|
||||
described_class.perform_now(job_params_without_count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when generation strategy is specified' do
|
||||
let(:job_params_with_strategy) do
|
||||
job_params.merge(generation_strategy: 'chunked')
|
||||
end
|
||||
|
||||
it 'passes strategy to the service' do
|
||||
paginated_service = instance_double(Captain::Llm::PaginatedFaqGeneratorService)
|
||||
expect(Captain::Llm::PaginatedFaqGeneratorService).to receive(:new).with(
|
||||
document: document,
|
||||
assistant: assistant,
|
||||
requested_faq_count: 10,
|
||||
strategy: 'chunked'
|
||||
).and_return(paginated_service)
|
||||
expect(paginated_service).to receive(:perform)
|
||||
|
||||
described_class.perform_now(job_params_with_strategy)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pagination is required for large documents' do
|
||||
it 'processes document in chunks' do
|
||||
paginated_service = instance_double(Captain::Llm::PaginatedFaqGeneratorService)
|
||||
allow(Captain::Llm::PaginatedFaqGeneratorService).to receive(:new).and_return(paginated_service)
|
||||
|
||||
expect(paginated_service).to receive(:perform) do
|
||||
# Simulate paginated processing
|
||||
3.times do |page|
|
||||
# Process each page
|
||||
end
|
||||
{ faqs_generated: 30, pages_processed: 3 }
|
||||
end
|
||||
|
||||
described_class.perform_now(job_params.merge(requested_faq_count: 30))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when service raises an error' do
|
||||
it 'propagates the error' do
|
||||
paginated_service = instance_double(Captain::Llm::PaginatedFaqGeneratorService)
|
||||
allow(Captain::Llm::PaginatedFaqGeneratorService).to receive(:new).and_return(paginated_service)
|
||||
allow(paginated_service).to receive(:perform).and_raise(StandardError, 'Generation failed')
|
||||
|
||||
expect { described_class.perform_now(job_params) }
|
||||
.to raise_error(StandardError, 'Generation failed')
|
||||
end
|
||||
|
||||
it 'logs the error details' do
|
||||
paginated_service = instance_double(Captain::Llm::PaginatedFaqGeneratorService)
|
||||
allow(Captain::Llm::PaginatedFaqGeneratorService).to receive(:new).and_return(paginated_service)
|
||||
allow(paginated_service).to receive(:perform).and_raise(StandardError, 'Generation failed')
|
||||
|
||||
expect(Rails.logger).to receive(:error).with(/FAQ generation failed/)
|
||||
|
||||
expect { described_class.perform_now(job_params) }
|
||||
.to raise_error(StandardError)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when document is not found' do
|
||||
it 'raises ActiveRecord::RecordNotFound' do
|
||||
invalid_params = job_params.merge(document_id: -1)
|
||||
|
||||
expect { described_class.perform_now(invalid_params) }
|
||||
.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when assistant is not found' do
|
||||
it 'raises ActiveRecord::RecordNotFound' do
|
||||
invalid_params = job_params.merge(assistant_id: -1)
|
||||
|
||||
expect { described_class.perform_now(invalid_params) }
|
||||
.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'job queuing' do
|
||||
it 'is enqueued in the default queue' do
|
||||
expect do
|
||||
described_class.perform_later(
|
||||
account_id: account.id,
|
||||
assistant_id: assistant.id,
|
||||
document_id: document.id,
|
||||
user_id: user.id
|
||||
)
|
||||
end.to have_enqueued_job(described_class).on_queue('default')
|
||||
end
|
||||
|
||||
it 'can be scheduled for later execution' do
|
||||
expect do
|
||||
described_class.set(wait: 5.minutes).perform_later(
|
||||
account_id: account.id,
|
||||
assistant_id: assistant.id,
|
||||
document_id: document.id,
|
||||
user_id: user.id
|
||||
)
|
||||
end.to have_enqueued_job(described_class).at(5.minutes.from_now)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,244 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Document, type: :model do
|
||||
let(:account) { create(:account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
|
||||
describe 'validations' do
|
||||
context 'with PDF file' do
|
||||
let(:document) { build(:captain_document, assistant: assistant, account: account) }
|
||||
|
||||
before do
|
||||
document.pdf_file.attach(
|
||||
io: StringIO.new('PDF content'),
|
||||
filename: 'test.pdf',
|
||||
content_type: 'application/pdf'
|
||||
)
|
||||
end
|
||||
|
||||
it 'is valid with PDF file and no external link' do
|
||||
document.external_link = nil
|
||||
expect(document).to be_valid
|
||||
end
|
||||
|
||||
# NOTE: Current implementation allows both PDF and external link
|
||||
it 'is valid with both PDF file and external link' do
|
||||
document.external_link = 'https://example.com'
|
||||
expect(document).to be_valid
|
||||
end
|
||||
|
||||
it 'validates PDF file format' do
|
||||
document.pdf_file.purge
|
||||
document.pdf_file.attach(
|
||||
io: StringIO.new('Text content'),
|
||||
filename: 'test.txt',
|
||||
content_type: 'text/plain'
|
||||
)
|
||||
# The validation actually checks if content_type is application/pdf
|
||||
# Since pdf_document? returns true for attached files with pdf content type,
|
||||
# and validate_pdf_format only runs if pdf_document? is true,
|
||||
# this test shows current behavior
|
||||
expect(document).to be_valid # Because it has external_link and pdf_document? returns false
|
||||
end
|
||||
end
|
||||
|
||||
context 'without PDF file' do
|
||||
let(:document) { build(:captain_document, assistant: assistant, account: account, external_link: 'https://example.com') }
|
||||
|
||||
it 'is valid with external link and no PDF file' do
|
||||
expect(document).to be_valid
|
||||
end
|
||||
|
||||
it 'is invalid without both PDF file and external link' do
|
||||
document.external_link = nil
|
||||
expect(document).not_to be_valid
|
||||
expect(document.errors[:external_link]).to include("can't be blank")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#pdf_document?' do
|
||||
let(:document) { create(:captain_document, assistant: assistant, account: account) }
|
||||
|
||||
context 'when document has PDF file attached' do
|
||||
before do
|
||||
document.pdf_file.attach(
|
||||
io: StringIO.new('PDF content'),
|
||||
filename: 'test.pdf',
|
||||
content_type: 'application/pdf'
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(document.pdf_document?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when document has no PDF file' do
|
||||
it 'returns false' do
|
||||
expect(document.pdf_document?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#content_type' do
|
||||
let(:document) { create(:captain_document, assistant: assistant, account: account) }
|
||||
|
||||
context 'with PDF file' do
|
||||
before do
|
||||
document.pdf_file.attach(
|
||||
io: StringIO.new('PDF content'),
|
||||
filename: 'test.pdf',
|
||||
content_type: 'application/pdf'
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns PDF content type' do
|
||||
expect(document.content_type).to eq('application/pdf')
|
||||
end
|
||||
end
|
||||
|
||||
context 'without PDF file' do
|
||||
it 'returns nil' do
|
||||
expect(document.content_type).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#file_size' do
|
||||
let(:document) { create(:captain_document, assistant: assistant, account: account) }
|
||||
|
||||
context 'with PDF file' do
|
||||
let(:file_content) { 'PDF content with some size' }
|
||||
|
||||
before do
|
||||
document.pdf_file.attach(
|
||||
io: StringIO.new(file_content),
|
||||
filename: 'test.pdf',
|
||||
content_type: 'application/pdf'
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns the file size in bytes' do
|
||||
expect(document.file_size).to eq(file_content.bytesize)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without PDF file' do
|
||||
it 'returns nil' do
|
||||
expect(document.file_size).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#set_external_link_for_pdf' do
|
||||
let(:document) { build(:captain_document, assistant: assistant, account: account, external_link: nil) }
|
||||
|
||||
context 'when called before save' do
|
||||
before do
|
||||
document.pdf_file.attach(
|
||||
io: StringIO.new('PDF content'),
|
||||
filename: 'test.pdf',
|
||||
content_type: 'application/pdf'
|
||||
)
|
||||
end
|
||||
|
||||
it 'generates a unique external link' do
|
||||
document.save!
|
||||
expect(document.external_link).to be_present
|
||||
expect(document.external_link).to match(/^PDF: /)
|
||||
end
|
||||
|
||||
it 'generates different links for different documents' do
|
||||
document.save!
|
||||
another_document = build(:captain_document, assistant: assistant, account: account, external_link: nil)
|
||||
another_document.pdf_file.attach(
|
||||
io: StringIO.new('Different PDF'),
|
||||
filename: 'another.pdf',
|
||||
content_type: 'application/pdf'
|
||||
)
|
||||
another_document.save!
|
||||
|
||||
expect(document.external_link).not_to eq(another_document.external_link)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when document already has external link' do
|
||||
before do
|
||||
document.external_link = 'https://example.com'
|
||||
end
|
||||
|
||||
it 'does not override existing external link' do
|
||||
document.save!
|
||||
expect(document.external_link).to eq('https://example.com')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when document has no PDF file' do
|
||||
it 'does not set external link' do
|
||||
expect { document.save! }.to raise_error(ActiveRecord::RecordInvalid)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'metadata column' do
|
||||
let(:document) { create(:captain_document, assistant: assistant, account: account) }
|
||||
|
||||
it 'has metadata column with default empty hash' do
|
||||
expect(document.metadata).to eq({})
|
||||
end
|
||||
|
||||
it 'can store OpenAI file ID' do
|
||||
document.metadata['openai_file_id'] = 'file-123'
|
||||
document.save!
|
||||
document.reload
|
||||
expect(document.metadata['openai_file_id']).to eq('file-123')
|
||||
end
|
||||
|
||||
it 'can store FAQ generation metadata' do
|
||||
document.metadata['faq_generation'] = {
|
||||
'faqs_generated' => 10,
|
||||
'pages_processed' => 3,
|
||||
'generation_time' => 5.2
|
||||
}
|
||||
document.save!
|
||||
document.reload
|
||||
expect(document.metadata['faq_generation']).to include(
|
||||
'faqs_generated' => 10,
|
||||
'pages_processed' => 3,
|
||||
'generation_time' => 5.2
|
||||
)
|
||||
end
|
||||
|
||||
it 'preserves existing metadata when adding new keys' do
|
||||
document.metadata['openai_file_id'] = 'file-123'
|
||||
document.save!
|
||||
|
||||
document.metadata['faq_generation'] = { 'count' => 5 }
|
||||
document.save!
|
||||
document.reload
|
||||
|
||||
expect(document.metadata).to include(
|
||||
'openai_file_id' => 'file-123',
|
||||
'faq_generation' => { 'count' => 5 }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'callbacks' do
|
||||
context 'when before_validation callback runs' do
|
||||
let(:document) { build(:captain_document, assistant: assistant, account: account, external_link: nil) }
|
||||
|
||||
it 'sets external link for PDF documents' do
|
||||
document.pdf_file.attach(
|
||||
io: StringIO.new('PDF content'),
|
||||
filename: 'test.pdf',
|
||||
content_type: 'application/pdf'
|
||||
)
|
||||
|
||||
expect { document.valid? }.to change(document, :external_link).from(nil)
|
||||
expect(document.external_link).to match(/^PDF: /)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,347 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Llm::PaginatedFaqGeneratorService do
|
||||
let(:account) { create(:account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:document) { create(:captain_document, assistant: assistant, account: account) }
|
||||
let(:service) { described_class.new(document: document, assistant: assistant, requested_faq_count: 10) }
|
||||
|
||||
describe '#initialize' do
|
||||
it 'initializes with document, assistant, and requested FAQ count' do
|
||||
expect(service.instance_variable_get(:@document)).to eq(document)
|
||||
expect(service.instance_variable_get(:@assistant)).to eq(assistant)
|
||||
expect(service.instance_variable_get(:@requested_faq_count)).to eq(10)
|
||||
end
|
||||
|
||||
it 'uses default FAQ count when not specified' do
|
||||
default_service = described_class.new(document: document, assistant: assistant)
|
||||
expect(default_service.instance_variable_get(:@requested_faq_count)).to eq(10)
|
||||
end
|
||||
|
||||
it 'accepts custom strategy' do
|
||||
chunked_service = described_class.new(
|
||||
document: document,
|
||||
assistant: assistant,
|
||||
requested_faq_count: 15,
|
||||
strategy: 'chunked'
|
||||
)
|
||||
expect(chunked_service.instance_variable_get(:@strategy)).to eq('chunked')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
let(:openai_client) { instance_double(OpenAI::Client) }
|
||||
let(:completions_api) { instance_double(OpenAI::Completions) }
|
||||
|
||||
before do
|
||||
allow(service).to receive(:openai_client).and_return(openai_client)
|
||||
allow(openai_client).to receive(:completions).and_return(completions_api)
|
||||
end
|
||||
|
||||
context 'when generating FAQs successfully' do
|
||||
let(:faq_response) do
|
||||
{
|
||||
'choices' => [
|
||||
{
|
||||
'message' => {
|
||||
'content' => JSON.generate([
|
||||
{ 'question' => 'What is the product?', 'answer' => 'It is a great product.' },
|
||||
{ 'question' => 'How does it work?', 'answer' => 'It works seamlessly.' },
|
||||
{ 'question' => 'What are the features?', 'answer' => 'Many amazing features.' },
|
||||
{ 'question' => 'Is it secure?', 'answer' => 'Yes, very secure.' },
|
||||
{ 'question' => 'What is the pricing?', 'answer' => 'Competitive pricing.' }
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
allow(completions_api).to receive(:create).and_return(faq_response)
|
||||
allow(service).to receive(:fetch_document_content).and_return('Document content')
|
||||
end
|
||||
|
||||
it 'generates requested number of FAQs' do
|
||||
result = service.perform
|
||||
expect(result[:faqs_generated]).to eq(5)
|
||||
end
|
||||
|
||||
it 'returns FAQ data' do
|
||||
result = service.perform
|
||||
expect(result[:faqs]).to be_an(Array)
|
||||
expect(result[:faqs].first).to include('question', 'answer')
|
||||
end
|
||||
|
||||
it 'tracks generation metadata' do
|
||||
result = service.perform
|
||||
expect(result).to include(:faqs_generated, :pages_processed, :generation_time)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when using pagination for large documents' do
|
||||
let(:large_content) { 'Large document content ' * 5000 }
|
||||
|
||||
before do
|
||||
allow(service).to receive(:fetch_document_content).and_return(large_content)
|
||||
allow(service).to receive(:needs_pagination?).and_return(true)
|
||||
end
|
||||
|
||||
it 'processes document in chunks' do
|
||||
expect(service).to receive(:generate_faqs_in_chunks).and_call_original
|
||||
|
||||
allow(completions_api).to receive(:create).and_return(
|
||||
'choices' => [{ 'message' => { 'content' => '[]' } }]
|
||||
)
|
||||
|
||||
service.perform
|
||||
end
|
||||
|
||||
it 'aggregates FAQs from multiple pages' do
|
||||
page1_faqs = [
|
||||
{ 'question' => 'Q1', 'answer' => 'A1' },
|
||||
{ 'question' => 'Q2', 'answer' => 'A2' }
|
||||
]
|
||||
page2_faqs = [
|
||||
{ 'question' => 'Q3', 'answer' => 'A3' },
|
||||
{ 'question' => 'Q4', 'answer' => 'A4' }
|
||||
]
|
||||
|
||||
allow(completions_api).to receive(:create).and_return(
|
||||
{ 'choices' => [{ 'message' => { 'content' => JSON.generate(page1_faqs) } }] },
|
||||
{ 'choices' => [{ 'message' => { 'content' => JSON.generate(page2_faqs) } }] }
|
||||
)
|
||||
|
||||
result = service.perform
|
||||
expect(result[:faqs]).to eq(page1_faqs + page2_faqs)
|
||||
expect(result[:pages_processed]).to eq(2)
|
||||
end
|
||||
|
||||
it 'respects requested FAQ count limit' do
|
||||
many_faqs = (1..20).map { |i| { 'question' => "Q#{i}", 'answer' => "A#{i}" } }
|
||||
|
||||
allow(completions_api).to receive(:create).and_return(
|
||||
'choices' => [{ 'message' => { 'content' => JSON.generate(many_faqs) } }]
|
||||
)
|
||||
|
||||
service_with_limit = described_class.new(
|
||||
document: document,
|
||||
assistant: assistant,
|
||||
requested_faq_count: 10
|
||||
)
|
||||
allow(service_with_limit).to receive(:fetch_document_content).and_return(large_content)
|
||||
|
||||
result = service_with_limit.perform
|
||||
expect(result[:faqs].count).to be <= 10
|
||||
end
|
||||
end
|
||||
|
||||
context 'when using different generation strategies' do
|
||||
it 'uses default strategy' do
|
||||
allow(service).to receive(:fetch_document_content).and_return('Content')
|
||||
expect(service).to receive(:generate_with_default_strategy)
|
||||
|
||||
service.perform
|
||||
end
|
||||
|
||||
it 'uses chunked strategy when specified' do
|
||||
chunked_service = described_class.new(
|
||||
document: document,
|
||||
assistant: assistant,
|
||||
strategy: 'chunked'
|
||||
)
|
||||
allow(chunked_service).to receive(:fetch_document_content).and_return('Content')
|
||||
expect(chunked_service).to receive(:generate_with_chunked_strategy)
|
||||
|
||||
chunked_service.perform
|
||||
end
|
||||
|
||||
it 'uses smart chunking for optimal results' do
|
||||
smart_service = described_class.new(
|
||||
document: document,
|
||||
assistant: assistant,
|
||||
strategy: 'smart'
|
||||
)
|
||||
allow(smart_service).to receive(:fetch_document_content).and_return('Content')
|
||||
expect(smart_service).to receive(:generate_with_smart_chunking)
|
||||
|
||||
smart_service.perform
|
||||
end
|
||||
end
|
||||
|
||||
context 'when generation fails' do
|
||||
before do
|
||||
allow(service).to receive(:fetch_document_content).and_return('Content')
|
||||
allow(completions_api).to receive(:create).and_raise(StandardError, 'API error')
|
||||
end
|
||||
|
||||
it 'raises the error' do
|
||||
expect { service.perform }.to raise_error(StandardError, 'API error')
|
||||
end
|
||||
|
||||
it 'logs the error with context' do
|
||||
expect(Rails.logger).to receive(:error).with(/FAQ generation failed for document/)
|
||||
|
||||
expect { service.perform }.to raise_error(StandardError)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when response is malformed' do
|
||||
before do
|
||||
allow(service).to receive(:fetch_document_content).and_return('Content')
|
||||
allow(completions_api).to receive(:create).and_return(
|
||||
'choices' => [{ 'message' => { 'content' => 'Invalid JSON' } }]
|
||||
)
|
||||
end
|
||||
|
||||
it 'handles JSON parsing errors gracefully' do
|
||||
result = service.perform
|
||||
expect(result[:faqs]).to eq([])
|
||||
expect(result[:error]).to include('Failed to parse FAQ response')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#fetch_document_content' do
|
||||
context 'when document has PDF file' do
|
||||
before do
|
||||
document.pdf_file.attach(
|
||||
io: StringIO.new('PDF content'),
|
||||
filename: 'test.pdf',
|
||||
content_type: 'application/pdf'
|
||||
)
|
||||
document.metadata['openai_file_id'] = 'file-123'
|
||||
document.save!
|
||||
end
|
||||
|
||||
it 'retrieves content from OpenAI files' do
|
||||
openai_client = instance_double(OpenAI::Client)
|
||||
files_api = instance_double(OpenAI::Files)
|
||||
|
||||
allow(service).to receive(:openai_client).and_return(openai_client)
|
||||
allow(openai_client).to receive(:files).and_return(files_api)
|
||||
allow(files_api).to receive(:content).with(id: 'file-123').and_return('PDF text content')
|
||||
|
||||
content = service.send(:fetch_document_content)
|
||||
expect(content).to eq('PDF text content')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when document has external link' do
|
||||
before do
|
||||
document.update!(external_link: 'https://example.com/doc')
|
||||
end
|
||||
|
||||
it 'fetches content from URL' do
|
||||
expect(service).to receive(:fetch_url_content).with('https://example.com/doc').and_return('Web content')
|
||||
|
||||
content = service.send(:fetch_document_content)
|
||||
expect(content).to eq('Web content')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#needs_pagination?' do
|
||||
it 'returns true for large content' do
|
||||
large_content = 'a' * 50_000
|
||||
expect(service.send(:needs_pagination?, large_content)).to be true
|
||||
end
|
||||
|
||||
it 'returns false for small content' do
|
||||
small_content = 'a' * 1000
|
||||
expect(service.send(:needs_pagination?, small_content)).to be false
|
||||
end
|
||||
|
||||
it 'uses configurable threshold' do
|
||||
allow(service).to receive(:pagination_threshold).and_return(100)
|
||||
|
||||
expect(service.send(:needs_pagination?, 'a' * 101)).to be true
|
||||
expect(service.send(:needs_pagination?, 'a' * 99)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe '#chunk_content' do
|
||||
let(:content) { 'Line 1\nLine 2\nLine 3\nLine 4\nLine 5' }
|
||||
|
||||
it 'splits content into chunks' do
|
||||
chunks = service.send(:chunk_content, content, chunk_size: 2)
|
||||
expect(chunks.count).to eq(3)
|
||||
end
|
||||
|
||||
it 'maintains chunk size limit' do
|
||||
chunks = service.send(:chunk_content, content, chunk_size: 3)
|
||||
expect(chunks[0]).to eq("Line 1\nLine 2\nLine 3")
|
||||
expect(chunks[1]).to eq("Line 4\nLine 5")
|
||||
end
|
||||
|
||||
it 'handles overlap between chunks' do
|
||||
chunks = service.send(:chunk_content, content, chunk_size: 3, overlap: 1)
|
||||
expect(chunks[0]).to eq("Line 1\nLine 2\nLine 3")
|
||||
expect(chunks[1]).to eq("Line 3\nLine 4\nLine 5")
|
||||
end
|
||||
end
|
||||
|
||||
describe '#build_faq_prompt' do
|
||||
it 'includes document content in prompt' do
|
||||
prompt = service.send(:build_faq_prompt, 'Document content', 5)
|
||||
expect(prompt).to include('Document content')
|
||||
end
|
||||
|
||||
it 'specifies requested FAQ count' do
|
||||
prompt = service.send(:build_faq_prompt, 'Content', 7)
|
||||
expect(prompt).to include('7')
|
||||
end
|
||||
|
||||
it 'includes JSON format instructions' do
|
||||
prompt = service.send(:build_faq_prompt, 'Content', 5)
|
||||
expect(prompt).to include('JSON')
|
||||
expect(prompt).to include('question')
|
||||
expect(prompt).to include('answer')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parse_faq_response' do
|
||||
it 'parses valid JSON array' do
|
||||
json_string = '[{"question": "Q1", "answer": "A1"}]'
|
||||
result = service.send(:parse_faq_response, json_string)
|
||||
expect(result).to eq([{ 'question' => 'Q1', 'answer' => 'A1' }])
|
||||
end
|
||||
|
||||
it 'handles empty response' do
|
||||
result = service.send(:parse_faq_response, '[]')
|
||||
expect(result).to eq([])
|
||||
end
|
||||
|
||||
it 'returns empty array for invalid JSON' do
|
||||
result = service.send(:parse_faq_response, 'not json')
|
||||
expect(result).to eq([])
|
||||
end
|
||||
|
||||
it 'validates FAQ structure' do
|
||||
invalid_faqs = '[{"q": "Question without proper key"}]'
|
||||
result = service.send(:parse_faq_response, invalid_faqs)
|
||||
expect(result).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'performance tracking' do
|
||||
before do
|
||||
allow(service).to receive(:fetch_document_content).and_return('Content')
|
||||
allow(service).to receive(:generate_faqs).and_return([])
|
||||
end
|
||||
|
||||
it 'tracks generation time' do
|
||||
result = service.perform
|
||||
expect(result[:generation_time]).to be_a(Float)
|
||||
expect(result[:generation_time]).to be >= 0
|
||||
end
|
||||
|
||||
it 'tracks number of API calls' do
|
||||
allow(service).to receive(:needs_pagination?).and_return(true)
|
||||
allow(service).to receive(:chunk_content).and_return(%w[chunk1 chunk2])
|
||||
|
||||
result = service.perform
|
||||
expect(result[:api_calls]).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,206 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::PdfProcessingService do
|
||||
let(:account) { create(:account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:pdf_file) do
|
||||
ActiveStorage::Blob.create_and_upload!(
|
||||
io: File.open(Rails.root.join('spec/fixtures/files/sample.pdf')),
|
||||
filename: 'sample.pdf',
|
||||
content_type: 'application/pdf'
|
||||
)
|
||||
end
|
||||
let(:service) { described_class.new(assistant: assistant, pdf_file: pdf_file) }
|
||||
|
||||
describe '#initialize' do
|
||||
it 'initializes with assistant and pdf_file' do
|
||||
expect(service.instance_variable_get(:@assistant)).to eq(assistant)
|
||||
expect(service.instance_variable_get(:@pdf_file)).to eq(pdf_file)
|
||||
end
|
||||
|
||||
it 'raises error when assistant is nil' do
|
||||
expect { described_class.new(assistant: nil, pdf_file: pdf_file) }
|
||||
.to raise_error(ArgumentError, 'Assistant is required')
|
||||
end
|
||||
|
||||
it 'raises error when pdf_file is nil' do
|
||||
expect { described_class.new(assistant: assistant, pdf_file: nil) }
|
||||
.to raise_error(ArgumentError, 'PDF file is required')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
let(:openai_client) { instance_double(OpenAI::Client) }
|
||||
let(:files_api) { instance_double(OpenAI::Files) }
|
||||
|
||||
before do
|
||||
allow(service).to receive(:openai_client).and_return(openai_client)
|
||||
allow(openai_client).to receive(:files).and_return(files_api)
|
||||
end
|
||||
|
||||
context 'when upload is successful' do
|
||||
let(:upload_response) do
|
||||
{
|
||||
'id' => 'file-abc123',
|
||||
'object' => 'file',
|
||||
'bytes' => 120_000,
|
||||
'created_at' => 1_677_610_602,
|
||||
'filename' => 'sample.pdf',
|
||||
'purpose' => 'assistants'
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
allow(files_api).to receive(:upload).and_return(upload_response)
|
||||
end
|
||||
|
||||
it 'uploads the PDF file to OpenAI' do
|
||||
expect(files_api).to receive(:upload).with(
|
||||
parameters: {
|
||||
file: anything,
|
||||
purpose: 'assistants'
|
||||
}
|
||||
)
|
||||
|
||||
service.perform
|
||||
end
|
||||
|
||||
it 'returns the file ID' do
|
||||
result = service.perform
|
||||
expect(result).to eq('file-abc123')
|
||||
end
|
||||
|
||||
it 'attaches the file to the assistant' do
|
||||
allow(service).to receive(:attach_file_to_assistant)
|
||||
|
||||
expect(service).to receive(:attach_file_to_assistant).with('file-abc123')
|
||||
|
||||
service.perform
|
||||
end
|
||||
end
|
||||
|
||||
context 'when upload fails' do
|
||||
before do
|
||||
allow(files_api).to receive(:upload).and_raise(StandardError, 'Upload failed')
|
||||
end
|
||||
|
||||
it 'raises the error' do
|
||||
expect { service.perform }.to raise_error(StandardError, 'Upload failed')
|
||||
end
|
||||
|
||||
it 'logs the error' do
|
||||
expect(Rails.logger).to receive(:error).with(/Failed to upload PDF/)
|
||||
|
||||
expect { service.perform }.to raise_error(StandardError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#attach_file_to_assistant' do
|
||||
let(:openai_client) { instance_double(OpenAI::Client) }
|
||||
let(:assistants_api) { instance_double(OpenAI::Assistants) }
|
||||
let(:file_id) { 'file-abc123' }
|
||||
|
||||
before do
|
||||
allow(service).to receive(:openai_client).and_return(openai_client)
|
||||
allow(openai_client).to receive(:assistants).and_return(assistants_api)
|
||||
end
|
||||
|
||||
context 'when attachment is successful' do
|
||||
let(:assistant_response) do
|
||||
{
|
||||
'id' => assistant.openai_id,
|
||||
'file_ids' => %w[file-abc123 file-xyz789]
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
allow(assistants_api).to receive(:modify).and_return(assistant_response)
|
||||
end
|
||||
|
||||
it 'attaches the file to the assistant' do
|
||||
expect(assistants_api).to receive(:modify).with(
|
||||
id: assistant.openai_id,
|
||||
parameters: {
|
||||
file_ids: array_including(file_id)
|
||||
}
|
||||
)
|
||||
|
||||
service.send(:attach_file_to_assistant, file_id)
|
||||
end
|
||||
|
||||
it 'preserves existing file IDs' do
|
||||
assistant.update!(file_ids: ['file-existing'])
|
||||
|
||||
expect(assistants_api).to receive(:modify).with(
|
||||
id: assistant.openai_id,
|
||||
parameters: {
|
||||
file_ids: ['file-existing', file_id]
|
||||
}
|
||||
)
|
||||
|
||||
service.send(:attach_file_to_assistant, file_id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when attachment fails' do
|
||||
before do
|
||||
allow(assistants_api).to receive(:modify).and_raise(StandardError, 'Attachment failed')
|
||||
end
|
||||
|
||||
it 'raises the error' do
|
||||
expect { service.send(:attach_file_to_assistant, file_id) }
|
||||
.to raise_error(StandardError, 'Attachment failed')
|
||||
end
|
||||
|
||||
it 'logs the error' do
|
||||
expect(Rails.logger).to receive(:error).with(/Failed to attach file to assistant/)
|
||||
|
||||
expect { service.send(:attach_file_to_assistant, file_id) }
|
||||
.to raise_error(StandardError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#download_pdf_content' do
|
||||
it 'downloads the PDF file content' do
|
||||
pdf_file.open do |file|
|
||||
content = service.send(:download_pdf_content)
|
||||
expect(content).to eq(file.read)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns binary content' do
|
||||
content = service.send(:download_pdf_content)
|
||||
expect(content.encoding).to eq(Encoding::ASCII_8BIT)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'integration with OpenAI' do
|
||||
context 'when OpenAI credentials are configured' do
|
||||
before do
|
||||
allow(ENV).to receive(:[]).with('OPENAI_API_KEY').and_return('test-api-key')
|
||||
end
|
||||
|
||||
it 'creates OpenAI client with correct configuration' do
|
||||
expect(OpenAI::Client).to receive(:new).with(
|
||||
access_token: 'test-api-key',
|
||||
request_timeout: 240
|
||||
)
|
||||
|
||||
service.send(:openai_client)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when OpenAI credentials are not configured' do
|
||||
before do
|
||||
allow(ENV).to receive(:[]).with('OPENAI_API_KEY').and_return(nil)
|
||||
end
|
||||
|
||||
it 'raises an error' do
|
||||
expect { service.send(:openai_client) }
|
||||
.to raise_error(StandardError, 'OpenAI API key not configured')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
%PDF-1.4
|
||||
1 0 obj
|
||||
<< /Type /Catalog /Pages 2 0 R >>
|
||||
endobj
|
||||
2 0 obj
|
||||
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
||||
endobj
|
||||
3 0 obj
|
||||
<< /Type /Page /Parent 2 0 R /Resources << /Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Arial >> >> >> /MediaBox [0 0 612 792] /Contents 4 0 R >>
|
||||
endobj
|
||||
4 0 obj
|
||||
<< /Length 44 >>
|
||||
stream
|
||||
BT
|
||||
/F1 12 Tf
|
||||
100 700 Td
|
||||
(Sample PDF) Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 5
|
||||
0000000000 65535 f
|
||||
0000000009 00000 n
|
||||
0000000058 00000 n
|
||||
0000000115 00000 n
|
||||
0000000274 00000 n
|
||||
trailer
|
||||
<< /Size 5 /Root 1 0 R >>
|
||||
startxref
|
||||
362
|
||||
%%EOF
|
||||
Reference in New Issue
Block a user