From f2fa1a30df2ef0c5697de40664cf8d6b53cdf8ec Mon Sep 17 00:00:00 2001 From: Patrick Bajao Date: Tue, 23 May 2023 14:44:43 +0800 Subject: [PATCH] Implement FillInMergeRequestTemplate AI action Adds support for `fillInMergeRequestTemplate` input for `aiAction` that has the following arguments: - `resourceId` (required) - project where the MR will be created on - `title` (required) - title of the MR to be created - `sourceProjectId` (optional, defaults to `resource`) - `sourceBranch` (required) - `targetBranch` (required) - `content` (required) The `sourceBranch` and `targetBranch` will be used to compare the changes between those branches so we can get the diff to send to the AI provider. That's why it's required. The `sourceProjectId` is needed for forks so we can compare the changes made on the fork's source branch with the target branch. Sections in the template content can add `` to the body to ensure that AI won't touch the section. This uses Vertex AI as the AI provider. This is behind the `fill_in_mr_template` feature flag. --- .../development/fill_in_mr_template.yml | 8 ++ doc/api/graphql/reference/index.md | 14 +++ ...ll_in_merge_request_template_input_type.rb | 29 ++++++ .../models/gitlab_subscriptions/features.rb | 1 + ee/app/policies/ee/project_policy.rb | 16 +++ ee/app/services/llm/execute_method_service.rb | 3 +- .../fill_in_merge_request_template_service.rb | 20 ++++ ee/lib/gitlab/llm/completions_factory.rb | 4 + ee/lib/gitlab/llm/stage_check.rb | 3 +- .../fill_in_merge_request_template.rb | 77 +++++++++++++++ .../fill_in_merge_request_template.rb | 33 +++++++ .../fill_in_merge_request_template_spec.rb | 61 ++++++++++++ .../fill_in_merge_request_template_spec.rb | 84 ++++++++++++++++ ee/spec/policies/project_policy_spec.rb | 78 +++++++++++++++ .../fill_in_merge_request_template_spec.rb | 97 +++++++++++++++++++ ..._in_merge_request_template_service_spec.rb | 60 ++++++++++++ 16 files changed, 586 insertions(+), 2 deletions(-) create mode 100644 config/feature_flags/development/fill_in_mr_template.yml create mode 100644 ee/app/graphql/types/ai/fill_in_merge_request_template_input_type.rb create mode 100644 ee/app/services/llm/fill_in_merge_request_template_service.rb create mode 100644 ee/lib/gitlab/llm/templates/fill_in_merge_request_template.rb create mode 100644 ee/lib/gitlab/llm/vertex_ai/completions/fill_in_merge_request_template.rb create mode 100644 ee/spec/lib/gitlab/llm/templates/fill_in_merge_request_template_spec.rb create mode 100644 ee/spec/lib/gitlab/llm/vertex_ai/completions/fill_in_merge_request_template_spec.rb create mode 100644 ee/spec/requests/api/graphql/mutations/projects/fill_in_merge_request_template_spec.rb create mode 100644 ee/spec/services/llm/fill_in_merge_request_template_service_spec.rb diff --git a/config/feature_flags/development/fill_in_mr_template.yml b/config/feature_flags/development/fill_in_mr_template.yml new file mode 100644 index 00000000000000..871d2d09f2ecad --- /dev/null +++ b/config/feature_flags/development/fill_in_mr_template.yml @@ -0,0 +1,8 @@ +--- +name: fill_in_mr_template +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121233 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/412796 +milestone: '16.1' +type: development +group: group::code review +default_enabled: false diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 810dae24f25093..bdf5d557f99ad2 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1043,6 +1043,7 @@ Input type: `AiActionInput` | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | `explainCode` | [`AiExplainCodeInput`](#aiexplaincodeinput) | Input for explain_code AI action. | | `explainVulnerability` | [`AiExplainVulnerabilityInput`](#aiexplainvulnerabilityinput) | Input for explain_vulnerability AI action. | +| `fillInMergeRequestTemplate` | [`AiFillInMergeRequestTemplateInput`](#aifillinmergerequesttemplateinput) | Input for fill_in_merge_request_template AI action. | | `generateCommitMessage` | [`AiGenerateCommitMessageInput`](#aigeneratecommitmessageinput) | Input for generate_commit_message AI action. | | `generateDescription` | [`AiGenerateDescriptionInput`](#aigeneratedescriptioninput) | Input for generate_description AI action. | | `generateTestFile` | [`GenerateTestFileInput`](#generatetestfileinput) | Input for generate_test_file AI action. | @@ -27918,6 +27919,19 @@ see the associated mutation type above. | ---- | ---- | ----------- | | `resourceId` | [`AiModelID!`](#aimodelid) | Global ID of the resource to mutate. | +### `AiFillInMergeRequestTemplateInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `content` | [`String!`](#string) | Template content to fill in. | +| `resourceId` | [`AiModelID!`](#aimodelid) | Global ID of the resource to mutate. | +| `sourceBranch` | [`String!`](#string) | Source branch of the changes. | +| `sourceProjectId` | [`ID`](#id) | ID of the project where the changes are from. | +| `targetBranch` | [`String!`](#string) | Target branch of where the changes will be merged into. | +| `title` | [`String!`](#string) | Title of the merge request to be created. | + ### `AiGenerateCommitMessageInput` #### Arguments diff --git a/ee/app/graphql/types/ai/fill_in_merge_request_template_input_type.rb b/ee/app/graphql/types/ai/fill_in_merge_request_template_input_type.rb new file mode 100644 index 00000000000000..c1d0cf64e5432b --- /dev/null +++ b/ee/app/graphql/types/ai/fill_in_merge_request_template_input_type.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Types + module Ai + class FillInMergeRequestTemplateInputType < BaseMethodInputType + graphql_name 'AiFillInMergeRequestTemplateInput' + + argument :title, ::GraphQL::Types::String, + required: true, + description: 'Title of the merge request to be created.' + + argument :source_project_id, ::GraphQL::Types::ID, + required: false, + description: 'ID of the project where the changes are from.' + + argument :source_branch, ::GraphQL::Types::String, + required: true, + description: 'Source branch of the changes.' + + argument :target_branch, ::GraphQL::Types::String, + required: true, + description: 'Target branch of where the changes will be merged into.' + + argument :content, ::GraphQL::Types::String, + required: true, + description: 'Template content to fill in.' + end + end +end diff --git a/ee/app/models/gitlab_subscriptions/features.rb b/ee/app/models/gitlab_subscriptions/features.rb index a289c504c479fb..6bddc3d62a4196 100644 --- a/ee/app/models/gitlab_subscriptions/features.rb +++ b/ee/app/models/gitlab_subscriptions/features.rb @@ -182,6 +182,7 @@ class Features api_fuzzing auto_rollback breach_and_attack_simulation + fill_in_merge_request_template no_code_automation ci_namespace_catalog cluster_image_scanning diff --git a/ee/app/policies/ee/project_policy.rb b/ee/app/policies/ee/project_policy.rb index 2ca2644bba9921..d6dc53b60cbb74 100644 --- a/ee/app/policies/ee/project_policy.rb +++ b/ee/app/policies/ee/project_policy.rb @@ -230,6 +230,18 @@ module ProjectPolicy @subject.can_suggest_reviewers? end + with_scope :subject + condition(:ai_features_enabled) do + ::Feature.enabled?(:openai_experimentation) + end + + with_scope :subject + condition(:fill_in_merge_request_template_enabled) do + ::Feature.enabled?(:fill_in_mr_template, subject) && + subject.licensed_feature_available?(:fill_in_merge_request_template) && + ::Gitlab::Llm::StageCheck.available?(subject.root_ancestor, :fill_in_merge_request_template) + end + rule { visual_review_bot }.policy do prevent :read_note enable :create_note @@ -580,6 +592,10 @@ module ProjectPolicy enable :create_pipeline enable :push_code end + + rule do + ai_features_enabled & fill_in_merge_request_template_enabled & can?(:create_merge_request_in) + end.enable :fill_in_merge_request_template end override :lookup_access_level! diff --git a/ee/app/services/llm/execute_method_service.rb b/ee/app/services/llm/execute_method_service.rb index 9c8d018f66e03e..c110fdffb5db68 100644 --- a/ee/app/services/llm/execute_method_service.rb +++ b/ee/app/services/llm/execute_method_service.rb @@ -14,7 +14,8 @@ class ExecuteMethodService < BaseService generate_test_file: Llm::GenerateTestFileService, generate_description: Llm::GenerateDescriptionService, generate_commit_message: Llm::GenerateCommitMessageService, - chat: Llm::ChatService + chat: Llm::ChatService, + fill_in_merge_request_template: Llm::FillInMergeRequestTemplateService }.freeze def initialize(user, resource, method, options = {}) diff --git a/ee/app/services/llm/fill_in_merge_request_template_service.rb b/ee/app/services/llm/fill_in_merge_request_template_service.rb new file mode 100644 index 00000000000000..1183ee9548cddb --- /dev/null +++ b/ee/app/services/llm/fill_in_merge_request_template_service.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Llm + class FillInMergeRequestTemplateService < BaseService + extend ::Gitlab::Utils::Override + + override :valid + def valid? + super && + resource.is_a?(Project) && + Ability.allowed?(user, :fill_in_merge_request_template, resource) + end + + private + + def perform + worker_perform(user, resource, :fill_in_merge_request_template, options) + end + end +end diff --git a/ee/lib/gitlab/llm/completions_factory.rb b/ee/lib/gitlab/llm/completions_factory.rb index e19bcdaa3c452d..96c872643ce1fa 100644 --- a/ee/lib/gitlab/llm/completions_factory.rb +++ b/ee/lib/gitlab/llm/completions_factory.rb @@ -39,6 +39,10 @@ class CompletionsFactory chat: { service_class: ::Gitlab::Llm::Completions::Chat, prompt_class: nil + }, + fill_in_merge_request_template: { + service_class: ::Gitlab::Llm::VertexAi::Completions::FillInMergeRequestTemplate, + prompt_class: ::Gitlab::Llm::Templates::FillInMergeRequestTemplate } }.freeze diff --git a/ee/lib/gitlab/llm/stage_check.rb b/ee/lib/gitlab/llm/stage_check.rb index 5d4125895f8b10..d497a7482d2dfb 100644 --- a/ee/lib/gitlab/llm/stage_check.rb +++ b/ee/lib/gitlab/llm/stage_check.rb @@ -12,7 +12,8 @@ class StageCheck :summarize_diff, :explain_vulnerability, :generate_commit_message, - :chat + :chat, + :fill_in_merge_request_template ].freeze BETA_FEATURES = [].freeze THIRD_PARTY_FEATURES = EXPERIMENTAL_FEATURES + BETA_FEATURES diff --git a/ee/lib/gitlab/llm/templates/fill_in_merge_request_template.rb b/ee/lib/gitlab/llm/templates/fill_in_merge_request_template.rb new file mode 100644 index 00000000000000..0a73340eff3aff --- /dev/null +++ b/ee/lib/gitlab/llm/templates/fill_in_merge_request_template.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Gitlab + module Llm + module Templates + class FillInMergeRequestTemplate + include Gitlab::Utils::StrongMemoize + + GIT_DIFF_PREFIX_REGEX = /\A@@( -\d+,\d+ \+\d+,\d+ )@@/ + + def initialize(user, project, params = {}) + @user = user + @project = project + @params = params + end + + def to_prompt + <<-PROMPT + You are an AI code assistant that can understand DIFF in Git diff format, TEMPLATE in a Markdown format and can produce Markdown as a result. + + You will be given TITLE, DIFF, and TEMPLATE. Do the following: + 1. Create a merge request description from the given TEMPLATE. + 2. Given the TITLE and DIFF, explain the diff in detail and add it to the section in the description for explaining the DIFF. + 3. For sections with placeholder in the description, copy the content from TEMPLATE. + 4. Return the merge request description. + + TITLE: #{params[:title]} + + DIFF: + #{extracted_diff} + + TEMPLATE: + #{content} + PROMPT + end + + private + + attr_reader :user, :project, :params + + def extracted_diff + compare = CompareService + .new(source_project, params[:source_branch]) + .execute(project, params[:target_branch]) + + return unless compare + + # Extract only the diff strings and discard everything else + compare.raw_diffs.to_a.map do |raw_diff| + # Each diff string starts with information about the lines changed, + # bracketed by @@. Removing this saves us tokens. + # + # Ex: @@ -0,0 +1,58 @@\n+# frozen_string_literal: true\n+\n+module MergeRequests\n+ + raw_diff.diff.sub(GIT_DIFF_PREFIX_REGEX, "") + end.join.truncate_words(2000) + end + + def content + # We truncate words of the template content to 600 words so we can + # ensure that it fits the maxOutputTokens of Vertex AI which is set to + # 1024 in the client. + params[:content]&.truncate_words(600) + end + + def source_project + return project unless params[:source_project_id] + + source_project = Project.find_by_id(params[:source_project_id]) + + return source_project if source_project.present? && user.can?(:create_merge_request_from, source_project) + + project + end + end + end + end +end diff --git a/ee/lib/gitlab/llm/vertex_ai/completions/fill_in_merge_request_template.rb b/ee/lib/gitlab/llm/vertex_ai/completions/fill_in_merge_request_template.rb new file mode 100644 index 00000000000000..58c4e25ddb7218 --- /dev/null +++ b/ee/lib/gitlab/llm/vertex_ai/completions/fill_in_merge_request_template.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Gitlab + module Llm + module VertexAi + module Completions + class FillInMergeRequestTemplate < Gitlab::Llm::Completions::Base + def execute(user, project, options) + response = response_for(user, project, options) + response_modifier = ::Gitlab::Llm::VertexAi::ResponseModifiers::Predictions.new(response) + + ::Gitlab::Llm::GraphqlSubscriptionResponseService.new( + user, project, response_modifier, options: { request_id: params[:request_id] } + ).execute + end + + private + + def response_for(user, project, options) + template = ai_prompt_class.new(user, project, options) + request(user, template) + end + + def request(user, template) + ::Gitlab::Llm::VertexAi::Client + .new(user) + .text(content: template.to_prompt) + end + end + end + end + end +end diff --git a/ee/spec/lib/gitlab/llm/templates/fill_in_merge_request_template_spec.rb b/ee/spec/lib/gitlab/llm/templates/fill_in_merge_request_template_spec.rb new file mode 100644 index 00000000000000..0e5fb445df20fe --- /dev/null +++ b/ee/spec/lib/gitlab/llm/templates/fill_in_merge_request_template_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Llm::Templates::FillInMergeRequestTemplate, feature_category: :code_review_workflow do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { project.owner } + let(:source_project) { project } + + let(:params) do + { + source_project_id: source_project.id, + source_branch: 'feature', + target_branch: 'master', + title: 'A merge request', + content: 'This is content' + } + end + + subject { described_class.new(user, project, params) } + + describe '#to_prompt' do + it 'includes title param' do + expect(subject.to_prompt).to include(params[:title]) + end + + it 'includes raw diff' do + expect(subject.to_prompt) + .to include("+class Feature\n+ def foo\n+ puts 'bar'\n+ end\n+end") + end + + it 'includes the content' do + expect(subject.to_prompt).to include('This is content') + end + + context 'when user cannot create merge request from source_project_id' do + let(:source_project) { create(:project) } + + it 'includes diff comparison from project' do + expect(subject.to_prompt) + .to include("+class Feature\n+ def foo\n+ puts 'bar'\n+ end\n+end") + end + end + + context 'when no source_project_id is specified' do + let(:params) do + { + source_branch: 'feature', + target_branch: 'master', + title: 'A merge request', + content: 'This is content' + } + end + + it 'includes diff comparison from project' do + expect(subject.to_prompt) + .to include("+class Feature\n+ def foo\n+ puts 'bar'\n+ end\n+end") + end + end + end +end diff --git a/ee/spec/lib/gitlab/llm/vertex_ai/completions/fill_in_merge_request_template_spec.rb b/ee/spec/lib/gitlab/llm/vertex_ai/completions/fill_in_merge_request_template_spec.rb new file mode 100644 index 00000000000000..b09c5bf2025fd7 --- /dev/null +++ b/ee/spec/lib/gitlab/llm/vertex_ai/completions/fill_in_merge_request_template_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Llm::VertexAi::Completions::FillInMergeRequestTemplate, feature_category: :code_review_workflow do + let(:prompt_class) { Gitlab::Llm::Templates::FillInMergeRequestTemplate } + let(:options) { { request_id: 'uuid' } } + let(:response_modifier) { double } + let(:response_service) { double } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let(:params) { [user, project, response_modifier, { options: { request_id: 'uuid' } }] } + + subject { described_class.new(prompt_class, options) } + + describe '#execute' do + context 'when the text client returns a successful response' do + let(:example_answer) { "AI filled in template" } + + let(:example_response) do + { + "predictions" => [ + { + "content" => example_answer, + "safetyAttributes" => { + "categories" => ["Violent"], + "scores" => [0.4000000059604645], + "blocked" => false + } + } + ] + } + end + + before do + allow_next_instance_of(Gitlab::Llm::VertexAi::Client) do |client| + allow(client).to receive(:text).and_return(example_response.to_json) + end + end + + it 'publishes the content from the AI response' do + expect(::Gitlab::Llm::VertexAi::ResponseModifiers::Predictions) + .to receive(:new) + .with(example_response.to_json) + .and_return(response_modifier) + + expect(::Gitlab::Llm::GraphqlSubscriptionResponseService) + .to receive(:new) + .with(*params) + .and_return(response_service) + + expect(response_service).to receive(:execute) + + subject.execute(user, project, options) + end + end + + context 'when the text client returns an unsuccessful response' do + let(:error) { { error: 'Error' } } + + before do + allow_next_instance_of(Gitlab::Llm::VertexAi::Client) do |client| + allow(client).to receive(:text).and_return(error.to_json) + end + end + + it 'publishes the error to the graphql subscription' do + expect(::Gitlab::Llm::VertexAi::ResponseModifiers::Predictions) + .to receive(:new) + .with(error.to_json) + .and_return(response_modifier) + + expect(::Gitlab::Llm::GraphqlSubscriptionResponseService) + .to receive(:new) + .with(*params) + .and_return(response_service) + + expect(response_service).to receive(:execute) + + subject.execute(user, project, options) + end + end + end +end diff --git a/ee/spec/policies/project_policy_spec.rb b/ee/spec/policies/project_policy_spec.rb index 51f46747700b57..fe2c76b139831a 100644 --- a/ee/spec/policies/project_policy_spec.rb +++ b/ee/spec/policies/project_policy_spec.rb @@ -2776,4 +2776,82 @@ def create_member_role(member, abilities = member_role_abilities) end end end + + describe 'fill_in_merge_request_template policy', :saas do + let_it_be(:namespace) { create(:group_with_plan, plan: :ultimate_plan) } + let_it_be(:project) { create(:project, group: namespace) } + let_it_be(:current_user) { owner } + + before do + namespace.add_owner(owner) + allow(project).to receive(:namespace).and_return(namespace) + stub_ee_application_setting(should_check_namespace_plan: true) + + stub_licensed_features( + fill_in_merge_request_template: true, + ai_features: true + ) + + stub_feature_flags( + openai_experimentation: true, + fill_in_mr_template: true + ) + + namespace.namespace_settings.update!( + experiment_features_enabled: true, + third_party_ai_features_enabled: true + ) + end + + it { is_expected.to be_allowed(:fill_in_merge_request_template) } + + context 'when global AI feature flag is disabled' do + before do + stub_feature_flags(openai_experimentation: false) + end + + it { is_expected.to be_disallowed(:fill_in_merge_request_template) } + end + + context 'when fill_in_mr_template feature flag is disabled' do + before do + stub_feature_flags( + openai_experimentation: true, + fill_in_mr_template: false + ) + end + + it { is_expected.to be_disallowed(:fill_in_merge_request_template) } + end + + context 'when license is not set' do + before do + stub_licensed_features(fill_in_merge_request_template: false) + end + + it { is_expected.to be_disallowed(:fill_in_merge_request_template) } + end + + context 'when experiment features are disabled' do + before do + namespace.namespace_settings.update!(experiment_features_enabled: false) + end + + it { is_expected.to be_disallowed(:fill_in_merge_request_template) } + end + + context 'when third party ai features are disabled' do + before do + namespace.namespace_settings.update!(third_party_ai_features_enabled: false) + end + + it { is_expected.to be_disallowed(:fill_in_merge_request_template) } + end + + context 'when user cannot create_merge_request_in' do + let(:current_user) { guest } + + it { is_expected.to be_disallowed(:fill_in_merge_request_template) } + 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 new file mode 100644 index 00000000000000..367d2d8ffa6dc8 --- /dev/null +++ b/ee/spec/requests/api/graphql/mutations/projects/fill_in_merge_request_template_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe 'AiAction for Fill In Merge Request Template', :saas, feature_category: :code_review_workflow do + include GraphqlHelpers + include Graphql::Subscriptions::Notes::Helper + + let_it_be(:group) { create(:group_with_plan, :public, plan: :ultimate_plan) } + let_it_be(:project) { create(:project, :public, group: group) } + let_it_be(:current_user) { create(:user, developer_projects: [project]) } + + let(:mutation) do + params = { + fill_in_merge_request_template: { + resource_id: project.to_gid, + source_project_id: project.id, + source_branch: 'feature', + target_branch: 'master', + title: 'A merge request', + content: 'This is content' + } + } + + graphql_mutation(:ai_action, params) do + <<-QL.strip_heredoc + errors + QL + end + end + + before do + stub_ee_application_setting(should_check_namespace_plan: true) + stub_licensed_features(fill_in_merge_request_template: true, ai_features: true) + group.namespace_settings.update!(third_party_ai_features_enabled: true, experiment_features_enabled: true) + end + + it 'successfully performs an explain code request' do + expect(Llm::CompletionWorker).to receive(:perform_async).with( + current_user.id, + project.id, + 'Project', + :fill_in_merge_request_template, + { + markup_format: :raw, + request_id: an_instance_of(String), + source_project_id: project.id.to_s, + source_branch: 'feature', + target_branch: 'master', + title: 'A merge request', + content: 'This is content' + } + ) + + post_graphql_mutation(mutation, current_user: current_user) + + expect(graphql_mutation_response(:ai_action)['errors']).to eq([]) + end + + context 'when openai_experimentation feature flag is disabled' do + before do + stub_feature_flags(openai_experimentation: false) + end + + it 'returns nil' do + expect(Llm::CompletionWorker).not_to receive(:perform_async) + + post_graphql_mutation(mutation, current_user: current_user) + + expect(fresh_response_data['errors'][0]['message']).to eq("`openai_experimentation` feature flag is disabled.") + end + end + + context 'when third_party_ai_features_enabled disabled' do + before do + group.namespace_settings.update!(third_party_ai_features_enabled: false) + end + + it 'returns nil' do + expect(Llm::CompletionWorker).not_to receive(:perform_async) + + post_graphql_mutation(mutation, current_user: current_user) + end + end + + context 'when experiment_features_enabled disabled' do + before do + group.namespace_settings.update!(experiment_features_enabled: false) + end + + it 'returns nil' do + expect(Llm::CompletionWorker).not_to receive(:perform_async) + + post_graphql_mutation(mutation, current_user: current_user) + 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 new file mode 100644 index 00000000000000..d0921248cafa10 --- /dev/null +++ b/ee/spec/services/llm/fill_in_merge_request_template_service_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Llm::FillInMergeRequestTemplateService, feature_category: :code_review_workflow do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group, :public) } + let_it_be(:resource) { create(:project, :public, group: group) } + + let(:fill_in_merge_request_template_enabled) { true } + let(:current_user) { user } + + describe '#perform' do + before do + group.add_guest(user) + + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability) + .to receive(:allowed?) + .with(user, :fill_in_merge_request_template, resource) + .and_return(fill_in_merge_request_template_enabled) + end + + subject { described_class.new(current_user, resource, {}).execute } + + it_behaves_like 'completion worker sync and async' do + subject { described_class.new(current_user, resource, options) } + + let(:action_name) { :fill_in_merge_request_template } + let(:options) { {} } + let(:content) { 'Fill in merge request template' } + end + + context 'when user is not member of project group' do + let(:current_user) { create(:user) } + + it { is_expected.to be_error.and have_attributes(message: eq(described_class::INVALID_MESSAGE)) } + end + + context 'when general feature flag is disabled' do + before do + stub_feature_flags(openai_experimentation: false) + end + + it { is_expected.to be_error.and have_attributes(message: eq(described_class::INVALID_MESSAGE)) } + end + + context 'when resource is not a project' do + let(:resource) { create(:epic, group: group) } + + it { is_expected.to be_error.and have_attributes(message: eq(described_class::INVALID_MESSAGE)) } + end + + context 'when user has no ability to fill_in_merge_request_template' do + let(:fill_in_merge_request_template_enabled) { false } + + it { is_expected.to be_error.and have_attributes(message: eq(described_class::INVALID_MESSAGE)) } + end + end +end -- GitLab