test(02-12.1): prove Vary on the serving path, not only the contents

Removing the Vary block left the disk-layer case green: it asserted what
is IN the cache, and the claim is what the cache will SERVE. On the
external-switch path nothing wipes those entries — the app never
initiated the switch — so Vary is the only thing between a cached
delegated response and the next request under another context.

- assert cache.match() hits under the stored context and misses under
  the new one; it fails when the Vary is removed
- correct the acceptance map and the client docblock to claim exactly
  what executes: no zero-entry count, no offline leg
This commit is contained in:
Marc-André Bombeck committed 2026-08-06 23:04:29 +02:00
1 parent 6e5b420e4e
commit 120323d995
3 files changed
+30 -16

No files matched your search

@@ -95,11 +95,11 @@ const CASES: AcceptanceCase[] = [
},
{
id: "FENCE-AC-07",
case: "IndexedDB and CacheStorage seeded with an owner snapshot, switch in, go offline",
case: "CacheStorage seeded with one record's responses; the disk layer cannot serve them under another",
home: "e2e/v137-record-session-fence-offline.spec.ts",
runner: "playwright",
instrument:
"Positive control first — the owner snapshot really was in CacheStorage before the switch, asserted by a non-zero entry count. Then the eviction claim of the cache-version policy, then a zero entry count after the switch, then offline with the target record still named. It lives in its own spec and its own Playwright project because every other project sets serviceWorkers: 'block', under which the data cache is never populated and the positive control could not pass whatever the application did.",
"Two tests, contents and serving, because the first alone is not the claim. CONTENTS: a non-zero seeded entry count as the positive control, the cache-version eviction, then every stored response carrying the target's scope and none the previous record's. SERVING: after an EXTERNAL switch — the path on which the app never runs its cache wipe, so the previous record's entries survive — cache.match() under the stored context hits (positive control) and under the new context misses. Removing the Vary block makes the serving test fail and leaves the contents test green, which is why both exist. NOT asserted: a zero entry count after a switch (the wipe is real but the reload refills it within milliseconds), and no offline leg — a bare page-context fetch cannot match a Vary-keyed entry, which is itself the property the serving test proves. Its own spec and its own Playwright project because every other project sets serviceWorkers: 'block', under which the data cache is never populated and the positive control could not pass whatever the application did.",
},
{
id: "FENCE-AC-08",
+17 -7
View File
@@ -38,13 +38,23 @@
* `/me` on every poll cycle, forever. Do not "tighten" this rule.
*
* The case mismatch-only appears to give up — a service-worker disk-cache hit
* that predates the fence and therefore carries no echo — is closed elsewhere:
* `public/sw.js` names its caches by the app release and its `activate` handler
* deletes every `healthlog-(static|pages|data)-*` cache not in the current set,
* so the deploy that ships the fence evicts every pre-fence cached response
* before one can be served. A POST-fence cached record response does carry the
* echo it was cached with, so replaying it after a switch is a mismatch and is
* discarded.
* that predates the fence and therefore carries no echo — is closed by two
* other things, and it is worth being exact about which one does what:
*
* * `public/sw.js` names its caches by the app release and its `activate`
* handler deletes every `healthlog-(static|pages|data)-*` cache not in the
* current set, so the deploy that ships the fence evicts every pre-fence
* cached response before one can be served. Executed in
* `e2e/v137-record-session-fence-offline.spec.ts`;
* * a POST-fence cached record response is stored under a `Vary` on the two
* fence headers (`attachRecordContextEcho`), so `cache.match()` will not
* hand it to a request asserting a different context — or none. That is
* what covers the EXTERNAL switch, where this browser never ran its own
* cache wipe and the previous record's entries are still on disk. Also
* executed in that spec, and it fails when the `Vary` is removed.
*
* The client-side discard below is the third layer, over responses that reach
* it. It is not what protects the disk.
*/
import {
+11 -7
View File
@@ -105,13 +105,17 @@ export function attachRecordContextEcho(
headers.set(RECORD_SCOPE_HEADER, recordScopeHeaderValue(context.scope));
// And declare that the answer DEPENDS on those two request headers.
//
// Nothing between the browser and this app caches today — the responses that
// reach here are private and same-origin. But a shared cache is one reverse
// proxy away, and without `Vary` the first one to appear would key a
// delegated record response on URL alone and hand it to the next request for
// the same URL under a different context. That is one person's health record
// served to another, produced by an infrastructure change nobody thought was
// a code change.
// Not a precaution against some future reverse proxy — it is load-bearing on
// a path that exists today. When a switch is driven EXTERNALLY (another tab,
// an account deletion, an operator), this browser never ran
// `clearOfflineCachesForRecordSwitch`, so the service worker's
// `healthlog-data-*` entries from the previous record are still on disk.
// `Vary` is the only thing that stops `cache.match()` handing one of them to
// the next request for the same URL under a different context — one person's
// health record served to another, out of the disk layer, with no request
// reaching the fence at all. Proved in
// `e2e/v137-record-session-fence-offline.spec.ts`, which fails the moment
// this block is removed.
//
// Appended rather than assigned: a route that already varies on something
// (`Accept-Encoding`, a locale header) must keep it. Case-insensitive