CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | /**
* Block S2 — service worker cache-bust pinned to deploy SHA.
*
* Covers the `/sw.js` handler extension that injects `const SW_VERSION`
* derived from `process.env.BUILD_SHA` (with a `dev-<pid>` fallback) and
* the cache-prefix invalidation logic that purges previous-version
* caches on activate.
*
* No mock pollution: this suite only manipulates `process.env.BUILD_SHA`
* via `beforeEach` / `afterEach` save+restore so the rest of the test
* run sees the original env. No DB stubs needed — the handler is pure.
*/
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
import app from "../app";
import {
buildSwVersion,
buildVersionedServiceWorker,
_resetSwShaWarningForTests,
} from "../routes/pwa";
describe("pwa cache-bust (S2) — GET /sw.js", () => {
let _savedSha: string | undefined;
beforeEach(() => {
_savedSha = process.env.BUILD_SHA;
delete process.env.BUILD_SHA;
_resetSwShaWarningForTests();
});
afterEach(() => {
if (_savedSha === undefined) delete process.env.BUILD_SHA;
else process.env.BUILD_SHA = _savedSha;
_resetSwShaWarningForTests();
});
it("returns 200 + Cache-Control: no-store", async () => {
const res = await app.request("/sw.js");
expect(res.status).toBe(200);
const cc = res.headers.get("cache-control") || "";
expect(cc).toContain("no-store");
});
it("response body contains a non-empty SW_VERSION literal", async () => {
const res = await app.request("/sw.js");
const body = await res.text();
const m = body.match(/const SW_VERSION = "([^"]+)"/);
expect(m).not.toBeNull();
expect((m as RegExpMatchArray)[1].length).toBeGreaterThan(0);
});
it("body calls self.skipWaiting() on install", async () => {
const res = await app.request("/sw.js");
const body = await res.text();
expect(body).toContain("self.skipWaiting()");
});
it("body calls clients.claim() on activate", async () => {
const res = await app.request("/sw.js");
const body = await res.text();
expect(body).toContain("clients.claim()");
});
it("body purges stale gluecron-* caches via caches.delete(...)", async () => {
const res = await app.request("/sw.js");
const body = await res.text();
expect(body).toContain("caches.delete");
expect(body).toContain("CACHE_PREFIX");
expect(body).toContain('"gluecron-"');
});
// 2026-05-16 — PWA rip-out. /sw.js now self-unregisters on activate so
// any browser with the old SW installed recovers automatically.
it("body unregisters itself on activate", async () => {
const res = await app.request("/sw.js");
const body = await res.text();
expect(body).toContain("self.registration.unregister");
});
it("when BUILD_SHA is set, that exact value appears as SW_VERSION", async () => {
process.env.BUILD_SHA = "abc123deadbeef";
const res = await app.request("/sw.js");
const body = await res.text();
expect(body).toContain('const SW_VERSION = "abc123deadbeef"');
});
it("when BUILD_SHA is unset, falls back to a non-empty dev-mode string", () => {
delete process.env.BUILD_SHA;
const v = buildSwVersion();
expect(typeof v).toBe("string");
expect(v.length).toBeGreaterThan(0);
expect(v.startsWith("dev-")).toBe(true);
});
it("buildVersionedServiceWorker pins CURRENT_CACHE to SW_VERSION", () => {
const src = buildVersionedServiceWorker("v9");
expect(src).toContain('const SW_VERSION = "v9"');
expect(src).toContain("const CURRENT_CACHE = CACHE_PREFIX + SW_VERSION");
});
it("buildVersionedServiceWorker escapes quotes + backslashes safely", () => {
const src = buildVersionedServiceWorker('weird"version\\stuff');
// The literal in the output must round-trip via the JS string parser —
// i.e. the embedded quote is backslash-escaped, no raw close on the
// string would prematurely terminate the version constant.
expect(src).toContain('const SW_VERSION = "weird\\"version\\\\stuff"');
});
it("content-type stays application/javascript", async () => {
const res = await app.request("/sw.js");
expect(res.headers.get("content-type") || "").toContain(
"application/javascript"
);
});
it("service-worker-allowed header is /", async () => {
const res = await app.request("/sw.js");
expect(res.headers.get("service-worker-allowed")).toBe("/");
});
});
describe("pwa cache-bust (S2) — /version alias", () => {
let _savedSha: string | undefined;
let _savedTime: string | undefined;
beforeEach(() => {
_savedSha = process.env.BUILD_SHA;
_savedTime = process.env.BUILD_TIME;
});
afterEach(() => {
if (_savedSha === undefined) delete process.env.BUILD_SHA;
else process.env.BUILD_SHA = _savedSha;
if (_savedTime === undefined) delete process.env.BUILD_TIME;
else process.env.BUILD_TIME = _savedTime;
});
it("returns {sha, buildAt} with BUILD_SHA + BUILD_TIME echoed", async () => {
process.env.BUILD_SHA = "deadbeef1234";
process.env.BUILD_TIME = "2026-05-14T00:00:00Z";
const res = await app.request("/version");
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, unknown>;
expect(body.sha).toBe("deadbeef1234");
expect(body.buildAt).toBe("2026-05-14T00:00:00Z");
});
it("falls back to {sha: 'dev', buildAt: null} when env unset", async () => {
delete process.env.BUILD_SHA;
delete process.env.BUILD_TIME;
const res = await app.request("/version");
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, unknown>;
expect(body.sha).toBe("dev");
expect(body.buildAt).toBeNull();
});
});
|