diff --git a/automatic_updates.services.yml b/automatic_updates.services.yml
index a592916bf7584623a500729171f0bbc74f56284b..dc587e01cec962cd812c435cfae147c31fd52f2b 100644
--- a/automatic_updates.services.yml
+++ b/automatic_updates.services.yml
@@ -59,11 +59,9 @@ services:
     tags:
       - { name: event_subscriber }
   automatic_updates.pending_updates_validator:
-    class: Drupal\automatic_updates\Validator\PendingUpdatesValidator
+    class: Drupal\automatic_updates\Validator\PackageManagerReadinessCheck
     arguments:
-      - '%app.root%'
-      - '@update.post_update_registry'
-      - '@string_translation'
+      - '@package_manager.validator.pending_updates'
     tags:
       - { name: event_subscriber }
   automatic_updates.validator.file_system_permissions:
diff --git a/package_manager/package_manager.services.yml b/package_manager/package_manager.services.yml
index fdcd9d96055a0de162de2cc5837211e8cabd6775..7ee8a28e629e5ee8793cf8bcc2c7f36c412caa11 100644
--- a/package_manager/package_manager.services.yml
+++ b/package_manager/package_manager.services.yml
@@ -100,3 +100,11 @@ services:
       - '@string_translation'
     tags:
       - { name: event_subscriber }
+  package_manager.validator.pending_updates:
+    class: Drupal\package_manager\EventSubscriber\PendingUpdatesValidator
+    arguments:
+      - '%app.root%'
+      - '@update.post_update_registry'
+      - '@string_translation'
+    tags:
+      - { name: event_subscriber }
diff --git a/src/Validator/PendingUpdatesValidator.php b/package_manager/src/EventSubscriber/PendingUpdatesValidator.php
similarity index 78%
rename from src/Validator/PendingUpdatesValidator.php
rename to package_manager/src/EventSubscriber/PendingUpdatesValidator.php
index f82c2793b0766288028a32e1e4e137b4c5160032..241a75d19169080aa2f9c7ff35aa2fac6522012a 100644
--- a/src/Validator/PendingUpdatesValidator.php
+++ b/package_manager/src/EventSubscriber/PendingUpdatesValidator.php
@@ -1,8 +1,7 @@
 <?php
 
-namespace Drupal\automatic_updates\Validator;
+namespace Drupal\package_manager\EventSubscriber;
 
-use Drupal\automatic_updates\Event\ReadinessCheckEvent;
 use Drupal\package_manager\Event\PreCreateEvent;
 use Drupal\package_manager\Event\StageEvent;
 use Drupal\package_manager\ValidationResult;
@@ -10,12 +9,11 @@ use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\StringTranslation\TranslationInterface;
 use Drupal\Core\Update\UpdateRegistry;
 use Drupal\Core\Url;
-use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 /**
  * Validates that there are no pending database updates.
  */
