postgresai 0.16.0-dev.3 → 0.16.0-dev.5
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/bun.lock +4 -4
- package/dist/bin/postgres-ai.js +695 -735
- package/lib/dblab.ts +38 -61
- package/lib/joe.ts +9 -0
- package/package.json +1 -1
- package/test/dblab.cli.test.ts +10 -10
- package/test/dblab.test.ts +45 -31
- package/test/joe.test.ts +4 -4
package/lib/dblab.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { formatHttpError, maskSecret, normalizeBaseUrl } from "./util";
|
|
27
|
+
import { listProjects, type ProjectListItem } from "./joe";
|
|
27
28
|
|
|
28
29
|
// ---------------------------------------------------------------------------
|
|
29
30
|
// Types
|
|
@@ -51,14 +52,12 @@ interface DblabCommon {
|
|
|
51
52
|
// ---------------------------------------------------------------------------
|
|
52
53
|
// Project → DBLab instance resolution
|
|
53
54
|
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
// resolver + per-project instance surfacing land, collapse this into that shared
|
|
61
|
-
// path so `--project` id/alias resolution lives in exactly one place.
|
|
55
|
+
// Resolution rides the org-level projects listing rpc (`v1.projects_list`,
|
|
56
|
+
// S.1): its rows carry each project's `dblab_instance_id` (DB.4), and the rpc
|
|
57
|
+
// authenticates with the CLI's opaque org `access-token` header. The previous
|
|
58
|
+
// interim resolver read the `v1.dblab_instances` PostgREST view, which is
|
|
59
|
+
// JWT-scoped — permanently empty/403 for a token-only caller — so it never
|
|
60
|
+
// worked outside mocks and was dropped at wiring (dedupe note resolved).
|
|
62
61
|
// ---------------------------------------------------------------------------
|
|
63
62
|
|
|
64
63
|
/** A bare numeric `--project` value is a project id; anything else is an alias/name. */
|
|
@@ -71,23 +70,21 @@ export interface ResolveDblabInstanceParams {
|
|
|
71
70
|
apiBaseUrl: string;
|
|
72
71
|
/** `--project <id|alias>` — a numeric project id, or a project alias/name. */
|
|
73
72
|
project: string;
|
|
74
|
-
/** Optional org scope (narrows the
|
|
73
|
+
/** Optional org scope (narrows the projects listing). */
|
|
75
74
|
orgId?: number;
|
|
76
75
|
debug?: boolean;
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
/**
|
|
80
|
-
* Resolve `--project <id|alias>` to
|
|
79
|
+
* Resolve `--project <id|alias>` to the project's single DBLab `instance_id`.
|
|
81
80
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* `
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* number-precision loss.
|
|
81
|
+
* Lists the org's projects via `v1.projects_list` (the same call behind
|
|
82
|
+
* `pgai projects`): a numeric ref matches `project_id`; anything else matches
|
|
83
|
+
* `alias` / `name` / `label` (case-insensitive). Each project has at most one
|
|
84
|
+
* active DBLab instance (`dblab_instance_id`). The returned id is a string so
|
|
85
|
+
* a 64-bit id survives without JS number-precision loss.
|
|
88
86
|
*/
|
|
89
87
|
export async function resolveDblabInstanceId(params: ResolveDblabInstanceParams): Promise<string> {
|
|
90
|
-
// TODO(joe-v2): unify --project resolution once projects_list (S.1) returns the dblab instance id
|
|
91
88
|
const { apiKey, apiBaseUrl, orgId, debug } = params;
|
|
92
89
|
if (!apiKey) {
|
|
93
90
|
throw new Error("API key is required");
|
|
@@ -97,55 +94,24 @@ export async function resolveDblabInstanceId(params: ResolveDblabInstanceParams)
|
|
|
97
94
|
throw new Error("project is required (--project <id|alias>)");
|
|
98
95
|
}
|
|
99
96
|
|
|
100
|
-
|
|
101
|
-
const url = new URL(`${base}/dblab_instances`);
|
|
102
|
-
url.searchParams.set("select", "id,project_id,project_name,project_alias,project_label_or_name,org_id");
|
|
103
|
-
if (typeof orgId === "number") {
|
|
104
|
-
url.searchParams.set("org_id", `eq.${orgId}`);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const headers: Record<string, string> = {
|
|
108
|
-
"access-token": apiKey,
|
|
109
|
-
"Content-Type": "application/json",
|
|
110
|
-
"Connection": "close",
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
if (debug) {
|
|
114
|
-
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
115
|
-
console.error(`Debug: GET URL: ${url.toString()}`);
|
|
116
|
-
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const response = await fetch(url.toString(), { method: "GET", headers });
|
|
120
|
-
const text = await response.text();
|
|
121
|
-
|
|
122
|
-
if (debug) {
|
|
123
|
-
console.error(`Debug: Response status: ${response.status}`);
|
|
124
|
-
console.error(`Debug: Response body: ${text}`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (!response.ok) {
|
|
128
|
-
throw new Error(formatHttpError("Failed to resolve project's DBLab instance", response.status, text));
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
let rows: DblabInstanceRow[];
|
|
97
|
+
let projects: ProjectListItem[];
|
|
132
98
|
try {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
throw new Error(`Failed to
|
|
99
|
+
projects = await listProjects({ apiKey, apiBaseUrl, orgId, debug });
|
|
100
|
+
} catch (err) {
|
|
101
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
102
|
+
throw new Error(`Failed to resolve project's DBLab instance: ${message}`);
|
|
137
103
|
}
|
|
138
104
|
|
|
139
105
|
const numeric = isNumericProjectRef(ref);
|
|
140
106
|
const needle = ref.toLowerCase();
|
|
141
|
-
const match =
|
|
107
|
+
const match = projects.find((p) => {
|
|
142
108
|
if (numeric) {
|
|
143
|
-
return String(
|
|
109
|
+
return String(p.project_id) === ref;
|
|
144
110
|
}
|
|
145
111
|
return (
|
|
146
|
-
(
|
|
147
|
-
(
|
|
148
|
-
(
|
|
112
|
+
(p.alias !== null && p.alias.toLowerCase() === needle) ||
|
|
113
|
+
(p.name !== null && p.name.toLowerCase() === needle) ||
|
|
114
|
+
(p.label !== null && p.label.toLowerCase() === needle)
|
|
149
115
|
);
|
|
150
116
|
});
|
|
151
117
|
|
|
@@ -154,7 +120,12 @@ export async function resolveDblabInstanceId(params: ResolveDblabInstanceParams)
|
|
|
154
120
|
`No DBLab instance found for project '${ref}'. Run 'pgai projects' to see available projects.`
|
|
155
121
|
);
|
|
156
122
|
}
|
|
157
|
-
|
|
123
|
+
if (match.dblab_instance_id == null) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Project '${ref}' has no active DBLab instance. Register a DBLab instance for it in the Console first.`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return String(match.dblab_instance_id);
|
|
158
129
|
}
|
|
159
130
|
|
|
160
131
|
// ---------------------------------------------------------------------------
|
|
@@ -444,11 +415,17 @@ export interface DestroySnapshotParams extends DblabCommon {
|
|
|
444
415
|
force?: boolean;
|
|
445
416
|
}
|
|
446
417
|
|
|
447
|
-
/** Destroy a snapshot — `/snapshot/<id>?force=<bool>` DELETE (Console `destroySnapshot`).
|
|
418
|
+
/** Destroy a snapshot — `/snapshot/<id>?force=<bool>` DELETE (Console `destroySnapshot`).
|
|
419
|
+
*
|
|
420
|
+
* The snapshot id is a MULTI-SEGMENT zfs path (`pool/branch/<b>/<clone>/r0@snap`),
|
|
421
|
+
* which the DBLab engine routes as a wildcard path — it must be passed RAW,
|
|
422
|
+
* exactly as the Console does. `encodeURIComponent` here turned `/`→`%2F` and
|
|
423
|
+
* `@`→`%40`, which the engine rejects with 400 `invalid snapshot name given`
|
|
424
|
+
* (verified live against DBLab CE 4.1.3). */
|
|
448
425
|
export async function destroySnapshot<T = unknown>(params: DestroySnapshotParams): Promise<T> {
|
|
449
426
|
const { apiKey, apiBaseUrl, instanceId, snapshotId, force, debug } = params;
|
|
450
427
|
if (!snapshotId) throw new Error("snapshotId is required");
|
|
451
|
-
const action = `/snapshot/${
|
|
428
|
+
const action = `/snapshot/${snapshotId}?force=${Boolean(force)}`;
|
|
452
429
|
return callDblabApi<T>({
|
|
453
430
|
apiKey, apiBaseUrl, instanceId,
|
|
454
431
|
action, method: "delete",
|
package/lib/joe.ts
CHANGED
|
@@ -119,6 +119,11 @@ export interface ProjectListItem {
|
|
|
119
119
|
joe_ready: boolean;
|
|
120
120
|
/** Whether the project's DBLab tunnel is connected. */
|
|
121
121
|
tunnel: boolean;
|
|
122
|
+
/** The project's active JOE instance id (the M1a submit/status/result target). */
|
|
123
|
+
instance_id: number | null;
|
|
124
|
+
/** The project's active DBLAB instance id — the SPEC §8 DBLab-companion resolver
|
|
125
|
+
* passes this to `v1.dblab_api_call` (distinct from the Joe `instance_id`). */
|
|
126
|
+
dblab_instance_id: number | null;
|
|
122
127
|
}
|
|
123
128
|
|
|
124
129
|
// ---------------------------------------------------------------------------
|
|
@@ -285,6 +290,8 @@ interface RawProjectRow {
|
|
|
285
290
|
joe_api_v2_enabled?: boolean;
|
|
286
291
|
tunnel?: boolean;
|
|
287
292
|
tunnel_ready?: boolean;
|
|
293
|
+
instance_id?: number | null;
|
|
294
|
+
dblab_instance_id?: number | null;
|
|
288
295
|
}
|
|
289
296
|
|
|
290
297
|
function normalizeProjectRow(row: RawProjectRow): ProjectListItem {
|
|
@@ -296,6 +303,8 @@ function normalizeProjectRow(row: RawProjectRow): ProjectListItem {
|
|
|
296
303
|
label: row.label ?? null,
|
|
297
304
|
joe_ready: Boolean(row.joe_ready ?? row.joe_api_v2_enabled ?? false),
|
|
298
305
|
tunnel: Boolean(row.tunnel ?? row.tunnel_ready ?? false),
|
|
306
|
+
instance_id: row.instance_id == null ? null : Number(row.instance_id),
|
|
307
|
+
dblab_instance_id: row.dblab_instance_id == null ? null : Number(row.dblab_instance_id),
|
|
299
308
|
};
|
|
300
309
|
}
|
|
301
310
|
|
package/package.json
CHANGED
package/test/dblab.cli.test.ts
CHANGED
|
@@ -48,14 +48,14 @@ interface Recorded {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* Fake Platform API: serves the `/
|
|
52
|
-
*
|
|
53
|
-
* payload the CLI produced.
|
|
51
|
+
* Fake Platform API: serves the `/rpc/projects_list` resolver listing (rows
|
|
52
|
+
* carry `dblab_instance_id`, S.1/DB.4) and echoes every `/rpc/dblab_api_call`
|
|
53
|
+
* proxy call so tests can assert the action/method/payload the CLI produced.
|
|
54
54
|
*/
|
|
55
|
-
async function startFakeApi(
|
|
55
|
+
async function startFakeApi(projects?: unknown[]) {
|
|
56
56
|
const requests: Recorded[] = [];
|
|
57
|
-
const rows =
|
|
58
|
-
{
|
|
57
|
+
const rows = projects ?? [
|
|
58
|
+
{ project_id: 12, alias: "main-db", name: "Main DB", label: "Main DB", joe_ready: true, tunnel: false, instance_id: 1, dblab_instance_id: 7 },
|
|
59
59
|
];
|
|
60
60
|
|
|
61
61
|
const server = Bun.serve({
|
|
@@ -68,7 +68,7 @@ async function startFakeApi(instances?: unknown[]) {
|
|
|
68
68
|
try { bodyJson = bodyText ? JSON.parse(bodyText) : null; } catch { bodyJson = null; }
|
|
69
69
|
requests.push({ method: req.method, pathname: url.pathname, search: url.search, bodyJson });
|
|
70
70
|
|
|
71
|
-
if (req.method === "
|
|
71
|
+
if (req.method === "POST" && url.pathname.endsWith("/rpc/projects_list")) {
|
|
72
72
|
return new Response(JSON.stringify(rows), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
73
73
|
}
|
|
74
74
|
if (req.method === "POST" && url.pathname.endsWith("/rpc/dblab_api_call")) {
|
|
@@ -88,7 +88,7 @@ async function startFakeApi(instances?: unknown[]) {
|
|
|
88
88
|
baseUrl,
|
|
89
89
|
requests,
|
|
90
90
|
proxyCalls: () => requests.filter((r) => r.pathname.endsWith("/rpc/dblab_api_call")),
|
|
91
|
-
resolverCalls: () => requests.filter((r) => r.pathname.endsWith("/
|
|
91
|
+
resolverCalls: () => requests.filter((r) => r.pathname.endsWith("/rpc/projects_list")),
|
|
92
92
|
stop: () => server.stop(true),
|
|
93
93
|
};
|
|
94
94
|
}
|
|
@@ -318,8 +318,8 @@ describe("CLI DBLab companion command groups", () => {
|
|
|
318
318
|
port: 0,
|
|
319
319
|
async fetch(req) {
|
|
320
320
|
const url = new URL(req.url);
|
|
321
|
-
if (req.method === "
|
|
322
|
-
return new Response(JSON.stringify([{
|
|
321
|
+
if (req.method === "POST" && url.pathname.endsWith("/rpc/projects_list")) {
|
|
322
|
+
return new Response(JSON.stringify([{ project_id: 12, alias: "main-db", dblab_instance_id: 7 }]), { status: 200 });
|
|
323
323
|
}
|
|
324
324
|
return new Response('{"message":"insufficient scope: joe:admin required"}', { status: 403 });
|
|
325
325
|
},
|
package/test/dblab.test.ts
CHANGED
|
@@ -40,13 +40,13 @@ function stubFetch(body: unknown, status = 200): { get: () => Captured | null }
|
|
|
40
40
|
return { get: () => captured };
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/** Route fetch by URL: `/
|
|
44
|
-
function routeFetch(
|
|
43
|
+
/** Route fetch by URL: `/rpc/projects_list` → projects, `/rpc/dblab_api_call` → reply. */
|
|
44
|
+
function routeFetch(projects: unknown[], reply: unknown): { calls: Captured[] } {
|
|
45
45
|
const calls: Captured[] = [];
|
|
46
46
|
globalThis.fetch = mock((url: string, options: RequestInit) => {
|
|
47
47
|
calls.push({ url, options });
|
|
48
|
-
if (String(url).includes("/
|
|
49
|
-
return Promise.resolve(new Response(JSON.stringify(
|
|
48
|
+
if (String(url).includes("/rpc/projects_list")) {
|
|
49
|
+
return Promise.resolve(new Response(JSON.stringify(projects), { status: 200 }));
|
|
50
50
|
}
|
|
51
51
|
return Promise.resolve(new Response(JSON.stringify(reply), { status: 200 }));
|
|
52
52
|
}) as unknown as typeof fetch;
|
|
@@ -66,9 +66,10 @@ afterEach(() => {
|
|
|
66
66
|
// ---------------------------------------------------------------------------
|
|
67
67
|
|
|
68
68
|
describe("resolveDblabInstanceId", () => {
|
|
69
|
-
const
|
|
70
|
-
{
|
|
71
|
-
{
|
|
69
|
+
const PROJECTS = [
|
|
70
|
+
{ project_id: 12, alias: "main-db", name: "Main DB", label: "Main DB", joe_ready: true, tunnel: false, instance_id: 1, dblab_instance_id: 7 },
|
|
71
|
+
{ project_id: 34, alias: "analytics", name: "Analytics", label: null, joe_ready: false, tunnel: false, instance_id: null, dblab_instance_id: 9 },
|
|
72
|
+
{ project_id: 56, alias: "no-dblab", name: "No DBLab", label: null, joe_ready: false, tunnel: false, instance_id: null, dblab_instance_id: null },
|
|
72
73
|
];
|
|
73
74
|
|
|
74
75
|
test("throws when apiKey is missing", async () => {
|
|
@@ -89,42 +90,48 @@ describe("resolveDblabInstanceId", () => {
|
|
|
89
90
|
expect(isNumericProjectRef("main-db")).toBe(false);
|
|
90
91
|
});
|
|
91
92
|
|
|
92
|
-
test("resolves a numeric project id to its instance id (string)", async () => {
|
|
93
|
-
const cap = stubFetch(
|
|
93
|
+
test("resolves a numeric project id to its dblab instance id (string) via projects_list", async () => {
|
|
94
|
+
const cap = stubFetch(PROJECTS);
|
|
94
95
|
const id = await resolveDblabInstanceId({ apiKey: "k", apiBaseUrl: API, project: "34" });
|
|
95
96
|
expect(id).toBe("9");
|
|
96
97
|
const c = cap.get()!;
|
|
97
|
-
expect(c.url).
|
|
98
|
-
expect(c.
|
|
99
|
-
expect(c.options.method).toBe("GET");
|
|
98
|
+
expect(c.url).toBe(`${API}/rpc/projects_list`);
|
|
99
|
+
expect(c.options.method).toBe("POST");
|
|
100
100
|
expect((c.options.headers as Record<string, string>)["access-token"]).toBe("k");
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
-
test("resolves an alias (case-insensitive) to its instance id", async () => {
|
|
104
|
-
stubFetch(
|
|
103
|
+
test("resolves an alias (case-insensitive) to its dblab instance id", async () => {
|
|
104
|
+
stubFetch(PROJECTS);
|
|
105
105
|
const id = await resolveDblabInstanceId({ apiKey: "k", apiBaseUrl: API, project: "Main-DB" });
|
|
106
106
|
expect(id).toBe("7");
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
-
test("resolves a project name to its instance id", async () => {
|
|
110
|
-
stubFetch(
|
|
109
|
+
test("resolves a project name to its dblab instance id", async () => {
|
|
110
|
+
stubFetch(PROJECTS);
|
|
111
111
|
const id = await resolveDblabInstanceId({ apiKey: "k", apiBaseUrl: API, project: "analytics" });
|
|
112
112
|
expect(id).toBe("9");
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
-
test("
|
|
116
|
-
const cap = stubFetch(
|
|
115
|
+
test("passes org_id in the rpc body when orgId is provided", async () => {
|
|
116
|
+
const cap = stubFetch(PROJECTS);
|
|
117
117
|
await resolveDblabInstanceId({ apiKey: "k", apiBaseUrl: API, project: "12", orgId: 5 });
|
|
118
|
-
expect(
|
|
118
|
+
expect(bodyOf(cap.get()).org_id).toBe(5);
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
-
test("throws a helpful error when no
|
|
122
|
-
stubFetch(
|
|
121
|
+
test("throws a helpful error when no project matches", async () => {
|
|
122
|
+
stubFetch(PROJECTS);
|
|
123
123
|
await expect(
|
|
124
124
|
resolveDblabInstanceId({ apiKey: "k", apiBaseUrl: API, project: "nope" })
|
|
125
125
|
).rejects.toThrow(/No DBLab instance found for project 'nope'/);
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
+
test("throws when the project exists but has no active DBLab instance", async () => {
|
|
129
|
+
stubFetch(PROJECTS);
|
|
130
|
+
await expect(
|
|
131
|
+
resolveDblabInstanceId({ apiKey: "k", apiBaseUrl: API, project: "no-dblab" })
|
|
132
|
+
).rejects.toThrow(/has no active DBLab instance/);
|
|
133
|
+
});
|
|
134
|
+
|
|
128
135
|
test("surfaces an HTTP error from the listing", async () => {
|
|
129
136
|
stubFetch('{"message":"boom"}', 500);
|
|
130
137
|
await expect(
|
|
@@ -295,6 +302,13 @@ describe("snapshot verbs → dblab_api_call", () => {
|
|
|
295
302
|
expect(b.method).toBe("delete");
|
|
296
303
|
});
|
|
297
304
|
|
|
305
|
+
test("destroySnapshot passes a multi-segment zfs snapshot id RAW (no percent-encoding)", async () => {
|
|
306
|
+
const cap = stubFetch(true);
|
|
307
|
+
await destroySnapshot({ ...common, snapshotId: "dblab_pool/branch/main/c-1/r0@20260707065703" });
|
|
308
|
+
// The engine 400s on %2F/%40 — the Console passes the id raw and so must we.
|
|
309
|
+
expect(bodyOf(cap.get()).action).toBe("/snapshot/dblab_pool/branch/main/c-1/r0@20260707065703?force=false");
|
|
310
|
+
});
|
|
311
|
+
|
|
298
312
|
test("destroySnapshot defaults force=false", async () => {
|
|
299
313
|
const cap = stubFetch(true);
|
|
300
314
|
await destroySnapshot({ ...common, snapshotId: "s-1" });
|
|
@@ -330,12 +344,12 @@ describe("proxy error paths", () => {
|
|
|
330
344
|
|
|
331
345
|
describe("project → instance → dblab_api_call", () => {
|
|
332
346
|
test("a verb driven off a resolved instance sends both requests", async () => {
|
|
333
|
-
const
|
|
334
|
-
const { calls } = routeFetch(
|
|
347
|
+
const projects = [{ project_id: 12, alias: "main-db", dblab_instance_id: 7 }];
|
|
348
|
+
const { calls } = routeFetch(projects, { id: "c-1", status: "ready" });
|
|
335
349
|
const instanceId = await resolveDblabInstanceId({ apiKey: "k", apiBaseUrl: API, project: "main-db" });
|
|
336
350
|
await createClone({ apiKey: "k", apiBaseUrl: API, instanceId });
|
|
337
351
|
expect(calls.length).toBe(2);
|
|
338
|
-
expect(calls[0].url).toContain("/
|
|
352
|
+
expect(calls[0].url).toContain("/rpc/projects_list");
|
|
339
353
|
expect(calls[1].url).toBe(`${API}/rpc/dblab_api_call`);
|
|
340
354
|
expect(bodyOf(calls[1]).instance_id).toBe("7");
|
|
341
355
|
});
|
|
@@ -349,14 +363,14 @@ describe("MCP DBLab tools (handleToolCall)", () => {
|
|
|
349
363
|
const rootOpts = { apiKey: "k", apiBaseUrl: API };
|
|
350
364
|
|
|
351
365
|
test("clone_create resolves project then proxies /clone POST", async () => {
|
|
352
|
-
const
|
|
353
|
-
const { calls } = routeFetch(
|
|
366
|
+
const projects = [{ project_id: 12, alias: "main-db", dblab_instance_id: 7 }];
|
|
367
|
+
const { calls } = routeFetch(projects, { ok: true });
|
|
354
368
|
const res = await handleToolCall(
|
|
355
369
|
{ params: { name: "clone_create", arguments: { project: "main-db" } } },
|
|
356
370
|
rootOpts
|
|
357
371
|
);
|
|
358
372
|
expect(res.isError).toBeFalsy();
|
|
359
|
-
expect(calls[0].url).toContain("/
|
|
373
|
+
expect(calls[0].url).toContain("/rpc/projects_list");
|
|
360
374
|
const b = bodyOf(calls[1]);
|
|
361
375
|
expect(b.instance_id).toBe("7");
|
|
362
376
|
expect(b.action).toBe("/clone");
|
|
@@ -364,8 +378,8 @@ describe("MCP DBLab tools (handleToolCall)", () => {
|
|
|
364
378
|
});
|
|
365
379
|
|
|
366
380
|
test("clone_destroy proxies /clone/<id> DELETE", async () => {
|
|
367
|
-
const
|
|
368
|
-
const { calls } = routeFetch(
|
|
381
|
+
const projects = [{ project_id: 12, alias: "main-db", dblab_instance_id: 7 }];
|
|
382
|
+
const { calls } = routeFetch(projects, { ok: true });
|
|
369
383
|
const res = await handleToolCall(
|
|
370
384
|
{ params: { name: "clone_destroy", arguments: { project: "main-db", clone_id: "c-1" } } },
|
|
371
385
|
rootOpts
|
|
@@ -377,8 +391,8 @@ describe("MCP DBLab tools (handleToolCall)", () => {
|
|
|
377
391
|
});
|
|
378
392
|
|
|
379
393
|
test("snapshot_create proxies /branch/snapshot POST with cloneID", async () => {
|
|
380
|
-
const
|
|
381
|
-
const { calls } = routeFetch(
|
|
394
|
+
const projects = [{ project_id: 12, alias: "main-db", dblab_instance_id: 7 }];
|
|
395
|
+
const { calls } = routeFetch(projects, { ok: true });
|
|
382
396
|
await handleToolCall(
|
|
383
397
|
{ params: { name: "snapshot_create", arguments: { project: "main-db", clone_id: "c-1", message: "m" } } },
|
|
384
398
|
rootOpts
|
package/test/joe.test.ts
CHANGED
|
@@ -176,14 +176,14 @@ describe("listProjects", () => {
|
|
|
176
176
|
installFetch({
|
|
177
177
|
projects_list: () =>
|
|
178
178
|
json([
|
|
179
|
-
{ id: 12, alias: "main-db", name: "Main DB", label: "Main DB", joe_api_v2_enabled: true, tunnel_ready: true },
|
|
179
|
+
{ id: 12, alias: "main-db", name: "Main DB", label: "Main DB", joe_api_v2_enabled: true, tunnel_ready: true, instance_id: 3, dblab_instance_id: 7 },
|
|
180
180
|
{ project_id: 13, alias: "dw", name: "Warehouse", joe_ready: false, tunnel: false },
|
|
181
181
|
]),
|
|
182
182
|
});
|
|
183
183
|
const projects = await listProjects({ apiKey: "k", apiBaseUrl: BASE });
|
|
184
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 },
|
|
185
|
+
{ project_id: 12, alias: "main-db", name: "Main DB", label: "Main DB", joe_ready: true, tunnel: true, instance_id: 3, dblab_instance_id: 7 },
|
|
186
|
+
{ project_id: 13, alias: "dw", name: "Warehouse", label: null, joe_ready: false, tunnel: false, instance_id: null, dblab_instance_id: null },
|
|
187
187
|
]);
|
|
188
188
|
});
|
|
189
189
|
|
|
@@ -498,7 +498,7 @@ describe("presentation helpers", () => {
|
|
|
498
498
|
|
|
499
499
|
test("formatProjectsTable renders the brief's column header", () => {
|
|
500
500
|
const table = formatProjectsTable([
|
|
501
|
-
{ project_id: 12, alias: "main-db", name: "Main DB", label: null, joe_ready: true, tunnel: true },
|
|
501
|
+
{ project_id: 12, alias: "main-db", name: "Main DB", label: null, joe_ready: true, tunnel: true, instance_id: null, dblab_instance_id: 7 },
|
|
502
502
|
]);
|
|
503
503
|
const [header, row] = table.split("\n");
|
|
504
504
|
expect(header).toContain("PROJECT_ID");
|