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/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.
|
|
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 (
|
|
6734
|
-
lastResult
|
|
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
|
-
|
|
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 (
|
|
6965
|
-
lastResult
|
|
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
|
-
|
|
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(
|