diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 6624d31a51d3c8037f45f4936e3439850a0e082f..e8f3ac2813ff6704346004f55b915ca1a70db009 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -154,6 +154,7 @@ variables:
   KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/report-master.json
   RSPEC_CHANGED_FILES_PATH: rspec/changed_files.txt
   RSPEC_FOSS_IMPACT_PIPELINE_TEMPLATE_YML: .gitlab/ci/rails/rspec-foss-impact.gitlab-ci.yml.erb
+  RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML: .gitlab/ci/rails/rspec-predictive.gitlab-ci.yml.erb
   RSPEC_LAST_RUN_RESULTS_FILE: rspec/rspec_last_run_results.txt
   RSPEC_MATCHING_JS_FILES_PATH: rspec/js_matching_files.txt
   RSPEC_VIEWS_INCLUDING_PARTIALS_PATH: rspec/views_including_partials.txt
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 89445fb931565548d78a490e8edf4e9b6a1f1e0c..d5480999d87bed13be28cba2833e906c3141d189 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -74,12 +74,6 @@ rspec migration pg13:
     - .rails:rules:ee-and-foss-migration
     - .rspec-migration-parallel
 
-rspec migration pg13 predictive:
-  extends:
-    - rspec migration pg13
-    - .predictive-rspec-tests
-    - .rails:rules:ee-and-foss-migration:predictive
-
 rspec background_migration pg13:
   extends:
     - .rspec-base-pg13
@@ -87,12 +81,6 @@ rspec background_migration pg13:
     - .rails:rules:ee-and-foss-background-migration
     - .rspec-background-migration-parallel
 
-rspec background_migration pg13 predictive:
-  extends:
-    - rspec background_migration pg13
-    - .predictive-rspec-tests
-    - .rails:rules:ee-and-foss-background-migration:predictive
-
 rspec migration pg13 single-db:
   extends:
     - rspec migration pg13
@@ -135,12 +123,6 @@ rspec unit pg13:
     - .rails:rules:ee-and-foss-unit
     - .rspec-unit-parallel
 
-rspec unit pg13 predictive:
-  extends:
-    - rspec unit pg13
-    - .predictive-rspec-tests
-    - .rails:rules:ee-and-foss-unit:predictive
-
 rspec unit pg13 single-db:
   extends:
     - rspec unit pg13
@@ -165,12 +147,6 @@ rspec integration pg13:
     - .rails:rules:ee-and-foss-integration
     - .rspec-integration-parallel
 
-rspec integration pg13 predictive:
-  extends:
-    - rspec integration pg13
-    - .predictive-rspec-tests
-    - .rails:rules:ee-and-foss-integration:predictive
-
 rspec integration pg13 single-db:
   extends:
     - rspec integration pg13
@@ -197,12 +173,6 @@ rspec system pg13:
   variables:
     DEBUG_GITLAB_TRANSACTION_STACK: "true"
 
-rspec system pg13 predictive:
-  extends:
-    - rspec system pg13
-    - .predictive-rspec-tests
-    - .rails:rules:ee-and-foss-system:predictive
-
 rspec system pg13 single-db:
   extends:
     - rspec system pg13
@@ -305,24 +275,12 @@ rspec:coverage:
     - rspec unit pg13
     - rspec integration pg13
     - rspec system pg13
-    # FOSS/EE predictive jobs
-    - rspec migration pg13 predictive
-    - rspec background_migration pg13 predictive
-    - rspec unit pg13 predictive
-    - rspec integration pg13 predictive
-    - rspec system pg13 predictive
     # EE jobs
     - rspec-ee migration pg13
     - rspec-ee background_migration pg13
     - rspec-ee unit pg13
     - rspec-ee integration pg13
     - rspec-ee system pg13
-    # EE predictive jobs
-    - rspec-ee migration pg13 predictive
-    - rspec-ee background_migration pg13 predictive
-    - rspec-ee unit pg13 predictive
-    - rspec-ee integration pg13 predictive
-    - rspec-ee system pg13 predictive
     # Memory jobs
     - memory-on-boot
   script:
@@ -402,6 +360,56 @@ rspec:flaky-tests-report:
 
 ##################################################
 # EE: default refs (MRs, default branch, schedules) jobs #
+rspec-predictive:pipeline-generate:
+  extends:
+    - .rails:rules:rspec-predictive
+  stage: prepare
+  needs: ["detect-tests", "retrieve-tests-metadata"]
+  script:
+    - scripts/generate_rspec_pipeline.rb -t "${RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML}" -k "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" -f "${RSPEC_MATCHING_TESTS_FOSS_PATH}" -o "${RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML}.yml"
+    - scripts/generate_rspec_pipeline.rb -t "${RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML}" -k "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" -f "${RSPEC_MATCHING_TESTS_EE_PATH}" -o "${RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML}-ee.yml" -p "ee/"
+    - echo "Content of ${RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML}.yml:"
+    - cat "${RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML}.yml"
+    - echo "\n================================================\n"
+    - echo "Content of ${RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML}-ee.yml:"
+    - cat "${RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML}-ee.yml"
+  artifacts:
+    expire_in: 1 day
+    paths:
+      - "${RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML}.yml"
+      - "${RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML}-ee.yml"
+
+rspec:predictive:trigger:
+  extends:
+    - .rails:rules:rspec-predictive
+  stage: test
+  needs:
+    - job: "setup-test-env"
+      artifacts: false
+    - job: "retrieve-tests-metadata"
+      artifacts: false
+    - job: "compile-test-assets"
+      artifacts: false
+    - job: "rspec-predictive:pipeline-generate"
+      artifacts: true
+  variables:
+    PARENT_PIPELINE_ID: $CI_PIPELINE_ID
+  trigger:
+    strategy: depend
+    forward:
+      yaml_variables: true
+      pipeline_variables: true
+    include:
+      - artifact: "${RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML}.yml"
+        job: rspec-predictive:pipeline-generate
+
+rspec-ee:predictive:trigger:
+  extends: rspec:predictive:trigger
+  trigger:
+    include:
+      - artifact: "${RSPEC_PREDICTIVE_PIPELINE_TEMPLATE_YML}-ee.yml"
+        job: rspec-predictive:pipeline-generate
+
 rspec migration pg13-as-if-foss:
   extends:
     - .rspec-base-pg13-as-if-foss
@@ -409,12 +417,6 @@ rspec migration pg13-as-if-foss:
     - .rails:rules:as-if-foss-migration
     - .rspec-migration-parallel
 
-rspec migration pg13-as-if-foss predictive:
-  extends:
-    - rspec migration pg13-as-if-foss
-    - .predictive-rspec-tests
-    - .rails:rules:as-if-foss-migration:predictive
-
 rspec background_migration pg13-as-if-foss:
   extends:
     - .rspec-base-pg13-as-if-foss
