# Pull Request Template ## Description Document auto-sync job pipeline without wiring to controllers or FE Fixes # (issue) ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. specs and locally ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules
259 lines
8.3 KiB
Ruby
259 lines
8.3 KiB
Ruby
require 'rails_helper'
|
|
|
|
RSpec.describe Captain::Document, type: :model do
|
|
let(:account) { create(:account) }
|
|
let(:assistant) { create(:captain_assistant, account: account) }
|
|
|
|
describe 'URL normalization' do
|
|
it 'removes a trailing slash before validation' do
|
|
document = create(:captain_document,
|
|
assistant: assistant,
|
|
account: account,
|
|
external_link: 'https://example.com/path/')
|
|
|
|
expect(document.external_link).to eq('https://example.com/path')
|
|
end
|
|
end
|
|
|
|
describe 'PDF support' do
|
|
let(:pdf_document) do
|
|
doc = build(:captain_document, assistant: assistant, account: account)
|
|
doc.pdf_file.attach(
|
|
io: StringIO.new('PDF content'),
|
|
filename: 'test.pdf',
|
|
content_type: 'application/pdf'
|
|
)
|
|
doc
|
|
end
|
|
|
|
describe 'validations' do
|
|
it 'allows PDF file without external link' do
|
|
pdf_document.external_link = nil
|
|
expect(pdf_document).to be_valid
|
|
end
|
|
|
|
it 'validates PDF file size' do
|
|
doc = build(:captain_document, assistant: assistant, account: account)
|
|
doc.pdf_file.attach(
|
|
io: StringIO.new('x' * 11.megabytes),
|
|
filename: 'large.pdf',
|
|
content_type: 'application/pdf'
|
|
)
|
|
doc.external_link = nil
|
|
expect(doc).not_to be_valid
|
|
expect(doc.errors[:pdf_file]).to include(I18n.t('captain.documents.pdf_size_error'))
|
|
end
|
|
end
|
|
|
|
describe '#pdf_document?' do
|
|
it 'returns true for attached PDF' do
|
|
expect(pdf_document.pdf_document?).to be true
|
|
end
|
|
|
|
it 'returns true for .pdf external links' do
|
|
doc = build(:captain_document, external_link: 'https://example.com/document.pdf')
|
|
expect(doc.pdf_document?).to be true
|
|
end
|
|
|
|
it 'returns true for PDF:-prefixed external links even when the blob is missing' do
|
|
doc = build(:captain_document, external_link: 'PDF: report_20250101120000')
|
|
expect(doc.pdf_document?).to be true
|
|
end
|
|
|
|
it 'returns false for non-PDF documents' do
|
|
doc = build(:captain_document, external_link: 'https://example.com')
|
|
expect(doc.pdf_document?).to be false
|
|
end
|
|
end
|
|
|
|
describe '#display_url' do
|
|
it 'returns Rails blob URL for attached PDFs' do
|
|
pdf_document.save!
|
|
# The display_url method calls rails_blob_url which returns a URL containing 'rails/active_storage'
|
|
url = pdf_document.display_url
|
|
expect(url).to be_present
|
|
end
|
|
|
|
it 'returns external_link for web documents' do
|
|
doc = create(:captain_document, external_link: 'https://example.com')
|
|
expect(doc.display_url).to eq('https://example.com')
|
|
end
|
|
end
|
|
|
|
describe '#store_openai_file_id' do
|
|
it 'stores the file ID in metadata' do
|
|
pdf_document.save!
|
|
pdf_document.store_openai_file_id('file-abc123')
|
|
|
|
expect(pdf_document.reload.openai_file_id).to eq('file-abc123')
|
|
end
|
|
end
|
|
|
|
describe 'automatic external_link generation' do
|
|
it 'generates unique external_link for PDFs' do
|
|
pdf_document.external_link = nil
|
|
pdf_document.save!
|
|
|
|
expect(pdf_document.external_link).to start_with('PDF: test_')
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'response builder job callback' do
|
|
before { clear_enqueued_jobs }
|
|
|
|
describe 'non-PDF documents' do
|
|
it 'enqueues when created with available status and content' do
|
|
expect do
|
|
create(:captain_document, assistant: assistant, account: account, status: :available)
|
|
end.to have_enqueued_job(Captain::Documents::ResponseBuilderJob)
|
|
end
|
|
|
|
it 'does not enqueue when created available without content' do
|
|
expect do
|
|
create(:captain_document, assistant: assistant, account: account, status: :available, content: nil)
|
|
end.not_to have_enqueued_job(Captain::Documents::ResponseBuilderJob)
|
|
end
|
|
|
|
it 'enqueues when status transitions to available with existing content' do
|
|
document = create(:captain_document, assistant: assistant, account: account, status: :in_progress)
|
|
|
|
expect do
|
|
document.update!(status: :available)
|
|
end.to have_enqueued_job(Captain::Documents::ResponseBuilderJob)
|
|
end
|
|
|
|
it 'does not enqueue when status transitions to available without content' do
|
|
document = create(
|
|
:captain_document,
|
|
assistant: assistant,
|
|
account: account,
|
|
status: :in_progress,
|
|
content: nil
|
|
)
|
|
|
|
expect do
|
|
document.update!(status: :available)
|
|
end.not_to have_enqueued_job(Captain::Documents::ResponseBuilderJob)
|
|
end
|
|
|
|
it 'enqueues when content is populated on an available document' do
|
|
document = create(
|
|
:captain_document,
|
|
assistant: assistant,
|
|
account: account,
|
|
status: :available,
|
|
content: nil
|
|
)
|
|
clear_enqueued_jobs
|
|
|
|
expect do
|
|
document.update!(content: 'Fresh content from crawl')
|
|
end.to have_enqueued_job(Captain::Documents::ResponseBuilderJob)
|
|
end
|
|
|
|
it 'enqueues when content changes on an available document' do
|
|
document = create(
|
|
:captain_document,
|
|
assistant: assistant,
|
|
account: account,
|
|
status: :available,
|
|
content: 'Initial content'
|
|
)
|
|
clear_enqueued_jobs
|
|
|
|
expect do
|
|
document.update!(content: 'Updated crawl content')
|
|
end.to have_enqueued_job(Captain::Documents::ResponseBuilderJob)
|
|
end
|
|
|
|
it 'does not enqueue when content is cleared on an available document' do
|
|
document = create(
|
|
:captain_document,
|
|
assistant: assistant,
|
|
account: account,
|
|
status: :available,
|
|
content: 'Initial content'
|
|
)
|
|
clear_enqueued_jobs
|
|
|
|
expect do
|
|
document.update!(content: nil)
|
|
end.not_to have_enqueued_job(Captain::Documents::ResponseBuilderJob)
|
|
end
|
|
|
|
it 'does not enqueue for metadata-only updates' do
|
|
document = create(:captain_document, assistant: assistant, account: account, status: :available)
|
|
clear_enqueued_jobs
|
|
|
|
expect do
|
|
document.update!(metadata: { 'title' => 'Updated Again' })
|
|
end.not_to have_enqueued_job(Captain::Documents::ResponseBuilderJob)
|
|
end
|
|
|
|
it 'does not enqueue while document remains in progress' do
|
|
document = create(:captain_document, assistant: assistant, account: account, status: :in_progress)
|
|
|
|
expect do
|
|
document.update!(metadata: { 'title' => 'Updated' })
|
|
end.not_to have_enqueued_job(Captain::Documents::ResponseBuilderJob)
|
|
end
|
|
end
|
|
|
|
describe 'PDF documents' do
|
|
def build_pdf_document(status:, content:)
|
|
build(
|
|
:captain_document,
|
|
assistant: assistant,
|
|
account: account,
|
|
status: status,
|
|
content: content
|
|
).tap do |doc|
|
|
doc.pdf_file.attach(
|
|
io: StringIO.new('PDF content'),
|
|
filename: 'sample.pdf',
|
|
content_type: 'application/pdf'
|
|
)
|
|
end
|
|
end
|
|
|
|
it 'enqueues when created available without content' do
|
|
document = build_pdf_document(status: :available, content: nil)
|
|
|
|
expect do
|
|
document.save!
|
|
end.to have_enqueued_job(Captain::Documents::ResponseBuilderJob)
|
|
end
|
|
|
|
it 'enqueues when status transitions to available' do
|
|
document = build_pdf_document(status: :in_progress, content: nil)
|
|
document.save!
|
|
clear_enqueued_jobs
|
|
|
|
expect do
|
|
document.update!(status: :available)
|
|
end.to have_enqueued_job(Captain::Documents::ResponseBuilderJob)
|
|
end
|
|
|
|
it 'does not enqueue when content updates without status change' do
|
|
document = build_pdf_document(status: :available, content: nil)
|
|
document.save!
|
|
clear_enqueued_jobs
|
|
|
|
expect do
|
|
document.update!(content: 'Extracted PDF text')
|
|
end.not_to have_enqueued_job(Captain::Documents::ResponseBuilderJob)
|
|
end
|
|
end
|
|
|
|
it 'does not enqueue when the document is destroyed' do
|
|
document = create(:captain_document, assistant: assistant, account: account, status: :available)
|
|
clear_enqueued_jobs
|
|
|
|
expect do
|
|
document.destroy!
|
|
end.not_to have_enqueued_job(Captain::Documents::ResponseBuilderJob)
|
|
end
|
|
end
|
|
end
|