Merge branch 'develop' into feat/expand-idb-coverage
This commit is contained in:
@@ -8,12 +8,12 @@ RSpec.describe 'Google::CallbacksController', type: :request do
|
||||
|
||||
describe 'GET /google/callback' do
|
||||
let(:response_body_success) do
|
||||
{ id_token: JWT.encode({ email: email, name: 'test' }, false), access_token: SecureRandom.hex(10), token_type: 'Bearer',
|
||||
{ id_token: JWT.encode({ email: email, name: 'test' }, nil, 'none'), access_token: SecureRandom.hex(10), token_type: 'Bearer',
|
||||
refresh_token: SecureRandom.hex(10) }
|
||||
end
|
||||
|
||||
let(:response_body_success_without_name) do
|
||||
{ id_token: JWT.encode({ email: email }, false), access_token: SecureRandom.hex(10), token_type: 'Bearer',
|
||||
{ id_token: JWT.encode({ email: email }, nil, 'none'), access_token: SecureRandom.hex(10), token_type: 'Bearer',
|
||||
refresh_token: SecureRandom.hex(10) }
|
||||
end
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do
|
||||
|
||||
describe 'GET /microsoft/callback' do
|
||||
let(:response_body_success) do
|
||||
{ id_token: JWT.encode({ email: email, name: 'test' }, false), access_token: SecureRandom.hex(10), token_type: 'Bearer',
|
||||
{ id_token: JWT.encode({ email: email, name: 'test' }, nil, 'none'), access_token: SecureRandom.hex(10), token_type: 'Bearer',
|
||||
refresh_token: SecureRandom.hex(10) }
|
||||
end
|
||||
|
||||
let(:response_body_success_without_name) do
|
||||
{ id_token: JWT.encode({ email: email }, false), access_token: SecureRandom.hex(10), token_type: 'Bearer',
|
||||
{ id_token: JWT.encode({ email: email }, nil, 'none'), access_token: SecureRandom.hex(10), token_type: 'Bearer',
|
||||
refresh_token: SecureRandom.hex(10) }
|
||||
end
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::BulkActions', type: :request do
|
||||
params: sync_params,
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
end.to have_enqueued_job(Captain::Documents::PerformSyncJob).exactly(documents.size).times
|
||||
end.to have_enqueued_job(Captain::Documents::PerformSyncJob).on_queue('low').exactly(documents.size).times
|
||||
|
||||
documents.each do |document|
|
||||
expect(document.reload).to have_attributes(
|
||||
@@ -190,7 +190,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::BulkActions', type: :request do
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'skips documents that already have a sync in progress' do
|
||||
it 'queues documents that already have a sync in progress' do
|
||||
syncing_document = create(:captain_document, assistant: assistant, account: account, status: :available)
|
||||
syncing_document.update!(sync_status: :syncing, last_sync_attempted_at: 1.minute.ago)
|
||||
|
||||
@@ -199,9 +199,10 @@ RSpec.describe 'Api::V1::Accounts::Captain::BulkActions', type: :request do
|
||||
params: sync_params.merge(ids: [syncing_document.id]),
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
end.not_to have_enqueued_job(Captain::Documents::PerformSyncJob)
|
||||
end.to have_enqueued_job(Captain::Documents::PerformSyncJob).with(syncing_document).on_queue('low')
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json_response).to eq({ ids: [syncing_document.id], count: 1 })
|
||||
end
|
||||
|
||||
it 'queues stale syncing documents again' do
|
||||
|
||||
@@ -243,7 +243,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::Documents', type: :request do
|
||||
before do
|
||||
create_list(:captain_document, 5, assistant: assistant, account: account)
|
||||
|
||||
create(:installation_config, name: 'CAPTAIN_CLOUD_PLAN_LIMITS', value: captain_limits.to_json)
|
||||
InstallationConfig.find_or_initialize_by(name: 'CAPTAIN_CLOUD_PLAN_LIMITS').update!(value: captain_limits.to_json)
|
||||
post "/api/v1/accounts/#{account.id}/captain/documents",
|
||||
params: valid_attributes,
|
||||
headers: admin.create_new_auth_token
|
||||
@@ -281,7 +281,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::Documents', type: :request do
|
||||
expect do
|
||||
post "/api/v1/accounts/#{account.id}/captain/documents/#{document.id}/sync",
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
end.to have_enqueued_job(Captain::Documents::PerformSyncJob).with(document)
|
||||
end.to have_enqueued_job(Captain::Documents::PerformSyncJob).with(document).on_queue('low')
|
||||
|
||||
expect(document.reload).to have_attributes(
|
||||
sync_status: 'syncing',
|
||||
@@ -292,15 +292,15 @@ RSpec.describe 'Api::V1::Accounts::Captain::Documents', type: :request do
|
||||
expect(response).to have_http_status(:accepted)
|
||||
end
|
||||
|
||||
it 'rejects documents that already have a sync in progress' do
|
||||
it 'queues documents that already have a sync in progress' do
|
||||
document.update!(sync_status: :syncing, last_sync_attempted_at: 1.minute.ago)
|
||||
|
||||
expect do
|
||||
post "/api/v1/accounts/#{account.id}/captain/documents/#{document.id}/sync",
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
end.not_to have_enqueued_job(Captain::Documents::PerformSyncJob)
|
||||
end.to have_enqueued_job(Captain::Documents::PerformSyncJob).with(document).on_queue('low')
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:accepted)
|
||||
end
|
||||
|
||||
it 'queues stale syncing documents again' do
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Documents::PerformSyncJob, type: :job do
|
||||
let(:account) { create(:account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:document) { create(:captain_document, assistant: assistant, account: account, status: :available) }
|
||||
|
||||
def stub_lock(job)
|
||||
allow(job).to receive(:with_lock).and_yield
|
||||
end
|
||||
|
||||
def stub_page_fetch(content: 'Updated content')
|
||||
fetch_result = Captain::Documents::SinglePageFetcher::Result.new(
|
||||
success: true,
|
||||
title: 'Updated title',
|
||||
content: content
|
||||
)
|
||||
fetcher = instance_double(Captain::Documents::SinglePageFetcher, fetch: fetch_result)
|
||||
allow(Captain::Documents::SinglePageFetcher).to receive(:new).and_return(fetcher)
|
||||
end
|
||||
|
||||
def stub_page_fetch_failure
|
||||
fetcher = instance_double(Captain::Documents::SinglePageFetcher)
|
||||
allow(fetcher).to receive(:fetch).and_raise(StandardError, 'boom')
|
||||
allow(Captain::Documents::SinglePageFetcher).to receive(:new).and_return(fetcher)
|
||||
end
|
||||
|
||||
it 'syncs the document content' do
|
||||
travel_to Time.zone.local(2026, 5, 18, 10, 0, 0) do
|
||||
job = described_class.new
|
||||
stub_lock(job)
|
||||
stub_page_fetch
|
||||
|
||||
job.perform(document)
|
||||
|
||||
expect(document.reload).to have_attributes(
|
||||
sync_status: 'synced',
|
||||
last_sync_attempted_at: Time.current,
|
||||
last_synced_at: Time.current,
|
||||
content: 'Updated content'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it 'marks unexpected failures as failed' do
|
||||
travel_to Time.zone.local(2026, 5, 18, 10, 0, 0) do
|
||||
job = described_class.new
|
||||
stub_lock(job)
|
||||
stub_page_fetch_failure
|
||||
|
||||
expect { job.perform(document) }.to raise_error(StandardError, 'boom')
|
||||
|
||||
expect(document.reload).to have_attributes(
|
||||
sync_status: 'failed',
|
||||
last_sync_error_code: 'sync_error',
|
||||
last_sync_attempted_at: Time.current
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -5,11 +5,28 @@ RSpec.describe Captain::Documents::ScheduleSyncsJob, type: :job do
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
|
||||
before do
|
||||
create(:installation_config, name: 'CAPTAIN_DOCUMENT_AUTO_SYNC_INTERVALS', value: { business: 24, hacker: nil }.to_json)
|
||||
set_installation_config('CAPTAIN_DOCUMENT_AUTO_SYNC_INTERVALS', { business: 168, enterprise: 24, startups: 720, hacker: nil }.to_json)
|
||||
set_installation_config('CAPTAIN_DOCUMENT_AUTO_SYNC_PER_ACCOUNT_BATCH_LIMIT', 50)
|
||||
set_installation_config('CAPTAIN_DOCUMENT_AUTO_SYNC_GLOBAL_BATCH_LIMIT', 1000)
|
||||
account.enable_features!('captain_document_auto_sync')
|
||||
clear_enqueued_jobs
|
||||
end
|
||||
|
||||
def set_installation_config(name, value)
|
||||
InstallationConfig.find_or_initialize_by(name: name).tap do |config|
|
||||
config.value = value
|
||||
config.save!
|
||||
end
|
||||
end
|
||||
|
||||
def update_sync_limit(name, value)
|
||||
InstallationConfig.find_by!(name: name).update!(value: value)
|
||||
end
|
||||
|
||||
def sync_job_for(document)
|
||||
have_enqueued_job(Captain::Documents::PerformSyncJob).with(document)
|
||||
end
|
||||
|
||||
context 'when the account has not enabled auto-sync' do
|
||||
before { account.disable_features!('captain_document_auto_sync') }
|
||||
|
||||
@@ -32,6 +49,25 @@ RSpec.describe Captain::Documents::ScheduleSyncsJob, type: :job do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a plan name is passed' do
|
||||
it 'queues due documents only for that plan' do
|
||||
enterprise_account = create(:account, custom_attributes: { plan_name: 'Enterprise' })
|
||||
enterprise_account.enable_features!('captain_document_auto_sync')
|
||||
enterprise_assistant = create(:captain_assistant, account: enterprise_account)
|
||||
business_document = create(:captain_document, assistant: assistant, account: account, status: :available)
|
||||
enterprise_document = create(:captain_document, assistant: enterprise_assistant, account: enterprise_account, status: :available)
|
||||
|
||||
business_document.update!(sync_status: :synced, last_synced_at: 3.days.ago, last_sync_attempted_at: 3.days.ago)
|
||||
enterprise_document.update!(sync_status: :synced, last_synced_at: 3.days.ago, last_sync_attempted_at: 3.days.ago)
|
||||
clear_enqueued_jobs
|
||||
|
||||
described_class.new.perform('enterprise')
|
||||
|
||||
expect(Captain::Documents::PerformSyncJob).to have_been_enqueued.with(enterprise_document)
|
||||
expect(Captain::Documents::PerformSyncJob).not_to have_been_enqueued.with(business_document)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an available document has backfilled sync metadata' do
|
||||
it 'leaves it alone when last synced within the plan cadence' do
|
||||
create(
|
||||
@@ -54,53 +90,34 @@ RSpec.describe Captain::Documents::ScheduleSyncsJob, type: :job do
|
||||
account: account,
|
||||
status: :available,
|
||||
sync_status: :synced,
|
||||
last_synced_at: 3.days.ago
|
||||
last_synced_at: 8.days.ago
|
||||
)
|
||||
clear_enqueued_jobs
|
||||
|
||||
expect { described_class.new.perform }
|
||||
.to have_enqueued_job(Captain::Documents::PerformSyncJob).with(document)
|
||||
.to sync_job_for(document).on_queue('purgable')
|
||||
end
|
||||
|
||||
it 'marks the due document as syncing before queueing' do
|
||||
it 'delays only the queued sync job' do
|
||||
travel_to Time.zone.local(2026, 4, 27, 10, 0, 0) do
|
||||
job = described_class.new
|
||||
allow(job).to receive(:rand).and_return(30.minutes.to_i)
|
||||
document = create(
|
||||
:captain_document,
|
||||
assistant: assistant,
|
||||
account: account,
|
||||
status: :available,
|
||||
sync_status: :synced,
|
||||
last_synced_at: 3.days.ago
|
||||
last_synced_at: 8.days.ago
|
||||
)
|
||||
clear_enqueued_jobs
|
||||
|
||||
described_class.new.perform
|
||||
job.perform
|
||||
|
||||
expect(document.reload).to have_attributes(
|
||||
sync_status: 'syncing',
|
||||
last_sync_attempted_at: Time.current
|
||||
)
|
||||
expect(Captain::Documents::PerformSyncJob)
|
||||
.to have_been_enqueued.with(document).at(30.minutes.from_now)
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not queue the same document again while the reserved sync is fresh' do
|
||||
document = create(
|
||||
:captain_document,
|
||||
assistant: assistant,
|
||||
account: account,
|
||||
status: :available,
|
||||
sync_status: :synced,
|
||||
last_synced_at: 2.days.ago
|
||||
)
|
||||
clear_enqueued_jobs
|
||||
|
||||
expect { described_class.new.perform }
|
||||
.to have_enqueued_job(Captain::Documents::PerformSyncJob).with(document)
|
||||
|
||||
clear_enqueued_jobs
|
||||
|
||||
expect { described_class.new.perform }.not_to have_enqueued_job(Captain::Documents::PerformSyncJob)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an available document was synced within the plan cadence' do
|
||||
@@ -116,78 +133,59 @@ RSpec.describe Captain::Documents::ScheduleSyncsJob, type: :job do
|
||||
context 'when an available document was last synced before the plan cadence' do
|
||||
it 'queues a sync for that document' do
|
||||
document = create(:captain_document, assistant: assistant, account: account, status: :available)
|
||||
document.update!(sync_status: :synced, last_synced_at: 2.days.ago, last_sync_attempted_at: 2.days.ago)
|
||||
document.update!(sync_status: :synced, last_synced_at: 8.days.ago, last_sync_attempted_at: 8.days.ago)
|
||||
clear_enqueued_jobs
|
||||
|
||||
expect { described_class.new.perform }
|
||||
.to have_enqueued_job(Captain::Documents::PerformSyncJob).with(document)
|
||||
.to sync_job_for(document)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when jitter spreads queued sync execution' do
|
||||
it 'uses a widened due window so jittered syncs do not skip the next plan run' do
|
||||
travel_to Time.zone.local(2026, 4, 27, 10, 0, 0) do
|
||||
job = described_class.new
|
||||
interval = 1.week
|
||||
due_window = (interval.to_i / 2).seconds
|
||||
allow(job).to receive(:rand).and_return(2.hours.to_i)
|
||||
document = create(:captain_document, assistant: assistant, account: account, status: :available)
|
||||
|
||||
document.update!(sync_status: :synced, last_synced_at: (due_window - 1.minute).ago)
|
||||
clear_enqueued_jobs
|
||||
|
||||
expect { job.perform }.not_to have_enqueued_job(Captain::Documents::PerformSyncJob)
|
||||
|
||||
document.update!(sync_status: :synced, last_synced_at: (due_window + 1.minute).ago)
|
||||
clear_enqueued_jobs
|
||||
|
||||
expect { job.perform }
|
||||
.to have_enqueued_job(Captain::Documents::PerformSyncJob)
|
||||
.with(document)
|
||||
.on_queue('purgable')
|
||||
.at(2.hours.from_now)
|
||||
end
|
||||
end
|
||||
|
||||
it 'skips invalid legacy documents without counting them against the account cap' do
|
||||
stub_const("#{described_class}::PER_ACCOUNT_HOURLY_CAP", 1)
|
||||
create(
|
||||
:captain_document,
|
||||
assistant: assistant,
|
||||
account: account,
|
||||
status: :in_progress,
|
||||
content: nil,
|
||||
external_link: 'https://example.com'
|
||||
)
|
||||
invalid_document = build(
|
||||
:captain_document,
|
||||
assistant: assistant,
|
||||
account: account,
|
||||
status: :available,
|
||||
sync_status: :synced,
|
||||
last_synced_at: 2.days.ago,
|
||||
last_sync_attempted_at: 2.days.ago,
|
||||
external_link: 'https://example.com/'
|
||||
)
|
||||
invalid_document.save!(validate: false)
|
||||
valid_document = create(:captain_document, assistant: assistant, account: account, status: :available)
|
||||
valid_document.update!(sync_status: :synced, last_synced_at: 2.days.ago, last_sync_attempted_at: 2.days.ago)
|
||||
clear_enqueued_jobs
|
||||
it 'uses a random delay inside the cadence window' do
|
||||
travel_to Time.zone.local(2026, 4, 27, 10, 0, 0) do
|
||||
document = create(:captain_document, assistant: assistant, account: account, status: :available)
|
||||
document.update!(sync_status: :synced, last_synced_at: 8.days.ago)
|
||||
job = described_class.new
|
||||
sync_execution_delay = 12_345.seconds
|
||||
|
||||
expect { described_class.new.perform }.not_to raise_error
|
||||
expect(Captain::Documents::PerformSyncJob).not_to have_been_enqueued.with(invalid_document)
|
||||
expect(Captain::Documents::PerformSyncJob).to have_been_enqueued.with(valid_document)
|
||||
end
|
||||
clear_enqueued_jobs
|
||||
allow(job).to receive(:rand).with(0..described_class::WEEKLY_SYNC_JITTER.to_i).and_return(sync_execution_delay.to_i)
|
||||
|
||||
it 'keeps paging due documents when invalid documents fill the first batch' do
|
||||
stub_const("#{described_class}::PER_ACCOUNT_HOURLY_CAP", 1)
|
||||
stub_const("#{described_class}::DUE_DOCUMENT_BATCH_SIZE", 1)
|
||||
create(
|
||||
:captain_document,
|
||||
assistant: assistant,
|
||||
account: account,
|
||||
status: :in_progress,
|
||||
content: nil,
|
||||
external_link: 'https://example.com'
|
||||
)
|
||||
invalid_document = build(
|
||||
:captain_document,
|
||||
assistant: assistant,
|
||||
account: account,
|
||||
status: :available,
|
||||
sync_status: :synced,
|
||||
last_synced_at: 2.days.ago,
|
||||
last_sync_attempted_at: 3.days.ago,
|
||||
external_link: 'https://example.com/'
|
||||
)
|
||||
invalid_document.save!(validate: false)
|
||||
valid_document = create(:captain_document, assistant: assistant, account: account, status: :available)
|
||||
valid_document.update!(sync_status: :synced, last_synced_at: 2.days.ago, last_sync_attempted_at: 2.days.ago)
|
||||
clear_enqueued_jobs
|
||||
|
||||
described_class.new.perform
|
||||
|
||||
expect(Captain::Documents::PerformSyncJob).to have_been_enqueued.with(valid_document)
|
||||
expect { job.perform }
|
||||
.to sync_job_for(document)
|
||||
.at(sync_execution_delay.from_now)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when more documents are due than the account cap allows' do
|
||||
before do
|
||||
stub_const("#{described_class}::PER_ACCOUNT_HOURLY_CAP", 2)
|
||||
update_sync_limit('CAPTAIN_DOCUMENT_AUTO_SYNC_PER_ACCOUNT_BATCH_LIMIT', 2)
|
||||
end
|
||||
|
||||
it 'queues backfilled and oldest-attempted documents first' do
|
||||
@@ -195,26 +193,47 @@ RSpec.describe Captain::Documents::ScheduleSyncsJob, type: :job do
|
||||
oldest_document = create(:captain_document, assistant: assistant, account: account, status: :available)
|
||||
backfilled_document = create(:captain_document, assistant: assistant, account: account, status: :available)
|
||||
|
||||
newest_document.update!(sync_status: :synced, last_synced_at: 2.days.ago, last_sync_attempted_at: 2.days.ago)
|
||||
oldest_document.update!(sync_status: :synced, last_synced_at: 3.days.ago, last_sync_attempted_at: 3.days.ago)
|
||||
backfilled_document.update!(sync_status: :synced, last_synced_at: 4.days.ago, last_sync_attempted_at: nil)
|
||||
newest_document.update!(sync_status: :synced, last_synced_at: 8.days.ago, last_sync_attempted_at: 8.days.ago)
|
||||
oldest_document.update!(sync_status: :synced, last_synced_at: 9.days.ago, last_sync_attempted_at: 9.days.ago)
|
||||
backfilled_document.update!(sync_status: :synced, last_synced_at: 10.days.ago, last_sync_attempted_at: nil)
|
||||
clear_enqueued_jobs
|
||||
|
||||
expect { described_class.new.perform }
|
||||
.to have_enqueued_job(Captain::Documents::PerformSyncJob).with(backfilled_document)
|
||||
.and have_enqueued_job(Captain::Documents::PerformSyncJob).with(oldest_document)
|
||||
.to sync_job_for(backfilled_document)
|
||||
.and sync_job_for(oldest_document)
|
||||
expect(Captain::Documents::PerformSyncJob).not_to have_been_enqueued.with(newest_document)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sync caps are configured' do
|
||||
it 'uses installation config caps for per-account and global limits' do
|
||||
update_sync_limit('CAPTAIN_DOCUMENT_AUTO_SYNC_PER_ACCOUNT_BATCH_LIMIT', 2)
|
||||
update_sync_limit('CAPTAIN_DOCUMENT_AUTO_SYNC_GLOBAL_BATCH_LIMIT', 3)
|
||||
|
||||
second_account = create(:account, custom_attributes: { plan_name: 'business' })
|
||||
second_account.enable_features!('captain_document_auto_sync')
|
||||
second_assistant = create(:captain_assistant, account: second_account)
|
||||
|
||||
first_account_documents = create_list(:captain_document, 3, assistant: assistant, account: account, status: :available)
|
||||
second_account_documents = create_list(:captain_document, 3, assistant: second_assistant, account: second_account, status: :available)
|
||||
(first_account_documents + second_account_documents).each do |document|
|
||||
document.update!(sync_status: :synced, last_synced_at: 8.days.ago, last_sync_attempted_at: 8.days.ago)
|
||||
end
|
||||
clear_enqueued_jobs
|
||||
|
||||
expect { described_class.new.perform }
|
||||
.to have_enqueued_job(Captain::Documents::PerformSyncJob).exactly(3).times
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an available document failed before the plan cadence' do
|
||||
it 'queues a sync for that document' do
|
||||
document = create(:captain_document, assistant: assistant, account: account, status: :available)
|
||||
document.update!(sync_status: :failed, last_sync_attempted_at: 2.days.ago)
|
||||
document.update!(sync_status: :failed, last_sync_attempted_at: 8.days.ago)
|
||||
clear_enqueued_jobs
|
||||
|
||||
expect { described_class.new.perform }
|
||||
.to have_enqueued_job(Captain::Documents::PerformSyncJob).with(document)
|
||||
.to sync_job_for(document)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -228,7 +247,7 @@ RSpec.describe Captain::Documents::ScheduleSyncsJob, type: :job do
|
||||
clear_enqueued_jobs
|
||||
|
||||
expect { described_class.new.perform }
|
||||
.to have_enqueued_job(Captain::Documents::PerformSyncJob).with(document)
|
||||
.to sync_job_for(document)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Internal::TriggerDailyScheduledItemsJob do
|
||||
before do
|
||||
allow(ChatwootHub).to receive(:installation_identifier).and_return('test-installation-id')
|
||||
allow(Captain::Documents::ScheduleSyncsJob).to receive(:perform_later)
|
||||
end
|
||||
|
||||
it 'enqueues enterprise Captain document auto-sync every day' do
|
||||
travel_to Time.zone.parse('2026-05-26 00:00:00 UTC') do
|
||||
described_class.perform_now
|
||||
end
|
||||
|
||||
expect(Captain::Documents::ScheduleSyncsJob).to have_received(:perform_later).with('enterprise')
|
||||
end
|
||||
|
||||
it 'enqueues business Captain document auto-sync weekly' do
|
||||
travel_to Time.zone.parse('2026-05-24 00:00:00 UTC') do
|
||||
described_class.perform_now
|
||||
end
|
||||
|
||||
expect(Captain::Documents::ScheduleSyncsJob).to have_received(:perform_later).with('business')
|
||||
end
|
||||
|
||||
it 'enqueues startup Captain document auto-sync monthly' do
|
||||
travel_to Time.zone.parse('2026-06-01 00:00:00 UTC') do
|
||||
described_class.perform_now
|
||||
end
|
||||
|
||||
expect(Captain::Documents::ScheduleSyncsJob).to have_received(:perform_later).with('startups')
|
||||
end
|
||||
|
||||
it 'does not enqueue business or startup Captain document auto-sync before their plan window' do
|
||||
travel_to Time.zone.parse('2026-05-25 00:00:00 UTC') do
|
||||
described_class.perform_now
|
||||
end
|
||||
|
||||
expect(Captain::Documents::ScheduleSyncsJob).to have_received(:perform_later).with('enterprise')
|
||||
expect(Captain::Documents::ScheduleSyncsJob).not_to have_received(:perform_later).with('business')
|
||||
expect(Captain::Documents::ScheduleSyncsJob).not_to have_received(:perform_later).with('startups')
|
||||
end
|
||||
end
|
||||
@@ -72,7 +72,7 @@ RSpec.describe Messages::AudioTranscriptionService, type: :service do
|
||||
content_type: 'audio/mpeg'
|
||||
)
|
||||
allow(service).to receive(:can_transcribe?).and_return(true)
|
||||
allow(attachment.file.blob).to receive(:byte_size).and_return(described_class::WHISPER_BYTE_LIMIT + 1)
|
||||
allow(attachment.file.blob).to receive(:byte_size).and_return(described_class::TRANSCRIPTION_BYTE_LIMIT + 1)
|
||||
end
|
||||
|
||||
it 'returns an error without calling Whisper' do
|
||||
|
||||
@@ -32,6 +32,9 @@ describe Whatsapp::IncomingCallService do
|
||||
|
||||
describe 'inbound connect' do
|
||||
let(:sdp_offer) { "v=0\r\n...sdp..." }
|
||||
let!(:agent) { create(:user, account: account) }
|
||||
|
||||
before { create(:inbox_member, inbox: inbox, user: agent) }
|
||||
|
||||
it 'creates the Call + Conversation + voice_call message and broadcasts voice_call.incoming' do
|
||||
allow(ActionCable.server).to receive(:broadcast)
|
||||
@@ -44,10 +47,16 @@ describe Whatsapp::IncomingCallService do
|
||||
expect(call).to have_attributes(provider: 'whatsapp', direction: 'incoming', status: 'ringing',
|
||||
provider_call_id: provider_call_id)
|
||||
expect(call.meta['sdp_offer']).to eq(sdp_offer)
|
||||
# No agent is online, so the call falls back to the inbox's agents (and
|
||||
# account admins) — never the whole-account stream.
|
||||
expect(ActionCable.server).to have_received(:broadcast).with(
|
||||
"account_#{account.id}",
|
||||
agent.pubsub_token,
|
||||
hash_including(event: 'voice_call.incoming', data: hash_including(sdp_offer: sdp_offer))
|
||||
)
|
||||
expect(ActionCable.server).not_to have_received(:broadcast).with(
|
||||
"account_#{account.id}",
|
||||
hash_including(event: 'voice_call.incoming')
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ RSpec.describe Instagram::IntegrationHelper do
|
||||
|
||||
context 'when client secret is not configured' do
|
||||
let(:client_secret) { nil }
|
||||
let(:valid_token) { 'any-token' }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(verify_instagram_token(valid_token)).to be_nil
|
||||
|
||||
@@ -65,6 +65,7 @@ RSpec.describe Linear::IntegrationHelper do
|
||||
|
||||
context 'when client secret is not configured' do
|
||||
let(:client_secret) { nil }
|
||||
let(:valid_token) { 'any-token' }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(verify_linear_token(valid_token)).to be_nil
|
||||
|
||||
@@ -65,6 +65,7 @@ RSpec.describe Shopify::IntegrationHelper do
|
||||
|
||||
context 'when client secret is not configured' do
|
||||
let(:client_secret) { nil }
|
||||
let(:valid_token) { 'any-token' }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(verify_shopify_token(valid_token)).to be_nil
|
||||
|
||||
@@ -223,12 +223,12 @@ RSpec.describe Channel::Whatsapp do
|
||||
expect(channel.voice_enabled?).to be true
|
||||
end
|
||||
|
||||
it 'returns false for whatsapp_cloud channels without embedded_signup source' do
|
||||
it 'returns true for manual whatsapp_cloud channels with calling_enabled' do
|
||||
channel = create(:channel_whatsapp, account: account, provider: 'whatsapp_cloud',
|
||||
validate_provider_config: false, sync_templates: false)
|
||||
channel.update!(provider_config: channel.provider_config.merge('source' => 'manual', 'calling_enabled' => true))
|
||||
|
||||
expect(channel.voice_enabled?).to be false
|
||||
expect(channel.voice_enabled?).to be true
|
||||
end
|
||||
|
||||
it 'returns false for default-provider channels (360dialog) even with calling_enabled' do
|
||||
|
||||
Reference in New Issue
Block a user