@testsmith/api-spector 0.7.7 → 0.7.9

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.
Files changed (31) hide show
  1. package/bin/cli.js +4 -0
  2. package/out/main/chunks/{broker-client-Df4PpJqB.js → broker-client-CJBfpvbd.js} +4 -1
  3. package/out/main/chunks/{flow-engine-BVzjz4lJ.js → flow-engine-BxwjEy37.js} +66 -1
  4. package/out/main/chunks/{handle-Bz0C56Eq.js → handle-Crv1i9hb.js} +60 -0
  5. package/out/main/chunks/{import-BhGBiW7j.js → import-DTcZMRNu.js} +2 -2
  6. package/out/main/chunks/perf-report-ChaO3K0w.js +124 -0
  7. package/out/main/chunks/perf-run-Co_5unBR.js +578 -0
  8. package/out/main/chunks/{request-collection-KGPuvN7o.js → request-collection-gkOYlWml.js} +68 -11
  9. package/out/main/chunks/{request-exec-B-F0K-pN.js → request-exec-CgyqbNWk.js} +445 -13
  10. package/out/main/chunks/run-concurrent-468tH0AX.js +26 -0
  11. package/out/main/chunks/{soap-handler-DqIPcIQL.js → soap-handler-Brr7BPWO.js} +2 -2
  12. package/out/main/contract.js +6 -3
  13. package/out/main/flow.js +8 -5
  14. package/out/main/index.js +1352 -185
  15. package/out/main/lib.js +6 -3
  16. package/out/main/perf-worker.js +45 -0
  17. package/out/main/perf.js +253 -0
  18. package/out/main/runner.js +181 -7
  19. package/out/main/socketio.js +172 -0
  20. package/out/main/wsdl.js +3 -3
  21. package/out/preload/index.js +127 -2
  22. package/out/renderer/assets/{GraphQLEditor-BXpdmpn6.js → GraphQLEditor-CXvLKrnv.js} +59 -14
  23. package/out/renderer/assets/{SoapEditor-De6WlP6E.js → SoapEditor-7hxhISPZ.js} +3 -3
  24. package/out/renderer/assets/ajv-CJPwFjiP.js +6718 -0
  25. package/out/renderer/assets/{codemirror-F6m3Hm1M.js → codemirror-CdIFBPs2.js} +1214 -1207
  26. package/out/renderer/assets/{index-Gcd_t6pX.js → index-BUFSn_qD.js} +15344 -12285
  27. package/out/renderer/assets/index-DkhdKzQJ.css +627 -0
  28. package/out/renderer/index.html +4 -4
  29. package/package.json +12 -6
  30. package/out/renderer/assets/ajv-2MAg6kGF.js +0 -6153
  31. package/out/renderer/assets/index-CYArpqm5.css +0 -627
package/out/main/lib.js CHANGED
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const mockServer = require("./chunks/mock-server-C4kTFkod.js");
4
- const requestExec = require("./chunks/request-exec-B-F0K-pN.js");
5
- const flowEngine = require("./chunks/flow-engine-BVzjz4lJ.js");
4
+ const requestExec = require("./chunks/request-exec-CgyqbNWk.js");
5
+ const flowEngine = require("./chunks/flow-engine-BxwjEy37.js");
6
6
  require("node:http");
7
7
  require("node:crypto");
8
8
  require("node:vm");
@@ -11,13 +11,16 @@ require("@xmldom/xmldom");
11
11
  require("undici");
12
12
  require("fs/promises");
13
13
  require("tls");
14
- require("./chunks/handle-Bz0C56Eq.js");
14
+ require("./chunks/handle-Crv1i9hb.js");
15
15
  require("node:fs");
16
16
  require("node:os");
17
17
  require("node:path");
18
18
  require("node:fs/promises");
19
19
  require("tv4");
20
20
  require("jsonpath-plus");
21
+ require("mqtt");
22
+ require("ws");
23
+ require("socket.io-client");
21
24
  require("ajv");
22
25
  const refKey = (ref) => `${ref.collectionId}:${ref.requestId}`;
