From 716d18b27a9b5af4da72eb0fad6763a8084e4bf5 Mon Sep 17 00:00:00 2001 From: Gosia Ksionek <mksionek@gitlab.com> Date: Tue, 9 Jan 2024 17:22:02 +0000 Subject: [PATCH] Enable Duo Chat for self-managed --- .../feature_flags/ops/ai_duo_chat_switch.yml | 9 + ee/app/graphql/mutations/ai/action.rb | 13 +- ee/app/policies/ee/global_policy.rb | 21 + ee/app/policies/ee/group_policy.rb | 18 + ee/app/policies/ee/project_policy.rb | 20 + ee/app/services/llm/chat_service.rb | 8 + .../llm/internal/completion_service.rb | 10 +- ee/config/saas_features/duo_chat_on_saas.yml | 5 + ee/lib/ee/gitlab/saas.rb | 1 + ee/lib/gitlab/llm/ai_message.rb | 4 + ee/lib/gitlab/llm/chain/utils/authorizer.rb | 24 +- ee/lib/gitlab/llm/chat_message.rb | 4 + ee/lib/gitlab/llm/tanuki_bot.rb | 8 +- ee/spec/features/tanuki_bot_chat_spec.rb | 125 ++++- ee/spec/graphql/mutations/ai/action_spec.rb | 53 +- ee/spec/lib/gitlab/llm/ai_message_spec.rb | 6 + .../chain/tools/json_reader/executor_spec.rb | 13 +- .../gitlab/llm/chain/utils/authorizer_spec.rb | 520 +++++++++++++----- ee/spec/lib/gitlab/llm/chat_message_spec.rb | 6 + .../summarize_all_open_notes_spec.rb | 20 +- ee/spec/lib/gitlab/llm/tanuki_bot_spec.rb | 220 +++++--- ee/spec/policies/global_policy_spec.rb | 51 ++ ee/spec/policies/group_policy_spec.rb | 80 +++ ee/spec/policies/project_policy_spec.rb | 101 ++++ .../graphql/mutations/projects/chat_spec.rb | 4 +- .../mutations/projects/explain_code_spec.rb | 2 +- .../fill_in_merge_request_template_spec.rb | 2 +- .../projects/generate_commit_message_spec.rb | 2 +- .../projects/generate_test_file_spec.rb | 2 +- ee/spec/services/llm/base_service_spec.rb | 78 +-- ee/spec/services/llm/chat_service_spec.rb | 79 ++- ..._in_merge_request_template_service_spec.rb | 6 +- .../llm/generate_description_service_spec.rb | 6 +- .../llm/generate_summary_service_spec.rb | 6 +- .../llm/internal/completion_service_spec.rb | 14 + .../summarize_merge_request_service_spec.rb | 6 +- ...summarize_submitted_review_service_spec.rb | 6 +- ...atures_enabled_for_group_shared_context.rb | 20 +- locale/gitlab.pot | 4 +- 39 files changed, 1221 insertions(+), 356 deletions(-) create mode 100644 config/feature_flags/ops/ai_duo_chat_switch.yml create mode 100644 ee/config/saas_features/duo_chat_on_saas.yml diff --git a/config/feature_flags/ops/ai_duo_chat_switch.yml b/config/feature_flags/ops/ai_duo_chat_switch.yml new file mode 100644 index 0000000000000..cd3fe3ab937f1 --- /dev/null +++ b/config/feature_flags/ops/ai_duo_chat_switch.yml @@ -0,0 +1,9 @@ +--- +name: ai_duo_chat_switch +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/434802 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140352 +rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/production/-/issues/17301 +milestone: '16.8' +group: group::ai framework +type: ops +default_enabled: true diff --git a/ee/app/graphql/mutations/ai/action.rb b/ee/app/graphql/mutations/ai/action.rb index abb54d765fe88..1605200725ac3 100644 --- a/ee/app/graphql/mutations/ai/action.rb +++ b/ee/app/graphql/mutations/ai/action.rb @@ -29,10 +29,12 @@ def ready?(**args) end def resolve(**attributes) - check_feature_flag_enabled! verify_rate_limit! resource_id, method, options = extract_method_params!(attributes) + + check_feature_flag_enabled!(method) + resource = resource_id&.then { |id| authorized_find!(id: id) } options[:referer_url] = context[:request].headers["Referer"] if method == :chat @@ -49,10 +51,13 @@ def resolve(**attributes) private - def check_feature_flag_enabled! - return if Feature.enabled?(:ai_global_switch, type: :ops) + def check_feature_flag_enabled!(method) + is_chat = method.eql?(:chat) + + return if Feature.enabled?(:ai_duo_chat_switch, type: :ops) && is_chat + return if Feature.enabled?(:ai_global_switch, type: :ops) && !is_chat - raise Gitlab::Graphql::Errors::ResourceNotAvailable, '`ai_global_switch` feature flag is disabled.' + raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'required feature flag is disabled.' end def verify_rate_limit! diff --git a/ee/app/policies/ee/global_policy.rb b/ee/app/policies/ee/global_policy.rb index 5fd1e63305b6f..098298e989aa6 100644 --- a/ee/app/policies/ee/global_policy.rb +++ b/ee/app/policies/ee/global_policy.rb @@ -79,6 +79,25 @@ module GlobalPolicy @user.code_suggestions_disabled_by_group? end + condition(:duo_chat_enabled_by_instance) do + next true if ::Gitlab::Saas.feature_available?(:duo_chat_on_saas) + + ::Gitlab::CurrentSettings.instance_level_ai_beta_features_enabled? + end + + condition(:duo_chat_licensed) do + next true if ::Gitlab::Saas.feature_available?(:duo_chat_on_saas) + + ::License.feature_available?(:ai_chat) + end + + condition(:user_allowed_to_use_chat) do + next false unless @user + next true unless ::Gitlab::Saas.feature_available?(:duo_chat_on_saas) + + user.any_group_with_ai_available? + end + condition(:user_belongs_to_paid_namespace) do next false unless @user @@ -155,6 +174,8 @@ module GlobalPolicy .enable :access_code_suggestions rule { code_suggestions_disabled_by_group }.prevent :access_code_suggestions + rule { duo_chat_licensed & duo_chat_enabled_by_instance & user_allowed_to_use_chat }.enable :access_duo_chat + rule { runner_upgrade_management_available | user_belongs_to_paid_namespace }.enable :read_runner_upgrade_status rule { security_policy_bot }.policy do diff --git a/ee/app/policies/ee/group_policy.rb b/ee/app/policies/ee/group_policy.rb index 26155b4c65604..cec0a34eb05ea 100644 --- a/ee/app/policies/ee/group_policy.rb +++ b/ee/app/policies/ee/group_policy.rb @@ -257,6 +257,22 @@ module GroupPolicy can?(:developer_access) end + condition(:chat_allowed_for_group, scope: :subject) do + next true unless ::Gitlab::Saas.feature_available?(:duo_chat_on_saas) + + ::Gitlab::Llm::StageCheck.available?(@subject, :chat) + end + + condition(:chat_available_for_user, scope: :global) do + Ability.allowed?(@user, :access_duo_chat) + end + + condition(:membership_for_chat) do + next Ability.allowed?(@user, :read_group, @subject) unless ::Gitlab::Saas.feature_available?(:duo_chat_on_saas) + + @subject.member?(@user) + end + rule { user_banned_from_namespace }.prevent_all rule { public_group | logged_in_viewable }.policy do @@ -639,6 +655,8 @@ module GroupPolicy end rule { guest }.enable :read_limit_alert + + rule { membership_for_chat & chat_allowed_for_group & chat_available_for_user }.enable :access_duo_chat end override :lookup_access_level! diff --git a/ee/app/policies/ee/project_policy.rb b/ee/app/policies/ee/project_policy.rb index 86689a19adb96..b9772a9784af5 100644 --- a/ee/app/policies/ee/project_policy.rb +++ b/ee/app/policies/ee/project_policy.rb @@ -344,6 +344,24 @@ module ProjectPolicy project.ci_cancellation_restriction.no_one_allowed? end + condition(:chat_allowed_for_parent_group, scope: :subject) do + next true unless ::Gitlab::Saas.feature_available?(:duo_chat_on_saas) + + ::Gitlab::Llm::StageCheck.available?(@subject.parent, :chat) + end + + condition(:chat_available_for_user, scope: :global) do + Ability.allowed?(@user, :access_duo_chat) + end + + condition(:membership_for_chat, scope: :global) do + unless ::Gitlab::Saas.feature_available?(:duo_chat_on_saas) + next Ability.allowed?(@user, :read_project, @subject) + end + + team_member? + end + rule { visual_review_bot }.policy do prevent :read_note enable :create_note @@ -852,6 +870,8 @@ module ProjectPolicy rule { can?(:reporter_access) & agent_registry_enabled }.policy do enable :write_ai_agents end + + rule { membership_for_chat & chat_allowed_for_parent_group & chat_available_for_user }.enable :access_duo_chat end override :lookup_access_level! diff --git a/ee/app/services/llm/chat_service.rb b/ee/app/services/llm/chat_service.rb index fc487d1d136cd..505642171ea73 100644 --- a/ee/app/services/llm/chat_service.rb +++ b/ee/app/services/llm/chat_service.rb @@ -17,5 +17,13 @@ def perform def content(_action_name) options[:content] end + + def ai_integration_enabled? + ::Feature.enabled?(:ai_duo_chat_switch, type: :ops) + end + + def user_can_send_to_ai? + user.can?(:access_duo_chat) + end end end diff --git a/ee/app/services/llm/internal/completion_service.rb b/ee/app/services/llm/internal/completion_service.rb index 60209067b4b3b..2c061bdbd96fe 100644 --- a/ee/app/services/llm/internal/completion_service.rb +++ b/ee/app/services/llm/internal/completion_service.rb @@ -15,7 +15,7 @@ def initialize(prompt_message, options = {}) end def execute - return unless Feature.enabled?(:ai_global_switch, type: :ops) + return unless ai_action_enabled?(prompt_message) with_tracking(prompt_message.ai_action) do break unless resource_authorized?(prompt_message) @@ -94,6 +94,14 @@ def update_duration_metric(ai_action_name, duration) ) end + def ai_action_enabled?(prompt_message) + if prompt_message.chat? + Feature.enabled?(:ai_duo_chat_switch, type: :ops) + else + Feature.enabled?(:ai_global_switch, type: :ops) + end + end + def logger @logger ||= Gitlab::Llm::Logger.build end diff --git a/ee/config/saas_features/duo_chat_on_saas.yml b/ee/config/saas_features/duo_chat_on_saas.yml new file mode 100644 index 0000000000000..5de1785485739 --- /dev/null +++ b/ee/config/saas_features/duo_chat_on_saas.yml @@ -0,0 +1,5 @@ +--- +name: duo_chat_on_saas +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140352 +milestone: '16.8' +group: group::ai framework diff --git a/ee/lib/ee/gitlab/saas.rb b/ee/lib/ee/gitlab/saas.rb index 8b94724e9594d..5e1bf4ec4158e 100644 --- a/ee/lib/ee/gitlab/saas.rb +++ b/ee/lib/ee/gitlab/saas.rb @@ -21,6 +21,7 @@ module Saas gitlab_saas_subscriptions duo_chat_categorize_question google_artifact_registry + duo_chat_on_saas ].freeze CONFIG_FILE_ROOT = 'ee/config/saas_features' diff --git a/ee/lib/gitlab/llm/ai_message.rb b/ee/lib/gitlab/llm/ai_message.rb index 0bf1d8e4471e5..6d16e5da625bd 100644 --- a/ee/lib/gitlab/llm/ai_message.rb +++ b/ee/lib/gitlab/llm/ai_message.rb @@ -77,6 +77,10 @@ def slash_command_and_input content.split(' ', 2) end + def chat? + false + end + def ==(other) super || ( self.class == other.class && diff --git a/ee/lib/gitlab/llm/chain/utils/authorizer.rb b/ee/lib/gitlab/llm/chain/utils/authorizer.rb index a992a1bdba25e..f991001d680d2 100644 --- a/ee/lib/gitlab/llm/chain/utils/authorizer.rb +++ b/ee/lib/gitlab/llm/chain/utils/authorizer.rb @@ -12,6 +12,8 @@ def allowed? end def self.context(context:) + return Response.new(allowed: false, message: no_access_message) unless context.current_user + if context.resource && context.container authorization_container = container(container: context.container, user: context.current_user) if authorization_container.allowed? @@ -33,10 +35,14 @@ def self.context_allowed?(context:) end def self.container(container:, user:) - return Response.new(allowed: false, message: not_found_message) unless container.member?(user) + return user(user: user) unless ::Gitlab::Saas.feature_available?(:duo_chat_on_saas) - allowed = Gitlab::Llm::StageCheck.available?(container, :chat) - message = s_("This feature is only allowed in groups that enable this feature.") unless allowed + allowed = user.can?(:access_duo_chat, container) + message = if !allowed && container.member?(user) + no_ai_message + elsif !allowed + not_found_message + end Response.new(allowed: allowed, message: message) end @@ -61,8 +67,8 @@ def self.resource(resource:, user:) end def self.user(user:) - allowed = user.any_group_with_ai_available? - message = s_("You do not have access to AI features.") unless allowed + allowed = user.can?(:access_duo_chat) + message = no_access_message unless allowed Response.new(allowed: allowed, message: message) end @@ -75,6 +81,14 @@ def self.user_as_resource(resource:, user:) def self.not_found_message s_("I am sorry, I am unable to find what you are looking for.") end + + def self.no_access_message + s_("You do not have access to chat feature.") + end + + def self.no_ai_message + s_("This feature is only allowed in groups that enable this feature.") + end end end end diff --git a/ee/lib/gitlab/llm/chat_message.rb b/ee/lib/gitlab/llm/chat_message.rb index b4c138dc8ee4c..cde1808fbda9c 100644 --- a/ee/lib/gitlab/llm/chat_message.rb +++ b/ee/lib/gitlab/llm/chat_message.rb @@ -28,6 +28,10 @@ def clean_history? def question? user? && !conversation_reset? && !clean_history? end + + def chat? + true + end end end end diff --git a/ee/lib/gitlab/llm/tanuki_bot.rb b/ee/lib/gitlab/llm/tanuki_bot.rb index e62a8bf5f7798..8778a29e13334 100644 --- a/ee/lib/gitlab/llm/tanuki_bot.rb +++ b/ee/lib/gitlab/llm/tanuki_bot.rb @@ -12,15 +12,11 @@ class TanukiBot MODEL = 'claude-instant-1.1' def self.enabled_for?(user:, container: nil) - return false unless Feature.enabled?(:ai_global_switch, type: :ops) + return false unless Feature.enabled?(:ai_duo_chat_switch, type: :ops) return false unless user - if container - container.member?(user) && Gitlab::Llm::StageCheck.available?(container.resource_parent, :chat) - else - user.any_group_with_ai_available? - end + container ? user.can?(:access_duo_chat, container) : user.can?(:access_duo_chat) end def self.show_breadcrumbs_entry_point?(user:, container: nil) diff --git a/ee/spec/features/tanuki_bot_chat_spec.rb b/ee/spec/features/tanuki_bot_chat_spec.rb index f1df78c7fabf6..f3946a5c4e53a 100644 --- a/ee/spec/features/tanuki_bot_chat_spec.rb +++ b/ee/spec/features/tanuki_bot_chat_spec.rb @@ -5,59 +5,126 @@ RSpec.describe 'GitLab Duo Chat', :js, feature_category: :global_search do let_it_be(:user) { create(:user) } - before do - allow(License).to receive(:feature_available?).and_return(true) - allow(user).to receive(:any_group_with_ai_available?).and_return(true) + context 'for saas', :saas do + let_it_be_with_reload(:group) { create(:group_with_plan, plan: :ultimate_plan) } - sign_in(user) - end + before_all do + group.add_developer(user) + end - describe 'Feature enabled and available' do before do - visit root_path + sign_in(user) end - shared_examples 'GitLab Duo drawer' do - it 'opens the drawer to chat with GitLab Duo' do - wait_for_requests + describe 'Feature enabled and available' do + include_context 'with ai features enabled for group' + + before do + visit root_path + end + + shared_examples 'GitLab Duo drawer' do + it 'opens the drawer to chat with GitLab Duo' do + wait_for_requests - within_testid('chat-component') do - expect(page).to have_text('GitLab Duo Chat') + within_testid('chat-component') do + expect(page).to have_text('GitLab Duo Chat') + end end end - end - context "when opening the drawer from the help center" do - before do - within_testid('super-sidebar') do - click_button('Help') - click_button('GitLab Duo Chat') + context "when opening the drawer from the help center" do + before do + within_testid('super-sidebar') do + click_button('Help') + click_button('GitLab Duo Chat') + end end + + it_behaves_like 'GitLab Duo drawer' end - it_behaves_like 'GitLab Duo drawer' + context "when opening the drawer from the breadcrumbs" do + before do + within_testid('top-bar') do + click_button('GitLab Duo Chat') + end + end + + it_behaves_like 'GitLab Duo drawer' + end end - context "when opening the drawer from the breadcrumbs" do + context 'when the :tanuki_bot_breadcrumbs_entry_point feature flag is off' do + include_context 'with ai features enabled for group' + before do + stub_feature_flags(tanuki_bot_breadcrumbs_entry_point: false) + visit root_path + end + + it 'does not show the entry point in the breadcrumbs' do within_testid('top-bar') do - click_button('GitLab Duo Chat') + expect(page).not_to have_button('GitLab Duo Chat') end end - - it_behaves_like 'GitLab Duo drawer' end end - context 'when the :tanuki_bot_breadcrumbs_entry_point feature flag is off' do + context 'for self-managed' do before do - stub_feature_flags(tanuki_bot_breadcrumbs_entry_point: false) - visit root_path + sign_in(user) + end + + describe 'Feature enabled and available' do + include_context 'with experiment features enabled for self-managed' + + before do + visit root_path + end + + shared_examples 'GitLab Duo drawer' do + it 'opens the drawer to chat with GitLab Duo' do + wait_for_requests + + within_testid('chat-component') do + expect(page).to have_text('GitLab Duo Chat') + end + end + end + + context "when opening the drawer from the help center" do + before do + within_testid('super-sidebar') do + click_button('Help') + click_button('GitLab Duo Chat') + end + end + + it_behaves_like 'GitLab Duo drawer' + end + + context "when opening the drawer from the breadcrumbs" do + before do + within_testid('top-bar') do + click_button('GitLab Duo Chat') + end + end + + it_behaves_like 'GitLab Duo drawer' + end end - it 'does not show the entry point in the breadcrumbs' do - within_testid('top-bar') do - expect(page).not_to have_button('GitLab Duo Chat') + context 'when the :tanuki_bot_breadcrumbs_entry_point feature flag is off' do + before do + stub_feature_flags(tanuki_bot_breadcrumbs_entry_point: false) + visit root_path + end + + it 'does not show the entry point in the breadcrumbs' do + within_testid('top-bar') do + expect(page).not_to have_button('GitLab Duo Chat') + end end end end diff --git a/ee/spec/graphql/mutations/ai/action_spec.rb b/ee/spec/graphql/mutations/ai/action_spec.rb index fe782686e3677..89d4ec4fd1465 100644 --- a/ee/spec/graphql/mutations/ai/action_spec.rb +++ b/ee/spec/graphql/mutations/ai/action_spec.rb @@ -39,6 +39,24 @@ mutation.resolve(**input) end + shared_examples_for 'an AI action when feature flag disabled' do + context 'when the user can perform AI action' do + before do + resource.project.add_developer(user) + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(ai_global_switch: false) + end + + it 'raises error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + end + end + shared_examples_for 'an AI action' do context 'when resource_id is not for an Ai::Model' do let(:resource_id) { "gid://gitlab/Note/#{resource.id}" } @@ -83,6 +101,10 @@ .to receive(:allowed?) .with(user, "read_#{resource.to_ability_name}", resource) .and_return(true) + + allow(Ability) + .to receive(:allowed?) + .and_call_original end context 'when the user is not a member' do @@ -105,16 +127,6 @@ resource.project.add_developer(user) end - context 'when feature flag is disabled' do - before do - stub_feature_flags(ai_global_switch: false) - end - - it 'raises error' do - expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) - end - end - it 'calls Llm::ExecuteMethodService' do expect_next_instance_of( Llm::ExecuteMethodService, @@ -190,6 +202,22 @@ let(:expected_method) { :chat } let(:expected_options) { { referer_url: "foobar", user_agent: "user-agent" } } end + + context 'when the user can perform AI action' do + before do + resource.project.add_developer(user) + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(ai_duo_chat_switch: false) + end + + it 'raises error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + end end context 'when summarize_comments input is set' do @@ -198,6 +226,7 @@ let(:expected_options) { { user_agent: "user-agent" } } it_behaves_like 'an AI action' + it_behaves_like 'an AI action when feature flag disabled' end context 'when client_subscription_id input is set' do @@ -206,9 +235,10 @@ let(:expected_options) { { client_subscription_id: 'id', user_agent: 'user-agent' } } it_behaves_like 'an AI action' + it_behaves_like 'an AI action when feature flag disabled' end - context 'when explain_vulnerability input is set' do + context 'when explain_vulnerability input is set', :saas do before do allow(Ability) .to receive(:allowed?) @@ -225,6 +255,7 @@ let(:expected_options) { { include_source_code: true, user_agent: 'user-agent' } } it_behaves_like 'an AI action' + it_behaves_like 'an AI action when feature flag disabled' end end end diff --git a/ee/spec/lib/gitlab/llm/ai_message_spec.rb b/ee/spec/lib/gitlab/llm/ai_message_spec.rb index 3ac7f6b13d48b..efa83d8cd4c23 100644 --- a/ee/spec/lib/gitlab/llm/ai_message_spec.rb +++ b/ee/spec/lib/gitlab/llm/ai_message_spec.rb @@ -187,4 +187,10 @@ expect(m1).not_to eq(m2) end end + + describe "#chat?" do + it 'returns true for chat message' do + expect(subject).not_to be_chat + end + end end diff --git a/ee/spec/lib/gitlab/llm/chain/tools/json_reader/executor_spec.rb b/ee/spec/lib/gitlab/llm/chain/tools/json_reader/executor_spec.rb index 38d6b06e62b60..ec43d8882d83e 100644 --- a/ee/spec/lib/gitlab/llm/chain/tools/json_reader/executor_spec.rb +++ b/ee/spec/lib/gitlab/llm/chain/tools/json_reader/executor_spec.rb @@ -6,8 +6,8 @@ subject(:reader) { described_class.new(context: context, options: options, stream_response_handler: nil) } let_it_be(:user) { create(:user) } - let_it_be(:group) { create(:group_with_plan, plan: :ultimate_plan) } - let_it_be(:project) { create(:project, group: group, name: "My sweet project with robots in it") } + let_it_be_with_reload(:group) { create(:group_with_plan, plan: :ultimate_plan) } + let_it_be_with_reload(:project) { create(:project, group: group, name: "My sweet project with robots in it") } let_it_be(:issue) do create(:issue, project: project, title: "AI should be included at birthdays", description: description_content) end @@ -34,7 +34,6 @@ allow(reader).to receive(:provider_prompt_class) .and_return(::Gitlab::Llm::Chain::Tools::JsonReader::Prompts::Anthropic) stub_const("::Gitlab::Llm::Chain::Tools::JsonReader::Prompts::Anthropic::MAX_CHARACTERS", 4) - allow(Gitlab::Llm::StageCheck).to receive(:available?).with(project, :chat).and_return(true) end describe '#name' do @@ -58,8 +57,14 @@ describe '#execute' do before do + allow(Gitlab).to receive(:org_or_com?).and_return(true) stub_ee_application_setting(should_check_namespace_plan: true) - stub_licensed_features(experimental_features: true, ai_features: true) + allow(group.namespace_settings).to receive(:experiment_settings_allowed?).and_return(true) + stub_licensed_features( + ai_features: true, + epics: true, + experimental_features: true + ) group.namespace_settings.update!(experiment_features_enabled: true) end diff --git a/ee/spec/lib/gitlab/llm/chain/utils/authorizer_spec.rb b/ee/spec/lib/gitlab/llm/chain/utils/authorizer_spec.rb index a10581288c241..2600fb8a73870 100644 --- a/ee/spec/lib/gitlab/llm/chain/utils/authorizer_spec.rb +++ b/ee/spec/lib/gitlab/llm/chain/utils/authorizer_spec.rb @@ -2,241 +2,471 @@ require 'spec_helper' -RSpec.describe Gitlab::Llm::Chain::Utils::Authorizer, :saas, feature_category: :duo_chat do - let_it_be(:group) { create(:group_with_plan, :public, plan: :ultimate_plan) } - let_it_be_with_reload(:project) { create(:project, group: group) } - let_it_be_with_reload(:resource) { create(:issue, project: project) } - let_it_be(:user) { create(:user) } - let(:container) { project } - let(:context) do - Gitlab::Llm::Chain::GitlabContext.new( - current_user: user, - container: container, - resource: resource, - ai_request: nil - ) - end +RSpec.describe Gitlab::Llm::Chain::Utils::Authorizer, feature_category: :duo_chat do + context 'for saas', :saas do + let_it_be(:group) { create(:group_with_plan, :public, plan: :ultimate_plan) } + let_it_be_with_reload(:project) { create(:project, group: group) } + let_it_be_with_reload(:resource) { create(:issue, project: project) } + let_it_be(:user) { create(:user) } + let(:container) { project } + let(:context) do + Gitlab::Llm::Chain::GitlabContext.new( + current_user: user, + container: container, + resource: resource, + ai_request: nil + ) + end - subject(:authorizer) { described_class } + subject(:authorizer) { described_class } - before_all do - group.add_developer(user) - end + before_all do + group.add_developer(user) + end - shared_examples 'user authorization' do - context 'when user has groups with ai available' do - include_context 'with ai features enabled for group' - it 'returns true' do - expect(authorizer.user(user: user).allowed?).to be(true) - end + before do + allow(Gitlab).to receive(:org_or_com?).and_return(true) end - context 'when user has no groups with ai available' do - include_context 'with experiment features disabled for group' + shared_examples 'user authorization' do + context 'when user has groups with ai available' do + include_context 'with ai features enabled for group' + it 'returns true' do + expect(authorizer.user(user: user).allowed?).to be(true) + end + end + + context 'when user has no groups with ai available' do + include_context 'with experiment features disabled for group' - it 'returns true when user has no groups with ai available' do - expect(authorizer.user(user: user).allowed?).to be(false) - expect(authorizer.user(user: user).message).to eq('You do not have access to AI features.') + it 'returns true when user has no groups with ai available' do + expect(authorizer.user(user: user).allowed?).to be(false) + expect(authorizer.user(user: user).message).to eq('You do not have access to chat feature.') + end end end - end - describe '.context.allowed?' do - context 'when both resource and container are present' do - context 'when container is authorized' do - include_context 'with ai features enabled for group' + describe '.context.allowed?' do + context 'when both resource and container are present' do + context 'when container is authorized' do + include_context 'with ai features enabled for group' + + it 'returns true if both resource and container are authorized' do + expect(authorizer.context(context: context).allowed?).to be(true) + end + + it 'returns false if resource is not authorized' do + group.members.first.destroy! - it 'returns true if both resource and container are authorized' do - expect(authorizer.context(context: context).allowed?).to be(true) + expect(authorizer.context(context: context).allowed?).to be(false) + expect(authorizer.context(context: context).message) + .to include('I am unable to find what you are looking for.') + end end - it 'returns false if resource is not authorized' do - group.members.first.destroy! + context 'when container is not authorized' do + include_context 'with experiment features disabled for group' - expect(authorizer.context(context: context).allowed?).to be(false) - expect(authorizer.context(context: context).message) - .to include('I am unable to find what you are looking for.') + it 'returns false if container is not authorized' do + expect(authorizer.context(context: context).allowed?).to be(false) + expect(authorizer.context(context: context).message) + .to eq('This feature is only allowed in groups that enable this feature.') + end end end - context 'when container is not authorized' do - include_context 'with experiment features disabled for group' + context 'when only resource is present' do + let(:context) do + Gitlab::Llm::Chain::GitlabContext.new( + current_user: user, + container: nil, + resource: resource, + ai_request: nil + ) + end + + context 'when resource container is authorized' do + include_context 'with ai features enabled for group' - it 'returns false if container is not authorized' do - expect(authorizer.context(context: context).allowed?).to be(false) - expect(authorizer.context(context: context).message) - .to eq('This feature is only allowed in groups that enable this feature.') + it 'returns true' do + expect(authorizer.context(context: context).allowed?).to be(true) + end end - end - end - context 'when only resource is present' do - let(:context) do - Gitlab::Llm::Chain::GitlabContext.new( - current_user: user, - container: nil, - resource: resource, - ai_request: nil - ) + context 'when container is not authorized' do + include_context 'with experiment features disabled for group' + + it 'returns false' do + expect(authorizer.context(context: context).allowed?).to be(false) + end + end end - context 'when resource container is authorized' do - include_context 'with ai features enabled for group' + context 'when only container is present' do + let(:context) do + Gitlab::Llm::Chain::GitlabContext.new( + current_user: nil, + container: container, + resource: nil, + ai_request: nil + ) + end - it 'returns true' do - expect(authorizer.context(context: context).allowed?).to be(true) + context 'when container is authorized' do + include_context 'with ai features enabled for group' + + it 'returns true' do + expect(authorizer.context(context: context).allowed?).to be(false) + end + end + + context 'when container is not authorized' do + include_context 'with experiment features disabled for group' + + it 'returns false' do + expect(authorizer.context(context: context).allowed?).to be(false) + end end end - context 'when container is not authorized' do - include_context 'with experiment features disabled for group' + context 'when neither resource nor container is present' do + let(:context) do + Gitlab::Llm::Chain::GitlabContext.new( + current_user: user, + container: nil, + resource: nil, + ai_request: nil + ) + end - it 'returns false' do - expect(authorizer.context(context: context).allowed?).to be(false) + context 'when user is authorized' do + include_context 'with ai features enabled for group' + + it 'returns true' do + expect(authorizer.context(context: context).allowed?).to be(true) + end + end + + context 'when user is not authorized' do + include_context 'with experiment features disabled for group' + + it 'returns false' do + expect(authorizer.context(context: context).allowed?).to be(false) + end end end end - context 'when only container is present' do - let(:context) do - Gitlab::Llm::Chain::GitlabContext.new( - current_user: nil, - container: container, - resource: nil, - ai_request: nil - ) + describe '.container' do + it "calls policy with the appropriate arguments" do + expect(user).to receive(:can?).with(:access_duo_chat, container) + + authorizer.container(container: context.container, user: user) end - context 'when container is authorized' do - include_context 'with ai features enabled for group' + it 'uses resource from argument' do + new_container = create(:group) + allow(user).to receive(:can?).with(:admin_all_resources).and_call_original - it 'returns true' do - expect(authorizer.context(context: context).allowed?).to be(false) - end + expect(user).to receive(:can?).at_least(:once).with(:access_duo_chat, new_container) + + authorizer.container(container: new_container, user: user) end + end - context 'when container is not authorized' do - include_context 'with experiment features disabled for group' + describe '.resource' do + context 'when resource is nil' do + let(:resource) { nil } it 'returns false' do - expect(authorizer.context(context: context).allowed?).to be(false) + expect(authorizer.resource(resource: context.resource, user: context.current_user).allowed?) + .to be(false) end end - end - context 'when neither resource nor container is present' do - let(:context) do - Gitlab::Llm::Chain::GitlabContext.new( - current_user: user, - container: nil, - resource: nil, - ai_request: nil - ) + context 'when resource parent is not authorized' do + include_context 'with experiment features disabled for group' + + it 'returns false' do + expect(authorizer.resource(resource: context.resource, user: context.current_user).allowed?) + .to be(false) + end end - context 'when user is authorized' do + context 'when resource container is authorized' do include_context 'with ai features enabled for group' - it 'returns true' do - expect(authorizer.context(context: context).allowed?).to be(true) + it 'calls user.can? with the appropriate arguments' do + expect(user).to receive(:can?).with('read_issue', resource) + + authorizer.resource(resource: context.resource, user: context.current_user) + end + + it 'uses resource from argument' do + new_resource = build(:epic) + + expect(new_resource).to receive(:resource_parent).and_return(group) + expect(user).to receive(:can?).with('read_epic', new_resource) + + authorizer.resource(resource: new_resource, user: context.current_user) end end - context 'when user is not authorized' do - include_context 'with experiment features disabled for group' + context 'when resource is current user' do + context 'when user is not in any group with ai' do + include_context 'with experiment features disabled for group' - it 'returns false' do - expect(authorizer.context(context: context).allowed?).to be(false) + it 'returns false' do + expect(authorizer.resource(resource: context.current_user, user: context.current_user).allowed?) + .to be(false) + end + end + + context 'when user is in any group with ai' do + include_context 'with ai features enabled for group' + + it 'returns true' do + expect(authorizer.resource(resource: context.current_user, user: context.current_user).allowed?) + .to be(true) + end + + context 'when resource is different user' do + let(:resource) { build(:user) } + + it 'returns false' do + expect(authorizer.resource(resource: resource, user: context.current_user).allowed?) + .to be(false) + end + end end end end - end - describe '.container?' do - it "calls Gitlab::Llm::StageCheck.available? with the appropriate arguments" do - expect(Gitlab::Llm::StageCheck).to receive(:available?).with(container, :chat) + describe '.user' do + it_behaves_like 'user authorization' + end + end - authorizer.container(container: context.container, user: user) + context 'for self-managed' do + let_it_be(:group) { create(:group) } + let_it_be_with_reload(:project) { create(:project, group: group) } + let_it_be_with_reload(:resource) { create(:issue, project: project) } + let_it_be(:user) { create(:user) } + let(:container) { project } + let(:context) do + Gitlab::Llm::Chain::GitlabContext.new( + current_user: user, + container: container, + resource: resource, + ai_request: nil + ) end - it 'uses resource from argument' do - new_container = build(:group) - expect(new_container).to receive(:member?).and_return(true) - expect(Gitlab::Llm::StageCheck).to receive(:available?).with(new_container, :chat) + subject(:authorizer) { described_class } - authorizer.container(container: new_container, user: user) + before_all do + group.add_developer(user) end - end - describe '.resource' do - context 'when resource is nil' do - let(:resource) { nil } + shared_examples 'user authorization' do + context 'when ai is enabled for self-managed' do + include_context 'with experiment features enabled for self-managed' + it 'returns true' do + expect(authorizer.user(user: user).allowed?).to be(true) + end + end + + context 'when ai is disabled for self-managed' do + include_context 'with experiment features disabled for self-managed' - it 'returns false' do - expect(authorizer.resource(resource: context.resource, user: context.current_user).allowed?) - .to be(false) + it 'returns true when user has no groups with ai available' do + expect(authorizer.user(user: user).allowed?).to be(false) + expect(authorizer.user(user: user).message).to eq('You do not have access to chat feature.') + end end end - context 'when resource parent is not authorized' do - include_context 'with experiment features disabled for group' + describe '.context.allowed?' do + context 'when both resource and container are present' do + context 'when ai is enabled for self-managed' do + include_context 'with experiment features enabled for self-managed' + + it 'returns true if both resource and container are authorized' do + expect(authorizer.context(context: context).allowed?).to be(true) + end + + it 'returns false if resource is not authorized' do + group.members.first.destroy! + + expect(authorizer.context(context: context).allowed?).to be(false) + expect(authorizer.context(context: context).message) + .to include('I am unable to find what you are looking for.') + end + end - it 'returns false' do - expect(authorizer.resource(resource: context.resource, user: context.current_user).allowed?) - .to be(false) + context 'when ai is disabled for self-managed' do + include_context 'with experiment features disabled for self-managed' + + it 'returns false if container is not authorized' do + expect(authorizer.context(context: context).allowed?).to be(false) + expect(authorizer.context(context: context).message) + .to eq('You do not have access to chat feature.') + end + end end - end - context 'when resource container is authorized' do - include_context 'with ai features enabled for group' + context 'when only resource is present' do + let(:context) do + Gitlab::Llm::Chain::GitlabContext.new( + current_user: user, + container: nil, + resource: resource, + ai_request: nil + ) + end + + context 'when ai is enabled for self-managed' do + include_context 'with experiment features enabled for self-managed' - it 'calls user.can? with the appropriate arguments' do - expect(user).to receive(:can?).with('read_issue', resource) + it 'returns true' do + expect(authorizer.context(context: context).allowed?).to be(true) + end + end - authorizer.resource(resource: context.resource, user: context.current_user) + context 'when ai is disabled for self-managed' do + include_context 'with experiment features disabled for self-managed' + + it 'returns false' do + expect(authorizer.context(context: context).allowed?).to be(false) + end + end end - it 'uses resource from argument' do - new_resource = build(:epic) + context 'when only container is present' do + let(:context) do + Gitlab::Llm::Chain::GitlabContext.new( + current_user: nil, + container: container, + resource: nil, + ai_request: nil + ) + end + + context 'when ai is enabled for self-managed' do + include_context 'with experiment features enabled for self-managed' + + it 'returns false' do + expect(authorizer.context(context: context).allowed?).to be(false) + end + end - expect(new_resource).to receive(:resource_parent).and_return(group) - expect(user).to receive(:can?).with('read_epic', new_resource) + context 'when ai is disabled for self-managed' do + include_context 'with experiment features disabled for self-managed' - authorizer.resource(resource: new_resource, user: context.current_user) + it 'returns false' do + expect(authorizer.context(context: context).allowed?).to be(false) + end + end + end + + context 'when neither resource nor container is present' do + let(:context) do + Gitlab::Llm::Chain::GitlabContext.new( + current_user: user, + container: nil, + resource: nil, + ai_request: nil + ) + end + + context 'when ai is enabled for self-managed' do + include_context 'with experiment features enabled for self-managed' + + it 'returns true' do + expect(authorizer.context(context: context).allowed?).to be(true) + end + end + + context 'when ai is disabled for self-managed' do + include_context 'with experiment features disabled for self-managed' + + it 'returns false' do + expect(authorizer.context(context: context).allowed?).to be(false) + end + end end end - context 'when resource is current user' do - context 'when user is not in any group with ai' do - include_context 'with experiment features disabled for group' + describe '.resource' do + context 'when resource is nil' do + let(:resource) { nil } it 'returns false' do - expect(authorizer.resource(resource: context.current_user, user: context.current_user).allowed?) + expect(authorizer.resource(resource: context.resource, user: context.current_user).allowed?) .to be(false) end end - context 'when user is in any group with ai' do - include_context 'with ai features enabled for group' + context 'when ai is disabled for self-managed' do + include_context 'with experiment features disabled for self-managed' - it 'returns true' do - expect(authorizer.resource(resource: context.current_user, user: context.current_user).allowed?) - .to be(true) + it 'returns false' do + expect(authorizer.resource(resource: context.resource, user: context.current_user).allowed?) + .to be(false) end + end + + context 'when ai is enabled for self-managed' do + include_context 'with experiment features enabled for self-managed' + + it 'calls user.can? with the appropriate arguments' do + expect(user).to receive(:can?).with('read_issue', resource) + + authorizer.resource(resource: context.resource, user: context.current_user) + end + + it 'uses resource from argument' do + new_resource = build(:epic) + + expect(new_resource).to receive(:resource_parent).and_return(group) + expect(user).to receive(:can?).with('read_epic', new_resource) + + authorizer.resource(resource: new_resource, user: context.current_user) + end + end - context 'when resource is different user' do - let(:resource) { build(:user) } + context 'when resource is current user' do + context 'when ai is disabled for self-managed' do + include_context 'with experiment features disabled for self-managed' it 'returns false' do - expect(authorizer.resource(resource: resource, user: context.current_user).allowed?) + expect(authorizer.resource(resource: context.current_user, user: context.current_user).allowed?) .to be(false) end end + + context 'when ai is enabled for self-managed' do + include_context 'with experiment features enabled for self-managed' + + it 'returns true' do + expect(authorizer.resource(resource: context.current_user, user: context.current_user).allowed?) + .to be(true) + end + + context 'when resource is different user' do + let(:resource) { build(:user) } + + it 'returns false' do + expect(authorizer.resource(resource: resource, user: context.current_user).allowed?) + .to be(false) + end + end + end end end - end - describe '.user' do - it_behaves_like 'user authorization' + describe '.user' do + it_behaves_like 'user authorization' + end end end diff --git a/ee/spec/lib/gitlab/llm/chat_message_spec.rb b/ee/spec/lib/gitlab/llm/chat_message_spec.rb index fbe1a852bef72..6cbdacc24db06 100644 --- a/ee/spec/lib/gitlab/llm/chat_message_spec.rb +++ b/ee/spec/lib/gitlab/llm/chat_message_spec.rb @@ -77,4 +77,10 @@ end end end + + describe "#chat?" do + it 'returns true for chat message' do + expect(subject).to be_chat + end + end end diff --git a/ee/spec/lib/gitlab/llm/completions/summarize_all_open_notes_spec.rb b/ee/spec/lib/gitlab/llm/completions/summarize_all_open_notes_spec.rb index c4ef1d3a843c7..f10d25f717e9a 100644 --- a/ee/spec/lib/gitlab/llm/completions/summarize_all_open_notes_spec.rb +++ b/ee/spec/lib/gitlab/llm/completions/summarize_all_open_notes_spec.rb @@ -66,7 +66,7 @@ let(:options) { {} } let_it_be(:user) { create(:user) } - let_it_be(:group) { create(:group_with_plan, plan: :ultimate_plan) } + let_it_be_with_reload(:group) { create(:group_with_plan, plan: :ultimate_plan) } let_it_be(:project) { create(:project, group: group) } before_all do @@ -75,15 +75,17 @@ end before do - stub_application_setting(check_namespace_plan: true) - stub_licensed_features(summarize_notes: true, ai_features: true, epics: true, experimental_features: true) - - group.namespace_settings.update!( - experiment_features_enabled: true - ) - project.root_ancestor.update!( - experiment_features_enabled: true + allow(Gitlab).to receive(:org_or_com?).and_return(true) + stub_ee_application_setting(should_check_namespace_plan: true) + allow(group.namespace_settings).to receive(:experiment_settings_allowed?).and_return(true) + stub_licensed_features( + summarize_notes: true, + ai_features: true, + epics: true, + experimental_features: true ) + group.namespace_settings.update!(experiment_features_enabled: true) + project.reload end context 'with invalid params' do diff --git a/ee/spec/lib/gitlab/llm/tanuki_bot_spec.rb b/ee/spec/lib/gitlab/llm/tanuki_bot_spec.rb index 9369804e8a7b4..f52c477f2a20a 100644 --- a/ee/spec/lib/gitlab/llm/tanuki_bot_spec.rb +++ b/ee/spec/lib/gitlab/llm/tanuki_bot_spec.rb @@ -28,125 +28,185 @@ subject(:execute) { instance.execute } - describe '.enabled_for?', :saas, :use_clean_rails_redis_caching do - let_it_be_with_reload(:group) { create(:group_with_plan, plan: :ultimate_plan) } - - context 'when user present and container is not present' do - where(:ai_global_switch_enabled, :ai_features_available_to_user, :result) do - [ - [true, true, true], - [true, false, false], - [false, true, false], - [false, false, false] - ] - end - - with_them do - before do - stub_feature_flags(ai_global_switch: ai_global_switch_enabled) - allow(user).to receive(:any_group_with_ai_available?).and_return(ai_features_available_to_user) + describe '.enabled_for?', :use_clean_rails_redis_caching do + context 'for saas', :saas do + let_it_be_with_reload(:group) { create(:group_with_plan, plan: :ultimate_plan) } + + context 'when user present and container is not present' do + where(:ai_duo_chat_switch_enabled, :ai_features_available_to_user, :result) do + [ + [true, true, true], + [true, false, false], + [false, true, false], + [false, false, false] + ] end - it 'returns correct result' do - expect(described_class.enabled_for?(user: user)).to be(result) + with_them do + before do + stub_feature_flags(ai_duo_chat_switch: ai_duo_chat_switch_enabled) + allow(user).to receive(:any_group_with_ai_available?).and_return(ai_features_available_to_user) + end + + it 'returns correct result' do + expect(described_class.enabled_for?(user: user)).to be(result) + end end end - end - context 'when user and container are both present' do - context 'when container is a group with AI enabled' do - include_context 'with ai features enabled for group' + context 'when user and container are both present' do + context 'when container is a group with AI enabled' do + include_context 'with ai features enabled for group' - context 'when user is a member of the group' do - before_all do - group.add_guest(user) - end + context 'when user is a member of the group' do + before_all do + group.add_guest(user) + end - context 'when container is a group' do - it 'returns true' do - expect( - described_class.enabled_for?(user: user, container: group) - ).to be(true) + context 'when container is a group' do + it 'returns true' do + expect( + described_class.enabled_for?(user: user, container: group) + ).to be(true) + end end - end - context 'when container is a project' do - let_it_be(:project) { create(:project, group: group) } + context 'when container is a project' do + let_it_be(:project) { create(:project, group: group) } - it 'returns true' do - expect( - described_class.enabled_for?(user: user, container: project) - ).to be(true) + it 'returns true' do + expect( + described_class.enabled_for?(user: user, container: project) + ).to be(true) + end end - end - context 'when the group does not have an Ultimate SaaS license' do - let_it_be(:group) { create(:group) } + context 'when the group does not have an Ultimate SaaS license' do + let_it_be(:group) { create(:group) } - it 'returns false' do - allow(user).to receive(:any_group_with_ai_available?).and_return(true) - # add user as a member of the non-licensed group to ensure the - # test isn't failing at the membership check - group.add_guest(user) + it 'returns false' do + allow(user).to receive(:any_group_with_ai_available?).and_return(true) + # add user as a member of the non-licensed group to ensure the + # test isn't failing at the membership check + group.add_guest(user) + + expect( + described_class.enabled_for?(user: user, container: group) + ).to be(false) + end + end + end + + context 'when user is not a member of the group' do + context 'when the user has AI enabled via another group' do + it 'returns false' do + allow(user).to receive(:any_group_with_ai_available?).and_return(true) - expect( - described_class.enabled_for?(user: user, container: group) - ).to be(false) + expect( + described_class.enabled_for?(user: user, container: group) + ).to be(false) + end end end end - context 'when user is not a member of the group' do - context 'when the user has AI enabled via another group' do - it 'returns false' do + context 'when container is not a group with AI enabled' do + context 'when user has AI enabled' do + before do allow(user).to receive(:any_group_with_ai_available?).and_return(true) + end - expect( - described_class.enabled_for?(user: user, container: group) - ).to be(false) + context 'when container is a group' do + include_context 'with experiment features disabled for group' + + it 'returns false' do + allow(user).to receive(:any_group_with_ai_available?).and_return(true) + + expect( + described_class.enabled_for?(user: user, container: group) + ).to be(false) + end + end + + context 'when container is a project in a personal namespace' do + let_it_be(:project) { create(:project, namespace: user.namespace) } + + it 'returns false' do + expect( + described_class.enabled_for?(user: user, container: project) + ).to be(false) + end end end end end - context 'when container is not a group with AI enabled' do - context 'when user has AI enabled' do + context 'when user not present, container is present' do + include_context 'with ai features enabled for group' + + it 'returns false' do + expect( + described_class.enabled_for?(user: nil, container: group) + ).to be(false) + end + end + end + + context 'for self-managed' do + let_it_be_with_reload(:group) { create(:group) } + + let_it_be(:user) { create(:user) } + + context 'for self-managed' do + before do + allow(Gitlab).to receive(:org_or_com?).and_return(false) + end + + context 'with experiments and beta enabled' do before do - allow(user).to receive(:any_group_with_ai_available?).and_return(true) + stub_application_setting(instance_level_ai_beta_features_enabled: true) end - context 'when container is a group' do - include_context 'with experiment features disabled for group' + context 'without licensed feature' do + before do + stub_licensed_features(ai_chat: false) + end it 'returns false' do - allow(user).to receive(:any_group_with_ai_available?).and_return(true) - - expect( - described_class.enabled_for?(user: user, container: group) - ).to be(false) + expect(described_class.enabled_for?(user: user)).to eq(false) end end - context 'when container is a project in a personal namespace' do - let_it_be(:project) { create(:project, namespace: user.namespace) } + context 'with licensed feature' do + before do + stub_licensed_features(ai_chat: true) + end - it 'returns false' do - expect( - described_class.enabled_for?(user: user, container: project) - ).to be(false) + it 'returns true' do + expect(described_class.enabled_for?(user: user)).to eq(true) end end end + + context 'with experiments and beta disabled' do + before do + stub_application_setting(instance_level_ai_beta_features_enabled: false) + end + + it 'returns false' do + expect(described_class.enabled_for?(user: user)).to eq(false) + end + end end - end - context 'when user not present, container is present' do - include_context 'with ai features enabled for group' + context 'when user not present, container is present' do + include_context 'with experiment features enabled for self-managed' - it 'returns false' do - expect( - described_class.enabled_for?(user: nil, container: group) - ).to be(false) + it 'returns false' do + expect( + described_class.enabled_for?(user: nil, container: group) + ).to be(false) + end end end end @@ -193,7 +253,7 @@ context 'with the tanuki_bot license available' do context 'when on Gitlab.com' do before do - allow(::Gitlab).to receive(:com?).and_return(true) + allow(::Gitlab).to receive(:org_or_com?).and_return(true) end context 'when no user is provided' do diff --git a/ee/spec/policies/global_policy_spec.rb b/ee/spec/policies/global_policy_spec.rb index ed0f44d84cd67..97919d2014e69 100644 --- a/ee/spec/policies/global_policy_spec.rb +++ b/ee/spec/policies/global_policy_spec.rb @@ -625,6 +625,57 @@ end end + describe 'access_duo_chat' do + let(:policy) { :access_duo_chat } + + let_it_be_with_reload(:current_user) { create(:user) } + + context 'when on .org or .com', :saas do + let_it_be_with_reload(:group) { create(:group_with_plan, plan: :ultimate_plan) } + + context 'when user is member of a group' do + before do + group.add_developer(current_user) + end + + context 'when group has AI licensing and settings' do + include_context 'with ai features enabled for group' + + it { is_expected.to be_allowed(:access_duo_chat) } + end + + context 'when group has not AI licensing and settings' do + include_context 'with experiment features disabled for group' + + it { is_expected.to be_disallowed(:access_duo_chat) } + end + end + + context 'when user is not a member of a group' do + it { is_expected.to be_disallowed(:access_duo_chat) } + end + end + + context 'when not on .org or .com' do + where(:licensed, :instance_level_ai_beta_features_enabled, :cs_matcher) do + true | false | be_disallowed(policy) + true | true | be_allowed(policy) + false | false | be_disallowed(policy) + false | true | be_disallowed(policy) + end + + with_them do + before do + allow(::Gitlab).to receive(:org_or_com?).and_return(false) + stub_ee_application_setting(instance_level_ai_beta_features_enabled: instance_level_ai_beta_features_enabled) + stub_licensed_features(ai_chat: licensed) + end + + it { is_expected.to cs_matcher } + end + end + end + describe 'git access' do context 'security policy bot' do let(:current_user) { security_policy_bot } diff --git a/ee/spec/policies/group_policy_spec.rb b/ee/spec/policies/group_policy_spec.rb index dcb84e5949d7f..51e4edd6558e3 100644 --- a/ee/spec/policies/group_policy_spec.rb +++ b/ee/spec/policies/group_policy_spec.rb @@ -2865,6 +2865,86 @@ def expect_private_group_permissions_as_if_non_member end end + describe 'access_duo_chat' do + let_it_be(:current_user) { create(:user) } + + subject { described_class.new(current_user, group) } + + context 'when on SaaS instance', :saas do + let_it_be_with_reload(:group) { create(:group_with_plan, plan: :ultimate_plan) } + + context 'when container is a group with AI enabled' do + include_context 'with ai features enabled for group' + + context 'when user is a member of the group' do + before do + group.add_guest(current_user) + end + + it { is_expected.to be_allowed(:access_duo_chat) } + + context 'when the group does not have an Ultimate SaaS license' do + let_it_be(:group) { create(:group) } + + it { is_expected.to be_disallowed(:access_duo_chat) } + end + end + + context 'when the user has AI enabled via another group' do + it 'is disallowed' do + allow(current_user).to receive(:any_group_with_ai_available?).and_return(true) + + is_expected.to be_disallowed(:access_duo_chat) + end + end + end + + context 'when group has not AI enabled' do + context 'when user has AI enabled' do + before do + allow(current_user).to receive(:any_group_with_ai_available?).and_return(true) + end + + context 'when container is a group' do + include_context 'with experiment features disabled for group' + + it 'returns false' do + allow(current_user).to receive(:any_group_with_ai_available?).and_return(true) + + is_expected.to be_disallowed(:access_duo_chat) + end + end + end + end + end + + context 'for self-managed' do + using RSpec::Parameterized::TableSyntax + + let_it_be_with_reload(:group) { create(:group) } + let(:policy) { :access_duo_chat } + + context 'when not on .org or .com' do + where(:licensed, :instance_level_ai_beta_features_enabled, :cs_matcher) do + true | false | be_disallowed(policy) + true | true | be_allowed(policy) + false | false | be_disallowed(policy) + false | true | be_disallowed(policy) + end + + with_them do + before do + allow(::Gitlab).to receive(:org_or_com?).and_return(false) + stub_ee_application_setting(instance_level_ai_beta_features_enabled: instance_level_ai_beta_features_enabled) + stub_licensed_features(ai_chat: licensed) + end + + it { is_expected.to cs_matcher } + end + end + end + end + context 'custom role' do let_it_be(:guest) { create(:user) } let_it_be(:parent_group) { create(:group) } diff --git a/ee/spec/policies/project_policy_spec.rb b/ee/spec/policies/project_policy_spec.rb index 9caa4cd833297..35b7aade6870e 100644 --- a/ee/spec/policies/project_policy_spec.rb +++ b/ee/spec/policies/project_policy_spec.rb @@ -3338,4 +3338,105 @@ def create_member_role(member, abilities = member_role_abilities) end end end + + describe 'access_duo_chat' do + let_it_be(:current_user) { create(:user) } + let(:project) { create(:project, group: group) } + + subject { described_class.new(current_user, project) } + + context 'when on SaaS instance', :saas do + let_it_be_with_reload(:group) { create(:group_with_plan, plan: :ultimate_plan) } + + context 'when container is a group with AI enabled' do + include_context 'with ai features enabled for group' + + context 'when user is a member of the group' do + before do + group.add_guest(current_user) + end + + it { is_expected.to be_allowed(:access_duo_chat) } + + context 'when the group does not have an Ultimate SaaS license' do + let_it_be(:group) { create(:group) } + + it { is_expected.to be_disallowed(:access_duo_chat) } + end + end + + context 'when user is not a member of the parent group' do + context 'when the user has AI enabled via another group' do + it 'is disallowed' do + allow(current_user).to receive(:any_group_with_ai_available?).and_return(true) + + is_expected.to be_disallowed(:access_duo_chat) + end + end + end + + context 'when user is a member of the project' do + before do + project.add_guest(current_user) + end + + context 'when the user has AI enabled through parent group' do + it 'is allowed' do + allow(current_user).to receive(:any_group_with_ai_available?).and_return(true) + + is_expected.to be_allowed(:access_duo_chat) + end + end + end + end + + context 'when group has not AI enabled' do + context 'when user has AI enabled' do + before do + allow(current_user).to receive(:any_group_with_ai_available?).and_return(true) + end + + context 'when container is a group' do + include_context 'with experiment features disabled for group' + + it 'returns false' do + allow(current_user).to receive(:any_group_with_ai_available?).and_return(true) + + is_expected.to be_disallowed(:access_duo_chat) + end + end + end + end + end + + context 'for self-managed' do + using RSpec::Parameterized::TableSyntax + + let_it_be_with_reload(:group) { create(:group) } + let(:policy) { :access_duo_chat } + + before do + project.add_guest(current_user) + end + + context 'when not on .org or .com' do + where(:licensed, :instance_level_ai_beta_features_enabled, :cs_matcher) do + true | false | be_disallowed(policy) + true | true | be_allowed(policy) + false | false | be_disallowed(policy) + false | true | be_disallowed(policy) + end + + with_them do + before do + allow(::Gitlab).to receive(:org_or_com?).and_return(false) + stub_ee_application_setting(instance_level_ai_beta_features_enabled: instance_level_ai_beta_features_enabled) + stub_licensed_features(ai_chat: licensed) + end + + it { is_expected.to cs_matcher } + end + end + end + end end diff --git a/ee/spec/requests/api/graphql/mutations/projects/chat_spec.rb b/ee/spec/requests/api/graphql/mutations/projects/chat_spec.rb index af44628629eaa..c8345c4bdbd9a 100644 --- a/ee/spec/requests/api/graphql/mutations/projects/chat_spec.rb +++ b/ee/spec/requests/api/graphql/mutations/projects/chat_spec.rb @@ -78,9 +78,9 @@ end end - context 'when ai_global_switch feature flag is disabled' do + context 'when ai_duo_chat_switch feature flag is disabled' do before do - stub_feature_flags(ai_global_switch: false) + stub_feature_flags(ai_duo_chat_switch: false) end it 'returns nil' do diff --git a/ee/spec/requests/api/graphql/mutations/projects/explain_code_spec.rb b/ee/spec/requests/api/graphql/mutations/projects/explain_code_spec.rb index 513bdb7c5774e..6d133252ca8e5 100644 --- a/ee/spec/requests/api/graphql/mutations/projects/explain_code_spec.rb +++ b/ee/spec/requests/api/graphql/mutations/projects/explain_code_spec.rb @@ -84,7 +84,7 @@ post_graphql_mutation(mutation, current_user: current_user) - expect(fresh_response_data['errors'][0]['message']).to eq("`ai_global_switch` feature flag is disabled.") + expect(fresh_response_data['errors'][0]['message']).to eq("required feature flag is disabled.") end end end diff --git a/ee/spec/requests/api/graphql/mutations/projects/fill_in_merge_request_template_spec.rb b/ee/spec/requests/api/graphql/mutations/projects/fill_in_merge_request_template_spec.rb index 02aaae59c1476..ed37567e46f9c 100644 --- a/ee/spec/requests/api/graphql/mutations/projects/fill_in_merge_request_template_spec.rb +++ b/ee/spec/requests/api/graphql/mutations/projects/fill_in_merge_request_template_spec.rb @@ -69,7 +69,7 @@ post_graphql_mutation(mutation, current_user: current_user) - expect(fresh_response_data['errors'][0]['message']).to eq("`ai_global_switch` feature flag is disabled.") + expect(fresh_response_data['errors'][0]['message']).to eq("required feature flag is disabled.") end end diff --git a/ee/spec/requests/api/graphql/mutations/projects/generate_commit_message_spec.rb b/ee/spec/requests/api/graphql/mutations/projects/generate_commit_message_spec.rb index 24d82db62ba59..973a9437484ec 100644 --- a/ee/spec/requests/api/graphql/mutations/projects/generate_commit_message_spec.rb +++ b/ee/spec/requests/api/graphql/mutations/projects/generate_commit_message_spec.rb @@ -55,7 +55,7 @@ post_graphql_mutation(mutation, current_user: current_user) - expect(fresh_response_data['errors'][0]['message']).to eq("`ai_global_switch` feature flag is disabled.") + expect(fresh_response_data['errors'][0]['message']).to eq("required feature flag is disabled.") end end diff --git a/ee/spec/requests/api/graphql/mutations/projects/generate_test_file_spec.rb b/ee/spec/requests/api/graphql/mutations/projects/generate_test_file_spec.rb index 8a299aa8f3a5b..ada08c1ca62de 100644 --- a/ee/spec/requests/api/graphql/mutations/projects/generate_test_file_spec.rb +++ b/ee/spec/requests/api/graphql/mutations/projects/generate_test_file_spec.rb @@ -68,7 +68,7 @@ post_graphql_mutation(mutation, current_user: current_user) - expect(fresh_response_data['errors'][0]['message']).to eq("`ai_global_switch` feature flag is disabled.") + expect(fresh_response_data['errors'][0]['message']).to eq("required feature flag is disabled.") end end diff --git a/ee/spec/services/llm/base_service_spec.rb b/ee/spec/services/llm/base_service_spec.rb index 983336b1775d5..4973c23e3fda9 100644 --- a/ee/spec/services/llm/base_service_spec.rb +++ b/ee/spec/services/llm/base_service_spec.rb @@ -2,11 +2,9 @@ require 'spec_helper' -RSpec.describe Llm::BaseService, :saas, feature_category: :ai_abstraction_layer do - let_it_be_with_reload(:group) { create(:group_with_plan, plan: :ultimate_plan) } +RSpec.describe Llm::BaseService, feature_category: :ai_abstraction_layer do let_it_be(:user) { create(:user) } - let_it_be(:project) { create(:project, group: group) } - let_it_be(:resource) { create(:issue, project: project) } + let(:options) { {} } subject { described_class.new(user, resource, options) } @@ -52,52 +50,66 @@ def ai_action end end - context 'when user has no access' do - it_behaves_like 'returns an error' - end + context 'for SaaS instance', :saas do + let_it_be_with_reload(:group) { create(:group_with_plan, plan: :ultimate_plan) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:resource) { create(:issue, project: project) } - context 'when user has access' do - before do - project.add_developer(user) - group.add_developer(user) + context 'when user has no access' do + it_behaves_like 'returns an error' end - context 'when ai_global_switch feature flag is not enabled' do + context 'when user has access' do before do - stub_feature_flags(ai_global_switch: false) + project.add_developer(user) + group.add_developer(user) end - it_behaves_like 'returns an error' - end + context 'when ai_global_switch feature flag is not enabled' do + before do + stub_feature_flags(ai_global_switch: false) + end - context 'when experimental features are disabled for the group' do - include_context 'with experiment features disabled for group' + it_behaves_like 'returns an error' + end - it_behaves_like 'returns an error' - end + context 'when experimental features are disabled for the group' do + include_context 'with experiment features disabled for group' - context 'when ai features are enabled' do - include_context 'with ai features enabled for group' + it_behaves_like 'returns an error' + end - it_behaves_like 'raises a NotImplementedError' + context 'when ai features are enabled' do + include_context 'with ai features enabled for group' - context 'when resource is an issue' do - let_it_be(:resource) { create(:issue, project: project) } + it_behaves_like 'raises a NotImplementedError' - it_behaves_like 'success when implemented' - end + context 'when resource is an issue' do + let_it_be(:resource) { create(:issue, project: project) } - context 'when resource is a user' do - let_it_be(:resource) { user } + it_behaves_like 'success when implemented' + end - it_behaves_like 'success when implemented' - end + context 'when resource is a user' do + let_it_be(:resource) { user } + + it_behaves_like 'success when implemented' + end - context 'when resource is nil' do - let_it_be(:resource) { nil } + context 'when resource is nil' do + let_it_be(:resource) { nil } - it_behaves_like 'success when implemented' + it_behaves_like 'success when implemented' + end end end end + + context 'for self-managed instance' do + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:resource) { create(:issue, project: project) } + + it_behaves_like 'returns an error' + end end diff --git a/ee/spec/services/llm/chat_service_spec.rb b/ee/spec/services/llm/chat_service_spec.rb index cf577118031e1..741f0c49cb536 100644 --- a/ee/spec/services/llm/chat_service_spec.rb +++ b/ee/spec/services/llm/chat_service_spec.rb @@ -2,26 +2,26 @@ require 'spec_helper' -RSpec.describe Llm::ChatService, :saas, feature_category: :shared do - let_it_be_with_reload(:group) { create(:group_with_plan, plan: :ultimate_plan) } +RSpec.describe Llm::ChatService, feature_category: :duo_chat do let_it_be(:user) { create(:user) } - let_it_be(:project) { create(:project, group: group) } - let_it_be(:issue) { create(:issue, project: project) } let(:resource) { issue } let(:stage_check_available) { true } let(:content) { "Summarize issue" } let(:options) { { content: content } } - subject { described_class.new(user, resource, options) } + context 'for self-managed' do + let_it_be_with_reload(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:issue) { create(:issue, project: project) } - describe '#perform' do - context 'when ai features are enabled for the group' do - include_context 'with ai features enabled for group' + subject { described_class.new(user, resource, options) } + + context 'when ai features are enabled for instance' do + include_context 'with experiment features enabled for self-managed' before do allow(SecureRandom).to receive(:uuid).and_return('uuid') - allow(Gitlab::Llm::StageCheck).to receive(:available?).with(group, :chat).and_return(stage_check_available) end context 'when user is part of the group' do @@ -57,5 +57,66 @@ end end end + + context 'when ai features are disabled for instance' do + include_context 'with experiment features disabled for self-managed' + + it 'returns an error' do + expect(Llm::CompletionWorker).not_to receive(:perform_for) + expect(subject.execute).to be_error + end + end + end + + context 'for saas', :saas do + let_it_be_with_reload(:group) { create(:group_with_plan, plan: :ultimate_plan) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:issue) { create(:issue, project: project) } + + subject { described_class.new(user, resource, options) } + + describe '#perform' do + context 'when ai features are enabled for the group' do + include_context 'with ai features enabled for group' + + before do + allow(SecureRandom).to receive(:uuid).and_return('uuid') + allow(Gitlab::Llm::StageCheck).to receive(:available?).with(group, :chat).and_return(stage_check_available) + end + + context 'when user is part of the group' do + before do + group.add_developer(user) + end + + context 'when resource is an issue' do + let(:resource) { issue } + let(:action_name) { :chat } + let(:content) { 'Summarize issue' } + + it_behaves_like 'schedules completion worker' + it_behaves_like 'llm service caches user request' + it_behaves_like 'service emitting message for user prompt' + end + + context 'when resource is a user' do + let(:resource) { user } + let(:action_name) { :chat } + let(:content) { 'How to reset the password' } + + it_behaves_like 'schedules completion worker' + it_behaves_like 'llm service caches user request' + it_behaves_like 'service emitting message for user prompt' + end + end + + context 'when user is not part of the group' do + it 'returns an error' do + expect(Llm::CompletionWorker).not_to receive(:perform_for) + expect(subject.execute).to be_error + end + end + end + end end end diff --git a/ee/spec/services/llm/fill_in_merge_request_template_service_spec.rb b/ee/spec/services/llm/fill_in_merge_request_template_service_spec.rb index ec92d8b7e5250..fc3fd06e2f016 100644 --- a/ee/spec/services/llm/fill_in_merge_request_template_service_spec.rb +++ b/ee/spec/services/llm/fill_in_merge_request_template_service_spec.rb @@ -2,15 +2,17 @@ require 'spec_helper' -RSpec.describe Llm::FillInMergeRequestTemplateService, feature_category: :code_review_workflow do +RSpec.describe Llm::FillInMergeRequestTemplateService, :saas, feature_category: :code_review_workflow do let_it_be(:user) { create(:user) } - let_it_be(:group) { create(:group, :public) } + let_it_be_with_reload(:group) { create(:group_with_plan, plan: :ultimate_plan) } let_it_be(:resource) { create(:project, :public, group: group) } let(:fill_in_merge_request_template_enabled) { true } let(:current_user) { user } describe '#perform' do + include_context 'with ai features enabled for group' + before do group.add_guest(user) diff --git a/ee/spec/services/llm/generate_description_service_spec.rb b/ee/spec/services/llm/generate_description_service_spec.rb index 0ce43e8e91089..233a56745a0a5 100644 --- a/ee/spec/services/llm/generate_description_service_spec.rb +++ b/ee/spec/services/llm/generate_description_service_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' -RSpec.describe Llm::GenerateDescriptionService, feature_category: :team_planning do +RSpec.describe Llm::GenerateDescriptionService, :saas, feature_category: :team_planning do let_it_be(:user) { create(:user) } - let_it_be(:group) { create(:group, :public) } + let_it_be_with_reload(:group) { create(:group_with_plan, plan: :ultimate_plan) } let_it_be(:project) { create(:project, :public, group: group) } let_it_be(:resource) { create(:issue, project: project) } @@ -14,6 +14,8 @@ let(:generate_description_license_enabled) { true } describe '#perform' do + include_context 'with ai features enabled for group' + before do stub_licensed_features(generate_description: true) group.add_guest(user) diff --git a/ee/spec/services/llm/generate_summary_service_spec.rb b/ee/spec/services/llm/generate_summary_service_spec.rb index 905b66ff3d0ad..0e313157b5cab 100644 --- a/ee/spec/services/llm/generate_summary_service_spec.rb +++ b/ee/spec/services/llm/generate_summary_service_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' -RSpec.describe Llm::GenerateSummaryService, feature_category: :ai_abstraction_layer do +RSpec.describe Llm::GenerateSummaryService, :saas, feature_category: :ai_abstraction_layer do let_it_be(:user) { create(:user) } - let_it_be(:group) { create(:group, :public) } + let_it_be_with_reload(:group) { create(:group_with_plan, plan: :ultimate_plan) } let_it_be(:project) { create(:project, :public, group: group) } let(:options) { {} } @@ -13,6 +13,8 @@ let(:current_user) { user } describe '#perform' do + include_context 'with ai features enabled for group' + before do group.add_guest(user) diff --git a/ee/spec/services/llm/internal/completion_service_spec.rb b/ee/spec/services/llm/internal/completion_service_spec.rb index 7fc6da3d39896..22123f8066854 100644 --- a/ee/spec/services/llm/internal/completion_service_spec.rb +++ b/ee/spec/services/llm/internal/completion_service_spec.rb @@ -114,6 +114,20 @@ it_behaves_like 'performs successfully' end + + context 'when it is chat request' do + let_it_be(:resource) { nil } + let(:ai_action_name) { :chat } + let(:prompt_message) do + build(:ai_chat_message, user: user, resource: resource, ai_action: ai_action_name, request_id: 'uuid') + end + + before do + stub_feature_flags(ai_duo_chat_switch: true, ai_global_switch: false) + end + + it_behaves_like 'performs successfully' + end end context 'with service failure' do diff --git a/ee/spec/services/llm/summarize_merge_request_service_spec.rb b/ee/spec/services/llm/summarize_merge_request_service_spec.rb index 218c74d362cb3..9493e1e5d13a1 100644 --- a/ee/spec/services/llm/summarize_merge_request_service_spec.rb +++ b/ee/spec/services/llm/summarize_merge_request_service_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' -RSpec.describe Llm::SummarizeMergeRequestService, feature_category: :code_review_workflow do +RSpec.describe Llm::SummarizeMergeRequestService, :saas, feature_category: :code_review_workflow do let_it_be(:user) { create(:user) } - let_it_be(:group) { create(:group, :public) } + let_it_be_with_reload(:group) { create(:group_with_plan, plan: :ultimate_plan) } let_it_be(:project) { create(:project, :public, group: group) } let_it_be(:resource) { create(:merge_request, source_project: project, target_project: project, author: user) } @@ -13,6 +13,8 @@ let(:options) { {} } describe '#perform' do + include_context 'with ai features enabled for group' + let(:action_name) { :summarize_merge_request } let(:content) { 'Summarize merge request' } diff --git a/ee/spec/services/llm/summarize_submitted_review_service_spec.rb b/ee/spec/services/llm/summarize_submitted_review_service_spec.rb index 48c3fdc0a96a6..dd52b85394840 100644 --- a/ee/spec/services/llm/summarize_submitted_review_service_spec.rb +++ b/ee/spec/services/llm/summarize_submitted_review_service_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' -RSpec.describe Llm::SummarizeSubmittedReviewService, feature_category: :code_review_workflow do +RSpec.describe Llm::SummarizeSubmittedReviewService, :saas, feature_category: :code_review_workflow do let_it_be(:user) { create(:user) } - let_it_be(:group) { create(:group, :public) } + let_it_be_with_reload(:group) { create(:group_with_plan, plan: :ultimate_plan) } let_it_be(:project) { create(:project, :public, group: group) } let_it_be(:resource) { create(:merge_request, source_project: project, target_project: project, author: user) } @@ -13,6 +13,8 @@ let(:options) { {} } describe '#perform' do + include_context 'with ai features enabled for group' + let(:action_name) { :summarize_submitted_review } let(:content) { 'Summarize submitted review' } diff --git a/ee/spec/support/shared_contexts/services/llm/ai_features_enabled_for_group_shared_context.rb b/ee/spec/support/shared_contexts/services/llm/ai_features_enabled_for_group_shared_context.rb index f3e5242a85830..1ddc3402db133 100644 --- a/ee/spec/support/shared_contexts/services/llm/ai_features_enabled_for_group_shared_context.rb +++ b/ee/spec/support/shared_contexts/services/llm/ai_features_enabled_for_group_shared_context.rb @@ -2,7 +2,7 @@ RSpec.shared_context 'with ai features enabled for group' do before do - allow(Gitlab).to receive(:com?).and_return(true) + allow(Gitlab).to receive(:org_or_com?).and_return(true) stub_ee_application_setting(should_check_namespace_plan: true) allow(group.namespace_settings).to receive(:experiment_settings_allowed?).and_return(true) stub_licensed_features( @@ -16,7 +16,7 @@ RSpec.shared_context 'with experiment features disabled for group' do before do - allow(Gitlab).to receive(:com?).and_return(true) + allow(Gitlab).to receive(:org_or_com?).and_return(true) stub_ee_application_setting(should_check_namespace_plan: true) allow(group.namespace_settings).to receive(:experiment_settings_allowed?).and_return(true) stub_licensed_features( @@ -27,3 +27,19 @@ group.namespace_settings.update!(experiment_features_enabled: false) end end + +RSpec.shared_context 'with experiment features enabled for self-managed' do + before do + allow(Gitlab).to receive(:org_or_com?).and_return(false) + stub_application_setting(instance_level_ai_beta_features_enabled: true) + stub_licensed_features(ai_chat: true) + end +end + +RSpec.shared_context 'with experiment features disabled for self-managed' do + before do + allow(Gitlab).to receive(:org_or_com?).and_return(false) + stub_application_setting(instance_level_ai_beta_features_enabled: false) + stub_licensed_features(ai_chat: true) + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 06531b63163fc..b396e6da18223 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -56317,10 +56317,10 @@ msgstr "" msgid "You do not belong to any projects yet." msgstr "" -msgid "You do not have access to AI features." +msgid "You do not have access to any projects for creating incidents." msgstr "" -msgid "You do not have access to any projects for creating incidents." +msgid "You do not have access to chat feature." msgstr "" msgid "You do not have any subscriptions yet" -- GitLab