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

module Ci
  # Takes in input a Ci::Bridge job and creates a downstream pipeline
  # (either multi-project or child pipeline) according to the Ci::Bridge
  # specifications.
  class CreateDownstreamPipelineService < ::BaseService
    include Gitlab::Utils::StrongMemoize
    include Ci::DownstreamPipelineHelpers

    DuplicateDownstreamPipelineError = Class.new(StandardError)

    MAX_NESTED_CHILDREN = 2

    def execute(bridge)
      @bridge = bridge

      if @bridge.has_downstream_pipeline?
        Gitlab::ErrorTracking.track_exception(
          DuplicateDownstreamPipelineError.new,
          bridge_id: @bridge.id, project_id: @bridge.project_id
        )

        return ServiceResponse.error(message: 'Already has a downstream pipeline')
      end

      pipeline_params = @bridge.downstream_pipeline_params
      target_ref = pipeline_params.dig(:target_revision, :ref)

      return ServiceResponse.error(message: 'Pre-conditions not met') unless ensure_preconditions!(target_ref)

      return ServiceResponse.error(message: 'Can not run the bridge') unless @bridge.run

      service = ::Ci::CreatePipelineService.new(
        pipeline_params.fetch(:project),
        current_user,
        pipeline_params.fetch(:target_revision))

      downstream_pipeline = service
        .execute(pipeline_params.fetch(:source), **pipeline_params[:execute_params])
        .payload

      log_downstream_pipeline_creation(downstream_pipeline)
      update_bridge_status!(@bridge, downstream_pipeline)
    rescue StandardError => e
      @bridge.reset.drop!(:data_integrity_failure)
      raise e
    end

    private

    def update_bridge_status!(bridge, pipeline)
      Gitlab::OptimisticLocking.retry_lock(bridge, name: 'create_downstream_pipeline_update_bridge_status') do |subject|
        if pipeline.created_successfully?
          # If bridge uses `strategy:depend` we leave it running
          # and update the status when the downstream pipeline completes.
          subject.success! unless subject.dependent?
          ServiceResponse.success(payload: pipeline)
        else
          message = pipeline.errors.full_messages
          subject.options[:downstream_errors] = message
          subject.drop!(:downstream_pipeline_creation_failed)
          ServiceResponse.error(payload: pipeline, message: message)
        end
      end
    rescue StateMachines::InvalidTransition => e
      Gitlab::ErrorTracking.track_exception(
        Ci::Bridge::InvalidTransitionError.new(e.message),
        bridge_id: bridge.id,
        downstream_pipeline_id: pipeline.id)
      ServiceResponse.error(payload: pipeline, message: e.message)
    end

    def ensure_preconditions!(target_ref)
      unless downstream_project_accessible?
        @bridge.drop!(:downstream_bridge_project_not_found)
        return false
      end

      # TODO: Remove this condition if favour of model validation
      # https://gitlab.com/gitlab-org/gitlab/issues/38338
      if downstream_project == project && !@bridge.triggers_child_pipeline?
        @bridge.drop!(:invalid_bridge_trigger)
        return false
      end

      # TODO: Remove this condition if favour of model validation
      # https://gitlab.com/gitlab-org/gitlab/issues/38338
      # only applies to parent-child pipelines not multi-project
      if has_max_nested_children?
        @bridge.drop!(:reached_max_descendant_pipelines_depth)
        return false
      end

      if pipeline_tree_too_large?
        @bridge.drop!(:reached_max_pipeline_hierarchy_size)
        return false
      end

      unless can_create_downstream_pipeline?(target_ref)
        @bridge.drop!(:insufficient_bridge_permissions)
        return false
      end

      if has_cyclic_dependency?
        @bridge.drop!(:pipeline_loop_detected)

        return false
      end

      true
    end

    def downstream_project_accessible?
      downstream_project.present? &&
        can?(current_user, :read_project, downstream_project)
    end

    def can_create_downstream_pipeline?(target_ref)
      can?(current_user, :update_pipeline, project) &&
        can?(current_user, :create_pipeline, downstream_project) &&
          can_update_branch?(target_ref)
    end

    def can_update_branch?(target_ref)
      ::Gitlab::UserAccess.new(current_user, container: downstream_project).can_update_branch?(target_ref)
    end

    def downstream_project
      strong_memoize(:downstream_project) do
        @bridge.downstream_project
      end
    end

    def has_cyclic_dependency?
      return false if @bridge.triggers_child_pipeline?

      pipeline_checksums = @bridge.pipeline.self_and_upstreams.filter_map do |pipeline|
        config_checksum(pipeline) unless pipeline.child?
      end

      # To avoid false positives we allow 1 cycle in the ancestry and
      # fail when 2 cycles are detected: A -> B -> A -> B -> A
      pipeline_checksums.tally.any? { |_checksum, occurrences| occurrences > 2 }
    end

    def has_max_nested_children?
      return false unless @bridge.triggers_child_pipeline?

      # only applies to parent-child pipelines not multi-project
      ancestors_of_new_child = @bridge.pipeline.self_and_project_ancestors
      ancestors_of_new_child.count > MAX_NESTED_CHILDREN
    end

    def pipeline_tree_too_large?
      return false unless @bridge.triggers_downstream_pipeline?

      # Applies to the entire pipeline tree across all projects
      # A pipeline tree can be shared between multiple namespaces (customers), the limit that is used here
      # is the limit of the namespace that has added a downstream pipeline to a pipeline tree.
      @bridge.project.actual_limits.exceeded?(:pipeline_hierarchy_size, complete_hierarchy_count)
    end

    def complete_hierarchy_count
      @bridge.pipeline.complete_hierarchy_count
    end

    def config_checksum(pipeline)
      [pipeline.project_id, pipeline.ref, pipeline.source].hash
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::CreateDownstreamPipelineService, '#execute', feature_category: :continuous_integration do
  include Ci::SourcePipelineHelpers

  # Using let_it_be on user and projects for these specs can cause
  # spec-ordering failures due to the project-based permissions
  # associating them. They should be recreated every time.
  let(:user) { create(:user) }
  let(:upstream_project) { create(:project, :repository) }
  let(:downstream_project) { create(:project, :repository) }

  let!(:upstream_pipeline) do
    create(:ci_pipeline, :created, project: upstream_project)
  end

  let(:trigger) do
    {
      trigger: {
        project: downstream_project.full_path,
        branch: 'feature'
      }
    }
  end

  let(:bridge) do
    create(
      :ci_bridge,
      status: :pending,
      user: user,
      options: trigger,
      pipeline: upstream_pipeline
    )
  end

  let(:service) { described_class.new(upstream_project, user) }
  let(:pipeline) { subject.payload }

  before do
    upstream_project.add_developer(user)
  end

  subject { service.execute(bridge) }

  context 'when downstream project has not been found' do
    let(:trigger) do
      { trigger: { project: 'unknown/project' } }
    end

    it 'does not create a pipeline' do
      expect { subject }
        .not_to change { Ci::Pipeline.count }
      expect(subject).to be_error
      expect(subject.message).to eq("Pre-conditions not met")
    end

    it 'changes pipeline bridge job status to failed' do
      subject

      expect(bridge.reload).to be_failed
      expect(bridge.failure_reason)
        .to eq 'downstream_bridge_project_not_found'
    end
  end

  context 'when user can not access downstream project' do
    it 'does not create a new pipeline' do
      expect { subject }
        .not_to change { Ci::Pipeline.count }
      expect(subject).to be_error
      expect(subject.message).to eq("Pre-conditions not met")
    end

    it 'changes status of the bridge build to failed' do
      subject

      expect(bridge.reload).to be_failed
      expect(bridge.failure_reason)
        .to eq 'downstream_bridge_project_not_found'
    end
  end

  context 'when user does not have access to create pipeline' do
    before do
      downstream_project.add_guest(user)
    end

    it 'does not create a new pipeline' do
      expect { subject }
        .not_to change { Ci::Pipeline.count }
      expect(subject).to be_error
      expect(subject.message).to eq("Pre-conditions not met")
    end

    it 'changes status of the bridge build to failed' do
      subject

      expect(bridge.reload).to be_failed
      expect(bridge.failure_reason).to eq 'insufficient_bridge_permissions'
    end
  end

  context 'when user can create pipeline in a downstream project' do
    let(:stub_config) { true }

    before do
      downstream_project.add_developer(user)
      stub_ci_pipeline_yaml_file(YAML.dump(rspec: { script: 'rspec' })) if stub_config
    end

    it 'creates only one new pipeline' do
      expect { subject }
        .to change { Ci::Pipeline.count }.by(1)
      expect(subject).to be_success
    end

    it 'creates a new pipeline in a downstream project' do
      expect(pipeline.user).to eq bridge.user
      expect(pipeline.project).to eq downstream_project
      expect(bridge.reload.sourced_pipeline.pipeline).to eq pipeline
      expect(pipeline.triggered_by_pipeline).to eq upstream_pipeline
      expect(pipeline.source_bridge).to eq bridge
      expect(pipeline.source_bridge).to be_a ::Ci::Bridge
    end

    it_behaves_like 'logs downstream pipeline creation' do
      let(:downstream_pipeline) { pipeline }
      let(:expected_root_pipeline) { upstream_pipeline }
      let(:expected_hierarchy_size) { 2 }
      let(:expected_downstream_relationship) { :multi_project }
    end

    it 'updates bridge status when downstream pipeline gets processed' do
      expect(pipeline.reload).to be_created
      expect(bridge.reload).to be_success
    end

    it 'triggers the upstream pipeline duration calculation', :sidekiq_inline do
      expect { subject }
        .to change { upstream_pipeline.reload.duration }.from(nil).to(an_instance_of(Integer))
    end

    context 'when bridge job has already any downstream pipeline' do
      before do
        bridge.create_sourced_pipeline!(
          source_pipeline: bridge.pipeline,
          source_project: bridge.project,
          project: bridge.project,
          pipeline: create(:ci_pipeline, project: bridge.project)
        )
      end

      it 'logs an error and exits' do
        expect(Gitlab::ErrorTracking)
          .to receive(:track_exception)
          .with(
            instance_of(described_class::DuplicateDownstreamPipelineError),
            bridge_id: bridge.id, project_id: bridge.project.id)
          .and_call_original
        expect(Ci::CreatePipelineService).not_to receive(:new)
        expect(subject).to be_error
        expect(subject.message).to eq("Already has a downstream pipeline")
      end
    end

    context 'when target ref is not specified' do
      let(:trigger) do
        { trigger: { project: downstream_project.full_path } }
      end

      it 'is using default branch name' do
        expect(pipeline.ref).to eq 'master'
      end
    end

    context 'when downstream pipeline has yaml configuration error' do
      before do
        stub_ci_pipeline_yaml_file(YAML.dump(job: { invalid: 'yaml' }))
      end

      it 'creates only one new pipeline' do
        expect { subject }
          .to change { Ci::Pipeline.count }.by(1)
        expect(subject).to be_error
        expect(subject.message).to match_array(["jobs job config should implement a script: or a trigger: keyword"])
      end

      it 'creates a new pipeline in a downstream project' do
        expect(pipeline.user).to eq bridge.user
        expect(pipeline.project).to eq downstream_project
        expect(bridge.reload.sourced_pipeline.pipeline).to eq pipeline
        expect(pipeline.triggered_by_pipeline).to eq upstream_pipeline
        expect(pipeline.source_bridge).to eq bridge
        expect(pipeline.source_bridge).to be_a ::Ci::Bridge
      end

      it 'updates the bridge status when downstream pipeline gets processed' do
        expect(pipeline.reload).to be_failed
        expect(bridge.reload).to be_failed
      end
    end

    context 'when downstream project is the same as the upstream project' do
      let(:trigger) do
        { trigger: { project: upstream_project.full_path } }
      end

      context 'detects a circular dependency' do
        it 'does not create a new pipeline' do
          expect { subject }
            .not_to change { Ci::Pipeline.count }
          expect(subject).to be_error
          expect(subject.message).to eq("Pre-conditions not met")
        end

        it 'changes status of the bridge build' do
          subject

          expect(bridge.reload).to be_failed
          expect(bridge.failure_reason).to eq 'invalid_bridge_trigger'
        end
      end

      context 'when "include" is provided' do
        let(:file_content) do
          YAML.dump(
            rspec: { script: 'rspec' },
            echo: { script: 'echo' })
        end

        shared_examples 'creates a child pipeline' do
          it 'creates only one new pipeline' do
            expect { subject }
              .to change { Ci::Pipeline.count }.by(1)
            expect(subject).to be_success
          end

          it 'creates a child pipeline in the same project' do
            expect(pipeline.builds.map(&:name)).to match_array(%w[rspec echo])
            expect(pipeline.user).to eq bridge.user
            expect(pipeline.project).to eq bridge.project
            expect(bridge.reload.sourced_pipeline.pipeline).to eq pipeline
            expect(pipeline.triggered_by_pipeline).to eq upstream_pipeline
            expect(pipeline.source_bridge).to eq bridge
            expect(pipeline.source_bridge).to be_a ::Ci::Bridge
          end

          it 'updates bridge status when downstream pipeline gets processed' do
            expect(pipeline.reload).to be_created
            expect(bridge.reload).to be_success
          end

          it 'propagates parent pipeline settings to the child pipeline' do
            expect(pipeline.ref).to eq(upstream_pipeline.ref)
            expect(pipeline.sha).to eq(upstream_pipeline.sha)
            expect(pipeline.source_sha).to eq(upstream_pipeline.source_sha)
            expect(pipeline.target_sha).to eq(upstream_pipeline.target_sha)
            expect(pipeline.target_sha).to eq(upstream_pipeline.target_sha)

            expect(pipeline.trigger_requests.last).to eq(bridge.trigger_request)
          end
        end

        before do
          upstream_project.repository.create_file(
            user, 'child-pipeline.yml', file_content, message: 'message', branch_name: 'master')

          upstream_pipeline.update!(sha: upstream_project.commit.id)
        end

        let(:stub_config) { false }

        let(:trigger) do
          {
            trigger: { include: 'child-pipeline.yml' }
          }
        end

        it_behaves_like 'creates a child pipeline'

        it_behaves_like 'logs downstream pipeline creation' do
          let(:downstream_pipeline) { pipeline }
          let(:expected_root_pipeline) { upstream_pipeline }
          let(:expected_hierarchy_size) { 2 }
          let(:expected_downstream_relationship) { :parent_child }
        end

        it 'updates the bridge job to success' do
          expect { subject }.to change { bridge.status }.to 'success'
          expect(subject).to be_success
        end

        context 'when bridge uses "depend" strategy' do
          let(:trigger) do
            {
              trigger: { include: 'child-pipeline.yml', strategy: 'depend' }
            }
          end

          it 'update the bridge job to running status' do
            expect { subject }.to change { bridge.status }.from('pending').to('running')
            expect(subject).to be_success
          end
        end

        context 'when latest sha for the ref changed in the meantime' do
          before do
            upstream_project.repository.create_file(
              user, 'another-change', 'test', message: 'message', branch_name: 'master')
          end

          # it does not auto-cancel pipelines from the same family
          it_behaves_like 'creates a child pipeline'
        end

        context 'when the parent is a merge request pipeline' do
          let(:merge_request) { create(:merge_request, source_project: bridge.project, target_project: bridge.project) }
          let(:file_content) do
            YAML.dump(
              workflow: { rules: [{ if: '$CI_MERGE_REQUEST_ID' }] },
              rspec: { script: 'rspec' },
              echo: { script: 'echo' })
          end

          before do
            bridge.pipeline.update!(source: :merge_request_event, merge_request: merge_request)
          end

          it_behaves_like 'creates a child pipeline'

          it 'propagates the merge request to the child pipeline' do
            expect(pipeline.merge_request).to eq(merge_request)
            expect(pipeline).to be_merge_request
          end
        end

        context 'when upstream pipeline has a parent pipeline' do
          before do
            create(:ci_sources_pipeline,
              source_pipeline: create(:ci_pipeline, project: upstream_pipeline.project),
              pipeline: upstream_pipeline
            )
          end

          it 'creates the pipeline' do
            expect { subject }
              .to change { Ci::Pipeline.count }.by(1)
            expect(subject).to be_success

            expect(bridge.reload).to be_success
          end

          it_behaves_like 'logs downstream pipeline creation' do
            let(:downstream_pipeline) { pipeline }
            let(:expected_root_pipeline) { upstream_pipeline.parent_pipeline }
            let(:expected_hierarchy_size) { 3 }
            let(:expected_downstream_relationship) { :parent_child }
          end
        end

        context 'when upstream pipeline has a parent pipeline, which has a parent pipeline' do
          before do
            parent_of_upstream_pipeline = create(:ci_pipeline, project: upstream_pipeline.project)

            create(:ci_sources_pipeline,
              source_pipeline: create(:ci_pipeline, project: upstream_pipeline.project),
              pipeline: parent_of_upstream_pipeline
            )

            create(:ci_sources_pipeline,
              source_pipeline: parent_of_upstream_pipeline,
              pipeline: upstream_pipeline
            )
          end

          it 'does not create a second descendant pipeline' do
            expect { subject }
              .not_to change { Ci::Pipeline.count }

            expect(bridge.reload).to be_failed
            expect(bridge.failure_reason).to eq 'reached_max_descendant_pipelines_depth'
          end
        end

        context 'when upstream pipeline has two level upstream pipelines from different projects' do
          before do
            upstream_of_upstream_of_upstream_pipeline = create(:ci_pipeline)
            upstream_of_upstream_pipeline = create(:ci_pipeline)

            create(:ci_sources_pipeline,
              source_pipeline: upstream_of_upstream_of_upstream_pipeline,
              pipeline: upstream_of_upstream_pipeline
            )

            create(:ci_sources_pipeline,
              source_pipeline: upstream_of_upstream_pipeline,
              pipeline: upstream_pipeline
            )
          end

          it 'create the pipeline' do
            expect { subject }.to change { Ci::Pipeline.count }.by(1)
            expect(subject).to be_success
          end
        end

        context 'when downstream project does not allow user-defined variables for child pipelines' do
          before do
            bridge.yaml_variables = [{ key: 'BRIDGE', value: '$PIPELINE_VARIABLE-var', public: true }]

            upstream_pipeline.project.update!(restrict_user_defined_variables: true)
          end

          it 'creates a new pipeline allowing variables to be passed downstream' do
            expect { subject }.to change { Ci::Pipeline.count }.by(1)
            expect(subject).to be_success
          end

          it 'passes variables downstream from the bridge' do
            pipeline.variables.map(&:key).tap do |variables|
              expect(variables).to include 'BRIDGE'
            end
          end
        end

        context 'when multi-project pipeline runs from child pipelines bridge job' do
          before do
            stub_ci_pipeline_yaml_file(YAML.dump(rspec: { script: 'rspec' }))
          end

          # instantiate new service, to clear memoized values from child pipeline run
          subject(:execute_with_trigger_project_bridge) do
            described_class.new(upstream_project, user).execute(trigger_project_bridge)
          end

          let!(:child_pipeline) do
            service.execute(bridge)
            bridge.downstream_pipeline
          end

          let!(:trigger_downstream_project) do
            {
              trigger: {
                project: downstream_project.full_path,
                branch: 'feature'
              }
            }
          end

          let!(:trigger_project_bridge) do
            create(
              :ci_bridge, status: :pending, user: user, options: trigger_downstream_project, pipeline: child_pipeline
            )
          end

          it 'creates a new pipeline' do
            expect { execute_with_trigger_project_bridge }
              .to change { Ci::Pipeline.count }.by(1)

            new_pipeline = trigger_project_bridge.downstream_pipeline

            expect(new_pipeline.child?).to eq(false)
            expect(new_pipeline.triggered_by_pipeline).to eq child_pipeline
            expect(trigger_project_bridge.reload).not_to be_failed
          end
        end
      end
    end

    describe 'cyclical dependency detection' do
      shared_examples 'detects cyclical pipelines' do
        it 'does not create a new pipeline' do
          expect { subject }
            .not_to change { Ci::Pipeline.count }
          expect(subject).to be_error
          expect(subject.message).to eq("Pre-conditions not met")
        end

        it 'changes status of the bridge build' do
          subject

          expect(bridge.reload).to be_failed
          expect(bridge.failure_reason).to eq 'pipeline_loop_detected'
        end
      end

      shared_examples 'passes cyclical pipeline precondition' do
        it 'creates a new pipeline' do
          expect { subject }
            .to change { Ci::Pipeline.count }
          expect(subject).to be_success
        end

        it 'expect bridge build not to be failed' do
          subject

          expect(bridge.reload).not_to be_failed
        end
      end

      context 'when pipeline ancestry contains 2 cycles of dependencies' do
        before do
          # A(push on master) -> B(pipeline on master) -> A(push on master) ->
          #   B(pipeline on master) -> A(push on master)
          pipeline_1 = create(:ci_pipeline, project: upstream_project, source: :push)
          pipeline_2 = create(:ci_pipeline, project: downstream_project, source: :pipeline)
          pipeline_3 = create(:ci_pipeline, project: upstream_project, source: :push)
          pipeline_4 = create(:ci_pipeline, project: downstream_project, source: :pipeline)

          create_source_pipeline(pipeline_1, pipeline_2)
          create_source_pipeline(pipeline_2, pipeline_3)
          create_source_pipeline(pipeline_3, pipeline_4)
          create_source_pipeline(pipeline_4, upstream_pipeline)
        end

        it_behaves_like 'detects cyclical pipelines'
      end

      context 'when source in the ancestry differ' do
        before do
          # A(push on master) -> B(pipeline on master) -> A(pipeline on master)
          pipeline_1 = create(:ci_pipeline, project: upstream_project, source: :push)
          pipeline_2 = create(:ci_pipeline, project: downstream_project, source: :pipeline)
          upstream_pipeline.update!(source: :pipeline)

          create_source_pipeline(pipeline_1, pipeline_2)
          create_source_pipeline(pipeline_2, upstream_pipeline)
        end

        it_behaves_like 'passes cyclical pipeline precondition'
      end

      context 'when ref in the ancestry differ' do
        before do
          # A(push on master) -> B(pipeline on master) -> A(push on feature-1)
          pipeline_1 = create(:ci_pipeline, ref: 'master', project: upstream_project, source: :push)
          pipeline_2 = create(:ci_pipeline, ref: 'master', project: downstream_project, source: :pipeline)
          upstream_pipeline.update!(ref: 'feature-1')

          create_source_pipeline(pipeline_1, pipeline_2)
          create_source_pipeline(pipeline_2, upstream_pipeline)
        end

        it_behaves_like 'passes cyclical pipeline precondition'
      end

      context 'when only 1 cycle is detected' do
        before do
          # A(push on master) -> B(pipeline on master) -> A(push on master)
          pipeline_1 = create(:ci_pipeline, ref: 'master', project: upstream_project, source: :push)
          pipeline_2 = create(:ci_pipeline, ref: 'master', project: downstream_project, source: :pipeline)

          create_source_pipeline(pipeline_1, pipeline_2)
          create_source_pipeline(pipeline_2, upstream_pipeline)
        end

        it_behaves_like 'passes cyclical pipeline precondition'
      end
    end

    context 'when downstream pipeline creation errors out' do
      let(:stub_config) { false }

      before do
        stub_ci_pipeline_yaml_file(YAML.dump(invalid: { yaml: 'error' }))
      end

      it 'creates only one new pipeline' do
        expect { subject }
          .to change { Ci::Pipeline.count }.by(1)
        expect(subject).to be_error
        expect(subject.message).to match_array(["jobs invalid config should implement a script: or a trigger: keyword"])
      end

      it 'creates a new pipeline in the downstream project' do
        expect(pipeline.user).to eq bridge.user
        expect(pipeline.project).to eq downstream_project
      end

      it 'drops the bridge' do
        expect(pipeline.reload).to be_failed
        expect(bridge.reload).to be_failed
        expect(bridge.failure_reason).to eq('downstream_pipeline_creation_failed')
      end
    end

    context 'when bridge job status update raises state machine errors' do
      let(:stub_config) { false }

      before do
        stub_ci_pipeline_yaml_file(YAML.dump(invalid: { yaml: 'error' }))
        bridge.drop!
      end

      it 'returns the error' do
        expect { subject }.not_to change(downstream_project.ci_pipelines, :count)
        expect(subject).to be_error
        expect(subject.message).to eq('Can not run the bridge')
      end
    end

    context 'when bridge job has YAML variables defined' do
      before do
        bridge.yaml_variables = [{ key: 'BRIDGE', value: 'var', public: true }]
      end

      it 'passes bridge variables to downstream pipeline' do
        expect(pipeline.variables.first)
          .to have_attributes(key: 'BRIDGE', value: 'var')
      end
    end

    context 'when pipeline variables are defined' do
      before do
        upstream_pipeline.variables.create!(key: 'PIPELINE_VARIABLE', value: 'my-value')
      end

      it 'does not pass pipeline variables directly downstream' do
        pipeline.variables.map(&:key).tap do |variables|
          expect(variables).not_to include 'PIPELINE_VARIABLE'
        end
      end

      context 'when using YAML variables interpolation' do
        before do
          bridge.yaml_variables = [{ key: 'BRIDGE', value: '$PIPELINE_VARIABLE-var', public: true }]
        end

        it 'makes it possible to pass pipeline variable downstream' do
          pipeline.variables.find_by(key: 'BRIDGE').tap do |variable|
            expect(variable.value).to eq 'my-value-var'
          end
        end

        context 'when downstream project does not allow user-defined variables for multi-project pipelines' do
          before do
            downstream_project.update!(restrict_user_defined_variables: true)
          end

          it 'does not create a new pipeline' do
            expect { subject }
              .not_to change { Ci::Pipeline.count }
            expect(subject).to be_error
            expect(subject.message).to match_array(["Insufficient permissions to set pipeline variables"])
          end

          it 'ignores variables passed downstream from the bridge' do
            pipeline.variables.map(&:key).tap do |variables|
              expect(variables).not_to include 'BRIDGE'
            end
          end

          it 'sets errors', :aggregate_failures do
            subject

            expect(bridge.reload).to be_failed
            expect(bridge.failure_reason).to eq('downstream_pipeline_creation_failed')
            expect(bridge.options[:downstream_errors]).to eq(['Insufficient permissions to set pipeline variables'])
          end
        end
      end
    end

    # TODO: Move this context into a feature spec that uses
    # multiple pipeline processing services. Location TBD in:
    # https://gitlab.com/gitlab-org/gitlab/issues/36216
    context 'when configured with bridge job rules', :sidekiq_inline do
      before do
        stub_ci_pipeline_yaml_file(config)
        downstream_project.add_maintainer(upstream_project.first_owner)
      end

      let(:config) do
        <<-EOY
          hello:
            script: echo world

          bridge-job:
            rules:
              - if: $CI_COMMIT_REF_NAME == "master"
            trigger:
              project: #{downstream_project.full_path}
              branch: master
        EOY
      end

      let(:primary_pipeline) do
        Ci::CreatePipelineService.new(upstream_project, upstream_project.first_owner, { ref: 'master' })
          .execute(:push, save_on_errors: false)
          .payload
      end

      let(:bridge)  { primary_pipeline.processables.find_by(name: 'bridge-job') }
      let(:service) { described_class.new(upstream_project, upstream_project.first_owner) }

      context 'that include the bridge job' do
        it 'creates the downstream pipeline' do
          expect { subject }
            .to change(downstream_project.ci_pipelines, :count).by(1)
          expect(subject).to be_error
          expect(subject.message).to eq("Already has a downstream pipeline")
        end
      end
    end

    context 'when user does not have access to push protected branch of downstream project' do
      before do
        create(:protected_branch, :maintainers_can_push, project: downstream_project, name: 'feature')
      end

      it 'changes status of the bridge build' do
        subject

        expect(bridge.reload).to be_failed
        expect(bridge.failure_reason).to eq 'insufficient_bridge_permissions'
      end
    end

    context 'when there is no such branch in downstream project' do
      let(:trigger) do
        {
          trigger: {
            project: downstream_project.full_path,
            branch: 'invalid_branch'
          }
        }
      end

      it 'does not create a pipeline and drops the bridge' do
        expect { subject }.not_to change(downstream_project.ci_pipelines, :count)
        expect(subject).to be_error
        expect(subject.message).to match_array(["Reference not found"])

        expect(bridge.reload).to be_failed
        expect(bridge.failure_reason).to eq('downstream_pipeline_creation_failed')
        expect(bridge.options[:downstream_errors]).to eq(['Reference not found'])
      end
    end

    context 'when downstream pipeline has a branch rule and does not satisfy' do
      before do
        stub_ci_pipeline_yaml_file(config)
      end

      let(:config) do
        <<-EOY
          hello:
            script: echo world
            only:
              - invalid_branch
        EOY
      end

      it 'does not create a pipeline and drops the bridge' do
        expect { subject }.not_to change(downstream_project.ci_pipelines, :count)
        expect(subject).to be_error
        expect(subject.message).to match_array(['Pipeline will not run for the selected trigger. ' \
          'The rules configuration prevented any jobs from being added to the pipeline.'])

        expect(bridge.reload).to be_failed
        expect(bridge.failure_reason).to eq('downstream_pipeline_creation_failed')
        expect(bridge.options[:downstream_errors]).to match_array(['Pipeline will not run for the selected trigger. ' \
          'The rules configuration prevented any jobs from being added to the pipeline.'])
      end
    end

    context 'when downstream pipeline has invalid YAML' do
      before do
        stub_ci_pipeline_yaml_file(config)
      end

      let(:config) do
        <<-EOY
          test:
            stage: testx
            script: echo 1
        EOY
      end

      it 'creates the pipeline but drops the bridge' do
        expect { subject }.to change(downstream_project.ci_pipelines, :count).by(1)
        expect(subject).to be_error
        expect(subject.message).to eq(
          ["test job: chosen stage does not exist; available stages are .pre, build, test, deploy, .post"]
        )

        expect(bridge.reload).to be_failed
        expect(bridge.failure_reason).to eq('downstream_pipeline_creation_failed')
        expect(bridge.options[:downstream_errors]).to eq(
          ['test job: chosen stage does not exist; available stages are .pre, build, test, deploy, .post']
        )
      end
    end

    context 'when downstream pipeline has workflow rule' do
      before do
        stub_ci_pipeline_yaml_file(config)
      end

      let(:config) do
        <<-EOY
          workflow:
            rules:
              - if: $my_var

          regular-job:
            script: 'echo Hello, World!'
        EOY
      end

      context 'when passing the required variable' do
        before do
          bridge.yaml_variables = [{ key: 'my_var', value: 'var', public: true }]
        end

        it 'creates the pipeline' do
          expect { subject }.to change(downstream_project.ci_pipelines, :count).by(1)
          expect(subject).to be_success

          expect(bridge.reload).to be_success
        end
      end

      context 'when not passing the required variable' do
        it 'does not create the pipeline' do
          expect { subject }.not_to change(downstream_project.ci_pipelines, :count)
        end
      end
    end

    context 'when a downstream pipeline has sibling pipelines' do
      it_behaves_like 'logs downstream pipeline creation' do
        let(:downstream_pipeline) { pipeline }
        let(:expected_root_pipeline) { upstream_pipeline }
        let(:expected_downstream_relationship) { :multi_project }

        # New downstream, plus upstream, plus two children of upstream created below
        let(:expected_hierarchy_size) { 4 }

        before do
          create_list(:ci_pipeline, 2, child_of: upstream_pipeline)
        end
      end
    end

    context 'when the pipeline tree is too large' do
      let_it_be(:parent)     { create(:ci_pipeline) }
      let_it_be(:child)      { create(:ci_pipeline, child_of: parent) }
      let_it_be(:sibling)    { create(:ci_pipeline, child_of: parent) }

      let(:project) { build(:project, :repository) }
      let(:bridge) do
        create(:ci_bridge, status: :pending, user: user, options: trigger, pipeline: child, project: project)
      end

      context 'when limit was specified by admin' do
        before do
          project.actual_limits.update!(pipeline_hierarchy_size: 3)
        end

        it 'does not create a new pipeline' do
          expect { subject }.not_to change { Ci::Pipeline.count }
        end

        it 'drops the trigger job with an explanatory reason' do
          subject

          expect(bridge.reload).to be_failed
          expect(bridge.failure_reason).to eq('reached_max_pipeline_hierarchy_size')
        end
      end

      context 'when there was no limit specified by admin' do
        before do
          allow(bridge.pipeline).to receive(:complete_hierarchy_count).and_return(1000)
        end

        context 'when pipeline count reaches the default limit of 1000' do
          it 'does not create a new pipeline' do
            expect { subject }.not_to change { Ci::Pipeline.count }
            expect(subject).to be_error
            expect(subject.message).to eq("Pre-conditions not met")
          end

          it 'drops the trigger job with an explanatory reason' do
            subject

            expect(bridge.reload).to be_failed
            expect(bridge.failure_reason).to eq('reached_max_pipeline_hierarchy_size')
          end
        end
      end
    end
  end

  context 'when downstream pipeline creation fails with unexpected errors', :aggregate_failures do
    before do
      downstream_project.add_developer(user)

      allow(::Ci::CreatePipelineService).to receive(:new)
        .and_raise(RuntimeError, 'undefined failure')
    end

    it 'drops the bridge without creating a pipeline' do
      expect { subject }
        .to raise_error(RuntimeError, /undefined failure/)
        .and change { Ci::Pipeline.count }.by(0)

      expect(bridge.reload).to be_failed
      expect(bridge.failure_reason).to eq('data_integrity_failure')
    end
  end
