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