apitestcenter 0.8.0 → 0.10.0

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 (60) hide show
  1. package/dist/THIRD-PARTY-NOTICES-UI.txt +1 -1
  2. package/dist/THIRD-PARTY-NOTICES.txt +1 -1
  3. package/dist/cli.mjs +677 -180
  4. package/dist/ui/404/index.html +1 -1
  5. package/dist/ui/404.html +1 -1
  6. package/dist/ui/__next.__PAGE__.txt +15 -14
  7. package/dist/ui/__next._full.txt +13 -12
  8. package/dist/ui/__next._tree.txt +2 -2
  9. package/dist/ui/_next/static/chunks/0f9cyrqzolhzx.js +18 -0
  10. package/dist/ui/_next/static/chunks/0imlzn2evcfw6.css +3 -0
  11. package/dist/ui/_next/static/chunks/{11nxnz4doxir5.js → 1-ogjtk9dipfq.js} +319 -133
  12. package/dist/ui/_next/static/chunks/{2idsynwg5lkk7.js → 115l_disvbdxa.js} +1 -1
  13. package/dist/ui/_next/static/chunks/1b31b8g5_5h-y.js +2 -0
  14. package/dist/ui/_next/static/chunks/2qchu_7euauai.js +1 -0
  15. package/dist/ui/_next/static/chunks/{3ad2zh-p8_xhv.js → 2sa_fr4vgkkrp.js} +2 -2
  16. package/dist/ui/_next/static/chunks/{02myfb0nlri-z.js → 3jywp_61ylz0z.js} +1 -1
  17. package/dist/ui/_next/static/chunks/3qxq1d9u_kohv.js +1 -0
  18. package/dist/ui/_next/static/media/icon.1oy4kb0w-k_2v.png +0 -0
  19. package/dist/ui/_not-found/__next._full.txt +12 -11
  20. package/dist/ui/_not-found/__next._not-found.__PAGE__.txt +17 -16
  21. package/dist/ui/_not-found/__next._tree.txt +2 -2
  22. package/dist/ui/_not-found/index.html +1 -1
  23. package/dist/ui/_not-found/index.txt +12 -11
  24. package/dist/ui/compare/__next._full.txt +13 -12
  25. package/dist/ui/compare/__next._tree.txt +2 -2
  26. package/dist/ui/compare/__next.compare.__PAGE__.txt +18 -17
  27. package/dist/ui/compare/index.html +1 -1
  28. package/dist/ui/compare/index.txt +13 -12
  29. package/dist/ui/history/__next._full.txt +14 -13
  30. package/dist/ui/history/__next._tree.txt +2 -2
  31. package/dist/ui/history/__next.history.__PAGE__.txt +19 -18
  32. package/dist/ui/history/index.html +1 -1
  33. package/dist/ui/history/index.txt +14 -13
  34. package/dist/ui/icon.png +0 -0
  35. package/dist/ui/index.html +1 -1
  36. package/dist/ui/index.txt +13 -12
  37. package/dist/ui/preview/__next._full.txt +14 -13
  38. package/dist/ui/preview/__next._tree.txt +2 -2
  39. package/dist/ui/preview/__next.preview.__PAGE__.txt +19 -18
  40. package/dist/ui/preview/index.html +1 -1
  41. package/dist/ui/preview/index.txt +14 -13
  42. package/dist/ui/suite/__next._full.txt +13 -12
  43. package/dist/ui/suite/__next._tree.txt +2 -2
  44. package/dist/ui/suite/__next.suite.__PAGE__.txt +18 -17
  45. package/dist/ui/suite/index.html +1 -1
  46. package/dist/ui/suite/index.txt +13 -12
  47. package/dist/ui/tests/__next._full.txt +12 -11
  48. package/dist/ui/tests/__next._tree.txt +2 -2
  49. package/dist/ui/tests/__next.tests.__PAGE__.txt +17 -16
  50. package/dist/ui/tests/index.html +1 -1
  51. package/dist/ui/tests/index.txt +12 -11
  52. package/package.json +1 -1
  53. package/dist/ui/_next/static/chunks/0-898cb98enjv.css +0 -3
  54. package/dist/ui/_next/static/chunks/0e1nwxi9i1ag7.js +0 -1
  55. package/dist/ui/_next/static/chunks/0vkcbl8nakdh1.js +0 -2
  56. package/dist/ui/_next/static/chunks/2nxnxp6p-40kg.js +0 -1
  57. package/dist/ui/_next/static/chunks/2xls_z5abvl-5.js +0 -1
  58. /package/dist/ui/_next/static/{BJz1RrA7HqZwUV22DBTCg → 9enzqastnBYCi6FVPT4mz}/_buildManifest.js +0 -0
  59. /package/dist/ui/_next/static/{BJz1RrA7HqZwUV22DBTCg → 9enzqastnBYCi6FVPT4mz}/_clientMiddlewareManifest.js +0 -0
  60. /package/dist/ui/_next/static/{BJz1RrA7HqZwUV22DBTCg → 9enzqastnBYCi6FVPT4mz}/_ssgManifest.js +0 -0
