From 912f3d89dde37e4e8e3911a0aa09b800a5da9715 Mon Sep 17 00:00:00 2001
From: Ted Bowman <41201-tedbow@users.noreply.drupalcode.org>
Date: Fri, 3 Mar 2023 00:12:32 -0500
Subject: [PATCH] Issue #3343827 by tedbow, Wim Leers: Update
 FixtureManipulator to work with InstalledPackagesList, real composer show
 command

---
 automatic_updates.services.yml                |   3 +
 package_manager/src/ComposerInspector.php     |  20 +-
 .../existing_correct_fixture/composer.json    |   1 -
 .../vendor/composer/installed.json            |   9 -
 .../vendor/composer/installed.php             |  17 -
 .../cweagans--composer-patches/composer.json  |   1 +
 .../src/FixtureManipulator.php                | 327 ++++++++++++++----
 .../Kernel/ComposerPatchesValidatorTest.php   | 179 +++++++---
 .../Kernel/ComposerPluginsValidatorTest.php   |  29 +-
 .../tests/src/Kernel/ComposerUtilityTest.php  |  84 +++--
 .../tests/src/Kernel/FakeSiteFixtureTest.php  |  47 ++-
 .../src/Kernel/FixtureManipulatorTest.php     | 114 +++---
 ...OverwriteExistingPackagesValidatorTest.php | 105 ++++--
 .../Kernel/PackageManagerKernelTestBase.php   |   6 -
 .../Kernel/PathExcluder/GitExcluderTest.php   |   1 -
 .../Kernel/SupportedReleaseValidatorTest.php  |  13 -
 .../src/Traits/ComposerInstallersTrait.php    |  68 ++++
 .../src/Unit/InstalledPackagesDataTest.php    |   2 +
 src/Validator/StagedProjectsValidator.php     |  56 +--
 .../ScaffoldFilePermissionsValidatorTest.php  |   1 +
 .../StagedProjectsValidatorTest.php           |  56 ++-
 21 files changed, 748 insertions(+), 391 deletions(-)
 delete mode 100644 package_manager/tests/fixtures/FixtureUtilityTraitTest/existing_correct_fixture/composer.json
 delete mode 100644 package_manager/tests/fixtures/FixtureUtilityTraitTest/existing_correct_fixture/vendor/composer/installed.json
 delete mode 100644 package_manager/tests/fixtures/FixtureUtilityTraitTest/existing_correct_fixture/vendor/composer/installed.php
 create mode 100644 package_manager/tests/src/Traits/ComposerInstallersTrait.php

diff --git a/automatic_updates.services.yml b/automatic_updates.services.yml
index 8c27147e6b..f59cf6531b 100644
--- a/automatic_updates.services.yml
+++ b/automatic_updates.services.yml
@@ -61,6 +61,9 @@ services:
       - { name: event_subscriber }
   automatic_updates.staged_projects_validator:
     class: Drupal\automatic_updates\Validator\StagedProjectsValidator
+    arguments:
+      - '@package_manager.path_locator'
+      - '@package_manager.composer_inspector'
     tags:
       - { name: event_subscriber }
   automatic_updates.release_chooser:
diff --git a/package_manager/src/ComposerInspector.php b/package_manager/src/ComposerInspector.php
index f70a9b3dc3..cf92a7a507 100644
--- a/package_manager/src/ComposerInspector.php
+++ b/package_manager/src/ComposerInspector.php
@@ -323,16 +323,20 @@ class ComposerInspector {
     // then merge the results together.
     $this->runner->run($options, $this->jsonCallback);
     $output = $this->jsonCallback->getOutputData();
-    foreach ($output['installed'] as $installed_package) {
-      $data[$installed_package['name']] = $installed_package;
-    }
+    // $output['installed'] will not be set if no packages are installed.
+    if (isset($output['installed'])) {
+      foreach ($output['installed'] as $installed_package) {
+        $data[$installed_package['name']] = $installed_package;
+      }
 
-    $options[] = '--path';
-    $this->runner->run($options, $this->jsonCallback);
-    $output = $this->jsonCallback->getOutputData();
-    foreach ($output['installed'] as $installed_package) {
-      $data[$installed_package['name']]['path'] = $installed_package['path'];
+      $options[] = '--path';
+      $this->runner->run($options, $this->jsonCallback);
+      $output = $this->jsonCallback->getOutputData();
+      foreach ($output['installed'] as $installed_package) {
+        $data[$installed_package['name']]['path'] = $installed_package['path'];
+      }
     }
+
     return $data;
   }
 
diff --git a/package_manager/tests/fixtures/FixtureUtilityTraitTest/existing_correct_fixture/composer.json b/package_manager/tests/fixtures/FixtureUtilityTraitTest/existing_correct_fixture/composer.json
deleted file mode 100644
index 0967ef424b..0000000000
--- a/package_manager/tests/fixtures/FixtureUtilityTraitTest/existing_correct_fixture/composer.json
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/package_manager/tests/fixtures/FixtureUtilityTraitTest/existing_correct_fixture/vendor/composer/installed.json b/package_manager/tests/fixtures/FixtureUtilityTraitTest/existing_correct_fixture/vendor/composer/installed.json
deleted file mode 100644
index 4c9f037cd9..0000000000
--- a/package_manager/tests/fixtures/FixtureUtilityTraitTest/existing_correct_fixture/vendor/composer/installed.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-    "packages": [
-        {
-            "name": "the-org/the-package",
-            "version": "9.8.0"
-        }
-    ],
-    "dev-package-names": []
-}
\ No newline at end of file
diff --git a/package_manager/tests/fixtures/FixtureUtilityTraitTest/existing_correct_fixture/vendor/composer/installed.php b/package_manager/tests/fixtures/FixtureUtilityTraitTest/existing_correct_fixture/vendor/composer/installed.php
deleted file mode 100644
index 4190670a2b..0000000000
--- a/package_manager/tests/fixtures/FixtureUtilityTraitTest/existing_correct_fixture/vendor/composer/installed.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-/**
- * @file
- * Fixture for  \Drupal\Tests\package_manager\Kernel\FixtureUtilityTraitTest::testModifyPackage().
- */
-
-return [
-  'versions' =>
-  [
-    'the-org/the-package' =>
-    [
-      'name' => 'the-org/the-package',
-      'install_path' => __DIR__ . '/../../a_new_path',
-    ],
-  ],
-];
diff --git a/package_manager/tests/fixtures/path_repos/cweagans--composer-patches/composer.json b/package_manager/tests/fixtures/path_repos/cweagans--composer-patches/composer.json
index d603b2f23d..90e9aaeda2 100644
--- a/package_manager/tests/fixtures/path_repos/cweagans--composer-patches/composer.json
+++ b/package_manager/tests/fixtures/path_repos/cweagans--composer-patches/composer.json
@@ -2,6 +2,7 @@
     "name": "cweagans/composer-patches",
     "description": "A fake version of cweagans/composer-patches",
     "type": "composer-plugin",
+    "version": "24.12.1999",
     "extra": {
         "class": "\\cweagans\\Fake\\ComposerPatches"
     },
diff --git a/package_manager/tests/modules/fixture_manipulator/src/FixtureManipulator.php b/package_manager/tests/modules/fixture_manipulator/src/FixtureManipulator.php
index 7935ca8818..b2fde1a3fb 100644
--- a/package_manager/tests/modules/fixture_manipulator/src/FixtureManipulator.php
+++ b/package_manager/tests/modules/fixture_manipulator/src/FixtureManipulator.php
@@ -3,16 +3,20 @@
 namespace Drupal\fixture_manipulator;
 
 use Composer\Semver\VersionParser;
+use Drupal\Component\FileSystem\FileSystem;
 use Drupal\Component\Utility\NestedArray;
-use Drupal\Core\Serialization\Yaml;
+use PhpTuf\ComposerStager\Domain\Service\ProcessOutputCallback\ProcessOutputCallbackInterface;
 use PhpTuf\ComposerStager\Domain\Service\ProcessRunner\ComposerRunnerInterface;
