Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
claude/adoring-hopper-5x74bqclaude/affectionate-feynman-ykrf1hclaude/architecture-audit-design-wxprenclaude/build-status-update-3MXsfclaude/charming-meitner-mllb5rclaude/compare-gate-gluecron-s4mFQclaude/confident-faraday-tikcwbclaude/continue-work-XMTlIclaude/crontech-gluecron-deploy-7MIECclaude/crontech-platform-setup-SeKfwclaude/design-2026claude/ecstatic-ptolemy-jMdigclaude/enhance-github-integration-QNHdGclaude/fix-aa-loop-issue-PonMQclaude/fix-actions-and-processclaude/fix-desktop-errors-XqoW8claude/fix-red-workflowsclaude/fix-website-access-6FKJNclaude/gatetest-integration-hardeningclaude/github-audit-improvements-bDFr9claude/gluecron-launch-status-FoMRlclaude/hopeful-lamport-olfCTclaude/issue-to-pr-and-protectionsclaude/jolly-heisenberg-2sg1Qclaude/launch-preparation-QmTb6claude/new-session-xk1l7claude/plan-platform-architecture-kkN4yclaude/platform-analysis-roadmap-1nUGLclaude/platform-launch-assessment-8dWV8claude/polish-platform-release-AeDrUclaude/resume-previous-work-KzyLwclaude/review-crontech-handoff-qYEVqclaude/review-project-completeness-lHhS2claude/review-readme-docs-ulqPKclaude/serene-edison-rj87weclaude/setup-multi-repo-dev-BCwNQclaude/ship-fixes-and-tests-Jvz1cclaude/site-audit-competitive-pctlwgclaude/site-migration-vercel-XstpKclaude/standalone-product-repos-XHFTDcopilot/feat-smart-empty-states-keyboard-first-enhancementcopilot/feat-smart-morning-digest-review-context-restorecopilot/fix-and-process-workflowscopilot/update-ai-powered-code-reviewfeat/debt-mapfeat/push-policy-codeowners-hardeningfeat/smart-digest-contextfeat/stage-impactfeat/t1-secret-migrationfeat/u-polishfeat/w-self-hostfeat/w2-claude-configfix/agent-journey-orphan-sweepgatetest/auto-fix-1776586424172gatetest/auto-fix-1776586534814gatetest/auto-fix-1776590685143gatetest/auto-fix-1776590808199mainops/redeploy-retriggerstyle/dxt-cta-themeworktree-agent-a3377aad30d55da26worktree-agent-a7ef607b7ee1d6c74
pwa-cache-bust.test.ts5.2 KB · 149 lines
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
/**
 * 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-"');
  });

  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();
  });
});