Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
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
platform-siblings.test.ts4.5 KB · 139 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
/**
 * Cross-product platform-status aggregator + admin widget smoke tests.
 *
 * The real siblings (crontech.ai, gluecron.com, gatetest.io) aren't
 * reachable in test, so we stub `globalThis.fetch` to exercise the three
 * branches: healthy JSON, non-2xx, and outright failure. Each branch must
 * resolve — a sibling being down never crashes the widget.
 */

import { describe, it, expect, afterEach, beforeEach } from "bun:test";
import app from "../app";
import {
  getSiblingStatuses,
  siblingUrls,
  __resetSiblingCache,
} from "../lib/platform-siblings";

const originalFetch = globalThis.fetch;

beforeEach(() => {
  __resetSiblingCache();
});

afterEach(() => {
  globalThis.fetch = originalFetch;
  __resetSiblingCache();
  delete process.env.CRONTECH_STATUS_URL;
  delete process.env.GLUECRON_STATUS_URL;
  delete process.env.GATETEST_STATUS_URL;
});

describe("platform-siblings — siblingUrls", () => {
  it("returns defaults pointing at production hosts", () => {
    const urls = siblingUrls();
    expect(urls.crontech).toBe("https://crontech.ai/api/platform-status");
    expect(urls.gluecron).toBe("https://gluecron.com/api/platform-status");
    expect(urls.gatetest).toBe("https://gatetest.io/api/platform-status");
  });

  it("honours env var overrides", () => {
    process.env.CRONTECH_STATUS_URL = "https://example.com/ct";
    process.env.GLUECRON_STATUS_URL = "https://example.com/gl";
    process.env.GATETEST_STATUS_URL = "https://example.com/gt";
    const urls = siblingUrls();
    expect(urls.crontech).toBe("https://example.com/ct");
    expect(urls.gluecron).toBe("https://example.com/gl");
    expect(urls.gatetest).toBe("https://example.com/gt");
  });
});

describe("platform-siblings — getSiblingStatuses", () => {
  it("reports healthy when sibling returns healthy JSON", async () => {
    globalThis.fetch = (async () =>
      new Response(
        JSON.stringify({
          product: "x",
          version: "1.2.3",
          commit: "abcdef1234567",
          healthy: true,
          timestamp: "2026-04-20T00:00:00Z",
        }),
        { status: 200, headers: { "content-type": "application/json" } }
      )) as unknown as typeof fetch;

    const rows = await getSiblingStatuses({ force: true });
    expect(rows).toHaveLength(3);
    for (const r of rows) {
      expect(r.reachable).toBe(true);
      expect(r.healthy).toBe(true);
      expect(r.version).toBe("1.2.3");
      expect(r.commit).toBe("abcdef1234567");
      expect(r.error).toBeNull();
      expect(typeof r.latencyMs).toBe("number");
    }
  });

  it("reports degraded (reachable but unhealthy) on non-2xx", async () => {
    globalThis.fetch = (async () =>
      new Response("nope", { status: 503 })) as unknown as typeof fetch;

    const rows = await getSiblingStatuses({ force: true });
    for (const r of rows) {
      expect(r.reachable).toBe(true);
      expect(r.healthy).toBe(false);
      expect(r.error).toBe("HTTP 503");
    }
  });

  it("reports unreachable on fetch error, never throws", async () => {
    globalThis.fetch = (async () => {
      throw new Error("ECONNREFUSED");
    }) as unknown as typeof fetch;

    const rows = await getSiblingStatuses({ force: true });
    expect(rows).toHaveLength(3);
    for (const r of rows) {
      expect(r.reachable).toBe(false);
      expect(r.healthy).toBe(false);
      expect(r.error).toBeTruthy();
    }
  });

  it("serves a cached result on the second call", async () => {
    let calls = 0;
    globalThis.fetch = (async () => {
      calls++;
      return new Response(
        JSON.stringify({ healthy: true, version: "v", commit: "c" }),
        { status: 200 }
      );
    }) as unknown as typeof fetch;

    await getSiblingStatuses({ force: true });
    expect(calls).toBe(3);
    await getSiblingStatuses();
    expect(calls).toBe(3);
  });
});

describe("platform-status endpoint", () => {
  it("GET /api/platform-status returns our own health JSON", async () => {
    const res = await app.request("/api/platform-status");
    expect(res.status).toBe(200);
    const body = (await res.json()) as any;
    expect(body.product).toBe("gluecron");
    expect(body.healthy).toBe(true);
    expect(typeof body.timestamp).toBe("string");
    expect(body.siblings).toBeDefined();
    expect(res.headers.get("access-control-allow-origin")).toBe("*");
  });
});

describe("admin platform widget — auth gate", () => {
  it("GET /admin/platform without auth → 302 /login", async () => {
    const res = await app.request("/admin/platform");
    expect(res.status).toBe(302);
    expect(res.headers.get("location") || "").toContain("/login");
  });
});