From fecea9b3d1ad8df6a2d92e1ab7b1f4cc37af811d Mon Sep 17 00:00:00 2001
From: omkar podey <58183-omkar.podey@users.noreply.drupalcode.org>
Date: Thu, 2 Mar 2023 14:44:34 +0000
Subject: [PATCH] Issue #3331355 by omkar.podey, tedbow, phenaproxima, Wim
 Leers, yash.rode: Refactor exception architecture

---
 .../src/BatchProcessor.php                    |  19 +---
 .../src/ExtensionUpdater.php                  |  28 -----
 .../src/Form/UpdateReady.php                  |   4 +-
 .../src/Form/UpdaterForm.php                  |   4 +-
 .../tests/src/Functional/UpdateErrorTest.php  |   4 +-
 ...tomaticUpdatesExtensionsKernelTestBase.php |  69 ++----------
 .../tests/src/Kernel/ExtensionUpdaterTest.php |  54 ---------
 package_manager/package_manager.api.php       |   2 +-
 package_manager/package_manager.install       |   6 +-
 .../src/Exception/ApplyFailedException.php    |   4 +
 .../src/Exception/StageEventException.php     |  56 ++++++++++
 .../src/Exception/StageException.php          |  15 +++
 .../Exception/StageFailureMarkerException.php |  22 ++++
 .../Exception/StageValidationException.php    |  67 -----------
 package_manager/src/FailureMarker.php         |   6 +-
 package_manager/src/Stage.php                 |  33 +++---
 .../ComposerMinimumStabilityValidatorTest.php |   6 +-
 .../Kernel/ComposerPatchesValidatorTest.php   |   7 +-
 .../Kernel/DuplicateInfoFileValidatorTest.php |   6 +-
 .../tests/src/Kernel/FailureMarkerTest.php    |   6 +-
 .../Kernel/PackageManagerKernelTestBase.php   | 104 ++++++------------
 .../Kernel/PendingUpdatesValidatorTest.php    |   6 +-
 .../tests/src/Kernel/StageEventsTest.php      |   3 +-
 .../tests/src/Kernel/StageTest.php            |  11 +-
 .../Kernel/StageValidationExceptionTest.php   |  84 --------------
 src/BatchProcessor.php                        |  20 +---
 src/CronUpdater.php                           |   6 +-
 src/Exception/UpdateException.php             |  19 ----
 src/Form/UpdateReady.php                      |   4 +-
 src/Form/UpdaterForm.php                      |   4 +-
 src/Updater.php                               |  28 -----
 tests/src/Functional/UpdateFailedTest.php     |   4 +-
 .../Kernel/AutomaticUpdatesKernelTestBase.php |   5 -
 tests/src/Kernel/CronUpdaterTest.php          |  77 ++++++-------
 .../StatusCheck/CronServerValidatorTest.php   |   5 +-
 .../RequestedUpdateValidatorTest.php          |   8 +-
 .../ScaffoldFilePermissionsValidatorTest.php  |  16 ++-
 .../StagedProjectsValidatorTest.php           |  18 +--
 .../VersionPolicyValidatorTest.php            |   6 +-
 tests/src/Kernel/UpdaterTest.php              |  63 +----------
 40 files changed, 273 insertions(+), 636 deletions(-)
 create mode 100644 package_manager/src/Exception/StageEventException.php
 create mode 100644 package_manager/src/Exception/StageFailureMarkerException.php
 delete mode 100644 package_manager/src/Exception/StageValidationException.php
 delete mode 100644 package_manager/tests/src/Kernel/StageValidationExceptionTest.php
 delete mode 100644 src/Exception/UpdateException.php

diff --git a/automatic_updates_extensions/src/BatchProcessor.php b/automatic_updates_extensions/src/BatchProcessor.php
index bf2f48a37a..9f1742301a 100644
--- a/automatic_updates_extensions/src/BatchProcessor.php
+++ b/automatic_updates_extensions/src/BatchProcessor.php
@@ -5,7 +5,6 @@ declare(strict_types = 1);
 namespace Drupal\automatic_updates_extensions;
 
 use Drupal\Core\Url;
-use Drupal\package_manager\Exception\StageValidationException;
 use Symfony\Component\HttpFoundation\RedirectResponse;
 
 /**
@@ -47,23 +46,7 @@ final class BatchProcessor {
    *   have been recorded.
    */
   protected static function handleException(\Throwable $error, array &$context): void {
-    $error_messages = [
-      $error->getMessage(),
-    ];
-
-    if ($error instanceof StageValidationException) {
-      foreach ($error->getResults() as $result) {
-        $messages = $result->getMessages();
-        if (count($messages) > 1) {
-          array_unshift($messages, $result->getSummary());
-        }
-        $error_messages = array_merge($error_messages, $messages);
-      }
-    }
-
-    foreach ($error_messages as $error_message) {
-      $context['results']['errors'][] = $error_message;
-    }
+    $context['results']['errors'][] = $error->getMessage();
     throw $error;
   }
 
diff --git a/automatic_updates_extensions/src/ExtensionUpdater.php b/automatic_updates_extensions/src/ExtensionUpdater.php
index 99031f8b30..7cae2d5fcd 100644
--- a/automatic_updates_extensions/src/ExtensionUpdater.php
+++ b/automatic_updates_extensions/src/ExtensionUpdater.php
@@ -4,12 +4,8 @@ declare(strict_types = 1);
 
 namespace Drupal\automatic_updates_extensions;
 
-use Drupal\automatic_updates\Exception\UpdateException;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
-use Drupal\package_manager\Exception\ApplyFailedException;
 use Drupal\package_manager\LegacyVersionUtility;
-use Drupal\package_manager\Event\StageEvent;
-use Drupal\package_manager\Exception\StageValidationException;
 use Drupal\package_manager\Stage;
 
 /**
@@ -102,30 +98,6 @@ class ExtensionUpdater extends Stage {
     $this->require($versions['production'], $versions['dev']);
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  protected function dispatch(StageEvent $event, callable $on_error = NULL): void {
-    try {
-      parent::dispatch($event, $on_error);
-    }
-    catch (StageValidationException $e) {
-      throw new UpdateException($e->getResults(), $e->getMessage(), $e->getCode(), $e);
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function apply(?int $timeout = 600): void {
-    try {
-      parent::apply($timeout);
-    }
-    catch (ApplyFailedException $exception) {
-      throw new UpdateException([], 'The update operation failed to apply. The update may have been partially applied. It is recommended that the site be restored from a code backup.', $exception->getCode(), $exception);
-    }
-  }
-
   /**
    * {@inheritdoc}
    */
diff --git a/automatic_updates_extensions/src/Form/UpdateReady.php b/automatic_updates_extensions/src/Form/UpdateReady.php
index 587514e45a..4503724ab0 100644
--- a/automatic_updates_extensions/src/Form/UpdateReady.php
+++ b/automatic_updates_extensions/src/Form/UpdateReady.php
@@ -5,7 +5,7 @@ declare(strict_types = 1);
 namespace Drupal\automatic_updates_extensions\Form;
 
 use Drupal\automatic_updates\Form\UpdateFormBase;
-use Drupal\package_manager\Exception\ApplyFailedException;
+use Drupal\package_manager\Exception\StageFailureMarkerException;
 use Drupal\package_manager\ProjectInfo;
 use Drupal\package_manager\ValidationResult;
 use Drupal\automatic_updates_extensions\BatchProcessor;
@@ -124,7 +124,7 @@ final class UpdateReady extends UpdateFormBase {
       $this->messenger()->addError($this->t('Cannot continue the update because another Composer operation is currently in progress.'));
       return $form;
     }
