postgresai 0.16.0-dev.1 → 0.16.0-dev.3
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/bin/postgres-ai.ts +728 -6
- package/dist/bin/postgres-ai.js +2496 -255
- package/dist/sql/06.helpers.sql +0 -122
- package/dist/sql/sql/06.helpers.sql +0 -122
- package/lib/dblab.ts +457 -0
- package/lib/init.ts +0 -8
- package/lib/joe.ts +730 -0
- package/lib/mcp-server.ts +597 -0
- package/lib/supabase.ts +0 -18
- package/package.json +1 -1
- package/sql/06.helpers.sql +0 -122
- package/test/dblab.cli.test.ts +339 -0
- package/test/dblab.test.ts +408 -0
- package/test/init.integration.test.ts +9 -79
- package/test/joe.cli.test.ts +298 -0
- package/test/joe.test.ts +726 -0
- package/test/monitoring.test.ts +54 -3
package/test/joe.test.ts
ADDED
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
import { describe, test, expect, mock, afterEach } from "bun:test";
|
|
2
|
+
import { mkdtempSync, existsSync, readFileSync } from "fs";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { resolve } from "path";
|
|
5
|
+
import {
|
|
6
|
+
submitCommand,
|
|
7
|
+
getCommandStatus,
|
|
8
|
+
getCommandResult,
|
|
9
|
+
listProjects,
|
|
10
|
+
resolveProjectId,
|
|
11
|
+
isNumericProjectRef,
|
|
12
|
+
computeIdempotencyKey,
|
|
13
|
+
runCommand,
|
|
14
|
+
executeJoeCommand,
|
|
15
|
+
readStoredSessionId,
|
|
16
|
+
writeStoredSessionId,
|
|
17
|
+
clearStoredSessionId,
|
|
18
|
+
clientSidePlanFlags,
|
|
19
|
+
formatProjectsTable,
|
|
20
|
+
formatJoeResult,
|
|
21
|
+
JOE_COMMANDS,
|
|
22
|
+
type JoeCommand,
|
|
23
|
+
type JoeStatus,
|
|
24
|
+
} from "../lib/joe";
|
|
25
|
+
import { handleToolCall, joeToolDefinitions, JOE_TOOL_TO_COMMAND, type McpToolRequest } from "../lib/mcp-server";
|
|
26
|
+
|
|
27
|
+
const BASE = "https://api.example.com";
|
|
28
|
+
const originalFetch = globalThis.fetch;
|
|
29
|
+
|
|
30
|
+
interface Captured {
|
|
31
|
+
url: string;
|
|
32
|
+
body: Record<string, unknown>;
|
|
33
|
+
headers: Record<string, string>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Install a fetch mock that routes on the /rpc/<fn> suffix and records requests. */
|
|
37
|
+
function installFetch(routes: Record<string, (body: Record<string, unknown>) => Response>): Captured[] {
|
|
38
|
+
const captured: Captured[] = [];
|
|
39
|
+
globalThis.fetch = mock((url: string, options: RequestInit) => {
|
|
40
|
+
const body = options.body ? (JSON.parse(options.body as string) as Record<string, unknown>) : {};
|
|
41
|
+
const headers = (options.headers as Record<string, string>) || {};
|
|
42
|
+
captured.push({ url, body, headers });
|
|
43
|
+
const fn = new URL(url).pathname.split("/rpc/")[1] ?? "";
|
|
44
|
+
const handler = routes[fn];
|
|
45
|
+
if (!handler) {
|
|
46
|
+
return Promise.resolve(new Response("not found", { status: 404 }));
|
|
47
|
+
}
|
|
48
|
+
return Promise.resolve(handler(body));
|
|
49
|
+
}) as unknown as typeof fetch;
|
|
50
|
+
return captured;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function json(obj: unknown, status = 200): Response {
|
|
54
|
+
return new Response(JSON.stringify(obj), { status, headers: { "Content-Type": "application/json" } });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function tempDir(): string {
|
|
58
|
+
return mkdtempSync(resolve(tmpdir(), "joe-test-"));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
globalThis.fetch = originalFetch;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("submitCommand", () => {
|
|
66
|
+
test("throws when apiKey missing", async () => {
|
|
67
|
+
await expect(
|
|
68
|
+
submitCommand({ apiKey: "", apiBaseUrl: BASE, command: "plan", projectId: 12 })
|
|
69
|
+
).rejects.toThrow("API key is required");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("maps to /rpc/joe_command_submit with the correct payload", async () => {
|
|
73
|
+
const captured = installFetch({
|
|
74
|
+
joe_command_submit: () => json({ command_id: "4711", session_id: "88", status: "queued" }),
|
|
75
|
+
});
|
|
76
|
+
const res = await submitCommand({
|
|
77
|
+
apiKey: "k",
|
|
78
|
+
apiBaseUrl: BASE,
|
|
79
|
+
command: "plan",
|
|
80
|
+
projectId: 12,
|
|
81
|
+
sql: "select 1",
|
|
82
|
+
sessionId: "88",
|
|
83
|
+
idempotencyKey: "idem-1",
|
|
84
|
+
});
|
|
85
|
+
expect(captured[0].url).toBe(`${BASE}/rpc/joe_command_submit`);
|
|
86
|
+
expect(captured[0].headers["access-token"]).toBe("k");
|
|
87
|
+
expect(captured[0].body).toEqual({
|
|
88
|
+
project_id: 12,
|
|
89
|
+
command: "plan",
|
|
90
|
+
sql: "select 1",
|
|
91
|
+
args: null,
|
|
92
|
+
session_id: "88",
|
|
93
|
+
idempotency_key: "idem-1",
|
|
94
|
+
});
|
|
95
|
+
expect(res).toEqual({ command_id: "4711", session_id: "88", status: "queued" });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("sends null sql/session_id and args for structured commands", async () => {
|
|
99
|
+
const captured = installFetch({
|
|
100
|
+
joe_command_submit: () => json({ command_id: "1", session_id: null, status: "queued" }),
|
|
101
|
+
});
|
|
102
|
+
await submitCommand({
|
|
103
|
+
apiKey: "k",
|
|
104
|
+
apiBaseUrl: BASE,
|
|
105
|
+
command: "terminate",
|
|
106
|
+
projectId: 5,
|
|
107
|
+
args: { pid: 4711 },
|
|
108
|
+
idempotencyKey: "x",
|
|
109
|
+
});
|
|
110
|
+
expect(captured[0].body.sql).toBeNull();
|
|
111
|
+
expect(captured[0].body.session_id).toBeNull();
|
|
112
|
+
expect(captured[0].body.args).toEqual({ pid: 4711 });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("surfaces the ai_enabled=false PT403 disabled-AI message", async () => {
|
|
116
|
+
installFetch({
|
|
117
|
+
joe_command_submit: () =>
|
|
118
|
+
json({ message: "AI features disabled for this org — enable in AI Assistant Settings" }, 403),
|
|
119
|
+
});
|
|
120
|
+
await expect(
|
|
121
|
+
submitCommand({ apiKey: "k", apiBaseUrl: BASE, command: "plan", projectId: 12, sql: "select 1" })
|
|
122
|
+
).rejects.toThrow(/AI features disabled for this org/);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("surfaces a scope refusal (PT403)", async () => {
|
|
126
|
+
installFetch({
|
|
127
|
+
joe_command_submit: () => json({ message: "missing scope joe:exec" }, 403),
|
|
128
|
+
});
|
|
129
|
+
await expect(
|
|
130
|
+
submitCommand({ apiKey: "k", apiBaseUrl: BASE, command: "exec", projectId: 12, sql: "create index" })
|
|
131
|
+
).rejects.toThrow(/missing scope joe:exec/);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("getCommandStatus / getCommandResult", () => {
|
|
136
|
+
test("status maps to /rpc/joe_command_status with command_id", async () => {
|
|
137
|
+
const captured = installFetch({
|
|
138
|
+
joe_command_status: () => json({ command_id: "4711", status: "running", error: null }),
|
|
139
|
+
});
|
|
140
|
+
const res = await getCommandStatus({ apiKey: "k", apiBaseUrl: BASE, commandId: "4711" });
|
|
141
|
+
expect(captured[0].url).toBe(`${BASE}/rpc/joe_command_status`);
|
|
142
|
+
expect(captured[0].body).toEqual({ command_id: "4711" });
|
|
143
|
+
expect(res.status).toBe("running");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("result maps to /rpc/joe_command_result and returns the plan body", async () => {
|
|
147
|
+
const captured = installFetch({
|
|
148
|
+
joe_command_result: () =>
|
|
149
|
+
json({
|
|
150
|
+
command_id: "4711",
|
|
151
|
+
status: "done",
|
|
152
|
+
queryid: "77",
|
|
153
|
+
plan_fingerprint: "a1b2",
|
|
154
|
+
plan_text: "Seq Scan on users",
|
|
155
|
+
plan_json: { Plan: { "Node Type": "Seq Scan", "Relation Name": "users" } },
|
|
156
|
+
error: null,
|
|
157
|
+
}),
|
|
158
|
+
});
|
|
159
|
+
const res = await getCommandResult({ apiKey: "k", apiBaseUrl: BASE, commandId: "4711" });
|
|
160
|
+
expect(captured[0].url).toBe(`${BASE}/rpc/joe_command_result`);
|
|
161
|
+
expect(res.plan_text).toBe("Seq Scan on users");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("PT404 (not found / out-of-allowlist) surfaces as an error", async () => {
|
|
165
|
+
installFetch({
|
|
166
|
+
joe_command_status: () => json({ message: "command not found" }, 404),
|
|
167
|
+
});
|
|
168
|
+
await expect(
|
|
169
|
+
getCommandStatus({ apiKey: "k", apiBaseUrl: BASE, commandId: "9999" })
|
|
170
|
+
).rejects.toThrow(/Failed to fetch command status/);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("listProjects", () => {
|
|
175
|
+
test("normalizes id/joe_api_v2_enabled/tunnel variants", async () => {
|
|
176
|
+
installFetch({
|
|
177
|
+
projects_list: () =>
|
|
178
|
+
json([
|
|
179
|
+
{ id: 12, alias: "main-db", name: "Main DB", label: "Main DB", joe_api_v2_enabled: true, tunnel_ready: true },
|
|
180
|
+
{ project_id: 13, alias: "dw", name: "Warehouse", joe_ready: false, tunnel: false },
|
|
181
|
+
]),
|
|
182
|
+
});
|
|
183
|
+
const projects = await listProjects({ apiKey: "k", apiBaseUrl: BASE });
|
|
184
|
+
expect(projects).toEqual([
|
|
185
|
+
{ project_id: 12, alias: "main-db", name: "Main DB", label: "Main DB", joe_ready: true, tunnel: true },
|
|
186
|
+
{ project_id: 13, alias: "dw", name: "Warehouse", label: null, joe_ready: false, tunnel: false },
|
|
187
|
+
]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("passes org_id when provided", async () => {
|
|
191
|
+
const captured = installFetch({ projects_list: () => json([]) });
|
|
192
|
+
await listProjects({ apiKey: "k", apiBaseUrl: BASE, orgId: 7 });
|
|
193
|
+
expect(captured[0].body).toEqual({ org_id: 7 });
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("resolveProjectId (id-or-alias)", () => {
|
|
198
|
+
test("isNumericProjectRef distinguishes ids from aliases", () => {
|
|
199
|
+
expect(isNumericProjectRef("12")).toBe(true);
|
|
200
|
+
expect(isNumericProjectRef(" 12 ")).toBe(true);
|
|
201
|
+
expect(isNumericProjectRef("main-db")).toBe(false);
|
|
202
|
+
expect(isNumericProjectRef("12a")).toBe(false);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("numeric ref resolves WITHOUT a projects lookup", async () => {
|
|
206
|
+
const captured = installFetch({ projects_list: () => json([]) });
|
|
207
|
+
const id = await resolveProjectId({ apiKey: "k", apiBaseUrl: BASE, project: "12" });
|
|
208
|
+
expect(id).toBe(12);
|
|
209
|
+
expect(captured.length).toBe(0);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("alias resolves via the projects list (case-insensitive)", async () => {
|
|
213
|
+
installFetch({
|
|
214
|
+
projects_list: () => json([{ id: 12, alias: "main-db", name: "Main DB", joe_ready: true }]),
|
|
215
|
+
});
|
|
216
|
+
const id = await resolveProjectId({ apiKey: "k", apiBaseUrl: BASE, project: "MAIN-DB" });
|
|
217
|
+
expect(id).toBe(12);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("--project 12 ≡ --project main-db", async () => {
|
|
221
|
+
installFetch({
|
|
222
|
+
projects_list: () => json([{ id: 12, alias: "main-db", name: "Main DB", joe_ready: true }]),
|
|
223
|
+
});
|
|
224
|
+
const byId = await resolveProjectId({ apiKey: "k", apiBaseUrl: BASE, project: "12" });
|
|
225
|
+
const byAlias = await resolveProjectId({ apiKey: "k", apiBaseUrl: BASE, project: "main-db" });
|
|
226
|
+
expect(byId).toBe(byAlias);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("unknown alias throws", async () => {
|
|
230
|
+
installFetch({ projects_list: () => json([{ id: 12, alias: "main-db", name: "Main DB" }]) });
|
|
231
|
+
await expect(
|
|
232
|
+
resolveProjectId({ apiKey: "k", apiBaseUrl: BASE, project: "nope" })
|
|
233
|
+
).rejects.toThrow(/Project not found for alias\/name 'nope'/);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("empty project ref throws", async () => {
|
|
237
|
+
await expect(
|
|
238
|
+
resolveProjectId({ apiKey: "k", apiBaseUrl: BASE, project: "" })
|
|
239
|
+
).rejects.toThrow(/project is required/);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe("computeIdempotencyKey", () => {
|
|
244
|
+
test("exec/explain keys are STABLE (deterministic) across calls", () => {
|
|
245
|
+
const a = computeIdempotencyKey("exec", 12, "create index on users (email)", null);
|
|
246
|
+
const b = computeIdempotencyKey("exec", 12, "create index on users (email)", null);
|
|
247
|
+
expect(a).toBe(b);
|
|
248
|
+
// A different statement diverges.
|
|
249
|
+
const c = computeIdempotencyKey("exec", 12, "create index on orders (id)", null);
|
|
250
|
+
expect(a).not.toBe(c);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("plan keys are fresh per invocation", () => {
|
|
254
|
+
const a = computeIdempotencyKey("plan", 12, "select 1", null);
|
|
255
|
+
const b = computeIdempotencyKey("plan", 12, "select 1", null);
|
|
256
|
+
expect(a).not.toBe(b);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe("runCommand — submit-then-poll", () => {
|
|
261
|
+
test("polls until terminal then fetches the result", async () => {
|
|
262
|
+
let statusCalls = 0;
|
|
263
|
+
installFetch({
|
|
264
|
+
joe_command_submit: () => json({ command_id: "4711", session_id: "88", status: "queued" }),
|
|
265
|
+
joe_command_status: () => {
|
|
266
|
+
statusCalls += 1;
|
|
267
|
+
return json({ command_id: "4711", status: statusCalls < 2 ? "running" : "done", error: null });
|
|
268
|
+
},
|
|
269
|
+
joe_command_result: () =>
|
|
270
|
+
json({ command_id: "4711", status: "done", plan_text: "Index Scan", plan_json: {}, error: null }),
|
|
271
|
+
});
|
|
272
|
+
const outcome = await runCommand({
|
|
273
|
+
apiKey: "k",
|
|
274
|
+
apiBaseUrl: BASE,
|
|
275
|
+
command: "plan",
|
|
276
|
+
projectId: 12,
|
|
277
|
+
sql: "select 1",
|
|
278
|
+
pollIntervalMs: 0,
|
|
279
|
+
sleep: async () => {},
|
|
280
|
+
});
|
|
281
|
+
expect(outcome.status).toBe("done");
|
|
282
|
+
expect(outcome.budgetExpired).toBe(false);
|
|
283
|
+
expect(outcome.commandId).toBe("4711");
|
|
284
|
+
expect(outcome.sessionId).toBe("88");
|
|
285
|
+
expect(outcome.result?.plan_text).toBe("Index Scan");
|
|
286
|
+
expect(statusCalls).toBeGreaterThanOrEqual(2);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("budget expiry returns a resume handle without a result", async () => {
|
|
290
|
+
installFetch({
|
|
291
|
+
joe_command_submit: () => json({ command_id: "4712", session_id: "88", status: "queued" }),
|
|
292
|
+
joe_command_status: () => json({ command_id: "4712", status: "running", error: null }),
|
|
293
|
+
joe_command_result: () => json({ command_id: "4712", status: "done", error: null }),
|
|
294
|
+
});
|
|
295
|
+
let clock = 1000;
|
|
296
|
+
const outcome = await runCommand({
|
|
297
|
+
apiKey: "k",
|
|
298
|
+
apiBaseUrl: BASE,
|
|
299
|
+
command: "plan",
|
|
300
|
+
projectId: 12,
|
|
301
|
+
sql: "select 1",
|
|
302
|
+
budgetMs: 5,
|
|
303
|
+
pollIntervalMs: 0,
|
|
304
|
+
now: () => (clock += 10),
|
|
305
|
+
sleep: async () => {},
|
|
306
|
+
});
|
|
307
|
+
expect(outcome.budgetExpired).toBe(true);
|
|
308
|
+
expect(outcome.status).toBe("running");
|
|
309
|
+
expect(outcome.result).toBeNull();
|
|
310
|
+
expect(outcome.commandId).toBe("4712");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("terminal error state fetches the result (error body)", async () => {
|
|
314
|
+
installFetch({
|
|
315
|
+
joe_command_submit: () => json({ command_id: "5", session_id: null, status: "queued" }),
|
|
316
|
+
joe_command_status: () => json({ command_id: "5", status: "error", error: "PT500" }),
|
|
317
|
+
joe_command_result: () => json({ command_id: "5", status: "error", error: "boom", plan_json: {} }),
|
|
318
|
+
});
|
|
319
|
+
const outcome = await runCommand({
|
|
320
|
+
apiKey: "k",
|
|
321
|
+
apiBaseUrl: BASE,
|
|
322
|
+
command: "plan",
|
|
323
|
+
projectId: 12,
|
|
324
|
+
sql: "select 1",
|
|
325
|
+
pollIntervalMs: 0,
|
|
326
|
+
sleep: async () => {},
|
|
327
|
+
});
|
|
328
|
+
expect(outcome.status).toBe("error");
|
|
329
|
+
expect(outcome.result?.error).toBe("boom");
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("exec submits a STABLE idempotency key", async () => {
|
|
333
|
+
const captured = installFetch({
|
|
334
|
+
joe_command_submit: () => json({ command_id: "6", session_id: "88", status: "queued" }),
|
|
335
|
+
joe_command_status: () => json({ command_id: "6", status: "done", error: null }),
|
|
336
|
+
joe_command_result: () => json({ command_id: "6", status: "done", row_count: 0, notices: ["CREATE INDEX"], error: null }),
|
|
337
|
+
});
|
|
338
|
+
await runCommand({
|
|
339
|
+
apiKey: "k",
|
|
340
|
+
apiBaseUrl: BASE,
|
|
341
|
+
command: "exec",
|
|
342
|
+
projectId: 12,
|
|
343
|
+
sql: "create index on users (email)",
|
|
344
|
+
pollIntervalMs: 0,
|
|
345
|
+
sleep: async () => {},
|
|
346
|
+
});
|
|
347
|
+
const expected = computeIdempotencyKey("exec", 12, "create index on users (email)", null);
|
|
348
|
+
expect(captured[0].body.idempotency_key).toBe(expected);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe("executeJoeCommand — session threading + persistence", () => {
|
|
353
|
+
test("auto-reuses the stored session id for the project", async () => {
|
|
354
|
+
const dir = tempDir();
|
|
355
|
+
writeStoredSessionId(12, "88", dir);
|
|
356
|
+
const captured = installFetch({
|
|
357
|
+
joe_command_submit: () => json({ command_id: "1", session_id: "88", status: "queued" }),
|
|
358
|
+
joe_command_status: () => json({ command_id: "1", status: "done", error: null }),
|
|
359
|
+
joe_command_result: () => json({ command_id: "1", status: "done", plan_text: "ok", error: null }),
|
|
360
|
+
});
|
|
361
|
+
await executeJoeCommand({
|
|
362
|
+
apiKey: "k",
|
|
363
|
+
apiBaseUrl: BASE,
|
|
364
|
+
command: "plan",
|
|
365
|
+
project: "12",
|
|
366
|
+
sql: "select 1",
|
|
367
|
+
sessionDir: dir,
|
|
368
|
+
pollIntervalMs: 0,
|
|
369
|
+
sleep: async () => {},
|
|
370
|
+
});
|
|
371
|
+
expect(captured[0].body.session_id).toBe("88");
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("persists the session id returned by submit", async () => {
|
|
375
|
+
const dir = tempDir();
|
|
376
|
+
installFetch({
|
|
377
|
+
joe_command_submit: () => json({ command_id: "1", session_id: "99", status: "queued" }),
|
|
378
|
+
joe_command_status: () => json({ command_id: "1", status: "done", error: null }),
|
|
379
|
+
joe_command_result: () => json({ command_id: "1", status: "done", plan_text: "ok", error: null }),
|
|
380
|
+
});
|
|
381
|
+
await executeJoeCommand({
|
|
382
|
+
apiKey: "k",
|
|
383
|
+
apiBaseUrl: BASE,
|
|
384
|
+
command: "plan",
|
|
385
|
+
project: "12",
|
|
386
|
+
sql: "select 1",
|
|
387
|
+
sessionDir: dir,
|
|
388
|
+
pollIntervalMs: 0,
|
|
389
|
+
sleep: async () => {},
|
|
390
|
+
});
|
|
391
|
+
expect(readStoredSessionId(12, dir)).toBe("99");
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test("--new-session clears the stored session and submits null", async () => {
|
|
395
|
+
const dir = tempDir();
|
|
396
|
+
writeStoredSessionId(12, "88", dir);
|
|
397
|
+
const captured = installFetch({
|
|
398
|
+
joe_command_submit: () => json({ command_id: "1", session_id: "100", status: "queued" }),
|
|
399
|
+
joe_command_status: () => json({ command_id: "1", status: "done", error: null }),
|
|
400
|
+
joe_command_result: () => json({ command_id: "1", status: "done", plan_text: "ok", error: null }),
|
|
401
|
+
});
|
|
402
|
+
await executeJoeCommand({
|
|
403
|
+
apiKey: "k",
|
|
404
|
+
apiBaseUrl: BASE,
|
|
405
|
+
command: "plan",
|
|
406
|
+
project: "12",
|
|
407
|
+
sql: "select 1",
|
|
408
|
+
newSession: true,
|
|
409
|
+
sessionDir: dir,
|
|
410
|
+
pollIntervalMs: 0,
|
|
411
|
+
sleep: async () => {},
|
|
412
|
+
});
|
|
413
|
+
expect(captured[0].body.session_id).toBeNull();
|
|
414
|
+
// The freshly returned session id is persisted for the next command.
|
|
415
|
+
expect(readStoredSessionId(12, dir)).toBe("100");
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("explicit --session overrides the stored session", async () => {
|
|
419
|
+
const dir = tempDir();
|
|
420
|
+
writeStoredSessionId(12, "88", dir);
|
|
421
|
+
const captured = installFetch({
|
|
422
|
+
joe_command_submit: () => json({ command_id: "1", session_id: "77", status: "queued" }),
|
|
423
|
+
joe_command_status: () => json({ command_id: "1", status: "done", error: null }),
|
|
424
|
+
joe_command_result: () => json({ command_id: "1", status: "done", plan_text: "ok", error: null }),
|
|
425
|
+
});
|
|
426
|
+
await executeJoeCommand({
|
|
427
|
+
apiKey: "k",
|
|
428
|
+
apiBaseUrl: BASE,
|
|
429
|
+
command: "plan",
|
|
430
|
+
project: "12",
|
|
431
|
+
sql: "select 1",
|
|
432
|
+
session: "77",
|
|
433
|
+
sessionDir: dir,
|
|
434
|
+
pollIntervalMs: 0,
|
|
435
|
+
sleep: async () => {},
|
|
436
|
+
});
|
|
437
|
+
expect(captured[0].body.session_id).toBe("77");
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("resolves an alias to its project id before submitting", async () => {
|
|
441
|
+
const dir = tempDir();
|
|
442
|
+
const captured = installFetch({
|
|
443
|
+
projects_list: () => json([{ id: 12, alias: "main-db", name: "Main DB", joe_ready: true }]),
|
|
444
|
+
joe_command_submit: () => json({ command_id: "1", session_id: "88", status: "queued" }),
|
|
445
|
+
joe_command_status: () => json({ command_id: "1", status: "done", error: null }),
|
|
446
|
+
joe_command_result: () => json({ command_id: "1", status: "done", plan_text: "ok", error: null }),
|
|
447
|
+
});
|
|
448
|
+
const outcome = await executeJoeCommand({
|
|
449
|
+
apiKey: "k",
|
|
450
|
+
apiBaseUrl: BASE,
|
|
451
|
+
command: "plan",
|
|
452
|
+
project: "main-db",
|
|
453
|
+
sql: "select 1",
|
|
454
|
+
sessionDir: dir,
|
|
455
|
+
pollIntervalMs: 0,
|
|
456
|
+
sleep: async () => {},
|
|
457
|
+
});
|
|
458
|
+
expect(outcome.projectId).toBe(12);
|
|
459
|
+
const submit = captured.find((c) => c.url.endsWith("/rpc/joe_command_submit"));
|
|
460
|
+
expect(submit?.body.project_id).toBe(12);
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
describe("session store helpers", () => {
|
|
465
|
+
test("write / read / clear round-trip", () => {
|
|
466
|
+
const dir = tempDir();
|
|
467
|
+
expect(readStoredSessionId(42, dir)).toBeNull();
|
|
468
|
+
writeStoredSessionId(42, "500", dir);
|
|
469
|
+
expect(readStoredSessionId(42, dir)).toBe("500");
|
|
470
|
+
expect(existsSync(resolve(dir, "joe-sessions.json"))).toBe(true);
|
|
471
|
+
clearStoredSessionId(42, dir);
|
|
472
|
+
expect(readStoredSessionId(42, dir)).toBeNull();
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test("does not clobber other projects' sessions", () => {
|
|
476
|
+
const dir = tempDir();
|
|
477
|
+
writeStoredSessionId(1, "a", dir);
|
|
478
|
+
writeStoredSessionId(2, "b", dir);
|
|
479
|
+
clearStoredSessionId(1, dir);
|
|
480
|
+
expect(readStoredSessionId(1, dir)).toBeNull();
|
|
481
|
+
expect(readStoredSessionId(2, dir)).toBe("b");
|
|
482
|
+
const raw = JSON.parse(readFileSync(resolve(dir, "joe-sessions.json"), "utf8"));
|
|
483
|
+
expect(raw).toEqual({ "2": "b" });
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
describe("presentation helpers", () => {
|
|
488
|
+
test("clientSidePlanFlags flags Seq Scans in nested plans", () => {
|
|
489
|
+
const flags = clientSidePlanFlags({
|
|
490
|
+
Plan: {
|
|
491
|
+
"Node Type": "Nested Loop",
|
|
492
|
+
Plans: [{ "Node Type": "Seq Scan", "Relation Name": "users" }],
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
expect(flags.length).toBe(1);
|
|
496
|
+
expect(flags[0]).toContain("Seq Scan on users");
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test("formatProjectsTable renders the brief's column header", () => {
|
|
500
|
+
const table = formatProjectsTable([
|
|
501
|
+
{ project_id: 12, alias: "main-db", name: "Main DB", label: null, joe_ready: true, tunnel: true },
|
|
502
|
+
]);
|
|
503
|
+
const [header, row] = table.split("\n");
|
|
504
|
+
expect(header).toContain("PROJECT_ID");
|
|
505
|
+
expect(header).toContain("ALIAS");
|
|
506
|
+
expect(header).toContain("JOE");
|
|
507
|
+
expect(header).toContain("TUNNEL");
|
|
508
|
+
expect(row).toContain("12");
|
|
509
|
+
expect(row).toContain("main-db");
|
|
510
|
+
expect(row).toContain("ready");
|
|
511
|
+
expect(row).toContain("yes");
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test("formatJoeResult renders per-command bodies", () => {
|
|
515
|
+
const plan = formatJoeResult("plan", {
|
|
516
|
+
command_id: "1",
|
|
517
|
+
status: "done",
|
|
518
|
+
error: null,
|
|
519
|
+
plan_text: "Seq Scan on users",
|
|
520
|
+
plan_json: { Plan: { "Node Type": "Seq Scan", "Relation Name": "users" } },
|
|
521
|
+
queryid: "77",
|
|
522
|
+
plan_fingerprint: "a1",
|
|
523
|
+
});
|
|
524
|
+
expect(plan).toContain("Seq Scan on users");
|
|
525
|
+
expect(plan).toContain("⚑");
|
|
526
|
+
|
|
527
|
+
const exec = formatJoeResult("exec", {
|
|
528
|
+
command_id: "2",
|
|
529
|
+
status: "done",
|
|
530
|
+
error: null,
|
|
531
|
+
row_count: 0,
|
|
532
|
+
notices: ["CREATE INDEX"],
|
|
533
|
+
});
|
|
534
|
+
expect(exec).toContain("CREATE INDEX");
|
|
535
|
+
|
|
536
|
+
const terminate = formatJoeResult("terminate", {
|
|
537
|
+
command_id: "3",
|
|
538
|
+
status: "done",
|
|
539
|
+
error: null,
|
|
540
|
+
terminated: true,
|
|
541
|
+
pid: 4711,
|
|
542
|
+
});
|
|
543
|
+
expect(terminate).toContain("pid 4711");
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
describe("MCP tools", () => {
|
|
548
|
+
const ROOT = { apiKey: "k", apiBaseUrl: BASE };
|
|
549
|
+
const originalXdg = process.env.XDG_CONFIG_HOME;
|
|
550
|
+
|
|
551
|
+
function makeReq(name: string, args?: Record<string, unknown>): McpToolRequest {
|
|
552
|
+
return { params: { name, arguments: args } };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
afterEach(() => {
|
|
556
|
+
globalThis.fetch = originalFetch;
|
|
557
|
+
if (originalXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
558
|
+
else process.env.XDG_CONFIG_HOME = originalXdg;
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
test("joeToolDefinitions exposes every SPEC §5 name + list_projects", () => {
|
|
562
|
+
const names = joeToolDefinitions().map((t) => t.name);
|
|
563
|
+
expect(names).toEqual([
|
|
564
|
+
"plan_query",
|
|
565
|
+
"explain_query",
|
|
566
|
+
"exec_sql",
|
|
567
|
+
"hypo_index",
|
|
568
|
+
"activity",
|
|
569
|
+
"terminate_backend",
|
|
570
|
+
"reset_clone",
|
|
571
|
+
"describe",
|
|
572
|
+
"list_projects",
|
|
573
|
+
]);
|
|
574
|
+
// Tool-name -> command map covers the 8 command tools.
|
|
575
|
+
expect(Object.keys(JOE_TOOL_TO_COMMAND).length).toBe(8);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test("exec_sql carries the stronger raw-data warning; plan/explain note the plan egress", () => {
|
|
579
|
+
const defs = joeToolDefinitions();
|
|
580
|
+
const exec = defs.find((t) => t.name === "exec_sql");
|
|
581
|
+
expect(exec?.description).toMatch(/REAL .*TABLE ROWS/i);
|
|
582
|
+
expect(exec?.description).toContain("LLM context");
|
|
583
|
+
const plan = defs.find((t) => t.name === "plan_query");
|
|
584
|
+
expect(plan?.description).toMatch(/plan-only/i);
|
|
585
|
+
expect(plan?.description).toMatch(/NEVER executes/i);
|
|
586
|
+
const explain = defs.find((t) => t.name === "explain_query");
|
|
587
|
+
expect(explain?.description).toMatch(/EXECUTES/);
|
|
588
|
+
expect(explain?.description).toMatch(/NOT raw SELECT result rows/i);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test("plan_query calls executeJoeCommand and returns the plan result", () => {
|
|
592
|
+
process.env.XDG_CONFIG_HOME = tempDir();
|
|
593
|
+
const captured = installFetch({
|
|
594
|
+
joe_command_submit: () => json({ command_id: "4711", session_id: "88", status: "queued" }),
|
|
595
|
+
joe_command_status: () => json({ command_id: "4711", status: "done", error: null }),
|
|
596
|
+
joe_command_result: () =>
|
|
597
|
+
json({ command_id: "4711", status: "done", plan_text: "Seq Scan on users", plan_json: {}, error: null }),
|
|
598
|
+
});
|
|
599
|
+
return handleToolCall(makeReq("plan_query", { sql: "select 1", project_id: "12" }), ROOT).then((res) => {
|
|
600
|
+
expect(res.isError).toBeFalsy();
|
|
601
|
+
const submit = captured.find((c) => c.url.endsWith("/rpc/joe_command_submit"));
|
|
602
|
+
expect(submit?.body.command).toBe("plan");
|
|
603
|
+
expect(submit?.body.project_id).toBe(12);
|
|
604
|
+
const payload = JSON.parse(res.content[0].text) as { status: string; result: { plan_text: string } };
|
|
605
|
+
expect(payload.status).toBe("done");
|
|
606
|
+
expect(payload.result.plan_text).toBe("Seq Scan on users");
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test("exec_sql maps sql; requires it", async () => {
|
|
611
|
+
process.env.XDG_CONFIG_HOME = tempDir();
|
|
612
|
+
const captured = installFetch({
|
|
613
|
+
joe_command_submit: () => json({ command_id: "1", session_id: "88", status: "queued" }),
|
|
614
|
+
joe_command_status: () => json({ command_id: "1", status: "done", error: null }),
|
|
615
|
+
joe_command_result: () => json({ command_id: "1", command: "exec", status: "done", row_count: 0, notices: ["CREATE INDEX"], error: null }),
|
|
616
|
+
});
|
|
617
|
+
const ok = await handleToolCall(makeReq("exec_sql", { sql: "create index on users (email)", project_id: "12" }), ROOT);
|
|
618
|
+
expect(ok.isError).toBeFalsy();
|
|
619
|
+
const submit = captured.find((c) => c.url.endsWith("/rpc/joe_command_submit"));
|
|
620
|
+
expect(submit?.body.command).toBe("exec");
|
|
621
|
+
|
|
622
|
+
const missing = await handleToolCall(makeReq("exec_sql", { project_id: "12" }), ROOT);
|
|
623
|
+
expect(missing.isError).toBe(true);
|
|
624
|
+
expect(missing.content[0].text).toContain("sql is required");
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
test("hypo_index requires query and maps args.query", async () => {
|
|
628
|
+
process.env.XDG_CONFIG_HOME = tempDir();
|
|
629
|
+
const captured = installFetch({
|
|
630
|
+
joe_command_submit: () => json({ command_id: "1", session_id: "88", status: "queued" }),
|
|
631
|
+
joe_command_status: () => json({ command_id: "1", status: "done", error: null }),
|
|
632
|
+
joe_command_result: () => json({ command_id: "1", status: "done", hypo_used: true, hypo_plan: {}, error: null }),
|
|
633
|
+
});
|
|
634
|
+
const missing = await handleToolCall(makeReq("hypo_index", { sql: "create index on orders (customer_id)", project_id: "12" }), ROOT);
|
|
635
|
+
expect(missing.isError).toBe(true);
|
|
636
|
+
expect(missing.content[0].text).toContain("query is required");
|
|
637
|
+
|
|
638
|
+
const ok = await handleToolCall(
|
|
639
|
+
makeReq("hypo_index", { sql: "create index on orders (customer_id)", query: "select 1", project_id: "12" }),
|
|
640
|
+
ROOT
|
|
641
|
+
);
|
|
642
|
+
expect(ok.isError).toBeFalsy();
|
|
643
|
+
const submit = captured.find((c) => c.url.endsWith("/rpc/joe_command_submit"));
|
|
644
|
+
expect(submit?.body.command).toBe("hypo");
|
|
645
|
+
expect(submit?.body.args).toEqual({ query: "select 1" });
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
test("terminate_backend maps args.pid; describe maps args.object", async () => {
|
|
649
|
+
process.env.XDG_CONFIG_HOME = tempDir();
|
|
650
|
+
const captured = installFetch({
|
|
651
|
+
joe_command_submit: () => json({ command_id: "1", session_id: null, status: "queued" }),
|
|
652
|
+
joe_command_status: () => json({ command_id: "1", status: "done", error: null }),
|
|
653
|
+
joe_command_result: () => json({ command_id: "1", status: "done", terminated: true, pid: 4711, snapshot: {}, error: null }),
|
|
654
|
+
});
|
|
655
|
+
await handleToolCall(makeReq("terminate_backend", { pid: 4711, project_id: "12" }), ROOT);
|
|
656
|
+
let submit = captured.find((c) => c.url.endsWith("/rpc/joe_command_submit"));
|
|
657
|
+
expect(submit?.body.command).toBe("terminate");
|
|
658
|
+
expect(submit?.body.args).toEqual({ pid: 4711 });
|
|
659
|
+
|
|
660
|
+
captured.length = 0;
|
|
661
|
+
await handleToolCall(makeReq("describe", { object: "users", variant: "\\d+", project_id: "12" }), ROOT);
|
|
662
|
+
submit = captured.find((c) => c.url.endsWith("/rpc/joe_command_submit"));
|
|
663
|
+
expect(submit?.body.command).toBe("describe");
|
|
664
|
+
expect(submit?.body.args).toEqual({ object: "users", variant: "\\d+" });
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
test("list_projects tool returns the normalized projects", async () => {
|
|
668
|
+
process.env.XDG_CONFIG_HOME = tempDir();
|
|
669
|
+
installFetch({
|
|
670
|
+
projects_list: () => json([{ id: 12, alias: "main-db", name: "Main DB", joe_ready: true, tunnel: true }]),
|
|
671
|
+
});
|
|
672
|
+
const res = await handleToolCall(makeReq("list_projects", {}), ROOT);
|
|
673
|
+
expect(res.isError).toBeFalsy();
|
|
674
|
+
const projects = JSON.parse(res.content[0].text) as Array<{ project_id: number; alias: string }>;
|
|
675
|
+
expect(projects[0].project_id).toBe(12);
|
|
676
|
+
expect(projects[0].alias).toBe("main-db");
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
test("resolves an alias project_id before submitting", async () => {
|
|
680
|
+
process.env.XDG_CONFIG_HOME = tempDir();
|
|
681
|
+
const captured = installFetch({
|
|
682
|
+
projects_list: () => json([{ id: 12, alias: "main-db", name: "Main DB", joe_ready: true }]),
|
|
683
|
+
joe_command_submit: () => json({ command_id: "1", session_id: "88", status: "queued" }),
|
|
684
|
+
joe_command_status: () => json({ command_id: "1", status: "done", error: null }),
|
|
685
|
+
joe_command_result: () => json({ command_id: "1", status: "done", plan_text: "ok", error: null }),
|
|
686
|
+
});
|
|
687
|
+
await handleToolCall(makeReq("plan_query", { sql: "select 1", project_id: "main-db" }), ROOT);
|
|
688
|
+
const submit = captured.find((c) => c.url.endsWith("/rpc/joe_command_submit"));
|
|
689
|
+
expect(submit?.body.project_id).toBe(12);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
test("ai_enabled=false PT403 surfaces as an MCP error", async () => {
|
|
693
|
+
process.env.XDG_CONFIG_HOME = tempDir();
|
|
694
|
+
installFetch({
|
|
695
|
+
joe_command_submit: () =>
|
|
696
|
+
json({ message: "AI features disabled for this org — enable in AI Assistant Settings" }, 403),
|
|
697
|
+
});
|
|
698
|
+
const res = await handleToolCall(makeReq("plan_query", { sql: "select 1", project_id: "12" }), ROOT);
|
|
699
|
+
expect(res.isError).toBe(true);
|
|
700
|
+
expect(res.content[0].text).toContain("AI features disabled for this org");
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
test("missing project_id is an error", async () => {
|
|
704
|
+
process.env.XDG_CONFIG_HOME = tempDir();
|
|
705
|
+
const res = await handleToolCall(makeReq("plan_query", { sql: "select 1" }), ROOT);
|
|
706
|
+
expect(res.isError).toBe(true);
|
|
707
|
+
expect(res.content[0].text).toContain("project_id is required");
|
|
708
|
+
});
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
describe("command set", () => {
|
|
712
|
+
test("JOE_COMMANDS is the SPEC command set", () => {
|
|
713
|
+
const expected: JoeCommand[] = ["plan", "explain", "exec", "hypo", "activity", "terminate", "reset", "describe"];
|
|
714
|
+
expect([...JOE_COMMANDS]).toEqual(expected);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
test("each command submits with its fixed command name", async () => {
|
|
718
|
+
for (const command of JOE_COMMANDS) {
|
|
719
|
+
const captured = installFetch({
|
|
720
|
+
joe_command_submit: () => json({ command_id: "1", session_id: null, status: "queued" as JoeStatus }),
|
|
721
|
+
});
|
|
722
|
+
await submitCommand({ apiKey: "k", apiBaseUrl: BASE, command, projectId: 12, idempotencyKey: "x" });
|
|
723
|
+
expect(captured[0].body.command).toBe(command);
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
});
|