Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Ci
  class BuildCancelService
    def initialize(build, user)
      @build = build
      @user = user
    end

    def execute
      return forbidden unless allowed?
      return unprocessable_entity unless build.cancelable?

      build.cancel

      ServiceResponse.success(payload: build)
    end

    private

    attr_reader :build, :user

    def allowed?
      user.can?(:cancel_build, build)
    end

    def forbidden
      ServiceResponse.error(message: 'Forbidden', http_status: :forbidden)
    end

    def unprocessable_entity
      ServiceResponse.error(message: 'Unprocessable entity', http_status: :unprocessable_entity)
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::BuildCancelService, feature_category: :continuous_integration do
  let_it_be(:user) { create(:user) }
  let_it_be(:project) { create(:project) }
  let_it_be(:pipeline) { create(:ci_pipeline, project: project) }

  describe '#execute' do
    subject(:execute) { described_class.new(build, user).execute }

    context 'when user is authorized to cancel the build' do
      before do
        project.add_maintainer(user)
      end

      context 'when build is cancelable' do
        let!(:build) { create(:ci_build, :cancelable, pipeline: pipeline) }

        it 'transits build to canceled', :aggregate_failures do
          response = execute

          expect(response).to be_success
          expect(response.payload.reload).to be_canceled
        end
      end

      context 'when build is not cancelable' do
        let!(:build) { create(:ci_build, :canceled, pipeline: pipeline) }

        it 'responds with unprocessable entity', :aggregate_failures do
          response = execute

          expect(response).to be_error
          expect(response.http_status).to eq(:unprocessable_entity)
        end
      end
    end

    context 'when user is not authorized to cancel the build' do
      let!(:build) { create(:ci_build, :cancelable, pipeline: pipeline) }

      it 'responds with forbidden', :aggregate_failures do
        response = execute

        expect(response).to be_error
        expect(response.http_status).to eq(:forbidden)
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Ci
  class RunScheduledBuildService < ::BaseService
    def execute(build)
      unless can?(current_user, :update_build, build)
        raise Gitlab::Access::AccessDeniedError
      end

      build.enqueue_scheduled!
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::RunScheduledBuildService, feature_category: :continuous_integration do
  let(:user) { create(:user) }
  let(:project) { create(:project) }
  let(:pipeline) { create(:ci_pipeline, project: project) }

  subject { described_class.new(project, user).execute(build) }

  context 'when user can update build' do
    before do
      project.add_developer(user)

      create(:protected_branch, :developers_can_merge, name: pipeline.ref, project: project)
    end

    context 'when build is scheduled' do
      context 'when scheduled_at is expired' do
        let(:build) { create(:ci_build, :expired_scheduled, user: user, project: project, pipeline: pipeline) }

        it 'can run the build' do
          expect { subject }.not_to raise_error

          expect(build).to be_pending
        end

        context 'when build requires resource' do
          let(:resource_group) { create(:ci_resource_group, project: project) }

          before do
            build.update!(resource_group: resource_group)
          end

          it 'transits to waiting for resource status' do
            expect { subject }.to change { build.status }.from('scheduled').to('waiting_for_resource')
          end
        end
      end

      context 'when scheduled_at is not expired' do
        let(:build) { create(:ci_build, :scheduled, user: user, project: project, pipeline: pipeline) }

        it 'can not run the build' do
          expect { subject }.to raise_error(StateMachines::InvalidTransition)

          expect(build).to be_scheduled
        end
      end
    end

    context 'when build is not scheduled' do
      let(:build) { create(:ci_build, :created, user: user, project: project, pipeline: pipeline) }

      it 'can not run the build' do
        expect { subject }.to raise_error(StateMachines::InvalidTransition)

        expect(build).to be_created
      end
    end
  end

  context 'when user can not update build' do
    context 'when build is scheduled' do
      let(:build) { create(:ci_build, :scheduled, user: user, project: project, pipeline: pipeline) }

      it 'can not run the build' do
        expect { subject }.to raise_error(Gitlab::Access::AccessDeniedError)

        expect(build).to be_scheduled
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Ci
  # TODO: a couple of points with this approach:
  # + reuses existing architecture and reactive caching
  # - it's not a report comparison and some comparing features must be turned off.
  # see CompareReportsBaseService for more notes.
  # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
  class GenerateCodequalityMrDiffReportService < CompareReportsBaseService
    def execute(base_pipeline, head_pipeline)
      merge_request = MergeRequest.find_by_id(params[:id])
      {
        status: :parsed,
        key: key(base_pipeline, head_pipeline),
        data: head_pipeline.pipeline_artifacts.find_by_file_type(:code_quality_mr_diff).present.for_files(merge_request)
      }
    rescue StandardError => e
      Gitlab::ErrorTracking.track_exception(e, project_id: project.id)
      {
        status: :error,
        key: key(base_pipeline, head_pipeline),
        status_reason: _('An error occurred while fetching codequality mr diff reports.')
      }
    end

    def latest?(base_pipeline, head_pipeline, data)
      data&.fetch(:key, nil) == key(base_pipeline, head_pipeline)
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::GenerateCodequalityMrDiffReportService, feature_category: :code_review_workflow do
  let(:service) { described_class.new(project) }
  let(:project) { create(:project, :repository) }

  describe '#execute' do
    subject { service.execute(base_pipeline, head_pipeline) }

    context 'when head pipeline has codequality mr diff report' do
      let!(:merge_request) { create(:merge_request, :with_codequality_mr_diff_reports, source_project: project, id: 123456789) }
      let!(:service) { described_class.new(project, nil, id: merge_request.id) }
      let!(:head_pipeline) { merge_request.head_pipeline }
      let!(:base_pipeline) { nil }

      it 'returns status and data', :aggregate_failures do
        expect_any_instance_of(Ci::PipelineArtifact) do |instance|
          expect(instance).to receive(:present)
          expect(instance).to receive(:for_files).with(merge_request).and_call_original
        end

        expect(subject[:status]).to eq(:parsed)
        expect(subject[:data]).to eq(files: {})
      end
    end

    context 'when head pipeline does not have a codequality mr diff report' do
      let!(:merge_request) { create(:merge_request, source_project: project) }
      let!(:service) { described_class.new(project, nil, id: merge_request.id) }
      let!(:head_pipeline) { merge_request.head_pipeline }
      let!(:base_pipeline) { nil }

      it 'returns status and error message' do
        expect(subject[:status]).to eq(:error)
        expect(subject[:status_reason]).to include('An error occurred while fetching codequality mr diff reports.')
      end
    end

    context 'when head pipeline has codequality mr diff report and no merge request associated' do
      let!(:head_pipeline) { create(:ci_pipeline, :with_codequality_mr_diff_report, project: project) }
      let!(:base_pipeline) { nil }

      it 'returns status and error message' do
        expect(subject[:status]).to eq(:error)
        expect(subject[:status_reason]).to include('An error occurred while fetching codequality mr diff reports.')
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Ci
  class GenerateKubeconfigService
    def initialize(pipeline, token:, environment:)
      @pipeline = pipeline
      @token = token
      @environment = environment

      @template = Gitlab::Kubernetes::Kubeconfig::Template.new
    end

    def execute
      template.add_cluster(
        name: cluster_name,
        url: Gitlab::Kas.tunnel_url
      )

      agent_authorizations.each do |authorization|
        agent = authorization.agent
        user = user_name(agent)

        template.add_user(
          name: user,
          token: agent_token(agent)
        )

        template.add_context(
          name: context_name(agent),
          namespace: context_namespace(authorization),
          cluster: cluster_name,
          user: user
        )
      end

      template
    end

    private

    attr_reader :pipeline, :token, :environment, :template

    def agent_authorizations
      ::Clusters::Agents::Authorizations::CiAccess::FilterService.new(
        pipeline.cluster_agent_authorizations,
        environment: environment
      ).execute
    end

    def cluster_name
      'gitlab'
    end

    def user_name(agent)
      ['agent', agent.id].join(delimiter)
    end

    def context_name(agent)
      [agent.project.full_path, agent.name].join(delimiter)
    end

    def context_namespace(authorization)
      authorization.config['default_namespace']
    end

    def agent_token(agent)
      ['ci', agent.id, token].join(delimiter)
    end

    def delimiter
      ':'
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::GenerateKubeconfigService, feature_category: :deployment_management do
  describe '#execute' do
    let_it_be(:group) { create(:group) }
    let_it_be(:project) { create(:project, group: group) }
    let_it_be(:pipeline) { create(:ci_empty_pipeline, project: project) }
    let_it_be(:build) { create(:ci_build, project: project, pipeline: pipeline) }

    let_it_be(:agent_project) { create(:project, group: group, name: 'project-containing-agent-config') }

    let_it_be(:project_agent_authorization) do
      agent = create(:cluster_agent, project: agent_project)
      create(:agent_ci_access_project_authorization, agent: agent, project: project)
    end

    let_it_be(:group_agent_authorization) do
      agent = create(:cluster_agent, project: agent_project)
      create(:agent_ci_access_group_authorization, agent: agent, group: group)
    end

    let(:template) do
      instance_double(
        Gitlab::Kubernetes::Kubeconfig::Template,
        add_cluster: nil,
        add_user: nil,
        add_context: nil
      )
    end

    let(:agent_authorizations) { [project_agent_authorization, group_agent_authorization] }
    let(:filter_service) do
      instance_double(
        ::Clusters::Agents::Authorizations::CiAccess::FilterService,
        execute: agent_authorizations
      )
    end

    subject(:execute) { described_class.new(pipeline, token: build.token, environment: nil).execute }

    before do
      allow(Gitlab::Kubernetes::Kubeconfig::Template).to receive(:new).and_return(template)
      allow(::Clusters::Agents::Authorizations::CiAccess::FilterService).to receive(:new).and_return(filter_service)
    end

    it 'returns a Kubeconfig Template' do
      expect(execute).to eq(template)
    end

    it 'adds a cluster' do
      expect(template).to receive(:add_cluster).with(
        name: 'gitlab',
        url: Gitlab::Kas.tunnel_url
      ).once

      execute
    end

    it "filters the pipeline's agents by `nil` environment" do
      expect(::Clusters::Agents::Authorizations::CiAccess::FilterService).to receive(:new).with(
        pipeline.cluster_agent_authorizations,
        environment: nil
      )

      execute
    end

    it 'adds user and context for all eligible agents', :aggregate_failures do
      agent_authorizations.each do |authorization|
        expect(template).to receive(:add_user).with(
          name: "agent:#{authorization.agent.id}",
          token: "ci:#{authorization.agent.id}:#{build.token}"
        )

        expect(template).to receive(:add_context).with(
          name: "#{agent_project.full_path}:#{authorization.agent.name}",
          namespace: 'production',
          cluster: 'gitlab',
          user: "agent:#{authorization.agent.id}"
        )
      end

      execute
    end

    context 'when environment is specified' do
      subject(:execute) { described_class.new(pipeline, token: build.token, environment: 'production').execute }

      it "filters the pipeline's agents by the specified environment" do
        expect(::Clusters::Agents::Authorizations::CiAccess::FilterService).to receive(:new).with(
          pipeline.cluster_agent_authorizations,
          environment: 'production'
        )

        execute
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Ci
  class UpdateBuildStateService
    include ::Gitlab::Utils::StrongMemoize
    include ::Gitlab::ExclusiveLeaseHelpers

    Result = Struct.new(:status, :backoff, keyword_init: true)
    InvalidTraceError = Class.new(StandardError)

    ACCEPT_TIMEOUT = 5.minutes.freeze

    attr_reader :build, :params, :metrics

    def initialize(build, params, metrics = ::Gitlab::Ci::Trace::Metrics.new)
      @build = build
      @params = params
      @metrics = metrics
    end

    def execute
      unless accept_available?
        return update_build_state!
      end

      ensure_pending_state!

      in_build_trace_lock do
        process_build_state!
      end
    end

    private

    def ensure_pending_state!
      pending_state.created_at
    end

    def process_build_state!
      if live_chunks_pending?
        if pending_state_outdated?
          discard_build_trace!
          update_build_state!
        else
          accept_build_state!
        end
      else
        validate_build_trace!
        update_build_state!
      end
    end

    def accept_build_state!
      build.trace_chunks.live.find_each do |chunk|
        chunk.schedule_to_persist!
      end

      metrics.increment_trace_operation(operation: :accepted)

      ::Gitlab::Ci::Runner::Backoff.new(pending_state.created_at).then do |backoff|
        Result.new(status: 202, backoff: backoff.to_seconds)
      end
    end

    def validate_build_trace!
      return unless has_chunks?

      unless live_chunks_pending?
        metrics.increment_trace_operation(operation: :finalized)
        metrics.observe_migration_duration(pending_state_seconds)
      end

      ::Gitlab::Ci::Trace::Checksum.new(build).then do |checksum|
        unless checksum.valid?
          metrics.increment_trace_operation(operation: :invalid)
          metrics.increment_error_counter(error_reason: :chunks_invalid_checksum)

          if checksum.corrupted?
            metrics.increment_trace_operation(operation: :corrupted)
            metrics.increment_error_counter(error_reason: :chunks_invalid_size)
          end

          next unless log_invalid_chunks?

          ::Gitlab::ErrorTracking.log_exception(InvalidTraceError.new,
            project_path: build.project.full_path,
            build_id: build.id,
            state_crc32: checksum.state_crc32,
            chunks_crc32: checksum.chunks_crc32,
            chunks_count: checksum.chunks_count,
            chunks_corrupted: checksum.corrupted?
          )
        end
      end
    end

    def update_build_state!
      case build_state
      when 'running'
        build.touch if build.needs_touch?

        Result.new(status: 200)
      when 'success'
        build.success!

        Result.new(status: 200)
      when 'failed'
        build.drop_with_exit_code!(params[:failure_reason], params[:exit_code])

        Result.new(status: 200)
      else
        Result.new(status: 400)
      end
    end

    def discard_build_trace!
      metrics.increment_trace_operation(operation: :discarded)
    end

    def accept_available?
      !build_running? && has_checksum? && chunks_migration_enabled?
    end

    def live_chunks_pending?
      build.trace_chunks.live.any?
    end

    def has_chunks?
      build.trace_chunks.any?
    end

    def pending_state_outdated?
      pending_state_duration > ACCEPT_TIMEOUT
    end

    def pending_state_duration
      Time.current - pending_state.created_at
    end

    def pending_state_seconds
      pending_state_duration.seconds
    end

    def build_state
      params.dig(:state).to_s
    end

    def has_checksum?
      trace_checksum.present?
    end

    def build_running?
      build_state == 'running'
    end

    def trace_checksum
      params.dig(:output, :checksum) || params.dig(:checksum)
    end

    def trace_bytesize
      params.dig(:output, :bytesize)
    end

    def pending_state
      strong_memoize(:pending_state) { ensure_pending_state }
    end

    def ensure_pending_state
      build_state = Ci::BuildPendingState.safe_find_or_create_by(
        build_id: build.id,
        partition_id: build.partition_id,
        state: params.fetch(:state),
        trace_checksum: trace_checksum,
        trace_bytesize: trace_bytesize,
        failure_reason: failure_reason
      )

      unless build_state.present?
        metrics.increment_trace_operation(operation: :conflict)
      end

      build_state || build.pending_state
    end

    def failure_reason
      reason = params.dig(:failure_reason)

      return unless reason

      Ci::BuildPendingState.failure_reasons.fetch(reason.to_s, 'unknown_failure')
    end

    ##
    # This method is releasing an exclusive lock on a build trace the moment we
    # conclude that build status has been written and the build state update
    # has been committed to the database.
    #
    # Because a build state machine schedules a bunch of workers to run after
    # build status transition to complete, we do not want to keep the lease
    # until all the workers are scheduled because it opens a possibility of
    # race conditions happening.
    #
    # Instead of keeping the lease until the transition is fully done and
    # workers are scheduled, we immediately release the lock after the database
    # commit happens.
    #
    def in_build_trace_lock(&block)
      build.trace.lock do |_, lease| # rubocop:disable CodeReuse/ActiveRecord
        build.run_on_status_commit { lease.cancel }

        yield
      end
    rescue ::Gitlab::Ci::Trace::LockedError
      metrics.increment_trace_operation(operation: :locked)

      accept_build_state!
    end

    def chunks_migration_enabled?
      ::Feature.enabled?(:ci_enable_live_trace, build.project) &&
        ::Feature.enabled?(:ci_accept_trace, build.project, type: :ops)
    end

    def log_invalid_chunks?
      ::Feature.enabled?(:ci_trace_log_invalid_chunks, build.project, type: :ops)
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::UpdateBuildStateService, feature_category: :continuous_integration do
  let_it_be(:project) { create(:project) }
  let_it_be(:pipeline) { create(:ci_pipeline, project: project) }

  let(:build) { create(:ci_build, :running, pipeline: pipeline) }
  let(:metrics) { spy('metrics') }

  subject { described_class.new(build, params) }

  before do
    stub_feature_flags(ci_enable_live_trace: true)
  end

  context 'when build has unknown failure reason' do
    let(:params) do
      {
        output: { checksum: 'crc32:12345678', bytesize: 123 },
        state: 'failed',
        failure_reason: 'no idea here',
        exit_code: 42
      }
    end

    it 'updates a build status' do
      result = subject.execute

      expect(build).to be_failed
      expect(result.status).to eq 200
    end
  end

  context 'when build has failed' do
    let(:params) do
      {
        output: { checksum: 'crc32:12345678', bytesize: 123 },
        state: 'failed',
        failure_reason: 'script_failure',
        exit_code: 7
      }
    end

    it 'sends a build failed event to Snowplow' do
      expect(::Ci::TrackFailedBuildWorker)
        .to receive(:perform_async).with(build.id, params[:exit_code], params[:failure_reason])

      subject.execute
    end
  end

  context 'when build does not have checksum' do
    context 'when state has changed' do
      let(:params) { { state: 'success' } }

      it 'updates a state of a running build' do
        subject.execute

        expect(build).to be_success
      end

      it 'returns 200 OK status' do
        result = subject.execute

        expect(result.status).to eq 200
      end

      it 'does not increment finalized trace metric' do
        execute_with_stubbed_metrics!

        expect(metrics)
          .not_to have_received(:increment_trace_operation)
          .with(operation: :finalized)
      end
    end

    context 'when it is a heartbeat request' do
      let(:params) { { state: 'success' } }

      it 'updates a build timestamp' do
        expect { subject.execute }.to change { build.updated_at }
      end
    end

    context 'when state is unknown' do
      let(:params) { { state: 'unknown' } }

      it 'responds with 400 bad request' do
        result = subject.execute

        expect(result.status).to eq 400
        expect(build).to be_running
      end
    end
  end

  context 'when build has a checksum' do
    let(:params) do
      {
        output: { checksum: 'crc32:12345678', bytesize: 123 },
        state: 'failed',
        failure_reason: 'script_failure',
        exit_code: 42
      }
    end

    context 'when build does not have associated trace chunks' do
      it 'updates a build status' do
        result = subject.execute

        expect(build).to be_failed
        expect(result.status).to eq 200
      end

      it 'updates the allow_failure flag' do
        expect(build)
          .to receive(:drop_with_exit_code!)
          .with('script_failure', 42)
          .and_call_original

        subject.execute
      end

      it 'does not increment invalid trace metric' do
        execute_with_stubbed_metrics!

        expect(metrics)
          .not_to have_received(:increment_trace_operation)
          .with(operation: :invalid)
      end

      it 'does not increment chunks_invalid_checksum trace metric' do
        execute_with_stubbed_metrics!

        expect(metrics)
          .not_to have_received(:increment_error_counter)
          .with(error_reason: :chunks_invalid_checksum)
      end
    end

    context 'when build trace has been migrated' do
      before do
        create(:ci_build_trace_chunk, :persisted, build: build, initial_data: 'abcd')
      end

      it 'updates a build state' do
        subject.execute

        expect(build).to be_failed
      end

      it 'updates the allow_failure flag' do
        expect(build)
          .to receive(:drop_with_exit_code!)
          .with('script_failure', 42)
          .and_call_original

        subject.execute
      end

      it 'responds with 200 OK status' do
        result = subject.execute

        expect(result.status).to eq 200
      end

      it 'does not set a backoff value' do
        result = subject.execute

        expect(result.backoff).to be_nil
      end

      it 'increments trace finalized operation metric' do
        execute_with_stubbed_metrics!

        expect(metrics)
          .to have_received(:increment_trace_operation)
          .with(operation: :finalized)
      end

      it 'records migration duration in a histogram' do
        freeze_time do
          create(:ci_build_pending_state, build: build, created_at: 0.5.seconds.ago)

          execute_with_stubbed_metrics!
        end

        expect(metrics)
          .to have_received(:observe_migration_duration)
          .with(0.5)
      end

      context 'when trace checksum is not valid' do
        it 'increments invalid trace metric' do
          execute_with_stubbed_metrics!

          expect(metrics)
            .to have_received(:increment_trace_operation)
            .with(operation: :invalid)
        end

        it 'increments chunks_invalid_checksum trace metric' do
          execute_with_stubbed_metrics!

          expect(metrics)
            .to have_received(:increment_error_counter)
            .with(error_reason: :chunks_invalid_checksum)
        end
      end

      context 'when trace checksum is valid' do
        let(:params) do
          { output: { checksum: 'crc32:ed82cd11', bytesize: 4 }, state: 'success' }
        end

        it 'does not increment invalid or corrupted trace metric' do
          execute_with_stubbed_metrics!

          expect(metrics)
            .not_to have_received(:increment_trace_operation)
            .with(operation: :invalid)

          expect(metrics)
            .not_to have_received(:increment_trace_operation)
            .with(operation: :corrupted)

          expect(metrics)
            .not_to have_received(:increment_error_counter)
            .with(error_reason: :chunks_invalid_checksum)

          expect(metrics)
            .not_to have_received(:increment_error_counter)
            .with(error_reason: :chunks_invalid_size)
        end

        context 'when using deprecated parameters' do
          let(:params) do
            { checksum: 'crc32:ed82cd11', state: 'success' }
          end

          it 'does not increment invalid or corrupted trace metric' do
            execute_with_stubbed_metrics!

            expect(metrics)
              .not_to have_received(:increment_trace_operation)
              .with(operation: :invalid)

            expect(metrics)
              .not_to have_received(:increment_trace_operation)
              .with(operation: :corrupted)

            expect(metrics)
              .not_to have_received(:increment_error_counter)
              .with(error_reason: :chunks_invalid_checksum)

            expect(metrics)
              .not_to have_received(:increment_error_counter)
              .with(error_reason: :chunks_invalid_size)
          end
        end
      end

      context 'when trace checksum is invalid and the log is corrupted' do
        let(:params) do
          { output: { checksum: 'crc32:12345678', bytesize: 1 }, state: 'success' }
        end

        it 'increments invalid and corrupted trace metrics' do
          execute_with_stubbed_metrics!

          expect(metrics)
            .to have_received(:increment_trace_operation)
            .with(operation: :invalid)

          expect(metrics)
            .to have_received(:increment_trace_operation)
            .with(operation: :corrupted)

          expect(metrics)
            .to have_received(:increment_error_counter)
            .with(error_reason: :chunks_invalid_checksum)

          expect(metrics)
            .to have_received(:increment_error_counter)
            .with(error_reason: :chunks_invalid_size)
        end
      end

      context 'when trace checksum is invalid but the log seems fine' do
        let(:params) do
          { output: { checksum: 'crc32:12345678', bytesize: 4 }, state: 'success' }
        end

        it 'does not increment corrupted trace metric' do
          execute_with_stubbed_metrics!

          expect(metrics)
            .to have_received(:increment_trace_operation)
            .with(operation: :invalid)

          expect(metrics)
            .to have_received(:increment_error_counter)
            .with(error_reason: :chunks_invalid_checksum)

          expect(metrics)
            .not_to have_received(:increment_trace_operation)
            .with(operation: :corrupted)

          expect(metrics)
            .not_to have_received(:increment_error_counter)
            .with(error_reason: :chunks_invalid_size)
        end
      end

      context 'when failed to acquire a build trace lock' do
        it 'accepts a state update request' do
          build.trace.lock do
            result = subject.execute

            expect(result.status).to eq 202
          end
        end

        it 'increment locked trace metric' do
          build.trace.lock do
            execute_with_stubbed_metrics!

            expect(metrics)
              .to have_received(:increment_trace_operation)
              .with(operation: :locked)
          end
        end
      end
    end

    context 'when build trace has not been migrated yet' do
      before do
        create(:ci_build_trace_chunk, :redis_with_data, build: build)
      end

      it 'does not update a build state' do
        subject.execute

        expect(build).to be_running
      end

      it 'responds with 202 accepted' do
        result = subject.execute

        expect(result.status).to eq 202
      end

      it 'sets a request backoff value' do
        result = subject.execute

        expect(result.backoff.to_i).to be > 0
      end

      it 'schedules live chunks for migration' do
        expect(Ci::BuildTraceChunkFlushWorker)
          .to receive(:perform_async)
          .with(build.trace_chunks.first.id)

        subject.execute
      end

      it 'creates a pending state record' do
        subject.execute

        build.pending_state.then do |status|
          expect(status).to be_present
          expect(status.state).to eq 'failed'
          expect(status.trace_checksum).to eq 'crc32:12345678'
          expect(status.failure_reason).to eq 'script_failure'
        end
      end

      it 'increments trace accepted operation metric' do
        execute_with_stubbed_metrics!

        expect(metrics)
          .to have_received(:increment_trace_operation)
          .with(operation: :accepted)
      end

      it 'does not increment invalid trace metric' do
        execute_with_stubbed_metrics!

        expect(metrics)
          .not_to have_received(:increment_trace_operation)
          .with(operation: :invalid)

        expect(metrics)
          .not_to have_received(:increment_error_counter)
          .with(error_reason: :chunks_invalid_checksum)
      end

      context 'when build pending state is outdated' do
        before do
          build.create_pending_state(
            state: 'failed',
            trace_checksum: 'crc32:12345678',
            failure_reason: 'script_failure',
            created_at: 10.minutes.ago
          )
        end

        it 'responds with 200 OK' do
          result = subject.execute

          expect(result.status).to eq 200
        end

        it 'updates build state' do
          subject.execute

          expect(build.reload).to be_failed
          expect(build.failure_reason).to eq 'script_failure'
        end

        it 'increments discarded traces metric' do
          execute_with_stubbed_metrics!

          expect(metrics)
            .to have_received(:increment_trace_operation)
            .with(operation: :discarded)
        end

        it 'does not increment finalized trace metric' do
          execute_with_stubbed_metrics!

          expect(metrics)
            .not_to have_received(:increment_trace_operation)
            .with(operation: :finalized)
        end
      end

      context 'when build pending state has changes' do
        before do
          build.create_pending_state(
            state: 'success',
            created_at: 10.minutes.ago
          )
        end

        it 'uses stored state and responds with 200 OK' do
          result = subject.execute

          expect(result.status).to eq 200
        end

        it 'increments conflict trace metric' do
          execute_with_stubbed_metrics!

          expect(metrics)
            .to have_received(:increment_trace_operation)
            .with(operation: :conflict)
        end
      end

      context 'when live traces are disabled' do
        before do
          stub_feature_flags(ci_enable_live_trace: false)
        end

        it 'responds with 200 OK' do
          result = subject.execute

          expect(result.status).to eq 200
        end
      end
    end
  end

  def execute_with_stubbed_metrics!
    described_class
      .new(build, params, metrics)
      .execute
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Ci
  class PlayManualStageService < BaseService
    def initialize(project, current_user, params)
      super

      @pipeline = params[:pipeline]
    end

    def execute(stage)
      stage.processables.manual.each do |processable|
        next unless processable.playable?

        processable.play(current_user)
      rescue Gitlab::Access::AccessDeniedError
        logger.error(message: 'Unable to play manual action', processable_id: processable.id)
      end
    end

    private

    attr_reader :pipeline, :current_user

    def logger
      Gitlab::AppLogger
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::PlayManualStageService, '#execute', feature_category: :continuous_integration do
  let(:current_user) { create(:user) }
  let(:pipeline) { create(:ci_pipeline, user: current_user) }
  let(:project) { pipeline.project }
  let(:downstream_project) { create(:project) }
  let(:service) { described_class.new(project, current_user, pipeline: pipeline) }
  let(:stage_status) { 'manual' }

  let(:stage) do
    create(:ci_stage, pipeline: pipeline, project: project, name: 'test')
  end

  before do
    project.add_maintainer(current_user)
    downstream_project.add_maintainer(current_user)
    create_builds_for_stage(status: stage_status)
    create_bridge_for_stage(status: stage_status)
  end

  context 'when pipeline has manual processables' do
    before do
      service.execute(stage)
    end

    it 'starts manual processables from pipeline' do
      expect(pipeline.processables.manual.count).to eq(0)
    end

    it 'updates manual processables' do
      pipeline.processables.each do |processable|
        expect(processable.user).to eq(current_user)
      end
    end
  end

  context 'when pipeline has no manual processables' do
    let(:stage_status) { 'failed' }

    before do
      service.execute(stage)
    end

    it 'does not update the processables' do
      expect(pipeline.processables.failed.count).to eq(4)
    end
  end

  context 'when user does not have permission on a specific processable' do
    before do
      allow_next_instance_of(Ci::Processable) do |instance|
        allow(instance).to receive(:play).and_raise(Gitlab::Access::AccessDeniedError)
      end

      service.execute(stage)
    end

    it 'logs the error' do
      expect(Gitlab::AppLogger).to receive(:error)
        .exactly(stage.processables.manual.count)

      service.execute(stage)
    end
  end

  private

  def create_builds_for_stage(options)
    options.merge!({
      when: 'manual',
      pipeline: pipeline,
      stage_id: stage.id,
      user: pipeline.user
    })

    create_list(:ci_build, 3, options)
  end

  def create_bridge_for_stage(options)
    options.merge!({
      when: 'manual',
      pipeline: pipeline,
      stage_id: stage.id,
      user: pipeline.user,
      downstream: downstream_project
    })

    create(:ci_bridge, options)
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Ci
  class BuildEraseService
    include BaseServiceUtility

    def initialize(build, current_user)
      @build = build
      @current_user = current_user
    end

    def execute
      unless build.erasable?
        return ServiceResponse.error(message: _('Build cannot be erased'), http_status: :unprocessable_entity)
      end

      if build.project.refreshing_build_artifacts_size?
        Gitlab::ProjectStatsRefreshConflictsLogger.warn_artifact_deletion_during_stats_refresh(
          method: 'Ci::BuildEraseService#execute',
          project_id: build.project_id
        )
      end

      destroy_artifacts
      erase_trace!
      update_erased!

      ServiceResponse.success(payload: build)
    end

    private

    attr_reader :build, :current_user

    def destroy_artifacts
      Ci::JobArtifacts::DestroyBatchService.new(build.job_artifacts).execute
    end

    def erase_trace!
      build.trace.erase!
    end

    def update_erased!
      build.update(erased_by: current_user, erased_at: Time.current, artifacts_expire_at: nil)
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::BuildEraseService, feature_category: :continuous_integration do
  let_it_be(:user) { user }

  let(:build) { create(:ci_build, :artifacts, :trace_artifact, artifacts_expire_at: 100.days.from_now) }

  subject(:service) { described_class.new(build, user) }

  describe '#execute' do
    context 'when build is erasable' do
      before do
        allow(build).to receive(:erasable?).and_return(true)
      end

      it 'is successful' do
        result = service.execute

        expect(result).to be_success
      end

      it 'erases artifacts' do
        service.execute

        expect(build.artifacts_file).not_to be_present
        expect(build.artifacts_metadata).not_to be_present
      end

      it 'erases trace' do
        service.execute

        expect(build.trace).not_to exist
      end

      it 'records erasure detail' do
        freeze_time do
          service.execute

          expect(build.erased_by).to eq(user)
          expect(build.erased_at).to eq(Time.current)
          expect(build.artifacts_expire_at).to be_nil
        end
      end

      context 'when project is undergoing statistics refresh' do
        before do
          allow(build.project).to receive(:refreshing_build_artifacts_size?).and_return(true)
        end

        it 'logs a warning' do
          expect(Gitlab::ProjectStatsRefreshConflictsLogger)
            .to receive(:warn_artifact_deletion_during_stats_refresh)
            .with(method: 'Ci::BuildEraseService#execute', project_id: build.project_id)

          service.execute
        end
      end
    end

    context 'when build is not erasable' do
      before do
        allow(build).to receive(:erasable?).and_return(false)
      end

      it 'is not successful' do
        result = service.execute

        expect(result).to be_error
        expect(result.http_status).to eq(:unprocessable_entity)
      end

      it 'does not erase artifacts' do
        service.execute

        expect(build.artifacts_file).to be_present
        expect(build.artifacts_metadata).to be_present
      end

      it 'does not erase trace' do
        service.execute

        expect(build.trace).to exist
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Ci
  class ListConfigVariablesService < ::BaseService
    include ReactiveCaching

    self.reactive_cache_key = ->(service) { [service.class.name, service.id] }
    self.reactive_cache_work_type = :external_dependency
    self.reactive_cache_worker_finder = ->(id, *_args) { from_cache(id) }

    def self.from_cache(id)
      project_id, user_id = id.split('-')

      project = Project.find(project_id)
      user = User.find(user_id)

      new(project, user)
    end

    def execute(ref)
      sha = project.commit(ref).try(:sha)

      with_reactive_cache(sha) { |result| result }
    end

    def calculate_reactive_cache(sha)
      config = ::Gitlab::Ci::ProjectConfig.new(project: project, sha: sha)

      return {} unless config.exists?

      result = Gitlab::Ci::YamlProcessor.new(
        config.content,
        project: project,
        user: current_user,
        sha: sha,
        verify_project_sha: true
      ).execute

      result.valid? ? result.root_variables_with_prefill_data : {}
    end

    # Required for ReactiveCaching, it is also used in `reactive_cache_worker_finder`
    def id
      "#{project.id}-#{current_user.id}"
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::ListConfigVariablesService,
  :use_clean_rails_memory_store_caching, feature_category: :secrets_management do
  include ReactiveCachingHelpers

  let(:ci_config) { {} }
  let(:files) { { '.gitlab-ci.yml' => YAML.dump(ci_config) } }
  let(:project) { create(:project, :custom_repo, :auto_devops_disabled, files: files) }
  let(:user) { project.creator }
  let(:ref) { project.default_branch }
  let(:sha) { project.commit(ref).sha }
  let(:service) { described_class.new(project, user) }

  subject(:result) { service.execute(ref) }

  context 'when sending a valid ref' do
    let(:ci_config) do
      {
        variables: {
          KEY1: { value: 'val 1', description: 'description 1' },
          KEY2: { value: 'val 2', description: '' },
          KEY3: { value: 'val 3' },
          KEY4: 'val 4'
        },
        test: {
          stage: 'test',
          script: 'echo'
        }
      }
    end

    let(:expected_result) do
      {
        'KEY1' => { value: 'val 1', description: 'description 1' },
        'KEY2' => { value: 'val 2', description: '' },
        'KEY3' => { value: 'val 3' },
        'KEY4' => { value: 'val 4' }
      }
    end

    before do
      synchronous_reactive_cache(service)
    end

    it 'returns variables list' do
      expect(result).to eq(expected_result)
    end

    context 'when the ref is a sha from a fork' do
      include_context 'when a project repository contains a forked commit'

      before do
        allow_next_instance_of(Gitlab::Ci::ProjectConfig) do |instance|
          allow(instance).to receive(:exists?).and_return(true)
          allow(instance).to receive(:content).and_return(YAML.dump(ci_config))
        end
      end

      let(:ref) { forked_commit_sha }

      context 'when a project ref contains the sha' do
        before do
          mock_branch_contains_forked_commit_sha
        end

        it 'returns variables list' do
          expect(result).to eq(expected_result)
        end
      end

      context 'when a project ref does not contain the sha' do
        it 'returns empty response' do
          expect(result).to eq({})
        end
      end
    end
  end

  context 'when config has includes' do
    let(:ci_config) do
      {
        include: [{ local: 'other_file.yml' }],
        variables: {
          KEY1: { value: 'val 1', description: 'description 1' }
        },
        test: {
          stage: 'test',
          script: 'echo'
        }
      }
    end

    let(:other_file) do
      {
        variables: {
          KEY2: { value: 'val 2', description: 'description 2' }
        }
      }
    end

    let(:files) { { '.gitlab-ci.yml' => YAML.dump(ci_config), 'other_file.yml' => YAML.dump(other_file) } }

    before do
      synchronous_reactive_cache(service)
    end

    it 'returns variable list' do
      expect(result['KEY1']).to eq({ value: 'val 1', description: 'description 1' })
      expect(result['KEY2']).to eq({ value: 'val 2', description: 'description 2' })
    end
  end

  context 'when project CI config is external' do
    let(:other_project_ci_config) do
      {
        variables: { KEY1: { value: 'val 1', description: 'description 1' } },
        test: { script: 'echo' }
      }
    end

    let(:other_project_files) { { '.gitlab-ci.yml' => YAML.dump(other_project_ci_config) } }
    let(:other_project) { create(:project, :custom_repo, files: other_project_files) }

    before do
      project.update!(ci_config_path: ".gitlab-ci.yml@#{other_project.full_path}:master")
      synchronous_reactive_cache(service)
    end

    context 'when the user has access to the external project' do
      before do
        other_project.add_developer(user)
      end

      it 'returns variable list' do
        expect(result['KEY1']).to eq({ value: 'val 1', description: 'description 1' })
      end
    end

    context 'when the user has no access to the external project' do
      it 'returns empty json' do
        expect(result).to eq({})
      end
    end
  end

  context 'when sending an invalid ref' do
    let(:ref) { 'invalid-ref' }
    let(:ci_config) { nil }

    before do
      synchronous_reactive_cache(service)
    end

    it 'returns empty json' do
      expect(result).to eq({})
    end
  end

  context 'when sending an invalid config' do
    let(:ci_config) do
      {
        variables: {
          KEY1: { value: 'val 1', description: 'description 1' }
        },
        test: {
          stage: 'invalid',
          script: 'echo'
        }
      }
    end

    before do
      synchronous_reactive_cache(service)
    end

    it 'returns empty result' do
      expect(result).to eq({})
    end
  end

  context 'when reading from cache' do
    let(:reactive_cache_params) { [sha] }
    let(:return_value) { { 'KEY1' => { value: 'val 1', description: 'description 1' } } }

    before do
      stub_reactive_cache(service, return_value, reactive_cache_params)
    end

    it 'returns variable list' do
      expect(result).to eq(return_value)
    end
  end

  context 'when the cache is empty' do
    let(:reactive_cache_params) { [sha] }

    it 'returns nil and enquques the worker to fill cache' do
      expect(ExternalServiceReactiveCachingWorker)
        .to receive(:perform_async)
        .with(service.class, service.id, *reactive_cache_params)

      expect(result).to be_nil
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Ci
  class PlayBridgeService < ::BaseService
    def execute(bridge)
      check_access!(bridge)

      Ci::EnqueueJobService.new(bridge, current_user: current_user).execute
    end

    private

    def check_access!(bridge)
      raise Gitlab::Access::AccessDeniedError unless can?(current_user, :play_job, bridge)
    end
  end
