@botbuddy/cli 1.4.2 → 1.5.0

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.
@@ -15,6 +15,8 @@ import {
15
15
  EXIT, STACK_HELP, STACK_SCHEMA_VERSION, DEFAULT_RECEIPT_MAX_BYTES,
16
16
  deriveSlot, parseStackArgs, buildReceipt, truncateReceipt,
17
17
  parseSupabaseStatus, eventStreamBase, resolveLocalSupabaseUrl, resolveStackPath,
18
+ runLocalExecPreflight, dockerEnvForSelector, dockerTargetFromPreflight, connectionWithDockerTarget,
19
+ proveLegacyLocalExecTarget, localTeardown, cmdUp, cmdDone,
18
20
  runStackLifecycle, materializeLeasedTestConfig, waitForLease,
19
21
  } from "./stack.mjs";
20
22
 
@@ -57,7 +59,7 @@ test("deriveSlot: throws when undeterminable", () => {
57
59
  test("parseStackArgs: up options", () => {
58
60
  const { command, opts, errors } = parseStackArgs([
59
61
  "up", "--slot", "56322", "--host", "mac", "--repo", "botbuddy-web",
60
- "--ticket", "BOT-1220", "--stack-path", "infra/local", "--idle-ttl", "600", "--timeout", "120", "--no-wait", "--local-exec",
62
+ "--ticket", "BOT-1220", "--stack-path", "infra/local", "--idle-ttl", "600", "--timeout", "120", "--no-wait", "--local-exec", "--docker-context", "orbstack",
61
63
  ]);
62
64
  assert.equal(errors.length, 0);
63
65
  assert.equal(command, "up");
@@ -68,6 +70,326 @@ test("parseStackArgs: up options", () => {
68
70
  assert.equal(opts.timeout, 120);
69
71
  assert.equal(opts.noWait, true);
70
72
  assert.equal(opts.localExec, true);
73
+ assert.equal(opts.dockerContext, "orbstack");
74
+ });
75
+ test("parseStackArgs: local-exec up requires exactly one explicit Docker selector", () => {
76
+ assert.match(parseStackArgs(["up", "--slot", "x", "--local-exec"]).errors.join(" "), /docker-context.*docker-endpoint/i);
77
+ assert.match(parseStackArgs([
78
+ "up", "--slot", "x", "--local-exec", "--docker-context", "orbstack", "--docker-endpoint", "unix:///tmp/docker.sock",
79
+ ]).errors.join(" "), /exactly one/i);
80
+ assert.deepEqual(parseStackArgs([
81
+ "up", "--slot", "x", "--local-exec", "--docker-endpoint", "unix:///Users/test/.orbstack/run/docker.sock",
82
+ ]).errors, []);
83
+ });
84
+
85
+ test("parseStackArgs: local-exec done requires exactly one explicit Docker selector", () => {
86
+ assert.match(parseStackArgs(["done", "lease-1", "--local-exec"]).errors.join(" "), /docker-context.*docker-endpoint/i);
87
+ assert.equal(parseStackArgs([
88
+ "done", "lease-1", "--local-exec", "--docker-context", "orbstack",
89
+ ]).errors.length, 0);
90
+ assert.ok(parseStackArgs([
91
+ "done", "lease-1", "--local-exec", "--docker-context", "orbstack", "--docker-endpoint", "unix:///tmp/docker.sock",
92
+ ]).errors.length > 0);
93
+ });
94
+ test("parseStackArgs: a missing Docker selector value preserves the following flag", () => {
95
+ const parsed = parseStackArgs(["up", "--slot", "x", "--local-exec", "--docker-context", "--json"]);
96
+ assert.equal(parsed.opts.json, true);
97
+ assert.match(parsed.errors.join(" "), /--docker-context needs a value/i);
98
+ });
99
+
100
+ test("runLocalExecPreflight delegates the explicit selector and preserves refusal", () => {
101
+ const calls = [];
102
+ const expected = { exitCode: 2, receipt: { outcome: "refused", pressure: { status: "fail" } }, json: true };
103
+ const actual = runLocalExecPreflight({ dockerContext: "orbstack", dockerEndpoint: null }, (args) => {
104
+ calls.push(args);
105
+ return expected;
106
+ });
107
+ assert.deepEqual(calls, [["preflight", "--context", "orbstack", "--json"]]);
108
+ assert.equal(actual, expected);
109
+ });
110
+
111
+ test("dockerEnvForSelector replaces ambient Docker routing with the preflight target", () => {
112
+ assert.deepEqual(dockerEnvForSelector(
113
+ { dockerContext: "orbstack", dockerEndpoint: null },
114
+ { PATH: "/bin", DOCKER_CONTEXT: "desktop-linux", DOCKER_HOST: "tcp://wrong" },
115
+ ), { PATH: "/bin", DOCKER_CONTEXT: "orbstack" });
116
+ assert.deepEqual(dockerEnvForSelector(
117
+ { dockerContext: null, dockerEndpoint: "unix:///Users/test/.orbstack/run/docker.sock" },
118
+ { PATH: "/bin", DOCKER_CONTEXT: "desktop-linux" },
119
+ ), { PATH: "/bin", DOCKER_HOST: "unix:///Users/test/.orbstack/run/docker.sock" });
120
+ });
121
+
122
+ test("localTeardown passes the explicit Docker selector to supabase stop", () => {
123
+ let invocation;
124
+ const ok = localTeardown(
125
+ { dockerContext: "orbstack", dockerEndpoint: null },
126
+ { validation: "orbstack", resolved_endpoint: "unix:///Users/test/.orbstack/run/docker.sock", server_id: "daemon-1" },
127
+ (command, args, options) => {
128
+ invocation = { command, args, env: options.env };
129
+ return { status: 0, stdout: "", stderr: "" };
130
+ },
131
+ { PATH: "/bin", DOCKER_CONTEXT: "desktop-linux", DOCKER_HOST: "tcp://wrong" },
132
+ );
133
+ assert.equal(ok, true);
134
+ assert.deepEqual(invocation, {
135
+ command: "supabase",
136
+ args: ["stop", "--workdir", process.cwd()],
137
+ env: { PATH: "/bin", DOCKER_HOST: "unix:///Users/test/.orbstack/run/docker.sock" },
138
+ });
139
+ });
140
+
141
+ const targetReceipt = (serverId, endpoint = "unix:///Users/test/.orbstack/run/docker.sock") => ({
142
+ context: {
143
+ validation: "orbstack",
144
+ resolved_endpoint: endpoint,
145
+ server: { id: serverId, name: "orbstack", operating_system: "OrbStack" },
146
+ },
147
+ pressure: { status: "ok" },
148
+ });
149
+
150
+ test("dockerTargetFromPreflight requires a resolved OrbStack server identity", () => {
151
+ assert.deepEqual(dockerTargetFromPreflight(targetReceipt("daemon-1")), {
152
+ validation: "orbstack",
153
+ resolved_endpoint: "unix:///Users/test/.orbstack/run/docker.sock",
154
+ server_id: "daemon-1",
155
+ });
156
+ assert.equal(dockerTargetFromPreflight(targetReceipt(null)), null);
157
+ });
158
+
159
+ test("connectionWithDockerTarget persists the validated target in the lease connection", () => {
160
+ const target = dockerTargetFromPreflight(targetReceipt("daemon-1"));
161
+ assert.deepEqual(connectionWithDockerTarget({ api_url: "http://127.0.0.1:56321" }, target), {
162
+ api_url: "http://127.0.0.1:56321",
163
+ botbuddy_docker_target: target,
164
+ });
165
+ });
166
+
167
+ test("cmdDone refuses before release when teardown targets a different Docker daemon", async () => {
168
+ const tools = [];
169
+ let emitted;
170
+ const code = await cmdDone("lease-1", {
171
+ localExec: true, dockerContext: "orbstack", dockerEndpoint: null,
172
+ disposition: "destroy", receiptMaxBytes: DEFAULT_RECEIPT_MAX_BYTES,
173
+ }, {
174
+ callTool: async (name) => {
175
+ tools.push(name);
176
+ return { ok: true, data: { success: true, state: "active", connection: {
177
+ botbuddy_docker_target: dockerTargetFromPreflight(targetReceipt("daemon-1")),
178
+ } } };
179
+ },
180
+ runPreflight: () => ({ exitCode: 0, receipt: targetReceipt("daemon-2") }),
181
+ localTeardownFn: () => { throw new Error("must not stop"); },
182
+ emitResult: (receipt, _opts, exitCode) => { emitted = receipt; return exitCode; },
183
+ });
184
+ assert.equal(code, EXIT.LEASE_FAILED);
185
+ assert.deepEqual(tools, ["get_stack_lease"]);
186
+ assert.equal(emitted.outcome, "refused");
187
+ });
188
+
189
+ test("proveLegacyLocalExecTarget accepts only an exact owning-worktree and live connection match", () => {
190
+ const target = dockerTargetFromPreflight(targetReceipt("daemon-1"));
191
+ const worktreeRoot = realpathSync(process.cwd());
192
+ const lease = {
193
+ worktree_root: worktreeRoot,
194
+ stack_path: ".",
195
+ connection: {
196
+ api_url: "http://127.0.0.1:56321",
197
+ db_url: "postgresql://postgres:postgres@127.0.0.1:56322/postgres",
198
+ },
199
+ };
200
+ const spawned = [];
201
+ const proof = proveLegacyLocalExecTarget(lease, target, { stackPath: "." }, (command, args, options) => {
202
+ spawned.push({ command, args, env: options.env });
203
+ return { status: 0, stdout: JSON.stringify({
204
+ API_URL: lease.connection.api_url,
205
+ DB_URL: lease.connection.db_url,
206
+ }), stderr: "" };
207
+ });
208
+
209
+ assert.equal(proof.ok, true);
210
+ assert.equal(proof.evidence.method, "legacy_worktree_connection_match");
211
+ assert.equal(spawned[0].env.DOCKER_HOST, target.resolved_endpoint);
212
+ assert.deepEqual(spawned[0].args, ["status", "-o", "json", "--workdir", worktreeRoot]);
213
+
214
+ const mismatch = proveLegacyLocalExecTarget(lease, target, { stackPath: "." }, () => ({
215
+ status: 0,
216
+ stdout: JSON.stringify({ API_URL: "http://127.0.0.1:64321", DB_URL: lease.connection.db_url }),
217
+ stderr: "",
218
+ }));
219
+ assert.equal(mismatch.ok, false);
220
+ assert.match(mismatch.error, /connection.*does not match/i);
221
+ });
222
+
223
+ test("cmdDone safely tears down a pre-1.5.0 lease after the legacy compatibility proof", async () => {
224
+ const tools = [];
225
+ let emitted;
226
+ const target = dockerTargetFromPreflight(targetReceipt("daemon-1"));
227
+ const code = await cmdDone("lease-legacy", {
228
+ localExec: true, dockerContext: "orbstack", dockerEndpoint: null, stackPath: ".",
229
+ disposition: "destroy", receiptMaxBytes: DEFAULT_RECEIPT_MAX_BYTES,
230
+ }, {
231
+ callTool: async (name) => {
232
+ tools.push(name);
233
+ if (name === "get_stack_lease") return { ok: true, data: {
234
+ success: true, state: "active", worktree_root: process.cwd(), stack_path: ".",
235
+ connection: { api_url: "http://127.0.0.1:56321", db_url: "postgresql://127.0.0.1:56322/postgres" },
236
+ } };
237
+ if (name === "release_stack_lease") return { ok: true, data: { success: true, state: "reaping" } };
238
+ if (name === "finalize_stack_lease") return { ok: true, data: { success: true, state: "reaped" } };
239
+ throw new Error(`unexpected tool ${name}`);
240
+ },
241
+ runPreflight: () => ({ exitCode: 0, receipt: targetReceipt("daemon-1") }),
242
+ proveLegacyTarget: () => ({ ok: true, evidence: { method: "legacy_worktree_connection_match" } }),
243
+ localTeardownFn: () => true,
244
+ emitResult: (receipt, _opts, exitCode) => { emitted = receipt; return exitCode; },
245
+ });
246
+
247
+ assert.equal(code, EXIT.OK);
248
+ assert.deepEqual(tools, ["get_stack_lease", "release_stack_lease", "finalize_stack_lease"]);
249
+ assert.deepEqual(emitted.legacy_target_proof, { method: "legacy_worktree_connection_match" });
250
+ });
251
+
252
+ test("cmdDone refuses a legacy lease before release when compatibility proof is absent", async () => {
253
+ const tools = [];
254
+ let emitted;
255
+ const code = await cmdDone("lease-legacy", {
256
+ localExec: true, dockerContext: "orbstack", dockerEndpoint: null, stackPath: ".",
257
+ disposition: "destroy", receiptMaxBytes: DEFAULT_RECEIPT_MAX_BYTES,
258
+ }, {
259
+ callTool: async (name) => {
260
+ tools.push(name);
261
+ return { ok: true, data: { success: true, state: "active", connection: { api_url: "http://127.0.0.1:56321" } } };
262
+ },
263
+ runPreflight: () => ({ exitCode: 0, receipt: targetReceipt("daemon-1") }),
264
+ proveLegacyTarget: () => ({ ok: false, error: "legacy worktree metadata is missing" }),
265
+ localTeardownFn: () => { throw new Error("must not stop"); },
266
+ emitResult: (receipt, _opts, exitCode) => { emitted = receipt; return exitCode; },
267
+ });
268
+
269
+ assert.equal(code, EXIT.LEASE_FAILED);
270
+ assert.deepEqual(tools, ["get_stack_lease"]);
271
+ assert.match(emitted.error, /legacy worktree metadata is missing/i);
272
+ });
273
+
274
+ test("cmdDone stops and finalizes only after the fresh Docker identity matches the lease", async () => {
275
+ const tools = [];
276
+ let teardownTarget;
277
+ const target = dockerTargetFromPreflight(targetReceipt("daemon-1"));
278
+ const code = await cmdDone("lease-1", {
279
+ localExec: true, dockerContext: "orbstack", dockerEndpoint: null,
280
+ disposition: "destroy", receiptMaxBytes: DEFAULT_RECEIPT_MAX_BYTES,
281
+ }, {
282
+ callTool: async (name) => {
283
+ tools.push(name);
284
+ if (name === "get_stack_lease") return { ok: true, data: { success: true, state: "active", connection: { botbuddy_docker_target: target } } };
285
+ if (name === "release_stack_lease") return { ok: true, data: { success: true, state: "reaping" } };
286
+ if (name === "finalize_stack_lease") return { ok: true, data: { success: true, state: "reaped" } };
287
+ throw new Error(`unexpected tool ${name}`);
288
+ },
289
+ runPreflight: () => ({ exitCode: 0, receipt: targetReceipt("daemon-1") }),
290
+ localTeardownFn: (_opts, actual) => { teardownTarget = actual; return true; },
291
+ emitResult: (_receipt, _opts, exitCode) => exitCode,
292
+ });
293
+ assert.equal(code, EXIT.OK);
294
+ assert.deepEqual(tools, ["get_stack_lease", "release_stack_lease", "finalize_stack_lease"]);
295
+ assert.deepEqual(teardownTarget, target);
296
+ });
297
+
298
+ test("cmdUp refuses pressure before lease request or local provisioning", async () => {
299
+ const events = [];
300
+ let emitted;
301
+ const code = await cmdUp({
302
+ slot: "botbuddy-web-bot-1405", repo: "botbuddy-web", ticket: "BOT-1405",
303
+ stackPath: ".", localExec: true, dockerContext: "orbstack", dockerEndpoint: null,
304
+ }, {
305
+ runPreflight: () => ({
306
+ exitCode: 2,
307
+ receipt: {
308
+ outcome: "refused", context: { selector: "orbstack" },
309
+ pressure: { status: "fail", projected_pressure_units: 224 },
310
+ warnings: [], errors: ["interface pressure refusal"], recommendation: "run hygiene",
311
+ },
312
+ json: true,
313
+ }),
314
+ callTool: async (...args) => { events.push(["tool", ...args]); throw new Error("must not request a lease"); },
315
+ localProvisionFn: () => { events.push(["provision"]); throw new Error("must not start Supabase"); },
316
+ emitResult: (receipt, _opts, exitCode) => { emitted = receipt; return exitCode; },
317
+ });
318
+
319
+ assert.equal(code, EXIT.LEASE_FAILED);
320
+ assert.equal(emitted.outcome, "refused");
321
+ assert.equal(emitted.preflight.pressure.status, "fail");
322
+ assert.deepEqual(events, []);
323
+ });
324
+
325
+ test("cmdUp atomically cancels an unclaimed lease when the just-in-time preflight refuses", async () => {
326
+ const tools = [];
327
+ let preflights = 0;
328
+ let emitted;
329
+ const okReceipt = {
330
+ ...targetReceipt("daemon-1"), outcome: "ok", pressure: { status: "ok" },
331
+ warnings: [], errors: [], recommendation: "none",
332
+ };
333
+ const refusedReceipt = {
334
+ ...targetReceipt("daemon-1"), outcome: "refused", pressure: { status: "fail" },
335
+ warnings: [], errors: ["interface pressure refusal"], recommendation: "run hygiene",
336
+ };
337
+ const code = await cmdUp({
338
+ slot: "botbuddy-web-bot-1405", host: "test-mac", repo: "botbuddy-web", ticket: "BOT-1405",
339
+ stackPath: ".", localExec: true, dockerContext: "orbstack", dockerEndpoint: null,
340
+ timeout: 60, noWait: false, json: false,
341
+ }, {
342
+ authProvider: () => ({ Authorization: "test" }),
343
+ runPreflight: () => ({
344
+ exitCode: preflights++ === 0 ? 0 : 2,
345
+ receipt: preflights === 1 ? okReceipt : refusedReceipt,
346
+ json: true,
347
+ }),
348
+ callTool: async (tool, args) => {
349
+ tools.push([tool, args]);
350
+ if (tool === "request_stack_lease") return { ok: true, data: { success: true, lease_id: "lease-1", state: "provisioning" } };
351
+ if (tool === "cancel_unclaimed_stack_lease") return { ok: true, data: { success: true, state: "reaped", provision_job_cancelled: true } };
352
+ throw new Error(`unexpected tool ${tool}`);
353
+ },
354
+ localProvisionFn: () => { throw new Error("must not start Supabase"); },
355
+ emitResult: (receipt, _opts, exitCode) => { emitted = receipt; return exitCode; },
356
+ });
357
+
358
+ assert.equal(code, EXIT.LEASE_FAILED);
359
+ assert.deepEqual(tools.map(([tool]) => tool), ["request_stack_lease", "cancel_unclaimed_stack_lease"]);
360
+ assert.equal(emitted.outcome, "refused");
361
+ assert.deepEqual(emitted.lease_cancellation, { success: true, state: "reaped", provision_job_cancelled: true });
362
+ });
363
+
364
+ test("cmdUp persists the validated Docker identity used by local provisioning", async () => {
365
+ const target = dockerTargetFromPreflight(targetReceipt("daemon-1"));
366
+ let provisionTarget;
367
+ let activatedConnection;
368
+ const code = await cmdUp({
369
+ slot: "botbuddy-web-bot-1405", host: "test-mac", repo: "botbuddy-web", ticket: "BOT-1405",
370
+ stackPath: ".", localExec: true, dockerContext: "orbstack", dockerEndpoint: null,
371
+ timeout: 60, noWait: false, json: false,
372
+ }, {
373
+ authProvider: () => ({ Authorization: "test" }),
374
+ runPreflight: () => ({ exitCode: 0, receipt: { ...targetReceipt("daemon-1"), outcome: "ok", warnings: [], errors: [] } }),
375
+ localProvisionFn: (_opts, actualTarget) => {
376
+ provisionTarget = actualTarget;
377
+ return { api_url: "http://127.0.0.1:56321" };
378
+ },
379
+ callTool: async (name, args) => {
380
+ if (name === "request_stack_lease") return { ok: true, data: { success: true, lease_id: "lease-1", state: "provisioning" } };
381
+ if (name === "activate_stack_lease") {
382
+ activatedConnection = args.connection;
383
+ return { ok: true, data: { success: true, state: "active" } };
384
+ }
385
+ if (name === "get_stack_lease") return { ok: true, data: { success: true, state: "active", slot: "botbuddy-web-bot-1405", connection: activatedConnection } };
386
+ throw new Error(`unexpected tool ${name}`);
387
+ },
388
+ emitResult: (_receipt, _opts, exitCode) => exitCode,
389
+ });
390
+ assert.equal(code, EXIT.OK);
391
+ assert.deepEqual(provisionTarget, target);
392
+ assert.deepEqual(activatedConnection.botbuddy_docker_target, target);
71
393
  });
72
394
  test("parseStackArgs: rejects absolute and escaping stack paths", () => {
73
395
  assert.ok(parseStackArgs(["up", "--stack-path", "/tmp/stack"]).errors.length > 0);
@@ -393,7 +715,7 @@ test("resolveLocalSupabaseUrl: ignores a NON-local exported origin (refuses →
393
715
 
394
716
  // ── --help snapshot (AC-4) ───────────────────────────────────────────────────
395
717
  test("STACK_HELP documents the four subcommands and every exit code", () => {
396
- for (const s of ["stack up", "stack status", "stack touch", "stack done", "--local-exec", "--no-wait"]) {
718
+ for (const s of ["stack up", "stack status", "stack touch", "stack done", "--local-exec", "--docker-context", "--no-wait"]) {
397
719
  assert.ok(STACK_HELP.includes(s), `help missing ${s}`);
398
720
  }
399
721
  for (const s of ["0 ok", "2 park timed out", "3 not authenticated", "4 invalid", "5 backend", "6 lease failed", "7 internal"]) {