@@ -422,12 +424,6 @@ rspec background_migration pg13-as-if-foss:
     - .rails:rules:as-if-foss-background-migration
     - .rspec-background-migration-parallel
 
-rspec background_migration pg13-as-if-foss predictive:
-  extends:
-    - rspec background_migration pg13-as-if-foss
-    - .predictive-rspec-tests
-    - .rails:rules:as-if-foss-background-migration:predictive
-
 rspec migration pg13-as-if-foss single-db:
   extends:
     - rspec migration pg13-as-if-foss
@@ -458,12 +454,6 @@ rspec unit pg13-as-if-foss:
     - .rails:rules:as-if-foss-unit
     - .rspec-unit-parallel
 
-rspec unit pg13-as-if-foss predictive:
-  extends:
-    - rspec unit pg13-as-if-foss
-    - .predictive-rspec-tests
-    - .rails:rules:as-if-foss-unit:predictive
-
 rspec unit pg13-as-if-foss single-db:
   extends:
     - rspec unit pg13-as-if-foss
@@ -482,12 +472,6 @@ rspec integration pg13-as-if-foss:
     - .rails:rules:as-if-foss-integration
     - .rspec-integration-parallel
 
-rspec integration pg13-as-if-foss predictive:
-  extends:
-    - rspec integration pg13-as-if-foss
-    - .predictive-rspec-tests
-    - .rails:rules:as-if-foss-integration:predictive
-
 rspec integration pg13-as-if-foss single-db:
   extends:
     - rspec integration pg13-as-if-foss
@@ -506,12 +490,6 @@ rspec system pg13-as-if-foss:
     - .rails:rules:as-if-foss-system
     - .rspec-system-parallel
 
-rspec system pg13-as-if-foss predictive:
-  extends:
-    - rspec system pg13-as-if-foss
-    - .predictive-rspec-tests
-    - .rails:rules:as-if-foss-system:predictive
-
 rspec system pg13-as-if-foss single-db:
   extends:
     - rspec system pg13-as-if-foss
@@ -531,12 +509,6 @@ rspec-ee migration pg13:
     - .rails:rules:ee-only-migration
     - .rspec-ee-migration-parallel
 
-rspec-ee migration pg13 predictive:
-  extends:
-    - rspec-ee migration pg13
-    - .predictive-rspec-tests
-    - .rails:rules:ee-only-migration:predictive
-
 rspec-ee background_migration pg13:
   extends:
     - .rspec-ee-base-pg13
@@ -544,12 +516,6 @@ rspec-ee background_migration pg13:
     - .rails:rules:ee-only-background-migration
     - .rspec-ee-background-migration-parallel
 
-rspec-ee background_migration pg13 predictive:
-  extends:
-    - rspec-ee background_migration pg13
-    - .predictive-rspec-tests
-    - .rails:rules:ee-only-background-migration:predictive
-
 rspec-ee migration pg13 single-db:
   extends:
     - rspec-ee migration pg13
@@ -597,12 +563,6 @@ rspec-ee unit pg13 es8:
     - .rspec-ee-base-pg13-es8
     - .rspec-ee-unit-parallel
 
-rspec-ee unit pg13 predictive:
-  extends:
-    - rspec-ee unit pg13
-    - .predictive-rspec-tests
-    - .rails:rules:ee-only-unit:predictive
-
 rspec-ee unit pg13 single-db:
   extends:
     - rspec-ee unit pg13
@@ -626,12 +586,6 @@ rspec-ee integration pg13 es8:
     - .rspec-ee-base-pg13-es8
     - .rspec-ee-integration-parallel
 
-rspec-ee integration pg13 predictive:
-  extends:
-    - rspec-ee integration pg13
-    - .predictive-rspec-tests
-    - .rails:rules:ee-only-integration:predictive
-
 rspec-ee integration pg13 single-db:
   extends:
     - rspec-ee integration pg13
@@ -655,12 +609,6 @@ rspec-ee system pg13 es8:
     - .rspec-ee-base-pg13-es8
     - .rspec-ee-system-parallel
 
-rspec-ee system pg13 predictive:
-  extends:
-    - rspec-ee system pg13
-    - .predictive-rspec-tests
-    - .rails:rules:ee-only-system:predictive
-
 rspec-ee system pg13 single-db:
   extends:
     - rspec-ee system pg13
diff --git a/.gitlab/ci/rails/rspec-foss-impact.gitlab-ci.yml.erb b/.gitlab/ci/rails/rspec-foss-impact.gitlab-ci.yml.erb
index 38d964af62acceef175a05fcc8841d4d58e1b4b2..e7a1ee6022fe55f34f6fe62474f0887b2042418c 100644
--- a/.gitlab/ci/rails/rspec-foss-impact.gitlab-ci.yml.erb
+++ b/.gitlab/ci/rails/rspec-foss-impact.gitlab-ci.yml.erb
@@ -1,4 +1,4 @@
-# RSpec FOSS impact pipeline loaded dynamically by script: scripts/generate-rspec-foss-impact-pipeline
+# RSpec FOSS impact pipeline loaded dynamically by script: scripts/generate_rspec_pipeline.rb
 
 include:
   - local: .gitlab/ci/rails/shared.gitlab-ci.yml
