diff --git a/ee/app/models/ai/duo_code_review.rb b/ee/app/models/ai/duo_code_review.rb new file mode 100644 index 0000000000000000000000000000000000000000..68c708ac26316283888b8b799b68bdd07cf68606 --- /dev/null +++ b/ee/app/models/ai/duo_code_review.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Ai + module DuoCodeReview + module_function + + def enabled?(user:, container:) + ModeResolver.new(user: user, container: container).enabled? + end + + def mode(user:, container:) + ModeResolver.new(user: user, container: container).mode + end + end +end diff --git a/ee/app/models/ai/duo_code_review/mode_resolver.rb b/ee/app/models/ai/duo_code_review/mode_resolver.rb new file mode 100644 index 0000000000000000000000000000000000000000..73c66f2f2db265b7a3b344e8c15eec6f653cbcdb --- /dev/null +++ b/ee/app/models/ai/duo_code_review/mode_resolver.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Ai + module DuoCodeReview + class ModeResolver + include ::Gitlab::Utils::StrongMemoize + + delegate :mode, :enabled?, to: :active_mode + + # Modes with higher precedence comes first. + MODES = [ + Modes::Dap, # Duo Code Review will use Duo Agent Platform with extended context support. + Modes::Classic, # Duo Code Review will use its classic prompt mode. + Modes::Disabled # Duo Code Review is disabled. + ].freeze + + def initialize(user:, container:) + @user = user + @container = container + end + + private + + attr_reader :user, :container + + def active_mode + MODES.find do |resolver| + mode = resolver.new(user: user, container: container) + break mode if mode.active? + end + end + strong_memoize_attr :active_mode + end + end +end diff --git a/ee/app/models/ai/duo_code_review/modes/base.rb b/ee/app/models/ai/duo_code_review/modes/base.rb new file mode 100644 index 0000000000000000000000000000000000000000..938a8b80b710bd535753b46d048726d401fb5fb5 --- /dev/null +++ b/ee/app/models/ai/duo_code_review/modes/base.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Ai + module DuoCodeReview + module Modes + class Base + def initialize(user:, container:) + @user = user + @container = container + end + + def mode + raise NotImplementedError + end + + def enabled? + raise NotImplementedError + end + + def active? + raise NotImplementedError + end + + private + + attr_reader :user, :container + end + end + end +end diff --git a/ee/app/models/ai/duo_code_review/modes/classic.rb b/ee/app/models/ai/duo_code_review/modes/classic.rb new file mode 100644 index 0000000000000000000000000000000000000000..d99a5ed1fc4cdb5dc929b6fda23c7ec56a2e6f36 --- /dev/null +++ b/ee/app/models/ai/duo_code_review/modes/classic.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Ai + module DuoCodeReview + module Modes + class Classic < Base + def mode + :classic + end + + def enabled? + true + end + + def active? + Ability.allowed?(user, :access_ai_review_mr, container) && + ::Gitlab::Llm::FeatureAuthorizer.new( + container: container, + feature_name: :review_merge_request, + user: user + ).allowed? + end + end + end + end +end diff --git a/ee/app/models/ai/duo_code_review/modes/dap.rb b/ee/app/models/ai/duo_code_review/modes/dap.rb new file mode 100644 index 0000000000000000000000000000000000000000..38a2929345afd05b65da582c86b18d559c144dd7 --- /dev/null +++ b/ee/app/models/ai/duo_code_review/modes/dap.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Ai + module DuoCodeReview + module Modes + class Dap < Base + def mode + :dap + end + + def enabled? + true + end + + def active? + return false unless ::Feature.enabled?(:duo_code_review_on_agent_platform, user) + return false unless container.duo_features_enabled + + # For Duo Enterprise: use duo agent platform only for internal GitLab users if feature flag is enabled + return ::Feature.enabled?(:duo_code_review_dap_internal_users, user) if user_has_duo_enterprise_add_on? + + # For Duo Pro/Core: use agent platform if: + # - user has duo_agent_platform access (checks user's add-on assignments) + # - GA rollout is enabled OR experimental features are enabled + # - DWS is configured (for self-managed) + user.allowed_to_use?(:duo_agent_platform) && + (ga_rollout_enabled? || experimental_features_enabled?) && + duo_agent_platform_configured? + end + + private + + def user_has_duo_enterprise_add_on? + ::GitlabSubscriptions::AddOnPurchase.for_active_add_ons([:duo_enterprise], user).exists? + end + + def ga_rollout_enabled? + ::Feature.enabled?(:ai_duo_agent_platform_ga_rollout, container) + end + + def experimental_features_enabled? + if ::Gitlab::Saas.feature_available?(:gitlab_com_subscriptions) + container.root_ancestor.experiment_features_enabled + else + ::Gitlab::CurrentSettings.instance_level_ai_beta_features_enabled? + end + end + + def duo_agent_platform_configured? + feature_setting = selected_feature_setting + + # SaaS customers always have DWS available + # Self-managed instances without a feature_setting record also default to cloud-connected models + # Only self-managed instances with self_hosted? == true need further validation + return true unless feature_setting&.self_hosted? + + # Self-hosted customers need compatible model and DWS configured + return false if feature_setting.self_hosted_model&.unsupported_family_for_duo_agent_platform_code_review? + + ::Gitlab::DuoWorkflow::Client.self_hosted_url.present? + end + + def selected_feature_setting + # rubocop: disable CodeReuse/ServiceClass -- The service below should probably be a model too. + service_result = ::Ai::FeatureSettingSelectionService.new( + user, + :duo_agent_platform, + container.root_ancestor + ).execute + # rubocop: enable CodeReuse/ServiceClass + + service_result.success? ? service_result.payload : nil + end + end + end + end +end diff --git a/ee/app/models/ai/duo_code_review/modes/disabled.rb b/ee/app/models/ai/duo_code_review/modes/disabled.rb new file mode 100644 index 0000000000000000000000000000000000000000..13c480af9840299ccdf76d0c92e71a2b208db3a0 --- /dev/null +++ b/ee/app/models/ai/duo_code_review/modes/disabled.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Ai + module DuoCodeReview + module Modes + class Disabled < Base + def mode + :disabled + end + + def enabled? + false + end + + def active? + true + end + end + end + end +end diff --git a/ee/spec/models/ai/duo_code_review/mode_resolver_spec.rb b/ee/spec/models/ai/duo_code_review/mode_resolver_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..ec02467dd8517ab41c38fbe38b094ce220f6c899 --- /dev/null +++ b/ee/spec/models/ai/duo_code_review/mode_resolver_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ai::DuoCodeReview::ModeResolver, feature_category: :code_suggestions do + let_it_be(:user) { build_stubbed(:user) } + let_it_be(:container) { build_stubbed(:group) } + + subject(:active_mode) { described_class.new(user: user, container: container) } + + shared_context 'with DAP mode active' do + before do + allow_next_instance_of(Ai::DuoCodeReview::Modes::Dap) do |instance| + allow(instance).to receive(:active?).and_return(true) + end + end + end + + shared_context 'with Classic mode active' do + before do + allow_next_instance_of(Ai::DuoCodeReview::Modes::Classic) do |instance| + allow(instance).to receive(:active?).and_return(true) + end + end + end + + shared_context 'when disabled' do + before do + allow_next_instance_of(Ai::DuoCodeReview::Modes::Dap) do |instance| + allow(instance).to receive(:active?).and_return(false) + end + + allow_next_instance_of(Ai::DuoCodeReview::Modes::Classic) do |instance| + allow(instance).to receive(:active?).and_return(false) + end + end + end + + describe '#mode' do + context 'when DAP mode is active' do + include_context 'with DAP mode active' + + it 'returns :dap' do + expect(active_mode.mode).to eq(:dap) + end + end + + context 'when Classic mode is active' do + include_context 'with Classic mode active' + + it 'returns :classic' do + expect(active_mode.mode).to eq(:classic) + end + end + + context 'when both DAP and Classic mode are not active' do + include_context 'when disabled' + + it 'returns :disabled' do + expect(active_mode.mode).to eq(:disabled) + end + end + end + + describe '#enabled?' do + context 'when DAP mode is active' do + include_context 'with DAP mode active' + + it 'is enabled' do + expect(active_mode).to be_enabled + end + end + + context 'when Classic mode is active' do + include_context 'with Classic mode active' + + it 'is enabled' do + expect(active_mode).to be_enabled + end + end + + context 'when both DAP and Classic mode are not active' do + include_context 'when disabled' + + it 'is disabled' do + expect(active_mode).not_to be_enabled + end + end + end +end diff --git a/ee/spec/models/ai/duo_code_review/modes/classic_spec.rb b/ee/spec/models/ai/duo_code_review/modes/classic_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..e6eb5b605f15e4c587685a5f54e19191aa1e8688 --- /dev/null +++ b/ee/spec/models/ai/duo_code_review/modes/classic_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ai::DuoCodeReview::Modes::Classic, feature_category: :code_suggestions do + subject(:mode) { described_class.new(user: user, container: container) } + + let_it_be(:project) { create(:project) } + let_it_be(:group) { create(:group) } + let_it_be(:merge_request) { create(:merge_request, source_project: project) } + let_it_be(:user) { create(:user, developer_of: project) } + let_it_be(:container) { project } + + describe '#mode' do + it 'returns the mode name' do + expect(mode.mode).to eq(:classic) + end + end + + describe '#enabled?' do + it 'always returns true' do + expect(mode).to be_enabled + end + end + + describe '#active?' do + let(:feature_authorizer) { instance_double(::Gitlab::Llm::FeatureAuthorizer) } + let(:user_has_access_ai_review_mr_ability) { true } + let(:feature_authorizer_allowed) { true } + + before do + allow(Ability).to receive(:allowed?) + .with(user, :access_ai_review_mr, container) + .and_return(user_has_access_ai_review_mr_ability) + + allow(::Gitlab::Llm::FeatureAuthorizer).to receive(:new) + .with( + container: container, + feature_name: :review_merge_request, + user: user + ) + .and_return(feature_authorizer) + + allow(feature_authorizer).to receive(:allowed?) + .and_return(feature_authorizer_allowed) + end + + shared_examples 'not active' do + it 'returns false' do + expect(mode).not_to be_active + end + end + + shared_examples 'active' do + it 'returns true' do + expect(mode).to be_active + end + end + + context 'when user has access_ai_review_mr ability and feature authorizer allows' do + include_examples 'active' + end + + context 'when user does not have access_ai_review_mr ability' do + let(:user_has_access_ai_review_mr_ability) { false } + + include_examples 'not active' + end + + context 'when feature authorizer does not allow' do + let(:feature_authorizer_allowed) { false } + + include_examples 'not active' + end + + context 'when both conditions fail' do + let(:user_has_access_ai_review_mr_ability) { false } + let(:feature_authorizer_allowed) { false } + + include_examples 'not active' + end + + context 'when user has ability but feature authorizer does not allow' do + let(:user_has_access_ai_review_mr_ability) { true } + let(:feature_authorizer_allowed) { false } + + include_examples 'not active' + end + + context 'when user does not have ability but feature authorizer allows' do + let(:user_has_access_ai_review_mr_ability) { false } + let(:feature_authorizer_allowed) { true } + + include_examples 'not active' + end + end +end diff --git a/ee/spec/models/ai/duo_code_review/modes/dap_spec.rb b/ee/spec/models/ai/duo_code_review/modes/dap_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..1ee71f1284f9b4b4a6006d5543b5cb8ebf3c43df --- /dev/null +++ b/ee/spec/models/ai/duo_code_review/modes/dap_spec.rb @@ -0,0 +1,286 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ai::DuoCodeReview::Modes::Dap, feature_category: :code_suggestions do + subject(:mode) { described_class.new(user: user, container: container) } + + let_it_be(:project) { create(:project) } + let_it_be(:group) { create(:group) } + let_it_be(:merge_request) { create(:merge_request, source_project: project) } + let_it_be(:user) { create(:user, developer_of: project) } + let_it_be(:container) { project } + + describe '#mode' do + it 'returns the mode name' do + expect(mode.mode).to eq(:dap) + end + end + + describe '#enabled?' do + it 'always returns true' do + expect(mode).to be_enabled + end + end + + describe '#active?' do + # Feature flags + let(:duo_code_review_on_agent_platform) { true } + let(:ai_duo_agent_platform_ga_rollout) { true } + let(:duo_code_review_dap_internal_users) { true } + + # Duo features + let(:duo_features_enabled) { true } + let(:experiment_features_enabled) { true } + let(:instance_level_ai_beta_features_enabled) { true } + + # User permissions + let(:user_allowed_to_use_duo_agent_platform) { true } + let(:user_has_duo_enterprise_add_on) { false } + + let(:add_on_purchase) do + class_double(::GitlabSubscriptions::AddOnPurchase, exists?: user_has_duo_enterprise_add_on) + end + + before do + allow(::Gitlab::Saas).to receive(:feature_available?).with(:gitlab_com_subscriptions).and_return(true) + + stub_feature_flags( + duo_code_review_on_agent_platform: duo_code_review_on_agent_platform, + ai_duo_agent_platform_ga_rollout: ai_duo_agent_platform_ga_rollout, + duo_code_review_dap_internal_users: duo_code_review_dap_internal_users + ) + + allow(container).to receive_messages( + duo_features_enabled: duo_features_enabled, + experiment_features_enabled: experiment_features_enabled, + root_ancestor: container + ) + + allow(user).to receive(:allowed_to_use?) + .with(:duo_agent_platform) + .and_return(user_allowed_to_use_duo_agent_platform) + + allow_next_instance_of(::Ai::FeatureSettingSelectionService) do |service| + allow(service).to receive(:execute).and_return( + ServiceResponse.success(payload: nil) + ) + end + + allow(::GitlabSubscriptions::AddOnPurchase) + .to receive(:for_active_add_ons) + .with([:duo_enterprise], user) + .and_return(add_on_purchase) + + allow(::Gitlab::CurrentSettings) + .to receive(:instance_level_ai_beta_features_enabled?) + .and_return(instance_level_ai_beta_features_enabled) + end + + shared_examples 'not active' do + it 'returns false' do + expect(mode).not_to be_active + end + end + + shared_examples 'active' do + it 'returns true' do + expect(mode).to be_active + end + end + + context 'when duo_code_review_on_agent_platform feature flag is disabled' do + let(:duo_code_review_on_agent_platform) { false } + + include_examples 'not active' + end + + context 'when Duo features are disabled' do + let(:duo_features_enabled) { false } + + include_examples 'not active' + end + + context 'when user has Duo Enterprise add-on' do + let(:user_has_duo_enterprise_add_on) { true } + + context 'and duo_code_review_dap_internal_users feature flag is disabled' do + let(:duo_code_review_dap_internal_users) { false } + + include_examples 'not active' + end + + context 'and duo_code_review_dap_internal_users feature flag is enabled' do + include_examples 'active' + end + end + + context 'when user is not allowed to use duo_agent_platform' do + let(:user_allowed_to_use_duo_agent_platform) { false } + + include_examples 'not active' + end + + context 'when ai_duo_agent_platform_ga_rollout is disabled' do + let(:ai_duo_agent_platform_ga_rollout) { false } + let(:experiment_features_enabled) { false } + + context 'and instance is SASS' do + context 'and experimental features are disabled' do + let(:experiment_features_enabled) { false } + + include_examples 'not active' + end + end + + context 'and instance is self-managed' do + context 'and experimental features are disabled' do + let(:instance_level_ai_beta_features_enabled) { false } + + include_examples 'not active' + end + end + end + + context 'when user does not have Duo Enterprise add-on' do + let(:user_has_duo_enterprise_add_on) { false } + + context 'and GA rollout is enabled' do + let(:ai_duo_agent_platform_ga_rollout) { true } + + include_examples 'active' + end + + context 'and GA rollout is disabled but experimental features are enabled' do + let(:ai_duo_agent_platform_ga_rollout) { false } + let(:experiment_features_enabled) { true } + + include_examples 'active' + end + + context 'and GA rollout is disabled and experimental features are disabled' do + let(:ai_duo_agent_platform_ga_rollout) { false } + let(:experiment_features_enabled) { false } + + include_examples 'not active' + end + end + + context 'when instance_level_ai_beta_features_enabled is true' do + let(:instance_level_ai_beta_features_enabled) { true } + let(:ai_duo_agent_platform_ga_rollout) { false } + + include_examples 'active' + end + + context 'when instance_level_ai_beta_features_enabled is false' do + let(:instance_level_ai_beta_features_enabled) { false } + let(:ai_duo_agent_platform_ga_rollout) { false } + let(:experiment_features_enabled) { false } + + include_examples 'not active' + end + + context 'when duo_agent_platform is not configured' do + before do + allow_next_instance_of(::Ai::FeatureSettingSelectionService) do |service| + allow(service).to receive(:execute).and_return( + ServiceResponse.success(payload: nil) + ) + end + end + + context 'and SaaS feature is available' do + include_examples 'active' + end + + context 'and SaaS feature is not available' do + before do + allow(::Gitlab::Saas).to receive(:feature_available?).with(:gitlab_com_subscriptions).and_return(false) + end + + include_examples 'active' + end + end + + context 'when self-hosted model is unsupported for duo agent platform code review' do + let(:self_hosted_model) { instance_double(::Ai::SelfHostedModel) } + let(:feature_setting) do + instance_double( + ::Ai::FeatureSetting, + self_hosted?: true, + self_hosted_model: self_hosted_model + ) + end + + before do + allow(self_hosted_model).to receive(:unsupported_family_for_duo_agent_platform_code_review?).and_return(true) + allow_next_instance_of(::Ai::FeatureSettingSelectionService) do |service| + allow(service).to receive(:execute).and_return( + ServiceResponse.success(payload: feature_setting) + ) + end + end + + include_examples 'not active' + end + + context 'when self-hosted model is supported but DWS URL is not configured' do + let(:self_hosted_model) { instance_double(::Ai::SelfHostedModel) } + let(:feature_setting) do + instance_double( + ::Ai::FeatureSetting, + self_hosted?: true, + self_hosted_model: self_hosted_model + ) + end + + before do + allow(self_hosted_model).to receive(:unsupported_family_for_duo_agent_platform_code_review?).and_return(false) + allow_next_instance_of(::Ai::FeatureSettingSelectionService) do |service| + allow(service).to receive(:execute).and_return( + ServiceResponse.success(payload: feature_setting) + ) + end + allow(::Gitlab::DuoWorkflow::Client).to receive(:self_hosted_url).and_return(nil) + end + + include_examples 'not active' + end + + context 'when self-hosted model is supported and DWS URL is configured' do + let(:self_hosted_model) { instance_double(::Ai::SelfHostedModel) } + let(:feature_setting) do + instance_double( + ::Ai::FeatureSetting, + self_hosted?: true, + self_hosted_model: self_hosted_model + ) + end + + before do + allow(self_hosted_model).to receive(:unsupported_family_for_duo_agent_platform_code_review?).and_return(false) + allow_next_instance_of(::Ai::FeatureSettingSelectionService) do |service| + allow(service).to receive(:execute).and_return( + ServiceResponse.success(payload: feature_setting) + ) + end + allow(::Gitlab::DuoWorkflow::Client).to receive(:self_hosted_url).and_return('https://dws.example.com') + end + + include_examples 'active' + end + + context 'when feature setting service returns failure' do + before do + allow_next_instance_of(::Ai::FeatureSettingSelectionService) do |service| + allow(service).to receive(:execute).and_return( + ServiceResponse.error(message: 'Service error') + ) + end + end + + include_examples 'active' + end + end +end diff --git a/ee/spec/models/ai/duo_code_review/modes/disabled_spec.rb b/ee/spec/models/ai/duo_code_review/modes/disabled_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..ba720030141e31b12c602f1d87265b89db50262b --- /dev/null +++ b/ee/spec/models/ai/duo_code_review/modes/disabled_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ai::DuoCodeReview::Modes::Disabled, feature_category: :code_suggestions do + subject(:mode) { described_class.new(user: user, container: container) } + + let_it_be(:project) { create(:project) } + let_it_be(:group) { create(:group) } + let_it_be(:merge_request) { create(:merge_request, source_project: project) } + let_it_be(:user) { create(:user, developer_of: project) } + let_it_be(:container) { project } + + describe '#mode' do + it 'returns the mode name' do + expect(mode.mode).to eq(:disabled) + end + end + + describe '#enabled?' do + it 'always returns false' do + expect(mode).not_to be_enabled + end + end + + describe '#active?' do + it 'always returns true' do + expect(mode).to be_active + end + end +end diff --git a/ee/spec/models/ai/duo_code_review_spec.rb b/ee/spec/models/ai/duo_code_review_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a2bfa87773a127cf14a1150ea489ac0771d63630 --- /dev/null +++ b/ee/spec/models/ai/duo_code_review_spec.rb @@ -0,0 +1,151 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ai::DuoCodeReview, feature_category: :code_suggestions do + let_it_be(:project) { create(:project) } + let_it_be(:group) { create(:group) } + let_it_be(:merge_request) { create(:merge_request, source_project: project) } + let_it_be(:user) { create(:user, developer_of: project) } + + shared_context 'with DAP Duo Code Review enabled' do + before do + stub_feature_flags(duo_code_review_on_agent_platform: true) + stub_feature_flags(ai_duo_agent_platform_ga_rollout: true) + + allow(project).to receive(:duo_features_enabled).and_return(true) + allow(group).to receive(:duo_features_enabled).and_return(true) + + allow(user).to receive(:allowed_to_use?) + .with(:duo_agent_platform) + .and_return(true) + allow(user).to receive(:allowed_to_use?) + .with(:review_merge_request, licensed_feature: :review_merge_request) + .and_return(true) + + allow_next_instance_of(::Ai::FeatureSettingSelectionService) do |service| + allow(service).to receive(:execute).and_return( + ServiceResponse.success(payload: nil) + ) + end + end + end + + shared_context 'with Classic Duo Code Review enabled' do + before do + stub_feature_flags(duo_code_review_on_agent_platform: false) + + allow(user).to receive(:allowed_to_use?) + .with(:duo_agent_platform) + .and_return(false) + allow(user).to receive(:allowed_to_use?) + .with(:review_merge_request, licensed_feature: :review_merge_request) + .and_return(true) + + allow_next_instance_of(::Gitlab::Llm::FeatureAuthorizer) do |instance| + allow(instance).to receive(:allowed?).and_return(true) + end + allow_next_instance_of(Ability) do |instance| + allow(instance).to receive(:allowed?).and_return(true) + end + end + end + + shared_context 'with DAP and Classic Duo Code Review disabled' do + before do + stub_feature_flags(duo_code_review_on_agent_platform: false) + + allow(user).to receive(:allowed_to_use?) + .with(:duo_agent_platform) + .and_return(false) + allow(user).to receive(:allowed_to_use?) + .with(:review_merge_request, licensed_feature: :review_merge_request) + .and_return(false) + + allow_next_instance_of(::Gitlab::Llm::FeatureAuthorizer) do |instance| + allow(instance).to receive(:allowed?).and_return(false) + end + allow_next_instance_of(Ability) do |instance| + allow(instance).to receive(:allowed?).and_return(false) + end + end + end + + describe '.enabled?' do + subject(:enabled) { described_class.enabled?(user: user, container: container) } + + before do + stub_licensed_features(review_merge_request: true) + end + + shared_examples 'enabled when DAP Duo Code Review is enabled' do + context 'when DAP Duo Code Review is enabled' do + include_context 'with DAP Duo Code Review enabled' + + it { is_expected.to be(true) } + end + end + + shared_examples 'enabled when Classic Duo Code Review is enabled' do + context 'when Classic Duo Code Review is enabled' do + include_context 'with Classic Duo Code Review enabled' + + it { is_expected.to be(true) } + end + end + + shared_examples 'disabled when DAP and Classic Duo Code Review are disabled' do + context 'when both DAP and Classic Duo Code Review are disabled' do + include_context 'with DAP and Classic Duo Code Review disabled' + + it { is_expected.to be(false) } + end + end + + context 'with a Group container' do + let(:container) { group } + + include_examples 'enabled when DAP Duo Code Review is enabled' + include_examples 'enabled when Classic Duo Code Review is enabled' + include_examples 'disabled when DAP and Classic Duo Code Review are disabled' + end + + context 'with a Project container' do + let(:container) { project } + + include_examples 'enabled when DAP Duo Code Review is enabled' + include_examples 'enabled when Classic Duo Code Review is enabled' + include_examples 'disabled when DAP and Classic Duo Code Review are disabled' + end + end + + describe '.mode' do + subject(:mode) { described_class.mode(user: user, container: container) } + + let(:container) { project } + + context 'when DAP Duo Code Review is enabled' do + include_context 'with DAP Duo Code Review enabled' + + it 'returns :dap' do + is_expected.to eq(:dap) + end + end + + context 'when Classic Duo Code Review is enabled' do + include_context 'with Classic Duo Code Review enabled' + + it 'returns :classic' do + is_expected.to eq(:classic) + end + end + + context 'when both DAP and Classic Duo Code Review are disabled' do + include_context 'with DAP and Classic Duo Code Review disabled' + + it 'returns :disabled' do + is_expected.to eq(:disabled) + end + end + end +end