#!/usr/bin/env node /** * ObsiGate - Service Worker caching policy guards. * * Regression tests for the mobile/Cloudflare stale-load bug: the SW used * cache-first with a fixed cache name and wrong precache paths, so old JS kept * being served even after flushing the browser/Cloudflare caches (the SW Cache * Storage is a separate store). These tests read the source and assert the * network-first policy, versioned caches and corrected paths stay in place. * * Usage: node tests/frontend/sw.test.mjs */ import { strict as assert } from "node:assert"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import path from "node:path"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const FRONTEND = path.resolve(__dirname, "..", "..", "frontend"); const sw = readFileSync(path.join(FRONTEND, "sw.js"), "utf-8"); const indexHtml = readFileSync(path.join(FRONTEND, "index.html"), "utf-8"); const sync = readFileSync(path.join(FRONTEND, "js", "sync.js"), "utf-8"); let testCount = 0; let passCount = 0; function test(name, fn) { testCount++; try { fn(); console.log(` \u2713 ${name}`); passCount++; } catch (e) { console.log(` \u2717 ${name}`); console.log(` ${e.message}`); } } // ── Service worker ───────────────────────────────────────────────────────── test("service worker uses a versioned cache name", () => { assert.ok(/const\s+SW_VERSION\s*=/.test(sw), "SW_VERSION constant present"); assert.ok(sw.includes("obsigate-code-${SW_VERSION}"), "code cache is versioned"); assert.ok(sw.includes("obsigate-runtime-${SW_VERSION}"), "runtime cache is versioned"); assert.ok(sw.includes("obsigate-api-${SW_VERSION}"), "api cache is versioned"); }); test("legacy fixed cache name is gone", () => { assert.ok(!/obsigate-v1['"]/.test(sw), "must not keep the fixed obsigate-v1 cache name"); }); test("code/navigation requests are network-first", () => { assert.ok(/function\s+networkFirst\s*\(/.test(sw), "networkFirst strategy present"); assert.ok(/isCodeRequest\s*\(/.test(sw), "code-request classifier present"); // The fetch handler must route code requests through networkFirst. assert.ok(/isCodeRequest\([\s\S]*?networkFirst\(/.test(sw), "code requests use networkFirst"); assert.ok(/request\.mode\s*===\s*['"]navigate['"]/.test(sw), "navigations are treated as code"); }); test("precache uses real /static paths", () => { assert.ok(sw.includes("'/static/js/app.js'"), "app.js precached at /static/js"); assert.ok(sw.includes("'/static/style.css'"), "style.css precached at /static"); assert.ok(!sw.includes("/frontend/js/"), "no stale /frontend/js path in precache"); }); test("precache failures are non-fatal", () => { assert.ok(/PRECACHE_URLS\.map[\s\S]*?\.catch\(/.test(sw), "per-URL catch so a 404 does not abort install"); }); test("worker activates immediately and claims clients", () => { assert.ok(sw.includes("skipWaiting()"), "skipWaiting present"); assert.ok(sw.includes("clients.claim()"), "clients.claim present"); }); // ── index.html migration ─────────────────────────────────────────────────── test("index.html uses a one-time keyed migration, not a session kill-switch", () => { assert.ok(indexHtml.includes("obsigate-sw-migration"), "keyed migration guard present"); assert.ok(!indexHtml.includes("_sw_killed"), "legacy session kill-switch removed"); assert.ok(!/getRegistrations\(\)[\s\S]{0,200}unregister/.test(indexHtml), "does not unregister on every session"); }); // ── sync.js registration ─────────────────────────────────────────────────── test("sync.js reloads once when a new worker takes control", () => { assert.ok(sync.includes("controllerchange"), "controllerchange listener present"); assert.ok(sync.includes("SKIP_WAITING"), "asks the waiting worker to activate"); }); console.log(`\n${passCount}/${testCount} service-worker tests passed`); if (passCount !== testCount) { process.exit(1); }