diff --git a/.gitlab/ci/rails/rspec-predictive.gitlab-ci.yml.erb b/.gitlab/ci/rails/rspec-predictive.gitlab-ci.yml.erb
new file mode 100644
index 0000000000000000000000000000000000000000..fcd8754c76a1428e6b0716283d8f025df4c8fa34
--- /dev/null
+++ b/.gitlab/ci/rails/rspec-predictive.gitlab-ci.yml.erb
@@ -0,0 +1,153 @@
+# RSpec preditive pipeline loaded dynamically by script: scripts/generate_rspec_pipeline.rb
+
+include:
+  - local: .gitlab/ci/rails/shared.gitlab-ci.yml
+
+default:
+  image: $DEFAULT_CI_IMAGE
+  tags:
+    - gitlab-org
+  # Default job timeout set to 90m https://gitlab.com/gitlab-com/gl-infra/infrastructure/-/issues/10520
+  timeout: 90m
+  interruptible: true
+
+stages:
+  - test
+
+dont-interrupt-me:
+  extends: .rules:dont-interrupt
+  stage: .pre
+  interruptible: false
+  script:
+    - echo "This jobs makes sure this pipeline won't be interrupted! See https://docs.gitlab.com/ee/ci/yaml/#interruptible."
+
+.base-predictive:
+  needs:
+    - pipeline: $PARENT_PIPELINE_ID
+      job: detect-tests
+    - pipeline: $PARENT_PIPELINE_ID
+      job: setup-test-env
+    - pipeline: $PARENT_PIPELINE_ID
+      job: retrieve-tests-metadata
+    - pipeline: $PARENT_PIPELINE_ID
+      job: compile-test-assets
+  rules:
+    - when: always
+  variables:
+    RSPEC_TESTS_MAPPING_ENABLED: "true"
+
+<% if test_suite_prefix.nil? %>
+.base-rspec-predictive:
+  extends:
+    - .rspec-base-pg12
+    - .base-predictive
+  variables:
+    # We're using the FOSS one here because we want to exclude EE-only ones
+    # For EE-only ones, we have EE-only jobs.
+    RSPEC_TESTS_FILTER_FILE: "${RSPEC_MATCHING_TESTS_FOSS_PATH}"
+
+<% if rspec_files_per_test_level.dig(:migration, :files).size > 0 %>
+rspec migration predictive:
+  extends:
+    - .base-rspec-predictive
+    - .rspec-base-migration
+<% if rspec_files_per_test_level.dig(:migration, :parallelization) > 1 %>
+  parallel: <%= rspec_files_per_test_level.dig(:migration, :parallelization) %>
+<% end %>
+<% end %>
+
+<% if rspec_files_per_test_level.dig(:background_migration, :files).size > 0 %>
+rspec background_migration predictive:
+  extends:
+    - .base-rspec-predictive
+    - .rspec-base-migration
+<% if rspec_files_per_test_level.dig(:background_migration, :parallelization) > 1 %>
+  parallel: <%= rspec_files_per_test_level.dig(:background_migration, :parallelization) %>
+<% end %>
+<% end %>
+
+<% if rspec_files_per_test_level.dig(:unit, :files).size > 0 %>
+rspec unit predictive:
+  extends:
+    - .base-rspec-predictive
+<% if rspec_files_per_test_level.dig(:unit, :parallelization) > 1 %>
+  parallel: <%= rspec_files_per_test_level.dig(:unit, :parallelization) %>
+<% end %>
+<% end %>
+
+<% if rspec_files_per_test_level.dig(:integration, :files).size > 0 %>
+rspec integration predictive:
+  extends:
+    - .base-rspec-predictive
+<% if rspec_files_per_test_level.dig(:integration, :parallelization) > 1 %>
+  parallel: <%= rspec_files_per_test_level.dig(:integration, :parallelization) %>
+<% end %>
+<% end %>
+
+<% if rspec_files_per_test_level.dig(:system, :files).size > 0 %>
+rspec system predictive:
+  extends:
+    - .base-rspec-predictive
+<% if rspec_files_per_test_level.dig(:system, :parallelization) > 1 %>
+  parallel: <%= rspec_files_per_test_level.dig(:system, :parallelization) %>
+<% end %>
+<% end %>
+
+<% end %>
+
+<% if test_suite_prefix == 'ee/' %>
+.base-rspec-ee-predictive:
+  extends:
+    - .rspec-ee-base-pg12
+    - .base-predictive
+  variables:
+    RSPEC_TESTS_FILTER_FILE: "${RSPEC_MATCHING_TESTS_EE_PATH}"
+
+<% if rspec_files_per_test_level.dig(:migration, :files).size > 0 %>
+rspec-ee migration predictive:
+  extends:
+    - .base-rspec-ee-predictive
+    - .rspec-base-migration
+<% if rspec_files_per_test_level.dig(:migration, :parallelization) > 1 %>
+  parallel: <%= rspec_files_per_test_level.dig(:migration, :parallelization) %>
+<% end %>
+<% end %>
+
+<% if rspec_files_per_test_level.dig(:background_migration, :files).size > 0 %>
+rspec-ee background_migration predictive:
+  extends:
+    - .base-rspec-ee-predictive
+    - .rspec-base-migration
+<% if rspec_files_per_test_level.dig(:background_migration, :parallelization) > 1 %>
+  parallel: <%= rspec_files_per_test_level.dig(:background_migration, :parallelization) %>
+<% end %>
+<% end %>
+
+<% if rspec_files_per_test_level.dig(:unit, :files).size > 0 %>
+rspec-ee unit predictive:
+  extends:
+    - .base-rspec-ee-predictive
+<% if rspec_files_per_test_level.dig(:unit, :parallelization) > 1 %>
+  parallel: <%= rspec_files_per_test_level.dig(:unit, :parallelization) %>
+<% end %>
+<% end %>
+
+<% if rspec_files_per_test_level.dig(:integration, :files).size > 0 %>
+rspec-ee integration predictive:
+  extends:
+    - .base-rspec-ee-predictive
+<% if rspec_files_per_test_level.dig(:integration, :parallelization) > 1 %>
+  parallel: <%= rspec_files_per_test_level.dig(:integration, :parallelization) %>
+<% end %>
+<% end %>
+
+<% if rspec_files_per_test_level.dig(:system, :files).size > 0 %>
+rspec-ee system predictive:
+  extends:
+    - .base-rspec-ee-predictive
+<% if rspec_files_per_test_level.dig(:system, :parallelization) > 1 %>
+  parallel: <%= rspec_files_per_test_level.dig(:system, :parallelization) %>
+<% end %>
+<% end %>
+
+<% end %>
diff --git a/.gitlab/ci/rails/shared.gitlab-ci.yml b/.gitlab/ci/rails/shared.gitlab-ci.yml
index 62e8547fa5af4ac4add77d76ecefa2835ae1ad05..adcfcd2010f360759fa2e315980ecbe590923a2c 100644
--- a/.gitlab/ci/rails/shared.gitlab-ci.yml
+++ b/.gitlab/ci/rails/shared.gitlab-ci.yml
@@ -28,10 +28,6 @@ include:
     - run_timed_command "scripts/gitaly-test-spawn"  # Do not use 'bundle exec' here
     - echo -e "\e[0Ksection_end:`date +%s`:gitaly-test-spawn\r\e[0K"
 
-.predictive-rspec-tests:
-  variables:
-    RSPEC_TESTS_MAPPING_ENABLED: "true"
-
 .single-db:
   variables:
     DECOMPOSED_DB: "false"
@@ -61,7 +57,6 @@ include:
     RUBY_GC_MALLOC_LIMIT_MAX: 134217728
     RECORD_DEPRECATIONS: "true"
     GEO_SECONDARY_PROXY: 0
-    RSPEC_TESTS_FILTER_FILE: "${RSPEC_MATCHING_TESTS_PATH}"
     SUCCESSFULLY_RETRIED_TEST_EXIT_CODE: 137
   needs:
     - job: "setup-test-env"
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index c61819d0a4c99d686da5d6b4572ea4d51105d8b0..9930efd90b5798314a87071eb0eb5a7f3a17781d 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -593,7 +593,6 @@
 ##################
 # Conditions set #
 ##################