-    catch (ApplyFailedException $e) {
+    catch (StageFailureMarkerException $e) {
       $this->messenger()->addError($e->getMessage());
       return $form;
     }
diff --git a/automatic_updates_extensions/src/Form/UpdaterForm.php b/automatic_updates_extensions/src/Form/UpdaterForm.php
index dec4b3fc60..d738728900 100644
--- a/automatic_updates_extensions/src/Form/UpdaterForm.php
+++ b/automatic_updates_extensions/src/Form/UpdaterForm.php
@@ -13,7 +13,7 @@ use Drupal\Core\State\StateInterface;
 use Drupal\Core\Messenger\MessengerInterface;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Url;
-use Drupal\package_manager\Exception\ApplyFailedException;
+use Drupal\package_manager\Exception\StageFailureMarkerException;
 use Drupal\package_manager\FailureMarker;
 use Drupal\package_manager\ProjectInfo;
 use Drupal\package_manager\ValidationResult;
@@ -114,7 +114,7 @@ final class UpdaterForm extends UpdateFormBase {
     try {
       $this->failureMarker->assertNotExists();
     }
-    catch (ApplyFailedException $e) {
+    catch (StageFailureMarkerException $e) {
       $this->messenger()->addError($e->getMessage());
       return $form;
     }
diff --git a/automatic_updates_extensions/tests/src/Functional/UpdateErrorTest.php b/automatic_updates_extensions/tests/src/Functional/UpdateErrorTest.php
index f72cd4a6ef..860f5e0a84 100644
--- a/automatic_updates_extensions/tests/src/Functional/UpdateErrorTest.php
+++ b/automatic_updates_extensions/tests/src/Functional/UpdateErrorTest.php
@@ -41,11 +41,11 @@ class UpdateErrorTest extends UpdaterFormTestBase {
     LoggingCommitter::setException(new \Exception('failed at committer'));
     $page->pressButton('Continue');
     $this->checkForMetaRefresh();
+    $failure_message = 'Automatic updates failed to apply, and the site is in an indeterminate state. Consider restoring the code and database from a backup.';
     $assert_session->pageTextContainsOnce('An error has occurred.');
-    $assert_session->pageTextContains('The update operation failed to apply. The update may have been partially applied. It is recommended that the site be restored from a code backup.');
+    $assert_session->pageTextContains($failure_message);
     $page->clickLink('the error page');
 
-    $failure_message = 'Automatic updates failed to apply, and the site is in an indeterminate state. Consider restoring the code and database from a backup.';
     // We should be on the form (i.e., 200 response code), but unable to
     // continue the update.
     $assert_session->statusCodeEquals(200);
diff --git a/automatic_updates_extensions/tests/src/Kernel/AutomaticUpdatesExtensionsKernelTestBase.php b/automatic_updates_extensions/tests/src/Kernel/AutomaticUpdatesExtensionsKernelTestBase.php
index 0e44926fc1..24ee03b575 100644
--- a/automatic_updates_extensions/tests/src/Kernel/AutomaticUpdatesExtensionsKernelTestBase.php
+++ b/automatic_updates_extensions/tests/src/Kernel/AutomaticUpdatesExtensionsKernelTestBase.php
@@ -4,13 +4,9 @@ declare(strict_types = 1);
 
 namespace Drupal\Tests\automatic_updates_extensions\Kernel;
 
-use Drupal\automatic_updates\Exception\UpdateException;
-use Drupal\automatic_updates_extensions\ExtensionUpdater;
-use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\package_manager\Event\PreOperationStageEvent;
+use Drupal\package_manager\Exception\StageEventException;
 use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase;
-use Drupal\Tests\package_manager\Kernel\TestStageTrait;
-use Drupal\Tests\package_manager\Kernel\TestStageValidationException;
-use PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactory;
 
 /**
  * Base class for kernel tests of the Automatic Updates Extensions module.
@@ -51,23 +47,6 @@ abstract class AutomaticUpdatesExtensionsKernelTestBase extends AutomaticUpdates
     parent::createTestProject($source_dir);
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function register(ContainerBuilder $container) {
-    parent::register($container);
-
-    // Use the test-only implementations of the regular and cron updaters.
-    $overrides = [
-      'automatic_updates_extensions.updater' => TestExtensionUpdater::class,
-    ];
-    foreach ($overrides as $service_id => $class) {
-      if ($container->hasDefinition($service_id)) {
-        $container->getDefinition($service_id)->setClass($class);
-      }
-    }
-  }
-
   /**
    * Asserts validation results are returned from a stage life cycle event.
    *
@@ -80,7 +59,7 @@ abstract class AutomaticUpdatesExtensionsKernelTestBase extends AutomaticUpdates
    *   be passed if $expected_results is not empty.
    */
   protected function assertUpdateResults(array $project_versions, array $expected_results, string $event_class = NULL): void {
-    $updater = $this->createExtensionUpdater();
+    $updater = $this->container->get('automatic_updates_extensions.updater');
 
     try {
       $updater->begin($project_versions);
@@ -92,45 +71,13 @@ abstract class AutomaticUpdatesExtensionsKernelTestBase extends AutomaticUpdates
       // If we did not get an exception, ensure we didn't expect any results.
       $this->assertEmpty($expected_results);
     }
-    catch (TestStageValidationException $e) {
+    catch (StageEventException $e) {
       $this->assertNotEmpty($expected_results);
-      $this->assertValidationResultsEqual($expected_results, $e->getResults());
-      // TestStage::dispatch() throws TestUpdateException with event object
-      // so that we can analyze it.
-      $this->assertInstanceOf(UpdateException::class, $e->getOriginalException());
-      $this->assertNotEmpty($event_class);
-      $this->assertInstanceOf($event_class, $e->getEvent());
+      $exception_event = $e->event;
+      $this->assertInstanceOf($event_class, $exception_event);
+      $this->assertInstanceOf(PreOperationStageEvent::class, $exception_event);
+      $this->assertValidationResultsEqual($expected_results, $e->event->getResults());
     }
   }
 
-  /**
-   * Creates an extension updater object for testing purposes.
-   *
-   * @return \Drupal\Tests\automatic_updates_extensions\Kernel\TestExtensionUpdater
-   *   A extension updater object, with test-only modifications.
-   */
-  protected function createExtensionUpdater(): TestExtensionUpdater {
-    return new TestExtensionUpdater(
-      $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('file_system'),
-      $this->container->get('event_dispatcher'),
-      $this->container->get('tempstore.shared'),
-      $this->container->get('datetime.time'),
-      new PathFactory(),
-      $this->container->get('package_manager.failure_marker')
-    );
-  }
-
-}
-
-/**
- * A test-only version of the regular extension updater to override internals.
- */
-class TestExtensionUpdater extends ExtensionUpdater {
-
-  use TestStageTrait;
-
 }
diff --git a/automatic_updates_extensions/tests/src/Kernel/ExtensionUpdaterTest.php b/automatic_updates_extensions/tests/src/Kernel/ExtensionUpdaterTest.php
index dd2da2f165..69d15693e9 100644
--- a/automatic_updates_extensions/tests/src/Kernel/ExtensionUpdaterTest.php
+++ b/automatic_updates_extensions/tests/src/Kernel/ExtensionUpdaterTest.php
@@ -4,13 +4,6 @@ declare(strict_types = 1);
 
 namespace Drupal\Tests\automatic_updates_extensions\Kernel;
 
-use Drupal\automatic_updates\Exception\UpdateException;
-use Drupal\automatic_updates_test\EventSubscriber\TestSubscriber1;
-use Drupal\package_manager\Event\PreApplyEvent;
-use Drupal\package_manager\Event\PreCreateEvent;
-use Drupal\package_manager\Event\PreRequireEvent;
-use Drupal\package_manager\ValidationResult;
-use Drupal\Tests\package_manager\Kernel\TestStageValidationException;
 use Drupal\Tests\user\Traits\UserCreationTrait;
 
 /**
@@ -152,51 +145,4 @@ class ExtensionUpdaterTest extends AutomaticUpdatesExtensionsKernelTestBase {
     ]);
   }
 
-  /**
-   * Tests UpdateException handling.
-   *
-   * @param string $event_class
-   *   The stage life cycle event which should raise an error.
-   *
-   * @dataProvider providerUpdateException
-   */
-  public function testUpdateException(string $event_class): void {
-    $extension_updater = $this->container->get('automatic_updates_extensions.updater');
-    $results = [
-      ValidationResult::createError([t('An error of some sorts.')]),
-    ];
-    TestSubscriber1::setTestResult($results, $event_class);
-    try {
-      $extension_updater->begin(['my_module' => '9.8.1']);
-      $extension_updater->stage();
-      $extension_updater->apply();
-      $this->fail('Expected an exception, but none was raised.');
-    }
-    catch (TestStageValidationException $e) {
-      $this->assertStringStartsWith('An error of some sorts.', $e->getMessage());
-      $this->assertInstanceOf(UpdateException::class, $e->getOriginalException());
-      $this->assertInstanceOf($event_class, $e->getEvent());
-    }
-  }
-
-  /**
-   * Data provider for testUpdateException().
-   *
-   * @return string[][]
-   *   The test cases.
-   */
-  public function providerUpdateException(): array {
-    return [
-      'pre-create exception' => [
-        PreCreateEvent::class,
-      ],
-      'pre-require exception' => [
-        PreRequireEvent::class,
-      ],
-      'pre-apply exception' => [
-        PreApplyEvent::class,
-      ],
-    ];
-  }
-
 }
diff --git a/package_manager/package_manager.api.php b/package_manager/package_manager.api.php
index 4e58300c4c..7da700f4d8 100644
--- a/package_manager/package_manager.api.php
+++ b/package_manager/package_manager.api.php
@@ -146,7 +146,7 @@
  * If problems occur during any point of the stage life cycle, a
  * \Drupal\package_manager\Exception\StageException is thrown. If problems were
  * detected during one of the "pre" operations, a subclass of that is thrown:
- * \Drupal\package_manager\Exception\StageValidationException. This will contain
+ * \Drupal\package_manager\Exception\StageEventException. This will contain
  * \Drupal\package_manager\ValidationResult objects.
  * The Package Manager module does not catch or handle these exceptions: they
  * provide the foundation for other modules to build user experiences for
diff --git a/package_manager/package_manager.install b/package_manager/package_manager.install
index fa864d70da..aab5d430d7 100644
--- a/package_manager/package_manager.install
+++ b/package_manager/package_manager.install
@@ -7,7 +7,7 @@
 
 declare(strict_types = 1);
 
-use Drupal\package_manager\Exception\ApplyFailedException;
+use Drupal\package_manager\Exception\StageFailureMarkerException;
 
 /**
  * Implements hook_requirements().
@@ -29,9 +29,9 @@ function package_manager_requirements(string $phase) {
   $service_id = 'package_manager.failure_marker';
   if (\Drupal::hasService($service_id)) {
     try {
-      \Drupal::service($service_id)->assertNotExists();
+      \Drupal::service($service_id)->assertNotExists(NULL);
     }
-    catch (ApplyFailedException $exception) {
+    catch (StageFailureMarkerException $exception) {
       $requirements['package_manager_failure_marker'] = [
         'title' => t('Failed update detected'),
         'description' => $exception->getMessage(),
diff --git a/package_manager/src/Exception/ApplyFailedException.php b/package_manager/src/Exception/ApplyFailedException.php
index 8445e8dcf8..7cf59d149a 100644
--- a/package_manager/src/Exception/ApplyFailedException.php
+++ b/package_manager/src/Exception/ApplyFailedException.php
@@ -12,6 +12,10 @@ namespace Drupal\package_manager\Exception;
  * indeterminate state. Package Manager does not provide a method for recovering
  * from this state. The site code should be restored from a backup.
  *
+ * This exception is different from StageFailureMarkerException in that it is
+ * thrown if an error happens *during* the apply operation, rather than before
+ * or after it.
+ *
  * Should not be thrown by external code.
  */
 final class ApplyFailedException extends StageException {
diff --git a/package_manager/src/Exception/StageEventException.php b/package_manager/src/Exception/StageEventException.php
new file mode 100644
index 0000000000..971faf1e26
--- /dev/null
+++ b/package_manager/src/Exception/StageEventException.php
@@ -0,0 +1,56 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\package_manager\Exception;
+
+use Drupal\package_manager\Event\PreOperationStageEvent;
+use Drupal\package_manager\Event\StageEvent;
+
+/**
+ * Exception thrown if an error related to an event occurs.
+ *
+ * This exception is thrown when an error strictly associated with an event
+ * occurs. This is also what makes it different from StageException.
+ *
+ * Should not be thrown by external code.
+ */
+class StageEventException extends StageException {
+
+  /**
+   * Constructs a StageEventException object.
+   *
+   * @param \Drupal\package_manager\Event\StageEvent $event
+   *   The stage event during which this exception is thrown.
+   * @param string|null $message
+   *   (optional) The exception message. Defaults to a plain text representation
+   *   of the validation results.
+   * @param mixed ...$arguments
+   *   Additional arguments to pass to the parent constructor.
+   */
+  public function __construct(public readonly StageEvent $event, ?string $message = NULL, ...$arguments) {
+    parent::__construct($event->stage, $message ?: $this->getResultsAsText(), ...$arguments);
+  }
+
+  /**
+   * Formats the validation results, if any, as plain text.
+   *
+   * @return string
+   *   The results, formatted as plain text.
+   */
+  protected function getResultsAsText(): string {
+    $text = '';
+    if ($this->event instanceof PreOperationStageEvent) {
+      foreach ($this->event->getResults() as $result) {
+        $messages = $result->getMessages();
+        $summary = $result->getSummary();
+        if ($summary) {
+          array_unshift($messages, $summary);
+        }
+        $text .= implode("\n", $messages) . "\n";
+      }
+    }
+    return $text;
+  }
+
+}
diff --git a/package_manager/src/Exception/StageException.php b/package_manager/src/Exception/StageException.php
index c9b1632a3e..2d2bc8a1ba 100644
--- a/package_manager/src/Exception/StageException.php
+++ b/package_manager/src/Exception/StageException.php
@@ -4,10 +4,25 @@ declare(strict_types = 1);
 
 namespace Drupal\package_manager\Exception;
 
+use Drupal\package_manager\Stage;
+
 /**
  * Base class for all exceptions related to stage operations.
  *
  * Should not be thrown by external code.
  */
 class StageException extends \RuntimeException {
+
+  /**
+   * Constructs a StageException object.
+   *
+   * @param \Drupal\package_manager\Stage $stage
+   *   The stage.
+   * @param mixed ...$arguments
+   *   Additional arguments to pass to the parent constructor.
+   */
+  public function __construct(public readonly Stage $stage, ...$arguments) {
+    parent::__construct(...$arguments);
+  }
+
 }
diff --git a/package_manager/src/Exception/StageFailureMarkerException.php b/package_manager/src/Exception/StageFailureMarkerException.php
new file mode 100644
index 0000000000..ede028c14e
--- /dev/null
+++ b/package_manager/src/Exception/StageFailureMarkerException.php
@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\package_manager\Exception;
+
+/**
+ * Exception thrown if a stage can't be created due to an earlier failed commit.
+ *
+ * If this exception is thrown it indicates that an earlier commit operation had
+ * failed. If this happens the site code is in an indeterminate state. Package
+ * Manager does not provide a method for recovering from this state. The site
+ * code should be restored from a backup.
+ *
+ * We are extending RuntimeException rather than StageException which makes it
+ * clearer that it's unrelated to the stage life cycle.
+ *
+ * This exception is different from ApplyFailedException as it focuses on
+ * the failure marker being detected outside the stage lifecycle.
+ */
+final class StageFailureMarkerException extends \RuntimeException {
+}
diff --git a/package_manager/src/Exception/StageValidationException.php b/package_manager/src/Exception/StageValidationException.php
deleted file mode 100644
index 7286d8adec..0000000000
--- a/package_manager/src/Exception/StageValidationException.php
+++ /dev/null
@@ -1,67 +0,0 @@
-<?php
-
-declare(strict_types = 1);
-
-namespace Drupal\package_manager\Exception;
-
-/**
- * Exception thrown if a stage has validation errors.
- *
- * Should not be thrown by external code.
- */
-class StageValidationException extends StageException {
-
-  /**
-   * Any relevant validation results.
-   *
-   * @var \Drupal\package_manager\ValidationResult[]
-   */
-  protected $results = [];
-
-  /**
-   * Constructs a StageException object.
-   *
-   * @param \Drupal\package_manager\ValidationResult[] $results
-   *   Any relevant validation results.
-   * @param string $message
-   *   (optional) The exception message. Defaults to a plain text representation
-   *   of the validation results.
-   * @param mixed ...$arguments
-   *   Additional arguments to pass to the parent constructor.
-   */
-  public function __construct(array $results = [], string $message = '', ...$arguments) {
-    $this->results = $results;
-    parent::__construct($message ?: $this->getResultsAsText(), ...$arguments);
-  }
-
-  /**
-   * Gets the validation results.
-   *
-   * @return \Drupal\package_manager\ValidationResult[]
-   *   The validation results.
-   */
-  public function getResults(): array {
-    return $this->results;
-  }
-
-  /**
-   * Formats the validation results as plain text.
-   *
-   * @return string
-   *   The results, formatted as plain text.
-   */
-  protected function getResultsAsText(): string {
-    $text = '';
-
-    foreach ($this->getResults() as $result) {
-      $messages = $result->getMessages();
-      $summary = $result->getSummary();
-      if ($summary) {
-        array_unshift($messages, $summary);
-      }
-      $text .= implode("\n", $messages) . "\n";
-    }
-    return $text;
-  }
-
-}
diff --git a/package_manager/src/FailureMarker.php b/package_manager/src/FailureMarker.php
index 3ef94e6532..d18e011aae 100644
--- a/package_manager/src/FailureMarker.php
+++ b/package_manager/src/FailureMarker.php
@@ -6,7 +6,7 @@ namespace Drupal\package_manager;
 
 use Drupal\Component\Serialization\Json;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
-use Drupal\package_manager\Exception\ApplyFailedException;
+use Drupal\package_manager\Exception\StageFailureMarkerException;
 
 /**
  * Handles failure marker file operation.
@@ -82,10 +82,10 @@ final class FailureMarker {
         $data = json_decode($data, TRUE, 512, JSON_THROW_ON_ERROR);
       }
       catch (\JsonException $exception) {
-        throw new ApplyFailedException('Failure marker file exists but cannot be decoded.', $exception->getCode(), $exception);
+        throw new StageFailureMarkerException('Failure marker file exists but cannot be decoded.', $exception->getCode(), $exception);
       }
 
-      throw new ApplyFailedException($data['message']);
+      throw new StageFailureMarkerException($data['message']);
     }
   }
 
diff --git a/package_manager/src/Stage.php b/package_manager/src/Stage.php
index 9d727e6155..9a6d5d7f18 100644
--- a/package_manager/src/Stage.php
+++ b/package_manager/src/Stage.php
@@ -24,9 +24,9 @@ use Drupal\package_manager\Event\PreOperationStageEvent;
 use Drupal\package_manager\Event\PreRequireEvent;
 use Drupal\package_manager\Event\StageEvent;
 use Drupal\package_manager\Exception\ApplyFailedException;
+use Drupal\package_manager\Exception\StageEventException;
 use Drupal\package_manager\Exception\StageException;
 use Drupal\package_manager\Exception\StageOwnershipException;
-use Drupal\package_manager\Exception\StageValidationException;
 use PhpTuf\ComposerStager\Domain\Core\Beginner\BeginnerInterface;
 use PhpTuf\ComposerStager\Domain\Core\Committer\CommitterInterface;
 use PhpTuf\ComposerStager\Domain\Core\Stager\StagerInterface;
@@ -267,7 +267,7 @@ class Stage implements LoggerAwareInterface {
     $this->failureMarker->assertNotExists();
 
     if (!$this->isAvailable()) {
-      throw new StageException('Cannot create a new stage because one already exists.');
+      throw new StageException($this, 'Cannot create a new stage because one already exists.');
     }
     // Mark the stage as unavailable as early as possible, before dispatching
     // the pre-create event. The idea is to prevent a race condition if the
@@ -292,7 +292,7 @@ class Stage implements LoggerAwareInterface {
       $ignored_paths = $this->getIgnoredPaths();
     }
     catch (\Throwable $throwable) {
-      throw new StageException($throwable->getMessage(), $throwable->getCode(), $throwable);
+      throw new StageException($this, $throwable->getMessage(), $throwable->getCode(), $throwable);
     }
     $event = new PreCreateEvent($this, $ignored_paths);
     // If an error occurs and we won't be able to create the stage, mark it as
@@ -377,7 +377,7 @@ class Stage implements LoggerAwareInterface {
       $ignored_paths = $this->getIgnoredPaths();
     }
     catch (\Throwable $throwable) {
-      throw new StageException($throwable->getMessage(), $throwable->getCode(), $throwable);
+      throw new StageException($this, $throwable->getMessage(), $throwable->getCode(), $throwable);
     }
 
     // If an error occurs while dispatching the events, ensure that ::destroy()
@@ -402,7 +402,7 @@ class Stage implements LoggerAwareInterface {
       // The commit operation has not started yet, so we can clear the failure
       // marker.
       $this->failureMarker->clear();
-      throw new StageException($e->getMessage(), $e->getCode(), $e);
+      throw new StageException($this, $e->getMessage(), $e->getCode(), $e);
     }
     catch (\Throwable $throwable) {
       // The commit operation may have failed midway through, and the site code
@@ -410,7 +410,7 @@ class Stage implements LoggerAwareInterface {
       // applying, because in this situation, the site owner should probably
       // restore everything from a backup.
       $this->setNotApplying()();
-      throw new ApplyFailedException($throwable->getMessage(), $throwable->getCode(), $throwable);
+      throw new ApplyFailedException($this, (string) $this->getFailureMarkerMessage(), $throwable->getCode(), $throwable);
     }
     $this->failureMarker->clear();
     $this->setMetadata(self::TEMPSTORE_CHANGES_APPLIED, TRUE);
@@ -469,7 +469,7 @@ class Stage implements LoggerAwareInterface {
       $this->checkOwnership();
     }
     if ($this->isApplying()) {
-      throw new StageException('Cannot destroy the stage directory while it is being applied to the active directory.');
+      throw new StageException($this, 'Cannot destroy the stage directory while it is being applied to the active directory.');
     }
 
     $this->dispatch(new PreDestroyEvent($this));
@@ -513,24 +513,21 @@ class Stage implements LoggerAwareInterface {
    *   (optional) A callback function to call if an error occurs, before any
    *   exceptions are thrown.
    *
-   * @throws \Drupal\package_manager\Exception\StageValidationException
+   * @throws \Drupal\package_manager\Exception\StageEventException
    *   If the event collects any validation errors.
-   * @throws \Drupal\package_manager\Exception\StageException
-   *   If any other sort of error occurs.
    */
   protected function dispatch(StageEvent $event, callable $on_error = NULL): void {
     try {
       $this->eventDispatcher->dispatch($event);
 
       if ($event instanceof PreOperationStageEvent) {
-        $results = $event->getResults();
-        if ($results) {
-          $error = new StageValidationException($results);
+        if ($event->getResults()) {
+          $error = new StageEventException($event);
         }
       }
     }
     catch (\Throwable $error) {
-      $error = new StageException($error->getMessage(), $error->getCode(), $error);
+      $error = new StageEventException($event, $error->getMessage(), $error->getCode(), $error);
     }
 
     if (isset($error)) {
@@ -595,7 +592,7 @@ class Stage implements LoggerAwareInterface {
     if ($this->isAvailable()) {
       // phpcs:disable DrupalPractice.General.ExceptionT.ExceptionT
       // @see https://www.drupal.org/project/automatic_updates/issues/3338651
-      throw new StageException($this->computeDestroyMessage(
+      throw new StageException($this, $this->computeDestroyMessage(
         $unique_id,
         $this->t('Cannot claim the stage because no stage has been created.')
       )->render());
@@ -603,7 +600,7 @@ class Stage implements LoggerAwareInterface {
 
     $stored_lock = $this->tempStore->getIfOwner(static::TEMPSTORE_LOCK_KEY);
     if (!$stored_lock) {
-      throw new StageOwnershipException($this->computeDestroyMessage(
+      throw new StageOwnershipException($this, $this->computeDestroyMessage(
         $unique_id,
         $this->t('Cannot claim the stage because it is not owned by the current user or session.')
       )->render());
@@ -614,7 +611,7 @@ class Stage implements LoggerAwareInterface {
       return $this;
     }
 
-    throw new StageOwnershipException($this->computeDestroyMessage(
+    throw new StageOwnershipException($this, $this->computeDestroyMessage(
       $unique_id,
       $this->t('Cannot claim the stage because the current lock does not match the stored lock.')
     )->render());
@@ -659,7 +656,7 @@ class Stage implements LoggerAwareInterface {
 
     $stored_lock = $this->tempStore->getIfOwner(static::TEMPSTORE_LOCK_KEY);
     if ($stored_lock !== $this->lock) {
-      throw new StageOwnershipException('Stage is not owned by the current user or session.');
+      throw new StageOwnershipException($this, 'Stage is not owned by the current user or session.');
     }
   }
 
diff --git a/package_manager/tests/src/Kernel/ComposerMinimumStabilityValidatorTest.php b/package_manager/tests/src/Kernel/ComposerMinimumStabilityValidatorTest.php
index 5cb13665d0..f3ff2e8ce6 100644
--- a/package_manager/tests/src/Kernel/ComposerMinimumStabilityValidatorTest.php
+++ b/package_manager/tests/src/Kernel/ComposerMinimumStabilityValidatorTest.php
@@ -4,7 +4,7 @@ declare(strict_types = 1);
 
 namespace Drupal\Tests\package_manager\Kernel;
 
-use Drupal\package_manager\Exception\StageValidationException;
+use Drupal\package_manager\Exception\StageEventException;
 use Drupal\package_manager\ValidationResult;
 
 /**
@@ -27,8 +27,8 @@ class ComposerMinimumStabilityValidatorTest extends PackageManagerKernelTestBase
       $stage->require(['drupal/core:9.8.1-beta1']);
       $this->fail('Able to require a package even though it did not meet minimum stability.');
     }
-    catch (StageValidationException $exception) {
-      $this->assertValidationResultsEqual([$result], $exception->getResults());
+    catch (StageEventException $exception) {
+      $this->assertValidationResultsEqual([$result], $exception->event->getResults());
     }
     $stage->destroy();
 
diff --git a/package_manager/tests/src/Kernel/ComposerPatchesValidatorTest.php b/package_manager/tests/src/Kernel/ComposerPatchesValidatorTest.php
index e2781023c4..d694b8b831 100644
--- a/package_manager/tests/src/Kernel/ComposerPatchesValidatorTest.php
+++ b/package_manager/tests/src/Kernel/ComposerPatchesValidatorTest.php
@@ -8,6 +8,7 @@ use Drupal\Core\Url;
 use Drupal\fixture_manipulator\ActiveFixtureManipulator;
 use Drupal\package_manager\Event\PreCreateEvent;
 use Drupal\package_manager\Event\StatusCheckEvent;
+use Drupal\package_manager\Exception\StageEventException;
 use Drupal\package_manager\ValidationResult;
 use Symfony\Component\Process\Process;
 
@@ -162,12 +163,12 @@ class ComposerPatchesValidatorTest extends PackageManagerKernelTestBase {
 
     try {
       $stage->apply();
-      // If we didn't get an exception, ensure we didn't expect any errors
+      // If we didn't get an exception, ensure we didn't expect any errors.
       $this->assertSame([], $expected_results);
     }
-    catch (TestStageValidationException $e) {
+    catch (StageEventException $e) {
       $this->assertNotEmpty($expected_results);
-      $this->assertValidationResultsEqual($expected_results, $e->getResults(), NULL, $stage_dir);
+      $this->assertValidationResultsEqual($expected_results, $e->event->getResults(), NULL, $stage_dir);
     }
   }
 
diff --git a/package_manager/tests/src/Kernel/DuplicateInfoFileValidatorTest.php b/package_manager/tests/src/Kernel/DuplicateInfoFileValidatorTest.php
index d5c4c3e3a2..980eb5cdd8 100644
--- a/package_manager/tests/src/Kernel/DuplicateInfoFileValidatorTest.php
+++ b/package_manager/tests/src/Kernel/DuplicateInfoFileValidatorTest.php
@@ -4,7 +4,7 @@ declare(strict_types = 1);
 
 namespace Drupal\Tests\package_manager\Kernel;
 
-use Drupal\package_manager\Exception\StageValidationException;
+use Drupal\package_manager\Exception\StageEventException;
 use Drupal\package_manager\ValidationResult;
 use Symfony\Component\Filesystem\Filesystem;
 
@@ -214,9 +214,9 @@ class DuplicateInfoFileValidatorTest extends PackageManagerKernelTestBase {
       $stage->apply();
       $this->assertEmpty($expected_results);
     }
-    catch (StageValidationException $e) {
+    catch (StageEventException $e) {
       $this->assertNotEmpty($expected_results);
-      $this->assertValidationResultsEqual($expected_results, $e->getResults());
+      $this->assertValidationResultsEqual($expected_results, $e->event->getResults());
     }
   }
 
diff --git a/package_manager/tests/src/Kernel/FailureMarkerTest.php b/package_manager/tests/src/Kernel/FailureMarkerTest.php
index 97708dabb0..c9cfbd831b 100644
--- a/package_manager/tests/src/Kernel/FailureMarkerTest.php
+++ b/package_manager/tests/src/Kernel/FailureMarkerTest.php
@@ -5,7 +5,7 @@ declare(strict_types = 1);
 namespace Drupal\Tests\package_manager\Kernel;
 
 use Drupal\Core\StringTranslation\StringTranslationTrait;
-use Drupal\package_manager\Exception\ApplyFailedException;
+use Drupal\package_manager\Exception\StageFailureMarkerException;
 
 /**
  * @coversDefaultClass \Drupal\package_manager\FailureMarker
@@ -22,7 +22,7 @@ class FailureMarkerTest extends PackageManagerKernelTestBase {
     $failure_marker = $this->container->get('package_manager.failure_marker');
     $failure_marker->write($this->createStage(), $this->t('Disastrous catastrophe!'));
 
-    $this->expectException(ApplyFailedException::class);
+    $this->expectException(StageFailureMarkerException::class);
     $this->expectExceptionMessage('Disastrous catastrophe!');
     $failure_marker->assertNotExists();
   }
@@ -37,7 +37,7 @@ class FailureMarkerTest extends PackageManagerKernelTestBase {
     // Write the failure marker with invalid JSON.
     file_put_contents($failure_marker->getPath(), '{}}');
 
-    $this->expectException(ApplyFailedException::class);
+    $this->expectException(StageFailureMarkerException::class);
     $this->expectExceptionMessage('Failure marker file exists but cannot be decoded.');
     $failure_marker->assertNotExists();
   }
diff --git a/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php b/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
index 95fc412280..7e9a9730ba 100644
--- a/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
+++ b/package_manager/tests/src/Kernel/PackageManagerKernelTestBase.php
@@ -10,11 +10,13 @@ use Drupal\Core\Site\Settings;
 use Drupal\fixture_manipulator\StageFixtureManipulator;
 use Drupal\KernelTests\KernelTestBase;
 use Drupal\package_manager\Event\PreApplyEvent;
-use Drupal\package_manager\Event\StageEvent;
+use Drupal\package_manager\Event\PreCreateEvent;
+use Drupal\package_manager\Event\PreOperationStageEvent;
+use Drupal\package_manager\Exception\StageEventException;
 use Drupal\package_manager\StatusCheckTrait;
 use Drupal\package_manager\Validator\DiskSpaceValidator;
-use Drupal\package_manager\Exception\StageValidationException;
 use Drupal\package_manager\Stage;
+use Drupal\system\SystemManager;
 use Drupal\Tests\package_manager\Traits\AssertPreconditionsTrait;
 use Drupal\Tests\package_manager\Traits\FixtureManipulatorTrait;
 use Drupal\Tests\package_manager\Traits\FixtureUtilityTrait;
@@ -165,14 +167,9 @@ abstract class PackageManagerKernelTestBase extends KernelTestBase {
       // If we did not get an exception, ensure we didn't expect any results.
       $this->assertValidationResultsEqual([], $expected_results);
     }
-    catch (TestStageValidationException $e) {
-      $this->assertValidationResultsEqual($expected_results, $e->getResults());
+    catch (StageEventException $e) {
       $this->assertNotEmpty($expected_results);
-      // TestStage::dispatch() throws TestStageValidationException with the
-      // event object so that we can analyze it.
-      $this->assertNotEmpty($event_class);
-      $this->assertInstanceOf(StageValidationException::class, $e->getOriginalException());
-      $this->assertInstanceOf($event_class, $e->getEvent());
+      $this->assertExpectedResultsFromException($expected_results, $e);
     }
     return $stage;
   }
@@ -384,77 +381,44 @@ abstract class PackageManagerKernelTestBase extends KernelTestBase {
     StageFixtureManipulator::handleTearDown();
   }
 
-}
-
-/**
- * Test-only class to associate event with StageValidationException.
- *
- * @todo Remove this class in https://drupal.org/i/3331355 or if that issue is
- *   closed without adding the ability to associate events with exceptions
- *   remove this comment.
- */
-final class TestStageValidationException extends StageValidationException {
-
   /**
-   * The stage event.
+   * Asserts that a StageEventException has a particular set of results.
    *
-   * @var \Drupal\package_manager\Event\StageEvent
+   * @param array $expected_results
+   *   The expected results.
+   * @param \Drupal\package_manager\Exception\StageEventException $exception
+   *   The exception.
    */
-  private $event;
-
-  /**
-   * The original exception.
-   *
-   * @var \Drupal\package_manager\Exception\StageValidationException
-   */
-  private $originalException;
-
-  public function __construct(StageValidationException $original_exception, StageEvent $event) {
-    parent::__construct($original_exception->getResults(), $original_exception->getMessage(), $original_exception->getCode(), $original_exception);
-    $this->originalException = $original_exception;
-    $this->event = $event;
+  protected function assertExpectedResultsFromException(array $expected_results, StageEventException $exception): void {
+    $event = $exception->event;
+    $this->assertInstanceOf(PreOperationStageEvent::class, $event);
+    $this->assertValidationResultsEqual($expected_results, $event->getResults());
   }
 
   /**
-   * Gets the original exception which is triggered at the event.
+   * Creates a StageEventException from an array of validation results.
    *
-   * @return \Drupal\package_manager\Exception\StageValidationException
-   *   Exception triggered at event.
-   */
-  public function getOriginalException(): StageValidationException {
-    return $this->originalException;
-  }
-
-  /**
-   * Gets the stage event which triggers the exception.
+   * @param \Drupal\package_manager\ValidationResult[] $expected_results
+   *   The validation results. Note that only errors will be added to the event;
+   *   warnings will be ignored.
+   * @param string $event_class
+   *   (optional) The event which raised the exception. Defaults to
+   *   PreCreateEvent.
+   * @param \Drupal\package_manager\Stage $stage
+   *   (optional) The stage which caused the exception.
    *
-   * @return \Drupal\package_manager\Event\StageEvent
-   *   Event triggering stage exception.
+   * @return \Drupal\package_manager\Exception\StageEventException
+   *   An exception with the given validation results.
    */
-  public function getEvent(): StageEvent {
-    return $this->event;
-  }
-
-}
-
-/**
- * Common functions for test stages.
- */
-trait TestStageTrait {
+  protected function createStageEventExceptionFromResults(array $expected_results, string $event_class = PreCreateEvent::class, Stage $stage = NULL): StageEventException {
+    $event = new $event_class($stage ?? $this->createStage(), []);
 
-  /**
-   * {@inheritdoc}
-   */
-  protected function dispatch(StageEvent $event, callable $on_error = NULL): void {
-    try {
-      parent::dispatch($event, $on_error);
-    }
-    catch (StageValidationException $e) {
-      // Throw TestStageValidationException with event object so that test
-      // code can verify that the exception was thrown when a specific event was
-      // dispatched.
-      throw new TestStageValidationException($e, $event);
+    foreach ($expected_results as $result) {
+      if ($result->getSeverity() === SystemManager::REQUIREMENT_ERROR) {
+        $event->addError($result->getMessages(), $result->getSummary());
+      }
     }
+    return new StageEventException($event);
   }
 
 }
@@ -464,8 +428,6 @@ trait TestStageTrait {
  */
 class TestStage extends Stage {
 
-  use TestStageTrait;
-
   /**
    * {@inheritdoc}
    *
diff --git a/package_manager/tests/src/Kernel/PendingUpdatesValidatorTest.php b/package_manager/tests/src/Kernel/PendingUpdatesValidatorTest.php
index 4156a56a3b..bf4c160057 100644
--- a/package_manager/tests/src/Kernel/PendingUpdatesValidatorTest.php
+++ b/package_manager/tests/src/Kernel/PendingUpdatesValidatorTest.php
@@ -5,7 +5,7 @@ declare(strict_types = 1);
 namespace Drupal\Tests\package_manager\Kernel;
 
 use Drupal\package_manager\Event\PreCreateEvent;
-use Drupal\package_manager\Exception\StageValidationException;
+use Drupal\package_manager\Exception\StageEventException;
 use Drupal\package_manager\ValidationResult;
 
 /**
@@ -81,8 +81,8 @@ class PendingUpdatesValidatorTest extends PackageManagerKernelTestBase {
       $stage->apply();
       $this->fail('Able to apply update even though there is pending update.');
     }
-    catch (StageValidationException $exception) {
-      $this->assertValidationResultsEqual([$result], $exception->getResults());
+    catch (StageEventException $exception) {
+      $this->assertExpectedResultsFromException([$result], $exception);
     }
   }
 
diff --git a/package_manager/tests/src/Kernel/StageEventsTest.php b/package_manager/tests/src/Kernel/StageEventsTest.php
index 425acbe59b..a2b34f14b5 100644
--- a/package_manager/tests/src/Kernel/StageEventsTest.php
+++ b/package_manager/tests/src/Kernel/StageEventsTest.php
@@ -15,6 +15,7 @@ use Drupal\package_manager\Event\PreDestroyEvent;
 use Drupal\package_manager\Event\PreOperationStageEvent;
 use Drupal\package_manager\Event\PreRequireEvent;
 use Drupal\package_manager\Event\StageEvent;
+use Drupal\package_manager\Exception\StageEventException;
 use Drupal\package_manager\ValidationResult;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
@@ -182,7 +183,7 @@ class StageEventsTest extends PackageManagerKernelTestBase implements EventSubsc
     };
     $this->addEventTestListener($listener, PreCreateEvent::class);
 
-    $this->expectException(TestStageValidationException::class);
+    $this->expectException(StageEventException::class);
     $this->expectExceptionMessage('Event propagation stopped without any errors added to the event. This bypasses the package_manager validation system.');
     $stage = $this->createStage();
     $stage->create();
diff --git a/package_manager/tests/src/Kernel/StageTest.php b/package_manager/tests/src/Kernel/StageTest.php
index 09fe3c335c..3ae2e3b17e 100644
--- a/package_manager/tests/src/Kernel/StageTest.php
+++ b/package_manager/tests/src/Kernel/StageTest.php
@@ -16,6 +16,7 @@ use Drupal\package_manager\Event\PreCreateEvent;
 use Drupal\package_manager\Event\StageEvent;
 use Drupal\package_manager\Exception\ApplyFailedException;
 use Drupal\package_manager\Exception\StageException;
+use Drupal\package_manager\Exception\StageFailureMarkerException;
 use Drupal\package_manager_bypass\LoggingCommitter;
 use PhpTuf\ComposerStager\Domain\Exception\InvalidArgumentException;
 use PhpTuf\ComposerStager\Domain\Exception\PreconditionException;
@@ -332,6 +333,12 @@ class StageTest extends PackageManagerKernelTestBase {
       $this->fail('Expected an exception.');
     }
     catch (\Throwable $exception) {
+      // This needs to be done because we always use the message from
+      // \Drupal\package_manager\Stage::getFailureMarkerMessage() when throwing
+      // ApplyFailedException.
+      if ($expected_class == ApplyFailedException::class) {
+        $thrown_message = 'Staged changes failed to apply, and the site is in an indeterminate state. It is strongly recommended to restore the code and database from a backup.';
+      }
       $this->assertInstanceOf($expected_class, $exception);
       $this->assertSame($thrown_message, $exception->getMessage());
       $this->assertSame(123, $exception->getCode());
@@ -357,7 +364,7 @@ class StageTest extends PackageManagerKernelTestBase {
 
     // Make the committer throw an exception, which should cause the failure
     // marker to be present.
-    $thrown = new \Exception('Disastrous catastrophe!');
+    $thrown = new \Exception('Staged changes failed to apply, and the site is in an indeterminate state. It is strongly recommended to restore the code and database from a backup.');
     LoggingCommitter::setException($thrown);
     try {
       $stage->apply();
@@ -376,7 +383,7 @@ class StageTest extends PackageManagerKernelTestBase {
       $stage->create();
       $this->fail('Expected an exception.');
     }
-    catch (ApplyFailedException $e) {
+    catch (StageFailureMarkerException $e) {
       $this->assertSame('Staged changes failed to apply, and the site is in an indeterminate state. It is strongly recommended to restore the code and database from a backup.', $e->getMessage());
       $this->assertFalse($stage->isApplying());
     }
diff --git a/package_manager/tests/src/Kernel/StageValidationExceptionTest.php b/package_manager/tests/src/Kernel/StageValidationExceptionTest.php
deleted file mode 100644
index 7f219c5333..0000000000
--- a/package_manager/tests/src/Kernel/StageValidationExceptionTest.php
+++ /dev/null
@@ -1,84 +0,0 @@
-<?php
-
-declare(strict_types = 1);
-
-namespace Drupal\Tests\package_manager\Kernel;
-
-use Drupal\package_manager\Event\PreCreateEvent;
-use Drupal\package_manager\Exception\StageValidationException;
-use Drupal\package_manager\ValidationResult;
-use Drupal\package_manager_test_validation\EventSubscriber\TestSubscriber;
-
-/**
- * @coversDefaultClass \Drupal\package_manager\Exception\StageValidationException
- * @group package_manager
- * @internal
- */
-class StageValidationExceptionTest extends PackageManagerKernelTestBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  protected static $modules = [
-    'package_manager_test_validation',
-  ];
-
-  /**
-   * Data provider for testErrors().
-   *
-   * @return string[][]
-   *   The test cases.
-   */
-  public function providerResultsAsText(): array {
-    $messages = ['Bang!', 'Pow!'];
-    $translated_messages = [t('Bang!'), t('Pow!')];
-    $summary = t('There was sadness.');
-
-    $result_no_summary = ValidationResult::createError([$translated_messages[0]]);
-    $result_with_summary = ValidationResult::createError($translated_messages, $summary);
-    $result_with_summary_message = "{$summary->getUntranslatedString()}\n{$messages[0]}\n{$messages[1]}\n";
-
-    return [
-      '1 result with summary' => [
-        [$result_with_summary],
-        $result_with_summary_message,
-      ],
-      '2 results, with summaries' => [
-        [$result_with_summary, $result_with_summary],
-        "$result_with_summary_message$result_with_summary_message",
-      ],
-      '1 result without summary' => [
-        [$result_no_summary],
-        $messages[0],
-      ],
-      '2 results without summaries' => [
-        [$result_no_summary, $result_no_summary],
-        $messages[0] . "\n" . $messages[0],
-      ],
-      '1 result with summary, 1 result without summary' => [
-        [$result_with_summary, $result_no_summary],
-        $result_with_summary_message . $messages[0] . "\n",
-      ],
-    ];
-  }
-
-  /**
-   * Tests formatting a set of validation results as plain text.
-   *
-   * @param \Drupal\package_manager\ValidationResult[] $validation_results
-   *   The expected validation results which should be logged.
-   * @param string $expected_message
-   *   The expected exception message.
-   *
-   * @dataProvider providerResultsAsText
-   *
-   * @covers ::getResultsAsText()
-   */
-  public function testResultsAsText(array $validation_results, string $expected_message): void {
-    TestSubscriber::setTestResult($validation_results, PreCreateEvent::class);
-    $this->expectException(StageValidationException::class);
-    $this->expectExceptionMessage($expected_message);
-    $this->createStage()->create();
-  }
-
-}
diff --git a/src/BatchProcessor.php b/src/BatchProcessor.php
index e991f25639..8bb86b0f59 100644
--- a/src/BatchProcessor.php
+++ b/src/BatchProcessor.php
@@ -5,7 +5,6 @@ declare(strict_types = 1);
 namespace Drupal\automatic_updates;
 
 use Drupal\Core\Url;
-use Drupal\package_manager\Exception\StageValidationException;
 use Drupal\system\Controller\DbUpdateController;
 use Symfony\Component\HttpFoundation\RedirectResponse;
 
@@ -56,24 +55,7 @@ final class BatchProcessor {
    *   have been recorded.
    */
   protected static function handleException(\Throwable $error, array &$context): void {
-    $error_messages = [];
-
-    if ($error instanceof StageValidationException) {
-      foreach ($error->getResults() as $result) {
-        $messages = $result->getMessages();
-        if (count($messages) > 1) {
-          array_unshift($messages, $result->getSummary());
-        }
-        $error_messages = array_merge($error_messages, $messages);
-      }
-    }
-    else {
-      $error_messages[] = $error->getMessage();
-    }
-
-    foreach ($error_messages as $error_message) {
-      $context['results']['errors'][] = $error_message;
-    }
+    $context['results']['errors'][] = $error->getMessage();
     throw $error;
   }
 
diff --git a/src/CronUpdater.php b/src/CronUpdater.php
index 2612bb8418..e3624b7bbb 100644
--- a/src/CronUpdater.php
+++ b/src/CronUpdater.php
@@ -9,7 +9,7 @@ use Drupal\Core\Mail\MailManagerInterface;
 use Drupal\Core\State\StateInterface;
 use Drupal\Core\Url;
 use Drupal\package_manager\Exception\ApplyFailedException;
-use Drupal\package_manager\Exception\StageValidationException;
+use Drupal\package_manager\Exception\StageEventException;
 use Drupal\package_manager\ProjectInfo;
 use Drupal\update\ProjectRelease;
 use GuzzleHttp\Psr7\Uri as GuzzleUri;
@@ -174,7 +174,7 @@ class CronUpdater extends Updater {
         'target_version' => $target_version,
         'error_message' => $e->getMessage(),
       ];
-      if ($e instanceof ApplyFailedException || $e->getPrevious() instanceof ApplyFailedException) {
+      if ($e instanceof ApplyFailedException) {
         $mail_params['urgent'] = TRUE;
         $key = 'cron_failed_apply';
       }
@@ -304,7 +304,7 @@ class CronUpdater extends Updater {
     try {
       $this->destroy();
     }
-    catch (StageValidationException $e) {
+    catch (StageEventException $e) {
       $this->logger->error($e->getMessage());
     }
 
diff --git a/src/Exception/UpdateException.php b/src/Exception/UpdateException.php
deleted file mode 100644
index 7279d62bb7..0000000000
--- a/src/Exception/UpdateException.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-declare(strict_types = 1);
-
-namespace Drupal\automatic_updates\Exception;
-
-use Drupal\package_manager\Exception\StageValidationException;
-
-/**
- * Defines a custom exception for a failure during an update.
- *
- * Should not be thrown by external code. This is only used to identify
- * validation errors that occurred during a stage operation performed by
- * Automatic Updates.
- *
- * @see \Drupal\automatic_updates\Updater::dispatch()
- */
-class UpdateException extends StageValidationException {
-}
diff --git a/src/Form/UpdateReady.php b/src/Form/UpdateReady.php
index 37bee5122f..414c2b2bb9 100644
--- a/src/Form/UpdateReady.php
+++ b/src/Form/UpdateReady.php
@@ -6,6 +6,7 @@ namespace Drupal\automatic_updates\Form;
 
 use Drupal\automatic_updates\BatchProcessor;
 use Drupal\automatic_updates\Updater;
+use Drupal\package_manager\Exception\StageFailureMarkerException;
 use Drupal\package_manager\ValidationResult;
 use Drupal\Core\Batch\BatchBuilder;
 use Drupal\Core\Extension\ModuleExtensionList;
@@ -13,7 +14,6 @@ use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Messenger\MessengerInterface;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\State\StateInterface;
-use Drupal\package_manager\Exception\ApplyFailedException;
 use Drupal\package_manager\Exception\StageException;
 use Drupal\package_manager\Exception\StageOwnershipException;
 use Drupal\system\SystemManager;
@@ -81,7 +81,7 @@ final class UpdateReady extends UpdateFormBase {
       $this->messenger()->addError($e->getMessage());
       return $form;
     }
-    catch (ApplyFailedException $e) {
+    catch (StageFailureMarkerException $e) {
       $this->messenger()->addError($e->getMessage());
       return $form;
     }
diff --git a/src/Form/UpdaterForm.php b/src/Form/UpdaterForm.php
index a6212210a0..14a637dcef 100644
--- a/src/Form/UpdaterForm.php
+++ b/src/Form/UpdaterForm.php
@@ -6,11 +6,11 @@ namespace Drupal\automatic_updates\Form;
 
 use Drupal\automatic_updates\BatchProcessor;
 use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\package_manager\Exception\StageFailureMarkerException;
 use Drupal\package_manager\FailureMarker;
 use Drupal\package_manager\ProjectInfo;
 use Drupal\automatic_updates\ReleaseChooser;
 use Drupal\automatic_updates\Updater;
-use Drupal\package_manager\Exception\ApplyFailedException;
 use Drupal\update\ProjectRelease;
 use Drupal\Core\Batch\BatchBuilder;
 use Drupal\Core\Extension\ExtensionVersion;
@@ -93,7 +93,7 @@ final class UpdaterForm extends UpdateFormBase {
     try {
       $this->failureMarker->assertNotExists();
     }
-    catch (ApplyFailedException $e) {
+    catch (StageFailureMarkerException $e) {
       $this->messenger()->addError($e->getMessage());
       return $form;
     }
diff --git a/src/Updater.php b/src/Updater.php
index 7a73c0cae6..cca3126508 100644
--- a/src/Updater.php
+++ b/src/Updater.php
@@ -4,11 +4,7 @@ declare(strict_types = 1);
 
 namespace Drupal\automatic_updates;
 
-use Drupal\automatic_updates\Exception\UpdateException;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
-use Drupal\package_manager\Event\StageEvent;
-use Drupal\package_manager\Exception\ApplyFailedException;
-use Drupal\package_manager\Exception\StageValidationException;
 use Drupal\package_manager\Stage;
 
 /**
@@ -96,30 +92,6 @@ class Updater extends Stage {
     $this->require($versions['production'], $versions['dev'], $timeout);
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  protected function dispatch(StageEvent $event, callable $on_error = NULL): void {
-    try {
-      parent::dispatch($event, $on_error);
-    }
-    catch (StageValidationException $e) {
-      throw new UpdateException($e->getResults(), $e->getMessage(), $e->getCode(), $e);
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function apply(?int $timeout = 600): void {
-    try {
-      parent::apply($timeout);
-    }
-    catch (ApplyFailedException $exception) {
-      throw new UpdateException([], "The update operation failed to apply completely. All the files necessary to run Drupal correctly and securely are probably not present. It is strongly recommended to restore your site's code and database from a backup.", $exception->getCode(), $exception);
-    }
-  }
-
   /**
    * {@inheritdoc}
    */
diff --git a/tests/src/Functional/UpdateFailedTest.php b/tests/src/Functional/UpdateFailedTest.php
index 8258256741..a09fb0483c 100644
--- a/tests/src/Functional/UpdateFailedTest.php
+++ b/tests/src/Functional/UpdateFailedTest.php
@@ -34,11 +34,11 @@ class UpdateFailedTest extends UpdaterFormTestBase {
     LoggingCommitter::setException(new \Exception('failed at committer'));
     $page->pressButton('Continue');
     $this->checkForMetaRefresh();
+    $failure_message = 'Automatic updates failed to apply, and the site is in an indeterminate state. Consider restoring the code and database from a backup.';
     $assert_session->pageTextContainsOnce('An error has occurred.');
-    $assert_session->pageTextContains("The update operation failed to apply completely. All the files necessary to run Drupal correctly and securely are probably not present. It is strongly recommended to restore your site's code and database from a backup.");
+    $assert_session->pageTextContains($failure_message);
     $page->clickLink('the error page');
 
-    $failure_message = 'Automatic updates failed to apply, and the site is in an indeterminate state. Consider restoring the code and database from a backup.';
     // We should be on the form (i.e., 200 response code), but unable to
     // continue the update.
     $assert_session->statusCodeEquals(200);
diff --git a/tests/src/Kernel/AutomaticUpdatesKernelTestBase.php b/tests/src/Kernel/AutomaticUpdatesKernelTestBase.php
index 44c25ce6d7..5a448f079b 100644
--- a/tests/src/Kernel/AutomaticUpdatesKernelTestBase.php
+++ b/tests/src/Kernel/AutomaticUpdatesKernelTestBase.php
@@ -10,7 +10,6 @@ use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\Url;
 use Drupal\Tests\automatic_updates\Traits\ValidationTestTrait;
 use Drupal\Tests\package_manager\Kernel\PackageManagerKernelTestBase;
-use Drupal\Tests\package_manager\Kernel\TestStageTrait;
 
 /**
  * Base class for kernel tests of the Automatic Updates module.
@@ -91,8 +90,6 @@ abstract class AutomaticUpdatesKernelTestBase extends PackageManagerKernelTestBa
  */
 class TestUpdater extends Updater {
 
-  use TestStageTrait;
-
   /**
    * {@inheritdoc}
    */
@@ -107,8 +104,6 @@ class TestUpdater extends Updater {
  */
 class TestCronUpdater extends CronUpdater {
 
-  use TestStageTrait;
-
   /**
    * {@inheritdoc}
    */
diff --git a/tests/src/Kernel/CronUpdaterTest.php b/tests/src/Kernel/CronUpdaterTest.php
index d0f80524d6..f347398058 100644
--- a/tests/src/Kernel/CronUpdaterTest.php
+++ b/tests/src/Kernel/CronUpdaterTest.php
@@ -17,8 +17,8 @@ use Drupal\package_manager\Event\PreApplyEvent;
 use Drupal\package_manager\Event\PreDestroyEvent;
 use Drupal\package_manager\Event\PreRequireEvent;
 use Drupal\package_manager\Event\PreCreateEvent;
+use Drupal\package_manager\Exception\StageEventException;
 use Drupal\package_manager\Exception\StageOwnershipException;
-use Drupal\package_manager\Exception\StageValidationException;
 use Drupal\package_manager\ValidationResult;
 use Drupal\package_manager_bypass\LoggingCommitter;
 use Drupal\Tests\automatic_updates\Traits\EmailNotificationsTestTrait;
@@ -223,19 +223,19 @@ class CronUpdaterTest extends AutomaticUpdatesKernelTestBase {
       // @see \Drupal\package_manager\Stage::dispatch()
       'pre-create validation error' => [
         PreCreateEvent::class,
-        StageValidationException::class,
+        StageEventException::class,
       ],
       'pre-require validation error' => [
         PreRequireEvent::class,
-        StageValidationException::class,
+        StageEventException::class,
       ],
       'pre-apply validation error' => [
         PreApplyEvent::class,
-        StageValidationException::class,
+        StageEventException::class,
       ],
       'pre-destroy validation error' => [
         PreDestroyEvent::class,
-        StageValidationException::class,
+        StageEventException::class,
       ],
     ];
   }
@@ -277,15 +277,18 @@ class CronUpdaterTest extends AutomaticUpdatesKernelTestBase {
       ->get('cron')
       ->addLogger($cron_logger);
 
-    // When the event specified by $event_class is fired, either throw an
-    // exception directly from the event subscriber, or set a validation error
-    // (if the exception class is StageValidationException).
-    if ($exception_class === StageValidationException::class) {
-      $results = [
-        ValidationResult::createError([t('Destroy the stage!')]),
-      ];
-      TestSubscriber1::setTestResult($results, $event_class);
-      $exception = new StageValidationException($results);
+    /** @var \Drupal\automatic_updates\CronUpdater $updater */
+    $updater = $this->container->get('automatic_updates.cron_updater');
+
+    // When the event specified by $event_class is dispatched, either throw an
+    // exception directly from the event subscriber, or prepare a
+    // StageEventException which will format the validation errors its own way.
+    if ($exception_class === StageEventException::class) {
+      $error = ValidationResult::createError([
+        t('Destroy the stage!'),
+      ]);
+      $exception = $this->createStageEventExceptionFromResults([$error], $event_class, $updater);
+      TestSubscriber1::setTestResult($exception->event->getResults(), $event_class);
     }
     else {
       /** @var \Throwable $exception */
@@ -298,8 +301,6 @@ class CronUpdaterTest extends AutomaticUpdatesKernelTestBase {
     $this->assertEmpty($cron_logger->records);
     $this->assertEmpty($this->logger->records);
 
-    /** @var \Drupal\automatic_updates\CronUpdater $updater */
-    $updater = $this->container->get('automatic_updates.cron_updater');
     $this->assertTrue($updater->isAvailable());
     $this->container->get('cron')->run();
 
@@ -324,27 +325,16 @@ class CronUpdaterTest extends AutomaticUpdatesKernelTestBase {
     // won't try to catch it. Instead, it will be caught and logged by the main
     // cron service.
     if ($event_class === PreDestroyEvent::class || $event_class === PostDestroyEvent::class) {
-      if ($exception instanceof StageValidationException) {
-        $this->assertTrue($logged_by_updater);
-        $this->assertFalse($logged_by_cron);
-      }
-      else {
-        $this->assertFalse($logged_by_updater);
-        $this->assertTrue($logged_by_cron);
-      }
       // If the pre-destroy event throws an exception or flags a validation
       // error, the stage won't be destroyed. But, once the post-destroy event
       // is fired, the stage should be fully destroyed and marked as available.
       $this->assertSame($event_class === PostDestroyEvent::class, $updater->isAvailable());
     }
-    // For all other events, the error should be caught and logged by the cron
-    // updater, not the main cron service, and the stage should always be
-    // destroyed and marked as available.
     else {
-      $this->assertTrue($logged_by_updater);
-      $this->assertFalse($logged_by_cron);
       $this->assertTrue($updater->isAvailable());
     }
+    $this->assertTrue($logged_by_updater);
+    $this->assertFalse($logged_by_cron);
   }
 
   /**
@@ -407,8 +397,8 @@ class CronUpdaterTest extends AutomaticUpdatesKernelTestBase {
       $stage->apply();
       $this->fail('Expected update to fail');
     }
-    catch (StageValidationException $exception) {
-      $this->assertValidationResultsEqual([ValidationResult::createError([$stop_error])], $exception->getResults());
+    catch (StageEventException $exception) {
+      $this->assertExpectedResultsFromException([ValidationResult::createError([$stop_error])], $exception);
     }
 
     $this->assertTrue($this->logger->hasRecord("Cron will not perform any updates as an existing staged update is applying. The site is currently on an insecure version of Drupal core but will attempt to update to a secure version next time cron is run. This update may be applied manually at the <a href=\"%url\">update form</a>.", (string) RfcLogLevel::NOTICE));
@@ -507,11 +497,12 @@ END;
       ->set('cron', CronUpdater::ALL)
       ->save();
 
-    $results = [
-      ValidationResult::createError([t('Error while updating!')]),
-    ];
-    TestSubscriber1::setTestResult($results, $event_class);
-    $exception = new StageValidationException($results);
+    $error = ValidationResult::createError([
+      t('Error while updating!'),
+    ]);
+    $exception = $this->createStageEventExceptionFromResults([$error], $event_class, $this->container->get('automatic_updates.cron_updater'));
+    TestSubscriber1::setTestResult($exception->event->getResults(), $event_class);
+
     $this->container->get('cron')->run();
 
     $url = Url::fromRoute('update.report_update')
@@ -546,11 +537,13 @@ END;
     if ($event_class !== PreCreateEvent::class) {
       $this->getStageFixtureManipulator()->setCorePackageVersion('9.8.1');
     }
-    $results = [
-      ValidationResult::createError([t('Error while updating!')]),
-    ];
-    TestSubscriber1::setTestResult($results, $event_class);
-    $exception = new StageValidationException($results);
+
+    $error = ValidationResult::createError([
+      t('Error while updating!'),
+    ]);
+    TestSubscriber1::setTestResult([$error], $event_class);
+    $exception = $this->createStageEventExceptionFromResults([$error], $event_class, $this->container->get('automatic_updates.cron_updater'));
+
     $this->container->get('cron')->run();
 
     $url = Url::fromRoute('update.report_update')
@@ -583,7 +576,7 @@ END;
     $expected_body = <<<END
 Drupal core failed to update automatically from 9.8.0 to 9.8.1. The following error was logged:
 
-The update operation failed to apply completely. All the files necessary to run Drupal correctly and securely are probably not present. It is strongly recommended to restore your site's code and database from a backup.
+Automatic updates failed to apply, and the site is in an indeterminate state. Consider restoring the code and database from a backup.
 
 This e-mail was sent by the Automatic Updates module. Unattended updates are not yet fully supported.
 
diff --git a/tests/src/Kernel/StatusCheck/CronServerValidatorTest.php b/tests/src/Kernel/StatusCheck/CronServerValidatorTest.php
index 8d7870d2e4..5383df65fb 100644
--- a/tests/src/Kernel/StatusCheck/CronServerValidatorTest.php
+++ b/tests/src/Kernel/StatusCheck/CronServerValidatorTest.php
@@ -8,7 +8,6 @@ use Drupal\automatic_updates\CronUpdater;
 use Drupal\automatic_updates\Validator\CronServerValidator;
 use Drupal\Core\Logger\RfcLogLevel;
 use Drupal\Core\Url;
-use Drupal\package_manager\Exception\StageValidationException;
 use Drupal\package_manager\ValidationResult;
 use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase;
 use ColinODell\PsrTestLogger\TestLogger;
@@ -129,7 +128,7 @@ class CronServerValidatorTest extends AutomaticUpdatesKernelTestBase {
       // Assert the update was not staged to ensure the error was flagged in
       // PreCreateEvent and not PreApplyEvent.
       $this->assertUpdateStagedTimes(0);
-      $error = new StageValidationException($expected_results);
+      $error = $this->createStageEventExceptionFromResults($expected_results);
       $this->assertTrue($logger->hasRecord($error->getMessage(), (string) RfcLogLevel::ERROR));
     }
     else {
@@ -190,7 +189,7 @@ class CronServerValidatorTest extends AutomaticUpdatesKernelTestBase {
     $this->container->get('cron')->run();
     if ($expected_results) {
       $this->assertUpdateStagedTimes(1);
-      $error = new StageValidationException($expected_results);
+      $error = $this->createStageEventExceptionFromResults($expected_results);
       $this->assertTrue($logger->hasRecord($error->getMessage(), (string) RfcLogLevel::ERROR));
     }
     else {
diff --git a/tests/src/Kernel/StatusCheck/RequestedUpdateValidatorTest.php b/tests/src/Kernel/StatusCheck/RequestedUpdateValidatorTest.php
index 1444ceba81..b9fa93ee75 100644
--- a/tests/src/Kernel/StatusCheck/RequestedUpdateValidatorTest.php
+++ b/tests/src/Kernel/StatusCheck/RequestedUpdateValidatorTest.php
@@ -4,7 +4,7 @@ declare(strict_types = 1);
 
 namespace Drupal\Tests\automatic_updates\Kernel\StatusCheck;
 
-use Drupal\package_manager\Exception\StageValidationException;
+use Drupal\package_manager\Exception\StageEventException;
 use Drupal\package_manager\ValidationResult;
 use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase;
 
@@ -52,8 +52,8 @@ class RequestedUpdateValidatorTest extends AutomaticUpdatesKernelTestBase {
       $updater->apply();
       $this->fail('Expecting an exception.');
     }
-    catch (StageValidationException $exception) {
-      $this->assertValidationResultsEqual($expected_results, $exception->getResults());
+    catch (StageEventException $exception) {
+      $this->assertExpectedResultsFromException($expected_results, $exception);
     }
   }
 
@@ -76,7 +76,7 @@ class RequestedUpdateValidatorTest extends AutomaticUpdatesKernelTestBase {
     $updater = $this->container->get('automatic_updates.updater');
     $updater->begin(['drupal' => '9.8.1']);
     $updater->stage();
-    $this->expectException(StageValidationException::class);
+    $this->expectException(StageEventException::class);
     $this->expectExceptionMessage('No updates detected in the staging area.');
     $updater->apply();
   }
diff --git a/tests/src/Kernel/StatusCheck/ScaffoldFilePermissionsValidatorTest.php b/tests/src/Kernel/StatusCheck/ScaffoldFilePermissionsValidatorTest.php
index d2352a17df..ccb8dd7684 100644
--- a/tests/src/Kernel/StatusCheck/ScaffoldFilePermissionsValidatorTest.php
+++ b/tests/src/Kernel/StatusCheck/ScaffoldFilePermissionsValidatorTest.php
@@ -5,7 +5,8 @@ declare(strict_types = 1);
 namespace Drupal\Tests\automatic_updates\Kernel\StatusCheck;
 
 use Drupal\fixture_manipulator\ActiveFixtureManipulator;
-use Drupal\package_manager\Exception\StageValidationException;
+use Drupal\package_manager\Exception\ApplyFailedException;
+use Drupal\package_manager\Exception\StageEventException;
 use Drupal\package_manager\PathLocator;
 use Drupal\package_manager\ValidationResult;
 use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase;
@@ -125,8 +126,8 @@ class ScaffoldFilePermissionsValidatorTest extends AutomaticUpdatesKernelTestBas
       // If no exception was thrown, ensure that we weren't expecting an error.
       $this->assertEmpty($expected_results);
     }
-    catch (StageValidationException $e) {
-      $this->assertValidationResultsEqual($expected_results, $e->getResults());
+    catch (StageEventException $e) {
+      $this->assertExpectedResultsFromException($expected_results, $e);
     }
   }
 
@@ -331,8 +332,13 @@ class ScaffoldFilePermissionsValidatorTest extends AutomaticUpdatesKernelTestBas
       // If no exception was thrown, ensure that we weren't expecting an error.
       $this->assertEmpty($expected_results);
     }
-    catch (StageValidationException $e) {
-      $this->assertValidationResultsEqual($expected_results, $e->getResults());
+    // If we try to overwrite any write-protected paths, even if they're not
+    // scaffold files, we'll get an ApplyFailedException.
+    catch (ApplyFailedException $e) {
+      $this->assertSame("Automatic updates failed to apply, and the site is in an indeterminate state. Consider restoring the code and database from a backup.", $e->getMessage());
+    }
+    catch (StageEventException $e) {
+      $this->assertExpectedResultsFromException($expected_results, $e);
     }
   }
 
diff --git a/tests/src/Kernel/StatusCheck/StagedProjectsValidatorTest.php b/tests/src/Kernel/StatusCheck/StagedProjectsValidatorTest.php
index 26b70642cf..d11b04a1de 100644
--- a/tests/src/Kernel/StatusCheck/StagedProjectsValidatorTest.php
+++ b/tests/src/Kernel/StatusCheck/StagedProjectsValidatorTest.php
@@ -6,7 +6,7 @@ namespace Drupal\Tests\automatic_updates\Kernel\StatusCheck;
 
 use Drupal\fixture_manipulator\ActiveFixtureManipulator;
 use Drupal\package_manager\Event\PreApplyEvent;
-use Drupal\package_manager\Exception\StageValidationException;
+use Drupal\package_manager\Exception\StageEventException;
 use Drupal\package_manager\ValidationResult;
 use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase;
 
@@ -62,8 +62,8 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
       $updater->apply();
       $this->fail('Expected an error, but none was raised.');
     }
-    catch (StageValidationException $e) {
-      $this->assertValidationResultsEqual([$error], $e->getResults());
+    catch (StageEventException $e) {
+      $this->assertExpectedResultsFromException([$error], $e);
     }
   }
 
@@ -153,8 +153,8 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
       $updater->apply();
       $this->fail('Expected an error, but none was raised.');
     }
-    catch (StageValidationException $e) {
-      $this->assertValidationResultsEqual([$error], $e->getResults());
+    catch (StageEventException $e) {
+      $this->assertExpectedResultsFromException([$error], $e);
     }
   }
 
@@ -230,8 +230,8 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
       $updater->apply();
       $this->fail('Expected an error, but none was raised.');
     }
-    catch (StageValidationException $e) {
-      $this->assertValidationResultsEqual([$error], $e->getResults());
+    catch (StageEventException $e) {
+      $this->assertExpectedResultsFromException([$error], $e);
     }
   }
 
@@ -293,8 +293,8 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
       $updater->apply();
       $this->fail('Expected an error, but none was raised.');
     }
-    catch (StageValidationException $e) {
-      $this->assertValidationResultsEqual([$error], $e->getResults());
+    catch (StageEventException $e) {
+      $this->assertExpectedResultsFromException([$error], $e);
     }
   }
 
diff --git a/tests/src/Kernel/StatusCheck/VersionPolicyValidatorTest.php b/tests/src/Kernel/StatusCheck/VersionPolicyValidatorTest.php
index d02a32db29..f84181b2e8 100644
--- a/tests/src/Kernel/StatusCheck/VersionPolicyValidatorTest.php
+++ b/tests/src/Kernel/StatusCheck/VersionPolicyValidatorTest.php
@@ -6,8 +6,8 @@ namespace Drupal\Tests\automatic_updates\Kernel\StatusCheck;
 
 use Drupal\automatic_updates\CronUpdater;
 use Drupal\package_manager\Event\PreCreateEvent;
+use Drupal\package_manager\Exception\StageEventException;
 use Drupal\package_manager\Exception\StageException;
-use Drupal\package_manager\Exception\StageValidationException;
 use Drupal\package_manager\ValidationResult;
 use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase;
 
@@ -315,8 +315,8 @@ class VersionPolicyValidatorTest extends AutomaticUpdatesKernelTestBase {
       // Reset the updater for the next iteration of the loop.
       $updater->destroy();
     }
-    catch (StageValidationException $e) {
-      $this->assertValidationResultsEqual($expected_results, $e->getResults());
+    catch (StageEventException $e) {
+      $this->assertExpectedResultsFromException($expected_results, $e);
     }
   }
 
diff --git a/tests/src/Kernel/UpdaterTest.php b/tests/src/Kernel/UpdaterTest.php
index 5ce6687760..98c8d27faf 100644
--- a/tests/src/Kernel/UpdaterTest.php
+++ b/tests/src/Kernel/UpdaterTest.php
@@ -4,15 +4,9 @@ declare(strict_types = 1);
 
 namespace Drupal\Tests\automatic_updates\Kernel;
 
-use Drupal\automatic_updates\Exception\UpdateException;
-use Drupal\automatic_updates_test\EventSubscriber\TestSubscriber1;
-use Drupal\package_manager\Event\PreApplyEvent;
-use Drupal\package_manager\Event\PreCreateEvent;
-use Drupal\package_manager\Event\PreRequireEvent;
+use Drupal\package_manager\Exception\ApplyFailedException;
 use Drupal\package_manager\Exception\StageException;
-use Drupal\package_manager\ValidationResult;
 use Drupal\package_manager_bypass\LoggingCommitter;
-use Drupal\Tests\package_manager\Kernel\TestStageValidationException;
 use Drupal\Tests\user\Traits\UserCreationTrait;
 use PhpTuf\ComposerStager\Domain\Exception\InvalidArgumentException;
 
@@ -156,7 +150,7 @@ class UpdaterTest extends AutomaticUpdatesKernelTestBase {
     return [
       'RuntimeException' => [
         'RuntimeException',
-        UpdateException::class,
+        ApplyFailedException::class,
       ],
       'InvalidArgumentException' => [
         InvalidArgumentException::class,
@@ -164,7 +158,7 @@ class UpdaterTest extends AutomaticUpdatesKernelTestBase {
       ],
       'Exception' => [
         'Exception',
-        UpdateException::class,
+        ApplyFailedException::class,
       ],
     ];
   }
@@ -190,8 +184,8 @@ class UpdaterTest extends AutomaticUpdatesKernelTestBase {
     $thrown_message = 'A very bad thing happened';
     LoggingCommitter::setException(new $thrown_class($thrown_message, 123));
     $this->expectException($expected_class);
-    $expected_message = $expected_class === UpdateException::class ?
-      "The update operation failed to apply completely. All the files necessary to run Drupal correctly and securely are probably not present. It is strongly recommended to restore your site's code and database from a backup."
+    $expected_message = $expected_class === ApplyFailedException::class ?
+      "Automatic updates failed to apply, and the site is in an indeterminate state. Consider restoring the code and database from a backup."
       : $thrown_message;
     $this->expectExceptionMessage($expected_message);
     $this->expectExceptionCode(123);
@@ -206,51 +200,4 @@ class UpdaterTest extends AutomaticUpdatesKernelTestBase {
     $this->assertSame('setLogger', $updater_method_calls[0][0]);
   }
 
-  /**
-   * Tests UpdateException handling.
-   *
-   * @param string $event_class
-   *   The stage life cycle event which should raise an error.
-   *
-   * @dataProvider providerUpdateException
-   */
-  public function testUpdateException(string $event_class) {
-    $updater = $this->container->get('automatic_updates.updater');
-    $results = [
-      ValidationResult::createError([t('An error of some sorts.')]),
-    ];
-    TestSubscriber1::setTestResult($results, $event_class);
-    try {
-      $updater->begin(['drupal' => '9.8.1']);
-      $updater->stage();
-      $updater->apply();
-      $this->fail('Expected an exception, but none was raised.');
-    }
-    catch (TestStageValidationException $e) {
-      $this->assertStringStartsWith('An error of some sorts.', $e->getMessage());
-      $this->assertInstanceOf(UpdateException::class, $e->getOriginalException());
-      $this->assertInstanceOf($event_class, $e->getEvent());
-    }
-  }
-
-  /**
-   * Data provider for testUpdateException().
-   *
-   * @return string[][]
-   *   The test cases.
-   */
-  public function providerUpdateException(): array {
-    return [
-      'pre-create exception' => [
-        PreCreateEvent::class,
-      ],
-      'pre-require exception' => [
-        PreRequireEvent::class,
-      ],
-      'pre-apply exception' => [
-        PreApplyEvent::class,
-      ],
-    ];
-  }
-
 }
-- 
GitLab