@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/src/auth.test.mjs DELETED
@@ -1,404 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import http from "node:http";
4
- import { EventEmitter } from "node:events";
5
-
6
- import { doLogin } from "./auth.mjs";
7
- import {
8
- createLoopbackReceiver,
9
- buildAuthorizeUrl,
10
- generatePkce,
11
- generateState,
12
- openBrowser,
13
- } from "./oauth-loopback.mjs";
14
-
15
- // ─── helpers ────────────────────────────────────────────────────────────────
16
-
17
- function httpGet(url) {
18
- return new Promise((resolve, reject) => {
19
- http
20
- .get(url, (res) => {
21
- let body = "";
22
- res.on("data", (c) => (body += c));
23
- res.on("end", () => resolve({ status: res.statusCode, body, headers: res.headers }));
24
- })
25
- .on("error", reject);
26
- });
27
- }
28
-
29
- function jsonResponse(obj, status = 200) {
30
- return { status, ok: status < 400, json: async () => obj };
31
- }
32
-
33
- // A mock fetch that only ever answers /register and /token. It records every
34
- // call so tests can assert /authorize is never fetched and the callback URI is
35
- // identical across registration and token exchange.
36
- function makeFetch({ tokenResponse, registerResponse } = {}) {
37
- const calls = [];
38
- const fetchImpl = async (url, opts = {}) => {
39
- calls.push({ url, opts });
40
- if (url.endsWith("/register")) {
41
- const body = JSON.parse(opts.body);
42
- return jsonResponse(registerResponse ?? { client_id: "client-123", redirect_uris: body.redirect_uris });
43
- }
44
- if (url.endsWith("/token")) {
45
- return jsonResponse(tokenResponse ?? { access_token: "tok-secret-abc", expires_in: 3600 });
46
- }
47
- throw new Error(`unexpected fetch: ${url}`);
48
- };
49
- return { fetchImpl, calls };
50
- }
51
-
52
- // A stand-in for the browser: given the authorize URL, it plays the role of the
53
- // server-driven redirect and hits the loopback callback with a valid code+state.
54
- function browserThatCompletes(code = "auth-code-xyz") {
55
- return async (authUrl) => {
56
- const u = new URL(authUrl);
57
- const redirectUri = u.searchParams.get("redirect_uri");
58
- const state = u.searchParams.get("state");
59
- await httpGet(`${redirectUri}?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`);
60
- };
61
- }
62
-
63
- function captureUrl(line) {
64
- const m = String(line).match(/https?:\/\/[^\s\x1b]+/);
65
- return m ? m[0] : null;
66
- }
67
-
68
- async function waitFor(pred, timeout = 2000) {
69
- const start = Date.now();
70
- while (!pred()) {
71
- if (Date.now() - start > timeout) throw new Error("waitFor timed out");
72
- await new Promise((r) => setTimeout(r, 10));
73
- }
74
- }
75
-
76
- const SERVER = "https://server.test/functions/v1/mcp-server";
77
-
78
- function baseDeps(overrides = {}) {
79
- return {
80
- serverUrl: SERVER,
81
- saveConfig: () => {},
82
- getConfig: () => ({}),
83
- log: () => {},
84
- errorLog: () => {},
85
- timeoutMs: 3000,
86
- now: () => 1000,
87
- ...overrides,
88
- };
89
- }
90
-
91
- // ─── AC-16: happy path ───────────────────────────────────────────────────────
92
-
93
- test("BOT-1383: happy path registers, authorizes via browser, exchanges token, saves config", async () => {
94
- const { fetchImpl, calls } = makeFetch();
95
- const out = [];
96
- let saved = null;
97
-
98
- const result = await doLogin(
99
- { noBrowser: false },
100
- baseDeps({
101
- fetch: fetchImpl,
102
- openBrowser: browserThatCompletes("code-1"),
103
- saveConfig: (c) => { saved = c; },
104
- getConfig: () => ({ existing: "keep" }),
105
- log: (s) => out.push(String(s)),
106
- }),
107
- );
108
-
109
- assert.ok(saved, "config was saved");
110
- assert.equal(saved.access_token, "tok-secret-abc");
111
- assert.equal(saved.client_id, "client-123");
112
- assert.equal(saved.existing, "keep", "preserves existing config fields");
113
- assert.equal(saved.token_expires_at, 1000 + 3600 * 1000);
114
- assert.match(result.redirectUri, /^http:\/\/127\.0\.0\.1:\d+\/callback$/);
115
- });
116
-
117
- // ─── AC-16: callback URI identical across registration and token exchange ─────
118
-
119
- test("BOT-1383: the callback URI is identical in registration and token exchange", async () => {
120
- const { fetchImpl, calls } = makeFetch();
121
- await doLogin({}, baseDeps({ fetch: fetchImpl, openBrowser: browserThatCompletes() }));
122
-
123
- const reg = calls.find((c) => c.url.endsWith("/register"));
124
- const tok = calls.find((c) => c.url.endsWith("/token"));
125
- const regRedirect = JSON.parse(reg.opts.body).redirect_uris[0];
126
- const tokRedirect = new URLSearchParams(tok.opts.body.toString()).get("redirect_uri");
127
-
128
- assert.equal(regRedirect, tokRedirect);
129
- assert.match(regRedirect, /^http:\/\/127\.0\.0\.1:\d+\/callback$/);
130
- assert.doesNotMatch(regRedirect, /19836/, "does not keep the old fixed port");
131
- });
132
-
133
- // ─── AC-16 regression: the CLI never fetches /authorize ───────────────────────
134
-
135
- test("BOT-1383 regression: the CLI never fetches /authorize and cannot hit the old redirect path", async () => {
136
- const { fetchImpl, calls } = makeFetch();
137
- await doLogin({}, baseDeps({ fetch: fetchImpl, openBrowser: browserThatCompletes() }));
138
-
139
- assert.ok(!calls.some((c) => c.url.includes("/authorize")), "no /authorize fetch");
140
- assert.deepEqual(
141
- calls.map((c) => c.url.replace(SERVER, "")),
142
- ["/register", "/token"],
143
- );
144
- });
145
-
146
- // ─── AC-4/AC-16: secrets never leak to output ─────────────────────────────────
147
-
148
- test("BOT-1383: verifier and access token never appear in terminal output", async () => {
149
- const { fetchImpl, calls } = makeFetch();
150
- const out = [];
151
- await doLogin(
152
- {},
153
- baseDeps({ fetch: fetchImpl, openBrowser: browserThatCompletes(), log: (s) => out.push(String(s)), errorLog: (s) => out.push(String(s)) }),
154
- );
155
-
156
- const joined = out.join("\n");
157
- const tok = calls.find((c) => c.url.endsWith("/token"));
158
- const verifier = new URLSearchParams(tok.opts.body.toString()).get("code_verifier");
159
-
160
- assert.ok(verifier && verifier.length >= 43, "verifier present in token exchange");
161
- assert.ok(!joined.includes(verifier), "verifier not printed");
162
- assert.ok(!joined.includes("tok-secret-abc"), "access token not printed");
163
- });
164
-
165
- // ─── AC-9: browser callback page discloses no secrets and is no-store ─────────
166
-
167
- test("BOT-1383: the callback success page is no-store and discloses no secrets", async () => {
168
- const receiver = createLoopbackReceiver({ expectedState: "state-1" });
169
- const { redirectUri } = await receiver.listen();
170
- const waited = receiver.waitForCallback({ timeoutMs: 2000 });
171
-
172
- const resp = await httpGet(`${redirectUri}?code=SUPERSECRETCODE&state=state-1`);
173
- const { code } = await waited;
174
-
175
- assert.equal(code, "SUPERSECRETCODE");
176
- assert.equal(resp.status, 200);
177
- assert.equal(resp.headers["cache-control"], "no-store");
178
- assert.ok(!resp.body.includes("SUPERSECRETCODE"), "code not in HTML");
179
- assert.ok(!resp.body.includes("state-1"), "state not in HTML");
180
- // The page fires before token exchange, so it must not claim final success;
181
- // it points the user to the terminal for the actual result.
182
- assert.match(resp.body, /terminal/i, "directs the user to the terminal");
183
- assert.doesNotMatch(resp.body, /succeeded/i, "does not over-claim authentication success");
184
- await receiver.close();
185
- });
186
-
187
- // ─── AC-8: state validation ───────────────────────────────────────────────────
188
-
189
- test("BOT-1383: a wrong-state request is rejected but the listener keeps waiting", async () => {
190
- const receiver = createLoopbackReceiver({ expectedState: "good" });
191
- const { redirectUri } = await receiver.listen();
192
- const waited = receiver.waitForCallback({ timeoutMs: 2000 });
193
-
194
- // A stale/malicious tab or probe with the wrong state must NOT abort the login.
195
- const bad = await httpGet(`${redirectUri}?code=stolen&state=bad`);
196
- assert.equal(bad.status, 400);
197
-
198
- // The legitimate callback still completes afterward.
199
- const good = await httpGet(`${redirectUri}?code=real&state=good`);
200
- assert.equal(good.status, 200);
201
- const { code } = await waited;
202
- assert.equal(code, "real");
203
- await receiver.close();
204
- });
205
-
206
- test("BOT-1383: a callback missing the code is rejected", async () => {
207
- const receiver = createLoopbackReceiver({ expectedState: "good" });
208
- const { redirectUri } = await receiver.listen();
209
- const rejected = assert.rejects(receiver.waitForCallback({ timeoutMs: 2000 }), /code/i);
210
-
211
- const resp = await httpGet(`${redirectUri}?state=good`);
212
- assert.equal(resp.status, 400);
213
- await rejected;
214
- await receiver.close();
215
- });
216
-
217
- // ─── AC-8: OAuth denial ───────────────────────────────────────────────────────
218
-
219
- test("BOT-1383: an OAuth error/denial callback is surfaced", async () => {
220
- const receiver = createLoopbackReceiver({ expectedState: "s" });
221
- const { redirectUri } = await receiver.listen();
222
- const rejected = assert.rejects(receiver.waitForCallback({ timeoutMs: 2000 }), (err) => err.code === "access_denied");
223
-
224
- await httpGet(`${redirectUri}?state=s&error=access_denied&error_description=User%20denied`);
225
- await rejected;
226
- await receiver.close();
227
- });
228
-
229
- // ─── AC-8: duplicate callback completes at most once ──────────────────────────
230
-
231
- test("BOT-1383: a duplicate callback completes at most once", async () => {
232
- const receiver = createLoopbackReceiver({ expectedState: "s" });
233
- const { redirectUri } = await receiver.listen();
234
- const waited = receiver.waitForCallback({ timeoutMs: 2000 });
235
-
236
- const r1 = await httpGet(`${redirectUri}?code=c1&state=s`);
237
- const { code } = await waited;
238
- const r2 = await httpGet(`${redirectUri}?code=c2&state=s`);
239
-
240
- assert.equal(code, "c1", "first code wins");
241
- assert.equal(r1.status, 200);
242
- assert.equal(r2.status, 200, "duplicate handled gracefully");
243
- await receiver.close();
244
- });
245
-
246
- // ─── AC-16: unrelated paths keep the listener waiting ─────────────────────────
247
-
248
- test("BOT-1383: a request to /favicon.ico does not complete the login", async () => {
249
- const receiver = createLoopbackReceiver({ expectedState: "s" });
250
- const { redirectUri, port } = await receiver.listen();
251
- const waited = receiver.waitForCallback({ timeoutMs: 2000 });
252
-
253
- const fav = await httpGet(`http://127.0.0.1:${port}/favicon.ico`);
254
- assert.equal(fav.status, 404);
255
-
256
- await httpGet(`${redirectUri}?code=ok&state=s`);
257
- const { code } = await waited;
258
- assert.equal(code, "ok");
259
- await receiver.close();
260
- });
261
-
262
- // ─── AC-11: token failure saves nothing ───────────────────────────────────────
263
-
264
- test("BOT-1383: a token-exchange failure errors and saves no credentials", async () => {
265
- const { fetchImpl } = makeFetch({ tokenResponse: { error: "invalid_grant" } });
266
- let saved = null;
267
- await assert.rejects(
268
- doLogin({}, baseDeps({ fetch: fetchImpl, openBrowser: browserThatCompletes(), saveConfig: () => { saved = "X"; } })),
269
- /Token exchange failed/,
270
- );
271
- assert.equal(saved, null);
272
- });
273
-
274
- test("BOT-1383: a registration failure errors before any browser launch", async () => {
275
- const { fetchImpl } = makeFetch({ registerResponse: { error: "bad" } });
276
- let opened = false;
277
- await assert.rejects(
278
- doLogin({}, baseDeps({ fetch: fetchImpl, openBrowser: async () => { opened = true; } })),
279
- /Client registration failed/,
280
- );
281
- assert.equal(opened, false, "no browser launched when registration fails");
282
- });
283
-
284
- // ─── AC-11: timeout closes the listener and saves nothing ─────────────────────
285
-
286
- test("BOT-1383: a timeout errors, saves nothing, and never opens a real browser", async () => {
287
- const { fetchImpl } = makeFetch();
288
- let saved = null;
289
- const silentBrowser = async () => {}; // never fires the callback
290
- await assert.rejects(
291
- doLogin({}, baseDeps({ fetch: fetchImpl, openBrowser: silentBrowser, saveConfig: () => { saved = "X"; }, timeoutMs: 120 })),
292
- /Timed out/i,
293
- );
294
- assert.equal(saved, null);
295
- });
296
-
297
- // ─── AC-12: browser-launch failure still allows manual completion ─────────────
298
-
299
- test("BOT-1383: a browser-launch failure still allows manual completion", async () => {
300
- const { fetchImpl, calls } = makeFetch();
301
- const out = [];
302
- let saved = null;
303
- const failingBrowser = async () => { throw new Error("no browser here"); };
304
-
305
- const login = doLogin(
306
- {},
307
- baseDeps({ fetch: fetchImpl, openBrowser: failingBrowser, saveConfig: (c) => { saved = c; }, log: (s) => out.push(String(s)) }),
308
- );
309
-
310
- // The user opens the printed URL manually: fire the callback ourselves.
311
- await waitFor(() => out.some((l) => l.includes("/authorize?")));
312
- const authUrl = captureUrl(out.find((l) => l.includes("/authorize?")));
313
- const u = new URL(authUrl);
314
- await httpGet(`${u.searchParams.get("redirect_uri")}?code=manual&state=${u.searchParams.get("state")}`);
315
-
316
- await login;
317
- assert.ok(saved && saved.access_token === "tok-secret-abc");
318
- assert.ok(out.join("\n").includes("Couldn't open a browser"), "prints manual guidance");
319
- });
320
-
321
- // ─── AC-13: re-running login uses a fresh listener and state ───────────────────
322
-
323
- test("BOT-1383: re-running login uses an independent state and listener", async () => {
324
- async function runOnce() {
325
- const { fetchImpl } = makeFetch();
326
- const out = [];
327
- let saved = null;
328
- await doLogin({}, baseDeps({ fetch: fetchImpl, openBrowser: browserThatCompletes(), saveConfig: (c) => { saved = c; }, log: (s) => out.push(String(s)) }));
329
- const authUrl = captureUrl(out.find((l) => l.includes("/authorize?")));
330
- return { state: new URL(authUrl).searchParams.get("state"), saved };
331
- }
332
- const a = await runOnce();
333
- const b = await runOnce();
334
-
335
- assert.ok(a.saved.access_token && b.saved.access_token, "both logins succeed");
336
- assert.notEqual(a.state, b.state, "each login mints a fresh state");
337
- });
338
-
339
- // ─── AC-11: listener cleanup frees the socket ─────────────────────────────────
340
-
341
- test("BOT-1383: closing the receiver frees the socket", async () => {
342
- const receiver = createLoopbackReceiver({ expectedState: "s" });
343
- const { port } = await receiver.listen();
344
- await receiver.close();
345
- await assert.rejects(httpGet(`http://127.0.0.1:${port}/callback?state=s&code=c`), /ECONNREFUSED/);
346
- });
347
-
348
- // ─── AC-5: browser opener uses argument-based spawning per platform ────────────
349
-
350
- test("BOT-1383: openBrowser spawns an argument-based opener per platform, never a shell", async () => {
351
- const spawned = [];
352
- const fakeSpawn = (cmd, args, opts) => {
353
- spawned.push({ cmd, args, opts });
354
- const ee = new EventEmitter();
355
- ee.unref = () => {};
356
- setImmediate(() => ee.emit("spawn"));
357
- return ee;
358
- };
359
- const url = "https://x.test/authorize?a=b&c=d";
360
- await openBrowser(url, { platform: "darwin", spawn: fakeSpawn });
361
- await openBrowser(url, { platform: "linux", spawn: fakeSpawn });
362
- await openBrowser(url, { platform: "win32", spawn: fakeSpawn });
363
-
364
- assert.deepEqual(spawned[0].args, [url]);
365
- assert.equal(spawned[0].cmd, "open");
366
- assert.equal(spawned[1].cmd, "xdg-open");
367
- // Windows must NOT route the URL through cmd.exe (which splits on `&`); the
368
- // whole URL is a single argv entry to rundll32's FileProtocolHandler.
369
- assert.equal(spawned[2].cmd, "rundll32");
370
- assert.deepEqual(spawned[2].args, ["url.dll,FileProtocolHandler", url], "URL is one unparsed argv entry");
371
- assert.ok(spawned.every((s) => s.cmd !== "cmd"), "never invokes cmd.exe");
372
- // The `&`-bearing URL survives intact as a single argument on every platform.
373
- assert.ok(spawned.every((s) => s.args.includes(url)), "full URL passed as one discrete argument");
374
- assert.ok(spawned.every((s) => s.opts.shell !== true), "never spawns a shell");
375
- });
376
-
377
- test("BOT-1383: openBrowser rejects when the opener cannot spawn", async () => {
378
- const fakeSpawn = () => {
379
- const ee = new EventEmitter();
380
- setImmediate(() => ee.emit("error", new Error("ENOENT xdg-open")));
381
- return ee;
382
- };
383
- await assert.rejects(openBrowser("https://x", { platform: "linux", spawn: fakeSpawn }), /ENOENT/);
384
- });
385
-
386
- // ─── unit sanity for the pure builders ────────────────────────────────────────
387
-
388
- test("BOT-1383: buildAuthorizeUrl carries PKCE S256 and never the verifier", () => {
389
- const { codeVerifier, codeChallenge } = generatePkce();
390
- const state = generateState();
391
- const url = buildAuthorizeUrl({
392
- serverUrl: SERVER,
393
- clientId: "c1",
394
- redirectUri: "http://127.0.0.1:5555/callback",
395
- state,
396
- codeChallenge,
397
- });
398
- const u = new URL(url);
399
- assert.equal(u.searchParams.get("code_challenge_method"), "S256");
400
- assert.equal(u.searchParams.get("code_challenge"), codeChallenge);
401
- assert.equal(u.searchParams.get("response_type"), "code");
402
- assert.equal(u.searchParams.get("redirect_uri"), "http://127.0.0.1:5555/callback");
403
- assert.ok(!url.includes(codeVerifier), "verifier never in the browser URL");
404
- });
@@ -1,195 +0,0 @@
1
- // BOT-876: unit coverage for the CLI's generic call + discovery commands.
2
-
3
- import test from "node:test";
4
- import assert from "node:assert/strict";
5
-
6
- import {
7
- CallUsageError,
8
- discoveryUrlFor,
9
- formatDiscovery,
10
- parseCallArgs,
11
- } from "./discovery.mjs";
12
-
13
- const SERVER = "https://api.bot-buddy.ai/functions/v1/mcp-server";
14
-
15
- // ─── parseCallArgs ──────────────────────────────────────────────
16
-
17
- test("call with no arguments yields an empty args object", () => {
18
- assert.deepEqual(parseCallArgs(["list_agents"]), {
19
- tool: "list_agents",
20
- args: {},
21
- usedJson: false,
22
- });
23
- });
24
-
25
- test("--json supplies the whole arguments object", () => {
26
- const { tool, args } = parseCallArgs([
27
- "register_agent",
28
- "--json",
29
- '{"name":"claude-1","type":"claude"}',
30
- ]);
31
- assert.equal(tool, "register_agent");
32
- assert.deepEqual(args, { name: "claude-1", type: "claude" });
33
- });
34
-
35
- test("--json accepts nested structures", () => {
36
- const { args } = parseCallArgs([
37
- "register_agent",
38
- "--json",
39
- '{"resources":[{"resource_type":"mcp_server","subtype":"playwright_lane"}]}',
40
- ]);
41
- assert.deepEqual(args.resources, [
42
- { resource_type: "mcp_server", subtype: "playwright_lane" },
43
- ]);
44
- });
45
-
46
- test("--key value flags build the arguments object", () => {
47
- const { args } = parseCallArgs(["create_task", "--title", "Fix the thing", "--priority", "2"]);
48
- assert.deepEqual(args, { title: "Fix the thing", priority: 2 });
49
- });
50
-
51
- test("numeric and boolean flag values are coerced off the command line", () => {
52
- // A tool whose schema says `number` would reject the string "2".
53
- const { args } = parseCallArgs([
54
- "t",
55
- "--count", "2",
56
- "--ratio", "1.5",
57
- "--negative", "-3",
58
- "--yes", "true",
59
- "--no", "false",
60
- "--nothing", "null",
61
- ]);
62
- assert.deepEqual(args, {
63
- count: 2, ratio: 1.5, negative: -3, yes: true, no: false, nothing: null,
64
- });
65
- });
66
-
67
- test("values that only look numeric stay strings", () => {
68
- const { args } = parseCallArgs([
69
- "t",
70
- "--ticket", "BOT-876",
71
- "--version", "1.2.3",
72
- "--hex", "0x10",
73
- "--padded", "007",
74
- ]);
75
- assert.equal(args.ticket, "BOT-876");
76
- assert.equal(args.version, "1.2.3");
77
- assert.equal(args.hex, "0x10");
78
- // "007" is unambiguously numeric; losing the padding is acceptable, but a
79
- // leading-zero identifier is common enough to pin the behaviour.
80
- assert.equal(args.padded, 7);
81
- });
82
-
83
- test("a valueless flag is a boolean true", () => {
84
- const { args } = parseCallArgs(["t", "--force", "--name", "x"]);
85
- assert.deepEqual(args, { force: true, name: "x" });
86
- });
87
-
88
- test("a trailing valueless flag is a boolean true", () => {
89
- const { args } = parseCallArgs(["t", "--force"]);
90
- assert.deepEqual(args, { force: true });
91
- });
92
-
93
- test("flags after --json override the blob", () => {
94
- const { args } = parseCallArgs(["t", "--json", '{"name":"a"}', "--name", "b"]);
95
- assert.equal(args.name, "b");
96
- });
97
-
98
- test("a missing tool name is a usage error", () => {
99
- assert.throws(() => parseCallArgs([]), CallUsageError);
100
- assert.throws(() => parseCallArgs(["--json", "{}"]), CallUsageError);
101
- });
102
-
103
- test("malformed --json is reported clearly, not swallowed", () => {
104
- assert.throws(() => parseCallArgs(["t", "--json", "{not json}"]), (e) => {
105
- assert.ok(e instanceof CallUsageError);
106
- assert.match(e.message, /not valid JSON/);
107
- return true;
108
- });
109
- });
110
-
111
- test("--json must be an object, not an array or scalar", () => {
112
- for (const bad of ["[1,2]", '"a string"', "42", "null"]) {
113
- assert.throws(() => parseCallArgs(["t", "--json", bad]), CallUsageError, `accepted ${bad}`);
114
- }
115
- });
116
-
117
- test("--json with no value is a usage error", () => {
118
- assert.throws(() => parseCallArgs(["t", "--json"]), CallUsageError);
119
- });
120
-
121
- test("a bare positional argument is rejected rather than silently ignored", () => {
122
- assert.throws(() => parseCallArgs(["t", "oops"]), (e) => {
123
- assert.match(e.message, /--key value or --json/);
124
- return true;
125
- });
126
- });
127
-
128
- // ─── discoveryUrlFor ────────────────────────────────────────────
129
-
130
- test("discovery URL is derived from the server URL", () => {
131
- assert.equal(discoveryUrlFor(SERVER), `${SERVER}/discovery`);
132
- assert.equal(discoveryUrlFor(`${SERVER}/`), `${SERVER}/discovery`);
133
- });
134
-
135
- test("discovery URL encodes a requested tool", () => {
136
- assert.equal(discoveryUrlFor(SERVER, "get_totp_code"), `${SERVER}/discovery?tool=get_totp_code`);
137
- });
138
-
139
- // ─── formatDiscovery ────────────────────────────────────────────
140
-
141
- const DOC = {
142
- tools: [
143
- { name: "register_agent", description: "Register an agent. Extra detail here.", inputSchema: { type: "object" } },
144
- { name: "list_agents", description: "List all agents", inputSchema: { type: "object" } },
145
- ],
146
- };
147
-
148
- test("the index lists every tool, sorted, one line each", () => {
149
- const out = formatDiscovery(DOC);
150
- const lines = out.split("\n").filter((l) => l.startsWith(" "));
151
- assert.equal(lines.length, 2);
152
- assert.match(lines[0], /list_agents/);
153
- assert.match(lines[1], /register_agent/);
154
- assert.match(out, /2 tools available/);
155
- });
156
-
157
- test("the index truncates a description to its first sentence", () => {
158
- const out = formatDiscovery(DOC);
159
- assert.match(out, /Register an agent\./);
160
- assert.doesNotMatch(out, /Extra detail here/);
161
- });
162
-
163
- test("a single tool renders its full schema and an invocation example", () => {
164
- const out = formatDiscovery(DOC, { tool: "register_agent" });
165
- assert.match(out, /^register_agent/);
166
- assert.match(out, /botbuddy call register_agent/);
167
- assert.match(out, /"type": "object"/);
168
- // register_agent carries no `auth` note → no Auth section.
169
- assert.doesNotMatch(out, /Auth:/);
170
- });
171
-
172
- test("a single tool with an agent-identity note renders an Auth section", () => {
173
- // BOT-971: the server stamps `auth` on discovery entries for agent-gated
174
- // tools; `botbuddy help <tool>` must surface it (matching help({ tool })).
175
- const doc = {
176
- tools: [{
177
- name: "acquire_resources",
178
- description: "Batch lock",
179
- inputSchema: { type: "object" },
180
- auth: "Requires an agent identity — call register_agent first.",
181
- }],
182
- };
183
- const out = formatDiscovery(doc, { tool: "acquire_resources" });
184
- assert.match(out, /Auth:/);
185
- assert.match(out, /register_agent/);
186
- });
187
-
188
- test("an unknown tool is reported, not rendered empty", () => {
189
- assert.match(formatDiscovery(DOC, { tool: "nope" }), /Unknown tool: nope/);
190
- });
191
-
192
- test("a malformed discovery document does not throw", () => {
193
- assert.equal(formatDiscovery(null), "No tools found.");
194
- assert.equal(formatDiscovery({}), "No tools found.");
195
- });
@@ -1,60 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
-
4
- import { buildAcquireResourcesPayload } from "./locks.mjs";
5
-
6
- test("locks --mcp emits a typed host-scoped Playwright lane request", () => {
7
- const payload = buildAcquireResourcesPayload(["--mcp", "--ticket", "BOT-660"], {
8
- defaultHost: "macbook-pro",
9
- });
10
-
11
- assert.deepEqual(payload, {
12
- resources: [
13
- {
14
- resource_type: "mcp_server",
15
- subtype: "playwright_lane",
16
- host: "macbook-pro",
17
- },
18
- ],
19
- ticket_id: "BOT-660",
20
- });
21
- });
22
-
23
- test("locks --mcp can pin a lane slot and preserve PR metadata", () => {
24
- const payload = buildAcquireResourcesPayload(
25
- ["--mcp", "--host", "Jonos-MBP.localdomain", "--lane", "4", "--pr", "#197"],
26
- { defaultHost: "ignored-host" },
27
- );
28
-
29
- assert.deepEqual(payload, {
30
- resources: [
31
- {
32
- resource_type: "mcp_server",
33
- subtype: "playwright_lane",
34
- host: "Jonos-MBP.localdomain",
35
- slot: "4",
36
- },
37
- ],
38
- pr_id: "#197",
39
- });
40
- });
41
-
42
- test("locks --port emits typed Vite port metadata with slot and URL", () => {
43
- const payload = buildAcquireResourcesPayload(
44
- ["--port", "frontend", "--vite-port", "4179", "--url", "http://127.0.0.1:4179"],
45
- { defaultHost: "macbook-pro" },
46
- );
47
-
48
- assert.deepEqual(payload, {
49
- resources: [
50
- {
51
- resource_type: "port",
52
- port_type: "frontend",
53
- subtype: "vite_port",
54
- host: "macbook-pro",
55
- slot: "4179",
56
- url: "http://127.0.0.1:4179",
57
- },
58
- ],
59
- });
60
- });