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
Blame · Line-by-line history

dxt-extension.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.

dxt-extension.test.tsBlame170 lines · 2 contributors
cd4f63bTest User1/**
2 * BLOCK Q1 — Claude Desktop (.dxt) extension tests.
3 *
4 * Covers:
5 * - extension/gluecron.dxt/manifest.json is valid JSON
6 * - Manifest declares all 15 tools (cross-checked against
7 * src/lib/mcp-tools.ts's defaultTools() exports)
8 * - server.endpoint contains the templated host placeholder
9 * - GET /gluecron.dxt returns 200 + correct Content-Type when the
10 * pre-built bundle exists in public/
11 * - GET /gluecron.dxt returns 404 with friendly JSON when missing
12 * - Landing page renders the "Add to Claude Desktop" CTA
13 *
14 * Static-content surface — no DB stubs, no mocks needed. The 404 case is
15 * exercised by temporarily renaming the bundle out of the way and
16 * restoring it before the next test.
17 */
18
19import { describe, it, expect, beforeAll, afterAll } from "bun:test";
20import { readFileSync, existsSync, renameSync } from "node:fs";
21import { join } from "node:path";
22import app from "../app";
23import { defaultTools } from "../lib/mcp-tools";
24
25const ROOT = join(import.meta.dir, "..", "..");
26const MANIFEST_PATH = join(ROOT, "extension", "gluecron.dxt", "manifest.json");
27const BUNDLE_PATH = join(ROOT, "public", "gluecron.dxt");
28
29type Manifest = {
30 dxt_version: string;
31 name: string;
32 display_name: string;
33 version: string;
34 server: {
35 type: string;
36 endpoint: string;
37 headers?: Record<string, string>;
38 };
39 user_config: Record<string, { required?: boolean; sensitive?: boolean }>;
40 tools: Array<{ name: string; description: string }>;
41};
42
43function loadManifest(): Manifest {
44 const raw = readFileSync(MANIFEST_PATH, "utf8");
45 return JSON.parse(raw) as Manifest;
46}
47
48describe("Block Q1 — .dxt manifest", () => {
49 it("extension/gluecron.dxt/manifest.json is valid JSON", () => {
50 expect(() => loadManifest()).not.toThrow();
51 const m = loadManifest();
52 expect(m.name).toBe("gluecron");
53 expect(m.display_name).toBe("Gluecron");
54 });
55
56 it("declares server.type=http with the templated host placeholder", () => {
57 const m = loadManifest();
58 expect(m.server.type).toBe("http");
59 expect(m.server.endpoint).toContain("${user_config.gluecron_host}/mcp");
60 expect(m.server.headers?.Authorization).toContain(
61 "${user_config.gluecron_pat}"
62 );
63 });
64
65 it("declares both user_config prompts (host + PAT, PAT marked sensitive)", () => {
66 const m = loadManifest();
67 expect(m.user_config.gluecron_host).toBeDefined();
68 expect(m.user_config.gluecron_host.required).toBe(true);
69 expect(m.user_config.gluecron_pat).toBeDefined();
70 expect(m.user_config.gluecron_pat.required).toBe(true);
71 expect(m.user_config.gluecron_pat.sensitive).toBe(true);
72 });
73
74 it("does not embed any sensitive default values", () => {
75 const raw = readFileSync(MANIFEST_PATH, "utf8");
76 // PAT prefixes — these must never be hard-coded into the manifest.
77 expect(raw).not.toMatch(/glc_[A-Za-z0-9_-]+/);
78 expect(raw).not.toMatch(/glct_[A-Za-z0-9_-]+/);
79 });
80
0feb720Claude81 it("declares every MCP tool, cross-checked against defaultTools()", () => {
cd4f63bTest User82 const m = loadManifest();
83 const manifestNames = new Set(m.tools.map((t) => t.name));
84 const handlerNames = new Set(Object.keys(defaultTools()));
85
86 // Every handler MUST appear in the manifest.
87 for (const name of handlerNames) {
88 expect(manifestNames.has(name)).toBe(true);
89 }
90 // ... and vice versa — no orphan declarations.
91 for (const name of manifestNames) {
92 expect(handlerNames.has(name)).toBe(true);
93 }
0feb720Claude94 // Floor at 50 to guarantee the expanded surface is exposed; the
95 // exact count travels with `defaultTools()` so adding a tool there
96 // and forgetting the manifest still trips the per-name assertions
97 // above.
98 expect(manifestNames.size).toBe(handlerNames.size);
99 expect(manifestNames.size).toBeGreaterThanOrEqual(50);
cd4f63bTest User100 });
101});
102
103describe("Block Q1 — GET /gluecron.dxt", () => {
104 // The build script writes public/gluecron.dxt. If a prior test (or a
105 // local dev session) has already produced it we use that; otherwise we
106 // build it on the fly via the same script so the route can be exercised.
107 beforeAll(async () => {
108 if (!existsSync(BUNDLE_PATH)) {
109 const proc = Bun.spawn(["bash", join(ROOT, "scripts", "build-dxt.sh")], {
110 cwd: ROOT,
111 stdout: "pipe",
112 stderr: "pipe",
113 });
114 await proc.exited;
115 }
116 });
117
118 it("returns 200 + octet-stream + Content-Disposition when bundle exists", async () => {
119 expect(existsSync(BUNDLE_PATH)).toBe(true);
120 const res = await app.request("/gluecron.dxt");
121 expect(res.status).toBe(200);
122 expect(res.headers.get("content-type")).toContain("application/octet-stream");
123 expect(res.headers.get("content-disposition") || "").toContain(
124 'filename="gluecron.dxt"'
125 );
126 // Cache-Control must be set so CDNs can cache the download.
127 expect(res.headers.get("cache-control") || "").toContain("max-age=3600");
128 // Body must be a non-trivial ZIP — first 2 bytes are the PK signature.
129 const buf = new Uint8Array(await res.arrayBuffer());
130 expect(buf[0]).toBe(0x50); // 'P'
131 expect(buf[1]).toBe(0x4b); // 'K'
132 });
133
134 it("returns 404 JSON with a friendly message when the bundle is missing", async () => {
135 // Move the file out of the way, hit the route, restore it.
136 const stash = `${BUNDLE_PATH}.test-stash`;
137 let movedAway = false;
138 try {
139 if (existsSync(BUNDLE_PATH)) {
140 renameSync(BUNDLE_PATH, stash);
141 movedAway = true;
142 }
143 const res = await app.request("/gluecron.dxt");
144 expect(res.status).toBe(404);
145 expect(res.headers.get("content-type") || "").toContain("application/json");
146 const body = (await res.json()) as {
147 error: string;
148 message: string;
149 fallback: string;
150 };
151 expect(body.error).toBe("extension_not_built");
152 expect(body.message).toContain("scripts/build-dxt.sh");
153 expect(body.fallback).toContain("/install");
154 } finally {
155 if (movedAway && existsSync(stash)) renameSync(stash, BUNDLE_PATH);
156 }
157 });
158});
159
74a8784Claude160describe("Block Q1 — .dxt discoverability", () => {
161 // 2026-06-10: the landing-page CTA was demoted in the 2030 landing
162 // reboot (GET / now serves the self-contained Landing2030Page). The
163 // bundle stays discoverable from the docs surface.
164 it("GET /docs/mcp-server links to /gluecron.dxt", async () => {
165 const res = await app.request("/docs/mcp-server");
cd4f63bTest User166 expect(res.status).toBe(200);
167 const body = await res.text();
168 expect(body).toContain('href="/gluecron.dxt"');
169 });
170});