23
26
  async function runFlowHeadless(input) {
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ const perfRun = require("./chunks/perf-run-Co_5unBR.js");
3
+ const requestExec = require("./chunks/request-exec-CgyqbNWk.js");
4
+ require("@grpc/grpc-js");
5
+ require("@grpc/proto-loader");
6
+ require("node:fs");
7
+ require("node:os");
8
+ require("node:path");
9
+ require("./chunks/flow-engine-BxwjEy37.js");
10
+ require("./chunks/request-collection-gkOYlWml.js");
11
+ require("uuid");
12
+ require("undici");
13
+ require("fs/promises");
14
+ require("tls");
15
+ require("./chunks/handle-Crv1i9hb.js");
16
+ require("node:crypto");
17
+ require("node:fs/promises");
18
+ require("dayjs");
19
+ require("node:vm");
20
+ require("tv4");
21
+ require("jsonpath-plus");
22
+ require("@xmldom/xmldom");
23
+ require("mqtt");
24
+ require("ws");
25
+ require("socket.io-client");
26
+ require("node:http");
27
+ require("ajv");
28
+ const port = process.parentPort;
29
+ port.on("message", (e) => {
30
+ const msg = e.data;
31
+ if (msg.type === "stop") {
32
+ perfRun.stopPerfRun();
33
+ return;
34
+ }
35
+ if (msg.type !== "run" || !msg.payload) return;
36
+ void (async () => {
37
+ try {
38
+ if (msg.payload.secretsConfig) requestExec.setSecretsConfig(msg.payload.secretsConfig);
39
+ const result = await perfRun.runPerfPlan(msg.payload, (p) => port.postMessage({ type: "progress", p }));
40
+ port.postMessage({ type: "result", result });
41
+ } catch (err) {
42
+ port.postMessage({ type: "error", message: err instanceof Error ? err.message : String(err) });
43
+ }
44
+ })();
45
+ });
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ const promises = require("node:fs/promises");
4
+ const path = require("node:path");
5
+ const requestExec = require("./chunks/request-exec-CgyqbNWk.js");
6
+ const environments = require("./chunks/environments-iM3SUM-4.js");
7
+ const flowEngine = require("./chunks/flow-engine-BxwjEy37.js");
8
+ const perfRun = require("./chunks/perf-run-Co_5unBR.js");
9
+ const perfReport = require("./chunks/perf-report-ChaO3K0w.js");
10
+ const cliCommon = require("./chunks/cli-common-GnihVSjU.js");
11
+ require("undici");
12
+ require("fs/promises");
13
+ require("tls");
14
+ require("./chunks/handle-Crv1i9hb.js");
15
+ require("node:fs");
16
+ require("node:os");
17
+ require("node:crypto");
18
+ require("dayjs");
19
+ require("node:vm");
20
+ require("tv4");
21
+ require("jsonpath-plus");
22
+ require("@xmldom/xmldom");
23
+ require("mqtt");
24
+ require("ws");
25
+ require("socket.io-client");
26
+ require("node:http");
27
+ require("ajv");
28
+ require("@grpc/grpc-js");
29
+ require("@grpc/proto-loader");
30
+ require("./chunks/request-collection-gkOYlWml.js");
31
+ require("uuid");
32
+ require("./chunks/data-files-BYqhsoPe.js");
33
+ function parseDurationSec(s) {
34
+ if (s == null) return void 0;
35
+ const str = String(s).trim();
36
+ if (/^\d+(\.\d+)?$/.test(str)) return Number(str);
37
+ let total = 0;
38
+ let matched = false;
39
+ for (const m of str.matchAll(/(\d+)\s*(h|m|s)/gi)) {
40
+ matched = true;
41
+ total += Number(m[1]) * (m[2].toLowerCase() === "h" ? 3600 : m[2].toLowerCase() === "m" ? 60 : 1);
42
+ }
43
+ return matched ? total : void 0;
44
+ }
45
+ function parseThreshold(spec) {
46
+ const m = spec.trim().match(/^(\S+)\s*(lt|lte|gt|gte|<=|>=|<|>)\s*([\d.]+)$/i);
47
+ if (!m) return null;
48
+ const opMap = { "<": "lt", "<=": "lte", ">": "gt", ">=": "gte", lt: "lt", lte: "lte", gt: "gt", gte: "gte" };
49
+ const metric = m[1];
50
+ const op = opMap[m[2].toLowerCase()];
51
+ const valid = ["iteration_p95", "iteration_p99", "req_p95", "error_rate", "throughput"];
52
+ if (!valid.includes(metric) || !op) return null;
53
+ return { metric, op, value: Number(m[3]) };
54
+ }
55
+ function fmtMs(v) {
56
+ return v < 1 ? "<1 ms" : v < 1e3 ? `${Math.round(v)} ms` : `${(v / 1e3).toFixed(2)} s`;
57
+ }
58
+ function applyOverrides(plan, args) {
59
+ const p = JSON.parse(JSON.stringify(plan));
60
+ const num = (k) => {
61
+ const v = args[k];
62
+ return typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v)) ? Number(v) : void 0;
63
+ };
64
+ const dur = (k) => typeof args[k] === "string" ? parseDurationSec(args[k]) : void 0;
65
+ if (typeof args.pattern === "string" && ["basic", "stepping", "arrivals", "custom"].includes(args.pattern)) p.pattern = args.pattern;
66
+ if (num("vus") !== void 0) p.basic.vus = num("vus");
67
+ if (dur("duration") !== void 0) {
68
+ p.basic.durationSec = dur("duration");
69
+ p.arrivals.durationSec = dur("duration");
70
+ }
71
+ if (dur("ramp-up") !== void 0) p.basic.rampUpSec = dur("ramp-up");
72
+ if (dur("ramp-down") !== void 0) p.basic.rampDownSec = dur("ramp-down");
73
+ if (num("rate") !== void 0) p.arrivals.rate = num("rate");
74
+ if (num("max-vus") !== void 0) p.arrivals.maxVus = num("max-vus");
75
+ if (num("start-users") !== void 0) p.stepping.startUsers = num("start-users");
76
+ if (num("step-users") !== void 0) p.stepping.stepUsers = num("step-users");
77
+ if (dur("step-duration") !== void 0) p.stepping.stepDurationSec = dur("step-duration");
78
+ if (num("max-users") !== void 0) p.stepping.maxUsers = num("max-users");
79
+ if (dur("timeout") !== void 0) p.timeoutSec = dur("timeout");
80
+ if (typeof args.think === "string") {
81
+ const t = args.think.trim();
82
+ const range = t.match(/^([\d.]+)\s*-\s*([\d.]+)$/);
83
+ if (range) p.thinkTime = { minSec: Number(range[1]), maxSec: Number(range[2]) };
84
+ else if (Number.isFinite(Number(t))) p.thinkTime = { minSec: Number(t), maxSec: Number(t) };
85
+ }
86
+ const thr = args.threshold;
87
+ if (thr !== void 0) {
88
+ const specs = Array.isArray(thr) ? thr : [thr];
89
+ const parsed = specs.map(parseThreshold).filter((x) => !!x);
90
+ p.thresholds = parsed;
91
+ }
92
+ return p;
93
+ }
94
+ async function main() {
95
+ const argv = process.argv.slice(2);
96
+ const positionals = [];
97
+ for (const a of argv) {
98
+ if (a.startsWith("--")) break;
99
+ positionals.push(a);
100
+ }
101
+ const sub = positionals[0];
102
+ const name = positionals[1];
103
+ const args = cliCommon.parseArgs(argv, ["threshold"]);
104
+ if (args.help || !sub || sub !== "run" && sub !== "list") {
105
+ console.log('\nUsage:\n api-spector perf list --workspace <path>\n api-spector perf run <plan> --workspace <path> [--vus N] [--duration 5m] [--rate N] [--threshold "iteration_p95 lt 500"] [--out report.html]\n');
106
+ process.exit(args.help ? 0 : 1);
107
+ }
108
+ const wsPath = args.workspace;
109
+ if (!wsPath) {
110
+ console.error(cliCommon.color("Error: --workspace is required", cliCommon.C.red));
111
+ process.exit(1);
112
+ }
113
+ let workspace, wsDir;
114
+ try {
115
+ ({ workspace, dir: wsDir } = await cliCommon.loadWorkspace(wsPath));
116
+ } catch {
117
+ console.error(cliCommon.color(`Error: could not read workspace: ${wsPath}`, cliCommon.C.red));
118
+ process.exit(1);
119
+ }
120
+ await requestExec.loadGlobals(wsDir);
121
+ requestExec.setSecretsConfig(workspace.settings?.secrets);
122
+ const plans = workspace.perfPlans ?? [];
123
+ if (sub === "list") {
124
+ if (plans.length === 0) {
125
+ console.log(cliCommon.color(" No performance plans in this workspace.", cliCommon.C.gray));
126
+ process.exit(0);
127
+ }
128
+ console.log("");
129
+ for (const pl of plans) {
130
+ const load2 = perfRun.deriveLoad(pl);
131
+ const tgt = pl.target.kind === "request" ? "request" : pl.target.kind === "sequence" ? "sequence" : "flow";
132
+ console.log(` ${cliCommon.color(pl.name, cliCommon.C.bold, cliCommon.C.white)} ${cliCommon.color(`(${tgt}, ${pl.pattern}, ~${load2.totalSec}s)`, cliCommon.C.gray)}`);
133
+ }
134
+ console.log("");
135
+ process.exit(0);
136
+ }
137
+ if (!name) {
138
+ console.error(cliCommon.color("Error: a plan name is required: api-spector perf run <plan>", cliCommon.C.red));
139
+ process.exit(1);
140
+ }
141
+ const basePlan = plans.find((pl) => pl.name.toLowerCase() === name.toLowerCase());
142
+ if (!basePlan) {
143
+ console.error(cliCommon.color(`Error: plan "${name}" not found. Run \`api-spector perf list\` to see plans.`, cliCommon.C.red));
144
+ process.exit(1);
145
+ }
146
+ const plan = applyOverrides(basePlan, args);
147
+ const collections = await cliCommon.loadCollections(workspace, wsDir, { onError: (relPath) => console.error(cliCommon.color(` [warn] Could not load collection: ${relPath}`, cliCommon.C.yellow)) });
148
+ const environments$1 = await cliCommon.loadEnvironments(workspace, wsDir);
149
+ const env = environments.selectEnvironment(workspace, environments$1, args.environment ?? args.env);
150
+ const flowsAll = (await cliCommon.loadFlows(workspace, wsDir, () => {
151
+ })).map(flowEngine.normalizeFlow);
152
+ const globals = requestExec.getGlobals();
153
+ const payload = { plan, environment: env ?? null, globals, proxy: workspace.settings?.proxy, tls: workspace.settings?.tls };
154
+ let targetLabel = "";
155
+ if (plan.target.kind === "request") {
156
+ let found;
157
+ let foundCol;
158
+ for (const c of collections) {
159
+ const r = c.requests[plan.target.requestId];
160
+ if (r) {
161
+ found = r;
162
+ foundCol = c;
163
+ break;
164
+ }
165
+ }
166
+ if (!found) {
167
+ console.error(cliCommon.color("Error: the plan's request was not found in this workspace.", cliCommon.C.red));
168
+ process.exit(1);
169
+ }
170
+ const envVars = await requestExec.buildEnvVars(env);
171
+ payload.request = found;
172
+ payload.vars = { ...globals, ...foundCol?.collectionVariables ?? {}, ...envVars };
173
+ targetLabel = `${found.method} ${found.name}`;
174
+ } else if (plan.target.kind === "sequence") {
175
+ const seqCol = collections.find((c) => c.id === plan.target.collectionId);
176
+ if (!seqCol) {
177
+ console.error(cliCommon.color("Error: the plan's collection was not found in this workspace.", cliCommon.C.red));
178
+ process.exit(1);
179
+ }
180
+ payload.collection = seqCol;
181
+ targetLabel = `sequence: ${seqCol.name}${plan.target.folderId ? " / folder" : ""}`;
182
+ } else {
183
+ const flow = flowsAll.find((f) => f.id === plan.target.flowId);
184
+ if (!flow) {
185
+ console.error(cliCommon.color("Error: the plan's flow was not found in this workspace.", cliCommon.C.red));
186
+ process.exit(1);
187
+ }
188
+ payload.flow = flow;
189
+ payload.flows = flowsAll;
190
+ payload.collections = collections;
191
+ targetLabel = `flow: ${flow.name}`;
192
+ }
193
+ const load = perfRun.deriveLoad(plan);
194
+ const version = `v${"0.7.9"}`;
195
+ console.log("");
196
+ console.log(cliCommon.color(" API Performance" + (version ? ` ${version}` : ""), cliCommon.C.bold, cliCommon.C.white));
197
+ console.log(cliCommon.color(` Plan: ${plan.name}`, cliCommon.C.gray));
198
+ console.log(cliCommon.color(` Target: ${targetLabel}`, cliCommon.C.gray));
199
+ console.log(cliCommon.color(` Pattern: ${plan.pattern} (${load.executor}, ~${load.totalSec}s)`, cliCommon.C.gray));
200
+ console.log(cliCommon.color(` Environment: ${env?.name ?? "(none)"}`, cliCommon.C.gray));
201
+ if (plan.thresholds.length) console.log(cliCommon.color(` Thresholds: ${plan.thresholds.map((t) => `${t.metric} ${t.op} ${t.value}`).join(", ")}`, cliCommon.C.gray));
202
+ console.log("");
203
+ const quiet = Boolean(args.quiet);
204
+ let lastPrint = 0;
205
+ const result = await perfRun.runPerfPlan(payload, (p) => {
206
+ if (quiet) return;
207
+ const now = Date.now();
208
+ if (now - lastPrint < 1e3) return;
209
+ lastPrint = now;
210
+ process.stdout.write(`\r ${cliCommon.color("running", cliCommon.C.cyan)} ${Math.round(p.elapsedMs / 1e3)}s iters ${p.iterations} active ${p.activeVus} ${p.iterationsPerSec.toFixed(1)}/s p95 ${fmtMs(p.p95)} failed ${p.iterationsFailed} `);
211
+ }).catch((err) => {
212
+ console.error("\n" + cliCommon.color(`Error: ${err instanceof Error ? err.message : String(err)}`, cliCommon.C.red));
213
+ process.exit(1);
214
+ });
215
+ if (!quiet) process.stdout.write("\n\n");
216
+ const errorRate = result.iterations > 0 ? result.iterationsFailed / result.iterations * 100 : 0;
217
+ console.log(cliCommon.color(" Results", cliCommon.C.bold, cliCommon.C.white));
218
+ console.log(` Throughput ${cliCommon.color(result.iterationsPerSec.toFixed(1) + " iter/s", cliCommon.C.cyan)} (${result.requestsPerSec.toFixed(1)} req/s)`);
219
+ console.log(` Iteration p95 ${fmtMs(result.iterationLatency.p95)} p99 ${fmtMs(result.iterationLatency.p99)}`);
220
+ console.log(` Error rate ${errorRate.toFixed(1)}% (${result.iterationsFailed}/${result.iterations})`);
221
+ console.log(` Peak users ${result.peakVus}${result.droppedArrivals ? ` dropped ${result.droppedArrivals}` : ""}`);
222
+ console.log(` Duration ${fmtMs(result.durationMs)}`);
223
+ console.log("");
224
+ console.log(cliCommon.color(" Steps", cliCommon.C.bold, cliCommon.C.white));
225
+ for (const [i, s] of result.steps.entries()) {
226
+ const label = result.mode === "request" ? s.name : `${i + 1}. ${s.name}`;
227
+ console.log(` ${label.padEnd(36).slice(0, 36)} p95 ${fmtMs(s.latency.p95).padStart(8)} ${cliCommon.color(s.failed ? `${s.failed} failed` : "ok", s.failed ? cliCommon.C.red : cliCommon.C.green)}`);
228
+ }
229
+ console.log("");
230
+ if (result.thresholds.length) {
231
+ console.log(cliCommon.color(" Thresholds", cliCommon.C.bold, cliCommon.C.white));
232
+ for (const t of result.thresholds) {
233
+ const actual = t.metric === "error_rate" ? `${t.actual.toFixed(1)}%` : t.metric === "throughput" ? t.actual.toFixed(1) : fmtMs(t.actual);
234
+ console.log(` ${t.passed ? cliCommon.color("✓", cliCommon.C.green) : cliCommon.color("✗", cliCommon.C.red)} ${t.metric} ${t.op} ${t.value} (actual ${actual})`);
235
+ }
236
+ console.log("");
237
+ console.log(result.passed ? cliCommon.color(" PASSED", cliCommon.C.green, cliCommon.C.bold) : cliCommon.color(" FAILED", cliCommon.C.red, cliCommon.C.bold));
238
+ console.log("");
239
+ }
240
+ const out = args.out;
241
+ if (out) {
242
+ const content = path.extname(out).toLowerCase() === ".json" ? JSON.stringify(result, null, 2) : perfReport.perfReportToHtml(result, { planName: plan.name, target: targetLabel, generatedAt: (/* @__PURE__ */ new Date()).toISOString() });
243
+ await promises.writeFile(out, content, "utf8");
244
+ console.log(cliCommon.color(` Report written to ${out}`, cliCommon.C.gray));
245
+ console.log("");
246
+ }
247
+ if (args.json) console.log(JSON.stringify(result, null, 2));
248
+ process.exit(result.passed ? 0 : 1);
249
+ }
250
+ main().catch((err) => {
251
+ console.error(err);
252
+ process.exit(1);
253
+ });
@@ -2,15 +2,16 @@
2
2
  "use strict";
