prowl-tools 0.1.9 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -74,7 +74,7 @@ var init_hunt_name = __esm({
74
74
  });
75
75
 
76
76
  // src/config/schema.ts
77
- var import_zod, webTargetSchema, macosTargetSchema, androidTargetSchema, iosTargetSchema, targetSchema, configSchema, navigateStepSchema, clickStepSchema, singleKeyValueSchema, fillStepSchema, typeStepSchema, pressStepSchema, waitForSelectorStepSchema, waitStepSchema, waitForUrlStepSchema, waitForNetworkIdleStepSchema, selectOptionStepSchema, selectStepSchema, onDialogStepSchema, setInputFilesStepSchema, inlineAssertStepSchema, runHuntStepSchema, hoverStepSchema, scrollStepSchema, scrollToStepSchema, screenshotStepSchema, ifStepSchema, repeatStepSchema, mockRouteStepSchema, unmockRouteStepSchema, evalScriptStepSchema, runScriptStepSchema, assertScreenshotStepSchema, assertWithAiStepSchema, copyTextStepSchema, waitForDownloadStepSchema, stepSchema, assertionSchema, huntSchema;
77
+ var import_zod, webTargetSchema, macosTargetSchema, androidTargetSchema, iosTargetSchema, targetSchema, configSchema, navigateStepSchema, clickStepSchema, singleKeyValueSchema, fillStepSchema, typeStepSchema, pressStepSchema, waitForSelectorStepSchema, waitStepSchema, waitForUrlStepSchema, waitForNetworkIdleStepSchema, waitForResponseStatusSchema, waitForResponseTimeoutSchema, waitForResponseStepSchema, selectOptionStepSchema, selectStepSchema, onDialogStepSchema, setInputFilesStepSchema, inlineAssertStepSchema, runHuntStepSchema, hoverStepSchema, scrollStepSchema, scrollToStepSchema, screenshotStepSchema, ifStepSchema, repeatStepSchema, mockRouteStepSchema, unmockRouteStepSchema, evalScriptStepSchema, runScriptStepSchema, assertScreenshotStepSchema, assertWithAiStepSchema, copyTextStepSchema, waitForDownloadStepSchema, stepSchema, assertionSchema, huntSchema;
78
78
  var init_schema = __esm({
79
79
  "src/config/schema.ts"() {
80
80
  "use strict";
@@ -199,6 +199,21 @@ var init_schema = __esm({
199
199
  waitForNetworkIdleStepSchema = import_zod.z.object({
200
200
  waitForNetworkIdle: import_zod.z.object({ timeout: import_zod.z.number().optional() }).strict()
201
201
  }).strict();
202
+ waitForResponseStatusSchema = import_zod.z.number().int().min(100).max(599);
203
+ waitForResponseTimeoutSchema = import_zod.z.number().int().min(0).finite();
204
+ waitForResponseStepSchema = import_zod.z.object({
205
+ waitForResponse: import_zod.z.object({
206
+ // Matched against each response URL as a glob (`*`/`**` match any run of
207
+ // characters including `/`, `?` matches one character); matching is
208
+ // unanchored, so a pattern with no wildcards behaves as a substring match.
209
+ url: import_zod.z.string().min(1).describe(
210
+ "URL glob to wait for; unanchored, so a wildcard-free pattern is a substring match"
211
+ ),
212
+ // Optional status filter: only resolve on a response with this exact HTTP status.
213
+ status: waitForResponseStatusSchema.optional(),
214
+ timeout: waitForResponseTimeoutSchema.optional()
215
+ }).strict()
216
+ }).strict();
202
217
  selectOptionStepSchema = import_zod.z.object({
203
218
  selectOption: import_zod.z.object({ selector: import_zod.z.string().min(1), value: import_zod.z.string() }).strict()
204
219
  }).strict();
@@ -360,6 +375,7 @@ var init_schema = __esm({
360
375
  waitForSelectorStepSchema,
361
376
  waitForUrlStepSchema,
362
377
  waitForNetworkIdleStepSchema,
378
+ waitForResponseStepSchema,
363
379
  hoverStepSchema,
364
380
  scrollStepSchema,
365
381
  scrollToStepSchema,
@@ -592,6 +608,16 @@ function interpolateStep(step, vars, stepPath2, redacted) {
592
608
  if ("waitForNetworkIdle" in step) {
593
609
  return { waitForNetworkIdle: { timeout: step.waitForNetworkIdle.timeout } };
594
610
  }
611
+ if ("waitForResponse" in step) {
612
+ const urlResult = interpolateString(step.waitForResponse.url, vars);
613
+ return {
614
+ waitForResponse: {
615
+ url: urlResult.value,
616
+ ...step.waitForResponse.status !== void 0 ? { status: step.waitForResponse.status } : {},
617
+ timeout: step.waitForResponse.timeout
618
+ }
619
+ };
620
+ }
595
621
  if ("hover" in step) {
596
622
  const selectorResult = interpolateString(step.hover.selector, vars);
597
623
  return { hover: { selector: selectorResult.value } };
@@ -1133,7 +1159,7 @@ var import_commander15 = require("commander");
1133
1159
  // package.json
1134
1160
  var package_default = {
1135
1161
  name: "prowl-tools",
1136
- version: "0.1.9",
1162
+ version: "0.1.10",
1137
1163
  description: "E2E testing for native macOS apps and web apps from declarative YAML hunts.",
1138
1164
  type: "module",
1139
1165
  license: "Apache-2.0",
@@ -1192,7 +1218,8 @@ var package_default = {
1192
1218
  build: "tsup",
1193
1219
  lint: "eslint .",
1194
1220
  test: "vitest run",
1195
- "test:watch": "vitest"
1221
+ "test:watch": "vitest",
1222
+ "audit:licenses": "node scripts/audit-licenses.mjs"
1196
1223
  },
1197
1224
  dependencies: {
1198
1225
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -1216,6 +1243,7 @@ var package_default = {
1216
1243
  "@typescript-eslint/eslint-plugin": "^7.18.0",
1217
1244
  "@typescript-eslint/parser": "^7.18.0",
1218
1245
  eslint: "^8.57.1",
1246
+ "license-checker-rseidelsohn": "^4.4.2",
1219
1247
  tsup: "^8.3.5",
1220
1248
  typescript: "^5.7.3",
1221
1249
  vitest: "^2.1.8"
@@ -1240,6 +1268,7 @@ var WEB_ONLY_STEP_TYPES = /* @__PURE__ */ new Set([
1240
1268
  "navigate",
1241
1269
  "waitForUrl",
1242
1270
  "waitForNetworkIdle",
1271
+ "waitForResponse",
1243
1272
  "mockRoute",
1244
1273
  "unmockRoute",
1245
1274
  "evalScript",
@@ -1625,6 +1654,9 @@ function createPlaywrightDriver(page) {
1625
1654
  async waitForNetworkIdle(options) {
1626
1655
  await page.waitForLoadState("networkidle", { timeout: options?.timeout });
1627
1656
  },
1657
+ async waitForResponse(predicate, options) {
1658
+ await page.waitForResponse((response) => predicate(response), { timeout: options?.timeout });
1659
+ },
1628
1660
  evaluate(pageFunction, arg) {
1629
1661
  const raw = page.evaluate;
1630
1662
  const result = arg === void 0 ? raw.call(page, pageFunction) : raw.call(page, pageFunction, arg);
@@ -1827,6 +1859,9 @@ function createMacDriver(client, options = {}) {
1827
1859
  waitForNetworkIdle() {
1828
1860
  return rejectUnsupported("waitForNetworkIdle");
1829
1861
  },
1862
+ waitForResponse() {
1863
+ return rejectUnsupported("waitForResponse");
1864
+ },
1830
1865
  // scripting & artifacts ------------------------------------------------
1831
1866
  evaluate() {
1832
1867
  return rejectUnsupported("evalScript");
@@ -2675,6 +2710,9 @@ function createAndroidDriver(client, options = {}) {
2675
2710
  waitForNetworkIdle() {
2676
2711
  return rejectUnsupported("waitForNetworkIdle");
2677
2712
  },
2713
+ waitForResponse() {
2714
+ return rejectUnsupported("waitForResponse");
2715
+ },
2678
2716
  // scripting & artifacts ------------------------------------------------
2679
2717
  evaluate() {
2680
2718
  return rejectUnsupported("evalScript");
@@ -3541,6 +3579,9 @@ function createIosDriver(client, options) {
3541
3579
  waitForNetworkIdle() {
3542
3580
  return rejectUnsupported("waitForNetworkIdle");
3543
3581
  },
3582
+ waitForResponse() {
3583
+ return rejectUnsupported("waitForResponse");
3584
+ },
3544
3585
  // scripting & artifacts ------------------------------------------------
3545
3586
  evaluate() {
3546
3587
  return rejectUnsupported("evalScript");
@@ -4805,6 +4846,7 @@ function getStepType(step) {
4805
4846
  if ("waitForSelector" in step) return "waitForSelector";
4806
4847
  if ("waitForUrl" in step) return "waitForUrl";
4807
4848
  if ("waitForNetworkIdle" in step) return "waitForNetworkIdle";
4849
+ if ("waitForResponse" in step) return "waitForResponse";
4808
4850
  if ("hover" in step) return "hover";
4809
4851
  if ("scroll" in step) return "scroll";
4810
4852
  if ("scrollTo" in step) return "scrollTo";
@@ -4821,6 +4863,11 @@ function getStepType(step) {
4821
4863
  if ("waitForDownload" in step) return "waitForDownload";
4822
4864
  return "step";
4823
4865
  }
4866
+ function compileResponseUrlMatcher(pattern) {
4867
+ const regexBody = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*").replace(/\\\?/g, ".");
4868
+ const regex = new RegExp(regexBody);
4869
+ return (url) => regex.test(url);
4870
+ }
4824
4871
  var RUNTIME_VAR_PATTERN = /\{\{([A-Z0-9_]+)\}\}/g;
4825
4872
  function substituteRuntimeVars(input, vars) {
4826
4873
  return input.replace(RUNTIME_VAR_PATTERN, (match, name) => {
@@ -4859,6 +4906,15 @@ function applyRuntimeVars(step, vars) {
4859
4906
  if ("waitForSelector" in step) {
4860
4907
  return { waitForSelector: { selector: sub(step.waitForSelector.selector), timeout: step.waitForSelector.timeout } };
4861
4908
  }
4909
+ if ("waitForResponse" in step) {
4910
+ return {
4911
+ waitForResponse: {
4912
+ url: sub(step.waitForResponse.url),
4913
+ ...step.waitForResponse.status !== void 0 ? { status: step.waitForResponse.status } : {},
4914
+ ...step.waitForResponse.timeout !== void 0 ? { timeout: step.waitForResponse.timeout } : {}
4915
+ }
4916
+ };
4917
+ }
4862
4918
  if ("evalScript" in step) {
4863
4919
  if (typeof step.evalScript === "string") return { evalScript: sub(step.evalScript) };
4864
4920
  return {
@@ -5468,6 +5524,32 @@ var STEP_HANDLERS = {
5468
5524
  };
5469
5525
  }
5470
5526
  },
5527
+ waitForResponse: {
5528
+ capabilities: ["response", "wait"],
5529
+ run: async (h) => {
5530
+ if (!("waitForResponse" in h.step)) unknownStep();
5531
+ const { url: pattern, status, timeout } = h.step.waitForResponse;
5532
+ const matchesResponseUrl = compileResponseUrlMatcher(pattern);
5533
+ try {
5534
+ await h.driver.waitForResponse(
5535
+ (response) => matchesResponseUrl(response.url()) && (status === void 0 || response.status() === status),
5536
+ { timeout }
5537
+ );
5538
+ } catch (err) {
5539
+ const isTimeout = err instanceof Error && (err.name === "TimeoutError" || /timeout/i.test(err.message));
5540
+ if (!isTimeout) throw err;
5541
+ const statusPart = status !== void 0 ? ` with status ${status}` : "";
5542
+ const timeoutPart = timeout !== void 0 ? ` within ${timeout}ms` : "";
5543
+ throw new Error(
5544
+ `waitForResponse: no response matching "${pattern}"${statusPart} was received${timeoutPart}.`
5545
+ );
5546
+ }
5547
+ return {
5548
+ kind: "result",
5549
+ result: { type: "waitForResponse", status: "pass", durationMs: Date.now() - h.stepStart, value: pattern }
5550
+ };
5551
+ }
5552
+ },
5471
5553
  waitForNetworkIdle: {
5472
5554
  capabilities: ["wait"],
5473
5555
  run: async (h) => {
@@ -6258,6 +6340,15 @@ function formatStep(step) {
6258
6340
  const error = step.error ? ` error=${escapeMd(step.error)}` : "";
6259
6341
  return `${base}${selector}${healed}${value}${error}`;
6260
6342
  }
6343
+ function formatRetryAttempt(attempt) {
6344
+ const base = `- attempt ${attempt.attempt}: ${attempt.status.toUpperCase()} (${attempt.durationMs}ms)`;
6345
+ if (attempt.status === "pass") {
6346
+ return base;
6347
+ }
6348
+ const step = attempt.failedStep ? ` failed-step=[${attempt.failedStep.index}] ${escapeMd(attempt.failedStep.type)}` : "";
6349
+ const error = attempt.error ? ` error=${escapeMd(attempt.error)}` : "";
6350
+ return `${base}${step}${error}`;
6351
+ }
6261
6352
  function formatAssertion(assertion) {
6262
6353
  const value = assertion.value !== void 0 ? ` value=${typeof assertion.value === "string" ? escapeMd(assertion.value) : assertion.value}` : "";
6263
6354
  const error = assertion.error ? ` error=${escapeMd(assertion.error)}` : "";
@@ -6272,6 +6363,16 @@ function writeSummary(runDir, result) {
6272
6363
  lines.push(`Target: ${result.targetUrl}`);
6273
6364
  lines.push(`Started: ${result.startedAt}`);
6274
6365
  lines.push(`Duration: ${result.durationMs}ms`);
6366
+ if (result.retryHistory && result.retryHistory.length > 1) {
6367
+ lines.push("");
6368
+ lines.push("## Retries");
6369
+ if (result.retrySummary) {
6370
+ lines.push(escapeMd(result.retrySummary));
6371
+ }
6372
+ for (const attempt of result.retryHistory) {
6373
+ lines.push(formatRetryAttempt(attempt));
6374
+ }
6375
+ }
6275
6376
  lines.push("");
6276
6377
  lines.push("## Steps");
6277
6378
  for (const step of result.steps) {
@@ -6432,7 +6533,8 @@ function isHistoryEntry(value) {
6432
6533
  return false;
6433
6534
  }
6434
6535
  const entry = value;
6435
- return typeof entry.hunt === "string" && (entry.status === "pass" || entry.status === "fail") && typeof entry.durationMs === "number" && Number.isFinite(entry.durationMs) && typeof entry.startedAt === "string" && (entry.runDir === void 0 || typeof entry.runDir === "string");
6536
+ return typeof entry.hunt === "string" && (entry.status === "pass" || entry.status === "fail") && typeof entry.durationMs === "number" && Number.isFinite(entry.durationMs) && typeof entry.startedAt === "string" && (entry.runDir === void 0 || typeof entry.runDir === "string") && // Optional (PROWL-033): older history.json files predate `retries`.
6537
+ (entry.retries === void 0 || typeof entry.retries === "number" && Number.isInteger(entry.retries) && entry.retries >= 0);
6436
6538
  }
6437
6539
  function readHistory(configDir) {
6438
6540
  const filePath = historyPath(configDir);
@@ -6684,6 +6786,60 @@ async function executeHuntAttempt(options, config, configDir, interpolatedHunt,
6684
6786
  function delay3(ms) {
6685
6787
  return new Promise((resolve) => setTimeout(resolve, ms));
6686
6788
  }
6789
+ function toRetryAttempt(attempt, result) {
6790
+ const record = {
6791
+ attempt,
6792
+ status: result.status,
6793
+ durationMs: result.durationMs
6794
+ };
6795
+ if (result.status === "fail") {
6796
+ const failedIndex = result.steps.findIndex((step) => step.status === "fail");
6797
+ if (failedIndex >= 0) {
6798
+ const failed = result.steps[failedIndex];
6799
+ record.failedStep = { index: failedIndex, type: failed.type };
6800
+ if (failed.error) {
6801
+ record.error = failed.error;
6802
+ }
6803
+ }
6804
+ if (!record.error) {
6805
+ const failedAssertion = result.assertions.find((assertion) => assertion.status === "fail");
6806
+ if (failedAssertion?.error) {
6807
+ record.error = failedAssertion.error;
6808
+ }
6809
+ }
6810
+ }
6811
+ return record;
6812
+ }
6813
+ function describeAttemptFailure(attempt) {
6814
+ if (attempt.status !== "fail") {
6815
+ return void 0;
6816
+ }
6817
+ const step = attempt.failedStep?.type;
6818
+ if (step && attempt.error) {
6819
+ return `${step} (${attempt.error})`;
6820
+ }
6821
+ return step ?? attempt.error;
6822
+ }
6823
+ function buildRetrySummary(attempts, maxRetries) {
6824
+ const final = attempts[attempts.length - 1];
6825
+ const firstFailure = describeAttemptFailure(attempts[0]);
6826
+ const suffix = firstFailure ? ` \u2014 first failure: ${firstFailure}` : "";
6827
+ if (final.status === "pass") {
6828
+ return `Passed on attempt ${final.attempt} of ${maxRetries + 1}${suffix}`;
6829
+ }
6830
+ return `Failed after ${attempts.length} attempts${suffix}`;
6831
+ }
6832
+ function finalizeRetryOutcome(outcome, attempts, maxRetries, junit) {
6833
+ outcome.result = writeReports(
6834
+ outcome.runDir,
6835
+ {
6836
+ ...outcome.result,
6837
+ retryHistory: attempts,
6838
+ retrySummary: buildRetrySummary(attempts, maxRetries)
6839
+ },
6840
+ { junit }
6841
+ );
6842
+ }
6687
6843
  async function runHunt(options) {
6688
6844
  const { config, configDir } = loadConfig(options.configPath);
6689
6845
  if (config.target.type === "macos") {
@@ -6713,7 +6869,9 @@ async function runHunt(options) {
6713
6869
  }
6714
6870
  const maxRetries = hunt.retry?.maxRetries ?? 0;
6715
6871
  const retryDelay = hunt.retry?.delay ?? 0;
6872
+ const junit = options.junit ?? config.artifacts.junit;
6716
6873
  let lastResult;
6874
+ const attempts = [];
6717
6875
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
6718
6876
  if (attempt > 0 && retryDelay > 0) {
6719
6877
  await delay3(retryDelay);
@@ -6729,19 +6887,20 @@ async function runHunt(options) {
6729
6887
  targetUrl,
6730
6888
  allowedDomains
6731
6889
  );
6890
+ attempts.push(toRetryAttempt(attempt + 1, lastResult.result));
6732
6891
  if (lastResult.result.status === "pass") {
6733
- if (attempt > 0) {
6734
- lastResult.result.artifacts.summary = `Passed on attempt ${attempt + 1} of ${maxRetries + 1}`;
6892
+ if (attempts.length > 1) {
6893
+ finalizeRetryOutcome(lastResult, attempts, maxRetries, junit);
6735
6894
  }
6736
- recordHistory(configDir, lastResult, config.history.maxRuns);
6895
+ recordHistory(configDir, lastResult, config.history.maxRuns, attempts.length - 1);
6737
6896
  return lastResult;
6738
6897
  }
6739
6898
  }
6740
- if (maxRetries > 0 && lastResult) {
6741
- lastResult.result.artifacts.summary = `Failed after ${maxRetries + 1} attempts`;
6742
- }
6743
6899
  if (lastResult) {
6744
- recordHistory(configDir, lastResult, config.history.maxRuns);
6900
+ if (attempts.length > 1) {
6901
+ finalizeRetryOutcome(lastResult, attempts, maxRetries, junit);
6902
+ }
6903
+ recordHistory(configDir, lastResult, config.history.maxRuns, attempts.length - 1);
6745
6904
  }
6746
6905
  return lastResult;
6747
6906
  }
@@ -6945,7 +7104,9 @@ async function runNativeHunt(options, config, configDir, target, native) {
6945
7104
  }
6946
7105
  const maxRetries = hunt.retry?.maxRetries ?? 0;
6947
7106
  const retryDelay = hunt.retry?.delay ?? 0;
7107
+ const junit = options.junit ?? config.artifacts.junit;
6948
7108
  let lastResult;
7109
+ const attempts = [];
6949
7110
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
6950
7111
  if (attempt > 0 && retryDelay > 0) {
6951
7112
  await delay3(retryDelay);
@@ -6960,23 +7121,24 @@ async function runNativeHunt(options, config, configDir, target, native) {
6960
7121
  randomVars,
6961
7122
  config.guardrails.allowedApps
6962
7123
  );
7124
+ attempts.push(toRetryAttempt(attempt + 1, lastResult.result));
6963
7125
  if (lastResult.result.status === "pass") {
6964
- if (attempt > 0) {
6965
- lastResult.result.artifacts.summary = `Passed on attempt ${attempt + 1} of ${maxRetries + 1}`;
7126
+ if (attempts.length > 1) {
7127
+ finalizeRetryOutcome(lastResult, attempts, maxRetries, junit);
6966
7128
  }
6967
- recordHistory(configDir, lastResult, config.history.maxRuns);
7129
+ recordHistory(configDir, lastResult, config.history.maxRuns, attempts.length - 1);
6968
7130
  return lastResult;
6969
7131
  }
6970
7132
  }
6971
- if (maxRetries > 0 && lastResult) {
6972
- lastResult.result.artifacts.summary = `Failed after ${maxRetries + 1} attempts`;
6973
- }
6974
7133
  if (lastResult) {
6975
- recordHistory(configDir, lastResult, config.history.maxRuns);
7134
+ if (attempts.length > 1) {
7135
+ finalizeRetryOutcome(lastResult, attempts, maxRetries, junit);
7136
+ }
7137
+ recordHistory(configDir, lastResult, config.history.maxRuns, attempts.length - 1);
6976
7138
  }
6977
7139
  return lastResult;
6978
7140
  }
6979
- function recordHistory(configDir, outcome, maxRuns) {
7141
+ function recordHistory(configDir, outcome, maxRuns, retries = 0) {
6980
7142
  try {
6981
7143
  const relativeRunDir = import_node_path15.default.relative(configDir, outcome.runDir);
6982
7144
  appendEntry(
@@ -6986,7 +7148,9 @@ function recordHistory(configDir, outcome, maxRuns) {
6986
7148
  status: outcome.result.status,
6987
7149
  durationMs: outcome.result.durationMs,
6988
7150
  startedAt: outcome.result.startedAt,
6989
- runDir: relativeRunDir || void 0
7151
+ runDir: relativeRunDir || void 0,
7152
+ // Omit when no retry ran so entries stay additive/backward-compatible.
7153
+ retries: retries > 0 ? retries : void 0
6990
7154
  },
6991
7155
  maxRuns
6992
7156
  );
@@ -7041,6 +7205,10 @@ function describeStep(step) {
7041
7205
  if ("waitForSelector" in step) return `waitForSelector "${step.waitForSelector.selector}"`;
7042
7206
  if ("waitForUrl" in step) return `waitForUrl "${step.waitForUrl.value}"`;
7043
7207
  if ("waitForNetworkIdle" in step) return "waitForNetworkIdle";
7208
+ if ("waitForResponse" in step) {
7209
+ const statusSuffix = step.waitForResponse.status !== void 0 ? ` (status ${step.waitForResponse.status})` : "";
7210
+ return `waitForResponse "${step.waitForResponse.url}"${statusSuffix}`;
7211
+ }
7044
7212
  if ("hover" in step) return `hover "${step.hover.selector}"`;
7045
7213
  if ("scroll" in step) return `scroll ${step.scroll.direction} ${step.scroll.amount ?? 500}px`;
7046
7214
  if ("scrollTo" in step) return `scrollTo "${step.scrollTo.selector}"`;
@@ -7104,6 +7272,10 @@ function printHuntSummary(result, runDir) {
7104
7272
  const duration = import_chalk.default.gray(`(${result.durationMs}ms)`);
7105
7273
  console.log(`
7106
7274
  ${status} ${import_chalk.default.bold(result.hunt)} ${duration} ${stepCount}`);
7275
+ if (result.retrySummary) {
7276
+ const color = result.status === "pass" ? import_chalk.default.yellow : import_chalk.default.red;
7277
+ console.log(` ${color(result.retrySummary)}`);
7278
+ }
7107
7279
  console.log(` ${import_chalk.default.gray("Artifacts:")} ${runDir}
7108
7280
  `);
7109
7281
  }
@@ -9223,6 +9395,7 @@ var STEP_REFERENCE = `
9223
9395
  - waitForSelector: { selector: "#el", timeout: 5000 }
9224
9396
  - waitForUrl: { value: "/path", timeout: 5000 }
9225
9397
  - waitForNetworkIdle: { timeout: 5000 }
9398
+ - waitForResponse: { url: "**/api/orders", status: 200, timeout: 10000 } \u2014 wait for a matching network response (url is a glob/substring; status is optional)
9226
9399
 
9227
9400
  ### Interaction
9228
9401
  - click: "Button Text" \u2014 click by text (tries role=button first)
@@ -9463,6 +9636,7 @@ function buildHistoryCommand() {
9463
9636
  }
9464
9637
  console.log();
9465
9638
  console.log(` ${import_chalk13.default.bold(huntName)} \u2014 last ${recent.length} of ${entries.length} runs`);
9639
+ console.log(` ${import_chalk13.default.gray(formatRetryFrequency(recent))}`);
9466
9640
  console.log();
9467
9641
  console.log(formatTable(recent));
9468
9642
  console.log();
@@ -9478,12 +9652,23 @@ function buildHistoryCommand() {
9478
9652
  });
9479
9653
  return command;
9480
9654
  }
9655
+ function formatRetryFrequency(entries) {
9656
+ const retried = entries.filter((entry) => (entry.retries ?? 0) > 0).length;
9657
+ if (retried === 0) {
9658
+ return "No retries in these runs";
9659
+ }
9660
+ return `Retried in ${retried} of ${entries.length} runs`;
9661
+ }
9662
+ function formatRetries(retries) {
9663
+ return retries && retries > 0 ? String(retries) : "-";
9664
+ }
9481
9665
  function formatTable(entries) {
9482
- const headers = ["Status", "Started", "Duration"];
9666
+ const headers = ["Status", "Started", "Duration", "Retries"];
9483
9667
  const rows = entries.map((entry) => [
9484
9668
  formatStatus(entry.status),
9485
9669
  entry.startedAt,
9486
- formatDuration(entry.durationMs)
9670
+ formatDuration(entry.durationMs),
9671
+ formatRetries(entry.retries)
9487
9672
  ]);
9488
9673
  const widths = headers.map(
9489
9674
  (header, index) => Math.max(