package/dist/cli.mjs CHANGED
@@ -231,15 +231,15 @@ function normalizeView(raw) {
231
231
  const v = raw;
232
232
  if (typeof v.id !== "string" || !v.id || typeof v.name !== "string") return null;
233
233
  const scope = v.scope === "compare" ? "compare" : "run";
234
- const seen = /* @__PURE__ */ new Set();
234
+ const seen2 = /* @__PURE__ */ new Set();
235
235
  const widgets = [];
236
236
  for (const item of Array.isArray(v.widgets) ? v.widgets : []) {
237
237
  const w = item;
238
238
  const def = METRICS[w.metric];
239
239
  if (!def || scope === "compare" && !def.compare) continue;
240
240
  let id = typeof w.id === "string" && w.id ? w.id : `${def.id}-${widgets.length}`;
241
- while (seen.has(id)) id = `${id}-x`;
242
- seen.add(id);
241
+ while (seen2.has(id)) id = `${id}-x`;
242
+ seen2.add(id);
243
243
  const kind = def.kinds.includes(w.kind) ? w.kind : def.kinds[0];
244
244
  const width = clamp(w.w ?? def.size.w, def.min.w, GRID_COLUMNS);
245
245
  widgets.push({
@@ -781,12 +781,12 @@ function variablesOf(env) {
781
781
  }
782
782
  function normalizeEnvironments(raw) {
783
783
  if (!Array.isArray(raw)) return [];
784
- const seen = /* @__PURE__ */ new Set();
784
+ const seen2 = /* @__PURE__ */ new Set();
785
785
  const out = [];
786
786
  for (const item of raw) {
787
787
  const e = item;
788
- if (typeof e?.id !== "string" || !e.id || seen.has(e.id)) continue;
789
- seen.add(e.id);
788
+ if (typeof e?.id !== "string" || !e.id || seen2.has(e.id)) continue;
789
+ seen2.add(e.id);
790
790
  const keys = /* @__PURE__ */ new Set();
791
791
  const variables = [];
792
792
  for (const v of Array.isArray(e.variables) ? e.variables : []) {
@@ -12334,14 +12334,14 @@ function initializeContext(params) {
12334
12334
  function process2(schema, ctx, _params = { path: [], schemaPath: [] }) {
12335
12335
  var _a3;
12336
12336
  const def = schema._zod.def;
12337
- const seen = ctx.seen.get(schema);
12338
- if (seen) {
12339
- seen.count++;
12337
+ const seen2 = ctx.seen.get(schema);
12338
+ if (seen2) {
12339
+ seen2.count++;
12340
12340
  const isCycle = _params.schemaPath.includes(schema);
12341
12341
  if (isCycle) {
12342
- seen.cycle = _params.path;
12342
+ seen2.cycle = _params.path;
12343
12343
  }
12344
- return seen.schema;
12344
+ return seen2.schema;
12345
12345
  }
12346
12346
  const result = { schema: {}, count: 1, cycle: void 0, path: _params.path };
12347
12347
  ctx.seen.set(schema, result);
@@ -12424,12 +12424,12 @@ function extractDefs(ctx, schema) {
12424
12424
  if (entry[1].schema.$ref) {
12425
12425
  return;
12426
12426
  }
12427
- const seen = entry[1];
12427
+ const seen2 = entry[1];
12428
12428
  const { ref, defId } = makeURI(entry);
12429
- seen.def = { ...seen.schema };
12429
+ seen2.def = { ...seen2.schema };
12430
12430
  if (defId)
12431
- seen.defId = defId;
12432
- const schema2 = seen.schema;
12431
+ seen2.defId = defId;
12432
+ const schema2 = seen2.schema;
12433
12433
  for (const key in schema2) {
12434
12434
  delete schema2[key];
12435
12435
  }
@@ -12437,16 +12437,16 @@ function extractDefs(ctx, schema) {
12437
12437
  };
12438
12438
  if (ctx.cycles === "throw") {
12439
12439
  for (const entry of ctx.seen.entries()) {
12440
- const seen = entry[1];
12441
- if (seen.cycle) {
12442
- throw new Error(`Cycle detected: #/${seen.cycle?.join("/")}/<root>
12440
+ const seen2 = entry[1];
12441
+ if (seen2.cycle) {
12442
+ throw new Error(`Cycle detected: #/${seen2.cycle?.join("/")}/<root>
12443
12443
 
12444
12444
  Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.`);
12445
12445
  }
12446
12446
  }
12447
12447
  }
12448
12448
  for (const entry of ctx.seen.entries()) {
12449
- const seen = entry[1];
12449
+ const seen2 = entry[1];
12450
12450
  if (schema === entry[0]) {
12451
12451
  extractToDef(entry);
12452
12452
  continue;
@@ -12463,11 +12463,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
12463
12463
  extractToDef(entry);
12464
12464
  continue;
12465
12465
  }
12466
- if (seen.cycle) {
12466
+ if (seen2.cycle) {
12467
12467
  extractToDef(entry);
12468
12468
  continue;
12469
12469
  }
12470
- if (seen.count > 1) {
12470
+ if (seen2.count > 1) {
12471
12471
  if (ctx.reused === "ref") {
12472
12472
  extractToDef(entry);
12473
12473
  continue;
@@ -12480,13 +12480,13 @@ function finalize(ctx, schema) {
12480
12480
  if (!root2)
12481
12481
  throw new Error("Unprocessed schema. This is a bug in Zod.");
12482
12482
  const flattenRef = (zodSchema) => {
12483
- const seen = ctx.seen.get(zodSchema);
12484
- if (seen.ref === null)
12483
+ const seen2 = ctx.seen.get(zodSchema);
12484
+ if (seen2.ref === null)
12485
12485
  return;
12486
- const schema2 = seen.def ?? seen.schema;
12486
+ const schema2 = seen2.def ?? seen2.schema;
12487
12487
  const _cached = { ...schema2 };
12488
- const ref = seen.ref;
12489
- seen.ref = null;
12488
+ const ref = seen2.ref;
12489
+ seen2.ref = null;
12490
12490
  if (ref) {
12491
12491
  flattenRef(ref);
12492
12492
  const refSeen = ctx.seen.get(ref);
@@ -12538,7 +12538,7 @@ function finalize(ctx, schema) {
12538
12538
  ctx.override({
12539
12539
  zodSchema,
12540
12540
  jsonSchema: schema2,
12541
- path: seen.path ?? []
12541
+ path: seen2.path ?? []
12542
12542
  });
12543
12543
  };
12544
12544
  for (const entry of [...ctx.seen.entries()].reverse()) {
@@ -12566,11 +12566,11 @@ function finalize(ctx, schema) {
12566
12566
  delete result.id;
12567
12567
  const defs = ctx.external?.defs ?? {};
12568
12568
  for (const entry of ctx.seen.entries()) {
12569
- const seen = entry[1];
12570
- if (seen.def && seen.defId) {
12571
- if (seen.def.id === seen.defId)
12572
- delete seen.def.id;
12573
- defs[seen.defId] = seen.def;
12569
+ const seen2 = entry[1];
12570
+ if (seen2.def && seen2.defId) {
12571
+ if (seen2.def.id === seen2.defId)
12572
+ delete seen2.def.id;
12573
+ defs[seen2.defId] = seen2.def;
12574
12574
  }
12575
12575
  }
12576
12576
  if (ctx.external) {
@@ -13106,9 +13106,9 @@ var init_json_schema_processors = __esm({
13106
13106
  nullableProcessor = (schema, ctx, json2, params) => {
13107
13107
  const def = schema._zod.def;
13108
13108
  const inner = process2(def.innerType, ctx, params);
13109
- const seen = ctx.seen.get(schema);
13109
+ const seen2 = ctx.seen.get(schema);
13110
13110
  if (ctx.target === "openapi-3.0") {
13111
- seen.ref = def.innerType;
13111
+ seen2.ref = def.innerType;
13112
13112
  json2.nullable = true;
13113
13113
  } else {
13114
13114
  json2.anyOf = [inner, { type: "null" }];
@@ -13117,29 +13117,29 @@ var init_json_schema_processors = __esm({
13117
13117
  nonoptionalProcessor = (schema, ctx, _json, params) => {
13118
13118
  const def = schema._zod.def;
13119
13119
  process2(def.innerType, ctx, params);
13120
- const seen = ctx.seen.get(schema);
13121
- seen.ref = def.innerType;
13120
+ const seen2 = ctx.seen.get(schema);
13121
+ seen2.ref = def.innerType;
13122
13122
  };
13123
13123
  defaultProcessor = (schema, ctx, json2, params) => {
13124
13124
  const def = schema._zod.def;
13125
13125
  process2(def.innerType, ctx, params);
13126
- const seen = ctx.seen.get(schema);
13127
- seen.ref = def.innerType;
13126
+ const seen2 = ctx.seen.get(schema);
13127
+ seen2.ref = def.innerType;
13128
13128
  json2.default = JSON.parse(JSON.stringify(def.defaultValue));
13129
13129
  };
13130
13130
  prefaultProcessor = (schema, ctx, json2, params) => {
13131
13131
  const def = schema._zod.def;
13132
13132
  process2(def.innerType, ctx, params);
13133
- const seen = ctx.seen.get(schema);
13134
- seen.ref = def.innerType;
13133
+ const seen2 = ctx.seen.get(schema);
13134
+ seen2.ref = def.innerType;
13135
13135
  if (ctx.io === "input")
13136
13136
  json2._prefault = JSON.parse(JSON.stringify(def.defaultValue));
13137
13137
  };
13138
13138
  catchProcessor = (schema, ctx, json2, params) => {
13139
13139
  const def = schema._zod.def;
13140
13140
  process2(def.innerType, ctx, params);
13141
- const seen = ctx.seen.get(schema);
13142
- seen.ref = def.innerType;
13141
+ const seen2 = ctx.seen.get(schema);
13142
+ seen2.ref = def.innerType;
13143
13143
  let catchValue;
13144
13144
  try {
13145
13145
  catchValue = def.catchValue(void 0);
@@ -13153,33 +13153,33 @@ var init_json_schema_processors = __esm({
13153
13153
  const inIsTransform = def.in._zod.traits.has("$ZodTransform");
13154
13154
  const innerType = ctx.io === "input" ? inIsTransform ? def.out : def.in : def.out;
13155
13155
  process2(innerType, ctx, params);
13156
- const seen = ctx.seen.get(schema);
13157
- seen.ref = innerType;
13156
+ const seen2 = ctx.seen.get(schema);
13157
+ seen2.ref = innerType;
13158
13158
  };
13159
13159
  readonlyProcessor = (schema, ctx, json2, params) => {
13160
13160
  const def = schema._zod.def;
13161
13161
  process2(def.innerType, ctx, params);
13162
- const seen = ctx.seen.get(schema);
13163
- seen.ref = def.innerType;
13162
+ const seen2 = ctx.seen.get(schema);
13163
+ seen2.ref = def.innerType;
13164
13164
  json2.readOnly = true;
13165
13165
  };
13166
13166
  promiseProcessor = (schema, ctx, _json, params) => {
13167
13167
  const def = schema._zod.def;
13168
13168
  process2(def.innerType, ctx, params);
13169
- const seen = ctx.seen.get(schema);
13170
- seen.ref = def.innerType;
13169
+ const seen2 = ctx.seen.get(schema);
13170
+ seen2.ref = def.innerType;
13171
13171
  };
13172
13172
  optionalProcessor = (schema, ctx, _json, params) => {
13173
13173
  const def = schema._zod.def;
13174
13174
  process2(def.innerType, ctx, params);
13175
- const seen = ctx.seen.get(schema);
13176
- seen.ref = def.innerType;
13175
+ const seen2 = ctx.seen.get(schema);
13176
+ seen2.ref = def.innerType;
13177
13177
  };
13178
13178
  lazyProcessor = (schema, ctx, _json, params) => {
13179
13179
  const innerType = schema._zod.innerType;
13180
13180
  process2(innerType, ctx, params);
13181
- const seen = ctx.seen.get(schema);
13182
- seen.ref = innerType;
13181
+ const seen2 = ctx.seen.get(schema);
13182
+ seen2.ref = innerType;
13183
13183
  };
13184
13184
  allProcessors = {
13185
13185
  string: stringProcessor,
@@ -16068,13 +16068,41 @@ function checkLabel(check2) {
16068
16068
  case "status":
16069
16069
  return `status is ${check2.expect.replace(/\s+/g, "")}`;
16070
16070
  case "body":
16071
+ if (check2.op === "is_json") return "body is JSON";
16072
+ if (check2.op === "matches") return `body matches /${clip(check2.value)}/`;
16071
16073
  return `body ${check2.op === "contains" ? "contains" : "does not contain"} "${clip(check2.value)}"`;
16072
- case "json":
16073
- return check2.op === "exists" ? `${jsonPathLabel(check2.path)} exists` : `${jsonPathLabel(check2.path)} ${check2.op === "equals" ? "=" : check2.op === "not_equals" ? "\u2260" : "contains"} "${clip(check2.value)}"`;
16074
+ case "json": {
16075
+ const path = jsonPathLabel(check2.path);
16076
+ switch (check2.op) {
16077
+ case "exists":
16078
+ return `${path} exists`;
16079
+ case "not_exists":
16080
+ return `${path} does not exist`;
16081
+ case "greater":
16082
+ return `${path} > ${clip(check2.value)}`;
16083
+ case "less":
16084
+ return `${path} < ${clip(check2.value)}`;
16085
+ case "matches":
16086
+ return `${path} matches /${clip(check2.value)}/`;
16087
+ case "type":
16088
+ return `${path} is ${/^[aeiou]/.test(check2.value) ? "an" : "a"} ${check2.value || "?"}`;
16089
+ case "length":
16090
+ return `length of ${path} ${lengthRule(check2.value).label}`;
16091
+ default:
16092
+ return `${path} ${check2.op === "equals" ? "=" : check2.op === "not_equals" ? "\u2260" : "contains"} "${clip(check2.value)}"`;
16093
+ }
16094
+ }
16074
16095
  case "header":
16075
- return check2.op === "exists" ? `header ${check2.name} exists` : `header ${check2.name} ${check2.op === "equals" ? "=" : "contains"} "${clip(check2.value)}"`;
16096
+ if (check2.op === "exists") return `header ${check2.name} exists`;
16097
+ if (check2.op === "not_exists") return `header ${check2.name} does not exist`;
16098
+ if (check2.op === "matches") return `header ${check2.name} matches /${clip(check2.value)}/`;
16099
+ return `header ${check2.name} ${check2.op === "equals" ? "=" : "contains"} "${clip(check2.value)}"`;
16076
16100
  case "time":
16077
16101
  return `responds within ${check2.maxMs} ms`;
16102
+ case "schema": {
16103
+ const title = schemaTitle(check2.schema);
16104
+ return title ? `body matches schema "${clip(title)}"` : "body matches the JSON schema";
16105
+ }
16078
16106
  }
16079
16107
  }
16080
16108
  function clip(text) {
@@ -16082,26 +16110,86 @@ function clip(text) {
16082
16110
  }
16083
16111
  function jsonPathLabel(path) {
16084
16112
  const trimmed = path.trim().replace(/^\$\.?/, "");
16085
- return `$.${trimmed}`;
16113
+ return trimmed ? `$.${trimmed}` : "$";
16114
+ }
16115
+ function schemaTitle(schema) {
16116
+ try {
16117
+ const parsed = JSON.parse(schema);
16118
+ return typeof parsed?.title === "string" && parsed.title.trim() ? parsed.title.trim() : void 0;
16119
+ } catch {
16120
+ return void 0;
16121
+ }
16122
+ }
16123
+ function lengthRule(value) {
16124
+ const m = /^\s*(>=|<=|!=|>|<|=)?\s*(\d+)\s*$/.exec(value);
16125
+ if (!m) return { label: `is ${value.trim() || "?"}` };
16126
+ const op = m[1] ?? "=";
16127
+ return { label: `${op === "=" ? "=" : op === "!=" ? "\u2260" : op === ">=" ? "\u2265" : op === "<=" ? "\u2264" : op} ${m[2]}` };
16086
16128
  }
16087
16129
  function checksNeedBody(checks) {
16088
- return checks.some((c) => c.type === "body" || c.type === "json");
16130
+ return checks.some((c) => c.type === "body" || c.type === "json" || c.type === "schema");
16089
16131
  }
16090
16132
  function evaluator() {
16091
16133
  compiled ??= new Function(`${CHECK_EVALUATOR_JS}
16092
- return __atcCheck;`)();
16134
+ return { check: __atcCheck, schemaError: __atcSchemaError, jsonGet: __atcJsonGet };`)();
16093
16135
  return compiled;
16094
16136
  }
16137
+ function explain(check2, res) {
16138
+ if (check2.type === "status") return `got ${res.status}`;
16139
+ if (check2.type === "time") return `took ${Math.round(res.durationMs)} ms`;
16140
+ if (check2.type === "header") {
16141
+ const h = res.headers[check2.name.toLowerCase()];
16142
+ return h === void 0 ? "no such header" : `is "${clip(h)}"`;
16143
+ }
16144
+ if (res.body === null) return "no body";
16145
+ if (check2.type === "body") return check2.op === "is_json" ? "not valid JSON" : void 0;
16146
+ let parsed;
16147
+ try {
16148
+ parsed = JSON.parse(res.body);
16149
+ } catch {
16150
+ return "the body is not JSON";
16151
+ }
16152
+ if (check2.type === "schema") {
16153
+ let schema;
16154
+ try {
16155
+ schema = JSON.parse(check2.schema);
16156
+ } catch {
16157
+ return "the schema is not valid JSON";
16158
+ }
16159
+ return evaluator().schemaError(schema, parsed, "", schema, 0) ?? void 0;
16160
+ }
16161
+ if (check2.type === "json") {
16162
+ const got = evaluator().jsonGet(parsed, check2.path);
16163
+ if (!got.found) return check2.op === "not_exists" ? void 0 : "not found";
16164
+ const v = got.value;
16165
+ if (check2.op === "type") return `is ${v === null ? "null" : Array.isArray(v) ? "an array" : typeof v === "object" ? "an object" : `a ${typeof v}`}`;
16166
+ if (check2.op === "length") {
16167
+ const n = typeof v === "string" || Array.isArray(v) ? v.length : v && typeof v === "object" ? Object.keys(v).length : void 0;
16168
+ return n === void 0 ? "has no length" : `length is ${n}`;
16169
+ }
16170
+ if (check2.op === "not_exists") return "it exists";
16171
+ const text = typeof v === "string" ? v : JSON.stringify(v);
16172
+ return `is ${clip(text ?? "")}`;
16173
+ }
16174
+ return void 0;
16175
+ }
16095
16176
  function evaluateChecks(checks, res) {
16096
- const run2 = evaluator();
16177
+ const run2 = evaluator().check;
16097
16178
  return checks.map((check2) => {
16098
16179
  let ok = false;
16099
16180
  try {
16100
- ok = Boolean(run2(check2, res));
16181
+ ok = Boolean(run2({ ...check2 }, res));
16101
16182
  } catch {
16102
16183
  ok = false;
16103
16184
  }
16104
- return { name: checkLabel(check2), ok };
16185
+ if (ok) return { name: checkLabel(check2), ok };
16186
+ let detail;
16187
+ try {
16188
+ detail = explain(check2, res);
16189
+ } catch {
16190
+ detail = void 0;
16191
+ }
16192
+ return detail ? { name: checkLabel(check2), ok, detail } : { name: checkLabel(check2), ok };
16105
16193
  });
16106
16194
  }
16107
16195
  var responseCheckSchema, CHECK_EVALUATOR_JS, compiled;
@@ -16111,20 +16199,28 @@ var init_checks3 = __esm({
16111
16199
  init_zod();
16112
16200
  responseCheckSchema = external_exports.discriminatedUnion("type", [
16113
16201
  external_exports.object({ type: external_exports.literal("status"), expect: external_exports.string().min(1).max(100) }).strict(),
16114
- external_exports.object({ type: external_exports.literal("body"), op: external_exports.enum(["contains", "not_contains"]), value: external_exports.string().min(1).max(2e3) }).strict(),
16202
+ external_exports.object({
16203
+ type: external_exports.literal("body"),
16204
+ op: external_exports.enum(["contains", "not_contains", "matches", "is_json"]),
16205
+ // Not used by is_json.
16206
+ value: external_exports.string().max(2e3).default("")
16207
+ }).strict(),
16115
16208
  external_exports.object({
16116
16209
  type: external_exports.literal("json"),
16117
16210
  path: external_exports.string().min(1).max(300),
16118
- op: external_exports.enum(["exists", "equals", "not_equals", "contains"]),
16211
+ op: external_exports.enum(["exists", "not_exists", "equals", "not_equals", "contains", "greater", "less", "matches", "type", "length"]),
16119
16212
  value: external_exports.string().max(2e3).default("")
16120
16213
  }).strict(),
16121
16214
  external_exports.object({
16122
16215
  type: external_exports.literal("header"),
16123
16216
  name: external_exports.string().min(1).max(200),
16124
- op: external_exports.enum(["exists", "equals", "contains"]),
16217
+ op: external_exports.enum(["exists", "not_exists", "equals", "contains", "matches"]),
16125
16218
  value: external_exports.string().max(2e3).default("")
16126
16219
  }).strict(),
16127
- external_exports.object({ type: external_exports.literal("time"), maxMs: external_exports.number().positive().max(6e5) }).strict()
16220
+ external_exports.object({ type: external_exports.literal("time"), maxMs: external_exports.number().positive().max(6e5) }).strict(),
16221
+ // A JSON Schema (draft 7 subset) the whole body must match.
16222
+ // Kept while still empty (being written); only a JSON object is sent.
16223
+ external_exports.object({ type: external_exports.literal("schema"), schema: external_exports.string().max(2e5) }).strict()
16128
16224
  ]);
16129
16225
  CHECK_EVALUATOR_JS = String.raw`
16130
16226
  function __atcStatusOk(expect, status) {
@@ -16159,27 +16255,167 @@ function __atcAsText(value) {
16159
16255
  if (value === undefined) return "";
16160
16256
  return JSON.stringify(value);
16161
16257
  }
16258
+ function __atcTypeOf(value) {
16259
+ if (value === null) return "null";
16260
+ if (Array.isArray(value)) return "array";
16261
+ return typeof value;
16262
+ }
16263
+ function __atcIsType(value, type) {
16264
+ if (type === "integer") return typeof value === "number" && isFinite(value) && Math.floor(value) === value;
16265
+ if (type === "number") return typeof value === "number" && isFinite(value);
16266
+ return __atcTypeOf(value) === type;
16267
+ }
16268
+ function __atcMatches(pattern, text) {
16269
+ try { return new RegExp(pattern).test(text); } catch (e) { return false; }
16270
+ }
16271
+ function __atcLengthOk(rule, n) {
16272
+ var m = String(rule).match(/^\s*(>=|<=|!=|>|<|=)?\s*(\d+)\s*$/);
16273
+ if (!m) return false;
16274
+ var want = Number(m[2]);
16275
+ var op = m[1] || "=";
16276
+ if (op === ">") return n > want;
16277
+ if (op === "<") return n < want;
16278
+ if (op === ">=") return n >= want;
16279
+ if (op === "<=") return n <= want;
16280
+ if (op === "!=") return n !== want;
16281
+ return n === want;
16282
+ }
16283
+ function __atcPathText(path) { return path === "" ? "$" : "$" + path; }
16284
+ // JSON Schema, the parts APIs use: type (and nullable), enum, const, properties, required,
16285
+ // additionalProperties, items, min/max (Length, Items, imum), pattern, allOf/anyOf/oneOf,
16286
+ // local $ref. Returns the first problem as "$.items[0].id should be number", or null.
16287
+ function __atcSchemaError(schema, value, path, root, depth) {
16288
+ if (schema === true || schema === undefined || schema === null) return null;
16289
+ if (schema === false) return __atcPathText(path) + " should not be there";
16290
+ if (depth > 60) return null;
16291
+ if (typeof schema.$ref === "string") {
16292
+ var ref = schema.$ref.replace(/^#\/?/, "").split("/");
16293
+ var target = root;
16294
+ for (var r = 0; r < ref.length && target; r++) if (ref[r]) target = target[ref[r].replace(/~1/g, "/").replace(/~0/g, "~")];
16295
+ return target ? __atcSchemaError(target, value, path, root, depth + 1) : null;
16296
+ }
16297
+ var where = __atcPathText(path);
16298
+ if (schema.type !== undefined) {
16299
+ var types = Array.isArray(schema.type) ? schema.type : [schema.type];
16300
+ if (schema.nullable === true) types = types.concat(["null"]);
16301
+ var okType = false;
16302
+ for (var t = 0; t < types.length; t++) if (__atcIsType(value, types[t])) okType = true;
16303
+ if (!okType) return where + " should be " + types.join(" or ") + " (it is " + __atcTypeOf(value) + ")";
16304
+ }
16305
+ if (schema.enum && Array.isArray(schema.enum)) {
16306
+ var inEnum = false;
16307
+ for (var e = 0; e < schema.enum.length; e++) if (JSON.stringify(schema.enum[e]) === JSON.stringify(value)) inEnum = true;
16308
+ if (!inEnum) return where + " should be one of " + JSON.stringify(schema.enum);
16309
+ }
16310
+ if (schema.const !== undefined && JSON.stringify(schema.const) !== JSON.stringify(value)) return where + " should be " + JSON.stringify(schema.const);
16311
+ if (typeof value === "string") {
16312
+ if (typeof schema.minLength === "number" && value.length < schema.minLength) return where + " should be at least " + schema.minLength + " characters";
16313
+ if (typeof schema.maxLength === "number" && value.length > schema.maxLength) return where + " should be at most " + schema.maxLength + " characters";
16314
+ if (typeof schema.pattern === "string" && !__atcMatches(schema.pattern, value)) return where + " should match /" + schema.pattern + "/";
16315
+ }
16316
+ if (typeof value === "number") {
16317
+ if (typeof schema.minimum === "number" && value < schema.minimum) return where + " should be at least " + schema.minimum;
16318
+ if (typeof schema.maximum === "number" && value > schema.maximum) return where + " should be at most " + schema.maximum;
16319
+ if (typeof schema.exclusiveMinimum === "number" && value <= schema.exclusiveMinimum) return where + " should be more than " + schema.exclusiveMinimum;
16320
+ if (typeof schema.exclusiveMaximum === "number" && value >= schema.exclusiveMaximum) return where + " should be less than " + schema.exclusiveMaximum;
16321
+ }
16322
+ if (Array.isArray(value)) {
16323
+ if (typeof schema.minItems === "number" && value.length < schema.minItems) return where + " should have at least " + schema.minItems + " items";
16324
+ if (typeof schema.maxItems === "number" && value.length > schema.maxItems) return where + " should have at most " + schema.maxItems + " items";
16325
+ if (schema.items && typeof schema.items === "object" && !Array.isArray(schema.items)) {
16326
+ for (var i = 0; i < value.length; i++) {
16327
+ var itemErr = __atcSchemaError(schema.items, value[i], path + "[" + i + "]", root, depth + 1);
16328
+ if (itemErr) return itemErr;
16329
+ }
16330
+ }
16331
+ }
16332
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
16333
+ var req = Array.isArray(schema.required) ? schema.required : [];
16334
+ for (var q = 0; q < req.length; q++) if (!Object.prototype.hasOwnProperty.call(value, req[q])) return where + " should have " + req[q];
16335
+ var props = schema.properties && typeof schema.properties === "object" ? schema.properties : {};
16336
+ for (var key in value) {
16337
+ if (!Object.prototype.hasOwnProperty.call(value, key)) continue;
16338
+ var sub = path + (/^[A-Za-z_$][\w$]*$/.test(key) ? "." + key : "[" + JSON.stringify(key) + "]");
16339
+ if (Object.prototype.hasOwnProperty.call(props, key)) {
16340
+ var propErr = __atcSchemaError(props[key], value[key], sub, root, depth + 1);
16341
+ if (propErr) return propErr;
16342
+ } else if (schema.additionalProperties === false) {
16343
+ return __atcPathText(sub) + " is not expected";
16344
+ } else if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
16345
+ var addErr = __atcSchemaError(schema.additionalProperties, value[key], sub, root, depth + 1);
16346
+ if (addErr) return addErr;
16347
+ }
16348
+ }
16349
+ }
16350
+ if (Array.isArray(schema.allOf)) {
16351
+ for (var a = 0; a < schema.allOf.length; a++) {
16352
+ var allErr = __atcSchemaError(schema.allOf[a], value, path, root, depth + 1);
16353
+ if (allErr) return allErr;
16354
+ }
16355
+ }
16356
+ var some = schema.anyOf || schema.oneOf;
16357
+ if (Array.isArray(some) && some.length) {
16358
+ var first = null;
16359
+ var matched = 0;
16360
+ for (var s = 0; s < some.length; s++) {
16361
+ var someErr = __atcSchemaError(some[s], value, path, root, depth + 1);
16362
+ if (!someErr) matched++;
16363
+ else if (!first) first = someErr;
16364
+ }
16365
+ if (!matched) return first;
16366
+ if (schema.oneOf && matched > 1) return where + " matches more than one of oneOf";
16367
+ }
16368
+ return null;
16369
+ }
16370
+ function __atcParsedSchema(check) {
16371
+ if (check.__parsed === undefined) {
16372
+ try { check.__parsed = JSON.parse(check.schema); } catch (e) { check.__parsed = null; }
16373
+ }
16374
+ return check.__parsed;
16375
+ }
16162
16376
  function __atcCheck(check, res) {
16163
16377
  if (check.type === "status") return __atcStatusOk(check.expect, res.status);
16164
16378
  if (check.type === "time") return res.durationMs <= check.maxMs;
16165
16379
  if (check.type === "header") {
16166
16380
  var h = res.headers[String(check.name).toLowerCase()];
16167
16381
  if (check.op === "exists") return h !== undefined;
16382
+ if (check.op === "not_exists") return h === undefined;
16168
16383
  if (h === undefined) return false;
16384
+ if (check.op === "matches") return __atcMatches(check.value, h);
16169
16385
  return check.op === "equals" ? h === check.value : h.indexOf(check.value) >= 0;
16170
16386
  }
16171
16387
  if (res.body === null || res.body === undefined) return false;
16172
16388
  if (check.type === "body") {
16389
+ if (check.op === "is_json") { try { JSON.parse(res.body); return true; } catch (e) { return false; } }
16390
+ if (check.op === "matches") return __atcMatches(check.value, res.body);
16173
16391
  var has = res.body.indexOf(check.value) >= 0;
16174
16392
  return check.op === "contains" ? has : !has;
16175
16393
  }
16394
+ var parsed;
16395
+ try { parsed = JSON.parse(res.body); } catch (e) { return false; }
16396
+ if (check.type === "schema") {
16397
+ var schema = __atcParsedSchema(check);
16398
+ return schema !== null && __atcSchemaError(schema, parsed, "", schema, 0) === null;
16399
+ }
16176
16400
  if (check.type === "json") {
16177
- var parsed;
16178
- try { parsed = JSON.parse(res.body); } catch (e) { return false; }
16179
16401
  var got = __atcJsonGet(parsed, check.path);
16180
16402
  if (check.op === "exists") return got.found;
16403
+ if (check.op === "not_exists") return !got.found;
16181
16404
  if (!got.found) return check.op === "not_equals";
16405
+ if (check.op === "type") return __atcIsType(got.value, String(check.value).trim().toLowerCase());
16406
+ if (check.op === "length") {
16407
+ var v = got.value;
16408
+ var n = typeof v === "string" || Array.isArray(v) ? v.length : v !== null && typeof v === "object" ? Object.keys(v).length : -1;
16409
+ return n >= 0 && __atcLengthOk(check.value, n);
16410
+ }
16411
+ if (check.op === "greater" || check.op === "less") {
16412
+ var num = Number(got.value);
16413
+ var lim = Number(check.value);
16414
+ if (got.value === null || got.value === "" || typeof got.value === "boolean" || !isFinite(num) || String(check.value).trim() === "" || !isFinite(lim)) return false;
16415
+ return check.op === "greater" ? num > lim : num < lim;
16416
+ }
16182
16417
  var text = __atcAsText(got.value);
16418
+ if (check.op === "matches") return __atcMatches(check.value, text);
16183
16419
  if (check.op === "equals") return text === check.value;
16184
16420
  if (check.op === "not_equals") return text !== check.value;
16185
16421
  return text.indexOf(check.value) >= 0;
@@ -16603,31 +16839,32 @@ var init_definition = __esm({
16603
16839
  });
16604
16840
 
16605
16841
  // ../shared/src/chain.ts
16606
- function fillText(text, vars, mode = "raw") {
16842
+ function fillText(text, vars, mode = "raw", builtins = true) {
16607
16843
  fillFn ??= new Function(`${FILL_JS}
16608
16844
  return __atcFill;`)();
16609
- return fillFn(text, vars, mode);
16845
+ return fillFn(text, vars, mode, builtins);
16610
16846
  }
16611
- function fillDeep(value, vars) {
16612
- if (typeof value === "string") return fillText(value, vars, "raw");
16613
- if (Array.isArray(value)) return value.map((v) => fillDeep(v, vars));
16847
+ function fillDeep(value, vars, builtins) {
16848
+ if (typeof value === "string") return fillText(value, vars, "raw", builtins);
16849
+ if (Array.isArray(value)) return value.map((v) => fillDeep(v, vars, builtins));
16614
16850
  if (value && typeof value === "object") {
16615
- return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, fillDeep(v, vars)]));
16851
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, fillDeep(v, vars, builtins)]));
16616
16852
  }
16617
16853
  return value;
16618
16854
  }
16619
- function fillLab(lab, vars) {
16620
- if (!Object.keys(vars).length) return lab;
16621
- const raw = (s) => fillText(s, vars, "raw");
16855
+ function fillLab(lab, vars, { builtins = true } = {}) {
16856
+ if (!Object.keys(vars).length && !builtins) return lab;
16857
+ const raw = (s) => fillText(s, vars, "raw", builtins);
16858
+ const deep = (v) => fillDeep(v, vars, builtins);
16622
16859
  const { request: request3, auth } = lab;
16623
16860
  const body = request3.body;
16624
- const filledBody = body.mode === "json" ? { ...body, json: fillDeep(body.json, vars) } : body.mode === "text" ? { ...body, text: raw(body.text) } : body.mode === "form" ? { ...body, fields: body.fields.map((f) => ({ key: raw(f.key), value: raw(f.value) })) } : body.mode === "raw" ? { ...body, raw: raw(body.raw) } : body;
16861
+ const filledBody = body.mode === "json" ? { ...body, json: deep(body.json) } : body.mode === "text" ? { ...body, text: raw(body.text) } : body.mode === "form" ? { ...body, fields: body.fields.map((f) => ({ key: raw(f.key), value: raw(f.value) })) } : body.mode === "raw" ? { ...body, raw: raw(body.raw) } : body;
16625
16862
  const filledAuth = auth.mode === "bearer" ? { ...auth, token: raw(auth.token) } : auth.mode === "basic" ? { ...auth, username: raw(auth.username), password: raw(auth.password) } : auth.mode === "api_key" ? { ...auth, name: raw(auth.name), value: raw(auth.value) } : auth;
16626
16863
  return {
16627
16864
  ...lab,
16628
16865
  request: {
16629
16866
  ...request3,
16630
- url: fillText(request3.url, vars, "url"),
16867
+ url: fillText(request3.url, vars, "url", builtins),
16631
16868
  query: request3.query.map((q) => ({ key: raw(q.key), value: raw(q.value) })),
16632
16869
  headers: request3.headers.map((h) => ({ key: h.key, value: raw(h.value) })),
16633
16870
  body: filledBody
@@ -16679,12 +16916,58 @@ var init_chain = __esm({
16679
16916
  init_checks3();
16680
16917
  init_definition();
16681
16918
  FILL_JS = String.raw`
16682
- function __atcFill(text, vars, mode) {
16919
+ var __ATC_FIRST = ["Alex", "Maria", "Sam", "Priya", "Omar", "Lena", "Kofi", "Yuki", "Diego", "Emma", "Noah", "Aisha", "Ivan", "Sofia", "Liam", "Mei"];
16920
+ var __ATC_LAST = ["Smith", "Garcia", "Kim", "Patel", "Muller", "Silva", "Nguyen", "Okafor", "Rossi", "Cohen", "Tanaka", "Novak", "Brown", "Khan", "Lopez", "Berg"];
16921
+ function __atcRand(n) { return Math.floor(Math.random() * n); }
16922
+ function __atcRandText(len, chars) {
16923
+ var out = "";
16924
+ for (var i = 0; i < len; i++) out += chars.charAt(__atcRand(chars.length));
16925
+ return out;
16926
+ }
16927
+ function __atcBuiltin(name, arg) {
16928
+ var args = arg === undefined || String(arg).trim() === "" ? [] : String(arg).split(",");
16929
+ for (var i = 0; i < args.length; i++) args[i] = Number(String(args[i]).trim());
16930
+ switch (name.slice(1).toLowerCase()) {
16931
+ case "uuid": case "guid": case "randomuuid":
16932
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { var r = __atcRand(16); return (c === "x" ? r : (r & 3) | 8).toString(16); });
16933
+ case "timestamp": return String(Math.floor(Date.now() / 1000));
16934
+ case "timestampms": return String(Date.now());
16935
+ case "isotimestamp": return new Date().toISOString();
16936
+ case "isodate": return new Date().toISOString().slice(0, 10);
16937
+ case "randomint": {
16938
+ var lo = args.length > 1 ? args[0] : 0;
16939
+ var hi = args.length > 1 ? args[1] : args.length ? args[0] : 1000;
16940
+ if (!isFinite(lo) || !isFinite(hi)) return undefined;
16941
+ var min = Math.ceil(Math.min(lo, hi)), max = Math.floor(Math.max(lo, hi));
16942
+ return String(min + __atcRand(max - min + 1));
16943
+ }
16944
+ case "randomstring": {
16945
+ var len = args.length && isFinite(args[0]) ? Math.max(1, Math.min(1000, Math.floor(args[0]))) : 8;
16946
+ return __atcRandText(len, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
16947
+ }
16948
+ case "randomemail": return "user." + __atcRandText(8, "abcdefghijklmnopqrstuvwxyz0123456789") + "@example.com";
16949
+ case "randomfirstname": return __ATC_FIRST[__atcRand(__ATC_FIRST.length)];
16950
+ case "randomlastname": return __ATC_LAST[__atcRand(__ATC_LAST.length)];
16951
+ case "randomfullname": return __ATC_FIRST[__atcRand(__ATC_FIRST.length)] + " " + __ATC_LAST[__atcRand(__ATC_LAST.length)];
16952
+ case "randomboolean": return Math.random() < 0.5 ? "true" : "false";
16953
+ }
16954
+ return undefined;
16955
+ }
16956
+ function __atcFill(text, vars, mode, builtins) {
16683
16957
  if (typeof text !== "string") return text;
16684
- return text.replace(/\{\{\s*([A-Za-z_][\w.-]*)\s*\}\}|%7B%7B([A-Za-z_][\w.-]*)%7D%7D/gi, function (whole, a, b) {
16958
+ return text.replace(/\{\{\s*(\$?[A-Za-z_][\w.-]*)(?:\(([^(){}]*)\))?\s*\}\}|%7B%7B(\$?[A-Za-z_][\w.-]*)(?:\(([^(){}]*)\))?%7D%7D/gi, function (whole, a, aArg, b, bArg) {
16685
16959
  var name = a || b;
16686
- if (!Object.prototype.hasOwnProperty.call(vars, name)) return whole;
16687
- var v = String(vars[name]);
16960
+ var arg = a ? aArg : bArg;
16961
+ var v;
16962
+ if (name.charAt(0) === "$") {
16963
+ if (builtins === false) return whole;
16964
+ if (b && arg !== undefined) { try { arg = decodeURIComponent(arg); } catch (e) {} }
16965
+ v = __atcBuiltin(name, arg);
16966
+ if (v === undefined) return whole;
16967
+ } else {
16968
+ if (arg !== undefined || !Object.prototype.hasOwnProperty.call(vars, name)) return whole;
16969
+ v = String(vars[name]);
16970
+ }
16688
16971
  if (mode === "url") return encodeURIComponent(v);
16689
16972
  if (mode === "json") return JSON.stringify(v).slice(1, -1);
16690
16973
  return v;
@@ -16696,6 +16979,32 @@ function __atcFill(text, vars, mode) {
16696
16979
  }
16697
16980
  });
16698
16981
 
16982
+ // ../shared/src/codegen.ts
16983
+ function parseSetCookie(header) {
16984
+ const [pair, ...attrs] = header.split(";");
16985
+ const eq = pair?.indexOf("=") ?? -1;
16986
+ if (!pair || eq <= 0) return null;
16987
+ const cookie = { name: pair.slice(0, eq).trim(), value: pair.slice(eq + 1).trim(), httpOnly: false, secure: false };
16988
+ for (const attr of attrs) {
16989
+ const i = attr.indexOf("=");
16990
+ const key = (i < 0 ? attr : attr.slice(0, i)).trim().toLowerCase();
16991
+ const value = i < 0 ? "" : attr.slice(i + 1).trim();
16992
+ if (key === "domain") cookie.domain = value;
16993
+ else if (key === "path") cookie.path = value;
16994
+ else if (key === "expires") cookie.expires = value;
16995
+ else if (key === "max-age" && /^-?\d+$/.test(value)) cookie.maxAge = Number(value);
16996
+ else if (key === "httponly") cookie.httpOnly = true;
16997
+ else if (key === "secure") cookie.secure = true;
16998
+ else if (key === "samesite") cookie.sameSite = value;
16999
+ }
17000
+ return cookie;
17001
+ }
17002
+ var init_codegen = __esm({
17003
+ "../shared/src/codegen.ts"() {
17004
+ "use strict";
17005
+ }
17006
+ });
17007
+
16699
17008
  // ../../node_modules/.pnpm/yaml@2.9.1/node_modules/yaml/dist/nodes/identity.js
16700
17009
  var require_identity = __commonJS({
16701
17010
  "../../node_modules/.pnpm/yaml@2.9.1/node_modules/yaml/dist/nodes/identity.js"(exports) {
@@ -24098,6 +24407,10 @@ function defaultRequestDraft() {
24098
24407
  params: [],
24099
24408
  headers: [],
24100
24409
  bodyMode: "none",
24410
+ graphqlQuery: "",
24411
+ graphqlVariables: "",
24412
+ graphqlOperation: "",
24413
+ graphqlErrorsFail: true,
24101
24414
  bodyText: "",
24102
24415
  authMode: "inherit",
24103
24416
  oauth: defaultOAuth(),
@@ -24115,7 +24428,9 @@ function defaultRequestDraft() {
24115
24428
  maxRedirects: 5,
24116
24429
  connectionReuse: true,
24117
24430
  useCookies: true,
24118
- tlsSkip: false,
24431
+ // On for new requests, so test servers with their own certificates just work. Saved requests
24432
+ // keep what they have; one saved without the setting checks certificates (as before).
24433
+ tlsSkip: true,
24119
24434
  checks: [],
24120
24435
  before: null,
24121
24436
  testData: null,
@@ -24157,7 +24472,7 @@ function requestFromUnknown(value) {
24157
24472
  const base = defaultRequestDraft();
24158
24473
  if (!value || typeof value !== "object") return base;
24159
24474
  const item = value;
24160
- const bodyMode = item.bodyMode === "json" || item.bodyMode === "text" ? item.bodyMode : "none";
24475
+ const bodyMode = item.bodyMode === "json" || item.bodyMode === "text" || item.bodyMode === "graphql" ? item.bodyMode : "none";
24161
24476
  const authMode2 = item.authMode === "bearer" || item.authMode === "basic" || item.authMode === "api_key" || item.authMode === "inherit" || item.authMode === "oauth2" || item.authMode === "digest" || item.authMode === "ntlm" || item.authMode === "aws_sigv4" || item.authMode === "jwt" || item.authMode === "oauth1" ? item.authMode : item.authMode === void 0 ? "inherit" : "none";
24162
24477
  return {
24163
24478
  method: METHODS.includes(item.method) ? item.method : "GET",
@@ -24168,6 +24483,10 @@ function requestFromUnknown(value) {
24168
24483
  headers: rows(item.headers),
24169
24484
  bodyMode,
24170
24485
  bodyText: str(item.bodyText, ""),
24486
+ graphqlQuery: str(item.graphqlQuery, ""),
24487
+ graphqlVariables: str(item.graphqlVariables, ""),
24488
+ graphqlOperation: str(item.graphqlOperation, ""),
24489
+ graphqlErrorsFail: item.graphqlErrorsFail !== false,
24171
24490
  authMode: authMode2,
24172
24491
  oauth: oauthFromUnknown(item.oauth),
24173
24492
  oauth1: oauth1FromUnknown(item.oauth1),
@@ -24298,8 +24617,21 @@ function requestParts(draft) {
24298
24617
  } catch (err) {
24299
24618
  const bare = draftRuntimeNames(draft).find((n) => new RegExp(`(^|[^"])\\{\\{\\s*${n.replace(/[.-]/g, "\\$&")}\\s*\\}\\}(?!")`).test(draft.bodyText));
24300
24619
  if (bare) throw new Error(`Put quotes around {{${bare}}} in the JSON body \u2014 values from test data and run-first steps are filled in as text`);
24620
+ const builtin = /(^|[^"])\{\{\s*(\$[A-Za-z]+)/.exec(draft.bodyText)?.[2];
24621
+ if (builtin) throw new Error(`Put quotes around {{${builtin}}} in the JSON body \u2014 built-in values are filled in as text`);
24301
24622
  throw new Error(`Body is not valid JSON: ${err instanceof Error ? err.message : "parse error"}`);
24302
24623
  }
24624
+ } else if (hasBody(draft.method) && draft.bodyMode === "graphql") {
24625
+ if (!draft.graphqlQuery.trim()) throw new Error("Write the GraphQL query");
24626
+ let variables = {};
24627
+ if (draft.graphqlVariables.trim()) {
24628
+ try {
24629
+ variables = JSON.parse(draft.graphqlVariables);
24630
+ } catch (err) {
24631
+ throw new Error(`GraphQL variables are not valid JSON: ${err instanceof Error ? err.message : "parse error"} \u2014 put quotes around "{{values}}" from test data`);
24632
+ }
24633
+ }
24634
+ body = { mode: "json", json: { query: draft.graphqlQuery, variables, ...draft.graphqlOperation.trim() ? { operationName: draft.graphqlOperation.trim() } : {} } };
24303
24635
  } else if (hasBody(draft.method) && draft.bodyMode === "text") {
24304
24636
  body = { mode: "text", text: draft.bodyText };
24305
24637
  }
@@ -24311,7 +24643,11 @@ function requestParts(draft) {
24311
24643
  headers: draft.headers.filter((h) => h.key.trim()),
24312
24644
  body,
24313
24645
  expectedStatus: [],
24314
- checks: usableChecks(draft.checks)
24646
+ // GraphQL answers 200 even when the query failed; "errors" in the answer is the failure.
24647
+ checks: [
24648
+ ...usableChecks(draft.checks),
24649
+ ...hasBody(draft.method) && draft.bodyMode === "graphql" && draft.graphqlErrorsFail ? [{ type: "json", path: "errors", op: "not_exists", value: "" }] : []
24650
+ ]
24315
24651
  },
24316
24652
  auth,
24317
24653
  options: {
@@ -24324,12 +24660,21 @@ function requestParts(draft) {
24324
24660
  }
24325
24661
  };
24326
24662
  }
24663
+ function isJsonObject(text) {
24664
+ try {
24665
+ const v = JSON.parse(text);
24666
+ return Boolean(v) && typeof v === "object" && !Array.isArray(v);
24667
+ } catch {
24668
+ return false;
24669
+ }
24670
+ }
24327
24671
  function usableChecks(checks) {
24328
24672
  return checks.filter((c) => {
24329
24673
  if (c.type === "status") return c.expect.trim().length > 0;
24330
- if (c.type === "body") return c.value.length > 0;
24674
+ if (c.type === "body") return c.op === "is_json" || c.value.length > 0;
24331
24675
  if (c.type === "json") return c.path.trim().length > 0;
24332
24676
  if (c.type === "header") return c.name.trim().length > 0;
24677
+ if (c.type === "schema") return isJsonObject(c.schema);
24333
24678
  return c.maxMs > 0;
24334
24679
  });
24335
24680
  }
@@ -24462,6 +24807,8 @@ var init_draft = __esm({
24462
24807
  "params",
24463
24808
  "headers",
24464
24809
  "bodyText",
24810
+ "graphqlQuery",
24811
+ "graphqlVariables",
24465
24812
  "token",
24466
24813
  "username",
24467
24814
  "password",
@@ -24604,10 +24951,10 @@ function sustainedFrom(buckets) {
24604
24951
  function percentileFromHist(hist, p) {
24605
24952
  const total = hist.reduce((a, n) => a + n, 0);
24606
24953
  if (!total) return null;
24607
- let seen = 0;
24954
+ let seen2 = 0;
24608
24955
  for (let i = 0; i < hist.length; i++) {
24609
- seen += hist[i];
24610
- if (seen / total >= p) {
24956
+ seen2 += hist[i];
24957
+ if (seen2 / total >= p) {
24611
24958
  const edge = LATENCY_BIN_EDGES_MS[i];
24612
24959
  return Number.isFinite(edge) ? edge : LATENCY_BIN_EDGES_MS[LATENCY_BIN_EDGES_MS.length - 2];
24613
24960
  }
@@ -25137,7 +25484,7 @@ function stepOutcome(r) {
25137
25484
  if (!failed.length) return { state: "passed" };
25138
25485
  return {
25139
25486
  state: "failed",
25140
- message: `${failed.length} of ${r.checks.length} check${r.checks.length === 1 ? "" : "s"} failed: ${failed.map((c) => c.name).join("; ")}`
25487
+ message: `${failed.length} of ${r.checks.length} check${r.checks.length === 1 ? "" : "s"} failed: ${failed.map((c) => c.detail ? `${c.name} (${c.detail})` : c.name).join("; ")}`
25141
25488
  };
25142
25489
  }
25143
25490
  if (r.status >= 200 && r.status < 400) return { state: "passed" };
@@ -25160,16 +25507,16 @@ function suitePassed(run2) {
25160
25507
  }
25161
25508
  function orderSuite(items, plan) {
25162
25509
  const byId = new Map(items.map((i) => [i.id, i]));
25163
- const seen = /* @__PURE__ */ new Set();
25510
+ const seen2 = /* @__PURE__ */ new Set();
25164
25511
  const out = [];
25165
25512
  for (const id of plan?.order ?? []) {
25166
25513
  const item = byId.get(id);
25167
- if (item && !seen.has(id)) {
25514
+ if (item && !seen2.has(id)) {
25168
25515
  out.push(item);
25169
- seen.add(id);
25516
+ seen2.add(id);
25170
25517
  }
25171
25518
  }
25172
- for (const item of items) if (!seen.has(item.id)) out.push(item);
25519
+ for (const item of items) if (!seen2.has(item.id)) out.push(item);
25173
25520
  const skip = new Set(plan?.skip ?? []);
25174
25521
  return out.map((item) => ({ item, skipped: skip.has(item.id) }));
25175
25522
  }
@@ -25679,6 +26026,13 @@ function __atcOAuth1(method, url, headers, body) {
25679
26026
  }
25680
26027
  });
25681
26028
 
26029
+ // ../shared/src/graphql.ts
26030
+ var init_graphql = __esm({
26031
+ "../shared/src/graphql.ts"() {
26032
+ "use strict";
26033
+ }
26034
+ });
26035
+
25682
26036
  // ../shared/src/export-fields.ts
25683
26037
  function roundN(n, digits = 2) {
25684
26038
  if (!Number.isFinite(n)) return 0;
@@ -26627,6 +26981,7 @@ var init_src = __esm({
26627
26981
  init_dashboard();
26628
26982
  init_workspace();
26629
26983
  init_chain();
26984
+ init_codegen();
26630
26985
  init_import();
26631
26986
  init_email_report();
26632
26987
  init_junit();
@@ -26636,6 +26991,7 @@ var init_src = __esm({
26636
26991
  init_ci_templates();
26637
26992
  init_sigv4();
26638
26993
  init_oauth1();
26994
+ init_graphql();
26639
26995
  init_latency_bins();
26640
26996
  init_checks3();
26641
26997
  init_definition();
@@ -26738,7 +27094,7 @@ token, password, or key.
26738
27094
  async scan() {
26739
27095
  await this.ensure();
26740
27096
  let changed = false;
26741
- const seen = /* @__PURE__ */ new Set();
27097
+ const seen2 = /* @__PURE__ */ new Set();
26742
27098
  for (const folder of ["runs", "compares"]) {
26743
27099
  let names = [];
26744
27100
  try {
@@ -26748,7 +27104,7 @@ token, password, or key.
26748
27104
  }
26749
27105
  for (const name of names) {
26750
27106
  const key = `${folder}/${name}`;
26751
- seen.add(key);
27107
+ seen2.add(key);
26752
27108
  let info;
26753
27109
  try {
26754
27110
  info = await stat(join(this.folder(folder), name));
@@ -26762,7 +27118,7 @@ token, password, or key.
26762
27118
  }
26763
27119
  }
26764
27120
  for (const key of [...this.entries.keys()]) {
26765
- if (!seen.has(key)) {
27121
+ if (!seen2.has(key)) {
26766
27122
  this.entries.delete(key);
26767
27123
  changed = true;
26768
27124
  }
@@ -27149,6 +27505,11 @@ function generateK6Script(run2, extra2 = {}) {
27149
27505
  ATC_NAME: run2.name,
27150
27506
  ATC_CHECKS: JSON.stringify(run2.request.checks.map((check2) => ({ label: checkLabel(check2), check: check2 })))
27151
27507
  };
27508
+ if (run2.request.checks.some((c) => c.type === "schema")) {
27509
+ files["checks.json"] = env.ATC_CHECKS;
27510
+ env.ATC_CHECKS = "";
27511
+ env.ATC_CHECKS_FILE = "1";
27512
+ }
27152
27513
  if (extra2.tlsAuth?.length) env.ATC_TLS_AUTH = JSON.stringify(extra2.tlsAuth);
27153
27514
  if (extra2.jwt) {
27154
27515
  const { tokens, ...rest } = extra2.jwt;
@@ -27179,15 +27540,14 @@ function generateK6Script(run2, extra2 = {}) {
27179
27540
  env.ATC_AUTH_USER = run2.auth.mode === "ntlm" && run2.auth.domain ? `${run2.auth.domain}\\${run2.auth.username}` : run2.auth.username;
27180
27541
  env.ATC_AUTH_PASS = run2.auth.password;
27181
27542
  }
27543
+ const bodyMode = run2.request.body.mode;
27544
+ env.ATC_BODY_FILL = bodyMode === "json" ? "json" : bodyMode === "form" ? "url" : "raw";
27182
27545
  if (data) {
27183
27546
  files["data.json"] = JSON.stringify(data.rows);
27184
27547
  env.ATC_DATA_COLUMNS = JSON.stringify(data.columns);
27185
27548
  env.ATC_DATA_ORDER = data.order;
27186
- const mode = run2.request.body.mode;
27187
- env.ATC_BODY_FILL = mode === "json" ? "json" : mode === "form" ? "url" : "raw";
27188
27549
  }
27189
27550
  const dataBlock = data ? `
27190
- ${FILL_JS}
27191
27551
  const ROWS = new SharedArray("rows", function () { return JSON.parse(open("./data.json")); });
27192
27552
  const COLUMNS = JSON.parse(__ENV.ATC_DATA_COLUMNS);
27193
27553
  function __atcRow() {
@@ -27211,12 +27571,12 @@ import { SharedArray } from "k6/data";
27211
27571
  export const options = Object.assign(${optionsBlock(run2)}, __ENV.ATC_TLS_AUTH ? { tlsAuth: JSON.parse(__ENV.ATC_TLS_AUTH) } : {});
27212
27572
 
27213
27573
  ${CHECK_EVALUATOR_JS}
27214
- ${dataBlock}${run2.auth.mode === "aws_sigv4" ? `
27574
+ ${FILL_JS}${dataBlock}${run2.auth.mode === "aws_sigv4" ? `
27215
27575
  const AWS = JSON.parse(__ENV.ATC_AWS);
27216
27576
  ${SIGV4_JS}` : ""}${run2.auth.mode === "oauth1" ? `
27217
27577
  const OAUTH1 = JSON.parse(__ENV.ATC_OAUTH1);
27218
27578
  ${OAUTH1_JS}` : ""}
27219
- const CHECKS = JSON.parse(__ENV.ATC_CHECKS || "[]");
27579
+ const CHECKS = __ENV.ATC_CHECKS_FILE ? JSON.parse(open("./checks.json")) : JSON.parse(__ENV.ATC_CHECKS || "[]");
27220
27580
  // JWT: tokens signed by the app for the whole run; each is used for its stretch of time.
27221
27581
  const JWT = __ENV.ATC_JWT ? JSON.parse(__ENV.ATC_JWT) : null;
27222
27582
  const JWTS = JWT ? JSON.parse(open("./jwt.json")) : [];
@@ -27244,7 +27604,9 @@ function __atcFreshToken() {
27244
27604
  }
27245
27605
 
27246
27606
  export default function () {
27247
- ${data ? 'const vars = __atcRow();\n const f = function (text, mode) { return __atcFill(text, vars, mode || "raw"); };' : "const f = function (text) { return text; };"}
27607
+ // Test-data values, and built-in values ({{$uuid}}, \u2026) made anew for every request.
27608
+ const vars = ${data ? "__atcRow()" : "{}"};
27609
+ const f = function (text, mode) { return __atcFill(text, vars, mode || "raw"); };
27248
27610
  const headers = JSON.parse(__ENV.ATC_HEADERS || "{}");
27249
27611
  for (const key in headers) headers[key] = f(headers[key]);
27250
27612
  if (__ENV.ATC_COOKIE) headers.Cookie = __ENV.ATC_COOKIE;
@@ -45340,13 +45702,13 @@ function toPem(m) {
45340
45702
  }
45341
45703
  async function k6TlsAuth(urls) {
45342
45704
  const out = [];
45343
- const seen = /* @__PURE__ */ new Set();
45705
+ const seen2 = /* @__PURE__ */ new Set();
45344
45706
  for (const target of urls) {
45345
45707
  const m = await certificateFor(target);
45346
45708
  if (!m) continue;
45347
45709
  const domain2 = new URL(target).hostname;
45348
- if (seen.has(domain2)) continue;
45349
- seen.add(domain2);
45710
+ if (seen2.has(domain2)) continue;
45711
+ seen2.add(domain2);
45350
45712
  const pem = toPem(m);
45351
45713
  out.push({ domains: [domain2], cert: pem.cert, key: pem.key, ...pem.password ? { password: pem.password } : {} });
45352
45714
  }
@@ -45451,7 +45813,7 @@ function defaultPath(url2) {
45451
45813
  if (!p.startsWith("/") || p.lastIndexOf("/") === 0) return "/";
45452
45814
  return p.slice(0, p.lastIndexOf("/"));
45453
45815
  }
45454
- function parseSetCookie(header, url2, now = Date.now()) {
45816
+ function parseSetCookie2(header, url2, now = Date.now()) {
45455
45817
  const [pair, ...attrs] = header.split(";");
45456
45818
  const eq = pair.indexOf("=");
45457
45819
  if (eq <= 0) return null;
@@ -45502,7 +45864,7 @@ async function storeCookies(target, setCookie) {
45502
45864
  const now = Date.now();
45503
45865
  let next = [...await load()];
45504
45866
  for (const header of Array.isArray(setCookie) ? setCookie : [setCookie]) {
45505
- const c = parseSetCookie(header, url2, now);
45867
+ const c = parseSetCookie2(header, url2, now);
45506
45868
  if (!c) continue;
45507
45869
  next = next.filter((o) => !(o.name === c.name && o.domain === c.domain && o.path === c.path));
45508
45870
  if (c.expires === null || c.expires > now) next.push(c);
@@ -69211,7 +69573,7 @@ var require_parse = __commonJS({
69211
69573
  var { maxNameValuePairSize, maxAttributeValueSize } = require_constants4();
69212
69574
  var { isCTLExcludingHtab } = require_util5();
69213
69575
  var assert2 = __require("node:assert");
69214
- function parseSetCookie2(header) {
69576
+ function parseSetCookie3(header) {
69215
69577
  if (isCTLExcludingHtab(header)) {
69216
69578
  return null;
69217
69579
  }
@@ -69335,7 +69697,7 @@ var require_parse = __commonJS({
69335
69697
  return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList);
69336
69698
  }
69337
69699
  module.exports = {
69338
- parseSetCookie: parseSetCookie2,
69700
+ parseSetCookie: parseSetCookie3,
69339
69701
  parseUnparsedAttributes
69340
69702
  };
69341
69703
  }
@@ -69345,7 +69707,7 @@ var require_parse = __commonJS({
69345
69707
  var require_cookies = __commonJS({
69346
69708
  "../../node_modules/.pnpm/undici@8.10.0/node_modules/undici/lib/web/cookies/index.js"(exports, module) {
69347
69709
  "use strict";
69348
- var { parseSetCookie: parseSetCookie2 } = require_parse();
69710
+ var { parseSetCookie: parseSetCookie3 } = require_parse();
69349
69711
  var { stringify } = require_util5();
69350
69712
  var { webidl } = require_webidl();
69351
69713
  var { Headers } = require_headers();
@@ -69384,11 +69746,11 @@ var require_cookies = __commonJS({
69384
69746
  if (!cookies) {
69385
69747
  return [];
69386
69748
  }
69387
- return cookies.map((pair) => parseSetCookie2(pair));
69749
+ return cookies.map((pair) => parseSetCookie3(pair));
69388
69750
  }
69389
69751
  function parseCookie(cookie) {
69390
69752
  cookie = webidl.converters.DOMString(cookie);
69391
- return parseSetCookie2(cookie);
69753
+ return parseSetCookie3(cookie);
69392
69754
  }
69393
69755
  function setCookie(headers, cookie) {
69394
69756
  webidl.argumentLengthCheck(arguments, 2, "setCookie");
@@ -73116,7 +73478,7 @@ function parseLab(input) {
73116
73478
  return labRequestSchema.parse(input);
73117
73479
  }
73118
73480
  async function runSetupStep(setup) {
73119
- const sent = await sendOnce(setup);
73481
+ const sent = await sendOnce(fillLab(setup, {}));
73120
73482
  const vars = {};
73121
73483
  const values = [];
73122
73484
  let problem = sent.result.error ? `could not reach it: ${sent.result.error}` : void 0;
@@ -73162,6 +73524,17 @@ async function runProbe(input) {
73162
73524
  const saved = parsed.save?.length && !sent.result.error ? takeValues(parsed.save, sent) : void 0;
73163
73525
  return { ...sent.result, setup, dataRow: parsed.data ? { row: 1, of: parsed.data.rows.length } : void 0, saved };
73164
73526
  }
73527
+ async function probeFull(input) {
73528
+ const parsed = parseLab(input);
73529
+ let vars = {};
73530
+ if (parsed.setup) {
73531
+ const step = await runSetupStep(parsed.setup);
73532
+ if (!step.result.ok) return { status: 0, body: "", error: step.result.error };
73533
+ vars = step.vars;
73534
+ }
73535
+ const sent = await sendOnce(fillLab(parsed, vars));
73536
+ return { status: sent.result.status, body: sent.raw.slice(0, 2e7), error: sent.result.error };
73537
+ }
73165
73538
  function takeValues(rules, sent) {
73166
73539
  const res = { status: sent.result.status, headers: sent.lowered, body: sent.raw, durationMs: sent.result.durationMs };
73167
73540
  return rules.map((rule) => {
@@ -73174,6 +73547,12 @@ function takeValues(rules, sent) {
73174
73547
  });
73175
73548
  }
73176
73549
  async function sendOnce(lab) {
73550
+ const out = await sendOnceAuthed(lab);
73551
+ const note = AUTH_NOTES[lab.auth.mode];
73552
+ if (note && out.result.sent) out.result.sent.authNote = note;
73553
+ return out;
73554
+ }
73555
+ async function sendOnceAuthed(lab) {
73177
73556
  if (lab.auth.mode === "digest" || lab.auth.mode === "ntlm") return sendChallenged(lab, lab.auth);
73178
73557
  if (lab.auth.mode === "jwt") {
73179
73558
  const { mode: _mode, ...config3 } = lab.auth;
@@ -73251,6 +73630,7 @@ async function sendRaw(lab, extra2 = {}) {
73251
73630
  headers[pair.key] = pair.value;
73252
73631
  }
73253
73632
  const secretNames = applyAuth(headers, url2, lab.auth);
73633
+ const secretQuery = lab.auth.mode === "api_key" && lab.auth.in === "query" ? [lab.auth.name] : [];
73254
73634
  if (extra2.authorization) {
73255
73635
  headers.authorization = extra2.authorization;
73256
73636
  secretNames.push("authorization");
@@ -73267,7 +73647,10 @@ async function sendRaw(lab, extra2 = {}) {
73267
73647
  if (signed.authorization) {
73268
73648
  headers.authorization = signed.authorization;
73269
73649
  secretNames.push("authorization");
73270
- } else target = new URL(signed.url);
73650
+ } else {
73651
+ target = new URL(signed.url);
73652
+ secretQuery.push("oauth_signature", "oauth_token");
73653
+ }
73271
73654
  }
73272
73655
  if (lab.auth.mode === "aws_sigv4") {
73273
73656
  const { mode: _mode, ...creds } = lab.auth;
@@ -73281,6 +73664,8 @@ async function sendRaw(lab, extra2 = {}) {
73281
73664
  const agent = extra2.dispatcher ?? new import_undici2.Agent({ connect: await tlsConnect(url2, options.tlsSkipVerify) });
73282
73665
  const dispatcher = extra2.dispatcher ?? (useJar ? agent : agent.compose(import_undici2.interceptors.redirect({ maxRedirections: options.maxRedirects })));
73283
73666
  const started = performance.now();
73667
+ let sent;
73668
+ const cookies = [];
73284
73669
  try {
73285
73670
  let current = target;
73286
73671
  let method = lab.request.method;
@@ -73292,9 +73677,16 @@ async function sendRaw(lab, extra2 = {}) {
73292
73677
  const jar2 = await cookieHeader(current);
73293
73678
  if (jar2) sendHeaders.cookie = jar2;
73294
73679
  }
73680
+ if (!sent) sent = describeSent(method, current, sendHeaders, body, [...secretNames, ...ALWAYS_SECRET], secretQuery, options.tlsSkipVerify);
73681
+ else (sent.redirects ??= []).push(`${method} ${current.toString()}`);
73295
73682
  response = await (0, import_undici2.request)(current, { method, headers: sendHeaders, body, headersTimeout: timeout, bodyTimeout: timeout, dispatcher });
73683
+ const setCookie = response.headers["set-cookie"];
73684
+ for (const line of typeof setCookie === "string" ? [setCookie] : setCookie ?? []) {
73685
+ const parsed = parseSetCookie(line);
73686
+ if (parsed) cookies.push(parsed);
73687
+ }
73296
73688
  if (!useJar) break;
73297
- await storeCookies(current, response.headers["set-cookie"]);
73689
+ await storeCookies(current, setCookie);
73298
73690
  const location = response.headers.location;
73299
73691
  if (![301, 302, 303, 307, 308].includes(response.statusCode) || typeof location !== "string" || hops >= options.maxRedirects) break;
73300
73692
  await response.body.dump();
@@ -73309,8 +73701,11 @@ async function sendRaw(lab, extra2 = {}) {
73309
73701
  }
73310
73702
  current = next;
73311
73703
  }
73312
- const raw = await response.body.text();
73704
+ const bytes2 = Buffer.from(await response.body.arrayBuffer());
73705
+ const raw = bytes2.toString("utf8");
73313
73706
  const durationMs = Math.round((performance.now() - started) * 10) / 10;
73707
+ const responseType = String(response.headers["content-type"] ?? "").toLowerCase();
73708
+ const bodyBase64 = /^image\/(png|jpe?g|gif|webp|bmp|svg\+xml|x-icon|vnd\.microsoft\.icon|avif)/.test(responseType) && bytes2.length <= IMAGE_LIMIT ? bytes2.toString("base64") : void 0;
73314
73709
  const truncated = raw.length > BODY_LIMIT;
73315
73710
  const headerBag = {};
73316
73711
  for (const [key, value] of Object.entries(response.headers)) {
@@ -73331,7 +73726,10 @@ async function sendRaw(lab, extra2 = {}) {
73331
73726
  truncated,
73332
73727
  durationMs,
73333
73728
  url: url2.toString(),
73334
- checks
73729
+ checks,
73730
+ sent,
73731
+ ...cookies.length ? { cookies } : {},
73732
+ ...bodyBase64 ? { bodyBase64 } : {}
73335
73733
  }
73336
73734
  };
73337
73735
  } catch (err) {
@@ -73348,14 +73746,29 @@ async function sendRaw(lab, extra2 = {}) {
73348
73746
  truncated: false,
73349
73747
  durationMs,
73350
73748
  url: url2.toString(),
73351
- error: err instanceof Error ? err.message : String(err)
73749
+ error: err instanceof Error ? err.message : String(err),
73750
+ sent
73352
73751
  }
73353
73752
  };
73354
73753
  } finally {
73355
73754
  if (!extra2.dispatcher) await dispatcher.close();
73356
73755
  }
73357
73756
  }
73358
- var import_undici2, BODY_LIMIT;
73757
+ function describeSent(method, url2, headers, body, secretHeaders, secretQuery, insecure) {
73758
+ const list = [["host", url2.host], ...Object.entries(headers)];
73759
+ if (body !== void 0) list.push(["content-length", String(Buffer.byteLength(body))]);
73760
+ return {
73761
+ method,
73762
+ url: url2.toString(),
73763
+ headers: list,
73764
+ ...body !== void 0 ? { body: body.length > BODY_LIMIT ? body.slice(0, BODY_LIMIT) : body, bodyTruncated: body.length > BODY_LIMIT || void 0 } : {},
73765
+ secretHeaders: [...new Set(secretHeaders.map((h) => h.toLowerCase()))],
73766
+ secretQuery,
73767
+ // Only https has a certificate to skip.
73768
+ insecure: insecure && url2.protocol === "https:" || void 0
73769
+ };
73770
+ }
73771
+ var import_undici2, BODY_LIMIT, IMAGE_LIMIT, ALWAYS_SECRET, AUTH_NOTES;
73359
73772
  var init_probe = __esm({
73360
73773
  "../../apps/engine/src/probe.ts"() {
73361
73774
  "use strict";
@@ -73369,6 +73782,16 @@ var init_probe = __esm({
73369
73782
  init_cookies();
73370
73783
  init_oauth();
73371
73784
  BODY_LIMIT = 64 * 1024;
73785
+ IMAGE_LIMIT = 5 * 1024 * 1024;
73786
+ ALWAYS_SECRET = ["authorization", "proxy-authorization", "cookie", "x-api-key", "api-key", "x-amz-security-token"];
73787
+ AUTH_NOTES = {
73788
+ oauth2: "OAuth 2.0 \u2014 the app got the token and sent it as a Bearer token",
73789
+ oauth1: "OAuth 1.0 \u2014 signed for this one request (nonce and timestamp)",
73790
+ aws_sigv4: "AWS Signature \u2014 signed for this one request",
73791
+ jwt: "JWT \u2014 signed by the app for this request",
73792
+ digest: "Digest \u2014 answers the server's 401 challenge; this is the second request",
73793
+ ntlm: "NTLM \u2014 signs in over one connection; this is the last of three requests"
73794
+ };
73372
73795
  }
73373
73796
  });
73374
73797
 
@@ -73409,10 +73832,10 @@ var init_latency_histogram = __esm({
73409
73832
  percentile(p) {
73410
73833
  if (this.count === 0) return 0;
73411
73834
  const rank = Math.ceil(p / 100 * this.count);
73412
- let seen = 0;
73835
+ let seen2 = 0;
73413
73836
  for (const bin of [...this.bins.keys()].sort((a, b) => a - b)) {
73414
- seen += this.bins.get(bin);
73415
- if (seen >= rank) {
73837
+ seen2 += this.bins.get(bin);
73838
+ if (seen2 >= rank) {
73416
73839
  const mid = FLOOR_MS * Math.exp((bin + 0.5) * GROWTH);
73417
73840
  return Math.min(this.max, Math.max(this.min, mid));
73418
73841
  }
@@ -73923,7 +74346,7 @@ async function startRun(lab, repoRoot2) {
73923
74346
  err.statusCode = 422;
73924
74347
  throw err;
73925
74348
  }
73926
- filled = fillLab(lab, step.vars);
74349
+ filled = fillLab(lab, step.vars, { builtins: false });
73927
74350
  }
73928
74351
  const id = crypto.randomUUID();
73929
74352
  const dir2 = await mkdtemp(join5(tmpdir2(), "atc-run-"));
@@ -74273,7 +74696,7 @@ function explainMissing(message, index, savers, steps) {
74273
74696
  });
74274
74697
  return hints.length ? hints.join("; ") : message;
74275
74698
  }
74276
- async function runStep(step, workspace, vars, envName, setups, explain = (m) => m) {
74699
+ async function runStep(step, workspace, vars, envName, setups, explain2 = (m) => m) {
74277
74700
  const saved = await workspace.getRequest(step.id);
74278
74701
  if (!saved) throw new StepProblem("This request is no longer saved");
74279
74702
  if (saved.secretsMissing?.length) {
@@ -74283,7 +74706,7 @@ async function runStep(step, workspace, vars, envName, setups, explain = (m) =>
74283
74706
  try {
74284
74707
  draft = await resolveSaved(workspace, saved, vars, envName);
74285
74708
  } catch (err) {
74286
- throw new StepProblem(explain(err instanceof Error ? err.message.replace(/ — pick one next to the URL$/, " \u2014 pick an environment for this run") : String(err)));
74709
+ throw new StepProblem(explain2(err instanceof Error ? err.message.replace(/ — pick one next to the URL$/, " \u2014 pick an environment for this run") : String(err)));
74287
74710
  }
74288
74711
  const payload = buildPayload(draft, toLoadConfig(DEFAULT_LOAD), saved.name);
74289
74712
  let runtime = {};
@@ -74309,23 +74732,23 @@ async function runStep(step, workspace, vars, envName, setups, explain = (m) =>
74309
74732
  runtime = done.vars;
74310
74733
  }
74311
74734
  if (payload.data) runtime = { ...runtime, ...firstRowVars(payload.data) };
74312
- const probe = await runProbe({ ...fillLab({ request: payload.request, auth: payload.auth, options: payload.options }, runtime), save: payload.save });
74313
- const outcome = stepOutcome(probe);
74735
+ const probe2 = await runProbe({ ...fillLab({ request: payload.request, auth: payload.auth, options: payload.options }, runtime), save: payload.save });
74736
+ const outcome = stepOutcome(probe2);
74314
74737
  step.state = outcome.state;
74315
74738
  step.message = outcome.message;
74316
- if (probe.saved?.length) {
74317
- step.saved = probe.saved.map((v) => ({ name: v.name, found: v.found, preview: v.preview }));
74318
- for (const v of probe.saved) if (v.found && v.value !== void 0) vars[v.name] = v.value;
74319
- const missing = probe.saved.filter((v) => !v.found);
74739
+ if (probe2.saved?.length) {
74740
+ step.saved = probe2.saved.map((v) => ({ name: v.name, found: v.found, preview: v.preview }));
74741
+ for (const v of probe2.saved) if (v.found && v.value !== void 0) vars[v.name] = v.value;
74742
+ const missing = probe2.saved.filter((v) => !v.found);
74320
74743
  if (missing.length && step.state === "passed") {
74321
74744
  step.state = "failed";
74322
74745
  step.message = missing.map((v) => `{{${v.name}}} was not found in the answer (looked for ${v.from})`).join("; ");
74323
74746
  }
74324
74747
  }
74325
- step.status = probe.status || void 0;
74326
- step.durationMs = probe.durationMs;
74327
- step.checks = probe.checks;
74328
- if (outcome.state !== "passed" && probe.body) step.bodyPreview = probe.body.slice(0, BODY_PREVIEW_CHARS);
74748
+ step.status = probe2.status || void 0;
74749
+ step.durationMs = probe2.durationMs;
74750
+ step.checks = probe2.checks;
74751
+ if (outcome.state !== "passed" && probe2.body) step.bodyPreview = probe2.body.slice(0, BODY_PREVIEW_CHARS);
74329
74752
  }
74330
74753
  function registerSuiteRoutes(api, workspace, plans, builtins) {
74331
74754
  api.get("/suites/plan", async (request3) => {
@@ -81412,32 +81835,32 @@ var require_err_helpers = __commonJS({
81412
81835
  return isErrorLike(cause) ? cause : void 0;
81413
81836
  }
81414
81837
  };
81415
- var _stackWithCauses = (err, seen) => {
81838
+ var _stackWithCauses = (err, seen2) => {
81416
81839
  if (!isErrorLike(err)) return "";
81417
81840
  const stack = err.stack || "";
81418
- if (seen.has(err)) {
81841
+ if (seen2.has(err)) {
81419
81842
  return stack + "\ncauses have become circular...";
81420
81843
  }
81421
81844
  const cause = getErrorCause(err);
81422
81845
  if (cause) {
81423
- seen.add(err);
81424
- return stack + "\ncaused by: " + _stackWithCauses(cause, seen);
81846
+ seen2.add(err);
81847
+ return stack + "\ncaused by: " + _stackWithCauses(cause, seen2);
81425
81848
  } else {
81426
81849
  return stack;
81427
81850
  }
81428
81851
  };
81429
81852
  var stackWithCauses = (err) => _stackWithCauses(err, /* @__PURE__ */ new Set());
81430
- var _messageWithCauses = (err, seen, skip) => {
81853
+ var _messageWithCauses = (err, seen2, skip) => {
81431
81854
  if (!isErrorLike(err)) return "";
81432
81855
  const message = skip ? "" : err.message || "";
81433
- if (seen.has(err)) {
81856
+ if (seen2.has(err)) {
81434
81857
  return message + ": ...";
81435
81858
  }
81436
81859
  const cause = getErrorCause(err);
81437
81860
  if (cause) {
81438
- seen.add(err);
81861
+ seen2.add(err);
81439
81862
  const skipIfVErrorStyleCause = typeof err.cause === "function";
81440
- return message + (skipIfVErrorStyleCause ? "" : ": ") + _messageWithCauses(cause, seen, skipIfVErrorStyleCause);
81863
+ return message + (skipIfVErrorStyleCause ? "" : ": ") + _messageWithCauses(cause, seen2, skipIfVErrorStyleCause);
81441
81864
  } else {
81442
81865
  return message;
81443
81866
  }
@@ -81456,7 +81879,7 @@ var require_err_helpers = __commonJS({
81456
81879
  var require_err_proto = __commonJS({
81457
81880
  "../../node_modules/.pnpm/pino-std-serializers@7.1.0/node_modules/pino-std-serializers/lib/err-proto.js"(exports, module) {
81458
81881
  "use strict";
81459
- var seen = /* @__PURE__ */ Symbol("circular-ref-tag");
81882
+ var seen2 = /* @__PURE__ */ Symbol("circular-ref-tag");
81460
81883
  var rawSymbol = /* @__PURE__ */ Symbol("pino-raw-err-ref");
81461
81884
  var pinoErrProto = Object.create({}, {
81462
81885
  type: {
@@ -81496,7 +81919,7 @@ var require_err_proto = __commonJS({
81496
81919
  module.exports = {
81497
81920
  pinoErrProto,
81498
81921
  pinoErrorSymbols: {
81499
- seen,
81922
+ seen: seen2,
81500
81923
  rawSymbol
81501
81924
  }
81502
81925
  };
@@ -81510,13 +81933,13 @@ var require_err = __commonJS({
81510
81933
  module.exports = errSerializer;
81511
81934
  var { messageWithCauses, stackWithCauses, isErrorLike } = require_err_helpers();
81512
81935
  var { pinoErrProto, pinoErrorSymbols } = require_err_proto();
81513
- var { seen } = pinoErrorSymbols;
81936
+ var { seen: seen2 } = pinoErrorSymbols;
81514
81937
  var { toString } = Object.prototype;
81515
81938
  function errSerializer(err) {
81516
81939
  if (!isErrorLike(err)) {
81517
81940
  return err;
81518
81941
  }
81519
- err[seen] = void 0;
81942
+ err[seen2] = void 0;
81520
81943
  const _err = Object.create(pinoErrProto);
81521
81944
  _err.type = toString.call(err.constructor) === "[object Function]" ? err.constructor.name : err.name;
81522
81945
  _err.message = messageWithCauses(err);
@@ -81528,7 +81951,7 @@ var require_err = __commonJS({
81528
81951
  if (_err[key] === void 0) {
81529
81952
  const val = err[key];
81530
81953
  if (isErrorLike(val)) {
81531
- if (key !== "cause" && !Object.prototype.hasOwnProperty.call(val, seen)) {
81954
+ if (key !== "cause" && !Object.prototype.hasOwnProperty.call(val, seen2)) {
81532
81955
  _err[key] = errSerializer(val);
81533
81956
  }
81534
81957
  } else {
@@ -81536,7 +81959,7 @@ var require_err = __commonJS({
81536
81959
  }
81537
81960
  }
81538
81961
  }
81539
- delete err[seen];
81962
+ delete err[seen2];
81540
81963
  _err.raw = err;
81541
81964
  return _err;
81542
81965
  }
@@ -81550,13 +81973,13 @@ var require_err_with_cause = __commonJS({
81550
81973
  module.exports = errWithCauseSerializer;
81551
81974
  var { isErrorLike } = require_err_helpers();
81552
81975
  var { pinoErrProto, pinoErrorSymbols } = require_err_proto();
81553
- var { seen } = pinoErrorSymbols;
81976
+ var { seen: seen2 } = pinoErrorSymbols;
81554
81977
  var { toString } = Object.prototype;
81555
81978
  function errWithCauseSerializer(err) {
81556
81979
  if (!isErrorLike(err)) {
81557
81980
  return err;
81558
81981
  }
81559
- err[seen] = void 0;
81982
+ err[seen2] = void 0;
81560
81983
  const _err = Object.create(pinoErrProto);
81561
81984
  _err.type = toString.call(err.constructor) === "[object Function]" ? err.constructor.name : err.name;
81562
81985
  _err.message = err.message;
@@ -81564,14 +81987,14 @@ var require_err_with_cause = __commonJS({
81564
81987
  if (Array.isArray(err.errors)) {
81565
81988
  _err.aggregateErrors = err.errors.map((err2) => errWithCauseSerializer(err2));
81566
81989
  }
81567
- if (isErrorLike(err.cause) && !Object.prototype.hasOwnProperty.call(err.cause, seen)) {
81990
+ if (isErrorLike(err.cause) && !Object.prototype.hasOwnProperty.call(err.cause, seen2)) {
81568
81991
  _err.cause = errWithCauseSerializer(err.cause);
81569
81992
  }
81570
81993
  for (const key in err) {
81571
81994
  if (_err[key] === void 0) {
81572
81995
  const val = err[key];
81573
81996
  if (isErrorLike(val)) {
81574
- if (!Object.prototype.hasOwnProperty.call(val, seen)) {
81997
+ if (!Object.prototype.hasOwnProperty.call(val, seen2)) {
81575
81998
  _err[key] = errWithCauseSerializer(val);
81576
81999
  }
81577
82000
  } else {
@@ -81579,7 +82002,7 @@ var require_err_with_cause = __commonJS({
81579
82002
  }
81580
82003
  }
81581
82004
  }
81582
- delete err[seen];
82005
+ delete err[seen2];
81583
82006
  _err.raw = err;
81584
82007
  return _err;
81585
82008
  }
@@ -108734,7 +109157,7 @@ var require_dist5 = __commonJS({
108734
109157
  exports.stringifyCookie = stringifyCookie;
108735
109158
  exports.stringifySetCookie = stringifySetCookie;
108736
109159
  exports.serialize = stringifySetCookie;
108737
- exports.parseSetCookie = parseSetCookie2;
109160
+ exports.parseSetCookie = parseSetCookie3;
108738
109161
  exports.stringifySetCookie = stringifySetCookie;
108739
109162
  exports.serialize = stringifySetCookie;
108740
109163
  var cookieNameRegExp = /^[\u0021-\u003A\u003C\u003E-\u007E]+$/;
@@ -108871,7 +109294,7 @@ var require_dist5 = __commonJS({
108871
109294
  }
108872
109295
  return str2;
108873
109296
  }
108874
- function parseSetCookie2(str2, options) {
109297
+ function parseSetCookie3(str2, options) {
108875
109298
  const dec = options?.decode || decode4;
108876
109299
  const len = str2.length;
108877
109300
  const endIdx = endIndex(str2, 0, len);
@@ -111984,7 +112407,7 @@ import http from "node:http";
111984
112407
  import https from "node:https";
111985
112408
  function run(cmd, args) {
111986
112409
  return new Promise((resolve5) => {
111987
- execFile2(cmd, args, { timeout: 4e3, windowsHide: true, maxBuffer: 8 * 1024 * 1024 }, (err, stdout) => resolve5(err && !stdout ? "" : String(stdout)));
112410
+ execFile2(cmd, args, { timeout: 2e4, windowsHide: true, maxBuffer: 8 * 1024 * 1024 }, (err, stdout) => resolve5(err && !stdout ? "" : String(stdout)));
111988
112411
  });
111989
112412
  }
111990
112413
  function local(address) {
@@ -112053,11 +112476,10 @@ function parseSs(text) {
112053
112476
  async function listPorts() {
112054
112477
  if (process.platform === "win32") {
112055
112478
  const tasks = run("tasklist", ["/FO", "CSV", "/NH"]);
112056
- const [v4, v6] = await Promise.all([run("netstat", ["-ano", "-p", "TCP"]), run("netstat", ["-ano", "-p", "TCPv6"])]);
112057
- const found2 = parseNetstat(v4);
112058
- for (const [port2, l] of parseNetstat(v6)) if (!found2.has(port2)) found2.set(port2, l);
112479
+ const found2 = parseNetstat(await run("netstat", ["-ano"]));
112059
112480
  const named = tasks.then((text) => {
112060
112481
  const names = parseTasklist(text);
112482
+ if (!names.size) return;
112061
112483
  for (const l of found2.values()) {
112062
112484
  const t = names.get(l.pid);
112063
112485
  l.name = t?.name;
@@ -112081,20 +112503,34 @@ function ask(scheme, host2, port2, verify, timeoutMs) {
112081
112503
  resolve5("yes");
112082
112504
  }
112083
112505
  );
112084
- req.on("timeout", () => req.destroy(new Error("timeout")));
112506
+ req.on("timeout", () => {
112507
+ resolve5("slow");
112508
+ req.destroy(new Error("timeout"));
112509
+ });
112085
112510
  req.on("error", (err) => resolve5(scheme === "https" && verify && /CERT|SELF_SIGNED|UNABLE_TO|SIGNATURE|HOSTNAME|ALTNAME/i.test(err.code ?? err.message) ? "cert" : "no"));
112086
112511
  req.end();
112087
112512
  });
112088
112513
  }
112089
112514
  async function identify(l, timeoutMs) {
112515
+ const before = seen.get(l.port);
112516
+ const server = await probe(l, timeoutMs);
112517
+ if (server && server !== "slow") {
112518
+ seen.set(l.port, { pid: l.pid, server });
112519
+ return server;
112520
+ }
112521
+ if (server === "slow" && before && before.pid === l.pid) return before.server;
112522
+ seen.delete(l.port);
112523
+ return null;
112524
+ }
112525
+ async function probe(l, timeoutMs) {
112090
112526
  const host2 = l.ipv4 ? "127.0.0.1" : "::1";
112091
112527
  const [plain, tls] = await Promise.all([ask("http", host2, l.port, true, timeoutMs), ask("https", host2, l.port, true, timeoutMs)]);
112092
112528
  if (plain === "yes") return { port: l.port, scheme: "http", name: l.name };
112093
112529
  if (tls === "yes") return { port: l.port, scheme: "https", name: l.name };
112094
112530
  if (tls === "cert" && await ask("https", host2, l.port, false, timeoutMs) === "yes") return { port: l.port, scheme: "https", name: l.name, selfSigned: true };
112095
- return null;
112531
+ return plain === "slow" || tls === "slow" ? "slow" : null;
112096
112532
  }
112097
- async function findLocalServers(skip = [], timeoutMs = 600) {
112533
+ async function findLocalServers(skip = [], timeoutMs = 1500) {
112098
112534
  const { ports, named } = await listPorts();
112099
112535
  const [answers] = await Promise.all([
112100
112536
  Promise.all(ports.filter((l) => !skip.includes(l.port) && !NOT_HTTP.has(l.port)).map(async (l) => ({ l, server: await identify(l, timeoutMs) }))),
@@ -112102,12 +112538,13 @@ async function findLocalServers(skip = [], timeoutMs = 600) {
112102
112538
  ]);
112103
112539
  return answers.filter(({ l, server }) => server && !l.service && !SYSTEM.has((l.name ?? "").toLowerCase())).map(({ l, server }) => ({ ...server, name: l.name })).sort((a, b) => a.port - b.port);
112104
112540
  }
112105
- var SYSTEM, NOT_HTTP;
112541
+ var SYSTEM, NOT_HTTP, seen;
112106
112542
  var init_local_servers = __esm({
112107
112543
  "../../apps/engine/src/local-servers.ts"() {
112108
112544
  "use strict";
112109
112545
  SYSTEM = /* @__PURE__ */ new Set(["system", "svchost", "lsass", "wininit", "services", "spoolsv", "launchd", "systemd", "systemd-resolved", "cupsd", "sshd", "rpcbind"]);
112110
112546
  NOT_HTTP = /* @__PURE__ */ new Set([22, 25, 53, 135, 139, 445, 1433, 1521, 3306, 3389, 5432, 5672, 6379, 9042, 11211, 27017]);
112547
+ seen = /* @__PURE__ */ new Map();
112111
112548
  }
112112
112549
  });
112113
112550
 
@@ -112123,6 +112560,8 @@ async function routes(api) {
112123
112560
  status: "ok",
112124
112561
  service: "engine",
112125
112562
  version: appVersion,
112563
+ // "cli": started by `atc ui` — the screens say so when it stops.
112564
+ host: process.env.ATC_HOST ?? (uiDir ? "desktop" : "dev"),
112126
112565
  persistence: "files",
112127
112566
  dataDir: history.dir,
112128
112567
  k6: await detectK6(root),
@@ -112149,6 +112588,13 @@ async function routes(api) {
112149
112588
  return usage.setEnabled(request3.body.enabled);
112150
112589
  });
112151
112590
  await api.register(async (demo) => registerDemoApi(demo), { prefix: "/demo-api", logLevel: "warn" });
112591
+ api.post("/graphql/introspect", async (request3, reply) => {
112592
+ try {
112593
+ return await probeFull(request3.body);
112594
+ } catch (err) {
112595
+ return reply.code(400).send({ error: err instanceof Error ? err.message : "invalid request" });
112596
+ }
112597
+ });
112152
112598
  api.post("/probe", async (request3, reply) => {
112153
112599
  const parsed = labRequestSchema.safeParse(request3.body);
112154
112600
  if (!parsed.success) {
@@ -112201,9 +112647,11 @@ async function routes(api) {
112201
112647
  }
112202
112648
  const key = skip.join(",");
112203
112649
  if (!localCache || localCache.key !== key || Date.now() - localCache.at > 2e3) {
112204
- localCache = { at: Date.now(), key, servers: await findLocalServers(skip) };
112650
+ const entry = { at: Number.POSITIVE_INFINITY, key, servers: findLocalServers(skip) };
112651
+ void entry.servers.finally(() => entry.at = Date.now());
112652
+ localCache = entry;
112205
112653
  }
112206
- return { servers: localCache.servers };
112654
+ return { servers: await localCache.servers };
112207
112655
  });
112208
112656
  registerSuiteRoutes(api, workspace, suitePlans, (port2) => ({ practiceApi: `http://127.0.0.1:${port2}${api.prefix}/demo-api` }));
112209
112657
  onRunFinished(async (report, error51) => {
@@ -112304,7 +112752,8 @@ init_src();
112304
112752
  init_draft();
112305
112753
  init_history_store();
112306
112754
  init_k6();
112307
- import { spawn as spawn3 } from "node:child_process";
112755
+ import { spawn as spawn3, spawnSync } from "node:child_process";
112756
+ import { tmpdir as tmpdir3 } from "node:os";
112308
112757
  import { randomUUID as randomUUID7 } from "node:crypto";
112309
112758
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
112310
112759
  import { readFile as readFile10, writeFile as writeFile9 } from "node:fs/promises";
@@ -112393,11 +112842,12 @@ init_cookies();
112393
112842
  init_workspace_store();
112394
112843
  var repoRoot = resolve4(dirname3(fileURLToPath2(import.meta.url)), "../../..");
112395
112844
  var EXIT = { passed: 0, failed: 1, error: 2 };
112396
- var VERSION = "0.8.0";
112845
+ var VERSION = "0.10.0";
112397
112846
  var HELP = `APITestCenter CLI ${VERSION} \u2014 https://apitestcenter.com/docs/cli-and-ci.html
112398
112847
 
112399
112848
  Usage (apitestcenter, or the short name atc once installed)
112400
- atc ui [collection] Open the app in your browser to build and edit tests
112849
+ atc ui [collection] Open the app in a window to build and edit tests
112850
+ (--browser: a tab in your usual browser; --no-open: just print the address)
112401
112851
  atc init [collection] Start api-tests/ in this repository, with a CI pipeline
112402
112852
  atc suite <collection> [options] Send every request in a collection once and check each one
112403
112853
  atc run <request> [options] Run a load test and exit 0 (passed) / 1 (failed) / 2 (error)
@@ -112482,7 +112932,8 @@ async function main() {
112482
112932
  help: { type: "boolean", short: "h" },
112483
112933
  version: { type: "boolean", short: "v" },
112484
112934
  data: { type: "string" },
112485
- "no-open": { type: "boolean" }
112935
+ "no-open": { type: "boolean" },
112936
+ browser: { type: "boolean" }
112486
112937
  }
112487
112938
  });
112488
112939
  const [command, target] = positionals;
@@ -112500,7 +112951,7 @@ async function main() {
112500
112951
  }
112501
112952
  if (command === "init") return init(target, values.data ?? "api-tests");
112502
112953
  const dataDir = resolveDataDir();
112503
- if (command === "ui") return ui(target, dataDir, values["no-open"] === true);
112954
+ if (command === "ui") return ui(target, dataDir, values["no-open"] === true, values.browser === true);
112504
112955
  const secrets2 = new SecretStore(dataDir);
112505
112956
  const workspace = new WorkspaceStore(dataDir, secrets2);
112506
112957
  useOAuthStore(secrets2);
@@ -112618,10 +113069,10 @@ Environments (${index.environments.length})`);
112618
113069
  }
112619
113070
  if (payload.data) log(`data ${payload.data.fileName} \xB7 ${payload.data.rows.length} rows \xB7 ${payload.data.order}`);
112620
113071
  if (!values["no-check"]) {
112621
- const probe = await runProbe({ request: payload.request, auth: payload.auth, options: payload.options, setup: payload.setup, data: payload.data });
112622
- const failedChecks = probe.checks?.filter((c) => !c.ok) ?? [];
112623
- log(`check ${probe.status || "ERR"} in ${probe.durationMs} ms${probe.checks ? ` \xB7 checks ${probe.checks.length - failedChecks.length}/${probe.checks.length}` : ""}`);
112624
- if (!probe.ok || probe.status < 200 || probe.status >= 300) fail(`check request returned ${probe.status || probe.error}; load did not start (use --no-check to force)`);
113072
+ const probe2 = await runProbe({ request: payload.request, auth: payload.auth, options: payload.options, setup: payload.setup, data: payload.data });
113073
+ const failedChecks = probe2.checks?.filter((c) => !c.ok) ?? [];
113074
+ log(`check ${probe2.status || "ERR"} in ${probe2.durationMs} ms${probe2.checks ? ` \xB7 checks ${probe2.checks.length - failedChecks.length}/${probe2.checks.length}` : ""}`);
113075
+ if (!probe2.ok || probe2.status < 200 || probe2.status >= 300) fail(`check request returned ${probe2.status || probe2.error}; load did not start (use --no-check to force)`);
112625
113076
  }
112626
113077
  const history2 = values.save ? new HistoryStore(dataDir) : null;
112627
113078
  if (history2) onRunFinished(async (report2, error51) => void await history2.saveRun(report2, { source: "run", error: error51 }));
@@ -112684,9 +113135,9 @@ compared with baseline \u201C${baseline.testName}\u201D (${baseline.startedAt.sl
112684
113135
  if (history2) log(` saved to History in ${dataDir}`);
112685
113136
  return failed ? EXIT.failed : EXIT.passed;
112686
113137
  }
112687
- async function ui(collection, dataDir, noOpen) {
113138
+ async function ui(collection, dataDir, noOpen, inBrowserTab = false) {
112688
113139
  const here = dirname3(fileURLToPath2(import.meta.url));
112689
- const uiDir2 = [resolve4(here, "ui"), resolve4(repoRoot, "apps/web/.next-desktop")].find((d) => existsSync2(resolve4(d, "index.html")));
113140
+ const uiDir2 = [process.env.ATC_UI_DIR, resolve4(here, "ui"), resolve4(repoRoot, "apps/web/.next-desktop")].find((d) => Boolean(d) && existsSync2(resolve4(d, "index.html")));
112690
113141
  if (!uiDir2) fail("this build has no app screens. Use the npm package (npx apitestcenter ui), or run: pnpm --filter @apitestcenter/desktop build:ui");
112691
113142
  Object.assign(process.env, {
112692
113143
  ATC_UI_DIR: uiDir2,
@@ -112697,7 +113148,9 @@ async function ui(collection, dataDir, noOpen) {
112697
113148
  ATC_USAGE_URL: "",
112698
113149
  ATC_NO_DOTENV: "1",
112699
113150
  ATC_NO_READY_LINE: "1",
112700
- ATC_LOG_LEVEL: process.env.ATC_LOG_LEVEL ?? "warn"
113151
+ ATC_LOG_LEVEL: process.env.ATC_LOG_LEVEL ?? "warn",
113152
+ // The screens show "stopped" when this engine goes away (a browser tab can't be closed for them).
113153
+ ATC_HOST: "cli"
112701
113154
  });
112702
113155
  if (!(await resolveK6Bin(repoRoot)).available) {
112703
113156
  const cached3 = cachedK6();
@@ -112712,10 +113165,54 @@ async function ui(collection, dataDir, noOpen) {
112712
113165
  console.log(` tests in: ${dataDir}`);
112713
113166
  console.log(" Saved secrets last until you stop it \u2014 use {{variables}} for tests you commit.");
112714
113167
  console.log(" Press Ctrl+C to stop.");
112715
- if (!noOpen) openBrowser(page2);
113168
+ if (!noOpen) {
113169
+ const win = inBrowserTab ? null : openAppWindow(page2);
113170
+ if (!win) openBrowser(page2);
113171
+ else {
113172
+ console.log(" Closing the window stops it too.");
113173
+ const stop = (why) => {
113174
+ if (why) console.log(why);
113175
+ closeWindow(win);
113176
+ process.exit(EXIT.passed);
113177
+ };
113178
+ process.on("SIGINT", () => stop("\nStopped."));
113179
+ process.on("SIGTERM", () => stop(""));
113180
+ win.on("exit", () => {
113181
+ console.log("Window closed \u2014 stopped.");
113182
+ process.exit(EXIT.passed);
113183
+ });
113184
+ }
113185
+ }
112716
113186
  return new Promise(() => {
112717
113187
  });
112718
113188
  }
113189
+ function findAppBrowser() {
113190
+ const env = process.env;
113191
+ const candidates = process.platform === "win32" ? [env["ProgramFiles(x86)"], env.ProgramFiles, env.LOCALAPPDATA].flatMap(
113192
+ (root2) => root2 ? [`${root2}\\Microsoft\\Edge\\Application\\msedge.exe`, `${root2}\\Google\\Chrome\\Application\\chrome.exe`, `${root2}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`] : []
113193
+ ) : process.platform === "darwin" ? ["/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Chromium.app/Contents/MacOS/Chromium", "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"] : ["/usr/bin/microsoft-edge", "/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium", "/usr/bin/chromium-browser", "/usr/bin/brave-browser", "/snap/bin/chromium"];
113194
+ return candidates.find((p) => existsSync2(p)) ?? null;
113195
+ }
113196
+ function openAppWindow(url2) {
113197
+ const bin = findAppBrowser();
113198
+ if (!bin) return null;
113199
+ const profile = resolve4(tmpdir3(), "apitestcenter-ui-window");
113200
+ try {
113201
+ const child = spawn3(bin, [`--app=${url2}`, `--user-data-dir=${profile}`, "--no-first-run", "--no-default-browser-check", "--window-size=1440,900"], { stdio: "ignore", windowsHide: false });
113202
+ child.on("error", () => openBrowser(url2));
113203
+ return child;
113204
+ } catch {
113205
+ return null;
113206
+ }
113207
+ }
113208
+ function closeWindow(win) {
113209
+ if (win.exitCode !== null || !win.pid) return;
113210
+ try {
113211
+ if (process.platform === "win32") spawnSync("taskkill", ["/pid", String(win.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
113212
+ else win.kill("SIGTERM");
113213
+ } catch {
113214
+ }
113215
+ }
112719
113216
  function openBrowser(url2) {
112720
113217
  const [cmd, args] = process.platform === "win32" ? ["cmd", ["/c", "start", "", url2]] : process.platform === "darwin" ? ["open", [url2]] : ["xdg-open", [url2]];
112721
113218
  try {