diff --git a/public/sw.js b/public/sw.js index 499698631..4095c17dd 100644 --- a/public/sw.js +++ b/public/sw.js @@ -36,7 +36,7 @@ try { // v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild. const CACHE_VERSION = (typeof self !== "undefined" && self.__APP_VERSION__) || - /* @sw-version-fallback */ "v1.34.0"; + /* @sw-version-fallback */ "v1.34.1"; const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`; const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`; // v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*` diff --git a/src/__tests__/service-worker-version-anchor.test.ts b/src/__tests__/service-worker-version-anchor.test.ts new file mode 100644 index 000000000..201204c29 --- /dev/null +++ b/src/__tests__/service-worker-version-anchor.test.ts @@ -0,0 +1,61 @@ +/** + * 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; + 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()}`); + }); +});