prowl-tools 0.1.8 → 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 +103 -14
- package/dist/{chunk-CWLRDV5P.js → chunk-SRWAF2RI.js} +180 -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 +843 -75
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +622 -43
- package/dist/index.js.map +1 -1
- package/dist/lib.cjs +204 -17
- package/dist/lib.cjs.map +1 -1
- package/dist/lib.d.cts +72 -1
- package/dist/lib.d.ts +72 -1
- package/dist/lib.js +2 -2
- package/dist/{loader-JTHA4BYG.js → loader-CAE4FPBK.js} +2 -2
- package/examples/hunts/form.yml +53 -0
- package/examples/hunts/macos-hello.yml +49 -0
- package/package.json +4 -2
- package/dist/chunk-CWLRDV5P.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/lib.cjs
CHANGED
|
@@ -394,6 +394,9 @@ function createMacDriver(client, options = {}) {
|
|
|
394
394
|
waitForNetworkIdle() {
|
|
395
395
|
return rejectUnsupported("waitForNetworkIdle");
|
|
396
396
|
},
|
|
397
|
+
waitForResponse() {
|
|
398
|
+
return rejectUnsupported("waitForResponse");
|
|
399
|
+
},
|
|
397
400
|
// scripting & artifacts ------------------------------------------------
|
|
398
401
|
evaluate() {
|
|
399
402
|
return rejectUnsupported("evalScript");
|
|
@@ -524,10 +527,15 @@ ${macdriverBuildInstructions()}`
|
|
|
524
527
|
${macdriverBuildInstructions()}`);
|
|
525
528
|
}
|
|
526
529
|
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
530
|
+
function errorMessage(error) {
|
|
531
|
+
return error instanceof Error ? error.message : String(error);
|
|
532
|
+
}
|
|
527
533
|
var SpawnMacHelperClient = class {
|
|
528
534
|
child;
|
|
529
535
|
pending = /* @__PURE__ */ new Map();
|
|
530
536
|
requestTimeoutMs;
|
|
537
|
+
onEvent;
|
|
538
|
+
onEventError;
|
|
531
539
|
stdoutBuffer = "";
|
|
532
540
|
stderrBuffer = "";
|
|
533
541
|
nextId = 1;
|
|
@@ -535,6 +543,9 @@ var SpawnMacHelperClient = class {
|
|
|
535
543
|
terminalError;
|
|
536
544
|
constructor(binaryPath, options = {}) {
|
|
537
545
|
this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
546
|
+
this.onEvent = options.onEvent;
|
|
547
|
+
this.onEventError = options.onEventError ?? ((message) => process.stderr.write(`${message}
|
|
548
|
+
`));
|
|
538
549
|
this.child = (0, import_node_child_process.spawn)(binaryPath, ["serve"], { stdio: ["pipe", "pipe", "pipe"] });
|
|
539
550
|
this.child.stdout?.setEncoding("utf-8");
|
|
540
551
|
this.child.stderr?.setEncoding("utf-8");
|
|
@@ -571,6 +582,10 @@ var SpawnMacHelperClient = class {
|
|
|
571
582
|
} catch {
|
|
572
583
|
return;
|
|
573
584
|
}
|
|
585
|
+
if (typeof message.event === "string" && typeof message.id !== "number") {
|
|
586
|
+
this.handleEvent(message);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
574
589
|
const id = typeof message.id === "number" ? message.id : void 0;
|
|
575
590
|
if (id === void 0) {
|
|
576
591
|
return;
|
|
@@ -587,6 +602,18 @@ var SpawnMacHelperClient = class {
|
|
|
587
602
|
pending.reject(new Error(typeof message.error === "string" ? message.error : "prowl-macdriver error"));
|
|
588
603
|
}
|
|
589
604
|
}
|
|
605
|
+
handleEvent(event) {
|
|
606
|
+
try {
|
|
607
|
+
this.onEvent?.(event);
|
|
608
|
+
} catch (error) {
|
|
609
|
+
try {
|
|
610
|
+
this.onEventError(
|
|
611
|
+
`prowl-macdriver event sink failed for event "${event.event}": ${errorMessage(error)}`
|
|
612
|
+
);
|
|
613
|
+
} catch {
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
590
617
|
failAll(error) {
|
|
591
618
|
for (const pending of this.pending.values()) {
|
|
592
619
|
clearTimeout(pending.timer);
|
|
@@ -1587,6 +1614,9 @@ function createAndroidDriver(client, options = {}) {
|
|
|
1587
1614
|
waitForNetworkIdle() {
|
|
1588
1615
|
return rejectUnsupported("waitForNetworkIdle");
|
|
1589
1616
|
},
|
|
1617
|
+
waitForResponse() {
|
|
1618
|
+
return rejectUnsupported("waitForResponse");
|
|
1619
|
+
},
|
|
1590
1620
|
// scripting & artifacts ------------------------------------------------
|
|
1591
1621
|
evaluate() {
|
|
1592
1622
|
return rejectUnsupported("evalScript");
|
|
@@ -2050,6 +2080,7 @@ var WEB_ONLY_STEP_TYPES = /* @__PURE__ */ new Set([
|
|
|
2050
2080
|
"navigate",
|
|
2051
2081
|
"waitForUrl",
|
|
2052
2082
|
"waitForNetworkIdle",
|
|
2083
|
+
"waitForResponse",
|
|
2053
2084
|
"mockRoute",
|
|
2054
2085
|
"unmockRoute",
|
|
2055
2086
|
"evalScript",
|
|
@@ -2668,6 +2699,9 @@ function createIosDriver(client, options) {
|
|
|
2668
2699
|
waitForNetworkIdle() {
|
|
2669
2700
|
return rejectUnsupported("waitForNetworkIdle");
|
|
2670
2701
|
},
|
|
2702
|
+
waitForResponse() {
|
|
2703
|
+
return rejectUnsupported("waitForResponse");
|
|
2704
|
+
},
|
|
2671
2705
|
// scripting & artifacts ------------------------------------------------
|
|
2672
2706
|
evaluate() {
|
|
2673
2707
|
return rejectUnsupported("evalScript");
|
|
@@ -3673,6 +3707,21 @@ var waitForUrlStepSchema = import_zod.z.object({
|
|
|
3673
3707
|
var waitForNetworkIdleStepSchema = import_zod.z.object({
|
|
3674
3708
|
waitForNetworkIdle: import_zod.z.object({ timeout: import_zod.z.number().optional() }).strict()
|
|
3675
3709
|
}).strict();
|
|
3710
|
+
var waitForResponseStatusSchema = import_zod.z.number().int().min(100).max(599);
|
|
3711
|
+
var waitForResponseTimeoutSchema = import_zod.z.number().int().min(0).finite();
|
|
3712
|
+
var waitForResponseStepSchema = import_zod.z.object({
|
|
3713
|
+
waitForResponse: import_zod.z.object({
|
|
3714
|
+
// Matched against each response URL as a glob (`*`/`**` match any run of
|
|
3715
|
+
// characters including `/`, `?` matches one character); matching is
|
|
3716
|
+
// unanchored, so a pattern with no wildcards behaves as a substring match.
|
|
3717
|
+
url: import_zod.z.string().min(1).describe(
|
|
3718
|
+
"URL glob to wait for; unanchored, so a wildcard-free pattern is a substring match"
|
|
3719
|
+
),
|
|
3720
|
+
// Optional status filter: only resolve on a response with this exact HTTP status.
|
|
3721
|
+
status: waitForResponseStatusSchema.optional(),
|
|
3722
|
+
timeout: waitForResponseTimeoutSchema.optional()
|
|
3723
|
+
}).strict()
|
|
3724
|
+
}).strict();
|
|
3676
3725
|
var selectOptionStepSchema = import_zod.z.object({
|
|
3677
3726
|
selectOption: import_zod.z.object({ selector: import_zod.z.string().min(1), value: import_zod.z.string() }).strict()
|
|
3678
3727
|
}).strict();
|
|
@@ -3834,6 +3883,7 @@ var stepSchema = import_zod.z.union([
|
|
|
3834
3883
|
waitForSelectorStepSchema,
|
|
3835
3884
|
waitForUrlStepSchema,
|
|
3836
3885
|
waitForNetworkIdleStepSchema,
|
|
3886
|
+
waitForResponseStepSchema,
|
|
3837
3887
|
hoverStepSchema,
|
|
3838
3888
|
scrollStepSchema,
|
|
3839
3889
|
scrollToStepSchema,
|
|
@@ -4068,6 +4118,16 @@ function interpolateStep(step, vars, stepPath2, redacted) {
|
|
|
4068
4118
|
if ("waitForNetworkIdle" in step) {
|
|
4069
4119
|
return { waitForNetworkIdle: { timeout: step.waitForNetworkIdle.timeout } };
|
|
4070
4120
|
}
|
|
4121
|
+
if ("waitForResponse" in step) {
|
|
4122
|
+
const urlResult = interpolateString(step.waitForResponse.url, vars);
|
|
4123
|
+
return {
|
|
4124
|
+
waitForResponse: {
|
|
4125
|
+
url: urlResult.value,
|
|
4126
|
+
...step.waitForResponse.status !== void 0 ? { status: step.waitForResponse.status } : {},
|
|
4127
|
+
timeout: step.waitForResponse.timeout
|
|
4128
|
+
}
|
|
4129
|
+
};
|
|
4130
|
+
}
|
|
4071
4131
|
if ("hover" in step) {
|
|
4072
4132
|
const selectorResult = interpolateString(step.hover.selector, vars);
|
|
4073
4133
|
return { hover: { selector: selectorResult.value } };
|
|
@@ -4687,6 +4747,9 @@ function createPlaywrightDriver(page) {
|
|
|
4687
4747
|
async waitForNetworkIdle(options) {
|
|
4688
4748
|
await page.waitForLoadState("networkidle", { timeout: options?.timeout });
|
|
4689
4749
|
},
|
|
4750
|
+
async waitForResponse(predicate, options) {
|
|
4751
|
+
await page.waitForResponse((response) => predicate(response), { timeout: options?.timeout });
|
|
4752
|
+
},
|
|
4690
4753
|
evaluate(pageFunction, arg) {
|
|
4691
4754
|
const raw = page.evaluate;
|
|
4692
4755
|
const result = arg === void 0 ? raw.call(page, pageFunction) : raw.call(page, pageFunction, arg);
|
|
@@ -5148,6 +5211,7 @@ function getStepType(step) {
|
|
|
5148
5211
|
if ("waitForSelector" in step) return "waitForSelector";
|
|
5149
5212
|
if ("waitForUrl" in step) return "waitForUrl";
|
|
5150
5213
|
if ("waitForNetworkIdle" in step) return "waitForNetworkIdle";
|
|
5214
|
+
if ("waitForResponse" in step) return "waitForResponse";
|
|
5151
5215
|
if ("hover" in step) return "hover";
|
|
5152
5216
|
if ("scroll" in step) return "scroll";
|
|
5153
5217
|
if ("scrollTo" in step) return "scrollTo";
|
|
@@ -5164,6 +5228,11 @@ function getStepType(step) {
|
|
|
5164
5228
|
if ("waitForDownload" in step) return "waitForDownload";
|
|
5165
5229
|
return "step";
|
|
5166
5230
|
}
|
|
5231
|
+
function compileResponseUrlMatcher(pattern) {
|
|
5232
|
+
const regexBody = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*").replace(/\\\?/g, ".");
|
|
5233
|
+
const regex = new RegExp(regexBody);
|
|
5234
|
+
return (url) => regex.test(url);
|
|
5235
|
+
}
|
|
5167
5236
|
var RUNTIME_VAR_PATTERN = /\{\{([A-Z0-9_]+)\}\}/g;
|
|
5168
5237
|
function substituteRuntimeVars(input, vars) {
|
|
5169
5238
|
return input.replace(RUNTIME_VAR_PATTERN, (match, name) => {
|
|
@@ -5202,6 +5271,15 @@ function applyRuntimeVars(step, vars) {
|
|
|
5202
5271
|
if ("waitForSelector" in step) {
|
|
5203
5272
|
return { waitForSelector: { selector: sub(step.waitForSelector.selector), timeout: step.waitForSelector.timeout } };
|
|
5204
5273
|
}
|
|
5274
|
+
if ("waitForResponse" in step) {
|
|
5275
|
+
return {
|
|
5276
|
+
waitForResponse: {
|
|
5277
|
+
url: sub(step.waitForResponse.url),
|
|
5278
|
+
...step.waitForResponse.status !== void 0 ? { status: step.waitForResponse.status } : {},
|
|
5279
|
+
...step.waitForResponse.timeout !== void 0 ? { timeout: step.waitForResponse.timeout } : {}
|
|
5280
|
+
}
|
|
5281
|
+
};
|
|
5282
|
+
}
|
|
5205
5283
|
if ("evalScript" in step) {
|
|
5206
5284
|
if (typeof step.evalScript === "string") return { evalScript: sub(step.evalScript) };
|
|
5207
5285
|
return {
|
|
@@ -5811,6 +5889,32 @@ var STEP_HANDLERS = {
|
|
|
5811
5889
|
};
|
|
5812
5890
|
}
|
|
5813
5891
|
},
|
|
5892
|
+
waitForResponse: {
|
|
5893
|
+
capabilities: ["response", "wait"],
|
|
5894
|
+
run: async (h) => {
|
|
5895
|
+
if (!("waitForResponse" in h.step)) unknownStep();
|
|
5896
|
+
const { url: pattern, status, timeout } = h.step.waitForResponse;
|
|
5897
|
+
const matchesResponseUrl = compileResponseUrlMatcher(pattern);
|
|
5898
|
+
try {
|
|
5899
|
+
await h.driver.waitForResponse(
|
|
5900
|
+
(response) => matchesResponseUrl(response.url()) && (status === void 0 || response.status() === status),
|
|
5901
|
+
{ timeout }
|
|
5902
|
+
);
|
|
5903
|
+
} catch (err) {
|
|
5904
|
+
const isTimeout = err instanceof Error && (err.name === "TimeoutError" || /timeout/i.test(err.message));
|
|
5905
|
+
if (!isTimeout) throw err;
|
|
5906
|
+
const statusPart = status !== void 0 ? ` with status ${status}` : "";
|
|
5907
|
+
const timeoutPart = timeout !== void 0 ? ` within ${timeout}ms` : "";
|
|
5908
|
+
throw new Error(
|
|
5909
|
+
`waitForResponse: no response matching "${pattern}"${statusPart} was received${timeoutPart}.`
|
|
5910
|
+
);
|
|
5911
|
+
}
|
|
5912
|
+
return {
|
|
5913
|
+
kind: "result",
|
|
5914
|
+
result: { type: "waitForResponse", status: "pass", durationMs: Date.now() - h.stepStart, value: pattern }
|
|
5915
|
+
};
|
|
5916
|
+
}
|
|
5917
|
+
},
|
|
5814
5918
|
waitForNetworkIdle: {
|
|
5815
5919
|
capabilities: ["wait"],
|
|
5816
5920
|
run: async (h) => {
|
|
@@ -6601,6 +6705,15 @@ function formatStep(step) {
|
|
|
6601
6705
|
const error = step.error ? ` error=${escapeMd(step.error)}` : "";
|
|
6602
6706
|
return `${base}${selector}${healed}${value}${error}`;
|
|
6603
6707
|
}
|
|
6708
|
+
function formatRetryAttempt(attempt) {
|
|
6709
|
+
const base = `- attempt ${attempt.attempt}: ${attempt.status.toUpperCase()} (${attempt.durationMs}ms)`;
|
|
6710
|
+
if (attempt.status === "pass") {
|
|
6711
|
+
return base;
|
|
6712
|
+
}
|
|
6713
|
+
const step = attempt.failedStep ? ` failed-step=[${attempt.failedStep.index}] ${escapeMd(attempt.failedStep.type)}` : "";
|
|
6714
|
+
const error = attempt.error ? ` error=${escapeMd(attempt.error)}` : "";
|
|
6715
|
+
return `${base}${step}${error}`;
|
|
6716
|
+
}
|
|
6604
6717
|
function formatAssertion(assertion) {
|
|
6605
6718
|
const value = assertion.value !== void 0 ? ` value=${typeof assertion.value === "string" ? escapeMd(assertion.value) : assertion.value}` : "";
|
|
6606
6719
|
const error = assertion.error ? ` error=${escapeMd(assertion.error)}` : "";
|
|
@@ -6615,6 +6728,16 @@ function writeSummary(runDir, result) {
|
|
|
6615
6728
|
lines.push(`Target: ${result.targetUrl}`);
|
|
6616
6729
|
lines.push(`Started: ${result.startedAt}`);
|
|
6617
6730
|
lines.push(`Duration: ${result.durationMs}ms`);
|
|
6731
|
+
if (result.retryHistory && result.retryHistory.length > 1) {
|
|
6732
|
+
lines.push("");
|
|
6733
|
+
lines.push("## Retries");
|
|
6734
|
+
if (result.retrySummary) {
|
|
6735
|
+
lines.push(escapeMd(result.retrySummary));
|
|
6736
|
+
}
|
|
6737
|
+
for (const attempt of result.retryHistory) {
|
|
6738
|
+
lines.push(formatRetryAttempt(attempt));
|
|
6739
|
+
}
|
|
6740
|
+
}
|
|
6618
6741
|
lines.push("");
|
|
6619
6742
|
lines.push("## Steps");
|
|
6620
6743
|
for (const step of result.steps) {
|
|
@@ -6775,7 +6898,8 @@ function isHistoryEntry(value) {
|
|
|
6775
6898
|
return false;
|
|
6776
6899
|
}
|
|
6777
6900
|
const entry = value;
|
|
6778
|
-
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")
|
|
6901
|
+
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`.
|
|
6902
|
+
(entry.retries === void 0 || typeof entry.retries === "number" && Number.isInteger(entry.retries) && entry.retries >= 0);
|
|
6779
6903
|
}
|
|
6780
6904
|
function readHistory(configDir) {
|
|
6781
6905
|
const filePath = historyPath(configDir);
|
|
@@ -7027,6 +7151,60 @@ async function executeHuntAttempt(options, config, configDir, interpolatedHunt,
|
|
|
7027
7151
|
function delay3(ms) {
|
|
7028
7152
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7029
7153
|
}
|
|
7154
|
+
function toRetryAttempt(attempt, result) {
|
|
7155
|
+
const record = {
|
|
7156
|
+
attempt,
|
|
7157
|
+
status: result.status,
|
|
7158
|
+
durationMs: result.durationMs
|
|
7159
|
+
};
|
|
7160
|
+
if (result.status === "fail") {
|
|
7161
|
+
const failedIndex = result.steps.findIndex((step) => step.status === "fail");
|
|
7162
|
+
if (failedIndex >= 0) {
|
|
7163
|
+
const failed = result.steps[failedIndex];
|
|
7164
|
+
record.failedStep = { index: failedIndex, type: failed.type };
|
|
7165
|
+
if (failed.error) {
|
|
7166
|
+
record.error = failed.error;
|
|
7167
|
+
}
|
|
7168
|
+
}
|
|
7169
|
+
if (!record.error) {
|
|
7170
|
+
const failedAssertion = result.assertions.find((assertion) => assertion.status === "fail");
|
|
7171
|
+
if (failedAssertion?.error) {
|
|
7172
|
+
record.error = failedAssertion.error;
|
|
7173
|
+
}
|
|
7174
|
+
}
|
|
7175
|
+
}
|
|
7176
|
+
return record;
|
|
7177
|
+
}
|
|
7178
|
+
function describeAttemptFailure(attempt) {
|
|
7179
|
+
if (attempt.status !== "fail") {
|
|
7180
|
+
return void 0;
|
|
7181
|
+
}
|
|
7182
|
+
const step = attempt.failedStep?.type;
|
|
7183
|
+
if (step && attempt.error) {
|
|
7184
|
+
return `${step} (${attempt.error})`;
|
|
7185
|
+
}
|
|
7186
|
+
return step ?? attempt.error;
|
|
7187
|
+
}
|
|
7188
|
+
function buildRetrySummary(attempts, maxRetries) {
|
|
7189
|
+
const final = attempts[attempts.length - 1];
|
|
7190
|
+
const firstFailure = describeAttemptFailure(attempts[0]);
|
|
7191
|
+
const suffix = firstFailure ? ` \u2014 first failure: ${firstFailure}` : "";
|
|
7192
|
+
if (final.status === "pass") {
|
|
7193
|
+
return `Passed on attempt ${final.attempt} of ${maxRetries + 1}${suffix}`;
|
|
7194
|
+
}
|
|
7195
|
+
return `Failed after ${attempts.length} attempts${suffix}`;
|
|
7196
|
+
}
|
|
7197
|
+
function finalizeRetryOutcome(outcome, attempts, maxRetries, junit) {
|
|
7198
|
+
outcome.result = writeReports(
|
|
7199
|
+
outcome.runDir,
|
|
7200
|
+
{
|
|
7201
|
+
...outcome.result,
|
|
7202
|
+
retryHistory: attempts,
|
|
7203
|
+
retrySummary: buildRetrySummary(attempts, maxRetries)
|
|
7204
|
+
},
|
|
7205
|
+
{ junit }
|
|
7206
|
+
);
|
|
7207
|
+
}
|
|
7030
7208
|
async function runHunt(options) {
|
|
7031
7209
|
const { config, configDir } = loadConfig(options.configPath);
|
|
7032
7210
|
if (config.target.type === "macos") {
|
|
@@ -7056,7 +7234,9 @@ async function runHunt(options) {
|
|
|
7056
7234
|
}
|
|
7057
7235
|
const maxRetries = hunt.retry?.maxRetries ?? 0;
|
|
7058
7236
|
const retryDelay = hunt.retry?.delay ?? 0;
|
|
7237
|
+
const junit = options.junit ?? config.artifacts.junit;
|
|
7059
7238
|
let lastResult;
|
|
7239
|
+
const attempts = [];
|
|
7060
7240
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
7061
7241
|
if (attempt > 0 && retryDelay > 0) {
|
|
7062
7242
|
await delay3(retryDelay);
|
|
@@ -7072,19 +7252,20 @@ async function runHunt(options) {
|
|
|
7072
7252
|
targetUrl,
|
|
7073
7253
|
allowedDomains
|
|
7074
7254
|
);
|
|
7255
|
+
attempts.push(toRetryAttempt(attempt + 1, lastResult.result));
|
|
7075
7256
|
if (lastResult.result.status === "pass") {
|
|
7076
|
-
if (
|
|
7077
|
-
lastResult
|
|
7257
|
+
if (attempts.length > 1) {
|
|
7258
|
+
finalizeRetryOutcome(lastResult, attempts, maxRetries, junit);
|
|
7078
7259
|
}
|
|
7079
|
-
recordHistory(configDir, lastResult, config.history.maxRuns);
|
|
7260
|
+
recordHistory(configDir, lastResult, config.history.maxRuns, attempts.length - 1);
|
|
7080
7261
|
return lastResult;
|
|
7081
7262
|
}
|
|
7082
7263
|
}
|
|
7083
|
-
if (maxRetries > 0 && lastResult) {
|
|
7084
|
-
lastResult.result.artifacts.summary = `Failed after ${maxRetries + 1} attempts`;
|
|
7085
|
-
}
|
|
7086
7264
|
if (lastResult) {
|
|
7087
|
-
|
|
7265
|
+
if (attempts.length > 1) {
|
|
7266
|
+
finalizeRetryOutcome(lastResult, attempts, maxRetries, junit);
|
|
7267
|
+
}
|
|
7268
|
+
recordHistory(configDir, lastResult, config.history.maxRuns, attempts.length - 1);
|
|
7088
7269
|
}
|
|
7089
7270
|
return lastResult;
|
|
7090
7271
|
}
|
|
@@ -7288,7 +7469,9 @@ async function runNativeHunt(options, config, configDir, target, native) {
|
|
|
7288
7469
|
}
|
|
7289
7470
|
const maxRetries = hunt.retry?.maxRetries ?? 0;
|
|
7290
7471
|
const retryDelay = hunt.retry?.delay ?? 0;
|
|
7472
|
+
const junit = options.junit ?? config.artifacts.junit;
|
|
7291
7473
|
let lastResult;
|
|
7474
|
+
const attempts = [];
|
|
7292
7475
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
7293
7476
|
if (attempt > 0 && retryDelay > 0) {
|
|
7294
7477
|
await delay3(retryDelay);
|
|
@@ -7303,23 +7486,24 @@ async function runNativeHunt(options, config, configDir, target, native) {
|
|
|
7303
7486
|
randomVars,
|
|
7304
7487
|
config.guardrails.allowedApps
|
|
7305
7488
|
);
|
|
7489
|
+
attempts.push(toRetryAttempt(attempt + 1, lastResult.result));
|
|
7306
7490
|
if (lastResult.result.status === "pass") {
|
|
7307
|
-
if (
|
|
7308
|
-
lastResult
|
|
7491
|
+
if (attempts.length > 1) {
|
|
7492
|
+
finalizeRetryOutcome(lastResult, attempts, maxRetries, junit);
|
|
7309
7493
|
}
|
|
7310
|
-
recordHistory(configDir, lastResult, config.history.maxRuns);
|
|
7494
|
+
recordHistory(configDir, lastResult, config.history.maxRuns, attempts.length - 1);
|
|
7311
7495
|
return lastResult;
|
|
7312
7496
|
}
|
|
7313
7497
|
}
|
|
7314
|
-
if (maxRetries > 0 && lastResult) {
|
|
7315
|
-
lastResult.result.artifacts.summary = `Failed after ${maxRetries + 1} attempts`;
|
|
7316
|
-
}
|
|
7317
7498
|
if (lastResult) {
|
|
7318
|
-
|
|
7499
|
+
if (attempts.length > 1) {
|
|
7500
|
+
finalizeRetryOutcome(lastResult, attempts, maxRetries, junit);
|
|
7501
|
+
}
|
|
7502
|
+
recordHistory(configDir, lastResult, config.history.maxRuns, attempts.length - 1);
|
|
7319
7503
|
}
|
|
7320
7504
|
return lastResult;
|
|
7321
7505
|
}
|
|
7322
|
-
function recordHistory(configDir, outcome, maxRuns) {
|
|
7506
|
+
function recordHistory(configDir, outcome, maxRuns, retries = 0) {
|
|
7323
7507
|
try {
|
|
7324
7508
|
const relativeRunDir = import_node_path16.default.relative(configDir, outcome.runDir);
|
|
7325
7509
|
appendEntry(
|
|
@@ -7329,7 +7513,9 @@ function recordHistory(configDir, outcome, maxRuns) {
|
|
|
7329
7513
|
status: outcome.result.status,
|
|
7330
7514
|
durationMs: outcome.result.durationMs,
|
|
7331
7515
|
startedAt: outcome.result.startedAt,
|
|
7332
|
-
runDir: relativeRunDir || void 0
|
|
7516
|
+
runDir: relativeRunDir || void 0,
|
|
7517
|
+
// Omit when no retry ran so entries stay additive/backward-compatible.
|
|
7518
|
+
retries: retries > 0 ? retries : void 0
|
|
7333
7519
|
},
|
|
7334
7520
|
maxRuns
|
|
7335
7521
|
);
|
|
@@ -8397,6 +8583,7 @@ var STEP_REFERENCE = `
|
|
|
8397
8583
|
- waitForSelector: { selector: "#el", timeout: 5000 }
|
|
8398
8584
|
- waitForUrl: { value: "/path", timeout: 5000 }
|
|
8399
8585
|
- waitForNetworkIdle: { timeout: 5000 }
|
|
8586
|
+
- waitForResponse: { url: "**/api/orders", status: 200, timeout: 10000 } \u2014 wait for a matching network response (url is a glob/substring; status is optional)
|
|
8400
8587
|
|
|
8401
8588
|
### Interaction
|
|
8402
8589
|
- click: "Button Text" \u2014 click by text (tries role=button first)
|