-use Symfony\Component\Filesystem\Filesystem;
+use Symfony\Component\Filesystem\Filesystem as SymfonyFileSystem;
+use Drupal\Component\Serialization\Yaml;
 
 /**
  * It manipulates.
  */
 class FixtureManipulator {
 
+  protected const PATH_REPO_STATE_KEY = self::class . '-path-repo-base';
+
   /**
    * Whether changes are currently being committed.
    *
@@ -39,7 +43,7 @@ class FixtureManipulator {
    *
    * @var string
    */
-  private string $dir;
+  protected string $dir;
 
   /**
    * Validate the fixtures still passes `composer validate`.
@@ -49,44 +53,48 @@ class FixtureManipulator {
     $runner = \Drupal::service(ComposerRunnerInterface::class);
     $runner->run([
       'validate',
-      // @todo Check the lock file in https://drupal.org/i/3343827.
-      '--no-check-lock',
       '--no-check-publish',
       '--with-dependencies',
       '--no-interaction',
       '--ansi',
       '--no-cache',
       "--working-dir={$this->dir}",
+      // Unlike ComposerInspector::validate(), explicitly do NOT validate
+      // plugins, to allow for testing edge cases.
+      '--no-plugins',
+      // @todo remove this after FixtureManipulator uses composer commands exclusively!
+      '--no-check-lock',
+      // Dummy packages are not meant for publishing, so do not validate that.
+      '--no-check-publish',
+      '--no-check-version',
     ]);
   }
 
   /**
    * Adds a package.
    *
-   * If $package contains an `install_path` key, it should be relative to the
-   * location of `installed.json` and `installed.php`, which are in
-   * `vendor/composer`. For example, if the package would be installed at
-   * `vendor/kirk/enterprise`, the install path should be `../kirk/enterprise`.
-   * If the package would be installed outside of vendor (for example, a Drupal
-   * module in the `modules` directory), it would be `../../modules/my_module`.
-   *
    * @param array $package
    *   The package info that should be added to installed.json and
    *   installed.php. Must include the `name` and `type` keys.
    * @param bool $is_dev_requirement
    *   Whether or not the package is a development requirement.
-   * @param bool $create_project
-   *   Whether or not the project info.yml file should be created.
+   * @param bool $allow_plugins
+   *   Whether or not to use the '--no-plugins' option.
+   * @param array|null $extra_files
+   *   An array extra files to create in the package. The keys are the file
+   *   paths under package and values are the file contents.
    */
-  public function addPackage(array $package, bool $is_dev_requirement = FALSE, bool $create_project = TRUE): self {
+  public function addPackage(array $package, bool $is_dev_requirement = FALSE, bool $allow_plugins = FALSE, ?array $extra_files = NULL): self {
     if (!$this->committingChanges) {
       // To pass Composer validation all packages must have a version specified.
       if (!isset($package['version'])) {
         $package['version'] = '1.2.3';
       }
-      $this->queueManipulation('addPackage', [$package, $is_dev_requirement, $create_project]);
+      $this->queueManipulation('addPackage', [$package, $is_dev_requirement, $allow_plugins, $extra_files]);
       return $this;
     }
+
+    // Basic validation so we can defer the rest to `composer` commands.
     foreach (['name', 'type'] as $required_key) {
       if (!isset($package[$required_key])) {
         throw new \UnexpectedValueException("The '$required_key' is required when calling ::addPackage().");
@@ -95,29 +103,61 @@ class FixtureManipulator {
     if (!preg_match('/\w+\/\w+/', $package['name'])) {
       throw new \UnexpectedValueException(sprintf("'%s' is not a valid package name.", $package['name']));
     }
-    $this->setPackage($package['name'], $package, FALSE, $is_dev_requirement);
-    $drupal_project_types = [
-      'drupal-module',
-      'drupal-theme',
-      'drupal-custom-module',
-      'drupal-custom-theme',
-    ];
-    if (!$create_project || !in_array($package['type'], $drupal_project_types, TRUE)) {
-      return $this;
+
+    // `composer require` happily will re-require already required packages.
+    // Prevent test authors from thinking this has any effect when it does not.
+    $json = $this->runComposerCommand(['show', '--name-only', '--format=json'])->stdout;
+    $installed_package_names = array_column(json_decode($json)->installed, 'name');
+    if (in_array($package['name'], $installed_package_names)) {
+      throw new \LogicException(sprintf("Expected package '%s' to not be installed, but it was.", $package['name']));
     }
-    if (empty($package['install_path'])) {
-      throw new \LogicException("'install_path' is not set.");
+
+    $repo_path = $this->addRepository($package);
+    if (is_null($extra_files) && isset($package['type']) && in_array($package['type'], ['drupal-module', 'drupal-theme', 'drupal-profile'], TRUE)) {
+      // For Drupal projects if no files are provided create an info.yml file
+      // that assumes the project and package names match.
+      [, $package_name] = explode('/', $package['name']);
+      $project_name = str_replace('-', '_', $package_name);
+      $project_info_data = [
+        'name' => $package['name'],
+        'project' => $project_name,
+      ];
+      $extra_files["$project_name.info.yml"] = Yaml::encode($project_info_data);
     }
-    $install_path = "vendor/composer/" . $package['install_path'];
-    $this->addProjectAtPath($install_path);
+    if (!empty($extra_files)) {
+      $fs = new SymfonyFileSystem();
+      foreach ($extra_files as $file_name => $file_contents) {
+        if (str_contains($file_name, DIRECTORY_SEPARATOR)) {
+          $file_dir = dirname("$repo_path/$file_name");
+          if (!is_dir($file_dir)) {
+            $fs->mkdir($file_dir);
+          }
+        }
+        file_put_contents("$repo_path/$file_name", $file_contents);
+      }
+    }
+    $command_options = ['require', "{$package['name']}:{$package['version']}"];
+    if ($is_dev_requirement) {
+      $command_options[] = '--dev';
+    }
+    // Unlike ComposerInspector::validate(), explicitly do NOT validate plugins.
+    if (!$allow_plugins) {
+      $command_options[] = '--no-plugins';
+    }
+    $this->runComposerCommand($command_options);
     return $this;
   }
 
   /**
    * Modifies a package's installed info.
    *
-   * See ::addPackage() for information on how the `install_path` key is
-   * handled, if $package has it.
+   * @todo Since ::setVersion() is not longer calling this method the only test
+   *   the is using this that is not just testing this method itself is
+   *   \Drupal\Tests\automatic_updates\Kernel\StatusCheck\ScaffoldFilePermissionsValidatorTest::testScaffoldFilesChanged
+   *   That test is passing, so we could leave it, then we have to leave
+   *   ::setPackage() which is very complicated. Will leave notes on
+   *   testScaffoldFilesChanged() how we might solve that with composer commands
+   *   instead of this method.
    *
    * @param string $name
    *   The name of the package to modify.
@@ -141,11 +181,23 @@ class FixtureManipulator {
    *   The package name.
    * @param string $version
    *   The version.
+   * @param bool $is_dev_requirement
+   *   Whether or not the package is a development requirement.
    *
    * @return $this
    */
-  public function setVersion(string $package_name, string $version): self {
-    return $this->modifyPackage($package_name, ['version' => $version]);
+  public function setVersion(string $package_name, string $version, bool $is_dev_requirement = FALSE): self {
+    if (!$this->committingChanges) {
+      $this->queueManipulation('setVersion', func_get_args());
+      return $this;
+    }
+    $package = [
+      'name' => $package_name,
+      'version' => $version,
+    ];
+    $this->addRepository($package);
+    $this->runComposerCommand(array_filter(['require', "$package_name:$version", $is_dev_requirement ? '--dev' : NULL]));
+    return $this;
   }
 
   /**
@@ -153,13 +205,27 @@ class FixtureManipulator {
    *
    * @param string $name
    *   The name of the package to remove.
+   * @param bool $is_dev_requirement
+   *   Whether or not the package is a developer requirement.
    */
-  public function removePackage(string $name): self {
+  public function removePackage(string $name, bool $is_dev_requirement = FALSE): self {
     if (!$this->committingChanges) {
       $this->queueManipulation('removePackage', func_get_args());
       return $this;
     }
-    $this->setPackage($name, NULL, TRUE);
+
+    $output = $this->runComposerCommand(array_filter(['remove', $name, $is_dev_requirement ? '--dev' : NULL, '--no-update']));
+    // `composer remove` will not set exit code 1 whenever a non-required
+    // package is being removed.
+    // @see \Composer\Command\RemoveCommand
+    if (str_contains($output->stderr, 'not required in your composer.json and has not been removed')) {
+      $output->stderr = str_replace("./composer.json has been updated\n", '', $output->stderr);
+      throw new \LogicException($output->stderr);
+    }
+
+    // Make sure that `installed.json` & `installed.php` are updated.
+    // @todo Remove this when ComposerUtility gets removed.
+    $this->runComposerCommand(['update', $name]);
     return $this;
   }
 
@@ -169,6 +235,8 @@ class FixtureManipulator {
    * This function is internal and should not be called directly. Use
    * ::addPackage(), ::modifyPackage(), and ::removePackage() instead.
    *
+   * @todo Remove this method once ::modifyPackage() doesn't call it.
+   *
    * @param string $pretty_name
    *   The name of the package to add, update, or remove.
    * @param array|null $package
@@ -214,7 +282,7 @@ class FixtureManipulator {
 
     if ($package) {
       $package = ['name' => $pretty_name] + $package;
-      $install_json_package = array_diff_key($package, array_flip(['install_path']));
+      $install_json_package = $package;
       // Composer will use 'version_normalized', if present, to determine the
       // version number.
       if (isset($install_json_package['version']) && !isset($install_json_package['version_normalized'])) {
@@ -230,16 +298,24 @@ class FixtureManipulator {
         $install_json_package = $install_json_package + $data['packages'][$position];
       }
 
-      // Remove the existing package; the array will be re-keyed by
-      // array_splice().
-      array_splice($data['packages'], $position, 1);
-      $is_existing_dev_package = in_array($name, $data['dev-package-names'], TRUE);
-      $data['dev-package-names'] = array_diff($data['dev-package-names'], [$name]);
-      $data['dev-package-names'] = array_values($data['dev-package-names']);
+      // If `$package === NULL`, the existing package should be removed.
+      if ($package === NULL) {
+        array_splice($data['packages'], $position, 1);
+        $is_existing_dev_package = in_array($name, $data['dev-package-names'], TRUE);
+        $data['dev-package-names'] = array_diff($data['dev-package-names'], [$name]);
+        $data['dev-package-names'] = array_values($data['dev-package-names']);
+      }
     }
     // Add the package back to the list, if we have data for it.
     if (isset($install_json_package)) {
-      $data['packages'][] = $install_json_package;
+      // If it previously existed, put it back in the previous position.
+      if ($position) {
+        $data['packages'][$i] = $install_json_package;
+      }
+      // Otherwise, it must be new: append it to the list.
+      else {
+        $data['packages'][] = $install_json_package;
+      }
 
       if ($is_dev_requirement || !empty($is_existing_dev_package)) {
         $data['dev-package-names'][] = $name;
@@ -259,23 +335,9 @@ class FixtureManipulator {
       throw new \LogicException($expected_package_message);
     }
     if ($package) {
-      // If an install path was provided, ensure it's relative.
-      if (array_key_exists('install_path', $package)) {
-        if (!str_starts_with($package['install_path'], '../')) {
-          throw new \UnexpectedValueException("'install_path' must start with '../'.");
-        }
-      }
       $install_php_package = $should_exist ?
         NestedArray::mergeDeep($data['versions'][$name], $package) :
         $package;
-
-      // The installation paths in $data will have been interpreted by the PHP
-      // runtime, so make them all relative again by stripping $this->dir out.
-      array_walk($data['versions'], function (array &$install_php_package) use ($composer_folder) : void {
-        if (array_key_exists('install_path', $install_php_package)) {
-          $install_php_package['install_path'] = str_replace("$composer_folder/", '', $install_php_package['install_path']);
-        }
-      });
       $data['versions'][$name] = $install_php_package;
     }
     else {
@@ -283,7 +345,6 @@ class FixtureManipulator {
     }
 
     $data = var_export($data, TRUE);
-    $data = str_replace("'install_path' => '../", "'install_path' => __DIR__ . '/../", $data);
     file_put_contents($file, "<?php\nreturn $data;");
   }
 
@@ -308,7 +369,7 @@ class FixtureManipulator {
     if (file_exists($path)) {
       throw new \LogicException("'$path' path already exists.");
     }
-    $fs = new Filesystem();
+    $fs = new SymfonyFileSystem();
     $fs->mkdir($path);
     if ($project_name === NULL) {
       $project_name = basename($path);
@@ -336,9 +397,6 @@ class FixtureManipulator {
   /**
    * Modifies a package's installed info.
    *
-   * See ::addPackage() for information on how the `install_path` key is
-   * handled, if $package has it.
-   *
    * @param array $additional_config
    *   The configuration to add.
    */
@@ -351,18 +409,24 @@ class FixtureManipulator {
       $this->queueManipulation('addConfig', func_get_args());
       return $this;
     }
-
-    $file = $this->dir . '/composer.json';
-    self::ensureFilePathIsWritable($file);
-
-    $data = file_get_contents($file);
-    $data = json_decode($data, TRUE, 512, JSON_THROW_ON_ERROR);
-
-    $config = $data['config'] ?? [];
-    $data['config'] = NestedArray::mergeDeep($config, $additional_config);
-
-    file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
-    self::ensureFilePathIsWritable($file);
+    $clean_value = function ($value) {
+      return $value === FALSE ? 'false' : $value;
+    };
+
+    foreach ($additional_config as $key => $value) {
+      $command = ['config'];
+      if (is_array($value)) {
+        $value = json_encode($value, JSON_UNESCAPED_SLASHES);
+        $command[] = '--json';
+      }
+      else {
+        $value = $clean_value($value);
+      }
+      $command[] = $key;
+      $command[] = $value;
+      $this->runComposerCommand($command);
+    }
+    $this->runComposerCommand(['update', '--lock']);
 
     return $this;
   }
@@ -386,10 +450,16 @@ class FixtureManipulator {
       throw new \BadMethodCallException('Already committed.');
     }
     $this->dir = $dir;
+    $this->setUpRepos();
     $this->committingChanges = TRUE;
     $manipulator_arguments = $this->getQueuedManipulationItems();
     $this->clearQueuedManipulationItems();
+    // @todo The following line make InstalledPackagesListTest pass but causes
+    // other tests to fail, at least DeleteExistingUpdateTest.
+    // $this->runComposerCommand(['update']);
     foreach ($manipulator_arguments as $method => $argument_sets) {
+      // @todo Attempt to make fewer Composer calls in
+      //   https://drupal.org/i/3345639.
       foreach ($argument_sets as $argument_set) {
         $this->{$method}(...$argument_set);
       }
@@ -428,7 +498,7 @@ class FixtureManipulator {
       $this->queueManipulation('addDotGitFolder', func_get_args());
       return $this;
     }
-    $fs = new Filesystem();
+    $fs = new SymfonyFileSystem();
     $git_directory_path = $path . "/.git";
     if (!is_dir($git_directory_path)) {
       $fs->mkdir($git_directory_path);
@@ -468,4 +538,111 @@ class FixtureManipulator {
     return $this->manipulatorArguments;
   }
 
+  protected function runComposerCommand(array $command_options): ProcessOutputCallbackInterface {
+    $plain_output = new class() implements ProcessOutputCallbackInterface {
+      public string $stdout = '';
+      public string $stderr = '';
+
+      /**
+       * {@inheritdoc}
+       */
+      public function __invoke(string $type, string $buffer): void {
+        if ($type === self::OUT) {
+          $this->stdout .= $buffer;
+          return;
+        }
+        elseif ($type === self::ERR) {
+          $this->stderr .= $buffer;
+          return;
+        }
+        throw new \InvalidArgumentException("Unsupported output type: '$type'");
+      }
+
+    };
+    /** @var \PhpTuf\ComposerStager\Domain\Service\ProcessRunner\ComposerRunnerInterface $runner */
+    $runner = \Drupal::service(ComposerRunnerInterface::class);
+    $command_options[] = "--working-dir={$this->dir}";
+    $runner->run($command_options, $plain_output);
+    return $plain_output;
+  }
+
+  /**
+   * Adds a path repository.
+   *
+   * @param array $package
+   *   The package.
+   *
+   * @return string
+   *   The repository path.
+   */
+  private function addRepository(array $package): string {
+    $name = $package['name'];
+    $path_repo_base = \Drupal::state()->get(self::PATH_REPO_STATE_KEY);
+    $repo_path = "$path_repo_base/" . str_replace('/', '--', $name);
+    $composer_json_path = $repo_path . DIRECTORY_SEPARATOR . 'composer.json';
+    $fs = new SymfonyFileSystem();
+    if (!is_dir($repo_path)) {
+      // Create the repo if it does not exist.
+      $fs->mkdir($repo_path);
+      file_put_contents($composer_json_path, json_encode($package, JSON_THROW_ON_ERROR));
+      $repository = json_encode([
+        'type' => 'path',
+        'url' => $repo_path,
+        'options' => [
+          'symlink' => FALSE,
+        ],
+      ], JSON_UNESCAPED_SLASHES);
+      $this->runComposerCommand(['config', "repo.$name", $repository]);
+    }
+    else {
+      $composer_json_data = json_decode(file_get_contents($composer_json_path), TRUE);
+      // Update the version if needed.
+      // @todo Should we create 1 repo per version.
+      if ($composer_json_data['version'] !== $package['version']) {
+        $composer_json_data['version'] = $package['version'];
+        file_put_contents($composer_json_path, json_encode($composer_json_data, JSON_THROW_ON_ERROR));
+      }
+    }
+    return $repo_path;
+  }
+
+  /**
+   * Sets up the path repos at absolute paths.
+   */
+  public function setUpRepos(): void {
+    // Some of the test coverage for FixtureManipulator tests edge cases for
+    // making installed.php invalid, and those test fixtures do NOT have a
+    // composer.json because ComposerUtility didn't look at that!
+    // @todo Remove this early return when ComposerUtility gets removed along
+    // with that edge case test coverage.
+    // @see fixtures/FixtureUtilityTraitTest/missing_installed_php
+    if (!file_exists($this->dir . '/composer.json')) {
+      return;
+    }
+    $fs = new SymfonyFileSystem();
+    $path_repo_base = \Drupal::state()->get(self::PATH_REPO_STATE_KEY);
+    if (empty($path_repo_base)) {
+      // @todo Is this better to setup in the base test class or trait?
+      $path_repo_base = FileSystem::getOsTemporaryDirectory() . '/base-repo-' . microtime(TRUE) . rand(0, 10000);
+      \Drupal::state()->set(self::PATH_REPO_STATE_KEY, $path_repo_base);
+      // Copy the existing repos that were used to make the fixtures into the
+      // new folder.
+      $fs->mirror(__DIR__ . '/../../../fixtures/path_repos', $path_repo_base);
+    }
+    // Update all the repos in the composer.json file to point to the new
+    // repos at the absolute path.
+    $json_data = json_decode(file_get_contents($this->dir . '/composer.json'), TRUE);
+    $composer_json_needs_update = FALSE;
+    foreach ($json_data['repositories'] as &$existing_repo_data) {
+      if (is_array($existing_repo_data) && isset($existing_repo_data['url']) && str_starts_with($existing_repo_data['url'], '../path_repos/')) {
+        $composer_json_needs_update = TRUE;
+        $existing_repo_data['url'] = str_replace('../path_repos/', "$path_repo_base/", $existing_repo_data['url']);
+      }
+    }
+    if ($composer_json_needs_update) {
+      file_put_contents($this->dir . '/composer.json', json_encode($json_data, JSON_THROW_ON_ERROR));
+    }
+    $this->runComposerCommand(['install']);
+  }
+
 }
diff --git a/package_manager/tests/src/Kernel/ComposerPatchesValidatorTest.php b/package_manager/tests/src/Kernel/ComposerPatchesValidatorTest.php
index d694b8b831..0048d62cc5 100644
--- a/package_manager/tests/src/Kernel/ComposerPatchesValidatorTest.php
+++ b/package_manager/tests/src/Kernel/ComposerPatchesValidatorTest.php
@@ -19,24 +19,47 @@ use Symfony\Component\Process\Process;
  */
 class ComposerPatchesValidatorTest extends PackageManagerKernelTestBase {
 
+  const ABSENT = 0;
+  const CONFIG_ALLOWED_PLUGIN = 1;
+  const EXTRA_EXIT_ON_PATCH_FAILURE = 2;
+  const REQUIRE_PACKAGE_FROM_ROOT = 4;
+  const REQUIRE_PACKAGE_INDIRECTLY = 8;
+
   /**
    * Data provider for testErrorDuringPreCreate().
    *
    * @return mixed[][]
    *   The test cases.
    */
-  public function providerPatcherConfiguration(): array {
+  public function providerErrorDuringPreCreate(): array {
+    $summary = t('Problems detected related to the Composer plugin <code>cweagans/composer-patches</code>.');
     return [
-      'exit-on-patch-failure missing' => [
-        FALSE,
+      'INVALID: exit-on-patch-failure missing' => [
+        static::CONFIG_ALLOWED_PLUGIN | static::REQUIRE_PACKAGE_FROM_ROOT,
         [
           ValidationResult::createError([
             t('The <code>composer-exit-on-patch-failure</code> key is not set to <code>true</code> in the <code>extra</code> section of <code>composer.json</code>.'),
-          ], t('Problems detected related to the Composer plugin <code>cweagans/composer-patches</code>.')),
+          ], $summary),
+        ],
+      ],
+      'INVALID: indirect dependency' => [
+        static::CONFIG_ALLOWED_PLUGIN | static::EXTRA_EXIT_ON_PATCH_FAILURE | static::REQUIRE_PACKAGE_INDIRECTLY,
+        [
+          ValidationResult::createError([
+            t('It must be a root dependency.'),
+          ], $summary),
+        ],
+        [
+          'package-manager-faq-composer-patches-not-a-root-dependency',
+          NULL,
         ],
       ],
-      'exit-on-patch-failure set' => [
-        TRUE,
+      'VALID: present' => [
+        static::CONFIG_ALLOWED_PLUGIN | static::EXTRA_EXIT_ON_PATCH_FAILURE | static::REQUIRE_PACKAGE_FROM_ROOT,
+        [],
+      ],
+      'VALID: absent' => [
+        static::ABSENT,
         [],
       ],
     ];
@@ -45,19 +68,34 @@ class ComposerPatchesValidatorTest extends PackageManagerKernelTestBase {
   /**
    * Tests that the patcher configuration is validated during pre-create.
    *
-   * @param bool $extra_key_set
-   *   Whether to set key in extra part of root package.
+   * @param int $options
+   *   What aspects of the patcher are installed how.
    * @param \Drupal\package_manager\ValidationResult[] $expected_results
    *   The expected validation results.
    *
-   *  @dataProvider providerPatcherConfiguration()
+   *  @dataProvider providerErrorDuringPreCreate()
    */
-  public function testPatcherConfiguration(bool $extra_key_set, array $expected_results): void {
-    $this->addPatcherToAllowedPlugins();
-    $this->setRootRequires();
-    if ($extra_key_set) {
+  public function testErrorDuringPreCreate(int $options, array $expected_results): void {
+    if ($options & static::CONFIG_ALLOWED_PLUGIN) {
+      $this->addPatcherToAllowedPlugins();
+    }
+    if ($options & static::EXTRA_EXIT_ON_PATCH_FAILURE) {
       $this->setRootExtra();
     }
+    if ($options & static::REQUIRE_PACKAGE_FROM_ROOT) {
+      $this->setRootRequires();
+    }
+    elseif ($options & static::REQUIRE_PACKAGE_INDIRECTLY) {
+      (new ActiveFixtureManipulator())
+        ->addPackage([
+          'type' => 'package',
+          'name' => 'dummy/depends-on-composer-patches',
+          'description' => 'A dummy package depending on cweagans/composer-patches',
+          'version' => '1.0.0',
+          'require' => ['cweagans/composer-patches' => '*'],
+        ])
+        ->commitChanges();
+    }
     $this->assertStatusCheckResults($expected_results);
     $this->assertResults($expected_results, PreCreateEvent::class);
   }
@@ -73,24 +111,54 @@ class ComposerPatchesValidatorTest extends PackageManagerKernelTestBase {
 
     return [
       'composer-patches present in stage, but not present in active' => [
-        FALSE,
-        TRUE,
+        static::ABSENT,
+        static::CONFIG_ALLOWED_PLUGIN | static::EXTRA_EXIT_ON_PATCH_FAILURE | static::REQUIRE_PACKAGE_FROM_ROOT,
+        [
+          ValidationResult::createError([
+            t('It cannot be installed by Package Manager.'),
+          ], $summary),
+        ],
+        [
+          'package-manager-faq-composer-patches-installed-or-removed',
+        ],
+      ],
+      'composer-patches partially present (exit missing)  in stage, but not present in active' => [
+        static::ABSENT,
+        static::CONFIG_ALLOWED_PLUGIN | static::REQUIRE_PACKAGE_FROM_ROOT,
         [
           ValidationResult::createError([
             t('It cannot be installed by Package Manager.'),
-            t('It must be a root dependency.'),
             t('The <code>composer-exit-on-patch-failure</code> key is not set to <code>true</code> in the <code>extra</code> section of <code>composer.json</code>.'),
           ], $summary),
         ],
+        [
+          'package-manager-faq-composer-patches-installed-or-removed',
+          NULL,
+        ],
+      ],
+      // phpcs:disable
+      // @todo uncomment, figure out why this causes a failure on DrupalCI but not locally — see https://www.drupal.org/pift-ci-job/2606688
+      /*
+      'composer-patches present due to non-root dependency in stage, but not present in active' => [
+        static::CONFIG_ALLOWED_PLUGIN | static::EXTRA_EXIT_ON_PATCH_FAILURE,
+        static::CONFIG_ALLOWED_PLUGIN | static::EXTRA_EXIT_ON_PATCH_FAILURE | static::REQUIRE_PACKAGE_INDIRECTLY,
+        [
+          ValidationResult::createError([
+            t('It cannot be installed by Package Manager.'),
+            t('It must be a root dependency.'),
+          ], $summary),
+        ],
         [
           'package-manager-faq-composer-patches-installed-or-removed',
           'package-manager-faq-composer-patches-not-a-root-dependency',
           NULL,
         ],
       ],
+      */
+      // phpcs:enable
       'composer-patches removed in stage, but present in active' => [
-        TRUE,
-        FALSE,
+        static::CONFIG_ALLOWED_PLUGIN | static::EXTRA_EXIT_ON_PATCH_FAILURE | static::REQUIRE_PACKAGE_FROM_ROOT,
+        static::ABSENT,
         [
           ValidationResult::createError([
             t('It cannot be removed by Package Manager.'),
@@ -101,14 +169,14 @@ class ComposerPatchesValidatorTest extends PackageManagerKernelTestBase {
         ],
       ],
       'composer-patches present in stage and active' => [
-        TRUE,
-        TRUE,
+        static::CONFIG_ALLOWED_PLUGIN | static::EXTRA_EXIT_ON_PATCH_FAILURE | static::REQUIRE_PACKAGE_FROM_ROOT,
+        static::CONFIG_ALLOWED_PLUGIN | static::EXTRA_EXIT_ON_PATCH_FAILURE | static::REQUIRE_PACKAGE_FROM_ROOT,
         [],
         [],
       ],
       'composer-patches not present in stage and active' => [
-        FALSE,
-        FALSE,
+        static::ABSENT,
+        static::ABSENT,
         [],
         [],
       ],
@@ -118,40 +186,57 @@ class ComposerPatchesValidatorTest extends PackageManagerKernelTestBase {
   /**
    * Tests the patcher's presence and configuration are validated on pre-apply.
    *
-   * @param bool $in_active
+   * @param int $in_active
    *   Whether patcher is installed in active.
-   * @param bool $in_stage
+   * @param int $in_stage
    *   Whether patcher is installed in stage.
    * @param \Drupal\package_manager\ValidationResult[] $expected_results
    *   The expected validation results.
    *
    * @dataProvider providerErrorDuringPreApply
    */
-  public function testErrorDuringPreApply(bool $in_active, bool $in_stage, array $expected_results): void {
-    if ($in_active) {
-      // Add patcher as a root dependency and set
-      // `composer-exit-on-patch-failure` to true.
+  public function testErrorDuringPreApply(int $in_active, int $in_stage, array $expected_results): void {
+    // Simulate in active.
+    if ($in_active & static::CONFIG_ALLOWED_PLUGIN) {
       $this->addPatcherToAllowedPlugins();
-      $this->setRootRequires();
+    }
+    if ($in_active & static::EXTRA_EXIT_ON_PATCH_FAILURE) {
       $this->setRootExtra();
     }
-    if ($in_stage && !$in_active) {
-      // Simulate a stage directory where the patcher is installed.
+    if ($in_active & static::REQUIRE_PACKAGE_FROM_ROOT) {
+      $this->setRootRequires();
+    }
+
+    // Simulate in stage.
+    $stage_manipulator = $this->getStageFixtureManipulator();
+    if ($in_stage & static::CONFIG_ALLOWED_PLUGIN) {
+      $stage_manipulator->addConfig([
+        'allow-plugins.cweagans/composer-patches' => TRUE,
+      ]);
+    }
+    if ($in_stage & static::EXTRA_EXIT_ON_PATCH_FAILURE) {
+      $stage_manipulator->addConfig([
+        'extra.composer-exit-on-patch-failure' => TRUE,
+      ]);
+    }
+    if ($in_stage & static::REQUIRE_PACKAGE_FROM_ROOT && !($in_active & static::REQUIRE_PACKAGE_FROM_ROOT)) {
       $package_data = json_decode(file_get_contents(__DIR__ . '/../../fixtures/path_repos/cweagans--composer-patches/composer.json'), TRUE);
       $package_data['version'] = '24.12.1999';
-      $this->getStageFixtureManipulator()
-        ->addPackage($package_data)
-        ->addConfig([
-          'allow-plugins' => [
-            'cweagans/composer-patches' => TRUE,
-          ],
-        ]);
+      $stage_manipulator->addPackage($package_data);
     }
-
-    if (!$in_stage && $in_active) {
-      $this->getStageFixtureManipulator()
+    if (!($in_stage & static::REQUIRE_PACKAGE_FROM_ROOT) && $in_active & static::REQUIRE_PACKAGE_FROM_ROOT) {
+      $stage_manipulator
         ->removePackage('cweagans/composer-patches');
     }
+    if ($in_stage & static::REQUIRE_PACKAGE_INDIRECTLY) {
+      $stage_manipulator->addPackage([
+        'type' => 'package',
+        'name' => 'dummy/depends-on-composer-patches',
+        'description' => 'A dummy package depending on cweagans/composer-patches',
+        'version' => '1.0.0',
+        'require' => ['cweagans/composer-patches' => '*'],
+      ]);
+    }
 
     $stage = $this->createStage();
     $stage->create();
@@ -175,9 +260,9 @@ class ComposerPatchesValidatorTest extends PackageManagerKernelTestBase {
   /**
    * Tests that validation errors can carry links to help.
    *
-   * @param bool $in_active
+   * @param int $in_active
    *   Whether patcher is installed in active.
-   * @param bool $in_stage
+   * @param int $in_stage
    *   Whether patcher is installed in stage.
    * @param \Drupal\package_manager\ValidationResult[] $expected_results
    *   The expected validation results.
@@ -188,7 +273,7 @@ class ComposerPatchesValidatorTest extends PackageManagerKernelTestBase {
    *
    * @dataProvider providerErrorDuringPreApply
    */
-  public function testErrorDuringPreApplyWithHelp(bool $in_active, bool $in_stage, array $expected_results, array $help_page_sections): void {
+  public function testErrorDuringPreApplyWithHelp(int $in_active, int $in_stage, array $expected_results, array $help_page_sections): void {
     $this->enableModules(['help']);
 
     foreach ($expected_results as $result_index => $result) {
@@ -215,11 +300,7 @@ class ComposerPatchesValidatorTest extends PackageManagerKernelTestBase {
    */
   private function addPatcherToAllowedPlugins(): void {
     (new ActiveFixtureManipulator())
-      ->addConfig([
-        'allow-plugins' => [
-          'cweagans/composer-patches' => TRUE,
-        ],
-      ])
+      ->addConfig(['allow-plugins.cweagans/composer-patches' => TRUE])
       ->commitChanges();
   }
 
diff --git a/package_manager/tests/src/Kernel/ComposerPluginsValidatorTest.php b/package_manager/tests/src/Kernel/ComposerPluginsValidatorTest.php
index 7967f50a55..c1f42d7e61 100644
--- a/package_manager/tests/src/Kernel/ComposerPluginsValidatorTest.php
+++ b/package_manager/tests/src/Kernel/ComposerPluginsValidatorTest.php
@@ -158,7 +158,6 @@ class ComposerPluginsValidatorTest extends PackageManagerKernelTestBase {
           'name' => "drupal/semver_test",
           'version' => '8.1.0',
           'type' => 'drupal-module',
-          'install_path' => '../../modules/semver_test',
         ],
       ],
       [],
@@ -191,9 +190,7 @@ class ComposerPluginsValidatorTest extends PackageManagerKernelTestBase {
 
     yield 'another supported composer plugin' => [
       [
-        'allow-plugins' => [
-          'drupal/core-vendor-hardening' => TRUE,
-        ],
+        'allow-plugins.drupal/core-vendor-hardening' => TRUE,
       ],
       [
         [
@@ -209,13 +206,11 @@ class ComposerPluginsValidatorTest extends PackageManagerKernelTestBase {
 
     yield 'one UNsupported but disallowed plugin — pretty package name' => [
       [
-        'allow-plugins' => [
-          'composer/plugin-A' => FALSE,
-        ],
+        'allow-plugins.composer/plugin-a' => FALSE,
       ],
       [
         [
-          'name' => 'composer/plugin-A',
+          'name' => 'composer/plugin-a',
           'version' => '6.1',
           'type' => 'composer-plugin',
           'require' => ['composer-plugin-api' => '*'],
@@ -227,9 +222,7 @@ class ComposerPluginsValidatorTest extends PackageManagerKernelTestBase {
 
     yield 'one UNsupported but disallowed plugin — normalized package name' => [
       [
-        'allow-plugins' => [
-          'composer/plugin-b' => FALSE,
-        ],
+        'allow-plugins.composer/plugin-b' => FALSE,
       ],
       [
         [
@@ -272,13 +265,11 @@ class ComposerPluginsValidatorTest extends PackageManagerKernelTestBase {
   public function providerSimpleInvalidCases(): \Generator {
     yield 'one UNsupported composer plugin — pretty package name' => [
       [
-        'allow-plugins' => [
-          'NOT-cweagans/NOT-composer-patches' => TRUE,
-        ],
+        'allow-plugins.not-cweagans/not-composer-patches' => TRUE,
       ],
       [
         [
-          'name' => 'NOT-cweagans/NOT-composer-patches',
+          'name' => 'not-cweagans/not-composer-patches',
           'require' => ['composer-plugin-api' => '*'],
           'extra' => ['class' => 'AnyClass'],
           'version' => '6.1',
@@ -288,7 +279,7 @@ class ComposerPluginsValidatorTest extends PackageManagerKernelTestBase {
       [
         ValidationResult::createError(
           [
-            new TranslatableMarkup('<code>NOT-cweagans/NOT-composer-patches</code>'),
+            new TranslatableMarkup('<code>not-cweagans/not-composer-patches</code>'),
           ],
           new TranslatableMarkup('An unsupported Composer plugin was detected.'),
         ),
@@ -297,9 +288,7 @@ class ComposerPluginsValidatorTest extends PackageManagerKernelTestBase {
 
     yield 'one UNsupported composer plugin — normalized package name' => [
       [
-        'allow-plugins' => [
-          'also-not-cweagans/also-not-composer-patches' => TRUE,
-        ],
+        'allow-plugins.also-not-cweagans/also-not-composer-patches' => TRUE,
       ],
       [
         [
@@ -344,7 +333,7 @@ class ComposerPluginsValidatorTest extends PackageManagerKernelTestBase {
       [
         ValidationResult::createError(
           [
-            new TranslatableMarkup('<code>NOT-cweagans/NOT-composer-patches</code>'),
+            new TranslatableMarkup('<code>not-cweagans/not-composer-patches</code>'),
             new TranslatableMarkup('<code>also-not-cweagans/also-not-composer-patches</code>'),
           ],
           new TranslatableMarkup('Unsupported Composer plugins were detected.'),
diff --git a/package_manager/tests/src/Kernel/ComposerUtilityTest.php b/package_manager/tests/src/Kernel/ComposerUtilityTest.php
index a9da1a5132..9f7a1f9945 100644
--- a/package_manager/tests/src/Kernel/ComposerUtilityTest.php
+++ b/package_manager/tests/src/Kernel/ComposerUtilityTest.php
@@ -5,10 +5,12 @@ declare(strict_types = 1);
 namespace Drupal\Tests\package_manager\Kernel;
 
 use Drupal\Component\FileSystem\FileSystem as DrupalFileSystem;
+use Drupal\Core\Serialization\Yaml;
 use Drupal\fixture_manipulator\FixtureManipulator;
 use Drupal\KernelTests\KernelTestBase;
 use Drupal\package_manager\ComposerUtility;
 use Drupal\Tests\package_manager\Traits\AssertPreconditionsTrait;
+use Drupal\Tests\package_manager\Traits\ComposerInstallersTrait;
 use Drupal\Tests\package_manager\Traits\FixtureUtilityTrait;
 use Symfony\Component\Filesystem\Filesystem;
 
@@ -20,6 +22,7 @@ use Symfony\Component\Filesystem\Filesystem;
 class ComposerUtilityTest extends KernelTestBase {
 
   use AssertPreconditionsTrait;
+  use ComposerInstallersTrait;
   use FixtureUtilityTrait;
 
   /**
@@ -48,60 +51,77 @@ class ComposerUtilityTest extends KernelTestBase {
     $fs->mkdir($this->rootDir);
     $fixture = $this->rootDir . DIRECTORY_SEPARATOR . 'fixture' . DIRECTORY_SEPARATOR;
     static::copyFixtureFilesTo(__DIR__ . '/../../fixtures/fake_site', $fixture);
-    $relative_projects_dir = '../../web/projects';
-    (new FixtureManipulator())
-      ->addPackage(
+    $this->installComposerInstallers($fixture);
+    $projects_dir = 'web/projects';
+    $manipulator = new FixtureManipulator();
+    $manipulator->addPackage(
         [
           'name' => 'drupal/package_project_match',
           'type' => 'drupal-module',
-          'install_path' => "$relative_projects_dir/package_project_match",
-        ]
-      )
-      ->addPackage(
+        ],
+        FALSE,
+        TRUE
+      );
+    $installer_paths["$projects_dir/package_project_match"] = ['drupal/package_project_match'];
+
+    $manipulator->addPackage(
         [
           'name' => 'drupal/not_match_package',
           'type' => 'drupal-module',
-          'install_path' => "$relative_projects_dir/not_match_project",
-        ]
-      )
-      ->addPackage(
+        ],
+        FALSE,
+        TRUE,
+        // Create an info.yml file with a different project name from the
+        // package.
+        ['not_match_project.info.yml' => Yaml::encode(['project' => 'not_match_project'])],
+      );
+    $installer_paths["$projects_dir/not_match_project"] = ['drupal/not_match_package'];
+    $manipulator->addPackage(
         [
           'name' => 'drupal/not_match_path_project',
           'type' => 'drupal-module',
-          'install_path' => "$relative_projects_dir/not_match_project",
         ],
         FALSE,
-        FALSE,
-      )
-      ->addProjectAtPath("web/projects/not_match_path_project", 'not_match_path_project')
-      ->addPackage(
+        TRUE,
+        []
+      );
+    $installer_paths["$projects_dir/not_match_path_project"] = ['drupal/not_match_path_project'];
+    $manipulator->addPackage(
         [
           'name' => 'drupal/nested_no_match_package',
           'type' => 'drupal-module',
-          'install_path' => "$relative_projects_dir/any_folder_name",
         ],
         FALSE,
-        FALSE,
-      )
-      ->addPackage(
+        TRUE,
+        // A test info.yml file where the folder names and info.yml file names
+        // do not match the project or package. Only the project key in this
+        // file need to match.
+        ['any_sub_folder/any_yml_file.info.yml' => Yaml::encode(['project' => 'nested_no_match_project'])],
+      );
+    $installer_paths["$projects_dir/any_folder_name"] = ['drupal/nested_no_match_package'];
+    $manipulator->addPackage(
         [
           'name' => 'non_drupal/other_project',
           'type' => 'drupal-module',
-          'install_path' => "$relative_projects_dir/other_project",
-        ]
-      )
-      ->addPackage(
+        ],
+        FALSE,
+        TRUE
+      );
+    $installer_paths["$projects_dir/other_project"] = ['non_drupal/other_project'];
+    $manipulator->addPackage(
         [
           'name' => 'drupal/custom_module',
           'type' => 'drupal-custom-module',
-          'install_path' => "$relative_projects_dir/custom_module",
-        ]
-      )
-      // A test info.yml file where the folder names and info.yml file names do
-      // not match the project or package. Only the project key in this file
-      // need to match.
-      ->addProjectAtPath("web/projects/any_folder_name/any_sub_folder", 'nested_no_match_project', 'any_yml_file.info.yml')
-      ->commitChanges($fixture);
+        ],
+        FALSE,
+        TRUE
+      );
+    $installer_paths["$projects_dir/custom_module"] = ['drupal/custom_module'];
+
+    // Commit the changes to 'installer-paths' first so that all the packages
+    // will be installed at the correct paths.
+    $this->setInstallerPaths($installer_paths, $fixture);
+    $manipulator->commitChanges($fixture);
   }
 
   /**
diff --git a/package_manager/tests/src/Kernel/FakeSiteFixtureTest.php b/package_manager/tests/src/Kernel/FakeSiteFixtureTest.php
index 91744594d4..01823104a7 100644
--- a/package_manager/tests/src/Kernel/FakeSiteFixtureTest.php
+++ b/package_manager/tests/src/Kernel/FakeSiteFixtureTest.php
@@ -4,6 +4,7 @@ declare(strict_types = 1);
 
 namespace Drupal\Tests\package_manager\Kernel;
 
+use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\fixture_manipulator\ActiveFixtureManipulator;
 use Drupal\package_manager\ComposerUtility;
 use Symfony\Component\Process\Process;
@@ -43,19 +44,24 @@ class FakeSiteFixtureTest extends PackageManagerKernelTestBase {
   }
 
   /**
-   * Tests if `modifyPackage` can be called on all packages in the fixture.
+   * Tests if `setVersion` can be called on all packages in the fixture.
    *
-   * @see \Drupal\fixture_manipulator\FixtureManipulator::modifyPackage()
+   * @see \Drupal\fixture_manipulator\FixtureManipulator::setVersion()
    */
-  public function testCallToModifyPackage(): void {
+  public function testCallToSetVersion(): void {
+    $active_dir = $this->container->get('package_manager.path_locator')->getProjectRoot();
     $stage = $this->createStage();
     $installed_packages = $stage->getActiveComposer()->getInstalledPackages();
     foreach (self::getExpectedFakeSitePackages() as $package_name) {
       $this->assertArrayHasKey($package_name, $installed_packages);
       $this->assertSame('9.8.0', $installed_packages[$package_name]->getPrettyVersion());
+      $list = $this->container->get('package_manager.composer_inspector')->getInstalledPackagesList($active_dir);
+      $this->assertSame($list[$package_name]->version, '9.8.0');
       (new ActiveFixtureManipulator())
-        ->modifyPackage($package_name, ['version' => '11.1.0'])
+        ->setVersion($package_name, '11.1.0')
         ->commitChanges();
+      $list = $this->container->get('package_manager.composer_inspector')->getInstalledPackagesList($active_dir);
+      $this->assertSame($list[$package_name]->version, '11.1.0');
     }
   }
 
@@ -65,6 +71,7 @@ class FakeSiteFixtureTest extends PackageManagerKernelTestBase {
    * @covers \Drupal\fixture_manipulator\FixtureManipulator::removePackage()
    */
   public function testCallToRemovePackage(): void {
+    $active_dir = $this->container->get('package_manager.path_locator')->getProjectRoot();
     $expected_packages = self::getExpectedFakeSitePackages();
     $stage = $this->createStage();
     $actual_packages = array_keys($stage->getActiveComposer()->getInstalledPackages());
@@ -72,9 +79,15 @@ class FakeSiteFixtureTest extends PackageManagerKernelTestBase {
     $this->assertSame($expected_packages, $actual_packages);
     foreach (self::getExpectedFakeSitePackages() as $package_name) {
       (new ActiveFixtureManipulator())
-        ->removePackage($package_name)
+        ->removePackage($package_name, $package_name === 'drupal/core-dev')
         ->commitChanges();
+      array_shift($expected_packages);
+      $list = $this->container->get('package_manager.composer_inspector')->getInstalledPackagesList($active_dir);
+      $actual_package_names = array_keys($list->getArrayCopy());
+      sort($actual_package_names);
+      $this->assertSame($expected_packages, $actual_package_names);
     }
+
   }
 
   /**
@@ -118,7 +131,15 @@ class FakeSiteFixtureTest extends PackageManagerKernelTestBase {
    * Tests that Composer show command can be used on the fixture.
    */
   public function testComposerShow(): void {
-    $process = new Process(['composer', 'show', '--format=json'], $this->container->get('package_manager.path_locator')->getProjectRoot());
+    $active_dir = $this->container->get('package_manager.path_locator')->getProjectRoot();
+    (new ActiveFixtureManipulator())
+      ->addPackage([
+        'type' => 'package',
+        'version' => '1.2.3',
+        'name' => 'any-org/any-package',
+      ])
+      ->commitChanges();
+    $process = new Process(['composer', 'show', '--format=json'], $active_dir);
     $process->run();
     if ($error = $process->getErrorOutput()) {
       $this->fail('Process error: ' . $error);
@@ -126,7 +147,19 @@ class FakeSiteFixtureTest extends PackageManagerKernelTestBase {
     $output = json_decode($process->getOutput(), TRUE);
     $package_names = array_map(fn (array $package) => $package['name'], $output['installed']);
     $this->assertTrue(asort($package_names));
-    $this->assertSame(['drupal/core', 'drupal/core-dev', 'drupal/core-recommended'], $package_names);
+    $this->assertSame(['any-org/any-package', 'drupal/core', 'drupal/core-dev', 'drupal/core-recommended'], $package_names);
+    $list = $this->container->get('package_manager.composer_inspector')->getInstalledPackagesList($active_dir);
+    $list_packages_names = array_keys($list->getArrayCopy());
+    $this->assertSame(['any-org/any-package', 'drupal/core', 'drupal/core-dev', 'drupal/core-recommended'], $list_packages_names);
+
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function register(ContainerBuilder $container) {
+    parent::register($container);
+    $container->getDefinition('package_manager.composer_inspector')->setPublic(TRUE);
   }
 
   /**
diff --git a/package_manager/tests/src/Kernel/FixtureManipulatorTest.php b/package_manager/tests/src/Kernel/FixtureManipulatorTest.php
index a69a0f8d9c..1728012d78 100644
--- a/package_manager/tests/src/Kernel/FixtureManipulatorTest.php
+++ b/package_manager/tests/src/Kernel/FixtureManipulatorTest.php
@@ -70,7 +70,6 @@ class FixtureManipulatorTest extends PackageManagerKernelTestBase {
           'name' => 'my/dev-package',
           'version' => '2.1.0',
           'type' => 'library',
-          'install_path' => '../relative/path',
         ],
         TRUE
       )
@@ -130,51 +129,39 @@ class FixtureManipulatorTest extends PackageManagerKernelTestBase {
       $this->assertStringContainsString("Expected package 'my/package' to not be installed, but it was.", $e->getMessage());
     }
 
-    // We should not be able to add a package with an absolute installation
-    // path.
-    try {
-      (new ActiveFixtureManipulator())
-        ->addPackage([
-          'name' => 'absolute/path',
-          'install_path' => '/absolute/path',
-          'type' => 'library',
-        ])
-        ->commitChanges();
-      $this->fail('Add package should have failed.');
-    }
-    catch (\UnexpectedValueException $e) {
-      $this->assertSame("'install_path' must start with '../'.", $e->getMessage());
-    }
-
     $installed_json_expected_packages = [
+      'my/dev-package' => [
+        'name' => 'my/dev-package',
+        'version' => '2.1.0',
+        'version_normalized' => '2.1.0.0',
+        'type' => 'library',
+      ],
       'my/package' => [
         'name' => 'my/package',
-        'type' => 'library',
         // If no version is specified in a new package it will be added.
         'version' => '1.2.3',
         'version_normalized' => '1.2.3.0',
-      ],
-      'my/dev-package' => [
-        'name' => 'my/dev-package',
-        'version' => '2.1.0',
         'type' => 'library',
-        'version_normalized' => '2.1.0.0',
       ],
     ];
     $installed_php_expected_packages = $installed_json_expected_packages;
-    // Composer stores `version_normalized`in 'installed.json' but not
-    // 'installed.php'.
-    unset($installed_php_expected_packages['my/dev-package']['version_normalized']);
-    unset($installed_php_expected_packages['my/package']['version_normalized']);
+    foreach ($installed_php_expected_packages as $package_name => &$expectation) {
+      // Composer stores `version_normalized`in 'installed.json' but in
+      // 'installed.php' that is just 'version', and 'version' is
+      // 'pretty_version'.
+      $expectation['pretty_version'] = $expectation['version'];
+      $expectation['version'] = $expectation['version_normalized'];
+      unset($expectation['version_normalized']);
+      // `name` is omitted in installed.php.
+      unset($expectation['name']);
+      // Compute the expected `install_path`.
+      $expectation['install_path'] = $expectation['type'] === 'metapackage' ? NULL : "$this->dir/vendor/composer/../$package_name";
+    }
     [$installed_json, $installed_php] = $this->getData();
     $installed_json['packages'] = array_intersect_key($installed_json['packages'], $installed_json_expected_packages);
-    $this->assertSame($installed_json_expected_packages, $installed_json['packages']);
+    $this->assertSame($installed_json_expected_packages, array_map(fn (array $package) => array_intersect_key($package, array_flip(['name', 'type', 'version', 'version_normalized'])), $installed_json['packages']));
     $this->assertContains('my/dev-package', $installed_json['dev-package-names']);
     $this->assertNotContains('my/package', $installed_json['dev-package-names']);
-    // In installed.php, the relative installation path of my/dev-package should
-    // have been prefixed with the __DIR__ constant, which should be interpreted
-    // when installed.php is loaded by the PHP runtime.
-    $installed_php_expected_packages['my/dev-package']['install_path'] = "$this->dir/vendor/composer/../relative/path";
 
     // None of the operations should have changed the original packages.
     $this->assertOriginalFixturePackagesUnchanged($installed_php);
@@ -182,7 +169,9 @@ class FixtureManipulatorTest extends PackageManagerKernelTestBase {
     // Remove the original packages since we have confirmed that they have not
     // changed.
     $installed_php = array_diff_key($installed_php, $this->originalInstalledPhp);
-    $this->assertSame($installed_php_expected_packages, $installed_php);
+    foreach ($installed_php_expected_packages as $package_name => $expected_data) {
+      $this->assertEquals($installed_php_expected_packages[$package_name], array_intersect_key($installed_php[$package_name], array_flip(['version', 'type', 'pretty_version', 'install_path'])), $package_name);
+    }
   }
 
   /**
@@ -192,17 +181,15 @@ class FixtureManipulatorTest extends PackageManagerKernelTestBase {
     $fs = (new Filesystem());
     // Assert ::modifyPackage() works with a package in an existing fixture not
     // created by ::addPackage().
-    $existing_fixture = __DIR__ . '/../../fixtures/FixtureUtilityTraitTest/existing_correct_fixture';
-    $temp_fixture = $this->siteDirectory . $this->randomMachineName('42');
-    $fs->mirror($existing_fixture, $temp_fixture);
-    $decode_installed_json = function () use ($temp_fixture) {
-      return json_decode(file_get_contents($temp_fixture . '/vendor/composer/installed.json'), TRUE, 512, JSON_THROW_ON_ERROR);
+    $decode_installed_json = function () {
+      return json_decode(file_get_contents($this->dir . '/vendor/composer/installed.json'), TRUE, 512, JSON_THROW_ON_ERROR);
     };
     $original_installed_json = $decode_installed_json();
     $this->assertIsArray($original_installed_json);
-    (new FixtureManipulator())
-      ->modifyPackage('the-org/the-package', ['install_path' => '../../a_new_path'])
-      ->commitChanges($temp_fixture);
+    (new ActiveFixtureManipulator())
+      // @see ::setUp()
+      ->modifyPackage('my/dev-package', ['version' => '2.1.0'])
+      ->commitChanges();
     $this->assertSame($original_installed_json, $decode_installed_json());
 
     // Assert that ::modifyPackage() throws an error if a package exists in the
@@ -214,7 +201,7 @@ class FixtureManipulatorTest extends PackageManagerKernelTestBase {
     $fs->mirror($existing_incorrect_fixture, $temp_fixture);
     try {
       (new FixtureManipulator())
-        ->modifyPackage('the-org/the-package', ['install_path' => '../../a_new_path'])
+        ->modifyPackage('the-org/the-package', ['irrelevant' => TRUE])
         ->commitChanges($temp_fixture);
       $this->fail('Modifying a non-existent package should raise an error.');
     }
@@ -237,7 +224,7 @@ class FixtureManipulatorTest extends PackageManagerKernelTestBase {
       // Add a key to an existing package.
       ->modifyPackage('my/package', ['type' => 'metapackage'])
       // Change a key in an existing package.
-      ->setVersion('my/dev-package', '3.2.1')
+      ->setVersion('my/dev-package', '3.2.1', TRUE)
       // Move an existing package to dev requirements.
       ->addPackage([
         'name' => 'my/other-package',
@@ -246,12 +233,6 @@ class FixtureManipulatorTest extends PackageManagerKernelTestBase {
       ->commitChanges();
 
     $install_json_expected_packages = [
-      'my/package' => [
-        'name' => 'my/package',
-        'type' => 'metapackage',
-        'version' => '1.2.3',
-        'version_normalized' => '1.2.3.0',
-      ],
       'my/dev-package' => [
         'name' => 'my/dev-package',
         'version' => '3.2.1',
@@ -260,21 +241,33 @@ class FixtureManipulatorTest extends PackageManagerKernelTestBase {
       ],
       'my/other-package' => [
         'name' => 'my/other-package',
+        'version' => '1.2.3',
+        'version_normalized' => '1.2.3.0',
         'type' => 'library',
+      ],
+      'my/package' => [
+        'name' => 'my/package',
         'version' => '1.2.3',
         'version_normalized' => '1.2.3.0',
+        'type' => 'metapackage',
       ],
     ];
     $installed_php_expected_packages = $install_json_expected_packages;
-    // Composer stores `version_normalized`in 'installed.json' but not
-    // 'installed.php'.
-    unset($installed_php_expected_packages['my/dev-package']['version_normalized']);
-    unset($installed_php_expected_packages['my/package']['version_normalized']);
-    unset($installed_php_expected_packages['my/other-package']['version_normalized']);
-    $installed_php_expected_packages['my/dev-package']['install_path'] = "$this->dir/vendor/composer/../relative/path";
+    foreach ($installed_php_expected_packages as $package_name => &$expectation) {
+      // Composer stores `version_normalized`in 'installed.json' but in
+      // 'installed.php' that is just 'version', and 'version' is
+      // 'pretty_version'.
+      $expectation['pretty_version'] = $expectation['version'];
+      $expectation['version'] = $expectation['version_normalized'];
+      unset($expectation['version_normalized']);
+      // `name` is omitted in installed.php.
+      unset($expectation['name']);
+      // Compute the expected `install_path`.
+      $expectation['install_path'] = $expectation['type'] === 'metapackage' ? NULL : "$this->dir/vendor/composer/../$package_name";
+    }
     [$installed_json, $installed_php] = $this->getData();
     $installed_json['packages'] = array_intersect_key($installed_json['packages'], $install_json_expected_packages);
-    $this->assertSame($install_json_expected_packages, $installed_json['packages']);
+    $this->assertSame($install_json_expected_packages, array_map(fn (array $package) => array_intersect_key($package, array_flip(['name', 'type', 'version', 'version_normalized'])), $installed_json['packages']));
     $this->assertContains('my/dev-package', $installed_json['dev-package-names']);
     $this->assertNotContains('my/other-package', $installed_json['dev-package-names']);
     $this->assertNotContains('my/package', $installed_json['dev-package-names']);
@@ -285,7 +278,9 @@ class FixtureManipulatorTest extends PackageManagerKernelTestBase {
     // Remove the original packages since we have confirmed that they have not
     // changed.
     $installed_php = array_diff_key($installed_php, $this->originalInstalledPhp);
-    $this->assertSame($installed_php_expected_packages, $installed_php);
+    foreach ($installed_php_expected_packages as $package_name => $expected_data) {
+      $this->assertEquals($installed_php_expected_packages[$package_name], array_intersect_key($installed_php[$package_name], array_flip(['version', 'type', 'pretty_version', 'install_path'])), $package_name);
+    }
   }
 
   /**
@@ -300,12 +295,12 @@ class FixtureManipulatorTest extends PackageManagerKernelTestBase {
       $this->fail('Removing a non-existent package should raise an error.');
     }
     catch (\LogicException $e) {
-      $this->assertStringContainsString("Expected package 'junk/drawer' to be installed, but it wasn't.", $e->getMessage());
+      $this->assertStringContainsString('junk/drawer is not required in your composer.json and has not been remove', $e->getMessage());
     }
 
     (new ActiveFixtureManipulator())
       ->removePackage('my/package')
-      ->removePackage('my/dev-package')
+      ->removePackage('my/dev-package', TRUE)
       ->commitChanges();
 
     foreach (['json', 'php'] as $extension) {
@@ -360,7 +355,6 @@ class FixtureManipulatorTest extends PackageManagerKernelTestBase {
     $fixture_manipulator = (new FixtureManipulator())
       ->addPackage([
         'name' => 'relative/project_path',
-        'install_path' => '../../relative/project_path',
         'type' => 'drupal-module',
       ])
       ->addDotGitFolder($project_root . "/relative/project_path")
diff --git a/package_manager/tests/src/Kernel/OverwriteExistingPackagesValidatorTest.php b/package_manager/tests/src/Kernel/OverwriteExistingPackagesValidatorTest.php
index cb54dc937c..ccda9ed7bd 100644
--- a/package_manager/tests/src/Kernel/OverwriteExistingPackagesValidatorTest.php
+++ b/package_manager/tests/src/Kernel/OverwriteExistingPackagesValidatorTest.php
@@ -5,9 +5,10 @@ declare(strict_types = 1);
 namespace Drupal\Tests\package_manager\Kernel;
 
 use Drupal\fixture_manipulator\ActiveFixtureManipulator;
+use Drupal\package_manager\Event\PostCreateEvent;
 use Drupal\package_manager\Event\PreApplyEvent;
 use Drupal\package_manager\ValidationResult;
-use Drupal\Tests\package_manager\Traits\FixtureUtilityTrait;
+use Drupal\Tests\package_manager\Traits\ComposerInstallersTrait;
 
 /**
  * @covers \Drupal\package_manager\Validator\OverwriteExistingPackagesValidator
@@ -16,7 +17,7 @@ use Drupal\Tests\package_manager\Traits\FixtureUtilityTrait;
  */
 class OverwriteExistingPackagesValidatorTest extends PackageManagerKernelTestBase {
 
-  use FixtureUtilityTrait;
+  use ComposerInstallersTrait;
 
   /**
    * {@inheritdoc}
@@ -26,98 +27,130 @@ class OverwriteExistingPackagesValidatorTest extends PackageManagerKernelTestBas
     // supported.
     $this->disableValidators[] = 'package_manager.validator.supported_releases';
     parent::setUp();
+
+    $this->installComposerInstallers($this->container->get('package_manager.path_locator')->getProjectRoot());
   }
 
   /**
    * Tests that new installed packages overwrite existing directories.
    *
-   * The fixture simulates a scenario where the active directory has three
-   * modules installed: module_1, module_2, and module_5. None of them are
-   * managed by Composer. These modules will be moved into the stage directory
-   * by the 'package_manager_bypass' module.
+   * The fixture simulates a scenario where the active directory has four
+   * modules installed: module_1, module_2, module_5 and module_6. None of them
+   * are managed by Composer. These modules will be moved into the stage
+   * directory by the 'package_manager_bypass' module.
    */
   public function testNewPackagesOverwriteExisting(): void {
     (new ActiveFixtureManipulator())
       ->addProjectAtPath('modules/module_1')
       ->addProjectAtPath('modules/module_2')
       ->addProjectAtPath('modules/module_5')
+      ->addProjectAtPath('modules/module_6')
       ->commitChanges();
     $stage_manipulator = $this->getStageFixtureManipulator();
 
+    $installer_paths = [];
     // module_1 and module_2 will raise errors because they would overwrite
     // non-Composer managed paths in the active directory.
-    $stage_manipulator
-      ->addPackage(
+    $stage_manipulator->addPackage(
         [
-          'name' => 'drupal/module_1',
+          'name' => 'drupal/other_module_1',
           'version' => '1.3.0',
           'type' => 'drupal-module',
-          'install_path' => '../../modules/module_1',
         ],
         FALSE,
-        FALSE
-      )
-      ->addPackage(
+        TRUE
+      );
+    $installer_paths['modules/module_1'] = ['drupal/other_module_1'];
+    $stage_manipulator->addPackage(
         [
-          'name' => 'drupal/module_2',
+          'name' => 'drupal/other_module_2',
           'version' => '1.3.0',
           'type' => 'drupal-module',
-          'install_path' => '../../modules/module_2',
         ],
         FALSE,
-        FALSE
+        TRUE,
       );
+    $installer_paths['modules/module_2'] = ['drupal/other_module_2'];
 
     // module_3 will cause no problems, since it doesn't exist in the active
     // directory at all.
     $stage_manipulator->addPackage([
-      'name' => 'drupal/module_3',
+      'name' => 'drupal/other_module_3',
       'version' => '1.3.0',
       'type' => 'drupal-module',
-      'install_path' => '../../modules/module_3',
-    ]);
+    ],
+    FALSE,
+        TRUE,
+    );
+    $installer_paths['modules/module_3'] = ['drupal/other_module_3'];
 
     // module_4 doesn't exist in the active directory but the 'install_path' as
-    // known to Composer in the staged directory collides with module_1 in the
+    // known to Composer in the staged directory collides with module_6 in the
     // active directory which will cause an error.
     $stage_manipulator->addPackage(
       [
         'name' => 'drupal/module_4',
         'version' => '1.3.0',
         'type' => 'drupal-module',
-        'install_path' => '../../modules/module_1',
       ],
       FALSE,
-      FALSE,
+      TRUE
     );
+    $installer_paths['modules/module_6'] = ['drupal/module_4'];
 
     // module_5_different_path will not cause a problem, even though its package
     // name is drupal/module_5, because its project name and path in the stage
     // directory differ from the active directory.
-    $stage_manipulator->addPackage([
-      'name' => 'drupal/module_5',
-      'version' => '1.3.0',
-      'type' => 'drupal-module',
-      'install_path' => '../../modules/module_5_different_path',
-    ]);
+    $stage_manipulator->addPackage(
+      [
+        'name' => 'drupal/other_module_5',
+        'version' => '1.3.0',
+        'type' => 'drupal-module',
+      ],
+      FALSE,
+      TRUE
+    );
+    $installer_paths['modules/module_5_different_path'] = ['drupal/other_module_5'];
+
+    // Set the installer path config in the active directory this will be
+    // copied to the stage directory where we install the packages.
+    $this->setInstallerPaths($installer_paths, $this->container->get('package_manager.path_locator')->getProjectRoot());
 
     // Add a package without an install_path set which will not raise an error.
     // The most common example of this in the Drupal ecosystem is a submodule.
-    $stage_manipulator->addPackage([
-      'name' => 'drupal/sub-module',
-      'version' => '1.3.0',
-      'type' => 'metapackage',
-    ]);
+    $stage_manipulator->addPackage(
+      [
+        'name' => 'drupal/sub-module',
+        'version' => '1.3.0',
+        'type' => 'metapackage',
+      ],
+      FALSE,
+      TRUE
+    );
+    $inspector = $this->container->get('package_manager.composer_inspector');
+    $listener = function (PostCreateEvent $event) use ($inspector) {
+      $list = $inspector->getInstalledPackagesList($event->stage->getStageDirectory());
+      $this->assertArrayHasKey('drupal/sub-module', $list->getArrayCopy());
+      $this->assertArrayHasKey('drupal/other_module_1', $list->getArrayCopy());
+      // Confirm that meta-package will have an install path that is the same
+      // as the stage directory.
+      // @todo Determine meta-packages should have a NULL path in
+      //   https://drupal.org/i/3345646.
+      $this->assertSame($list['drupal/sub-module']->path, $event->stage->getStageDirectory());
+      // Confirm another package has specified install path.
+      $this->assertSame($list['drupal/other_module_1']->path, $event->stage->getStageDirectory() . '/modules/module_1');
+    };
+    $this->addEventTestListener($listener, PostCreateEvent::class);
 
     $expected_results = [
       ValidationResult::createError([
-        t('The new package drupal/module_1 will be installed in the directory /vendor/composer/../../modules/module_1, which already exists but is not managed by Composer.'),
+        t('The new package drupal/module_4 will be installed in the directory /vendor/composer/../../modules/module_6, which already exists but is not managed by Composer.'),
       ]),
       ValidationResult::createError([
-        t('The new package drupal/module_2 will be installed in the directory /vendor/composer/../../modules/module_2, which already exists but is not managed by Composer.'),
+        t('The new package drupal/other_module_1 will be installed in the directory /vendor/composer/../../modules/module_1, which already exists but is not managed by Composer.'),
       ]),
       ValidationResult::createError([
-        t('The new package drupal/module_4 will be installed in the directory /vendor/composer/../../modules/module_1, which already exists but is not managed by Composer.'),
+        t('The new package drupal/other_module_2 will be installed in the directory /vendor/composer/../../modules/module_2, which already exists but is not managed by Composer.'),
       ]),
     ];
     $this->assertResults($expected_results, PreApplyEvent::class);
diff --git a/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php b/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
index 8dbf7cac6c..93763d131a 100644
--- a/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
+++ b/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
@@ -240,12 +240,6 @@ abstract class PackageManagerKernelTestBase extends KernelTestBase {
     $this->assertTrue(mkdir($active_dir));
     static::copyFixtureFilesTo($source_dir, $active_dir);
 
-    // Make sure that the path repositories exist in the test project too.
-    (new Filesystem())->mirror(__DIR__ . '/../../fixtures/path_repos', $root . DIRECTORY_SEPARATOR . 'path_repos', NULL, [
-      'override' => TRUE,
-      'delete' => FALSE,
-    ]);
-
     // Removing 'vfs://root/' from site path set in
     // \Drupal\KernelTests\KernelTestBase::setUpFilesystem as we don't use vfs.
     $test_site_path = str_replace('vfs://root/', '', $this->siteDirectory);
diff --git a/package_manager/tests/src/Kernel/PathExcluder/GitExcluderTest.php b/package_manager/tests/src/Kernel/PathExcluder/GitExcluderTest.php
index d1d620cae3..af74830be7 100644
--- a/package_manager/tests/src/Kernel/PathExcluder/GitExcluderTest.php
+++ b/package_manager/tests/src/Kernel/PathExcluder/GitExcluderTest.php
@@ -36,7 +36,6 @@ class GitExcluderTest extends PackageManagerKernelTestBase {
         'name' => 'foo/package_known_to_composer_removed_later',
         'type' => 'drupal-module',
         'version' => '1.0.0',
-        'install_path' => "../../modules/module_known_to_composer_removed_later",
       ])
       ->addProjectAtPath("modules/module_not_known_to_composer_in_active")
       ->addDotGitFolder($path_locator->getProjectRoot() . "/modules/module_not_known_to_composer_in_active")
diff --git a/package_manager/tests/src/Kernel/SupportedReleaseValidatorTest.php b/package_manager/tests/src/Kernel/SupportedReleaseValidatorTest.php
index f2f1e61c27..8e076e5354 100644
--- a/package_manager/tests/src/Kernel/SupportedReleaseValidatorTest.php
+++ b/package_manager/tests/src/Kernel/SupportedReleaseValidatorTest.php
@@ -33,25 +33,21 @@ class SupportedReleaseValidatorTest extends PackageManagerKernelTestBase {
         'name' => "drupal/semver_test",
         'version' => '8.1.0',
         'type' => 'drupal-module',
-        'install_path' => '../../modules/semver_test',
       ])
       ->addPackage([
         'name' => "drupal/aaa_update_test",
         'version' => '2.0.0',
         'type' => 'drupal-module',
-        'install_path' => '../../modules/aaa_update_test',
       ])
       ->addPackage([
         'name' => "drupal/package_manager_theme",
         'version' => '8.1.0',
         'type' => 'drupal-theme',
-        'install_path' => '../../modules/package_manager_theme',
       ])
       ->addPackage([
         'name' => "somewhere/a_drupal_module",
         'version' => '8.1.0',
         'type' => 'drupal-module',
-        'install_path' => '../../modules/a_drupal_module',
       ])
       ->commitChanges();
   }
@@ -75,7 +71,6 @@ class SupportedReleaseValidatorTest extends PackageManagerKernelTestBase {
           'name' => "drupal/semver_test",
           'version' => '8.1.1',
           'type' => 'drupal-module',
-          'install_path' => NULL,
         ],
         [],
       ],
@@ -88,7 +83,6 @@ class SupportedReleaseValidatorTest extends PackageManagerKernelTestBase {
           'name' => "drupal/semver_test",
           'version' => '8.2.0',
           'type' => 'drupal-module',
-          'install_path' => NULL,
         ],
         [
           ValidationResult::createError([t('semver_test (drupal/semver_test) 8.2.0')], $summary),
@@ -103,7 +97,6 @@ class SupportedReleaseValidatorTest extends PackageManagerKernelTestBase {
           'name' => "drupal/aaa_update_test",
           'version' => '2.1.0',
           'type' => 'drupal-module',
-          'install_path' => NULL,
         ],
         [],
       ],
@@ -116,7 +109,6 @@ class SupportedReleaseValidatorTest extends PackageManagerKernelTestBase {
           'name' => "drupal/aaa_update_test",
           'version' => '3.0.0',
           'type' => 'drupal-module',
-          'install_path' => NULL,
         ],
         [
           ValidationResult::createError([t('aaa_update_test (drupal/aaa_update_test) 3.0.0')], $summary),
@@ -131,7 +123,6 @@ class SupportedReleaseValidatorTest extends PackageManagerKernelTestBase {
           'name' => "drupal/aaa_automatic_updates_test",
           'version' => '7.0.1-dev',
           'type' => 'drupal-module',
-          'install_path' => '../../modules/aaa_automatic_updates_test',
         ],
         [
           ValidationResult::createError([t('aaa_automatic_updates_test (drupal/aaa_automatic_updates_test) 7.0.1-dev')], $summary),
@@ -146,7 +137,6 @@ class SupportedReleaseValidatorTest extends PackageManagerKernelTestBase {
           'name' => "drupal/aaa_automatic_updates_test",
           'version' => '7.0.1',
           'type' => 'drupal-module',
-          'install_path' => '../../modules/aaa_automatic_updates_test',
         ],
         [],
       ],
@@ -159,7 +149,6 @@ class SupportedReleaseValidatorTest extends PackageManagerKernelTestBase {
           'name' => "drupal/package_manager_theme",
           'version' => '8.1.1',
           'type' => 'drupal-theme',
-          'install_path' => NULL,
         ],
         [],
       ],
@@ -172,7 +161,6 @@ class SupportedReleaseValidatorTest extends PackageManagerKernelTestBase {
           'name' => "drupal/package_manager_theme",
           'version' => '8.2.0',
           'type' => 'drupal-theme',
-          'install_path' => NULL,
         ],
         [
           ValidationResult::createError(['package_manager_theme (drupal/package_manager_theme) 8.2.0'], $summary),
@@ -188,7 +176,6 @@ class SupportedReleaseValidatorTest extends PackageManagerKernelTestBase {
           'name' => "somewhere/a_drupal_module",
           'version' => '8.1.1',
           'type' => 'drupal-module',
-          'install_path' => NULL,
         ],
         [],
       ],
diff --git a/package_manager/tests/src/Traits/ComposerInstallersTrait.php b/package_manager/tests/src/Traits/ComposerInstallersTrait.php
new file mode 100644
index 0000000000..d3ce039a63
--- /dev/null
+++ b/package_manager/tests/src/Traits/ComposerInstallersTrait.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Drupal\Tests\package_manager\Traits;
+
+use Composer\Autoload\ClassLoader;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\fixture_manipulator\FixtureManipulator;
+use Symfony\Component\Process\Process;
+
+/**
+ * A utility for kernel tests that need to use 'composer/installers'.
+ *
+ * @internal
+ */
+trait ComposerInstallersTrait {
+
+  /**
+   * Installs the composer/installers package.
+   *
+   * @param string $dir
+   *   The fixture directory to install into.
+   */
+  private function installComposerInstallers(string $dir): void {
+    $loaders = ClassLoader::getRegisteredLoaders();
+    $real_project_root = key($loaders) . '/..';
+    $package_list = $this->container->get('package_manager.composer_inspector')->getInstalledPackagesList($real_project_root);
+    $this->assertArrayHasKey('composer/installers', $package_list);
+    $package_path = $package_list['composer/installers']->path;
+    $repository = json_encode([
+      'type' => 'path',
+      'url' => $package_path,
+      'options' => [
+        'symlink' => FALSE,
+      ],
+    ], JSON_UNESCAPED_SLASHES);
+    $working_dir_option = "--working-dir=$dir";
+    (new Process(['composer', 'config', 'repo.composer-installers-real', $repository, $working_dir_option]))->mustRun();
+    (new FixtureManipulator())
+      ->addConfig(['allow-plugins.composer/installers' => TRUE])
+      ->commitChanges($dir);
+    (new Process(['composer', 'require', 'composer/installers:@dev', $working_dir_option]))->mustRun();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function register(ContainerBuilder $container) {
+    parent::register($container);
+    $container->getDefinition('package_manager.composer_inspector')->setPublic(TRUE);
+  }
+
+  /**
+   * Sets the installer paths config.
+   *
+   * @param array $installer_paths
+   *   The installed paths.
+   * @param string $directory
+   *   The fixture directory.
+   */
+  private function setInstallerPaths(array $installer_paths, string $directory):void {
+    (new FixtureManipulator())
+      ->addConfig([
+        'extra.installer-paths' => $installer_paths,
+      ])
+      ->commitChanges($directory);
+  }
+
+}
diff --git a/package_manager/tests/src/Unit/InstalledPackagesDataTest.php b/package_manager/tests/src/Unit/InstalledPackagesDataTest.php
index c7957bf7e4..a486184f27 100644
--- a/package_manager/tests/src/Unit/InstalledPackagesDataTest.php
+++ b/package_manager/tests/src/Unit/InstalledPackagesDataTest.php
@@ -14,6 +14,8 @@ use Drupal\Tests\UnitTestCase;
  * certain operations. This test is intended as an early warning if the file's
  * internal structure changes in a way that would break our functionality.
  *
+ * @todo Delete this test in https://drupal.org/i/3316368.
+ *
  * @group package_manager
  * @internal
  */
diff --git a/src/Validator/StagedProjectsValidator.php b/src/Validator/StagedProjectsValidator.php
index 1fc86f7ab9..7049429cac 100644
--- a/src/Validator/StagedProjectsValidator.php
+++ b/src/Validator/StagedProjectsValidator.php
@@ -4,10 +4,12 @@ declare(strict_types = 1);
 
 namespace Drupal\automatic_updates\Validator;
 
-use Composer\Package\PackageInterface;
 use Drupal\automatic_updates\Updater;
+use Drupal\package_manager\ComposerInspector;
 use Drupal\package_manager\Event\PreApplyEvent;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\package_manager\InstalledPackage;
+use Drupal\package_manager\PathLocator;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 /**
@@ -22,6 +24,17 @@ final class StagedProjectsValidator implements EventSubscriberInterface {
 
   use StringTranslationTrait;
 
+  /**
+   * Constructs a StagedProjectsValidator object.
+   *
+   * @param \Drupal\package_manager\PathLocator $pathLocator
+   *   The path locator service.
+   * @param \Drupal\package_manager\ComposerInspector $composerInspector
+   *   The Composer inspector service.
+   */
+  public function __construct(private PathLocator $pathLocator, private ComposerInspector $composerInspector) {
+  }
+
   /**
    * Validates the staged packages.
    *
@@ -35,12 +48,14 @@ final class StagedProjectsValidator implements EventSubscriberInterface {
       return;
     }
 
+    // @todo Remove or update the try/catch blocks around these calls to
+    //   getInstalledPackagesList() in https://drupal.org/i/3344039.
     try {
-      $active = $stage->getActiveComposer();
-      $stage = $stage->getStageComposer();
+      $active_list = $this->composerInspector->getInstalledPackagesList($this->pathLocator->getProjectRoot());
+      $stage_list = $this->composerInspector->getInstalledPackagesList($stage->getStageDirectory());
     }
     catch (\Throwable $e) {
-      $event->addErrorFromThrowable($e);
+      $event->addError([$this->t('Unable to determine installed packages.')]);
       return;
     }
 
@@ -50,23 +65,23 @@ final class StagedProjectsValidator implements EventSubscriberInterface {
       'drupal-theme' => $this->t('theme'),
       'drupal-custom-theme' => $this->t('custom theme'),
     ];
-    $filter = function (PackageInterface $package) use ($type_map): bool {
-      return array_key_exists($package->getType(), $type_map);
+    $filter = function (InstalledPackage $package) use ($type_map): bool {
+      return array_key_exists($package->type, $type_map);
     };
-    $new_packages = $stage->getPackagesNotIn($active);
-    $removed_packages = $active->getPackagesNotIn($stage);
-    $updated_packages = $active->getPackagesWithDifferentVersionsIn($stage);
+    $new_packages = $stage_list->getPackagesNotIn($active_list);
+    $removed_packages = $active_list->getPackagesNotIn($stage_list);
+    $updated_packages = $active_list->getPackagesWithDifferentVersionsIn($stage_list);
 
     // Check if any new Drupal projects were installed.
-    if ($new_packages = array_filter($new_packages, $filter)) {
+    if ($new_packages = array_filter($new_packages->getArrayCopy(), $filter)) {
       $new_packages_messages = [];
 
       foreach ($new_packages as $new_package) {
         $new_packages_messages[] = $this->t(
           "@type '@name' installed.",
           [
-            '@type' => $type_map[$new_package->getType()],
-            '@name' => $new_package->getName(),
+            '@type' => $type_map[$new_package->type],
+            '@name' => $new_package->name,
           ]
         );
       }
@@ -79,14 +94,14 @@ final class StagedProjectsValidator implements EventSubscriberInterface {
     }
 
     // Check if any Drupal projects were removed.
-    if ($removed_packages = array_filter($removed_packages, $filter)) {
+    if ($removed_packages = array_filter($removed_packages->getArrayCopy(), $filter)) {
       $removed_packages_messages = [];
       foreach ($removed_packages as $removed_package) {
         $removed_packages_messages[] = $this->t(
           "@type '@name' removed.",
           [
-            '@type' => $type_map[$removed_package->getType()],
-            '@name' => $removed_package->getName(),
+            '@type' => $type_map[$removed_package->type],
+            '@name' => $removed_package->name,
           ]
         );
       }
@@ -100,18 +115,17 @@ final class StagedProjectsValidator implements EventSubscriberInterface {
 
     // Check if any Drupal projects were neither installed or removed, but had
     // their version numbers changed.
-    if ($updated_packages = array_filter($updated_packages, $filter)) {
-      $staged_packages = $stage->getInstalledPackages();
+    if ($updated_packages = array_filter($updated_packages->getArrayCopy(), $filter)) {
 
       $version_change_messages = [];
       foreach ($updated_packages as $name => $updated_package) {
         $version_change_messages[] = $this->t(
           "@type '@name' from @active_version to @staged_version.",
           [
-            '@type' => $type_map[$updated_package->getType()],
-            '@name' => $updated_package->getName(),
-            '@staged_version' => $staged_packages[$name]->getPrettyVersion(),
-            '@active_version' => $updated_package->getPrettyVersion(),
+            '@type' => $type_map[$updated_package->type],
+            '@name' => $updated_package->name,
+            '@staged_version' => $stage_list[$name]->version,
+            '@active_version' => $updated_package->version,
           ]
         );
       }
diff --git a/tests/src/Kernel/StatusCheck/ScaffoldFilePermissionsValidatorTest.php b/tests/src/Kernel/StatusCheck/ScaffoldFilePermissionsValidatorTest.php
index ccb8dd7684..e2c6bde263 100644
--- a/tests/src/Kernel/StatusCheck/ScaffoldFilePermissionsValidatorTest.php
+++ b/tests/src/Kernel/StatusCheck/ScaffoldFilePermissionsValidatorTest.php
@@ -295,6 +295,7 @@ class ScaffoldFilePermissionsValidatorTest extends AutomaticUpdatesKernelTestBas
   public function testScaffoldFilesChanged(array $write_protected_paths, array $active_scaffold_files, array $staged_scaffold_files, array $expected_results): void {
     // Rewrite the active and staged installed.json files, inserting the given
     // lists of scaffold files.
+    // @todo Remove the use of modifyPackage() in https://drupal.org/i/3345633.
     (new ActiveFixtureManipulator())
       ->modifyPackage('drupal/core', [
         'extra' => [
diff --git a/tests/src/Kernel/StatusCheck/StagedProjectsValidatorTest.php b/tests/src/Kernel/StatusCheck/StagedProjectsValidatorTest.php
index d11b04a1de..f0a7b9d12f 100644
--- a/tests/src/Kernel/StatusCheck/StagedProjectsValidatorTest.php
+++ b/tests/src/Kernel/StatusCheck/StagedProjectsValidatorTest.php
@@ -57,7 +57,7 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
     $updater->begin(['drupal' => '9.8.1']);
     $updater->stage();
 
-    $error = ValidationResult::createError([t("Composer could not find the config file: @composer_json\n", ["@composer_json" => $composer_json])]);
+    $error = ValidationResult::createError([t('Unable to determine installed packages.')]);
     try {
       $updater->apply();
       $this->fail('Expected an error, but none was raised.');
@@ -73,10 +73,9 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
   public function testProjectsAdded(): void {
     (new ActiveFixtureManipulator())
       ->addPackage([
-        'name' => 'drupal/test_module',
+        'name' => 'drupal/test-module',
         'version' => '1.3.0',
-        'type' => 'drupal_module',
-        'install_path' => '../../modules/test_module',
+        'type' => 'drupal-module',
       ])
       ->addPackage([
         'name' => 'other/removed',
@@ -85,10 +84,9 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
       ])
       ->addPackage(
         [
-          'name' => 'drupal/dev-test_module',
+          'name' => 'drupal/dev-test-module',
           'version' => '1.3.0',
           'type' => 'drupal-module',
-          'install_path' => '../../modules/dev_test_module',
         ],
         TRUE
       )
@@ -106,17 +104,15 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
     $stage_manipulator
       ->setCorePackageVersion('9.8.1')
       ->addPackage([
-        'name' => 'drupal/test_module2',
+        'name' => 'drupal/test-module2',
         'version' => '1.3.1',
         'type' => 'drupal-module',
-        'install_path' => '../../modules/test_module2',
       ])
       ->addPackage(
         [
-          'name' => 'drupal/dev-test_module2',
+          'name' => 'drupal/dev-test-module2',
           'version' => '1.3.1',
           'type' => 'drupal-custom-module',
-          'install_path' => '../../modules/dev-test_module2',
         ],
         TRUE
       )
@@ -126,14 +122,12 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
         'name' => 'other/new_project',
         'version' => '1.3.1',
         'type' => 'library',
-        'install_path' => '../other/new_project',
       ])
       ->addPackage(
         [
           'name' => 'other/dev-new_project',
           'version' => '1.3.1',
           'type' => 'library',
-          'install_path' => '../other/dev-new_project',
         ],
         TRUE
       )
@@ -141,8 +135,8 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
       ->removePackage('other/dev-removed');
 
     $messages = [
-      t("module 'drupal/test_module2' installed."),
-      t("custom module 'drupal/dev-test_module2' installed."),
+      t("custom module 'drupal/dev-test-module2' installed."),
+      t("module 'drupal/test-module2' installed."),
     ];
     $error = ValidationResult::createError($messages, t('The update cannot proceed because the following Drupal projects were installed during the update.'));
 
@@ -168,13 +162,11 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
         'name' => 'drupal/test_theme',
         'version' => '1.3.0',
         'type' => 'drupal-theme',
-        'install_path' => '../../themes/test_theme',
       ])
       ->addPackage([
-        'name' => 'drupal/test_module2',
+        'name' => 'drupal/test-module2',
         'version' => '1.3.1',
         'type' => 'drupal-module',
-        'install_path' => '../../modules/test_module2',
       ])
       ->addPackage([
         'name' => 'other/removed',
@@ -186,16 +178,14 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
           'name' => 'drupal/dev-test_theme',
           'version' => '1.3.0',
           'type' => 'drupal-custom-theme',
-          'install_path' => '../../modules/dev_test_theme',
         ],
         TRUE
       )
       ->addPackage(
         [
-          'name' => 'drupal/dev-test_module2',
+          'name' => 'drupal/dev-test-module2',
           'version' => '1.3.1',
           'type' => 'drupal-module',
-          'install_path' => '../../modules/dev_test_module2',
         ],
         TRUE
       )
@@ -211,7 +201,7 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
 
     $stage_manipulator = $this->getStageFixtureManipulator();
     $stage_manipulator->removePackage('drupal/test_theme')
-      ->removePackage('drupal/dev-test_theme')
+      ->removePackage('drupal/dev-test_theme', TRUE)
     // The validator shouldn't complain about these packages being removed,
     // since it only cares about Drupal modules and themes.
       ->removePackage('other/removed')
@@ -219,8 +209,8 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
       ->setCorePackageVersion('9.8.1');
 
     $messages = [
-      t("theme 'drupal/test_theme' removed."),
       t("custom theme 'drupal/dev-test_theme' removed."),
+      t("theme 'drupal/test_theme' removed."),
     ];
     $error = ValidationResult::createError($messages, t('The update cannot proceed because the following Drupal projects were removed during the update.'));
     $updater = $this->container->get('automatic_updates.updater');
@@ -242,10 +232,9 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
     (new ActiveFixtureManipulator())
       ->setCorePackageVersion('9.8.0')
       ->addPackage([
-        'name' => 'drupal/test_module',
+        'name' => 'drupal/test-module',
         'version' => '1.3.0',
         'type' => 'drupal-module',
-        'install_path' => '../../modules/test_module',
       ])
       ->addPackage([
         'name' => 'other/changed',
@@ -254,10 +243,9 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
       ])
       ->addPackage(
         [
-          'name' => 'drupal/dev-test_module',
+          'name' => 'drupal/dev-test-module',
           'version' => '1.3.0',
           'type' => 'drupal-module',
-          'install_path' => '../../modules/dev_test_module',
         ],
         TRUE
       )
@@ -272,8 +260,8 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
       ->commitChanges();
 
     $stage_manipulator = $this->getStageFixtureManipulator();
-    $stage_manipulator->setVersion('drupal/test_module', '1.3.1')
-      ->setVersion('drupal/dev-test_module', '1.3.1')
+    $stage_manipulator->setVersion('drupal/test-module', '1.3.1')
+      ->setVersion('drupal/dev-test-module', '1.3.1')
     // The validator shouldn't complain about these packages being updated,
     // because it only cares about Drupal modules and themes.
       ->setVersion('other/changed', '1.3.2')
@@ -281,8 +269,8 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
       ->setCorePackageVersion('9.8.1');
 
     $messages = [
-      t("module 'drupal/test_module' from 1.3.0 to 1.3.1."),
-      t("module 'drupal/dev-test_module' from 1.3.0 to 1.3.1."),
+      t("module 'drupal/dev-test-module' from 1.3.0 to 1.3.1."),
+      t("module 'drupal/test-module' from 1.3.0 to 1.3.1."),
     ];
     $error = ValidationResult::createError($messages, t('The update cannot proceed because the following Drupal projects were unexpectedly updated. Only Drupal Core updates are currently supported.'));
     $updater = $this->container->get('automatic_updates.updater');
@@ -305,10 +293,9 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
     (new ActiveFixtureManipulator())
       ->setCorePackageVersion('9.8.0')
       ->addPackage([
-        'name' => 'drupal/test_module',
+        'name' => 'drupal/test-module',
         'version' => '1.3.0',
         'type' => 'drupal-module',
-        'install_path' => '../../modules/test_module',
       ])
       ->addPackage([
         'name' => 'other/removed',
@@ -322,10 +309,9 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
       ])
       ->addPackage(
         [
-          'name' => 'drupal/dev-test_module',
+          'name' => 'drupal/dev-test-module',
           'version' => '1.3.0',
           'type' => 'drupal-module',
-          'install_path' => '../../modules/dev_test_module',
         ],
         TRUE
       )
@@ -355,14 +341,12 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
         'name' => 'other/new_project',
         'version' => '1.3.1',
         'type' => 'library',
-        'install_path' => '../other/new_project',
       ])
       ->addPackage(
         [
           'name' => 'other/dev-new_project',
           'version' => '1.3.1',
           'type' => 'library',
-          'install_path' => '../other/dev-new_project',
         ],
         TRUE
       )
-- 
GitLab