end

Ci::PlayBridgeService.prepend_mod_with('Ci::PlayBridgeService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::PlayBridgeService, '#execute', feature_category: :continuous_integration do
  let(:project) { create(:project) }
  let(:user) { create(:user) }
  let(:pipeline) { create(:ci_pipeline, project: project) }
  let(:downstream_project) { create(:project) }
  let(:bridge) { create(:ci_bridge, :playable, pipeline: pipeline, downstream: downstream_project) }
  let(:instance) { described_class.new(project, user) }

  subject(:execute_service) { instance.execute(bridge) }

  context 'when user can run the bridge' do
    before do
      allow(instance).to receive(:can?).with(user, :play_job, bridge).and_return(true)
    end

    it 'marks the bridge pending' do
      execute_service

      expect(bridge.reload).to be_pending
    end

    it "updates bridge's user" do
      execute_service

      expect(bridge.reload.user).to eq(user)
    end

    it 'enqueues Ci::CreateDownstreamPipelineWorker' do
      expect(::Ci::CreateDownstreamPipelineWorker).to receive(:perform_async).with(bridge.id)

      execute_service
    end

    context 'when a subsequent job is skipped' do
      let!(:job) { create(:ci_build, :skipped, pipeline: pipeline, stage_idx: bridge.stage_idx + 1) }

      before do
        create(:ci_build_need, build: job, name: bridge.name)
      end

      it 'marks the subsequent job as processable' do
        expect { execute_service }.to change { job.reload.status }.from('skipped').to('created')
      end
    end

    context 'when bridge is not playable' do
      let(:bridge) { create(:ci_bridge, :failed, pipeline: pipeline, downstream: downstream_project) }

      it 'raises StateMachines::InvalidTransition' do
        expect { execute_service }.to raise_error StateMachines::InvalidTransition
      end
    end
  end

  context 'when user can not run the bridge' do
    before do
      allow(instance).to receive(:can?).with(user, :play_job, bridge).and_return(false)
    end

    it 'allows user with developer role to play a bridge' do
      expect { execute_service }.to raise_error Gitlab::Access::AccessDeniedError
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Ci
  # This class loops through all builds with exposed artifacts and returns
  # basic information about exposed artifacts for given jobs for the frontend
  # to display them as custom links in the merge request.
  #
  # This service must be used with care.
  # Looking for exposed artifacts is very slow and should be done asynchronously.
  class FindExposedArtifactsService < ::BaseService
    include Gitlab::Routing

    MAX_EXPOSED_ARTIFACTS = 10

    def for_pipeline(pipeline, limit: MAX_EXPOSED_ARTIFACTS)
      results = []

      pipeline.builds.latest.with_exposed_artifacts.find_each do |job|
        if job_exposed_artifacts = for_job(job)
          results << job_exposed_artifacts
        end

        break if results.size >= limit
      end

      results
    end

    def for_job(job)
      return unless job.has_exposed_artifacts?

      metadata_entries = first_2_metadata_entries_for_artifacts_paths(job)
      return if metadata_entries.empty?

      {
        text: job.artifacts_expose_as,
        url: path_for_entries(metadata_entries, job),
        job_path: project_job_path(job.project, job),
        job_name: job.name
      }
    end

    private

    # we don't need to fetch all artifacts entries for a job because
    # it could contain many. We only need to know whether it has 1 or more
    # artifacts, so fetching the first 2 would be sufficient.
    def first_2_metadata_entries_for_artifacts_paths(job)
      return [] unless job.artifacts_metadata

      job.artifacts_paths
        .lazy
        .map { |path| job.artifacts_metadata_entry(path, recursive: true) }
        .select { |entry| entry.exists? }
        .first(2)
    end

    def path_for_entries(entries, job)
      return if entries.empty?

      if single_artifact?(entries)
        file_project_job_artifacts_path(job.project, job, entries.first.path)
      else
        browse_project_job_artifacts_path(job.project, job)
      end
    end

    def single_artifact?(entries)
      entries.size == 1 && entries.first.file?
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::FindExposedArtifactsService, feature_category: :build_artifacts do
  include Gitlab::Routing

  let(:metadata) do
    Gitlab::Ci::Build::Artifacts::Metadata
      .new(metadata_file_stream, path, { recursive: true })
  end

  let(:metadata_file_stream) do
    File.open(Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz')
  end

  let_it_be(:project) { create(:project) }

  let(:user) { nil }

  after do
    metadata_file_stream&.close
  end

  def create_job_with_artifacts(options)
    create(:ci_build, pipeline: pipeline, options: options).tap do |job|
      create(:ci_job_artifact, :metadata, job: job)
    end
  end

  describe '#for_pipeline' do
    shared_examples 'finds a single match' do
      it 'returns the artifact with exact location' do
        expect(subject).to eq([{
          text: 'Exposed artifact',
          url: file_project_job_artifacts_path(project, job, 'other_artifacts_0.1.2/doc_sample.txt'),
          job_name: job.name,
          job_path: project_job_path(project, job)
        }])
      end
    end

    shared_examples 'finds multiple matches' do
      it 'returns the path to the artifacts browser' do
        expect(subject).to eq([{
          text: 'Exposed artifact',
          url: browse_project_job_artifacts_path(project, job),
          job_name: job.name,
          job_path: project_job_path(project, job)
        }])
      end
    end

    shared_examples 'does not find any matches' do
      it 'returns empty array' do
        expect(subject).to eq []
      end
    end

    let_it_be(:pipeline) { create(:ci_pipeline, project: project) }

    subject { described_class.new(project, user).for_pipeline(pipeline) }

    context 'with jobs having no exposed artifacts' do
      let!(:job) do
        create_job_with_artifacts(artifacts: {
          paths: ['other_artifacts_0.1.2/doc_sample.txt', 'something-else.html']
        })
      end

      it_behaves_like 'does not find any matches'
    end

    context 'with jobs having no artifacts (metadata)' do
      let!(:job) do
        create(:ci_build, pipeline: pipeline, options: {
          artifacts: {
            expose_as: 'Exposed artifact',
            paths: ['other_artifacts_0.1.2/doc_sample.txt', 'something-else.html']
          }
        })
      end

      it_behaves_like 'does not find any matches'
    end

    context 'with jobs having at most 1 matching exposed artifact' do
      let!(:job) do
        create_job_with_artifacts(artifacts: {
          expose_as: 'Exposed artifact',
          paths: ['other_artifacts_0.1.2/doc_sample.txt', 'something-else.html']
        })
      end

      it_behaves_like 'finds a single match'
    end

    context 'with jobs having more than 1 matching exposed artifacts' do
      let!(:job) do
        create_job_with_artifacts(artifacts: {
          expose_as: 'Exposed artifact',
          paths: [
            'ci_artifacts.txt',
            'other_artifacts_0.1.2/doc_sample.txt',
            'something-else.html'
          ]
        })
      end

      it_behaves_like 'finds multiple matches'
    end

    context 'with jobs having more than 1 matching exposed artifacts inside a directory' do
      let!(:job) do
        create_job_with_artifacts(artifacts: {
          expose_as: 'Exposed artifact',
          paths: ['tests_encoding/']
        })
      end

      it_behaves_like 'finds multiple matches'
    end

    context 'with jobs having paths with glob expression' do
      let!(:job) do
        create_job_with_artifacts(artifacts: {
          expose_as: 'Exposed artifact',
          paths: ['other_artifacts_0.1.2/doc_sample.txt', 'tests_encoding/*.*']
        })
      end

      it_behaves_like 'finds a single match' # because those with * are ignored
    end

    context 'limiting results' do
      let!(:job1) do
        create_job_with_artifacts(artifacts: {
          expose_as: 'artifact 1',
          paths: ['ci_artifacts.txt']
        })
      end

      let!(:job2) do
        create_job_with_artifacts(artifacts: {
          expose_as: 'artifact 2',
          paths: ['tests_encoding/']
        })
      end

      let!(:job3) do
        create_job_with_artifacts(artifacts: {
          expose_as: 'should not be exposed',
          paths: ['other_artifacts_0.1.2/doc_sample.txt']
        })
      end

      subject { described_class.new(project, user).for_pipeline(pipeline, limit: 2) }

      it 'returns first 2 results' do
        expect(subject).to eq(
          [
            {
              text: 'artifact 1',
              url: file_project_job_artifacts_path(project, job1, 'ci_artifacts.txt'),
              job_name: job1.name,
              job_path: project_job_path(project, job1)
            },
            {
              text: 'artifact 2',
              url: browse_project_job_artifacts_path(project, job2),
              job_name: job2.name,
              job_path: project_job_path(project, job2)
            }
          ])
      end
    end

    context 'cross-project MR' do
      let!(:foreign_project) { create(:project) }
      let!(:pipeline) { create(:ci_pipeline, project: foreign_project) }

      let!(:job_show) do
        create_job_with_artifacts({
          artifacts: {
            expose_as: 'file artifact',
            paths: ['ci_artifacts.txt']
          }
        })
      end

      let!(:job_browse) do
        create_job_with_artifacts({
          artifacts: {
            expose_as: 'directory artifact',
            paths: ['tests_encoding/']
          }
        })
      end

      subject { described_class.new(project, user).for_pipeline(pipeline, limit: 2) }

      it 'returns the correct path for cross-project MRs' do
        expect(subject).to eq(
          [
            {
              text: 'file artifact',
              url: file_project_job_artifacts_path(foreign_project, job_show, 'ci_artifacts.txt'),
              job_name: job_show.name,
              job_path: project_job_path(foreign_project, job_show)
            },
            {
              text: 'directory artifact',
              url: browse_project_job_artifacts_path(foreign_project, job_browse),
              job_name: job_browse.name,
              job_path: project_job_path(foreign_project, job_browse)
            }
          ])
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Ci
  class UpdatePendingBuildService
    VALID_PARAMS = %i[instance_runners_enabled namespace_id namespace_traversal_ids].freeze

    InvalidParamsError = Class.new(StandardError)
    InvalidModelError = Class.new(StandardError)

    def initialize(model, update_params)
      @model = model
      @update_params = update_params.symbolize_keys

      validations!
    end

    def execute
      @model.pending_builds.each_batch do |relation|
        relation.update_all(@update_params)
      end
    end

    private

    def validations!
      validate_model! && validate_params!
    end

    def validate_model!
      raise InvalidModelError unless @model.is_a?(::Project) || @model.is_a?(::Group)

      true
    end

    def validate_params!
      extra_params = @update_params.keys - VALID_PARAMS

      raise InvalidParamsError, "Unvalid params: #{extra_params.join(', ')}" unless extra_params.empty?

      true
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::UpdatePendingBuildService, feature_category: :continuous_integration do
  let_it_be(:group) { create(:group) }
  let_it_be(:project) { create(:project, namespace: group) }
  let_it_be_with_reload(:pending_build_1) { create(:ci_pending_build, project: project, instance_runners_enabled: false) }
  let_it_be_with_reload(:pending_build_2) { create(:ci_pending_build, project: project, instance_runners_enabled: true) }
  let_it_be(:update_params) { { instance_runners_enabled: true } }

  let(:service) { described_class.new(model, update_params) }

  describe '#execute' do
    subject(:update_pending_builds) { service.execute }

    context 'validations' do
      context 'when model is invalid' do
        let(:model) { pending_build_1 }

        it 'raises an error' do
          expect { update_pending_builds }.to raise_error(described_class::InvalidModelError)
        end
      end

      context 'when params is invalid' do
        let(:model) { group }
        let(:update_params) { { minutes_exceeded: true } }

        it 'raises an error' do
          expect { update_pending_builds }.to raise_error(described_class::InvalidParamsError)
        end
      end
    end

    context 'when model is a group with pending builds' do
      let(:model) { group }

      it 'updates all pending builds', :aggregate_failures do
        update_pending_builds

        expect(pending_build_1.instance_runners_enabled).to be_truthy
        expect(pending_build_2.instance_runners_enabled).to be_truthy
      end
    end

    context 'when model is a project with pending builds' do
      let(:model) { project }

      it 'updates all pending builds', :aggregate_failures do
        update_pending_builds

        expect(pending_build_1.instance_runners_enabled).to be_truthy
        expect(pending_build_2.instance_runners_enabled).to be_truthy
      end
    end
  end
end