postgresai 0.16.0-dev.0 → 0.16.0-dev.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -1
- package/bin/postgres-ai.ts +789 -6
- package/bun.lock +4 -4
- package/dist/bin/postgres-ai.js +2646 -283
- package/dist/sql/06.helpers.sql +0 -122
- package/dist/sql/sql/06.helpers.sql +0 -122
- package/lib/aas-onboard.ts +41 -7
- package/lib/dblab.ts +449 -0
- package/lib/init.ts +0 -8
- package/lib/joe.ts +761 -0
- package/lib/mcp-server.ts +625 -0
- package/lib/supabase.ts +0 -18
- package/lib/util.ts +125 -18
- package/package.json +1 -1
- package/sql/06.helpers.sql +0 -122
- package/test/aas-onboard.test.ts +84 -0
- package/test/dblab.cli.test.ts +373 -0
- package/test/dblab.test.ts +488 -0
- package/test/e2e/pgai-e2e-smoke.sh +192 -0
- package/test/init.integration.test.ts +9 -79
- package/test/joe.cli.test.ts +469 -0
- package/test/joe.test.ts +858 -0
- package/test/monitoring.test.ts +54 -3
- package/test/util.test.ts +111 -1
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { mkdtempSync } from "fs";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
|
|
6
|
+
function runCli(args: string[], env: Record<string, string> = {}) {
|
|
7
|
+
const cliPath = resolve(import.meta.dir, "..", "bin", "postgres-ai.ts");
|
|
8
|
+
const bunBin = typeof process.execPath === "string" && process.execPath.length > 0 ? process.execPath : "bun";
|
|
9
|
+
const result = Bun.spawnSync([bunBin, cliPath, ...args], {
|
|
10
|
+
env: { ...process.env, ...env },
|
|
11
|
+
});
|
|
12
|
+
return {
|
|
13
|
+
status: result.exitCode,
|
|
14
|
+
stdout: new TextDecoder().decode(result.stdout),
|
|
15
|
+
stderr: new TextDecoder().decode(result.stderr),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Async spawn — MUST be used for any test that hits the in-process fake server:
|
|
20
|
+
// Bun.spawnSync would block this process's event loop, so the fake `Bun.serve`
|
|
21
|
+
// could never answer the subprocess's request (deadlock).
|
|
22
|
+
async function runCliAsync(args: string[], env: Record<string, string> = {}) {
|
|
23
|
+
const cliPath = resolve(import.meta.dir, "..", "bin", "postgres-ai.ts");
|
|
24
|
+
const bunBin = typeof process.execPath === "string" && process.execPath.length > 0 ? process.execPath : "bun";
|
|
25
|
+
const proc = Bun.spawn([bunBin, cliPath, ...args], {
|
|
26
|
+
env: { ...process.env, ...env },
|
|
27
|
+
stdout: "pipe",
|
|
28
|
+
stderr: "pipe",
|
|
29
|
+
});
|
|
30
|
+
const [status, stdout, stderr] = await Promise.all([
|
|
31
|
+
proc.exited,
|
|
32
|
+
new Response(proc.stdout).text(),
|
|
33
|
+
new Response(proc.stderr).text(),
|
|
34
|
+
]);
|
|
35
|
+
return { status, stdout, stderr };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isolatedEnv(extra: Record<string, string> = {}) {
|
|
39
|
+
const cfgHome = mkdtempSync(resolve(tmpdir(), "postgresai-dblab-test-"));
|
|
40
|
+
return { XDG_CONFIG_HOME: cfgHome, HOME: cfgHome, ...extra };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface Recorded {
|
|
44
|
+
method: string;
|
|
45
|
+
pathname: string;
|
|
46
|
+
search: string;
|
|
47
|
+
bodyJson: any | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
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
|
+
*/
|
|
55
|
+
async function startFakeApi(projects?: unknown[]) {
|
|
56
|
+
const requests: Recorded[] = [];
|
|
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
|
+
];
|
|
60
|
+
|
|
61
|
+
const server = Bun.serve({
|
|
62
|
+
hostname: "127.0.0.1",
|
|
63
|
+
port: 0,
|
|
64
|
+
async fetch(req) {
|
|
65
|
+
const url = new URL(req.url);
|
|
66
|
+
const bodyText = req.method === "POST" ? await req.text() : "";
|
|
67
|
+
let bodyJson: any | null = null;
|
|
68
|
+
try { bodyJson = bodyText ? JSON.parse(bodyText) : null; } catch { bodyJson = null; }
|
|
69
|
+
requests.push({ method: req.method, pathname: url.pathname, search: url.search, bodyJson });
|
|
70
|
+
|
|
71
|
+
if (req.method === "POST" && url.pathname.endsWith("/rpc/projects_list")) {
|
|
72
|
+
return new Response(JSON.stringify(rows), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
73
|
+
}
|
|
74
|
+
if (req.method === "POST" && url.pathname.endsWith("/rpc/dblab_api_call")) {
|
|
75
|
+
// Echo a plausible reply keyed off the action.
|
|
76
|
+
const action = String(bodyJson?.action ?? "");
|
|
77
|
+
const reply = action === "/clones" || action.startsWith("/branches") || action.startsWith("/snapshots") || action.endsWith("/log")
|
|
78
|
+
? []
|
|
79
|
+
: { instance_id: bodyJson?.instance_id, action, method: bodyJson?.method, ok: true };
|
|
80
|
+
return new Response(JSON.stringify(reply), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
81
|
+
}
|
|
82
|
+
return new Response("not found", { status: 404 });
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const baseUrl = `http://${server.hostname}:${server.port}/api/general`;
|
|
87
|
+
return {
|
|
88
|
+
baseUrl,
|
|
89
|
+
requests,
|
|
90
|
+
proxyCalls: () => requests.filter((r) => r.pathname.endsWith("/rpc/dblab_api_call")),
|
|
91
|
+
resolverCalls: () => requests.filter((r) => r.pathname.endsWith("/rpc/projects_list")),
|
|
92
|
+
stop: () => server.stop(true),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe("CLI DBLab companion command groups (grouped under `pgai dblab …`)", () => {
|
|
97
|
+
test("top-level help lists the dblab group", () => {
|
|
98
|
+
const r = runCli(["--help"], isolatedEnv());
|
|
99
|
+
const out = `${r.stdout}\n${r.stderr}`;
|
|
100
|
+
expect(out).toContain("dblab");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("dblab help exposes the clone, branch, and snapshot groups", () => {
|
|
104
|
+
const r = runCli(["dblab", "--help"], isolatedEnv());
|
|
105
|
+
expect(r.status).toBe(0);
|
|
106
|
+
const out = `${r.stdout}\n${r.stderr}`;
|
|
107
|
+
expect(out).toContain("clone");
|
|
108
|
+
expect(out).toContain("branch");
|
|
109
|
+
expect(out).toContain("snapshot");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("clone help exposes create/list/status/reset/destroy", () => {
|
|
113
|
+
const r = runCli(["dblab", "clone", "--help"], isolatedEnv());
|
|
114
|
+
expect(r.status).toBe(0);
|
|
115
|
+
const out = `${r.stdout}\n${r.stderr}`;
|
|
116
|
+
expect(out).toContain("create");
|
|
117
|
+
expect(out).toContain("list");
|
|
118
|
+
expect(out).toContain("status");
|
|
119
|
+
expect(out).toContain("reset");
|
|
120
|
+
expect(out).toContain("destroy");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("branch help exposes list/create/delete/log", () => {
|
|
124
|
+
const r = runCli(["dblab", "branch", "--help"], isolatedEnv());
|
|
125
|
+
expect(r.status).toBe(0);
|
|
126
|
+
const out = `${r.stdout}\n${r.stderr}`;
|
|
127
|
+
expect(out).toContain("list");
|
|
128
|
+
expect(out).toContain("create");
|
|
129
|
+
expect(out).toContain("delete");
|
|
130
|
+
expect(out).toContain("log");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("snapshot help exposes list/create/destroy", () => {
|
|
134
|
+
const r = runCli(["dblab", "snapshot", "--help"], isolatedEnv());
|
|
135
|
+
expect(r.status).toBe(0);
|
|
136
|
+
const out = `${r.stdout}\n${r.stderr}`;
|
|
137
|
+
expect(out).toContain("list");
|
|
138
|
+
expect(out).toContain("create");
|
|
139
|
+
expect(out).toContain("destroy");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("clone create fails fast without an API key", () => {
|
|
143
|
+
const r = runCli(["dblab", "clone", "create", "--project", "main-db"], isolatedEnv());
|
|
144
|
+
expect(r.status).toBe(1);
|
|
145
|
+
expect(`${r.stdout}\n${r.stderr}`).toContain("API key is required");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("clone list without --project errors", () => {
|
|
149
|
+
const r = runCli(["dblab", "clone", "list"], isolatedEnv({ PGAI_API_KEY: "test-key" }));
|
|
150
|
+
expect(r.status).not.toBe(0);
|
|
151
|
+
expect(`${r.stdout}\n${r.stderr}`.toLowerCase()).toContain("project");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("clone create resolves the alias then proxies /clone POST", async () => {
|
|
155
|
+
const api = await startFakeApi();
|
|
156
|
+
try {
|
|
157
|
+
const r = await runCliAsync(
|
|
158
|
+
["dblab", "clone", "create", "--project", "main-db", "--json"],
|
|
159
|
+
isolatedEnv({ PGAI_API_KEY: "test-key", PGAI_API_BASE_URL: api.baseUrl })
|
|
160
|
+
);
|
|
161
|
+
expect(r.status).toBe(0);
|
|
162
|
+
// The resolver GET happened.
|
|
163
|
+
expect(api.resolverCalls().length).toBe(1);
|
|
164
|
+
// Exactly one proxy call, with the right action/method/instance.
|
|
165
|
+
const proxied = api.proxyCalls();
|
|
166
|
+
expect(proxied.length).toBe(1);
|
|
167
|
+
expect(proxied[0].bodyJson.instance_id).toBe("7");
|
|
168
|
+
expect(proxied[0].bodyJson.action).toBe("/clone");
|
|
169
|
+
expect(proxied[0].bodyJson.method).toBe("post");
|
|
170
|
+
expect(proxied[0].bodyJson.data).toEqual({ protected: false });
|
|
171
|
+
} finally {
|
|
172
|
+
api.stop();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("clone create maps branch/snapshot/protected into the payload", async () => {
|
|
177
|
+
const api = await startFakeApi();
|
|
178
|
+
try {
|
|
179
|
+
const r = await runCliAsync(
|
|
180
|
+
["dblab", "clone", "create", "--project", "main-db", "--branch", "feature-idx", "--snapshot", "s-9", "--protected", "--json"],
|
|
181
|
+
isolatedEnv({ PGAI_API_KEY: "test-key", PGAI_API_BASE_URL: api.baseUrl })
|
|
182
|
+
);
|
|
183
|
+
expect(r.status).toBe(0);
|
|
184
|
+
const body = api.proxyCalls()[0].bodyJson;
|
|
185
|
+
expect(body.data).toEqual({ protected: true, branch: "feature-idx", snapshot: { id: "s-9" } });
|
|
186
|
+
} finally {
|
|
187
|
+
api.stop();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("clone reset proxies /clone/<id>/reset POST", async () => {
|
|
192
|
+
const api = await startFakeApi();
|
|
193
|
+
try {
|
|
194
|
+
const r = await runCliAsync(
|
|
195
|
+
["dblab", "clone", "reset", "c-8f21", "--project", "main-db", "--json"],
|
|
196
|
+
isolatedEnv({ PGAI_API_KEY: "test-key", PGAI_API_BASE_URL: api.baseUrl })
|
|
197
|
+
);
|
|
198
|
+
expect(r.status).toBe(0);
|
|
199
|
+
const body = api.proxyCalls()[0].bodyJson;
|
|
200
|
+
expect(body.action).toBe("/clone/c-8f21/reset");
|
|
201
|
+
expect(body.method).toBe("post");
|
|
202
|
+
expect(body.data).toEqual({ latest: true });
|
|
203
|
+
} finally {
|
|
204
|
+
api.stop();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("clone destroy proxies /clone/<id> DELETE and resolves a numeric project id", async () => {
|
|
209
|
+
const api = await startFakeApi();
|
|
210
|
+
try {
|
|
211
|
+
const r = await runCliAsync(
|
|
212
|
+
["dblab", "clone", "destroy", "c-8f21", "--project", "12", "--json"],
|
|
213
|
+
isolatedEnv({ PGAI_API_KEY: "test-key", PGAI_API_BASE_URL: api.baseUrl })
|
|
214
|
+
);
|
|
215
|
+
expect(r.status).toBe(0);
|
|
216
|
+
const body = api.proxyCalls()[0].bodyJson;
|
|
217
|
+
expect(body.instance_id).toBe("7"); // resolved from project_id 12
|
|
218
|
+
expect(body.action).toBe("/clone/c-8f21");
|
|
219
|
+
expect(body.method).toBe("delete");
|
|
220
|
+
} finally {
|
|
221
|
+
api.stop();
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("branch create proxies /branch POST with branchName + snapshotID", async () => {
|
|
226
|
+
const api = await startFakeApi();
|
|
227
|
+
try {
|
|
228
|
+
const r = await runCliAsync(
|
|
229
|
+
["dblab", "branch", "create", "feature-idx", "--project", "main-db", "--snapshot", "latest", "--json"],
|
|
230
|
+
isolatedEnv({ PGAI_API_KEY: "test-key", PGAI_API_BASE_URL: api.baseUrl })
|
|
231
|
+
);
|
|
232
|
+
expect(r.status).toBe(0);
|
|
233
|
+
const body = api.proxyCalls()[0].bodyJson;
|
|
234
|
+
expect(body.action).toBe("/branch");
|
|
235
|
+
expect(body.method).toBe("post");
|
|
236
|
+
expect(body.data).toEqual({ branchName: "feature-idx", snapshotID: "latest" });
|
|
237
|
+
} finally {
|
|
238
|
+
api.stop();
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("branch log proxies /branch/<name>/log GET", async () => {
|
|
243
|
+
const api = await startFakeApi();
|
|
244
|
+
try {
|
|
245
|
+
const r = await runCliAsync(
|
|
246
|
+
["dblab", "branch", "log", "feature-idx", "--project", "main-db", "--json"],
|
|
247
|
+
isolatedEnv({ PGAI_API_KEY: "test-key", PGAI_API_BASE_URL: api.baseUrl })
|
|
248
|
+
);
|
|
249
|
+
expect(r.status).toBe(0);
|
|
250
|
+
const body = api.proxyCalls()[0].bodyJson;
|
|
251
|
+
expect(body.action).toBe("/branch/feature-idx/log");
|
|
252
|
+
expect(body.method).toBe("get");
|
|
253
|
+
} finally {
|
|
254
|
+
api.stop();
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("snapshot list proxies /snapshots GET", async () => {
|
|
259
|
+
const api = await startFakeApi();
|
|
260
|
+
try {
|
|
261
|
+
const r = await runCliAsync(
|
|
262
|
+
["dblab", "snapshot", "list", "--project", "main-db", "--json"],
|
|
263
|
+
isolatedEnv({ PGAI_API_KEY: "test-key", PGAI_API_BASE_URL: api.baseUrl })
|
|
264
|
+
);
|
|
265
|
+
expect(r.status).toBe(0);
|
|
266
|
+
const body = api.proxyCalls()[0].bodyJson;
|
|
267
|
+
expect(body.action).toBe("/snapshots");
|
|
268
|
+
expect(body.method).toBe("get");
|
|
269
|
+
} finally {
|
|
270
|
+
api.stop();
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("snapshot create proxies /branch/snapshot POST with cloneID", async () => {
|
|
275
|
+
const api = await startFakeApi();
|
|
276
|
+
try {
|
|
277
|
+
const r = await runCliAsync(
|
|
278
|
+
["dblab", "snapshot", "create", "--project", "main-db", "--clone", "c-8f21", "--message", "before idx", "--json"],
|
|
279
|
+
isolatedEnv({ PGAI_API_KEY: "test-key", PGAI_API_BASE_URL: api.baseUrl })
|
|
280
|
+
);
|
|
281
|
+
expect(r.status).toBe(0);
|
|
282
|
+
const body = api.proxyCalls()[0].bodyJson;
|
|
283
|
+
expect(body.action).toBe("/branch/snapshot");
|
|
284
|
+
expect(body.data).toEqual({ cloneID: "c-8f21", message: "before idx" });
|
|
285
|
+
} finally {
|
|
286
|
+
api.stop();
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("snapshot destroy proxies /snapshot/<id>?force=<bool> DELETE", async () => {
|
|
291
|
+
const api = await startFakeApi();
|
|
292
|
+
try {
|
|
293
|
+
const r = await runCliAsync(
|
|
294
|
+
["dblab", "snapshot", "destroy", "s-1", "--project", "main-db", "--force", "--json"],
|
|
295
|
+
isolatedEnv({ PGAI_API_KEY: "test-key", PGAI_API_BASE_URL: api.baseUrl })
|
|
296
|
+
);
|
|
297
|
+
expect(r.status).toBe(0);
|
|
298
|
+
const body = api.proxyCalls()[0].bodyJson;
|
|
299
|
+
expect(body.action).toBe("/snapshot/s-1?force=true");
|
|
300
|
+
expect(body.method).toBe("delete");
|
|
301
|
+
} finally {
|
|
302
|
+
api.stop();
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("an unknown project alias exits 1 with a helpful message (no proxy call)", async () => {
|
|
307
|
+
const api = await startFakeApi();
|
|
308
|
+
try {
|
|
309
|
+
const r = await runCliAsync(
|
|
310
|
+
["dblab", "clone", "list", "--project", "does-not-exist"],
|
|
311
|
+
isolatedEnv({ PGAI_API_KEY: "test-key", PGAI_API_BASE_URL: api.baseUrl })
|
|
312
|
+
);
|
|
313
|
+
expect(r.status).toBe(1);
|
|
314
|
+
expect(`${r.stdout}\n${r.stderr}`).toContain("No DBLab instance found");
|
|
315
|
+
expect(api.proxyCalls().length).toBe(0);
|
|
316
|
+
} finally {
|
|
317
|
+
api.stop();
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("a backend scope error (403) surfaces and exits 1", async () => {
|
|
322
|
+
// Fake server that 403s the proxy call to emulate a missing joe:admin scope.
|
|
323
|
+
const server = Bun.serve({
|
|
324
|
+
hostname: "127.0.0.1",
|
|
325
|
+
port: 0,
|
|
326
|
+
async fetch(req) {
|
|
327
|
+
const url = new URL(req.url);
|
|
328
|
+
if (req.method === "POST" && url.pathname.endsWith("/rpc/projects_list")) {
|
|
329
|
+
return new Response(JSON.stringify([{ project_id: 12, alias: "main-db", dblab_instance_id: 7 }]), { status: 200 });
|
|
330
|
+
}
|
|
331
|
+
return new Response('{"message":"insufficient scope: joe:admin required"}', { status: 403 });
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
try {
|
|
335
|
+
const baseUrl = `http://${server.hostname}:${server.port}/api/general`;
|
|
336
|
+
const r = await runCliAsync(
|
|
337
|
+
["dblab", "clone", "destroy", "c-1", "--project", "main-db"],
|
|
338
|
+
isolatedEnv({ PGAI_API_KEY: "test-key", PGAI_API_BASE_URL: baseUrl })
|
|
339
|
+
);
|
|
340
|
+
expect(r.status).toBe(1);
|
|
341
|
+
expect(`${r.stdout}\n${r.stderr}`).toContain("Failed to destroy clone");
|
|
342
|
+
} finally {
|
|
343
|
+
server.stop(true);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
// --help must accurately reflect the backend scope gate (user-approved
|
|
350
|
+
// tightening, !612): the destructive DELETE verbs (clone destroy, branch
|
|
351
|
+
// delete, snapshot destroy) require joe:admin; clone RESET (a POST) does NOT.
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
describe("dblab --help scope annotations match the backend gate", () => {
|
|
354
|
+
test("clone destroy --help notes joe:admin", () => {
|
|
355
|
+
const r = runCli(["dblab", "clone", "destroy", "--help"], isolatedEnv());
|
|
356
|
+
expect(`${r.stdout}\n${r.stderr}`).toMatch(/joe:admin/);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("branch delete --help notes joe:admin", () => {
|
|
360
|
+
const r = runCli(["dblab", "branch", "delete", "--help"], isolatedEnv());
|
|
361
|
+
expect(`${r.stdout}\n${r.stderr}`).toMatch(/joe:admin/);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("snapshot destroy --help notes joe:admin", () => {
|
|
365
|
+
const r = runCli(["dblab", "snapshot", "destroy", "--help"], isolatedEnv());
|
|
366
|
+
expect(`${r.stdout}\n${r.stderr}`).toMatch(/joe:admin/);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("clone reset --help does NOT claim joe:admin (reset is a POST, ungated)", () => {
|
|
370
|
+
const r = runCli(["dblab", "clone", "reset", "--help"], isolatedEnv());
|
|
371
|
+
expect(`${r.stdout}\n${r.stderr}`).not.toMatch(/joe:admin/);
|
|
372
|
+
});
|
|
373
|
+
});
|