fix: stabilize conversation FAQ lock spec (#15236)

## Description

Backend CI no longer flakes when the conversation FAQ grouping-lock spec
runs after Rails has reloaded application constants.

The job already raises the intended lock-acquisition error and prevents
concurrent suggestion generation. This updates the assertion to compare
the error class name, preserving that behavior without depending on a
reload-sensitive Ruby `Class` object.

The failure was reproduced in [CircleCI backend job
171299](https://app.circleci.com/pipelines/github/chatwoot/chatwoot/116560/workflows/efcee2f1-5af8-4121-82c3-920002dc420b/jobs/171299).

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

The isolated job spec passes, the exact 18-way CircleCI shard selection
passes all 442 examples, and the changed spec passes Ruby lint.

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
- [x] New and existing unit tests pass locally with my changes
This commit is contained in:
Sony Mathew
2026-07-29 17:06:31 +05:30
committed by GitHub
parent 8fcd32f442
commit bc9839ed38
2 changed files with 17 additions and 16 deletions

View File

@@ -165,9 +165,8 @@ RSpec.describe DeviseOverrides::SessionsController, type: :controller do
end end
describe 'session limit enforcement' do describe 'session limit enforcement' do
before { stub_const('DeviseOverrides::SessionsController::MAX_SESSIONS', 5) }
let(:user) { create(:user, password: 'Test@123456') } let(:user) { create(:user, password: 'Test@123456') }
let(:session_limit) { described_class::MAX_SESSIONS }
let(:browser_ua) { 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15' } let(:browser_ua) { 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15' }
let(:mobile_ua) { 'okhttp/4.9.3' } let(:mobile_ua) { 'okhttp/4.9.3' }
@@ -195,9 +194,9 @@ RSpec.describe DeviseOverrides::SessionsController, type: :controller do
it 'does not count expired tokens toward the cap' do it 'does not count expired tokens toward the cap' do
request.env['HTTP_USER_AGENT'] = browser_ua request.env['HTTP_USER_AGENT'] = browser_ua
# 3 expired + 2 active = 5 raw entries, but only 2 active # Raw token count reaches the cap, but three tokens are expired
3.times { |i| seed_token("expired#{i}", expiry_offset_days: -1, with_session: false) } 3.times { |i| seed_token("expired#{i}", expiry_offset_days: -1, with_session: false) }
2.times { |i| seed_token("active#{i}", expiry_offset_days: 30) } (session_limit - 3).times { |i| seed_token("active#{i}", expiry_offset_days: 30) }
post :create, params: login_params post :create, params: login_params
@@ -208,7 +207,7 @@ RSpec.describe DeviseOverrides::SessionsController, type: :controller do
context 'when at the limit from a browser with full tracking' do context 'when at the limit from a browser with full tracking' do
before do before do
request.env['HTTP_USER_AGENT'] = browser_ua request.env['HTTP_USER_AGENT'] = browser_ua
5.times { |i| seed_token("c#{i}", expiry_offset_days: 30) } session_limit.times { |i| seed_token("c#{i}", expiry_offset_days: 30) }
end end
it 'returns 409 with the session list (picker)' do it 'returns 409 with the session list (picker)' do
@@ -217,7 +216,7 @@ RSpec.describe DeviseOverrides::SessionsController, type: :controller do
expect(response).to have_http_status(:conflict) expect(response).to have_http_status(:conflict)
body = response.parsed_body body = response.parsed_body
expect(body['sessions_limit_reached']).to be true expect(body['sessions_limit_reached']).to be true
expect(body['sessions'].size).to eq(5) expect(body['sessions'].size).to eq(session_limit)
end end
it 'does not create a new session row' do it 'does not create a new session row' do
@@ -228,7 +227,7 @@ RSpec.describe DeviseOverrides::SessionsController, type: :controller do
context 'when at the limit from a non-browser client' do context 'when at the limit from a non-browser client' do
before do before do
request.env['HTTP_USER_AGENT'] = mobile_ua request.env['HTTP_USER_AGENT'] = mobile_ua
5.times { |i| seed_token("c#{i}", expiry_offset_days: 30 + i, with_session: false) } session_limit.times { |i| seed_token("c#{i}", expiry_offset_days: 30 + i, with_session: false) }
end end
it 'silently evicts the oldest token and lets login proceed' do it 'silently evicts the oldest token and lets login proceed' do
@@ -242,9 +241,9 @@ RSpec.describe DeviseOverrides::SessionsController, type: :controller do
context 'when at the limit but tracking is partial (legacy tokens present)' do context 'when at the limit but tracking is partial (legacy tokens present)' do
before do before do
request.env['HTTP_USER_AGENT'] = browser_ua request.env['HTTP_USER_AGENT'] = browser_ua
# one tracked, four legacy (no user_session rows) # One tracked session, with the remaining tokens having no user_session rows
seed_token('tracked', expiry_offset_days: 60, with_session: true) seed_token('tracked', expiry_offset_days: 60, with_session: true)
4.times { |i| seed_token("legacy#{i}", expiry_offset_days: 10 + i, with_session: false) } (session_limit - 1).times { |i| seed_token("legacy#{i}", expiry_offset_days: 10 + i, with_session: false) }
end end
it 'silent-evicts instead of showing a partial picker' do it 'silent-evicts instead of showing a partial picker' do
@@ -266,10 +265,10 @@ RSpec.describe DeviseOverrides::SessionsController, type: :controller do
context 'when at the limit with full tracking (no legacy gap)' do context 'when at the limit with full tracking (no legacy gap)' do
before do before do
request.env['HTTP_USER_AGENT'] = mobile_ua request.env['HTTP_USER_AGENT'] = mobile_ua
# Five tracked sessions, varying activity timestamps # Tracked sessions with varying activity timestamps
5.times do |i| session_limit.times do |i|
seed_token("tracked#{i}", expiry_offset_days: 30) seed_token("tracked#{i}", expiry_offset_days: 30)
user.user_sessions.find_by(client_id: "tracked#{i}").update!(last_activity_at: (5 - i).days.ago) user.user_sessions.find_by(client_id: "tracked#{i}").update!(last_activity_at: (session_limit - i).days.ago)
end end
end end
@@ -277,7 +276,7 @@ RSpec.describe DeviseOverrides::SessionsController, type: :controller do
post :create, params: login_params post :create, params: login_params
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
# tracked0 had the oldest last_activity_at (5 days ago) # tracked0 has the oldest last_activity_at
expect(user.reload.tokens.keys).not_to include('tracked0') expect(user.reload.tokens.keys).not_to include('tracked0')
expect(user.user_sessions.exists?(client_id: 'tracked0')).to be false expect(user.user_sessions.exists?(client_id: 'tracked0')).to be false
end end
@@ -286,7 +285,7 @@ RSpec.describe DeviseOverrides::SessionsController, type: :controller do
context 'with revoke_session_id during login' do context 'with revoke_session_id during login' do
before do before do
request.env['HTTP_USER_AGENT'] = browser_ua request.env['HTTP_USER_AGENT'] = browser_ua
5.times { |i| seed_token("c#{i}", expiry_offset_days: 30) } session_limit.times { |i| seed_token("c#{i}", expiry_offset_days: 30) }
end end
it 'revokes the chosen session and proceeds with login' do it 'revokes the chosen session and proceeds with login' do
@@ -303,7 +302,7 @@ RSpec.describe DeviseOverrides::SessionsController, type: :controller do
context 'with revoke_all_sessions during login' do context 'with revoke_all_sessions during login' do
before do before do
request.env['HTTP_USER_AGENT'] = browser_ua request.env['HTTP_USER_AGENT'] = browser_ua
5.times { |i| seed_token("c#{i}", expiry_offset_days: 30) } session_limit.times { |i| seed_token("c#{i}", expiry_offset_days: 30) }
end end
it 'wipes all sessions and tokens, then proceeds with login' do it 'wipes all sessions and tokens, then proceeds with login' do

View File

@@ -50,7 +50,9 @@ RSpec.describe Captain::Llm::ConversationFaqJob, type: :job do
expect do expect do
described_class.new.perform(conversation, assistant) described_class.new.perform(conversation, assistant)
end.to raise_error(MutexApplicationJob::LockAcquisitionError) end.to(raise_error do |error|
expect(error.class.name).to eq('MutexApplicationJob::LockAcquisitionError')
end)
end end
end end
end end