diff --git a/automatic_updates.info.yml b/automatic_updates.info.yml
index 37d097bec121bc7e350c9a022692699ecf4e23bd..119d1f4b970b530d95ea7f019206fa28ad85f366 100644
--- a/automatic_updates.info.yml
+++ b/automatic_updates.info.yml
@@ -1,7 +1,7 @@
 name: 'Automatic Updates'
 type: module
 description: 'Automatically updates Drupal core.'
-core_version_requirement: ^9.3
+core_version_requirement: ^9.3 || ^10
 dependencies:
   - drupal:package_manager
   - drupal:update
diff --git a/automatic_updates.module b/automatic_updates.module
index 83f563cfda0b822cde202cb0681d3ab58be87070..4f86a05f32636d07dd32ef13ad5486fcb07b54e4 100644
--- a/automatic_updates.module
+++ b/automatic_updates.module
@@ -164,6 +164,12 @@ function automatic_updates_module_implements_alter(&$implementations, $hook) {
  * Implements hook_cron().
  */
 function automatic_updates_cron() {
+  // @todo Refactor this after https://www.drupal.org/project/drupal/issues/2969056
+  // @todo Remove this after https://www.drupal.org/project/drupal/issues/3318964
+  if (defined('MAINTENANCE_MODE') || stripos($_SERVER['PHP_SELF'], 'update.php') !== FALSE) {
+    return;
+  }
+
   /** @var \Drupal\automatic_updates\CronUpdater $updater */
   $updater = \Drupal::service('automatic_updates.cron_updater');
   $updater->handleCron();
diff --git a/automatic_updates_extensions/automatic_updates_extensions.info.yml b/automatic_updates_extensions/automatic_updates_extensions.info.yml
index 1dc4083332e5a1eeb203d85dfffe37664a5350bf..d430f2f7819f995a9e89696fc68f79656077567c 100644
--- a/automatic_updates_extensions/automatic_updates_extensions.info.yml
+++ b/automatic_updates_extensions/automatic_updates_extensions.info.yml
@@ -1,7 +1,7 @@
 name: 'Automatic Updates Extensions'
 type: module
 description: 'Allows updates to themes and modules'
-core_version_requirement: ^9.3
+core_version_requirement: ^9.3 || ^10
 lifecycle: experimental
 dependencies:
   - drupal:automatic_updates
diff --git a/automatic_updates_extensions/tests/fixtures/new_module/1.0.0/new_module.info.yml.hide b/automatic_updates_extensions/tests/fixtures/new_module/1.0.0/new_module.info.yml.hide
index 24cbeec14e6d1b8966cf9bfdeccdbee49e8c069b..4a4b0ec8038fd68108e9e7064bbeb823807b6154 100644
--- a/automatic_updates_extensions/tests/fixtures/new_module/1.0.0/new_module.info.yml.hide
+++ b/automatic_updates_extensions/tests/fixtures/new_module/1.0.0/new_module.info.yml.hide
@@ -1,4 +1,4 @@
 name: 'New module'
 type: module
-core_version_requirement: ^9
+core_version_requirement: ^9 || ^10
 project: new_module
diff --git a/automatic_updates_extensions/tests/fixtures/new_module/1.1.0/new_module.info.yml.hide b/automatic_updates_extensions/tests/fixtures/new_module/1.1.0/new_module.info.yml.hide
index 24cbeec14e6d1b8966cf9bfdeccdbee49e8c069b..4a4b0ec8038fd68108e9e7064bbeb823807b6154 100644
--- a/automatic_updates_extensions/tests/fixtures/new_module/1.1.0/new_module.info.yml.hide
+++ b/automatic_updates_extensions/tests/fixtures/new_module/1.1.0/new_module.info.yml.hide
@@ -1,4 +1,4 @@
 name: 'New module'
 type: module
-core_version_requirement: ^9
+core_version_requirement: ^9 || ^10
 project: new_module
diff --git a/composer.json b/composer.json
index 816fe8e97266df413f7c3c7ec3c4cd0ce252bd10..981d23d6407590698bd0f86c9e03e12be5d6a5e3 100644
--- a/composer.json
+++ b/composer.json
@@ -12,7 +12,7 @@
   },
   "require": {
     "ext-json": "*",
-    "drupal/core": "^9.3",
+    "drupal/core": "^9.3 || ^10",
     "php-tuf/composer-stager": "^1.2",
     "composer/composer": "^2.2.12 || ^2.3.5",
     "composer-runtime-api": "^2.1",
@@ -39,6 +39,9 @@
     "phpcs": "Checks code for standards compliance.",
     "test": "Runs PHPUnit tests.",
     "core-convert": "Converts this module to a core merge request. Excepts 2 arguments. 1) The core clone directory. 2) The core merge request branch."
+  },
+  "require-dev": {
+    "colinodell/psr-testlogger": "^1"
   },
     "autoload": {
         "psr-4": {
diff --git a/dictionary.txt b/dictionary.txt
index 7b028abdb9523c32e6d99b10c7cd6c07e771874b..0665a7c4f1069b7c6e197ebcf8d63ac0b54aeee6 100644
--- a/dictionary.txt
+++ b/dictionary.txt
@@ -1,8 +1,10 @@
-unrequested
 updater's
 stager's
 syncer
 syncers
+unrequested
 kirk
+colinodell
+testlogger
 unwritable
 filedate
diff --git a/drupalci.yml b/drupalci.yml
index f2a7f57ff567b63582c5fd3673dc4e84091de05c..9f466e09e62e2946f9f8244d1f9225fdc99665df 100644
--- a/drupalci.yml
+++ b/drupalci.yml
@@ -32,6 +32,8 @@ build:
           - sed -i "s/yarn run -s spellcheck/yarn run -s spellcheck --root \$TOP_LEVEL/" modules/contrib/automatic_updates/commit-code-check.sh
           # Add our words to the dictionary.
           - cat modules/contrib/automatic_updates/dictionary.txt >> core/misc/cspell/dictionary.txt
+          # Ensure we have the full path to PHPStan.
+          - sed -i "s/vendor\/bin\/phpstan/\/var\/www\/html\/vendor\/bin\/phpstan/" modules/contrib/automatic_updates/commit-code-check.sh
           # After all of the shenanigans above, we're finally ready to run core's `commit-code-check.sh`! :)
           - "modules/contrib/automatic_updates/commit-code-check.sh --drupalci"
           # Restore the original permissions.
diff --git a/package_manager/package_manager.info.yml b/package_manager/package_manager.info.yml
index cf0acc27fbb3977de5d0d717c160d248fc85e3ea..714c91503b1379aeb434e4a7a6ed8d930f9c0a05 100644
--- a/package_manager/package_manager.info.yml
+++ b/package_manager/package_manager.info.yml
@@ -1,7 +1,7 @@
 name: 'Package Manager'
 type: module
 description: 'API module providing functionality for staging package installs and updates with Composer.'
-core_version_requirement: ^9.3
+core_version_requirement: ^9.3 || ^10
 php: 7.4
 dependencies:
   - drupal:update
diff --git a/package_manager/src/ComposerUtility.php b/package_manager/src/ComposerUtility.php
index be4053b1273bba512a891d4a995ffb567b3cf6f8..70e5b891c6d9659dc340aa5e57b786ed690a2468 100644
--- a/package_manager/src/ComposerUtility.php
+++ b/package_manager/src/ComposerUtility.php
@@ -8,6 +8,7 @@ use Composer\IO\NullIO;
 use Composer\Package\Loader\ValidatingArrayLoader;
 use Composer\Package\PackageInterface;
 use Composer\Package\Version\VersionParser;
+use Composer\PartialComposer;
 use Composer\Semver\Comparator;
 use Drupal\Component\Serialization\Yaml;
 
@@ -30,23 +31,46 @@ class ComposerUtility {
    */
   private static $corePackages;
 
+  /**
+   * Whether to raise a deprecation error when the constructor is called.
+   *
+   * @var bool
+   */
+  private static $triggerConstructorDeprecation = TRUE;
+
   /**
    * Constructs a new ComposerUtility object.
    *
-   * @param \Composer\Composer $composer
+   * @param \Composer\Composer|\Composer\PartialComposer $composer
    *   The Composer instance.
    */
-  public function __construct(Composer $composer) {
+  public function __construct(object $composer) {
+    // @todo Remove this in https://www.drupal.org/project/automatic_updates/issues/3321474.
+    if (self::$triggerConstructorDeprecation) {
+      @trigger_error(__METHOD__ . '() is deprecated in automatic_updates:8.x-2.5 and will be removed in automatic_updates:3.0.0. Use ' . __CLASS__ . '::createForDirectory() instead. See https://www.drupal.org/node/3314137.', E_USER_DEPRECATED);
+    }
+    self::$triggerConstructorDeprecation = TRUE;
+
+    // @todo Remove this check when either:
+    //   - PHP 8 or later is required, in which case the $composer type hint
+    //     should be Composer|PartialComposer.
+    //   - Composer 2.3 or later is required, in which case the $composer type
+    //     hint should be changed to PartialComposer.
+    // @todo Update in https://www.drupal.org/project/automatic_updates/issues/3321474
+    // @todo Update in https://www.drupal.org/project/automatic_updates/issues/3321476
+    if (!$composer instanceof Composer && !$composer instanceof PartialComposer) {
+      throw new \InvalidArgumentException('The $composer argument must be an instance of ' . Composer::class . ' or ' . PartialComposer::class);
+    }
     $this->composer = $composer;
   }
 
   /**
    * Returns the underlying Composer instance.
    *
-   * @return \Composer\Composer
+   * @return \Composer\Composer|\Composer\PartialComposer
    *   The Composer instance.
    */
-  public function getComposer(): Composer {
+  public function getComposer(): object {
     return $this->composer;
   }
 
@@ -58,10 +82,21 @@ class ComposerUtility {
    *
    * @return \Drupal\package_manager\ComposerUtility
    *   The utility object.
+   *
+   * @throws \InvalidArgumentException
+   *   When $dir does not contain a composer.json file.
    */
   public static function createForDirectory(string $dir): self {
     $io = new NullIO();
+
+    // Pre-load the contents of composer.json so that Factory::createComposer()
+    // won't try to call realpath(), which will fail if composer.json is in a
+    // virtual file system.
     $configuration = $dir . DIRECTORY_SEPARATOR . 'composer.json';
+    if (file_exists($configuration)) {
+      $configuration = file_get_contents($configuration);
+      $configuration = json_decode($configuration, TRUE, 512, JSON_THROW_ON_ERROR);
+    }
 
     // The Composer factory requires that either the HOME or COMPOSER_HOME
     // environment variables be set, so momentarily set the COMPOSER_HOME
@@ -87,6 +122,7 @@ class ComposerUtility {
     putenv("COMPOSER_HOME=$home");
     putenv("COMPOSER_HTACCESS_PROTECT=$htaccess");
 
+    self::$triggerConstructorDeprecation = FALSE;
     return new static($composer);
   }
 
diff --git a/package_manager/tests/fixtures/alpha/1.0.0/alpha.info.yml.hide b/package_manager/tests/fixtures/alpha/1.0.0/alpha.info.yml.hide
index 4b12c91c911ffd8506961b4e1aff65baedf28c9b..877af2ef708fc4ee6ff62ed5c8d0b9a7dd611e31 100644
--- a/package_manager/tests/fixtures/alpha/1.0.0/alpha.info.yml.hide
+++ b/package_manager/tests/fixtures/alpha/1.0.0/alpha.info.yml.hide
@@ -1,4 +1,4 @@
 name: Alpha
 type: module
-core_version_requirement: ^9
+core_version_requirement: ^9 || ^10
 project: alpha
diff --git a/package_manager/tests/fixtures/alpha/1.1.0/alpha.info.yml.hide b/package_manager/tests/fixtures/alpha/1.1.0/alpha.info.yml.hide
index 4b12c91c911ffd8506961b4e1aff65baedf28c9b..877af2ef708fc4ee6ff62ed5c8d0b9a7dd611e31 100644
--- a/package_manager/tests/fixtures/alpha/1.1.0/alpha.info.yml.hide
+++ b/package_manager/tests/fixtures/alpha/1.1.0/alpha.info.yml.hide
@@ -1,4 +1,4 @@
 name: Alpha
 type: module
-core_version_requirement: ^9
+core_version_requirement: ^9 || ^10
 project: alpha
diff --git a/package_manager/tests/fixtures/post_update.php b/package_manager/tests/fixtures/post_update.php
new file mode 100644
index 0000000000000000000000000000000000000000..a596cbe1d0f8f73ea3c53aaa58b3fc8b4f1e4acd
--- /dev/null
+++ b/package_manager/tests/fixtures/post_update.php
@@ -0,0 +1,12 @@
+<?php
+
+/**
+ * @file
+ * Contains a fake database post-update function for testing.
+ */
+
+/**
+ * Here is a fake post-update hook.
+ */
+function package_manager_post_update_test() {
+}
diff --git a/package_manager/tests/src/Build/TemplateProjectTestBase.php b/package_manager/tests/src/Build/TemplateProjectTestBase.php
index 969f42bd342d06f15a6c6d2a986928253aca978a..d183bf6924f39d7a922ddb44e8d5fa49c577fe1f 100644
--- a/package_manager/tests/src/Build/TemplateProjectTestBase.php
+++ b/package_manager/tests/src/Build/TemplateProjectTestBase.php
@@ -217,6 +217,13 @@ END;
     // Disable Packagist entirely so that we don't test the Internet.
     $this->runComposer('composer config repo.packagist.org false', $template_dir);
 
+    // Allow any version of the Drupal core packages in the template project.
+    $this->runComposer('composer require --no-update drupal/core-recommended:* drupal/core-project-message:* drupal/core-composer-scaffold:*', $template_dir);
+    $this->runComposer('composer require --no-update --dev drupal/core-dev:*', $template_dir);
+    if ($template === 'LegacyProject') {
+      $this->runComposer('composer require --no-update drupal/core-vendor-hardening:*', $template_dir);
+    }
+
     // Create the test project, defining its repository as part of the
     // `composer create-project` command.
     $repository = [
@@ -327,6 +334,11 @@ END;
           $package['require']['symfony/polyfill-php72'],
           $package['require']['symfony/polyfill-php73']
         );
+        // If we're running on Drupal 10, which requires PHP 8.1 or later, this
+        // polyfill won't be installed, so make sure it's not required.
+        if (str_starts_with(\Drupal::VERSION, '10.')) {
+          unset($package['require']['symfony/polyfill-php80']);
+        }
         // Disabling symlinks in the transport options doesn't seem to have an
         // effect, so we use the COMPOSER_MIRROR_PATH_REPOS environment variable
         // to force mirroring in ::createTestProject().
diff --git a/package_manager/tests/src/Kernel/ComposerPatchesValidatorTest.php b/package_manager/tests/src/Kernel/ComposerPatchesValidatorTest.php
index 482f0a167d9261702de726c39abe03835b55ec45..389000e23f0e4e23b9b0781efd16528d21fd9b58 100644
--- a/package_manager/tests/src/Kernel/ComposerPatchesValidatorTest.php
+++ b/package_manager/tests/src/Kernel/ComposerPatchesValidatorTest.php
@@ -32,8 +32,11 @@ class ComposerPatchesValidatorTest extends PackageManagerKernelTestBase {
     ];
     $file->write($data);
 
+    // Because ComposerUtility reads composer.json and passes it to the Composer
+    // factory as an array, Composer will assume that the configuration is
+    // coming from a config.json file, even if one doesn't exist.
     $error = ValidationResult::createError([
-      'The <code>cweagans/composer-patches</code> plugin is installed, but the <code>composer-exit-on-patch-failure</code> key is not set to <code>true</code> in the <code>extra</code> section of composer.json.',
+      "The <code>cweagans/composer-patches</code> plugin is installed, but the <code>composer-exit-on-patch-failure</code> key is not set to <code>true</code> in the <code>extra</code> section of $dir/config.json.",
     ]);
     $this->assertStatusCheckResults([$error]);
     $this->assertResults([$error], PreCreateEvent::class);
diff --git a/package_manager/tests/src/Kernel/ComposerUtilityTest.php b/package_manager/tests/src/Kernel/ComposerUtilityTest.php
index 5433200767efba239712f45ce94535a22daffd83..b60bcc3cd626be3e2884102cb724f626b98714e9 100644
--- a/package_manager/tests/src/Kernel/ComposerUtilityTest.php
+++ b/package_manager/tests/src/Kernel/ComposerUtilityTest.php
@@ -32,6 +32,17 @@ class ComposerUtilityTest extends KernelTestBase {
     static::copyFixtureFilesTo(__DIR__ . '/../../fixtures/project_package_conversion', $fixture->url());
   }
 
+  /**
+   * Tests that ComposerUtility::CreateForDirectory() validates the directory.
+   */
+  public function testCreateForDirectoryValidation(): void {
+    $this->expectException(\InvalidArgumentException::class);
+    $this->expectExceptionMessage('Composer could not find the config file: vfs://root/composer.json');
+
+    $dir = vfsStream::setup()->url();
+    ComposerUtility::createForDirectory($dir);
+  }
+
   /**
    * Tests that ComposerUtility disables automatic creation of .htaccess files.
    */
diff --git a/package_manager/tests/src/Kernel/ExecutableFinderTest.php b/package_manager/tests/src/Kernel/ExecutableFinderTest.php
index ff7ed9933a83b3a466090a116f4d54a75956c298..c8da1b68dff9a21b7dd8fa7382653fcf5f80424a 100644
--- a/package_manager/tests/src/Kernel/ExecutableFinderTest.php
+++ b/package_manager/tests/src/Kernel/ExecutableFinderTest.php
@@ -24,7 +24,7 @@ class ExecutableFinderTest extends PackageManagerKernelTestBase {
       /**
        * {@inheritdoc}
        */
-      public function find($name, $default = NULL, array $extraDirs = []) {
+      public function find($name, $default = NULL, array $extraDirs = []): ?string {
         return '/dev/null';
       }
 
diff --git a/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php b/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
index 354f3099e869ecea0fc228242e5119321e4ce5c3..5170d92aa6571d266e303f7aa5ef3e59e03575c9 100644
--- a/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
+++ b/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
@@ -76,8 +76,9 @@ abstract class PackageManagerKernelTestBase extends KernelTestBase {
     $this->createVirtualProject();
 
     // The Update module's default configuration must be installed for our
-    // fake release metadata to be fetched.
-    $this->installConfig('update');
+    // fake release metadata to be fetched, and the System module's to ensure
+    // the site has a name.
+    $this->installConfig(['system', 'update']);
 
     // Make the update system think that all of System's post-update functions
     // have run.
diff --git a/package_manager/tests/src/Kernel/PendingUpdatesValidatorTest.php b/package_manager/tests/src/Kernel/PendingUpdatesValidatorTest.php
index e41ccd8c9e8d5dbd84f572c92457c9706abda589..192073a644f7784a7bc048ff1374c73ae116b89c 100644
--- a/package_manager/tests/src/Kernel/PendingUpdatesValidatorTest.php
+++ b/package_manager/tests/src/Kernel/PendingUpdatesValidatorTest.php
@@ -52,8 +52,9 @@ class PendingUpdatesValidatorTest extends PackageManagerKernelTestBase {
    */
   public function testPendingPostUpdate(): void {
     $this->registerPostUpdateFunctions();
-    // The System module's post-update functions have not been registered, so
-    // the update registry will think they're pending.
+    // Make an additional post-update function available; the update registry
+    // will think it's pending.
+    require_once __DIR__ . '/../../fixtures/post_update.php';
     $result = ValidationResult::createError([
       'Some modules have database schema updates to install. You should run the <a href="/update.php">database update script</a> immediately.',
     ]);
diff --git a/package_manager/tests/src/Kernel/StageOwnershipTest.php b/package_manager/tests/src/Kernel/StageOwnershipTest.php
index b945927b6fbfe52b3a47fd39fdf63ac0df745d2a..84f465782f5e7277428bb8bbcb6f459d3a06aedb 100644
--- a/package_manager/tests/src/Kernel/StageOwnershipTest.php
+++ b/package_manager/tests/src/Kernel/StageOwnershipTest.php
@@ -11,7 +11,7 @@ use Drupal\package_manager\Exception\StageOwnershipException;
 use Drupal\package_manager_bypass\Stager;
 use Drupal\package_manager_test_validation\EventSubscriber\TestSubscriber;
 use Drupal\Tests\user\Traits\UserCreationTrait;
-use Psr\Log\Test\TestLogger;
+use ColinODell\PsrTestLogger\TestLogger;
 
 /**
  * Tests that ownership of the stage is enforced.
diff --git a/package_manager/tests/src/Kernel/StageTest.php b/package_manager/tests/src/Kernel/StageTest.php
index 392d4e697c38a9f2758b493d94a02c80fa25a76b..61cf2a80ec53a6c5349601d6a673a9c56af5680a 100644
--- a/package_manager/tests/src/Kernel/StageTest.php
+++ b/package_manager/tests/src/Kernel/StageTest.php
@@ -20,7 +20,7 @@ use PhpTuf\ComposerStager\Domain\Exception\PreconditionException;
 use Drupal\package_manager_bypass\Beginner;
 use PhpTuf\ComposerStager\Domain\Service\Precondition\PreconditionInterface;
 use Psr\Log\LogLevel;
-use Psr\Log\Test\TestLogger;
+use ColinODell\PsrTestLogger\TestLogger;
 
 /**
  * @coversDefaultClass \Drupal\package_manager\Stage
diff --git a/src/CronUpdater.php b/src/CronUpdater.php
index 6f1e715b7d78daf15e3581cf56a0e3139f542a7a..d6eb0a1e417ff90b26ee47c0cfd04226c43c2ea6 100644
--- a/src/CronUpdater.php
+++ b/src/CronUpdater.php
@@ -54,13 +54,6 @@ class CronUpdater extends Updater {
    */
   public const ALL = 'patch';
 
-  /**
-   * The logger.
-   *
-   * @var \Psr\Log\LoggerInterface
-   */
-  protected $logger;
-
   /**
    * The cron release chooser service.
    *
diff --git a/src/Validator/CronServerValidator.php b/src/Validator/CronServerValidator.php
index 41e3fe3e61f18dc383bad3f85b5daafde3801f65..44d2eabdf9f16825a731f81ac4f705813d379ca2 100644
--- a/src/Validator/CronServerValidator.php
+++ b/src/Validator/CronServerValidator.php
@@ -5,13 +5,13 @@ namespace Drupal\automatic_updates\Validator;
 use Drupal\automatic_updates\CronUpdater;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
-use Drupal\Core\Http\RequestStack;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\Url;
 use Drupal\package_manager\Event\PreCreateEvent;
 use Drupal\package_manager\Event\PreOperationStageEvent;
 use Drupal\package_manager\Event\StatusCheckEvent;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
 
 /**
  * Validates that the current server configuration can run cron updates.
@@ -59,7 +59,7 @@ final class CronServerValidator implements EventSubscriberInterface {
   /**
    * Constructs a CronServerValidator object.
    *
-   * @param \Drupal\Core\Http\RequestStack $request_stack
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
    *   The request stack service.
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The config factory service.
diff --git a/src/Validator/VersionPolicy/ForbidDowngrade.php b/src/Validator/VersionPolicy/ForbidDowngrade.php
index c71fc54fbeabfd78a5954610032bb4a8c476a256..3ddb310cc3c5118081890d64c30c3f52e5879920 100644
--- a/src/Validator/VersionPolicy/ForbidDowngrade.php
+++ b/src/Validator/VersionPolicy/ForbidDowngrade.php
@@ -29,6 +29,8 @@ final class ForbidDowngrade {
    *   The error messages, if any.
    */
   public function validate(string $installed_version, ?string $target_version): array {
+    // TRICKY: \Drupal\automatic_updates\Validator\VersionPolicyValidator::getTargetVersion() may potentially not be able to determine a version.
+    $target_version = $target_version ?? '';
     if (Comparator::lessThan($target_version, $installed_version)) {
       return [
         $this->t('Update version @target_version is lower than @installed_version, downgrading is not supported.', [
diff --git a/tests/fixtures/automatic_updates-installed.php b/tests/fixtures/automatic_updates-installed.php
index 57c418d12b63f5ea5445af49eaa414014014fe50..b2f3c29f429989af6e3629b879cb238dd5c3eccf 100644
--- a/tests/fixtures/automatic_updates-installed.php
+++ b/tests/fixtures/automatic_updates-installed.php
@@ -45,6 +45,11 @@ $extensions = $connection->select('config', 'c')
 $extensions = unserialize($extensions);
 $extensions['module']['automatic_updates'] = 0;
 $extensions['module']['package_manager'] = 0;
+// Install the mysql module manually because
+// system_post_update_enable_provider_database_driver() makes reliable update
+// path testing impossible.
+// @see https://www.drupal.org/project/automatic_updates/issues/3314137#comment-14772840
+$extensions['module']['mysql'] = 0;
 $connection->update('config')
   ->fields([
     'data' => serialize($extensions),
@@ -52,6 +57,26 @@ $connection->update('config')
   ->condition('name', 'core.extension')
   ->execute();
 
+// Add system_post_update_enable_provider_database_driver() as an existing
+// update.
+// @see https://www.drupal.org/project/automatic_updates/issues/3314137#comment-14772840
+$existing_updates = $connection->select('key_value')
+  ->fields('key_value', ['value'])
+  ->condition('collection', 'post_update')
+  ->condition('name', 'existing_updates')
+  ->execute()
+  ->fetchField();
+$existing_updates = unserialize($existing_updates);
+$existing_updates = array_merge(
+  $existing_updates,
+  ['system_post_update_enable_provider_database_driver'],
+);
+$connection->update('key_value')
+  ->fields(['value' => serialize($existing_updates)])
+  ->condition('collection', 'post_update')
+  ->condition('name', 'existing_updates')
+  ->execute();
+
 $connection->insert('key_value')
   ->fields(array(
     'collection',
diff --git a/tests/src/Functional/UpdatePathTest.php b/tests/src/Functional/UpdatePathTest.php
index 15430da29b23bdfcfd9146bc6dfbf79221797c0e..ef600d67032c6001ada85da0e3c36cc46991c79f 100644
--- a/tests/src/Functional/UpdatePathTest.php
+++ b/tests/src/Functional/UpdatePathTest.php
@@ -14,8 +14,14 @@ class UpdatePathTest extends UpdatePathTestBase {
    * {@inheritdoc}
    */
   protected function setDatabaseDumpFiles() {
+    // phpcs on 9.5 expects one thing, on 10.0 another. 🤷
+    // @see https://www.drupal.org/project/automatic_updates/issues/3314137#comment-14771510
+    // phpcs:disable
+    [$version] = explode('.', \Drupal::VERSION, 2);
     $this->databaseDumpFiles = [
-      $this->getDrupalRoot() . '/core/modules/system/tests/fixtures/update/drupal-9.3.0.filled.standard.php.gz',
+      $version == 9
+        ? $this->getDrupalRoot() . '/core/modules/system/tests/fixtures/update/drupal-9.3.0.filled.standard.php.gz'
+        : $this->getDrupalRoot() . '/core/modules/system/tests/fixtures/update/drupal-9.4.0.filled.standard.php.gz',
       __DIR__ . '/../../fixtures/automatic_updates-installed.php',
     ];
   }
@@ -32,11 +38,15 @@ class UpdatePathTest extends UpdatePathTestBase {
       'readiness_validation_last_run' => 'status_check_last_run',
       'readiness_check_timestamp' => 'status_check_timestamp',
     ];
+    $expected_values = [];
     foreach ($map as $old_key => $new_key) {
       $this->assertFalse($key_value->has($new_key));
 
       $value = $key_value->get($old_key);
       $this->assertNotEmpty($value);
+      // Allow testing that the values post-update are indeed the values
+      // pre-update and not recomputed ones.
+      $expected_values[$new_key] = $value;
       // Ensure the stored value will still be retrievable.
       $key_value->setWithExpire($old_key, $value, 3600);
     }
@@ -44,9 +54,14 @@ class UpdatePathTest extends UpdatePathTestBase {
 
     $this->runUpdates();
 
-    foreach ($map as $new_key) {
-      $this->assertNotEmpty($key_value->get($new_key));
-    }
+    // TRICKY: we do expect `readiness_validation_last_run` to have been renamed
+    // to `status_check_last_run`, but then
+    // automatic_updates_post_update_create_status_check_mail_config() should
+    // cause that to be erased.
+    // @see automatic_updates_post_update_create_status_check_mail_config()
+    // @see \Drupal\automatic_updates\EventSubscriber\ConfigSubscriber::onConfigSave()
+    unset($expected_values['status_check_last_run']);
+    $this->assertSame($expected_values, $key_value->getMultiple(array_values($map)));
     $this->assertSame(StatusCheckMailer::ERRORS_ONLY, $this->config('automatic_updates.settings')->get('status_check_mail'));
 
     // Ensure that the router was rebuilt and routes have the expected changes.
diff --git a/tests/src/Kernel/CronUpdaterTest.php b/tests/src/Kernel/CronUpdaterTest.php
index de849ce684fe601cb0c35989be368ff28a91d25b..d1c6d84c697622d76ff9319ea69e98c48295ef10 100644
--- a/tests/src/Kernel/CronUpdaterTest.php
+++ b/tests/src/Kernel/CronUpdaterTest.php
@@ -23,7 +23,8 @@ use Drupal\Tests\automatic_updates\Traits\EmailNotificationsTestTrait;
 use Drupal\Tests\package_manager\Traits\PackageManagerBypassTestTrait;
 use Drupal\Tests\user\Traits\UserCreationTrait;
 use Drupal\update\UpdateSettingsForm;
-use Psr\Log\Test\TestLogger;
+use ColinODell\PsrTestLogger\TestLogger;
+use Prophecy\Argument;
 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 
 /**
@@ -163,6 +164,7 @@ class CronUpdaterTest extends AutomaticUpdatesKernelTestBase {
     // are called as expected, disable validation by replacing the event
     // dispatcher with a dummy version.
     $event_dispatcher = $this->prophesize(EventDispatcherInterface::class);
+    $event_dispatcher->dispatch(Argument::type('object'))->willReturnArgument(0);
     $this->container->set('event_dispatcher', $event_dispatcher->reveal());
 
     // Run cron and ensure that Package Manager's services were called or
diff --git a/tests/src/Kernel/StatusCheck/CronServerValidatorTest.php b/tests/src/Kernel/StatusCheck/CronServerValidatorTest.php
index 0b4ecf939bbdd6bb3208b3b340f886fd3e0da580..5c39b0159655e324ba8299768ee7a6b5eadd8fc1 100644
--- a/tests/src/Kernel/StatusCheck/CronServerValidatorTest.php
+++ b/tests/src/Kernel/StatusCheck/CronServerValidatorTest.php
@@ -9,7 +9,7 @@ use Drupal\Core\Url;
 use Drupal\package_manager\Exception\StageValidationException;
 use Drupal\package_manager\ValidationResult;
 use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase;
-use Psr\Log\Test\TestLogger;
+use ColinODell\PsrTestLogger\TestLogger;
 
 /**
  * @covers \Drupal\automatic_updates\Validator\CronServerValidator
diff --git a/tests/src/Kernel/StatusCheck/StagedDatabaseUpdateValidatorTest.php b/tests/src/Kernel/StatusCheck/StagedDatabaseUpdateValidatorTest.php
index 29e7ae14d80655d7c90a01bdf5a41acc0e02adf3..8b5e709541e6f78f433b75c38ad6bc77b9a3a188 100644
--- a/tests/src/Kernel/StatusCheck/StagedDatabaseUpdateValidatorTest.php
+++ b/tests/src/Kernel/StatusCheck/StagedDatabaseUpdateValidatorTest.php
@@ -5,7 +5,7 @@ namespace Drupal\Tests\automatic_updates\Kernel\StatusCheck;
 use Drupal\Core\Logger\RfcLogLevel;
 use Drupal\package_manager\Event\PreApplyEvent;
 use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase;
-use Psr\Log\Test\TestLogger;
+use ColinODell\PsrTestLogger\TestLogger;
 
 /**
  * @covers \Drupal\automatic_updates\Validator\StagedDatabaseUpdateValidator
diff --git a/tests/src/Kernel/StatusCheck/XdebugValidatorTest.php b/tests/src/Kernel/StatusCheck/XdebugValidatorTest.php
index 14bdaffa7e1c04656c6b9d596e540403c6e37e81..7f23751d02f9d78b3a9ac6ee5c85bac80967d151 100644
--- a/tests/src/Kernel/StatusCheck/XdebugValidatorTest.php
+++ b/tests/src/Kernel/StatusCheck/XdebugValidatorTest.php
@@ -7,7 +7,7 @@ use Drupal\Core\Logger\RfcLogLevel;
 use Drupal\package_manager\ValidationResult;
 use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase;
 use Drupal\Tests\package_manager\Traits\PackageManagerBypassTestTrait;
-use Psr\Log\Test\TestLogger;
+use ColinODell\PsrTestLogger\TestLogger;
 
 /**
  * @covers \Drupal\automatic_updates\Validator\XdebugValidator