3
3
  const promises = require("node:fs/promises");
4
4
  const path = require("node:path");
5
- const requestExec = require("./chunks/request-exec-B-F0K-pN.js");
5
+ const requestExec = require("./chunks/request-exec-CgyqbNWk.js");
6
6
  const escape = require("./chunks/escape-DDVAlOvZ.js");
7
- const requestCollection = require("./chunks/request-collection-KGPuvN7o.js");
7
+ const requestCollection = require("./chunks/request-collection-gkOYlWml.js");
8
+ const runConcurrent = require("./chunks/run-concurrent-468tH0AX.js");
8
9
  const environments = require("./chunks/environments-iM3SUM-4.js");
9
10
  const cliCommon = require("./chunks/cli-common-GnihVSjU.js");
10
11
  require("undici");
11
12
  require("fs/promises");
12
13
  require("tls");
13
- require("./chunks/handle-Bz0C56Eq.js");
14
+ require("./chunks/handle-Crv1i9hb.js");
14
15
  require("node:fs");
15
16
  require("node:os");
16
17
  require("node:crypto");
@@ -19,6 +20,9 @@ require("node:vm");
19
20
  require("tv4");
20
21
  require("jsonpath-plus");
21
22
  require("@xmldom/xmldom");
23
+ require("mqtt");
24
+ require("ws");
25
+ require("socket.io-client");
22
26
  require("node:http");
