From bc62150f24dbcdbda5c7d6b3c62239686f1d0cdd Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Thu, 29 Oct 2026 00:59:54 +0000 Subject: [PATCH 1/4] Clean up before an unstorable write only when an entry is there An unstorable response (its own code sets Cache-Control private/no-store) is never written, so every GET of it misses and reaches the write, which ran the pre-write cleanup each time: both pools invalidated and the CDN purged on every request. Decide unstorability first and clean up only when an earlier write left an entry for the URI (a value/view, or a donut template), so a stale entry is still dropped while a plain miss writes and purges nothing. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017CaQyRS7w44aWJTTCn8uys --- CHANGELOG.md | 1 + src/DonutRepository.php | 25 +++++++++++++++++---- src/QueryRepository.php | 23 +++++++++++-------- tests/DonutRepositoryTest.php | 42 +++++++++++++++++++++++++++++++++++ tests/QueryRepositoryTest.php | 29 ++++++++++++++++++++++++ 5 files changed, 107 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bc30dbe..e461090d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Deploy this version with a new `CacheVersionModule` version (an app that binds its own cache adapter rather than this package's `Storage*Module`s must clear its pools instead): entries written by earlier versions may hold per-user content, and an earlier version reading this version's donut cache shows raw `[le2:...]` placeholders during a rolling deploy. - A `#[Cacheable]`/`#[CacheableResponse]`/`#[DonutCache]` response whose own code, including a `$headers` property default, sets `Cache-Control` to `private` or `no-store` is not stored server-side. To keep server-side caching with a client-facing `no-store`, declare `#[HttpCache(noStore: true)]` instead. +- Writing such an unstorable response invalidates the pools and purges the CDN only when an earlier write left an entry for its URI, not on every GET. - `ResourceDonut::FORMAT` is no longer public; use `ResourceDonut::placeholder()`. - Requires `koriym/semantic-logger` `^0.11`. Written logs use its `log` document shape, and diagnostic `kind` is now `code`. - Requires `bear/resource` `^1.22.4`. diff --git a/src/DonutRepository.php b/src/DonutRepository.php index fe15dbdb..f9e592cd 100644 --- a/src/DonutRepository.php +++ b/src/DonutRepository.php @@ -156,8 +156,7 @@ private function doPutStatic(ResourceObject $ro, int|null $ttl, int|null $sMaxAg $unstorable = $this->declaredCacheControl->reason($ro); $surrogateKeys = $this->setHeaders($keys, $ro, $sMaxAge); $this->logCdnHeaders($ro); - $this->logger->event(new PreWriteCleanupContext((string) $ro->uri)); - $this->resourceStorage->invalidateTags([($this->uriTag)($ro->uri)]); + $this->cleanUpBeforeWrite($ro, $unstorable); if ($unstorable !== null) { $this->logger->event(new PutSkippedContext((string) $ro->uri, $unstorable)); @@ -186,8 +185,7 @@ private function doPutDonut(ResourceObject $ro, int|null $donutTtl): void } $this->logCdnHeaders($ro); - $this->logger->event(new PreWriteCleanupContext((string) $ro->uri)); - $this->resourceStorage->invalidateTags([($this->uriTag)($ro->uri)]); + $this->cleanUpBeforeWrite($ro, $unstorable); if ($unstorable !== null) { $this->logger->event(new PutSkippedContext((string) $ro->uri, $unstorable)); @@ -197,6 +195,25 @@ private function doPutDonut(ResourceObject $ro, int|null $donutTtl): void $this->resourceStorage->saveDonut($ro->uri, $donut, $donutTtl, $templateKeys); } + /** + * Drop what an earlier write left under this URI before it is written again + * + * An unstorable response is never written, so every GET of it misses and lands here: it is + * cleaned up only when an earlier write actually left a view or a template, or each of those + * GETs would invalidate both pools and purge the CDN for nothing. + * + * @param 'unstorable'|null $unstorable + */ + private function cleanUpBeforeWrite(ResourceObject $ro, string|null $unstorable): void + { + if ($unstorable !== null && $this->resourceStorage->get($ro->uri) === null && $this->resourceStorage->getDonut($ro->uri) === null) { + return; + } + + $this->logger->event(new PreWriteCleanupContext((string) $ro->uri)); + $this->resourceStorage->invalidateTags([($this->uriTag)($ro->uri)]); + } + /** * Register body children the page's onGet() already ran as dependencies, before the template keys read the header * diff --git a/src/QueryRepository.php b/src/QueryRepository.php index 2624688c..a3d6976a 100644 --- a/src/QueryRepository.php +++ b/src/QueryRepository.php @@ -69,11 +69,20 @@ public function put(ResourceObject $ro) private function doPut(ResourceObject $ro): bool { - // The writer knows its own purpose: this deleteEtag clears the entry about to be - // rewritten below. The marker records that, so readers never have to infer - // cleanup-vs-invalidation from tag correlation. - $this->logger->event(new PreWriteCleanupContext((string) $ro->uri)); - $this->storage->deleteEtag($ro->uri); + // Checked against the still-unmutated header, before headerSetter runs the identical + // check - a dynamically set Cache-Control trips this; #[HttpCache]'s own value is exempt. + $unstorable = $this->declaredCacheControl->reason($ro); + // An unstorable response is never written, so every GET of it misses and lands here: + // clean up only when an earlier write actually left an entry, or each of those GETs + // would invalidate both pools and purge the CDN for nothing. + if ($unstorable === null || $this->storage->get($ro->uri) !== null) { + // The writer knows its own purpose: this deleteEtag clears the entry about to be + // rewritten below. The marker records that, so readers never have to infer + // cleanup-vs-invalidation from tag correlation. + $this->logger->event(new PreWriteCleanupContext((string) $ro->uri)); + $this->storage->deleteEtag($ro->uri); + } + if ($ro->code === 200) { $this->setCacheDependency($ro); } @@ -93,10 +102,6 @@ private function doPut(ResourceObject $ro): bool $this->logger->event($this->cachePolicy($ro, $cacheable, $ttl)); } - // Checked against the still-unmutated header, before headerSetter runs the identical - // check - a dynamically set Cache-Control trips this; #[HttpCache]'s own value is exempt. - $unstorable = $this->declaredCacheControl->reason($ro); - ($this->headerSetter)($ro, $ttl, $httpCache); if ($unstorable !== null) { diff --git a/tests/DonutRepositoryTest.php b/tests/DonutRepositoryTest.php index 11f8798d..d00349ee 100644 --- a/tests/DonutRepositoryTest.php +++ b/tests/DonutRepositoryTest.php @@ -186,6 +186,48 @@ public function testDonutCacheTemplateSkipsSaveWhenThePageWritesPrivateIntoItsOw $this->assertNotNull(self::eventContextJsonOf($tree2, 'cache_miss')); } + public function testRepeatedUnstorableDonutGetDoesNotInvalidateOrPurge(): void + { + // Nothing is stored for an unstorable page, so every GET of it is a miss that reaches + // the donut write. With no earlier view or template there is nothing to clean up: each + // GET must not invalidate the pools and purge the CDN - for #[CacheableResponse] and + // #[DonutCache] alike. + $injector = $this->getInjector(); + $resource = $injector->getInstance(ResourceInterface::class); + $logger = $injector->getInstance(SemanticLoggerInterface::class, CacheLog::class); + + foreach (['page://self/html/unstorable-donut', 'page://self/html/unstorable-donut-cache-template'] as $uri) { + $resource->get($uri); + $logger->flush(); + $resource->get($uri); + $types = self::collectTypes($this->flushAndValidate($logger)); + + $this->assertContains('put_skipped', $types, $uri); + $this->assertNotContains('pre_write_cleanup', $types, $uri); + $this->assertNotContains('invalidate', $types, $uri); + } + } + + public function testUnstorableDonutWriteStillDropsAnEarlierEntry(): void + { + // A view and template stored while the page was storable must not outlive a later + // write of the same URI that turns out unstorable. + $injector = $this->getInjector(); + $resource = $injector->getInstance(ResourceInterface::class); + $storage = $injector->getInstance(ResourceStorageInterface::class); + $donutRepository = $injector->getInstance(DonutRepositoryInterface::class); + $uri = new Uri('page://self/html/unstorable-donut?unstorable=0'); + + $ro = $resource->get((string) $uri); + $this->assertNotNull($storage->get($uri)); + $this->assertNotNull($storage->getDonut($uri)); + + $ro->headers[Header::CACHE_CONTROL] = 'private'; + $donutRepository->putStatic($ro); + $this->assertNull($storage->get($uri)); + $this->assertNull($storage->getDonut($uri)); + } + public function testDonutCacheWithAMultiParamEmbedRendersAndRefreshesBothQueryParams(): void { // End-to-end through the real #[Embed] pipeline (URI-template expansion, not a diff --git a/tests/QueryRepositoryTest.php b/tests/QueryRepositoryTest.php index 558f33c6..cb5b361e 100644 --- a/tests/QueryRepositoryTest.php +++ b/tests/QueryRepositoryTest.php @@ -311,6 +311,35 @@ public function testRuntimePrivateResponseSkipsStoreAndOrdinaryResponsesStillSto $this->assertNotContains('cache_hit', self::collectTypes($tree2)); } + public function testRepeatedUnstorableGetDoesNotInvalidateOrPurge(): void + { + // Nothing is stored for an unstorable response, so every GET of it is a miss that + // reaches put(). With no earlier entry there is nothing to clean up: each GET must not + // invalidate the pools and purge the CDN. + $this->resource->get('app://self/unstorable-value'); + $this->logger->flush(); + $this->resource->get('app://self/unstorable-value'); + $tree = $this->flushAndValidate($this->logger); + + $types = self::collectTypes($tree); + $this->assertContains('put_skipped', $types); + $this->assertNotContains('pre_write_cleanup', $types); + $this->assertNotContains('invalidate', $types); + } + + public function testUnstorablePutStillDropsAnEarlierEntry(): void + { + // An entry stored while the response was storable must not outlive a later write of + // the same URI that turns out unstorable. + $uri = 'app://self/unstorable-value?unstorable=0'; + $ro = $this->resource->get($uri); + $this->assertNotNull($this->repository->get(new Uri($uri))); + + $ro->headers[Header::CACHE_CONTROL] = 'private'; + $this->assertFalse($this->repository->put($ro)); + $this->assertNull($this->repository->get(new Uri($uri))); + } + public function testCacheableWithNoHttpCacheStillStores(): void { // #[NoHttpCache] declares its own Cache-Control (private, no-store, no-cache, From 7ad7d4f5fcee7a788415d33689d7760b137d8a50 Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Thu, 29 Oct 2026 01:02:29 +0000 Subject: [PATCH 2/4] Move QueryRepository's pre-write cleanup decision into its own method Keeps doPut() within PHPMD's cyclomatic complexity threshold, in the same shape as DonutRepository::cleanUpBeforeWrite(). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017CaQyRS7w44aWJTTCn8uys --- src/QueryRepository.php | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/QueryRepository.php b/src/QueryRepository.php index a3d6976a..78318ab4 100644 --- a/src/QueryRepository.php +++ b/src/QueryRepository.php @@ -72,17 +72,7 @@ private function doPut(ResourceObject $ro): bool // Checked against the still-unmutated header, before headerSetter runs the identical // check - a dynamically set Cache-Control trips this; #[HttpCache]'s own value is exempt. $unstorable = $this->declaredCacheControl->reason($ro); - // An unstorable response is never written, so every GET of it misses and lands here: - // clean up only when an earlier write actually left an entry, or each of those GETs - // would invalidate both pools and purge the CDN for nothing. - if ($unstorable === null || $this->storage->get($ro->uri) !== null) { - // The writer knows its own purpose: this deleteEtag clears the entry about to be - // rewritten below. The marker records that, so readers never have to infer - // cleanup-vs-invalidation from tag correlation. - $this->logger->event(new PreWriteCleanupContext((string) $ro->uri)); - $this->storage->deleteEtag($ro->uri); - } - + $this->cleanUpBeforeWrite($ro, $unstorable); if ($ro->code === 200) { $this->setCacheDependency($ro); } @@ -123,6 +113,28 @@ private function doPut(ResourceObject $ro): bool return $this->storage->saveValue($ro, $ttl); } + /** + * Drop what an earlier write left under this URI before it is written again + * + * An unstorable response is never written, so every GET of it misses and lands here: it is + * cleaned up only when an earlier write actually left an entry, or each of those GETs would + * invalidate both pools and purge the CDN for nothing. + * + * @param 'unstorable'|null $unstorable + */ + private function cleanUpBeforeWrite(ResourceObject $ro, string|null $unstorable): void + { + if ($unstorable !== null && $this->storage->get($ro->uri) === null) { + return; + } + + // The writer knows its own purpose: this deleteEtag clears the entry about to be + // rewritten below. The marker records that, so readers never have to infer + // cleanup-vs-invalidation from tag correlation. + $this->logger->event(new PreWriteCleanupContext((string) $ro->uri)); + $this->storage->deleteEtag($ro->uri); + } + /** * The declaration that decided the lifetime, as declared. * From c5f2f77e68014abf8b7de2b9e2806e189ce5b3f5 Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Thu, 29 Oct 2026 01:06:29 +0000 Subject: [PATCH 3/4] Assert the purger is never called, not what the log records The regression tests now count PurgerInterface calls with FakeCountingPurger instead of looking for pre_write_cleanup/invalidate log events: the log's vocabulary is not a contract, the CDN call is. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017CaQyRS7w44aWJTTCn8uys --- tests/DonutRepositoryTest.php | 48 +++++++++++++++++++------------ tests/QueryRepositoryTest.php | 53 +++++++++++++++++++++++++---------- 2 files changed, 68 insertions(+), 33 deletions(-) diff --git a/tests/DonutRepositoryTest.php b/tests/DonutRepositoryTest.php index d00349ee..ddb69a49 100644 --- a/tests/DonutRepositoryTest.php +++ b/tests/DonutRepositoryTest.php @@ -12,6 +12,7 @@ use BEAR\Resource\Uri; use Koriym\SemanticLogger\SemanticLoggerInterface; use Madapaja\TwigModule\TwigModule; +use Override; use PHPUnit\Framework\TestCase; use Ray\Di\AbstractModule; use Ray\Di\Injector; @@ -57,11 +58,24 @@ public function setUp(): void parent::setUp(); } - private function getInjector(): Injector + private function getInjector(PurgerInterface|null $purger = null): Injector { $namespace = 'FakeVendor\HelloWorld'; $module = new FakeEtagPoolModule(ModuleFactory::getInstance($namespace)); $module->override(new TwigModule([dirname(__DIR__) . '/tests/Fake/fake-app/var/templates'])); + if ($purger !== null) { + $module->override(new class ($purger) extends AbstractModule { + public function __construct(private readonly PurgerInterface $purger) + { + } + + #[Override] + protected function configure(): void + { + $this->bind(PurgerInterface::class)->toInstance($this->purger); + } + }); + } return new Injector($module, __DIR__ . '/tmp'); } @@ -186,25 +200,20 @@ public function testDonutCacheTemplateSkipsSaveWhenThePageWritesPrivateIntoItsOw $this->assertNotNull(self::eventContextJsonOf($tree2, 'cache_miss')); } - public function testRepeatedUnstorableDonutGetDoesNotInvalidateOrPurge(): void + public function testRepeatedUnstorableDonutGetDoesNotPurge(): void { // Nothing is stored for an unstorable page, so every GET of it is a miss that reaches - // the donut write. With no earlier view or template there is nothing to clean up: each - // GET must not invalidate the pools and purge the CDN - for #[CacheableResponse] and - // #[DonutCache] alike. - $injector = $this->getInjector(); - $resource = $injector->getInstance(ResourceInterface::class); - $logger = $injector->getInstance(SemanticLoggerInterface::class, CacheLog::class); - + // the donut write. With no earlier view or template there is nothing to clean up: no + // GET may purge - for #[CacheableResponse] and #[DonutCache] alike. foreach (['page://self/html/unstorable-donut', 'page://self/html/unstorable-donut-cache-template'] as $uri) { - $resource->get($uri); - $logger->flush(); - $resource->get($uri); - $types = self::collectTypes($this->flushAndValidate($logger)); - - $this->assertContains('put_skipped', $types, $uri); - $this->assertNotContains('pre_write_cleanup', $types, $uri); - $this->assertNotContains('invalidate', $types, $uri); + $purger = new FakeCountingPurger(); + $resource = $this->getInjector($purger)->getInstance(ResourceInterface::class); + + for ($i = 0; $i < 3; $i++) { + $resource->get($uri); + } + + $this->assertSame([], $purger->tags, $uri); } } @@ -212,7 +221,8 @@ public function testUnstorableDonutWriteStillDropsAnEarlierEntry(): void { // A view and template stored while the page was storable must not outlive a later // write of the same URI that turns out unstorable. - $injector = $this->getInjector(); + $purger = new FakeCountingPurger(); + $injector = $this->getInjector($purger); $resource = $injector->getInstance(ResourceInterface::class); $storage = $injector->getInstance(ResourceStorageInterface::class); $donutRepository = $injector->getInstance(DonutRepositoryInterface::class); @@ -221,11 +231,13 @@ public function testUnstorableDonutWriteStillDropsAnEarlierEntry(): void $ro = $resource->get((string) $uri); $this->assertNotNull($storage->get($uri)); $this->assertNotNull($storage->getDonut($uri)); + $purger->reset(); $ro->headers[Header::CACHE_CONTROL] = 'private'; $donutRepository->putStatic($ro); $this->assertNull($storage->get($uri)); $this->assertNull($storage->getDonut($uri)); + $this->assertNotSame([], $purger->tags); } public function testDonutCacheWithAMultiParamEmbedRendersAndRefreshesBothQueryParams(): void diff --git a/tests/QueryRepositoryTest.php b/tests/QueryRepositoryTest.php index cb5b361e..b6cefc3c 100644 --- a/tests/QueryRepositoryTest.php +++ b/tests/QueryRepositoryTest.php @@ -17,6 +17,7 @@ use FakeVendor\HelloWorld\Resource\App\User\Profile; use FakeVendor\HelloWorld\Resource\Page\None; use Koriym\SemanticLogger\SemanticLoggerInterface; +use Override; use PHPUnit\Framework\TestCase; use Psr\Cache\CacheItemPoolInterface; use Ray\Di\AbstractModule; @@ -311,33 +312,55 @@ public function testRuntimePrivateResponseSkipsStoreAndOrdinaryResponsesStillSto $this->assertNotContains('cache_hit', self::collectTypes($tree2)); } - public function testRepeatedUnstorableGetDoesNotInvalidateOrPurge(): void + public function testRepeatedUnstorableGetDoesNotPurge(): void { // Nothing is stored for an unstorable response, so every GET of it is a miss that - // reaches put(). With no earlier entry there is nothing to clean up: each GET must not - // invalidate the pools and purge the CDN. - $this->resource->get('app://self/unstorable-value'); - $this->logger->flush(); - $this->resource->get('app://self/unstorable-value'); - $tree = $this->flushAndValidate($this->logger); + // reaches put(). With no earlier entry there is nothing to clean up: no GET may purge. + $purger = new FakeCountingPurger(); + $resource = $this->injectorWithPurger($purger)->getInstance(ResourceInterface::class); + + for ($i = 0; $i < 3; $i++) { + $resource->get('app://self/unstorable-value'); + } - $types = self::collectTypes($tree); - $this->assertContains('put_skipped', $types); - $this->assertNotContains('pre_write_cleanup', $types); - $this->assertNotContains('invalidate', $types); + $this->assertSame([], $purger->tags); } public function testUnstorablePutStillDropsAnEarlierEntry(): void { // An entry stored while the response was storable must not outlive a later write of // the same URI that turns out unstorable. + $purger = new FakeCountingPurger(); + $injector = $this->injectorWithPurger($purger); + $resource = $injector->getInstance(ResourceInterface::class); + $repository = $injector->getInstance(QueryRepositoryInterface::class); $uri = 'app://self/unstorable-value?unstorable=0'; - $ro = $this->resource->get($uri); - $this->assertNotNull($this->repository->get(new Uri($uri))); + $ro = $resource->get($uri); + $this->assertNotNull($repository->get(new Uri($uri))); + $purger->reset(); $ro->headers[Header::CACHE_CONTROL] = 'private'; - $this->assertFalse($this->repository->put($ro)); - $this->assertNull($this->repository->get(new Uri($uri))); + $this->assertFalse($repository->put($ro)); + $this->assertNull($repository->get(new Uri($uri))); + $this->assertNotSame([], $purger->tags); + } + + private function injectorWithPurger(PurgerInterface $purger): Injector + { + $module = new FakeEtagPoolModule(ModuleFactory::getInstance('FakeVendor\HelloWorld')); + $module->override(new class ($purger) extends AbstractModule { + public function __construct(private readonly PurgerInterface $purger) + { + } + + #[Override] + protected function configure(): void + { + $this->bind(PurgerInterface::class)->toInstance($this->purger); + } + }); + + return new Injector($module, __DIR__ . '/tmp'); } public function testCacheableWithNoHttpCacheStillStores(): void From bb2806712562d0acf882dae062df9987b8c049b2 Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Thu, 29 Oct 2026 01:18:09 +0000 Subject: [PATCH 4/4] Drop the comments the method and test names already say Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017CaQyRS7w44aWJTTCn8uys --- src/DonutRepository.php | 9 --------- src/QueryRepository.php | 12 +----------- tests/DonutRepositoryTest.php | 5 ----- tests/QueryRepositoryTest.php | 4 ---- 4 files changed, 1 insertion(+), 29 deletions(-) diff --git a/src/DonutRepository.php b/src/DonutRepository.php index f9e592cd..7964f68d 100644 --- a/src/DonutRepository.php +++ b/src/DonutRepository.php @@ -195,15 +195,6 @@ private function doPutDonut(ResourceObject $ro, int|null $donutTtl): void $this->resourceStorage->saveDonut($ro->uri, $donut, $donutTtl, $templateKeys); } - /** - * Drop what an earlier write left under this URI before it is written again - * - * An unstorable response is never written, so every GET of it misses and lands here: it is - * cleaned up only when an earlier write actually left a view or a template, or each of those - * GETs would invalidate both pools and purge the CDN for nothing. - * - * @param 'unstorable'|null $unstorable - */ private function cleanUpBeforeWrite(ResourceObject $ro, string|null $unstorable): void { if ($unstorable !== null && $this->resourceStorage->get($ro->uri) === null && $this->resourceStorage->getDonut($ro->uri) === null) { diff --git a/src/QueryRepository.php b/src/QueryRepository.php index 78318ab4..ffd4af23 100644 --- a/src/QueryRepository.php +++ b/src/QueryRepository.php @@ -69,8 +69,7 @@ public function put(ResourceObject $ro) private function doPut(ResourceObject $ro): bool { - // Checked against the still-unmutated header, before headerSetter runs the identical - // check - a dynamically set Cache-Control trips this; #[HttpCache]'s own value is exempt. + // Before headerSetter mutates the header $unstorable = $this->declaredCacheControl->reason($ro); $this->cleanUpBeforeWrite($ro, $unstorable); if ($ro->code === 200) { @@ -113,15 +112,6 @@ private function doPut(ResourceObject $ro): bool return $this->storage->saveValue($ro, $ttl); } - /** - * Drop what an earlier write left under this URI before it is written again - * - * An unstorable response is never written, so every GET of it misses and lands here: it is - * cleaned up only when an earlier write actually left an entry, or each of those GETs would - * invalidate both pools and purge the CDN for nothing. - * - * @param 'unstorable'|null $unstorable - */ private function cleanUpBeforeWrite(ResourceObject $ro, string|null $unstorable): void { if ($unstorable !== null && $this->storage->get($ro->uri) === null) { diff --git a/tests/DonutRepositoryTest.php b/tests/DonutRepositoryTest.php index ddb69a49..ae78e210 100644 --- a/tests/DonutRepositoryTest.php +++ b/tests/DonutRepositoryTest.php @@ -202,9 +202,6 @@ public function testDonutCacheTemplateSkipsSaveWhenThePageWritesPrivateIntoItsOw public function testRepeatedUnstorableDonutGetDoesNotPurge(): void { - // Nothing is stored for an unstorable page, so every GET of it is a miss that reaches - // the donut write. With no earlier view or template there is nothing to clean up: no - // GET may purge - for #[CacheableResponse] and #[DonutCache] alike. foreach (['page://self/html/unstorable-donut', 'page://self/html/unstorable-donut-cache-template'] as $uri) { $purger = new FakeCountingPurger(); $resource = $this->getInjector($purger)->getInstance(ResourceInterface::class); @@ -219,8 +216,6 @@ public function testRepeatedUnstorableDonutGetDoesNotPurge(): void public function testUnstorableDonutWriteStillDropsAnEarlierEntry(): void { - // A view and template stored while the page was storable must not outlive a later - // write of the same URI that turns out unstorable. $purger = new FakeCountingPurger(); $injector = $this->getInjector($purger); $resource = $injector->getInstance(ResourceInterface::class); diff --git a/tests/QueryRepositoryTest.php b/tests/QueryRepositoryTest.php index b6cefc3c..e8ed2322 100644 --- a/tests/QueryRepositoryTest.php +++ b/tests/QueryRepositoryTest.php @@ -314,8 +314,6 @@ public function testRuntimePrivateResponseSkipsStoreAndOrdinaryResponsesStillSto public function testRepeatedUnstorableGetDoesNotPurge(): void { - // Nothing is stored for an unstorable response, so every GET of it is a miss that - // reaches put(). With no earlier entry there is nothing to clean up: no GET may purge. $purger = new FakeCountingPurger(); $resource = $this->injectorWithPurger($purger)->getInstance(ResourceInterface::class); @@ -328,8 +326,6 @@ public function testRepeatedUnstorableGetDoesNotPurge(): void public function testUnstorablePutStillDropsAnEarlierEntry(): void { - // An entry stored while the response was storable must not outlive a later write of - // the same URI that turns out unstorable. $purger = new FakeCountingPurger(); $injector = $this->injectorWithPurger($purger); $resource = $injector->getInstance(ResourceInterface::class);