postgresai 0.14.0-dev.7 → 0.14.0-dev.70

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.
Files changed (82) hide show
  1. package/README.md +161 -61
  2. package/bin/postgres-ai.ts +1957 -404
  3. package/bun.lock +258 -0
  4. package/bunfig.toml +20 -0
  5. package/dist/bin/postgres-ai.js +29351 -1576
  6. package/dist/sql/01.role.sql +16 -0
  7. package/dist/sql/02.permissions.sql +37 -0
  8. package/dist/sql/03.optional_rds.sql +6 -0
  9. package/dist/sql/04.optional_self_managed.sql +8 -0
  10. package/dist/sql/05.helpers.sql +439 -0
  11. package/dist/sql/sql/01.role.sql +16 -0
  12. package/dist/sql/sql/02.permissions.sql +37 -0
  13. package/dist/sql/sql/03.optional_rds.sql +6 -0
  14. package/dist/sql/sql/04.optional_self_managed.sql +8 -0
  15. package/dist/sql/sql/05.helpers.sql +439 -0
  16. package/lib/auth-server.ts +124 -106
  17. package/lib/checkup-api.ts +386 -0
  18. package/lib/checkup.ts +1396 -0
  19. package/lib/config.ts +6 -3
  20. package/lib/init.ts +512 -156
  21. package/lib/issues.ts +400 -191
  22. package/lib/mcp-server.ts +213 -90
  23. package/lib/metrics-embedded.ts +79 -0
  24. package/lib/metrics-loader.ts +127 -0
  25. package/lib/supabase.ts +769 -0
  26. package/lib/util.ts +61 -0
  27. package/package.json +20 -10
  28. package/packages/postgres-ai/README.md +26 -0
  29. package/packages/postgres-ai/bin/postgres-ai.js +27 -0
  30. package/packages/postgres-ai/package.json +27 -0
  31. package/scripts/embed-metrics.ts +154 -0
  32. package/sql/01.role.sql +16 -0
  33. package/sql/02.permissions.sql +37 -0
  34. package/sql/03.optional_rds.sql +6 -0
  35. package/sql/04.optional_self_managed.sql +8 -0
  36. package/sql/05.helpers.sql +439 -0
  37. package/test/auth.test.ts +258 -0
  38. package/test/checkup.integration.test.ts +321 -0
  39. package/test/checkup.test.ts +1117 -0
  40. package/test/init.integration.test.ts +500 -0
  41. package/test/init.test.ts +527 -0
  42. package/test/issues.cli.test.ts +314 -0
  43. package/test/issues.test.ts +456 -0
  44. package/test/mcp-server.test.ts +988 -0
  45. package/test/schema-validation.test.ts +81 -0
  46. package/test/supabase.test.ts +568 -0
  47. package/test/test-utils.ts +128 -0
  48. package/tsconfig.json +12 -20
  49. package/dist/bin/postgres-ai.d.ts +0 -3
  50. package/dist/bin/postgres-ai.d.ts.map +0 -1
  51. package/dist/bin/postgres-ai.js.map +0 -1
  52. package/dist/lib/auth-server.d.ts +0 -31
  53. package/dist/lib/auth-server.d.ts.map +0 -1
  54. package/dist/lib/auth-server.js +0 -263
  55. package/dist/lib/auth-server.js.map +0 -1
  56. package/dist/lib/config.d.ts +0 -45
  57. package/dist/lib/config.d.ts.map +0 -1
  58. package/dist/lib/config.js +0 -181
  59. package/dist/lib/config.js.map +0 -1
  60. package/dist/lib/init.d.ts +0 -61
  61. package/dist/lib/init.d.ts.map +0 -1
  62. package/dist/lib/init.js +0 -359
  63. package/dist/lib/init.js.map +0 -1
  64. package/dist/lib/issues.d.ts +0 -75
  65. package/dist/lib/issues.d.ts.map +0 -1
  66. package/dist/lib/issues.js +0 -336
  67. package/dist/lib/issues.js.map +0 -1
  68. package/dist/lib/mcp-server.d.ts +0 -9
  69. package/dist/lib/mcp-server.d.ts.map +0 -1
  70. package/dist/lib/mcp-server.js +0 -168
  71. package/dist/lib/mcp-server.js.map +0 -1
  72. package/dist/lib/pkce.d.ts +0 -32
  73. package/dist/lib/pkce.d.ts.map +0 -1
  74. package/dist/lib/pkce.js +0 -101
  75. package/dist/lib/pkce.js.map +0 -1
  76. package/dist/lib/util.d.ts +0 -27
  77. package/dist/lib/util.d.ts.map +0 -1
  78. package/dist/lib/util.js +0 -46
  79. package/dist/lib/util.js.map +0 -1
  80. package/dist/package.json +0 -46
  81. package/test/init.integration.test.cjs +0 -269
  82. package/test/init.test.cjs +0 -69