-
 .strict-ee-only-rules:
   rules:
     - <<: *if-not-ee
@@ -610,15 +609,6 @@
     - <<: *if-merge-request-labels-pipeline-expedite
       when: never
 
-.rails:rules:predictive-default-rules:
-  rules:
-    - <<: *if-merge-request-approved
-      when: never
-    - <<: *if-automated-merge-request
-      when: never
-    - <<: *if-security-merge-request
-      when: never
-
 .rails:rules:run-search-tests:
   rules:
     - !reference [".rails:rules:default-branch-schedule-nightly--code-backstage-ee-only", rules]
@@ -639,6 +629,40 @@
     - <<: *if-merge-request-not-approved
       when: never
 
+.rails:rules:system-default-rules:
+  rules:
+    - <<: *if-merge-request-labels-run-all-rspec
+    - <<: *if-merge-request
+      changes: *core-backend-patterns
+    - <<: *if-merge-request
+      changes: *workhorse-patterns
+    - <<: *if-automated-merge-request
+      changes: *code-backstage-patterns
+    - <<: *if-security-merge-request
+      changes: *code-backstage-patterns
+    - <<: *if-merge-request-not-approved
+      when: never
+
+.rails:rules:previous-failed-tests-default-rules:
+  rules:
+    - <<: *if-security-merge-request
+      when: never
+    - <<: *if-merge-request-labels-run-all-rspec
+    - <<: *if-merge-request
+      changes: *code-backstage-patterns
+
+###########################
+# Conditions set for JiHu #
+###########################
+.rails:rules:predictive-default-rules:
+  rules:
+    - <<: *if-merge-request-approved
+      when: never
+    - <<: *if-automated-merge-request
+      when: never
+    - <<: *if-security-merge-request
+      when: never
+
 .rails:rules:as-if-foss-migration-unit-integration:predictive-default-rules:
   rules:
     - <<: *if-merge-request
@@ -654,43 +678,115 @@
       when: never
     - !reference [".rails:rules:as-if-foss-migration-unit-integration:predictive-default-rules", rules]
 
-.rails:rules:system-default-rules:
+.rails:rules:system:predictive-default-rules:
   rules:
     - <<: *if-merge-request-labels-run-all-rspec
+      when: never
     - <<: *if-merge-request
       changes: *core-backend-patterns
+      when: never
     - <<: *if-merge-request
       changes: *workhorse-patterns
-    - <<: *if-automated-merge-request
-      changes: *code-backstage-patterns
-    - <<: *if-security-merge-request
-      changes: *code-backstage-patterns
-    - <<: *if-merge-request-not-approved
       when: never
+    - <<: *if-merge-request
+      changes: *ci-patterns
+      when: never
+    - <<: *if-merge-request
+      changes: *code-backstage-patterns
 
-.rails:rules:system:predictive-default-rules:
+.rails:rules:ee-and-foss-migration:predictive:
   rules:
-    - <<: *if-merge-request-labels-run-all-rspec
+    - <<: *if-fork-merge-request
+      changes: *db-patterns
+    - !reference [".rails:rules:predictive-default-rules", rules]
+    - !reference [".rails:rules:unit-integration:predictive-default-rules", rules]
+    # When DB schema changes, many migrations spec may be affected. However, the test mapping from Crystalball does not map db change to a specific migration spec well.
+    # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68840.
+    - <<: *if-merge-request
+      changes: *db-patterns
       when: never
+
+.rails:rules:ee-and-foss-background-migration:predictive:
+  rules:
+    - !reference [".rails:rules:ee-and-foss-migration:predictive", rules]
     - <<: *if-merge-request
-      changes: *core-backend-patterns
+      changes: *backend-patterns
+
+.rails:rules:ee-and-foss-unit:predictive:
+  rules:
+    - <<: *if-fork-merge-request
+      changes: *backend-patterns
+    - !reference [".rails:rules:predictive-default-rules", rules]
+    - !reference [".rails:rules:unit-integration:predictive-default-rules", rules]
+    - <<: *if-merge-request
+      changes: *backend-patterns
+    - <<: *if-merge-request
+      changes: *backstage-patterns
+
+.rails:rules:ee-and-foss-integration:predictive:
+  rules:
+    - <<: *if-fork-merge-request
+      changes: *backend-patterns
+    - !reference [".rails:rules:predictive-default-rules", rules]
+    - !reference [".rails:rules:unit-integration:predictive-default-rules", rules]
+    - <<: *if-merge-request
+      changes: *backend-patterns
+
+.rails:rules:ee-and-foss-system:predictive:
+  rules:
+    - <<: *if-fork-merge-request
+      changes: *code-backstage-patterns
+    - !reference [".rails:rules:predictive-default-rules", rules]
+    - !reference [".rails:rules:system:predictive-default-rules", rules]
+
+.rails:rules:ee-only-migration:predictive:
+  rules:
+    - <<: *if-not-ee
       when: never
+    - !reference [".rails:rules:predictive-default-rules", rules]
+    - !reference [".rails:rules:unit-integration:predictive-default-rules", rules]
+    # When DB schema changes, many migrations spec may be affected. However, the test mapping from Crystalball does not map db change to a specific migration spec well.
+    # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68840.
     - <<: *if-merge-request
-      changes: *workhorse-patterns
+      changes: *db-patterns
       when: never
+
+.rails:rules:ee-only-background-migration:predictive:
+  rules:
+    - !reference [".rails:rules:ee-only-migration:predictive", rules]
     - <<: *if-merge-request
-      changes: *ci-patterns
+      changes: *backend-patterns
+
+.rails:rules:ee-only-unit:predictive:
+  rules:
+    - <<: *if-not-ee
       when: never
+    - <<: *if-fork-merge-request
+      changes: *backend-patterns
+    - !reference [".rails:rules:predictive-default-rules", rules]
+    - !reference [".rails:rules:unit-integration:predictive-default-rules", rules]
     - <<: *if-merge-request
-      changes: *code-backstage-patterns
+      changes: *backend-patterns
 
-.rails:rules:previous-failed-tests-default-rules:
+.rails:rules:ee-only-integration:predictive:
   rules:
-    - <<: *if-security-merge-request
+    - <<: *if-not-ee
       when: never
-    - <<: *if-merge-request-labels-run-all-rspec
+    - <<: *if-fork-merge-request
+      changes: *backend-patterns
+    - !reference [".rails:rules:predictive-default-rules", rules]
+    - !reference [".rails:rules:unit-integration:predictive-default-rules", rules]
     - <<: *if-merge-request
