From 05c637f6b76436500985743d3e37c50051d7ad76 Mon Sep 17 00:00:00 2001 From: Alex Standiford Date: Fri, 18 Sep 2026 07:32:16 -0400 Subject: [PATCH 1/7] Define coordinated cleanup failure cause retention --- ...dinatedOperationCleanupFailedException.php | 22 ++++++++ ...CoordinationCleanupFailureContractTest.php | 56 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 lib/Exceptions/CoordinatedOperationCleanupFailedException.php create mode 100644 tests/Unit/Contracts/CoordinationCleanupFailureContractTest.php diff --git a/lib/Exceptions/CoordinatedOperationCleanupFailedException.php b/lib/Exceptions/CoordinatedOperationCleanupFailedException.php new file mode 100644 index 0000000..5d98baf --- /dev/null +++ b/lib/Exceptions/CoordinatedOperationCleanupFailedException.php @@ -0,0 +1,22 @@ +markTestIncomplete('Coordinated cleanup failure implementation assignment is pending.'); + } + + /** @dataProvider causeKinds */ + public function testBothOriginalFailuresRemainInspectableAsAnUnknownOutcome(bool $operationIsError, bool $cleanupIsError): void + { + $operation = $operationIsError ? new Error('Private operation detail') : new RuntimeException('Private operation detail'); + $cleanup = $cleanupIsError ? new Error('Private cleanup detail') : new RuntimeException('Private cleanup detail'); + $failure = new CoordinatedOperationCleanupFailedException($operation, $cleanup); + + self::assertInstanceOf(CoordinatedOperationOutcomeUnknownException::class, $failure); + self::assertInstanceOf(DatastoreErrorException::class, $failure); + self::assertNotInstanceOf(CoordinatedOperationConflictException::class, $failure); + self::assertSame($operation, $failure->getOperationFailure()); + self::assertSame($cleanup, $failure->getPrevious()); + self::assertSame('The coordinated operation failed and cleanup could not be confirmed.', $failure->getMessage()); + self::assertSame(0, $failure->getCode()); + self::assertNull($operation->getPrevious()); + self::assertNull($cleanup->getPrevious()); + + $otherOperation = new RuntimeException('Another operation'); + $otherCleanup = new RuntimeException('Another cleanup'); + $other = new CoordinatedOperationCleanupFailedException($otherOperation, $otherCleanup); + self::assertSame($otherOperation, $other->getOperationFailure()); + self::assertSame($otherCleanup, $other->getPrevious()); + self::assertSame($operation, $failure->getOperationFailure()); + self::assertSame($cleanup, $failure->getPrevious()); + } + + /** @return array */ + public static function causeKinds(): array + { + return [ + 'exceptions' => [false, false], 'operation error' => [true, false], + 'cleanup error' => [false, true], 'both errors' => [true, true], + ]; + } +} From fffd0db6e08313930e382f80aecd23799fc29bf9 Mon Sep 17 00:00:00 2001 From: Alex Standiford Date: Fri, 18 Sep 2026 07:12:36 -0400 Subject: [PATCH 2/7] Align PHPUnit CI with Composer lock --- .github/workflows/phpunit.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 93c7b1a..1be5a7b 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -10,8 +10,11 @@ jobs: - uses: actions/checkout@v3 - uses: php-actions/composer@v6 + with: + php_version: "8.1" - name: PHPUnit Tests - uses: php-actions/phpunit@v3 + uses: php-actions/phpunit@v4 with: - configuration: phpunit.xml \ No newline at end of file + configuration: phpunit.xml + php_version: "8.1" From aa1368f270f242b9898ef127e5a8db784c28ee65 Mon Sep 17 00:00:00 2001 From: Alex Standiford Date: Fri, 18 Sep 2026 07:14:23 -0400 Subject: [PATCH 3/7] Use lock-compatible PHP in test CI --- .github/workflows/phpunit.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 1be5a7b..6308093 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -11,10 +11,10 @@ jobs: - uses: php-actions/composer@v6 with: - php_version: "8.1" + php_version: "8.2" - name: PHPUnit Tests uses: php-actions/phpunit@v4 with: configuration: phpunit.xml - php_version: "8.1" + php_version: "8.2" From 102b696b421b500e46107c3ba2077c5181a46a3b Mon Sep 17 00:00:00 2001 From: Alex Standiford Date: Fri, 18 Sep 2026 14:17:48 -0400 Subject: [PATCH 4/7] Clarify the cleanup contract test boundary --- tests/Unit/Contracts/CoordinationCleanupFailureContractTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/Contracts/CoordinationCleanupFailureContractTest.php b/tests/Unit/Contracts/CoordinationCleanupFailureContractTest.php index a025e31..a343f66 100644 --- a/tests/Unit/Contracts/CoordinationCleanupFailureContractTest.php +++ b/tests/Unit/Contracts/CoordinationCleanupFailureContractTest.php @@ -10,7 +10,7 @@ use PHPNomad\Database\Tests\TestCase; use RuntimeException; -/** Storage-neutral cause retention. Real cleanup behavior belongs to each adapter. */ +/** Keeps both causes without depending on a storage adapter. Each adapter tests its cleanup behavior. */ final class CoordinationCleanupFailureContractTest extends TestCase { protected function setUp(): void From 38f50487ada82de70f9bc1c1bf956463e1463def Mon Sep 17 00:00:00 2001 From: Alex Standiford Date: Fri, 18 Sep 2026 14:25:36 -0400 Subject: [PATCH 5/7] Implement coordinated cleanup failure retention --- .../CoordinatedOperationCleanupFailedException.php | 13 ++++++++++--- .../CoordinationCleanupFailureContractTest.php | 6 ------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/Exceptions/CoordinatedOperationCleanupFailedException.php b/lib/Exceptions/CoordinatedOperationCleanupFailedException.php index 5d98baf..2566897 100644 --- a/lib/Exceptions/CoordinatedOperationCleanupFailedException.php +++ b/lib/Exceptions/CoordinatedOperationCleanupFailedException.php @@ -2,21 +2,28 @@ namespace PHPNomad\Database\Exceptions; -use RuntimeException; use Throwable; /** An operation failed and its cleanup could not establish a safe outcome. */ class CoordinatedOperationCleanupFailedException extends CoordinatedOperationOutcomeUnknownException { + private Throwable $operationFailure; + /** Retain the cleanup failure as previous and the earlier operation failure separately. */ public function __construct(Throwable $operationFailure, Throwable $cleanupFailure) { - parent::__construct('The coordinated operation failed and cleanup could not be confirmed.'); + $this->operationFailure = $operationFailure; + + parent::__construct( + 'The coordinated operation failed and cleanup could not be confirmed.', + 0, + $cleanupFailure + ); } /** Return the exact earlier failure that required cleanup. */ public function getOperationFailure(): Throwable { - return new RuntimeException(); + return $this->operationFailure; } } diff --git a/tests/Unit/Contracts/CoordinationCleanupFailureContractTest.php b/tests/Unit/Contracts/CoordinationCleanupFailureContractTest.php index a343f66..6d78d02 100644 --- a/tests/Unit/Contracts/CoordinationCleanupFailureContractTest.php +++ b/tests/Unit/Contracts/CoordinationCleanupFailureContractTest.php @@ -13,12 +13,6 @@ /** Keeps both causes without depending on a storage adapter. Each adapter tests its cleanup behavior. */ final class CoordinationCleanupFailureContractTest extends TestCase { - protected function setUp(): void - { - parent::setUp(); - $this->markTestIncomplete('Coordinated cleanup failure implementation assignment is pending.'); - } - /** @dataProvider causeKinds */ public function testBothOriginalFailuresRemainInspectableAsAnUnknownOutcome(bool $operationIsError, bool $cleanupIsError): void { From 8e0df0a4f87a70a5754d53228c1b37866b41b8d2 Mon Sep 17 00:00:00 2001 From: Alex Standiford Date: Fri, 18 Sep 2026 14:26:30 -0400 Subject: [PATCH 6/7] Preserve cleanup contract test setup --- .../Contracts/CoordinationCleanupFailureContractTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/Unit/Contracts/CoordinationCleanupFailureContractTest.php b/tests/Unit/Contracts/CoordinationCleanupFailureContractTest.php index 6d78d02..83b91bb 100644 --- a/tests/Unit/Contracts/CoordinationCleanupFailureContractTest.php +++ b/tests/Unit/Contracts/CoordinationCleanupFailureContractTest.php @@ -13,6 +13,11 @@ /** Keeps both causes without depending on a storage adapter. Each adapter tests its cleanup behavior. */ final class CoordinationCleanupFailureContractTest extends TestCase { + protected function setUp(): void + { + parent::setUp(); + } + /** @dataProvider causeKinds */ public function testBothOriginalFailuresRemainInspectableAsAnUnknownOutcome(bool $operationIsError, bool $cleanupIsError): void { From 7a37f9155aa5a4f4c75a3dc295d51afb914b64ff Mon Sep 17 00:00:00 2001 From: Alex Standiford Date: Fri, 18 Sep 2026 15:22:34 -0400 Subject: [PATCH 7/7] Remove empty cleanup test setup override --- .../Contracts/CoordinationCleanupFailureContractTest.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/Unit/Contracts/CoordinationCleanupFailureContractTest.php b/tests/Unit/Contracts/CoordinationCleanupFailureContractTest.php index 83b91bb..6d78d02 100644 --- a/tests/Unit/Contracts/CoordinationCleanupFailureContractTest.php +++ b/tests/Unit/Contracts/CoordinationCleanupFailureContractTest.php @@ -13,11 +13,6 @@ /** Keeps both causes without depending on a storage adapter. Each adapter tests its cleanup behavior. */ final class CoordinationCleanupFailureContractTest extends TestCase { - protected function setUp(): void - { - parent::setUp(); - } - /** @dataProvider causeKinds */ public function testBothOriginalFailuresRemainInspectableAsAnUnknownOutcome(bool $operationIsError, bool $cleanupIsError): void {