@testsmith/api-spector 0.3.5 → 0.3.6

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
@@ -27,7 +27,7 @@ const COMMANDS = {
27
27
 
28
28
  function printHelp() {
29
29
  console.log('')
30
- console.log(' API Spector local-first API testing tool')
30
+ console.log(' API Spector - local-first API testing tool')
31
31
  console.log('')
32
32
  console.log(' Usage:')
33
33
  console.log(' api-spector ui Launch the app')
@@ -58,7 +58,7 @@ if (cmd === '--help' || cmd === '-h') {
58
58
 
59
59
  const command = COMMANDS[cmd]
60
60
  if (!command) {
61
- console.error(`API Spector unknown command: "${cmd}"`)
61
+ console.error(`API Spector - unknown command: "${cmd}"`)
62
62
  printHelp()
63
63
  process.exit(1)
64
64
  }
@@ -144,7 +144,7 @@ function tryRepairElectron() {
144
144
  const zip = findCachedElectronZip(version)
145
145
  if (!zip) return 'no-zip'
146
146
 
147
- console.error(` Electron ${version} was already downloaded repairing the`)
147
+ console.error(` Electron ${version} was already downloaded - repairing the`)
148
148
  console.error(' installation from the local cache...')
149
149
  const distDir = path.join(electronDir, 'dist')
150
150
  try { fs.rmSync(distDir, { recursive: true, force: true }) } catch { /* best effort */ }
@@ -180,6 +180,10 @@ const TROUBLESHOOTING_URL =
180
180
 
181
181
  // ui: spawn electron with the app dir
182
182
  if (command.runner === 'electron') {
183
+ // Immediate feedback: on machines where antivirus scans Electron's binary
184
+ // at every launch (common on managed Windows), the window can take 10-30 s
185
+ // to appear — without this line the command looks hung.
186
+ console.log('Launching API Spector...')
183
187
  let loaded = loadElectron()
184
188
  let failedZip = null
185
189
 
@@ -194,7 +198,7 @@ if (command.runner === 'electron') {
194
198
 
195
199
  if (loaded.status !== 'ok') {
196
200
  console.error('')
197
- console.error(' API Spector failed to launch the UI.')
201
+ console.error(' API Spector - failed to launch the UI.')
198
202
  console.error('')
199
203
  if (loaded.status === 'not-installed') {
200
204
  console.error(' The electron package is not installed alongside API Spector.')
@@ -221,7 +225,7 @@ if (command.runner === 'electron') {
221
225
  if (failedZip) {
222
226
  console.error(' Electron\'s binary is missing. A downloaded copy exists at')
223
227
  console.error(` ${failedZip}`)
224
- console.error(' but it could not be extracted the file may be corrupt (delete')
228
+ console.error(' but it could not be extracted - the file may be corrupt (delete')
225
229
  console.error(' it and reinstall), or antivirus is blocking the extraction.')
226
230
  } else {
227
231
  console.error(' Electron is installed, but its platform binary is missing and no')
@@ -231,7 +235,7 @@ if (command.runner === 'electron') {
231
235
  console.error(' Common causes on corporate machines:')
232
236
  console.error('')
233
237
  console.error(' - Proxy blocks github.com downloads. Note: npm\'s proxy settings')
234
- console.error(' do NOT apply to electron\'s downloader it needs:')
238
+ console.error(' do NOT apply to electron\'s downloader - it needs:')
235
239
  console.error(' ELECTRON_GET_USE_PROXY=1')
236
240
  console.error(' GLOBAL_AGENT_HTTPS_PROXY=http://your-proxy:port')
237
241
  console.error(' then: npm install -D @testsmith/api-spector --force')
@@ -241,7 +245,7 @@ if (command.runner === 'electron') {
241
245
  console.error(' NODE_EXTRA_CA_CERTS=/path/to/corporate-root-ca.pem')
242
246
  console.error('')
243
247
  console.error(' - ELECTRON_SKIP_BINARY_DOWNLOAD=1 set machine-wide (some IT')
244
- console.error(' images do this) unset it and reinstall.')
248
+ console.error(' images do this) - unset it and reinstall.')
245
249
  }
246
250
  console.error('')
247
251
  console.error(' Manual fix (works without any of the above): download')
@@ -152,7 +152,7 @@ function listAgents() {
152
152
  }
153
153
  function printHelp() {
154
154
  console.log(`
155
- ${cliCommon.colorAlways("api-spector agents", cliCommon.C.bold)} manage AI agent configurations
155
+ ${cliCommon.colorAlways("api-spector agents", cliCommon.C.bold)} - manage AI agent configurations
156
156
 
157
157
  ${cliCommon.colorAlways("Commands:", cliCommon.C.bold)}
158
158
  agents init <name> Scaffold agent instruction files in the current directory
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ function resolveEnvironmentChain(env, all) {
3
+ if (!env.extends) return env;
4
+ const chain = [env];
5
+ const seen = /* @__PURE__ */ new Set([env.name]);
6
+ let parentName = env.extends;
7
+ while (parentName && !seen.has(parentName)) {
8
+ const parent = all.find((e) => e.name === parentName);
9
+ if (!parent) break;
10
+ chain.push(parent);
11
+ seen.add(parent.name);
12
+ parentName = parent.extends;
13
+ }
14
+ if (chain.length === 1) return env;
15
+ const merged = /* @__PURE__ */ new Map();
16
+ for (const link of [...chain].reverse()) {
17
+ for (const v of link.variables) merged.set(v.key, v);
18
+ }
19
+ return { ...env, variables: [...merged.values()] };
20
+ }
21
+ function selectEnvironment(workspace, environments, requestedName) {
22
+ const name = requestedName ?? workspace.settings?.defaultEnvironment;
23
+ if (!name) return null;
24
+ const env = environments.find((e) => e.name.toLowerCase() === name.toLowerCase());
25
+ return env ? resolveEnvironmentChain(env, environments) : null;
26
+ }
27
+ exports.resolveEnvironmentChain = resolveEnvironmentChain;
28
+ exports.selectEnvironment = selectEnvironment;
@@ -103,7 +103,9 @@ const IPC = {
103
103
  captureSnapshot: "contract:captureSnapshot",
104
104
  listSnapshots: "contract:listSnapshots",
105
105
  loadSnapshot: "contract:loadSnapshot",
106
- deleteSnapshot: "contract:deleteSnapshot"
106
+ deleteSnapshot: "contract:deleteSnapshot",
107
+ recordResult: "contract:recordResult",
108
+ fuzz: "contract:fuzz"
107
109
  },
108
110
  // ─── Script hooks ──────────────────────────────────────────────────────────
109
111
  script: {
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const uuid = require("uuid");
4
- const soapHandler = require("./soap-handler-B9x_YCtj.js");
5
- require("./handle-C0IQL-Vl.js");
4
+ const soapHandler = require("./soap-handler-CqXVreNa.js");
5
+ require("./handle-BtRMtQJg.js");
6
6
  require("https");
7
7
  require("http");
8
8
  require("@xmldom/xmldom");
@@ -58,7 +58,7 @@ function buildCollectionFromWsdl(name, parsed) {
58
58
  version: "1.0",
59
59
  id: uuid.v4(),
60
60
  name,
61
- description: `Imported from WSDL ${parsed.endpoints[0]?.address ?? parsed.targetNamespace}`,
61
+ description: `Imported from WSDL - ${parsed.endpoints[0]?.address ?? parsed.targetNamespace}`,
62
62
  rootFolder: {
63
63
  id: uuid.v4(),
64
64
  name: "root",
@@ -104,7 +104,7 @@ function buildMockFromWsdl(name, parsed, existingPorts = []) {
104
104
  statusCode: 200,
105
105
  headers: { "Content-Type": contentTypeForSoap(version) },
106
106
  body: "",
107
- description: `SOAP ${version} dispatched per operation`,
107
+ description: `SOAP ${version} - dispatched per operation`,
108
108
  script: soapHandler.buildMockDispatchScript(),
109
109
  // Externalized so workspace JSON stays compact: the dispatch script reads
110
110
  // these via the script-runner's `metadata` context binding instead of
@@ -208,7 +208,7 @@ function entriesToMockServer(entries, upstream, name, port) {
208
208
  path: e.request.path,
209
209
  statusCode: e.response.status,
210
210
  headers: respHeaders,
211
- body: e.response.binary ? `[binary recorded from ${upstream}${e.request.path}]` : e.response.body ?? "",
211
+ body: e.response.binary ? `[binary - recorded from ${upstream}${e.request.path}]` : e.response.body ?? "",
212
212
  description: `Recorded from ${upstream} at ${e.timestamp}`
213
213
  });
214
214
  }
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ function makeHook(req, collectionVars, hookType, scopeId, scopeAncestors, scopePath, mainRequestId) {
3
+ return { request: req, collectionVars, isHook: true, hookType, scopeId, scopeAncestors, scopePath, mainRequestId };
4
+ }
5
+ function buildFolderPlan(folder, requests, collectionVars, filterTags, scopeId, ancestorIds, parentPath, wrappers, isRoot) {
6
+ const result = [];
7
+ const scopePath = isRoot ? [] : [...parentPath, folder.name];
8
+ const folderReqs = folder.requestIds.map((id) => requests[id]).filter((r) => r && !r.disabled);
9
+ const beforeAllHooks = folderReqs.filter((r) => r.hookType === "beforeAll");
10
+ const beforeHooks = folderReqs.filter((r) => r.hookType === "before");
11
+ const afterHooks = folderReqs.filter((r) => r.hookType === "after");
12
+ const afterAllHooks = folderReqs.filter((r) => r.hookType === "afterAll");
13
+ const regularReqs = folderReqs.filter((r) => !r.hookType);
14
+ const myWrapper = { scopeId, ancestors: ancestorIds, scopePath, before: beforeHooks, after: afterHooks };
15
+ const allWrappers = [...wrappers, myWrapper];
16
+ for (const req of beforeAllHooks) {
17
+ result.push(makeHook(req, collectionVars, "beforeAll", scopeId, ancestorIds, scopePath));
18
+ }
19
+ for (const req of regularReqs) {
20
+ const tags = req.meta?.tags ?? [];
21
+ if (filterTags.length > 0 && !filterTags.some((t) => tags.includes(t))) continue;
22
+ for (const w of allWrappers) {
23
+ for (const hookReq of w.before) {
24
+ result.push(makeHook(hookReq, collectionVars, "before", w.scopeId, w.ancestors, w.scopePath, req.id));
25
+ }
26
+ }
27
+ result.push({ request: req, collectionVars, scopeId, scopeAncestors: ancestorIds, scopePath });
28
+ for (const w of [...allWrappers].reverse()) {
29
+ for (const hookReq of w.after) {
30
+ result.push(makeHook(hookReq, collectionVars, "after", w.scopeId, w.ancestors, w.scopePath, req.id));
31
+ }
32
+ }
33
+ }
34
+ for (const sub of folder.folders) {
35
+ const folderTags = sub.tags ?? [];
36
+ const effectiveFilter = filterTags.length === 0 ? filterTags : folderTags.some((t) => filterTags.includes(t)) ? [] : filterTags;
37
+ result.push(...buildFolderPlan(
38
+ sub,
39
+ requests,
40
+ collectionVars,
41
+ effectiveFilter,
42
+ sub.id,
43
+ [...ancestorIds, scopeId],
44
+ scopePath,
45
+ allWrappers,
46
+ false
47
+ ));
48
+ }
49
+ for (const req of afterAllHooks) {
50
+ result.push(makeHook(req, collectionVars, "afterAll", scopeId, ancestorIds, scopePath));
51
+ }
52
+ return result;
53
+ }
54
+ function folderPathTo(root, requestId) {
55
+ if (root.requestIds.includes(requestId)) return [root];
56
+ for (const sub of root.folders) {
57
+ const path = folderPathTo(sub, requestId);
58
+ if (path.length > 0) return [root, ...path];
59
+ }
60
+ return [];
61
+ }
62
+ function getAllApplicableHooks(folderId, collection) {
63
+ function chainToFolder(root, targetId) {
64
+ if (root.id === targetId) return [root];
65
+ for (const sub of root.folders) {
66
+ const chain2 = chainToFolder(sub, targetId);
67
+ if (chain2.length) return [root, ...chain2];
68
+ }
69
+ return [];
70
+ }
71
+ const chain = chainToFolder(collection.rootFolder, folderId);
72
+ const beforeAll = [];
73
+ const before = [];
74
+ const after = [];
75
+ const afterAll = [];
76
+ for (const folder of chain) {
77
+ const reqs = folder.requestIds.map((id) => collection.requests[id]).filter((r) => r && !r.disabled);
78
+ beforeAll.push(...reqs.filter((r) => r.hookType === "beforeAll"));
79
+ before.push(...reqs.filter((r) => r.hookType === "before"));
80
+ }
81
+ for (const folder of [...chain].reverse()) {
82
+ const reqs = folder.requestIds.map((id) => collection.requests[id]).filter((r) => r && !r.disabled);
83
+ after.push(...reqs.filter((r) => r.hookType === "after"));
84
+ afterAll.push(...reqs.filter((r) => r.hookType === "afterAll"));
85
+ }
86
+ return { beforeAll, before, after, afterAll };
87
+ }
88
+ function resolveInheritedAuthAndHeaders(requestId, collection) {
89
+ let inheritedAuth = collection.auth && collection.auth.type !== "none" ? collection.auth : null;
90
+ let inheritedHeaders = collection.headers?.filter((h) => h.enabled && h.key) ?? [];
91
+ const path = folderPathTo(collection.rootFolder, requestId);
92
+ for (const folder of path) {
93
+ if (folder.auth && folder.auth.type !== "none") inheritedAuth = folder.auth;
94
+ if (folder.headers?.length) {
95
+ inheritedHeaders = [...inheritedHeaders, ...folder.headers.filter((h) => h.enabled && h.key)];
96
+ }
97
+ }
98
+ return { auth: inheritedAuth, headers: inheritedHeaders };
99
+ }
100
+ function buildRunPlan(collection, folderId, filterTags) {
101
+ const collectionVars = collection.collectionVariables ?? {};
102
+ {
103
+ return buildFolderPlan(
104
+ collection.rootFolder,
105
+ collection.requests,
106
+ collectionVars,
107
+ filterTags,
108
+ collection.rootFolder.id,
109
+ [],
110
+ [],
111
+ [],
112
+ true
113
+ );
114
+ }
115
+ }
116
+ exports.buildRunPlan = buildRunPlan;
117
+ exports.getAllApplicableHooks = getAllApplicableHooks;
118
+ exports.resolveInheritedAuthAndHeaders = resolveInheritedAuthAndHeaders;