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
dxt-extension.test.ts6.3 KB · 169 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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
/**
 * BLOCK Q1 — Claude Desktop (.dxt) extension tests.
 *
 * Covers:
 *   - extension/gluecron.dxt/manifest.json is valid JSON
 *   - Manifest declares all 15 tools (cross-checked against
 *     src/lib/mcp-tools.ts's defaultTools() exports)
 *   - server.endpoint contains the templated host placeholder
 *   - GET /gluecron.dxt returns 200 + correct Content-Type when the
 *     pre-built bundle exists in public/
 *   - GET /gluecron.dxt returns 404 with friendly JSON when missing
 *   - Landing page renders the "Add to Claude Desktop" CTA
 *
 * Static-content surface — no DB stubs, no mocks needed. The 404 case is
 * exercised by temporarily renaming the bundle out of the way and
 * restoring it before the next test.
 */

import { describe, it, expect, beforeAll, afterAll } from "bun:test";
import { readFileSync, existsSync, renameSync } from "node:fs";
import { join } from "node:path";
import app from "../app";
import { defaultTools } from "../lib/mcp-tools";

const ROOT = join(import.meta.dir, "..", "..");
const MANIFEST_PATH = join(ROOT, "extension", "gluecron.dxt", "manifest.json");
const BUNDLE_PATH = join(ROOT, "public", "gluecron.dxt");

type Manifest = {
  dxt_version: string;
  name: string;
  display_name: string;
  version: string;
  server: {
    type: string;
    endpoint: string;
    headers?: Record<string, string>;
  };
  user_config: Record<string, { required?: boolean; sensitive?: boolean }>;
  tools: Array<{ name: string; description: string }>;
};

function loadManifest(): Manifest {
  const raw = readFileSync(MANIFEST_PATH, "utf8");
  return JSON.parse(raw) as Manifest;
}

describe("Block Q1 — .dxt manifest", () => {
  it("extension/gluecron.dxt/manifest.json is valid JSON", () => {
    expect(() => loadManifest()).not.toThrow();
    const m = loadManifest();
    expect(m.name).toBe("gluecron");
    expect(m.display_name).toBe("Gluecron");
  });

  it("declares server.type=http with the templated host placeholder", () => {
    const m = loadManifest();
    expect(m.server.type).toBe("http");
    expect(m.server.endpoint).toContain("${user_config.gluecron_host}/mcp");
    expect(m.server.headers?.Authorization).toContain(
      "${user_config.gluecron_pat}"
    );
  });

  it("declares both user_config prompts (host + PAT, PAT marked sensitive)", () => {
    const m = loadManifest();
    expect(m.user_config.gluecron_host).toBeDefined();
    expect(m.user_config.gluecron_host.required).toBe(true);
    expect(m.user_config.gluecron_pat).toBeDefined();
    expect(m.user_config.gluecron_pat.required).toBe(true);
    expect(m.user_config.gluecron_pat.sensitive).toBe(true);
  });

  it("does not embed any sensitive default values", () => {
    const raw = readFileSync(MANIFEST_PATH, "utf8");
    // PAT prefixes — these must never be hard-coded into the manifest.
    expect(raw).not.toMatch(/glc_[A-Za-z0-9_-]+/);
    expect(raw).not.toMatch(/glct_[A-Za-z0-9_-]+/);
  });

  it("declares all 15 MCP tools, cross-checked against defaultTools()", () => {
    const m = loadManifest();
    const manifestNames = new Set(m.tools.map((t) => t.name));
    const handlerNames = new Set(Object.keys(defaultTools()));

    // Every handler MUST appear in the manifest.
    for (const name of handlerNames) {
      expect(manifestNames.has(name)).toBe(true);
    }
    // ... and vice versa — no orphan declarations.
    for (const name of manifestNames) {
      expect(handlerNames.has(name)).toBe(true);
    }
    // Lock the count at 15 so a future tool addition forces this test
    // (and therefore the manifest) to be updated in lockstep.
    expect(manifestNames.size).toBe(15);
    expect(handlerNames.size).toBe(15);
  });
});

describe("Block Q1 — GET /gluecron.dxt", () => {
  // The build script writes public/gluecron.dxt. If a prior test (or a
  // local dev session) has already produced it we use that; otherwise we
  // build it on the fly via the same script so the route can be exercised.
  beforeAll(async () => {
    if (!existsSync(BUNDLE_PATH)) {
      const proc = Bun.spawn(["bash", join(ROOT, "scripts", "build-dxt.sh")], {
        cwd: ROOT,
        stdout: "pipe",
        stderr: "pipe",
      });
      await proc.exited;
    }
  });

  it("returns 200 + octet-stream + Content-Disposition when bundle exists", async () => {
    expect(existsSync(BUNDLE_PATH)).toBe(true);
    const res = await app.request("/gluecron.dxt");
    expect(res.status).toBe(200);
    expect(res.headers.get("content-type")).toContain("application/octet-stream");
    expect(res.headers.get("content-disposition") || "").toContain(
      'filename="gluecron.dxt"'
    );
    // Cache-Control must be set so CDNs can cache the download.
    expect(res.headers.get("cache-control") || "").toContain("max-age=3600");
    // Body must be a non-trivial ZIP — first 2 bytes are the PK signature.
    const buf = new Uint8Array(await res.arrayBuffer());
    expect(buf[0]).toBe(0x50); // 'P'
    expect(buf[1]).toBe(0x4b); // 'K'
  });

  it("returns 404 JSON with a friendly message when the bundle is missing", async () => {
    // Move the file out of the way, hit the route, restore it.
    const stash = `${BUNDLE_PATH}.test-stash`;
    let movedAway = false;
    try {
      if (existsSync(BUNDLE_PATH)) {
        renameSync(BUNDLE_PATH, stash);
        movedAway = true;
      }
      const res = await app.request("/gluecron.dxt");
      expect(res.status).toBe(404);
      expect(res.headers.get("content-type") || "").toContain("application/json");
      const body = (await res.json()) as {
        error: string;
        message: string;
        fallback: string;
      };
      expect(body.error).toBe("extension_not_built");
      expect(body.message).toContain("scripts/build-dxt.sh");
      expect(body.fallback).toContain("/install");
    } finally {
      if (movedAway && existsSync(stash)) renameSync(stash, BUNDLE_PATH);
    }
  });
});

describe("Block Q1 — landing CTA", () => {
  it("GET / renders the 'Add to Claude Desktop' CTA pointing at /gluecron.dxt", async () => {
    const res = await app.request("/");
    expect(res.status).toBe(200);
    const body = await res.text();
    expect(body).toContain("Add to Claude Desktop");
    expect(body).toContain('href="/gluecron.dxt"');
    // The CTA carries the test hook + the download attribute so browsers
    // know to save rather than navigate.
    expect(body).toContain('data-testid="cta-dxt"');
  });
});