postgresai 0.14.0-dev.8 → 0.14.0-dev.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -61
- package/bin/postgres-ai.ts +2596 -428
- package/bun.lock +258 -0
- package/bunfig.toml +20 -0
- package/dist/bin/postgres-ai.js +31277 -1575
- package/dist/sql/01.role.sql +16 -0
- package/dist/sql/02.extensions.sql +8 -0
- package/dist/sql/03.permissions.sql +38 -0
- package/dist/sql/04.optional_rds.sql +6 -0
- package/dist/sql/05.optional_self_managed.sql +8 -0
- package/dist/sql/06.helpers.sql +439 -0
- package/dist/sql/sql/01.role.sql +16 -0
- package/dist/sql/sql/02.extensions.sql +8 -0
- package/dist/sql/sql/03.permissions.sql +38 -0
- package/dist/sql/sql/04.optional_rds.sql +6 -0
- package/dist/sql/sql/05.optional_self_managed.sql +8 -0
- package/dist/sql/sql/06.helpers.sql +439 -0
- package/dist/sql/sql/uninit/01.helpers.sql +5 -0
- package/dist/sql/sql/uninit/02.permissions.sql +30 -0
- package/dist/sql/sql/uninit/03.role.sql +27 -0
- package/dist/sql/uninit/01.helpers.sql +5 -0
- package/dist/sql/uninit/02.permissions.sql +30 -0
- package/dist/sql/uninit/03.role.sql +27 -0
- package/lib/auth-server.ts +124 -106
- package/lib/checkup-api.ts +386 -0
- package/lib/checkup-dictionary.ts +113 -0
- package/lib/checkup.ts +1512 -0
- package/lib/config.ts +6 -3
- package/lib/init.ts +655 -189
- package/lib/issues.ts +848 -193
- package/lib/mcp-server.ts +391 -91
- package/lib/metrics-loader.ts +127 -0
- package/lib/supabase.ts +824 -0
- package/lib/util.ts +61 -0
- package/package.json +22 -10
- package/packages/postgres-ai/README.md +26 -0
- package/packages/postgres-ai/bin/postgres-ai.js +27 -0
- package/packages/postgres-ai/package.json +27 -0
- package/scripts/embed-checkup-dictionary.ts +106 -0
- package/scripts/embed-metrics.ts +154 -0
- package/sql/01.role.sql +16 -0
- package/sql/02.extensions.sql +8 -0
- package/sql/03.permissions.sql +38 -0
- package/sql/04.optional_rds.sql +6 -0
- package/sql/05.optional_self_managed.sql +8 -0
- package/sql/06.helpers.sql +439 -0
- package/sql/uninit/01.helpers.sql +5 -0
- package/sql/uninit/02.permissions.sql +30 -0
- package/sql/uninit/03.role.sql +27 -0
- package/test/auth.test.ts +258 -0
- package/test/checkup.integration.test.ts +321 -0
- package/test/checkup.test.ts +1116 -0
- package/test/config-consistency.test.ts +36 -0
- package/test/init.integration.test.ts +508 -0
- package/test/init.test.ts +916 -0
- package/test/issues.cli.test.ts +538 -0
- package/test/issues.test.ts +456 -0
- package/test/mcp-server.test.ts +1527 -0
- package/test/schema-validation.test.ts +81 -0
- package/test/supabase.test.ts +568 -0
- package/test/test-utils.ts +128 -0
- package/tsconfig.json +12 -20
- package/dist/bin/postgres-ai.d.ts +0 -3
- package/dist/bin/postgres-ai.d.ts.map +0 -1
- package/dist/bin/postgres-ai.js.map +0 -1
- package/dist/lib/auth-server.d.ts +0 -31
- package/dist/lib/auth-server.d.ts.map +0 -1
- package/dist/lib/auth-server.js +0 -263
- package/dist/lib/auth-server.js.map +0 -1
- package/dist/lib/config.d.ts +0 -45
- package/dist/lib/config.d.ts.map +0 -1
- package/dist/lib/config.js +0 -181
- package/dist/lib/config.js.map +0 -1
- package/dist/lib/init.d.ts +0 -64
- package/dist/lib/init.d.ts.map +0 -1
- package/dist/lib/init.js +0 -399
- package/dist/lib/init.js.map +0 -1
- package/dist/lib/issues.d.ts +0 -75
- package/dist/lib/issues.d.ts.map +0 -1
- package/dist/lib/issues.js +0 -336
- package/dist/lib/issues.js.map +0 -1
- package/dist/lib/mcp-server.d.ts +0 -9
- package/dist/lib/mcp-server.d.ts.map +0 -1
- package/dist/lib/mcp-server.js +0 -168
- package/dist/lib/mcp-server.js.map +0 -1
- package/dist/lib/pkce.d.ts +0 -32
- package/dist/lib/pkce.d.ts.map +0 -1
- package/dist/lib/pkce.js +0 -101
- package/dist/lib/pkce.js.map +0 -1
- package/dist/lib/util.d.ts +0 -27
- package/dist/lib/util.d.ts.map +0 -1
- package/dist/lib/util.js +0 -46
- package/dist/lib/util.js.map +0 -1
- package/dist/package.json +0 -46
- package/test/init.integration.test.cjs +0 -269
- package/test/init.test.cjs +0 -76
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { mkdtempSync } from "fs";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
|
|
6
|
+
function runCli(args: string[], env: Record<string, string> = {}) {
|
|
7
|
+
const cliPath = resolve(import.meta.dir, "..", "bin", "postgres-ai.ts");
|
|
8
|
+
const bunBin = typeof process.execPath === "string" && process.execPath.length > 0 ? process.execPath : "bun";
|
|
9
|
+
const result = Bun.spawnSync([bunBin, cliPath, ...args], {
|
|
10
|
+
env: { ...process.env, ...env },
|
|
11
|
+
});
|
|
12
|
+
return {
|
|
13
|
+
status: result.exitCode,
|
|
14
|
+
stdout: new TextDecoder().decode(result.stdout),
|
|
15
|
+
stderr: new TextDecoder().decode(result.stderr),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function runCliAsync(args: string[], env: Record<string, string> = {}) {
|
|
20
|
+
const cliPath = resolve(import.meta.dir, "..", "bin", "postgres-ai.ts");
|
|
21
|
+
const bunBin = typeof process.execPath === "string" && process.execPath.length > 0 ? process.execPath : "bun";
|
|
22
|
+
const proc = Bun.spawn([bunBin, cliPath, ...args], {
|
|
23
|
+
env: { ...process.env, ...env },
|
|
24
|
+
stdout: "pipe",
|
|
25
|
+
stderr: "pipe",
|
|
26
|
+
});
|
|
27
|
+
const [status, stdout, stderr] = await Promise.all([
|
|
28
|
+
proc.exited,
|
|
29
|
+
new Response(proc.stdout).text(),
|
|
30
|
+
new Response(proc.stderr).text(),
|
|
31
|
+
]);
|
|
32
|
+
return { status, stdout, stderr };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isolatedEnv(extra: Record<string, string> = {}) {
|
|
36
|
+
// Ensure tests do not depend on any real user config on the machine running them.
|
|
37
|
+
const cfgHome = mkdtempSync(resolve(tmpdir(), "postgresai-cli-test-"));
|
|
38
|
+
return {
|
|
39
|
+
XDG_CONFIG_HOME: cfgHome,
|
|
40
|
+
HOME: cfgHome,
|
|
41
|
+
...extra,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function startFakeApi() {
|
|
46
|
+
const requests: Array<{
|
|
47
|
+
method: string;
|
|
48
|
+
pathname: string;
|
|
49
|
+
headers: Record<string, string>;
|
|
50
|
+
bodyText: string;
|
|
51
|
+
bodyJson: any | null;
|
|
52
|
+
}> = [];
|
|
53
|
+
|
|
54
|
+
const server = Bun.serve({
|
|
55
|
+
hostname: "127.0.0.1",
|
|
56
|
+
port: 0,
|
|
57
|
+
async fetch(req) {
|
|
58
|
+
const url = new URL(req.url);
|
|
59
|
+
const headers: Record<string, string> = {};
|
|
60
|
+
for (const [k, v] of req.headers.entries()) headers[k.toLowerCase()] = v;
|
|
61
|
+
|
|
62
|
+
const bodyText = await req.text();
|
|
63
|
+
let bodyJson: any | null = null;
|
|
64
|
+
try {
|
|
65
|
+
bodyJson = bodyText ? JSON.parse(bodyText) : null;
|
|
66
|
+
} catch {
|
|
67
|
+
bodyJson = null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
requests.push({
|
|
71
|
+
method: req.method,
|
|
72
|
+
pathname: url.pathname,
|
|
73
|
+
headers,
|
|
74
|
+
bodyText,
|
|
75
|
+
bodyJson,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Minimal fake PostgREST RPC endpoints used by our CLI.
|
|
79
|
+
if (req.method === "POST" && url.pathname.endsWith("/rpc/issue_create")) {
|
|
80
|
+
return new Response(
|
|
81
|
+
JSON.stringify({
|
|
82
|
+
id: "issue-1",
|
|
83
|
+
title: bodyJson?.title ?? "",
|
|
84
|
+
description: bodyJson?.description ?? null,
|
|
85
|
+
created_at: "2025-01-01T00:00:00Z",
|
|
86
|
+
status: 0,
|
|
87
|
+
project_id: bodyJson?.project_id ?? null,
|
|
88
|
+
labels: bodyJson?.labels ?? null,
|
|
89
|
+
}),
|
|
90
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (req.method === "POST" && url.pathname.endsWith("/rpc/issue_update")) {
|
|
95
|
+
return new Response(
|
|
96
|
+
JSON.stringify({
|
|
97
|
+
id: bodyJson?.p_id ?? "issue-1",
|
|
98
|
+
title: bodyJson?.p_title ?? "unchanged",
|
|
99
|
+
description: bodyJson?.p_description ?? null,
|
|
100
|
+
status: bodyJson?.p_status ?? 0,
|
|
101
|
+
updated_at: "2025-01-02T00:00:00Z",
|
|
102
|
+
labels: bodyJson?.p_labels ?? null,
|
|
103
|
+
}),
|
|
104
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (req.method === "POST" && url.pathname.endsWith("/rpc/issue_comment_update")) {
|
|
109
|
+
return new Response(
|
|
110
|
+
JSON.stringify({
|
|
111
|
+
id: bodyJson?.p_id ?? "comment-1",
|
|
112
|
+
issue_id: "issue-1",
|
|
113
|
+
content: bodyJson?.p_content ?? "",
|
|
114
|
+
updated_at: "2025-01-02T00:00:00Z",
|
|
115
|
+
}),
|
|
116
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (req.method === "POST" && url.pathname.endsWith("/rpc/issue_comment_create")) {
|
|
121
|
+
return new Response(
|
|
122
|
+
JSON.stringify({
|
|
123
|
+
id: "comment-1",
|
|
124
|
+
issue_id: bodyJson?.issue_id ?? "issue-1",
|
|
125
|
+
author_id: 1,
|
|
126
|
+
parent_comment_id: bodyJson?.parent_comment_id ?? null,
|
|
127
|
+
content: bodyJson?.content ?? "",
|
|
128
|
+
created_at: "2025-01-01T00:00:00Z",
|
|
129
|
+
updated_at: "2025-01-01T00:00:00Z",
|
|
130
|
+
data: null,
|
|
131
|
+
}),
|
|
132
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Action Items endpoints
|
|
137
|
+
if (req.method === "GET" && url.pathname.endsWith("/issue_action_items")) {
|
|
138
|
+
const issueIdParam = url.searchParams.get("issue_id");
|
|
139
|
+
const idParam = url.searchParams.get("id");
|
|
140
|
+
if (issueIdParam) {
|
|
141
|
+
// list_action_items
|
|
142
|
+
return new Response(
|
|
143
|
+
JSON.stringify([
|
|
144
|
+
{ id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", issue_id: issueIdParam.replace("eq.", ""), title: "Action 1", is_done: false, status: "waiting_for_approval" },
|
|
145
|
+
{ id: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", issue_id: issueIdParam.replace("eq.", ""), title: "Action 2", is_done: true, status: "approved" },
|
|
146
|
+
]),
|
|
147
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
if (idParam) {
|
|
151
|
+
// view_action_item
|
|
152
|
+
const actionId = idParam.replace("eq.", "").replace("in.(", "").replace(")", "").split(",")[0];
|
|
153
|
+
return new Response(
|
|
154
|
+
JSON.stringify([
|
|
155
|
+
{ id: actionId, issue_id: "11111111-1111-1111-1111-111111111111", title: "Test Action", description: "Test description", is_done: false, status: "waiting_for_approval", sql_action: "SELECT 1;", configs: [] },
|
|
156
|
+
]),
|
|
157
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (req.method === "POST" && url.pathname.endsWith("/rpc/issue_action_item_create")) {
|
|
163
|
+
return new Response(
|
|
164
|
+
JSON.stringify("cccccccc-cccc-cccc-cccc-cccccccccccc"),
|
|
165
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (req.method === "POST" && url.pathname.endsWith("/rpc/issue_action_item_update")) {
|
|
170
|
+
return new Response("", { status: 200, headers: { "Content-Type": "application/json" } });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return new Response("not found", { status: 404 });
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const baseUrl = `http://${server.hostname}:${server.port}/api/general`;
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
baseUrl,
|
|
181
|
+
requests,
|
|
182
|
+
stop: () => server.stop(true),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
describe("CLI issues command group", () => {
|
|
187
|
+
test("issues help exposes the canonical subcommands and no legacy names", () => {
|
|
188
|
+
const r = runCli(["issues", "--help"], isolatedEnv());
|
|
189
|
+
expect(r.status).toBe(0);
|
|
190
|
+
|
|
191
|
+
const out = `${r.stdout}\n${r.stderr}`;
|
|
192
|
+
|
|
193
|
+
// Canonical subcommands
|
|
194
|
+
expect(out).toContain("create [options] <title>");
|
|
195
|
+
expect(out).toContain("update [options] <issueId>");
|
|
196
|
+
expect(out).toContain("update-comment [options] <commentId> <content>");
|
|
197
|
+
expect(out).toContain("post-comment [options] <issueId> <content>");
|
|
198
|
+
|
|
199
|
+
// Legacy / removed names
|
|
200
|
+
expect(out).not.toContain("create-issue");
|
|
201
|
+
expect(out).not.toContain("update-issue");
|
|
202
|
+
expect(out).not.toContain("update-issue-comment");
|
|
203
|
+
expect(out).not.toContain("post_comment");
|
|
204
|
+
expect(out).not.toContain("create_issue");
|
|
205
|
+
expect(out).not.toContain("update_issue");
|
|
206
|
+
expect(out).not.toContain("update_issue_comment");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("issues create fails fast when API key is missing", () => {
|
|
210
|
+
const r = runCli(["issues", "create", "Test issue"], isolatedEnv());
|
|
211
|
+
expect(r.status).toBe(1);
|
|
212
|
+
expect(`${r.stdout}\n${r.stderr}`).toContain("API key is required");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("issues create fails fast when org id is missing (no config fallback)", () => {
|
|
216
|
+
const r = runCli(["issues", "create", "Test issue"], isolatedEnv({ PGAI_API_KEY: "test-key" }));
|
|
217
|
+
expect(r.status).toBe(1);
|
|
218
|
+
expect(`${r.stdout}\n${r.stderr}`).toContain("org_id is required");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("issues update fails fast when API key is missing", () => {
|
|
222
|
+
const r = runCli(["issues", "update", "00000000-0000-0000-0000-000000000000", "--title", "New title"], isolatedEnv());
|
|
223
|
+
expect(r.status).toBe(1);
|
|
224
|
+
expect(`${r.stdout}\n${r.stderr}`).toContain("API key is required");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("issues update-comment fails fast when API key is missing", () => {
|
|
228
|
+
const r = runCli(["issues", "update-comment", "00000000-0000-0000-0000-000000000000", "hello"], isolatedEnv());
|
|
229
|
+
expect(r.status).toBe(1);
|
|
230
|
+
expect(`${r.stdout}\n${r.stderr}`).toContain("API key is required");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("issues post-comment fails fast when API key is missing", () => {
|
|
234
|
+
const r = runCli(["issues", "post-comment", "00000000-0000-0000-0000-000000000000", "hello"], isolatedEnv());
|
|
235
|
+
expect(r.status).toBe(1);
|
|
236
|
+
expect(`${r.stdout}\n${r.stderr}`).toContain("API key is required");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("issues create succeeds against a fake API and sends the expected request", async () => {
|
|
240
|
+
const api = await startFakeApi();
|
|
241
|
+
try {
|
|
242
|
+
const r = await runCliAsync(
|
|
243
|
+
["issues", "create", "Hello", "--org-id", "123", "--description", "line1\\nline2", "--label", "a", "--label", "b"],
|
|
244
|
+
isolatedEnv({
|
|
245
|
+
PGAI_API_KEY: "test-key",
|
|
246
|
+
PGAI_API_BASE_URL: api.baseUrl,
|
|
247
|
+
})
|
|
248
|
+
);
|
|
249
|
+
expect(r.status).toBe(0);
|
|
250
|
+
|
|
251
|
+
const out = JSON.parse(r.stdout.trim());
|
|
252
|
+
expect(out.id).toBe("issue-1");
|
|
253
|
+
expect(out.title).toBe("Hello");
|
|
254
|
+
expect(out.description).toBe("line1\nline2");
|
|
255
|
+
expect(out.labels).toEqual(["a", "b"]);
|
|
256
|
+
|
|
257
|
+
const req = api.requests.find((x) => x.pathname.endsWith("/rpc/issue_create"));
|
|
258
|
+
expect(req).toBeTruthy();
|
|
259
|
+
expect(req!.headers["access-token"]).toBe("test-key");
|
|
260
|
+
expect(req!.method).toBe("POST");
|
|
261
|
+
expect(req!.bodyJson.org_id).toBe(123);
|
|
262
|
+
expect(req!.bodyJson.title).toBe("Hello");
|
|
263
|
+
expect(req!.bodyJson.description).toBe("line1\nline2");
|
|
264
|
+
expect(req!.bodyJson.labels).toEqual(["a", "b"]);
|
|
265
|
+
} finally {
|
|
266
|
+
api.stop();
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("issues update succeeds against a fake API (including status mapping)", async () => {
|
|
271
|
+
const api = await startFakeApi();
|
|
272
|
+
try {
|
|
273
|
+
const r = await runCliAsync(
|
|
274
|
+
["issues", "update", "issue-1", "--title", "New title", "--status", "closed"],
|
|
275
|
+
isolatedEnv({
|
|
276
|
+
PGAI_API_KEY: "test-key",
|
|
277
|
+
PGAI_API_BASE_URL: api.baseUrl,
|
|
278
|
+
})
|
|
279
|
+
);
|
|
280
|
+
expect(r.status).toBe(0);
|
|
281
|
+
|
|
282
|
+
const out = JSON.parse(r.stdout.trim());
|
|
283
|
+
expect(out.id).toBe("issue-1");
|
|
284
|
+
expect(out.title).toBe("New title");
|
|
285
|
+
expect(out.status).toBe(1);
|
|
286
|
+
|
|
287
|
+
const req = api.requests.find((x) => x.pathname.endsWith("/rpc/issue_update"));
|
|
288
|
+
expect(req).toBeTruthy();
|
|
289
|
+
expect(req!.headers["access-token"]).toBe("test-key");
|
|
290
|
+
expect(req!.bodyJson.p_id).toBe("issue-1");
|
|
291
|
+
expect(req!.bodyJson.p_title).toBe("New title");
|
|
292
|
+
expect(req!.bodyJson.p_status).toBe(1);
|
|
293
|
+
} finally {
|
|
294
|
+
api.stop();
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("issues update-comment succeeds against a fake API and decodes escapes", async () => {
|
|
299
|
+
const api = await startFakeApi();
|
|
300
|
+
try {
|
|
301
|
+
const r = await runCliAsync(
|
|
302
|
+
["issues", "update-comment", "comment-1", "hello\\nworld"],
|
|
303
|
+
isolatedEnv({
|
|
304
|
+
PGAI_API_KEY: "test-key",
|
|
305
|
+
PGAI_API_BASE_URL: api.baseUrl,
|
|
306
|
+
})
|
|
307
|
+
);
|
|
308
|
+
expect(r.status).toBe(0);
|
|
309
|
+
|
|
310
|
+
const out = JSON.parse(r.stdout.trim());
|
|
311
|
+
expect(out.id).toBe("comment-1");
|
|
312
|
+
expect(out.content).toBe("hello\nworld");
|
|
313
|
+
|
|
314
|
+
const req = api.requests.find((x) => x.pathname.endsWith("/rpc/issue_comment_update"));
|
|
315
|
+
expect(req).toBeTruthy();
|
|
316
|
+
expect(req!.headers["access-token"]).toBe("test-key");
|
|
317
|
+
expect(req!.bodyJson.p_id).toBe("comment-1");
|
|
318
|
+
expect(req!.bodyJson.p_content).toBe("hello\nworld");
|
|
319
|
+
} finally {
|
|
320
|
+
api.stop();
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("issues post-comment succeeds against a fake API and decodes escapes", async () => {
|
|
325
|
+
const api = await startFakeApi();
|
|
326
|
+
try {
|
|
327
|
+
const r = await runCliAsync(
|
|
328
|
+
["issues", "post-comment", "issue-1", "hello\\nworld"],
|
|
329
|
+
isolatedEnv({
|
|
330
|
+
PGAI_API_KEY: "test-key",
|
|
331
|
+
PGAI_API_BASE_URL: api.baseUrl,
|
|
332
|
+
})
|
|
333
|
+
);
|
|
334
|
+
expect(r.status).toBe(0);
|
|
335
|
+
|
|
336
|
+
const out = JSON.parse(r.stdout.trim());
|
|
337
|
+
expect(out.id).toBe("comment-1");
|
|
338
|
+
expect(out.issue_id).toBe("issue-1");
|
|
339
|
+
expect(out.content).toBe("hello\nworld");
|
|
340
|
+
|
|
341
|
+
const req = api.requests.find((x) => x.pathname.endsWith("/rpc/issue_comment_create"));
|
|
342
|
+
expect(req).toBeTruthy();
|
|
343
|
+
expect(req!.headers["access-token"]).toBe("test-key");
|
|
344
|
+
expect(req!.bodyJson.issue_id).toBe("issue-1");
|
|
345
|
+
expect(req!.bodyJson.content).toBe("hello\nworld");
|
|
346
|
+
} finally {
|
|
347
|
+
api.stop();
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe("CLI action items commands", () => {
|
|
353
|
+
test("issues action-items fails fast when API key is missing", () => {
|
|
354
|
+
const r = runCli(["issues", "action-items", "00000000-0000-0000-0000-000000000000"], isolatedEnv());
|
|
355
|
+
expect(r.status).toBe(1);
|
|
356
|
+
expect(`${r.stdout}\n${r.stderr}`).toContain("API key is required");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("issues action-items fails when issue_id is not a valid UUID", () => {
|
|
360
|
+
const r = runCli(["issues", "action-items", "invalid-id"], isolatedEnv({ PGAI_API_KEY: "test-key" }));
|
|
361
|
+
expect(r.status).toBe(1);
|
|
362
|
+
expect(`${r.stdout}\n${r.stderr}`).toContain("issueId must be a valid UUID");
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("issues action-items succeeds against a fake API", async () => {
|
|
366
|
+
const api = await startFakeApi();
|
|
367
|
+
try {
|
|
368
|
+
const r = await runCliAsync(
|
|
369
|
+
["issues", "action-items", "11111111-1111-1111-1111-111111111111"],
|
|
370
|
+
isolatedEnv({
|
|
371
|
+
PGAI_API_KEY: "test-key",
|
|
372
|
+
PGAI_API_BASE_URL: api.baseUrl,
|
|
373
|
+
})
|
|
374
|
+
);
|
|
375
|
+
expect(r.status).toBe(0);
|
|
376
|
+
|
|
377
|
+
const out = JSON.parse(r.stdout.trim());
|
|
378
|
+
expect(Array.isArray(out)).toBe(true);
|
|
379
|
+
expect(out.length).toBe(2);
|
|
380
|
+
expect(out[0].title).toBe("Action 1");
|
|
381
|
+
} finally {
|
|
382
|
+
api.stop();
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("issues view-action-item fails fast when API key is missing", () => {
|
|
387
|
+
const r = runCli(["issues", "view-action-item", "00000000-0000-0000-0000-000000000000"], isolatedEnv());
|
|
388
|
+
expect(r.status).toBe(1);
|
|
389
|
+
expect(`${r.stdout}\n${r.stderr}`).toContain("API key is required");
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("issues view-action-item fails when action_item_id is not a valid UUID", () => {
|
|
393
|
+
const r = runCli(["issues", "view-action-item", "invalid-id"], isolatedEnv({ PGAI_API_KEY: "test-key" }));
|
|
394
|
+
expect(r.status).toBe(1);
|
|
395
|
+
expect(`${r.stdout}\n${r.stderr}`).toContain("actionItemId is required and must be a valid UUID");
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("issues view-action-item succeeds against a fake API", async () => {
|
|
399
|
+
const api = await startFakeApi();
|
|
400
|
+
try {
|
|
401
|
+
const r = await runCliAsync(
|
|
402
|
+
["issues", "view-action-item", "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"],
|
|
403
|
+
isolatedEnv({
|
|
404
|
+
PGAI_API_KEY: "test-key",
|
|
405
|
+
PGAI_API_BASE_URL: api.baseUrl,
|
|
406
|
+
})
|
|
407
|
+
);
|
|
408
|
+
expect(r.status).toBe(0);
|
|
409
|
+
|
|
410
|
+
const out = JSON.parse(r.stdout.trim());
|
|
411
|
+
expect(out[0].title).toBe("Test Action");
|
|
412
|
+
expect(out[0].sql_action).toBe("SELECT 1;");
|
|
413
|
+
} finally {
|
|
414
|
+
api.stop();
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("issues create-action-item fails fast when API key is missing", () => {
|
|
419
|
+
const r = runCli(["issues", "create-action-item", "00000000-0000-0000-0000-000000000000", "Test title"], isolatedEnv());
|
|
420
|
+
expect(r.status).toBe(1);
|
|
421
|
+
expect(`${r.stdout}\n${r.stderr}`).toContain("API key is required");
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("issues create-action-item fails when issue_id is not a valid UUID", () => {
|
|
425
|
+
const r = runCli(["issues", "create-action-item", "invalid-id", "Test title"], isolatedEnv({ PGAI_API_KEY: "test-key" }));
|
|
426
|
+
expect(r.status).toBe(1);
|
|
427
|
+
expect(`${r.stdout}\n${r.stderr}`).toContain("issueId must be a valid UUID");
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("issues create-action-item succeeds against a fake API", async () => {
|
|
431
|
+
const api = await startFakeApi();
|
|
432
|
+
try {
|
|
433
|
+
const r = await runCliAsync(
|
|
434
|
+
["issues", "create-action-item", "11111111-1111-1111-1111-111111111111", "New action item", "--description", "Test description"],
|
|
435
|
+
isolatedEnv({
|
|
436
|
+
PGAI_API_KEY: "test-key",
|
|
437
|
+
PGAI_API_BASE_URL: api.baseUrl,
|
|
438
|
+
})
|
|
439
|
+
);
|
|
440
|
+
expect(r.status).toBe(0);
|
|
441
|
+
|
|
442
|
+
const out = JSON.parse(r.stdout.trim());
|
|
443
|
+
expect(out.id).toBe("cccccccc-cccc-cccc-cccc-cccccccccccc");
|
|
444
|
+
|
|
445
|
+
const req = api.requests.find((x) => x.pathname.endsWith("/rpc/issue_action_item_create"));
|
|
446
|
+
expect(req).toBeTruthy();
|
|
447
|
+
expect(req!.headers["access-token"]).toBe("test-key");
|
|
448
|
+
expect(req!.bodyJson.issue_id).toBe("11111111-1111-1111-1111-111111111111");
|
|
449
|
+
expect(req!.bodyJson.title).toBe("New action item");
|
|
450
|
+
expect(req!.bodyJson.description).toBe("Test description");
|
|
451
|
+
} finally {
|
|
452
|
+
api.stop();
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test("issues create-action-item interprets escape sequences", async () => {
|
|
457
|
+
const api = await startFakeApi();
|
|
458
|
+
try {
|
|
459
|
+
const r = await runCliAsync(
|
|
460
|
+
["issues", "create-action-item", "11111111-1111-1111-1111-111111111111", "Title\\nwith newline", "--description", "Desc\\twith tab"],
|
|
461
|
+
isolatedEnv({
|
|
462
|
+
PGAI_API_KEY: "test-key",
|
|
463
|
+
PGAI_API_BASE_URL: api.baseUrl,
|
|
464
|
+
})
|
|
465
|
+
);
|
|
466
|
+
expect(r.status).toBe(0);
|
|
467
|
+
|
|
468
|
+
const req = api.requests.find((x) => x.pathname.endsWith("/rpc/issue_action_item_create"));
|
|
469
|
+
expect(req).toBeTruthy();
|
|
470
|
+
expect(req!.bodyJson.title).toBe("Title\nwith newline");
|
|
471
|
+
expect(req!.bodyJson.description).toBe("Desc\twith tab");
|
|
472
|
+
} finally {
|
|
473
|
+
api.stop();
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test("issues update-action-item fails fast when API key is missing", () => {
|
|
478
|
+
const r = runCli(["issues", "update-action-item", "00000000-0000-0000-0000-000000000000", "--done"], isolatedEnv());
|
|
479
|
+
expect(r.status).toBe(1);
|
|
480
|
+
expect(`${r.stdout}\n${r.stderr}`).toContain("API key is required");
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("issues update-action-item fails when action_item_id is not a valid UUID", () => {
|
|
484
|
+
const r = runCli(["issues", "update-action-item", "invalid-id", "--done"], isolatedEnv({ PGAI_API_KEY: "test-key" }));
|
|
485
|
+
expect(r.status).toBe(1);
|
|
486
|
+
expect(`${r.stdout}\n${r.stderr}`).toContain("actionItemId must be a valid UUID");
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
test("issues update-action-item fails when no update fields provided", () => {
|
|
490
|
+
const r = runCli(["issues", "update-action-item", "00000000-0000-0000-0000-000000000000"], isolatedEnv({ PGAI_API_KEY: "test-key" }));
|
|
491
|
+
expect(r.status).toBe(1);
|
|
492
|
+
expect(`${r.stdout}\n${r.stderr}`).toContain("At least one update option is required");
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("issues update-action-item succeeds with --done flag", async () => {
|
|
496
|
+
const api = await startFakeApi();
|
|
497
|
+
try {
|
|
498
|
+
const r = await runCliAsync(
|
|
499
|
+
["issues", "update-action-item", "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "--done"],
|
|
500
|
+
isolatedEnv({
|
|
501
|
+
PGAI_API_KEY: "test-key",
|
|
502
|
+
PGAI_API_BASE_URL: api.baseUrl,
|
|
503
|
+
})
|
|
504
|
+
);
|
|
505
|
+
expect(r.status).toBe(0);
|
|
506
|
+
|
|
507
|
+
const req = api.requests.find((x) => x.pathname.endsWith("/rpc/issue_action_item_update"));
|
|
508
|
+
expect(req).toBeTruthy();
|
|
509
|
+
expect(req!.headers["access-token"]).toBe("test-key");
|
|
510
|
+
expect(req!.bodyJson.action_item_id).toBe("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
|
|
511
|
+
expect(req!.bodyJson.is_done).toBe(true);
|
|
512
|
+
} finally {
|
|
513
|
+
api.stop();
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test("issues update-action-item succeeds with --status flag", async () => {
|
|
518
|
+
const api = await startFakeApi();
|
|
519
|
+
try {
|
|
520
|
+
const r = await runCliAsync(
|
|
521
|
+
["issues", "update-action-item", "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "--status", "approved", "--status-reason", "LGTM"],
|
|
522
|
+
isolatedEnv({
|
|
523
|
+
PGAI_API_KEY: "test-key",
|
|
524
|
+
PGAI_API_BASE_URL: api.baseUrl,
|
|
525
|
+
})
|
|
526
|
+
);
|
|
527
|
+
expect(r.status).toBe(0);
|
|
528
|
+
|
|
529
|
+
const req = api.requests.find((x) => x.pathname.endsWith("/rpc/issue_action_item_update"));
|
|
530
|
+
expect(req).toBeTruthy();
|
|
531
|
+
expect(req!.bodyJson.status).toBe("approved");
|
|
532
|
+
expect(req!.bodyJson.status_reason).toBe("LGTM");
|
|
533
|
+
} finally {
|
|
534
|
+
api.stop();
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
|