@botbuddy/cli 1.4.2 → 1.5.1
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/package.json +3 -2
- package/src/commands.mjs +7 -0
- package/src/docker-hygiene.mjs +1062 -0
- package/src/run.mjs +5 -1
- package/src/stack-file-lock.mjs +180 -0
- package/src/stack.mjs +257 -36
- package/src/auth.test.mjs +0 -404
- package/src/discovery.test.mjs +0 -195
- package/src/locks.test.mjs +0 -60
- package/src/profile-bootstrap.test.mjs +0 -205
- package/src/publish-equal.test.mjs +0 -176
- package/src/publish-workflow.test.mjs +0 -122
- package/src/quiet-runner.test.mjs +0 -109
- package/src/run.test.mjs +0 -173
- package/src/stack.test.mjs +0 -434
- package/src/wait-profile.test.mjs +0 -30
- package/src/wait.test.mjs +0 -266
package/src/wait.test.mjs
DELETED
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
3
|
-
import { execFile, spawn } from "node:child_process";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
6
|
-
import { promisify } from "node:util";
|
|
7
|
-
import { once } from "node:events";
|
|
8
|
-
import { createServer } from "node:http";
|
|
9
|
-
import { mkdtemp } from "node:fs/promises";
|
|
10
|
-
import { tmpdir } from "node:os";
|
|
11
|
-
import { VERSION } from "./version.mjs";
|
|
12
|
-
|
|
13
|
-
const exec = promisify(execFile);
|
|
14
|
-
const BIN = join(dirname(dirname(fileURLToPath(import.meta.url))), "bin", "botbuddy.mjs");
|
|
15
|
-
const npmEnv = {
|
|
16
|
-
PATH: process.env.PATH,
|
|
17
|
-
HOME: process.env.HOME,
|
|
18
|
-
TMPDIR: process.env.TMPDIR,
|
|
19
|
-
npm_config_userconfig: "/dev/null",
|
|
20
|
-
npm_config_globalconfig: join(tmpdir(), "botbuddy-empty-npmrc"),
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
test("BOT-1344: public CLI exposes the wait command", async () => {
|
|
24
|
-
const { stdout } = await exec("node", [BIN, "wait", "--help"]);
|
|
25
|
-
assert.match(stdout, /botbuddy wait/);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test("BOT-1344: local argument failures emit one typed terminal receipt", async () => {
|
|
29
|
-
for (const [args, error] of [
|
|
30
|
-
[["--bogus"], "invalid_arguments"],
|
|
31
|
-
[["timer:duration=1", "--timeout", "0"], "invalid_timeout"],
|
|
32
|
-
[["timer:duration=1", "--receipt-max-bytes", "300"], "invalid_receipt_max_bytes"],
|
|
33
|
-
]) {
|
|
34
|
-
const result = await exec("node", [BIN, "wait", ...args]).then(
|
|
35
|
-
({ stdout, stderr }) => ({ code: 0, stdout, stderr }),
|
|
36
|
-
(failure) => ({ code: failure.code, stdout: failure.stdout, stderr: failure.stderr }),
|
|
37
|
-
);
|
|
38
|
-
assert.equal(result.code, 4);
|
|
39
|
-
const lines = result.stdout.trim().split("\n");
|
|
40
|
-
assert.equal(lines.length, 1, `${error} emits one receipt`);
|
|
41
|
-
const receipt = JSON.parse(lines[0]);
|
|
42
|
-
assert.equal(receipt.outcome, "error");
|
|
43
|
-
assert.equal(receipt.error, error);
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("BOT-1344: missing wait-option values fail closed with one bounded receipt", async () => {
|
|
48
|
-
for (const option of ["--url", "--since", "--profile"]) {
|
|
49
|
-
const result = await exec("node", [BIN, "wait", "timer:duration=1", option]).then(
|
|
50
|
-
({ stdout, stderr }) => ({ code: 0, stdout, stderr }),
|
|
51
|
-
(failure) => ({ code: failure.code, stdout: failure.stdout, stderr: failure.stderr }),
|
|
52
|
-
);
|
|
53
|
-
assert.equal(result.code, 4);
|
|
54
|
-
const receipt = JSON.parse(result.stdout.trim());
|
|
55
|
-
assert.equal(receipt.error, "invalid_arguments");
|
|
56
|
-
assert.equal(receipt.option, option);
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test("BOT-1344: local error receipts honour a valid byte cap", async () => {
|
|
61
|
-
const longInvalidCondition = `unknown:type=${"x".repeat(2_000)}`;
|
|
62
|
-
const result = await exec("node", [
|
|
63
|
-
BIN, "wait", longInvalidCondition, "--receipt-max-bytes", "512",
|
|
64
|
-
]).then(
|
|
65
|
-
({ stdout, stderr }) => ({ code: 0, stdout, stderr }),
|
|
66
|
-
(failure) => ({ code: failure.code, stdout: failure.stdout, stderr: failure.stderr }),
|
|
67
|
-
);
|
|
68
|
-
assert.equal(result.code, 4);
|
|
69
|
-
const line = result.stdout.trim();
|
|
70
|
-
assert.ok(Buffer.byteLength(line, "utf8") <= 512);
|
|
71
|
-
const receipt = JSON.parse(line);
|
|
72
|
-
assert.equal(receipt.error, "invalid_conditions");
|
|
73
|
-
assert.equal(receipt.errors_truncated, true);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test("BOT-1344: profile recovery is installation-free", async () => {
|
|
77
|
-
const server = createServer(async (req, res) => {
|
|
78
|
-
for await (const _chunk of req) {
|
|
79
|
-
// Drain the registration request before replying.
|
|
80
|
-
}
|
|
81
|
-
res.writeHead(401, { "content-type": "application/json" });
|
|
82
|
-
res.end(JSON.stringify({ error: "unauthorized", detail: "credential revoked" }));
|
|
83
|
-
});
|
|
84
|
-
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
85
|
-
const { port } = server.address();
|
|
86
|
-
try {
|
|
87
|
-
const result = await exec("node", [
|
|
88
|
-
BIN, "wait", "chat:channel=*", "--profile", "botbuddy-dev", "--token", "test-key",
|
|
89
|
-
"--url", `http://127.0.0.1:${port}`,
|
|
90
|
-
]).then(
|
|
91
|
-
({ stdout, stderr }) => ({ code: 0, stdout, stderr }),
|
|
92
|
-
(error) => ({ code: error.code, stdout: error.stdout, stderr: error.stderr }),
|
|
93
|
-
);
|
|
94
|
-
assert.equal(result.code, 3);
|
|
95
|
-
assert.equal(
|
|
96
|
-
JSON.parse(result.stdout.trim()).recovery,
|
|
97
|
-
"npx --yes @botbuddy/cli@latest profile setup botbuddy-dev",
|
|
98
|
-
);
|
|
99
|
-
} finally {
|
|
100
|
-
await new Promise((resolve) => server.close(resolve));
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("BOT-1344: client_outdated is a typed receipt with a copy-paste upgrade", async () => {
|
|
105
|
-
let registration;
|
|
106
|
-
const server = createServer(async (req, res) => {
|
|
107
|
-
const chunks = [];
|
|
108
|
-
for await (const chunk of req) chunks.push(chunk);
|
|
109
|
-
registration = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
110
|
-
res.writeHead(426, { "content-type": "application/json" });
|
|
111
|
-
res.end(JSON.stringify({
|
|
112
|
-
error: "client_outdated",
|
|
113
|
-
detail: "upgrade required",
|
|
114
|
-
minimum_wait_protocol: 2,
|
|
115
|
-
upgrade_command: "npx --yes @botbuddy/cli@latest wait",
|
|
116
|
-
}));
|
|
117
|
-
});
|
|
118
|
-
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
119
|
-
const { port } = server.address();
|
|
120
|
-
try {
|
|
121
|
-
const result = await exec("node", [
|
|
122
|
-
BIN, "wait", "chat:channel=*", "--profile", "botbuddy-dev", "--token", "test-key",
|
|
123
|
-
"--url", `http://127.0.0.1:${port}`,
|
|
124
|
-
]).then(
|
|
125
|
-
({ stdout, stderr }) => ({ code: 0, stdout, stderr }),
|
|
126
|
-
(error) => ({ code: error.code, stdout: error.stdout, stderr: error.stderr }),
|
|
127
|
-
);
|
|
128
|
-
assert.equal(result.code, 4);
|
|
129
|
-
assert.equal(registration.client_version, VERSION);
|
|
130
|
-
assert.equal(registration.wait_protocol_version, 1);
|
|
131
|
-
const receipt = JSON.parse(result.stdout.trim());
|
|
132
|
-
assert.equal(receipt.error, "client_outdated");
|
|
133
|
-
assert.equal(receipt.upgrade_command, "npx --yes @botbuddy/cli@latest wait");
|
|
134
|
-
assert.deepEqual(receipt.client, {
|
|
135
|
-
package: "@botbuddy/cli", version: VERSION, wait_protocol: 1,
|
|
136
|
-
});
|
|
137
|
-
} finally {
|
|
138
|
-
await new Promise((resolve) => server.close(resolve));
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test("BOT-1344: active-wait cap emits one typed terminal receipt", async () => {
|
|
143
|
-
const server = createServer(async (req, res) => {
|
|
144
|
-
for await (const _chunk of req) {
|
|
145
|
-
// Drain the registration request before replying.
|
|
146
|
-
}
|
|
147
|
-
res.writeHead(429, { "content-type": "application/json" });
|
|
148
|
-
res.end(JSON.stringify({
|
|
149
|
-
error: "wait_session_cap_exceeded",
|
|
150
|
-
detail: "too many active waits for this agent",
|
|
151
|
-
}));
|
|
152
|
-
});
|
|
153
|
-
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
154
|
-
const { port } = server.address();
|
|
155
|
-
try {
|
|
156
|
-
const result = await exec("node", [
|
|
157
|
-
BIN, "wait", "chat:channel=*", "--profile", "botbuddy-dev", "--token", "test-key",
|
|
158
|
-
"--url", `http://127.0.0.1:${port}`,
|
|
159
|
-
]).then(
|
|
160
|
-
({ stdout, stderr }) => ({ code: 0, stdout, stderr }),
|
|
161
|
-
(error) => ({ code: error.code, stdout: error.stdout, stderr: error.stderr }),
|
|
162
|
-
);
|
|
163
|
-
assert.equal(result.code, 4);
|
|
164
|
-
const lines = result.stdout.trim().split("\n");
|
|
165
|
-
assert.equal(lines.length, 1, "the public command always emits exactly one receipt");
|
|
166
|
-
const receipt = JSON.parse(lines[0]);
|
|
167
|
-
assert.equal(receipt.outcome, "error");
|
|
168
|
-
assert.equal(receipt.error, "wait_session_cap_exceeded");
|
|
169
|
-
} finally {
|
|
170
|
-
await new Promise((resolve) => server.close(resolve));
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
test("BOT-1344: receipt cap includes public client metadata", async () => {
|
|
175
|
-
const cap = 512;
|
|
176
|
-
const { stdout } = await exec("node", [
|
|
177
|
-
BIN, "wait", "timer:duration=1", "--receipt-max-bytes", String(cap),
|
|
178
|
-
]);
|
|
179
|
-
const line = stdout.trim();
|
|
180
|
-
assert.ok(Buffer.byteLength(line, "utf8") <= cap, "client metadata is inside the requested receipt cap");
|
|
181
|
-
const receipt = JSON.parse(line);
|
|
182
|
-
assert.deepEqual(receipt.client, {
|
|
183
|
-
package: "@botbuddy/cli", version: VERSION, wait_protocol: 1,
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
test("BOT-1344: packed CLI runs a deterministic wait outside this checkout", async () => {
|
|
188
|
-
const packageDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
189
|
-
const packedDir = await mkdtemp(join(tmpdir(), "botbuddy-cli-pack-"));
|
|
190
|
-
const installedDir = await mkdtemp(join(tmpdir(), "botbuddy-cli-install-"));
|
|
191
|
-
const { stdout: packed } = await exec("npm", ["pack", "--json", "--pack-destination", packedDir], { cwd: packageDir, env: npmEnv });
|
|
192
|
-
const tarball = join(packedDir, JSON.parse(packed)[0].filename);
|
|
193
|
-
await exec("npm", ["install", "--ignore-scripts", "--no-audit", "--no-fund", tarball], { cwd: installedDir, env: npmEnv });
|
|
194
|
-
const { stdout } = await exec("node", [
|
|
195
|
-
join(installedDir, "node_modules", ".bin", "botbuddy"), "wait", "timer:duration=1",
|
|
196
|
-
], { cwd: installedDir });
|
|
197
|
-
const receipt = JSON.parse(stdout.trim());
|
|
198
|
-
assert.equal(receipt.outcome, "matched");
|
|
199
|
-
assert.equal(receipt.client.package, "@botbuddy/cli");
|
|
200
|
-
assert.equal(receipt.client.wait_protocol, 1);
|
|
201
|
-
const { stdout: profileEnv } = await exec("node", [
|
|
202
|
-
join(installedDir, "node_modules", ".bin", "botbuddy"), "profile", "env", "botbuddy-dev",
|
|
203
|
-
], { cwd: installedDir });
|
|
204
|
-
assert.match(profileEnv, /security find-generic-password/);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
// BOT-1383: the loopback-login runtime must load from an installed tarball and
|
|
208
|
-
// actually START the RFC 8252 flow (register → print URL → wait), never the old
|
|
209
|
-
// "No authorization code in redirect" early exit. --no-browser keeps it from
|
|
210
|
-
// opening a real browser, and a local mock server stands in for production.
|
|
211
|
-
test("BOT-1383: packed CLI starts loopback login from outside this checkout", async () => {
|
|
212
|
-
const packageDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
213
|
-
const packedDir = await mkdtemp(join(tmpdir(), "botbuddy-cli-login-pack-"));
|
|
214
|
-
const installedDir = await mkdtemp(join(tmpdir(), "botbuddy-cli-login-install-"));
|
|
215
|
-
const fakeHome = await mkdtemp(join(tmpdir(), "botbuddy-cli-login-home-"));
|
|
216
|
-
const { stdout: packed } = await exec("npm", ["pack", "--json", "--pack-destination", packedDir], { cwd: packageDir, env: npmEnv });
|
|
217
|
-
const tarball = join(packedDir, JSON.parse(packed)[0].filename);
|
|
218
|
-
await exec("npm", ["install", "--ignore-scripts", "--no-audit", "--no-fund", tarball], { cwd: installedDir, env: npmEnv });
|
|
219
|
-
const bin = join(installedDir, "node_modules", ".bin", "botbuddy");
|
|
220
|
-
|
|
221
|
-
// login --help loads the new modules and documents the browser behavior.
|
|
222
|
-
const { stdout: help } = await exec("node", [bin, "login", "--help"], { cwd: installedDir });
|
|
223
|
-
assert.match(help, /--no-browser/);
|
|
224
|
-
assert.match(help, /loopback callback/);
|
|
225
|
-
|
|
226
|
-
// A local mock authorization server so --no-browser can register a client.
|
|
227
|
-
const server = createServer((req, res) => {
|
|
228
|
-
if (req.url.endsWith("/register")) {
|
|
229
|
-
res.writeHead(201, { "content-type": "application/json" });
|
|
230
|
-
res.end(JSON.stringify({ client_id: "smoke-client" }));
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
res.writeHead(404).end();
|
|
234
|
-
});
|
|
235
|
-
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
236
|
-
const { port } = server.address();
|
|
237
|
-
|
|
238
|
-
try {
|
|
239
|
-
const child = spawn("node", [bin, "login", "--no-browser"], {
|
|
240
|
-
cwd: installedDir,
|
|
241
|
-
env: {
|
|
242
|
-
...process.env,
|
|
243
|
-
HOME: fakeHome,
|
|
244
|
-
BOTBUDDY_SERVER_URL: `http://127.0.0.1:${port}/functions/v1/mcp-server`,
|
|
245
|
-
},
|
|
246
|
-
});
|
|
247
|
-
let out = "";
|
|
248
|
-
child.stdout.on("data", (c) => (out += c));
|
|
249
|
-
child.stderr.on("data", (c) => (out += c));
|
|
250
|
-
|
|
251
|
-
// Wait until it has registered and is listening for the callback, then stop.
|
|
252
|
-
const deadline = Date.now() + 8000;
|
|
253
|
-
while (!/Waiting for authorization/.test(out) && Date.now() < deadline) {
|
|
254
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
255
|
-
}
|
|
256
|
-
child.kill("SIGKILL");
|
|
257
|
-
await once(child, "exit");
|
|
258
|
-
|
|
259
|
-
assert.match(out, /127\.0\.0\.1/, "prints the loopback callback host");
|
|
260
|
-
assert.match(out, /callback/, "prints the callback path");
|
|
261
|
-
assert.match(out, /Waiting for authorization/, "starts the callback listener");
|
|
262
|
-
assert.doesNotMatch(out, /No authorization code in redirect/, "old early-exit path is gone");
|
|
263
|
-
} finally {
|
|
264
|
-
await new Promise((resolve) => server.close(resolve));
|
|
265
|
-
}
|
|
266
|
-
});
|