end


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

module Ci
  class ArchiveTraceService
    include ::Gitlab::ExclusiveLeaseHelpers

    EXCLUSIVE_LOCK_KEY = 'archive_trace_service:batch_execute:lock'
    LOCK_TIMEOUT = 56.minutes
    LOOP_TIMEOUT = 55.minutes
    LOOP_LIMIT = 2000
    BATCH_SIZE = 100

    # rubocop: disable CodeReuse/ActiveRecord
    def batch_execute(worker_name:)
      start_time = Time.current
      in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do
        Ci::Build.with_stale_live_trace.find_each(batch_size: BATCH_SIZE).with_index do |build, index|
          break if Time.current - start_time > LOOP_TIMEOUT

          if index > LOOP_LIMIT
            Sidekiq.logger.warn(class: worker_name, message: 'Loop limit reached.', job_id: build.id)
            break
          end

          begin
            execute(build, worker_name: worker_name)
          rescue StandardError
            next
          end
        end
      end
    end
    # rubocop: enable CodeReuse/ActiveRecord

    def execute(job, worker_name:)
      unless job.trace.archival_attempts_available?
        Sidekiq.logger.warn(class: worker_name, message: 'The job is out of archival attempts.', job_id: job.id)

        job.trace.attempt_archive_cleanup!
        return
      end

      unless job.trace.can_attempt_archival_now?
        Sidekiq.logger.warn(class: worker_name, message: 'The job can not be archived right now.', job_id: job.id)
        return
      end

      job.trace.archive!
      job.remove_pending_state!

      if job.job_artifacts_trace.present?
        job.project.execute_integrations(Gitlab::DataBuilder::ArchiveTrace.build(job), :archive_trace_hooks)
      end
    rescue ::Gitlab::Ci::Trace::AlreadyArchivedError
      # It's already archived, thus we can safely ignore this exception.
    rescue StandardError => e
      job.trace.increment_archival_attempts!

      # Tracks this error with application logs, Sentry, and Prometheus.
      # If `archive!` keeps failing for over a week, that could incur data loss.
      # (See more https://docs.gitlab.com/ee/administration/job_logs.html#new-incremental-logging-architecture)
      # In order to avoid interrupting the system, we do not raise an exception here.
      archive_error(e, job, worker_name)
    end

    private

    def failed_archive_counter
      @failed_archive_counter ||=
        Gitlab::Metrics.counter(:job_trace_archive_failed_total, "Counter of failed attempts of trace archiving")
    end

    def archive_error(error, job, worker_name)
      failed_archive_counter.increment

      Sidekiq.logger.warn(
        class: worker_name,
        message: "Failed to archive trace. message: #{error.message}.",
        job_id: job.id
      )

      Gitlab::ErrorTracking.track_and_raise_for_dev_exception(
        error,
        issue_url: 'https://gitlab.com/gitlab-org/gitlab-foss/issues/51502',
        job_id: job.id
      )
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::ArchiveTraceService, '#execute', feature_category: :continuous_integration do
  subject { described_class.new.execute(job, worker_name: Ci::ArchiveTraceWorker.name) }

  context 'when job is finished' do
    let(:job) { create(:ci_build, :success, :trace_live) }

    it 'creates an archived trace' do
      expect { subject }.not_to raise_error

      expect(job.reload.job_artifacts_trace).to be_exist
      expect(job.trace_metadata.trace_artifact).to eq(job.job_artifacts_trace)
    end

    context 'integration hooks' do
      it do
        expect(job.project).to receive(:execute_integrations) do |data, hook_type|
          expect(data).to eq Gitlab::DataBuilder::ArchiveTrace.build(job)
          expect(hook_type).to eq :archive_trace_hooks
        end
        expect { subject }.not_to raise_error
      end
    end

    context 'when trace is already archived' do
      let!(:job) { create(:ci_build, :success, :trace_artifact) }

      it 'ignores an exception' do
        expect { subject }.not_to raise_error
      end

      it 'does not create an archived trace' do
        expect { subject }.not_to change { Ci::JobArtifact.trace.count }
      end

      context 'when live trace chunks still exist' do
        before do
          create(:ci_build_trace_chunk, build: job, chunk_index: 0)
        end

        it 'removes the trace chunks' do
          expect { subject }.to change { job.trace_chunks.count }.to(0)
        end

        context 'when associated data does not exist' do
          before do
            job.job_artifacts_trace.file.remove!
          end

          it 'removes the trace artifact and builds a new one' do
            existing_trace = job.job_artifacts_trace
            expect(existing_trace).to receive(:destroy!).and_call_original

            subject

            expect(job.reload.job_artifacts_trace).to be_present
            expect(job.reload.job_artifacts_trace.file.file).to be_present
          end
        end
      end
    end

    context 'when the job is out of archival attempts' do
      before do
        create(:ci_build_trace_metadata,
          build: job,
          archival_attempts: Ci::BuildTraceMetadata::MAX_ATTEMPTS + 1,
          last_archival_attempt_at: 1.week.ago)
      end

      it 'skips archiving' do
        expect(job.trace).not_to receive(:archive!)

        subject
      end

      it 'leaves a warning message in sidekiq log' do
        expect(Sidekiq.logger).to receive(:warn).with(
          class: Ci::ArchiveTraceWorker.name,
          message: 'The job is out of archival attempts.',
          job_id: job.id)

        subject
      end

      context 'job has archive and chunks' do
        let(:job) { create(:ci_build, :success, :trace_artifact) }

        before do
          create(:ci_build_trace_chunk, build: job, chunk_index: 0)
        end

        context 'archive is not completed' do
          before do
            job.job_artifacts_trace.file.remove!
          end

          it 'cleanups any stale archive data' do
            expect(job.job_artifacts_trace).to be_present

            subject

            expect(job.reload.job_artifacts_trace).to be_nil
          end
        end

        it 'removes trace chunks' do
          expect { subject }.to change { job.trace_chunks.count }.to(0)
        end
      end
    end

    context 'when the archival process is backed off' do
      before do
        create(:ci_build_trace_metadata,
          build: job,
          archival_attempts: Ci::BuildTraceMetadata::MAX_ATTEMPTS - 1,
          last_archival_attempt_at: 1.hour.ago)
      end

      it 'skips archiving' do
        expect(job.trace).not_to receive(:archive!)

        subject
      end

      it 'leaves a warning message in sidekiq log' do
        expect(Sidekiq.logger).to receive(:warn).with(
          class: Ci::ArchiveTraceWorker.name,
          message: 'The job can not be archived right now.',
          job_id: job.id)

        subject
      end
    end
  end

  context 'when job is running' do
    let(:job) { create(:ci_build, :running, :trace_live) }

    it 'increments Prometheus counter, sends crash report to Sentry and ignore an error for continuing to archive' do
      expect(Gitlab::ErrorTracking)
        .to receive(:track_and_raise_for_dev_exception)
        .with(::Gitlab::Ci::Trace::ArchiveError,
          issue_url: 'https://gitlab.com/gitlab-org/gitlab-foss/issues/51502',
          job_id: job.id).once

      expect(Sidekiq.logger).to receive(:warn).with(
        class: Ci::ArchiveTraceWorker.name,
        message: "Failed to archive trace. message: Job is not finished yet.",
        job_id: job.id).and_call_original

      expect(Gitlab::Metrics)
        .to receive(:counter)
        .with(:job_trace_archive_failed_total, "Counter of failed attempts of trace archiving")
        .and_call_original

      expect { subject }.not_to raise_error
      expect(job.trace_metadata.archival_attempts).to eq(1)
    end
  end

  describe '#batch_execute' do
    subject { described_class.new.batch_execute(worker_name: Ci::ArchiveTraceWorker.name) }

    let_it_be_with_reload(:job) { create(:ci_build, :success, :trace_live, finished_at: 1.day.ago) }
    let_it_be_with_reload(:job2) { create(:ci_build, :success, :trace_live, finished_at: 1.day.ago) }

    it 'archives multiple traces' do
      expect { subject }.not_to raise_error

      expect(job.reload.job_artifacts_trace).to be_exist
      expect(job2.reload.job_artifacts_trace).to be_exist
    end

    it 'processes traces independently' do
      allow_next_instance_of(Gitlab::Ci::Trace) do |instance|
        orig_method = instance.method(:archive!)
        allow(instance).to receive(:archive!) do
          raise('Unexpected error') if instance.job.id == job.id

          orig_method.call
        end
      end

      expect { subject }.not_to raise_error

      expect(job.reload.job_artifacts_trace).to be_nil
      expect(job2.reload.job_artifacts_trace).to be_exist
    end

    context 'when timeout is reached' do
      before do
        stub_const("#{described_class}::LOOP_TIMEOUT", 0.seconds)
      end

      it 'stops executing traces' do
        expect { subject }.not_to raise_error

        expect(job.reload.job_artifacts_trace).to be_nil
      end
    end

    context 'when loop limit is reached' do
      before do
        stub_const("#{described_class}::LOOP_LIMIT", -1)
      end

      it 'skips archiving' do
        expect(job.trace).not_to receive(:archive!)

        subject
      end

      it 'stops executing traces' do
        expect(Sidekiq.logger).to receive(:warn).with(
          class: Ci::ArchiveTraceWorker.name,
          message: "Loop limit reached.",
          job_id: job.id)

        expect { subject }.not_to raise_error

        expect(job.reload.job_artifacts_trace).to be_nil
      end
    end
  end
