postgresai 0.14.0-dev.43 → 0.14.0-dev.45
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 +649 -310
- package/bun.lock +258 -0
- package/dist/bin/postgres-ai.js +29491 -1910
- package/dist/sql/01.role.sql +16 -0
- package/dist/sql/02.permissions.sql +37 -0
- package/dist/sql/03.optional_rds.sql +6 -0
- package/dist/sql/04.optional_self_managed.sql +8 -0
- package/dist/sql/05.helpers.sql +415 -0
- package/lib/auth-server.ts +58 -97
- package/lib/checkup-api.ts +175 -0
- package/lib/checkup.ts +837 -0
- package/lib/config.ts +3 -0
- package/lib/init.ts +106 -74
- package/lib/issues.ts +121 -194
- package/lib/mcp-server.ts +6 -17
- package/lib/metrics-loader.ts +156 -0
- package/package.json +13 -9
- package/sql/02.permissions.sql +9 -5
- package/sql/05.helpers.sql +415 -0
- package/test/checkup.test.ts +953 -0
- package/test/init.integration.test.ts +396 -0
- package/test/init.test.ts +345 -0
- package/test/schema-validation.test.ts +188 -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 -85
- package/dist/lib/init.d.ts.map +0 -1
- package/dist/lib/init.js +0 -644
- 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 -382
- package/test/init.test.cjs +0 -392
|
@@ -0,0 +1,396 @@
|
|
|
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
|
+
await waitFor(
|
|
122
|
+
async () => {
|
|
123
|
+
if (postgresProc.exitCode === null) throw new Error("still running");
|
|
124
|
+
},
|
|
125
|
+
{ timeoutMs: 5000, intervalMs: 100 }
|
|
126
|
+
);
|
|
127
|
+
} catch {
|
|
128
|
+
postgresProc.kill("SIGKILL");
|
|
129
|
+
}
|
|
130
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const connectLocal = async (database = "postgres"): Promise<Client> => {
|
|
134
|
+
const c = new Client({ host: socketDir, port, user: "postgres", database });
|
|
135
|
+
await c.connect();
|
|
136
|
+
return c;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
await waitFor(async () => {
|
|
140
|
+
const c = await connectLocal();
|
|
141
|
+
await c.end();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const postgresPassword = "postgrespw";
|
|
145
|
+
{
|
|
146
|
+
const c = await connectLocal();
|
|
147
|
+
await c.query(`alter user postgres password ${sqlLiteral(postgresPassword)};`);
|
|
148
|
+
await c.query("create database testdb");
|
|
149
|
+
await c.end();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const adminUri = `postgresql://postgres:${postgresPassword}@127.0.0.1:${port}/testdb`;
|
|
153
|
+
return { port, socketDir, adminUri, postgresPassword, cleanup };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function runCliInit(
|
|
157
|
+
args: string[],
|
|
158
|
+
env: Record<string, string> = {}
|
|
159
|
+
): { status: number | null; stdout: string; stderr: string } {
|
|
160
|
+
const cliPath = path.resolve(import.meta.dir, "..", "bin", "postgres-ai.ts");
|
|
161
|
+
const result = Bun.spawnSync(["bun", cliPath, "prepare-db", ...args], {
|
|
162
|
+
env: { ...process.env, ...env },
|
|
163
|
+
});
|
|
164
|
+
return {
|
|
165
|
+
status: result.exitCode,
|
|
166
|
+
stdout: new TextDecoder().decode(result.stdout),
|
|
167
|
+
stderr: new TextDecoder().decode(result.stderr),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Skip all tests if PostgreSQL binaries are not available or running as root
|
|
172
|
+
// (initdb cannot be run as root)
|
|
173
|
+
const skipTests = !havePostgresBinaries() || isRunningAsRoot();
|
|
174
|
+
|
|
175
|
+
describe.skipIf(skipTests)("integration: prepare-db", () => {
|
|
176
|
+
let pg: TempPostgres;
|
|
177
|
+
|
|
178
|
+
// Use a shared postgres instance for all tests in this describe block
|
|
179
|
+
// Each test will reset state as needed
|
|
180
|
+
|
|
181
|
+
test("supports URI / conninfo / psql-like connection styles", async () => {
|
|
182
|
+
pg = await createTempPostgres();
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
// 1) positional URI
|
|
186
|
+
{
|
|
187
|
+
const r = runCliInit([pg.adminUri, "--password", "monpw", "--skip-optional-permissions"]);
|
|
188
|
+
expect(r.status).toBe(0);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 2) conninfo
|
|
192
|
+
{
|
|
193
|
+
const conninfo = `dbname=testdb host=127.0.0.1 port=${pg.port} user=postgres password=${pg.postgresPassword}`;
|
|
194
|
+
const r = runCliInit([conninfo, "--password", "monpw2", "--skip-optional-permissions"]);
|
|
195
|
+
expect(r.status).toBe(0);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 3) psql-like options (+ PGPASSWORD)
|
|
199
|
+
{
|
|
200
|
+
const r = runCliInit(
|
|
201
|
+
[
|
|
202
|
+
"-h", "127.0.0.1",
|
|
203
|
+
"-p", String(pg.port),
|
|
204
|
+
"-U", "postgres",
|
|
205
|
+
"-d", "testdb",
|
|
206
|
+
"--password", "monpw3",
|
|
207
|
+
"--skip-optional-permissions",
|
|
208
|
+
],
|
|
209
|
+
{ PGPASSWORD: pg.postgresPassword }
|
|
210
|
+
);
|
|
211
|
+
expect(r.status).toBe(0);
|
|
212
|
+
}
|
|
213
|
+
} finally {
|
|
214
|
+
await pg.cleanup();
|
|
215
|
+
}
|
|
216
|
+
}, { timeout: 30000 });
|
|
217
|
+
|
|
218
|
+
test("requires explicit monitoring password in non-interactive mode", async () => {
|
|
219
|
+
pg = await createTempPostgres();
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
// Should fail without --print-password in non-interactive mode
|
|
223
|
+
{
|
|
224
|
+
const r = runCliInit([pg.adminUri, "--skip-optional-permissions"]);
|
|
225
|
+
expect(r.status).not.toBe(0);
|
|
226
|
+
expect(r.stderr).toMatch(/not printed in non-interactive mode/i);
|
|
227
|
+
expect(r.stderr).toMatch(/--print-password/);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// With explicit opt-in, it should succeed
|
|
231
|
+
{
|
|
232
|
+
const r = runCliInit([pg.adminUri, "--print-password", "--skip-optional-permissions"]);
|
|
233
|
+
expect(r.status).toBe(0);
|
|
234
|
+
expect(r.stderr).toMatch(/Generated monitoring password for postgres_ai_mon/i);
|
|
235
|
+
expect(r.stderr).toMatch(/PGAI_MON_PASSWORD=/);
|
|
236
|
+
}
|
|
237
|
+
} finally {
|
|
238
|
+
await pg.cleanup();
|
|
239
|
+
}
|
|
240
|
+
}, { timeout: 30000 });
|
|
241
|
+
|
|
242
|
+
test("fixes slightly-off permissions idempotently", async () => {
|
|
243
|
+
pg = await createTempPostgres();
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
// Create monitoring role with wrong password, no grants.
|
|
247
|
+
{
|
|
248
|
+
const c = new Client({ connectionString: pg.adminUri });
|
|
249
|
+
await c.connect();
|
|
250
|
+
await c.query(
|
|
251
|
+
"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 $$;"
|
|
252
|
+
);
|
|
253
|
+
await c.end();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Run init (should grant everything).
|
|
257
|
+
{
|
|
258
|
+
const r = runCliInit([pg.adminUri, "--password", "correctpw", "--skip-optional-permissions"]);
|
|
259
|
+
expect(r.status).toBe(0);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Verify privileges.
|
|
263
|
+
{
|
|
264
|
+
const c = new Client({ connectionString: pg.adminUri });
|
|
265
|
+
await c.connect();
|
|
266
|
+
const dbOk = await c.query(
|
|
267
|
+
"select has_database_privilege('postgres_ai_mon', current_database(), 'CONNECT') as ok"
|
|
268
|
+
);
|
|
269
|
+
expect(dbOk.rows[0].ok).toBe(true);
|
|
270
|
+
const roleOk = await c.query("select pg_has_role('postgres_ai_mon', 'pg_monitor', 'member') as ok");
|
|
271
|
+
expect(roleOk.rows[0].ok).toBe(true);
|
|
272
|
+
const idxOk = await c.query(
|
|
273
|
+
"select has_table_privilege('postgres_ai_mon', 'pg_catalog.pg_index', 'SELECT') as ok"
|
|
274
|
+
);
|
|
275
|
+
expect(idxOk.rows[0].ok).toBe(true);
|
|
276
|
+
const viewOk = await c.query(
|
|
277
|
+
"select has_table_privilege('postgres_ai_mon', 'postgres_ai.pg_statistic', 'SELECT') as ok"
|
|
278
|
+
);
|
|
279
|
+
expect(viewOk.rows[0].ok).toBe(true);
|
|
280
|
+
const sp = await c.query("select rolconfig from pg_roles where rolname='postgres_ai_mon'");
|
|
281
|
+
expect(Array.isArray(sp.rows[0].rolconfig)).toBe(true);
|
|
282
|
+
expect(sp.rows[0].rolconfig.some((v: string) => String(v).includes("search_path="))).toBe(true);
|
|
283
|
+
await c.end();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Run init again (idempotent).
|
|
287
|
+
{
|
|
288
|
+
const r = runCliInit([pg.adminUri, "--password", "correctpw", "--skip-optional-permissions"]);
|
|
289
|
+
expect(r.status).toBe(0);
|
|
290
|
+
}
|
|
291
|
+
} finally {
|
|
292
|
+
await pg.cleanup();
|
|
293
|
+
}
|
|
294
|
+
}, { timeout: 30000 });
|
|
295
|
+
|
|
296
|
+
test("reports nicely when lacking permissions", async () => {
|
|
297
|
+
pg = await createTempPostgres();
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
// Create limited user that can connect but cannot create roles / grant.
|
|
301
|
+
const limitedPw = "limitedpw";
|
|
302
|
+
{
|
|
303
|
+
const c = new Client({ connectionString: pg.adminUri });
|
|
304
|
+
await c.connect();
|
|
305
|
+
await c.query(`do $$ begin
|
|
306
|
+
if not exists (select 1 from pg_roles where rolname='limited') then
|
|
307
|
+
begin
|
|
308
|
+
create role limited login password ${sqlLiteral(limitedPw)};
|
|
309
|
+
exception when duplicate_object then
|
|
310
|
+
null;
|
|
311
|
+
end;
|
|
312
|
+
end if;
|
|
313
|
+
end $$;`);
|
|
314
|
+
await c.query("grant connect on database testdb to limited");
|
|
315
|
+
await c.end();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const limitedUri = `postgresql://limited:${limitedPw}@127.0.0.1:${pg.port}/testdb`;
|
|
319
|
+
const r = runCliInit([limitedUri, "--password", "monpw", "--skip-optional-permissions"]);
|
|
320
|
+
expect(r.status).not.toBe(0);
|
|
321
|
+
expect(r.stderr).toMatch(/Error: prepare-db:/);
|
|
322
|
+
expect(r.stderr).toMatch(/Failed at step "/);
|
|
323
|
+
expect(r.stderr).toMatch(/Fix: connect as a superuser/i);
|
|
324
|
+
} finally {
|
|
325
|
+
await pg.cleanup();
|
|
326
|
+
}
|
|
327
|
+
}, { timeout: 30000 });
|
|
328
|
+
|
|
329
|
+
test("--verify returns 0 when ok and non-zero when missing", async () => {
|
|
330
|
+
pg = await createTempPostgres();
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
// Prepare: run init
|
|
334
|
+
{
|
|
335
|
+
const r = runCliInit([pg.adminUri, "--password", "monpw", "--skip-optional-permissions"]);
|
|
336
|
+
expect(r.status).toBe(0);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Verify should pass
|
|
340
|
+
{
|
|
341
|
+
const r = runCliInit([pg.adminUri, "--verify", "--skip-optional-permissions"]);
|
|
342
|
+
expect(r.status).toBe(0);
|
|
343
|
+
expect(r.stdout).toMatch(/prepare-db verify: OK/i);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Break a required privilege and ensure verify fails
|
|
347
|
+
{
|
|
348
|
+
const c = new Client({ connectionString: pg.adminUri });
|
|
349
|
+
await c.connect();
|
|
350
|
+
await c.query("revoke select on pg_catalog.pg_index from public");
|
|
351
|
+
await c.query("revoke select on pg_catalog.pg_index from postgres_ai_mon");
|
|
352
|
+
await c.end();
|
|
353
|
+
}
|
|
354
|
+
{
|
|
355
|
+
const r = runCliInit([pg.adminUri, "--verify", "--skip-optional-permissions"]);
|
|
356
|
+
expect(r.status).not.toBe(0);
|
|
357
|
+
expect(r.stderr).toMatch(/prepare-db verify failed/i);
|
|
358
|
+
expect(r.stderr).toMatch(/pg_catalog\.pg_index/i);
|
|
359
|
+
}
|
|
360
|
+
} finally {
|
|
361
|
+
await pg.cleanup();
|
|
362
|
+
}
|
|
363
|
+
}, { timeout: 30000 });
|
|
364
|
+
|
|
365
|
+
test("--reset-password updates the monitoring role login password", async () => {
|
|
366
|
+
pg = await createTempPostgres();
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
// Initial setup with password pw1
|
|
370
|
+
{
|
|
371
|
+
const r = runCliInit([pg.adminUri, "--password", "pw1", "--skip-optional-permissions"]);
|
|
372
|
+
expect(r.status).toBe(0);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Reset to pw2
|
|
376
|
+
{
|
|
377
|
+
const r = runCliInit([pg.adminUri, "--reset-password", "--password", "pw2", "--skip-optional-permissions"]);
|
|
378
|
+
expect(r.status).toBe(0);
|
|
379
|
+
expect(r.stdout).toMatch(/password reset/i);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Connect as monitoring user with new password should work
|
|
383
|
+
{
|
|
384
|
+
const c = new Client({
|
|
385
|
+
connectionString: `postgresql://postgres_ai_mon:pw2@127.0.0.1:${pg.port}/testdb`,
|
|
386
|
+
});
|
|
387
|
+
await c.connect();
|
|
388
|
+
const ok = await c.query("select 1 as ok");
|
|
389
|
+
expect(ok.rows[0].ok).toBe(1);
|
|
390
|
+
await c.end();
|
|
391
|
+
}
|
|
392
|
+
} finally {
|
|
393
|
+
await pg.cleanup();
|
|
394
|
+
}
|
|
395
|
+
}, { timeout: 30000 });
|
|
396
|
+
});
|