@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/run.test.mjs
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdtempSync, readFileSync, renameSync, unlinkSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
|
|
7
|
-
import { EventEmitter } from "node:events";
|
|
8
|
-
import { EXIT, parseRunArgs, terminalStatus, commandHash, contextPath, launchRun, retryTerminalUpdate, runWorker, cancelRun } from "./run.mjs";
|
|
9
|
-
|
|
10
|
-
test("run: rejects an unowned workload before it can start", () => {
|
|
11
|
-
const parsed = parseRunArgs(["--environment", "local", "--", "node", "-e", "process.exit(0)"]);
|
|
12
|
-
assert.ok(parsed.errors.some((e) => e.includes("session-id")));
|
|
13
|
-
assert.equal(parsed.command[0], "node");
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test("run: accepts and forwards a structured rerun reason", async () => {
|
|
17
|
-
const parsed = parseRunArgs(["--session-id", "session-1", "--environment", "local", "--rerun-reason", '{"code":"source_changed","detail":"retry after fix"}', "--", "node", "-e", "process.exit(0)"]);
|
|
18
|
-
assert.deepEqual(parsed.opts.rerunReason, { code: "source_changed", detail: "retry after fix" });
|
|
19
|
-
|
|
20
|
-
const calls = [];
|
|
21
|
-
const result = await launchRun(["--session-id", "session-1", "--environment", "local", "--rerun-reason", '{"code":"source_changed","detail":"retry after fix"}', "--", "node", "-e", "process.exit(0)"], {
|
|
22
|
-
call: async (name, args) => {
|
|
23
|
-
calls.push({ name, args });
|
|
24
|
-
return { ok: true, data: { accepted: true, run_credential: "x".repeat(64), receipt_upload: "receipt://tenant/session/run" } };
|
|
25
|
-
},
|
|
26
|
-
spawnImpl: () => Object.assign(new EventEmitter(), { unref() {} }),
|
|
27
|
-
});
|
|
28
|
-
assert.equal(result.exitCode, EXIT.OK);
|
|
29
|
-
assert.deepEqual(calls[0].args.rerun_reason, { code: "source_changed", detail: "retry after fix" });
|
|
30
|
-
unlinkSync(contextPath(result.receipt.run_id));
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test("run: rejects an invalid rerun reason before registering a run", () => {
|
|
34
|
-
const parsed = parseRunArgs(["--session-id", "session-1", "--environment", "local", "--rerun-reason", "not-json", "--", "node", "-e", "process.exit(0)"]);
|
|
35
|
-
assert.ok(parsed.errors.some((error) => error.includes("rerun-reason")));
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("run: derives complete terminal outcomes", () => {
|
|
39
|
-
assert.equal(terminalStatus({ exitCode: 0, signal: null }), "success");
|
|
40
|
-
assert.equal(terminalStatus({ exitCode: 1, signal: null }), "failure");
|
|
41
|
-
assert.equal(terminalStatus({ exitCode: null, signal: "SIGTERM" }), "cancelled");
|
|
42
|
-
assert.equal(terminalStatus({ exitCode: null, signal: "SIGTERM", timedOut: true }), "timed_out");
|
|
43
|
-
assert.equal(terminalStatus({ exitCode: null, signal: null, ownerLost: true }), "owner_lost");
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("run: the detached worker records one receipt after its launcher is gone", async () => {
|
|
47
|
-
const dir = mkdtempSync(join(tmpdir(), "bb-run-"));
|
|
48
|
-
const receiptPath = join(dir, "receipt.json");
|
|
49
|
-
const contextPath = join(dir, "context.json");
|
|
50
|
-
const updates = [];
|
|
51
|
-
const context = {
|
|
52
|
-
context_path: contextPath, run_id: "run-survives-launcher", run_credential: "x".repeat(64), receipt_upload: "receipt://botbuddy/session/run-survives-launcher",
|
|
53
|
-
command: [process.execPath, "-e", "setTimeout(() => process.exit(0), 30)"], cwd: dir, timeout_seconds: 2, receipt_path: receiptPath,
|
|
54
|
-
};
|
|
55
|
-
await import("node:fs/promises").then(({ writeFile }) => writeFile(contextPath, JSON.stringify(context)));
|
|
56
|
-
await runWorker(contextPath, { call: async (name, args) => { updates.push({ name, args }); return { ok: true, data: {} }; } });
|
|
57
|
-
const receipt = JSON.parse(readFileSync(receiptPath, "utf8"));
|
|
58
|
-
assert.equal(receipt.status, "success");
|
|
59
|
-
assert.equal(updates.length, 1);
|
|
60
|
-
assert.equal(updates[0].name, "update_command_run");
|
|
61
|
-
assert.equal(updates[0].args.status, "succeeded");
|
|
62
|
-
assert.equal(updates[0].args.run_id, context.run_id);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("run: terminal callbacks retain and retry their sealed context until accepted", async () => {
|
|
66
|
-
const dir = mkdtempSync(join(tmpdir(), "bb-run-retry-"));
|
|
67
|
-
const context = { context_path: join(dir, "context.json") };
|
|
68
|
-
const update = { run_id: "retry-run", run_credential: "x".repeat(64), status: "succeeded" };
|
|
69
|
-
const attempts = [];
|
|
70
|
-
await retryTerminalUpdate(context, update, async () => {
|
|
71
|
-
attempts.push(true);
|
|
72
|
-
return attempts.length === 1 ? { ok: false, error: "offline" } : { ok: true, data: { accepted: true } };
|
|
73
|
-
}, { sleep: async () => {} });
|
|
74
|
-
assert.equal(attempts.length, 2);
|
|
75
|
-
const retained = JSON.parse(readFileSync(context.context_path, "utf8"));
|
|
76
|
-
assert.equal(retained.terminal_update.attempts, 1);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("run: bounds captured output while preserving its total size", async () => {
|
|
80
|
-
const dir = mkdtempSync(join(tmpdir(), "bb-run-output-"));
|
|
81
|
-
const receiptPath = join(dir, "receipt.json");
|
|
82
|
-
const contextPath = join(dir, "context.json");
|
|
83
|
-
const context = {
|
|
84
|
-
context_path: contextPath, run_id: "bounded-output", run_credential: "x".repeat(64), receipt_upload: "receipt://botbuddy/session/bounded-output",
|
|
85
|
-
command: [process.execPath, "-e", "process.stdout.write('x'.repeat(50000))"], cwd: dir, timeout_seconds: 2, receipt_path: receiptPath,
|
|
86
|
-
};
|
|
87
|
-
await import("node:fs/promises").then(({ writeFile }) => writeFile(contextPath, JSON.stringify(context)));
|
|
88
|
-
await runWorker(contextPath, { call: async () => ({ ok: true, data: { accepted: true } }) });
|
|
89
|
-
const receipt = JSON.parse(readFileSync(receiptPath, "utf8"));
|
|
90
|
-
assert.equal(receipt.output_characters, 50000);
|
|
91
|
-
assert.ok(receipt.bounded_log.length <= 8192);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test("run: escalates a timed-out SIGTERM-resistant process group to SIGKILL", async () => {
|
|
95
|
-
const dir = mkdtempSync(join(tmpdir(), "bb-run-timeout-"));
|
|
96
|
-
const receiptPath = join(dir, "receipt.json");
|
|
97
|
-
const contextPath = join(dir, "context.json");
|
|
98
|
-
const context = {
|
|
99
|
-
context_path: contextPath, run_id: "timeout-escalation", run_credential: "x".repeat(64), receipt_upload: "receipt://botbuddy/session/timeout-escalation",
|
|
100
|
-
command: [process.execPath, "-e", "process.on('SIGTERM', () => {}); setInterval(() => {}, 1_000)"], cwd: dir, timeout_seconds: 0.03, timeout_grace_ms: 20, receipt_path: receiptPath,
|
|
101
|
-
};
|
|
102
|
-
await import("node:fs/promises").then(({ writeFile }) => writeFile(contextPath, JSON.stringify(context)));
|
|
103
|
-
await runWorker(contextPath, { call: async () => ({ ok: true, data: { accepted: true } }) });
|
|
104
|
-
const receipt = JSON.parse(readFileSync(receiptPath, "utf8"));
|
|
105
|
-
assert.equal(receipt.status, "timed_out");
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("run: asynchronous workload spawn errors become terminal owner-loss receipts", async () => {
|
|
109
|
-
const dir = mkdtempSync(join(tmpdir(), "bb-run-spawn-error-"));
|
|
110
|
-
const receiptPath = join(dir, "receipt.json");
|
|
111
|
-
const contextPath = join(dir, "context.json");
|
|
112
|
-
const context = {
|
|
113
|
-
context_path: contextPath, run_id: "async-spawn-error", run_credential: "x".repeat(64), receipt_upload: "receipt://botbuddy/session/async-spawn-error",
|
|
114
|
-
command: ["missing"], cwd: dir, timeout_seconds: 2, receipt_path: receiptPath,
|
|
115
|
-
};
|
|
116
|
-
await import("node:fs/promises").then(({ writeFile }) => writeFile(contextPath, JSON.stringify(context)));
|
|
117
|
-
const child = Object.assign(new EventEmitter(), { pid: 123, stdout: null, stderr: null });
|
|
118
|
-
// Emit the spawn error only AFTER runWorker has synchronously attached its
|
|
119
|
-
// 'error' listener (waitForChild runs before the first await). A fixed timer
|
|
120
|
-
// could otherwise fire while runWorker is still in its initial `await
|
|
121
|
-
// readFile`, making 'error' an unhandled event that throws under load.
|
|
122
|
-
await runWorker(contextPath, {
|
|
123
|
-
spawnImpl: () => { queueMicrotask(() => child.emit("error", new Error("ENOENT"))); return child; },
|
|
124
|
-
call: async () => ({ ok: true, data: { accepted: true } }),
|
|
125
|
-
});
|
|
126
|
-
const receipt = JSON.parse(readFileSync(receiptPath, "utf8"));
|
|
127
|
-
assert.equal(receipt.status, "owner_lost");
|
|
128
|
-
assert.match(receipt.reason, /ENOENT/);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
test("run: command hashes are stable and command-sensitive", () => {
|
|
132
|
-
assert.equal(commandHash(["pnpm", "test"]), commandHash(["pnpm", "test"]));
|
|
133
|
-
assert.notEqual(commandHash(["pnpm", "test"]), commandHash(["pnpm", "test:e2e"]));
|
|
134
|
-
assert.equal(EXIT.BACKEND, 5);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test("run: cancellation terminates the detached workload process group and records one cancelled receipt", async () => {
|
|
138
|
-
const dir = mkdtempSync(join(tmpdir(), "bb-run-cancel-"));
|
|
139
|
-
const runId = `cancel-${Date.now()}`;
|
|
140
|
-
const receiptPath = join(dir, "receipt.json");
|
|
141
|
-
const sourceContext = join(dir, "context.json");
|
|
142
|
-
const updates = [];
|
|
143
|
-
const canonical = join(process.env.HOME ?? "", ".botbuddy", "command-runs", `${runId}.context.json`);
|
|
144
|
-
const context = {
|
|
145
|
-
context_path: canonical, run_id: runId, run_credential: "x".repeat(64), receipt_upload: `receipt://botbuddy/session/${runId}`,
|
|
146
|
-
command: [process.execPath, "-e", "setTimeout(() => process.exit(0), 10_000)"], cwd: dir, timeout_seconds: 20, receipt_path: receiptPath,
|
|
147
|
-
};
|
|
148
|
-
await import("node:fs/promises").then(({ writeFile }) => writeFile(sourceContext, JSON.stringify(context)));
|
|
149
|
-
// cancelRun intentionally uses the canonical per-user runner directory; link
|
|
150
|
-
// this deterministic fixture into it without giving the test a live server.
|
|
151
|
-
await import("node:fs/promises").then(({ mkdir }) => mkdir(join(process.env.HOME ?? "", ".botbuddy", "command-runs"), { recursive: true }));
|
|
152
|
-
renameSync(sourceContext, canonical);
|
|
153
|
-
const worker = runWorker(canonical, { call: async (name, args) => { updates.push({ name, args }); return { ok: true, data: {} }; } });
|
|
154
|
-
for (let attempt = 0; attempt < 20; attempt++) {
|
|
155
|
-
// The worker rewrites this file to stamp workload_pid; tolerate a transient
|
|
156
|
-
// read (an unfinished write, though writeJson is atomic) by retrying.
|
|
157
|
-
let current;
|
|
158
|
-
try {
|
|
159
|
-
current = JSON.parse(readFileSync(canonical, "utf8"));
|
|
160
|
-
} catch {
|
|
161
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
if (Number.isInteger(current.workload_pid)) break;
|
|
165
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
166
|
-
}
|
|
167
|
-
const cancelled = await cancelRun(runId, { call: async () => ({ ok: true, data: {} }) });
|
|
168
|
-
assert.equal(cancelled.receipt.outcome, "cancellation_requested", JSON.stringify(cancelled.receipt));
|
|
169
|
-
await worker;
|
|
170
|
-
const receipt = JSON.parse(readFileSync(receiptPath, "utf8"));
|
|
171
|
-
assert.equal(receipt.status, "cancelled");
|
|
172
|
-
assert.equal(updates.filter((u) => u.name === "update_command_run").length, 1);
|
|
173
|
-
});
|
package/src/stack.test.mjs
DELETED
|
@@ -1,434 +0,0 @@
|
|
|
1
|
-
// BOT-1220 — `botbuddy stack` unit + spawn tests (bb-wait pattern).
|
|
2
|
-
// * pure helpers: slot derivation, arg parsing, receipt build/truncate, status parse
|
|
3
|
-
// * --help snapshot (AC-4)
|
|
4
|
-
// * spawn tests: documented exit codes + exactly ONE JSON-line receipt to stdout (AC-4)
|
|
5
|
-
import test from "node:test";
|
|
6
|
-
import assert from "node:assert/strict";
|
|
7
|
-
import { spawnSync } from "node:child_process";
|
|
8
|
-
import { EventEmitter } from "node:events";
|
|
9
|
-
import { fileURLToPath } from "node:url";
|
|
10
|
-
import { dirname, join } from "node:path";
|
|
11
|
-
import { mkdtempSync, writeFileSync, mkdirSync, realpathSync, readFileSync } from "node:fs";
|
|
12
|
-
import { tmpdir } from "node:os";
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
EXIT, STACK_HELP, STACK_SCHEMA_VERSION, DEFAULT_RECEIPT_MAX_BYTES,
|
|
16
|
-
deriveSlot, parseStackArgs, buildReceipt, truncateReceipt,
|
|
17
|
-
parseSupabaseStatus, eventStreamBase, resolveLocalSupabaseUrl, resolveStackPath,
|
|
18
|
-
runStackLifecycle, materializeLeasedTestConfig, waitForLease,
|
|
19
|
-
} from "./stack.mjs";
|
|
20
|
-
|
|
21
|
-
const BIN = join(dirname(fileURLToPath(import.meta.url)), "..", "bin", "botbuddy.mjs");
|
|
22
|
-
|
|
23
|
-
// Run the real CLI with a throwaway HOME so no ambient ~/.botbuddy config leaks in.
|
|
24
|
-
function runStack(args, extraEnv = {}) {
|
|
25
|
-
const home = mkdtempSync(join(tmpdir(), "bb-stack-home-"));
|
|
26
|
-
return spawnSync("node", [BIN, "stack", ...args], {
|
|
27
|
-
encoding: "utf8",
|
|
28
|
-
env: { ...process.env, HOME: home, USERPROFILE: home, ...extraEnv },
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// The single JSON-line receipt discipline: stdout is exactly one line and it parses.
|
|
33
|
-
function soleReceipt(res) {
|
|
34
|
-
const lines = res.stdout.split("\n").filter((l) => l.trim() !== "");
|
|
35
|
-
assert.equal(lines.length, 1, `expected exactly one stdout line, got ${lines.length}:\n${res.stdout}`);
|
|
36
|
-
return JSON.parse(lines[0]);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// ── deriveSlot ───────────────────────────────────────────────────────────────
|
|
40
|
-
test("deriveSlot: explicit --slot wins", () => {
|
|
41
|
-
assert.equal(deriveSlot({ slot: "56322" }, {}), "56322");
|
|
42
|
-
});
|
|
43
|
-
test("deriveSlot: derives <repo>-<ticket> when both given", () => {
|
|
44
|
-
assert.equal(deriveSlot({ repo: "botbuddy-web", ticket: "BOT-1220" }, {}), "botbuddy-web-bot-1220");
|
|
45
|
-
});
|
|
46
|
-
test("deriveSlot: BB_STACK_SLOT env fallback", () => {
|
|
47
|
-
assert.equal(deriveSlot({}, { BB_STACK_SLOT: "56999" }), "56999");
|
|
48
|
-
});
|
|
49
|
-
test('deriveSlot: rejects "default"', () => {
|
|
50
|
-
assert.throws(() => deriveSlot({ slot: "default" }, {}), /default.*rejected/i);
|
|
51
|
-
});
|
|
52
|
-
test("deriveSlot: throws when undeterminable", () => {
|
|
53
|
-
assert.throws(() => deriveSlot({}, {}), /cannot determine a stack slot/);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// ── parseStackArgs ───────────────────────────────────────────────────────────
|
|
57
|
-
test("parseStackArgs: up options", () => {
|
|
58
|
-
const { command, opts, errors } = parseStackArgs([
|
|
59
|
-
"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",
|
|
61
|
-
]);
|
|
62
|
-
assert.equal(errors.length, 0);
|
|
63
|
-
assert.equal(command, "up");
|
|
64
|
-
assert.equal(opts.slot, "56322");
|
|
65
|
-
assert.equal(opts.host, "mac");
|
|
66
|
-
assert.equal(opts.stackPath, "infra/local");
|
|
67
|
-
assert.equal(opts.idleTtl, 600);
|
|
68
|
-
assert.equal(opts.timeout, 120);
|
|
69
|
-
assert.equal(opts.noWait, true);
|
|
70
|
-
assert.equal(opts.localExec, true);
|
|
71
|
-
});
|
|
72
|
-
test("parseStackArgs: rejects absolute and escaping stack paths", () => {
|
|
73
|
-
assert.ok(parseStackArgs(["up", "--stack-path", "/tmp/stack"]).errors.length > 0);
|
|
74
|
-
assert.ok(parseStackArgs(["up", "--stack-path", "../stack"]).errors.length > 0);
|
|
75
|
-
assert.ok(parseStackArgs(["up", "--stack-path", "sub/../../stack"]).errors.length > 0);
|
|
76
|
-
});
|
|
77
|
-
test("resolveStackPath: canonicalizes an in-worktree directory", () => {
|
|
78
|
-
const root = mkdtempSync(join(tmpdir(), "bb-stack-root-"));
|
|
79
|
-
mkdirSync(join(root, "stack"));
|
|
80
|
-
assert.deepEqual(resolveStackPath(root, "stack"), { worktreeRoot: realpathSync(root), stackPath: "stack" });
|
|
81
|
-
});
|
|
82
|
-
test("parseStackArgs: status/touch/done require a lease_id", () => {
|
|
83
|
-
for (const c of ["status", "touch", "done"]) {
|
|
84
|
-
const { errors } = parseStackArgs([c]);
|
|
85
|
-
assert.ok(errors.some((e) => e.includes("lease_id")), `${c} should require lease_id`);
|
|
86
|
-
}
|
|
87
|
-
const { leaseId, errors } = parseStackArgs(["status", "abc-123"]);
|
|
88
|
-
assert.equal(errors.length, 0);
|
|
89
|
-
assert.equal(leaseId, "abc-123");
|
|
90
|
-
});
|
|
91
|
-
test("parseStackArgs: unknown option is an error", () => {
|
|
92
|
-
const { errors } = parseStackArgs(["up", "--bogus"]);
|
|
93
|
-
assert.ok(errors.some((e) => e.includes("--bogus")));
|
|
94
|
-
});
|
|
95
|
-
test("parseStackArgs: --idle-ttl / --timeout must be positive integers", () => {
|
|
96
|
-
assert.ok(parseStackArgs(["up", "--idle-ttl", "0"]).errors.length > 0);
|
|
97
|
-
assert.ok(parseStackArgs(["up", "--timeout", "-5"]).errors.length > 0);
|
|
98
|
-
assert.ok(parseStackArgs(["up", "--idle-ttl", "x"]).errors.length > 0);
|
|
99
|
-
});
|
|
100
|
-
test("parseStackArgs: --stop sets disposition stop (default destroy)", () => {
|
|
101
|
-
assert.equal(parseStackArgs(["done", "id"]).opts.disposition, "destroy");
|
|
102
|
-
assert.equal(parseStackArgs(["done", "id", "--stop"]).opts.disposition, "stop");
|
|
103
|
-
});
|
|
104
|
-
test("BOT-1346: run requires an argv boundary and rejects direct Docker fallback", () => {
|
|
105
|
-
assert.ok(parseStackArgs(["run", "--repo", "botbuddy-web", "--ticket", "BOT-1346"]).errors.some((e) => e.includes("executable")));
|
|
106
|
-
assert.ok(parseStackArgs(["run", "--repo", "botbuddy-web", "--ticket", "BOT-1346", "--local-exec", "--", "echo", "ok"]).errors.some((e) => e.includes("forbidden")));
|
|
107
|
-
const parsed = parseStackArgs(["run", "--repo", "botbuddy-web", "--ticket", "BOT-1346", "--", "node", "-e", "process.exit(0)"]);
|
|
108
|
-
assert.equal(parsed.errors.length, 0);
|
|
109
|
-
assert.deepEqual(parsed.childArgv, ["node", "-e", "process.exit(0)"]);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test("BOT-1346: a clean lease-stream EOF honours the original deadline", async () => {
|
|
113
|
-
const requests = [];
|
|
114
|
-
const expired = await waitForLease(
|
|
115
|
-
"lease-1346", () => false, () => false,
|
|
116
|
-
{ timeoutSec: 5, deadlineMs: Date.now() - 1, auth: { Authorization: "Bearer test" } },
|
|
117
|
-
async (_url, options = {}) => {
|
|
118
|
-
requests.push(options.method ?? "GET");
|
|
119
|
-
if (options.method === "POST") return new Response(JSON.stringify({ cursor_start: null }), { status: 200 });
|
|
120
|
-
return new Response(new ReadableStream({ start(controller) { controller.close(); } }), { status: 200 });
|
|
121
|
-
},
|
|
122
|
-
);
|
|
123
|
-
assert.deepEqual(expired, { timeout: true });
|
|
124
|
-
assert.deepEqual(requests, ["POST", "GET"]);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test("BOT-1346: a stalled lease-wait registration is aborted at its deadline", async () => {
|
|
128
|
-
let registrationSignal = null;
|
|
129
|
-
const result = await waitForLease(
|
|
130
|
-
"lease-1346", () => false, () => false,
|
|
131
|
-
{ timeoutSec: 0.02, auth: { Authorization: "Bearer test" } },
|
|
132
|
-
async (_url, options = {}) => {
|
|
133
|
-
registrationSignal = options.signal;
|
|
134
|
-
return await new Promise((_resolve, reject) => options.signal.addEventListener("abort", () => reject(new Error("aborted")), { once: true }));
|
|
135
|
-
},
|
|
136
|
-
);
|
|
137
|
-
assert.equal(registrationSignal.aborted, true);
|
|
138
|
-
assert.deepEqual(result, { timeout: true });
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test("BOT-1346: stack run owns active → argv child → signed reap and never exposes connection", async () => {
|
|
142
|
-
const calls = [];
|
|
143
|
-
const signals = new EventEmitter();
|
|
144
|
-
const fakeChild = new EventEmitter(); fakeChild.pid = 4242;
|
|
145
|
-
let written = null; const removed = []; let childCall = null;
|
|
146
|
-
const result = await runStackLifecycle(
|
|
147
|
-
{ repo: "botbuddy-web", ticket: "BOT-1346", stackPath: ".", timeout: 5, reapTimeout: 5, connectionFile: "/tmp/bot-1346-connection.json" },
|
|
148
|
-
["pnpm", "test:all"],
|
|
149
|
-
{
|
|
150
|
-
auth: { Authorization: "Bearer test" }, signals,
|
|
151
|
-
api: {
|
|
152
|
-
request: async () => ({ ok: true, data: { success: true, lease_id: "lease-1346", state: "active" } }),
|
|
153
|
-
get: async () => ({ ok: true, data: { success: true, state: "active", idle_ttl_secs: 30, connection: { db_url: "postgres://secret" } } }),
|
|
154
|
-
touch: async () => ({ ok: true, data: { success: true } }),
|
|
155
|
-
release: async (id) => { calls.push(["release", id]); return { ok: true, data: { success: true, state: "reaping" } }; },
|
|
156
|
-
},
|
|
157
|
-
wait: async (_id, done) => done("reaped") ? { woke: true, state: "reaped" } : { woke: true, state: "active" },
|
|
158
|
-
writeConnection: async (path, connection) => { written = { path, connection }; return path; },
|
|
159
|
-
materializeTestConfig: async () => ({
|
|
160
|
-
envFile: "/tmp/bot-1346-integration.env",
|
|
161
|
-
stackConfig: "/tmp/bot-1346-stack.toml",
|
|
162
|
-
}),
|
|
163
|
-
removeConnection: async (path) => { removed.push(path); },
|
|
164
|
-
startChild: (argv, env, cwd) => { childCall = { argv, env, cwd }; queueMicrotask(() => fakeChild.emit("exit", 7, null)); return fakeChild; },
|
|
165
|
-
clock: { setInterval: () => ({ unref() {} }), clearInterval() {}, setTimeout: () => ({ unref() {} }), clearTimeout() {} },
|
|
166
|
-
},
|
|
167
|
-
);
|
|
168
|
-
assert.equal(result.exitCode, 7, "successful cleanup must not mask the child failure");
|
|
169
|
-
assert.deepEqual(childCall.argv, ["pnpm", "test:all"], "argv is forwarded without shell parsing");
|
|
170
|
-
assert.equal(childCall.env.BOTBUDDY_STACK_LEASE_ID, "lease-1346");
|
|
171
|
-
assert.equal(childCall.env.BOTBUDDY_STACK_CONNECTION_FILE, written.path);
|
|
172
|
-
assert.equal(childCall.env.BB_INTEGRATION_ENV_FILE, "/tmp/bot-1346-integration.env");
|
|
173
|
-
assert.equal(childCall.env.BB_STACK_CONFIG, "/tmp/bot-1346-stack.toml");
|
|
174
|
-
assert.equal(written.connection.db_url, "postgres://secret");
|
|
175
|
-
assert.deepEqual(removed.sort(), [written.path, "/tmp/bot-1346-integration.env", "/tmp/bot-1346-stack.toml"].sort());
|
|
176
|
-
assert.deepEqual(calls, [["release", "lease-1346"]]);
|
|
177
|
-
assert.doesNotMatch(JSON.stringify(result), /postgres:\/\/secret/);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test("BOT-1346: materialized integration environment uses the leased database port", async () => {
|
|
181
|
-
const root = mkdtempSync(join(tmpdir(), "bb-stack-integration-"));
|
|
182
|
-
const integrationDir = join(root, "supabase", "functions", "_test", "integration");
|
|
183
|
-
mkdirSync(integrationDir, { recursive: true });
|
|
184
|
-
writeFileSync(join(integrationDir, ".env.integration"), "SUPABASE_URL=http://127.0.0.1:56321\nINTEGRATION_DB_PORT=56322\n");
|
|
185
|
-
const config = await materializeLeasedTestConfig(root, "lease-port", {
|
|
186
|
-
api_url: "http://127.0.0.1:64321", db_port: 64322, anon_key: "anon", service_role_key: "service", project_id: "leased-project",
|
|
187
|
-
});
|
|
188
|
-
assert.match(readFileSync(config.envFile, "utf8"), /^INTEGRATION_DB_PORT=64322$/m);
|
|
189
|
-
assert.match(readFileSync(config.stackConfig, "utf8"), /port = 64322/);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test("BOT-1346: materialized integration config removes dotenv if its TOML write fails", async () => {
|
|
193
|
-
const root = mkdtempSync(join(tmpdir(), "bb-stack-integration-"));
|
|
194
|
-
const integrationDir = join(root, "supabase", "functions", "_test", "integration");
|
|
195
|
-
mkdirSync(integrationDir, { recursive: true });
|
|
196
|
-
writeFileSync(join(integrationDir, ".env.integration"), "SUPABASE_URL=http://127.0.0.1:56321\n");
|
|
197
|
-
const writes = []; const removed = [];
|
|
198
|
-
await assert.rejects(
|
|
199
|
-
materializeLeasedTestConfig(root, "lease-cleanup", {
|
|
200
|
-
api_url: "http://127.0.0.1:64321", db_port: 64322, anon_key: "anon", service_role_key: "service", project_id: "leased-project",
|
|
201
|
-
}, {
|
|
202
|
-
writePrivate: async (path) => { writes.push(path); if (writes.length === 2) throw new Error("ENOSPC"); },
|
|
203
|
-
removePrivate: async (path) => { removed.push(path); },
|
|
204
|
-
}),
|
|
205
|
-
/ENOSPC/,
|
|
206
|
-
);
|
|
207
|
-
assert.equal(writes.length, 2);
|
|
208
|
-
assert.deepEqual(removed, [writes[0]]);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
test("BOT-1346: queue wait errors retain a failed signed-cleanup receipt", async () => {
|
|
212
|
-
const result = await runStackLifecycle(
|
|
213
|
-
{ repo: "botbuddy-web", ticket: "BOT-1346", stackPath: ".", timeout: 5, reapTimeout: 5 },
|
|
214
|
-
["pnpm", "test:integration"],
|
|
215
|
-
{
|
|
216
|
-
auth: { Authorization: "Bearer test" }, signals: new EventEmitter(),
|
|
217
|
-
api: {
|
|
218
|
-
request: async () => ({ ok: true, data: { success: true, lease_id: "lease-queue-error", state: "queued" } }),
|
|
219
|
-
release: async (_leaseId, signal) => {
|
|
220
|
-
assert(signal instanceof AbortSignal, "cleanup release receives the reap deadline signal");
|
|
221
|
-
return { ok: false, error: "signed reap unavailable" };
|
|
222
|
-
},
|
|
223
|
-
},
|
|
224
|
-
wait: async () => ({ error: "lease stream broke" }),
|
|
225
|
-
},
|
|
226
|
-
);
|
|
227
|
-
assert.equal(result.exitCode, EXIT.LEASE_FAILED);
|
|
228
|
-
assert.equal(result.cleanup.ok, false);
|
|
229
|
-
assert.match(result.cleanup.error, /signed reap unavailable/);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
test("BOT-1346: queue timeout receipt retains a failed cleanup result", async () => {
|
|
233
|
-
const result = await runStackLifecycle(
|
|
234
|
-
{ repo: "botbuddy-web", ticket: "BOT-1346", stackPath: ".", timeout: 5, reapTimeout: 5 }, ["pnpm", "test:integration"],
|
|
235
|
-
{
|
|
236
|
-
auth: { Authorization: "Bearer test" }, signals: new EventEmitter(),
|
|
237
|
-
api: {
|
|
238
|
-
request: async () => ({ ok: true, data: { success: true, lease_id: "lease-queued", state: "queued" } }),
|
|
239
|
-
release: async () => ({ ok: false, error: "release rejected" }),
|
|
240
|
-
},
|
|
241
|
-
wait: async () => ({ timeout: true }),
|
|
242
|
-
clock: { setInterval: () => ({ unref() {} }), clearInterval() {}, setTimeout: () => ({ unref() {} }), clearTimeout() {} },
|
|
243
|
-
},
|
|
244
|
-
);
|
|
245
|
-
assert.equal(result.exitCode, EXIT.TIMEOUT);
|
|
246
|
-
assert.equal(result.cleanup.ok, false);
|
|
247
|
-
assert.match(result.cleanup.error, /release rejected/);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
test("BOT-1346: interruption during lease creation releases the lease once its ID arrives", async () => {
|
|
251
|
-
const signals = new EventEmitter();
|
|
252
|
-
let resolveRequest; let releases = 0;
|
|
253
|
-
const pending = runStackLifecycle(
|
|
254
|
-
{ repo: "botbuddy-web", ticket: "BOT-1346", stackPath: ".", timeout: 5, reapTimeout: 5 }, ["pnpm", "test:integration"],
|
|
255
|
-
{
|
|
256
|
-
auth: { Authorization: "Bearer test" }, signals,
|
|
257
|
-
api: {
|
|
258
|
-
request: () => new Promise((resolve) => { resolveRequest = resolve; }),
|
|
259
|
-
release: async () => { releases++; return { ok: true, data: { success: true, state: "reaping" } }; },
|
|
260
|
-
},
|
|
261
|
-
wait: async () => ({ woke: true, state: "reaped" }),
|
|
262
|
-
clock: { setInterval: () => ({ unref() {} }), clearInterval() {}, setTimeout: () => ({ unref() {} }), clearTimeout() {} },
|
|
263
|
-
},
|
|
264
|
-
);
|
|
265
|
-
signals.emit("SIGINT");
|
|
266
|
-
resolveRequest({ ok: true, data: { success: true, lease_id: "lease-interrupted-request", state: "queued" } });
|
|
267
|
-
const result = await pending;
|
|
268
|
-
assert.equal(result.exitCode, 130);
|
|
269
|
-
assert.equal(releases, 1);
|
|
270
|
-
assert.equal(result.cleanup.ok, true);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
test("BOT-1346: forced hard TTL cannot turn an incomplete child run into success", async () => {
|
|
274
|
-
const fakeChild = new EventEmitter(); fakeChild.pid = 4244;
|
|
275
|
-
let hardTimeout;
|
|
276
|
-
const result = await runStackLifecycle(
|
|
277
|
-
{ repo: "botbuddy-web", ticket: "BOT-1346", stackPath: ".", timeout: 5, reapTimeout: 5, hardTtl: 1 }, ["node", "-e", "process.exit(0)"],
|
|
278
|
-
{
|
|
279
|
-
auth: { Authorization: "Bearer test" }, signals: new EventEmitter(),
|
|
280
|
-
api: {
|
|
281
|
-
request: async () => ({ ok: true, data: { success: true, lease_id: "lease-ttl", state: "active" } }),
|
|
282
|
-
get: async () => ({ ok: true, data: { success: true, state: "active", connection: {} } }),
|
|
283
|
-
touch: async () => ({ ok: true, data: { success: true } }),
|
|
284
|
-
release: async () => ({ ok: true, data: { success: true, state: "reaping" } }),
|
|
285
|
-
},
|
|
286
|
-
wait: async () => ({ woke: true, state: "reaped" }), writeConnection: async (path) => path, removeConnection: async () => {},
|
|
287
|
-
startChild: () => { queueMicrotask(() => { hardTimeout(); fakeChild.emit("exit", 0, null); }); return fakeChild; },
|
|
288
|
-
clock: { setInterval: () => ({ unref() {} }), clearInterval() {}, setTimeout: (fn) => { hardTimeout = fn; return { unref() {} }; }, clearTimeout() {} },
|
|
289
|
-
},
|
|
290
|
-
);
|
|
291
|
-
assert.equal(result.exitCode, EXIT.TIMEOUT);
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
test("BOT-1346: a fenced lease cannot report a gracefully-stopped child as success", async () => {
|
|
295
|
-
const fakeChild = new EventEmitter(); fakeChild.pid = 4245;
|
|
296
|
-
let heartbeat;
|
|
297
|
-
const result = await runStackLifecycle(
|
|
298
|
-
{ repo: "botbuddy-web", ticket: "BOT-1346", stackPath: ".", timeout: 5, reapTimeout: 5 }, ["node", "-e", "process.exit(0)"],
|
|
299
|
-
{
|
|
300
|
-
auth: { Authorization: "Bearer test" }, signals: new EventEmitter(),
|
|
301
|
-
api: {
|
|
302
|
-
request: async () => ({ ok: true, data: { success: true, lease_id: "lease-fenced", state: "active" } }),
|
|
303
|
-
get: async () => ({ ok: true, data: { success: true, state: "active", connection: {} } }),
|
|
304
|
-
touch: async () => ({ ok: false, error: "lease unavailable" }),
|
|
305
|
-
release: async () => ({ ok: true, data: { success: true, state: "reaping" } }),
|
|
306
|
-
},
|
|
307
|
-
wait: async () => ({ woke: true, state: "reaped" }), writeConnection: async (path) => path, removeConnection: async () => {},
|
|
308
|
-
startChild: () => { queueMicrotask(async () => { await heartbeat(); fakeChild.emit("exit", 0, null); }); return fakeChild; },
|
|
309
|
-
clock: { setInterval: (fn) => { heartbeat = fn; return { unref() {} }; }, clearInterval() {}, setTimeout: () => ({ unref() {} }), clearTimeout() {} },
|
|
310
|
-
},
|
|
311
|
-
);
|
|
312
|
-
assert.equal(result.exitCode, EXIT.LEASE_FAILED);
|
|
313
|
-
assert.equal(result.fenced, true);
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
test("BOT-1346: stack run reports failed signed cleanup after a successful child", async () => {
|
|
317
|
-
const fakeChild = new EventEmitter(); fakeChild.pid = 4243;
|
|
318
|
-
const result = await runStackLifecycle(
|
|
319
|
-
{ repo: "botbuddy-web", ticket: "BOT-1346", stackPath: ".", timeout: 5, reapTimeout: 5 },
|
|
320
|
-
["node", "-e", "process.exit(0)"],
|
|
321
|
-
{
|
|
322
|
-
auth: { Authorization: "Bearer test" }, signals: new EventEmitter(),
|
|
323
|
-
api: {
|
|
324
|
-
request: async () => ({ ok: true, data: { success: true, lease_id: "lease-unreaped", state: "active" } }),
|
|
325
|
-
get: async () => ({ ok: true, data: { success: true, state: "active", connection: {} } }),
|
|
326
|
-
touch: async () => ({ ok: true, data: { success: true } }),
|
|
327
|
-
release: async () => ({ ok: true, data: { success: true, state: "reaping" } }),
|
|
328
|
-
},
|
|
329
|
-
wait: async () => ({ timeout: true }),
|
|
330
|
-
writeConnection: async (path) => path,
|
|
331
|
-
removeConnection: async () => {},
|
|
332
|
-
startChild: () => { queueMicrotask(() => fakeChild.emit("exit", 0, null)); return fakeChild; },
|
|
333
|
-
clock: { setInterval: () => ({ unref() {} }), clearInterval() {}, setTimeout: () => ({ unref() {} }), clearTimeout() {} },
|
|
334
|
-
},
|
|
335
|
-
);
|
|
336
|
-
assert.equal(result.childExitCode, 0);
|
|
337
|
-
assert.equal(result.exitCode, EXIT.CLEANUP_FAILED);
|
|
338
|
-
assert.equal(result.cleanup.ok, false);
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
// ── receipts ─────────────────────────────────────────────────────────────────
|
|
342
|
-
test("buildReceipt: stamps schema_version", () => {
|
|
343
|
-
assert.equal(buildReceipt({ command: "up" }).schema_version, STACK_SCHEMA_VERSION);
|
|
344
|
-
});
|
|
345
|
-
test("truncateReceipt: in-budget receipt is returned unchanged", () => {
|
|
346
|
-
const r = buildReceipt({ command: "up", outcome: "active", lease_id: "x", exit_code: 0 });
|
|
347
|
-
assert.deepEqual(truncateReceipt(r, DEFAULT_RECEIPT_MAX_BYTES), r);
|
|
348
|
-
});
|
|
349
|
-
test("truncateReceipt: collapses the oversized connection block first, keeps lease_id + exit_code", () => {
|
|
350
|
-
const big = "k".repeat(20000);
|
|
351
|
-
const r = buildReceipt({ command: "up", outcome: "active", lease_id: "lease-1", exit_code: 0, connection: { service_role_key: big } });
|
|
352
|
-
const t = truncateReceipt(r, DEFAULT_RECEIPT_MAX_BYTES);
|
|
353
|
-
assert.ok(Buffer.byteLength(JSON.stringify(t), "utf8") <= DEFAULT_RECEIPT_MAX_BYTES);
|
|
354
|
-
assert.equal(t.lease_id, "lease-1");
|
|
355
|
-
assert.equal(t.exit_code, 0);
|
|
356
|
-
assert.ok(t.connection.connection_truncated || t.truncated);
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
// ── parseSupabaseStatus ──────────────────────────────────────────────────────
|
|
360
|
-
test("parseSupabaseStatus: JSON form", () => {
|
|
361
|
-
const conn = parseSupabaseStatus(JSON.stringify({ API_URL: "http://127.0.0.1:56321", DB_URL: "postgresql://x", ANON_KEY: "anon", SERVICE_ROLE_KEY: "svc" }));
|
|
362
|
-
assert.equal(conn.api_url, "http://127.0.0.1:56321");
|
|
363
|
-
assert.equal(conn.service_role_key, "svc");
|
|
364
|
-
});
|
|
365
|
-
test("parseSupabaseStatus: plain key/value fallback", () => {
|
|
366
|
-
const conn = parseSupabaseStatus("API URL: http://127.0.0.1:56321\nanon key: theanon\nservice_role key: thesvc\n");
|
|
367
|
-
assert.equal(conn.api_url, "http://127.0.0.1:56321");
|
|
368
|
-
assert.equal(conn.anon_key, "theanon");
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
// ── eventStreamBase ──────────────────────────────────────────────────────────
|
|
372
|
-
test("eventStreamBase: derives the event-stream URL from the mcp-server URL", () => {
|
|
373
|
-
assert.equal(eventStreamBase("https://api.bot-buddy.ai/functions/v1/mcp-server"), "https://api.bot-buddy.ai/functions/v1/event-stream");
|
|
374
|
-
assert.equal(eventStreamBase("http://127.0.0.1:56321/functions/v1/mcp-server/"), "http://127.0.0.1:56321/functions/v1/event-stream");
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
// ── resolveLocalSupabaseUrl (Codex P1: never bake the prod origin) ───────────
|
|
378
|
-
test("resolveLocalSupabaseUrl: honours an already-local VITE_SUPABASE_URL", () => {
|
|
379
|
-
assert.equal(resolveLocalSupabaseUrl({ VITE_SUPABASE_URL: "http://127.0.0.1:56321" }), "http://127.0.0.1:56321");
|
|
380
|
-
assert.equal(resolveLocalSupabaseUrl({ VITE_SUPABASE_URL: "http://localhost:54321" }), "http://localhost:54321");
|
|
381
|
-
});
|
|
382
|
-
test("resolveLocalSupabaseUrl: derives the [api] port from supabase/config.toml", () => {
|
|
383
|
-
const dir = mkdtempSync(join(tmpdir(), "bb-stack-cfg-"));
|
|
384
|
-
mkdirSync(join(dir, "supabase"));
|
|
385
|
-
writeFileSync(join(dir, "supabase", "config.toml"), "[api]\nenabled = true\nport = 56321\n\n[db]\nport = 56322\n");
|
|
386
|
-
assert.equal(resolveLocalSupabaseUrl({}, dir), "http://127.0.0.1:56321");
|
|
387
|
-
});
|
|
388
|
-
test("resolveLocalSupabaseUrl: ignores a NON-local exported origin (refuses → null)", () => {
|
|
389
|
-
// A prod VITE_SUPABASE_URL must NOT be trusted, and no config.toml here ⇒ null (caller refuses).
|
|
390
|
-
const empty = mkdtempSync(join(tmpdir(), "bb-stack-nocfg-"));
|
|
391
|
-
assert.equal(resolveLocalSupabaseUrl({ VITE_SUPABASE_URL: "https://api.bot-buddy.ai" }, empty), null);
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
// ── --help snapshot (AC-4) ───────────────────────────────────────────────────
|
|
395
|
-
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"]) {
|
|
397
|
-
assert.ok(STACK_HELP.includes(s), `help missing ${s}`);
|
|
398
|
-
}
|
|
399
|
-
for (const s of ["0 ok", "2 park timed out", "3 not authenticated", "4 invalid", "5 backend", "6 lease failed", "7 internal"]) {
|
|
400
|
-
assert.ok(STACK_HELP.includes(s), `help missing exit-code doc ${s}`);
|
|
401
|
-
}
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
// ── spawn tests: exit codes + single-line receipt (bb-wait pattern) ──────────
|
|
405
|
-
test("spawn: `stack help` prints usage and exits 0", () => {
|
|
406
|
-
const res = runStack(["help"]);
|
|
407
|
-
assert.equal(res.status, 0);
|
|
408
|
-
assert.ok(res.stdout.includes("botbuddy stack"));
|
|
409
|
-
});
|
|
410
|
-
test("spawn: invalid slot 'default' → exit 4 with a single JSON-line receipt", () => {
|
|
411
|
-
const res = runStack(["up", "--slot", "default"]);
|
|
412
|
-
assert.equal(res.status, EXIT.INVALID);
|
|
413
|
-
const r = soleReceipt(res);
|
|
414
|
-
assert.equal(r.schema_version, STACK_SCHEMA_VERSION);
|
|
415
|
-
assert.equal(r.command, "up");
|
|
416
|
-
assert.equal(r.outcome, "error");
|
|
417
|
-
assert.equal(r.exit_code, EXIT.INVALID);
|
|
418
|
-
});
|
|
419
|
-
test("spawn: `stack status` without a lease_id → exit 4", () => {
|
|
420
|
-
const res = runStack(["status"]);
|
|
421
|
-
assert.equal(res.status, EXIT.INVALID);
|
|
422
|
-
soleReceipt(res);
|
|
423
|
-
});
|
|
424
|
-
test("spawn: `stack up` with a valid slot but no auth → exit 3", () => {
|
|
425
|
-
const res = runStack(["up", "--slot", "56322"]);
|
|
426
|
-
assert.equal(res.status, EXIT.AUTH);
|
|
427
|
-
const r = soleReceipt(res);
|
|
428
|
-
assert.equal(r.exit_code, EXIT.AUTH);
|
|
429
|
-
assert.equal(r.command, "up");
|
|
430
|
-
});
|
|
431
|
-
test("spawn: unknown subcommand → exit 4", () => {
|
|
432
|
-
const res = runStack(["frobnicate"]);
|
|
433
|
-
assert.equal(res.status, EXIT.INVALID);
|
|
434
|
-
});
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
3
|
-
|
|
4
|
-
import { resolveAgentProfile } from "./wait-profile.mjs";
|
|
5
|
-
|
|
6
|
-
test("BOT-1344: a profiled public wait reads its tenant-bound Keychain credential without a shell export", async () => {
|
|
7
|
-
const profile = await resolveAgentProfile({
|
|
8
|
-
explicitProfile: "botbuddy-dev",
|
|
9
|
-
env: {},
|
|
10
|
-
readCredential: async (name) => {
|
|
11
|
-
assert.equal(name, "botbuddy-dev");
|
|
12
|
-
return "fixture-keychain-agent-credential";
|
|
13
|
-
},
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
assert.equal(profile.token, "fixture-keychain-agent-credential");
|
|
17
|
-
assert.equal(profile.tenant, "botbuddy");
|
|
18
|
-
assert(!JSON.stringify(profile).includes("BOTBUDDY_AGENT_KEY"));
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test("BOT-1344: a one-off wait token overrides the Keychain profile credential", async () => {
|
|
22
|
-
const profile = await resolveAgentProfile({
|
|
23
|
-
explicitProfile: "botbuddy-dev",
|
|
24
|
-
explicitToken: "fixture-diagnostic-token",
|
|
25
|
-
env: { BOTBUDDY_BB_AGENT_KEY: "fixture-ci-token" },
|
|
26
|
-
readCredential: async () => "fixture-keychain-agent-credential",
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
assert.equal(profile.token, "fixture-diagnostic-token");
|
|
30
|
-
});
|