@testsmith/api-spector 0.2.0 → 0.2.1

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.
@@ -3,7 +3,7 @@
3
3
  const promises = require("fs/promises");
4
4
  const path = require("path");
5
5
  const undici = require("undici");
6
- const requestHandler = require("./chunks/request-handler-AFBOd__c.js");
6
+ const requestCollection = require("./chunks/request-collection-Dx0ZqB54.js");
7
7
  require("crypto");
8
8
  require("dayjs");
9
9
  require("vm");
@@ -136,80 +136,109 @@ function buildHtmlReport(results, summary, meta = {}) {
136
136
  <meta name="viewport" content="width=device-width, initial-scale=1">
137
137
  <title>${esc(collection)} — Test Results</title>
138
138
  <style>
139
+ /* ─── CSS custom properties for theming ──────────────────────────────── */
140
+ :root {
141
+ --bg: #0f1117; --bg-surface: #161b22; --bg-panel: #0d1117;
142
+ --border: #21262d; --border-hover:#30363d;
143
+ --text: #c9d1d9; --text-bright: #e6edf3; --text-muted: #8b949e; --text-dim: #484f58;
144
+ --green: #3fb950; --red: #f85149; --amber: #d29922; --blue: #79c0ff;
145
+ --badge-pass-bg: #0d3a1e; --badge-fail-bg: #3d1014; --badge-err-bg: #3d2a00; --badge-skip-bg: #21262d;
146
+ --hook-before-border: #7c3aed; --hook-after-border: #0e7490;
147
+ --hook-before-bg: #4c1d95; --hook-before-fg: #c4b5fd;
148
+ --hook-after-bg: #164e63; --hook-after-fg: #67e8f9;
149
+ --link: #3d7fb2;
150
+ }
151
+ html.light {
152
+ --bg: #ffffff; --bg-surface: #f6f8fa; --bg-panel: #ffffff;
153
+ --border: #d0d7de; --border-hover:#c4c9cf;
154
+ --text: #1f2328; --text-bright: #1f2328; --text-muted: #656d76; --text-dim: #8b949e;
155
+ --green: #1a7f37; --red: #cf222e; --amber: #9a6700; --blue: #0969da;
156
+ --badge-pass-bg: #dafbe1; --badge-fail-bg: #ffebe9; --badge-err-bg: #fff8c5; --badge-skip-bg: #f6f8fa;
157
+ --hook-before-border: #8b5cf6; --hook-after-border: #06b6d4;
158
+ --hook-before-bg: #ede9fe; --hook-before-fg: #6d28d9;
159
+ --hook-after-bg: #ecfeff; --hook-after-fg: #0e7490;
160
+ --link: #0969da;
161
+ }
162
+ /* ─── Base ───────────────────────────────────────────────────────────── */
139
163
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
140
- body { font-family: system-ui, sans-serif; background: #0f1117; color: #c9d1d9; font-size: 13px; line-height: 1.5; }
164
+ body { font-family: system-ui, sans-serif; background: var(--bg); color: var(--text); font-size: 13px; line-height: 1.5; }
141
165
  .wrap { max-width: 1200px; margin: 0 auto; padding: 32px 24px; }
142
- h1 { font-size: 20px; font-weight: 600; color: #e6edf3; margin-bottom: 4px; }
143
- .meta-line { color: #8b949e; font-size: 11px; margin-bottom: 24px; }
166
+ h1 { font-size: 20px; font-weight: 600; color: var(--text-bright); margin-bottom: 4px; }
167
+ .meta-line { color: var(--text-muted); font-size: 11px; margin-bottom: 24px; }
168
+ /* Theme toggle */
169
+ .theme-toggle { position: fixed; top: 16px; right: 16px; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px; padding: 4px 10px; font-size: 16px; cursor: pointer; z-index: 100; line-height: 1; }
170
+ .theme-toggle:hover { border-color: var(--blue); }
171
+ /* Summary */
144
172
  .summary { display: flex; gap: 12px; margin-bottom: 28px; flex-wrap: wrap; }
145
- .stat { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 12px 20px; min-width: 90px; }
146
- .stat-val { font-size: 22px; font-weight: 700; color: #e6edf3; }
147
- .stat-lbl { font-size: 11px; color: #8b949e; margin-top: 2px; }
148
- .stat-pass .stat-val { color: #3fb950; }
149
- .stat-fail .stat-val { color: #f85149; }
150
- .stat-err .stat-val { color: #d29922; }
151
- .stat-skip .stat-val { color: #8b949e; }
173
+ .stat { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 8px; padding: 12px 20px; min-width: 90px; }
174
+ .stat-val { font-size: 22px; font-weight: 700; color: var(--text-bright); }
175
+ .stat-lbl { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
176
+ .stat-pass .stat-val { color: var(--green); }
177
+ .stat-fail .stat-val { color: var(--red); }
178
+ .stat-err .stat-val { color: var(--amber); }
179
+ .stat-skip .stat-val { color: var(--text-muted); }
152
180
  /* Cards */
153
- .card { border: 1px solid #21262d; border-radius: 8px; margin-bottom: 8px; overflow: hidden; }
154
- .scope-heading { margin: 18px 0 6px; padding: 4px 2px; font-size: 11px; font-weight: 600; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid #21262d; }
155
- .scope-heading::before { content: "▸ "; color: #484f58; }
181
+ .card { border: 1px solid var(--border); border-radius: 8px; margin-bottom: 8px; overflow: hidden; }
182
+ .scope-heading { margin: 18px 0 6px; padding: 4px 2px; font-size: 11px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid var(--border); }
183
+ .scope-heading::before { content: "\\25B8 "; color: var(--text-dim); }
156
184
  .card-header { display: flex; align-items: baseline; gap: 8px; padding: 10px 14px; cursor: pointer; user-select: none; }
157
- .card-header:hover { background: #161b22; }
158
- .chevron { font-size: 10px; color: #8b949e; min-width: 10px; transition: transform .15s; }
159
- .card-name { font-weight: 500; color: #e6edf3; white-space: nowrap; }
185
+ .card-header:hover { background: var(--bg-surface); }
186
+ .chevron { font-size: 10px; color: var(--text-muted); min-width: 10px; transition: transform .15s; }
187
+ .card-name { font-weight: 500; color: var(--text-bright); white-space: nowrap; }
160
188
  .card-url { font-family: monospace; font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
161
- .card-body { padding: 12px 14px; border-top: 1px solid #21262d; display: flex; flex-direction: column; gap: 10px; }
189
+ .card-body { padding: 12px 14px; border-top: 1px solid var(--border); display: flex; flex-direction: column; gap: 10px; }
162
190
  /* Badges */
163
191
  .badge { display: inline-block; padding: 1px 7px; border-radius: 10px; font-size: 11px; font-weight: 600; white-space: nowrap; }
164
- .badge-pass { background: #0d3a1e; color: #3fb950; }
165
- .badge-fail { background: #3d1014; color: #f85149; }
166
- .badge-err { background: #3d2a00; color: #d29922; }
167
- .badge-skip { background: #21262d; color: #8b949e; }
168
- .method { font-family: monospace; font-size: 11px; font-weight: 700; color: #79c0ff; white-space: nowrap; }
169
- .method-badge { display: inline-block; font-family: monospace; font-size: 11px; font-weight: 700; color: #79c0ff; min-width: 52px; }
192
+ .badge-pass { background: var(--badge-pass-bg); color: var(--green); }
193
+ .badge-fail { background: var(--badge-fail-bg); color: var(--red); }
194
+ .badge-err { background: var(--badge-err-bg); color: var(--amber); }
195
+ .badge-skip { background: var(--badge-skip-bg); color: var(--text-muted); }
196
+ .method { font-family: monospace; font-size: 11px; font-weight: 700; color: var(--blue); white-space: nowrap; }
197
+ .method-badge { display: inline-block; font-family: monospace; font-size: 11px; font-weight: 700; color: var(--blue); min-width: 52px; }
170
198
  .http-badge { font-family: monospace; font-size: 11px; font-weight: 600; white-space: nowrap; }
171
- .http-ok { color: #3fb950; }
172
- .http-redir { color: #79c0ff; }
173
- .http-err { color: #f85149; }
199
+ .http-ok { color: var(--green); }
200
+ .http-redir { color: var(--blue); }
201
+ .http-err { color: var(--red); }
174
202
  .dur { font-family: monospace; font-size: 11px; white-space: nowrap; }
175
- .muted { color: #8b949e; }
203
+ .muted { color: var(--text-muted); }
176
204
  .mono { font-family: monospace; font-size: 12px; }
177
205
  /* Request / Response */
178
206
  .req-resp-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
179
207
  @media (max-width: 700px) { .req-resp-grid { grid-template-columns: 1fr; } }
180
- .panel-label { font-size: 11px; font-weight: 600; color: #8b949e; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 6px; }
181
- .panel { background: #0d1117; border: 1px solid #21262d; border-radius: 6px; padding: 10px 12px; display: flex; flex-direction: column; gap: 8px; }
182
- .req-line { font-family: monospace; font-size: 12px; color: #c9d1d9; word-break: break-all; }
208
+ .panel-label { font-size: 11px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: .05em; margin-bottom: 6px; }
209
+ .panel { background: var(--bg-panel); border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; display: flex; flex-direction: column; gap: 8px; }
210
+ .req-line { font-family: monospace; font-size: 12px; color: var(--text); word-break: break-all; }
183
211
  .resp-status { font-family: monospace; font-size: 13px; font-weight: 700; }
184
- .section-label { font-size: 10px; font-weight: 600; color: #8b949e; text-transform: uppercase; letter-spacing: .04em; margin-top: 2px; }
212
+ .section-label { font-size: 10px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: .04em; margin-top: 2px; }
185
213
  .htable { width: 100%; border-collapse: collapse; }
186
214
  .htable td { font-family: monospace; font-size: 11px; padding: 1px 0; vertical-align: top; }
187
- .hk { color: #79c0ff; padding-right: 12px; white-space: nowrap; }
188
- .hv { color: #c9d1d9; word-break: break-all; }
189
- .code-block { font-family: monospace; font-size: 11px; color: #c9d1d9; background: #0d1117; border: 1px solid #21262d; border-radius: 4px; padding: 8px; white-space: pre-wrap; word-break: break-all; max-height: 300px; overflow-y: auto; }
215
+ .hk { color: var(--blue); padding-right: 12px; white-space: nowrap; }
216
+ .hv { color: var(--text); word-break: break-all; }
217
+ .code-block { font-family: monospace; font-size: 11px; color: var(--text); background: var(--bg-panel); border: 1px solid var(--border); border-radius: 4px; padding: 8px; white-space: pre-wrap; word-break: break-all; max-height: 300px; overflow-y: auto; }
190
218
  /* Tests */
191
- .section-label { font-size: 11px; font-weight: 600; color: #8b949e; }
219
+ .section-label { font-size: 11px; font-weight: 600; color: var(--text-muted); }
192
220
  .tests-wrap { display: flex; flex-direction: column; gap: 2px; }
193
221
  .test-row { font-size: 12px; display: flex; flex-wrap: wrap; gap: 4px; }
194
- .test-pass { color: #3fb950; }
195
- .test-fail { color: #f85149; }
196
- .test-err { color: #8b949e; padding-left: 16px; width: 100%; font-family: monospace; font-size: 11px; }
222
+ .test-pass { color: var(--green); }
223
+ .test-fail { color: var(--red); }
224
+ .test-err { color: var(--text-muted); padding-left: 16px; width: 100%; font-family: monospace; font-size: 11px; }
197
225
  .dot { font-weight: 700; }
198
226
  /* Console */
199
- .err-row { color: #f85149; font-size: 12px; }
227
+ .err-row { color: var(--red); font-size: 12px; }
200
228
  /* Footer */
201
- .report-footer { margin-top: 32px; text-align: center; font-size: 11px; color: #484f58; }
202
- .footer-link { color: #3d7fb2; text-decoration: none; }
229
+ .report-footer { margin-top: 32px; text-align: center; font-size: 11px; color: var(--text-dim); }
230
+ .footer-link { color: var(--link); text-decoration: none; }
203
231
  .footer-link:hover { text-decoration: underline; }
204
232
  /* Hook cards */
205
- .card-hook-before { border-left: 3px solid #7c3aed; }
206
- .card-hook-after { border-left: 3px solid #0e7490; }
233
+ .card-hook-before { border-left: 3px solid var(--hook-before-border); }
234
+ .card-hook-after { border-left: 3px solid var(--hook-after-border); }
207
235
  .hook-badge { display: inline-block; padding: 1px 7px; border-radius: 10px; font-size: 10px; font-weight: 700; white-space: nowrap; letter-spacing: .04em; }
208
- .hook-before { background: #4c1d95; color: #c4b5fd; }
209
- .hook-after { background: #164e63; color: #67e8f9; }
236
+ .hook-before { background: var(--hook-before-bg); color: var(--hook-before-fg); }
237
+ .hook-after { background: var(--hook-after-bg); color: var(--hook-after-fg); }
210
238
  </style>
211
239
  </head>
212
240
  <body>
241
+ <button class="theme-toggle" onclick="toggleTheme()" title="Toggle dark/light mode" id="themeBtn">☀️</button>
213
242
  <div class="wrap">
214
243
  <h1>${esc(collection)}</h1>
215
244
  <div class="meta-line">Environment: ${esc(env)} &nbsp;·&nbsp; ${esc(ts)}</div>
@@ -223,7 +252,7 @@ function buildHtmlReport(results, summary, meta = {}) {
223
252
  <div class="stat"><div class="stat-val">${summary.durationMs} ms</div><div class="stat-lbl">Duration</div></div>
224
253
  </div>
225
254
  <div class="cards">${cards}</div>
226
- <div class="report-footer">Generated by <a href="https://testsmith.io" target="_blank" rel="noopener" class="footer-link">Testsmith</a> · api Spector</div>
255
+ <div class="report-footer">Generated by <a href="https://testsmith.io" target="_blank" rel="noopener" class="footer-link">Testsmith</a> · API Spector</div>
227
256
  </div>
228
257
  <script>
229
258
  function toggle(i) {
@@ -233,6 +262,22 @@ function buildHtmlReport(results, summary, meta = {}) {
233
262
  body.style.display = open ? 'none' : 'block'
234
263
  ch.style.transform = open ? '' : 'rotate(90deg)'
235
264
  }
265
+ function toggleTheme() {
266
+ const html = document.documentElement
267
+ const isLight = html.classList.toggle('light')
268
+ document.getElementById('themeBtn').textContent = isLight ? '🌙' : '☀️'
269
+ try { localStorage.setItem('theme', isLight ? 'light' : 'dark') } catch {}
270
+ }
271
+ // Restore saved preference or respect OS preference
272
+ (function() {
273
+ try {
274
+ const saved = localStorage.getItem('theme')
275
+ if (saved === 'light' || (!saved && window.matchMedia('(prefers-color-scheme: light)').matches)) {
276
+ document.documentElement.classList.add('light')
277
+ document.getElementById('themeBtn').textContent = '🌙'
278
+ }
279
+ } catch {}
280
+ })()
236
281
  <\/script>
237
282
  </body>
238
283
  </html>
@@ -289,23 +334,6 @@ ${failures.join("\n")}
289
334
  ];
290
335
  return lines.join("\n") + "\n";
291
336
  }
292
- function collectTagged(folder, requests, collectionVars, filterTags, parentPath = [], isRoot = true) {
293
- const results = [];
294
- const scopePath = isRoot ? parentPath : [...parentPath, folder.name];
295
- for (const reqId of folder.requestIds) {
296
- const req = requests[reqId];
297
- if (!req || req.hookType || req.disabled) continue;
298
- const tags = req.meta?.tags ?? [];
299
- if (filterTags.length > 0 && !filterTags.some((t) => tags.includes(t))) continue;
300
- results.push({ request: req, collectionVars, scopePath });
301
- }
302
- for (const sub of folder.folders) {
303
- const folderTags = sub.tags ?? [];
304
- const effectiveTags = filterTags.length === 0 ? filterTags : folderTags.some((t) => filterTags.includes(t)) ? [] : filterTags;
305
- results.push(...collectTagged(sub, requests, collectionVars, effectiveTags, scopePath, false));
306
- }
307
- return results;
308
- }
309
337
  const C = {
310
338
  reset: "\x1B[0m",
311
339
  bold: "\x1B[1m",
@@ -374,6 +402,10 @@ async function loadEnvironments(workspace, dir) {
374
402
  return envs;
375
403
  }
376
404
  async function executeRequest(req, collectionVars, envVars, globals, localVars, verbose, tls) {
405
+ if (!req.headers) req.headers = [];
406
+ if (!req.params) req.params = [];
407
+ if (!req.body) req.body = { mode: "none" };
408
+ if (!req.auth) req.auth = { type: "none" };
377
409
  const base = {
378
410
  requestId: req.id,
379
411
  name: req.name,
@@ -385,10 +417,10 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
385
417
  let updatedCollectionVars = { ...collectionVars };
386
418
  let updatedGlobals = { ...globals };
387
419
  let preScriptError;
388
- const dynamicVars = await requestHandler.buildDynamicVars();
389
- let vars = requestHandler.mergeVars(envVars, collectionVars, globals, localVars, dynamicVars);
420
+ const dynamicVars = await requestCollection.buildDynamicVars();
421
+ let vars = requestCollection.mergeVars(envVars, collectionVars, globals, localVars, dynamicVars);
390
422
  if (req.preRequestScript?.trim()) {
391
- const r = await requestHandler.runScript(requestHandler.interpolate(req.preRequestScript, vars), {
423
+ const r = await requestCollection.runScript(requestCollection.interpolate(req.preRequestScript, vars), {
392
424
  envVars: { ...envVars },
393
425
  collectionVars: { ...collectionVars },
394
426
  globals: { ...globals },
@@ -399,41 +431,41 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
399
431
  updatedEnvVars = r.updatedEnvVars;
400
432
  updatedCollectionVars = r.updatedCollectionVars;
401
433
  updatedGlobals = r.updatedGlobals;
402
- requestHandler.patchGlobals(r.updatedGlobals);
403
- await requestHandler.persistGlobals();
434
+ requestCollection.patchGlobals(r.updatedGlobals);
435
+ await requestCollection.persistGlobals();
404
436
  if (verbose && r.consoleOutput.length) r.consoleOutput.forEach((l) => console.log(color(` [pre] ${l}`, C.gray)));
405
437
  if (r.error) console.error(color(` [pre-script error] ${r.error}`, C.red));
406
438
  }
407
- vars = requestHandler.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
408
- const resolvedUrl = requestHandler.buildUrl(req.url, req.params, vars);
439
+ vars = requestCollection.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
440
+ const resolvedUrl = requestCollection.buildUrl(req.url, req.params, vars);
409
441
  base.resolvedUrl = resolvedUrl;
410
442
  const start = Date.now();
411
443
  try {
412
444
  const authH = {};
413
445
  if (req.auth.type === "bearer") {
414
446
  let token = req.auth.token ?? "";
415
- if (!token && req.auth.tokenSecretRef) token = await requestHandler.getSecret(req.auth.tokenSecretRef) ?? "";
416
- if (token) authH["Authorization"] = `Bearer ${requestHandler.interpolate(token, vars)}`;
447
+ if (!token && req.auth.tokenSecretRef) token = await requestCollection.getSecret(req.auth.tokenSecretRef) ?? "";
448
+ if (token) authH["Authorization"] = `Bearer ${requestCollection.interpolate(token, vars)}`;
417
449
  }
418
450
  const headers = new undici.Headers();
419
451
  for (const h of req.headers) {
420
- if (h.enabled && h.key) headers.set(requestHandler.interpolate(h.key, vars), requestHandler.interpolate(h.value, vars));
452
+ if (h.enabled && h.key) headers.set(requestCollection.interpolate(h.key, vars), requestCollection.interpolate(h.value, vars));
421
453
  }
422
454
  for (const [k, v] of Object.entries(authH)) headers.set(k, v);
423
455
  let body;
424
456
  if (req.body.mode === "json" && req.body.json) {
425
- body = requestHandler.interpolate(req.body.json, vars);
457
+ body = requestCollection.interpolate(req.body.json, vars);
426
458
  if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
427
459
  } else if (req.body.mode === "raw" && req.body.raw) {
428
- body = requestHandler.interpolate(req.body.raw, vars);
460
+ body = requestCollection.interpolate(req.body.raw, vars);
429
461
  if (!headers.has("content-type")) headers.set("Content-Type", req.body.rawContentType ?? "text/plain");
430
462
  } else if (req.body.mode === "graphql" && req.body.graphql) {
431
463
  const gql = req.body.graphql;
432
- const gqlBody = { query: requestHandler.interpolate(gql.query, vars) };
464
+ const gqlBody = { query: requestCollection.interpolate(gql.query, vars) };
433
465
  const rawVars = gql.variables?.trim();
434
466
  if (rawVars) {
435
467
  try {
436
- gqlBody.variables = JSON.parse(requestHandler.interpolate(rawVars, vars));
468
+ gqlBody.variables = JSON.parse(requestCollection.interpolate(rawVars, vars));
437
469
  } catch {
438
470
  }
439
471
  }
@@ -441,7 +473,7 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
441
473
  body = JSON.stringify(gqlBody);
442
474
  if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
443
475
  }
444
- const dispatcher = await requestHandler.buildDispatcher(void 0, tls);
476
+ const dispatcher = await requestCollection.buildDispatcher(void 0, tls);
445
477
  const fetchResp = await undici.fetch(resolvedUrl, {
446
478
  method: req.method,
447
479
  headers,
@@ -466,7 +498,7 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
466
498
  let consoleOutput = [];
467
499
  let postScriptError;
468
500
  if (req.postRequestScript?.trim()) {
469
- const r = await requestHandler.runScript(requestHandler.interpolate(req.postRequestScript, vars), {
501
+ const r = await requestCollection.runScript(requestCollection.interpolate(req.postRequestScript, vars), {
470
502
  envVars: updatedEnvVars,
471
503
  collectionVars: updatedCollectionVars,
472
504
  globals: updatedGlobals,
@@ -480,15 +512,15 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
480
512
  updatedCollectionVars = r.updatedCollectionVars;
481
513
  updatedGlobals = r.updatedGlobals;
482
514
  localVars = r.updatedLocalVars;
483
- requestHandler.patchGlobals(r.updatedGlobals);
484
- await requestHandler.persistGlobals();
515
+ requestCollection.patchGlobals(r.updatedGlobals);
516
+ await requestCollection.persistGlobals();
485
517
  if (verbose && r.consoleOutput.length) r.consoleOutput.forEach((l) => console.log(color(` [post] ${l}`, C.gray)));
486
518
  }
487
519
  const allPassed = testResults.every((t) => t.passed);
488
520
  const httpFailed = fetchResp.status >= 400;
489
521
  const hasTests = testResults.length > 0;
490
- const status = postScriptError ? "error" : httpFailed ? "failed" : hasTests ? allPassed ? "passed" : "failed" : "skipped";
491
- if (httpFailed && !testResults.some((t) => !t.passed)) {
522
+ const status = postScriptError ? "error" : hasTests ? allPassed ? "passed" : "failed" : httpFailed ? "failed" : "skipped";
523
+ if (httpFailed && testResults.length === 0) {
492
524
  testResults = [
493
525
  ...testResults,
494
526
  {
@@ -581,7 +613,7 @@ async function main() {
581
613
  console.error(color(`Error: could not read workspace file: ${wsPath}`, C.red));
582
614
  process.exit(1);
583
615
  }
584
- await requestHandler.loadGlobals(wsDir);
616
+ await requestCollection.loadGlobals(wsDir);
585
617
  const collections = await loadCollections(workspace, wsDir);
586
618
  const environments = await loadEnvironments(workspace, wsDir);
587
619
  const env = envName ? environments.find((e) => e.name.toLowerCase() === envName.toLowerCase()) ?? null : null;
@@ -594,7 +626,7 @@ async function main() {
594
626
  console.log(color(` Environment: ${env?.name ?? "(none)"}`, C.gray));
595
627
  if (filterTags.length) console.log(color(` Tags: ${filterTags.join(", ")}`, C.gray));
596
628
  console.log("");
597
- const envVarsSnapshot = await requestHandler.buildEnvVars(env);
629
+ const envVarsSnapshot = await requestCollection.buildEnvVars(env);
598
630
  const secretValuesToMask = (env?.variables ?? []).filter((v) => v.secret && v.enabled).map((v) => envVarsSnapshot[v.key]).filter((v) => typeof v === "string" && v.length > 0);
599
631
  function redact(s) {
600
632
  let out = s;
@@ -621,16 +653,27 @@ async function main() {
621
653
  let firstColName;
622
654
  for (const col of collections) {
623
655
  if (colName && col.name.toLowerCase() !== colName.toLowerCase()) continue;
624
- const items = collectTagged(col.rootFolder, col.requests, col.collectionVariables ?? {}, filterTags);
656
+ const items = requestCollection.collectTagged(col.rootFolder, col.requests, col.collectionVariables ?? {}, filterTags);
625
657
  if (items.length === 0) continue;
626
658
  if (!firstColName) firstColName = col.name;
627
- let runEnvVars = await requestHandler.buildEnvVars(env);
628
- let runGlobals = requestHandler.getGlobals();
659
+ let runEnvVars = await requestCollection.buildEnvVars(env);
660
+ let runGlobals = requestCollection.getGlobals();
629
661
  let runCollectionVars = { ...col.collectionVariables ?? {} };
630
662
  let runLocalVars = {};
631
663
  console.log(color(` ┌ ${col.name}`, C.bold, C.white));
632
664
  const workspaceTls = workspace.settings?.tls;
633
665
  const effectiveTls = col.tls ? { ...workspaceTls, ...col.tls } : workspaceTls;
666
+ for (const item of items) {
667
+ const req = item.request;
668
+ const inherited = requestCollection.resolveInheritedAuthAndHeaders(req.id, col);
669
+ if (req.auth.type === "none" && inherited.auth && inherited.auth.type !== "none") {
670
+ req.auth = inherited.auth;
671
+ }
672
+ const inheritedHeaders = inherited.headers.filter((h) => h.enabled && h.key);
673
+ if (inheritedHeaders.length) {
674
+ req.headers = [...inheritedHeaders, ...req.headers];
675
+ }
676
+ }
634
677
  let bailed = false;
635
678
  let lastPrintedScope = null;
636
679
  for (const item of items) {