+      changes: *backend-patterns
+
+.rails:rules:ee-only-system:predictive:
+  rules:
+    - <<: *if-not-ee
+      when: never
+    - <<: *if-fork-merge-request
       changes: *code-backstage-patterns
+    - !reference [".rails:rules:predictive-default-rules", rules]
+    - !reference [".rails:rules:system:predictive-default-rules", rules]
 
 ################
 # Shared rules #
@@ -1335,17 +1431,18 @@
     - <<: *if-default-refs
       changes: *db-patterns
 
-.rails:rules:ee-and-foss-migration:predictive:
+.rails:rules:rspec-predictive:
   rules:
-    - <<: *if-fork-merge-request
-      changes: *db-patterns
-    - !reference [".rails:rules:predictive-default-rules", rules]
-    - !reference [".rails:rules:unit-integration:predictive-default-rules", rules]
-    # When DB schema changes, many migrations spec may be affected. However, the test mapping from Crystalball does not map db change to a specific migration spec well.
-    # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68840.
-    - <<: *if-merge-request
-      changes: *db-patterns
+    - <<: *if-merge-request-approved
+      when: never
+    - <<: *if-automated-merge-request
+      when: never
+    - <<: *if-security-merge-request
+      when: never
+    - <<: *if-merge-request-labels-run-all-rspec
       when: never
+    - <<: *if-merge-request
+      changes: *code-backstage-patterns
 
 .rails:rules:ee-and-foss-background-migration:
   rules:
@@ -1353,12 +1450,6 @@
     - <<: *if-default-refs
       changes: *backend-patterns
 
-.rails:rules:ee-and-foss-background-migration:predictive:
-  rules:
-    - !reference [".rails:rules:ee-and-foss-migration:predictive", rules]
-    - <<: *if-merge-request
-      changes: *backend-patterns
-
 .rails:rules:ee-and-foss-mr-with-migration:
   rules:
     - <<: *if-merge-request
@@ -1383,17 +1474,6 @@
     - <<: *if-default-refs
       changes: *backstage-patterns
 
-.rails:rules:ee-and-foss-unit:predictive:
-  rules:
-    - <<: *if-fork-merge-request
-      changes: *backend-patterns
-    - !reference [".rails:rules:predictive-default-rules", rules]
-    - !reference [".rails:rules:unit-integration:predictive-default-rules", rules]
-    - <<: *if-merge-request
-      changes: *backend-patterns
-    - <<: *if-merge-request
-      changes: *backstage-patterns
-
 .rails:rules:ee-and-foss-integration:
   rules:
     - <<: *if-fork-merge-request
@@ -1402,15 +1482,6 @@
     - <<: *if-default-refs
       changes: *backend-patterns
 
-.rails:rules:ee-and-foss-integration:predictive:
-  rules:
-    - <<: *if-fork-merge-request
-      changes: *backend-patterns
-    - !reference [".rails:rules:predictive-default-rules", rules]
-    - !reference [".rails:rules:unit-integration:predictive-default-rules", rules]
-    - <<: *if-merge-request
-      changes: *backend-patterns
-
 .rails:rules:ee-and-foss-system:
   rules:
     - <<: *if-fork-merge-request
@@ -1419,13 +1490,6 @@
     - <<: *if-default-refs
       changes: *code-backstage-patterns
 
-.rails:rules:ee-and-foss-system:predictive:
-  rules:
-    - <<: *if-fork-merge-request
-      changes: *code-backstage-patterns
-    - !reference [".rails:rules:predictive-default-rules", rules]
-    - !reference [".rails:rules:system:predictive-default-rules", rules]
-
 .rails:rules:ee-and-foss-fast_spec_helper:
   rules:
     - <<: *if-merge-request-labels-run-all-rspec
@@ -1460,30 +1524,12 @@
     - <<: *if-default-refs
       changes: *db-patterns
 
-.rails:rules:ee-only-migration:predictive:
-  rules:
-    - <<: *if-not-ee
-      when: never
-    - !reference [".rails:rules:predictive-default-rules", rules]
-    - !reference [".rails:rules:unit-integration:predictive-default-rules", rules]
-    # When DB schema changes, many migrations spec may be affected. However, the test mapping from Crystalball does not map db change to a specific migration spec well.
-    # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68840.
-    - <<: *if-merge-request
-      changes: *db-patterns
-      when: never
-
 .rails:rules:ee-only-background-migration:
   rules:
     - !reference [".rails:rules:ee-only-migration", rules]
     - <<: *if-default-refs
       changes: *backend-patterns
 
-.rails:rules:ee-only-background-migration:predictive:
-  rules:
-    - !reference [".rails:rules:ee-only-migration:predictive", rules]
-    - <<: *if-merge-request
-      changes: *backend-patterns
-
 .rails:rules:ee-only-unit:
   rules:
     - <<: *if-not-ee
@@ -1494,17 +1540,6 @@
     - <<: *if-default-refs
       changes: *backend-patterns
 
-.rails:rules:ee-only-unit:predictive:
-  rules:
-    - <<: *if-not-ee
-      when: never
-    - <<: *if-fork-merge-request
-      changes: *backend-patterns
-    - !reference [".rails:rules:predictive-default-rules", rules]
-    - !reference [".rails:rules:unit-integration:predictive-default-rules", rules]
-    - <<: *if-merge-request
-      changes: *backend-patterns
-
 .rails:rules:ee-only-integration:
   rules:
     - <<: *if-not-ee
@@ -1515,17 +1550,6 @@
     - <<: *if-default-refs
       changes: *backend-patterns
 
-.rails:rules:ee-only-integration:predictive:
-  rules:
-    - <<: *if-not-ee
-      when: never
-    - <<: *if-fork-merge-request
-      changes: *backend-patterns
-    - !reference [".rails:rules:predictive-default-rules", rules]
-    - !reference [".rails:rules:unit-integration:predictive-default-rules", rules]
-    - <<: *if-merge-request
-      changes: *backend-patterns
-
 .rails:rules:ee-only-system:
   rules:
     - <<: *if-not-ee
@@ -1536,15 +1560,6 @@
     - <<: *if-default-refs
       changes: *code-backstage-patterns
 
-.rails:rules:ee-only-system:predictive:
-  rules:
-    - <<: *if-not-ee
-      when: never
-    - <<: *if-fork-merge-request
-      changes: *code-backstage-patterns
-    - !reference [".rails:rules:predictive-default-rules", rules]
-    - !reference [".rails:rules:system:predictive-default-rules", rules]
-
 .rails:rules:as-if-foss-migration:
   rules:
     - <<: *if-not-ee