-class PendingUpdatesValidator implements EventSubscriberInterface {
+class PendingUpdatesValidator implements StageValidatorInterface {
 
   use StringTranslationTrait;
 
@@ -50,12 +48,9 @@ class PendingUpdatesValidator implements EventSubscriberInterface {
   }
 
   /**
-   * Validates that there are no pending database updates.
-   *
-   * @param \Drupal\package_manager\Event\StageEvent $event
-   *   The event object.
+   * {@inheritdoc}
    */
-  public function checkPendingUpdates(StageEvent $event): void {
+  public function validateStage(StageEvent $event): void {
     require_once $this->appRoot . '/core/includes/install.inc';
     require_once $this->appRoot . '/core/includes/update.inc';
 
@@ -77,8 +72,7 @@ class PendingUpdatesValidator implements EventSubscriberInterface {
    */
   public static function getSubscribedEvents() {
     return [
-      PreCreateEvent::class => 'checkPendingUpdates',
-      ReadinessCheckEvent::class => 'checkPendingUpdates',
+      PreCreateEvent::class => 'validateStage',
     ];
   }
 
diff --git a/package_manager/tests/fixtures/db_update.php b/package_manager/tests/fixtures/db_update.php
new file mode 100644
index 0000000000000000000000000000000000000000..7adacfb2e2e8d8bcc498735e7472f6fde2577340
--- /dev/null
+++ b/package_manager/tests/fixtures/db_update.php
@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * @file
+ * Contains a fake database update function for testing.
+ */
+
+/**
+ * Here is a fake update hook.
+ *
+ * The schema version is the maximum possible value for a 32-bit integer.
+ */
+function package_manager_update_2147483647() {
+}
diff --git a/package_manager/tests/src/Kernel/ComposerExecutableValidatorTest.php b/package_manager/tests/src/Kernel/ComposerExecutableValidatorTest.php
index 73379cbf9e03a9dde13e9ac528bccac072e430dc..8905f788eddac081411dcadc5088121e7658ff96 100644
--- a/package_manager/tests/src/Kernel/ComposerExecutableValidatorTest.php
+++ b/package_manager/tests/src/Kernel/ComposerExecutableValidatorTest.php
@@ -2,11 +2,9 @@
 
 namespace Drupal\Tests\package_manager\Kernel;
 
-use Drupal\KernelTests\KernelTestBase;
 use Drupal\package_manager\Event\PreCreateEvent;
 use Drupal\package_manager\EventSubscriber\ComposerExecutableValidator;
 use Drupal\package_manager\ValidationResult;
-use Drupal\Tests\package_manager\Traits\ValidationTestTrait;
 use PhpTuf\ComposerStager\Exception\IOException;
 use PhpTuf\ComposerStager\Infrastructure\Process\ExecutableFinderInterface;
 use Prophecy\Argument;
@@ -16,29 +14,7 @@ use Prophecy\Argument;
  *
  * @group package_manager
  */
-class ComposerExecutableValidatorTest extends KernelTestBase {
-
-  use ValidationTestTrait;
-
-  /**
-   * {@inheritdoc}
-   */
-  protected static $modules = ['package_manager'];
-
-  /**
-   * Runs the validator under test, and asserts its results match expectations.
-   *
-   * @param \Drupal\package_manager\ValidationResult[] $expected_results
-   *   The expected validation results.
-   */
-  private function assertResults(array $expected_results): void {
-    $stage = $this->prophesize('\Drupal\package_manager\Stage');
-    $event = new PreCreateEvent($stage->reveal());
-    $this->container->get('package_manager.validator.composer_executable')
-      ->validateStage($event);
-
-    $this->assertValidationResultsEqual($expected_results, $event->getResults());
-  }
+class ComposerExecutableValidatorTest extends PackageManagerKernelTestBase {
 
   /**
    * Tests that an error is raised if the Composer executable isn't found.
@@ -58,7 +34,7 @@ class ComposerExecutableValidatorTest extends KernelTestBase {
     $error = ValidationResult::createError([
       $exception->getMessage(),
     ]);
-    $this->assertResults([$error]);
+    $this->assertResults([$error], PreCreateEvent::class);
   }
 
   /**
@@ -155,7 +131,7 @@ class ComposerExecutableValidatorTest extends KernelTestBase {
 
     // If the validator can't find a recognized, supported version of Composer,
     // it should produce errors.
-    $this->assertResults($expected_results);
+    $this->assertResults($expected_results, PreCreateEvent::class);
   }
 
 }
diff --git a/package_manager/tests/src/Kernel/DiskSpaceValidatorTest.php b/package_manager/tests/src/Kernel/DiskSpaceValidatorTest.php
index 9e33cdc73adaf9fd584571b4c18396674f7a75e6..75def576670e91cfdc36ac0fe57455077d5b56bb 100644
--- a/package_manager/tests/src/Kernel/DiskSpaceValidatorTest.php
+++ b/package_manager/tests/src/Kernel/DiskSpaceValidatorTest.php
@@ -2,26 +2,17 @@
 
 namespace Drupal\Tests\package_manager\Kernel;
 
-use Drupal\KernelTests\KernelTestBase;
 use Drupal\package_manager\Event\PreCreateEvent;
 use Drupal\package_manager\EventSubscriber\DiskSpaceValidator;
 use Drupal\package_manager\ValidationResult;
 use Drupal\Component\Utility\Bytes;
-use Drupal\Tests\package_manager\Traits\ValidationTestTrait;
 
 /**
  * @covers \Drupal\package_manager\EventSubscriber\DiskSpaceValidator
  *
  * @group package_manager
  */
-class DiskSpaceValidatorTest extends KernelTestBase {
-
-  use ValidationTestTrait;
-
-  /**
-   * {@inheritdoc}
-   */
-  protected static $modules = ['package_manager'];
+class DiskSpaceValidatorTest extends PackageManagerKernelTestBase {
 
   /**
    * Data provider for ::testDiskSpaceValidation().
@@ -201,11 +192,7 @@ class DiskSpaceValidatorTest extends KernelTestBase {
     $validator->sharedDisk = $shared_disk;
     $validator->freeSpace = array_map([Bytes::class, 'toNumber'], $free_space);
 
-    $stage = $this->prophesize('\Drupal\package_manager\Stage');
-    $event = new PreCreateEvent($stage->reveal());
-    $validator->validateStage($event);
-
-    $this->assertValidationResultsEqual($expected_results, $event->getResults());
+    $this->assertResults($expected_results, PreCreateEvent::class);
   }
 
 }
diff --git a/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php b/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..012914fecba57475211eda8139dc9b676e24cf31
--- /dev/null
+++ b/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace Drupal\Tests\package_manager\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\package_manager\Event\StageEvent;
+use Drupal\package_manager\Stage;
+use Drupal\package_manager\StageException;
+use Drupal\Tests\package_manager\Traits\ValidationTestTrait;
+
+/**
+ * Base class for kernel tests of Package Manager's functionality.
+ */
+abstract class PackageManagerKernelTestBase extends KernelTestBase {
+
+  use ValidationTestTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'package_manager',
+    'package_manager_bypass',
+  ];
+
+  /**
+   * Asserts validation results are returned from a stage life cycle event.
+   *
+   * @param \Drupal\package_manager\ValidationResult[] $expected_results
+   *   The expected validation results.
+   * @param string $event_class
+   *   The class of the event which should return the results.
+   */
+  protected function assertResults(array $expected_results, string $event_class): void {
+    $stage = new TestStage(
+      $this->container->get('package_manager.path_locator'),
+      $this->container->get('package_manager.beginner'),
+      $this->container->get('package_manager.stager'),
+      $this->container->get('package_manager.committer'),
+      $this->container->get('package_manager.cleaner'),
+      $this->container->get('event_dispatcher'),
+    );
+    try {
+      $stage->create();
+      $stage->require(['drupal/core:9.8.1']);
+      $stage->apply();
+      $stage->destroy();
+    }
+    catch (StageException $e) {
+      $this->assertValidationResultsEqual($expected_results, $e->getResults());
+      // TestStage::dispatch() attaches the event object to the exception so
+      // that we can analyze it.
+      $this->assertInstanceOf($event_class, $e->event);
+    }
+    // If no errors are raised, we won't have asserted anything and the test
+    // will be marked as risky. To prevent that, assert an eternal truth.
+    $this->assertTrue(TRUE);
+  }
+
+}
+
+/**
+ * Defines a stage specifically for testing purposes.
+ */
+class TestStage extends Stage {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function dispatch(StageEvent $event): void {
+    try {
+      parent::dispatch($event);
+    }
+    catch (StageException $e) {
+      // Attach the event object to the exception so that test code can verify
+      // that the exception was thrown when a specific event was dispatched.
+      $e->event = $event;
+      throw $e;
+    }
+  }
+
+}
diff --git a/package_manager/tests/src/Kernel/PendingUpdatesValidatorTest.php b/package_manager/tests/src/Kernel/PendingUpdatesValidatorTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..a1a26042637c3f7730493ee2e4937e3721804172
--- /dev/null
+++ b/package_manager/tests/src/Kernel/PendingUpdatesValidatorTest.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace Drupal\Tests\package_manager\Kernel;
+
+use Drupal\package_manager\Event\PreCreateEvent;
+use Drupal\package_manager\ValidationResult;
+
+/**
+ * @covers \Drupal\package_manager\EventSubscriber\PendingUpdatesValidator
+ *
+ * @group package_manager
+ */
+class PendingUpdatesValidatorTest extends PackageManagerKernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['system'];
+
+  /**
+   * Registers all of the System module's post-update functions.
+   *
+   * Since kernel tests don't normally install modules and register their
+   * updates, this method makes sure that the validator is tested from a clean,
+   * fully up-to-date state.
+   */
+  private function registerPostUpdateFunctions(): void {
+    $updates = $this->container->get('update.post_update_registry')
+      ->getPendingUpdateFunctions();
+
+    $this->container->get('keyvalue')
+      ->get('post_update')
+      ->set('existing_updates', $updates);
+  }
+
+  /**
+   * Tests that no error is raised if there are no pending updates.
+   */
+  public function testNoPendingUpdates(): void {
+    $this->registerPostUpdateFunctions();
+    $this->assertResults([], PreCreateEvent::class);
+  }
+
+  /**
+   * Tests that an error is raised if there are pending schema updates.
+   *
+   * @depends testNoPendingUpdates
+   */
+  public function testPendingUpdateHook(): void {
+    // Register the System module's post-update functions, so that any detected
+    // pending updates are guaranteed to be schema updates.
+    $this->registerPostUpdateFunctions();
+
+    // Set the installed schema version of Package Manager to its default value
+    // and import an empty update hook which is numbered much higher than will
+    // ever exist in the real world.
+    $this->container->get('keyvalue')
+      ->get('system.schema')
+      ->set('package_manager', \Drupal::CORE_MINIMUM_SCHEMA_VERSION);
+
+    require_once __DIR__ . '/../../fixtures/db_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.',
+    ]);
+    $this->assertResults([$result], PreCreateEvent::class);
+  }
+
+  /**
+   * Tests that an error is raised if there are pending post-updates.
+   */
+  public function testPendingPostUpdate(): void {
+    // The System module's post-update functions have not been registered, so
+    // the update registry will think they're pending.
+    $result = ValidationResult::createError([
+      'Some modules have database schema updates to install. You should run the <a href="/update.php">database update script</a> immediately.',
+    ]);
+    $this->assertResults([$result], PreCreateEvent::class);
+  }
+
+}
diff --git a/tests/fixtures/db_update.php b/tests/fixtures/db_update.php
deleted file mode 100644
index dcebeff41012ad40820b3e17880b40c74f31eb3c..0000000000000000000000000000000000000000
--- a/tests/fixtures/db_update.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains a fake database update function for testing.
- */
-
-/**
- * Here is a fake update.
- */
-function automatic_updates_update_50000() {
-}
diff --git a/tests/fixtures/post_update.php b/tests/fixtures/post_update.php
deleted file mode 100644
index a77909b6d4db212fd19041e4a379d68dcfae4b5c..0000000000000000000000000000000000000000
--- a/tests/fixtures/post_update.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains a fake post-update function for testing.
- */
-
-/**
- * Here is a fake post-update.
- */
-function automatic_updates_post_update_test() {
-}
diff --git a/tests/src/Kernel/ReadinessValidation/PackageManagerReadinessChecksTest.php b/tests/src/Kernel/ReadinessValidation/PackageManagerReadinessChecksTest.php
index 003e4ea86b6419a61fe993a4ca6ed5db43774da2..94a8060c9856c86fbbab3aca02f4d77da13f6bd7 100644
--- a/tests/src/Kernel/ReadinessValidation/PackageManagerReadinessChecksTest.php
+++ b/tests/src/Kernel/ReadinessValidation/PackageManagerReadinessChecksTest.php
@@ -16,6 +16,7 @@ use Prophecy\Argument;
  *
  * @see \Drupal\Tests\package_manager\Kernel\ComposerExecutableValidatorTest
  * @see \Drupal\Tests\package_manager\Kernel\DiskSpaceValidatorTest
+ * @see \Drupal\Tests\package_manager\Kernel\PendingUpdatesValidatorTest
  */
 class PackageManagerReadinessChecksTest extends AutomaticUpdatesKernelTestBase {
 
@@ -37,6 +38,7 @@ class PackageManagerReadinessChecksTest extends AutomaticUpdatesKernelTestBase {
     return [
       ['package_manager.validator.composer_executable'],
       ['package_manager.validator.disk_space'],
+      ['package_manager.validator.pending_updates'],
     ];
   }
 
diff --git a/tests/src/Kernel/ReadinessValidation/PendingUpdatesValidatorTest.php b/tests/src/Kernel/ReadinessValidation/PendingUpdatesValidatorTest.php
deleted file mode 100644
index c0ddd3b224f5b7fbf00dea0a4b39e9d02e081178..0000000000000000000000000000000000000000
--- a/tests/src/Kernel/ReadinessValidation/PendingUpdatesValidatorTest.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-
-namespace Drupal\Tests\automatic_updates\Kernel\ReadinessValidation;
-
-use Drupal\package_manager\ValidationResult;
-use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase;
-
-/**
- * @covers \Drupal\automatic_updates\Validator\PendingUpdatesValidator
- *
- * @group automatic_updates
- */
-class PendingUpdatesValidatorTest extends AutomaticUpdatesKernelTestBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  protected static $modules = [
-    'automatic_updates',
-    'package_manager',
-  ];
-
-  /**
-   * Tests that no error is raised if there are no pending updates.
-   */
-  public function testNoPendingUpdates(): void {
-    $this->assertCheckerResultsFromManager([], TRUE);
-  }
-
-  /**
-   * Tests that an error is raised if there are pending schema updates.
-   */
-  public function testPendingUpdateHook(): void {
-    require __DIR__ . '/../../../fixtures/db_update.php';
-
-    $this->container->get('keyvalue')
-      ->get('system.schema')
-      ->set('automatic_updates', \Drupal::CORE_MINIMUM_SCHEMA_VERSION);
-
-    $result = ValidationResult::createError(['Some modules have database schema updates to install. You should run the <a href="/update.php">database update script</a> immediately.']);
-    $this->assertCheckerResultsFromManager([$result], TRUE);
-  }
-
-  /**
-   * Tests that an error is raised if there are pending post-updates.
-   */
-  public function testPendingPostUpdate(): void {
-    require __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.']);
-    $this->assertCheckerResultsFromManager([$result], TRUE);
-  }
-
-}