end


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

module Ci
  class UnlockArtifactsService < ::BaseService
    BATCH_SIZE = 100

    def execute(ci_ref, before_pipeline = nil)
      results = {
        unlocked_pipelines: 0,
        unlocked_job_artifacts: 0,
        unlocked_pipeline_artifacts: 0
      }

      loop do
        unlocked_pipelines = []
        unlocked_job_artifacts = []

        ::Ci::Pipeline.transaction do
          unlocked_pipelines = unlock_pipelines(ci_ref, before_pipeline)
          unlocked_job_artifacts = unlock_job_artifacts(unlocked_pipelines)

          results[:unlocked_pipeline_artifacts] += unlock_pipeline_artifacts(unlocked_pipelines)
        end

        break if unlocked_pipelines.empty?

        results[:unlocked_pipelines] += unlocked_pipelines.length
        results[:unlocked_job_artifacts] += unlocked_job_artifacts.length
      end

      results
    end

    # rubocop:disable CodeReuse/ActiveRecord
    def unlock_job_artifacts_query(pipeline_ids)
      ci_job_artifacts = ::Ci::JobArtifact.arel_table

      build_ids = ::Ci::Build.select(:id).where(commit_id: pipeline_ids)

      returning = Arel::Nodes::Grouping.new(ci_job_artifacts[:id])

      Arel::UpdateManager.new
        .table(ci_job_artifacts)
        .where(ci_job_artifacts[:job_id].in(Arel.sql(build_ids.to_sql)))
        .set([[ci_job_artifacts[:locked], ::Ci::JobArtifact.lockeds[:unlocked]]])
        .to_sql + " RETURNING #{returning.to_sql}"
    end
    # rubocop:enable CodeReuse/ActiveRecord

    # rubocop:disable CodeReuse/ActiveRecord
    def unlock_pipelines_query(ci_ref, before_pipeline)
      ci_pipelines = ::Ci::Pipeline.arel_table

      pipelines_scope = ci_ref.pipelines.artifacts_locked
      pipelines_scope = pipelines_scope.before_pipeline(before_pipeline) if before_pipeline
      pipelines_scope = pipelines_scope.select(:id).limit(BATCH_SIZE).lock('FOR UPDATE SKIP LOCKED')

      returning = Arel::Nodes::Grouping.new(ci_pipelines[:id])

      Arel::UpdateManager.new
        .table(ci_pipelines)
        .where(ci_pipelines[:id].in(Arel.sql(pipelines_scope.to_sql)))
        .set([[ci_pipelines[:locked], ::Ci::Pipeline.lockeds[:unlocked]]])
        .to_sql + " RETURNING #{returning.to_sql}"
    end
    # rubocop:enable CodeReuse/ActiveRecord

    private

    def unlock_job_artifacts(pipelines)
      return if pipelines.empty?

      ::Ci::JobArtifact.connection.exec_query(
        unlock_job_artifacts_query(pipelines.rows.flatten)
      )
    end

    # rubocop:disable CodeReuse/ActiveRecord
    def unlock_pipeline_artifacts(pipelines)
      return 0 if pipelines.empty?

      ::Ci::PipelineArtifact.where(pipeline_id: pipelines.rows.flatten).update_all(locked: :unlocked)
    end
    # rubocop:enable CodeReuse/ActiveRecord

    def unlock_pipelines(ci_ref, before_pipeline)
      ::Ci::Pipeline.connection.exec_query(unlock_pipelines_query(ci_ref, before_pipeline))
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::UnlockArtifactsService, feature_category: :continuous_integration do
  using RSpec::Parameterized::TableSyntax

  where(:tag) do
    [
      [false],
      [true]
    ]
  end

  with_them do
    let(:ref) { 'master' }
    let(:ref_path) { tag ? "#{::Gitlab::Git::TAG_REF_PREFIX}#{ref}" : "#{::Gitlab::Git::BRANCH_REF_PREFIX}#{ref}" }
    let(:ci_ref) { create(:ci_ref, ref_path: ref_path) }
    let(:project) { ci_ref.project }
    let(:source_job) { create(:ci_build, pipeline: pipeline) }

    let!(:old_unlocked_pipeline) { create(:ci_pipeline, :with_persisted_artifacts, ref: ref, tag: tag, project: project, locked: :unlocked) }
    let!(:older_pipeline) { create(:ci_pipeline, :with_persisted_artifacts, ref: ref, tag: tag, project: project, locked: :artifacts_locked) }
    let!(:older_ambiguous_pipeline) { create(:ci_pipeline, :with_persisted_artifacts, ref: ref, tag: !tag, project: project, locked: :artifacts_locked) }
    let!(:code_coverage_pipeline) { create(:ci_pipeline, :with_coverage_report_artifact, ref: ref, tag: tag, project: project, locked: :artifacts_locked) }
    let!(:pipeline) { create(:ci_pipeline, :with_persisted_artifacts, ref: ref, tag: tag, project: project, locked: :artifacts_locked) }
    let!(:child_pipeline) { create(:ci_pipeline, :with_persisted_artifacts, ref: ref, tag: tag, child_of: pipeline, project: project, locked: :artifacts_locked) }
    let!(:newer_pipeline) { create(:ci_pipeline, :with_persisted_artifacts, ref: ref, tag: tag, project: project, locked: :artifacts_locked) }
    let!(:other_ref_pipeline) { create(:ci_pipeline, :with_persisted_artifacts, ref: 'other_ref', tag: tag, project: project, locked: :artifacts_locked) }
    let!(:sources_pipeline) { create(:ci_sources_pipeline, source_job: source_job, source_project: project, pipeline: child_pipeline, project: project) }

    before do
      stub_const("#{described_class}::BATCH_SIZE", 1)
    end

    describe '#execute' do
      subject(:execute) { described_class.new(pipeline.project, pipeline.user).execute(ci_ref, before_pipeline) }

      context 'when running on a ref before a pipeline' do
        let(:before_pipeline) { pipeline }

        it 'unlocks artifacts from older pipelines' do
          expect { execute }.to change { older_pipeline.reload.locked }.from('artifacts_locked').to('unlocked')
        end

        it 'does not unlock artifacts for tag or branch with same name as ref' do
          expect { execute }.not_to change { older_ambiguous_pipeline.reload.locked }.from('artifacts_locked')
        end

        it 'does not unlock artifacts from newer pipelines' do
          expect { execute }.not_to change { newer_pipeline.reload.locked }.from('artifacts_locked')
        end

        it 'does not lock artifacts from old unlocked pipelines' do
          expect { execute }.not_to change { old_unlocked_pipeline.reload.locked }.from('unlocked')
        end

        it 'does not unlock artifacts from the same pipeline' do
          expect { execute }.not_to change { pipeline.reload.locked }.from('artifacts_locked')
        end

        it 'does not unlock artifacts for other refs' do
          expect { execute }.not_to change { other_ref_pipeline.reload.locked }.from('artifacts_locked')
        end

        it 'does not unlock artifacts for child pipeline' do
          expect { execute }.not_to change { child_pipeline.reload.locked }.from('artifacts_locked')
        end

        it 'unlocks job artifact records' do
          expect { execute }.to change { ::Ci::JobArtifact.artifact_unlocked.count }.from(0).to(2)
        end

        it 'unlocks pipeline artifact records' do
          expect { execute }.to change { ::Ci::PipelineArtifact.artifact_unlocked.count }.from(0).to(1)
        end
      end

      context 'when running on just the ref' do
        let(:before_pipeline) { nil }

        it 'unlocks artifacts from older pipelines' do
          expect { execute }.to change { older_pipeline.reload.locked }.from('artifacts_locked').to('unlocked')
        end

        it 'unlocks artifacts from newer pipelines' do
          expect { execute }.to change { newer_pipeline.reload.locked }.from('artifacts_locked').to('unlocked')
        end

        it 'unlocks artifacts from the same pipeline' do
          expect { execute }.to change { pipeline.reload.locked }.from('artifacts_locked').to('unlocked')
        end

        it 'does not unlock artifacts for tag or branch with same name as ref' do
          expect { execute }.not_to change { older_ambiguous_pipeline.reload.locked }.from('artifacts_locked')
        end

        it 'does not lock artifacts from old unlocked pipelines' do
          expect { execute }.not_to change { old_unlocked_pipeline.reload.locked }.from('unlocked')
        end

        it 'does not unlock artifacts for other refs' do
          expect { execute }.not_to change { other_ref_pipeline.reload.locked }.from('artifacts_locked')
        end

        it 'unlocks job artifact records' do
          expect { execute }.to change { ::Ci::JobArtifact.artifact_unlocked.count }.from(0).to(8)
        end

        it 'unlocks pipeline artifact records' do
          expect { execute }.to change { ::Ci::PipelineArtifact.artifact_unlocked.count }.from(0).to(1)
        end
      end
    end

    describe '#unlock_pipelines_query' do
      subject { described_class.new(pipeline.project, pipeline.user).unlock_pipelines_query(ci_ref, before_pipeline) }

      context 'when running on a ref before a pipeline' do
        let(:before_pipeline) { pipeline }

        it 'produces the expected SQL string' do
          # To be removed when the ignored column id_convert_to_bigint for ci_pipelines is removed
          # see https://gitlab.com/gitlab-org/gitlab/-/issues/397000
          selected_columns =
            Ci::Pipeline.column_names.map do |field|
              Ci::Pipeline.connection.quote_table_name("#{Ci::Pipeline.table_name}.#{field}")
            end.join(', ')
          expect(subject.squish).to eq <<~SQL.squish
            UPDATE
                "ci_pipelines"
            SET
                "locked" = 0
            WHERE
                "ci_pipelines"."id" IN
                    (SELECT
                        "ci_pipelines"."id"
                    FROM
                        "ci_pipelines"
                    WHERE
                        "ci_pipelines"."ci_ref_id" = #{ci_ref.id}
                        AND "ci_pipelines"."locked" = 1
                        AND "ci_pipelines"."id" < #{before_pipeline.id}
                        AND "ci_pipelines"."id" NOT IN
                            (WITH RECURSIVE
                                "base_and_descendants"
                            AS
                                ((SELECT
                                    #{selected_columns}
                                FROM
                                    "ci_pipelines"
                                WHERE
                                    "ci_pipelines"."id" = #{before_pipeline.id})
                            UNION
                                (SELECT
                                    #{selected_columns}
                                FROM
                                    "ci_pipelines",
                                    "base_and_descendants",
                                    "ci_sources_pipelines"
                                WHERE
                                    "ci_sources_pipelines"."pipeline_id" = "ci_pipelines"."id"
                                    AND "ci_sources_pipelines"."source_pipeline_id" = "base_and_descendants"."id"
                                    AND "ci_sources_pipelines"."source_project_id" = "ci_sources_pipelines"."project_id"))
                            SELECT
                                "id"
                            FROM
                                "base_and_descendants"
                            AS
                                "ci_pipelines")
                    LIMIT 1
                    FOR UPDATE
                    SKIP LOCKED)
            RETURNING ("ci_pipelines"."id")
          SQL
        end
      end

      context 'when running on just the ref' do
        let(:before_pipeline) { nil }

        it 'produces the expected SQL string' do
          expect(subject.squish).to eq <<~SQL.squish
            UPDATE
                "ci_pipelines"
            SET
                "locked" = 0
            WHERE
                "ci_pipelines"."id" IN
                    (SELECT
                        "ci_pipelines"."id"
                    FROM
                        "ci_pipelines"
                    WHERE
                        "ci_pipelines"."ci_ref_id" = #{ci_ref.id}
                        AND "ci_pipelines"."locked" = 1
                    LIMIT 1
                    FOR UPDATE
                        SKIP LOCKED)
            RETURNING
                ("ci_pipelines"."id")
          SQL
        end
      end
    end

    describe '#unlock_job_artifacts_query' do
      subject { described_class.new(pipeline.project, pipeline.user).unlock_job_artifacts_query(pipeline_ids) }

      let(:builds_table) { Ci::Build.quoted_table_name }

      context 'when given a single pipeline ID' do
        let(:pipeline_ids) { [older_pipeline.id] }

        it 'produces the expected SQL string' do
          expect(subject.squish).to eq <<~SQL.squish
            UPDATE
                "ci_job_artifacts"
            SET
                "locked" = 0
            WHERE
                "ci_job_artifacts"."job_id" IN
                    (SELECT
                        #{builds_table}."id"
                    FROM
                        #{builds_table}
                    WHERE
                        #{builds_table}."type" = 'Ci::Build'
                        AND #{builds_table}."commit_id" = #{older_pipeline.id})
            RETURNING
                ("ci_job_artifacts"."id")
          SQL
        end
      end

      context 'when given multiple pipeline IDs' do
        let(:pipeline_ids) { [older_pipeline.id, newer_pipeline.id, pipeline.id] }

        it 'produces the expected SQL string' do
          expect(subject.squish).to eq <<~SQL.squish
            UPDATE
                "ci_job_artifacts"
            SET
                "locked" = 0
            WHERE
                "ci_job_artifacts"."job_id" IN
                    (SELECT
                        #{builds_table}."id"
                    FROM
                        #{builds_table}
                    WHERE
                        #{builds_table}."type" = 'Ci::Build'
                        AND #{builds_table}."commit_id" IN (#{pipeline_ids.join(', ')}))
            RETURNING
                ("ci_job_artifacts"."id")
          SQL
        end
      end
    end
  end
end


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

module Ci
  # Cancel a pipelines cancelable jobs and optionally it's child pipelines cancelable jobs
  class CancelPipelineService
    include Gitlab::OptimisticLocking
    include Gitlab::Allowable

    ##
    # @cascade_to_children - if true cancels all related child pipelines for parent child pipelines
    # @auto_canceled_by_pipeline - store the pipeline_id of the pipeline that triggered cancellation
    # @execute_async - if true cancel the children asyncronously
    def initialize(
      pipeline:,
      current_user:,
      cascade_to_children: true,
      auto_canceled_by_pipeline: nil,
      execute_async: true)
      @pipeline = pipeline
      @current_user = current_user
      @cascade_to_children = cascade_to_children
      @auto_canceled_by_pipeline = auto_canceled_by_pipeline
      @execute_async = execute_async
    end

    def execute
      return permission_error_response unless can?(current_user, :cancel_pipeline, pipeline)

      force_execute
    end

    # This method should be used only when we want to always cancel the pipeline without
    # checking whether the current_user has permissions to do so, or when we don't have
    # a current_user available in the context.
    def force_execute
      return ServiceResponse.error(message: 'No pipeline provided', reason: :no_pipeline) unless pipeline

      unless pipeline.cancelable?
        return ServiceResponse.error(message: 'Pipeline is not cancelable', reason: :pipeline_not_cancelable)
      end

      log_pipeline_being_canceled

      pipeline.update_column(:auto_canceled_by_id, @auto_canceled_by_pipeline.id) if @auto_canceled_by_pipeline
      cancel_jobs(pipeline.cancelable_statuses)

      return ServiceResponse.success unless cascade_to_children?

      # cancel any bridges that could spin up new child pipelines
      cancel_jobs(pipeline.bridges_in_self_and_project_descendants.cancelable)
      cancel_children

      ServiceResponse.success
    end

    private

    attr_reader :pipeline, :current_user

    def log_pipeline_being_canceled
      Gitlab::AppJsonLogger.info(
        event: 'pipeline_cancel_running',
        pipeline_id: pipeline.id,
        auto_canceled_by_pipeline_id: @auto_canceled_by_pipeline&.id,
        cascade_to_children: cascade_to_children?,
        execute_async: execute_async?,
        **Gitlab::ApplicationContext.current
      )
    end

    def cascade_to_children?
      @cascade_to_children
    end

    def execute_async?
      @execute_async
    end

    def cancel_jobs(jobs)
      retries = 3
      retry_lock(jobs, retries, name: 'ci_pipeline_cancel_running') do |jobs_to_cancel|
        preloaded_relations = [:project, :pipeline, :deployment, :taggings]

        jobs_to_cancel.find_in_batches do |batch|
          relation = CommitStatus.id_in(batch)
          Preloaders::CommitStatusPreloader.new(relation).execute(preloaded_relations)

          relation.each { |job| cancel_job(job) }
        end
      end
    end

    def cancel_job(job)
      if @auto_canceled_by_pipeline
        job.auto_canceled_by_id = @auto_canceled_by_pipeline.id
        job.auto_canceled_by_partition_id = @auto_canceled_by_pipeline.partition_id
      end

      job.cancel
    end

    def permission_error_response
      ServiceResponse.error(
        message: 'Insufficient permissions to cancel the pipeline',
        reason: :insufficient_permissions
      )
    end

    # For parent child-pipelines only (not multi-project)
    def cancel_children
      pipeline.all_child_pipelines.each do |child_pipeline|
        if execute_async?
          ::Ci::CancelPipelineWorker.perform_async(
            child_pipeline.id,
            @auto_canceled_by_pipeline&.id
          )
        else
          # cascade_to_children is false because we iterate through children
          # we also cancel bridges prior to prevent more children
          self.class.new(
            pipeline: child_pipeline.reset,
            current_user: nil,
            cascade_to_children: false,
            execute_async: execute_async?,
            auto_canceled_by_pipeline: @auto_canceled_by_pipeline
          ).force_execute
        end
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::CancelPipelineService, :aggregate_failures, feature_category: :continuous_integration do
  let_it_be(:project) { create(:project) }
  let_it_be(:current_user) { project.owner }
  let_it_be(:pipeline) { create(:ci_pipeline, project: project) }

  let(:service) do
    described_class.new(
      pipeline: pipeline,
      current_user: current_user,
      cascade_to_children: cascade_to_children,
      auto_canceled_by_pipeline: auto_canceled_by_pipeline,
      execute_async: execute_async)
  end

  let(:cascade_to_children) { true }
  let(:auto_canceled_by_pipeline) { nil }
  let(:execute_async) { true }

  shared_examples 'force_execute' do
    context 'when pipeline is not cancelable' do
      it 'returns an error' do
        expect(response).to be_error
        expect(response.reason).to eq(:pipeline_not_cancelable)
      end
    end

    context 'when pipeline is cancelable' do
      before do
        create(:ci_build, :running, pipeline: pipeline)
        create(:ci_build, :created, pipeline: pipeline)
        create(:ci_build, :success, pipeline: pipeline)
      end

      it 'logs the event' do
        allow(Gitlab::AppJsonLogger).to receive(:info)

        subject

        expect(Gitlab::AppJsonLogger)
          .to have_received(:info)
          .with(
            a_hash_including(
              event: 'pipeline_cancel_running',
              pipeline_id: pipeline.id,
              auto_canceled_by_pipeline_id: nil,
              cascade_to_children: true,
              execute_async: true
            )
          )
      end

      it 'cancels all cancelable jobs' do
        expect(response).to be_success
        expect(pipeline.all_jobs.pluck(:status)).to match_array(%w[canceled canceled success])
      end

      context 'when auto_canceled_by_pipeline is provided' do
        let(:auto_canceled_by_pipeline) { create(:ci_pipeline) }

        it 'updates the pipeline and jobs with it' do
          subject

          expect(pipeline.auto_canceled_by_id).to eq(auto_canceled_by_pipeline.id)

          expect(pipeline.all_jobs.canceled.pluck(:auto_canceled_by_id).uniq)
            .to eq([auto_canceled_by_pipeline.id])

          expect(pipeline.all_jobs.canceled.pluck(:auto_canceled_by_partition_id).uniq)
            .to eq([auto_canceled_by_pipeline.partition_id])
        end
      end

      context 'when pipeline has child pipelines' do
        let(:child_pipeline) { create(:ci_pipeline, child_of: pipeline) }
        let!(:child_job) { create(:ci_build, :running, pipeline: child_pipeline) }
        let(:grandchild_pipeline) { create(:ci_pipeline, child_of: child_pipeline) }
        let!(:grandchild_job) { create(:ci_build, :running, pipeline: grandchild_pipeline) }

        before do
          child_pipeline.source_bridge.update!(status: :running)
          grandchild_pipeline.source_bridge.update!(status: :running)
        end

        context 'when execute_async: false' do
          let(:execute_async) { false }

          it 'cancels the bridge jobs and child jobs' do
            expect(response).to be_success

            expect(pipeline.bridges.pluck(:status)).to be_all('canceled')
            expect(child_pipeline.bridges.pluck(:status)).to be_all('canceled')
            expect(child_job.reload).to be_canceled
            expect(grandchild_job.reload).to be_canceled
          end
        end

        context 'when execute_async: true' do
          it 'schedules the child pipelines for async cancelation' do
            expect(::Ci::CancelPipelineWorker)
              .to receive(:perform_async)
              .with(child_pipeline.id, nil)

            expect(::Ci::CancelPipelineWorker)
              .to receive(:perform_async)
              .with(grandchild_pipeline.id, nil)

            expect(response).to be_success

            expect(pipeline.bridges.pluck(:status)).to be_all('canceled')
          end
        end

        context 'when cascade_to_children: false' do
          let(:execute_async) { true }
          let(:cascade_to_children) { false }

          it 'does not cancel child pipelines' do
            expect(::Ci::CancelPipelineWorker)
              .not_to receive(:perform_async)

            expect(response).to be_success

            expect(pipeline.bridges.pluck(:status)).to be_all('canceled')
            expect(child_job.reload).to be_running
          end
        end
      end

      context 'when preloading relations' do
        let(:pipeline1) { create(:ci_pipeline, :created) }
        let(:pipeline2) { create(:ci_pipeline, :created) }

        before do
          create(:ci_build, :pending, pipeline: pipeline1)
          create(:generic_commit_status, :pending, pipeline: pipeline1)

          create(:ci_build, :pending, pipeline: pipeline2)
          create(:ci_build, :pending, pipeline: pipeline2)
          create(:generic_commit_status, :pending, pipeline: pipeline2)
          create(:generic_commit_status, :pending, pipeline: pipeline2)
          create(:generic_commit_status, :pending, pipeline: pipeline2)
        end

        it 'preloads relations for each build to avoid N+1 queries' do
          control1 = ActiveRecord::QueryRecorder.new do
            described_class.new(pipeline: pipeline1, current_user: current_user).force_execute
          end

          control2 = ActiveRecord::QueryRecorder.new do
            described_class.new(pipeline: pipeline2, current_user: current_user).force_execute
          end

          extra_update_queries = 4 # transition ... => :canceled, queue pop
          extra_generic_commit_status_validation_queries = 2 # name_uniqueness_across_types

          expect(control2.count)
            .to eq(control1.count + extra_update_queries + extra_generic_commit_status_validation_queries)
        end
      end
    end
  end

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

    it_behaves_like 'force_execute'

    context 'when user does not have permissions to cancel the pipeline' do
      let(:current_user) { create(:user) }

      it 'returns an error when user does not have permissions to cancel pipeline' do
        expect(response).to be_error
        expect(response.reason).to eq(:insufficient_permissions)
      end
    end
  end

  describe '#force_execute' do
    subject(:response) { service.force_execute }

    it_behaves_like 'force_execute'

    context 'when pipeline is not provided' do
      let(:pipeline) { nil }

      it 'returns an error' do
        expect(response).to be_error
        expect(response.reason).to eq(:no_pipeline)
      end
    end
  end
end


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

module Ci
  # This service resets skipped jobs so they can be processed again.
  # It affects the jobs that depend on the passed in job parameter.
  class ResetSkippedJobsService < ::BaseService
    def execute(processables)
      @processables = Array.wrap(processables)
      @pipeline = @processables.first.pipeline

      process_subsequent_jobs
      reset_source_bridge
    end

    private

    def process_subsequent_jobs
      dependent_jobs.each do |job|
        process(job)
      end
    end

    def reset_source_bridge
      @pipeline.reset_source_bridge!(current_user)
    end

    # rubocop: disable CodeReuse/ActiveRecord
    def dependent_jobs
      ordered_by_dag(
        @pipeline.processables
          .from_union(needs_dependent_jobs, stage_dependent_jobs)
          .skipped
          .ordered_by_stage
          .preload(:needs)
      )
    end

    def process(job)
      Gitlab::OptimisticLocking.retry_lock(job, name: 'ci_requeue_job') do |job|
        job.process(current_user)
      end
    end

    def stage_dependent_jobs
      # Get all jobs after the earliest stage of the inputted jobs
      min_stage_idx = @processables.map(&:stage_idx).min
      @pipeline.processables.after_stage(min_stage_idx)
    end

    def needs_dependent_jobs
      # We must include the hierarchy base here because @processables may include both a parent job
      # and its dependents, and we do not want to exclude those dependents from being processed.
      ::Gitlab::Ci::ProcessableObjectHierarchy.new(
        ::Ci::Processable.where(id: @processables.map(&:id))
      ).base_and_descendants
    end

    def ordered_by_dag(jobs)
      sorted_job_names = sort_jobs(jobs).each_with_index.to_h

      jobs.group_by(&:stage_idx).flat_map do |_, stage_jobs|
        stage_jobs.sort_by { |job| sorted_job_names.fetch(job.name) }
      end
    end

    def sort_jobs(jobs)
      Gitlab::Ci::YamlProcessor::Dag.order(
        jobs.to_h do |job|
          [job.name, job.needs.map(&:name)]
        end
      )
    end
    # rubocop: enable CodeReuse/ActiveRecord
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::ResetSkippedJobsService, :sidekiq_inline, feature_category: :continuous_integration do
  let_it_be(:project) { create(:project, :empty_repo) }
  let_it_be(:user) { project.first_owner }

  let(:pipeline) do
    Ci::CreatePipelineService.new(project, user, { ref: 'master' }).execute(:push).payload
  end

  let(:a1) { find_job('a1') }
  let(:a2) { find_job('a2') }
  let(:b1) { find_job('b1') }
  let(:input_processables) { a1 } # This is the input used when running service.execute()

  before_all do
    project.repository.create_file(user, 'init', 'init', message: 'init', branch_name: 'master')
  end

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

  shared_examples 'with a stage-dag mixed pipeline' do
    let(:config) do
      <<-YAML
      stages: [a, b, c]

      a1:
        stage: a
        script: exit $(($RANDOM % 2))

      a2:
        stage: a
        script: exit 0
        needs: [a1]

      a3:
        stage: a
        script: exit 0
        needs: [a2]

      b1:
        stage: b
        script: exit 0
        needs: []

      b2:
        stage: b
        script: exit 0
        needs: [a2]

      c1:
        stage: c
        script: exit 0
        needs: [b2]

      c2:
        stage: c
        script: exit 0
      YAML
    end

    before do
      stub_ci_pipeline_yaml_file(config)
      check_jobs_statuses(
        a1: 'pending',
        a2: 'created',
        a3: 'created',
        b1: 'pending',
        b2: 'created',
        c1: 'created',
        c2: 'created'
      )

      b1.success!
      check_jobs_statuses(
        a1: 'pending',
        a2: 'created',
        a3: 'created',
        b1: 'success',
        b2: 'created',
        c1: 'created',
        c2: 'created'
      )

      a1.drop!
      check_jobs_statuses(
        a1: 'failed',
        a2: 'skipped',
        a3: 'skipped',
        b1: 'success',
        b2: 'skipped',
        c1: 'skipped',
        c2: 'skipped'
      )

      new_a1 = Ci::RetryJobService.new(project, user).clone!(a1)
      new_a1.enqueue!
      check_jobs_statuses(
        a1: 'pending',
        a2: 'skipped',
        a3: 'skipped',
        b1: 'success',
        b2: 'skipped',
        c1: 'skipped',
        c2: 'skipped'
      )
    end

    it 'marks subsequent skipped jobs as processable' do
      service.execute(input_processables)

      check_jobs_statuses(
        a1: 'pending',
        a2: 'created',
        a3: 'created',
        b1: 'success',
        b2: 'created',
        c1: 'created',
        c2: 'created'
      )
    end

    context 'when executed by a different user than the original owner' do
      let(:retryer) { create(:user).tap { |u| project.add_maintainer(u) } }
      let(:service) { described_class.new(project, retryer) }

      it 'reassigns jobs with updated statuses to the retryer' do
        expect(jobs_name_status_owner_needs).to contain_exactly(
          { 'name' => 'a1', 'status' => 'pending', 'user_id' => user.id, 'needs' => [] },
          { 'name' => 'a2', 'status' => 'skipped', 'user_id' => user.id, 'needs' => ['a1'] },
          { 'name' => 'a3', 'status' => 'skipped', 'user_id' => user.id, 'needs' => ['a2'] },
          { 'name' => 'b1', 'status' => 'success', 'user_id' => user.id, 'needs' => [] },
          { 'name' => 'b2', 'status' => 'skipped', 'user_id' => user.id, 'needs' => ['a2'] },
          { 'name' => 'c1', 'status' => 'skipped', 'user_id' => user.id, 'needs' => ['b2'] },
          { 'name' => 'c2', 'status' => 'skipped', 'user_id' => user.id, 'needs' => [] }
        )

        service.execute(input_processables)

        expect(jobs_name_status_owner_needs).to contain_exactly(
          { 'name' => 'a1', 'status' => 'pending', 'user_id' => user.id,    'needs' => [] },
          { 'name' => 'a2', 'status' => 'created', 'user_id' => retryer.id, 'needs' => ['a1'] },
          { 'name' => 'a3', 'status' => 'created', 'user_id' => retryer.id, 'needs' => ['a2'] },
          { 'name' => 'b1', 'status' => 'success', 'user_id' => user.id,    'needs' => [] },
          { 'name' => 'b2', 'status' => 'created', 'user_id' => retryer.id, 'needs' => ['a2'] },
          { 'name' => 'c1', 'status' => 'created', 'user_id' => retryer.id, 'needs' => ['b2'] },
          { 'name' => 'c2', 'status' => 'created', 'user_id' => retryer.id, 'needs' => [] }
        )
      end
    end
  end

  shared_examples 'with stage-dag mixed pipeline with some same-stage needs' do
    let(:config) do
      <<-YAML
      stages: [a, b, c]

      a1:
        stage: a
        script: exit $(($RANDOM % 2))

      a2:
        stage: a
        script: exit 0
        needs: [a1]

      b1:
        stage: b
        script: exit 0
        needs: [b2]

      b2:
        stage: b
        script: exit 0

      c1:
        stage: c
        script: exit 0
        needs: [b2]

      c2:
        stage: c
        script: exit 0
      YAML
    end

    before do
      stub_ci_pipeline_yaml_file(config)
      check_jobs_statuses(
        a1: 'pending',
        a2: 'created',
        b1: 'created',
        b2: 'created',
        c1: 'created',
        c2: 'created'
      )

      a1.drop!
      check_jobs_statuses(
        a1: 'failed',
        a2: 'skipped',
        b1: 'skipped',
        b2: 'skipped',
        c1: 'skipped',
        c2: 'skipped'
      )

      new_a1 = Ci::RetryJobService.new(project, user).clone!(a1)
      new_a1.enqueue!
      check_jobs_statuses(
        a1: 'pending',
        a2: 'skipped',
        b1: 'skipped',
        b2: 'skipped',
        c1: 'skipped',
        c2: 'skipped'
      )
    end

    it 'marks subsequent skipped jobs as processable' do
      service.execute(input_processables)

      check_jobs_statuses(
        a1: 'pending',
        a2: 'created',
        b1: 'created',
        b2: 'created',
        c1: 'created',
        c2: 'created'
      )
    end
  end

  shared_examples 'with same-stage needs' do
    let(:config) do
      <<-YAML
      a1:
        script: exit $(($RANDOM % 2))

      b1:
        script: exit 0
        needs: [a1]

      c1:
        script: exit 0
        needs: [b1]
      YAML
    end

    before do
      stub_ci_pipeline_yaml_file(config)
      check_jobs_statuses(
        a1: 'pending',
        b1: 'created',
        c1: 'created'
      )

      a1.drop!
      check_jobs_statuses(
        a1: 'failed',
        b1: 'skipped',
        c1: 'skipped'
      )

      new_a1 = Ci::RetryJobService.new(project, user).clone!(a1)
      new_a1.enqueue!
      check_jobs_statuses(
        a1: 'pending',
        b1: 'skipped',
        c1: 'skipped'
      )
    end

    it 'marks subsequent skipped jobs as processable' do
      service.execute(input_processables)

      check_jobs_statuses(
        a1: 'pending',
        b1: 'created',
        c1: 'created'
      )
    end
  end

  context 'with same-stage needs where the parent jobs do not share the same descendants' do
    let(:config) do
      <<-YAML
      a1:
        script: exit $(($RANDOM % 2))

      a2:
        script: exit $(($RANDOM % 2))

      b1:
        script: exit 0
        needs: [a1]

      b2:
        script: exit 0
        needs: [a2]

      c1:
        script: exit 0
        needs: [b1]

      c2:
        script: exit 0
        needs: [b2]
      YAML
    end

    before do
      stub_ci_pipeline_yaml_file(config)
      check_jobs_statuses(
        a1: 'pending',
        a2: 'pending',
        b1: 'created',
        b2: 'created',
        c1: 'created',
        c2: 'created'
      )

      a1.drop!
      a2.drop!

      check_jobs_statuses(
        a1: 'failed',
        a2: 'failed',
        b1: 'skipped',
        b2: 'skipped',
        c1: 'skipped',
        c2: 'skipped'
      )

      new_a1 = Ci::RetryJobService.new(project, user).clone!(a1)
      new_a1.enqueue!

      check_jobs_statuses(
        a1: 'pending',
        a2: 'failed',
        b1: 'skipped',
        b2: 'skipped',
        c1: 'skipped',
        c2: 'skipped'
      )

      new_a2 = Ci::RetryJobService.new(project, user).clone!(a2)
      new_a2.enqueue!

      check_jobs_statuses(
        a1: 'pending',
        a2: 'pending',
        b1: 'skipped',
        b2: 'skipped',
        c1: 'skipped',
        c2: 'skipped'
      )
    end

    # This demonstrates that when only a1 is inputted, only the *1 subsequent jobs are reset.
    # This is in contrast to the following example when both a1 and a2 are inputted.
    it 'marks subsequent skipped jobs as processable' do
      service.execute(input_processables)

      check_jobs_statuses(
        a1: 'pending',
        a2: 'pending',
        b1: 'created',
        b2: 'skipped',
        c1: 'created',
        c2: 'skipped'
      )
    end

    context 'when multiple processables are inputted' do
      # When both a1 and a2 are inputted, all subsequent jobs are reset.
      it 'marks subsequent skipped jobs as processable' do
        input_processables = [a1, a2]
        service.execute(input_processables)

        check_jobs_statuses(
          a1: 'pending',
          a2: 'pending',
          b1: 'created',
          b2: 'created',
          c1: 'created',
          c2: 'created'
        )
      end
    end
  end

  context 'when a single processable is inputted' do
    it_behaves_like 'with a stage-dag mixed pipeline'
    it_behaves_like 'with stage-dag mixed pipeline with some same-stage needs'
    it_behaves_like 'with same-stage needs'
  end

  context 'when multiple processables are inputted' do
    let(:input_processables) { [a1, b1] }

    it_behaves_like 'with a stage-dag mixed pipeline'
    it_behaves_like 'with stage-dag mixed pipeline with some same-stage needs'
    it_behaves_like 'with same-stage needs'
  end

  private

  def find_job(name)
    processables.find_by!(name: name)
  end

  def check_jobs_statuses(statuses)
    expect(processables.order(:name).pluck(:name, :status)).to contain_exactly(*statuses.stringify_keys.to_a)
  end

  def processables
    pipeline.processables.latest
  end

  def jobs_name_status_owner_needs
    processables.reload.map do |job|
      job.attributes.slice('name', 'status', 'user_id').merge('needs' => job.needs.map(&:name))
    end
  end
end


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

module Ci
  class CopyCrossDatabaseAssociationsService
    def execute(old_build, new_build)
      ServiceResponse.success
    end
  end
end

Ci::CopyCrossDatabaseAssociationsService.prepend_mod_with('Ci::CopyCrossDatabaseAssociationsService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::CopyCrossDatabaseAssociationsService, feature_category: :continuous_integration do
  let_it_be(:project) { create(:project) }
  let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
  let_it_be(:old_build) { create(:ci_build, pipeline: pipeline) }
  let_it_be(:new_build) { create(:ci_build, pipeline: pipeline) }

  subject(:execute) { described_class.new.execute(old_build, new_build) }

  describe '#execute' do
    it 'returns a success response' do
      expect(execute).to be_success
    end
  end
end


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

module Ci
  class CompareTestReportsService < CompareReportsBaseService
    def comparer_class
      Gitlab::Ci::Reports::TestReportsComparer
    end

    def serializer_class
      TestReportsComparerSerializer
    end

    def get_report(pipeline)
      pipeline&.test_reports
    end

    def build_comparer(base_report, head_report)
      # We need to load the test failure history on the test comparer because we display
      # this on the MR widget
      super.tap do |test_reports_comparer|
        ::Gitlab::Ci::Reports::TestFailureHistory.new(failed_test_cases(test_reports_comparer), project).load!
      end
    end

    def failed_test_cases(test_reports_comparer)
      test_reports_comparer.suite_comparers.flat_map do |suite_comparer|
        suite_comparer.limited_tests.new_failures + suite_comparer.limited_tests.existing_failures
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

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

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

    context 'when head pipeline has test reports' do
      let!(:base_pipeline) { nil }
      let!(:head_pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }

      it 'returns status and data' do
        expect(comparison[:status]).to eq(:parsed)
        expect(comparison[:data]).to match_schema('entities/test_reports_comparer')
      end
    end

    context 'when base and head pipelines have test reports' do
      let!(:base_pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
      let!(:head_pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }

      it 'returns status and data' do
        expect(comparison[:status]).to eq(:parsed)
        expect(comparison[:data]).to match_schema('entities/test_reports_comparer')
      end
    end

    context 'when head pipeline has corrupted test reports' do
      let!(:base_pipeline) { nil }
      let!(:head_pipeline) { create(:ci_pipeline, project: project) }

      before do
        build = create(:ci_build, pipeline: head_pipeline, project: head_pipeline.project)
        create(:ci_job_artifact, :junit_with_corrupted_data, job: build, project: project)
      end

      it 'returns a parsed TestReports success status and failure on the individual suite' do
        expect(comparison[:status]).to eq(:parsed)
        expect(comparison.dig(:data, 'status')).to eq('success')
        expect(comparison.dig(:data, 'suites', 0, 'status')).to eq('error')
      end
    end

    context 'test failure history' do
      let!(:base_pipeline) { nil }
      let!(:head_pipeline) { create(:ci_pipeline, :with_test_reports_with_three_failures, project: project) }

      let(:new_failures) do
        comparison.dig(:data, 'suites', 0, 'new_failures')
      end

      let(:recent_failures_per_test_case) do
        new_failures.map { |f| f['recent_failures'] }
      end

      # Create test case failure records based on the head pipeline build
      before do
        stub_const("Gitlab::Ci::Reports::TestSuiteComparer::DEFAULT_MAX_TESTS", 2)
        stub_const("Gitlab::Ci::Reports::TestSuiteComparer::DEFAULT_MIN_TESTS", 1)

        build = head_pipeline.builds.last
        build.update_column(:finished_at, 1.day.ago) # Just to be sure we are included in the report window

        # The JUnit fixture for the given build has 3 failures.
        # This service will create 1 test case failure record for each.
        Ci::TestFailureHistoryService.new(head_pipeline).execute
      end

      it 'loads recent failures on limited test cases to avoid building up a huge DB query', :aggregate_failures do
        expect(comparison[:data]).to match_schema('entities/test_reports_comparer')
        expect(recent_failures_per_test_case).to eq(
          [
            { 'count' => 1, 'base_branch' => 'master' },
            { 'count' => 1, 'base_branch' => 'master' }
          ])
        expect(new_failures.count).to eq(2)
      end
    end
  end
end


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

module Ci
  class PlayBuildService < ::BaseService
    def execute(build, job_variables_attributes = nil)
      check_access!(build, job_variables_attributes)

      Ci::EnqueueJobService.new(build, current_user: current_user, variables: job_variables_attributes || []).execute
    rescue StateMachines::InvalidTransition
      retry_build(build.reset)
    end

    private

    def retry_build(build)
      Ci::RetryJobService.new(project, current_user).execute(build)[:job]
    end

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

      if job_variables_attributes.present? && !can?(current_user, :set_pipeline_variables, project)
        raise Gitlab::Access::AccessDeniedError
      end
    end
  end
end

Ci::PlayBuildService.prepend_mod_with('Ci::PlayBuildService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::PlayBuildService, '#execute', feature_category: :continuous_integration do
  let(:user) { create(:user, developer_projects: [project]) }
  let(:project) { create(:project) }
  let(:pipeline) { create(:ci_pipeline, project: project) }
  let(:build) { create(:ci_build, :manual, pipeline: pipeline) }

  let(:service) do
    described_class.new(project, user)
  end

  context 'when project does not have repository yet' do
    let(:project) { create(:project) }

    it 'allows user to play build if protected branch rules are met' do
      create(:protected_branch, :developers_can_merge, name: build.ref, project: project)

      service.execute(build)

      expect(build.reload).to be_pending
    end

    it 'does not allow user with developer role to play build' do
      expect { service.execute(build) }
        .to raise_error Gitlab::Access::AccessDeniedError
    end
  end

  context 'when project has repository' do
    let(:project) { create(:project, :repository) }

    it 'allows user with developer role to play a build' do
      service.execute(build)

      expect(build.reload).to be_pending
    end

    it 'prevents a blocked developer from playing a build' do
      user.block!

      expect { service.execute(build) }.to raise_error(Gitlab::Access::AccessDeniedError)
    end
  end

  context 'when build is a playable manual action' do
    let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
    let!(:branch) { create(:protected_branch, :developers_can_merge, name: build.ref, project: project) }

    it 'enqueues the build' do
      expect(service.execute(build)).to eq build
      expect(build.reload).to be_pending
    end

    it 'reassignes build user correctly' do
      service.execute(build)

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

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

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

    context 'when variables are supplied' do
      let(:job_variables) do
        [{ key: 'first', secret_value: 'first' },
         { key: 'second', secret_value: 'second' }]
      end

      subject { service.execute(build, job_variables) }

      it 'assigns the variables to the build' do
        subject

        expect(build.reload.job_variables.map(&:key)).to contain_exactly('first', 'second')
      end

      context 'and variables are invalid' do
        let(:job_variables) { [{}] }

        it 'resets the attributes of the build' do
          build.update!(job_variables_attributes: [{ key: 'old', value: 'old variable' }])

          subject

          expect(build.job_variables.map(&:key)).to contain_exactly('old')
        end
      end

      context 'when user defined variables are restricted' do
        before do
          project.update!(restrict_user_defined_variables: true)
        end

        context 'when user is maintainer' do
          before do
            project.add_maintainer(user)
          end

          it 'assigns the variables to the build' do
            subject

            expect(build.reload.job_variables.map(&:key)).to contain_exactly('first', 'second')
          end
        end

        context 'when user is developer' do
          it 'raises an error' do
            expect { subject }.to raise_error Gitlab::Access::AccessDeniedError
          end
        end
      end
    end
  end

  context 'when build is not a playable manual action' do
    let(:build) { create(:ci_build, :success, pipeline: pipeline) }
    let!(:branch) { create(:protected_branch, :developers_can_merge, name: build.ref, project: project) }

    it 'duplicates the build' do
      duplicate = service.execute(build)

      expect(duplicate).not_to eq build
      expect(duplicate).to be_pending
    end

    it 'assigns users correctly' do
      duplicate = service.execute(build)

      expect(build.user).not_to eq user
      expect(duplicate.user).to eq user
    end

    context 'and is not retryable' do
      let(:build) { create(:ci_build, :deployment_rejected, pipeline: pipeline) }

      it 'does not duplicate the build' do
        expect { service.execute(build) }.not_to change { Ci::Build.count }
      end

      it 'does not enqueue the build' do
        expect { service.execute(build) }.not_to change { build.status }
      end
    end
  end

  context 'when build is not action' do
    let(:user) { create(:user) }
    let(:build) { create(:ci_build, :success, pipeline: pipeline) }

    it 'raises an error' do
      expect { service.execute(build) }
        .to raise_error Gitlab::Access::AccessDeniedError
    end
  end

  context 'when user does not have ability to trigger action' do
    let(:user) { create(:user) }
    let!(:branch) { create(:protected_branch, :developers_can_merge, name: build.ref, project: project) }

    it 'raises an error' do
      expect { service.execute(build) }
        .to raise_error Gitlab::Access::AccessDeniedError
    end
  end
end