@@ -1563,30 +1578,12 @@
     - <<: *if-merge-request-not-approved
       when: never
 
-.rails:rules:as-if-foss-migration:predictive:
-  rules:
-    - <<: *if-not-ee
-      when: never
-    - !reference [".rails:rules:predictive-default-rules", rules]
-    - !reference [".rails:rules:as-if-foss-migration-unit-integration:predictive-default-rules", rules]
-    # When DB schema changes, many migrations spec may be affected. However, the test mapping from Crystalball does not map db change to a specific migration spec well.
-    # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68840.
-    - <<: *if-merge-request-labels-as-if-foss
-      changes: *db-patterns
-      when: never
-
 .rails:rules:as-if-foss-background-migration:
   rules:
     - !reference [".rails:rules:as-if-foss-migration", rules]
     - <<: *if-merge-request-labels-as-if-foss
       changes: *backend-patterns
 
-.rails:rules:as-if-foss-background-migration:predictive:
-  rules:
-    - !reference [".rails:rules:as-if-foss-migration:predictive", rules]
-    - <<: *if-merge-request-labels-as-if-foss
-      changes: *backend-patterns
-
 .rails:rules:as-if-foss-unit:
   rules:
     - <<: *if-not-ee
@@ -1597,17 +1594,6 @@
     - <<: *if-merge-request-labels-as-if-foss
       changes: *backend-patterns
 
-.rails:rules:as-if-foss-unit:predictive:
-  rules:
-    - <<: *if-not-ee
-      when: never
-    - <<: *if-fork-merge-request
-      when: never
-    - !reference [".rails:rules:predictive-default-rules", rules]
-    - !reference [".rails:rules:as-if-foss-migration-unit-integration:predictive-default-rules", rules]
-    - <<: *if-merge-request-labels-as-if-foss
-      changes: *backend-patterns
-
 .rails:rules:as-if-foss-integration:
   rules:
     - <<: *if-not-ee
@@ -1618,17 +1604,6 @@
     - <<: *if-merge-request-labels-as-if-foss
       changes: *backend-patterns
 
-.rails:rules:as-if-foss-integration:predictive:
-  rules:
-    - <<: *if-not-ee
-      when: never
-    - <<: *if-fork-merge-request
-      when: never
-    - !reference [".rails:rules:predictive-default-rules", rules]
-    - !reference [".rails:rules:as-if-foss-migration-unit-integration:predictive-default-rules", rules]
-    - <<: *if-merge-request-labels-as-if-foss
-      changes: *backend-patterns
-
 .rails:rules:as-if-foss-system:
   rules:
     - <<: *if-not-ee
@@ -1639,25 +1614,6 @@
     - <<: *if-merge-request-labels-as-if-foss
       changes: *code-backstage-patterns
 
-.rails:rules:as-if-foss-system:predictive:
-  rules:
-    - <<: *if-not-ee
-      when: never
-    - <<: *if-fork-merge-request
-      when: never
-    - !reference [".rails:rules:predictive-default-rules", rules]
-    - <<: *if-merge-request
-      changes: *core-backend-patterns
-      when: never
-    - <<: *if-merge-request
-      changes: *workhorse-patterns
-      when: never
-    - <<: *if-merge-request
-      changes: *ci-patterns
-      when: never
-    - <<: *if-merge-request-labels-as-if-foss
-      changes: *code-backstage-patterns
-
 .rails:rules:ee-and-foss-db-library-code:
   rules:
     - <<: *if-default-refs
@@ -1749,6 +1705,10 @@
       when: never
     - <<: *if-merge-request-labels-skip-undercoverage
       when: never
+    # We cannot get the coverage data from child pipeline so we only run undercoverage on full pipelines for now
+    # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113410#note_1335422806
+    - <<: *if-merge-request-not-approved
+      when: never
     - <<: *if-merge-request-labels-run-all-rspec
     - <<: *if-merge-request
       changes: *backend-patterns
diff --git a/scripts/generate_rspec_pipeline.rb b/scripts/generate_rspec_pipeline.rb
index e226acc0430f4657362366889ae0e92138219afe..292b3d85b207b346aead9ba94d2490992eaa206d 100755
--- a/scripts/generate_rspec_pipeline.rb
+++ b/scripts/generate_rspec_pipeline.rb
@@ -43,12 +43,20 @@ class GenerateRspecPipeline
   DEFAULT_AVERAGE_TEST_FILE_DURATION_IN_SECONDS =
     DURATION_OF_THE_TEST_SUITE_IN_SECONDS / NUMBER_OF_TESTS_IN_TOTAL_IN_THE_TEST_SUITE
 
-  # rspec_files_path: A file containing RSpec files to run, separated by a space
   # pipeline_template_path: A YAML pipeline configuration template to generate the final pipeline config from
-  def initialize(pipeline_template_path:, rspec_files_path: nil, knapsack_report_path: nil)
+  # rspec_files_path: A file containing RSpec files to run, separated by a space
+  # knapsack_report_path: A file containing a Knapsack report
+  # test_suite_prefix: An optional test suite folder prefix (e.g. `ee/` or `jh/`)
+  # generated_pipeline_path: An optional filename where to write the pipeline config (defaults to
+  #                          `"#{pipeline_template_path}.yml"`)
+  def initialize(
+    pipeline_template_path:, rspec_files_path: nil, knapsack_report_path: nil, test_suite_prefix: nil,
+    generated_pipeline_path: nil)
     @pipeline_template_path = pipeline_template_path.to_s
     @rspec_files_path = rspec_files_path.to_s
     @knapsack_report_path = knapsack_report_path.to_s
+    @test_suite_prefix = test_suite_prefix
+    @generated_pipeline_path = generated_pipeline_path || "#{pipeline_template_path}.yml"
 
     raise ArgumentError unless File.exist?(@pipeline_template_path)
   end
