Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

agent-marketplace.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.

agent-marketplace.test.tsBlame251 lines · 1 contributor
055ebc4Claude1/**
2 * Block K10 — Agent Marketplace tests.
3 *
4 * Pure form-parser tests are fully exercised. Route smokes only assert auth
5 * behaviour — DB-backed flows live in the integration suite.
6 */
7
8import { describe, it, expect } from "bun:test";
9import app from "../app";
10import {
11 parseListingForm,
12 ALLOWED_LISTING_KINDS,
13 SLUG_RE,
14 TAGLINE_MAX,
15 DESCRIPTION_MAX,
16 PRICING_MAX_CENTS,
17 type ListingFormInput,
18} from "../routes/agent-marketplace";
19
20// ---------------------------------------------------------------------------
21// parseListingForm
22// ---------------------------------------------------------------------------
23
24describe("agent-marketplace — parseListingForm", () => {
25 const validInput: ListingFormInput = {
26 slug: "my-agent",
27 name: "My Agent",
28 tagline: "It does things.",
29 description: "Hello world.",
30 kind: "triage",
31 homepage_url: "https://example.com",
32 icon_url: "https://example.com/icon.png",
33 pricing_cents_per_month: "500",
34 };
35
36 it("accepts a fully valid form", () => {
37 const r = parseListingForm(validInput);
38 expect(r.ok).toBe(true);
39 if (r.ok) {
40 expect(r.data.slug).toBe("my-agent");
41 expect(r.data.name).toBe("My Agent");
42 expect(r.data.kind).toBe("triage");
43 expect(r.data.pricingCentsPerMonth).toBe(500);
44 expect(r.data.homepageUrl).toBe("https://example.com");
45 }
46 });
47
48 it("rejects slug shorter than 3 chars", () => {
49 const r = parseListingForm({ ...validInput, slug: "ab" });
50 expect(r.ok).toBe(false);
51 });
52
53 it("rejects slug starting with digit", () => {
54 const r = parseListingForm({ ...validInput, slug: "1abc" });
55 expect(r.ok).toBe(false);
56 });
57
58 it("rejects slug with uppercase", () => {
59 const r = parseListingForm({ ...validInput, slug: "MyAgent" });
60 expect(r.ok).toBe(false);
61 });
62
63 it("accepts slug with hyphens and digits", () => {
64 const r = parseListingForm({ ...validInput, slug: "my-agent-v2" });
65 expect(r.ok).toBe(true);
66 });
67
68 it("rejects empty name", () => {
69 const r = parseListingForm({ ...validInput, name: " " });
70 expect(r.ok).toBe(false);
71 });
72
73 it("rejects name over 100 chars", () => {
74 const r = parseListingForm({ ...validInput, name: "x".repeat(101) });
75 expect(r.ok).toBe(false);
76 });
77
78 it("rejects empty tagline", () => {
79 const r = parseListingForm({ ...validInput, tagline: "" });
80 expect(r.ok).toBe(false);
81 });
82
83 it("rejects tagline > TAGLINE_MAX", () => {
84 const r = parseListingForm({
85 ...validInput,
86 tagline: "x".repeat(TAGLINE_MAX + 1),
87 });
88 expect(r.ok).toBe(false);
89 });
90
91 it("silently truncates description to DESCRIPTION_MAX", () => {
92 const r = parseListingForm({
93 ...validInput,
94 description: "x".repeat(DESCRIPTION_MAX + 500),
95 });
96 expect(r.ok).toBe(true);
97 if (r.ok) {
98 expect(r.data.description.length).toBe(DESCRIPTION_MAX);
99 }
100 });
101
102 it("rejects unknown kind", () => {
103 const r = parseListingForm({ ...validInput, kind: "hackerman" });
104 expect(r.ok).toBe(false);
105 });
106
107 it("accepts every allowed kind", () => {
108 for (const k of ALLOWED_LISTING_KINDS) {
109 const r = parseListingForm({ ...validInput, kind: k });
110 expect(r.ok).toBe(true);
111 }
112 });
113
114 it("rejects negative pricing", () => {
115 const r = parseListingForm({
116 ...validInput,
117 pricing_cents_per_month: "-5",
118 });
119 expect(r.ok).toBe(false);
120 });
121
122 it("rejects non-numeric pricing", () => {
123 const r = parseListingForm({
124 ...validInput,
125 pricing_cents_per_month: "abc",
126 });
127 // parseInt -> NaN, isFinite false, rejected.
128 expect(r.ok).toBe(false);
129 });
130
131 it("caps pricing at PRICING_MAX_CENTS", () => {
132 const r = parseListingForm({
133 ...validInput,
134 pricing_cents_per_month: "999999999",
135 });
136 expect(r.ok).toBe(true);
137 if (r.ok) expect(r.data.pricingCentsPerMonth).toBe(PRICING_MAX_CENTS);
138 });
139
140 it("nullifies invalid homepage URLs", () => {
141 const r = parseListingForm({
142 ...validInput,
143 homepage_url: "javascript:alert(1)",
144 });
145 expect(r.ok).toBe(true);
146 if (r.ok) expect(r.data.homepageUrl).toBeNull();
147 });
148
149 it("keeps https homepage URLs", () => {
150 const r = parseListingForm({
151 ...validInput,
152 homepage_url: "https://safe.example.com/path",
153 });
154 expect(r.ok).toBe(true);
155 if (r.ok) expect(r.data.homepageUrl).toBe("https://safe.example.com/path");
156 });
157
158 it("default pricing is 0 when omitted", () => {
159 const { pricing_cents_per_month, ...rest } = validInput;
160 void pricing_cents_per_month;
161 const r = parseListingForm(rest as ListingFormInput);
162 expect(r.ok).toBe(true);
163 if (r.ok) expect(r.data.pricingCentsPerMonth).toBe(0);
164 });
165});
166
167// ---------------------------------------------------------------------------
168// Constants are sane
169// ---------------------------------------------------------------------------
170
171describe("agent-marketplace — constants", () => {
172 it("SLUG_RE matches typical slugs", () => {
173 expect(SLUG_RE.test("my-agent")).toBe(true);
174 expect(SLUG_RE.test("abc")).toBe(true);
175 expect(SLUG_RE.test("A")).toBe(false);
176 });
177 it("ALLOWED_LISTING_KINDS contains every expected kind", () => {
178 expect(ALLOWED_LISTING_KINDS).toContain("triage");
179 expect(ALLOWED_LISTING_KINDS).toContain("fix");
180 expect(ALLOWED_LISTING_KINDS).toContain("heal_bot");
181 expect(ALLOWED_LISTING_KINDS).toContain("deploy_watch");
182 });
183});
184
185// ---------------------------------------------------------------------------
186// Route auth smokes
187// ---------------------------------------------------------------------------
188
189describe("agent-marketplace — route auth smokes", () => {
190 it("GET /marketplace/agents (public) → 200", async () => {
191 const res = await app.fetch(new Request("http://test/marketplace/agents"));
192 expect(res.status).toBe(200);
193 });
194
195 it("POST /marketplace/agents/:slug/install without session → 302 /login", async () => {
196 const res = await app.fetch(
197 new Request("http://test/marketplace/agents/any/install", {
198 method: "POST",
199 headers: { "content-type": "application/x-www-form-urlencoded" },
200 body: new URLSearchParams({ repo_id: "x" }),
201 })
202 );
203 expect(res.status).toBe(302);
204 expect(res.headers.get("location")).toMatch(/\/login/);
205 });
206
207 it("POST /marketplace/agents/:slug/uninstall without session → 302 /login", async () => {
208 const res = await app.fetch(
209 new Request("http://test/marketplace/agents/any/uninstall", {
210 method: "POST",
211 })
212 );
213 expect(res.status).toBe(302);
214 expect(res.headers.get("location")).toMatch(/\/login/);
215 });
216
217 it("GET /settings/agent-listings without session → 302 /login", async () => {
218 const res = await app.fetch(
219 new Request("http://test/settings/agent-listings")
220 );
221 expect(res.status).toBe(302);
222 expect(res.headers.get("location")).toMatch(/\/login/);
223 });
224
225 it("POST /settings/agent-listings without session → 302 /login", async () => {
226 const res = await app.fetch(
227 new Request("http://test/settings/agent-listings", { method: "POST" })
228 );
229 expect(res.status).toBe(302);
230 expect(res.headers.get("location")).toMatch(/\/login/);
231 });
232
233 it("POST /settings/agent-listings/:id/publish without session → 302 /login", async () => {
234 const res = await app.fetch(
235 new Request(
236 "http://test/settings/agent-listings/00000000-0000-0000-0000-000000000000/publish",
237 { method: "POST" }
238 )
239 );
240 expect(res.status).toBe(302);
241 expect(res.headers.get("location")).toMatch(/\/login/);
242 });
243
244 it("GET /admin/marketplace/agents without session → 302 /login", async () => {
245 const res = await app.fetch(
246 new Request("http://test/admin/marketplace/agents")
247 );
248 expect(res.status).toBe(302);
249 expect(res.headers.get("location")).toMatch(/\/login/);
250 });
251});