@botbuddy/cli 1.61.6 → 1.61.8
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.
|
Binary file
|
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.61.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.61.8",
|
|
3
|
+
"source_commit": "c781ee6542c93c05a5ec08e7348fddc0330f33d8",
|
|
4
|
+
"source_sha256": "b1b8c8cfaa0f4c91f023a78360d2039089885a65367182f00df7dd4312865608",
|
|
5
5
|
"target": "darwin-arm64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "cdaab9fc0b5116cbed342c31c5e7c6544022d8c927cd987daa6d9c6986cc99c4"
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.61.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.61.8",
|
|
3
|
+
"source_commit": "c781ee6542c93c05a5ec08e7348fddc0330f33d8",
|
|
4
|
+
"source_sha256": "b1b8c8cfaa0f4c91f023a78360d2039089885a65367182f00df7dd4312865608",
|
|
5
5
|
"target": "linux-x64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "4047d1635cace367c281d3d8c68268d45ffba24cbae48ff07f08cb384985ca03"
|
|
7
7
|
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
|
-
"release_version": "1.61.
|
|
4
|
-
"source_commit": "
|
|
5
|
-
"source_sha256": "
|
|
3
|
+
"release_version": "1.61.8",
|
|
4
|
+
"source_commit": "c781ee6542c93c05a5ec08e7348fddc0330f33d8",
|
|
5
|
+
"source_sha256": "b1b8c8cfaa0f4c91f023a78360d2039089885a65367182f00df7dd4312865608",
|
|
6
6
|
"targets": {
|
|
7
7
|
"darwin-arm64": {
|
|
8
8
|
"goos": "darwin",
|
|
9
9
|
"goarch": "arm64",
|
|
10
10
|
"binary": "daemon/bin/darwin-arm64/botbuddy-daemon",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "cdaab9fc0b5116cbed342c31c5e7c6544022d8c927cd987daa6d9c6986cc99c4",
|
|
12
12
|
"mode": "0755"
|
|
13
13
|
},
|
|
14
14
|
"linux-x64": {
|
|
15
15
|
"goos": "linux",
|
|
16
16
|
"goarch": "amd64",
|
|
17
17
|
"binary": "daemon/bin/linux-x64/botbuddy-daemon",
|
|
18
|
-
"sha256": "
|
|
18
|
+
"sha256": "4047d1635cace367c281d3d8c68268d45ffba24cbae48ff07f08cb384985ca03",
|
|
19
19
|
"mode": "0755"
|
|
20
20
|
}
|
|
21
21
|
}
|
package/package.json
CHANGED
package/src/test-lane.mjs
CHANGED
|
@@ -9,8 +9,13 @@
|
|
|
9
9
|
// the lane keeps executing detached. A worker (later phase) tails the reporter's
|
|
10
10
|
// NDJSON and flushes per-case verdicts through `sync_test_run_cases`.
|
|
11
11
|
//
|
|
12
|
-
//
|
|
12
|
+
// Test-run telemetry down (create_test_run fails) but the durable launcher
|
|
13
|
+
// accepts? The lane still runs; the JSON line says telemetry:"unavailable",
|
|
13
14
|
// wait:null, and the child exit code is preserved.
|
|
15
|
+
//
|
|
16
|
+
// Launcher refused (BOT-2685)? Nothing runs. The line carries the launcher's
|
|
17
|
+
// own outcome/error/recovery and exit code, wait:null, and the pre-created run
|
|
18
|
+
// is closed `completed` with metadata.launch_failed so its waiters wake.
|
|
14
19
|
|
|
15
20
|
import { randomUUID } from "node:crypto";
|
|
16
21
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -45,6 +50,7 @@ export function parseTestArgs(argv, { env = process.env } = {}) {
|
|
|
45
50
|
environment: "local",
|
|
46
51
|
ticket: null, pr: null, repo: null,
|
|
47
52
|
laneKind: null,
|
|
53
|
+
rerunReason: null,
|
|
48
54
|
wait: false, json: false,
|
|
49
55
|
};
|
|
50
56
|
const errors = [];
|
|
@@ -66,6 +72,17 @@ export function parseTestArgs(argv, { env = process.env } = {}) {
|
|
|
66
72
|
case "--pr": { const raw = value(flag, i); i++; opts.pr = raw == null ? null : Number(raw); if (raw != null && !Number.isInteger(opts.pr)) errors.push("--pr must be an integer"); break; }
|
|
67
73
|
case "--repo": opts.repo = value(flag, i); i++; break;
|
|
68
74
|
case "--lane-kind": opts.laneKind = value(flag, i); i++; break;
|
|
75
|
+
// BOT-2685: the durable launcher refuses an identical repeat without a
|
|
76
|
+
// reason; validate the shape here, the server validates the code.
|
|
77
|
+
case "--rerun-reason": {
|
|
78
|
+
const raw = value(flag, i); i++;
|
|
79
|
+
if (raw == null) break;
|
|
80
|
+
let parsed = null;
|
|
81
|
+
try { parsed = JSON.parse(raw); } catch { /* reported below */ }
|
|
82
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) errors.push("--rerun-reason must be a JSON object, e.g. '{\"code\":\"flaky_suspected\"}'");
|
|
83
|
+
else opts.rerunReason = raw;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
69
86
|
case "--wait": opts.wait = true; break;
|
|
70
87
|
case "--json": opts.json = true; break;
|
|
71
88
|
default:
|
|
@@ -140,18 +157,44 @@ export function defaultGitInfo({ cwd = process.cwd(), ticket = null, pr = null,
|
|
|
140
157
|
|
|
141
158
|
// The production lane launcher: run the lane through the durable `botbuddy run`
|
|
142
159
|
// worker, carrying the telemetry env into its detached child.
|
|
143
|
-
async function defaultLaunchLane({ command, sessionId, sessionToken, environment, expectedDurationSeconds, childEnv, testRun, cwd, call }) {
|
|
160
|
+
async function defaultLaunchLane({ command, sessionId, sessionToken, environment, expectedDurationSeconds, childEnv, testRun, cwd, call, rerunReason }) {
|
|
144
161
|
// BOT-1572/1582: pass --session-id only when a plain id is used; a token-only lane
|
|
145
162
|
// relies on the inherited $BOTBUDDY_AGENT_SESSION_TOKEN (and an explicit --agent-session-token
|
|
146
163
|
// so the child never falls back to a machine credential).
|
|
147
164
|
const argv = [
|
|
148
165
|
...(sessionId ? ["--session-id", sessionId] : []),
|
|
149
166
|
...(sessionToken ? ["--agent-session-token", sessionToken] : []),
|
|
167
|
+
...(rerunReason ? ["--rerun-reason", rerunReason] : []),
|
|
150
168
|
"--environment", environment, "--category", "validation", "--kind", "full_suite",
|
|
151
169
|
"--expected-duration", String(expectedDurationSeconds ?? 0), "--", ...command,
|
|
152
170
|
];
|
|
153
171
|
const result = await launchRun(argv, { cwd, call: sessionToken ? null : call, childEnv, testRun });
|
|
154
|
-
return { exitCode: result.exitCode, runId: result.receipt?.run_id ?? null };
|
|
172
|
+
return { exitCode: result.exitCode, runId: result.receipt?.run_id ?? null, receipt: result.receipt ?? null };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// BOT-2685: the BOT-816 rerun policy refusals carry no recovery of their own,
|
|
176
|
+
// and they are the usual reason a lane retry on an unchanged commit is refused.
|
|
177
|
+
const REFUSAL_RECOVERY = Object.freeze({
|
|
178
|
+
rerun_reason_required: "this lane already ran on this commit and environment; retry with --rerun-reason '{\"code\":\"flaky_suspected\"}' (codes: source_changed|environment_changed|focused_reproducer_passed|flaky_suspected|infrastructure_failure|dependency_drift|approval_retry|user_requested|other+detail), or commit a change",
|
|
179
|
+
// owner_lost: the command registered (so a plain retry is now a repeat) but
|
|
180
|
+
// its detached worker never started.
|
|
181
|
+
launcher_failed: "the lane's worker process failed to start; retry with --rerun-reason '{\"code\":\"infrastructure_failure\"}'",
|
|
182
|
+
full_suite_rerun_blocked: "this full suite already failed twice on this commit; commit a fix, or prove a focused reproducer with `bb run --kind focused` and retry with --rerun-reason '{\"code\":\"focused_reproducer_passed\",\"focused_reproducer_run_id\":\"<run_id>\"}'",
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// BOT-2685: a refused launch has no worker, so nothing else will ever complete
|
|
186
|
+
// the run created for it. Close it here, once, with the refusal on the record.
|
|
187
|
+
// A denied or failed update is reported as such — never retried into a claim.
|
|
188
|
+
async function reconcileRefusedRun(update, { testRunId, laneName, runner, outcome, error, exitCode, commandRunId }) {
|
|
189
|
+
let res;
|
|
190
|
+
try {
|
|
191
|
+
res = await update("update_test_run", {
|
|
192
|
+
test_run_id: testRunId, status: "completed",
|
|
193
|
+
metadata: { lane: laneName, runner, launch_failed: true, launch_outcome: outcome, launch_error: error, exit_code: exitCode, command_run_id: commandRunId },
|
|
194
|
+
});
|
|
195
|
+
} catch (e) { res = { ok: false, error: e instanceof Error ? e.message : String(e) }; }
|
|
196
|
+
if (res?.ok && !res.isError) return { test_run_reconciled: true };
|
|
197
|
+
return { test_run_reconciled: false, reconcile_error: res?.data?.error ?? res?.error ?? "update_test_run_failed" };
|
|
155
198
|
}
|
|
156
199
|
|
|
157
200
|
function waitCommand(testRunId, sessionId, sessionToken) {
|
|
@@ -288,12 +331,41 @@ export async function launchTestLane(argv, {
|
|
|
288
331
|
const testRun = testRunId
|
|
289
332
|
? { test_run_id: testRunId, events_path: eventsPath, lane: laneName, runner: resolved.lane.runner, git_sha: git.sha ?? null, branch: git.branch ?? null }
|
|
290
333
|
: null;
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
334
|
+
let launch;
|
|
335
|
+
try {
|
|
336
|
+
launch = await launchLane({
|
|
337
|
+
command: [...resolved.lane.command, ...extra],
|
|
338
|
+
sessionId: opts.sessionId, sessionToken: opts.sessionToken, environment: opts.environment,
|
|
339
|
+
expectedDurationSeconds: resolved.lane.expected_duration_seconds ?? 0,
|
|
340
|
+
childEnv, testRun, cwd, call, testRunId, lane: laneName, runner: resolved.lane.runner,
|
|
341
|
+
rerunReason: opts.rerunReason,
|
|
342
|
+
});
|
|
343
|
+
} catch (e) {
|
|
344
|
+
const error = `launch_exception:${e instanceof Error ? e.message : String(e)}`;
|
|
345
|
+
launch = { exitCode: EXIT_TEST.INTERNAL, runId: null, receipt: { outcome: "rejected", error, exit_code: EXIT_TEST.INTERNAL } };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// BOT-2685: the durable launcher's verdict is authoritative. Anything but OK
|
|
349
|
+
// means no worker exists — say so with its own reason, never "launched".
|
|
350
|
+
if (launch.exitCode !== EXIT_TEST.OK) {
|
|
351
|
+
const receipt = launch.receipt ?? {};
|
|
352
|
+
const outcome = receipt.outcome === "owner_lost" ? "owner_lost" : "rejected";
|
|
353
|
+
// owner_lost is the one refusal whose command run was registered (and
|
|
354
|
+
// already marked owner_lost server-side); any other run id never existed.
|
|
355
|
+
const commandRunId = outcome === "owner_lost" ? launch.runId ?? null : null;
|
|
356
|
+
const error = receipt.error
|
|
357
|
+
?? (Array.isArray(receipt.errors) ? receipt.errors.join("; ") : outcome === "owner_lost" ? "launcher_failed" : "launch_refused");
|
|
358
|
+
const reconciled = testRunId
|
|
359
|
+
? await reconcileRefusedRun(callWithSessionRetry, { testRunId, laneName, runner: resolved.lane.runner, outcome, error, exitCode: launch.exitCode, commandRunId })
|
|
360
|
+
: {};
|
|
361
|
+
const recovery = receipt.recovery ?? REFUSAL_RECOVERY[error] ?? null;
|
|
362
|
+
const line = {
|
|
363
|
+
outcome, error, ...(recovery ? { recovery } : {}), exit_code: launch.exitCode,
|
|
364
|
+
test_run_id: testRunId, ...reconciled, command_run_id: commandRunId, lane: laneName,
|
|
365
|
+
wait: null, receipt_path: null, waits_url: WAITS_URL,
|
|
366
|
+
};
|
|
367
|
+
return { exitCode: launch.exitCode, line: JSON.stringify(line), testRunId, commandRunId, opts, refused: true };
|
|
368
|
+
}
|
|
297
369
|
|
|
298
370
|
const commandRunId = launch.runId;
|
|
299
371
|
const line = testRunId
|
|
@@ -308,7 +380,7 @@ export async function launchTestLane(argv, {
|
|
|
308
380
|
receipt_path: commandRunId ? receiptPath(commandRunId) : null, waits_url: WAITS_URL,
|
|
309
381
|
};
|
|
310
382
|
|
|
311
|
-
return { exitCode: launch.exitCode, line: JSON.stringify(line), testRunId, commandRunId, opts };
|
|
383
|
+
return { exitCode: launch.exitCode, line: JSON.stringify(line), testRunId, commandRunId, opts, refused: false };
|
|
312
384
|
}
|
|
313
385
|
|
|
314
386
|
// AC-9 (--wait foreground convenience): block until the run reaches a terminal
|
|
@@ -349,7 +421,7 @@ function laneList(cwd) {
|
|
|
349
421
|
}
|
|
350
422
|
|
|
351
423
|
function testHelp() {
|
|
352
|
-
console.log(`botbuddy test <lane> [--session-id <uuid>] [--agent-session-token <token>] [--environment local] [--ticket <KEY>] [--pr <n>] [--repo <owner/repo>] [--lane-kind <${LANE_KINDS.join("|")}>] [--wait] [--json] [-- <extra args>]
|
|
424
|
+
console.log(`botbuddy test <lane> [--session-id <uuid>] [--agent-session-token <token>] [--environment local] [--ticket <KEY>] [--pr <n>] [--repo <owner/repo>] [--lane-kind <${LANE_KINDS.join("|")}>] [--rerun-reason <json>] [--wait] [--json] [-- <extra args>]
|
|
353
425
|
--agent-session-token <token> session credential (env $BOTBUDDY_AGENT_SESSION_TOKEN); --agent-key / --session-token are one-release legacy aliases
|
|
354
426
|
botbuddy test list List the lanes configured in .botbuddy/lanes.json
|
|
355
427
|
botbuddy test help Show this help
|
|
@@ -370,8 +442,8 @@ export async function cmdTest(args, { cwd = process.cwd() } = {}) {
|
|
|
370
442
|
process.stdout.write(result.line + "\n");
|
|
371
443
|
// --wait (AC-9): after launch, block until the run completes and exit with the
|
|
372
444
|
// lane's exit code; a timeout exits 2 without killing the lane. Only meaningful
|
|
373
|
-
// when telemetry is live (there is a run to wait on).
|
|
374
|
-
if (result.testRunId && result.opts?.wait) {
|
|
445
|
+
// when telemetry is live (there is a run to wait on) and the lane launched.
|
|
446
|
+
if (result.testRunId && result.opts?.wait && !result.refused) {
|
|
375
447
|
const wait = await waitForLane({ testRunId: result.testRunId, commandRunId: result.commandRunId });
|
|
376
448
|
process.exitCode = wait.exitCode;
|
|
377
449
|
return;
|