Skip to content
140 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { readFileSync, readdirSync } from "node:fs";
3import { DatabaseSync } from "node:sqlite";
4import { test } from "node:test";
5
6import { MAX_OPEN_REQUESTS, cleanRequestNote, parseListing } from "@g1t/contracts/marketplace";
7
8import { answerLine, findListing, listRequests, listingPath, openRequest, resolveListing, resolveRequest } from "./installs.ts";
9
10/** D1, as far as install requests use it, over node's SQLite with the service's migrations. */
11function fakeD1(): D1Database {
12 const db = new DatabaseSync(":memory:");
13 const dir = new URL("../migrations/", import.meta.url);
14 for (const file of readdirSync(dir).sort()) db.exec(readFileSync(new URL(file, dir), "utf8"));
15 const statement = (sql: string, params: unknown[] = []) => ({
16 bind: (...values: unknown[]) => statement(sql, values),
17 first: async () => (db.prepare(sql).get(...(params as never[])) as unknown) ?? null,
18 run: async () => db.prepare(sql).run(...(params as never[])),
19 all: async () => ({ results: db.prepare(sql).all(...(params as never[])) }),
20 });
21 return { prepare: (sql: string) => statement(sql) } as unknown as D1Database;
22}
23
24let ids = 0;
25const nextId = () => `ins_${++ids}`;
26const ws = "wsp_acme";
27const ana = { id: "usr_ana", username: "ana" };
28const bo = { id: "usr_bo", username: "bo" };
29const owner = { id: "usr_own", username: "chase" };
30const at = new Date("2026-10-10T12:00:00.000Z");
31const later = (days: number) => new Date(at.getTime() + days * 86_400_000);
32
33test("only what a workspace can add today is a listing", () => {
34 assert.deepEqual(findListing("integration:sentry"), { ref: "integration:sentry", kind: "integration", id: "sentry", name: "Sentry" });
35 assert.equal(findListing("agent:engineering"), null, "agents start from templates in Agents, not the Marketplace");
36 assert.equal(findListing("integration:astronaut"), null, "no such integration");
37 assert.equal(findListing("integration:slack"), null, "Slack is planned, not available");
38 assert.equal(findListing("integration:mcp"), null, "MCP clients are connected by each person, not a workspace");
39 assert.equal(findListing("extension:support"), null, "no extensions are listed yet");
40 assert.equal(findListing(42), null);
41});
42
43test("references parse strictly, and notes are tidied", () => {
44 assert.deepEqual(parseListing(" integration:alerts-webhook "), { kind: "integration", id: "alerts-webhook" });
45 assert.equal(parseListing("integration:"), null);
46 assert.equal(parseListing("integration:Sentry"), null);
47 assert.equal(parseListing("integration:a b"), null);
48 assert.equal(parseListing("agent:qa"), null);
49 assert.equal(cleanRequestNote(" for on-call "), "for on-call");
50 assert.equal(cleanRequestNote(" "), null);
51 assert.equal(cleanRequestNote("x".repeat(400))?.length, 280);
52 assert.equal(cleanRequestNote(null), null);
53});
54
55test("a member's request is kept, and asking twice is one request", async () => {
56 const db = fakeD1();
57 const listing = findListing("integration:sentry")!;
58 const first = await openRequest(db, ws, nextId(), listing, ana, "Flaky checks", at);
59 assert.ok(first.ok);
60 assert.equal(first.value.status, "open");
61 assert.equal(first.value.name, "Sentry");
62 assert.equal(first.value.kind, "integration");
63 assert.equal(first.value.note, "Flaky checks");
64 const again = await openRequest(db, ws, nextId(), listing, ana, null, later(0.1));
65 assert.equal(again.ok, false);
66 assert.equal(!again.ok && again.error.code, "conflict");
67 // Someone else asking for the same thing is their own request.
68 assert.ok((await openRequest(db, ws, nextId(), listing, bo, null, at)).ok);
69});
70
71test("an owner sees every request; a member sees their own", async () => {
72 const db = fakeD1();
73 await openRequest(db, ws, nextId(), findListing("integration:jira")!, ana, null, at);
74 await openRequest(db, ws, nextId(), findListing("integration:linear")!, bo, null, later(0.01));
75 await openRequest(db, "wsp_other", nextId(), findListing("integration:datadog")!, ana, null, at);
76 assert.deepEqual((await listRequests(db, ws, owner, true, at)).map((r) => r.listing), ["integration:linear", "integration:jira"]);
77 assert.deepEqual((await listRequests(db, ws, ana, false, at)).map((r) => r.listing), ["integration:jira"]);
78});
79
80test("answering a request: once, by status, and answered ones age out", async () => {
81 const db = fakeD1();
82 const opened = await openRequest(db, ws, nextId(), findListing("integration:sentry")!, ana, null, at);
83 assert.ok(opened.ok);
84 const answered = await resolveRequest(db, ws, opened.value.id, "declined", owner, later(1));
85 assert.ok(answered.ok);
86 assert.equal(answered.value.request.status, "declined");
87 assert.equal(answered.value.request.resolved_by, "chase");
88 assert.equal(answered.value.requested_by_id, "usr_ana");
89 const twice = await resolveRequest(db, ws, opened.value.id, "done", owner, later(1));
90 assert.equal(!twice.ok && twice.error.code, "conflict");
91 const elsewhere = await resolveRequest(db, "wsp_other", opened.value.id, "done", owner, later(1));
92 assert.equal(!elsewhere.ok && elsewhere.error.code, "not_found");
93 // Turned down, the person may ask again.
94 assert.ok((await openRequest(db, ws, nextId(), findListing("integration:sentry")!, ana, null, later(2))).ok);
95 // Answered requests stay listed for 30 days.
96 assert.equal((await listRequests(db, ws, owner, true, later(20))).length, 2);
97 assert.equal((await listRequests(db, ws, owner, true, later(40))).length, 1, "only the open one");
98});
99
100test("adding a listing answers every open request for it", async () => {
101 const db = fakeD1();
102 await openRequest(db, ws, nextId(), findListing("integration:linear")!, ana, null, at);
103 await openRequest(db, ws, nextId(), findListing("integration:linear")!, bo, null, at);
104 await openRequest(db, ws, nextId(), findListing("integration:jira")!, bo, null, at);
105 const answered = await resolveListing(db, ws, "integration:linear", owner, later(1));
106 assert.deepEqual(answered.map((a) => a.requested_by_id).sort(), ["usr_ana", "usr_bo"]);
107 assert.ok(answered.every((a) => a.request.status === "done"));
108 const open = (await listRequests(db, ws, owner, true, later(1))).filter((r) => r.status === "open");
109 assert.deepEqual(open.map((r) => r.listing), ["integration:jira"]);
110});
111
112test("a request kept from a kind the Marketplace no longer lists is left out", async () => {
113 const db = fakeD1();
114 await openRequest(db, ws, nextId(), findListing("integration:sentry")!, ana, null, at);
115 // An agent request, as the table could hold one from before agents left the Marketplace.
116 await db
117 .prepare("INSERT INTO install_requests (id, workspace_id, listing, name, requested_by, requested_by_id, requested_at) VALUES (?, ?, ?, ?, ?, ?, ?)")
118 .bind("ins_old", ws, "agent:qa", "QA Engineer", "ana", "usr_ana", at.toISOString())
119 .run();
120 assert.deepEqual((await listRequests(db, ws, owner, true, at)).map((r) => r.listing), ["integration:sentry"]);
121 assert.deepEqual(await resolveListing(db, ws, "agent:qa", owner, later(1)), []);
122});
123
124test("a person keeps at most so many requests open", async () => {
125 const db = fakeD1();
126 const listings = ["integration:github", "integration:webhooks", "integration:linear", "integration:jira", "integration:sentry", "integration:datadog", "integration:alerts-webhook", "integration:anthropic", "integration:openai", "integration:gemini", "integration:xai", "integration:mistral", "integration:deepseek", "integration:azure-openai", "integration:openrouter", "integration:together", "integration:fireworks", "integration:cerebras", "integration:anthropic-endpoint", "integration:openai-endpoint"];
127 assert.equal(listings.length, MAX_OPEN_REQUESTS);
128 for (const ref of listings) assert.ok((await openRequest(db, ws, nextId(), findListing(ref)!, ana, null, at)).ok, ref);
129 const over = await openRequest(db, ws, nextId(), findListing("integration:groq")!, ana, null, at);
130 assert.equal(!over.ok && over.error.code, "limit");
131});
132
133test("what the asker is told, and where it leads", () => {
134 const base = { id: "ins_1", listing: "integration:sentry", kind: "integration" as const, name: "Sentry", note: null, requested_by: "ana", requested_at: at.toISOString(), resolved_by: "chase", resolved_at: at.toISOString() };
135 assert.equal(answerLine({ ...base, status: "done" }, "@chase").title, "Sentry was added to the workspace");
136 assert.equal(answerLine({ ...base, status: "done" }, "@chase").body, "@chase connected it.");
137 assert.match(answerLine({ ...base, status: "declined" }, "@chase").title, /@chase turned down your request for Sentry/);
138 assert.equal(listingPath("acme", "extension:support"), "/acme/-/marketplace/extensions/support");
139 assert.equal(listingPath("acme", "integration:sentry"), "/acme/-/marketplace/integrations");
140});