@schwabyio/gta 0.9.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.
@@ -54452,8 +54452,8 @@ var MultipartPartSchema = external_exports.union(
54452
54452
  file: relativePath("a multipart file"),
54453
54453
  /** Absent: from the file's extension, else application/octet-stream. */
54454
54454
  contentType: external_exports.string().min(1).optional(),
54455
- /** The name sent for it; absent, the file's own. */
54456
- filename: external_exports.string().min(1).optional()
54455
+ /** The name sent for it; absent, the file's own. `''`, as a browser sends no file chosen. */
54456
+ filename: external_exports.string().optional()
54457
54457
  })
54458
54458
  ],
54459
54459
  {
@@ -54539,6 +54539,16 @@ var StepSchema = external_exports.looseObject({
54539
54539
  tags: TagsSchema.optional(),
54540
54540
  /** Feature flags this step needs; unmet, it is skipped (SPEC.md §2.9). */
54541
54541
  flags: FlagConditionsSchema.optional(),
54542
+ /**
54543
+ * Send the request once for each item of a list, read as `{{item}}` and
54544
+ * `item` in code: `'{{roots}}'`, a variable holding a JSON array (SPEC.md §2.1).
54545
+ */
54546
+ forEach: external_exports.string().min(1).optional(),
54547
+ /**
54548
+ * In a request set: the use step's own `tests` check this step's response,
54549
+ * rather than the set's last (SPEC.md §2.5). At most one step has it.
54550
+ */
54551
+ useTests: external_exports.literal(true).optional(),
54542
54552
  docs: external_exports.string().optional(),
54543
54553
  headers: HeadersSchema.optional(),
54544
54554
  body: BodySchema.optional(),
@@ -54558,6 +54568,13 @@ var StepSchema = external_exports.looseObject({
54558
54568
  }
54559
54569
  const methods = HTTP_METHODS.filter((m) => m in step);
54560
54570
  if ("use" in step) {
54571
+ if ("forEach" in step) {
54572
+ ctx.addIssue({
54573
+ code: "custom",
54574
+ path: ["forEach"],
54575
+ message: "forEach repeats one request, so a use: step cannot have it (SPEC.md \xA72.1)"
54576
+ });
54577
+ }
54561
54578
  const own2 = [...methods, ...USE_FORBIDDEN.filter((key) => key in step)];
54562
54579
  for (const key of own2) {
54563
54580
  ctx.addIssue({
@@ -54606,6 +54623,8 @@ var STEP_KEYS = /* @__PURE__ */ new Set([
54606
54623
  "tests",
54607
54624
  "tags",
54608
54625
  "flags",
54626
+ "forEach",
54627
+ "useTests",
54609
54628
  "base",
54610
54629
  "docs"
54611
54630
  ]);
@@ -54688,8 +54707,49 @@ var CollectionSchema = external_exports.strictObject({
54688
54707
  * and as `params.name` in code.
54689
54708
  */
54690
54709
  params: external_exports.record(external_exports.string(), ParamSpecSchema).optional(),
54691
- steps: external_exports.array(StepSchema).default([])
54710
+ /**
54711
+ * Steps run once before `steps`, and before every row of a data file:
54712
+ * what they set lasts the whole run (SPEC.md §2.10).
54713
+ */
54714
+ setup: external_exports.array(StepSchema).optional(),
54715
+ steps: external_exports.array(StepSchema).default([]),
54716
+ /** Steps run once after the rest, even when they failed (SPEC.md §2.10). */
54717
+ teardown: external_exports.array(StepSchema).optional()
54692
54718
  }).superRefine((collection, ctx) => {
54719
+ for (const stage of STAGES) {
54720
+ if (!collection[stage]) continue;
54721
+ if (collection.params) {
54722
+ ctx.addIssue({
54723
+ code: "custom",
54724
+ path: [stage],
54725
+ message: `a request set runs inside another collection, so it has no ${stage} (SPEC.md \xA72.10)`
54726
+ });
54727
+ }
54728
+ collection[stage].forEach((step, index) => {
54729
+ if (step.tags) {
54730
+ ctx.addIssue({
54731
+ code: "custom",
54732
+ path: [stage, index, "tags"],
54733
+ message: `a ${stage} step runs whenever its collection does, so it has no tags (SPEC.md \xA72.10)`
54734
+ });
54735
+ }
54736
+ });
54737
+ }
54738
+ const marked = collection.steps.flatMap((step, index) => step.useTests ? [index] : []);
54739
+ if (marked.length > 0 && !collection.params) {
54740
+ ctx.addIssue({
54741
+ code: "custom",
54742
+ path: ["steps", marked[0], "useTests"],
54743
+ message: "useTests marks the request set step whose response a use step's tests check; this collection has no params, so it is not a request set (SPEC.md \xA72.5)"
54744
+ });
54745
+ }
54746
+ if (marked.length > 1) {
54747
+ ctx.addIssue({
54748
+ code: "custom",
54749
+ path: ["steps", marked[1], "useTests"],
54750
+ message: `only one step can have useTests; step ${marked[0] + 1} has it already (SPEC.md \xA72.5)`
54751
+ });
54752
+ }
54693
54753
  if (collection.params) {
54694
54754
  collection.steps.forEach((step, index) => {
54695
54755
  if (isUseStep(step)) {
@@ -54719,6 +54779,8 @@ var CollectionSchema = external_exports.strictObject({
54719
54779
  }
54720
54780
  });
54721
54781
  });
54782
+ var STAGES = ["setup", "teardown"];
54783
+ var STEP_LISTS = ["setup", "steps", "teardown"];
54722
54784
  var EnvironmentVarSchema = external_exports.union([
54723
54785
  VarValueSchema,
54724
54786
  external_exports.strictObject({
@@ -54996,25 +55058,32 @@ function decodeEntities(text) {
54996
55058
  }
54997
55059
 
54998
55060
  // ../../packages/core/src/model/path.ts
55061
+ var SEGMENT = (
55062
+ // eslint-disable-next-line no-control-regex
55063
+ /\[("(?:[^"\\\u0000-\u001f]|\\["\\/bfnrt]|\\u[0-9a-fA-F]{4})*")\]|([^.[\]]+)|\[(\d*)\]/g
55064
+ );
54999
55065
  function parsePath(path12) {
55000
55066
  const segments = [];
55001
- const pattern = /([^.[\]]+)|\[(\d*)\]/g;
55002
- for (const match of path12.matchAll(pattern)) {
55003
- if (match[1] !== void 0) segments.push({ kind: "key", key: match[1] });
55004
- else if (match[2] === "") segments.push({ kind: "each" });
55005
- else segments.push({ kind: "index", index: Number(match[2]) });
55067
+ for (const match of path12.matchAll(SEGMENT)) {
55068
+ if (match[1] !== void 0) segments.push({ kind: "key", key: JSON.parse(match[1]) });
55069
+ else if (match[2] !== void 0) segments.push({ kind: "key", key: match[2] });
55070
+ else if (match[3] === "") segments.push({ kind: "each" });
55071
+ else segments.push({ kind: "index", index: Number(match[3]) });
55006
55072
  }
55007
55073
  return segments;
55008
55074
  }
55009
55075
  function formatPath(segments) {
55010
55076
  let out = "";
55011
55077
  for (const segment of segments) {
55012
- if (segment.kind === "key") out += out === "" ? segment.key : `.${segment.key}`;
55013
- else if (segment.kind === "index") out += `[${segment.index}]`;
55078
+ if (segment.kind === "key") {
55079
+ if (!plainKey(segment.key)) out += `[${JSON.stringify(segment.key)}]`;
55080
+ else out += out === "" ? segment.key : `.${segment.key}`;
55081
+ } else if (segment.kind === "index") out += `[${segment.index}]`;
55014
55082
  else out += "[]";
55015
55083
  }
55016
55084
  return out;
55017
55085
  }
55086
+ var plainKey = (key) => key !== "" && !/[.[\]]/.test(key);
55018
55087
  var isIndexKey = (key) => /^\d+$/.test(key);
55019
55088
  function resolvePath(root, pattern) {
55020
55089
  let frontier = [{ path: [], value: root }];
@@ -55156,7 +55225,7 @@ var RunErrorSchema = external_exports.object({
55156
55225
  * Which stage failed, so the UI can say where without guessing. `body` is a
55157
55226
  * file the body names that could not be read (SPEC.md §2.2).
55158
55227
  */
55159
- phase: external_exports.enum(["use", "flags", "interpolate", "body", "pre-request", "http", "tests"]),
55228
+ phase: external_exports.enum(["use", "flags", "forEach", "interpolate", "body", "pre-request", "http", "tests"]),
55160
55229
  message: external_exports.string(),
55161
55230
  code: external_exports.string().optional(),
55162
55231
  stack: external_exports.string().optional(),
@@ -55188,16 +55257,30 @@ var RunResultSchema = external_exports.object({
55188
55257
  logs: external_exports.array(LogEntrySchema).optional(),
55189
55258
  error: RunErrorSchema.nullable(),
55190
55259
  status: RunStatusSchema,
55191
- /** Why a `skipped` step did not run: a feature flag it needs (SPEC.md §2.9). */
55260
+ /**
55261
+ * Why a `skipped` step did not run: a feature flag it needs (SPEC.md §2.9),
55262
+ * `gta.skip` or `gta.skipRest` (§5), or an empty `forEach` list.
55263
+ */
55192
55264
  skipped: external_exports.object({ reason: external_exports.string() }).optional(),
55193
55265
  /** Wall-clock milliseconds for the whole item, scripts included. */
55194
55266
  durationMs: external_exports.number(),
55195
55267
  /**
55196
- * For a request run by a use step: which step of the set it was (0-based),
55197
- * of how many, and the set's file. Absent for a collection's own request.
55268
+ * For a request run by a use step: the set as `use:` names it, the use
55269
+ * step's own `name` when it has one, and which step of the set it was
55270
+ * (0-based) of how many. Absent for a collection's own request.
55198
55271
  */
55199
- use: external_exports.object({ set: external_exports.string(), child: external_exports.number(), of: external_exports.number() }).optional()
55200
- });
55272
+ use: external_exports.object({ set: external_exports.string(), name: external_exports.string().optional(), child: external_exports.number(), of: external_exports.number() }).optional(),
55273
+ /** For a `setup` or `teardown` step: which list it is in (SPEC.md §2.10). Absent for `steps`. */
55274
+ stage: external_exports.enum(["setup", "teardown"]).optional(),
55275
+ /** For a step with `forEach`: which item this request was for, from 0, of how many. */
55276
+ forEach: external_exports.object({ index: external_exports.number(), of: external_exports.number(), item: external_exports.string() }).optional()
55277
+ });
55278
+ function resultName(result) {
55279
+ const use = result.use;
55280
+ const name = !use?.name ? result.item.name : use.of === 1 ? use.name : `${use.name} \u203A ${result.item.name}`;
55281
+ const each = result.forEach ? ` (item ${result.forEach.index + 1} of ${result.forEach.of})` : "";
55282
+ return `${result.stage ? `${result.stage} \u203A ` : ""}${name}${each}`;
55283
+ }
55201
55284
  var CollectionRunSummarySchema = external_exports.object({
55202
55285
  total: external_exports.number(),
55203
55286
  passed: external_exports.number(),
@@ -55578,8 +55661,10 @@ var STEP_KEY_ORDER = [
55578
55661
  "use",
55579
55662
  "with",
55580
55663
  ...HTTP_METHODS,
55664
+ "forEach",
55581
55665
  "tags",
55582
55666
  "flags",
55667
+ "useTests",
55583
55668
  "base",
55584
55669
  "docs",
55585
55670
  "headers",
@@ -55631,116 +55716,36 @@ var runtime = defaultRuntime();
55631
55716
  // ../../packages/core/src/git/diff.ts
55632
55717
  var MAX_DIFF_BYTES = 2 * 1024 * 1024;
55633
55718
 
55634
- // ../../packages/core/src/workspace/collection.ts
55635
- import fs4 from "node:fs/promises";
55636
- import path4 from "node:path";
55637
-
55638
- // ../../packages/core/src/workspace/ids.ts
55639
- import path from "node:path";
55640
- var ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
55641
- var idOfFile = (file2) => path.basename(file2, DOC_EXTENSION);
55642
- function idProblem(doc, file2) {
55643
- const expected = idOfFile(file2);
55644
- if (!ID_PATTERN.test(expected)) {
55645
- return `${expected}${DOC_EXTENSION}: a collection's file name is its id, and an id is letters, digits and - _ . (SPEC.md \xA72)`;
55646
- }
55647
- const unusable = nameProblem(path.basename(file2));
55648
- if (unusable) return `${path.basename(file2)}: ${unusable} (SPEC.md \xA71.2)`;
55649
- if (doc.id === void 0) {
55650
- return `id: missing \u2014 a collection file says its id, its file name: id: ${expected} (SPEC.md \xA72)`;
55651
- }
55652
- if (doc.id !== expected) {
55653
- return `id: ${doc.id} does not match the file name, ${expected}${DOC_EXTENSION} \u2014 they must be the same (SPEC.md \xA72)`;
55654
- }
55655
- return null;
55656
- }
55657
- function duplicateIds(files, home) {
55658
- const byId = /* @__PURE__ */ new Map();
55659
- for (const file2 of files) {
55660
- const key = idOfFile(file2).toLowerCase();
55661
- byId.set(key, [...byId.get(key) ?? [], file2]);
55662
- }
55663
- const problems = /* @__PURE__ */ new Map();
55664
- for (const group of byId.values()) {
55665
- if (group.length < 2) continue;
55666
- for (const file2 of group) {
55667
- const others = group.filter((other) => other !== file2).map((other) => `${path.basename(home)}/${relativePosix(home, other)}`);
55668
- problems.set(
55669
- file2,
55670
- `id: ${idOfFile(file2)} is also the id of ${others.join(", ")} \u2014 an id is unique in ${path.basename(home)}/, ignoring case (SPEC.md \xA72)`
55671
- );
55672
- }
55673
- }
55674
- return problems;
55675
- }
55676
-
55677
- // ../../packages/core/src/workspace/dataFile.ts
55678
- import fs2 from "node:fs/promises";
55679
- import path2 from "node:path";
55680
- var MAX_BYTES = 10 * 1024 * 1024;
55681
- async function findDataFile(collectionFile) {
55682
- const base2 = collectionFile.endsWith(DOC_EXTENSION) ? collectionFile.slice(0, -DOC_EXTENSION.length) : collectionFile;
55683
- for (const extension of [".csv", ".json"]) {
55684
- try {
55685
- const candidate = `${base2}${extension}`;
55686
- const names = await fs2.readdir(path2.dirname(candidate));
55687
- if (names.includes(path2.basename(candidate))) return candidate;
55688
- } catch {
55689
- return null;
55690
- }
55691
- }
55692
- return null;
55693
- }
55694
- async function readDataTable(file2) {
55695
- const stat = await fs2.stat(file2);
55696
- if (stat.size > MAX_BYTES) {
55697
- throw new DataFileError(`${path2.basename(file2)}: larger than ${MAX_BYTES / 1024 / 1024} MB`);
55698
- }
55699
- const source = await fs2.readFile(file2, "utf8");
55700
- return { source, table: parseDataTable(source, path2.basename(file2), dataKindOf(file2)) };
55701
- }
55702
- var rowsOf = (table) => table.rows.map((values) => ({ values, label: rowLabel(values) }));
55703
- async function readDataFile(file2) {
55704
- const { table } = await readDataTable(file2);
55705
- const problem = dataTableProblems(table)[0];
55706
- if (problem) throw new DataFileError(`${path2.basename(file2)}: ${problem}`);
55707
- return { path: file2, rows: rowsOf(table) };
55708
- }
55709
- async function loadDataFile(collectionFile) {
55710
- const file2 = await findDataFile(collectionFile);
55711
- return file2 ? readDataFile(file2) : null;
55712
- }
55713
-
55714
55719
  // ../../packages/core/src/workspace/project.ts
55715
- import fs3 from "node:fs/promises";
55716
- import path3 from "node:path";
55720
+ import fs2 from "node:fs/promises";
55721
+ import path from "node:path";
55717
55722
  var HOMES = /* @__PURE__ */ new Set([COLLECTIONS_DIR, REQUESTS_DIR, ENDPOINTS_DIR, BASES_DIR]);
55718
55723
  function projectRootOf(collectionFile) {
55719
- const directory = path3.dirname(path3.resolve(collectionFile));
55720
- if (HOMES.has(path3.basename(directory))) return path3.dirname(directory);
55721
- const parent = path3.dirname(directory);
55722
- if (HOMES.has(path3.basename(parent))) return path3.dirname(parent);
55724
+ const directory = path.dirname(path.resolve(collectionFile));
55725
+ if (HOMES.has(path.basename(directory))) return path.dirname(directory);
55726
+ const parent = path.dirname(directory);
55727
+ if (HOMES.has(path.basename(parent))) return path.dirname(parent);
55723
55728
  return null;
55724
55729
  }
55725
55730
  function directoryOf(collectionFile) {
55726
- const directory = path3.dirname(path3.resolve(collectionFile));
55727
- return HOMES.has(path3.basename(directory)) ? null : path3.basename(directory);
55731
+ const directory = path.dirname(path.resolve(collectionFile));
55732
+ return HOMES.has(path.basename(directory)) ? null : path.basename(directory);
55728
55733
  }
55729
55734
  var isHidden = (name) => name.startsWith(".") || IGNORED_DIRECTORIES.has(name);
55730
55735
  var isCollectionFile = (name) => name.endsWith(DOC_EXTENSION) && !name.startsWith(".");
55731
55736
  async function discoverProject(root) {
55732
- const collectionsDir = path3.join(root, COLLECTIONS_DIR);
55737
+ const collectionsDir = path.join(root, COLLECTIONS_DIR);
55733
55738
  const layout = { root, collectionsDir, files: [], directories: [], problems: [] };
55734
55739
  const entries2 = await readDir(collectionsDir);
55735
55740
  for (const entry of entries2) {
55736
55741
  if (entry.isFile() && isCollectionFile(entry.name)) {
55737
- layout.files.push(path3.join(collectionsDir, entry.name));
55742
+ layout.files.push(path.join(collectionsDir, entry.name));
55738
55743
  } else if (entry.isDirectory() && !isHidden(entry.name)) {
55739
55744
  layout.directories.push(entry.name);
55740
- const directory = path3.join(collectionsDir, entry.name);
55745
+ const directory = path.join(collectionsDir, entry.name);
55741
55746
  for (const inner of await readDir(directory)) {
55742
55747
  if (inner.isFile() && isCollectionFile(inner.name)) {
55743
- layout.files.push(path3.join(directory, inner.name));
55748
+ layout.files.push(path.join(directory, inner.name));
55744
55749
  } else if (inner.isDirectory() && !isHidden(inner.name)) {
55745
55750
  layout.problems.push({
55746
55751
  path: `${entry.name}/${inner.name}`,
@@ -55780,14 +55785,14 @@ async function portabilityProblems(root) {
55780
55785
  }
55781
55786
  }
55782
55787
  for (const home of [COLLECTIONS_DIR, REQUESTS_DIR, ENDPOINTS_DIR, BASES_DIR]) {
55783
- const directories = (await readDir(path3.join(root, home))).filter((entry) => entry.isDirectory() && !isHidden(entry.name)).map((entry) => entry.name);
55788
+ const directories = (await readDir(path.join(root, home))).filter((entry) => entry.isDirectory() && !isHidden(entry.name)).map((entry) => entry.name);
55784
55789
  problems.push(...portableNameProblems(home, directories));
55785
55790
  }
55786
55791
  for (const [home, extension] of [
55787
55792
  [ENVIRONMENTS_DIR, DOC_EXTENSION],
55788
55793
  [CHECKS_DIR, ".js"]
55789
55794
  ]) {
55790
- const files = (await readDir(path3.join(root, home))).filter((entry) => entry.isFile() && entry.name.endsWith(extension)).map((entry) => entry.name);
55795
+ const files = (await readDir(path.join(root, home))).filter((entry) => entry.isFile() && entry.name.endsWith(extension)).map((entry) => entry.name);
55791
55796
  problems.push(...portableNameProblems(home, files));
55792
55797
  }
55793
55798
  return problems;
@@ -55856,7 +55861,7 @@ async function readProject(root) {
55856
55861
  async function readProjectFile(root) {
55857
55862
  let source;
55858
55863
  try {
55859
- source = await fs3.readFile(path3.join(root, PROJECT_FILE), "utf8");
55864
+ source = await fs2.readFile(path.join(root, PROJECT_FILE), "utf8");
55860
55865
  } catch {
55861
55866
  return { doc: null, source: null, problem: null };
55862
55867
  }
@@ -55874,18 +55879,18 @@ async function readProjectFile(root) {
55874
55879
  }
55875
55880
  }
55876
55881
  async function projectEnvironments(root, global) {
55877
- const own2 = await readEnvironments(path3.join(root, ENVIRONMENTS_DIR), "project");
55878
- const shared = global ? await readEnvironments(path3.join(global.root, ENVIRONMENTS_DIR), "global") : [];
55882
+ const own2 = await readEnvironments(path.join(root, ENVIRONMENTS_DIR), "project");
55883
+ const shared = global ? await readEnvironments(path.join(global.root, ENVIRONMENTS_DIR), "global") : [];
55879
55884
  return [...own2, ...shared];
55880
55885
  }
55881
55886
  async function readEnvironments(directory, source = "project") {
55882
55887
  const environments = [];
55883
55888
  for (const entry of await readDir(directory)) {
55884
55889
  if (!entry.isFile() || !entry.name.endsWith(DOC_EXTENSION)) continue;
55885
- const file2 = path3.join(directory, entry.name);
55886
- let name = path3.basename(entry.name, DOC_EXTENSION);
55890
+ const file2 = path.join(directory, entry.name);
55891
+ let name = path.basename(entry.name, DOC_EXTENSION);
55887
55892
  try {
55888
- name = parseEnvironment(await fs3.readFile(file2, "utf8"), file2).data.name ?? name;
55893
+ name = parseEnvironment(await fs2.readFile(file2, "utf8"), file2).data.name ?? name;
55889
55894
  } catch {
55890
55895
  }
55891
55896
  environments.push({ name, path: file2, source });
@@ -55894,12 +55899,92 @@ async function readEnvironments(directory, source = "project") {
55894
55899
  }
55895
55900
  async function readDir(directory) {
55896
55901
  try {
55897
- return await fs3.readdir(directory, { withFileTypes: true });
55902
+ return await fs2.readdir(directory, { withFileTypes: true });
55898
55903
  } catch {
55899
55904
  return [];
55900
55905
  }
55901
55906
  }
55902
55907
 
55908
+ // ../../packages/core/src/workspace/collection.ts
55909
+ import fs4 from "node:fs/promises";
55910
+ import path4 from "node:path";
55911
+
55912
+ // ../../packages/core/src/workspace/ids.ts
55913
+ import path2 from "node:path";
55914
+ var ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
55915
+ var idOfFile = (file2) => path2.basename(file2, DOC_EXTENSION);
55916
+ function idProblem(doc, file2) {
55917
+ const expected = idOfFile(file2);
55918
+ if (!ID_PATTERN.test(expected)) {
55919
+ return `${expected}${DOC_EXTENSION}: a collection's file name is its id, and an id is letters, digits and - _ . (SPEC.md \xA72)`;
55920
+ }
55921
+ const unusable = nameProblem(path2.basename(file2));
55922
+ if (unusable) return `${path2.basename(file2)}: ${unusable} (SPEC.md \xA71.2)`;
55923
+ if (doc.id === void 0) {
55924
+ return `id: missing \u2014 a collection file says its id, its file name: id: ${expected} (SPEC.md \xA72)`;
55925
+ }
55926
+ if (doc.id !== expected) {
55927
+ return `id: ${doc.id} does not match the file name, ${expected}${DOC_EXTENSION} \u2014 they must be the same (SPEC.md \xA72)`;
55928
+ }
55929
+ return null;
55930
+ }
55931
+ function duplicateIds(files, home) {
55932
+ const byId = /* @__PURE__ */ new Map();
55933
+ for (const file2 of files) {
55934
+ const key = idOfFile(file2).toLowerCase();
55935
+ byId.set(key, [...byId.get(key) ?? [], file2]);
55936
+ }
55937
+ const problems = /* @__PURE__ */ new Map();
55938
+ for (const group of byId.values()) {
55939
+ if (group.length < 2) continue;
55940
+ for (const file2 of group) {
55941
+ const others = group.filter((other) => other !== file2).map((other) => `${path2.basename(home)}/${relativePosix(home, other)}`);
55942
+ problems.set(
55943
+ file2,
55944
+ `id: ${idOfFile(file2)} is also the id of ${others.join(", ")} \u2014 an id is unique in ${path2.basename(home)}/, ignoring case (SPEC.md \xA72)`
55945
+ );
55946
+ }
55947
+ }
55948
+ return problems;
55949
+ }
55950
+
55951
+ // ../../packages/core/src/workspace/dataFile.ts
55952
+ import fs3 from "node:fs/promises";
55953
+ import path3 from "node:path";
55954
+ var MAX_BYTES = 10 * 1024 * 1024;
55955
+ async function findDataFile(collectionFile) {
55956
+ const base2 = collectionFile.endsWith(DOC_EXTENSION) ? collectionFile.slice(0, -DOC_EXTENSION.length) : collectionFile;
55957
+ for (const extension of [".csv", ".json"]) {
55958
+ try {
55959
+ const candidate = `${base2}${extension}`;
55960
+ const names = await fs3.readdir(path3.dirname(candidate));
55961
+ if (names.includes(path3.basename(candidate))) return candidate;
55962
+ } catch {
55963
+ return null;
55964
+ }
55965
+ }
55966
+ return null;
55967
+ }
55968
+ async function readDataTable(file2) {
55969
+ const stat = await fs3.stat(file2);
55970
+ if (stat.size > MAX_BYTES) {
55971
+ throw new DataFileError(`${path3.basename(file2)}: larger than ${MAX_BYTES / 1024 / 1024} MB`);
55972
+ }
55973
+ const source = await fs3.readFile(file2, "utf8");
55974
+ return { source, table: parseDataTable(source, path3.basename(file2), dataKindOf(file2)) };
55975
+ }
55976
+ var rowsOf = (table) => table.rows.map((values) => ({ values, label: rowLabel(values) }));
55977
+ async function readDataFile(file2) {
55978
+ const { table } = await readDataTable(file2);
55979
+ const problem = dataTableProblems(table)[0];
55980
+ if (problem) throw new DataFileError(`${path3.basename(file2)}: ${problem}`);
55981
+ return { path: file2, rows: rowsOf(table) };
55982
+ }
55983
+ async function loadDataFile(collectionFile) {
55984
+ const file2 = await findDataFile(collectionFile);
55985
+ return file2 ? readDataFile(file2) : null;
55986
+ }
55987
+
55903
55988
  // ../../packages/core/src/workspace/collection.ts
55904
55989
  async function loadCollection(file2, root) {
55905
55990
  const absolute = path4.resolve(file2);
@@ -56223,6 +56308,7 @@ async function loadEndpoints(root, global) {
56223
56308
  }
56224
56309
  function baseProblem(doc) {
56225
56310
  if (doc.steps.length > 0) return "a base collection has no steps of its own";
56311
+ if (doc.setup || doc.teardown) return "a base collection has no setup or teardown";
56226
56312
  if (doc.params) return "a base collection has no params";
56227
56313
  if (doc.extends) return "a base collection cannot extend another (SPEC.md \xA72.7)";
56228
56314
  return void 0;
@@ -56451,6 +56537,14 @@ var VariableScope = class {
56451
56537
  * `gta.flag`; null when none are known, so any flag named is unknown.
56452
56538
  */
56453
56539
  flags = null;
56540
+ /**
56541
+ * Values that last the whole collection run, across its data rows and into
56542
+ * teardown (SPEC.md §2.10): where `gta.set(…, { scope: 'run' })` writes.
56543
+ * Null outside a collection run, where there is nothing to outlast.
56544
+ */
56545
+ runValues = null;
56546
+ /** In setup and teardown, every value set lasts the run. */
56547
+ runWide = false;
56454
56548
  values = /* @__PURE__ */ new Map();
56455
56549
  origins = /* @__PURE__ */ new Map();
56456
56550
  secrets = /* @__PURE__ */ new Set();
@@ -56494,6 +56588,12 @@ var VariableScope = class {
56494
56588
  set(name, value, source = "runtime") {
56495
56589
  this.values.set(name, value);
56496
56590
  this.origins.set(name, source);
56591
+ if (this.runWide) this.runValues?.set(name, value);
56592
+ }
56593
+ /** `set`, and keep the value for every row after this one and for teardown. */
56594
+ setForRun(name, value, source = "runtime") {
56595
+ this.set(name, value, source);
56596
+ this.runValues?.set(name, value);
56497
56597
  }
56498
56598
  names() {
56499
56599
  return [...this.values.keys()].sort();
@@ -56503,35 +56603,49 @@ var VariableScope = class {
56503
56603
  return Object.fromEntries(this.values);
56504
56604
  }
56505
56605
  };
56506
- var ParamsScope = class extends VariableScope {
56507
- constructor(run, params) {
56606
+ var OverlayScope = class extends VariableScope {
56607
+ constructor(run, own2, origin) {
56508
56608
  super();
56509
56609
  this.run = run;
56510
- this.params = new Map(Object.entries(params).map(([name, value]) => [`params.${name}`, value]));
56610
+ this.own = own2;
56611
+ this.origin = origin;
56511
56612
  this.flags = run.flags;
56512
56613
  }
56513
56614
  run;
56514
- params;
56615
+ own;
56616
+ origin;
56515
56617
  has(name) {
56516
- return this.params.has(name) || this.run.has(name);
56618
+ return this.own.has(name) || this.run.has(name);
56517
56619
  }
56518
56620
  get(name) {
56519
- return this.params.has(name) ? this.params.get(name) : this.run.get(name);
56621
+ return this.own.has(name) ? this.own.get(name) : this.run.get(name);
56520
56622
  }
56521
56623
  isSecret(name) {
56522
- return !this.params.has(name) && this.run.isSecret(name);
56624
+ return !this.own.has(name) && this.run.isSecret(name);
56523
56625
  }
56524
56626
  originOf(name) {
56525
- return this.params.has(name) ? "params" : this.run.originOf(name);
56627
+ return this.own.has(name) ? this.origin : this.run.originOf(name);
56526
56628
  }
56527
56629
  set(name, value, source = "runtime") {
56528
56630
  this.run.set(name, value, source);
56529
56631
  }
56632
+ setForRun(name, value, source = "runtime") {
56633
+ this.run.setForRun(name, value, source);
56634
+ }
56530
56635
  names() {
56531
- return [.../* @__PURE__ */ new Set([...this.params.keys(), ...this.run.names()])].sort();
56636
+ return [.../* @__PURE__ */ new Set([...this.own.keys(), ...this.run.names()])].sort();
56532
56637
  }
56533
56638
  all() {
56534
- return { ...this.run.all(), ...Object.fromEntries(this.params) };
56639
+ return { ...this.run.all(), ...Object.fromEntries(this.own) };
56640
+ }
56641
+ };
56642
+ var ParamsScope = class extends OverlayScope {
56643
+ constructor(run, params) {
56644
+ super(
56645
+ run,
56646
+ new Map(Object.entries(params).map(([name, value]) => [`params.${name}`, value])),
56647
+ "params"
56648
+ );
56535
56649
  }
56536
56650
  };
56537
56651
  var BUILT_INS = {
@@ -56545,19 +56659,19 @@ var BUILT_INS = {
56545
56659
  var REFERENCE = /\{\{\s*([^{}\s]+)\s*\}\}/g;
56546
56660
  var SOLE_REFERENCE = /^\{\{\s*([^{}\s]+)\s*\}\}$/;
56547
56661
  var MAX_DEPTH = 16;
56548
- function interpolate(value, scope) {
56662
+ function interpolate(value, scope, options = {}) {
56549
56663
  const sole = SOLE_REFERENCE.exec(value);
56550
56664
  if (sole?.[1]) {
56551
56665
  const resolved = resolveName(sole[1], scope, []);
56552
- return typeof resolved === "string" ? resolveString(resolved, scope, [sole[1]]) : resolved;
56666
+ return typeof resolved === "string" ? resolveString(resolved, scope, [sole[1]], options) : resolved;
56553
56667
  }
56554
- return resolveString(value, scope, []);
56668
+ return resolveString(value, scope, [], options);
56555
56669
  }
56556
- function interpolateToString(value, scope) {
56557
- const result = interpolate(value, scope);
56558
- return result === null || result === void 0 ? "" : String(result);
56670
+ function interpolateToString(value, scope, options = {}) {
56671
+ const result = interpolate(value, scope, options);
56672
+ return result === null || result === void 0 ? options.nullAs ?? "" : String(result);
56559
56673
  }
56560
- function resolveString(text, scope, seen) {
56674
+ function resolveString(text, scope, seen, options) {
56561
56675
  if (seen.length > MAX_DEPTH) {
56562
56676
  throw new InterpolationError(
56563
56677
  `Variable nesting is too deep (${seen.join(" -> ")}); this is probably a loop`
@@ -56571,11 +56685,12 @@ function resolveString(text, scope, seen) {
56571
56685
  );
56572
56686
  }
56573
56687
  const resolved = resolveName(name, scope, seen);
56574
- if (resolved === null || resolved === void 0) return "";
56575
- return typeof resolved === "string" ? resolveString(resolved, scope, [...seen, name]) : String(resolved);
56688
+ if (resolved === null || resolved === void 0) return options.nullAs ?? "";
56689
+ return typeof resolved === "string" ? resolveString(resolved, scope, [...seen, name], options) : String(resolved);
56576
56690
  });
56577
56691
  }
56578
56692
  function resolveName(name, scope, seen) {
56693
+ if (name.length > 1 && name.startsWith("@")) return resolveNamed(name, scope, seen);
56579
56694
  const builtIn = BUILT_INS[name];
56580
56695
  if (builtIn) return builtIn();
56581
56696
  if (!scope.has(name)) {
@@ -56592,6 +56707,31 @@ function resolveName(name, scope, seen) {
56592
56707
  }
56593
56708
  return value;
56594
56709
  }
56710
+ function resolveNamed(name, scope, seen) {
56711
+ const holder = name.slice(1);
56712
+ const through = [...seen, name];
56713
+ const held = resolveName(holder, scope, through);
56714
+ const target = typeof held === "string" ? resolveString(held, scope, [...through, holder], {}) : held;
56715
+ if (typeof target !== "string" || target.trim() === "") {
56716
+ throw new InterpolationError(
56717
+ `{{${name}}} reads the variable ${holder} names, but ${holder} is ${JSON.stringify(target)}, not a variable name`,
56718
+ holder
56719
+ );
56720
+ }
56721
+ if (through.includes(target)) {
56722
+ throw new InterpolationError(
56723
+ `Variable "${target}" refers to itself: ${[...through, target].join(" -> ")}`,
56724
+ target
56725
+ );
56726
+ }
56727
+ if (!BUILT_INS[target] && !target.startsWith("@") && !scope.has(target)) {
56728
+ throw new InterpolationError(
56729
+ `Variable "${target}", which {{${name}}} names, is not defined in this environment.`,
56730
+ target
56731
+ );
56732
+ }
56733
+ return resolveName(target, scope, through);
56734
+ }
56595
56735
 
56596
56736
  // ../../packages/core/src/vars/resolve.ts
56597
56737
  import fs7 from "node:fs/promises";
@@ -56623,9 +56763,14 @@ async function buildScope(context) {
56623
56763
  if (context.environmentName) {
56624
56764
  layers.push(...await environmentLayers(context, context.environmentName));
56625
56765
  }
56766
+ if (context.run) layers.push({ source: "run", vars: Object.fromEntries(context.run.values) });
56626
56767
  const row = context.dataRow !== void 0 ? context.dataRow : await firstRow(context.collectionPath);
56627
56768
  if (row) layers.push(row);
56628
56769
  const scope = new VariableScope(layers, context.env ?? process.env);
56770
+ if (context.run) {
56771
+ scope.runValues = context.run.values;
56772
+ scope.runWide = context.run.wide === true;
56773
+ }
56629
56774
  scope.flags = context.flags !== void 0 ? context.flags : context.environmentName ? (await environmentFlags(root, context.environmentName, context.environments)).values : null;
56630
56775
  return scope;
56631
56776
  }
@@ -57130,7 +57275,11 @@ async function prepareRequest(written, body, scope, root) {
57130
57275
  };
57131
57276
  }
57132
57277
  return {
57133
- request: { ...request, body: written.body === null ? null : resolve(written.body) },
57278
+ request: {
57279
+ ...request,
57280
+ // In a JSON body a null is JSON's own, not a gap that leaves it malformed.
57281
+ body: written.body === null ? null : body?.json !== void 0 ? interpolateToString(written.body, scope, { nullAs: "null" }) : resolve(written.body)
57282
+ },
57134
57283
  payload: null
57135
57284
  };
57136
57285
  }
@@ -57160,9 +57309,20 @@ async function readBodyFile(root, path12, what) {
57160
57309
 
57161
57310
  // ../../packages/core/src/run/runRequest.ts
57162
57311
  import assert3 from "node:assert/strict";
57163
- import path8 from "node:path";
57312
+ import path9 from "node:path";
57164
57313
 
57165
57314
  // ../../packages/core/src/assert/evaluate.ts
57315
+ function locate(root, segments, nullAlong) {
57316
+ const located = resolvePath(root, segments);
57317
+ if (located.length > 0 || !nullAlong || segments.some((s) => s.kind === "each")) return located;
57318
+ for (let end = 1; end < segments.length; end++) {
57319
+ const [reached] = resolvePath(root, segments.slice(0, end));
57320
+ if (!reached) return [];
57321
+ if (reached.value === null) return [reached];
57322
+ }
57323
+ return [];
57324
+ }
57325
+ var isNullCheck = (wanted) => wanted.equals === null && Object.keys(wanted).length === 1;
57166
57326
  function checkScalarTarget(target, label, wanted, found, context) {
57167
57327
  const base2 = { target, expected: describe3(wanted) };
57168
57328
  const name = `${label} ${describe3(wanted, { brief: true })}`;
@@ -57207,7 +57367,7 @@ function checkBody(path12, segments, wanted, located, context) {
57207
57367
  const covered = [];
57208
57368
  let failure2 = null;
57209
57369
  for (const { path: at, value } of located) {
57210
- const check2 = checkBodyValue(wanted, value, context);
57370
+ const check2 = checkBodyValue(wanted, value, context, context.claimed?.(formatPath(at)));
57211
57371
  covered.push(...check2.covered.map((c) => ({ ...c, pattern: [...at, ...c.pattern] })));
57212
57372
  if (check2.failure && !failure2) {
57213
57373
  failure2 = fanOut ? `At ${formatPath(at)}: ${check2.failure}` : check2.failure;
@@ -57219,7 +57379,7 @@ function checkBody(path12, segments, wanted, located, context) {
57219
57379
  covered
57220
57380
  };
57221
57381
  }
57222
- function checkBodyValue(wanted, value, context) {
57382
+ function checkBodyValue(wanted, value, context, claimed = /* @__PURE__ */ new Set()) {
57223
57383
  const failures = [];
57224
57384
  let whole = !(value !== null && typeof value === "object");
57225
57385
  const covered = [];
@@ -57258,7 +57418,7 @@ function checkBodyValue(wanted, value, context) {
57258
57418
  const used = /* @__PURE__ */ new Set();
57259
57419
  for (const expected of wanted.unordered) {
57260
57420
  const matches = value.map((item, index) => ({ index, match: matchItem(item, expected, context) })).filter((candidate) => candidate.match !== null);
57261
- const chosen = matches.find((m) => !used.has(m.index)) ?? matches[0];
57421
+ const chosen = matches.find((m) => !used.has(m.index) && !claimed.has(m.index)) ?? matches.find((m) => !used.has(m.index)) ?? matches[0];
57262
57422
  if (!chosen) {
57263
57423
  missing.push(expected);
57264
57424
  continue;
@@ -57269,6 +57429,7 @@ function checkBodyValue(wanted, value, context) {
57269
57429
  covered.push({ pattern: [{ kind: "index", index: chosen.index }, ...c.pattern] });
57270
57430
  }
57271
57431
  }
57432
+ for (const index of used) claimed.add(index);
57272
57433
  if (missing.length > 0)
57273
57434
  failures.push(`Missing from the array: ${missing.map(show).join(", ")}`);
57274
57435
  }
@@ -57382,7 +57543,11 @@ function matchItem(item, expected, context) {
57382
57543
  const captures = [];
57383
57544
  for (const [key, want] of entries2) {
57384
57545
  const segments = parsePath(key);
57385
- const located = resolvePath(item, segments);
57546
+ const located = locate(
57547
+ item,
57548
+ segments,
57549
+ isMatcherObject(want) ? isNullCheck(want) : want === null
57550
+ );
57386
57551
  if (isMatcherObject(want)) {
57387
57552
  if (want.absent) {
57388
57553
  if (located.length > 0) return null;
@@ -57538,6 +57703,10 @@ var CheckSession = class {
57538
57703
  /** While true, checks recorded are defaults: an endpoint base's (SPEC.md §2.6). */
57539
57704
  recordingDefaults = false;
57540
57705
  defaults = /* @__PURE__ */ new Set();
57706
+ /** Checks whose meaning waits on strict validation: what each would be with it on. */
57707
+ byStrictness = [];
57708
+ /** Items of each array unordered checks matched, by the array's path. */
57709
+ claims = /* @__PURE__ */ new Map();
57541
57710
  /** Turn strict validation on or off. The last word wins. */
57542
57711
  setStrict(strict) {
57543
57712
  this.strict = strict;
@@ -57553,6 +57722,7 @@ var CheckSession = class {
57553
57722
  if (properties.length === 0) return;
57554
57723
  this.sortKeys = [...properties, ...this.sortKeys];
57555
57724
  this.sorted = void 0;
57725
+ this.claims.clear();
57556
57726
  }
57557
57727
  status(matcher) {
57558
57728
  return this.record(
@@ -57571,10 +57741,36 @@ var CheckSession = class {
57571
57741
  const body = this.body_();
57572
57742
  if (body === BODY_UNAVAILABLE) return null;
57573
57743
  const segments = parsePath(path12);
57574
- const result = checkBody(path12, segments, matcher, resolvePath(body, segments), this.context);
57744
+ const located = locate(body, segments, isNullCheck(matcher));
57745
+ const result = checkBody(path12, segments, matcher, located, {
57746
+ ...this.context,
57747
+ claimed: (arrayPath) => this.claimsOn(arrayPath)
57748
+ });
57575
57749
  this.covered.push(...result.covered);
57576
57750
  return this.record(result.assertion);
57577
57751
  }
57752
+ /**
57753
+ * A body check that means one thing under strict validation and another
57754
+ * without, as xtest's lone `notThisExpectedValue` entry did (SPEC.md §3).
57755
+ * Both are checked now, against the body as it is now, and `finish()` keeps
57756
+ * the one the step's last word on strict validation asks for.
57757
+ */
57758
+ bodyByStrictness(path12, matchers) {
57759
+ const body = this.body_();
57760
+ if (body === BODY_UNAVAILABLE) return null;
57761
+ const segments = parsePath(path12);
57762
+ const located = locate(body, segments, false);
57763
+ const strict = checkBody(path12, segments, matchers.strict, located, this.context);
57764
+ const lenient = checkBody(path12, segments, matchers.lenient, located, this.context);
57765
+ const assertion = this.record(lenient.assertion);
57766
+ this.byStrictness.push({ assertion, strict });
57767
+ return assertion;
57768
+ }
57769
+ claimsOn(arrayPath) {
57770
+ let claimed = this.claims.get(arrayPath);
57771
+ if (!claimed) this.claims.set(arrayPath, claimed = /* @__PURE__ */ new Set());
57772
+ return claimed;
57773
+ }
57578
57774
  /** Account for a path under strict validation without asserting anything. */
57579
57775
  ignore(path12) {
57580
57776
  this.covered.push({ pattern: parsePath(path12) });
@@ -57614,9 +57810,17 @@ var CheckSession = class {
57614
57810
  }
57615
57811
  }
57616
57812
  finish() {
57813
+ if (this.strict) {
57814
+ for (const { assertion, strict } of this.byStrictness) {
57815
+ for (const key of Object.keys(assertion)) delete assertion[key];
57816
+ Object.assign(assertion, strict.assertion);
57817
+ this.covered.push(...strict.covered);
57818
+ }
57819
+ }
57617
57820
  this.replaceDefaults();
57618
57821
  if (this.strict) {
57619
- const body = this.body_();
57822
+ const readable = !["binary", "html"].includes(this.context.response.bodyKind);
57823
+ const body = readable ? this.body_() : {};
57620
57824
  if (body !== BODY_UNAVAILABLE) {
57621
57825
  const unasserted = findUnasserted(body, this.covered);
57622
57826
  this.assertions.push({
@@ -57781,13 +57985,30 @@ function variableName(value) {
57781
57985
  }
57782
57986
  return value;
57783
57987
  }
57988
+ function pathOf2(path12, what) {
57989
+ if (typeof path12 === "string") return path12;
57990
+ if (!Array.isArray(path12)) {
57991
+ throw new GtaUsageError(
57992
+ `${what} is a string, such as "user.name", or a list of keys, such as ["user", "name"]; got ${show(path12)}`
57993
+ );
57994
+ }
57995
+ if (path12.length === 0) throw new GtaUsageError(`${what} is an empty list; it needs a key`);
57996
+ return formatPath(
57997
+ path12.map((key) => {
57998
+ if (typeof key === "string") return { kind: "key", key };
57999
+ if (typeof key === "number") return { kind: "key", key: String(key) };
58000
+ throw new GtaUsageError(`${what}'s keys are strings or numbers; got ${show(key)}`);
58001
+ })
58002
+ );
58003
+ }
57784
58004
  var isValidationEntry = (value) => value !== null && typeof value === "object" && "pathToProperty" in value;
57785
58005
  function itemFrom(list, valueKey) {
57786
58006
  const item = {};
57787
58007
  for (const entry of list) {
57788
- const path12 = entry.pathToProperty;
58008
+ const path12 = pathOf2(entry.pathToProperty, "pathToProperty");
57789
58009
  if (valueKey === "compareValue") {
57790
- item[path12] = entry.compareValue;
58010
+ const value = entry.compareValue;
58011
+ item[path12] = isRegExp(value) ? { matches: value } : value;
57791
58012
  continue;
57792
58013
  }
57793
58014
  const matcher = xtestMatcher(valueKey in entry, entry[valueKey], entry.specialHandling);
@@ -57795,12 +58016,22 @@ function itemFrom(list, valueKey) {
57795
58016
  }
57796
58017
  return item;
57797
58018
  }
58019
+ function loneNotThisValue(list) {
58020
+ const [entry] = list;
58021
+ if (list.length !== 1 || !isValidationEntry(entry)) return null;
58022
+ if (entry.specialHandling !== "notThisExpectedValue" || !("expectedValue" in entry)) return null;
58023
+ const value = entry.expectedValue;
58024
+ const path12 = pathOf2(entry.pathToProperty, "pathToProperty");
58025
+ return { unorderedNot: [{ [path12]: isRegExp(value) ? { matches: value } : value }] };
58026
+ }
57798
58027
  function toList(validationList) {
57799
58028
  if (!Array.isArray(validationList)) {
57800
58029
  throw new GtaUsageError(`validationList must be an array, got ${show(validationList)}`);
57801
58030
  }
57802
58031
  return validationList;
57803
58032
  }
58033
+ var newStepControl = () => ({ skip: null, rest: null });
58034
+ var reasonOf = (reason, fallback) => reason === void 0 || reason === null || String(reason).trim() === "" ? fallback : String(reason);
57804
58035
  function guarded(session, label, fn) {
57805
58036
  return (...args) => {
57806
58037
  try {
@@ -57811,11 +58042,26 @@ function guarded(session, label, fn) {
57811
58042
  }
57812
58043
  };
57813
58044
  }
57814
- function testsGta({ session, scope, pending, warn = () => {
57815
- } }) {
58045
+ function testsGta({
58046
+ session,
58047
+ scope,
58048
+ pending,
58049
+ warn = () => {
58050
+ },
58051
+ control = newStepControl()
58052
+ }) {
57816
58053
  const shared = sharedGta(scope);
57817
58054
  return {
57818
58055
  ...shared,
58056
+ skip() {
58057
+ throw new Error(
58058
+ "gta.skip() stops a request before it is sent, so it belongs in before.script; gta.skipRest() skips the steps after this one"
58059
+ );
58060
+ },
58061
+ /** Run none of the steps after this one in this row; the next row, and teardown, still run. */
58062
+ skipRest(reason) {
58063
+ control.rest ??= reasonOf(reason, "gta.skipRest() in an earlier step");
58064
+ },
57819
58065
  expectResponseStatusCodeToBe: guarded(
57820
58066
  session,
57821
58067
  "expectResponseStatusCodeToBe",
@@ -57837,39 +58083,62 @@ function testsGta({ session, scope, pending, warn = () => {
57837
58083
  "expectResponseBodyToHaveProperty",
57838
58084
  (...args) => {
57839
58085
  const [path12, value, specialHandling] = args;
57840
- session.body(path12, xtestMatcher(args.length > 1, value, specialHandling));
58086
+ session.body(
58087
+ pathOf2(path12, "jsonPathToProperty"),
58088
+ xtestMatcher(args.length > 1, value, specialHandling)
58089
+ );
57841
58090
  }
57842
58091
  ),
57843
58092
  expectResponseBodyToHaveUnorderedArray: guarded(
57844
58093
  session,
57845
58094
  "expectResponseBodyToHaveUnorderedArray",
57846
58095
  (jsonPathToArray, validationList) => {
58096
+ const path12 = pathOf2(jsonPathToArray, "jsonPathToArray");
57847
58097
  const list = toList(validationList);
57848
58098
  const unordered = list.every(isValidationEntry) ? [itemFrom(list, "expectedValue")] : list;
57849
- session.body(jsonPathToArray, { unordered });
58099
+ const lone = loneNotThisValue(list);
58100
+ if (lone) session.bodyByStrictness(path12, { strict: { unordered }, lenient: lone });
58101
+ else session.body(path12, { unordered });
57850
58102
  }
57851
58103
  ),
57852
58104
  expectResponseBodyToHaveUnorderedArrayNotThisItem: guarded(
57853
58105
  session,
57854
58106
  "expectResponseBodyToHaveUnorderedArrayNotThisItem",
57855
58107
  (jsonPathToArray, validationList) => {
58108
+ const path12 = pathOf2(jsonPathToArray, "jsonPathToArray");
57856
58109
  const list = toList(validationList);
57857
58110
  const unorderedNot = list.every(isValidationEntry) ? [itemFrom(list, "compareValue")] : list;
57858
- session.body(jsonPathToArray, { unorderedNot });
58111
+ session.body(path12, { unorderedNot });
58112
+ }
58113
+ ),
58114
+ ignoreResponseBodyProperty: guarded(
58115
+ session,
58116
+ "ignoreResponseBodyProperty",
58117
+ (jsonPathToProperty) => {
58118
+ session.ignore(pathOf2(jsonPathToProperty, "jsonPathToProperty"));
58119
+ }
58120
+ ),
58121
+ ignoreResponseBodyArrayObjectProperty: guarded(
58122
+ session,
58123
+ "ignoreResponseBodyArrayObjectProperty",
58124
+ (jsonPathToArray, jsonPathOfObjectProperty) => {
58125
+ const array2 = parsePath(pathOf2(jsonPathToArray, "jsonPathToArray"));
58126
+ const property = parsePath(pathOf2(jsonPathOfObjectProperty, "jsonPathOfObjectProperty"));
58127
+ session.ignore(formatPath([...array2, { kind: "each" }, ...property]));
57859
58128
  }
57860
58129
  ),
57861
- ignoreResponseBodyProperty(jsonPathToProperty) {
57862
- session.ignore(jsonPathToProperty);
57863
- },
57864
- ignoreResponseBodyArrayObjectProperty(jsonPathToArray, jsonPathOfObjectProperty) {
57865
- session.ignore(`${jsonPathToArray}[].${jsonPathOfObjectProperty}`);
57866
- },
57867
58130
  sortResponseBodyArrays(propertyName) {
57868
- if (typeof propertyName !== "string" || propertyName === "") {
58131
+ const named = typeof propertyName === "string" && propertyName !== "" || Array.isArray(propertyName) && propertyName.length > 0;
58132
+ if (!named) {
57869
58133
  warn("gta.sortResponseBodyArrays() needs a property name to sort by; nothing was sorted");
57870
58134
  return;
57871
58135
  }
57872
- session.sortBy([propertyName]);
58136
+ try {
58137
+ session.sortBy([pathOf2(propertyName, "propertyName")]);
58138
+ } catch (cause) {
58139
+ if (!(cause instanceof GtaUsageError)) throw cause;
58140
+ warn(`gta.sortResponseBodyArrays(): ${cause.message}; nothing was sorted`);
58141
+ }
57873
58142
  },
57874
58143
  /** Strict validation for this step. Replaces `startXTest`'s second argument. */
57875
58144
  useStrictValidation(enabled = true) {
@@ -57908,12 +58177,25 @@ function testsGta({ session, scope, pending, warn = () => {
57908
58177
  }
57909
58178
  };
57910
58179
  }
57911
- function preRequestGta(scope) {
58180
+ function preRequestGta(scope, control = newStepControl()) {
57912
58181
  const notHere = (name) => () => {
57913
58182
  throw new Error(`gta.${name}() checks a response, so it belongs in tests, not before.script`);
57914
58183
  };
57915
58184
  return {
57916
58185
  ...sharedGta(scope),
58186
+ /**
58187
+ * Send nothing for this step, and report it skipped with the reason. The
58188
+ * script runs to its end; no later `before.script` does.
58189
+ */
58190
+ skip(reason) {
58191
+ control.skip ??= reasonOf(reason, "gta.skip() in before.script");
58192
+ },
58193
+ /** `skip`, and run none of the steps after this one in this row either. */
58194
+ skipRest(reason) {
58195
+ const given = reasonOf(reason, "gta.skipRest() in before.script");
58196
+ control.skip ??= given;
58197
+ control.rest ??= given;
58198
+ },
57917
58199
  expectResponseStatusCodeToBe: notHere("expectResponseStatusCodeToBe"),
57918
58200
  expectResponseToHaveHeader: notHere("expectResponseToHaveHeader"),
57919
58201
  expectResponseBodyToHaveProperty: notHere("expectResponseBodyToHaveProperty"),
@@ -57926,9 +58208,18 @@ function sharedGta(scope) {
57926
58208
  get(name) {
57927
58209
  return scope.get(name);
57928
58210
  },
57929
- /** Set a variable for the rest of this run, like a capture. */
57930
- set(name, value) {
57931
- scope.set(String(name), toVarValue2(value), "script");
58211
+ /**
58212
+ * Set a variable for the rest of this run, like a capture. With
58213
+ * `{ scope: 'run' }` it lasts past this data row too, into every row after
58214
+ * it and teardown (SPEC.md §2.10).
58215
+ */
58216
+ set(name, value, options) {
58217
+ const lasting = options?.scope;
58218
+ if (lasting !== void 0 && lasting !== "run") {
58219
+ throw new Error(`gta.set(): scope is 'run' or left out, got ${show(lasting)}`);
58220
+ }
58221
+ if (lasting === "run") scope.setForRun(String(name), toVarValue2(value), "script");
58222
+ else scope.set(String(name), toVarValue2(value), "script");
57932
58223
  },
57933
58224
  /**
57934
58225
  * A feature flag's value in this run (SPEC.md §2.9) — to check something
@@ -58165,7 +58456,127 @@ function captureConsole(logs, phase) {
58165
58456
  return { log: write("log"), info: write("info"), warn: write("warn"), error: write("error") };
58166
58457
  }
58167
58458
 
58459
+ // ../../packages/core/src/run/plan.ts
58460
+ import path8 from "node:path";
58461
+ async function planSteps(collection, collectionPath) {
58462
+ const planned = [];
58463
+ let project;
58464
+ for (const [index, step] of collection.steps.entries()) {
58465
+ if (!isUseStep(step)) {
58466
+ planned.push({ kind: "request", index, step });
58467
+ continue;
58468
+ }
58469
+ try {
58470
+ if (!collectionPath)
58471
+ throw new Error(`use: ${step.use} \u2014 save the collection in a project first`);
58472
+ project ??= await projectOf(collectionPath);
58473
+ const set2 = await resolveRequestSet(project.root, project.global, step.use);
58474
+ checkWith(step.use, set2.doc, step.with ?? {});
58475
+ if (set2.doc.steps.length === 0) throw new Error(`use: ${step.use} \u2014 the set has no steps`);
58476
+ const planSet = {
58477
+ name: step.use,
58478
+ useName: step.name?.trim() ? step.name : void 0,
58479
+ path: set2.path,
58480
+ doc: set2.doc,
58481
+ with: step.with ?? {},
58482
+ useTests: step.tests
58483
+ };
58484
+ set2.doc.steps.forEach((child, number4) => {
58485
+ planned.push({ kind: "request", index, step: child, set: planSet, child: number4 });
58486
+ });
58487
+ } catch (cause) {
58488
+ planned.push({
58489
+ kind: "broken",
58490
+ index,
58491
+ step,
58492
+ error: { phase: "use", message: cause instanceof Error ? cause.message : String(cause) }
58493
+ });
58494
+ }
58495
+ }
58496
+ return planned;
58497
+ }
58498
+ async function projectOf(collectionPath) {
58499
+ const root = projectRootOf(collectionPath) ?? path8.dirname(collectionPath);
58500
+ const { global } = await readProject(root);
58501
+ return { root, global };
58502
+ }
58503
+ function checkWith(name, set2, given) {
58504
+ const params = set2.params ?? {};
58505
+ const unknown2 = Object.keys(given).filter((key) => !(key in params));
58506
+ if (unknown2.length > 0) {
58507
+ const takes = Object.keys(params).join(", ") || "none";
58508
+ throw new Error(`use: ${name} \u2014 it takes no ${unknown2.join(", ")} (it takes: ${takes})`);
58509
+ }
58510
+ const missing = Object.entries(params).filter(([key, spec]) => readParam(spec).required && !(key in given)).map(([key]) => key);
58511
+ if (missing.length > 0) {
58512
+ throw new Error(`use: ${name} \u2014 needs ${missing.join(", ")} in with:`);
58513
+ }
58514
+ }
58515
+ function resolveParams(specs, given, scope) {
58516
+ const params = {};
58517
+ const defaults = /* @__PURE__ */ new Map();
58518
+ for (const [key, spec] of Object.entries(specs ?? {})) {
58519
+ const value = given[key];
58520
+ if (value !== void 0)
58521
+ params[key] = typeof value === "string" ? interpolate(value, scope) : value;
58522
+ else {
58523
+ const fallback = readParam(spec).default;
58524
+ if (fallback !== void 0) defaults.set(key, fallback);
58525
+ }
58526
+ }
58527
+ const resolving = [];
58528
+ const resolve = (key) => {
58529
+ const fallback = defaults.get(key);
58530
+ if (fallback === void 0 || Object.hasOwn(params, key)) return;
58531
+ if (resolving.includes(key)) {
58532
+ const chain = [...resolving, key].map((name) => `params.${name}`);
58533
+ throw new InterpolationError(
58534
+ `params.${key} refers to itself: ${chain.join(" -> ")}`,
58535
+ `params.${key}`
58536
+ );
58537
+ }
58538
+ if (typeof fallback !== "string") {
58539
+ params[key] = fallback;
58540
+ return;
58541
+ }
58542
+ resolving.push(key);
58543
+ for (const [, name] of fallback.matchAll(PARAM_REFERENCE)) resolve(name);
58544
+ resolving.pop();
58545
+ try {
58546
+ params[key] = interpolate(fallback, new ParamsScope(scope, params));
58547
+ } catch (cause) {
58548
+ if (!(cause instanceof InterpolationError)) throw cause;
58549
+ throw new InterpolationError(`params.${key}'s default: ${cause.message}`, cause.variable);
58550
+ }
58551
+ };
58552
+ for (const key of defaults.keys()) resolve(key);
58553
+ return params;
58554
+ }
58555
+ var PARAM_REFERENCE = /\{\{\s*params\.([^{}\s]+)\s*\}\}/g;
58556
+ async function referenceProblems(collection, collectionPath) {
58557
+ const problems = [];
58558
+ if (collection.extends) {
58559
+ try {
58560
+ const { root, global } = await projectOf(collectionPath);
58561
+ await resolveBase(root, global, collection.extends);
58562
+ } catch (cause) {
58563
+ problems.push({ step: null, message: cause.message });
58564
+ }
58565
+ }
58566
+ for (const list of STEP_LISTS) {
58567
+ const steps = collection[list];
58568
+ if (!steps) continue;
58569
+ for (const planned of await planSteps({ ...collection, steps }, collectionPath)) {
58570
+ if (planned.kind !== "broken") continue;
58571
+ const stage = list === "steps" ? {} : { stage: list };
58572
+ problems.push({ step: planned.index, ...stage, message: planned.error.message });
58573
+ }
58574
+ }
58575
+ return problems;
58576
+ }
58577
+
58168
58578
  // ../../packages/core/src/run/runRequest.ts
58579
+ var itemText = (value) => value === null || ["string", "number", "boolean"].includes(typeof value) ? value : JSON.stringify(value) ?? String(value);
58169
58580
  function folded(layers, collection) {
58170
58581
  let headers;
58171
58582
  let settings = {};
@@ -58205,16 +58616,9 @@ function endpointValues(endpoint, step, scope) {
58205
58616
  }
58206
58617
  return values;
58207
58618
  }
58208
- function defaultsOf(params) {
58209
- const defaults = {};
58210
- for (const [key, spec] of Object.entries(params)) {
58211
- const value = readParam(spec).default;
58212
- if (value !== void 0) defaults[key] = value;
58213
- }
58214
- return defaults;
58215
- }
58216
58619
  async function runRequest(input2) {
58217
- const { step, itemPath = null, signal, context, scope: provided, set: set2 } = input2;
58620
+ const { step, itemPath = null, signal, context, scope: provided, set: set2, item: each } = input2;
58621
+ const control = input2.control ?? newStepControl();
58218
58622
  if (isUseStep(step)) {
58219
58623
  return {
58220
58624
  item: { path: itemPath, name: stepLabel(step), seq: null },
@@ -58226,7 +58630,6 @@ async function runRequest(input2) {
58226
58630
  durationMs: 0
58227
58631
  };
58228
58632
  }
58229
- const params = set2?.params ?? input2.params ?? (input2.collection?.params ? defaultsOf(input2.collection.params) : void 0);
58230
58633
  const library = context && needsLibrary(input2) ? await loadLibrary(context.collectionPath) : null;
58231
58634
  const checks = input2.checks ?? library?.checks;
58232
58635
  const endpoints = input2.endpoints ?? library?.endpoints ?? [];
@@ -58264,9 +58667,18 @@ async function runRequest(input2) {
58264
58667
  const logs = [];
58265
58668
  const withLogs = () => ({
58266
58669
  ...logs.length > 0 ? { logs } : {},
58267
- ...set2 ? { use: { set: set2.name, child: set2.child, of: set2.of } } : {}
58670
+ ...each ? { forEach: { index: each.index, of: each.of, item: String(itemText(each.value)) } } : {},
58671
+ ...set2 ? {
58672
+ use: {
58673
+ set: set2.name,
58674
+ ...set2.useName ? { name: set2.useName } : {},
58675
+ child: set2.child,
58676
+ of: set2.of
58677
+ }
58678
+ } : {}
58268
58679
  });
58269
- const last = set2 ? set2.child === set2.of - 1 : false;
58680
+ const marked = set2 ? set2.doc.steps.findIndex((s) => s.useTests === true) : -1;
58681
+ const useChecksThis = set2 ? set2.child === (marked >= 0 ? marked : set2.of - 1) : false;
58270
58682
  const layers = [];
58271
58683
  if (endpoint) {
58272
58684
  layers.push({
@@ -58286,7 +58698,8 @@ async function runRequest(input2) {
58286
58698
  layers.push({ script: "collection", label: "collection", ...partsOf2(input2.collection) });
58287
58699
  if (set2) layers.push({ script: "set", label: "set", ...partsOf2(set2.doc) });
58288
58700
  layers.push({ script: "step", label: "", before: step.before, tests: step.tests });
58289
- if (last && set2?.useTests) layers.push({ script: "use", label: "use", tests: set2.useTests });
58701
+ if (useChecksThis && set2?.useTests)
58702
+ layers.push({ script: "use", label: "use", tests: set2.useTests });
58290
58703
  const collection = folded(layers, input2.collection);
58291
58704
  const failed = (error62) => ({
58292
58705
  ...failedBeforeSending(step, collection, item, error62, startedHr, logs),
@@ -58294,40 +58707,64 @@ async function runRequest(input2) {
58294
58707
  });
58295
58708
  let spec;
58296
58709
  let scope;
58710
+ let params;
58297
58711
  try {
58298
58712
  const run = provided ?? (context ? await buildScope(context) : new VariableScope());
58299
- scope = params ? new ParamsScope(run, params) : run;
58713
+ params = set2?.params ?? input2.params ?? (input2.collection?.params ? resolveParams(input2.collection.params, {}, run) : void 0);
58714
+ const own2 = params ? new ParamsScope(run, params) : run;
58715
+ scope = each ? new OverlayScope(own2, /* @__PURE__ */ new Map([["item", itemText(each.value)]]), "forEach") : own2;
58300
58716
  } catch (cause) {
58301
58717
  return failed(interpolateError(cause));
58302
58718
  }
58303
58719
  const shared = (adopt) => ({
58304
58720
  assert: assert3,
58305
58721
  ...params ? { params: adopt(params) } : {},
58722
+ ...each ? { item: adopt(each.value) } : {},
58306
58723
  ...endpoint ? { endpoint: adopt(endpointValues(endpoint, step, scope)) } : {}
58307
58724
  });
58308
58725
  const filenameOf = (layer, kind) => layer.label === "" ? kind : `${layer.label} ${kind}`;
58726
+ let edited = null;
58309
58727
  for (const layer of layers) {
58310
58728
  const before = layer.before;
58311
58729
  const script = layer.script;
58312
58730
  if (before?.script) {
58731
+ const shown = edited ?? toSentRequestSafely(step, collection);
58732
+ let view;
58313
58733
  const error62 = await runScript(before.script, {
58314
58734
  phase: "pre-request",
58315
58735
  filename: filenameOf(layer, "before.script"),
58316
58736
  logs,
58317
58737
  ...checks ? { checks } : {},
58318
- globals: (adopt) => ({
58319
- ...shared(adopt),
58320
- gta: preRequestGta(scope),
58321
- req: requestView(toSentRequestSafely(step, collection), adopt)
58322
- })
58738
+ globals: (adopt) => {
58739
+ view = requestView(shown, adopt);
58740
+ return { ...shared(adopt), gta: preRequestGta(scope, control), req: view };
58741
+ }
58323
58742
  });
58324
58743
  if (error62) return failed({ ...error62, script });
58744
+ try {
58745
+ edited = changedRequest(shown, view, step.body) ?? edited;
58746
+ } catch (cause) {
58747
+ return failed({ phase: "pre-request", message: cause.message, script });
58748
+ }
58749
+ if (control.skip !== null) {
58750
+ return {
58751
+ item,
58752
+ request: edited ?? toSentRequestSafely(step, collection),
58753
+ response: null,
58754
+ assertions: [],
58755
+ ...withLogs(),
58756
+ error: null,
58757
+ status: "skipped",
58758
+ skipped: { reason: control.skip },
58759
+ durationMs: performance.now() - startedHr
58760
+ };
58761
+ }
58325
58762
  }
58326
58763
  }
58327
58764
  let payload;
58328
58765
  try {
58329
58766
  const prepared = await prepareRequest(
58330
- toSentRequest(step, collection),
58767
+ edited ?? toSentRequest(step, collection),
58331
58768
  step.body,
58332
58769
  scope,
58333
58770
  filesRoot(set2?.path ?? context?.collectionPath ?? itemPath)
@@ -58382,6 +58819,7 @@ async function runRequest(input2) {
58382
58819
  session,
58383
58820
  scope,
58384
58821
  pending,
58822
+ control,
58385
58823
  warn: (message) => logs.push({ level: "warn", phase: "tests", message })
58386
58824
  }),
58387
58825
  req: requestView(outcome.request, adopt),
@@ -58411,8 +58849,36 @@ async function runRequest(input2) {
58411
58849
  durationMs: performance.now() - startedHr
58412
58850
  };
58413
58851
  }
58852
+ function changedRequest(shown, view, body) {
58853
+ if (!view) return null;
58854
+ let next = null;
58855
+ if (view.body !== shown.body) {
58856
+ if (typeof view.body !== "string") {
58857
+ throw new Error(`req.body is the body's text; a script set it to ${typeof view.body}`);
58858
+ }
58859
+ const text = body?.json ?? body?.xml ?? body?.text ?? body?.graphql;
58860
+ if (text === void 0) {
58861
+ throw new Error(
58862
+ "req.body can be changed for a json, xml, text or graphql body; a form, multipart or file body is built from its parts, and a step with no body has none to change"
58863
+ );
58864
+ }
58865
+ next = { ...shown, body: view.body };
58866
+ }
58867
+ const was = Object.fromEntries(shown.headers.map((header) => [header.name, header.value]));
58868
+ if (JSON.stringify(view.headers) !== JSON.stringify(was)) {
58869
+ const headers = view.headers;
58870
+ if (headers === null || typeof headers !== "object" || Array.isArray(headers)) {
58871
+ throw new Error("req.headers is a map of header names to their values");
58872
+ }
58873
+ next = {
58874
+ ...next ?? shown,
58875
+ headers: Object.entries(headers).filter(([, value]) => value !== void 0 && value !== null).map(([name, value]) => ({ name, value: String(value) }))
58876
+ };
58877
+ }
58878
+ return next;
58879
+ }
58414
58880
  function filesRoot(file2) {
58415
- return file2 ? projectRootOf(file2) ?? path8.dirname(file2) : null;
58881
+ return file2 ? projectRootOf(file2) ?? path9.dirname(file2) : null;
58416
58882
  }
58417
58883
  function interpolateError(cause) {
58418
58884
  return {
@@ -58443,81 +58909,15 @@ function toSentRequestSafely(step, collection) {
58443
58909
 
58444
58910
  // ../../packages/core/src/run/runCollection.ts
58445
58911
  import path10 from "node:path";
58446
-
58447
- // ../../packages/core/src/run/plan.ts
58448
- import path9 from "node:path";
58449
- async function planSteps(collection, collectionPath) {
58450
- const planned = [];
58451
- let project;
58452
- for (const [index, step] of collection.steps.entries()) {
58453
- if (!isUseStep(step)) {
58454
- planned.push({ kind: "request", index, step });
58455
- continue;
58456
- }
58457
- try {
58458
- if (!collectionPath)
58459
- throw new Error(`use: ${step.use} \u2014 save the collection in a project first`);
58460
- project ??= await projectOf(collectionPath);
58461
- const set2 = await resolveRequestSet(project.root, project.global, step.use);
58462
- checkWith(step.use, set2.doc, step.with ?? {});
58463
- if (set2.doc.steps.length === 0) throw new Error(`use: ${step.use} \u2014 the set has no steps`);
58464
- const planSet = {
58465
- name: step.use,
58466
- path: set2.path,
58467
- doc: set2.doc,
58468
- with: step.with ?? {},
58469
- useTests: step.tests
58470
- };
58471
- set2.doc.steps.forEach((child, number4) => {
58472
- planned.push({ kind: "request", index, step: child, set: planSet, child: number4 });
58473
- });
58474
- } catch (cause) {
58475
- planned.push({
58476
- kind: "broken",
58477
- index,
58478
- step,
58479
- error: { phase: "use", message: cause instanceof Error ? cause.message : String(cause) }
58480
- });
58481
- }
58482
- }
58483
- return planned;
58484
- }
58485
- async function projectOf(collectionPath) {
58486
- const root = projectRootOf(collectionPath) ?? path9.dirname(collectionPath);
58487
- const { global } = await readProject(root);
58488
- return { root, global };
58489
- }
58490
- function checkWith(name, set2, given) {
58491
- const params = set2.params ?? {};
58492
- const unknown2 = Object.keys(given).filter((key) => !(key in params));
58493
- if (unknown2.length > 0) {
58494
- const takes = Object.keys(params).join(", ") || "none";
58495
- throw new Error(`use: ${name} \u2014 it takes no ${unknown2.join(", ")} (it takes: ${takes})`);
58496
- }
58497
- const missing = Object.entries(params).filter(([key, spec]) => readParam(spec).required && !(key in given)).map(([key]) => key);
58498
- if (missing.length > 0) {
58499
- throw new Error(`use: ${name} \u2014 needs ${missing.join(", ")} in with:`);
58500
- }
58501
- }
58502
- function resolveParams(set2, scope) {
58503
- const params = {};
58504
- for (const [key, spec] of Object.entries(set2.doc.params ?? {})) {
58505
- const given = set2.with[key];
58506
- if (given !== void 0)
58507
- params[key] = typeof given === "string" ? interpolate(given, scope) : given;
58508
- else {
58509
- const fallback = readParam(spec).default;
58510
- if (fallback !== void 0) params[key] = fallback;
58511
- }
58512
- }
58513
- return params;
58514
- }
58515
-
58516
- // ../../packages/core/src/run/runCollection.ts
58517
58912
  async function runCollection(options) {
58518
58913
  const { collection, collectionPath = null, context, signal, onResult, bail = false } = options;
58914
+ const stage = options.stage ?? "steps";
58519
58915
  const startedHr = performance.now();
58520
- const scope = context ? await buildScope({ collectionExtends: collection.extends ?? null, ...context }) : new VariableScope();
58916
+ const scope = context ? await buildScope({
58917
+ collectionExtends: collection.extends ?? null,
58918
+ ...context,
58919
+ ...options.run ? { run: options.run } : {}
58920
+ }) : withRun(new VariableScope(), options.run);
58521
58921
  const library = collectionPath ? await loadLibrary(collectionPath) : null;
58522
58922
  const checks = library?.checks ?? [];
58523
58923
  const endpoints = library?.endpoints ?? [];
@@ -58532,50 +58932,88 @@ async function runCollection(options) {
58532
58932
  baseError = { phase: "use", message: cause.message };
58533
58933
  }
58534
58934
  }
58935
+ const running = stage === "steps" ? collection : { ...collection, steps: collection[stage] ?? [] };
58535
58936
  const own2 = collection.params ? selfAsSet(collection, collectionPath) : void 0;
58536
- const plan = await planSteps(collection, collectionPath);
58937
+ const plan = await planSteps(running, collectionPath);
58537
58938
  const results = [];
58538
58939
  let params = {};
58539
- for (const planned of plan) {
58940
+ let unstarted = null;
58941
+ let rest = null;
58942
+ let attempted = 0;
58943
+ const emit = (index, result) => {
58944
+ const marked = stage === "steps" ? result : { ...result, stage };
58945
+ results.push(marked);
58946
+ onResult?.(index, marked);
58947
+ };
58948
+ const failing = (result) => result.status === "fail" || result.status === "error";
58949
+ planning: for (const planned of plan) {
58540
58950
  if (signal?.aborted) break;
58541
- let result;
58951
+ attempted++;
58542
58952
  if (baseError) {
58543
- result = brokenUse(planned.step, baseError, collectionPath);
58544
- } else if (planned.kind === "broken") {
58545
- result = brokenUse(planned.step, planned.error, collectionPath);
58546
- } else {
58547
- const set2 = planned.set ? positionOf(planned.set, planned.child ?? 0) : void 0;
58548
- if (set2 || own2) {
58549
- const starting = set2 ? set2.child === 0 : planned.index === 0;
58550
- if (starting) {
58551
- try {
58552
- params = resolveParams(set2?.planned ?? own2, scope);
58553
- } catch (cause) {
58554
- params = {};
58555
- result = brokenUse(
58556
- planned.step,
58557
- {
58558
- phase: "use",
58559
- message: `use: ${(set2?.planned ?? own2).name} \u2014 ${cause.message}`
58560
- },
58561
- collectionPath
58562
- );
58563
- results.push(result);
58564
- onResult?.(planned.index, result);
58565
- if (bail) break;
58566
- continue;
58567
- }
58953
+ emit(planned.index, brokenUse(planned.step, baseError, collectionPath));
58954
+ if (bail) break;
58955
+ continue;
58956
+ }
58957
+ if (planned.kind === "broken") {
58958
+ emit(planned.index, brokenUse(planned.step, planned.error, collectionPath));
58959
+ if (bail) break;
58960
+ continue;
58961
+ }
58962
+ const set2 = planned.set ? positionOf(planned.set, planned.child ?? 0) : void 0;
58963
+ const inSet = set2 ? { use: useOf(set2) } : {};
58964
+ if (rest !== null) {
58965
+ emit(planned.index, { ...skipped(planned.step, rest, collectionPath), ...inSet });
58966
+ continue;
58967
+ }
58968
+ if (set2 || own2) {
58969
+ const using = set2?.planned ?? own2;
58970
+ const starting = set2 ? set2.child === 0 : planned.index === 0;
58971
+ if (starting) {
58972
+ try {
58973
+ params = resolveParams(using.doc.params, using.with, scope);
58974
+ unstarted = null;
58975
+ } catch (cause) {
58976
+ params = {};
58977
+ unstarted = { phase: "use", message: `use: ${using.name} \u2014 ${cause.message}` };
58568
58978
  }
58569
58979
  }
58570
- const gate = flagGate(collection, planned.index, planned.step, scope);
58571
- if (gate) {
58572
- result = gate(collectionPath);
58573
- results.push(result);
58574
- onResult?.(planned.index, result);
58575
- if (bail && result.status === "error") break;
58980
+ }
58981
+ const gate = flagGate(running, planned.index, planned.step, scope);
58982
+ if (gate) {
58983
+ const result = gate(collectionPath);
58984
+ emit(planned.index, result);
58985
+ if (bail && result.status === "error") break;
58986
+ continue;
58987
+ }
58988
+ if (unstarted && (set2 || own2)) {
58989
+ emit(planned.index, { ...brokenUse(planned.step, unstarted, collectionPath), ...inSet });
58990
+ if (bail) break;
58991
+ continue;
58992
+ }
58993
+ let items = [null];
58994
+ if (planned.step.forEach !== void 0) {
58995
+ try {
58996
+ items = forEachItems(planned.step.forEach, scope).map((value) => ({ value }));
58997
+ } catch (cause) {
58998
+ const error62 = { phase: "forEach", message: cause.message };
58999
+ emit(planned.index, { ...brokenUse(planned.step, error62, collectionPath), ...inSet });
59000
+ if (bail) break;
59001
+ continue;
59002
+ }
59003
+ if (items.length === 0) {
59004
+ const reason = `forEach: ${planned.step.forEach} is an empty list`;
59005
+ emit(planned.index, { ...skipped(planned.step, reason, collectionPath), ...inSet });
59006
+ continue;
59007
+ }
59008
+ }
59009
+ for (const [index, each] of items.entries()) {
59010
+ if (signal?.aborted) break planning;
59011
+ if (rest !== null) {
59012
+ emit(planned.index, { ...skipped(planned.step, rest, collectionPath), ...inSet });
58576
59013
  continue;
58577
59014
  }
58578
- result = await runRequest({
59015
+ const control = newStepControl();
59016
+ const result = await runRequest({
58579
59017
  step: planned.step,
58580
59018
  collection,
58581
59019
  itemPath: collectionPath,
@@ -58583,10 +59021,13 @@ async function runCollection(options) {
58583
59021
  checks,
58584
59022
  endpoints,
58585
59023
  base: base2,
59024
+ control,
58586
59025
  ...library ? { tls: library.tls } : {},
59026
+ ...each ? { item: { value: each.value, index, of: items.length } } : {},
58587
59027
  ...set2 ? {
58588
59028
  set: {
58589
59029
  name: set2.planned.name,
59030
+ ...set2.planned.useName ? { useName: set2.planned.useName } : {},
58590
59031
  path: set2.planned.path,
58591
59032
  doc: set2.planned.doc,
58592
59033
  params,
@@ -58597,31 +59038,59 @@ async function runCollection(options) {
58597
59038
  } : own2 ? { params } : {},
58598
59039
  ...signal ? { signal } : {}
58599
59040
  });
59041
+ emit(planned.index, result);
59042
+ rest ??= control.rest;
59043
+ if (bail && failing(result)) break planning;
58600
59044
  }
58601
- results.push(result);
58602
- onResult?.(planned.index, result);
58603
- if (bail && result.status !== "pass") break;
58604
59045
  }
59046
+ const notBegun = plan.length - attempted;
58605
59047
  return {
58606
- total: plan.length,
59048
+ total: results.length + notBegun,
58607
59049
  passed: results.filter((r) => r.status === "pass").length,
58608
59050
  failed: results.filter((r) => r.status === "fail").length,
58609
59051
  errored: results.filter((r) => r.status === "error").length,
58610
- // A step a feature flag skipped, and one never attempted, are both not run.
58611
- skipped: plan.length - results.length + results.filter((r) => r.status === "skipped").length,
59052
+ // A step a feature flag or a script skipped, and one never begun, are all not run.
59053
+ skipped: notBegun + results.filter((r) => r.status === "skipped").length,
58612
59054
  durationMs: performance.now() - startedHr,
58613
59055
  results
58614
59056
  };
58615
59057
  }
59058
+ function withRun(scope, run) {
59059
+ if (!run) return scope;
59060
+ const built = new VariableScope([{ source: "run", vars: Object.fromEntries(run.values) }]);
59061
+ built.runValues = run.values;
59062
+ built.runWide = run.wide === true;
59063
+ return built;
59064
+ }
59065
+ function forEachItems(expression, scope) {
59066
+ const value = interpolate(expression, scope);
59067
+ if (typeof value === "string") {
59068
+ try {
59069
+ const parsed = JSON.parse(value);
59070
+ if (Array.isArray(parsed)) return parsed;
59071
+ } catch {
59072
+ }
59073
+ }
59074
+ throw new Error(
59075
+ `forEach: ${expression} is ${show(value)}, not a list; it needs a JSON array, such as ["a", "b"]`
59076
+ );
59077
+ }
58616
59078
  function positionOf(planned, child) {
58617
59079
  return { planned, child, of: planned.doc.steps.length };
58618
59080
  }
59081
+ var useOf = (set2) => ({
59082
+ set: set2.planned.name,
59083
+ ...set2.planned.useName ? { name: set2.planned.useName } : {},
59084
+ child: set2.child,
59085
+ of: set2.of
59086
+ });
58619
59087
  function selfAsSet(collection, collectionPath) {
58620
59088
  return {
58621
59089
  name: collectionPath ? path10.basename(collectionPath, ".yml") : collection.id ?? "set",
58622
59090
  path: collectionPath ?? "",
58623
59091
  // Its own before, tests, headers and settings already apply as the collection's.
58624
59092
  doc: { params: collection.params, steps: collection.steps },
59093
+ useName: void 0,
58625
59094
  with: {},
58626
59095
  useTests: void 0
58627
59096
  };
@@ -58657,6 +59126,10 @@ function flagGate(collection, index, step, scope) {
58657
59126
  return (collectionPath) => brokenUse(step, { phase: "flags", message: cause.message }, collectionPath);
58658
59127
  }
58659
59128
  }
59129
+ var skipped = (step, reason, collectionPath) => ({
59130
+ ...notRun(step, collectionPath),
59131
+ skipped: { reason }
59132
+ });
58660
59133
  var notRun = (step, collectionPath) => ({
58661
59134
  item: { path: collectionPath, name: stepLabel(step), seq: null },
58662
59135
  request: {
@@ -58672,6 +59145,62 @@ var notRun = (step, collectionPath) => ({
58672
59145
  durationMs: 0
58673
59146
  });
58674
59147
 
59148
+ // ../../packages/core/src/run/runSuite.ts
59149
+ async function runSuite(options) {
59150
+ const { collection, collectionPath = null, context, signal, bail = false, onResult } = options;
59151
+ const values = /* @__PURE__ */ new Map();
59152
+ const summaries = [];
59153
+ const once = (stage, stopEarly) => runCollection({
59154
+ collection,
59155
+ collectionPath,
59156
+ stage,
59157
+ run: { values, wide: true },
59158
+ ...context ? { context: { ...context, dataRow: null } } : {},
59159
+ ...signal ? { signal } : {},
59160
+ bail: stopEarly,
59161
+ onResult: (index, result) => onResult?.(index, result, void 0)
59162
+ });
59163
+ let stopped = false;
59164
+ if (collection.setup?.length) {
59165
+ const setup = await once("setup", bail);
59166
+ summaries.push(setup);
59167
+ stopped = setup.failed + setup.errored > 0;
59168
+ }
59169
+ const rows = options.rows?.length ? options.rows : null;
59170
+ const passes = rows ?? [null];
59171
+ let unrun = 0;
59172
+ for (const [i, row] of passes.entries()) {
59173
+ if (stopped || signal?.aborted) {
59174
+ unrun = (await planSteps(collection, collectionPath)).length * (passes.length - i);
59175
+ break;
59176
+ }
59177
+ const iteration = rows && row ? { index: i + 1, of: rows.length, label: row.label } : void 0;
59178
+ const summary = await runCollection({
59179
+ collection,
59180
+ collectionPath,
59181
+ run: { values },
59182
+ ...context ? {
59183
+ context: row ? { ...context, dataRow: { source: row.source, vars: row.vars } } : context
59184
+ } : {},
59185
+ ...signal ? { signal } : {},
59186
+ bail,
59187
+ onResult: (index, result) => onResult?.(index, result, iteration)
59188
+ });
59189
+ summaries.push(summary);
59190
+ if (bail && summary.failed + summary.errored > 0) stopped = true;
59191
+ }
59192
+ if (collection.teardown?.length && !signal?.aborted) summaries.push(await once("teardown", false));
59193
+ return {
59194
+ total: summaries.reduce((n, s) => n + s.total, 0) + unrun,
59195
+ passed: summaries.reduce((n, s) => n + s.passed, 0),
59196
+ failed: summaries.reduce((n, s) => n + s.failed, 0),
59197
+ errored: summaries.reduce((n, s) => n + s.errored, 0),
59198
+ skipped: summaries.reduce((n, s) => n + s.skipped, 0) + unrun,
59199
+ durationMs: summaries.reduce((n, s) => n + s.durationMs, 0),
59200
+ results: summaries.flatMap((s) => s.results)
59201
+ };
59202
+ }
59203
+
58675
59204
  // ../../packages/core/src/flags/resolve.ts
58676
59205
  import { spawn } from "node:child_process";
58677
59206
  var FLAG_COMMAND_TIMEOUT_MS = 6e4;
@@ -58860,41 +59389,29 @@ async function runJob(job) {
58860
59389
  const context = { collectionPath: loaded.path, environmentName: job.environment };
58861
59390
  const lines = await stepLines(loaded.path);
58862
59391
  const data = loaded.dataFile ? await readDataFile(loaded.dataFile.path) : null;
58863
- const rows = data ? data.rows : [null];
58864
- const summaries = [];
58865
59392
  const refs = [];
58866
- let unrun = 0;
58867
- for (const [i, row] of rows.entries()) {
58868
- const iteration = row ? { index: i + 1, of: rows.length, label: row.label } : void 0;
58869
- const summary2 = await runCollection({
58870
- collection,
58871
- collectionPath: loaded.path,
58872
- context: {
58873
- ...context,
58874
- flags: job.flags,
58875
- dataRow: row ? { source: `${path11.basename(data.path)} row ${i + 1}`, vars: row.values } : null
58876
- },
58877
- bail: job.bail,
58878
- onResult: (index) => {
58879
- const step = job.steps ? job.steps[index] ?? index : index;
58880
- refs.push({ index: step, line: lines[step] ?? null, ...iteration ? { iteration } : {} });
58881
- }
58882
- });
58883
- summaries.push(summary2);
58884
- if (job.bail && summary2.failed + summary2.errored > 0) {
58885
- unrun = summary2.total * (rows.length - i - 1);
58886
- break;
59393
+ const summary = await runSuite({
59394
+ collection,
59395
+ collectionPath: loaded.path,
59396
+ context: { ...context, flags: job.flags, dataRow: null },
59397
+ rows: data ? data.rows.map((row, i) => ({
59398
+ source: `${path11.basename(data.path)} row ${i + 1}`,
59399
+ vars: row.values,
59400
+ label: row.label
59401
+ })) : null,
59402
+ bail: job.bail,
59403
+ // Each result reports the step it ran under — a use step's requests all under it.
59404
+ onResult: (index, result, iteration) => {
59405
+ const stage = result.stage;
59406
+ const step = stage ? index : job.steps ? job.steps[index] ?? index : index;
59407
+ refs.push({
59408
+ index: step,
59409
+ line: lines[stage ?? "steps"][step] ?? null,
59410
+ ...stage ? { stage } : {},
59411
+ ...iteration ? { iteration } : {}
59412
+ });
58887
59413
  }
58888
- }
58889
- const summary = {
58890
- total: summaries.reduce((n, s) => n + s.total, 0) + unrun,
58891
- passed: summaries.reduce((n, s) => n + s.passed, 0),
58892
- failed: summaries.reduce((n, s) => n + s.failed, 0),
58893
- errored: summaries.reduce((n, s) => n + s.errored, 0),
58894
- skipped: summaries.reduce((n, s) => n + s.skipped, 0) + unrun,
58895
- durationMs: summaries.reduce((n, s) => n + s.durationMs, 0),
58896
- results: summaries.flatMap((s) => s.results)
58897
- };
59414
+ });
58898
59415
  return {
58899
59416
  ok: true,
58900
59417
  summary: redact(summary, await secretsOf(context)),
@@ -58935,22 +59452,25 @@ function redact(value, secrets) {
58935
59452
  return walk(value);
58936
59453
  }
58937
59454
  async function stepLines(file2) {
59455
+ const lines = { setup: [], steps: [], teardown: [] };
58938
59456
  try {
58939
59457
  const counter = new import_yaml7.LineCounter();
58940
59458
  const document = (0, import_yaml7.parseDocument)(await fs9.readFile(file2, "utf8"), { lineCounter: counter });
58941
- const steps = document.get("steps", true);
58942
- if (!steps || typeof steps !== "object" || !("items" in steps)) return [];
58943
- return steps.items.map(
58944
- (item) => item?.range ? counter.linePos(item.range[0]).line : null
58945
- );
59459
+ const of = (key) => {
59460
+ const list = document.get(key, true);
59461
+ if (!list || typeof list !== "object" || !("items" in list)) return [];
59462
+ return list.items.map(
59463
+ (item) => item?.range ? counter.linePos(item.range[0]).line : null
59464
+ );
59465
+ };
59466
+ return { setup: of("setup"), steps: of("steps"), teardown: of("teardown") };
58946
59467
  } catch {
58947
- return [];
59468
+ return lines;
58948
59469
  }
58949
59470
  }
58950
59471
 
58951
59472
  export {
58952
59473
  __toESM,
58953
- require_dist,
58954
59474
  external_exports,
58955
59475
  TagSchema,
58956
59476
  stepsForTags,
@@ -58959,7 +59479,9 @@ export {
58959
59479
  samePath,
58960
59480
  relativePosix,
58961
59481
  environmentNames,
59482
+ resultName,
58962
59483
  parseMarkdown,
59484
+ require_dist,
58963
59485
  COLLECTIONS_DIR,
58964
59486
  SETTINGS_FILE,
58965
59487
  DOC_EXTENSION,
@@ -58968,6 +59490,7 @@ export {
58968
59490
  projectEnvironments,
58969
59491
  loadProjectCollections,
58970
59492
  loadProjectTls,
59493
+ referenceProblems,
58971
59494
  resolveFlags,
58972
59495
  flagOverrides,
58973
59496
  unattempted,