@test-lab-ai/cli 0.2.16 → 0.2.18
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/bin/testlab.mjs +33 -1
- package/lib/api.mjs +28 -1
- package/package.json +2 -2
package/bin/testlab.mjs
CHANGED
|
@@ -40,6 +40,9 @@ Usage:
|
|
|
40
40
|
(credentials, labels, fixtures, plans)
|
|
41
41
|
testlab plans list List your test plans
|
|
42
42
|
testlab plans create -f plan.json Create one plan from JSON
|
|
43
|
+
testlab plans update <id> [-f patch.json] [--prompt P] [--name N]
|
|
44
|
+
Update an existing plan; only the
|
|
45
|
+
fields you pass change
|
|
43
46
|
testlab projects list List your projects
|
|
44
47
|
testlab credentials set <key> --value <value> Set a credential ({{credentials.<key>}})
|
|
45
48
|
testlab credentials list List credential keys (values never shown)
|
|
@@ -216,6 +219,34 @@ async function cmdPlansCreate(flags) {
|
|
|
216
219
|
log(`✓ Created plan #${r.json.testPlan.id}: ${r.json.testPlan.name}`)
|
|
217
220
|
}
|
|
218
221
|
|
|
222
|
+
async function cmdPlansUpdate(flags, args) {
|
|
223
|
+
const { apiKey, apiUrl } = requireAuth(flags)
|
|
224
|
+
const planId = parsePlanIdArg(args[2] ?? flags.plan)
|
|
225
|
+
if (!Number.isInteger(planId)) {
|
|
226
|
+
errExit("usage: testlab plans update <id> [-f patch.json] [--prompt P] [--name N] (numeric plan id; see `testlab plans list`)")
|
|
227
|
+
}
|
|
228
|
+
// Build a partial patch. A JSON file is the full surface (name, prompt,
|
|
229
|
+
// testType, agentType, devices, paused, projectId); --prompt / --name are
|
|
230
|
+
// quick overrides for the two common edits. Only the fields present are sent,
|
|
231
|
+
// and the server changes only those.
|
|
232
|
+
let patch = {}
|
|
233
|
+
if (flags.file) {
|
|
234
|
+
try {
|
|
235
|
+
patch = JSON.parse(fs.readFileSync(flags.file, "utf8"))
|
|
236
|
+
} catch (e) {
|
|
237
|
+
errExit(`could not read ${flags.file}: ${e.message}`)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (flags.prompt !== undefined) patch.prompt = flags.prompt
|
|
241
|
+
if (flags.name !== undefined) patch.name = flags.name
|
|
242
|
+
if (Object.keys(patch).length === 0) {
|
|
243
|
+
errExit("nothing to update: pass -f <patch.json> and/or --prompt / --name. Editable fields: name, prompt, testType, agentType, devices, paused, projectId")
|
|
244
|
+
}
|
|
245
|
+
const r = await apiFetch(apiUrl, apiKey, "PUT", `/api/v1/test-plans/${planId}`, patch)
|
|
246
|
+
if (!r.ok) errExit(`${r.status}: ${r.json?.error || ""}`)
|
|
247
|
+
log(`✓ Updated plan #${r.json.testPlan.id}: ${r.json.testPlan.name}`)
|
|
248
|
+
}
|
|
249
|
+
|
|
219
250
|
async function cmdCredentialsSet(flags, args) {
|
|
220
251
|
const { apiKey, apiUrl } = requireAuth(flags)
|
|
221
252
|
const key = args[2]
|
|
@@ -639,7 +670,8 @@ async function main() {
|
|
|
639
670
|
case "plans":
|
|
640
671
|
if (args[1] === "list") return cmdPlansList(flags)
|
|
641
672
|
if (args[1] === "create") return cmdPlansCreate(flags)
|
|
642
|
-
|
|
673
|
+
if (args[1] === "update") return cmdPlansUpdate(flags, args)
|
|
674
|
+
return errExit("usage: testlab plans <list|create|update>")
|
|
643
675
|
case "projects":
|
|
644
676
|
if (args[1] === "list") return cmdProjectsList(flags)
|
|
645
677
|
return errExit("usage: testlab projects list")
|
package/lib/api.mjs
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs"
|
|
2
|
+
import { fileURLToPath } from "node:url"
|
|
3
|
+
import { dirname, join } from "node:path"
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
6
|
* Tiny fetch wrapper for the test-lab public API. Uses global fetch (Node 18+).
|
|
3
7
|
* Returns { ok, status, json } - json is the parsed body (or { raw } on non-JSON).
|
|
@@ -10,11 +14,34 @@
|
|
|
10
14
|
* short-circuits before the app's JSON error handler) from surfacing as a hard
|
|
11
15
|
* "500: upload failed" when a one-second retry would have gone through.
|
|
12
16
|
*/
|
|
17
|
+
|
|
18
|
+
// Client User-Agent: defaults to the CLI's own name+version so the server's
|
|
19
|
+
// audit log can attribute an action to its source surface. The MCP server reuses
|
|
20
|
+
// this same apiFetch, so it overrides the tag at boot via setClientUserAgent
|
|
21
|
+
// ("testlab-mcp/<ver>"); the browser extension hits the API directly and carries
|
|
22
|
+
// its own UA. Kept module-level (not per-call) so every wrapped call is tagged
|
|
23
|
+
// without threading the value through each higher-level lib function.
|
|
24
|
+
function cliVersion() {
|
|
25
|
+
try {
|
|
26
|
+
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json")
|
|
27
|
+
return JSON.parse(readFileSync(pkgPath, "utf8")).version || "0.0.0"
|
|
28
|
+
} catch {
|
|
29
|
+
return "0.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let CLIENT_USER_AGENT = `testlab-cli/${cliVersion()}`
|
|
34
|
+
|
|
35
|
+
/** Override the User-Agent sent on every apiFetch call (MCP calls this at boot). */
|
|
36
|
+
export function setClientUserAgent(ua) {
|
|
37
|
+
if (ua && typeof ua === "string") CLIENT_USER_AGENT = ua
|
|
38
|
+
}
|
|
39
|
+
|
|
13
40
|
export async function apiFetch(apiUrl, apiKey, method, pathname, body, opts = {}) {
|
|
14
41
|
const retries = Number.isInteger(opts.retries) ? opts.retries : method === "GET" ? 2 : 0
|
|
15
42
|
const backoffMs = Number.isInteger(opts.backoffMs) ? opts.backoffMs : 800
|
|
16
43
|
|
|
17
|
-
const headers = {}
|
|
44
|
+
const headers = { "User-Agent": CLIENT_USER_AGENT }
|
|
18
45
|
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`
|
|
19
46
|
if (body !== undefined) headers["Content-Type"] = "application/json"
|
|
20
47
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@test-lab-ai/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.18",
|
|
4
4
|
"description": "Import existing test plans into test-lab.ai from the command line (or an AI agent).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"AGENTS.md"
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
|
-
"test": "node test/toposort.test.mjs",
|
|
21
|
+
"test": "node test/toposort.test.mjs && node test/api-useragent.test.mjs",
|
|
22
22
|
"prepublishOnly": "node scripts/bundle-skill.mjs && npm test"
|
|
23
23
|
},
|
|
24
24
|
"keywords": [
|