From 689cc114f89f980dfe4c29c64afe9a922aeb65d0 Mon Sep 17 00:00:00 2001 From: Codex CLI Date: Mon, 4 May 2026 13:16:47 +0000 Subject: [PATCH] fix(captain): allow agents to manage FAQs --- .../captain/assistant_responses_controller.rb | 5 +- .../captain/bulk_actions_controller.rb | 6 +- .../captain/assistant_response_policy.rb | 33 +++++++++ .../assistant_responses_controller_spec.rb | 69 ++++++++++++++----- .../captain/bulk_actions_controller_spec.rb | 39 ++++++++++- 5 files changed, 128 insertions(+), 24 deletions(-) create mode 100644 enterprise/app/policies/captain/assistant_response_policy.rb diff --git a/enterprise/app/controllers/api/v1/accounts/captain/assistant_responses_controller.rb b/enterprise/app/controllers/api/v1/accounts/captain/assistant_responses_controller.rb index dfaf710bc..c0b778922 100644 --- a/enterprise/app/controllers/api/v1/accounts/captain/assistant_responses_controller.rb +++ b/enterprise/app/controllers/api/v1/accounts/captain/assistant_responses_controller.rb @@ -1,6 +1,6 @@ class Api::V1::Accounts::Captain::AssistantResponsesController < Api::V1::Accounts::BaseController before_action :current_account - before_action -> { check_authorization(Captain::Assistant) } + before_action -> { check_authorization(Captain::AssistantResponse) } before_action :set_current_page, only: [:index] before_action :set_assistant, only: [:create] @@ -21,6 +21,7 @@ class Api::V1::Accounts::Captain::AssistantResponsesController < Api::V1::Accoun @response = Current.account.captain_assistant_responses.new(response_params) @response.documentable = Current.user @response.save! + Captain::Llm::UpdateEmbeddingJob.perform_now(@response, "#{@response.question}: #{@response.answer}") end def update @@ -28,7 +29,7 @@ class Api::V1::Accounts::Captain::AssistantResponsesController < Api::V1::Accoun end def destroy - @response.destroy! + @response.destroy head :no_content end diff --git a/enterprise/app/controllers/api/v1/accounts/captain/bulk_actions_controller.rb b/enterprise/app/controllers/api/v1/accounts/captain/bulk_actions_controller.rb index 00408c8df..a4e46748b 100644 --- a/enterprise/app/controllers/api/v1/accounts/captain/bulk_actions_controller.rb +++ b/enterprise/app/controllers/api/v1/accounts/captain/bulk_actions_controller.rb @@ -1,6 +1,6 @@ class Api::V1::Accounts::Captain::BulkActionsController < Api::V1::Accounts::BaseController before_action :current_account - before_action -> { check_authorization(Captain::Assistant) } + before_action -> { check_authorization(Captain::AssistantResponse) } before_action :validate_params before_action :type_matches? @@ -19,7 +19,7 @@ class Api::V1::Accounts::Captain::BulkActionsController < Api::V1::Accounts::Bas end def type_matches? - return false if MODEL_TYPE.include?(params[:type]) + return if MODEL_TYPE.include?(params[:type]) render json: { success: false }, status: :unprocessable_entity end @@ -37,7 +37,7 @@ class Api::V1::Accounts::Captain::BulkActionsController < Api::V1::Accounts::Bas case params[:fields][:status] when 'approve' - responses.pending.update!(status: 'approved') + responses.pending.update(status: 'approved') responses when 'delete' responses.destroy_all diff --git a/enterprise/app/policies/captain/assistant_response_policy.rb b/enterprise/app/policies/captain/assistant_response_policy.rb new file mode 100644 index 000000000..7504a7e05 --- /dev/null +++ b/enterprise/app/policies/captain/assistant_response_policy.rb @@ -0,0 +1,33 @@ +class Captain::AssistantResponsePolicy < ApplicationPolicy + def index? + @account_user.administrator? || @account_user.agent? + end + + def show? + index? + end + + def create? + manage? + end + + def update? + manage? + end + + def destroy? + manage? + end + + private + + def manage? + return true if @account_user.administrator? + + if @account_user.custom_role.present? + return @account_user.custom_role.permissions.include?('knowledge_base_manage') + end + + @account_user.agent? + end +end diff --git a/spec/enterprise/controllers/api/v1/accounts/captain/assistant_responses_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/captain/assistant_responses_controller_spec.rb index 4a26d4aac..b3ff77bd6 100644 --- a/spec/enterprise/controllers/api/v1/accounts/captain/assistant_responses_controller_spec.rb +++ b/spec/enterprise/controllers/api/v1/accounts/captain/assistant_responses_controller_spec.rb @@ -6,6 +6,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::AssistantResponses', type: :request let(:document) { create(:captain_document, assistant: assistant, account: account) } let(:admin) { create(:user, account: account, role: :administrator) } let(:agent) { create(:user, account: account, role: :agent) } + let(:agent_with_custom_role) { create(:user, account: account, role: :agent) } let(:another_assistant) { create(:captain_assistant, account: account) } let(:another_document) { create(:captain_document, account: account, assistant: assistant) } @@ -179,6 +180,46 @@ RSpec.describe 'Api::V1::Accounts::Captain::AssistantResponses', type: :request expect(json_response[:answer]).to eq('Test answer') end + it 'creates a new response if the user is an agent' do + expect do + post "/api/v1/accounts/#{account.id}/captain/assistant_responses", + params: valid_params, + headers: agent.create_new_auth_token, + as: :json + end.to change(Captain::AssistantResponse, :count).by(1) + + expect(response).to have_http_status(:success) + expect(json_response[:question]).to eq('Test question?') + end + + it 'creates a new response if the user has a custom role with knowledge base permission' do + custom_role = create(:custom_role, account: account, permissions: ['knowledge_base_manage']) + AccountUser.find_by!(account: account, user: agent_with_custom_role).update!(custom_role: custom_role) + + expect do + post "/api/v1/accounts/#{account.id}/captain/assistant_responses", + params: valid_params, + headers: agent_with_custom_role.create_new_auth_token, + as: :json + end.to change(Captain::AssistantResponse, :count).by(1) + + expect(response).to have_http_status(:success) + end + + it 'does not create a response if the custom role lacks knowledge base permission' do + custom_role = create(:custom_role, account: account, permissions: ['conversation_manage']) + AccountUser.find_by!(account: account, user: agent_with_custom_role).update!(custom_role: custom_role) + + expect do + post "/api/v1/accounts/#{account.id}/captain/assistant_responses", + params: valid_params, + headers: agent_with_custom_role.create_new_auth_token, + as: :json + end.not_to change(Captain::AssistantResponse, :count) + + expect(response).to have_http_status(:forbidden) + end + context 'with invalid params' do let(:invalid_params) do { @@ -197,22 +238,6 @@ RSpec.describe 'Api::V1::Accounts::Captain::AssistantResponses', type: :request expect(response).to have_http_status(:unprocessable_entity) end - - it 'returns unprocessable entity when question exceeds 255 characters' do - long_question = 'a' * 256 - post "/api/v1/accounts/#{account.id}/captain/assistant_responses", - params: { - assistant_response: { - question: long_question, - answer: 'Test answer', - assistant_id: assistant.id - } - }, - headers: admin.create_new_auth_token, - as: :json - - expect(response).to have_http_status(:unprocessable_entity) - end end end @@ -239,6 +264,16 @@ RSpec.describe 'Api::V1::Accounts::Captain::AssistantResponses', type: :request expect(json_response[:answer]).to eq('Updated answer') end + it 'updates the response if the user is an agent' do + patch "/api/v1/accounts/#{account.id}/captain/assistant_responses/#{response_record.id}", + params: update_params, + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:ok) + expect(json_response[:question]).to eq('Updated question?') + end + context 'with invalid params' do let(:invalid_params) do { @@ -266,7 +301,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::AssistantResponses', type: :request it 'deletes the response' do expect do delete "/api/v1/accounts/#{account.id}/captain/assistant_responses/#{response_record.id}", - headers: admin.create_new_auth_token, + headers: agent.create_new_auth_token, as: :json end.to change(Captain::AssistantResponse, :count).by(-1) diff --git a/spec/enterprise/controllers/api/v1/accounts/captain/bulk_actions_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/captain/bulk_actions_controller_spec.rb index 968649085..da4b8c61c 100644 --- a/spec/enterprise/controllers/api/v1/accounts/captain/bulk_actions_controller_spec.rb +++ b/spec/enterprise/controllers/api/v1/accounts/captain/bulk_actions_controller_spec.rb @@ -5,6 +5,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::BulkActions', type: :request do let(:assistant) { create(:captain_assistant, account: account) } let(:admin) { create(:user, account: account, role: :administrator) } let(:agent) { create(:user, account: account, role: :agent) } + let(:agent_with_custom_role) { create(:user, account: account, role: :agent) } let!(:pending_responses) do create_list( :captain_assistant_response, @@ -32,7 +33,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::BulkActions', type: :request do it 'approves the responses and returns the updated records' do post "/api/v1/accounts/#{account.id}/captain/bulk_actions", params: valid_params, - headers: admin.create_new_auth_token, + headers: agent.create_new_auth_token, as: :json expect(response).to have_http_status(:ok) @@ -59,7 +60,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::BulkActions', type: :request do expect do post "/api/v1/accounts/#{account.id}/captain/bulk_actions", params: delete_params, - headers: admin.create_new_auth_token, + headers: agent.create_new_auth_token, as: :json end.to change(Captain::AssistantResponse, :count).by(-2) @@ -73,6 +74,40 @@ RSpec.describe 'Api::V1::Accounts::Captain::BulkActions', type: :request do end end + context 'when the user has a custom role' do + let(:approve_params) do + { + type: 'AssistantResponse', + ids: pending_responses.map(&:id), + fields: { status: 'approve' } + } + end + + it 'allows bulk actions with knowledge base permission' do + custom_role = create(:custom_role, account: account, permissions: ['knowledge_base_manage']) + AccountUser.find_by!(account: account, user: agent_with_custom_role).update!(custom_role: custom_role) + + post "/api/v1/accounts/#{account.id}/captain/bulk_actions", + params: approve_params, + headers: agent_with_custom_role.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:ok) + end + + it 'rejects bulk actions without knowledge base permission' do + custom_role = create(:custom_role, account: account, permissions: ['conversation_manage']) + AccountUser.find_by!(account: account, user: agent_with_custom_role).update!(custom_role: custom_role) + + post "/api/v1/accounts/#{account.id}/captain/bulk_actions", + params: approve_params, + headers: agent_with_custom_role.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:forbidden) + end + end + context 'with invalid type' do let(:invalid_params) do {