23
27
  require("ajv");
24
28
  require("uuid");
@@ -437,6 +441,58 @@ function buildSarifReport(results, _summary, meta = {}) {
437
441
  }]
438
442
  }, null, 2);
439
443
  }
444
+ const ASK_RE = /\{\{\s*@([^}]+?)\s*\}\}/g;
445
+ function parseInner(inner) {
446
+ const m = /^(.*?)\[([^\]]*)\]\s*$/.exec(inner);
447
+ if (m) {
448
+ const choices = m[2].split(",").map((s) => s.trim()).filter(Boolean);
449
+ return { message: m[1].trim(), choices: choices.length ? choices : void 0 };
450
+ }
451
+ return { message: inner.trim() };
452
+ }
453
+ function extractAskVars(texts) {
454
+ const seen = /* @__PURE__ */ new Map();
455
+ for (const text of texts) {
456
+ if (!text) continue;
457
+ for (const m of text.matchAll(ASK_RE)) {
458
+ const inner = m[1].trim();
459
+ const key = `@${inner}`;
460
+ if (seen.has(key)) continue;
461
+ const { message, choices } = parseInner(inner);
462
+ if (!message) continue;
463
+ seen.set(key, { key, message, choices });
464
+ }
465
+ }
466
+ return [...seen.values()];
467
+ }
468
+ function askVarsInRequest(req) {
469
+ const texts = [req.url];
470
+ for (const p of req.params ?? []) texts.push(p.key, p.value);
471
+ for (const h of req.headers ?? []) texts.push(h.key, h.value);
472
+ const b = req.body;
473
+ if (b) {
474
+ texts.push(b.json, b.raw);
475
+ if (b.graphql) texts.push(b.graphql.query, b.graphql.variables);
476
+ for (const f of b.form ?? []) texts.push(f.key, f.value);
477
+ if (b.soap) texts.push(b.soap.envelope);
478
+ if (b.grpc) texts.push(b.grpc.message);
479
+ }
480
+ try {
481
+ texts.push(JSON.stringify(req.auth ?? {}));
482
+ } catch {
483
+ }
484
+ return extractAskVars(texts);
485
+ }
486
+ function resolveAskValues(asks, answers) {
487
+ const out = {};
488
+ for (const a of asks) out[a.key] = answers[a.message] ?? "";
489
+ return out;
490
+ }
491
+ function missingAskMessages(asks, answers) {
492
+ const missing = [];
493
+ for (const a of asks) if (!(a.message in answers) && !missing.includes(a.message)) missing.push(a.message);
494
+ return missing;
495
+ }
440
496
  function printResult(r, verbose) {
441
497
  const icon = r.status === "passed" ? cliCommon.color("✓", cliCommon.C.green, cliCommon.C.bold) : r.status === "failed" ? cliCommon.color("✗", cliCommon.C.red, cliCommon.C.bold) : r.status === "skipped" ? cliCommon.color("○", cliCommon.C.gray, cliCommon.C.bold) : cliCommon.color("⚠", cliCommon.C.yellow, cliCommon.C.bold);
442
498
  const http = r.httpStatus ? cliCommon.color(` ${r.httpStatus}`, r.httpStatus < 400 ? cliCommon.C.green : cliCommon.C.red) : "";
@@ -457,7 +513,7 @@ async function main() {
457
513
  const args = cliCommon.parseArgs(process.argv.slice(2));
458
514
  if (args.help) {
459
515
  console.log(
460
- "\nUsage:\n api-spector run --workspace <path> [--environment <name>] [--tags <a,b>]\n [--collection <name>] [--output <path>] [--format json|junit|html|sarif]\n [--verbose] [--bail]\n"
516
+ '\nUsage:\n api-spector run --workspace <path> [--environment <name>] [--tags <a,b>]\n [--collection <name>] [--output <path>] [--format json|junit|html|sarif]\n [--ask "Message=value"] [--concurrency <n>] [--verbose] [--bail]\n\n --ask "Message=value" Supply an ask variable ({{@Message}}) for this run\n (repeatable). Requests whose ask variables are not\n supplied are skipped.\n --concurrency <n> Run n requests at a time (default 1 = sequential).\n Hooks run as barriers; best for independent requests.\n'
461
517
  );
462
518
  process.exit(0);
463
519
  }
@@ -495,13 +551,21 @@ async function main() {
495
551
  } else if (!envName && workspace.settings?.defaultEnvironment && !env) {
496
552
  console.warn(cliCommon.color(`Warning: default environment "${workspace.settings.defaultEnvironment}" not found. Running without environment.`, cliCommon.C.yellow));
497
553
  }
498
- const version = `v${"0.7.7"}`;
554
+ const version = `v${"0.7.9"}`;
499
555
  console.log("");
500
556
  console.log(cliCommon.color(" API Test Runner" + (version ? ` ${version}` : ""), cliCommon.C.bold, cliCommon.C.white));
501
557
  console.log(cliCommon.color(` Workspace: ${wsPath}`, cliCommon.C.gray));
502
558
  console.log(cliCommon.color(` Environment: ${env?.name ?? "(none)"}`, cliCommon.C.gray));
503
559
  if (filterTags.length) console.log(cliCommon.color(` Tags: ${filterTags.join(", ")}`, cliCommon.C.gray));
504
560
  console.log("");
561
+ const askAnswers = {};
562
+ const askArgRaw = args["ask"];
563
+ for (const entry of Array.isArray(askArgRaw) ? askArgRaw : askArgRaw ? [askArgRaw] : []) {
564
+ if (typeof entry !== "string") continue;
565
+ const eq = entry.indexOf("=");
566
+ if (eq > 0) askAnswers[entry.slice(0, eq).trim()] = entry.slice(eq + 1);
567
+ }
568
+ const concurrency = Math.max(1, Math.min(20, Math.floor(Number(args["concurrency"] ?? 1) || 1)));
505
569
  const envVarsSnapshot = await requestExec.buildEnvVars(env);
506
570
  const secretValuesToMask = (env?.variables ?? []).filter((v) => (v.secret || v.secretRef) && v.enabled).map((v) => envVarsSnapshot[v.key]).filter((v) => typeof v === "string" && v.length > 0);
507
571
  function redact(s) {
@@ -556,6 +620,7 @@ async function main() {
556
620
  let runGlobals = requestExec.getGlobals();
557
621
  let runCollectionVars = { ...col.collectionVariables };
558
622
  let runLocalVars = {};
623
+ const runFolderVars = {};
559
624
  console.log(cliCommon.color(` ┌ ${col.name}`, cliCommon.C.bold, cliCommon.C.white));
560
625
  const workspaceTls = workspace.settings?.tls;
561
626
  const effectiveTls = col.tls ? { ...workspaceTls, ...col.tls } : workspaceTls;
@@ -580,9 +645,11 @@ async function main() {
580
645
  if (verbose && lines.length) lines.forEach((l) => console.log(cliCommon.color(` [${phase}] ${l}`, cliCommon.C.gray)));
581
646
  if (phase === "pre" && error) console.error(cliCommon.color(` [pre-script error] ${error}`, cliCommon.C.red));
582
647
  };
583
- for (const item of items) {
648
+ if (concurrency <= 1) for (const item of items) {
584
649
  const { isHook, hookType, scopeId } = item;
585
650
  const skipReason = skipTracker.shouldSkip(item);
651
+ const itemAsks = askVarsInRequest(item.request);
652
+ const missingAsks = itemAsks.length ? missingAskMessages(itemAsks, askAnswers) : [];
586
653
  let result;
587
654
  if (skipReason) {
588
655
  result = {
@@ -599,13 +666,32 @@ async function main() {
599
666
  scopePath: item.scopePath,
600
667
  iterationLabel: item.iterationLabel
601
668
  };
669
+ } else if (missingAsks.length) {
670
+ result = {
671
+ requestId: item.request.id,
672
+ name: item.request.name,
673
+ method: item.request.method,
674
+ resolvedUrl: item.request.url,
675
+ status: "skipped",
676
+ error: `Ask variable(s) not provided: ${missingAsks.join(", ")} (use --ask "${missingAsks[0]}=value")`,
677
+ isHook,
678
+ hookType,
679
+ scopeId,
680
+ collection: item.collection,
681
+ scopePath: item.scopePath,
682
+ iterationLabel: item.iterationLabel
683
+ };
602
684
  } else {
603
685
  const out = await requestExec.executeRunnerRequest({
604
686
  req: item.request,
605
- collectionVars: { ...item.collectionVars, ...runCollectionVars },
687
+ collectionVars: { ...item.collectionVars, ...runCollectionVars, ...resolveAskValues(itemAsks, askAnswers) },
606
688
  envVars: runEnvVars,
607
689
  globals: runGlobals,
608
690
  localVars: { ...runLocalVars, ...item.dataRow },
691
+ folderVars: {
692
+ ...item.folderVars ?? {},
693
+ ...item.folderId ? runFolderVars[item.folderId] ?? {} : {}
694
+ },
609
695
  dispatcher,
610
696
  piiMaskPatterns: piiPatterns,
611
697
  tls: effectiveTls,
@@ -618,6 +704,9 @@ async function main() {
618
704
  runCollectionVars = out.updatedCollectionVars;
619
705
  runGlobals = out.updatedGlobals;
620
706
  runLocalVars = out.updatedLocalVars;
707
+ if (item.folderId) {
708
+ runFolderVars[item.folderId] = { ...runFolderVars[item.folderId] ?? {}, ...out.updatedFolderVars };
709
+ }
621
710
  result.isHook = isHook;
622
711
  result.hookType = hookType;
623
712
  result.scopeId = scopeId;
@@ -644,6 +733,91 @@ async function main() {
644
733
  break;
645
734
  }
646
735
  }
736
+ else {
737
+ const resultsByIndex = new Array(items.length);
738
+ const preResult = (item, status, error) => ({
739
+ requestId: item.request.id,
740
+ name: item.request.name,
741
+ method: item.request.method,
742
+ resolvedUrl: item.request.url,
743
+ status,
744
+ error,
745
+ isHook: item.isHook,
746
+ hookType: item.hookType,
747
+ scopeId: item.scopeId,
748
+ collection: item.collection,
749
+ scopePath: item.scopePath,
750
+ iterationLabel: item.iterationLabel
751
+ });
752
+ const runItem = async (item, i) => {
753
+ const skipReason = skipTracker.shouldSkip(item);
754
+ const itemAsks = askVarsInRequest(item.request);
755
+ const missingAsks = itemAsks.length ? missingAskMessages(itemAsks, askAnswers) : [];
756
+ if (skipReason) {
757
+ resultsByIndex[i] = preResult(item, "failed", skipReason);
758
+ return;
759
+ }
760
+ if (missingAsks.length) {
761
+ resultsByIndex[i] = preResult(item, "skipped", `Ask variable(s) not provided: ${missingAsks.join(", ")}`);
762
+ return;
763
+ }
764
+ try {
765
+ const out = await requestExec.executeRunnerRequest({
766
+ req: item.request,
767
+ collectionVars: { ...item.collectionVars, ...runCollectionVars, ...resolveAskValues(itemAsks, askAnswers) },
768
+ envVars: runEnvVars,
769
+ globals: runGlobals,
770
+ localVars: { ...runLocalVars, ...item.dataRow },
771
+ folderVars: { ...item.folderVars ?? {}, ...item.folderId ? runFolderVars[item.folderId] ?? {} : {} },
772
+ dispatcher,
773
+ piiMaskPatterns: piiPatterns,
774
+ tls: effectiveTls,
775
+ expectStatus: item.expectStatus,
776
+ owaspTag: item.owaspTag
777
+ });
778
+ runEnvVars = { ...runEnvVars, ...out.updatedEnvVars };
779
+ runCollectionVars = { ...runCollectionVars, ...out.updatedCollectionVars };
780
+ runGlobals = { ...runGlobals, ...out.updatedGlobals };
781
+ runLocalVars = { ...runLocalVars, ...out.updatedLocalVars };
782
+ if (item.folderId) runFolderVars[item.folderId] = { ...runFolderVars[item.folderId] ?? {}, ...out.updatedFolderVars };
783
+ const result = out.result;
784
+ result.isHook = item.isHook;
785
+ result.hookType = item.hookType;
786
+ result.scopeId = item.scopeId;
787
+ result.collection = item.collection;
788
+ result.scopePath = item.scopePath;
789
+ result.iterationLabel = item.iterationLabel;
790
+ skipTracker.recordResult(item, result.status);
791
+ resultsByIndex[i] = result;
792
+ } catch (e) {
793
+ resultsByIndex[i] = preResult(item, "error", e instanceof Error ? e.message : String(e));
794
+ skipTracker.recordResult(item, "error");
795
+ }
796
+ };
797
+ await runConcurrent.runPlanConcurrent(concurrency, items, (it) => Boolean(it.isHook), runItem);
798
+ for (let i = 0; i < items.length; i++) {
799
+ const result = resultsByIndex[i];
800
+ if (!result) continue;
801
+ const item = items[i];
802
+ const scopeKey = (item.scopePath ?? []).join(" / ");
803
+ if (scopeKey !== lastPrintedScope) {
804
+ if (scopeKey) console.log(cliCommon.color(` ${scopeKey}`, cliCommon.C.gray, cliCommon.C.bold));
805
+ lastPrintedScope = scopeKey;
806
+ }
807
+ printResult(result, verbose);
808
+ allResults.push(result);
809
+ summary.total++;
810
+ if (result.status === "passed") summary.passed++;
811
+ else if (result.status === "failed") summary.failed++;
812
+ else if (result.status === "skipped") summary.skipped++;
813
+ else summary.errors++;
814
+ if (bail && (result.status === "failed" || result.status === "error")) {
815
+ console.log(cliCommon.color("\n Bailing after first failure.", cliCommon.C.yellow));
816
+ bailed = true;
817
+ break;
818
+ }
819
+ }
820
+ }
647
821
  console.log("");
648
822
  if (bailed) break;
649
823
  }