@@ -0,0 +1,500 @@
1
+ /**
2
+ * Integration tests for prepare-db command
3
+ * Requires PostgreSQL binaries (initdb, postgres) to be available
4
+ * These tests spin up a temporary PostgreSQL instance for realistic testing
5
+ */
6
+ import { describe, test, expect, afterAll } from "bun:test";
7
+ import * as fs from "fs";
8
+ import * as os from "os";
9
+ import * as path from "path";
10
+ import * as net from "net";
11
+ import { Client } from "pg";
12
+
13
+ function sqlLiteral(value: string): string {
14
+ return `'${String(value).replace(/'/g, "''")}'`;
15
+ }
16
+
17
+ function findOnPath(cmd: string): string | null {
18
+ const result = Bun.spawnSync(["sh", "-c", `command -v ${cmd}`]);
19
+ if (result.exitCode === 0) {
20
+ return new TextDecoder().decode(result.stdout).trim();
21
+ }
22
+ return null;
23
+ }
24
+
25
+ function findPgBin(cmd: string): string | null {
26
+ const p = findOnPath(cmd);
27
+ if (p) return p;
28
+
29
+ // Debian/Ubuntu (GitLab CI node:*-bullseye images): binaries usually live here.
30
+ const probe = Bun.spawnSync([
31
+ "sh",
32
+ "-c",
33
+ `ls -1 /usr/lib/postgresql/*/bin/${cmd} 2>/dev/null | head -n 1 || true`,
34
+ ]);
35
+ const out = new TextDecoder().decode(probe.stdout).trim();
36
+ if (out) return out;
37
+
38
+ return null;
39
+ }
40
+
41
+ function havePostgresBinaries(): boolean {
42
+ return !!(findPgBin("initdb") && findPgBin("postgres"));
43
+ }
44
+
45
+ function isRunningAsRoot(): boolean {
46
+ return process.getuid?.() === 0;
47
+ }
48
+
49
+ async function getFreePort(): Promise<number> {
50
+ return new Promise((resolve, reject) => {
51
+ const srv = net.createServer();
52
+ srv.listen(0, "127.0.0.1", () => {
53
+ const addr = srv.address() as net.AddressInfo;
54
+ srv.close((err) => {
55
+ if (err) return reject(err);
56
+ resolve(addr.port);
57
+ });
58
+ });
59
+ srv.on("error", reject);
60
+ });
61
+ }
62
+
63
+ async function waitFor<T>(
64
+ fn: () => Promise<T>,
65
+ { timeoutMs = 10000, intervalMs = 100 } = {}
66
+ ): Promise<T> {
67
+ const start = Date.now();
68
+ while (true) {
69
+ try {
70
+ return await fn();
71
+ } catch (e) {
72
+ if (Date.now() - start > timeoutMs) throw e;
73
+ await new Promise((r) => setTimeout(r, intervalMs));
74
+ }
75
+ }
76
+ }
77
+
78
+ interface TempPostgres {
79
+ port: number;
80
+ socketDir: string;
81
+ adminUri: string;
82
+ postgresPassword: string;
83
+ cleanup: () => Promise<void>;
84
+ }
85
+
86
+ async function createTempPostgres(): Promise<TempPostgres> {
87
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "postgresai-init-"));
88
+ const dataDir = path.join(tmpRoot, "data");
89
+ const socketDir = path.join(tmpRoot, "sock");
90
+ fs.mkdirSync(socketDir, { recursive: true });
91
+
92
+ const initdb = findPgBin("initdb");
93
+ const postgresBin = findPgBin("postgres");
94
+ if (!initdb || !postgresBin) {
95
+ throw new Error("PostgreSQL binaries not found (need initdb and postgres)");
96
+ }
97
+
98
+ const init = Bun.spawnSync([initdb, "-D", dataDir, "-U", "postgres", "-A", "trust"]);
99
+ if (init.exitCode !== 0) {
100
+ throw new Error(new TextDecoder().decode(init.stderr) || new TextDecoder().decode(init.stdout));
101
+ }
102
+
103
+ // Configure: local socket trust, TCP scram.
104
+ const hbaPath = path.join(dataDir, "pg_hba.conf");
105
+ fs.appendFileSync(
106
+ hbaPath,
107
+ "\n# Added by postgresai init integration tests\nlocal all all trust\nhost all all 127.0.0.1/32 scram-sha-256\nhost all all ::1/128 scram-sha-256\n",
108
+ "utf8"
109
+ );
110
+
111
+ const port = await getFreePort();
112
+
113
+ const postgresProc = Bun.spawn(
114
+ [postgresBin, "-D", dataDir, "-k", socketDir, "-h", "127.0.0.1", "-p", String(port)],
115
+ { stdio: ["ignore", "pipe", "pipe"] }
116
+ );
117
+
118
+ const cleanup = async () => {
119
+ postgresProc.kill("SIGTERM");
120
+ try {
121
+ // 30s timeout to handle slower CI environments gracefully
122
+ await waitFor(
123
+ async () => {
124
+ if (postgresProc.exitCode === null) throw new Error("still running");
125
+ },
126
+ { timeoutMs: 30000, intervalMs: 100 }
127
+ );
128
+ } catch {
129
+ postgresProc.kill("SIGKILL");
130
+ }
131
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
132
+ };
133
+
134
+ const connectLocal = async (database = "postgres"): Promise<Client> => {
135
+ const c = new Client({ host: socketDir, port, user: "postgres", database });
136
+ await c.connect();
137
+ return c;
138
+ };
139
+
140
+ // Wait for Postgres to start (30s timeout for slower CI environments)
141
+ await waitFor(async () => {
142
+ const c = await connectLocal();
143
+ await c.end();
144
+ }, { timeoutMs: 30000, intervalMs: 100 });
145
+
146
+ const postgresPassword = "postgrespw";
147
+ {
148
+ const c = await connectLocal();
149
+ await c.query(`alter user postgres password ${sqlLiteral(postgresPassword)};`);
150
+ await c.query("create database testdb");
151
+ await c.end();
152
+ }
153
+
154
+ const adminUri = `postgresql://postgres:${postgresPassword}@127.0.0.1:${port}/testdb`;
155
+ return { port, socketDir, adminUri, postgresPassword, cleanup };
156
+ }
157
+
158
+ function runCliInit(
159
+ args: string[],
160
+ env: Record<string, string> = {}
161
+ ): { status: number | null; stdout: string; stderr: string } {
162
+ const cliPath = path.resolve(import.meta.dir, "..", "bin", "postgres-ai.ts");
163
+ const result = Bun.spawnSync(["bun", cliPath, "prepare-db", ...args], {
164
+ env: { ...process.env, ...env },
165
+ });
166
+ return {
167
+ status: result.exitCode,
168
+ stdout: new TextDecoder().decode(result.stdout),
169
+ stderr: new TextDecoder().decode(result.stderr),
170
+ };
171
+ }
172
+
173
+ // Skip all tests if PostgreSQL binaries are not available or running as root
174
+ // (initdb cannot be run as root)
175
+ const skipTests = !havePostgresBinaries() || isRunningAsRoot();
176
+
177
+ describe.skipIf(skipTests)("integration: prepare-db", () => {
178
+ let pg: TempPostgres;
179
+
180
+ // Use a shared postgres instance for all tests in this describe block
181
+ // Each test will reset state as needed
182
+
183
+ test("supports URI / conninfo / psql-like connection styles", async () => {
184
+ pg = await createTempPostgres();
185
+
186
+ try {
187
+ // 1) positional URI
188
+ {
189
+ const r = runCliInit([pg.adminUri, "--password", "monpw", "--skip-optional-permissions"]);
190
+ expect(r.status).toBe(0);
191
+ }
192
+
193
+ // 2) conninfo
194
+ {
195
+ const conninfo = `dbname=testdb host=127.0.0.1 port=${pg.port} user=postgres password=${pg.postgresPassword}`;
196
+ const r = runCliInit([conninfo, "--password", "monpw2", "--skip-optional-permissions"]);
197
+ expect(r.status).toBe(0);
198
+ }
199
+
200
+ // 3) psql-like options (+ PGPASSWORD)
201
+ {
202
+ const r = runCliInit(
203
+ [
204
+ "-h", "127.0.0.1",
205
+ "-p", String(pg.port),
206
+ "-U", "postgres",
207
+ "-d", "testdb",
208
+ "--password", "monpw3",
209
+ "--skip-optional-permissions",
210
+ ],
211
+ { PGPASSWORD: pg.postgresPassword }
212
+ );
213
+ expect(r.status).toBe(0);
214
+ }
215
+ } finally {
216
+ await pg.cleanup();
217
+ }
218
+ });
219
+
220
+ test("requires explicit monitoring password in non-interactive mode", async () => {
221
+ pg = await createTempPostgres();
222
+
223
+ try {
224
+ // Should fail without --print-password in non-interactive mode
225
+ {
226
+ const r = runCliInit([pg.adminUri, "--skip-optional-permissions"]);
227
+ expect(r.status).not.toBe(0);
228
+ expect(r.stderr).toMatch(/not printed in non-interactive mode/i);
229
+ expect(r.stderr).toMatch(/--print-password/);
230
+ }
231
+
232
+ // With explicit opt-in, it should succeed
233
+ {
234
+ const r = runCliInit([pg.adminUri, "--print-password", "--skip-optional-permissions"]);
235
+ expect(r.status).toBe(0);
236
+ expect(r.stderr).toMatch(/Generated monitoring password for postgres_ai_mon/i);
237
+ expect(r.stderr).toMatch(/PGAI_MON_PASSWORD=/);
238
+ }
239
+ } finally {
240
+ await pg.cleanup();
241
+ }
242
+ });
243
+
244
+ test("fixes slightly-off permissions idempotently", async () => {
245
+ pg = await createTempPostgres();
246
+
247
+ try {
248
+ // Create monitoring role with wrong password, no grants.
249
+ {
250
+ const c = new Client({ connectionString: pg.adminUri });
251
+ await c.connect();
252
+ await c.query(
253
+ "do $$ begin if not exists (select 1 from pg_roles where rolname='postgres_ai_mon') then create role postgres_ai_mon login password 'wrong'; end if; end $$;"
254
+ );
255
+ await c.end();
256
+ }
257
+
258
+ // Run init (should grant everything).
259
+ {
260
+ const r = runCliInit([pg.adminUri, "--password", "correctpw", "--skip-optional-permissions"]);
261
+ expect(r.status).toBe(0);
262
+ }
263
+
264
+ // Verify privileges.
265
+ {
266
+ const c = new Client({ connectionString: pg.adminUri });
267
+ await c.connect();
268
+ const dbOk = await c.query(
269
+ "select has_database_privilege('postgres_ai_mon', current_database(), 'CONNECT') as ok"
270
+ );
271
+ expect(dbOk.rows[0].ok).toBe(true);
272
+ const roleOk = await c.query("select pg_has_role('postgres_ai_mon', 'pg_monitor', 'member') as ok");
273
+ expect(roleOk.rows[0].ok).toBe(true);
274
+ const idxOk = await c.query(
275
+ "select has_table_privilege('postgres_ai_mon', 'pg_catalog.pg_index', 'SELECT') as ok"
276
+ );
277
+ expect(idxOk.rows[0].ok).toBe(true);
278
+ const viewOk = await c.query(
279
+ "select has_table_privilege('postgres_ai_mon', 'postgres_ai.pg_statistic', 'SELECT') as ok"
280
+ );
281
+ expect(viewOk.rows[0].ok).toBe(true);
282
+ const sp = await c.query("select rolconfig from pg_roles where rolname='postgres_ai_mon'");
283
+ expect(Array.isArray(sp.rows[0].rolconfig)).toBe(true);
284
+ expect(sp.rows[0].rolconfig.some((v: string) => String(v).includes("search_path="))).toBe(true);
285
+ await c.end();
286
+ }
287
+
288
+ // Run init again (idempotent).
289
+ {
290
+ const r = runCliInit([pg.adminUri, "--password", "correctpw", "--skip-optional-permissions"]);
291
+ expect(r.status).toBe(0);
292
+ }
293
+ } finally {
294
+ await pg.cleanup();
295
+ }
296
+ });
297
+
298
+ test("reports nicely when lacking permissions", async () => {
299
+ pg = await createTempPostgres();
300
+
301
+ try {
302
+ // Create limited user that can connect but cannot create roles / grant.
303
+ const limitedPw = "limitedpw";
304
+ {
305
+ const c = new Client({ connectionString: pg.adminUri });
306
+ await c.connect();
307
+ await c.query(`do $$ begin
308
+ if not exists (select 1 from pg_roles where rolname='limited') then
309
+ begin
310
+ create role limited login password ${sqlLiteral(limitedPw)};
311
+ exception when duplicate_object then
312
+ null;
313
+ end;
314
+ end if;
315
+ end $$;`);
316
+ await c.query("grant connect on database testdb to limited");
317
+ await c.end();
318
+ }
319
+
320
+ const limitedUri = `postgresql://limited:${limitedPw}@127.0.0.1:${pg.port}/testdb`;
321
+ const r = runCliInit([limitedUri, "--password", "monpw", "--skip-optional-permissions"]);
322
+ expect(r.status).not.toBe(0);
323
+ expect(r.stderr).toMatch(/Error: prepare-db:/);
324
+ expect(r.stderr).toMatch(/Failed at step "/);
325
+ expect(r.stderr).toMatch(/Fix: connect as a superuser/i);
326
+ } finally {
327
+ await pg.cleanup();
328
+ }
329
+ });
330
+
331
+ test(
332
+ "--verify returns 0 when ok and non-zero when missing",
333
+ async () => {
334
+ pg = await createTempPostgres();
335
+
336
+ try {
337
+ // Prepare: run init
338
+ {
339
+ const r = runCliInit([pg.adminUri, "--password", "monpw", "--skip-optional-permissions"]);
340
+ expect(r.status).toBe(0);
341
+ }
342
+
343
+ // Verify should pass
344
+ {
345
+ const r = runCliInit([pg.adminUri, "--verify", "--skip-optional-permissions"]);
346
+ expect(r.status).toBe(0);
347
+ expect(r.stdout).toMatch(/prepare-db verify: OK/i);
348
+ }
349
+
350
+ // Break a required privilege and ensure verify fails
351
+ {
352
+ const c = new Client({ connectionString: pg.adminUri });
353
+ await c.connect();
354
+ await c.query("revoke select on pg_catalog.pg_index from public");
355
+ await c.query("revoke select on pg_catalog.pg_index from postgres_ai_mon");
356
+ await c.end();
357
+ }
358
+ {
359
+ const r = runCliInit([pg.adminUri, "--verify", "--skip-optional-permissions"]);
360
+ expect(r.status).not.toBe(0);
361
+ expect(r.stderr).toMatch(/prepare-db verify failed/i);
362
+ expect(r.stderr).toMatch(/pg_catalog\.pg_index/i);
363
+ }
364
+ } finally {
365
+ await pg.cleanup();
366
+ }
367
+ }
368
+ );
369
+
370
+ test("--reset-password updates the monitoring role login password", async () => {
371
+ pg = await createTempPostgres();
372
+
373
+ try {
374
+ // Initial setup with password pw1
375
+ {
376
+ const r = runCliInit([pg.adminUri, "--password", "pw1", "--skip-optional-permissions"]);
377
+ expect(r.status).toBe(0);
378
+ }
379
+
380
+ // Reset to pw2
381
+ {
382
+ const r = runCliInit([pg.adminUri, "--reset-password", "--password", "pw2", "--skip-optional-permissions"]);
383
+ expect(r.status).toBe(0);
384
+ expect(r.stdout).toMatch(/password reset/i);
385
+ }
386
+
387
+ // Connect as monitoring user with new password should work
388
+ {
389
+ const c = new Client({
390
+ connectionString: `postgresql://postgres_ai_mon:pw2@127.0.0.1:${pg.port}/testdb`,
391
+ });
392
+ await c.connect();
393
+ const ok = await c.query("select 1 as ok");
394
+ expect(ok.rows[0].ok).toBe(1);
395
+ await c.end();
396
+ }
397
+ } finally {
398
+ await pg.cleanup();
399
+ }
400
+ });
401
+
402
+ // 60s timeout for PostgreSQL startup + multiple SQL queries in slow CI
403
+ test("explain_generic validates input and prevents SQL injection", async () => {
404
+ pg = await createTempPostgres();
405
+
406
+ try {
407
+ // Run init first
408
+ {
409
+ const r = runCliInit([pg.adminUri, "--password", "pw1", "--skip-optional-permissions"]);
410
+ expect(r.status).toBe(0);
411
+ }
412
+
413
+ const c = new Client({ connectionString: pg.adminUri });
414
+ await c.connect();
415
+
416
+ try {
417
+ // Check PostgreSQL version - generic_plan requires 16+
418
+ const versionRes = await c.query("show server_version_num");
419
+ const version = parseInt(versionRes.rows[0].server_version_num, 10);
420
+
421
+ if (version < 160000) {
422
+ // Skip this test on older PostgreSQL versions
423
+ console.log("Skipping explain_generic tests: requires PostgreSQL 16+");
424
+ return;
425
+ }
426
+
427
+ // Test 1: Empty query should be rejected
428
+ await expect(
429
+ c.query("select postgres_ai.explain_generic('')")
430
+ ).rejects.toThrow(/query cannot be empty/);
431
+
432
+ // Test 2: Null query should be rejected
433
+ await expect(
434
+ c.query("select postgres_ai.explain_generic(null)")
435
+ ).rejects.toThrow(/query cannot be empty/);
436
+
437
+ // Test 3: Multiple statements (semicolon in middle) should be rejected
438
+ await expect(
439
+ c.query("select postgres_ai.explain_generic('select 1; select 2')")
440
+ ).rejects.toThrow(/semicolon|multiple statements/i);
441
+
442
+ // Test 4: Trailing semicolon should be stripped and work
443
+ {
444
+ const res = await c.query("select postgres_ai.explain_generic('select 1;') as result");
445
+ expect(res.rows[0].result).toBeTruthy();
446
+ expect(res.rows[0].result).toMatch(/Result/i);
447
+ }
448
+
449
+ // Test 5: Valid query should work
450
+ {
451
+ const res = await c.query("select postgres_ai.explain_generic('select $1::int', 'text') as result");
452
+ expect(res.rows[0].result).toBeTruthy();
453
+ }
454
+
455
+ // Test 6: JSON format should work
456
+ {
457
+ const res = await c.query("select postgres_ai.explain_generic('select 1', 'json') as result");
458
+ const plan = JSON.parse(res.rows[0].result);
459
+ expect(Array.isArray(plan)).toBe(true);
460
+ expect(plan[0].Plan).toBeTruthy();
461
+ }
462
+
463
+ // Test 7: Whitespace-only query should be rejected
464
+ await expect(
465
+ c.query("select postgres_ai.explain_generic(' ')")
466
+ ).rejects.toThrow(/query cannot be empty/);
467
+
468
+ // Test 8: Semicolon in string literal is rejected (documented limitation)
469
+ // Note: This is a known limitation - the simple heuristic cannot parse SQL strings
470
+ await expect(
471
+ c.query("select postgres_ai.explain_generic('select ''hello;world''')")
472
+ ).rejects.toThrow(/semicolon/i);
473
+
474
+ // Test 9: SQL comments should work (no semicolons)
475
+ {
476
+ const res = await c.query("select postgres_ai.explain_generic('select 1 -- comment') as result");
477
+ expect(res.rows[0].result).toBeTruthy();
478
+ }
479
+
480
+ // Test 10: Escaped quotes should work (no semicolons)
481
+ {
482
+ const res = await c.query("select postgres_ai.explain_generic('select ''test''''s value''') as result");
483
+ expect(res.rows[0].result).toBeTruthy();
484
+ }
485
+
486
+ // Test 11: Case-insensitive format parameter
487
+ {
488
+ const res = await c.query("select postgres_ai.explain_generic('select 1', 'JSON') as result");
489
+ const plan = JSON.parse(res.rows[0].result);
490
+ expect(Array.isArray(plan)).toBe(true);
491
+ }
492
+
493
+ } finally {
494
+ await c.end();
495
+ }
496
+ } finally {
497
+ await pg.cleanup();
498
+ }
499
+ }, { timeout: 60000 });
500
+ });