@testsmith/api-spector 0.2.3 → 0.2.5

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/cli.js CHANGED
@@ -6,6 +6,23 @@ const { spawn } = require('child_process')
6
6
 
7
7
  const [, , cmd = 'ui', ...rest] = process.argv
8
8
 
9
+ // ─── Command dispatch ────────────────────────────────────────────────────────
10
+ //
11
+ // Each entry maps a top-level command to the bundled JS file that handles it.
12
+ // `entrypoint: null` is reserved for `ui`, which spawns electron itself rather
13
+ // than a node script. Adding a new CLI surface is one line here + one entry in
14
+ // electron.vite.config.ts.
15
+
16
+ const COMMANDS = {
17
+ ui: { entrypoint: null, runner: 'electron' },
18
+ run: { entrypoint: 'runner.js', runner: 'node' },
19
+ mock: { entrypoint: 'mock.js', runner: 'node' },
20
+ record: { entrypoint: 'record.js', runner: 'node' },
21
+ agents: { entrypoint: 'agents.js', runner: 'node' },
22
+ contract: { entrypoint: 'contract.js',runner: 'node' },
23
+ wsdl: { entrypoint: 'wsdl.js', runner: 'node' },
24
+ }
25
+
9
26
  function printHelp() {
10
27
  console.log('')
11
28
  console.log(' API Spector — local-first API testing tool')
@@ -16,6 +33,7 @@ function printHelp() {
16
33
  console.log(' api-spector mock --workspace <path> Start mock servers from CLI')
17
34
  console.log(' api-spector record --upstream <url> Record API traffic as mock stubs')
18
35
  console.log(' api-spector contract list|run Manage & run pinned contract snapshots')
36
+ console.log(' api-spector wsdl describe|import-* Inspect a WSDL or import as collection/mock')
19
37
  console.log('')
20
38
  console.log(' Options:')
21
39
  console.log(' api-spector agents init <name> Initialize AI agent files')
@@ -34,51 +52,34 @@ function printHelp() {
34
52
  if (cmd === '--help' || cmd === '-h') {
35
53
  printHelp()
36
54
  process.exit(0)
37
- } else if (cmd === 'ui') {
55
+ }
56
+
57
+ const command = COMMANDS[cmd]
58
+ if (!command) {
59
+ console.error(`API Spector — unknown command: "${cmd}"`)
60
+ printHelp()
61
+ process.exit(1)
62
+ }
63
+
64
+ // ui: spawn electron with the app dir
65
+ if (command.runner === 'electron') {
38
66
  const electron = require('electron')
39
67
  const appDir = path.join(__dirname, '..')
68
+ // Forward the user's cwd so the main process can decide whether to open a
69
+ // workspace in this folder, or fall through to the welcome screen. Without
70
+ // this, the app would always auto-load the previously-opened workspace
71
+ // even when launched from an empty/different directory.
40
72
  const proc = spawn(String(electron), [appDir, ...rest], {
41
73
  stdio: 'inherit',
42
- env: process.env,
43
- })
44
- proc.on('close', code => process.exit(code ?? 0))
45
- } else if (cmd === 'run') {
46
- const runnerPath = path.join(__dirname, '..', 'out', 'main', 'runner.js')
47
- const proc = spawn(process.execPath, [runnerPath, ...rest], {
48
- stdio: 'inherit',
49
- env: process.env,
50
- })
51
- proc.on('close', code => process.exit(code ?? 0))
52
- } else if (cmd === 'mock') {
53
- const mockPath = path.join(__dirname, '..', 'out', 'main', 'mock.js')
54
- const proc = spawn(process.execPath, [mockPath, ...rest], {
55
- stdio: 'inherit',
56
- env: process.env,
57
- })
58
- proc.on('close', code => process.exit(code ?? 0))
59
- } else if (cmd === 'record') {
60
- const recordPath = path.join(__dirname, '..', 'out', 'main', 'record.js')
61
- const proc = spawn(process.execPath, [recordPath, ...rest], {
62
- stdio: 'inherit',
63
- env: process.env,
64
- })
65
- proc.on('close', code => process.exit(code ?? 0))
66
- } else if (cmd === 'agents') {
67
- const agentsPath = path.join(__dirname, '..', 'out', 'main', 'agents.js')
68
- const proc = spawn(process.execPath, [agentsPath, ...rest], {
69
- stdio: 'inherit',
70
- env: process.env,
74
+ env: { ...process.env, API_SPECTOR_LAUNCH_CWD: process.cwd() },
71
75
  })
72
76
  proc.on('close', code => process.exit(code ?? 0))
73
- } else if (cmd === 'contract') {
74
- const contractPath = path.join(__dirname, '..', 'out', 'main', 'contract.js')
75
- const proc = spawn(process.execPath, [contractPath, ...rest], {
77
+ } else {
78
+ // node-runnable bundles in out/main/<entrypoint>
79
+ const target = path.join(__dirname, '..', 'out', 'main', command.entrypoint)
80
+ const proc = spawn(process.execPath, [target, ...rest], {
76
81
  stdio: 'inherit',
77
82
  env: process.env,
78
83
  })
79
84
  proc.on('close', code => process.exit(code ?? 0))
80
- } else {
81
- console.error(`API Spector — unknown command: "${cmd}"`)
82
- printHelp()
83
- process.exit(1)
84
85
  }
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const uuid = require("uuid");
4
+ const soapHandler = require("./soap-handler-Cpj-JwyA.js");
5
+ require("https");
6
+ require("http");
7
+ require("@xmldom/xmldom");
8
+ const SOAP_11_CONTENT_TYPE = "text/xml; charset=utf-8";
9
+ const SOAP_12_CONTENT_TYPE = "application/soap+xml; charset=utf-8";
10
+ function contentTypeForSoap(version) {
11
+ return version === "1.2" ? SOAP_12_CONTENT_TYPE : SOAP_11_CONTENT_TYPE;
12
+ }
13
+ function withContentType(headers, value) {
14
+ const idx = headers.findIndex((h) => h.key.toLowerCase() === "content-type");
15
+ const next = { key: "Content-Type", value, enabled: true };
16
+ if (idx === -1) return [...headers, next];
17
+ return headers.map((h, i) => i === idx ? { ...h, value, enabled: true } : h);
18
+ }
19
+ function buildRequestsFromWsdl(parsed) {
20
+ const out = [];
21
+ const byName = /* @__PURE__ */ new Map();
22
+ for (const op of parsed.operations) {
23
+ const existing = byName.get(op.name);
24
+ if (!existing || existing.soapVersion === "1.2" && op.soapVersion === "1.1") byName.set(op.name, op);
25
+ }
26
+ for (const op of byName.values()) {
27
+ const ct = contentTypeForSoap(op.soapVersion);
28
+ const req = {
29
+ id: uuid.v4(),
30
+ name: op.name,
31
+ method: "POST",
32
+ url: op.endpoint ?? "",
33
+ headers: withContentType([], ct),
34
+ params: [],
35
+ auth: { type: "none" },
36
+ protocol: "soap",
37
+ body: {
38
+ mode: "soap",
39
+ soap: {
40
+ wsdlUrl: "",
41
+ operationName: op.name,
42
+ soapAction: op.soapAction ?? "",
43
+ envelope: op.inputTemplate
44
+ }
45
+ },
46
+ description: op.soapAction ? `SOAPAction: ${op.soapAction}` : void 0
47
+ };
48
+ out.push(req);
49
+ }
50
+ return out;
51
+ }
52
+ function buildCollectionFromWsdl(name, parsed) {
53
+ const requests = buildRequestsFromWsdl(parsed);
54
+ const requestMap = {};
55
+ for (const r of requests) requestMap[r.id] = r;
56
+ const collection = {
57
+ version: "1.0",
58
+ id: uuid.v4(),
59
+ name,
60
+ description: `Imported from WSDL — ${parsed.endpoints[0]?.address ?? parsed.targetNamespace}`,
61
+ rootFolder: {
62
+ id: uuid.v4(),
63
+ name: "root",
64
+ description: "",
65
+ folders: [],
66
+ requestIds: requests.map((r) => r.id)
67
+ },
68
+ requests: requestMap
69
+ };
70
+ return { collection, requestIds: requests.map((r) => r.id) };
71
+ }
72
+ function pathFromUrl(url) {
73
+ try {
74
+ return new URL(url).pathname || "/";
75
+ } catch {
76
+ return "/";
77
+ }
78
+ }
79
+ function pickFreePort(existing) {
80
+ let p = 3900;
81
+ while (existing.includes(p)) p++;
82
+ return p;
83
+ }
84
+ function buildMockFromWsdl(name, parsed, existingPorts = []) {
85
+ const opMap = {};
86
+ const seen = /* @__PURE__ */ new Set();
87
+ for (const op of parsed.operations) {
88
+ if (seen.has(op.name)) continue;
89
+ seen.add(op.name);
90
+ opMap[op.name] = soapHandler.buildResponseEnvelope(op.name, parsed.targetNamespace, op.soapVersion);
91
+ }
92
+ const pathsByVersion = /* @__PURE__ */ new Map();
93
+ for (const ep of parsed.endpoints) {
94
+ pathsByVersion.set(pathFromUrl(ep.address), ep.soapVersion);
95
+ }
96
+ if (pathsByVersion.size === 0) pathsByVersion.set("/", "1.1");
97
+ const routes = [];
98
+ for (const [routePath, version] of pathsByVersion.entries()) {
99
+ routes.push({
100
+ id: uuid.v4(),
101
+ method: "POST",
102
+ path: routePath,
103
+ statusCode: 200,
104
+ headers: { "Content-Type": contentTypeForSoap(version) },
105
+ body: "",
106
+ description: `SOAP ${version} — dispatched per operation`,
107
+ script: soapHandler.buildMockDispatchScript(),
108
+ // Externalized so workspace JSON stays compact: the dispatch script reads
109
+ // these via the script-runner's `metadata` context binding instead of
110
+ // baking each envelope as a JS string literal.
111
+ metadata: { soapEnvelopes: opMap, soapVersion: version }
112
+ });
113
+ }
114
+ return {
115
+ version: "1.0",
116
+ id: uuid.v4(),
117
+ name,
118
+ port: pickFreePort(existingPorts),
119
+ routes
120
+ };
121
+ }
122
+ function importWsdl(wsdlText, opts = {}) {
123
+ const parsed = soapHandler.parseWsdl(wsdlText);
124
+ const baseName = opts.name?.trim() || (() => {
125
+ try {
126
+ return new URL(parsed.endpoints[0]?.address ?? "").hostname || "WSDL service";
127
+ } catch {
128
+ return "WSDL service";
129
+ }
130
+ })();
131
+ const { collection } = buildCollectionFromWsdl(baseName, parsed);
132
+ const mock = buildMockFromWsdl(`${baseName} (mock)`, parsed, opts.existingMockPorts ?? []);
133
+ return { parsed, collection, mock };
134
+ }
135
+ function defaultCollectionRelPath(ws, collection) {
136
+ const safe = collection.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "wsdl";
137
+ let i = 0;
138
+ let candidate = `collections/${safe}.json`;
139
+ const existing = new Set(ws.collections);
140
+ while (existing.has(candidate)) {
141
+ i++;
142
+ candidate = `collections/${safe}-${i}.json`;
143
+ }
144
+ return candidate;
145
+ }
146
+ function defaultMockRelPath(mock) {
147
+ return `mocks/${mock.id}.mock.json`;
148
+ }
149
+ exports.buildCollectionFromWsdl = buildCollectionFromWsdl;
150
+ exports.buildMockFromWsdl = buildMockFromWsdl;
151
+ exports.buildRequestsFromWsdl = buildRequestsFromWsdl;
152
+ exports.defaultCollectionRelPath = defaultCollectionRelPath;
153
+ exports.defaultMockRelPath = defaultMockRelPath;
154
+ exports.importWsdl = importWsdl;
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const Ajv = require("ajv");
4
+ const ajv = new Ajv({
5
+ allErrors: true,
6
+ strict: false,
7
+ coerceTypes: false,
8
+ allowUnionTypes: true
9
+ });
10
+ function compile(schema) {
11
+ const fn = ajv.compile(schema);
12
+ return function validate(data) {
13
+ if (!fn(data)) {
14
+ const err = (fn.errors ?? []).map((e) => `${e.instancePath || "<root>"} ${e.message ?? "invalid"}`).join("; ");
15
+ throw new Error(`Invalid IPC payload: ${err || "unknown"}`);
16
+ }
17
+ };
18
+ }
19
+ const apiRequestSchema = {
20
+ type: "object",
21
+ properties: {
22
+ id: { type: "string" },
23
+ name: { type: "string" },
24
+ method: { type: "string" },
25
+ url: { type: "string" },
26
+ headers: { type: "array" },
27
+ params: { type: "array" },
28
+ auth: {
29
+ type: "object",
30
+ properties: { type: { type: "string" } },
31
+ required: ["type"],
32
+ additionalProperties: true
33
+ },
34
+ body: {
35
+ type: "object",
36
+ properties: { mode: { type: "string" } },
37
+ required: ["mode"],
38
+ additionalProperties: true
39
+ }
40
+ },
41
+ required: ["id", "name", "method", "url", "headers", "params"],
42
+ additionalProperties: true
43
+ };
44
+ const stringMap = {
45
+ type: "object",
46
+ additionalProperties: { type: "string" }
47
+ };
48
+ const validateSendRequestPayload = compile({
49
+ type: "object",
50
+ properties: {
51
+ request: apiRequestSchema,
52
+ collectionVars: stringMap,
53
+ globals: stringMap,
54
+ piiMaskPatterns: { type: "array", items: { type: "string" } }
55
+ // `environment`, `proxy`, `tls` are accepted as anything — they're
56
+ // covered by the IPC handler's own type-narrow + defaults.
57
+ },
58
+ required: ["request"],
59
+ additionalProperties: true
60
+ });
61
+ const validateContractRunPayload = compile({
62
+ type: "object",
63
+ properties: {
64
+ mode: { type: "string", enum: ["consumer", "provider", "bidirectional"] },
65
+ requests: { type: "array", items: apiRequestSchema },
66
+ envVars: stringMap,
67
+ collectionVars: stringMap,
68
+ specUrl: { type: "string" },
69
+ specPath: { type: "string" },
70
+ specSnapshotRelPath: { type: "string" },
71
+ requestBaseUrl: { type: "string" }
72
+ },
73
+ required: ["mode", "requests"],
74
+ additionalProperties: true
75
+ });
76
+ const validateWsdlFetchUrl = (url) => {
77
+ if (typeof url !== "string" || !url.trim()) throw new Error("Invalid IPC payload: url must be a non-empty string");
78
+ if (!/^https?:\/\//i.test(url)) throw new Error("Invalid IPC payload: url must start with http:// or https://");
79
+ };
80
+ const validateWsdlImport = compile({
81
+ type: "object",
82
+ properties: {
83
+ url: { type: "string" },
84
+ xml: { type: "string" },
85
+ name: { type: "string" },
86
+ extraHeaders: stringMap,
87
+ existingMockPorts: { type: "array", items: { type: "number" } }
88
+ },
89
+ additionalProperties: true
90
+ });
91
+ exports.validateContractRunPayload = validateContractRunPayload;
92
+ exports.validateSendRequestPayload = validateSendRequestPayload;
93
+ exports.validateWsdlFetchUrl = validateWsdlFetchUrl;
94
+ exports.validateWsdlImport = validateWsdlImport;
@@ -169,6 +169,10 @@ async function handleRequest(serverId, req, res, reqStart) {
169
169
  vm__namespace.runInNewContext(route.script, {
170
170
  request: requestCtx,
171
171
  response: responseDraft,
172
+ // Free-form per-route data the importer/user can stash (e.g. SOAP
173
+ // envelopes by operation, lookup tables, fixtures). Frozen so a script
174
+ // can't mutate it across calls.
175
+ metadata: route.metadata ? Object.freeze({ ...route.metadata }) : {},
172
176
  faker: faker2,
173
177
  dayjs,
174
178
  console: { log: (...args) => console.log("[mock-script]", ...args) }
@@ -32,6 +32,7 @@ const jsonpathPlus = require("jsonpath-plus");
32
32
  const xmldom = require("@xmldom/xmldom");
33
33
  const path = require("path");
34
34
  const Ajv = require("ajv");
35
+ const ipcValidate = require("./ipc-validate-CscN4HfG.js");
35
36
  function _interopNamespaceDefault(e) {
36
37
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
37
38
  if (e) {
@@ -253,13 +254,15 @@ class AssertionError extends Error {
253
254
  }
254
255
  }
255
256
  function buildAt(ctx, testResults, consoleOutput) {
256
- const { envVars, collectionVars, globals: globals2, localVars } = ctx;
257
+ const { envVars, collectionVars, globals: globals2, localVars, piiMaskPatterns = [] } = ctx;
258
+ const isSensitiveKey = (key) => piiMaskPatterns.some((p) => key.toLowerCase().includes(p.toLowerCase()));
257
259
  function makeVarScope(store, scopeName) {
258
260
  return {
259
261
  get: (key) => store[key] ?? null,
260
262
  set: (key, value) => {
261
263
  store[key] = String(value);
262
- consoleOutput.push(`[set] ${scopeName}.${key} = ${JSON.stringify(String(value))}`);
264
+ const display = isSensitiveKey(key) ? '"[REDACTED]"' : JSON.stringify(String(value));
265
+ consoleOutput.push(`[set] ${scopeName}.${key} = ${display}`);
263
266
  },
264
267
  clear: (key) => {
265
268
  delete store[key];
@@ -382,7 +385,8 @@ async function runScript(code, ctx, timeoutMs = 5e3) {
382
385
  collectionVars: collectionCopy,
383
386
  globals: globalsCopy,
384
387
  localVars: localVarsCopy,
385
- response: ctx.response
388
+ response: ctx.response,
389
+ piiMaskPatterns: ctx.piiMaskPatterns
386
390
  };
387
391
  const sp = buildAt(scriptCtx, testResults, consoleOutput);
388
392
  const captureConsole = {
@@ -601,6 +605,40 @@ function buildSchemaTestResults(schemaText, body) {
601
605
  error: err.message ?? "Schema violation"
602
606
  }));
603
607
  }
608
+ function buildProtocolFaultTests(bodyMode, body) {
609
+ if (!body) return [];
610
+ if (bodyMode === "soap") {
611
+ const isFault = /<(?:[\w-]+:)?Fault(?:\s|>)/i.test(body);
612
+ if (isFault) {
613
+ const reason = /<(?:[\w-]+:)?(?:faultstring|Text)[^>]*>([\s\S]*?)<\/(?:[\w-]+:)?(?:faultstring|Text)>/i.exec(body);
614
+ return [{
615
+ name: "[soap] response is not a Fault",
616
+ passed: false,
617
+ error: reason?.[1]?.trim() ?? "SOAP Fault returned"
618
+ }];
619
+ }
620
+ return [{ name: "[soap] response is not a Fault", passed: true }];
621
+ }
622
+ if (bodyMode === "graphql") {
623
+ let parsed;
624
+ try {
625
+ parsed = JSON.parse(body);
626
+ } catch {
627
+ return [];
628
+ }
629
+ const errors = parsed?.errors;
630
+ if (Array.isArray(errors) && errors.length > 0) {
631
+ const first = errors[0];
632
+ return [{
633
+ name: "[graphql] response has no errors",
634
+ passed: false,
635
+ error: first?.message ?? "GraphQL response contained an `errors` array"
636
+ }];
637
+ }
638
+ return [{ name: "[graphql] response has no errors", passed: true }];
639
+ }
640
+ return [];
641
+ }
604
642
  async function buildDispatcher(proxy, tls) {
605
643
  const connectOpts = {};
606
644
  let hasTls = false;
@@ -642,6 +680,7 @@ async function buildDispatcher(proxy, tls) {
642
680
  }
643
681
  function registerRequestHandler(ipc) {
644
682
  ipc.handle("request:send", async (_e, payload) => {
683
+ ipcValidate.validateSendRequestPayload(payload);
645
684
  const {
646
685
  request: req,
647
686
  environment,
@@ -695,6 +734,7 @@ function registerRequestHandler(ipc) {
695
734
  vars = authBuilder.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
696
735
  }
697
736
  let response;
737
+ let scriptResponse;
698
738
  let sentRequest = { method: req.method, url: "", headers: {} };
699
739
  const resolvedUrl = authBuilder.buildUrl(req.url, req.params, vars);
700
740
  const secretValues = /* @__PURE__ */ new Set();
@@ -838,6 +878,14 @@ function registerRequestHandler(ipc) {
838
878
  bodySize: Buffer.byteLength(responseBody, "utf8"),
839
879
  durationMs
840
880
  };
881
+ scriptResponse = {
882
+ status: fetchResp.status,
883
+ statusText: fetchResp.statusText,
884
+ headers: rawResponseHeaders,
885
+ body: responseBody,
886
+ bodySize: Buffer.byteLength(responseBody, "utf8"),
887
+ durationMs
888
+ };
841
889
  } catch (err) {
842
890
  const diagnostic = formatRequestError(err, {
843
891
  requestId: req.id,
@@ -856,8 +904,9 @@ function registerRequestHandler(ipc) {
856
904
  durationMs: Date.now() - start,
857
905
  error: diagnostic
858
906
  };
907
+ scriptResponse = response;
859
908
  }
860
- const schemaTestResults = !response.error ? buildSchemaTestResults(req.schema, response.body) : [];
909
+ const schemaTestResults = !response.error ? buildSchemaTestResults(req.schema, scriptResponse.body) : [];
861
910
  let postTestResults = [];
862
911
  let postConsole = [];
863
912
  let postError;
@@ -867,7 +916,9 @@ function registerRequestHandler(ipc) {
867
916
  collectionVars: { ...updatedCollectionVars },
868
917
  globals: { ...updatedGlobals },
869
918
  localVars: { ...localVars },
870
- response
919
+ // Pass the *unmasked* response so the script can extract real values
920
+ // (tokens, ids, …). The displayed `response` keeps the redacted copy.
921
+ response: scriptResponse
871
922
  });
872
923
  postTestResults = result.testResults;
873
924
  postConsole = result.consoleOutput;
@@ -914,22 +965,57 @@ function registerRequestHandler(ipc) {
914
965
  };
915
966
  });
916
967
  }
917
- function collectTagged(folder, requests, collectionVars, filterTags, parentPath = [], isRoot = true) {
918
- const results = [];
919
- const scopePath = isRoot ? parentPath : [...parentPath, folder.name];
920
- for (const reqId of folder.requestIds) {
921
- const req = requests[reqId];
922
- if (!req || req.hookType || req.disabled) continue;
968
+ function makeHook(req, collectionVars, hookType, scopeId, scopeAncestors, scopePath, mainRequestId) {
969
+ return { request: req, collectionVars, isHook: true, hookType, scopeId, scopeAncestors, scopePath, mainRequestId };
970
+ }
971
+ function buildFolderPlan(folder, requests, collectionVars, filterTags, scopeId, ancestorIds, parentPath, wrappers, isRoot) {
972
+ const result = [];
973
+ const scopePath = isRoot ? [] : [...parentPath, folder.name];
974
+ const folderReqs = folder.requestIds.map((id) => requests[id]).filter((r) => r && !r.disabled);
975
+ const beforeAllHooks = folderReqs.filter((r) => r.hookType === "beforeAll");
976
+ const beforeHooks = folderReqs.filter((r) => r.hookType === "before");
977
+ const afterHooks = folderReqs.filter((r) => r.hookType === "after");
978
+ const afterAllHooks = folderReqs.filter((r) => r.hookType === "afterAll");
979
+ const regularReqs = folderReqs.filter((r) => !r.hookType);
980
+ const myWrapper = { scopeId, ancestors: ancestorIds, scopePath, before: beforeHooks, after: afterHooks };
981
+ const allWrappers = [...wrappers, myWrapper];
982
+ for (const req of beforeAllHooks) {
983
+ result.push(makeHook(req, collectionVars, "beforeAll", scopeId, ancestorIds, scopePath));
984
+ }
985
+ for (const req of regularReqs) {
923
986
  const tags = req.meta?.tags ?? [];
924
987
  if (filterTags.length > 0 && !filterTags.some((t) => tags.includes(t))) continue;
925
- results.push({ request: req, collectionVars, scopePath });
988
+ for (const w of allWrappers) {
989
+ for (const hookReq of w.before) {
990
+ result.push(makeHook(hookReq, collectionVars, "before", w.scopeId, w.ancestors, w.scopePath, req.id));
991
+ }
992
+ }
993
+ result.push({ request: req, collectionVars, scopeId, scopeAncestors: ancestorIds, scopePath });
994
+ for (const w of [...allWrappers].reverse()) {
995
+ for (const hookReq of w.after) {
996
+ result.push(makeHook(hookReq, collectionVars, "after", w.scopeId, w.ancestors, w.scopePath, req.id));
997
+ }
998
+ }
926
999
  }
927
1000
  for (const sub of folder.folders) {
928
1001
  const folderTags = sub.tags ?? [];
929
- const effectiveTags = filterTags.length === 0 ? filterTags : folderTags.some((t) => filterTags.includes(t)) ? [] : filterTags;
930
- results.push(...collectTagged(sub, requests, collectionVars, effectiveTags, scopePath, false));
1002
+ const effectiveFilter = filterTags.length === 0 ? filterTags : folderTags.some((t) => filterTags.includes(t)) ? [] : filterTags;
1003
+ result.push(...buildFolderPlan(
1004
+ sub,
1005
+ requests,
1006
+ collectionVars,
1007
+ effectiveFilter,
1008
+ sub.id,
1009
+ [...ancestorIds, scopeId],
1010
+ scopePath,
1011
+ allWrappers,
1012
+ false
1013
+ ));
931
1014
  }
932
- return results;
1015
+ for (const req of afterAllHooks) {
1016
+ result.push(makeHook(req, collectionVars, "afterAll", scopeId, ancestorIds, scopePath));
1017
+ }
1018
+ return result;
933
1019
  }
934
1020
  function folderPathTo(root, requestId) {
935
1021
  if (root.requestIds.includes(requestId)) return [root];
@@ -977,9 +1063,26 @@ function resolveInheritedAuthAndHeaders(requestId, collection) {
977
1063
  }
978
1064
  return { auth: inheritedAuth, headers: inheritedHeaders };
979
1065
  }
1066
+ function buildRunPlan(collection, folderId, filterTags) {
1067
+ const collectionVars = collection.collectionVariables ?? {};
1068
+ {
1069
+ return buildFolderPlan(
1070
+ collection.rootFolder,
1071
+ collection.requests,
1072
+ collectionVars,
1073
+ filterTags,
1074
+ collection.rootFolder.id,
1075
+ [],
1076
+ [],
1077
+ [],
1078
+ true
1079
+ );
1080
+ }
1081
+ }
980
1082
  exports.buildDispatcher = buildDispatcher;
1083
+ exports.buildProtocolFaultTests = buildProtocolFaultTests;
1084
+ exports.buildRunPlan = buildRunPlan;
981
1085
  exports.buildSchemaTestResults = buildSchemaTestResults;
982
- exports.collectTagged = collectTagged;
983
1086
  exports.getAllApplicableHooks = getAllApplicableHooks;
984
1087
  exports.getGlobals = getGlobals;
985
1088
  exports.loadGlobals = loadGlobals;