@@ -56,11 +64,14 @@ def initialize(pipeline_template_path:, rspec_files_path: nil, knapsack_report_p
   def generate!
     if all_rspec_files.empty?
       info "Using #{SKIP_PIPELINE_YML_FILE} due to no RSpec files to run"
-      FileUtils.cp(SKIP_PIPELINE_YML_FILE, pipeline_filename)
+      FileUtils.cp(SKIP_PIPELINE_YML_FILE, generated_pipeline_path)
       return
     end
 
-    File.open(pipeline_filename, 'w') do |handle|
+    info "pipeline_template_path: #{pipeline_template_path}"
+    info "generated_pipeline_path: #{generated_pipeline_path}"
+
+    File.open(generated_pipeline_path, 'w') do |handle|
       pipeline_yaml = ERB.new(File.read(pipeline_template_path)).result_with_hash(**erb_binding)
       handle.write(pipeline_yaml.squeeze("\n").strip)
     end
@@ -68,7 +79,8 @@ def generate!
 
   private
 
-  attr_reader :pipeline_template_path, :rspec_files_path, :knapsack_report_path
+  attr_reader :pipeline_template_path, :rspec_files_path, :knapsack_report_path, :test_suite_prefix,
+    :generated_pipeline_path
 
   def info(text)
     $stdout.puts "[#{self.class.name}] #{text}"
@@ -78,12 +90,11 @@ def all_rspec_files
     @all_rspec_files ||= File.exist?(rspec_files_path) ? File.read(rspec_files_path).split(' ') : []
   end
 
-  def pipeline_filename
-    @pipeline_filename ||= "#{pipeline_template_path}.yml"
-  end
-
   def erb_binding
-    { rspec_files_per_test_level: rspec_files_per_test_level }
+    {
+      rspec_files_per_test_level: rspec_files_per_test_level,
+      test_suite_prefix: test_suite_prefix
+    }
   end
 
   def rspec_files_per_test_level
@@ -91,7 +102,7 @@ def rspec_files_per_test_level
       all_remaining_rspec_files = all_rspec_files.dup
       TEST_LEVELS.each_with_object(Hash.new { |h, k| h[k] = {} }) do |test_level, memo| # rubocop:disable Rails/IndexWith
         memo[test_level][:files] = all_remaining_rspec_files
-          .grep(Quality::TestLevel.new.regexp(test_level))
+          .grep(test_level_service.regexp(test_level, true))
           .tap { |files| files.each { |file| all_remaining_rspec_files.delete(file) } }
         memo[test_level][:parallelization] = optimal_nodes_count(test_level, memo[test_level][:files])
       end
@@ -125,10 +136,15 @@ def average_test_file_duration_in_seconds_per_test_level
         remaining_knapsack_report = knapsack_report.dup
         TEST_LEVELS.each_with_object({}) do |test_level, memo|
           matching_data_per_test_level = remaining_knapsack_report
-            .select { |test_file, _| test_file.match?(Quality::TestLevel.new.regexp(test_level)) }
+            .select { |test_file, _| test_file.match?(test_level_service.regexp(test_level, true)) }
             .tap { |test_data| test_data.each { |file, _| remaining_knapsack_report.delete(file) } }
+
           memo[test_level] =
-            matching_data_per_test_level.values.sum / matching_data_per_test_level.keys.size
+            if matching_data_per_test_level.empty?
+              DEFAULT_AVERAGE_TEST_FILE_DURATION_IN_SECONDS
+            else
+              matching_data_per_test_level.values.sum / matching_data_per_test_level.keys.size
+            end
         end
       else
         TEST_LEVELS.each_with_object({}) do |test_level, memo| # rubocop:disable Rails/IndexWith
@@ -146,6 +162,10 @@ def knapsack_report
         {}
       end
   end
+
+  def test_level_service
+    @test_level_service ||= Quality::TestLevel.new(test_suite_prefix)
+  end
 end
 
 if $PROGRAM_NAME == __FILE__
@@ -166,6 +186,15 @@ def knapsack_report
       options[:knapsack_report_path] = value
     end
 
+    opts.on("-p", "--test-suite-prefix test_suite_prefix", String, "Test suite folder prefix") do |value|
+      options[:test_suite_prefix] = value
+    end
+
+    opts.on("-o", "--generated-pipeline-path generated_pipeline_path", String, "Path where to write the pipeline " \
+                                                                               "config") do |value|
+      options[:generated_pipeline_path] = value
+    end
+
     opts.on("-h", "--help", "Prints this help") do
       puts opts
       exit
diff --git a/spec/scripts/generate_rspec_pipeline_spec.rb b/spec/scripts/generate_rspec_pipeline_spec.rb
index b3eaf9e9127224e903737f039cb5b76f4ea3fa1d..91b5739cf6386855a9a64145c8b0d2b677ef7d2f 100644
--- a/spec/scripts/generate_rspec_pipeline_spec.rb
+++ b/spec/scripts/generate_rspec_pipeline_spec.rb
@@ -13,42 +13,49 @@
         "spec/lib/gitlab/background_migration/a_spec.rb spec/lib/gitlab/background_migration/b_spec.rb " \
         "spec/models/a_spec.rb spec/models/b_spec.rb " \
         "spec/controllers/a_spec.rb spec/controllers/b_spec.rb " \
-        "spec/features/a_spec.rb spec/features/b_spec.rb"
+        "spec/features/a_spec.rb spec/features/b_spec.rb " \
+        "ee/spec/features/a_spec.rb"
     end
 
     let(:pipeline_template) { Tempfile.new(['pipeline_template', '.yml.erb']) }
     let(:pipeline_template_content) do
       <<~YAML
-      <% if rspec_files_per_test_level[:migration][:files].size > 0 %>
+      <% if test_suite_prefix.nil? && rspec_files_per_test_level[:migration][:files].size > 0 %>
       rspec migration:
       <% if rspec_files_per_test_level[:migration][:parallelization] > 1 %>
         parallel: <%= rspec_files_per_test_level[:migration][:parallelization] %>
       <% end %>
       <% end %>
-      <% if rspec_files_per_test_level[:background_migration][:files].size > 0 %>
+      <% if test_suite_prefix.nil? && rspec_files_per_test_level[:background_migration][:files].size > 0 %>
       rspec background_migration:
       <% if rspec_files_per_test_level[:background_migration][:parallelization] > 1 %>
         parallel: <%= rspec_files_per_test_level[:background_migration][:parallelization] %>
       <% end %>
       <% end %>
-      <% if rspec_files_per_test_level[:unit][:files].size > 0 %>
+      <% if test_suite_prefix.nil? && rspec_files_per_test_level[:unit][:files].size > 0 %>
       rspec unit:
       <% if rspec_files_per_test_level[:unit][:parallelization] > 1 %>
         parallel: <%= rspec_files_per_test_level[:unit][:parallelization] %>
       <% end %>
       <% end %>
-      <% if rspec_files_per_test_level[:integration][:files].size > 0 %>
+      <% if test_suite_prefix.nil? && rspec_files_per_test_level[:integration][:files].size > 0 %>
       rspec integration:
       <% if rspec_files_per_test_level[:integration][:parallelization] > 1 %>
         parallel: <%= rspec_files_per_test_level[:integration][:parallelization] %>
       <% end %>
       <% end %>
-      <% if rspec_files_per_test_level[:system][:files].size > 0 %>
+      <% if test_suite_prefix.nil? && rspec_files_per_test_level[:system][:files].size > 0 %>
       rspec system:
       <% if rspec_files_per_test_level[:system][:parallelization] > 1 %>
         parallel: <%= rspec_files_per_test_level[:system][:parallelization] %>
       <% end %>
       <% end %>
+      <% if test_suite_prefix == 'ee/' && rspec_files_per_test_level[:system][:files].size > 0 %>
+      rspec-ee system:
+      <% if rspec_files_per_test_level[:system][:parallelization] > 1 %>
+        parallel: <%= rspec_files_per_test_level[:system][:parallelization] %>
+      <% end %>
+      <% end %>
       YAML
     end
 
@@ -65,7 +72,8 @@
         "spec/controllers/a_spec.rb": 60.2,
         "spec/controllers/ab_spec.rb": 180.4,
         "spec/features/a_spec.rb": 360.1,
-        "spec/features/b_spec.rb": 180.5
+        "spec/features/b_spec.rb": 180.5,
+        "ee/spec/features/a_spec.rb": 180.5
       }
       JSON
     end
