@botiverse/testbed-cli 0.2.0 → 0.4.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.
- package/lib/main.mjs +48 -3
- package/package.json +3 -2
- package/skills/testbed/SKILL.md +84 -0
package/lib/main.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// testbed command set. Output is human tables by default; `--json` prints the
|
|
2
2
|
// raw API response (what an agent wants). Secrets: `acquire` returns a one-time ADB
|
|
3
3
|
// private key; it is printed only in --json output or written to --key-out (0600).
|
|
4
|
-
import { openSync, writeSync, closeSync } from "node:fs";
|
|
4
|
+
import { openSync, writeSync, closeSync, readFileSync, mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
5
7
|
import { resolveAuth } from "./auth.mjs";
|
|
6
8
|
import { makeClient, ApiError } from "./client.mjs";
|
|
7
9
|
import { runAgentLogin } from "./agent-login.mjs";
|
|
@@ -30,8 +32,14 @@ export const USAGE = `testbed — cloud phones + the acceptance testbed in the R
|
|
|
30
32
|
testbed verify login <session-id> --account <account-id>
|
|
31
33
|
testbed verify reinstall <session-id> --apk-key fast/<id> --apk-sha256 HEX
|
|
32
34
|
|
|
35
|
+
testbed skill print the agent skill (SKILL.md)
|
|
36
|
+
testbed skill --install [-a AGENT...] [--global]
|
|
37
|
+
install for any runtime via "npx skills add" (Claude Code, Codex, Cursor, Gemini, OpenCode, ...)
|
|
38
|
+
testbed skill --copy-to DIR plain copy of SKILL.md (offline / unsupported runtime)
|
|
39
|
+
|
|
33
40
|
testbed cases acceptance case library
|
|
34
41
|
testbed run <case-id...> [--app --ref --apk --apk-run --release --by]
|
|
42
|
+
testbed run --all [...] the whole case table in one run (release smoke)
|
|
35
43
|
testbed runs
|
|
36
44
|
testbed show <run-id>
|
|
37
45
|
|
|
@@ -254,6 +262,36 @@ export async function main(argv, opts) {
|
|
|
254
262
|
throw new Error("usage: testbed verify <start|show|renew|end|accounts|login|reinstall> …");
|
|
255
263
|
}
|
|
256
264
|
}
|
|
265
|
+
case "skill": {
|
|
266
|
+
const install = takeSwitch(args, "install");
|
|
267
|
+
const global = takeSwitch(args, "global") || takeSwitch(args, "g");
|
|
268
|
+
const copyTo = takeFlag(args, "copy-to");
|
|
269
|
+
const agents = [];
|
|
270
|
+
for (let i = 0; i < args.length;) {
|
|
271
|
+
if ((args[i] === "-a" || args[i] === "--agent") && args[i + 1]) { agents.push(args[i + 1]); args.splice(i, 2); } else i += 1;
|
|
272
|
+
}
|
|
273
|
+
const pkgDir = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
274
|
+
const src = join(pkgDir, "skills", "testbed", "SKILL.md");
|
|
275
|
+
const text = readFileSync(src, "utf8");
|
|
276
|
+
if (copyTo) {
|
|
277
|
+
mkdirSync(copyTo, { recursive: true });
|
|
278
|
+
writeFileSync(join(copyTo, "SKILL.md"), text);
|
|
279
|
+
out(`copied skill 'testbed' -> ${join(copyTo, "SKILL.md")}`);
|
|
280
|
+
return exit(0);
|
|
281
|
+
}
|
|
282
|
+
if (!install) { out(text); return exit(0); }
|
|
283
|
+
// The open Agent Skills installer (npm `skills`, vercel-labs/skills) knows
|
|
284
|
+
// every runtime's skills directory (77 agents on 2026-09-04) and keeps one
|
|
285
|
+
// canonical copy under .agents/skills with per-agent links. We hand it our
|
|
286
|
+
// package directory; it discovers skills/testbed/SKILL.md itself.
|
|
287
|
+
const argv = ["-y", "skills", "add", pkgDir, "--skill", "testbed", "-y", ...(global ? ["-g"] : []), ...agents.flatMap((a) => ["-a", a])];
|
|
288
|
+
const spawn = opts.spawnImpl ?? (await import("node:child_process")).spawnSync;
|
|
289
|
+
const r = spawn("npx", argv, { stdio: "inherit", env: { ...process.env, ...env } });
|
|
290
|
+
if (r.error) { err(`testbed: could not run npx skills (${r.error.message}); fall back to: testbed skill --copy-to <dir>`); return exit(1); }
|
|
291
|
+
if (r.status !== 0) { err(`testbed: npx skills add exited ${r.status}; fall back to: testbed skill --copy-to <dir>`); return exit(r.status ?? 1); }
|
|
292
|
+
out(`installed skill 'testbed' via npx skills${agents.length ? ` for ${agents.join(", ")}` : ""}${global ? " (global)" : ""}`);
|
|
293
|
+
return exit(0);
|
|
294
|
+
}
|
|
257
295
|
case "cases": {
|
|
258
296
|
const d = await api("GET", "/cases");
|
|
259
297
|
emit(d, () => table(d.cases.map((c) => ({ id: c.id, tier: c.tier, rev: c.revision, title: c.title })), ["id", "tier", "rev", "title"], out));
|
|
@@ -266,8 +304,15 @@ export async function main(argv, opts) {
|
|
|
266
304
|
const apkRun = takeFlag(args, "apk-run");
|
|
267
305
|
const release = takeFlag(args, "release");
|
|
268
306
|
const by = takeFlag(args, "by", env.USER || "testbed-cli");
|
|
269
|
-
const
|
|
270
|
-
|
|
307
|
+
const all = takeSwitch(args, "all");
|
|
308
|
+
let caseIds = args.filter((a) => !a.startsWith("--"));
|
|
309
|
+
if (all) {
|
|
310
|
+
if (caseIds.length) throw new Error("usage: --all takes no case ids");
|
|
311
|
+
const cat = await api("GET", "/cases");
|
|
312
|
+
caseIds = cat.cases.map((c) => c.id);
|
|
313
|
+
if (!caseIds.length) throw new Error("the case table is empty");
|
|
314
|
+
}
|
|
315
|
+
if (!caseIds.length) throw new Error("usage: testbed run <case-id...> | --all [--app slug] [--ref branch] [--apk url] [--apk-run ci-run-id] [--release id] [--by name]");
|
|
271
316
|
const d = await api("POST", "/runs", { app_slug: app, case_ids: caseIds, apk_ref: apk, ref, apk_run_id: apkRun, hands_release_id: release, requested_by: by });
|
|
272
317
|
emit(d, () => { out(`run ${d.run_id} — ${d.cases} case(s), ${d.dispatched} dispatched`); out(`${auth.apiBase}/runs/${d.run_id}`); });
|
|
273
318
|
return exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@botiverse/testbed-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "CLI for testbed \u2014 cloud phones and the acceptance testbed in the Raft release loop",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"bin",
|
|
12
|
-
"lib"
|
|
12
|
+
"lib",
|
|
13
|
+
"skills"
|
|
13
14
|
],
|
|
14
15
|
"engines": {
|
|
15
16
|
"node": ">=20"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testbed
|
|
3
|
+
description: "Use the testbed CLI to lease cloud Android phones, open human verification sessions, and run the acceptance case table for Raft app releases. Use when asked to smoke-test an APK, verify a build on a real device, hand a phone to a human verifier, or run release acceptance. Login is `testbed login` (managed Raft agents: PKCE through raft, no browser)."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# testbed — cloud phones and release acceptance
|
|
7
|
+
|
|
8
|
+
The testbed is the acceptance gate for Raft app releases: a warm pool of cloud Android
|
|
9
|
+
phones, human verification sessions on those phones, and an acceptance case table that
|
|
10
|
+
runs against an APK. Everything is driven by the `testbed` CLI; the Raft manifest only
|
|
11
|
+
publishes `agent-login`.
|
|
12
|
+
|
|
13
|
+
## Setup (once per seat)
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm i -g @botiverse/testbed-cli # or: npx @botiverse/testbed-cli …
|
|
17
|
+
# non-root: npm config set prefix ~/.npm-global && export PATH="$HOME/.npm-global/bin:$PATH"
|
|
18
|
+
testbed login # managed Raft agent: no browser; human/CI: set TESTBED_TOKEN
|
|
19
|
+
testbed whoami # who the API thinks you are
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`TESTBED_URL` selects the instance (default https://testbed.botiverse.build). Add `--json`
|
|
23
|
+
to any command to get the raw API body for parsing.
|
|
24
|
+
|
|
25
|
+
## Workflow 1 — run the acceptance table against a build
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
testbed cases # the case table (id, tier, revision, title)
|
|
29
|
+
testbed run --all --apk <download-url> --release <id> # whole table in one run (release smoke)
|
|
30
|
+
testbed run <case-id...> --apk <download-url> # a subset
|
|
31
|
+
testbed runs # recent runs
|
|
32
|
+
testbed show <run-id> # per-case verdicts, turns, evidence
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Release SOP rules (artin, 2026-09-04): the default 3-step smoke (login, channel list,
|
|
36
|
+
open a channel) is NOT a full smoke. A release smoke is `run --all`. A cell that was not
|
|
37
|
+
run is recorded as `NOT RUN` with the reason (hotfix exemption: `NOT RUN·hotfix 豁免`),
|
|
38
|
+
never left blank. Receipts bind `versionCode + SHA-256`, not just the version name.
|
|
39
|
+
|
|
40
|
+
## Workflow 2 — hand a phone to a human verifier
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
testbed verify start --ttl 30 --app-ref "1.11.0 (1110001)" \
|
|
44
|
+
[--apk-key fast/<id> --apk-sha256 <hex>] # pre-install an APK; key and sha go together
|
|
45
|
+
# prints the verify page URL — give it to the human
|
|
46
|
+
testbed verify accounts <session-id> # QA accounts they may log in with
|
|
47
|
+
testbed verify login <session-id> --account <account-id>
|
|
48
|
+
testbed verify renew <session-id> --minutes 30
|
|
49
|
+
testbed verify reinstall <session-id> --apk-key fast/<id> --apk-sha256 <hex>
|
|
50
|
+
testbed verify end <session-id> # always end; the device is reclaimed
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
One verification session is live at a time; `start` answers 409 when another is live.
|
|
54
|
+
`show` on an ended session answers 410.
|
|
55
|
+
|
|
56
|
+
## Workflow 3 — drive a phone yourself over ADB
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
testbed pool # free / leased
|
|
60
|
+
testbed acquire --ttl 20 --key-out ./adb.key # lease; private key written 0600, never printed
|
|
61
|
+
adb connect <adb_endpoint> # from the acquire output
|
|
62
|
+
testbed renew <lease-id> --ttl 20
|
|
63
|
+
testbed diagnose-adb <lease-id> [--pubkey-sha256 <hex>]
|
|
64
|
+
testbed release <lease-id> # always release
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Device safety (non-negotiable):
|
|
68
|
+
- Never paste `adb_private_key` into Raft messages, tasks, logs, or artifacts.
|
|
69
|
+
- Before connecting, derive the saved key's android_pubkey SHA-256 and require it to equal
|
|
70
|
+
`adb_public_key_sha256` from the acquire response; on mismatch, release and do not connect.
|
|
71
|
+
- Release every lease. Devices in `reclaiming` / `reclaim_failed` are never yours.
|
|
72
|
+
- Cloud-phone screenshots can return a stale frame: to prove "nothing changed", the
|
|
73
|
+
capture must include something that would have changed if it were live.
|
|
74
|
+
|
|
75
|
+
## Errors
|
|
76
|
+
|
|
77
|
+
- `401` → `testbed login` (agent) or check `TESTBED_TOKEN` (human/CI).
|
|
78
|
+
- `409 POOL_EXHAUSTED` / `ANOTHER_SESSION_LIVE` → wait or end/release what you hold.
|
|
79
|
+
- `410` on a verification session → it is over; start a new one.
|
|
80
|
+
|
|
81
|
+
## Reference
|
|
82
|
+
|
|
83
|
+
`GET /api/help` on the instance lists every command and the raw routes behind them.
|
|
84
|
+
Manifest action for Raft Agent Login: `agent-login` (the CLI performs it for you).
|