@validate.qa/runner 1.0.12 → 1.0.14
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/cli.js +1068 -133
- package/dist/cli.mjs +1068 -133
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -233,6 +233,9 @@ var require_ai_provider = __commonJS({
|
|
|
233
233
|
exports2.getNoThinkingRequestOptions = getNoThinkingRequestOptions;
|
|
234
234
|
exports2.getAIClient = getAIClient;
|
|
235
235
|
exports2.getClientForModel = getClientForModel;
|
|
236
|
+
exports2.shouldProxyAIThroughServerForLocalRunner = shouldProxyAIThroughServerForLocalRunner;
|
|
237
|
+
exports2.getRunnerAIClient = getRunnerAIClient;
|
|
238
|
+
exports2.getRunnerClientForModel = getRunnerClientForModel;
|
|
236
239
|
exports2.getModel = getModel;
|
|
237
240
|
exports2.resolveModelForTask = resolveModelForTask;
|
|
238
241
|
exports2.getAvailableModels = getAvailableModels;
|
|
@@ -510,6 +513,21 @@ var require_ai_provider = __commonJS({
|
|
|
510
513
|
function getClientForModel(modelId) {
|
|
511
514
|
return buildClientForProvider(getProviderForModel(modelId));
|
|
512
515
|
}
|
|
516
|
+
function shouldProxyAIThroughServerForLocalRunner() {
|
|
517
|
+
const serverUrl = (process.env.SERVER_URL ?? "").trim();
|
|
518
|
+
const token = (process.env.AUTH_TOKEN || process.env.RUNNER_TOKEN || "").trim();
|
|
519
|
+
return serverUrl.length > 0 && token.startsWith("urt_");
|
|
520
|
+
}
|
|
521
|
+
function getRunnerAIClient() {
|
|
522
|
+
if (shouldProxyAIThroughServerForLocalRunner())
|
|
523
|
+
return getServerProxiedAIClient();
|
|
524
|
+
return getAIClient();
|
|
525
|
+
}
|
|
526
|
+
function getRunnerClientForModel(modelId) {
|
|
527
|
+
if (shouldProxyAIThroughServerForLocalRunner())
|
|
528
|
+
return getServerProxiedClientForModel(modelId);
|
|
529
|
+
return getClientForModel(modelId);
|
|
530
|
+
}
|
|
513
531
|
function getModel(type) {
|
|
514
532
|
return MODEL_TASK_MAP[getActiveModelId()][type];
|
|
515
533
|
}
|
|
@@ -580,7 +598,7 @@ var require_ai_provider = __commonJS({
|
|
|
580
598
|
const create = async (params, options) => {
|
|
581
599
|
const modelId = typeof params.model === "string" ? params.model : "";
|
|
582
600
|
if (!modelId) {
|
|
583
|
-
throw new Error("server-proxied AI client requires params.model
|
|
601
|
+
throw new Error("server-proxied AI client requires params.model");
|
|
584
602
|
}
|
|
585
603
|
const body = {
|
|
586
604
|
modelId,
|
|
@@ -750,6 +768,21 @@ var require_mobile_source_parser = __commonJS({
|
|
|
750
768
|
const attrText = inner.slice(tag.length);
|
|
751
769
|
return { tag, attrs: parseAttributes(attrText), selfClosing };
|
|
752
770
|
}
|
|
771
|
+
function findTagEnd(xml, from) {
|
|
772
|
+
let quote = null;
|
|
773
|
+
for (let i = from; i < xml.length; i++) {
|
|
774
|
+
const ch = xml[i];
|
|
775
|
+
if (quote !== null) {
|
|
776
|
+
if (ch === quote)
|
|
777
|
+
quote = null;
|
|
778
|
+
} else if (ch === '"' || ch === "'") {
|
|
779
|
+
quote = ch;
|
|
780
|
+
} else if (ch === ">") {
|
|
781
|
+
return i;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return -1;
|
|
785
|
+
}
|
|
753
786
|
function tokenize(xml) {
|
|
754
787
|
if (!xml || typeof xml !== "string")
|
|
755
788
|
return null;
|
|
@@ -772,7 +805,7 @@ var require_mobile_source_parser = __commonJS({
|
|
|
772
805
|
i = end === -1 ? len : end + 3;
|
|
773
806
|
continue;
|
|
774
807
|
}
|
|
775
|
-
const gt = xml
|
|
808
|
+
const gt = findTagEnd(xml, lt + 1);
|
|
776
809
|
if (gt === -1)
|
|
777
810
|
break;
|
|
778
811
|
const tagBody = xml.slice(lt + 1, gt);
|
|
@@ -1324,6 +1357,9 @@ var init_constants = __esm({
|
|
|
1324
1357
|
function toJsStringLiteral(value) {
|
|
1325
1358
|
return JSON.stringify(value);
|
|
1326
1359
|
}
|
|
1360
|
+
function toInlineCommentText(value) {
|
|
1361
|
+
return value.replace(/[\r\n\u2028\u2029]+/g, " ");
|
|
1362
|
+
}
|
|
1327
1363
|
function isPlainObject(value) {
|
|
1328
1364
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1329
1365
|
}
|
|
@@ -1529,7 +1565,7 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
1529
1565
|
`// 3. A booted device/simulator matching the capabilities below`,
|
|
1530
1566
|
`// This is NOT a WDIO testrunner spec: it uses the standalone remote() client`,
|
|
1531
1567
|
`// (driver.$, no auto-injected $/expect/describe globals) so it has no harness`,
|
|
1532
|
-
`// dependency and runs with a plain \`tsx ${testName}.spec.ts\`.`,
|
|
1568
|
+
`// dependency and runs with a plain \`tsx ${toInlineCommentText(testName)}.spec.ts\`.`,
|
|
1533
1569
|
`import { remote } from 'webdriverio';`,
|
|
1534
1570
|
``,
|
|
1535
1571
|
`// Resolve the first selector that exists, mirroring the runner's locator`,
|
|
@@ -1558,6 +1594,10 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
1558
1594
|
` }`,
|
|
1559
1595
|
`}`,
|
|
1560
1596
|
``,
|
|
1597
|
+
`async function __clearFocused(driver: WebdriverIO.Browser) {`,
|
|
1598
|
+
` await (await driver.getActiveElement() as unknown as WebdriverIO.Element).clearValue();`,
|
|
1599
|
+
`}`,
|
|
1600
|
+
``,
|
|
1561
1601
|
`async function __textCandidates(el: WebdriverIO.Element) {`,
|
|
1562
1602
|
` const __values: string[] = [];`,
|
|
1563
1603
|
` const __push = (__value: unknown) => {`,
|
|
@@ -1608,7 +1648,7 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
1608
1648
|
` // Canonical execution source: steps JSON`
|
|
1609
1649
|
];
|
|
1610
1650
|
const stepLines = steps.flatMap((step) => {
|
|
1611
|
-
const lines = [` // Step ${step.order + 1}: ${step.description}`];
|
|
1651
|
+
const lines = [` // Step ${step.order + 1}: ${toInlineCommentText(step.description)}`];
|
|
1612
1652
|
const candidates = selectorCandidatesForStep(step);
|
|
1613
1653
|
const candidatesLiteral = `[${candidates.map(toJsStringLiteral).join(", ")}]`;
|
|
1614
1654
|
const hasSelector = candidates.length > 0;
|
|
@@ -1651,6 +1691,12 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
1651
1691
|
lines.push(` await __typeIntoFocused(driver, ${toJsStringLiteral(step.value ?? "")});`);
|
|
1652
1692
|
} else if (step.action === "clear" && hasSelector) {
|
|
1653
1693
|
lines.push(` await (await findFirst(driver, ${candidatesLiteral})).clearValue();`);
|
|
1694
|
+
} else if (step.action === "clear" && bounds) {
|
|
1695
|
+
const x = Math.round(bounds.x + bounds.width / 2);
|
|
1696
|
+
const y = Math.round(bounds.y + bounds.height / 2);
|
|
1697
|
+
lines.push(` await __tapAt(driver, ${x}, ${y});`);
|
|
1698
|
+
lines.push(` await driver.pause(${MOBILE_FOCUS_SETTLE_MS});`);
|
|
1699
|
+
lines.push(` await __clearFocused(driver);`);
|
|
1654
1700
|
} else if (step.action === "assertVisible" && hasSelector) {
|
|
1655
1701
|
lines.push(` if (!(await (await findFirst(driver, ${candidatesLiteral})).isDisplayed())) {`);
|
|
1656
1702
|
lines.push(` throw new Error('Assertion failed: element not displayed: ' + ${toJsStringLiteral(candidatesLiteral)});`);
|
|
@@ -1658,6 +1704,7 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
1658
1704
|
} else if (step.action === "assertText" && hasSelector) {
|
|
1659
1705
|
lines.push(` {`);
|
|
1660
1706
|
lines.push(` const __expected = ${toJsStringLiteral(step.assertion ?? "")};`);
|
|
1707
|
+
lines.push(` if (__expected.length === 0) throw new Error('assertText step is missing expected text');`);
|
|
1661
1708
|
lines.push(` const __actual = await __textCandidates(await findFirst(driver, ${candidatesLiteral}));`);
|
|
1662
1709
|
lines.push(` if (!__actual.some((__value) => __value.includes(__expected))) {`);
|
|
1663
1710
|
lines.push(` throw new Error('Assertion failed: expected text to contain ' + JSON.stringify(__expected) + ' but got ' + JSON.stringify(__actual));`);
|
|
@@ -4366,6 +4413,7 @@ var require_mobile_observation = __commonJS({
|
|
|
4366
4413
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
4367
4414
|
exports2.renderMobileObservation = renderMobileObservation;
|
|
4368
4415
|
exports2.summarizeScreenForEvidence = summarizeScreenForEvidence;
|
|
4416
|
+
exports2.buildCollectedElementsForScreen = buildCollectedElementsForScreen;
|
|
4369
4417
|
var DEFAULT_MAX_ELEMENTS = 60;
|
|
4370
4418
|
var MAX_LABEL_LENGTH = 80;
|
|
4371
4419
|
var EVIDENCE_LABEL_LIMIT = 40;
|
|
@@ -4407,6 +4455,46 @@ var require_mobile_observation = __commonJS({
|
|
|
4407
4455
|
}
|
|
4408
4456
|
return { elementCount, actionableCount, labels };
|
|
4409
4457
|
}
|
|
4458
|
+
function buildCollectedElementsForScreen(elements, opts = {}) {
|
|
4459
|
+
const navigationRefs = opts.navigationRefs ?? EMPTY_REF_SET;
|
|
4460
|
+
const out = [];
|
|
4461
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4462
|
+
for (const element of elements) {
|
|
4463
|
+
if (!element.actionable)
|
|
4464
|
+
continue;
|
|
4465
|
+
const collected = toCollectedPageElement(element, navigationRefs.has(element.ref));
|
|
4466
|
+
const identity = `${collected.elementType}|${collected.testId ?? ""}|${collected.label ?? ""}`;
|
|
4467
|
+
if (seen.has(identity))
|
|
4468
|
+
continue;
|
|
4469
|
+
seen.add(identity);
|
|
4470
|
+
out.push(collected);
|
|
4471
|
+
}
|
|
4472
|
+
return out;
|
|
4473
|
+
}
|
|
4474
|
+
var EMPTY_REF_SET = /* @__PURE__ */ new Set();
|
|
4475
|
+
function toCollectedPageElement(element, triggersNavigation) {
|
|
4476
|
+
const label = element.label?.trim() || element.text?.trim() || void 0;
|
|
4477
|
+
const stableId = element.accessibilityId?.trim() || element.resourceId?.trim() || void 0;
|
|
4478
|
+
const locators = [];
|
|
4479
|
+
if (stableId)
|
|
4480
|
+
locators.push({ strategy: "getByTestId", value: stableId, confidence: 0.95 });
|
|
4481
|
+
const xpath = element.xpath?.trim();
|
|
4482
|
+
if (xpath)
|
|
4483
|
+
locators.push({ strategy: "css", value: xpath, confidence: 0.4 });
|
|
4484
|
+
const collected = {
|
|
4485
|
+
elementType: element.role || "View",
|
|
4486
|
+
triggersNavigation
|
|
4487
|
+
};
|
|
4488
|
+
if (label)
|
|
4489
|
+
collected.label = label;
|
|
4490
|
+
if (stableId)
|
|
4491
|
+
collected.testId = stableId;
|
|
4492
|
+
if (locators.length > 0)
|
|
4493
|
+
collected.locators = locators;
|
|
4494
|
+
if (element.enabled === false)
|
|
4495
|
+
collected.isDisabled = true;
|
|
4496
|
+
return collected;
|
|
4497
|
+
}
|
|
4410
4498
|
function buildHeaderLine(screen) {
|
|
4411
4499
|
const parts = ["screen:"];
|
|
4412
4500
|
const name = screenDisplayName(screen.screenSignal);
|
|
@@ -4855,7 +4943,7 @@ var require_mobile_explorer = __commonJS({
|
|
|
4855
4943
|
}
|
|
4856
4944
|
return sections.join("\n");
|
|
4857
4945
|
}
|
|
4858
|
-
function buildSystemPrompt(mode, platform3, enableRecordJourney, maxIterations, appBrief, targetAppId) {
|
|
4946
|
+
function buildSystemPrompt(mode, platform3, enableRecordJourney, maxIterations, appBrief, targetAppId, existingScreens) {
|
|
4859
4947
|
const platformLabel = platform3 === "IOS" ? "iOS" : "Android";
|
|
4860
4948
|
const targetLine = targetAppId ? ` Target app package/bundle: ${targetAppId}.` : "";
|
|
4861
4949
|
const surveyBody = `You are a mobile QA exploration agent driving a real ${platformLabel} app on a connected device. Your goal: MAP every reachable screen so downstream agents know what to test. You are NOT testing \u2014 you are building a complete screen map.
|
|
@@ -4901,13 +4989,18 @@ Max iterations: ${maxIterations}. You will be warned near the end.`;
|
|
|
4901
4989
|
## Application Brief
|
|
4902
4990
|
${appBrief}
|
|
4903
4991
|
Use this to prioritize which screens and flows to cover.` : "";
|
|
4904
|
-
|
|
4992
|
+
const existingScreensBlock = mode === "survey" && existingScreens && existingScreens.length > 0 ? `
|
|
4993
|
+
|
|
4994
|
+
## ALREADY DISCOVERED SCREENS (skip these; find new)
|
|
4995
|
+
These screens were mapped in a previous survey. Do NOT re-map them \u2014 use your budget to find NEW screens and flows not listed here. If you land on one of these, move on to something new.
|
|
4996
|
+
${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.name}"` : ""}${s.screenType ? ` [${s.screenType}]` : ""}`).join("\n")}` : "";
|
|
4997
|
+
return `${mode === "survey" ? surveyBody : deepBody}${briefBlock}${existingScreensBlock}`;
|
|
4905
4998
|
}
|
|
4906
4999
|
function buildKickoffMessage(mode) {
|
|
4907
5000
|
return mode === "survey" ? "Start by calling mobile_snapshot on the launch screen, then capture_screen. Map the entire app: open every tab, menu, and one row per list. Record transitions as you go." : "Start by calling mobile_snapshot on the launch screen. Then use the features: fill fields, submit forms, open menus, and observe outcomes. Record journeys after meaningful flows.";
|
|
4908
5001
|
}
|
|
4909
5002
|
async function runMobileExplorePhase(args) {
|
|
4910
|
-
const { ctx, driver, state: state2, config, onLog, onAICall, onScreenshot } = args;
|
|
5003
|
+
const { ctx, driver, state: state2, config, onLog, onAICall, onScreenshot, onScreenshotUpload, existingScreens } = args;
|
|
4911
5004
|
const logs2 = [];
|
|
4912
5005
|
const log2 = (line) => {
|
|
4913
5006
|
logs2.push(line);
|
|
@@ -4931,6 +5024,9 @@ Use this to prioritize which screens and flows to cover.` : "";
|
|
|
4931
5024
|
log2(`Budget: ${maxIterations} iterations | app: ${ctx.target?.appId ?? "(unknown)"}`);
|
|
4932
5025
|
const screenshots = [];
|
|
4933
5026
|
const pageScreenshots = {};
|
|
5027
|
+
const pageScreenshotKeys = {};
|
|
5028
|
+
const uploadedScreenIds = /* @__PURE__ */ new Set();
|
|
5029
|
+
const pendingScreenshotUploads = [];
|
|
4934
5030
|
const collectedTransitions = [];
|
|
4935
5031
|
const journeys = [];
|
|
4936
5032
|
const counters = {
|
|
@@ -4949,6 +5045,14 @@ Use this to prioritize which screens and flows to cover.` : "";
|
|
|
4949
5045
|
screenshots.push(base64);
|
|
4950
5046
|
if (screenId)
|
|
4951
5047
|
pageScreenshots[screenId] = base64;
|
|
5048
|
+
if (screenId && onScreenshotUpload && !uploadedScreenIds.has(screenId)) {
|
|
5049
|
+
uploadedScreenIds.add(screenId);
|
|
5050
|
+
pendingScreenshotUploads.push(onScreenshotUpload(screenId, base64).then((key) => {
|
|
5051
|
+
if (key)
|
|
5052
|
+
pageScreenshotKeys[screenId] = key;
|
|
5053
|
+
}).catch(() => {
|
|
5054
|
+
}));
|
|
5055
|
+
}
|
|
4952
5056
|
try {
|
|
4953
5057
|
onScreenshot?.(base64);
|
|
4954
5058
|
} catch {
|
|
@@ -5053,7 +5157,7 @@ Use this to prioritize which screens and flows to cover.` : "";
|
|
|
5053
5157
|
return null;
|
|
5054
5158
|
return { element, bounds: element.bounds };
|
|
5055
5159
|
};
|
|
5056
|
-
const systemPrompt = buildSystemPrompt(mode, platform3, enableRecordJourney, maxIterations, ctx.appBrief, targetAppId);
|
|
5160
|
+
const systemPrompt = buildSystemPrompt(mode, platform3, enableRecordJourney, maxIterations, ctx.appBrief, targetAppId, existingScreens);
|
|
5057
5161
|
const kickoff = buildKickoffMessage(mode);
|
|
5058
5162
|
const recentExchanges = [];
|
|
5059
5163
|
const transientDirectives = [];
|
|
@@ -5261,6 +5365,9 @@ Exploration complete: ${summary}`);
|
|
|
5261
5365
|
summary = `Explore phase aborted (${truncatedReason}) after mapping ${state2.getScreenCount()} screen(s).`;
|
|
5262
5366
|
}
|
|
5263
5367
|
}
|
|
5368
|
+
if (pendingScreenshotUploads.length > 0) {
|
|
5369
|
+
await Promise.allSettled(pendingScreenshotUploads);
|
|
5370
|
+
}
|
|
5264
5371
|
const collectedPages = state2.buildInMemoryScreenMap();
|
|
5265
5372
|
counters.pagesVisited = collectedPages.length;
|
|
5266
5373
|
if (counters.pagesDiscovered < collectedPages.length) {
|
|
@@ -5295,6 +5402,9 @@ Exploration complete: ${summary}`);
|
|
|
5295
5402
|
if (Object.keys(pageScreenshots).length > 0) {
|
|
5296
5403
|
result.pageScreenshots = pageScreenshots;
|
|
5297
5404
|
}
|
|
5405
|
+
if (Object.keys(pageScreenshotKeys).length > 0) {
|
|
5406
|
+
result.pageScreenshotKeys = pageScreenshotKeys;
|
|
5407
|
+
}
|
|
5298
5408
|
if (truncated) {
|
|
5299
5409
|
result.truncated = true;
|
|
5300
5410
|
result.truncatedReason = truncatedReason;
|
|
@@ -5796,6 +5906,44 @@ var require_snapshot_parser = __commonJS({
|
|
|
5796
5906
|
};
|
|
5797
5907
|
var SUBMIT_LABEL_RE = /^(submit|sign\s*in|sign\s*up|log\s*in|register|create|save|send|confirm|continue|next|apply|post|publish|add|update|go)$/i;
|
|
5798
5908
|
var GENERIC_LABEL_RE = /^(input field|textbox|button|link|item|field|value|tab)$/i;
|
|
5909
|
+
var RICH_CONTEXT_ROLES = /* @__PURE__ */ new Set(["table", "grid", "row", "radiogroup"]);
|
|
5910
|
+
var RICH_CONTEXT_LABEL_MAX = 60;
|
|
5911
|
+
var VALUE_BEARING_ROLES = /* @__PURE__ */ new Set(["textbox", "searchbox", "combobox", "spinbutton", "listbox", "slider"]);
|
|
5912
|
+
var SENSITIVE_VALUE_LABEL_RE = /\bpass(?:word|code|phrase)?\b|\bpassport\b|\bpwd\b|\bsecrets?\b|\btokens?\b|\botp\b|one[- ]?time|\bpin\b|\bcvv\b|\bcvc\b|card\s*number|\bssn\b|social\s*security|security\s*(?:answer|question)|api[-_ ]?key|private\s*key|\bauth\b|authoriz|authenticat|credential/i;
|
|
5913
|
+
var REDACTED_VALUE = "[redacted]";
|
|
5914
|
+
var VALUE_TEXT_MAX = 120;
|
|
5915
|
+
var SIGNAL_TEXT_RE = /\b(?:showing|displaying|page)\b[\s\S]*\b(?:of|results?|items?|entries)\b|\b(?:no|nothing)\b[\s\S]*\b(?:found|yet|available|results?|items?)\b|\bempty\b|\b(?:error|invalid|required|failed|expired?|success(?:fully)?|saved|updated|deleted|created|welcome)\b/i;
|
|
5916
|
+
var STATIC_MESSAGES_MAX = 12;
|
|
5917
|
+
var OPTIONS_PER_ELEMENT_MAX = 24;
|
|
5918
|
+
var URL_CHILD_LINE_RE = /^(\s*)- \/url:\s*(.+)$/;
|
|
5919
|
+
var MESSAGE_TOKEN_RE = /\b(?:sk|pk|rk|ak|key|tok)[-_][A-Za-z0-9_-]{8,}\b|\b[A-Za-z0-9_-]{24,}\b/g;
|
|
5920
|
+
function stripYamlQuotes(raw) {
|
|
5921
|
+
const trimmed = raw.trim();
|
|
5922
|
+
if (trimmed.length >= 2 && (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
5923
|
+
return trimmed.slice(1, -1);
|
|
5924
|
+
}
|
|
5925
|
+
return trimmed;
|
|
5926
|
+
}
|
|
5927
|
+
function extractTrailingText(attrs) {
|
|
5928
|
+
const lastBracket = attrs.lastIndexOf("]");
|
|
5929
|
+
const tail = lastBracket >= 0 ? attrs.slice(lastBracket + 1) : attrs;
|
|
5930
|
+
const match = /^\s*:\s+(.+)$/.exec(tail);
|
|
5931
|
+
if (!match)
|
|
5932
|
+
return null;
|
|
5933
|
+
const text = stripYamlQuotes(match[1]);
|
|
5934
|
+
if (!text || /^(?:\||\|-|>|>-)$/.test(text))
|
|
5935
|
+
return null;
|
|
5936
|
+
return text;
|
|
5937
|
+
}
|
|
5938
|
+
function isSensitiveValueField(name, placeholder) {
|
|
5939
|
+
return SENSITIVE_VALUE_LABEL_RE.test(`${name ?? ""} ${placeholder ?? ""}`);
|
|
5940
|
+
}
|
|
5941
|
+
function scrubMessageText(text) {
|
|
5942
|
+
return text.replace(MESSAGE_TOKEN_RE, REDACTED_VALUE);
|
|
5943
|
+
}
|
|
5944
|
+
function capText(text, max) {
|
|
5945
|
+
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
5946
|
+
}
|
|
5799
5947
|
function isProbablyDynamicTestId(testId) {
|
|
5800
5948
|
if (testId.length < 6)
|
|
5801
5949
|
return false;
|
|
@@ -5871,9 +6019,12 @@ var require_snapshot_parser = __commonJS({
|
|
|
5871
6019
|
}
|
|
5872
6020
|
return e.name;
|
|
5873
6021
|
}
|
|
5874
|
-
function parseSnapshot(snapshotText) {
|
|
6022
|
+
function parseSnapshot(snapshotText, options = {}) {
|
|
6023
|
+
const richParse = options.richParse ?? process.env.DISCOVERY_SNAPSHOT_RICH_PARSE === "true";
|
|
5875
6024
|
const lines = snapshotText.split("\n");
|
|
5876
6025
|
const elements = [];
|
|
6026
|
+
const liveMessages = [];
|
|
6027
|
+
const textMessages = [];
|
|
5877
6028
|
const seenRefs = /* @__PURE__ */ new Set();
|
|
5878
6029
|
let parsedElementLineCount = 0;
|
|
5879
6030
|
let genericLabelCount = 0;
|
|
@@ -5882,6 +6033,21 @@ var require_snapshot_parser = __commonJS({
|
|
|
5882
6033
|
const contextStack = [];
|
|
5883
6034
|
const completedForms = [];
|
|
5884
6035
|
for (const line of lines) {
|
|
6036
|
+
if (richParse) {
|
|
6037
|
+
const urlMatch = URL_CHILD_LINE_RE.exec(line);
|
|
6038
|
+
if (urlMatch) {
|
|
6039
|
+
const urlIndent = urlMatch[1].length;
|
|
6040
|
+
for (let i = elements.length - 1; i >= 0; i--) {
|
|
6041
|
+
const el = elements[i];
|
|
6042
|
+
if (el.indentDepth < urlIndent) {
|
|
6043
|
+
if (el.role === "link" && !el.href)
|
|
6044
|
+
el.href = capText(stripYamlQuotes(urlMatch[2]), 300);
|
|
6045
|
+
break;
|
|
6046
|
+
}
|
|
6047
|
+
}
|
|
6048
|
+
continue;
|
|
6049
|
+
}
|
|
6050
|
+
}
|
|
5885
6051
|
const match = ELEMENT_LINE_RE.exec(line);
|
|
5886
6052
|
if (!match)
|
|
5887
6053
|
continue;
|
|
@@ -5903,7 +6069,7 @@ var require_snapshot_parser = __commonJS({
|
|
|
5903
6069
|
}
|
|
5904
6070
|
const headingLevelMatch = LEVEL_RE.exec(attrs);
|
|
5905
6071
|
const headingLevel = headingLevelMatch ? Number.parseInt(headingLevelMatch[1], 10) : null;
|
|
5906
|
-
const contextLabel = buildContextLabel(role, name ?? null, headingLevel);
|
|
6072
|
+
const contextLabel = buildContextLabel(role, name ?? null, headingLevel, richParse);
|
|
5907
6073
|
if (contextLabel) {
|
|
5908
6074
|
contextStack.push({ indent: indentDepth, label: contextLabel });
|
|
5909
6075
|
}
|
|
@@ -5916,6 +6082,40 @@ var require_snapshot_parser = __commonJS({
|
|
|
5916
6082
|
});
|
|
5917
6083
|
continue;
|
|
5918
6084
|
}
|
|
6085
|
+
if (richParse) {
|
|
6086
|
+
if (role === "option" && name) {
|
|
6087
|
+
for (let i = elements.length - 1; i >= 0; i--) {
|
|
6088
|
+
const el = elements[i];
|
|
6089
|
+
if (el.indentDepth < indentDepth) {
|
|
6090
|
+
if (el.role === "combobox" || el.role === "listbox") {
|
|
6091
|
+
el.options ??= [];
|
|
6092
|
+
if (el.options.length < OPTIONS_PER_ELEMENT_MAX) {
|
|
6093
|
+
el.options.push({ label: capText(name, 80), selected: parseBooleanState(SELECTED_RE.exec(attrs)) === true });
|
|
6094
|
+
}
|
|
6095
|
+
}
|
|
6096
|
+
break;
|
|
6097
|
+
}
|
|
6098
|
+
}
|
|
6099
|
+
}
|
|
6100
|
+
if (role === "alert" || role === "status" || role === "paragraph" || role === "text") {
|
|
6101
|
+
const text = extractTrailingText(attrs) ?? (name || null);
|
|
6102
|
+
const isLiveRegion = role === "alert" || role === "status";
|
|
6103
|
+
if (text && (isLiveRegion || SIGNAL_TEXT_RE.test(text))) {
|
|
6104
|
+
const capped = capText(scrubMessageText(text), VALUE_TEXT_MAX);
|
|
6105
|
+
const ref2 = REF_RE.exec(attrs)?.[1] ?? null;
|
|
6106
|
+
if (isLiveRegion) {
|
|
6107
|
+
const textDupIndex = textMessages.findIndex((m) => m.text === capped);
|
|
6108
|
+
if (textDupIndex >= 0)
|
|
6109
|
+
textMessages.splice(textDupIndex, 1);
|
|
6110
|
+
if (liveMessages.length < STATIC_MESSAGES_MAX && !liveMessages.some((m) => m.text === capped)) {
|
|
6111
|
+
liveMessages.push({ role, text: capped, ref: ref2 });
|
|
6112
|
+
}
|
|
6113
|
+
} else if (textMessages.length < STATIC_MESSAGES_MAX && !textMessages.some((m) => m.text === capped) && !liveMessages.some((m) => m.text === capped)) {
|
|
6114
|
+
textMessages.push({ role: "text", text: capped, ref: ref2 });
|
|
6115
|
+
}
|
|
6116
|
+
}
|
|
6117
|
+
}
|
|
6118
|
+
}
|
|
5919
6119
|
if (!INTERACTIVE_ROLES.has(role))
|
|
5920
6120
|
continue;
|
|
5921
6121
|
const refMatch = REF_RE.exec(attrs);
|
|
@@ -5948,6 +6148,13 @@ var require_snapshot_parser = __commonJS({
|
|
|
5948
6148
|
}
|
|
5949
6149
|
const isLink = role === "link";
|
|
5950
6150
|
const isSubmitButton = role === "button" && name != null && SUBMIT_LABEL_RE.test(name);
|
|
6151
|
+
let value = null;
|
|
6152
|
+
if (richParse && VALUE_BEARING_ROLES.has(role)) {
|
|
6153
|
+
const trailing = extractTrailingText(attrs);
|
|
6154
|
+
if (trailing) {
|
|
6155
|
+
value = isSensitiveValueField(name ?? null, placeholder) ? REDACTED_VALUE : capText(trailing, VALUE_TEXT_MAX);
|
|
6156
|
+
}
|
|
6157
|
+
}
|
|
5951
6158
|
const element = {
|
|
5952
6159
|
ref,
|
|
5953
6160
|
role,
|
|
@@ -5969,7 +6176,10 @@ var require_snapshot_parser = __commonJS({
|
|
|
5969
6176
|
pressed,
|
|
5970
6177
|
headingLevel,
|
|
5971
6178
|
currentState,
|
|
5972
|
-
containerPath
|
|
6179
|
+
containerPath,
|
|
6180
|
+
href: null,
|
|
6181
|
+
value,
|
|
6182
|
+
options: null
|
|
5973
6183
|
};
|
|
5974
6184
|
if (isGenericLabel(element.name))
|
|
5975
6185
|
genericLabelCount++;
|
|
@@ -5997,6 +6207,9 @@ var require_snapshot_parser = __commonJS({
|
|
|
5997
6207
|
return {
|
|
5998
6208
|
elements,
|
|
5999
6209
|
formGroups: completedForms,
|
|
6210
|
+
// live regions first: they carry their own reservation so an end-of-body toast can
|
|
6211
|
+
// never be starved out by signal-matching plain text earlier in the document.
|
|
6212
|
+
staticMessages: [...liveMessages, ...textMessages].slice(0, STATIC_MESSAGES_MAX),
|
|
6000
6213
|
rawLineCount: lines.length,
|
|
6001
6214
|
parsedElementLineCount,
|
|
6002
6215
|
genericLabelCount,
|
|
@@ -6107,12 +6320,17 @@ var require_snapshot_parser = __commonJS({
|
|
|
6107
6320
|
return "mixed";
|
|
6108
6321
|
return false;
|
|
6109
6322
|
}
|
|
6110
|
-
function buildContextLabel(role, name, headingLevel) {
|
|
6323
|
+
function buildContextLabel(role, name, headingLevel, richContext = false) {
|
|
6111
6324
|
if (role === "heading") {
|
|
6112
6325
|
if (!name)
|
|
6113
6326
|
return null;
|
|
6114
6327
|
return headingLevel ? `heading${headingLevel}:${name}` : `heading:${name}`;
|
|
6115
6328
|
}
|
|
6329
|
+
if (richContext && RICH_CONTEXT_ROLES.has(role)) {
|
|
6330
|
+
if (!name)
|
|
6331
|
+
return null;
|
|
6332
|
+
return `${role}:${capText(name.replace(/>/g, "\u203A"), RICH_CONTEXT_LABEL_MAX)}`;
|
|
6333
|
+
}
|
|
6116
6334
|
if (!CONTEXT_ROLES.has(role))
|
|
6117
6335
|
return null;
|
|
6118
6336
|
if (name)
|
|
@@ -7822,7 +8040,7 @@ Each entry is exactly one of:
|
|
|
7822
8040
|
const systemPrompt = options.systemPromptOverride ?? (context.mode === "survey" ? SURVEY_SYSTEM_PROMPT : exports2.SCENARIO_SYSTEM_PROMPT);
|
|
7823
8041
|
const userPrompt = options.userPromptOverride ?? formatEvidenceForPlanner(compressed, evidence, context);
|
|
7824
8042
|
logs2.push(`AI planner: ~${Math.ceil(userPrompt.length / 3.5)} input tokens`);
|
|
7825
|
-
const aiClient = options.aiClient ?? (options.modelOverride ? (0, ai_provider_js_1.
|
|
8043
|
+
const aiClient = options.aiClient ?? (options.modelOverride ? (0, ai_provider_js_1.getRunnerClientForModel)(options.modelOverride) : (0, ai_provider_js_1.getRunnerAIClient)());
|
|
7826
8044
|
const messages = [
|
|
7827
8045
|
{ role: "system", content: systemPrompt },
|
|
7828
8046
|
{ role: "user", content: userPrompt }
|
|
@@ -7830,18 +8048,22 @@ Each entry is exactly one of:
|
|
|
7830
8048
|
const callStart = Date.now();
|
|
7831
8049
|
let response;
|
|
7832
8050
|
try {
|
|
7833
|
-
response = await (0, ai_retry_js_1.
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
8051
|
+
response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
8052
|
+
arm();
|
|
8053
|
+
return aiClient.chat.completions.create({
|
|
8054
|
+
model: agentModel,
|
|
8055
|
+
messages,
|
|
8056
|
+
max_tokens: plannerMaxTokens,
|
|
8057
|
+
temperature: 0.3,
|
|
8058
|
+
...(0, ai_provider_js_1.getJsonObjectResponseFormat)(agentModel),
|
|
8059
|
+
...(0, ai_provider_js_1.getNoThinkingRequestOptions)(agentModel)
|
|
8060
|
+
}, { signal });
|
|
8061
|
+
}), {
|
|
7841
8062
|
label: "ai-planner",
|
|
7842
8063
|
baseDelayMs: 5e3,
|
|
7843
8064
|
maxRetries: 2,
|
|
7844
|
-
|
|
8065
|
+
deferTimeoutUntilArmed: true,
|
|
8066
|
+
onRetry: (attempt, reason, delay) => logs2.push(`AI planner call failed (${reason}) \u2014 retry ${attempt} in ${Math.round(delay / 1e3)}s`)
|
|
7845
8067
|
});
|
|
7846
8068
|
} catch (err) {
|
|
7847
8069
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -7907,18 +8129,25 @@ Each entry is exactly one of:
|
|
|
7907
8129
|
const retryStart = Date.now();
|
|
7908
8130
|
let retryResponse = null;
|
|
7909
8131
|
try {
|
|
7910
|
-
retryResponse = await (0, ai_retry_js_1.
|
|
7911
|
-
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
8132
|
+
retryResponse = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
8133
|
+
arm();
|
|
8134
|
+
return aiClient.chat.completions.create({
|
|
8135
|
+
model: agentModel,
|
|
8136
|
+
messages: [
|
|
8137
|
+
...messages,
|
|
8138
|
+
{ role: "assistant", content: raw },
|
|
8139
|
+
{ role: "user", content: retryContext }
|
|
8140
|
+
],
|
|
8141
|
+
max_tokens: plannerMaxTokens,
|
|
8142
|
+
temperature: 0.3,
|
|
8143
|
+
...(0, ai_provider_js_1.getJsonObjectResponseFormat)(agentModel),
|
|
8144
|
+
...(0, ai_provider_js_1.getNoThinkingRequestOptions)(agentModel)
|
|
8145
|
+
}, { signal });
|
|
8146
|
+
}), {
|
|
8147
|
+
label: "ai-planner-retry",
|
|
8148
|
+
deferTimeoutUntilArmed: true,
|
|
8149
|
+
onRetry: (attempt, reason, delay) => logs2.push(`AI planner retry call failed (${reason}) \u2014 retry ${attempt} in ${Math.round(delay / 1e3)}s`)
|
|
8150
|
+
});
|
|
7922
8151
|
} catch (err) {
|
|
7923
8152
|
const message = err instanceof Error ? err.message : String(err);
|
|
7924
8153
|
logs2.push(`AI planner retry failed (${message}) \u2014 using first-pass result`);
|
|
@@ -8446,12 +8675,12 @@ ${constraints.join("\n")}`);
|
|
|
8446
8675
|
const orderedScreens = targetAppScreens(graph, appId).filter((state2) => state2.screenType?.toLowerCase() !== "error").sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
8447
8676
|
const includedScreenIds = new Set(orderedScreens.map((state2) => state2.screenId));
|
|
8448
8677
|
for (const state2 of orderedScreens) {
|
|
8449
|
-
const groupName = state2.screenType === "auth" ? "Authentication" : screenDisplayName(state2);
|
|
8678
|
+
const groupName = state2.screenType?.toLowerCase() === "auth" ? "Authentication" : screenDisplayName(state2);
|
|
8450
8679
|
const group = groups.get(groupName) ?? {
|
|
8451
8680
|
name: groupName,
|
|
8452
8681
|
description: `Native mobile screens related to ${groupName}.`,
|
|
8453
8682
|
routes: [],
|
|
8454
|
-
criticality: state2.screenType === "auth" ? "critical" : "medium",
|
|
8683
|
+
criticality: state2.screenType?.toLowerCase() === "auth" ? "critical" : "medium",
|
|
8455
8684
|
pages: [],
|
|
8456
8685
|
flows: []
|
|
8457
8686
|
};
|
|
@@ -8474,7 +8703,7 @@ ${constraints.join("\n")}`);
|
|
|
8474
8703
|
const to = graph.screens.get(transition.toScreenId);
|
|
8475
8704
|
if (!from || !to)
|
|
8476
8705
|
continue;
|
|
8477
|
-
const groupName = from.screenType === "auth" ? "Authentication" : screenDisplayName(from);
|
|
8706
|
+
const groupName = from.screenType?.toLowerCase() === "auth" ? "Authentication" : screenDisplayName(from);
|
|
8478
8707
|
const group = groups.get(groupName);
|
|
8479
8708
|
if (!group)
|
|
8480
8709
|
continue;
|
|
@@ -8519,9 +8748,9 @@ ${constraints.join("\n")}`);
|
|
|
8519
8748
|
scenarios.push({
|
|
8520
8749
|
name,
|
|
8521
8750
|
type: "HAPPY_PATH",
|
|
8522
|
-
priority: state2.screenType === "auth" ? 1 : 3,
|
|
8751
|
+
priority: state2.screenType?.toLowerCase() === "auth" ? 1 : 3,
|
|
8523
8752
|
variant: "happy",
|
|
8524
|
-
featureGroup: state2.screenType === "auth" ? "Authentication" : screenName,
|
|
8753
|
+
featureGroup: state2.screenType?.toLowerCase() === "auth" ? "Authentication" : screenName,
|
|
8525
8754
|
goal: `User can reach and inspect the ${screenName} screen`,
|
|
8526
8755
|
steps,
|
|
8527
8756
|
expectedOutcomes: [{
|
|
@@ -8667,6 +8896,7 @@ var require_mobile_generator = __commonJS({
|
|
|
8667
8896
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
8668
8897
|
var mobile_boundary_js_1 = require_mobile_boundary();
|
|
8669
8898
|
var MAX_SCENARIO_ITERATIONS = 30;
|
|
8899
|
+
var MAX_CONSECUTIVE_INFRA_FAILURES = 3;
|
|
8670
8900
|
var MAX_RECENT_EXCHANGES = 24;
|
|
8671
8901
|
var OBSERVATION_MAX_ELEMENTS = 50;
|
|
8672
8902
|
function finalizeAction(input) {
|
|
@@ -9391,14 +9621,31 @@ ${statePacket}` },
|
|
|
9391
9621
|
if (!snapshot || snapshot.screen.elements.length === 0)
|
|
9392
9622
|
return null;
|
|
9393
9623
|
const elements = snapshot.screen.elements;
|
|
9394
|
-
const
|
|
9395
|
-
const
|
|
9624
|
+
const stableAnchors = elements.filter((el) => hasStableId(el) && !isAlwaysPresentContainer(el));
|
|
9625
|
+
const contentfulNonButton = stableAnchors.find((el) => !/button/i.test(el.role) && (nonEmpty2(el.label) || nonEmpty2(el.text)));
|
|
9626
|
+
if (contentfulNonButton)
|
|
9627
|
+
return contentfulNonButton;
|
|
9628
|
+
const nonButton = stableAnchors.find((el) => !/button/i.test(el.role));
|
|
9396
9629
|
if (nonButton)
|
|
9397
9630
|
return nonButton;
|
|
9398
|
-
if (
|
|
9399
|
-
return
|
|
9400
|
-
const labelled = elements.find((el) => nonEmpty2(el.label) || nonEmpty2(el.text));
|
|
9401
|
-
return labelled ?? elements[0];
|
|
9631
|
+
if (stableAnchors.length > 0)
|
|
9632
|
+
return stableAnchors[0];
|
|
9633
|
+
const labelled = elements.find((el) => !isAlwaysPresentContainer(el) && (nonEmpty2(el.label) || nonEmpty2(el.text)));
|
|
9634
|
+
return labelled ?? elements.find((el) => !isAlwaysPresentContainer(el)) ?? elements[0];
|
|
9635
|
+
}
|
|
9636
|
+
var ALWAYS_PRESENT_CONTAINER_ROLES = /^(XCUIElementTypeApplication|XCUIElementTypeWindow)$/;
|
|
9637
|
+
var ALWAYS_PRESENT_CONTAINER_RESOURCE_IDS = /* @__PURE__ */ new Set([
|
|
9638
|
+
"android:id/content",
|
|
9639
|
+
"android:id/decor",
|
|
9640
|
+
"android:id/action_bar_root",
|
|
9641
|
+
"android:id/statusBarBackground",
|
|
9642
|
+
"android:id/navigationBarBackground"
|
|
9643
|
+
]);
|
|
9644
|
+
function isAlwaysPresentContainer(element) {
|
|
9645
|
+
if (ALWAYS_PRESENT_CONTAINER_ROLES.test(element.role))
|
|
9646
|
+
return true;
|
|
9647
|
+
const resourceId = element.resourceId?.trim();
|
|
9648
|
+
return !!resourceId && ALWAYS_PRESENT_CONTAINER_RESOURCE_IDS.has(resourceId);
|
|
9402
9649
|
}
|
|
9403
9650
|
function appendHybridFloor(actions, recording, platform3) {
|
|
9404
9651
|
const anchor = pickLandingAnchor(recording.lastSnapshot);
|
|
@@ -9452,12 +9699,31 @@ ${statePacket}` },
|
|
|
9452
9699
|
steps: compiled.steps,
|
|
9453
9700
|
playwrightCode: "",
|
|
9454
9701
|
verified,
|
|
9455
|
-
tags
|
|
9702
|
+
tags,
|
|
9703
|
+
// Carry the planner scenario identity so incremental/resume can match an
|
|
9704
|
+
// already-generated scenario reliably (scenarioRef.name === the plan's
|
|
9705
|
+
// scenario name). Without this the resume skip-set falls back to the
|
|
9706
|
+
// model-chosen test name and may regenerate completed scenarios. Mobile has
|
|
9707
|
+
// no live command/locator ledger, so evidence is an empty (but valid) bundle
|
|
9708
|
+
// — scenarioRef is the load-bearing field for the skip-set.
|
|
9709
|
+
healingContext: {
|
|
9710
|
+
scenarioRef: {
|
|
9711
|
+
name: scenario.name,
|
|
9712
|
+
featureGroup: scenario.featureGroup,
|
|
9713
|
+
goal: scenario.goal,
|
|
9714
|
+
type: scenario.type,
|
|
9715
|
+
variant: scenario.variant,
|
|
9716
|
+
entryRoute: scenario.entryRoute,
|
|
9717
|
+
startFromLanding: scenario.startFromLanding,
|
|
9718
|
+
targetPages: [...scenario.targetPages]
|
|
9719
|
+
},
|
|
9720
|
+
evidence: { observedLocators: [], observedTransitions: [] }
|
|
9721
|
+
}
|
|
9456
9722
|
};
|
|
9457
9723
|
return test;
|
|
9458
9724
|
}
|
|
9459
9725
|
async function runMobileGeneratePhase(args) {
|
|
9460
|
-
const { scenarios, driver, ctx, config, onLog, onAICall, onTestGenerated, onScreenshot } = args;
|
|
9726
|
+
const { scenarios: allScenarios, driver, ctx, config, onLog, onAICall, onTestGenerated, onScreenshot, skipScenarioNames, onInfraStop } = args;
|
|
9461
9727
|
const log2 = (line) => {
|
|
9462
9728
|
try {
|
|
9463
9729
|
onLog?.(line);
|
|
@@ -9472,8 +9738,16 @@ ${statePacket}` },
|
|
|
9472
9738
|
const aiClient = modelOverride ? (0, ai_provider_js_1.getMobileClientForModel)(modelOverride) : (0, ai_provider_js_1.getMobileAIClient)();
|
|
9473
9739
|
const auditGroupId = `mobile-generate-${(0, crypto_1.randomUUID)().slice(0, 8)}`;
|
|
9474
9740
|
const targetAppId = ctx.target?.appId?.trim() || void 0;
|
|
9741
|
+
const skipSet = new Set(skipScenarioNames ?? []);
|
|
9742
|
+
const scenarios = skipSet.size > 0 ? allScenarios.filter((s) => !skipSet.has(s.name)) : allScenarios;
|
|
9743
|
+
if (skipSet.size > 0) {
|
|
9744
|
+
log2(`Resume: skipping ${allScenarios.length - scenarios.length} already-generated scenario(s); ${scenarios.length} remaining to generate`);
|
|
9745
|
+
}
|
|
9475
9746
|
log2(`Mobile generator starting \u2014 platform: ${platform3}, provider: ${provider}, model: ${agentModel}, scenarios: ${scenarios.length}`);
|
|
9476
9747
|
const tests = [];
|
|
9748
|
+
const generatedScenarioNames = /* @__PURE__ */ new Set();
|
|
9749
|
+
let consecutiveInfraFailures = 0;
|
|
9750
|
+
let infraStopped = false;
|
|
9477
9751
|
for (let i = 0; i < scenarios.length; i++) {
|
|
9478
9752
|
const scenario = scenarios[i];
|
|
9479
9753
|
log2(`
|
|
@@ -9494,6 +9768,7 @@ ${statePacket}` },
|
|
|
9494
9768
|
targetAppId,
|
|
9495
9769
|
log: log2
|
|
9496
9770
|
});
|
|
9771
|
+
consecutiveInfraFailures = 0;
|
|
9497
9772
|
const test = assembleTest({ scenario, recording, ctx, platform: platform3, log: log2 });
|
|
9498
9773
|
if (!test) {
|
|
9499
9774
|
log2(` scenario "${scenario.name}" produced no compilable test \u2014 skipped.`);
|
|
@@ -9510,13 +9785,31 @@ ${statePacket}` },
|
|
|
9510
9785
|
log2(` onTestGenerated callback error: ${cbErr instanceof Error ? cbErr.message : String(cbErr)}`);
|
|
9511
9786
|
}
|
|
9512
9787
|
tests.push(test);
|
|
9788
|
+
generatedScenarioNames.add(scenario.name);
|
|
9513
9789
|
} catch (err) {
|
|
9790
|
+
if ((0, ai_retry_js_1.isAIProviderError)(err)) {
|
|
9791
|
+
consecutiveInfraFailures += 1;
|
|
9792
|
+
log2(` scenario "${scenario.name}" failed \u2014 AI provider error (${consecutiveInfraFailures}/${MAX_CONSECUTIVE_INFRA_FAILURES}): ${err instanceof Error ? err.message : String(err)}`);
|
|
9793
|
+
if (consecutiveInfraFailures >= MAX_CONSECUTIVE_INFRA_FAILURES) {
|
|
9794
|
+
const remainingScenarioNames = scenarios.filter((s) => !generatedScenarioNames.has(s.name)).map((s) => s.name);
|
|
9795
|
+
infraStopped = true;
|
|
9796
|
+
log2(`
|
|
9797
|
+
\u26D4 Stopping mobile generation: ${consecutiveInfraFailures} consecutive AI provider failures \u2014 the provider appears overloaded/unavailable. ${tests.length}/${scenarios.length} tests generated and saved; the remaining ${remainingScenarioNames.length} scenario(s) will resume later.`);
|
|
9798
|
+
try {
|
|
9799
|
+
onInfraStop?.({ remainingScenarioNames, reason: "ai_rate_limit" });
|
|
9800
|
+
} catch (cbErr) {
|
|
9801
|
+
log2(` onInfraStop callback error: ${cbErr instanceof Error ? cbErr.message : String(cbErr)}`);
|
|
9802
|
+
}
|
|
9803
|
+
break;
|
|
9804
|
+
}
|
|
9805
|
+
continue;
|
|
9806
|
+
}
|
|
9514
9807
|
log2(` scenario "${scenario.name}" failed: ${err instanceof Error ? err.message : String(err)} \u2014 skipping.`);
|
|
9515
9808
|
continue;
|
|
9516
9809
|
}
|
|
9517
9810
|
}
|
|
9518
9811
|
log2(`
|
|
9519
|
-
Mobile generate phase complete: ${tests.length}/${scenarios.length} test(s) generated (${tests.filter((t) => t.verified).length} verified).`);
|
|
9812
|
+
Mobile generate phase complete: ${tests.length}/${scenarios.length} test(s) generated (${tests.filter((t) => t.verified).length} verified)${infraStopped ? " \u2014 PAUSED on AI provider overload" : ""}.`);
|
|
9520
9813
|
return tests;
|
|
9521
9814
|
}
|
|
9522
9815
|
}
|
|
@@ -12795,6 +13088,10 @@ var require_snapshot_observation = __commonJS({
|
|
|
12795
13088
|
var snapshot_parser_js_1 = require_snapshot_parser();
|
|
12796
13089
|
var GENERIC_LABEL_RE = /^(input field|textbox|button|link|item|field|value|tab)$/i;
|
|
12797
13090
|
var NOISE_LABEL_RE = /^(skip\s+to|skip\s+nav|back\s+to\s+top|cookie|accept\s+(all\s+)?cookies)$/i;
|
|
13091
|
+
var DIALOG_SEGMENT_RE = /(?:^|>\s*)(?:alert)?dialog\b/i;
|
|
13092
|
+
var FORM_SEGMENT_RE = /(?:^|>\s*)form\b/i;
|
|
13093
|
+
var TABPANEL_SEGMENT_RE = /(?:^|>\s*)tabpanel\b/i;
|
|
13094
|
+
var DIALOG_NAME_SEGMENT_RE = /(?:^|>\s*)(?:alert)?dialog:([^>]+)/i;
|
|
12798
13095
|
var INTERACTIVE_ROLE_RE = /^(button|link|textbox|searchbox|checkbox|radio|switch|combobox|listbox|option|menuitem|tab|slider|spinbutton|textarea)$/i;
|
|
12799
13096
|
var INTERACTIVE_ELEMENT_TYPE_RE = /^(button|link|input|textarea|select|checkbox|radio|switch|tab|combobox|listbox|option|menuitem|slider|spinbutton)$/i;
|
|
12800
13097
|
function renderRetrievedSnapshotMemoryBlock(priorSnapshotEvidence, priorDiscoveryCheckpoints, options = {}) {
|
|
@@ -13039,10 +13336,12 @@ var require_snapshot_observation = __commonJS({
|
|
|
13039
13336
|
summary
|
|
13040
13337
|
}));
|
|
13041
13338
|
}
|
|
13339
|
+
var TEXT_DISAMBIGUATING_INPUT_TYPES = /* @__PURE__ */ new Set(["password", "text", "email", "tel", "number", "search", "url", "date", "datetime-local", "time", "month", "week"]);
|
|
13042
13340
|
function renderCompactSnapshotBlock(page, context = {}, options = {}) {
|
|
13043
13341
|
const metrics = page.snapshotMetrics ?? page.snapshotArtifact?.metrics;
|
|
13044
13342
|
const sourceKind = page.snapshotMeta?.sourceKind ?? page.snapshotArtifact?.sourceKind ?? "missing";
|
|
13045
13343
|
const artifact = page.snapshotArtifact;
|
|
13344
|
+
const richRender = options.richRender ?? process.env.DISCOVERY_SNAPSHOT_RICH_RENDER === "true";
|
|
13046
13345
|
const activeScope = inferActiveSnapshotScope(page);
|
|
13047
13346
|
const ranked = rankSnapshotElements(getScopedSnapshotElements(page, activeScope), withScopeContext(context, activeScope));
|
|
13048
13347
|
const allInteractive = ranked.filter(({ element }) => isInteractiveCandidate(element)).filter(({ element }) => {
|
|
@@ -13088,7 +13387,7 @@ var require_snapshot_observation = __commonJS({
|
|
|
13088
13387
|
if (activeScope.rootRef) {
|
|
13089
13388
|
lines.push(` root_ref: ${yamlScalar(activeScope.rootRef)}`);
|
|
13090
13389
|
}
|
|
13091
|
-
const scopePreview = buildScopeSnapshotPreview(activeScope.scopeSnapshotText);
|
|
13390
|
+
const scopePreview = buildScopeSnapshotPreview(activeScope.scopeSnapshotText, richRender);
|
|
13092
13391
|
if (scopePreview.length > 0) {
|
|
13093
13392
|
lines.push(" scope_snapshot_preview: |");
|
|
13094
13393
|
for (const previewLine of scopePreview) {
|
|
@@ -13130,6 +13429,19 @@ var require_snapshot_observation = __commonJS({
|
|
|
13130
13429
|
lines.push(` - ${yamlScalar(modal)}`);
|
|
13131
13430
|
}
|
|
13132
13431
|
}
|
|
13432
|
+
const staticMessages = page.staticMessages ?? [];
|
|
13433
|
+
if (richRender && staticMessages.length > 0) {
|
|
13434
|
+
const messageLimit = 6;
|
|
13435
|
+
const ordered = [...staticMessages].sort((a, b) => messageRolePriority(a.role) - messageRolePriority(b.role));
|
|
13436
|
+
lines.push("messages:");
|
|
13437
|
+
lines.push(" # Non-interactive page text worth asserting on (validation / toast / empty-state / counts). Text is the capture instant \u2014 toasts may have dismissed since.");
|
|
13438
|
+
for (const message of ordered.slice(0, messageLimit)) {
|
|
13439
|
+
lines.push(` - [${message.role}] ${yamlScalar(message.text)}`);
|
|
13440
|
+
}
|
|
13441
|
+
if (ordered.length > messageLimit) {
|
|
13442
|
+
lines.push(` # +${ordered.length - messageLimit} more message(s) not listed`);
|
|
13443
|
+
}
|
|
13444
|
+
}
|
|
13133
13445
|
lines.push("session_state:");
|
|
13134
13446
|
if (page.sessionState) {
|
|
13135
13447
|
lines.push(` authenticated: ${yamlScalar(page.sessionState.authenticated)}`);
|
|
@@ -13192,7 +13504,10 @@ var require_snapshot_observation = __commonJS({
|
|
|
13192
13504
|
}
|
|
13193
13505
|
const hasRowContext = group.rowContexts.some((rc) => !!rc);
|
|
13194
13506
|
if (hasRowContext) {
|
|
13195
|
-
|
|
13507
|
+
const firstRowRole = group.rowRoles[0] ?? null;
|
|
13508
|
+
const rowRole = firstRowRole && group.rowRoles.every((r) => r === firstRowRole) ? firstRowRole : null;
|
|
13509
|
+
const scopeHint = rowRole ? `getByRole(${yamlScalar(rowRole)}).filter({ has: getByText("<row>", { exact: true }) }).getByRole(${yamlScalar(group.role)}, { name: ${yamlScalar(group.name)} })` : `locate the container by its text (getByText("<row>", { exact: true })), then getByRole(${yamlScalar(group.role)}, { name: ${yamlScalar(group.name)} }) within it`;
|
|
13510
|
+
lines.push(` distinguish_by_row: # same name across rows \u2014 scope by the ROW/card container (not the control); nested same-role containers need an extra cell-level scope: ${scopeHint}`);
|
|
13196
13511
|
for (let i = 0; i < group.rowContexts.length; i++) {
|
|
13197
13512
|
const rowContext = group.rowContexts[i];
|
|
13198
13513
|
if (!rowContext)
|
|
@@ -13222,8 +13537,13 @@ var require_snapshot_observation = __commonJS({
|
|
|
13222
13537
|
lines.push(` placeholder: ${yamlScalar(rankedElement.element.placeholder)}`);
|
|
13223
13538
|
}
|
|
13224
13539
|
const stateTokens = buildStateSummary(rankedElement.element);
|
|
13540
|
+
const addedDisabled = richRender && !!rankedElement.element.isDisabled && !stateTokens.includes("disabled");
|
|
13541
|
+
if (addedDisabled) {
|
|
13542
|
+
stateTokens.unshift("disabled");
|
|
13543
|
+
}
|
|
13225
13544
|
if (stateTokens.length > 0) {
|
|
13226
|
-
|
|
13545
|
+
const note = addedDisabled ? " # disabled = captured instant; may enable after valid input \u2014 still exercise fill\u2192submit" : "";
|
|
13546
|
+
lines.push(` state: [${stateTokens.map((token) => yamlScalar(token)).join(", ")}]${note}`);
|
|
13227
13547
|
}
|
|
13228
13548
|
if (rankedElement.element.containerPath) {
|
|
13229
13549
|
lines.push(` container_path: ${yamlScalar(rankedElement.element.containerPath)}`);
|
|
@@ -13233,6 +13553,22 @@ var require_snapshot_observation = __commonJS({
|
|
|
13233
13553
|
lines.push(` locator: ${yamlScalar((0, snapshot_parser_js_1.formatLocator)(topLocator))}`);
|
|
13234
13554
|
}
|
|
13235
13555
|
const enr = rankedElement.element.enrichment;
|
|
13556
|
+
if (richRender && enr?.inputType && TEXT_DISAMBIGUATING_INPUT_TYPES.has(enr.inputType)) {
|
|
13557
|
+
lines.push(` type: ${yamlScalar(enr.inputType)}`);
|
|
13558
|
+
}
|
|
13559
|
+
if (richRender && rankedElement.element.href) {
|
|
13560
|
+
lines.push(` url: ${yamlScalar(rankedElement.element.href)}`);
|
|
13561
|
+
}
|
|
13562
|
+
if (richRender && rankedElement.element.value && enr?.inputType !== "password") {
|
|
13563
|
+
lines.push(` value: ${yamlScalar(rankedElement.element.value)}`);
|
|
13564
|
+
}
|
|
13565
|
+
const elementOptions = rankedElement.element.options ?? [];
|
|
13566
|
+
if (richRender && elementOptions.length > 0) {
|
|
13567
|
+
const optionLimit = 12;
|
|
13568
|
+
const rendered = elementOptions.slice(0, optionLimit).map((option) => yamlScalar(option.selected ? `${option.label} (selected)` : option.label));
|
|
13569
|
+
const overflow = elementOptions.length > optionLimit ? ` # +${elementOptions.length - optionLimit} more option(s)` : "";
|
|
13570
|
+
lines.push(` options: [${rendered.join(", ")}]${overflow}`);
|
|
13571
|
+
}
|
|
13236
13572
|
if (enr?.rowContext) {
|
|
13237
13573
|
lines.push(` row_context: ${yamlScalar(enr.rowContext)}`);
|
|
13238
13574
|
}
|
|
@@ -13245,6 +13581,12 @@ var require_snapshot_observation = __commonJS({
|
|
|
13245
13581
|
lines.push(` visibility: [${vis.map((token) => yamlScalar(token)).join(", ")}]`);
|
|
13246
13582
|
}
|
|
13247
13583
|
}
|
|
13584
|
+
if (richRender) {
|
|
13585
|
+
const hidden = allInteractive.length - interactive.length;
|
|
13586
|
+
if (hidden > 0) {
|
|
13587
|
+
lines.push(` # +${hidden} more interactive element(s) not individually listed (capped) \u2014 some may appear in the duplicates block above; if your target isn't here, narrow the scope or act on it by ref.`);
|
|
13588
|
+
}
|
|
13589
|
+
}
|
|
13248
13590
|
}
|
|
13249
13591
|
const recoveredClickables = options.recoveredClickables ?? [];
|
|
13250
13592
|
if (recoveredClickables.length > 0) {
|
|
@@ -13401,11 +13743,11 @@ var require_snapshot_observation = __commonJS({
|
|
|
13401
13743
|
score += 10;
|
|
13402
13744
|
if (element.isExpanded)
|
|
13403
13745
|
score += 8;
|
|
13404
|
-
if (element.containerPath
|
|
13746
|
+
if (element.containerPath && DIALOG_SEGMENT_RE.test(element.containerPath))
|
|
13405
13747
|
score += 12;
|
|
13406
|
-
if (element.containerPath
|
|
13748
|
+
if (element.containerPath && FORM_SEGMENT_RE.test(element.containerPath))
|
|
13407
13749
|
score += 10;
|
|
13408
|
-
if (element.containerPath
|
|
13750
|
+
if (element.containerPath && TABPANEL_SEGMENT_RE.test(element.containerPath))
|
|
13409
13751
|
score += 8;
|
|
13410
13752
|
if (element.inputType === "email" || element.inputType === "password" || element.inputType === "search")
|
|
13411
13753
|
score += 6;
|
|
@@ -13476,10 +13818,10 @@ var require_snapshot_observation = __commonJS({
|
|
|
13476
13818
|
}
|
|
13477
13819
|
return merged;
|
|
13478
13820
|
}
|
|
13479
|
-
function buildScopeSnapshotPreview(scopeSnapshotText) {
|
|
13821
|
+
function buildScopeSnapshotPreview(scopeSnapshotText, stripTrailingValues = false) {
|
|
13480
13822
|
if (!scopeSnapshotText)
|
|
13481
13823
|
return [];
|
|
13482
|
-
return scopeSnapshotText.split("\n").map((line) => line.trim()).filter(Boolean).slice(0, 6);
|
|
13824
|
+
return scopeSnapshotText.split("\n").map((line) => line.trim()).map((line) => stripTrailingValues ? line.replace(/\]\s*:\s.+$/, "]") : line).filter(Boolean).slice(0, 6);
|
|
13483
13825
|
}
|
|
13484
13826
|
function dedupeLines(lines) {
|
|
13485
13827
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -13606,11 +13948,12 @@ var require_snapshot_observation = __commonJS({
|
|
|
13606
13948
|
const key = `${role}\0${name}`;
|
|
13607
13949
|
let group = groups.get(key);
|
|
13608
13950
|
if (!group) {
|
|
13609
|
-
group = { role, name, containers: [], rowContexts: [], refs: [] };
|
|
13951
|
+
group = { role, name, containers: [], rowContexts: [], rowRoles: [], refs: [] };
|
|
13610
13952
|
groups.set(key, group);
|
|
13611
13953
|
}
|
|
13612
13954
|
group.containers.push(element.containerPath ?? null);
|
|
13613
13955
|
group.rowContexts.push(element.enrichment?.rowContext ?? null);
|
|
13956
|
+
group.rowRoles.push(element.enrichment?.rowRole ?? null);
|
|
13614
13957
|
group.refs.push(element.ref ?? null);
|
|
13615
13958
|
}
|
|
13616
13959
|
return [...groups.values()].filter((group) => group.containers.length > 1);
|
|
@@ -13620,7 +13963,7 @@ var require_snapshot_observation = __commonJS({
|
|
|
13620
13963
|
for (const element of elements) {
|
|
13621
13964
|
const role = element.role?.toLowerCase() ?? "";
|
|
13622
13965
|
const elementType = element.elementType?.toLowerCase() ?? "";
|
|
13623
|
-
const isDialog = role === "dialog" || elementType === "dialog" || (element.containerPath
|
|
13966
|
+
const isDialog = role === "dialog" || elementType === "dialog" || DIALOG_NAME_SEGMENT_RE.test(element.containerPath ?? "");
|
|
13624
13967
|
if (!isDialog)
|
|
13625
13968
|
continue;
|
|
13626
13969
|
const hint = qualifyElementLabel(element) ?? dialogNameFromContainerPath(element.containerPath) ?? element.label ?? element.testId ?? "dialog";
|
|
@@ -13633,7 +13976,7 @@ var require_snapshot_observation = __commonJS({
|
|
|
13633
13976
|
function dialogNameFromContainerPath(containerPath) {
|
|
13634
13977
|
if (!containerPath)
|
|
13635
13978
|
return null;
|
|
13636
|
-
const match = containerPath.match(
|
|
13979
|
+
const match = containerPath.match(DIALOG_NAME_SEGMENT_RE);
|
|
13637
13980
|
return match ? match[1].trim() : null;
|
|
13638
13981
|
}
|
|
13639
13982
|
function renderObservationCue(observation) {
|
|
@@ -13642,6 +13985,13 @@ var require_snapshot_observation = __commonJS({
|
|
|
13642
13985
|
const base2 = observation.outcomeType === "SUCCESS" && outcome ? outcome : `${action} -> ${outcome || observation.outcomeType}`;
|
|
13643
13986
|
return base2.length > 120 ? `${base2.slice(0, 117)}...` : base2;
|
|
13644
13987
|
}
|
|
13988
|
+
function messageRolePriority(role) {
|
|
13989
|
+
if (role === "alert")
|
|
13990
|
+
return 0;
|
|
13991
|
+
if (role === "status")
|
|
13992
|
+
return 1;
|
|
13993
|
+
return 2;
|
|
13994
|
+
}
|
|
13645
13995
|
function yamlScalar(value) {
|
|
13646
13996
|
if (value == null || value === "")
|
|
13647
13997
|
return "null";
|
|
@@ -20190,7 +20540,10 @@ ${testOpen}`;
|
|
|
20190
20540
|
pressed: se.pressed,
|
|
20191
20541
|
headingLevel: se.headingLevel,
|
|
20192
20542
|
currentState: se.currentState,
|
|
20193
|
-
containerPath: se.containerPath || void 0
|
|
20543
|
+
containerPath: se.containerPath || void 0,
|
|
20544
|
+
href: se.href || void 0,
|
|
20545
|
+
value: se.value || void 0,
|
|
20546
|
+
options: se.options && se.options.length > 0 ? se.options : void 0
|
|
20194
20547
|
}));
|
|
20195
20548
|
const structuredPage = {
|
|
20196
20549
|
route: pageUrl || "/",
|
|
@@ -20198,6 +20551,7 @@ ${testOpen}`;
|
|
|
20198
20551
|
pageType: "unknown",
|
|
20199
20552
|
elements: collectedElements,
|
|
20200
20553
|
formGroups: parsed.formGroups,
|
|
20554
|
+
staticMessages: parsed.staticMessages.length > 0 ? parsed.staticMessages : void 0,
|
|
20201
20555
|
snapshotMetrics: {
|
|
20202
20556
|
rawLineCount: parsed.rawLineCount,
|
|
20203
20557
|
parsedElementLineCount: parsed.parsedElementLineCount,
|
|
@@ -21764,7 +22118,7 @@ Do NOT assert that the button/tab/heading you interacted with is merely visible
|
|
|
21764
22118
|
let stopped;
|
|
21765
22119
|
const agentModel = options?.modelOverride ? (0, ai_provider_js_1.resolveModelForTask)(options.modelOverride, "agent") : (0, ai_provider_js_1.getModel)("agent");
|
|
21766
22120
|
const auditGroupId = `generate-${(0, crypto_1.randomUUID)().slice(0, 8)}`;
|
|
21767
|
-
const aiClient = options?.modelOverride ? (0, ai_provider_js_1.
|
|
22121
|
+
const aiClient = options?.modelOverride ? (0, ai_provider_js_1.getRunnerClientForModel)(options.modelOverride) : (0, ai_provider_js_1.getRunnerAIClient)();
|
|
21768
22122
|
const credentials = variableContext.allTestVariables ?? variableContext.publicTestVariables;
|
|
21769
22123
|
logs2.push(`Phase 3 \u2014 Generate: ${(plan.scenarios ?? []).length} scenarios, max ${maxTests} tests`);
|
|
21770
22124
|
logs2.push(`Model: ${agentModel}`);
|
|
@@ -235729,7 +236083,7 @@ var require_mcp_healer = __commonJS({
|
|
|
235729
236083
|
messages.push(system, firstUser, { role: "user", content: lines.join("\n") }, ...tail);
|
|
235730
236084
|
}
|
|
235731
236085
|
var DEFAULT_MAX_SCRIPT_HEAL_ATTEMPTS = 6;
|
|
235732
|
-
function
|
|
236086
|
+
function readPositiveIntEnv2(env, name, fallback) {
|
|
235733
236087
|
const raw = env[name]?.trim();
|
|
235734
236088
|
if (!raw || !/^\d+$/.test(raw))
|
|
235735
236089
|
return fallback;
|
|
@@ -235737,7 +236091,7 @@ var require_mcp_healer = __commonJS({
|
|
|
235737
236091
|
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
235738
236092
|
}
|
|
235739
236093
|
function getScriptHealMaxAttempts(env = process.env) {
|
|
235740
|
-
return
|
|
236094
|
+
return readPositiveIntEnv2(env, "MCP_HEAL_SCRIPT_MAX_ATTEMPTS", DEFAULT_MAX_SCRIPT_HEAL_ATTEMPTS);
|
|
235741
236095
|
}
|
|
235742
236096
|
exports2.SCRIPT_HEAL_SYSTEM_PROMPT = `You are a QA engineer fixing a failing Playwright UI test. You will receive the test code, the error message, step-level results, and a screenshot of the failure state.
|
|
235743
236097
|
|
|
@@ -236073,7 +236427,7 @@ ${lines.join("\n")}
|
|
|
236073
236427
|
const landingViolation = (tags ?? []).includes("@landing-violation");
|
|
236074
236428
|
const agentModel = modelOverride ? (0, ai_provider_js_1.resolveModelForTask)(modelOverride, "strong") : (0, ai_provider_js_1.getModel)("strong");
|
|
236075
236429
|
const provider = modelOverride ? (0, ai_provider_js_1.getProviderForModel)(modelOverride) : (0, ai_provider_js_1.getAIProvider)();
|
|
236076
|
-
const aiClient = options.aiClient ?? (modelOverride ? (0, ai_provider_js_1.
|
|
236430
|
+
const aiClient = options.aiClient ?? (modelOverride ? (0, ai_provider_js_1.getRunnerClientForModel)(modelOverride) : (0, ai_provider_js_1.getRunnerAIClient)());
|
|
236077
236431
|
const runNativeTest = options.runNativeTest ?? playwright_native_js_1.executeTestViaNativePlaywright;
|
|
236078
236432
|
const modelSupportsVision = /gpt-4o|claude-3|claude-4|minimax|grok/i.test(agentModel ?? "");
|
|
236079
236433
|
const groupId = `script-heal-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -236895,10 +237249,10 @@ ${(verifyResult.logs ?? "").slice(0, 2e3)}`;
|
|
|
236895
237249
|
var DEFAULT_MAX_PHASE2_ITERATIONS = 4;
|
|
236896
237250
|
var DEFAULT_MAX_MCP_EXPLORE_ITERATIONS = 25;
|
|
236897
237251
|
function getMcpHealPhase2MaxIterations(env = process.env) {
|
|
236898
|
-
return
|
|
237252
|
+
return readPositiveIntEnv2(env, "MCP_HEAL_PHASE2_MAX_ITERATIONS", DEFAULT_MAX_PHASE2_ITERATIONS);
|
|
236899
237253
|
}
|
|
236900
237254
|
function getMcpHealExploreMaxIterations(env = process.env) {
|
|
236901
|
-
return
|
|
237255
|
+
return readPositiveIntEnv2(env, "MCP_HEAL_EXPLORE_MAX_ITERATIONS", DEFAULT_MAX_MCP_EXPLORE_ITERATIONS);
|
|
236902
237256
|
}
|
|
236903
237257
|
var DEFAULT_MCP_HEAL_MAX_WALL_CLOCK_MS = 10 * 60 * 1e3;
|
|
236904
237258
|
var DEFAULT_MCP_HEAL_MAX_TOTAL_AI_CALLS = 60;
|
|
@@ -236950,7 +237304,7 @@ Only explore the failing page and the area around the broken step.`;
|
|
|
236950
237304
|
let lastVideo;
|
|
236951
237305
|
const agentModel = modelOverride ? (0, ai_provider_js_1.resolveModelForTask)(modelOverride, "agent") : (0, ai_provider_js_1.getModel)("agent");
|
|
236952
237306
|
const provider = modelOverride ? (0, ai_provider_js_1.getProviderForModel)(modelOverride) : (0, ai_provider_js_1.getAIProvider)();
|
|
236953
|
-
const aiClient = options?.phase2Harness?.aiClient ?? (modelOverride ? (0, ai_provider_js_1.
|
|
237307
|
+
const aiClient = options?.phase2Harness?.aiClient ?? (modelOverride ? (0, ai_provider_js_1.getRunnerClientForModel)(modelOverride) : (0, ai_provider_js_1.getRunnerAIClient)());
|
|
236954
237308
|
const runPhase2NativeTest = options?.phase2Harness?.runNativeTest ?? playwright_native_js_1.executeTestViaNativePlaywright;
|
|
236955
237309
|
const modelSupportsVision = /gpt-4o|claude-3|claude-4|minimax|grok/i.test(agentModel ?? "");
|
|
236956
237310
|
const groupId = `phase2-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -237640,7 +237994,10 @@ Fix the syntax error and provide ONLY the complete corrected Playwright test as
|
|
|
237640
237994
|
pressed: se.pressed,
|
|
237641
237995
|
headingLevel: se.headingLevel,
|
|
237642
237996
|
currentState: se.currentState,
|
|
237643
|
-
containerPath: se.containerPath || void 0
|
|
237997
|
+
containerPath: se.containerPath || void 0,
|
|
237998
|
+
href: se.href || void 0,
|
|
237999
|
+
value: se.value || void 0,
|
|
238000
|
+
options: se.options && se.options.length > 0 ? se.options : void 0
|
|
237644
238001
|
}));
|
|
237645
238002
|
const structuredPage = {
|
|
237646
238003
|
route: pageUrl || "/",
|
|
@@ -237648,6 +238005,7 @@ Fix the syntax error and provide ONLY the complete corrected Playwright test as
|
|
|
237648
238005
|
pageType: "unknown",
|
|
237649
238006
|
elements: collectedElements,
|
|
237650
238007
|
formGroups: parsed.formGroups,
|
|
238008
|
+
staticMessages: parsed.staticMessages.length > 0 ? parsed.staticMessages : void 0,
|
|
237651
238009
|
snapshotMetrics: {
|
|
237652
238010
|
rawLineCount: parsed.rawLineCount,
|
|
237653
238011
|
parsedElementLineCount: parsed.parsedElementLineCount,
|
|
@@ -238084,7 +238442,7 @@ ${testBlocks}
|
|
|
238084
238442
|
- By iteration ${Math.floor(maxIterations * 0.3)}: you should have completed 1-2 tests
|
|
238085
238443
|
- By iteration ${Math.floor(maxIterations * 0.7)}: you should be past the halfway point
|
|
238086
238444
|
- At iteration ${Math.floor(maxIterations * 0.9)}: wrap up and call finish_capture`;
|
|
238087
|
-
const aiClient = modelOverride ? (0, ai_provider_js_1.
|
|
238445
|
+
const aiClient = modelOverride ? (0, ai_provider_js_1.getRunnerClientForModel)(modelOverride) : (0, ai_provider_js_1.getRunnerAIClient)();
|
|
238088
238446
|
const messages = [
|
|
238089
238447
|
{ role: "system", content: systemPrompt },
|
|
238090
238448
|
{ role: "user", content: "Start executing the tests. Navigate to the base URL and begin with Test 1." }
|
|
@@ -238267,7 +238625,10 @@ ${testBlocks}
|
|
|
238267
238625
|
pressed: se.pressed,
|
|
238268
238626
|
headingLevel: se.headingLevel,
|
|
238269
238627
|
currentState: se.currentState,
|
|
238270
|
-
containerPath: se.containerPath || void 0
|
|
238628
|
+
containerPath: se.containerPath || void 0,
|
|
238629
|
+
href: se.href || void 0,
|
|
238630
|
+
value: se.value || void 0,
|
|
238631
|
+
options: se.options && se.options.length > 0 ? se.options : void 0
|
|
238271
238632
|
}));
|
|
238272
238633
|
const structuredPage = {
|
|
238273
238634
|
route: pageUrl || "/",
|
|
@@ -238275,6 +238636,7 @@ ${testBlocks}
|
|
|
238275
238636
|
pageType: "unknown",
|
|
238276
238637
|
elements: collectedElements,
|
|
238277
238638
|
formGroups: parsed.formGroups,
|
|
238639
|
+
staticMessages: parsed.staticMessages.length > 0 ? parsed.staticMessages : void 0,
|
|
238278
238640
|
snapshotMetrics: {
|
|
238279
238641
|
rawLineCount: parsed.rawLineCount,
|
|
238280
238642
|
parsedElementLineCount: parsed.parsedElementLineCount,
|
|
@@ -300064,8 +300426,9 @@ var require_capture_page_normalizer = __commonJS({
|
|
|
300064
300426
|
let snapshotElements = [];
|
|
300065
300427
|
let formGroups = [];
|
|
300066
300428
|
let snapshotMetrics;
|
|
300429
|
+
let staticMessages;
|
|
300067
300430
|
if (normalizedSnapshotResult.snapshotText) {
|
|
300068
|
-
const parsed = (0, snapshot_parser_js_1.parseSnapshot)(normalizedSnapshotResult.snapshotText);
|
|
300431
|
+
const parsed = (0, snapshot_parser_js_1.parseSnapshot)(normalizedSnapshotResult.snapshotText, { richParse: context.richParse });
|
|
300069
300432
|
snapshotMetrics = {
|
|
300070
300433
|
rawLineCount: parsed.rawLineCount,
|
|
300071
300434
|
parsedElementLineCount: parsed.parsedElementLineCount,
|
|
@@ -300094,9 +300457,13 @@ var require_capture_page_normalizer = __commonJS({
|
|
|
300094
300457
|
pressed: se.pressed,
|
|
300095
300458
|
headingLevel: se.headingLevel,
|
|
300096
300459
|
currentState: se.currentState,
|
|
300097
|
-
containerPath: se.containerPath || void 0
|
|
300460
|
+
containerPath: se.containerPath || void 0,
|
|
300461
|
+
href: se.href || void 0,
|
|
300462
|
+
value: se.value || void 0,
|
|
300463
|
+
options: se.options && se.options.length > 0 ? se.options : void 0
|
|
300098
300464
|
}));
|
|
300099
300465
|
formGroups = parsed.formGroups;
|
|
300466
|
+
staticMessages = parsed.staticMessages.length > 0 ? parsed.staticMessages : void 0;
|
|
300100
300467
|
}
|
|
300101
300468
|
const aiElements = Array.isArray(toolArgs.elements) ? toolArgs.elements.map(normalizeAiElement) : [];
|
|
300102
300469
|
const noYamlKinds = /* @__PURE__ */ new Set(["missing", "timeout", "error", "empty"]);
|
|
@@ -300192,6 +300559,7 @@ var require_capture_page_normalizer = __commonJS({
|
|
|
300192
300559
|
} : void 0,
|
|
300193
300560
|
readinessSignals,
|
|
300194
300561
|
viewportHints,
|
|
300562
|
+
staticMessages,
|
|
300195
300563
|
provenCommands: provenCmds,
|
|
300196
300564
|
observations: rawObservations,
|
|
300197
300565
|
routeCorrected,
|
|
@@ -300500,6 +300868,10 @@ var require_perception_enricher = __commonJS({
|
|
|
300500
300868
|
const e = {};
|
|
300501
300869
|
if (p.rowContext)
|
|
300502
300870
|
e.rowContext = p.rowContext;
|
|
300871
|
+
if (p.rowContext && p.rowRole)
|
|
300872
|
+
e.rowRole = p.rowRole;
|
|
300873
|
+
if (p.inputType)
|
|
300874
|
+
e.inputType = p.inputType;
|
|
300503
300875
|
if (p.position && p.position !== "in-view")
|
|
300504
300876
|
e.position = p.position;
|
|
300505
300877
|
if (p.occluded)
|
|
@@ -300749,6 +301121,23 @@ var require_perception_enricher = __commonJS({
|
|
|
300749
301121
|
const cls = (el.getAttribute("class") || "").toLowerCase();
|
|
300750
301122
|
return /(^|[\s_-])(card|row|item|listitem|cell|tile)([\s_-]|$)/.test(cls);
|
|
300751
301123
|
};
|
|
301124
|
+
const rowRoleOf = (el) => {
|
|
301125
|
+
const explicit = (el.getAttribute("role") || "").toLowerCase().trim();
|
|
301126
|
+
if (explicit) {
|
|
301127
|
+
return explicit === "row" || explicit === "listitem" || explicit === "article" || explicit === "option" ? explicit : null;
|
|
301128
|
+
}
|
|
301129
|
+
const tag = el.tagName;
|
|
301130
|
+
if (tag === "TR") {
|
|
301131
|
+
const tbl = el.closest("table");
|
|
301132
|
+
const tblRole = tbl ? (tbl.getAttribute("role") || "").toLowerCase().trim() : "";
|
|
301133
|
+
return tblRole === "presentation" || tblRole === "none" ? null : "row";
|
|
301134
|
+
}
|
|
301135
|
+
if (tag === "LI")
|
|
301136
|
+
return el.closest('ul:not([role]),ol:not([role]),menu:not([role]),[role="list"]') ? "listitem" : null;
|
|
301137
|
+
if (tag === "ARTICLE")
|
|
301138
|
+
return "article";
|
|
301139
|
+
return null;
|
|
301140
|
+
};
|
|
300752
301141
|
const rowContextOf = (el, ownNameLower) => {
|
|
300753
301142
|
let cur = el.parentElement;
|
|
300754
301143
|
let depth = 0;
|
|
@@ -300767,7 +301156,7 @@ var require_perception_enricher = __commonJS({
|
|
|
300767
301156
|
txt = txt.slice(0, 80);
|
|
300768
301157
|
const low = txt.toLowerCase();
|
|
300769
301158
|
if (txt && low !== ownNameLower && low.length > 1)
|
|
300770
|
-
return txt;
|
|
301159
|
+
return { text: txt, role: rowRoleOf(cur) };
|
|
300771
301160
|
return null;
|
|
300772
301161
|
}
|
|
300773
301162
|
cur = cur.parentElement;
|
|
@@ -300857,11 +301246,14 @@ var require_perception_enricher = __commonJS({
|
|
|
300857
301246
|
const name = accName(el);
|
|
300858
301247
|
const lname = lower(name);
|
|
300859
301248
|
const rect = vis.rect;
|
|
301249
|
+
const rc = rowContextOf(el, lname);
|
|
300860
301250
|
out.push({
|
|
300861
301251
|
domOrder: myOrder,
|
|
300862
301252
|
roleClass: roleClassOf(tag, roleAttr, inputType),
|
|
300863
301253
|
name: lname,
|
|
300864
|
-
rowContext:
|
|
301254
|
+
rowContext: rc ? rc.text : null,
|
|
301255
|
+
rowRole: rc ? rc.role : null,
|
|
301256
|
+
inputType: inputType || null,
|
|
300865
301257
|
position: positionOf(rect),
|
|
300866
301258
|
occluded: occlusionOf(el, rect),
|
|
300867
301259
|
cursorPointer,
|
|
@@ -300898,11 +301290,14 @@ var require_perception_enricher = __commonJS({
|
|
|
300898
301290
|
if (!name)
|
|
300899
301291
|
continue;
|
|
300900
301292
|
const lname = lower(name);
|
|
301293
|
+
const rc = rowContextOf(el, lname);
|
|
300901
301294
|
out.push({
|
|
300902
301295
|
domOrder: order++,
|
|
300903
301296
|
roleClass: "button",
|
|
300904
301297
|
name: lname,
|
|
300905
|
-
rowContext:
|
|
301298
|
+
rowContext: rc ? rc.text : null,
|
|
301299
|
+
rowRole: rc ? rc.role : null,
|
|
301300
|
+
inputType: null,
|
|
300906
301301
|
position: "in-view",
|
|
300907
301302
|
occluded: false,
|
|
300908
301303
|
cursorPointer: true,
|
|
@@ -301404,7 +301799,7 @@ var require_discover_explorer = __commonJS({
|
|
|
301404
301799
|
var DEFAULT_MAX_EXPLORE_ITERATIONS_DEEP = 100;
|
|
301405
301800
|
var DEFAULT_MAX_EXPLORE_ITERATIONS_SURVEY = 100;
|
|
301406
301801
|
var DEFAULT_MAX_EXPLORE_ITERATIONS_INCREMENTAL = 40;
|
|
301407
|
-
function
|
|
301802
|
+
function readPositiveIntEnv2(env, name, fallback) {
|
|
301408
301803
|
const raw = env[name]?.trim();
|
|
301409
301804
|
if (!raw || !/^\d+$/.test(raw))
|
|
301410
301805
|
return fallback;
|
|
@@ -301535,15 +301930,15 @@ var require_discover_explorer = __commonJS({
|
|
|
301535
301930
|
}
|
|
301536
301931
|
function getMaxIterations(mode, incremental, env = process.env) {
|
|
301537
301932
|
if (mode === "survey" && incremental) {
|
|
301538
|
-
return
|
|
301933
|
+
return readPositiveIntEnv2(env, "DISCOVERY_EXPLORE_MAX_ITERATIONS_INCREMENTAL", DEFAULT_MAX_EXPLORE_ITERATIONS_INCREMENTAL);
|
|
301539
301934
|
}
|
|
301540
301935
|
if (mode === "survey") {
|
|
301541
|
-
return
|
|
301936
|
+
return readPositiveIntEnv2(env, "DISCOVERY_EXPLORE_MAX_ITERATIONS_SURVEY", DEFAULT_MAX_EXPLORE_ITERATIONS_SURVEY);
|
|
301542
301937
|
}
|
|
301543
301938
|
if (mode === "deep") {
|
|
301544
|
-
return
|
|
301939
|
+
return readPositiveIntEnv2(env, "DISCOVERY_EXPLORE_MAX_ITERATIONS_DEEP", DEFAULT_MAX_EXPLORE_ITERATIONS_DEEP);
|
|
301545
301940
|
}
|
|
301546
|
-
return
|
|
301941
|
+
return readPositiveIntEnv2(env, "DISCOVERY_EXPLORE_MAX_ITERATIONS_DEFAULT", DEFAULT_MAX_EXPLORE_ITERATIONS_DEFAULT);
|
|
301547
301942
|
}
|
|
301548
301943
|
function updatePendingRouteScreenshot(pending, { imageData, detectedRoute, fallbackRoute }) {
|
|
301549
301944
|
if (imageData) {
|
|
@@ -302269,7 +302664,7 @@ ${inboxPromptBlock}`;
|
|
|
302269
302664
|
const browserCreds = variableContext.allTestVariables ?? variableContext.publicTestVariables;
|
|
302270
302665
|
const { secrets: browserSecretsForHint } = (0, credential_tools_js_1.partitionTestVariables)(browserCreds);
|
|
302271
302666
|
const hasLoginCredentials = Object.keys(browserSecretsForHint).length > 0;
|
|
302272
|
-
const aiClient = options?.modelOverride ? (0, ai_provider_js_1.
|
|
302667
|
+
const aiClient = options?.modelOverride ? (0, ai_provider_js_1.getRunnerClientForModel)(options.modelOverride) : (0, ai_provider_js_1.getRunnerAIClient)();
|
|
302273
302668
|
const isDeepMode = options?.mode === "deep";
|
|
302274
302669
|
const maxIterations = config?.maxIterationsOverride && config.maxIterationsOverride > 0 ? config.maxIterationsOverride : getMaxIterations(options?.mode ?? "full", options?.incremental);
|
|
302275
302670
|
const configKeepTail = isDeepMode ? Math.ceil(ctxConfig.keepTail * 1.5) : ctxConfig.keepTail;
|
|
@@ -304808,7 +305203,7 @@ var require_auth_capture_run = __commonJS({
|
|
|
304808
305203
|
var scrub_credentials_js_1 = require_scrub_credentials();
|
|
304809
305204
|
var credential_tools_js_1 = require_credential_tools();
|
|
304810
305205
|
var DEFAULT_MAX_LOGIN_CAPTURE_ITERATIONS = 12;
|
|
304811
|
-
function
|
|
305206
|
+
function readPositiveIntEnv2(env, name, fallback) {
|
|
304812
305207
|
const raw = env[name]?.trim();
|
|
304813
305208
|
if (!raw || !/^\d+$/.test(raw))
|
|
304814
305209
|
return fallback;
|
|
@@ -304816,7 +305211,7 @@ var require_auth_capture_run = __commonJS({
|
|
|
304816
305211
|
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
304817
305212
|
}
|
|
304818
305213
|
function getLoginCaptureMaxIterations(env = process.env) {
|
|
304819
|
-
return
|
|
305214
|
+
return readPositiveIntEnv2(env, "AUTH_CAPTURE_LOGIN_MAX_ITERATIONS", DEFAULT_MAX_LOGIN_CAPTURE_ITERATIONS);
|
|
304820
305215
|
}
|
|
304821
305216
|
var CAPTURE_RATE_LIMIT_MAX_RETRIES = 2;
|
|
304822
305217
|
var CAPTURE_RATE_LIMIT_MAX_DELAY_MS = 8e3;
|
|
@@ -305099,7 +305494,7 @@ ${keyLines || "- (none provided)"}
|
|
|
305099
305494
|
}
|
|
305100
305495
|
var DEFAULT_MAX_REGISTER_CAPTURE_ITERATIONS = 18;
|
|
305101
305496
|
function getRegisterCaptureMaxIterations(env = process.env) {
|
|
305102
|
-
return
|
|
305497
|
+
return readPositiveIntEnv2(env, "AUTH_CAPTURE_REGISTER_MAX_ITERATIONS", DEFAULT_MAX_REGISTER_CAPTURE_ITERATIONS);
|
|
305103
305498
|
}
|
|
305104
305499
|
function buildRegisterCaptureSystemPrompt(baseUrl, availableKeys, maxIterations = getRegisterCaptureMaxIterations()) {
|
|
305105
305500
|
const keyLines = availableKeys.map((k) => `- \`${k}\``).join("\n");
|
|
@@ -311329,6 +311724,7 @@ var require_mobile_exploration_state = __commonJS({
|
|
|
311329
311724
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
311330
311725
|
exports2.MobileExplorationState = void 0;
|
|
311331
311726
|
exports2.deriveScreenId = deriveScreenId;
|
|
311727
|
+
var mobile_observation_js_1 = require_mobile_observation();
|
|
311332
311728
|
var DEFAULT_STALE_THRESHOLD = 4;
|
|
311333
311729
|
function deriveScreenId(signal) {
|
|
311334
311730
|
const namespace = signal.activity ?? signal.bundleId;
|
|
@@ -311554,21 +311950,33 @@ var require_mobile_exploration_state = __commonJS({
|
|
|
311554
311950
|
* Build the planner evidence shape: one `CollectedPageData` per screen, with
|
|
311555
311951
|
* the screen-id carried in BOTH `route` (the on-the-wire positional field the
|
|
311556
311952
|
* server keys screenshots by) AND the explicit `screenId` field (per the
|
|
311557
|
-
* Phase-0 `CollectedPageData.screenId` contract). `elements`
|
|
311558
|
-
* the
|
|
311559
|
-
*
|
|
311560
|
-
*
|
|
311561
|
-
*
|
|
311953
|
+
* Phase-0 `CollectedPageData.screenId` contract). `elements` are mapped from
|
|
311954
|
+
* the screen's parsed actionable elements into the platform-neutral
|
|
311955
|
+
* `CollectedPageElement[]` shape the server persists as SiteElements — without
|
|
311956
|
+
* this, mobile screens land with zero elements (coverage always 0%, no element
|
|
311957
|
+
* intelligence). `triggersNavigation` is derived from transition-source
|
|
311958
|
+
* membership: an element is flagged when its ref drove a transition FROM this
|
|
311959
|
+
* screen. Insertion order matches first-seen order.
|
|
311562
311960
|
*/
|
|
311563
311961
|
buildInMemoryScreenMap() {
|
|
311564
311962
|
const pages = [];
|
|
311565
311963
|
const ordered = [...this.screens.values()].sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
311964
|
+
const navRefsByScreen = /* @__PURE__ */ new Map();
|
|
311965
|
+
for (const transition of this.transitions) {
|
|
311966
|
+
if (!transition.viaRef)
|
|
311967
|
+
continue;
|
|
311968
|
+
const set = navRefsByScreen.get(transition.fromScreenId) ?? /* @__PURE__ */ new Set();
|
|
311969
|
+
set.add(transition.viaRef);
|
|
311970
|
+
navRefsByScreen.set(transition.fromScreenId, set);
|
|
311971
|
+
}
|
|
311566
311972
|
for (const state2 of ordered) {
|
|
311567
311973
|
const page = {
|
|
311568
311974
|
route: state2.screenId,
|
|
311569
311975
|
screenId: state2.screenId,
|
|
311570
311976
|
pageType: state2.screenType ?? "UNKNOWN",
|
|
311571
|
-
elements:
|
|
311977
|
+
elements: (0, mobile_observation_js_1.buildCollectedElementsForScreen)(state2.elements, {
|
|
311978
|
+
navigationRefs: navRefsByScreen.get(state2.screenId)
|
|
311979
|
+
})
|
|
311572
311980
|
};
|
|
311573
311981
|
if (state2.screenName) {
|
|
311574
311982
|
page.title = state2.screenName;
|
|
@@ -311598,6 +312006,7 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
311598
312006
|
"../runner-core/dist/services/mobile/mobile-discovery-agent.js"(exports2) {
|
|
311599
312007
|
"use strict";
|
|
311600
312008
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
312009
|
+
exports2.getMobileExploreBudget = getMobileExploreBudget;
|
|
311601
312010
|
exports2.runMobileDiscoveryOnRunner = runMobileDiscoveryOnRunner2;
|
|
311602
312011
|
var mobile_explorer_js_1 = require_mobile_explorer();
|
|
311603
312012
|
var mobile_discovery_prompts_js_1 = require_mobile_discovery_prompts();
|
|
@@ -311611,8 +312020,37 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
311611
312020
|
const message = checkpointErrorMessage(err);
|
|
311612
312021
|
return /Discovery plan checkpoint rejected|Discovery run is .*not RUNNING|not RUNNING|cancelled/i.test(message);
|
|
311613
312022
|
}
|
|
311614
|
-
var
|
|
311615
|
-
var
|
|
312023
|
+
var MOBILE_EXPLORE_BUDGET_SURVEY_FIRST = 90;
|
|
312024
|
+
var MOBILE_EXPLORE_BUDGET_SURVEY_INCREMENTAL = 40;
|
|
312025
|
+
var MOBILE_EXPLORE_BUDGET_DEEP = 60;
|
|
312026
|
+
var MOBILE_EXPLORE_BUDGET_FULL = 90;
|
|
312027
|
+
function readPositiveIntEnv2(name, fallback, env = process.env) {
|
|
312028
|
+
const raw = env[name]?.trim();
|
|
312029
|
+
if (!raw || !/^\d+$/.test(raw))
|
|
312030
|
+
return fallback;
|
|
312031
|
+
const parsed = Number(raw);
|
|
312032
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
312033
|
+
}
|
|
312034
|
+
function getMobileExploreBudget(mode, incremental, env = process.env) {
|
|
312035
|
+
if (mode === "deep")
|
|
312036
|
+
return readPositiveIntEnv2("MOBILE_EXPLORE_MAX_ITERATIONS_DEEP", MOBILE_EXPLORE_BUDGET_DEEP, env);
|
|
312037
|
+
if (mode === "full")
|
|
312038
|
+
return readPositiveIntEnv2("MOBILE_EXPLORE_MAX_ITERATIONS_FULL", MOBILE_EXPLORE_BUDGET_FULL, env);
|
|
312039
|
+
if (incremental) {
|
|
312040
|
+
return readPositiveIntEnv2("MOBILE_EXPLORE_MAX_ITERATIONS_SURVEY_INCREMENTAL", MOBILE_EXPLORE_BUDGET_SURVEY_INCREMENTAL, env);
|
|
312041
|
+
}
|
|
312042
|
+
return readPositiveIntEnv2("MOBILE_EXPLORE_MAX_ITERATIONS_SURVEY", MOBILE_EXPLORE_BUDGET_SURVEY_FIRST, env);
|
|
312043
|
+
}
|
|
312044
|
+
function compactExistingScreens(siteMap) {
|
|
312045
|
+
if (!siteMap || !Array.isArray(siteMap.pages) || siteMap.pages.length === 0)
|
|
312046
|
+
return void 0;
|
|
312047
|
+
const screens = siteMap.pages.filter((p) => typeof p.route === "string" && p.route.trim().length > 0).map((p) => ({
|
|
312048
|
+
screenId: p.route,
|
|
312049
|
+
...p.title ? { name: p.title } : {},
|
|
312050
|
+
...p.pageType ? { screenType: p.pageType } : {}
|
|
312051
|
+
}));
|
|
312052
|
+
return screens.length > 0 ? screens : void 0;
|
|
312053
|
+
}
|
|
311616
312054
|
function platformForMedium(medium) {
|
|
311617
312055
|
return medium === "IOS_NATIVE" ? "IOS" : "ANDROID";
|
|
311618
312056
|
}
|
|
@@ -311631,6 +312069,9 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
311631
312069
|
// Native screenshots are multi-megabyte PNGs. The live mirror streams them
|
|
311632
312070
|
// through heartbeat telemetry; discovery checkpoints keep the screen graph
|
|
311633
312071
|
// and transitions so large Android surveys cannot exceed server body limits.
|
|
312072
|
+
// D-D6 — pageScreenshotKeys is a small screen-id → storage-key map (NOT base64),
|
|
312073
|
+
// so it DOES ride the checkpoint: the server links each key to SitePage.screenshotKey.
|
|
312074
|
+
...explore.pageScreenshotKeys && Object.keys(explore.pageScreenshotKeys).length > 0 ? { pageScreenshotKeys: explore.pageScreenshotKeys } : {},
|
|
311634
312075
|
exploreStats: explore.exploreStats,
|
|
311635
312076
|
healthObservations: explore.healthObservations
|
|
311636
312077
|
};
|
|
@@ -311645,6 +312086,7 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
311645
312086
|
const onLog = callbacks?.onLog;
|
|
311646
312087
|
const onAICall = callbacks?.onAICall;
|
|
311647
312088
|
const onScreenshot = callbacks?.onScreenshot;
|
|
312089
|
+
const onScreenshotUpload = callbacks?.onScreenshotUpload;
|
|
311648
312090
|
const runExplore = callbacks?.runExplore ?? mobile_explorer_js_1.runMobileExplorePhase;
|
|
311649
312091
|
const runPlan = callbacks?.runPlan ?? mobile_discovery_prompts_js_1.runMobileAIPlanner;
|
|
311650
312092
|
const runGenerate = callbacks?.runGenerate ?? mobile_generator_js_1.runMobileGeneratePhase;
|
|
@@ -311671,15 +312113,77 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
311671
312113
|
elementsFound: 0,
|
|
311672
312114
|
transitionsFound: 0
|
|
311673
312115
|
};
|
|
312116
|
+
const resumeSavedScenarios = ctx.resume?.savedPlan?.scenarios ?? [];
|
|
312117
|
+
const isPlanReuseResume = mode !== "survey" && resumeSavedScenarios.length > 0;
|
|
311674
312118
|
try {
|
|
312119
|
+
if (isPlanReuseResume) {
|
|
312120
|
+
const alreadyGenerated = ctx.resume?.alreadyGeneratedScenarioNames ?? [];
|
|
312121
|
+
log2(`
|
|
312122
|
+
\u2500\u2500 Resume (plan-reuse): ${resumeSavedScenarios.length} saved scenario(s); ${alreadyGenerated.length} already generated. Skipping explore + plan; generating only the remainder. \u2500\u2500`);
|
|
312123
|
+
const scenarios2 = resumeSavedScenarios;
|
|
312124
|
+
const plans2 = toPlans(scenarios2);
|
|
312125
|
+
if (callbacks?.onPlanReady) {
|
|
312126
|
+
try {
|
|
312127
|
+
await callbacks.onPlanReady({ scenarios: scenarios2, featureGroups: void 0, fallbackUsed: false });
|
|
312128
|
+
log2("Plan checkpoint: reused scenario plan re-POSTed to server");
|
|
312129
|
+
} catch (err) {
|
|
312130
|
+
const message = checkpointErrorMessage(err);
|
|
312131
|
+
if (isTerminalDiscoveryCheckpointError(err)) {
|
|
312132
|
+
log2(`Plan checkpoint rejected for inactive discovery run: ${message}`);
|
|
312133
|
+
throw err;
|
|
312134
|
+
}
|
|
312135
|
+
log2(`Plan checkpoint POST failed (non-fatal): ${message}`);
|
|
312136
|
+
}
|
|
312137
|
+
}
|
|
312138
|
+
let paused = false;
|
|
312139
|
+
let pauseReason;
|
|
312140
|
+
let remainingScenarios;
|
|
312141
|
+
const tests2 = await runGenerate({
|
|
312142
|
+
scenarios: scenarios2,
|
|
312143
|
+
driver,
|
|
312144
|
+
ctx,
|
|
312145
|
+
onLog: (line) => log2(line),
|
|
312146
|
+
onAICall,
|
|
312147
|
+
onScreenshot,
|
|
312148
|
+
onTestGenerated: (test) => callbacks?.onTestGenerated?.(test, { phase: "verified" }),
|
|
312149
|
+
skipScenarioNames: alreadyGenerated,
|
|
312150
|
+
onInfraStop: (info) => {
|
|
312151
|
+
paused = true;
|
|
312152
|
+
pauseReason = info.reason;
|
|
312153
|
+
remainingScenarios = info.remainingScenarioNames;
|
|
312154
|
+
}
|
|
312155
|
+
});
|
|
312156
|
+
const duration2 = (Date.now() - startTime) / 1e3;
|
|
312157
|
+
const summary2 = `Mobile resume complete: generated ${tests2.length} test(s) from ${scenarios2.length} saved scenario(s) in ${duration2.toFixed(1)}s${paused ? " \u2014 PAUSED again on AI provider overload" : ""}`;
|
|
312158
|
+
log2(`
|
|
312159
|
+
=== Mobile Discovery Resume Complete (${duration2.toFixed(1)}s) ===`);
|
|
312160
|
+
log2(summary2);
|
|
312161
|
+
return {
|
|
312162
|
+
collectedPages: [],
|
|
312163
|
+
collectedTransitions: [],
|
|
312164
|
+
tests: tests2,
|
|
312165
|
+
plans: plans2,
|
|
312166
|
+
scenarios: scenarios2.length > 0 ? scenarios2 : void 0,
|
|
312167
|
+
logs: logs2.join("\n"),
|
|
312168
|
+
screenshots,
|
|
312169
|
+
summary: summary2,
|
|
312170
|
+
exploreStats: { pagesDiscovered: 0, pagesVisited: 0, elementsFound: 0, transitionsFound: 0 },
|
|
312171
|
+
duration: duration2,
|
|
312172
|
+
mode,
|
|
312173
|
+
...paused ? { paused: true, pauseReason, remainingScenarios } : {}
|
|
312174
|
+
// apiCalls intentionally UNSET — CHUNK K (runner.ts) merges them.
|
|
312175
|
+
};
|
|
312176
|
+
}
|
|
311675
312177
|
log2("\n\u2500\u2500 Phase 1: EXPLORE \u2500\u2500");
|
|
311676
312178
|
const state2 = new mobile_exploration_state_js_1.MobileExplorationState();
|
|
311677
|
-
const maxIterations = mode
|
|
312179
|
+
const maxIterations = getMobileExploreBudget(mode, ctx.incremental);
|
|
312180
|
+
const existingScreens = compactExistingScreens(ctx.existingSiteMap);
|
|
311678
312181
|
const explore = await runExplore({
|
|
311679
312182
|
ctx,
|
|
311680
312183
|
driver,
|
|
311681
312184
|
state: state2,
|
|
311682
312185
|
config: { maxIterations },
|
|
312186
|
+
existingScreens,
|
|
311683
312187
|
onLog: (line) => log2(line),
|
|
311684
312188
|
onAICall,
|
|
311685
312189
|
onScreenshot: (base64) => {
|
|
@@ -311689,7 +312193,8 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
311689
312193
|
onScreenshot?.(base64);
|
|
311690
312194
|
} catch {
|
|
311691
312195
|
}
|
|
311692
|
-
}
|
|
312196
|
+
},
|
|
312197
|
+
onScreenshotUpload
|
|
311693
312198
|
});
|
|
311694
312199
|
const collectedTransitions = toCollectedTransitions(explore);
|
|
311695
312200
|
partialCollectedPages = explore.collectedPages;
|
|
@@ -311790,6 +312295,9 @@ Plan: ${planResult.scenarios.length} scenarios, ${planResult.featureGroups.lengt
|
|
|
311790
312295
|
};
|
|
311791
312296
|
}
|
|
311792
312297
|
log2("\n\u2500\u2500 Phase 3: GENERATE \u2500\u2500");
|
|
312298
|
+
let generatePaused = false;
|
|
312299
|
+
let generatePauseReason;
|
|
312300
|
+
let generateRemainingScenarios;
|
|
311793
312301
|
const tests = await runGenerate({
|
|
311794
312302
|
scenarios,
|
|
311795
312303
|
driver,
|
|
@@ -311797,7 +312305,15 @@ Plan: ${planResult.scenarios.length} scenarios, ${planResult.featureGroups.lengt
|
|
|
311797
312305
|
onLog: (line) => log2(line),
|
|
311798
312306
|
onAICall,
|
|
311799
312307
|
onScreenshot,
|
|
311800
|
-
onTestGenerated: (test) => callbacks?.onTestGenerated?.(test, { phase: "verified" })
|
|
312308
|
+
onTestGenerated: (test) => callbacks?.onTestGenerated?.(test, { phase: "verified" }),
|
|
312309
|
+
// Legacy resume (no saved plan): still skip scenarios already generated by the
|
|
312310
|
+
// paused run so the AI isn't re-invoked for them. Undefined on a fresh run.
|
|
312311
|
+
skipScenarioNames: ctx.resume?.alreadyGeneratedScenarioNames,
|
|
312312
|
+
onInfraStop: (info) => {
|
|
312313
|
+
generatePaused = true;
|
|
312314
|
+
generatePauseReason = info.reason;
|
|
312315
|
+
generateRemainingScenarios = info.remainingScenarioNames;
|
|
312316
|
+
}
|
|
311801
312317
|
});
|
|
311802
312318
|
const generateElapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
311803
312319
|
log2(`
|
|
@@ -311813,7 +312329,7 @@ Generation complete: ${tests.length} tests generated (${generateElapsed}s elapse
|
|
|
311813
312329
|
};
|
|
311814
312330
|
}
|
|
311815
312331
|
const duration = (Date.now() - startTime) / 1e3;
|
|
311816
|
-
const summary = `Mobile discovery complete: explored ${explore.exploreStats.pagesDiscovered} screens, planned ${scenarios.length} scenarios, generated ${tests.length} tests in ${duration.toFixed(1)}s`;
|
|
312332
|
+
const summary = `Mobile discovery complete: explored ${explore.exploreStats.pagesDiscovered} screens, planned ${scenarios.length} scenarios, generated ${tests.length} tests in ${duration.toFixed(1)}s${generatePaused ? " \u2014 PAUSED on AI provider overload (resumable)" : ""}`;
|
|
311817
312333
|
log2(`
|
|
311818
312334
|
=== Mobile Discovery Pipeline Complete (${duration.toFixed(1)}s) ===`);
|
|
311819
312335
|
log2(summary);
|
|
@@ -311838,7 +312354,11 @@ Generation complete: ${tests.length} tests generated (${generateElapsed}s elapse
|
|
|
311838
312354
|
summary,
|
|
311839
312355
|
exploreStats: explore.exploreStats,
|
|
311840
312356
|
duration,
|
|
311841
|
-
mode
|
|
312357
|
+
mode,
|
|
312358
|
+
// D-D4 — a circuit-breaker trip PAUSES the run (resumable) instead of
|
|
312359
|
+
// finalizing COMPLETED/FAILED. The server's isDiscoveryPaused reads these
|
|
312360
|
+
// (forwarded by runner.ts buildFinalBody, byte-identical to the web path).
|
|
312361
|
+
...generatePaused ? { paused: true, pauseReason: generatePauseReason, remainingScenarios: generateRemainingScenarios } : {}
|
|
311842
312362
|
// apiCalls intentionally UNSET — CHUNK K (runner.ts) merges them.
|
|
311843
312363
|
};
|
|
311844
312364
|
} catch (fatalErr) {
|
|
@@ -311884,6 +312404,7 @@ var require_discover_agent = __commonJS({
|
|
|
311884
312404
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
311885
312405
|
exports2.runMobileDiscoveryOnRunner = exports2.buildInMemorySiteMap = void 0;
|
|
311886
312406
|
exports2.runCanvasPreflight = runCanvasPreflight;
|
|
312407
|
+
exports2.getFatalExploreError = getFatalExploreError;
|
|
311887
312408
|
exports2.runSiteDiscoveryOnRunner = runSiteDiscoveryOnRunner2;
|
|
311888
312409
|
exports2.runFeatureDiscoveryOnRunner = runFeatureDiscoveryOnRunner2;
|
|
311889
312410
|
var browser_config_js_1 = require_browser_config();
|
|
@@ -312066,7 +312587,7 @@ Respond with ONLY valid JSON:
|
|
|
312066
312587
|
"keyFindings": "What was discovered - forms, validations, behaviors. Mark inferred items with '(inferred, not tested)'.",
|
|
312067
312588
|
"testingNotes": "Gotchas, validation behaviors, error messages. List visible-but-untested routes as candidates for future exploration."
|
|
312068
312589
|
}`;
|
|
312069
|
-
const client = modelOverride ? (0, ai_provider_js_1.
|
|
312590
|
+
const client = modelOverride ? (0, ai_provider_js_1.getRunnerClientForModel)(modelOverride) : (0, ai_provider_js_1.getRunnerAIClient)();
|
|
312070
312591
|
const agentModel = modelOverride ? (0, ai_provider_js_1.resolveModelForTask)(modelOverride, "agent") : (0, ai_provider_js_1.getModel)("agent");
|
|
312071
312592
|
const callStart = Date.now();
|
|
312072
312593
|
const response = await (0, ai_retry_js_1.withAIRetry)(() => (0, ai_queue_js_1.queuedAICall)(() => client.chat.completions.create({
|
|
@@ -312197,6 +312718,10 @@ Respond with ONLY valid JSON:
|
|
|
312197
312718
|
});
|
|
312198
312719
|
return timed.catch(() => null);
|
|
312199
312720
|
}
|
|
312721
|
+
function getFatalExploreError(exploreResult) {
|
|
312722
|
+
const prefix = "Fatal error:";
|
|
312723
|
+
return exploreResult.summary.startsWith(prefix) ? exploreResult.summary.slice(prefix.length).trim() || exploreResult.summary : void 0;
|
|
312724
|
+
}
|
|
312200
312725
|
async function runSiteDiscoveryOnRunner2(ctx, onLog, onAICall, sharedBrowser, onScreenshot, callbackOptions) {
|
|
312201
312726
|
const logs2 = [];
|
|
312202
312727
|
const screenshots = [];
|
|
@@ -312381,6 +312906,7 @@ Explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elemen
|
|
|
312381
312906
|
if (loginRunP)
|
|
312382
312907
|
await loginRunP;
|
|
312383
312908
|
const duration2 = (Date.now() - startTime) / 1e3;
|
|
312909
|
+
const fatalExploreError = getFatalExploreError(exploreResult);
|
|
312384
312910
|
return {
|
|
312385
312911
|
collectedPages: exploreResult.collectedPages,
|
|
312386
312912
|
collectedTransitions: exploreResult.collectedTransitions,
|
|
@@ -312391,6 +312917,7 @@ Explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elemen
|
|
|
312391
312917
|
pageScreenshots: onScreenshot ? void 0 : Object.fromEntries(mergedScreenshots),
|
|
312392
312918
|
pageScreenshotKeys: onScreenshot ? Object.fromEntries(screenshotKeys) : void 0,
|
|
312393
312919
|
summary: exploreResult.summary || "Exploration completed but no page data captured.",
|
|
312920
|
+
error: fatalExploreError,
|
|
312394
312921
|
exploreStats: {
|
|
312395
312922
|
pagesDiscovered: exploreResult.pagesDiscovered,
|
|
312396
312923
|
pagesVisited: exploreResult.pagesVisited,
|
|
@@ -312932,6 +313459,7 @@ Feature explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResul
|
|
|
312932
313459
|
discoveryApiCalls = earlyClose.apiCalls;
|
|
312933
313460
|
postCloseHealthSummary = earlyClose.healthSummary;
|
|
312934
313461
|
const duration2 = (Date.now() - startTime) / 1e3;
|
|
313462
|
+
const fatalExploreError = getFatalExploreError(exploreResult);
|
|
312935
313463
|
return {
|
|
312936
313464
|
collectedPages: exploreResult.collectedPages,
|
|
312937
313465
|
collectedTransitions: exploreResult.collectedTransitions,
|
|
@@ -312942,7 +313470,8 @@ Feature explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResul
|
|
|
312942
313470
|
healthObservations: earlyClose.healthSummary,
|
|
312943
313471
|
pageScreenshots: onScreenshot ? void 0 : Object.fromEntries(mergedScreenshots),
|
|
312944
313472
|
pageScreenshotKeys: onScreenshot ? Object.fromEntries(screenshotKeys) : void 0,
|
|
312945
|
-
summary: "Feature exploration completed but no page data captured.",
|
|
313473
|
+
summary: fatalExploreError ? exploreResult.summary : "Feature exploration completed but no page data captured.",
|
|
313474
|
+
error: fatalExploreError,
|
|
312946
313475
|
exploreStats: {
|
|
312947
313476
|
pagesDiscovered: exploreResult.pagesDiscovered,
|
|
312948
313477
|
pagesVisited: exploreResult.pagesVisited,
|
|
@@ -326331,7 +326860,7 @@ var require_package4 = __commonJS({
|
|
|
326331
326860
|
"package.json"(exports2, module2) {
|
|
326332
326861
|
module2.exports = {
|
|
326333
326862
|
name: "@validate.qa/runner",
|
|
326334
|
-
version: "1.0.
|
|
326863
|
+
version: "1.0.14",
|
|
326335
326864
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
326336
326865
|
bin: {
|
|
326337
326866
|
"validate-runner": "dist/cli.js"
|
|
@@ -326339,7 +326868,7 @@ var require_package4 = __commonJS({
|
|
|
326339
326868
|
scripts: {
|
|
326340
326869
|
build: "tsup",
|
|
326341
326870
|
dev: "tsx src/cli.ts",
|
|
326342
|
-
"test:mobile": "node --import tsx --test src/runner.mobile-ai-env.test.ts src/services/mobile/mobile-network-capture.smoke.test.ts",
|
|
326871
|
+
"test:mobile": "node --import tsx --test src/runner.mobile-ai-env.test.ts src/services/mobile/mobile-network-capture.smoke.test.ts src/services/mobile/mobile-host-proxy.test.ts",
|
|
326343
326872
|
prepublishOnly: "npm run build"
|
|
326344
326873
|
},
|
|
326345
326874
|
files: [
|
|
@@ -326566,8 +327095,10 @@ async function startAppiumServer(options) {
|
|
|
326566
327095
|
return serverState;
|
|
326567
327096
|
}
|
|
326568
327097
|
const port = options?.port ?? APPIUM_PORT;
|
|
326569
|
-
const
|
|
326570
|
-
|
|
327098
|
+
const args = ["--port", String(port)];
|
|
327099
|
+
if (options?.drivers && options.drivers.length > 0) {
|
|
327100
|
+
args.push("--use-drivers", options.drivers.join(","));
|
|
327101
|
+
}
|
|
326571
327102
|
intentionalStop = false;
|
|
326572
327103
|
lastStartOptions = { ...lastStartOptions, ...options };
|
|
326573
327104
|
if (appiumGaveUp) {
|
|
@@ -326577,11 +327108,12 @@ async function startAppiumServer(options) {
|
|
|
326577
327108
|
killPort(port);
|
|
326578
327109
|
return new Promise((resolve, reject) => {
|
|
326579
327110
|
try {
|
|
326580
|
-
|
|
327111
|
+
const child = (0, import_child_process.spawn)("appium", args, {
|
|
326581
327112
|
stdio: ["pipe", "pipe", "pipe"],
|
|
326582
327113
|
env: { ...process.env },
|
|
326583
327114
|
detached: false
|
|
326584
327115
|
});
|
|
327116
|
+
appiumProcess = child;
|
|
326585
327117
|
serverState.pid = appiumProcess.pid ?? null;
|
|
326586
327118
|
serverState.port = port;
|
|
326587
327119
|
appiumProcess.stdout?.on("data", (data) => {
|
|
@@ -326598,7 +327130,11 @@ async function startAppiumServer(options) {
|
|
|
326598
327130
|
options?.onLog?.(line);
|
|
326599
327131
|
}
|
|
326600
327132
|
});
|
|
326601
|
-
|
|
327133
|
+
child.on("exit", (code, signal) => {
|
|
327134
|
+
if (appiumProcess !== child) {
|
|
327135
|
+
appendLog(`[appium] Ignoring exit (code=${code}, signal=${signal}) from a superseded process`);
|
|
327136
|
+
return;
|
|
327137
|
+
}
|
|
326602
327138
|
appendLog(`[appium] Process exited (code=${code}, signal=${signal})`);
|
|
326603
327139
|
const currentPort = serverState.port;
|
|
326604
327140
|
const currentRestartCount = serverState.restartCount;
|
|
@@ -326637,7 +327173,11 @@ async function startAppiumServer(options) {
|
|
|
326637
327173
|
startupAbort.abort();
|
|
326638
327174
|
reject(err);
|
|
326639
327175
|
};
|
|
326640
|
-
|
|
327176
|
+
child.on("error", (err) => {
|
|
327177
|
+
if (appiumProcess !== child) {
|
|
327178
|
+
appendLog(`[appium] Ignoring error from a superseded process: ${err.message}`);
|
|
327179
|
+
return;
|
|
327180
|
+
}
|
|
326641
327181
|
appendLog(`[appium] Process error: ${err.message}`);
|
|
326642
327182
|
resetServerState(port);
|
|
326643
327183
|
settleReject(new Error(`Failed to start Appium: ${err.message}. Is appium installed? Run: npm i -g appium`));
|
|
@@ -326756,6 +327296,7 @@ function runCommand(command, args, timeout = 1e4) {
|
|
|
326756
327296
|
}
|
|
326757
327297
|
var realIOSProfilerNegativeUntil = 0;
|
|
326758
327298
|
var REAL_IOS_PROFILER_TTL_MS = 6e4;
|
|
327299
|
+
var REAL_IOS_NO_UDID_HINT = "no paired UDID \u2014 install libimobiledevice (brew install libimobiledevice) and trust this computer on the device";
|
|
326759
327300
|
function discoverIOSSimulators() {
|
|
326760
327301
|
if ((0, import_os2.platform)() !== "darwin") return [];
|
|
326761
327302
|
const json = runCommand("xcrun", ["simctl", "list", "devices", "--json"]);
|
|
@@ -326765,7 +327306,8 @@ function discoverIOSSimulators() {
|
|
|
326765
327306
|
const devices = [];
|
|
326766
327307
|
for (const [runtime, runtimeDevices] of Object.entries(parsed.devices)) {
|
|
326767
327308
|
const versionMatch = runtime.match(/iOS[- ](\d+(?:[-.]\d+)*)/i);
|
|
326768
|
-
|
|
327309
|
+
if (!versionMatch) continue;
|
|
327310
|
+
const platformVersion = versionMatch[1].replace(/-/g, ".");
|
|
326769
327311
|
for (const device of runtimeDevices) {
|
|
326770
327312
|
const state2 = device.state === "Booted" ? "BOOTED" : device.state === "Shutdown" ? "SHUTDOWN" : "LOADING";
|
|
326771
327313
|
if (state2 === "BOOTED") {
|
|
@@ -326838,11 +327380,11 @@ function discoverRealIOSDevices() {
|
|
|
326838
327380
|
const version = runCommand("ideviceinfo", ["-u", serial, "-k", "ProductVersion"]) || "unknown";
|
|
326839
327381
|
devices.push({
|
|
326840
327382
|
id: serial,
|
|
326841
|
-
name
|
|
327383
|
+
name: `${name} (${REAL_IOS_NO_UDID_HINT})`,
|
|
326842
327384
|
platform: "IOS",
|
|
326843
327385
|
platformVersion: version,
|
|
326844
327386
|
state: "BOOTED",
|
|
326845
|
-
isAvailable:
|
|
327387
|
+
isAvailable: false,
|
|
326846
327388
|
isPhysical: true
|
|
326847
327389
|
});
|
|
326848
327390
|
}
|
|
@@ -326858,11 +327400,11 @@ function discoverRealIOSDevices() {
|
|
|
326858
327400
|
if (hasIPhone || hasIPad) {
|
|
326859
327401
|
devices.push({
|
|
326860
327402
|
id: "usb-ios-device",
|
|
326861
|
-
name: hasIPhone ? "iPhone (USB)" : "iPad (USB)"
|
|
327403
|
+
name: `${hasIPhone ? "iPhone (USB)" : "iPad (USB)"} (${REAL_IOS_NO_UDID_HINT})`,
|
|
326862
327404
|
platform: "IOS",
|
|
326863
327405
|
platformVersion: "unknown",
|
|
326864
327406
|
state: "BOOTED",
|
|
326865
|
-
isAvailable:
|
|
327407
|
+
isAvailable: false,
|
|
326866
327408
|
isPhysical: true
|
|
326867
327409
|
});
|
|
326868
327410
|
}
|
|
@@ -326940,7 +327482,9 @@ function discoverAndroidDevices() {
|
|
|
326940
327482
|
const adbDevices = parseAdbDevices();
|
|
326941
327483
|
return adbDevices.filter((d) => d.type === "device" || d.type === "unauthorized").map((d) => {
|
|
326942
327484
|
const isEmulator = d.serial.startsWith("emulator-");
|
|
326943
|
-
const
|
|
327485
|
+
const authorized = d.type === "device";
|
|
327486
|
+
const bootCompleted = authorized && getAndroidProp(d.serial, "sys.boot_completed") === "1";
|
|
327487
|
+
const isAvailable = authorized && bootCompleted;
|
|
326944
327488
|
const name = d.model || d.device || (isEmulator ? "Android Emulator" : "Android Device");
|
|
326945
327489
|
const platformVersion = isAvailable ? getAndroidProp(d.serial, "ro.build.version.release") || "unknown" : "unknown";
|
|
326946
327490
|
return {
|
|
@@ -326948,7 +327492,10 @@ function discoverAndroidDevices() {
|
|
|
326948
327492
|
name,
|
|
326949
327493
|
platform: "ANDROID",
|
|
326950
327494
|
platformVersion,
|
|
326951
|
-
|
|
327495
|
+
// LOADING = authorized but still booting; an unauthorized device stays
|
|
327496
|
+
// BOOTED (isAvailable false) so the dashboard shows it with an
|
|
327497
|
+
// "accept USB debugging" hint rather than hiding it.
|
|
327498
|
+
state: authorized && !bootCompleted ? "LOADING" : "BOOTED",
|
|
326952
327499
|
isAvailable,
|
|
326953
327500
|
// Mark if it's a real device vs emulator
|
|
326954
327501
|
...isEmulator ? {} : { isPhysical: true }
|
|
@@ -326994,15 +327541,25 @@ function discoverAllDevices(options) {
|
|
|
326994
327541
|
}
|
|
326995
327542
|
return [...iosDevices, ...androidDevices];
|
|
326996
327543
|
}
|
|
327544
|
+
var DEVICE_SNAPSHOT_TTL_MS = 8e3;
|
|
327545
|
+
var cachedDeviceSnapshot = null;
|
|
327546
|
+
function getCachedDeviceSnapshot() {
|
|
327547
|
+
const now = Date.now();
|
|
327548
|
+
if (cachedDeviceSnapshot && now - cachedDeviceSnapshot.at < DEVICE_SNAPSHOT_TTL_MS) {
|
|
327549
|
+
return cachedDeviceSnapshot.devices;
|
|
327550
|
+
}
|
|
327551
|
+
const devices = discoverAllDevices();
|
|
327552
|
+
cachedDeviceSnapshot = { at: now, devices };
|
|
327553
|
+
return devices;
|
|
327554
|
+
}
|
|
326997
327555
|
function getRunnerCapabilities() {
|
|
326998
|
-
const
|
|
326999
|
-
const androidDevices = discoverAndroidDevices();
|
|
327556
|
+
const devices = getCachedDeviceSnapshot();
|
|
327000
327557
|
const isReadyDevice = (device) => device.isAvailable && device.state === "BOOTED";
|
|
327001
327558
|
return {
|
|
327002
327559
|
web: true,
|
|
327003
327560
|
// Always capable of web testing via Playwright
|
|
327004
|
-
ios:
|
|
327005
|
-
android:
|
|
327561
|
+
ios: devices.some((d) => d.platform === "IOS" && isReadyDevice(d)),
|
|
327562
|
+
android: devices.some((d) => d.platform === "ANDROID" && isReadyDevice(d))
|
|
327006
327563
|
};
|
|
327007
327564
|
}
|
|
327008
327565
|
|
|
@@ -327022,6 +327579,8 @@ var DEVICE_CMD_TIMEOUT_MS = 15e3;
|
|
|
327022
327579
|
var CA_KEY_BITS = 2048;
|
|
327023
327580
|
var ANDROID_EMULATOR_HOST_LOOPBACK = "10.0.2.2";
|
|
327024
327581
|
var PENDING_CLEANUP_DIR = (0, import_node_path.join)((0, import_node_os.tmpdir)(), "validateqa-mobile-capture-pending");
|
|
327582
|
+
var IOS_SIM_HOST_PROXY_ENV = "VALIDATEQA_MOBILE_IOS_SIM_HOST_PROXY";
|
|
327583
|
+
var STALE_MARKER_RECONCILE_MS = 6 * 60 * 60 * 1e3;
|
|
327025
327584
|
function envFlag(name) {
|
|
327026
327585
|
const value = (process.env[name] ?? "").trim().toLowerCase();
|
|
327027
327586
|
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
@@ -327138,6 +327697,9 @@ var MitmProxy = class {
|
|
|
327138
327697
|
await server3.forAnyRequest().thenPassThrough({ ignoreHostHttpsErrors: true });
|
|
327139
327698
|
await server3.on("request", (req) => this.onRequest(req));
|
|
327140
327699
|
await server3.on("response", (res) => this.onResponse(res));
|
|
327700
|
+
await server3.on("abort", (req) => {
|
|
327701
|
+
this.inFlight.delete(req.id);
|
|
327702
|
+
});
|
|
327141
327703
|
await server3.on("tls-client-error", (failure) => this.onTlsClientError(failure));
|
|
327142
327704
|
await server3.on("tls-passthrough-opened", (event) => this.onTlsPassthroughOpened(event));
|
|
327143
327705
|
await (preferredPort > 0 ? server3.start(preferredPort) : server3.start());
|
|
@@ -327434,6 +327996,125 @@ async function iosSimRemoveCa(deviceId, onLog) {
|
|
|
327434
327996
|
function errMsg(err) {
|
|
327435
327997
|
return err instanceof Error ? err.message : String(err);
|
|
327436
327998
|
}
|
|
327999
|
+
function parsePrimaryNetworkService(stdout) {
|
|
328000
|
+
for (const line of stdout.split("\n")) {
|
|
328001
|
+
const m = line.match(/^\(\d+\)\s+(.+)$/);
|
|
328002
|
+
if (m) return m[1].trim();
|
|
328003
|
+
}
|
|
328004
|
+
return null;
|
|
328005
|
+
}
|
|
328006
|
+
function parseWebProxyState(stdout) {
|
|
328007
|
+
const field = (label) => {
|
|
328008
|
+
const m = stdout.match(new RegExp(`^${label}:[ \\t]*(.*)$`, "im"));
|
|
328009
|
+
return m ? m[1].trim() : "";
|
|
328010
|
+
};
|
|
328011
|
+
return {
|
|
328012
|
+
enabled: field("Enabled").toLowerCase() === "yes",
|
|
328013
|
+
server: field("Server"),
|
|
328014
|
+
port: field("Port")
|
|
328015
|
+
};
|
|
328016
|
+
}
|
|
328017
|
+
function buildHostProxySetCommands(service, host, port) {
|
|
328018
|
+
const portStr = String(port);
|
|
328019
|
+
return [
|
|
328020
|
+
["-setwebproxy", service, host, portStr],
|
|
328021
|
+
["-setsecurewebproxy", service, host, portStr]
|
|
328022
|
+
];
|
|
328023
|
+
}
|
|
328024
|
+
function buildProxyRestoreArgs(setCmd, stateCmd, service, prior) {
|
|
328025
|
+
if (prior.server) {
|
|
328026
|
+
return [
|
|
328027
|
+
[setCmd, service, prior.server, prior.port || "0"],
|
|
328028
|
+
[stateCmd, service, prior.enabled ? "on" : "off"]
|
|
328029
|
+
];
|
|
328030
|
+
}
|
|
328031
|
+
return [[stateCmd, service, "off"]];
|
|
328032
|
+
}
|
|
328033
|
+
function buildHostProxyRestoreCommands(service, prior) {
|
|
328034
|
+
return [
|
|
328035
|
+
...buildProxyRestoreArgs("-setwebproxy", "-setwebproxystate", service, prior.web),
|
|
328036
|
+
...buildProxyRestoreArgs("-setsecurewebproxy", "-setsecurewebproxystate", service, prior.secure)
|
|
328037
|
+
];
|
|
328038
|
+
}
|
|
328039
|
+
function hostProxyPriorFromMarker(marker) {
|
|
328040
|
+
return {
|
|
328041
|
+
web: {
|
|
328042
|
+
enabled: Boolean(marker.hostWebProxyWasEnabled),
|
|
328043
|
+
server: marker.hostWebProxyPriorServer ?? "",
|
|
328044
|
+
port: marker.hostWebProxyPriorPort ?? ""
|
|
328045
|
+
},
|
|
328046
|
+
secure: {
|
|
328047
|
+
enabled: Boolean(marker.hostSecureProxyWasEnabled),
|
|
328048
|
+
server: marker.hostSecureProxyPriorServer ?? "",
|
|
328049
|
+
port: marker.hostSecureProxyPriorPort ?? ""
|
|
328050
|
+
}
|
|
328051
|
+
};
|
|
328052
|
+
}
|
|
328053
|
+
function hostProxyControlSupported(platform3, isPhysical, platformName = process.platform, optedIn = envFlag(IOS_SIM_HOST_PROXY_ENV)) {
|
|
328054
|
+
if (platform3 !== "IOS") return { supported: false };
|
|
328055
|
+
if (isPhysical) {
|
|
328056
|
+
return { supported: false, reason: "physical iOS device requires a manual Wi-Fi HTTP proxy; host proxy not applied" };
|
|
328057
|
+
}
|
|
328058
|
+
if (platformName !== "darwin") {
|
|
328059
|
+
return { supported: false, reason: `iOS simulator host proxy is only controllable on macOS (host is ${platformName})` };
|
|
328060
|
+
}
|
|
328061
|
+
if (!optedIn) {
|
|
328062
|
+
return {
|
|
328063
|
+
supported: false,
|
|
328064
|
+
reason: `iOS simulator HTTPS body capture is disabled: it requires repointing the macOS SYSTEM-WIDE HTTP/HTTPS proxy, which would break host HTTPS (Safari/Chrome/Electron) and capture the host's own traffic for this run. Not applied without consent \u2014 set ${IOS_SIM_HOST_PROXY_ENV}=1 to enable it.`
|
|
328065
|
+
};
|
|
328066
|
+
}
|
|
328067
|
+
return { supported: true };
|
|
328068
|
+
}
|
|
328069
|
+
async function iosSimConfigureHostProxy(host, port, onLog) {
|
|
328070
|
+
let service;
|
|
328071
|
+
let prior;
|
|
328072
|
+
try {
|
|
328073
|
+
const { stdout: order } = await execFileAsync("networksetup", ["-listnetworkserviceorder"], {
|
|
328074
|
+
timeout: DEVICE_CMD_TIMEOUT_MS
|
|
328075
|
+
});
|
|
328076
|
+
const svc = parsePrimaryNetworkService(order);
|
|
328077
|
+
if (!svc) {
|
|
328078
|
+
return { configured: false, reason: "iOS simulator host proxy could not be configured: no active macOS network service found" };
|
|
328079
|
+
}
|
|
328080
|
+
service = svc;
|
|
328081
|
+
const [{ stdout: webOut }, { stdout: secureOut }] = await Promise.all([
|
|
328082
|
+
execFileAsync("networksetup", ["-getwebproxy", service], { timeout: DEVICE_CMD_TIMEOUT_MS }),
|
|
328083
|
+
execFileAsync("networksetup", ["-getsecurewebproxy", service], { timeout: DEVICE_CMD_TIMEOUT_MS })
|
|
328084
|
+
]);
|
|
328085
|
+
prior = { web: parseWebProxyState(webOut), secure: parseWebProxyState(secureOut) };
|
|
328086
|
+
for (const args of buildHostProxySetCommands(service, host, port)) {
|
|
328087
|
+
await execFileAsync("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
|
|
328088
|
+
}
|
|
328089
|
+
onLog?.(`[mobile-net] macOS host proxy set on "${service}" \u2192 ${host}:${port} (iOS simulator shares the host network).`);
|
|
328090
|
+
return { configured: true, service, prior };
|
|
328091
|
+
} catch (err) {
|
|
328092
|
+
if (service && prior) {
|
|
328093
|
+
onLog?.(`[mobile-net] host proxy set failed mid-way \u2014 rolling back "${service}" to prior state: ${errMsg(err)}`);
|
|
328094
|
+
for (const args of buildHostProxyRestoreCommands(service, prior)) {
|
|
328095
|
+
try {
|
|
328096
|
+
await execFileAsync("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
|
|
328097
|
+
} catch {
|
|
328098
|
+
}
|
|
328099
|
+
}
|
|
328100
|
+
}
|
|
328101
|
+
return { configured: false, reason: `iOS simulator host proxy could not be configured: ${errMsg(err)}` };
|
|
328102
|
+
}
|
|
328103
|
+
}
|
|
328104
|
+
async function iosSimRestoreHostProxy(service, prior, onLog) {
|
|
328105
|
+
const restore = prior ?? {
|
|
328106
|
+
web: { enabled: false, server: "", port: "" },
|
|
328107
|
+
secure: { enabled: false, server: "", port: "" }
|
|
328108
|
+
};
|
|
328109
|
+
for (const args of buildHostProxyRestoreCommands(service, restore)) {
|
|
328110
|
+
try {
|
|
328111
|
+
await execFileAsync("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
|
|
328112
|
+
} catch (err) {
|
|
328113
|
+
onLog?.(`[mobile-net] networksetup restore (${args[0]}) failed: ${errMsg(err)}`);
|
|
328114
|
+
}
|
|
328115
|
+
}
|
|
328116
|
+
onLog?.(`[mobile-net] macOS host proxy restored on "${service}".`);
|
|
328117
|
+
}
|
|
327437
328118
|
var activeCleanups = /* @__PURE__ */ new Map();
|
|
327438
328119
|
var exitHandlersInstalled = false;
|
|
327439
328120
|
function markerPath(sessionKey) {
|
|
@@ -327468,6 +328149,14 @@ function restoreDeviceSync(marker, onLog) {
|
|
|
327468
328149
|
run2(["-s", marker.deviceId, "shell", "rm", "-f", "/sdcard/validateqa-mobile-ca.pem"]);
|
|
327469
328150
|
}
|
|
327470
328151
|
}
|
|
328152
|
+
if (marker.platform === "IOS" && marker.hostProxySet && marker.hostProxyService && process.platform === "darwin") {
|
|
328153
|
+
for (const args of buildHostProxyRestoreCommands(marker.hostProxyService, hostProxyPriorFromMarker(marker))) {
|
|
328154
|
+
try {
|
|
328155
|
+
(0, import_node_child_process.execFileSync)("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS, stdio: "ignore" });
|
|
328156
|
+
} catch {
|
|
328157
|
+
}
|
|
328158
|
+
}
|
|
328159
|
+
}
|
|
327471
328160
|
onLog?.(`[mobile-net] restored device ${marker.deviceId} from pending-cleanup marker.`);
|
|
327472
328161
|
}
|
|
327473
328162
|
async function reconcilePendingCleanups(onLog) {
|
|
@@ -327479,6 +328168,8 @@ async function reconcilePendingCleanups(onLog) {
|
|
|
327479
328168
|
}
|
|
327480
328169
|
for (const file of files) {
|
|
327481
328170
|
if (!file.endsWith(".json")) continue;
|
|
328171
|
+
const sessionKey = file.slice(0, -".json".length);
|
|
328172
|
+
if (activeCleanups.has(sessionKey)) continue;
|
|
327482
328173
|
const full = (0, import_node_path.join)(PENDING_CLEANUP_DIR, file);
|
|
327483
328174
|
try {
|
|
327484
328175
|
const raw = await (0, import_promises.readFile)(full, "utf-8");
|
|
@@ -327508,8 +328199,10 @@ function ensureExitHandlers() {
|
|
|
327508
328199
|
for (const signal of ["SIGTERM", "SIGINT"]) {
|
|
327509
328200
|
process.on(signal, () => {
|
|
327510
328201
|
drain();
|
|
327511
|
-
process.
|
|
327512
|
-
|
|
328202
|
+
if (process.listenerCount(signal) <= 1) {
|
|
328203
|
+
process.removeAllListeners(signal);
|
|
328204
|
+
process.kill(process.pid, signal);
|
|
328205
|
+
}
|
|
327513
328206
|
});
|
|
327514
328207
|
}
|
|
327515
328208
|
}
|
|
@@ -327559,6 +328252,9 @@ async function startMobileNetworkCapture(options) {
|
|
|
327559
328252
|
let wiringReason = "HTTPS interception pending: capture begins once the device routes a TLS handshake through the proxy.";
|
|
327560
328253
|
let proxySet = false;
|
|
327561
328254
|
let caInstalledOnDevice = false;
|
|
328255
|
+
let hostProxySet = false;
|
|
328256
|
+
let hostProxyService;
|
|
328257
|
+
let hostProxyPrior;
|
|
327562
328258
|
try {
|
|
327563
328259
|
if (platform3 === "ANDROID") {
|
|
327564
328260
|
proxySet = await androidSetProxy(deviceId, hostPort, onLog);
|
|
@@ -327573,25 +328269,50 @@ async function startMobileNetworkCapture(options) {
|
|
|
327573
328269
|
}
|
|
327574
328270
|
} else {
|
|
327575
328271
|
if (!isPhysical && caCertPath) {
|
|
327576
|
-
const { trusted
|
|
328272
|
+
const { trusted } = await iosSimTrustCa(deviceId, caCertPath);
|
|
327577
328273
|
caInstalledOnDevice = trusted;
|
|
327578
|
-
|
|
328274
|
+
}
|
|
328275
|
+
const support = hostProxyControlSupported(platform3, isPhysical);
|
|
328276
|
+
if (support.supported) {
|
|
328277
|
+
const hp = await iosSimConfigureHostProxy(proxyHost, proxyPort, onLog);
|
|
328278
|
+
if (hp.configured) {
|
|
328279
|
+
hostProxySet = true;
|
|
328280
|
+
hostProxyService = hp.service;
|
|
328281
|
+
hostProxyPrior = hp.prior;
|
|
328282
|
+
wiringReason = "iOS simulator traffic routed via the macOS host proxy; HTTPS interception pending until the app completes a TLS handshake through the proxy" + (caCertPath ? ` (trust ${caCertPath} in the simulator if the app rejects our CA).` : ".");
|
|
328283
|
+
} else {
|
|
328284
|
+
wiringReason = hp.reason ?? "iOS simulator host proxy could not be configured";
|
|
328285
|
+
onLog?.(`[mobile-net] ${wiringReason}`);
|
|
328286
|
+
}
|
|
327579
328287
|
} else if (isPhysical) {
|
|
327580
328288
|
wiringReason = "Physical iOS HTTPS is tunneled without MITM by default. Set the Wi-Fi HTTP proxy and trust the CA manually, then run with VALIDATEQA_MOBILE_FORCE_TLS_MITM=1 to decrypt HTTPS bodies" + (caCertPath ? ` (${caCertPath}).` : ".");
|
|
328289
|
+
} else {
|
|
328290
|
+
wiringReason = support.reason ?? "iOS simulator host proxy is unsupported on this host";
|
|
328291
|
+
onLog?.(`[mobile-net] ${wiringReason}`);
|
|
327581
328292
|
}
|
|
327582
328293
|
}
|
|
327583
328294
|
} catch (err) {
|
|
327584
328295
|
wiringReason = `device wiring degraded: ${errMsg(err)}`;
|
|
327585
328296
|
onLog?.(`[mobile-net] ${wiringReason}`);
|
|
327586
328297
|
}
|
|
327587
|
-
if (proxySet || caInstalledOnDevice) {
|
|
328298
|
+
if (proxySet || caInstalledOnDevice || hostProxySet) {
|
|
327588
328299
|
const marker = {
|
|
327589
328300
|
platform: platform3,
|
|
327590
328301
|
deviceId,
|
|
327591
328302
|
isPhysical,
|
|
327592
328303
|
proxySet,
|
|
327593
328304
|
originalProxy,
|
|
327594
|
-
caInstalledOnDevice
|
|
328305
|
+
caInstalledOnDevice,
|
|
328306
|
+
...hostProxySet ? {
|
|
328307
|
+
hostProxySet: true,
|
|
328308
|
+
hostProxyService,
|
|
328309
|
+
hostWebProxyWasEnabled: hostProxyPrior?.web.enabled,
|
|
328310
|
+
hostWebProxyPriorServer: hostProxyPrior?.web.server,
|
|
328311
|
+
hostWebProxyPriorPort: hostProxyPrior?.web.port,
|
|
328312
|
+
hostSecureProxyWasEnabled: hostProxyPrior?.secure.enabled,
|
|
328313
|
+
hostSecureProxyPriorServer: hostProxyPrior?.secure.server,
|
|
328314
|
+
hostSecureProxyPriorPort: hostProxyPrior?.secure.port
|
|
328315
|
+
} : {}
|
|
327595
328316
|
};
|
|
327596
328317
|
activeCleanups.set(sessionKey, marker);
|
|
327597
328318
|
await writePendingCleanup(sessionKey, marker);
|
|
@@ -327611,6 +328332,9 @@ async function startMobileNetworkCapture(options) {
|
|
|
327611
328332
|
if (proxySet && platform3 === "ANDROID") {
|
|
327612
328333
|
await androidClearProxy(deviceId, originalProxy, onLog);
|
|
327613
328334
|
}
|
|
328335
|
+
if (hostProxySet && hostProxyService && platform3 === "IOS" && !isPhysical) {
|
|
328336
|
+
await iosSimRestoreHostProxy(hostProxyService, hostProxyPrior, onLog);
|
|
328337
|
+
}
|
|
327614
328338
|
if (caInstalledOnDevice && capturedCaCertPath) {
|
|
327615
328339
|
if (platform3 === "ANDROID") {
|
|
327616
328340
|
await androidRemoveCa(deviceId, onLog);
|
|
@@ -327646,6 +328370,9 @@ async function startMobileNetworkCapture(options) {
|
|
|
327646
328370
|
|
|
327647
328371
|
// src/services/appium-executor.ts
|
|
327648
328372
|
var DEFAULT_STEP_TIMEOUT_MS = 1e4;
|
|
328373
|
+
var WD_SESSION_CREATE_TIMEOUT_MS = 12e4;
|
|
328374
|
+
var WD_ACTION_TIMEOUT_MS = 3e4;
|
|
328375
|
+
var MAX_SESSION_CREATE_ATTEMPTS = 3;
|
|
327649
328376
|
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
|
|
327650
328377
|
var MOBILE_TEXT_ATTRIBUTE_NAMES = ["text", "content-desc", "contentDescription", "name", "label", "value"];
|
|
327651
328378
|
var WebDriverTransportError = class extends Error {
|
|
@@ -327659,13 +328386,21 @@ function isTransportError(err) {
|
|
|
327659
328386
|
return err instanceof WebDriverTransportError || typeof err === "object" && err !== null && err.isTransport === true;
|
|
327660
328387
|
}
|
|
327661
328388
|
async function wdRequest(path, options) {
|
|
328389
|
+
const isSessionCreate = (options?.method ?? "GET").toUpperCase() === "POST" && path === "/session";
|
|
328390
|
+
const timeoutMs = isSessionCreate ? WD_SESSION_CREATE_TIMEOUT_MS : WD_ACTION_TIMEOUT_MS;
|
|
327662
328391
|
let res;
|
|
327663
328392
|
try {
|
|
327664
328393
|
res = await fetch(`http://localhost:${getAppiumState().port}${path}`, {
|
|
327665
328394
|
...options,
|
|
328395
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
327666
328396
|
headers: { "Content-Type": "application/json", ...options?.headers }
|
|
327667
328397
|
});
|
|
327668
328398
|
} catch (err) {
|
|
328399
|
+
if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
|
|
328400
|
+
throw new WebDriverTransportError(
|
|
328401
|
+
`WebDriver request to ${path} timed out after ${timeoutMs}ms (device/Appium unresponsive)`
|
|
328402
|
+
);
|
|
328403
|
+
}
|
|
327669
328404
|
throw new WebDriverTransportError(`WebDriver connection error: ${err instanceof Error ? err.message : String(err)}`);
|
|
327670
328405
|
}
|
|
327671
328406
|
if (!res.ok) {
|
|
@@ -327750,6 +328485,31 @@ async function createSession(target) {
|
|
|
327750
328485
|
if (!sessionId) throw new Error("Appium session creation returned no session ID");
|
|
327751
328486
|
return sessionId;
|
|
327752
328487
|
}
|
|
328488
|
+
var TERMINAL_SESSION_START = /not installed|no such (app|file)|could not confirm|bundle ?id .*(not|missing)|xcodeorgid|signing|provisioning|not authorized|unauthorized/i;
|
|
328489
|
+
var RETRYABLE_SESSION_START = /timed out|timeout|webdriveragent|xcodebuild|instruments|could not connect|lockdownd|failed to (start|launch|create)|unable to launch|session is either terminated|unavailable|econnrefused|connection error|xcuitest|\b50[234]\b/i;
|
|
328490
|
+
function isRetryableSessionStartError(err) {
|
|
328491
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
328492
|
+
if (TERMINAL_SESSION_START.test(message)) return false;
|
|
328493
|
+
if (isTransportError(err)) return true;
|
|
328494
|
+
return RETRYABLE_SESSION_START.test(message);
|
|
328495
|
+
}
|
|
328496
|
+
async function createSessionWithRetry(target, onRetry) {
|
|
328497
|
+
let lastErr;
|
|
328498
|
+
for (let attempt = 1; attempt <= MAX_SESSION_CREATE_ATTEMPTS; attempt++) {
|
|
328499
|
+
try {
|
|
328500
|
+
return await createSession(target);
|
|
328501
|
+
} catch (err) {
|
|
328502
|
+
lastErr = err;
|
|
328503
|
+
if (attempt >= MAX_SESSION_CREATE_ATTEMPTS || !isRetryableSessionStartError(err)) throw err;
|
|
328504
|
+
const delayMs = Math.pow(2, attempt) * 1e3;
|
|
328505
|
+
onRetry?.(
|
|
328506
|
+
`Appium session start failed (attempt ${attempt}/${MAX_SESSION_CREATE_ATTEMPTS}, retrying in ${delayMs}ms): ${err instanceof Error ? err.message : String(err)}`
|
|
328507
|
+
);
|
|
328508
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
328509
|
+
}
|
|
328510
|
+
}
|
|
328511
|
+
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
328512
|
+
}
|
|
327753
328513
|
async function openDeepLink(sessionId, platform3, deeplink, appId) {
|
|
327754
328514
|
const args = platform3 === "IOS" ? [{ url: deeplink, bundleId: appId }] : [{ url: deeplink, package: appId }];
|
|
327755
328515
|
await wdRequest(`/session/${sessionId}/execute/sync`, {
|
|
@@ -327851,6 +328611,61 @@ async function takeScreenshot(sessionId) {
|
|
|
327851
328611
|
return null;
|
|
327852
328612
|
}
|
|
327853
328613
|
}
|
|
328614
|
+
function mobileRecordingEnabled() {
|
|
328615
|
+
const value = process.env.VALIDATEQA_MOBILE_RUN_VIDEO;
|
|
328616
|
+
return value !== "0" && value !== "false";
|
|
328617
|
+
}
|
|
328618
|
+
function readPositiveIntEnv(name, fallback, min, max) {
|
|
328619
|
+
const raw = process.env[name];
|
|
328620
|
+
const parsed = raw ? Number.parseInt(raw, 10) : NaN;
|
|
328621
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
328622
|
+
return Math.max(min, Math.min(max, parsed));
|
|
328623
|
+
}
|
|
328624
|
+
async function startScreenRecording(sessionId, platform3, log2) {
|
|
328625
|
+
if (!mobileRecordingEnabled()) {
|
|
328626
|
+
log2("[mobile-video] recording disabled by VALIDATEQA_MOBILE_RUN_VIDEO=0");
|
|
328627
|
+
return false;
|
|
328628
|
+
}
|
|
328629
|
+
const timeLimit = readPositiveIntEnv("VALIDATEQA_MOBILE_RECORDING_SECONDS", 180, 5, 180);
|
|
328630
|
+
const options = {
|
|
328631
|
+
timeLimit: String(timeLimit),
|
|
328632
|
+
forceRestart: true
|
|
328633
|
+
};
|
|
328634
|
+
if (platform3 === "ANDROID") {
|
|
328635
|
+
options.bitRate = readPositiveIntEnv("VALIDATEQA_MOBILE_RECORDING_BITRATE", 75e4, 1e5, 4e6);
|
|
328636
|
+
} else {
|
|
328637
|
+
options.videoQuality = process.env.VALIDATEQA_MOBILE_RECORDING_IOS_QUALITY || "low";
|
|
328638
|
+
}
|
|
328639
|
+
try {
|
|
328640
|
+
await wdRequest(`/session/${sessionId}/appium/start_recording_screen`, {
|
|
328641
|
+
method: "POST",
|
|
328642
|
+
body: JSON.stringify({ options })
|
|
328643
|
+
});
|
|
328644
|
+
log2(`[mobile-video] screen recording started (${timeLimit}s max)`);
|
|
328645
|
+
return true;
|
|
328646
|
+
} catch (err) {
|
|
328647
|
+
log2(`[mobile-video] screen recording unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
328648
|
+
return false;
|
|
328649
|
+
}
|
|
328650
|
+
}
|
|
328651
|
+
async function stopScreenRecording(sessionId, log2) {
|
|
328652
|
+
try {
|
|
328653
|
+
const result = await wdRequest(`/session/${sessionId}/appium/stop_recording_screen`, {
|
|
328654
|
+
method: "POST",
|
|
328655
|
+
body: JSON.stringify({ options: {} })
|
|
328656
|
+
});
|
|
328657
|
+
const value = typeof result?.value === "string" ? result.value.trim() : "";
|
|
328658
|
+
if (!value) {
|
|
328659
|
+
log2("[mobile-video] screen recording stopped but Appium returned no video");
|
|
328660
|
+
return void 0;
|
|
328661
|
+
}
|
|
328662
|
+
log2(`[mobile-video] screen recording captured (${Math.round(value.length / 1024)}KB base64)`);
|
|
328663
|
+
return value;
|
|
328664
|
+
} catch (err) {
|
|
328665
|
+
log2(`[mobile-video] screen recording stop failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
328666
|
+
return void 0;
|
|
328667
|
+
}
|
|
328668
|
+
}
|
|
327854
328669
|
function boundsFromStep(step) {
|
|
327855
328670
|
const bounds = step.metadata?.bounds;
|
|
327856
328671
|
if (bounds && typeof bounds === "object" && !Array.isArray(bounds) && typeof bounds.x === "number" && typeof bounds.y === "number" && typeof bounds.width === "number" && typeof bounds.height === "number") {
|
|
@@ -327970,7 +328785,8 @@ async function sendKeysToFocusedInput(sessionId, value) {
|
|
|
327970
328785
|
});
|
|
327971
328786
|
}
|
|
327972
328787
|
function encodeAndroidInputText(value) {
|
|
327973
|
-
|
|
328788
|
+
const inputEncoded = value.replace(/%/g, "%25").replace(/\s/g, "%s");
|
|
328789
|
+
return `'${inputEncoded.replace(/'/g, `'\\''`)}'`;
|
|
327974
328790
|
}
|
|
327975
328791
|
async function sendAndroidTextToFocusedInput(deviceId, value) {
|
|
327976
328792
|
if (!deviceId) {
|
|
@@ -328228,6 +329044,9 @@ async function executeMobileTest(options) {
|
|
|
328228
329044
|
let sessionId = null;
|
|
328229
329045
|
let capture = null;
|
|
328230
329046
|
let captureStopped = false;
|
|
329047
|
+
let recordingStarted = false;
|
|
329048
|
+
let recordingStopped = false;
|
|
329049
|
+
let video;
|
|
328231
329050
|
const stopCapture = async () => {
|
|
328232
329051
|
if (!capture || captureStopped) return apiCalls;
|
|
328233
329052
|
captureStopped = true;
|
|
@@ -328242,6 +329061,15 @@ async function executeMobileTest(options) {
|
|
|
328242
329061
|
}
|
|
328243
329062
|
return apiCalls;
|
|
328244
329063
|
};
|
|
329064
|
+
const stopRecording = async () => {
|
|
329065
|
+
if (!sessionId || !recordingStarted || recordingStopped) return video;
|
|
329066
|
+
recordingStopped = true;
|
|
329067
|
+
const captured = await stopScreenRecording(sessionId, log2);
|
|
329068
|
+
if (captured) {
|
|
329069
|
+
video = captured;
|
|
329070
|
+
}
|
|
329071
|
+
return video;
|
|
329072
|
+
};
|
|
328245
329073
|
try {
|
|
328246
329074
|
log2("Checking Appium server health...");
|
|
328247
329075
|
const health = await checkAppiumHealth();
|
|
@@ -328295,13 +329123,13 @@ async function executeMobileTest(options) {
|
|
|
328295
329123
|
}
|
|
328296
329124
|
log2(`Starting Appium session for ${options.target.appId} on ${selectedDevice.name}...`);
|
|
328297
329125
|
try {
|
|
328298
|
-
sessionId = await
|
|
329126
|
+
sessionId = await createSessionWithRetry({
|
|
328299
329127
|
appId: options.target.appId,
|
|
328300
329128
|
platform: options.target.platform,
|
|
328301
329129
|
deviceId: selectedDevice.id,
|
|
328302
329130
|
platformVersion: options.target.platformVersion ?? selectedDevice.platformVersion,
|
|
328303
329131
|
isPhysical: selectedDevice.isPhysical
|
|
328304
|
-
});
|
|
329132
|
+
}, (msg) => log2(msg));
|
|
328305
329133
|
} catch (err) {
|
|
328306
329134
|
const base2 = err instanceof Error ? err.message : String(err);
|
|
328307
329135
|
if (selectedDevice.isPhysical) {
|
|
@@ -328316,6 +329144,7 @@ async function executeMobileTest(options) {
|
|
|
328316
329144
|
throw err;
|
|
328317
329145
|
}
|
|
328318
329146
|
log2(`Session created: ${sessionId}`);
|
|
329147
|
+
recordingStarted = await startScreenRecording(sessionId, options.target.platform, log2);
|
|
328319
329148
|
if (options.target.launchMode === "DEEP_LINK" && options.target.deeplink) {
|
|
328320
329149
|
try {
|
|
328321
329150
|
log2(`Opening deep link: ${options.target.deeplink}`);
|
|
@@ -328376,7 +329205,9 @@ async function executeMobileTest(options) {
|
|
|
328376
329205
|
break;
|
|
328377
329206
|
}
|
|
328378
329207
|
}
|
|
328379
|
-
|
|
329208
|
+
if (step.action !== "home" && step.action !== "back") {
|
|
329209
|
+
await restoreTargetAppIfExternal(sessionId, options.target, log2, `step ${step.order} (${step.action})`, true);
|
|
329210
|
+
}
|
|
328380
329211
|
} catch (err) {
|
|
328381
329212
|
status = "failed";
|
|
328382
329213
|
stepError = err instanceof Error ? err.message : String(err);
|
|
@@ -328405,10 +329236,12 @@ async function executeMobileTest(options) {
|
|
|
328405
329236
|
const allPassed = !environmental && stepResults.every((r) => r.status === "passed" || r.status === "skipped");
|
|
328406
329237
|
const firstError = environmental ?? (stepResults.find((r) => r.status === "failed")?.error ?? null);
|
|
328407
329238
|
await stopCapture();
|
|
329239
|
+
await stopRecording();
|
|
328408
329240
|
return {
|
|
328409
329241
|
passed: allPassed,
|
|
328410
329242
|
stepResults,
|
|
328411
329243
|
screenshots,
|
|
329244
|
+
...video ? { video, videoMimeType: "video/mp4" } : {},
|
|
328412
329245
|
apiCalls,
|
|
328413
329246
|
logs: logLines.join("\n"),
|
|
328414
329247
|
error: firstError,
|
|
@@ -328419,10 +329252,12 @@ async function executeMobileTest(options) {
|
|
|
328419
329252
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
328420
329253
|
log2(`Fatal error: ${errorMsg}`);
|
|
328421
329254
|
await stopCapture();
|
|
329255
|
+
await stopRecording();
|
|
328422
329256
|
return {
|
|
328423
329257
|
passed: false,
|
|
328424
329258
|
stepResults,
|
|
328425
329259
|
screenshots,
|
|
329260
|
+
...video ? { video, videoMimeType: "video/mp4" } : {},
|
|
328426
329261
|
apiCalls,
|
|
328427
329262
|
logs: logLines.join("\n"),
|
|
328428
329263
|
error: errorMsg,
|
|
@@ -328434,6 +329269,7 @@ async function executeMobileTest(options) {
|
|
|
328434
329269
|
};
|
|
328435
329270
|
} finally {
|
|
328436
329271
|
await stopCapture();
|
|
329272
|
+
await stopRecording();
|
|
328437
329273
|
if (sessionId) {
|
|
328438
329274
|
log2("Tearing down Appium session...");
|
|
328439
329275
|
await deleteSession(sessionId);
|
|
@@ -328474,7 +329310,7 @@ async function createMobileDiscoverySession(target, options) {
|
|
|
328474
329310
|
isPhysical: selectedDevice.isPhysical === true,
|
|
328475
329311
|
platformVersion: selectedDevice.platformVersion
|
|
328476
329312
|
});
|
|
328477
|
-
const sessionId = await
|
|
329313
|
+
const sessionId = await createSessionWithRetry({
|
|
328478
329314
|
appId: target.appId,
|
|
328479
329315
|
platform: target.platform,
|
|
328480
329316
|
deviceId: selectedDevice.id,
|
|
@@ -329285,7 +330121,7 @@ function stopSessionReaper() {
|
|
|
329285
330121
|
}
|
|
329286
330122
|
}
|
|
329287
330123
|
async function reapIdleSession() {
|
|
329288
|
-
if (!state.appiumSessionId || state.mirrorClients.size > 0 || mirrorClientsEmptyAt === null || Date.now() - mirrorClientsEmptyAt < SESSION_IDLE_GRACE_MS) {
|
|
330124
|
+
if (!state.appiumSessionId || mirrorSessionOwner !== "recording" || state.mirrorClients.size > 0 || mirrorClientsEmptyAt === null || Date.now() - mirrorClientsEmptyAt < SESSION_IDLE_GRACE_MS) {
|
|
329289
330125
|
return;
|
|
329290
330126
|
}
|
|
329291
330127
|
const sessionId = state.appiumSessionId;
|
|
@@ -330101,6 +330937,79 @@ function printDoctorReport(report) {
|
|
|
330101
330937
|
console.log("");
|
|
330102
330938
|
}
|
|
330103
330939
|
|
|
330940
|
+
// src/result-payload-budget.ts
|
|
330941
|
+
var SERVER_RESULT_BODY_LIMIT_BYTES = 50 * 1024 * 1024;
|
|
330942
|
+
var RESULT_BODY_BUDGET_BYTES = 48 * 1024 * 1024;
|
|
330943
|
+
var SERVER_VIDEO_STORE_CAP_BYTES = 24 * 1024 * 1024;
|
|
330944
|
+
function serializedBytes(value) {
|
|
330945
|
+
return Buffer.byteLength(JSON.stringify(value) ?? "", "utf8");
|
|
330946
|
+
}
|
|
330947
|
+
function budgetResultPayload(result, maxBytes = RESULT_BODY_BUDGET_BYTES, videoStoreCapBytes = SERVER_VIDEO_STORE_CAP_BYTES) {
|
|
330948
|
+
const oversizedVideo = typeof result.video === "string" && result.video.length > videoStoreCapBytes;
|
|
330949
|
+
let bytes = serializedBytes(result);
|
|
330950
|
+
if (bytes <= maxBytes && !oversizedVideo) {
|
|
330951
|
+
return { payload: result, shed: [], bytes };
|
|
330952
|
+
}
|
|
330953
|
+
const shed = [];
|
|
330954
|
+
let payload = result;
|
|
330955
|
+
const drop = (fields, label) => {
|
|
330956
|
+
const next = { ...payload };
|
|
330957
|
+
for (const field of fields) next[field] = void 0;
|
|
330958
|
+
payload = next;
|
|
330959
|
+
shed.push(label);
|
|
330960
|
+
bytes = serializedBytes(payload);
|
|
330961
|
+
};
|
|
330962
|
+
if ((bytes > maxBytes || oversizedVideo) && payload.video != null) {
|
|
330963
|
+
drop(["video", "videoMimeType"], "video");
|
|
330964
|
+
}
|
|
330965
|
+
if (bytes > maxBytes && Array.isArray(payload.screenshots) && payload.screenshots.length > 0) {
|
|
330966
|
+
const shots = [...payload.screenshots];
|
|
330967
|
+
const bySizeDesc = shots.map((shot, i) => ({ i, len: typeof shot === "string" ? shot.length : 0 })).sort((a, b) => b.len - a.len);
|
|
330968
|
+
const overshoot = bytes - maxBytes;
|
|
330969
|
+
let shaved = 0;
|
|
330970
|
+
let blanked = 0;
|
|
330971
|
+
for (const { i, len } of bySizeDesc) {
|
|
330972
|
+
if (shaved >= overshoot || len === 0) break;
|
|
330973
|
+
if (shots[i] === "") continue;
|
|
330974
|
+
shots[i] = "";
|
|
330975
|
+
shaved += len;
|
|
330976
|
+
blanked++;
|
|
330977
|
+
}
|
|
330978
|
+
if (blanked > 0) {
|
|
330979
|
+
payload = { ...payload, screenshots: shots };
|
|
330980
|
+
bytes = serializedBytes(payload);
|
|
330981
|
+
shed.push(blanked === shots.length ? "screenshots" : `${blanked}/${shots.length} screenshots`);
|
|
330982
|
+
}
|
|
330983
|
+
}
|
|
330984
|
+
if (bytes > maxBytes && payload.harApiCalls != null) drop(["harApiCalls"], "harApiCalls");
|
|
330985
|
+
if (bytes > maxBytes && payload.apiCalls != null) drop(["apiCalls"], "apiCalls");
|
|
330986
|
+
if (shed.length > 0) {
|
|
330987
|
+
const note = `
|
|
330988
|
+
|
|
330989
|
+
\u26A0 [runner] Result media trimmed before upload \u2014 payload exceeded the ${Math.round(maxBytes / (1024 * 1024))}MB server limit; dropped: ${shed.join(", ")}.`;
|
|
330990
|
+
payload = {
|
|
330991
|
+
...payload,
|
|
330992
|
+
logs: (typeof payload.logs === "string" ? payload.logs : "") + note
|
|
330993
|
+
};
|
|
330994
|
+
bytes = serializedBytes(payload);
|
|
330995
|
+
}
|
|
330996
|
+
if (bytes > maxBytes && typeof payload.logs === "string" && payload.logs.length > 4096) {
|
|
330997
|
+
const over = bytes - maxBytes;
|
|
330998
|
+
const targetLen = Math.max(2048, payload.logs.length - over - 1024);
|
|
330999
|
+
if (targetLen < payload.logs.length) {
|
|
331000
|
+
const half = Math.floor(targetLen / 2);
|
|
331001
|
+
const head = payload.logs.slice(0, half);
|
|
331002
|
+
const tail = payload.logs.slice(payload.logs.length - half);
|
|
331003
|
+
payload = { ...payload, logs: `${head}
|
|
331004
|
+
\u2026 [logs truncated to fit upload] \u2026
|
|
331005
|
+
${tail}` };
|
|
331006
|
+
if (!shed.includes("logs (truncated)")) shed.push("logs (truncated)");
|
|
331007
|
+
bytes = serializedBytes(payload);
|
|
331008
|
+
}
|
|
331009
|
+
}
|
|
331010
|
+
return { payload, shed, bytes };
|
|
331011
|
+
}
|
|
331012
|
+
|
|
330104
331013
|
// src/runner.ts
|
|
330105
331014
|
init_dist();
|
|
330106
331015
|
var import_runner_core12 = __toESM(require_dist2());
|
|
@@ -330269,15 +331178,33 @@ async function claimRun(serverUrl, headers, runId) {
|
|
|
330269
331178
|
async function postResult(serverUrl, headers, runId, result) {
|
|
330270
331179
|
const MAX_ATTEMPTS = 3;
|
|
330271
331180
|
let lastError;
|
|
331181
|
+
const budgeted = budgetResultPayload(result);
|
|
331182
|
+
if (budgeted.shed.length > 0) {
|
|
331183
|
+
console.warn(
|
|
331184
|
+
`[runner] Result payload for run ${runId} exceeded the upload budget (${Math.round(budgeted.bytes / (1024 * 1024))}MB after trim); dropped: ${budgeted.shed.join(", ")}.`
|
|
331185
|
+
);
|
|
331186
|
+
}
|
|
331187
|
+
let payload = budgeted.payload;
|
|
330272
331188
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
330273
331189
|
try {
|
|
330274
331190
|
const res = await fetch(`${serverUrl}/api/runner/runs/${runId}/result`, {
|
|
330275
331191
|
method: "POST",
|
|
330276
331192
|
headers,
|
|
330277
|
-
body: JSON.stringify(
|
|
331193
|
+
body: JSON.stringify(payload),
|
|
330278
331194
|
signal: AbortSignal.timeout(6e4)
|
|
330279
331195
|
});
|
|
330280
331196
|
if (res.ok) return;
|
|
331197
|
+
if (res.status === 413) {
|
|
331198
|
+
const minimal = budgetResultPayload(payload, 0);
|
|
331199
|
+
if (minimal.shed.length > 0) {
|
|
331200
|
+
console.warn(
|
|
331201
|
+
`[runner] Server rejected result for run ${runId} as too large (413); retrying with a verdict-only payload (dropped: ${minimal.shed.join(", ")}).`
|
|
331202
|
+
);
|
|
331203
|
+
payload = minimal.payload;
|
|
331204
|
+
lastError = new Error("HTTP 413 (payload trimmed, retrying)");
|
|
331205
|
+
continue;
|
|
331206
|
+
}
|
|
331207
|
+
}
|
|
330281
331208
|
lastError = new Error(`HTTP ${res.status}`);
|
|
330282
331209
|
} catch (err) {
|
|
330283
331210
|
lastError = err instanceof Error ? err : new Error(String(err));
|
|
@@ -331802,14 +332729,20 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331802
332729
|
});
|
|
331803
332730
|
requestLiveBrowserHeartbeat();
|
|
331804
332731
|
},
|
|
332732
|
+
// D-D6 — upload ONE baseline PNG per newly-observed screen through the
|
|
332733
|
+
// same streaming /page-screenshot relay the web path uses (mobile has no
|
|
332734
|
+
// route, so the screen-id is the key). The returned storage key rides the
|
|
332735
|
+
// explore checkpoint so the server links it to SitePage.screenshotKey.
|
|
332736
|
+
onScreenshotUpload: (screenId, base64) => onScreenshot(screenId, base64),
|
|
331805
332737
|
onExploreComplete,
|
|
331806
332738
|
onTestGenerated,
|
|
331807
332739
|
onPlanReady
|
|
331808
332740
|
});
|
|
331809
332741
|
} finally {
|
|
331810
|
-
|
|
332742
|
+
const activeCapture = capture;
|
|
332743
|
+
if (activeCapture) {
|
|
331811
332744
|
try {
|
|
331812
|
-
const apiCalls = await
|
|
332745
|
+
const apiCalls = await activeCapture.stop();
|
|
331813
332746
|
if (discoveryResult) {
|
|
331814
332747
|
discoveryResult.apiCalls = apiCalls.length ? attachMobileDiscoveryNetworkContext(apiCalls, discoveryResult, session?.appId ?? mobileTarget.appId) : void 0;
|
|
331815
332748
|
}
|
|
@@ -332144,6 +333077,8 @@ ${result2.logs}`);
|
|
|
332144
333077
|
status,
|
|
332145
333078
|
stepResults: result2.stepResults,
|
|
332146
333079
|
screenshots: result2.screenshots,
|
|
333080
|
+
video: result2.video,
|
|
333081
|
+
videoMimeType: result2.videoMimeType,
|
|
332147
333082
|
apiCalls: result2.apiCalls,
|
|
332148
333083
|
logs: result2.logs,
|
|
332149
333084
|
error: reportedError,
|
|
@@ -332455,7 +333390,7 @@ async function startRunner(opts) {
|
|
|
332455
333390
|
};
|
|
332456
333391
|
const sendHeartbeat = async () => {
|
|
332457
333392
|
try {
|
|
332458
|
-
const devices = enableMobile ?
|
|
333393
|
+
const devices = enableMobile ? getCachedDeviceSnapshot() : [];
|
|
332459
333394
|
const capabilities = enableMobile ? {
|
|
332460
333395
|
web: true,
|
|
332461
333396
|
ios: devices.some((d) => d.platform === "IOS" && d.isAvailable && d.state === "BOOTED"),
|