@@ -177,6 +185,53 @@
       end
     end
 
+    context 'when test_suite_prefix is given' do
+      subject do
+        described_class.new(
+          rspec_files_path: rspec_files.path,
+          pipeline_template_path: pipeline_template.path,
+          knapsack_report_path: knapsack_report.path,
+          test_suite_prefix: 'ee/'
+        )
+      end
+
+      it 'generates the pipeline config based on the test_suite_prefix' do
+        subject.generate!
+
+        expect(File.read("#{pipeline_template.path}.yml"))
+          .to eq("rspec-ee system:")
+      end
+    end
+
+    context 'when generated_pipeline_path is given' do
+      let(:custom_pipeline_filename) { Tempfile.new(['custom_pipeline_filename', '.yml']) }
+
+      around do |example|
+        example.run
+      ensure
+        custom_pipeline_filename.close
+        custom_pipeline_filename.unlink
+      end
+
+      subject do
+        described_class.new(
+          rspec_files_path: rspec_files.path,
+          pipeline_template_path: pipeline_template.path,
+          generated_pipeline_path: custom_pipeline_filename.path
+        )
+      end
+
+      it 'writes the pipeline config in the given generated_pipeline_path' do
+        subject.generate!
+
+        expect(File.read(custom_pipeline_filename.path))
+          .to eq(
+            "rspec migration:\nrspec background_migration:\nrspec unit:\n" \
+            "rspec integration:\nrspec system:"
+          )
+      end
+    end
+
     context 'when rspec_files does not exist' do
       subject { described_class.new(rspec_files_path: nil, pipeline_template_path: pipeline_template.path) }
 
diff --git a/spec/tooling/quality/test_level_spec.rb b/spec/tooling/quality/test_level_spec.rb
index aac7d19c0795b73baa4b2990e7ee7dbf9b80a891..a7e4e42206a25c8ec197c2e273e13501db5985e7 100644
--- a/spec/tooling/quality/test_level_spec.rb
+++ b/spec/tooling/quality/test_level_spec.rb
@@ -46,7 +46,7 @@
     context 'when level is unit' do
       it 'returns a pattern' do
         expect(subject.pattern(:unit))
-          .to eq("spec/{bin,channels,config,contracts,db,dependencies,elastic,elastic_integration,experiments,factories,finders,frontend,graphql,haml_lint,helpers,initializers,lib,metrics_server,models,policies,presenters,rack_servers,replicators,routing,rubocop,scripts,serializers,services,sidekiq,sidekiq_cluster,spam,support_specs,tasks,uploaders,validators,views,workers,tooling,components}{,/**/}*_spec.rb")
+          .to eq("spec/{bin,channels,components,config,contracts,db,dependencies,elastic,elastic_integration,experiments,factories,finders,frontend,graphql,haml_lint,helpers,initializers,lib,metrics_server,models,policies,presenters,rack_servers,replicators,routing,rubocop,scripts,serializers,services,sidekiq,sidekiq_cluster,spam,support_specs,tasks,uploaders,validators,views,workers,tooling}{,/**/}*_spec.rb")
       end
     end
 
@@ -121,7 +121,7 @@
     context 'when level is unit' do
       it 'returns a regexp' do
         expect(subject.regexp(:unit))
-          .to eq(%r{spec/(bin|channels|config|contracts|db|dependencies|elastic|elastic_integration|experiments|factories|finders|frontend|graphql|haml_lint|helpers|initializers|lib|metrics_server|models|policies|presenters|rack_servers|replicators|routing|rubocop|scripts|serializers|services|sidekiq|sidekiq_cluster|spam|support_specs|tasks|uploaders|validators|views|workers|tooling|components)/})
+          .to eq(%r{spec/(bin|channels|components|config|contracts|db|dependencies|elastic|elastic_integration|experiments|factories|finders|frontend|graphql|haml_lint|helpers|initializers|lib|metrics_server|models|policies|presenters|rack_servers|replicators|routing|rubocop|scripts|serializers|services|sidekiq|sidekiq_cluster|spam|support_specs|tasks|uploaders|validators|views|workers|tooling)/})
       end
     end
 
@@ -167,6 +167,13 @@
       end
     end
 
+    context 'when start_with == true' do
+      it 'returns a regexp' do
+        expect(described_class.new(['ee/']).regexp(:system, true))
+          .to eq(%r{^(ee/)spec/(features)/})
+      end
+    end
+
     describe 'performance' do
       it 'memoizes the regexp for a given level' do
         expect(subject.regexp(:system).object_id).to eq(subject.regexp(:system).object_id)
diff --git a/tooling/quality/test_level.rb b/tooling/quality/test_level.rb
index eeda135f3eec980972f3d2651aaf4dad95aa8ba1..20e00763f65438fb3a167c5abe2618ead8baa5fb 100644
--- a/tooling/quality/test_level.rb
+++ b/tooling/quality/test_level.rb
@@ -18,6 +18,7 @@ class TestLevel
       unit: %w[
         bin
         channels
+        components
         config
         contracts
         db
@@ -54,7 +55,6 @@ class TestLevel
         views
         workers
         tooling
-        components
       ],
       integration: %w[
         commands
@@ -77,8 +77,8 @@ def pattern(level)
       @patterns[level] ||= "#{prefixes_for_pattern}spec/#{folders_pattern(level)}{,/**/}*#{suffix(level)}".freeze # rubocop:disable Style/RedundantFreeze
     end
 
-    def regexp(level)
-      @regexps[level] ||= Regexp.new("#{prefixes_for_regex}spec/#{folders_regex(level)}").freeze
+    def regexp(level, start_with = false)
+      @regexps[level] ||= Regexp.new("#{'^' if start_with}#{prefixes_for_regex}spec/#{folders_regex(level)}").freeze
     end
 
     def level_for(file_path)