Files
HealthLog/src/__tests__/service-worker-version-anchor.test.ts
T
Marc-André Bombeck 17ea7bea2d fix(sw): re-anchor the fallback cache version and gate it against the release
The service worker derives its cache names from the version injected by
`sw-version.js`, and falls back to a literal compiled into `sw.js` when that
import fails. The prebuild step rewrites both from `package.json`, so both were
correct as long as a release rebuilt. One did not, and the literal trailed the
release by a version while every test stayed green.

Nothing checked it. The generated file is compared against `package.json`, and
the behavioural suite deliberately reads the literal out of the source so its
cache-name assertions cannot drift from the literal they exercise. Reasonable
apart, and together they left the literal itself unverified.

The effect is confined to the degraded path and is still worth closing: a client
that cannot load `sw-version.js` computes the previous release's cache names, so
it keeps serving the old shell and its cleanup treats the current caches as the
stale ones.

Re-anchors the literal and adds the equality as a gate, since it is a question
with an exact answer. A version bump committed without a rebuild now fails
locally instead of shipping.
2026-07-30 11:55:16 +02:00

62 lines
2.5 KiB
TypeScript

/**
* The service worker's fallback version must equal the release version.
*
* `public/sw.js` derives its cache names from `self.__APP_VERSION__`, injected by
* `public/sw-version.js`, which `scripts/generate-sw-version.mjs` writes from
* `package.json` on every prebuild. When `importScripts('/sw-version.js')`
* fails, the SW falls back to a literal compiled into `sw.js` behind the
* `@sw-version-fallback` anchor, and the same prebuild step rewrites that
* literal too.
*
* Two places therefore declare one version, and until this file existed nothing
* made them agree. `scripts/__tests__/generate-sw-version.test.ts` checks the
* GENERATED file against `package.json`; the behavioural suite in
* `service-worker.test.ts` reads the fallback literal out of the source so its
* cache-name assertions "never drift from the literal it is exercising". Both
* are reasonable in isolation and together they leave the literal unchecked: a
* release whose version was bumped without a rebuild ships a fallback pointing
* at the PREVIOUS release, and every test stays green.
*
* The consequence is small but real, and it only bites on the degraded path. A
* client that cannot load `/sw-version.js` computes last release's cache names,
* so it keeps serving the previous shell and its cleanup step treats the current
* caches as the stale ones.
*
* This is a one-line equality, which makes it a gate rather than a review aid.
* Failing it means the tree needs a rebuild before it is committed, which is
* exactly the step whose absence caused the drift.
*/
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { join } from "node:path";
const ROOT = process.cwd();
function swFallbackVersion(): string {
const source = readFileSync(join(ROOT, "public/sw.js"), "utf8");
const match = /\/\* @sw-version-fallback \*\/\s*"(v[^"]*)"/.exec(source);
if (!match) {
throw new Error(
"no @sw-version-fallback anchor in public/sw.js — the prebuild generator keys on it",
);
}
return match[1];
}
function packageVersion(): string {
const pkg = JSON.parse(
readFileSync(join(ROOT, "package.json"), "utf8"),
) as Record<string, unknown>;
const version = pkg.version;
if (typeof version !== "string") {
throw new Error("package.json carries no string version");
}
return version;
}
describe("the service-worker fallback version is anchored to the release", () => {
it("matches package.json", () => {
expect(swFallbackVersion()).toBe(`v${packageVersion()}`);
});
});