From 5ab3a35c43ab2d8cd2540f54eb2379afbb14bf8a Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Tue, 3 Nov 2026 11:26:16 -0600 Subject: [PATCH] Settle the pending payload deletion before asserting the bytes are gone (#970) Cleanup commits the object's last registration together with a durable deletion intent, and deletes the bytes after that commit. On SQLite the after-commit step can lose the write lock to the writer the test has just released; it then keeps the intent for the next cleanup pass, as designed. The test asserted the bytes were gone immediately, so it failed intermittently on unrelated PRs. Bytes may now outlive the commit only with exactly that intent pending. The test runs the retry pass, as the other retention tests do, then requires both the bytes and the intent to be gone. Co-Authored-By: Claude Opus 5.5 --- .../Feature/WorkflowStreamPayloadRaceTest.php | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/Feature/WorkflowStreamPayloadRaceTest.php b/tests/Feature/WorkflowStreamPayloadRaceTest.php index 8b0c0abbd..267d32c72 100644 --- a/tests/Feature/WorkflowStreamPayloadRaceTest.php +++ b/tests/Feature/WorkflowStreamPayloadRaceTest.php @@ -8,6 +8,7 @@ use App\Models\WorkflowNamespace; use App\Support\HistoryRetentionEnforcer; use App\Support\NamespaceExternalPayloadStorage; +use App\Support\RuntimeExternalPayloadDeletions; use App\Support\RuntimeExternalPayloadException; use App\Support\RuntimeExternalPayloadObjectLock; use App\Support\RuntimeExternalPayloadRegistry; @@ -598,9 +599,25 @@ private function assertCleanup(string $cleanup, array $result, bool $objectDelet $this->assertSame($objectDeleted ? 1 : 0, $result['external_payloads_deleted'], json_encode($result)); } + /** + * Cleanup commits the object's last registration together with a durable + * deletion intent; the provider deletion runs after that commit. On + * SQLite the after-commit step can lose the write lock to the writer the + * test has just released (an immediate busy snapshot error) and leave the + * intent for the next cleanup pass (#970). Bytes may outlive the commit + * only with that intent pending, and that pass must remove both. + */ private function assertObjectDeleted(array $object): void { - $this->assertFileDoesNotExist(rawurldecode((string) parse_url($object['uri'], PHP_URL_PATH))); + $path = rawurldecode((string) parse_url($object['uri'], PHP_URL_PATH)); + $intents = static fn (): int => DB::table('runtime_external_payload_deletions') + ->where('storage_uri', $object['uri'])->count(); + if (is_file($path)) { + $this->assertSame(1, $intents(), 'surviving bytes need a pending deletion intent'); + app(RuntimeExternalPayloadDeletions::class)->retryPending('default', 10); + } + $this->assertFileDoesNotExist($path); + $this->assertSame(0, $intents()); $this->assertSame(0, RuntimeExternalPayload::query()->where('storage_uri', $object['uri'])->count()); }