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

demo-page.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.

demo-page.test.tsBlame177 lines · 1 contributor
52ad8b1Claude1/**
2 * Block L3 — public /demo page + JSON endpoint smoke tests.
3 *
4 * Exercises route status codes + content-type + cache headers, plus the
5 * pure-helper behaviour of the activity module (returns `[]` on DB
6 * unavailability) and idempotency of `ensureDemoActivity()`.
7 *
8 * No DB writes from the test — when the test process has no usable
9 * `DATABASE_URL`, the helpers fail-soft and we assert that they still
10 * return shaped data rather than crashing.
11 */
12
13import { describe, it, expect } from "bun:test";
14import app from "../app";
15import {
16 listRecentAutoMerges,
17 listRecentAiReviews,
18 listQueuedAiBuildIssues,
19 listDemoActivityFeed,
20 countAiReviewsSince,
21 __test as activityTest,
22} from "../lib/demo-activity";
23import { ensureDemoActivity } from "../lib/demo-activity-seed";
24
25describe("GET /demo (Block L3 landing page)", () => {
26 it("returns 200 HTML", async () => {
27 const res = await app.request("/demo");
28 expect(res.status).toBe(200);
29 expect(res.headers.get("content-type") || "").toContain("text/html");
30 });
31
32 it("body mentions 'demo' (page is self-describing)", async () => {
33 const res = await app.request("/demo");
34 const body = await res.text();
35 // Lower-cased substring check — the page mentions the demo repos,
36 // the demo user, and the word "demo" multiple times.
37 expect(body.toLowerCase()).toContain("demo");
38 });
39
40 it("body contains all three tile headings", async () => {
41 const res = await app.request("/demo");
42 const body = await res.text();
43 expect(body).toContain("Issues queued for AI build");
44 expect(body).toContain("PRs auto-merged in the last 24h");
45 expect(body).toContain("AI reviews posted today");
46 });
47
48 it("body contains the sign-up CTA", async () => {
49 const res = await app.request("/demo");
50 const body = await res.text();
51 expect(body).toContain("Sign up free");
52 expect(body).toContain('href="/register"');
53 });
54
55 it("body contains the live activity feed section", async () => {
56 const res = await app.request("/demo");
57 const body = await res.text();
58 expect(body).toContain("Live activity");
59 expect(body).toContain('id="demo-feed-list"');
60 });
61});
62
63describe("GET /api/v2/demo/activity", () => {
64 it("returns 200 JSON with Cache-Control: public, max-age=30", async () => {
65 const res = await app.request("/api/v2/demo/activity");
66 expect(res.status).toBe(200);
67 const ct = res.headers.get("content-type") || "";
68 expect(ct).toContain("application/json");
69 const cc = res.headers.get("cache-control") || "";
70 expect(cc).toContain("public");
71 expect(cc).toContain("max-age=30");
72 const body = await res.json();
73 expect(body).toHaveProperty("entries");
74 expect(Array.isArray(body.entries)).toBe(true);
75 });
76});
77
78describe("GET /api/v2/demo/queued", () => {
79 it("returns 200 JSON with items array", async () => {
80 const res = await app.request("/api/v2/demo/queued");
81 expect(res.status).toBe(200);
82 const cc = res.headers.get("cache-control") || "";
83 expect(cc).toContain("max-age=30");
84 const body = await res.json();
85 expect(body).toHaveProperty("items");
86 expect(Array.isArray(body.items)).toBe(true);
87 });
88});
89
90describe("GET /api/v2/demo/merges", () => {
91 it("returns 200 JSON with items array", async () => {
92 const res = await app.request("/api/v2/demo/merges");
93 expect(res.status).toBe(200);
94 const body = await res.json();
95 expect(body).toHaveProperty("items");
96 expect(Array.isArray(body.items)).toBe(true);
97 });
98});
99
100describe("GET /api/v2/demo/reviews", () => {
101 it("returns 200 JSON with count + items", async () => {
102 const res = await app.request("/api/v2/demo/reviews");
103 expect(res.status).toBe(200);
104 const body = await res.json();
105 expect(body).toHaveProperty("count");
106 expect(typeof body.count).toBe("number");
107 expect(body).toHaveProperty("items");
108 expect(Array.isArray(body.items)).toBe(true);
109 });
110});
111
112describe("demo-activity helpers — graceful on DB error", () => {
113 it("listRecentAutoMerges returns an array", async () => {
114 activityTest.demoActivityCache.clear();
115 const r = await listRecentAutoMerges();
116 expect(Array.isArray(r)).toBe(true);
117 });
118
119 it("listRecentAiReviews returns an array", async () => {
120 activityTest.demoActivityCache.clear();
121 const r = await listRecentAiReviews();
122 expect(Array.isArray(r)).toBe(true);
123 });
124
125 it("listQueuedAiBuildIssues returns an array", async () => {
126 activityTest.demoActivityCache.clear();
127 const r = await listQueuedAiBuildIssues();
128 expect(Array.isArray(r)).toBe(true);
129 });
130
131 it("listDemoActivityFeed returns an array", async () => {
132 activityTest.demoActivityCache.clear();
133 const r = await listDemoActivityFeed();
134 expect(Array.isArray(r)).toBe(true);
135 });
136
137 it("countAiReviewsSince returns a number", async () => {
138 activityTest.demoActivityCache.clear();
139 const n = await countAiReviewsSince();
140 expect(typeof n).toBe("number");
141 expect(n).toBeGreaterThanOrEqual(0);
142 });
143});
144
145describe("ensureDemoActivity — idempotency", () => {
146 it("never throws on repeated calls", async () => {
147 let first: unknown = null;
148 let second: unknown = null;
149 let firstErr: unknown = null;
150 let secondErr: unknown = null;
151 try {
152 first = await ensureDemoActivity();
153 } catch (e) {
154 firstErr = e;
155 }
156 try {
157 second = await ensureDemoActivity();
158 } catch (e) {
159 secondErr = e;
160 }
161 expect(firstErr).toBeNull();
162 expect(secondErr).toBeNull();
163 // Both calls return a result shape.
164 expect(first).toBeDefined();
165 expect(second).toBeDefined();
166 if (first && typeof first === "object" && "added" in first) {
167 const r2 = second as { added: { issues: number; prs: number; auditRows: number } };
168 // Second run added either zero rows (idempotent) or the same DB
169 // simply isn't reachable; either way, second pass must not add MORE
170 // than first.
171 const f = first as { added: { issues: number; prs: number; auditRows: number } };
172 expect(r2.added.issues).toBeLessThanOrEqual(f.added.issues);
173 expect(r2.added.prs).toBeLessThanOrEqual(f.added.prs);
174 expect(r2.added.auditRows).toBeLessThanOrEqual(f.added.auditRows);
175 }
176 });
177});