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/README.md +61 -8
- package/dist/{chunk-2RNOK64Y.js → chunk-SRWAF2RI.js} +156 -19
- package/dist/chunk-SRWAF2RI.js.map +1 -0
- package/dist/{chunk-JFJQNJSJ.js → chunk-TTZ2IL3P.js} +27 -1
- package/dist/chunk-TTZ2IL3P.js.map +1 -0
- package/dist/index.cjs +207 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +29 -7
- package/dist/index.js.map +1 -1
- package/dist/lib.cjs +180 -17
- package/dist/lib.cjs.map +1 -1
- package/dist/lib.d.cts +48 -1
- package/dist/lib.d.ts +48 -1
- package/dist/lib.js +2 -2
- package/dist/{loader-JTHA4BYG.js → loader-CAE4FPBK.js} +2 -2
- package/package.json +4 -2
- package/dist/chunk-2RNOK64Y.js.map +0 -1
- package/dist/chunk-JFJQNJSJ.js.map +0 -1
- /package/dist/{loader-JTHA4BYG.js.map → loader-CAE4FPBK.js.map} +0 -0
package/README.md
CHANGED
|
@@ -389,7 +389,7 @@ macOS target (with a clear error), and `prowl login` / URL guardrails do not app
|
|
|
389
389
|
|
|
390
390
|
| Portable (web + macOS) | Web-only (rejected on macOS) |
|
|
391
391
|
|---|---|
|
|
392
|
-
| `click`, `fill`, `type`, `press` | `navigate`, `waitForUrl`, `waitForNetworkIdle` |
|
|
392
|
+
| `click`, `fill`, `type`, `press` | `navigate`, `waitForUrl`, `waitForNetworkIdle`, `waitForResponse` |
|
|
393
393
|
| `wait`, `waitForSelector` | `mockRoute` / `unmockRoute` |
|
|
394
394
|
| `assert: visible` / `notVisible` | `evalScript`, `runScript` |
|
|
395
395
|
| `screenshot`, `assertScreenshot` | `onDialog`, `select` / `selectOption` |
|
|
@@ -665,6 +665,21 @@ Wait for all network requests to complete.
|
|
|
665
665
|
timeout: 5000
|
|
666
666
|
```
|
|
667
667
|
|
|
668
|
+
### waitForResponse
|
|
669
|
+
|
|
670
|
+
Wait for a specific network response before continuing — more precise than `waitForNetworkIdle`, which waits for *all* traffic to settle.
|
|
671
|
+
|
|
672
|
+
```yaml
|
|
673
|
+
- waitForResponse:
|
|
674
|
+
url: "**/api/orders"
|
|
675
|
+
status: 200
|
|
676
|
+
timeout: 10000
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
- `url` (required) is matched against each response URL as a glob: `*`/`**` match any run of characters (including `/`) and `?` matches a single character. Matching is unanchored, so a pattern with no wildcards behaves as a substring match (e.g. `/api/orders` matches `https://shop.test/api/orders?page=2`).
|
|
680
|
+
- `status` (optional) resolves the step only on a response with that exact HTTP status.
|
|
681
|
+
- `timeout` (optional, milliseconds) fails the step with a clear message if no matching response arrives in time.
|
|
682
|
+
|
|
668
683
|
### onDialog
|
|
669
684
|
|
|
670
685
|
Handle browser-native dialogs (alert, confirm, prompt). Register the handler **before** the action that triggers the dialog.
|
|
@@ -1014,6 +1029,39 @@ tracing:
|
|
|
1014
1029
|
This is a correlation bridge only — Prowl does not generate or propagate its own
|
|
1015
1030
|
spans. When the app emits no trace headers, nothing is recorded (no noise).
|
|
1016
1031
|
|
|
1032
|
+
### Retry Diagnostics (why a retried hunt passed)
|
|
1033
|
+
|
|
1034
|
+
When a hunt sets a `retry` block and eventually passes — or exhausts its
|
|
1035
|
+
attempts — Prowl records what each attempt did instead of retrying silently:
|
|
1036
|
+
|
|
1037
|
+
```yaml
|
|
1038
|
+
# In a hunt file
|
|
1039
|
+
retry:
|
|
1040
|
+
maxRetries: 2 # up to 2 extra attempts (3 total)
|
|
1041
|
+
delay: 500 # optional ms to wait between attempts
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
The final attempt's `result.json` (and the returned run result) then carries:
|
|
1045
|
+
|
|
1046
|
+
- `retryHistory` — one record per attempt: its `attempt` number, `status`,
|
|
1047
|
+
`durationMs`, the first `failedStep` (`index` + `type`), and the `error`.
|
|
1048
|
+
- `retrySummary` — a one-line headline, e.g.
|
|
1049
|
+
`Passed on attempt 2 of 3 — first failure: navigate (timeout)`, or
|
|
1050
|
+
`Failed after 3 attempts — first failure: …` when the retries were exhausted.
|
|
1051
|
+
|
|
1052
|
+
Both fields appear **only** when more than one attempt ran, so a hunt that passes
|
|
1053
|
+
on the first try (and any run artifact written before this feature) stays
|
|
1054
|
+
unchanged and keeps parsing. `summary.md` gains a matching **Retries** section
|
|
1055
|
+
with the headline and a per-attempt breakdown, and the `prowl run` summary prints
|
|
1056
|
+
the headline. JUnit continues to report the final attempt's outcome — the
|
|
1057
|
+
per-attempt history lives in `result.json`, not the XML — so a retried-and-passed
|
|
1058
|
+
hunt is a green testcase for CI while the diagnostics remain available alongside
|
|
1059
|
+
it.
|
|
1060
|
+
|
|
1061
|
+
This is what lets you distinguish a **flaky test** (passes on retry, intermittent
|
|
1062
|
+
first-attempt failures) from a **slow environment** (retries needed but the same
|
|
1063
|
+
step lags) from a **real regression** (all attempts fail the same way).
|
|
1064
|
+
|
|
1017
1065
|
---
|
|
1018
1066
|
|
|
1019
1067
|
## CLI Reference
|
|
@@ -1097,10 +1145,11 @@ remedy.
|
|
|
1097
1145
|
### Run History
|
|
1098
1146
|
|
|
1099
1147
|
Every `prowl run` and `prowl ci` appends an entry to `.prowl/history.json`
|
|
1100
|
-
with the hunt name, status, start time, duration,
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1148
|
+
with the hunt name, status, start time, duration, run directory, and — when the
|
|
1149
|
+
run used `retry` — a `retries` count (the number of extra attempts beyond the
|
|
1150
|
+
first; omitted when the run passed on its first attempt). Retention is capped
|
|
1151
|
+
per hunt by `history.maxRuns` (default 100) — once a hunt exceeds the cap, its
|
|
1152
|
+
oldest entries are dropped on the next write. Other hunts are not affected.
|
|
1104
1153
|
|
|
1105
1154
|
```yaml
|
|
1106
1155
|
# In .prowl/config.yml
|
|
@@ -1109,7 +1158,11 @@ history:
|
|
|
1109
1158
|
```
|
|
1110
1159
|
|
|
1111
1160
|
Use `prowl history <hunt-name>` for a quick status/duration table, or
|
|
1112
|
-
`--json` to feed the entries into dashboards, flake detectors, or agents.
|
|
1161
|
+
`--json` to feed the entries into dashboards, flake detectors, or agents. The
|
|
1162
|
+
table includes a **Retries** column and a retry-frequency line
|
|
1163
|
+
(`Retried in N of M runs`) so you can tell a flaky hunt apart from a slow
|
|
1164
|
+
environment or a real regression at a glance. History from before retry
|
|
1165
|
+
tracking (entries without a `retries` field) still loads and reads as `0`.
|
|
1113
1166
|
|
|
1114
1167
|
### Failure Clustering
|
|
1115
1168
|
|
|
@@ -1395,7 +1448,7 @@ Portable steps run on the Android target; web-only steps are rejected up front
|
|
|
1395
1448
|
|
|
1396
1449
|
| Portable (Android) | Not supported on Android |
|
|
1397
1450
|
|---|---|
|
|
1398
|
-
| `click`, `fill`, `type`, `press` | `navigate`, `waitForUrl`, `waitForNetworkIdle` |
|
|
1451
|
+
| `click`, `fill`, `type`, `press` | `navigate`, `waitForUrl`, `waitForNetworkIdle`, `waitForResponse` |
|
|
1399
1452
|
| `wait`, `waitForSelector` | `mockRoute` / `unmockRoute`, `evalScript`, `runScript` |
|
|
1400
1453
|
| `assert: visible` / `notVisible` | `onDialog`, `select` / `selectOption`, `setInputFiles` |
|
|
1401
1454
|
| `screenshot`, `assertScreenshot` | `waitForDownload`, `scroll`, `assert: urlIncludes` / `urlEquals` |
|
|
@@ -1545,7 +1598,7 @@ clear error), and URL guardrails do not apply.
|
|
|
1545
1598
|
|
|
1546
1599
|
| Portable (iOS) | Not supported on iOS |
|
|
1547
1600
|
|---|---|
|
|
1548
|
-
| `click`, `fill`, `type`, `press` | `navigate`, `waitForUrl`, `waitForNetworkIdle` |
|
|
1601
|
+
| `click`, `fill`, `type`, `press` | `navigate`, `waitForUrl`, `waitForNetworkIdle`, `waitForResponse` |
|
|
1549
1602
|
| `wait`, `waitForSelector` | `mockRoute` / `unmockRoute`, `evalScript`, `runScript` |
|
|
1550
1603
|
| `assert: visible` / `notVisible` | `onDialog`, `select` / `selectOption`, `setInputFiles` |
|
|
1551
1604
|
| `screenshot`, `assertScreenshot` | `waitForDownload`, `scroll`, `assert: urlIncludes` / `urlEquals` |
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
loadHunt,
|
|
9
9
|
loadHuntTags,
|
|
10
10
|
resolveViewport
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-TTZ2IL3P.js";
|
|
12
12
|
|
|
13
13
|
// src/config/target.ts
|
|
14
14
|
import { execFileSync } from "child_process";
|
|
@@ -18,6 +18,7 @@ var WEB_ONLY_STEP_TYPES = /* @__PURE__ */ new Set([
|
|
|
18
18
|
"navigate",
|
|
19
19
|
"waitForUrl",
|
|
20
20
|
"waitForNetworkIdle",
|
|
21
|
+
"waitForResponse",
|
|
21
22
|
"mockRoute",
|
|
22
23
|
"unmockRoute",
|
|
23
24
|
"evalScript",
|
|
@@ -372,6 +373,9 @@ function createMacDriver(client, options = {}) {
|
|
|
372
373
|
waitForNetworkIdle() {
|
|
373
374
|
return rejectUnsupported("waitForNetworkIdle");
|
|
374
375
|
},
|
|
376
|
+
waitForResponse() {
|
|
377
|
+
return rejectUnsupported("waitForResponse");
|
|
378
|
+
},
|
|
375
379
|
// scripting & artifacts ------------------------------------------------
|
|
376
380
|
evaluate() {
|
|
377
381
|
return rejectUnsupported("evalScript");
|
|
@@ -1294,6 +1298,9 @@ function createAndroidDriver(client, options = {}) {
|
|
|
1294
1298
|
waitForNetworkIdle() {
|
|
1295
1299
|
return rejectUnsupported("waitForNetworkIdle");
|
|
1296
1300
|
},
|
|
1301
|
+
waitForResponse() {
|
|
1302
|
+
return rejectUnsupported("waitForResponse");
|
|
1303
|
+
},
|
|
1297
1304
|
// scripting & artifacts ------------------------------------------------
|
|
1298
1305
|
evaluate() {
|
|
1299
1306
|
return rejectUnsupported("evalScript");
|
|
@@ -2156,6 +2163,9 @@ function createIosDriver(client, options) {
|
|
|
2156
2163
|
waitForNetworkIdle() {
|
|
2157
2164
|
return rejectUnsupported("waitForNetworkIdle");
|
|
2158
2165
|
},
|
|
2166
|
+
waitForResponse() {
|
|
2167
|
+
return rejectUnsupported("waitForResponse");
|
|
2168
|
+
},
|
|
2159
2169
|
// scripting & artifacts ------------------------------------------------
|
|
2160
2170
|
evaluate() {
|
|
2161
2171
|
return rejectUnsupported("evalScript");
|
|
@@ -3080,7 +3090,8 @@ function isHistoryEntry(value) {
|
|
|
3080
3090
|
return false;
|
|
3081
3091
|
}
|
|
3082
3092
|
const entry = value;
|
|
3083
|
-
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")
|
|
3093
|
+
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`.
|
|
3094
|
+
(entry.retries === void 0 || typeof entry.retries === "number" && Number.isInteger(entry.retries) && entry.retries >= 0);
|
|
3084
3095
|
}
|
|
3085
3096
|
function readHistory(configDir) {
|
|
3086
3097
|
const filePath = historyPath(configDir);
|
|
@@ -3351,6 +3362,9 @@ function createPlaywrightDriver(page) {
|
|
|
3351
3362
|
async waitForNetworkIdle(options) {
|
|
3352
3363
|
await page.waitForLoadState("networkidle", { timeout: options?.timeout });
|
|
3353
3364
|
},
|
|
3365
|
+
async waitForResponse(predicate, options) {
|
|
3366
|
+
await page.waitForResponse((response) => predicate(response), { timeout: options?.timeout });
|
|
3367
|
+
},
|
|
3354
3368
|
evaluate(pageFunction, arg) {
|
|
3355
3369
|
const raw = page.evaluate;
|
|
3356
3370
|
const result = arg === void 0 ? raw.call(page, pageFunction) : raw.call(page, pageFunction, arg);
|
|
@@ -3758,6 +3772,7 @@ function getStepType(step) {
|
|
|
3758
3772
|
if ("waitForSelector" in step) return "waitForSelector";
|
|
3759
3773
|
if ("waitForUrl" in step) return "waitForUrl";
|
|
3760
3774
|
if ("waitForNetworkIdle" in step) return "waitForNetworkIdle";
|
|
3775
|
+
if ("waitForResponse" in step) return "waitForResponse";
|
|
3761
3776
|
if ("hover" in step) return "hover";
|
|
3762
3777
|
if ("scroll" in step) return "scroll";
|
|
3763
3778
|
if ("scrollTo" in step) return "scrollTo";
|
|
@@ -3774,6 +3789,11 @@ function getStepType(step) {
|
|
|
3774
3789
|
if ("waitForDownload" in step) return "waitForDownload";
|
|
3775
3790
|
return "step";
|
|
3776
3791
|
}
|
|
3792
|
+
function compileResponseUrlMatcher(pattern) {
|
|
3793
|
+
const regexBody = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*").replace(/\\\?/g, ".");
|
|
3794
|
+
const regex = new RegExp(regexBody);
|
|
3795
|
+
return (url) => regex.test(url);
|
|
3796
|
+
}
|
|
3777
3797
|
var RUNTIME_VAR_PATTERN = /\{\{([A-Z0-9_]+)\}\}/g;
|
|
3778
3798
|
function substituteRuntimeVars(input, vars) {
|
|
3779
3799
|
return input.replace(RUNTIME_VAR_PATTERN, (match, name) => {
|
|
@@ -3812,6 +3832,15 @@ function applyRuntimeVars(step, vars) {
|
|
|
3812
3832
|
if ("waitForSelector" in step) {
|
|
3813
3833
|
return { waitForSelector: { selector: sub(step.waitForSelector.selector), timeout: step.waitForSelector.timeout } };
|
|
3814
3834
|
}
|
|
3835
|
+
if ("waitForResponse" in step) {
|
|
3836
|
+
return {
|
|
3837
|
+
waitForResponse: {
|
|
3838
|
+
url: sub(step.waitForResponse.url),
|
|
3839
|
+
...step.waitForResponse.status !== void 0 ? { status: step.waitForResponse.status } : {},
|
|
3840
|
+
...step.waitForResponse.timeout !== void 0 ? { timeout: step.waitForResponse.timeout } : {}
|
|
3841
|
+
}
|
|
3842
|
+
};
|
|
3843
|
+
}
|
|
3815
3844
|
if ("evalScript" in step) {
|
|
3816
3845
|
if (typeof step.evalScript === "string") return { evalScript: sub(step.evalScript) };
|
|
3817
3846
|
return {
|
|
@@ -4421,6 +4450,32 @@ var STEP_HANDLERS = {
|
|
|
4421
4450
|
};
|
|
4422
4451
|
}
|
|
4423
4452
|
},
|
|
4453
|
+
waitForResponse: {
|
|
4454
|
+
capabilities: ["response", "wait"],
|
|
4455
|
+
run: async (h) => {
|
|
4456
|
+
if (!("waitForResponse" in h.step)) unknownStep();
|
|
4457
|
+
const { url: pattern, status, timeout } = h.step.waitForResponse;
|
|
4458
|
+
const matchesResponseUrl = compileResponseUrlMatcher(pattern);
|
|
4459
|
+
try {
|
|
4460
|
+
await h.driver.waitForResponse(
|
|
4461
|
+
(response) => matchesResponseUrl(response.url()) && (status === void 0 || response.status() === status),
|
|
4462
|
+
{ timeout }
|
|
4463
|
+
);
|
|
4464
|
+
} catch (err) {
|
|
4465
|
+
const isTimeout = err instanceof Error && (err.name === "TimeoutError" || /timeout/i.test(err.message));
|
|
4466
|
+
if (!isTimeout) throw err;
|
|
4467
|
+
const statusPart = status !== void 0 ? ` with status ${status}` : "";
|
|
4468
|
+
const timeoutPart = timeout !== void 0 ? ` within ${timeout}ms` : "";
|
|
4469
|
+
throw new Error(
|
|
4470
|
+
`waitForResponse: no response matching "${pattern}"${statusPart} was received${timeoutPart}.`
|
|
4471
|
+
);
|
|
4472
|
+
}
|
|
4473
|
+
return {
|
|
4474
|
+
kind: "result",
|
|
4475
|
+
result: { type: "waitForResponse", status: "pass", durationMs: Date.now() - h.stepStart, value: pattern }
|
|
4476
|
+
};
|
|
4477
|
+
}
|
|
4478
|
+
},
|
|
4424
4479
|
waitForNetworkIdle: {
|
|
4425
4480
|
capabilities: ["wait"],
|
|
4426
4481
|
run: async (h) => {
|
|
@@ -5211,6 +5266,15 @@ function formatStep(step) {
|
|
|
5211
5266
|
const error = step.error ? ` error=${escapeMd(step.error)}` : "";
|
|
5212
5267
|
return `${base}${selector}${healed}${value}${error}`;
|
|
5213
5268
|
}
|
|
5269
|
+
function formatRetryAttempt(attempt) {
|
|
5270
|
+
const base = `- attempt ${attempt.attempt}: ${attempt.status.toUpperCase()} (${attempt.durationMs}ms)`;
|
|
5271
|
+
if (attempt.status === "pass") {
|
|
5272
|
+
return base;
|
|
5273
|
+
}
|
|
5274
|
+
const step = attempt.failedStep ? ` failed-step=[${attempt.failedStep.index}] ${escapeMd(attempt.failedStep.type)}` : "";
|
|
5275
|
+
const error = attempt.error ? ` error=${escapeMd(attempt.error)}` : "";
|
|
5276
|
+
return `${base}${step}${error}`;
|
|
5277
|
+
}
|
|
5214
5278
|
function formatAssertion(assertion) {
|
|
5215
5279
|
const value = assertion.value !== void 0 ? ` value=${typeof assertion.value === "string" ? escapeMd(assertion.value) : assertion.value}` : "";
|
|
5216
5280
|
const error = assertion.error ? ` error=${escapeMd(assertion.error)}` : "";
|
|
@@ -5225,6 +5289,16 @@ function writeSummary(runDir, result) {
|
|
|
5225
5289
|
lines.push(`Target: ${result.targetUrl}`);
|
|
5226
5290
|
lines.push(`Started: ${result.startedAt}`);
|
|
5227
5291
|
lines.push(`Duration: ${result.durationMs}ms`);
|
|
5292
|
+
if (result.retryHistory && result.retryHistory.length > 1) {
|
|
5293
|
+
lines.push("");
|
|
5294
|
+
lines.push("## Retries");
|
|
5295
|
+
if (result.retrySummary) {
|
|
5296
|
+
lines.push(escapeMd(result.retrySummary));
|
|
5297
|
+
}
|
|
5298
|
+
for (const attempt of result.retryHistory) {
|
|
5299
|
+
lines.push(formatRetryAttempt(attempt));
|
|
5300
|
+
}
|
|
5301
|
+
}
|
|
5228
5302
|
lines.push("");
|
|
5229
5303
|
lines.push("## Steps");
|
|
5230
5304
|
for (const step of result.steps) {
|
|
@@ -5535,6 +5609,60 @@ async function executeHuntAttempt(options, config, configDir, interpolatedHunt,
|
|
|
5535
5609
|
function delay3(ms) {
|
|
5536
5610
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
5537
5611
|
}
|
|
5612
|
+
function toRetryAttempt(attempt, result) {
|
|
5613
|
+
const record = {
|
|
5614
|
+
attempt,
|
|
5615
|
+
status: result.status,
|
|
5616
|
+
durationMs: result.durationMs
|
|
5617
|
+
};
|
|
5618
|
+
if (result.status === "fail") {
|
|
5619
|
+
const failedIndex = result.steps.findIndex((step) => step.status === "fail");
|
|
5620
|
+
if (failedIndex >= 0) {
|
|
5621
|
+
const failed = result.steps[failedIndex];
|
|
5622
|
+
record.failedStep = { index: failedIndex, type: failed.type };
|
|
5623
|
+
if (failed.error) {
|
|
5624
|
+
record.error = failed.error;
|
|
5625
|
+
}
|
|
5626
|
+
}
|
|
5627
|
+
if (!record.error) {
|
|
5628
|
+
const failedAssertion = result.assertions.find((assertion) => assertion.status === "fail");
|
|
5629
|
+
if (failedAssertion?.error) {
|
|
5630
|
+
record.error = failedAssertion.error;
|
|
5631
|
+
}
|
|
5632
|
+
}
|
|
5633
|
+
}
|
|
5634
|
+
return record;
|
|
5635
|
+
}
|
|
5636
|
+
function describeAttemptFailure(attempt) {
|
|
5637
|
+
if (attempt.status !== "fail") {
|
|
5638
|
+
return void 0;
|
|
5639
|
+
}
|
|
5640
|
+
const step = attempt.failedStep?.type;
|
|
5641
|
+
if (step && attempt.error) {
|
|
5642
|
+
return `${step} (${attempt.error})`;
|
|
5643
|
+
}
|
|
5644
|
+
return step ?? attempt.error;
|
|
5645
|
+
}
|
|
5646
|
+
function buildRetrySummary(attempts, maxRetries) {
|
|
5647
|
+
const final = attempts[attempts.length - 1];
|
|
5648
|
+
const firstFailure = describeAttemptFailure(attempts[0]);
|
|
5649
|
+
const suffix = firstFailure ? ` \u2014 first failure: ${firstFailure}` : "";
|
|
5650
|
+
if (final.status === "pass") {
|
|
5651
|
+
return `Passed on attempt ${final.attempt} of ${maxRetries + 1}${suffix}`;
|
|
5652
|
+
}
|
|
5653
|
+
return `Failed after ${attempts.length} attempts${suffix}`;
|
|
5654
|
+
}
|
|
5655
|
+
function finalizeRetryOutcome(outcome, attempts, maxRetries, junit) {
|
|
5656
|
+
outcome.result = writeReports(
|
|
5657
|
+
outcome.runDir,
|
|
5658
|
+
{
|
|
5659
|
+
...outcome.result,
|
|
5660
|
+
retryHistory: attempts,
|
|
5661
|
+
retrySummary: buildRetrySummary(attempts, maxRetries)
|
|
5662
|
+
},
|
|
5663
|
+
{ junit }
|
|
5664
|
+
);
|
|
5665
|
+
}
|
|
5538
5666
|
async function runHunt(options) {
|
|
5539
5667
|
const { config, configDir } = loadConfig(options.configPath);
|
|
5540
5668
|
if (config.target.type === "macos") {
|
|
@@ -5564,7 +5692,9 @@ async function runHunt(options) {
|
|
|
5564
5692
|
}
|
|
5565
5693
|
const maxRetries = hunt.retry?.maxRetries ?? 0;
|
|
5566
5694
|
const retryDelay = hunt.retry?.delay ?? 0;
|
|
5695
|
+
const junit = options.junit ?? config.artifacts.junit;
|
|
5567
5696
|
let lastResult;
|
|
5697
|
+
const attempts = [];
|
|
5568
5698
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
5569
5699
|
if (attempt > 0 && retryDelay > 0) {
|
|
5570
5700
|
await delay3(retryDelay);
|
|
@@ -5580,19 +5710,20 @@ async function runHunt(options) {
|
|
|
5580
5710
|
targetUrl,
|
|
5581
5711
|
allowedDomains
|
|
5582
5712
|
);
|
|
5713
|
+
attempts.push(toRetryAttempt(attempt + 1, lastResult.result));
|
|
5583
5714
|
if (lastResult.result.status === "pass") {
|
|
5584
|
-
if (
|
|
5585
|
-
lastResult
|
|
5715
|
+
if (attempts.length > 1) {
|
|
5716
|
+
finalizeRetryOutcome(lastResult, attempts, maxRetries, junit);
|
|
5586
5717
|
}
|
|
5587
|
-
recordHistory(configDir, lastResult, config.history.maxRuns);
|
|
5718
|
+
recordHistory(configDir, lastResult, config.history.maxRuns, attempts.length - 1);
|
|
5588
5719
|
return lastResult;
|
|
5589
5720
|
}
|
|
5590
5721
|
}
|
|
5591
|
-
if (maxRetries > 0 && lastResult) {
|
|
5592
|
-
lastResult.result.artifacts.summary = `Failed after ${maxRetries + 1} attempts`;
|
|
5593
|
-
}
|
|
5594
5722
|
if (lastResult) {
|
|
5595
|
-
|
|
5723
|
+
if (attempts.length > 1) {
|
|
5724
|
+
finalizeRetryOutcome(lastResult, attempts, maxRetries, junit);
|
|
5725
|
+
}
|
|
5726
|
+
recordHistory(configDir, lastResult, config.history.maxRuns, attempts.length - 1);
|
|
5596
5727
|
}
|
|
5597
5728
|
return lastResult;
|
|
5598
5729
|
}
|
|
@@ -5796,7 +5927,9 @@ async function runNativeHunt(options, config, configDir, target, native) {
|
|
|
5796
5927
|
}
|
|
5797
5928
|
const maxRetries = hunt.retry?.maxRetries ?? 0;
|
|
5798
5929
|
const retryDelay = hunt.retry?.delay ?? 0;
|
|
5930
|
+
const junit = options.junit ?? config.artifacts.junit;
|
|
5799
5931
|
let lastResult;
|
|
5932
|
+
const attempts = [];
|
|
5800
5933
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
5801
5934
|
if (attempt > 0 && retryDelay > 0) {
|
|
5802
5935
|
await delay3(retryDelay);
|
|
@@ -5811,23 +5944,24 @@ async function runNativeHunt(options, config, configDir, target, native) {
|
|
|
5811
5944
|
randomVars,
|
|
5812
5945
|
config.guardrails.allowedApps
|
|
5813
5946
|
);
|
|
5947
|
+
attempts.push(toRetryAttempt(attempt + 1, lastResult.result));
|
|
5814
5948
|
if (lastResult.result.status === "pass") {
|
|
5815
|
-
if (
|
|
5816
|
-
lastResult
|
|
5949
|
+
if (attempts.length > 1) {
|
|
5950
|
+
finalizeRetryOutcome(lastResult, attempts, maxRetries, junit);
|
|
5817
5951
|
}
|
|
5818
|
-
recordHistory(configDir, lastResult, config.history.maxRuns);
|
|
5952
|
+
recordHistory(configDir, lastResult, config.history.maxRuns, attempts.length - 1);
|
|
5819
5953
|
return lastResult;
|
|
5820
5954
|
}
|
|
5821
5955
|
}
|
|
5822
|
-
if (maxRetries > 0 && lastResult) {
|
|
5823
|
-
lastResult.result.artifacts.summary = `Failed after ${maxRetries + 1} attempts`;
|
|
5824
|
-
}
|
|
5825
5956
|
if (lastResult) {
|
|
5826
|
-
|
|
5957
|
+
if (attempts.length > 1) {
|
|
5958
|
+
finalizeRetryOutcome(lastResult, attempts, maxRetries, junit);
|
|
5959
|
+
}
|
|
5960
|
+
recordHistory(configDir, lastResult, config.history.maxRuns, attempts.length - 1);
|
|
5827
5961
|
}
|
|
5828
5962
|
return lastResult;
|
|
5829
5963
|
}
|
|
5830
|
-
function recordHistory(configDir, outcome, maxRuns) {
|
|
5964
|
+
function recordHistory(configDir, outcome, maxRuns, retries = 0) {
|
|
5831
5965
|
try {
|
|
5832
5966
|
const relativeRunDir = path13.relative(configDir, outcome.runDir);
|
|
5833
5967
|
appendEntry(
|
|
@@ -5837,7 +5971,9 @@ function recordHistory(configDir, outcome, maxRuns) {
|
|
|
5837
5971
|
status: outcome.result.status,
|
|
5838
5972
|
durationMs: outcome.result.durationMs,
|
|
5839
5973
|
startedAt: outcome.result.startedAt,
|
|
5840
|
-
runDir: relativeRunDir || void 0
|
|
5974
|
+
runDir: relativeRunDir || void 0,
|
|
5975
|
+
// Omit when no retry ran so entries stay additive/backward-compatible.
|
|
5976
|
+
retries: retries > 0 ? retries : void 0
|
|
5841
5977
|
},
|
|
5842
5978
|
maxRuns
|
|
5843
5979
|
);
|
|
@@ -6939,6 +7075,7 @@ var STEP_REFERENCE = `
|
|
|
6939
7075
|
- waitForSelector: { selector: "#el", timeout: 5000 }
|
|
6940
7076
|
- waitForUrl: { value: "/path", timeout: 5000 }
|
|
6941
7077
|
- waitForNetworkIdle: { timeout: 5000 }
|
|
7078
|
+
- waitForResponse: { url: "**/api/orders", status: 200, timeout: 10000 } \u2014 wait for a matching network response (url is a glob/substring; status is optional)
|
|
6942
7079
|
|
|
6943
7080
|
### Interaction
|
|
6944
7081
|
- click: "Button Text" \u2014 click by text (tries role=button first)
|
|
@@ -7506,4 +7643,4 @@ export {
|
|
|
7506
7643
|
collectMacdriverStatus,
|
|
7507
7644
|
tccGuidance
|
|
7508
7645
|
};
|
|
7509
|
-
//# sourceMappingURL=chunk-
|
|
7646
|
+
//# sourceMappingURL=chunk-SRWAF2RI.js.map
|