@validate.qa/runner 1.0.9 → 1.0.13
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 +1035 -154
- package/dist/cli.mjs +1035 -154
- package/package.json +2 -2
package/dist/cli.mjs
CHANGED
|
@@ -238,6 +238,9 @@ var require_ai_provider = __commonJS({
|
|
|
238
238
|
exports.getNoThinkingRequestOptions = getNoThinkingRequestOptions;
|
|
239
239
|
exports.getAIClient = getAIClient;
|
|
240
240
|
exports.getClientForModel = getClientForModel;
|
|
241
|
+
exports.shouldProxyAIThroughServerForLocalRunner = shouldProxyAIThroughServerForLocalRunner;
|
|
242
|
+
exports.getRunnerAIClient = getRunnerAIClient;
|
|
243
|
+
exports.getRunnerClientForModel = getRunnerClientForModel;
|
|
241
244
|
exports.getModel = getModel;
|
|
242
245
|
exports.resolveModelForTask = resolveModelForTask;
|
|
243
246
|
exports.getAvailableModels = getAvailableModels;
|
|
@@ -268,14 +271,16 @@ var require_ai_provider = __commonJS({
|
|
|
268
271
|
{ id: "minimax-m2.7", label: "MiniMax M2.7", provider: "minimax", description: "Fast M2.7 tier (Highspeed); auto-fails over to standard M2.7, ~200K ctx", contextWindow: 2e5 },
|
|
269
272
|
{ id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", provider: "anthropic", description: "Anthropic - fast & intelligent, 200k ctx", contextWindow: 2e5 },
|
|
270
273
|
{ id: "nvidia-nemotron-nano", label: "NVIDIA Nemotron Nano", provider: "nvidia", description: "NVIDIA NIM - Nemotron 30B Nano, reasoning model, 128k ctx", contextWindow: 128e3 },
|
|
271
|
-
{ id: "nvidia-kimi-k2.6", label: "NVIDIA Kimi K2.6", provider: "nvidia", description: "NVIDIA NIM - Moonshot Kimi K2.6, multimodal coding and agentic model, 256k ctx", contextWindow: 256e3 }
|
|
274
|
+
{ id: "nvidia-kimi-k2.6", label: "NVIDIA Kimi K2.6", provider: "nvidia", description: "NVIDIA NIM - Moonshot Kimi K2.6, multimodal coding and agentic model, 256k ctx", contextWindow: 256e3 },
|
|
275
|
+
{ id: "deepseek-v4-flash", label: "DeepSeek V4 Flash", provider: "deepseek", description: "DeepSeek - V4 Flash, OpenAI-compatible, 1M ctx", contextWindow: 1e6 }
|
|
272
276
|
];
|
|
273
277
|
var PROVIDER_DEFAULT_MODEL = {
|
|
274
278
|
openai: "gpt-4o",
|
|
275
279
|
xai: "grok-build-0.1",
|
|
276
280
|
anthropic: "claude-sonnet-4-6",
|
|
277
281
|
minimax: "minimax-m3",
|
|
278
|
-
nvidia: "nvidia-nemotron-nano"
|
|
282
|
+
nvidia: "nvidia-nemotron-nano",
|
|
283
|
+
deepseek: "deepseek-v4-flash"
|
|
279
284
|
};
|
|
280
285
|
function getContextConfig(modelId) {
|
|
281
286
|
const info = exports.AI_MODEL_CATALOG.find((m) => m.id === modelId);
|
|
@@ -326,7 +331,8 @@ var require_ai_provider = __commonJS({
|
|
|
326
331
|
"minimax-m2.7-highspeed": { strong: "MiniMax-M2.7-highspeed", fast: "MiniMax-M2.7-highspeed", agent: "MiniMax-M2.7-highspeed" },
|
|
327
332
|
"claude-sonnet-4-6": { strong: "claude-sonnet-4-6", fast: "claude-sonnet-4-6", agent: "claude-sonnet-4-6" },
|
|
328
333
|
"nvidia-nemotron-nano": { strong: "nvidia/nemotron-3-nano-30b-a3b", fast: "nvidia/nemotron-3-nano-30b-a3b", agent: "nvidia/nemotron-3-nano-30b-a3b" },
|
|
329
|
-
"nvidia-kimi-k2.6": { strong: "moonshotai/kimi-k2.6", fast: "moonshotai/kimi-k2.6", agent: "moonshotai/kimi-k2.6" }
|
|
334
|
+
"nvidia-kimi-k2.6": { strong: "moonshotai/kimi-k2.6", fast: "moonshotai/kimi-k2.6", agent: "moonshotai/kimi-k2.6" },
|
|
335
|
+
"deepseek-v4-flash": { strong: "deepseek-v4-flash", fast: "deepseek-v4-flash", agent: "deepseek-v4-flash" }
|
|
330
336
|
};
|
|
331
337
|
var _requestModelStorage = new async_hooks_1.AsyncLocalStorage();
|
|
332
338
|
function withModelContext(modelId, fn) {
|
|
@@ -388,6 +394,9 @@ var require_ai_provider = __commonJS({
|
|
|
388
394
|
if (modelId === "nvidia/nemotron-3-nano-30b-a3b") {
|
|
389
395
|
return { chat_template_kwargs: { enable_thinking: false } };
|
|
390
396
|
}
|
|
397
|
+
if (modelId === "deepseek-v4-flash") {
|
|
398
|
+
return { thinking: { type: "disabled" } };
|
|
399
|
+
}
|
|
391
400
|
return {};
|
|
392
401
|
}
|
|
393
402
|
exports.MINIMAX_FALLBACK_CHAIN = ["MiniMax-M3", "MiniMax-M2.7-highspeed", "MiniMax-M2.7"];
|
|
@@ -489,6 +498,11 @@ var require_ai_provider = __commonJS({
|
|
|
489
498
|
if (!apiKey)
|
|
490
499
|
throw new Error("NVIDIA_API_KEY is required when using an NVIDIA model");
|
|
491
500
|
client = new openai_1.default({ apiKey, baseURL: process.env.NVIDIA_API_BASE ?? "https://integrate.api.nvidia.com/v1" });
|
|
501
|
+
} else if (provider === "deepseek") {
|
|
502
|
+
const apiKey = process.env.DEEPSEEK_API_KEY;
|
|
503
|
+
if (!apiKey)
|
|
504
|
+
throw new Error("DEEPSEEK_API_KEY is required when using a DeepSeek model");
|
|
505
|
+
client = new openai_1.default({ apiKey, baseURL: process.env.DEEPSEEK_API_BASE ?? "https://api.deepseek.com" });
|
|
492
506
|
} else {
|
|
493
507
|
const apiKey = process.env.OPENAI_API_KEY;
|
|
494
508
|
if (!apiKey)
|
|
@@ -504,6 +518,21 @@ var require_ai_provider = __commonJS({
|
|
|
504
518
|
function getClientForModel(modelId) {
|
|
505
519
|
return buildClientForProvider(getProviderForModel(modelId));
|
|
506
520
|
}
|
|
521
|
+
function shouldProxyAIThroughServerForLocalRunner() {
|
|
522
|
+
const serverUrl = (process.env.SERVER_URL ?? "").trim();
|
|
523
|
+
const token = (process.env.AUTH_TOKEN || process.env.RUNNER_TOKEN || "").trim();
|
|
524
|
+
return serverUrl.length > 0 && token.startsWith("urt_");
|
|
525
|
+
}
|
|
526
|
+
function getRunnerAIClient() {
|
|
527
|
+
if (shouldProxyAIThroughServerForLocalRunner())
|
|
528
|
+
return getServerProxiedAIClient();
|
|
529
|
+
return getAIClient();
|
|
530
|
+
}
|
|
531
|
+
function getRunnerClientForModel(modelId) {
|
|
532
|
+
if (shouldProxyAIThroughServerForLocalRunner())
|
|
533
|
+
return getServerProxiedClientForModel(modelId);
|
|
534
|
+
return getClientForModel(modelId);
|
|
535
|
+
}
|
|
507
536
|
function getModel(type) {
|
|
508
537
|
return MODEL_TASK_MAP[getActiveModelId()][type];
|
|
509
538
|
}
|
|
@@ -522,6 +551,8 @@ var require_ai_provider = __commonJS({
|
|
|
522
551
|
return !!process.env.ANTHROPIC_API_KEY?.trim();
|
|
523
552
|
if (provider === "nvidia")
|
|
524
553
|
return !!process.env.NVIDIA_API_KEY?.trim();
|
|
554
|
+
if (provider === "deepseek")
|
|
555
|
+
return !!process.env.DEEPSEEK_API_KEY?.trim();
|
|
525
556
|
return false;
|
|
526
557
|
}
|
|
527
558
|
function getAvailableModels() {
|
|
@@ -572,7 +603,7 @@ var require_ai_provider = __commonJS({
|
|
|
572
603
|
const create = async (params, options) => {
|
|
573
604
|
const modelId = typeof params.model === "string" ? params.model : "";
|
|
574
605
|
if (!modelId) {
|
|
575
|
-
throw new Error("server-proxied AI client requires params.model
|
|
606
|
+
throw new Error("server-proxied AI client requires params.model");
|
|
576
607
|
}
|
|
577
608
|
const body = {
|
|
578
609
|
modelId,
|
|
@@ -742,6 +773,21 @@ var require_mobile_source_parser = __commonJS({
|
|
|
742
773
|
const attrText = inner.slice(tag.length);
|
|
743
774
|
return { tag, attrs: parseAttributes(attrText), selfClosing };
|
|
744
775
|
}
|
|
776
|
+
function findTagEnd(xml, from) {
|
|
777
|
+
let quote = null;
|
|
778
|
+
for (let i = from; i < xml.length; i++) {
|
|
779
|
+
const ch = xml[i];
|
|
780
|
+
if (quote !== null) {
|
|
781
|
+
if (ch === quote)
|
|
782
|
+
quote = null;
|
|
783
|
+
} else if (ch === '"' || ch === "'") {
|
|
784
|
+
quote = ch;
|
|
785
|
+
} else if (ch === ">") {
|
|
786
|
+
return i;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return -1;
|
|
790
|
+
}
|
|
745
791
|
function tokenize(xml) {
|
|
746
792
|
if (!xml || typeof xml !== "string")
|
|
747
793
|
return null;
|
|
@@ -764,7 +810,7 @@ var require_mobile_source_parser = __commonJS({
|
|
|
764
810
|
i = end === -1 ? len : end + 3;
|
|
765
811
|
continue;
|
|
766
812
|
}
|
|
767
|
-
const gt = xml
|
|
813
|
+
const gt = findTagEnd(xml, lt + 1);
|
|
768
814
|
if (gt === -1)
|
|
769
815
|
break;
|
|
770
816
|
const tagBody = xml.slice(lt + 1, gt);
|
|
@@ -1549,6 +1595,25 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
1549
1595
|
` await driver.keys(text);`,
|
|
1550
1596
|
` }`,
|
|
1551
1597
|
`}`,
|
|
1598
|
+
``,
|
|
1599
|
+
`async function __clearFocused(driver: WebdriverIO.Browser) {`,
|
|
1600
|
+
` await (await driver.getActiveElement() as unknown as WebdriverIO.Element).clearValue();`,
|
|
1601
|
+
`}`,
|
|
1602
|
+
``,
|
|
1603
|
+
`async function __textCandidates(el: WebdriverIO.Element) {`,
|
|
1604
|
+
` const __values: string[] = [];`,
|
|
1605
|
+
` const __push = (__value: unknown) => {`,
|
|
1606
|
+
` if (typeof __value !== 'string') return;`,
|
|
1607
|
+
` const __trimmed = __value.trim();`,
|
|
1608
|
+
` if (__trimmed.length === 0 || __values.includes(__trimmed)) return;`,
|
|
1609
|
+
` __values.push(__trimmed);`,
|
|
1610
|
+
` };`,
|
|
1611
|
+
` __push(await el.getText().catch(() => ''));`,
|
|
1612
|
+
` for (const __attr of ['text', 'content-desc', 'contentDescription', 'name', 'label', 'value']) {`,
|
|
1613
|
+
` __push(await el.getAttribute(__attr).catch(() => ''));`,
|
|
1614
|
+
` }`,
|
|
1615
|
+
` return __values;`,
|
|
1616
|
+
`}`,
|
|
1552
1617
|
...hasSwipe ? [
|
|
1553
1618
|
``,
|
|
1554
1619
|
`// Read the device window with a few retries (matches the runtime executor).`,
|
|
@@ -1628,6 +1693,12 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
1628
1693
|
lines.push(` await __typeIntoFocused(driver, ${toJsStringLiteral(step.value ?? "")});`);
|
|
1629
1694
|
} else if (step.action === "clear" && hasSelector) {
|
|
1630
1695
|
lines.push(` await (await findFirst(driver, ${candidatesLiteral})).clearValue();`);
|
|
1696
|
+
} else if (step.action === "clear" && bounds) {
|
|
1697
|
+
const x = Math.round(bounds.x + bounds.width / 2);
|
|
1698
|
+
const y = Math.round(bounds.y + bounds.height / 2);
|
|
1699
|
+
lines.push(` await __tapAt(driver, ${x}, ${y});`);
|
|
1700
|
+
lines.push(` await driver.pause(${MOBILE_FOCUS_SETTLE_MS});`);
|
|
1701
|
+
lines.push(` await __clearFocused(driver);`);
|
|
1631
1702
|
} else if (step.action === "assertVisible" && hasSelector) {
|
|
1632
1703
|
lines.push(` if (!(await (await findFirst(driver, ${candidatesLiteral})).isDisplayed())) {`);
|
|
1633
1704
|
lines.push(` throw new Error('Assertion failed: element not displayed: ' + ${toJsStringLiteral(candidatesLiteral)});`);
|
|
@@ -1635,8 +1706,9 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
1635
1706
|
} else if (step.action === "assertText" && hasSelector) {
|
|
1636
1707
|
lines.push(` {`);
|
|
1637
1708
|
lines.push(` const __expected = ${toJsStringLiteral(step.assertion ?? "")};`);
|
|
1638
|
-
lines.push(`
|
|
1639
|
-
lines.push(`
|
|
1709
|
+
lines.push(` if (__expected.length === 0) throw new Error('assertText step is missing expected text');`);
|
|
1710
|
+
lines.push(` const __actual = await __textCandidates(await findFirst(driver, ${candidatesLiteral}));`);
|
|
1711
|
+
lines.push(` if (!__actual.some((__value) => __value.includes(__expected))) {`);
|
|
1640
1712
|
lines.push(` throw new Error('Assertion failed: expected text to contain ' + JSON.stringify(__expected) + ' but got ' + JSON.stringify(__actual));`);
|
|
1641
1713
|
lines.push(` }`);
|
|
1642
1714
|
lines.push(` }`);
|
|
@@ -4343,6 +4415,7 @@ var require_mobile_observation = __commonJS({
|
|
|
4343
4415
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4344
4416
|
exports.renderMobileObservation = renderMobileObservation;
|
|
4345
4417
|
exports.summarizeScreenForEvidence = summarizeScreenForEvidence;
|
|
4418
|
+
exports.buildCollectedElementsForScreen = buildCollectedElementsForScreen;
|
|
4346
4419
|
var DEFAULT_MAX_ELEMENTS = 60;
|
|
4347
4420
|
var MAX_LABEL_LENGTH = 80;
|
|
4348
4421
|
var EVIDENCE_LABEL_LIMIT = 40;
|
|
@@ -4384,6 +4457,46 @@ var require_mobile_observation = __commonJS({
|
|
|
4384
4457
|
}
|
|
4385
4458
|
return { elementCount, actionableCount, labels };
|
|
4386
4459
|
}
|
|
4460
|
+
function buildCollectedElementsForScreen(elements, opts = {}) {
|
|
4461
|
+
const navigationRefs = opts.navigationRefs ?? EMPTY_REF_SET;
|
|
4462
|
+
const out = [];
|
|
4463
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4464
|
+
for (const element of elements) {
|
|
4465
|
+
if (!element.actionable)
|
|
4466
|
+
continue;
|
|
4467
|
+
const collected = toCollectedPageElement(element, navigationRefs.has(element.ref));
|
|
4468
|
+
const identity = `${collected.elementType}|${collected.testId ?? ""}|${collected.label ?? ""}`;
|
|
4469
|
+
if (seen.has(identity))
|
|
4470
|
+
continue;
|
|
4471
|
+
seen.add(identity);
|
|
4472
|
+
out.push(collected);
|
|
4473
|
+
}
|
|
4474
|
+
return out;
|
|
4475
|
+
}
|
|
4476
|
+
var EMPTY_REF_SET = /* @__PURE__ */ new Set();
|
|
4477
|
+
function toCollectedPageElement(element, triggersNavigation) {
|
|
4478
|
+
const label = element.label?.trim() || element.text?.trim() || void 0;
|
|
4479
|
+
const stableId = element.accessibilityId?.trim() || element.resourceId?.trim() || void 0;
|
|
4480
|
+
const locators = [];
|
|
4481
|
+
if (stableId)
|
|
4482
|
+
locators.push({ strategy: "getByTestId", value: stableId, confidence: 0.95 });
|
|
4483
|
+
const xpath = element.xpath?.trim();
|
|
4484
|
+
if (xpath)
|
|
4485
|
+
locators.push({ strategy: "css", value: xpath, confidence: 0.4 });
|
|
4486
|
+
const collected = {
|
|
4487
|
+
elementType: element.role || "View",
|
|
4488
|
+
triggersNavigation
|
|
4489
|
+
};
|
|
4490
|
+
if (label)
|
|
4491
|
+
collected.label = label;
|
|
4492
|
+
if (stableId)
|
|
4493
|
+
collected.testId = stableId;
|
|
4494
|
+
if (locators.length > 0)
|
|
4495
|
+
collected.locators = locators;
|
|
4496
|
+
if (element.enabled === false)
|
|
4497
|
+
collected.isDisabled = true;
|
|
4498
|
+
return collected;
|
|
4499
|
+
}
|
|
4387
4500
|
function buildHeaderLine(screen) {
|
|
4388
4501
|
const parts = ["screen:"];
|
|
4389
4502
|
const name = screenDisplayName(screen.screenSignal);
|
|
@@ -4832,7 +4945,7 @@ var require_mobile_explorer = __commonJS({
|
|
|
4832
4945
|
}
|
|
4833
4946
|
return sections.join("\n");
|
|
4834
4947
|
}
|
|
4835
|
-
function buildSystemPrompt(mode, platform3, enableRecordJourney, maxIterations, appBrief, targetAppId) {
|
|
4948
|
+
function buildSystemPrompt(mode, platform3, enableRecordJourney, maxIterations, appBrief, targetAppId, existingScreens) {
|
|
4836
4949
|
const platformLabel = platform3 === "IOS" ? "iOS" : "Android";
|
|
4837
4950
|
const targetLine = targetAppId ? ` Target app package/bundle: ${targetAppId}.` : "";
|
|
4838
4951
|
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.
|
|
@@ -4878,13 +4991,18 @@ Max iterations: ${maxIterations}. You will be warned near the end.`;
|
|
|
4878
4991
|
## Application Brief
|
|
4879
4992
|
${appBrief}
|
|
4880
4993
|
Use this to prioritize which screens and flows to cover.` : "";
|
|
4881
|
-
|
|
4994
|
+
const existingScreensBlock = mode === "survey" && existingScreens && existingScreens.length > 0 ? `
|
|
4995
|
+
|
|
4996
|
+
## ALREADY DISCOVERED SCREENS (skip these; find new)
|
|
4997
|
+
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.
|
|
4998
|
+
${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.name}"` : ""}${s.screenType ? ` [${s.screenType}]` : ""}`).join("\n")}` : "";
|
|
4999
|
+
return `${mode === "survey" ? surveyBody : deepBody}${briefBlock}${existingScreensBlock}`;
|
|
4882
5000
|
}
|
|
4883
5001
|
function buildKickoffMessage(mode) {
|
|
4884
5002
|
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.";
|
|
4885
5003
|
}
|
|
4886
5004
|
async function runMobileExplorePhase(args) {
|
|
4887
|
-
const { ctx, driver, state: state2, config, onLog, onAICall, onScreenshot } = args;
|
|
5005
|
+
const { ctx, driver, state: state2, config, onLog, onAICall, onScreenshot, onScreenshotUpload, existingScreens } = args;
|
|
4888
5006
|
const logs2 = [];
|
|
4889
5007
|
const log2 = (line) => {
|
|
4890
5008
|
logs2.push(line);
|
|
@@ -4908,6 +5026,9 @@ Use this to prioritize which screens and flows to cover.` : "";
|
|
|
4908
5026
|
log2(`Budget: ${maxIterations} iterations | app: ${ctx.target?.appId ?? "(unknown)"}`);
|
|
4909
5027
|
const screenshots = [];
|
|
4910
5028
|
const pageScreenshots = {};
|
|
5029
|
+
const pageScreenshotKeys = {};
|
|
5030
|
+
const uploadedScreenIds = /* @__PURE__ */ new Set();
|
|
5031
|
+
const pendingScreenshotUploads = [];
|
|
4911
5032
|
const collectedTransitions = [];
|
|
4912
5033
|
const journeys = [];
|
|
4913
5034
|
const counters = {
|
|
@@ -4926,6 +5047,14 @@ Use this to prioritize which screens and flows to cover.` : "";
|
|
|
4926
5047
|
screenshots.push(base64);
|
|
4927
5048
|
if (screenId)
|
|
4928
5049
|
pageScreenshots[screenId] = base64;
|
|
5050
|
+
if (screenId && onScreenshotUpload && !uploadedScreenIds.has(screenId)) {
|
|
5051
|
+
uploadedScreenIds.add(screenId);
|
|
5052
|
+
pendingScreenshotUploads.push(onScreenshotUpload(screenId, base64).then((key) => {
|
|
5053
|
+
if (key)
|
|
5054
|
+
pageScreenshotKeys[screenId] = key;
|
|
5055
|
+
}).catch(() => {
|
|
5056
|
+
}));
|
|
5057
|
+
}
|
|
4929
5058
|
try {
|
|
4930
5059
|
onScreenshot?.(base64);
|
|
4931
5060
|
} catch {
|
|
@@ -5030,7 +5159,7 @@ Use this to prioritize which screens and flows to cover.` : "";
|
|
|
5030
5159
|
return null;
|
|
5031
5160
|
return { element, bounds: element.bounds };
|
|
5032
5161
|
};
|
|
5033
|
-
const systemPrompt = buildSystemPrompt(mode, platform3, enableRecordJourney, maxIterations, ctx.appBrief, targetAppId);
|
|
5162
|
+
const systemPrompt = buildSystemPrompt(mode, platform3, enableRecordJourney, maxIterations, ctx.appBrief, targetAppId, existingScreens);
|
|
5034
5163
|
const kickoff = buildKickoffMessage(mode);
|
|
5035
5164
|
const recentExchanges = [];
|
|
5036
5165
|
const transientDirectives = [];
|
|
@@ -5238,6 +5367,9 @@ Exploration complete: ${summary}`);
|
|
|
5238
5367
|
summary = `Explore phase aborted (${truncatedReason}) after mapping ${state2.getScreenCount()} screen(s).`;
|
|
5239
5368
|
}
|
|
5240
5369
|
}
|
|
5370
|
+
if (pendingScreenshotUploads.length > 0) {
|
|
5371
|
+
await Promise.allSettled(pendingScreenshotUploads);
|
|
5372
|
+
}
|
|
5241
5373
|
const collectedPages = state2.buildInMemoryScreenMap();
|
|
5242
5374
|
counters.pagesVisited = collectedPages.length;
|
|
5243
5375
|
if (counters.pagesDiscovered < collectedPages.length) {
|
|
@@ -5272,6 +5404,9 @@ Exploration complete: ${summary}`);
|
|
|
5272
5404
|
if (Object.keys(pageScreenshots).length > 0) {
|
|
5273
5405
|
result.pageScreenshots = pageScreenshots;
|
|
5274
5406
|
}
|
|
5407
|
+
if (Object.keys(pageScreenshotKeys).length > 0) {
|
|
5408
|
+
result.pageScreenshotKeys = pageScreenshotKeys;
|
|
5409
|
+
}
|
|
5275
5410
|
if (truncated) {
|
|
5276
5411
|
result.truncated = true;
|
|
5277
5412
|
result.truncatedReason = truncatedReason;
|
|
@@ -5773,6 +5908,44 @@ var require_snapshot_parser = __commonJS({
|
|
|
5773
5908
|
};
|
|
5774
5909
|
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;
|
|
5775
5910
|
var GENERIC_LABEL_RE = /^(input field|textbox|button|link|item|field|value|tab)$/i;
|
|
5911
|
+
var RICH_CONTEXT_ROLES = /* @__PURE__ */ new Set(["table", "grid", "row", "radiogroup"]);
|
|
5912
|
+
var RICH_CONTEXT_LABEL_MAX = 60;
|
|
5913
|
+
var VALUE_BEARING_ROLES = /* @__PURE__ */ new Set(["textbox", "searchbox", "combobox", "spinbutton", "listbox", "slider"]);
|
|
5914
|
+
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;
|
|
5915
|
+
var REDACTED_VALUE = "[redacted]";
|
|
5916
|
+
var VALUE_TEXT_MAX = 120;
|
|
5917
|
+
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;
|
|
5918
|
+
var STATIC_MESSAGES_MAX = 12;
|
|
5919
|
+
var OPTIONS_PER_ELEMENT_MAX = 24;
|
|
5920
|
+
var URL_CHILD_LINE_RE = /^(\s*)- \/url:\s*(.+)$/;
|
|
5921
|
+
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;
|
|
5922
|
+
function stripYamlQuotes(raw) {
|
|
5923
|
+
const trimmed = raw.trim();
|
|
5924
|
+
if (trimmed.length >= 2 && (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
5925
|
+
return trimmed.slice(1, -1);
|
|
5926
|
+
}
|
|
5927
|
+
return trimmed;
|
|
5928
|
+
}
|
|
5929
|
+
function extractTrailingText(attrs) {
|
|
5930
|
+
const lastBracket = attrs.lastIndexOf("]");
|
|
5931
|
+
const tail = lastBracket >= 0 ? attrs.slice(lastBracket + 1) : attrs;
|
|
5932
|
+
const match = /^\s*:\s+(.+)$/.exec(tail);
|
|
5933
|
+
if (!match)
|
|
5934
|
+
return null;
|
|
5935
|
+
const text = stripYamlQuotes(match[1]);
|
|
5936
|
+
if (!text || /^(?:\||\|-|>|>-)$/.test(text))
|
|
5937
|
+
return null;
|
|
5938
|
+
return text;
|
|
5939
|
+
}
|
|
5940
|
+
function isSensitiveValueField(name, placeholder) {
|
|
5941
|
+
return SENSITIVE_VALUE_LABEL_RE.test(`${name ?? ""} ${placeholder ?? ""}`);
|
|
5942
|
+
}
|
|
5943
|
+
function scrubMessageText(text) {
|
|
5944
|
+
return text.replace(MESSAGE_TOKEN_RE, REDACTED_VALUE);
|
|
5945
|
+
}
|
|
5946
|
+
function capText(text, max) {
|
|
5947
|
+
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
5948
|
+
}
|
|
5776
5949
|
function isProbablyDynamicTestId(testId) {
|
|
5777
5950
|
if (testId.length < 6)
|
|
5778
5951
|
return false;
|
|
@@ -5848,9 +6021,12 @@ var require_snapshot_parser = __commonJS({
|
|
|
5848
6021
|
}
|
|
5849
6022
|
return e.name;
|
|
5850
6023
|
}
|
|
5851
|
-
function parseSnapshot(snapshotText) {
|
|
6024
|
+
function parseSnapshot(snapshotText, options = {}) {
|
|
6025
|
+
const richParse = options.richParse ?? process.env.DISCOVERY_SNAPSHOT_RICH_PARSE === "true";
|
|
5852
6026
|
const lines = snapshotText.split("\n");
|
|
5853
6027
|
const elements = [];
|
|
6028
|
+
const liveMessages = [];
|
|
6029
|
+
const textMessages = [];
|
|
5854
6030
|
const seenRefs = /* @__PURE__ */ new Set();
|
|
5855
6031
|
let parsedElementLineCount = 0;
|
|
5856
6032
|
let genericLabelCount = 0;
|
|
@@ -5859,6 +6035,21 @@ var require_snapshot_parser = __commonJS({
|
|
|
5859
6035
|
const contextStack = [];
|
|
5860
6036
|
const completedForms = [];
|
|
5861
6037
|
for (const line of lines) {
|
|
6038
|
+
if (richParse) {
|
|
6039
|
+
const urlMatch = URL_CHILD_LINE_RE.exec(line);
|
|
6040
|
+
if (urlMatch) {
|
|
6041
|
+
const urlIndent = urlMatch[1].length;
|
|
6042
|
+
for (let i = elements.length - 1; i >= 0; i--) {
|
|
6043
|
+
const el = elements[i];
|
|
6044
|
+
if (el.indentDepth < urlIndent) {
|
|
6045
|
+
if (el.role === "link" && !el.href)
|
|
6046
|
+
el.href = capText(stripYamlQuotes(urlMatch[2]), 300);
|
|
6047
|
+
break;
|
|
6048
|
+
}
|
|
6049
|
+
}
|
|
6050
|
+
continue;
|
|
6051
|
+
}
|
|
6052
|
+
}
|
|
5862
6053
|
const match = ELEMENT_LINE_RE.exec(line);
|
|
5863
6054
|
if (!match)
|
|
5864
6055
|
continue;
|
|
@@ -5880,7 +6071,7 @@ var require_snapshot_parser = __commonJS({
|
|
|
5880
6071
|
}
|
|
5881
6072
|
const headingLevelMatch = LEVEL_RE.exec(attrs);
|
|
5882
6073
|
const headingLevel = headingLevelMatch ? Number.parseInt(headingLevelMatch[1], 10) : null;
|
|
5883
|
-
const contextLabel = buildContextLabel(role, name ?? null, headingLevel);
|
|
6074
|
+
const contextLabel = buildContextLabel(role, name ?? null, headingLevel, richParse);
|
|
5884
6075
|
if (contextLabel) {
|
|
5885
6076
|
contextStack.push({ indent: indentDepth, label: contextLabel });
|
|
5886
6077
|
}
|
|
@@ -5893,6 +6084,40 @@ var require_snapshot_parser = __commonJS({
|
|
|
5893
6084
|
});
|
|
5894
6085
|
continue;
|
|
5895
6086
|
}
|
|
6087
|
+
if (richParse) {
|
|
6088
|
+
if (role === "option" && name) {
|
|
6089
|
+
for (let i = elements.length - 1; i >= 0; i--) {
|
|
6090
|
+
const el = elements[i];
|
|
6091
|
+
if (el.indentDepth < indentDepth) {
|
|
6092
|
+
if (el.role === "combobox" || el.role === "listbox") {
|
|
6093
|
+
el.options ??= [];
|
|
6094
|
+
if (el.options.length < OPTIONS_PER_ELEMENT_MAX) {
|
|
6095
|
+
el.options.push({ label: capText(name, 80), selected: parseBooleanState(SELECTED_RE.exec(attrs)) === true });
|
|
6096
|
+
}
|
|
6097
|
+
}
|
|
6098
|
+
break;
|
|
6099
|
+
}
|
|
6100
|
+
}
|
|
6101
|
+
}
|
|
6102
|
+
if (role === "alert" || role === "status" || role === "paragraph" || role === "text") {
|
|
6103
|
+
const text = extractTrailingText(attrs) ?? (name || null);
|
|
6104
|
+
const isLiveRegion = role === "alert" || role === "status";
|
|
6105
|
+
if (text && (isLiveRegion || SIGNAL_TEXT_RE.test(text))) {
|
|
6106
|
+
const capped = capText(scrubMessageText(text), VALUE_TEXT_MAX);
|
|
6107
|
+
const ref2 = REF_RE.exec(attrs)?.[1] ?? null;
|
|
6108
|
+
if (isLiveRegion) {
|
|
6109
|
+
const textDupIndex = textMessages.findIndex((m) => m.text === capped);
|
|
6110
|
+
if (textDupIndex >= 0)
|
|
6111
|
+
textMessages.splice(textDupIndex, 1);
|
|
6112
|
+
if (liveMessages.length < STATIC_MESSAGES_MAX && !liveMessages.some((m) => m.text === capped)) {
|
|
6113
|
+
liveMessages.push({ role, text: capped, ref: ref2 });
|
|
6114
|
+
}
|
|
6115
|
+
} else if (textMessages.length < STATIC_MESSAGES_MAX && !textMessages.some((m) => m.text === capped) && !liveMessages.some((m) => m.text === capped)) {
|
|
6116
|
+
textMessages.push({ role: "text", text: capped, ref: ref2 });
|
|
6117
|
+
}
|
|
6118
|
+
}
|
|
6119
|
+
}
|
|
6120
|
+
}
|
|
5896
6121
|
if (!INTERACTIVE_ROLES.has(role))
|
|
5897
6122
|
continue;
|
|
5898
6123
|
const refMatch = REF_RE.exec(attrs);
|
|
@@ -5925,6 +6150,13 @@ var require_snapshot_parser = __commonJS({
|
|
|
5925
6150
|
}
|
|
5926
6151
|
const isLink = role === "link";
|
|
5927
6152
|
const isSubmitButton = role === "button" && name != null && SUBMIT_LABEL_RE.test(name);
|
|
6153
|
+
let value = null;
|
|
6154
|
+
if (richParse && VALUE_BEARING_ROLES.has(role)) {
|
|
6155
|
+
const trailing = extractTrailingText(attrs);
|
|
6156
|
+
if (trailing) {
|
|
6157
|
+
value = isSensitiveValueField(name ?? null, placeholder) ? REDACTED_VALUE : capText(trailing, VALUE_TEXT_MAX);
|
|
6158
|
+
}
|
|
6159
|
+
}
|
|
5928
6160
|
const element = {
|
|
5929
6161
|
ref,
|
|
5930
6162
|
role,
|
|
@@ -5946,7 +6178,10 @@ var require_snapshot_parser = __commonJS({
|
|
|
5946
6178
|
pressed,
|
|
5947
6179
|
headingLevel,
|
|
5948
6180
|
currentState,
|
|
5949
|
-
containerPath
|
|
6181
|
+
containerPath,
|
|
6182
|
+
href: null,
|
|
6183
|
+
value,
|
|
6184
|
+
options: null
|
|
5950
6185
|
};
|
|
5951
6186
|
if (isGenericLabel(element.name))
|
|
5952
6187
|
genericLabelCount++;
|
|
@@ -5974,6 +6209,9 @@ var require_snapshot_parser = __commonJS({
|
|
|
5974
6209
|
return {
|
|
5975
6210
|
elements,
|
|
5976
6211
|
formGroups: completedForms,
|
|
6212
|
+
// live regions first: they carry their own reservation so an end-of-body toast can
|
|
6213
|
+
// never be starved out by signal-matching plain text earlier in the document.
|
|
6214
|
+
staticMessages: [...liveMessages, ...textMessages].slice(0, STATIC_MESSAGES_MAX),
|
|
5977
6215
|
rawLineCount: lines.length,
|
|
5978
6216
|
parsedElementLineCount,
|
|
5979
6217
|
genericLabelCount,
|
|
@@ -6084,12 +6322,17 @@ var require_snapshot_parser = __commonJS({
|
|
|
6084
6322
|
return "mixed";
|
|
6085
6323
|
return false;
|
|
6086
6324
|
}
|
|
6087
|
-
function buildContextLabel(role, name, headingLevel) {
|
|
6325
|
+
function buildContextLabel(role, name, headingLevel, richContext = false) {
|
|
6088
6326
|
if (role === "heading") {
|
|
6089
6327
|
if (!name)
|
|
6090
6328
|
return null;
|
|
6091
6329
|
return headingLevel ? `heading${headingLevel}:${name}` : `heading:${name}`;
|
|
6092
6330
|
}
|
|
6331
|
+
if (richContext && RICH_CONTEXT_ROLES.has(role)) {
|
|
6332
|
+
if (!name)
|
|
6333
|
+
return null;
|
|
6334
|
+
return `${role}:${capText(name.replace(/>/g, "\u203A"), RICH_CONTEXT_LABEL_MAX)}`;
|
|
6335
|
+
}
|
|
6093
6336
|
if (!CONTEXT_ROLES.has(role))
|
|
6094
6337
|
return null;
|
|
6095
6338
|
if (name)
|
|
@@ -7799,7 +8042,7 @@ Each entry is exactly one of:
|
|
|
7799
8042
|
const systemPrompt = options.systemPromptOverride ?? (context.mode === "survey" ? SURVEY_SYSTEM_PROMPT : exports.SCENARIO_SYSTEM_PROMPT);
|
|
7800
8043
|
const userPrompt = options.userPromptOverride ?? formatEvidenceForPlanner(compressed, evidence, context);
|
|
7801
8044
|
logs2.push(`AI planner: ~${Math.ceil(userPrompt.length / 3.5)} input tokens`);
|
|
7802
|
-
const aiClient = options.aiClient ?? (options.modelOverride ? (0, ai_provider_js_1.
|
|
8045
|
+
const aiClient = options.aiClient ?? (options.modelOverride ? (0, ai_provider_js_1.getRunnerClientForModel)(options.modelOverride) : (0, ai_provider_js_1.getRunnerAIClient)());
|
|
7803
8046
|
const messages = [
|
|
7804
8047
|
{ role: "system", content: systemPrompt },
|
|
7805
8048
|
{ role: "user", content: userPrompt }
|
|
@@ -7807,18 +8050,22 @@ Each entry is exactly one of:
|
|
|
7807
8050
|
const callStart = Date.now();
|
|
7808
8051
|
let response;
|
|
7809
8052
|
try {
|
|
7810
|
-
response = await (0, ai_retry_js_1.
|
|
7811
|
-
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
|
|
8053
|
+
response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
8054
|
+
arm();
|
|
8055
|
+
return aiClient.chat.completions.create({
|
|
8056
|
+
model: agentModel,
|
|
8057
|
+
messages,
|
|
8058
|
+
max_tokens: plannerMaxTokens,
|
|
8059
|
+
temperature: 0.3,
|
|
8060
|
+
...(0, ai_provider_js_1.getJsonObjectResponseFormat)(agentModel),
|
|
8061
|
+
...(0, ai_provider_js_1.getNoThinkingRequestOptions)(agentModel)
|
|
8062
|
+
}, { signal });
|
|
8063
|
+
}), {
|
|
7818
8064
|
label: "ai-planner",
|
|
7819
8065
|
baseDelayMs: 5e3,
|
|
7820
8066
|
maxRetries: 2,
|
|
7821
|
-
|
|
8067
|
+
deferTimeoutUntilArmed: true,
|
|
8068
|
+
onRetry: (attempt, reason, delay) => logs2.push(`AI planner call failed (${reason}) \u2014 retry ${attempt} in ${Math.round(delay / 1e3)}s`)
|
|
7822
8069
|
});
|
|
7823
8070
|
} catch (err) {
|
|
7824
8071
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -7884,18 +8131,25 @@ Each entry is exactly one of:
|
|
|
7884
8131
|
const retryStart = Date.now();
|
|
7885
8132
|
let retryResponse = null;
|
|
7886
8133
|
try {
|
|
7887
|
-
retryResponse = await (0, ai_retry_js_1.
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
8134
|
+
retryResponse = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
8135
|
+
arm();
|
|
8136
|
+
return aiClient.chat.completions.create({
|
|
8137
|
+
model: agentModel,
|
|
8138
|
+
messages: [
|
|
8139
|
+
...messages,
|
|
8140
|
+
{ role: "assistant", content: raw },
|
|
8141
|
+
{ role: "user", content: retryContext }
|
|
8142
|
+
],
|
|
8143
|
+
max_tokens: plannerMaxTokens,
|
|
8144
|
+
temperature: 0.3,
|
|
8145
|
+
...(0, ai_provider_js_1.getJsonObjectResponseFormat)(agentModel),
|
|
8146
|
+
...(0, ai_provider_js_1.getNoThinkingRequestOptions)(agentModel)
|
|
8147
|
+
}, { signal });
|
|
8148
|
+
}), {
|
|
8149
|
+
label: "ai-planner-retry",
|
|
8150
|
+
deferTimeoutUntilArmed: true,
|
|
8151
|
+
onRetry: (attempt, reason, delay) => logs2.push(`AI planner retry call failed (${reason}) \u2014 retry ${attempt} in ${Math.round(delay / 1e3)}s`)
|
|
8152
|
+
});
|
|
7899
8153
|
} catch (err) {
|
|
7900
8154
|
const message = err instanceof Error ? err.message : String(err);
|
|
7901
8155
|
logs2.push(`AI planner retry failed (${message}) \u2014 using first-pass result`);
|
|
@@ -8423,12 +8677,12 @@ ${constraints.join("\n")}`);
|
|
|
8423
8677
|
const orderedScreens = targetAppScreens(graph, appId).filter((state2) => state2.screenType?.toLowerCase() !== "error").sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
8424
8678
|
const includedScreenIds = new Set(orderedScreens.map((state2) => state2.screenId));
|
|
8425
8679
|
for (const state2 of orderedScreens) {
|
|
8426
|
-
const groupName = state2.screenType === "auth" ? "Authentication" : screenDisplayName(state2);
|
|
8680
|
+
const groupName = state2.screenType?.toLowerCase() === "auth" ? "Authentication" : screenDisplayName(state2);
|
|
8427
8681
|
const group = groups.get(groupName) ?? {
|
|
8428
8682
|
name: groupName,
|
|
8429
8683
|
description: `Native mobile screens related to ${groupName}.`,
|
|
8430
8684
|
routes: [],
|
|
8431
|
-
criticality: state2.screenType === "auth" ? "critical" : "medium",
|
|
8685
|
+
criticality: state2.screenType?.toLowerCase() === "auth" ? "critical" : "medium",
|
|
8432
8686
|
pages: [],
|
|
8433
8687
|
flows: []
|
|
8434
8688
|
};
|
|
@@ -8451,7 +8705,7 @@ ${constraints.join("\n")}`);
|
|
|
8451
8705
|
const to = graph.screens.get(transition.toScreenId);
|
|
8452
8706
|
if (!from || !to)
|
|
8453
8707
|
continue;
|
|
8454
|
-
const groupName = from.screenType === "auth" ? "Authentication" : screenDisplayName(from);
|
|
8708
|
+
const groupName = from.screenType?.toLowerCase() === "auth" ? "Authentication" : screenDisplayName(from);
|
|
8455
8709
|
const group = groups.get(groupName);
|
|
8456
8710
|
if (!group)
|
|
8457
8711
|
continue;
|
|
@@ -8496,9 +8750,9 @@ ${constraints.join("\n")}`);
|
|
|
8496
8750
|
scenarios.push({
|
|
8497
8751
|
name,
|
|
8498
8752
|
type: "HAPPY_PATH",
|
|
8499
|
-
priority: state2.screenType === "auth" ? 1 : 3,
|
|
8753
|
+
priority: state2.screenType?.toLowerCase() === "auth" ? 1 : 3,
|
|
8500
8754
|
variant: "happy",
|
|
8501
|
-
featureGroup: state2.screenType === "auth" ? "Authentication" : screenName,
|
|
8755
|
+
featureGroup: state2.screenType?.toLowerCase() === "auth" ? "Authentication" : screenName,
|
|
8502
8756
|
goal: `User can reach and inspect the ${screenName} screen`,
|
|
8503
8757
|
steps,
|
|
8504
8758
|
expectedOutcomes: [{
|
|
@@ -8644,6 +8898,7 @@ var require_mobile_generator = __commonJS({
|
|
|
8644
8898
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
8645
8899
|
var mobile_boundary_js_1 = require_mobile_boundary();
|
|
8646
8900
|
var MAX_SCENARIO_ITERATIONS = 30;
|
|
8901
|
+
var MAX_CONSECUTIVE_INFRA_FAILURES = 3;
|
|
8647
8902
|
var MAX_RECENT_EXCHANGES = 24;
|
|
8648
8903
|
var OBSERVATION_MAX_ELEMENTS = 50;
|
|
8649
8904
|
function finalizeAction(input) {
|
|
@@ -9368,14 +9623,31 @@ ${statePacket}` },
|
|
|
9368
9623
|
if (!snapshot || snapshot.screen.elements.length === 0)
|
|
9369
9624
|
return null;
|
|
9370
9625
|
const elements = snapshot.screen.elements;
|
|
9371
|
-
const
|
|
9372
|
-
const
|
|
9626
|
+
const stableAnchors = elements.filter((el) => hasStableId(el) && !isAlwaysPresentContainer(el));
|
|
9627
|
+
const contentfulNonButton = stableAnchors.find((el) => !/button/i.test(el.role) && (nonEmpty2(el.label) || nonEmpty2(el.text)));
|
|
9628
|
+
if (contentfulNonButton)
|
|
9629
|
+
return contentfulNonButton;
|
|
9630
|
+
const nonButton = stableAnchors.find((el) => !/button/i.test(el.role));
|
|
9373
9631
|
if (nonButton)
|
|
9374
9632
|
return nonButton;
|
|
9375
|
-
if (
|
|
9376
|
-
return
|
|
9377
|
-
const labelled = elements.find((el) => nonEmpty2(el.label) || nonEmpty2(el.text));
|
|
9378
|
-
return labelled ?? elements[0];
|
|
9633
|
+
if (stableAnchors.length > 0)
|
|
9634
|
+
return stableAnchors[0];
|
|
9635
|
+
const labelled = elements.find((el) => !isAlwaysPresentContainer(el) && (nonEmpty2(el.label) || nonEmpty2(el.text)));
|
|
9636
|
+
return labelled ?? elements.find((el) => !isAlwaysPresentContainer(el)) ?? elements[0];
|
|
9637
|
+
}
|
|
9638
|
+
var ALWAYS_PRESENT_CONTAINER_ROLES = /^(XCUIElementTypeApplication|XCUIElementTypeWindow)$/;
|
|
9639
|
+
var ALWAYS_PRESENT_CONTAINER_RESOURCE_IDS = /* @__PURE__ */ new Set([
|
|
9640
|
+
"android:id/content",
|
|
9641
|
+
"android:id/decor",
|
|
9642
|
+
"android:id/action_bar_root",
|
|
9643
|
+
"android:id/statusBarBackground",
|
|
9644
|
+
"android:id/navigationBarBackground"
|
|
9645
|
+
]);
|
|
9646
|
+
function isAlwaysPresentContainer(element) {
|
|
9647
|
+
if (ALWAYS_PRESENT_CONTAINER_ROLES.test(element.role))
|
|
9648
|
+
return true;
|
|
9649
|
+
const resourceId = element.resourceId?.trim();
|
|
9650
|
+
return !!resourceId && ALWAYS_PRESENT_CONTAINER_RESOURCE_IDS.has(resourceId);
|
|
9379
9651
|
}
|
|
9380
9652
|
function appendHybridFloor(actions, recording, platform3) {
|
|
9381
9653
|
const anchor = pickLandingAnchor(recording.lastSnapshot);
|
|
@@ -9434,7 +9706,7 @@ ${statePacket}` },
|
|
|
9434
9706
|
return test;
|
|
9435
9707
|
}
|
|
9436
9708
|
async function runMobileGeneratePhase(args) {
|
|
9437
|
-
const { scenarios, driver, ctx, config, onLog, onAICall, onTestGenerated, onScreenshot } = args;
|
|
9709
|
+
const { scenarios: allScenarios, driver, ctx, config, onLog, onAICall, onTestGenerated, onScreenshot, skipScenarioNames, onInfraStop } = args;
|
|
9438
9710
|
const log2 = (line) => {
|
|
9439
9711
|
try {
|
|
9440
9712
|
onLog?.(line);
|
|
@@ -9449,8 +9721,16 @@ ${statePacket}` },
|
|
|
9449
9721
|
const aiClient = modelOverride ? (0, ai_provider_js_1.getMobileClientForModel)(modelOverride) : (0, ai_provider_js_1.getMobileAIClient)();
|
|
9450
9722
|
const auditGroupId = `mobile-generate-${(0, crypto_1.randomUUID)().slice(0, 8)}`;
|
|
9451
9723
|
const targetAppId = ctx.target?.appId?.trim() || void 0;
|
|
9724
|
+
const skipSet = new Set(skipScenarioNames ?? []);
|
|
9725
|
+
const scenarios = skipSet.size > 0 ? allScenarios.filter((s) => !skipSet.has(s.name)) : allScenarios;
|
|
9726
|
+
if (skipSet.size > 0) {
|
|
9727
|
+
log2(`Resume: skipping ${allScenarios.length - scenarios.length} already-generated scenario(s); ${scenarios.length} remaining to generate`);
|
|
9728
|
+
}
|
|
9452
9729
|
log2(`Mobile generator starting \u2014 platform: ${platform3}, provider: ${provider}, model: ${agentModel}, scenarios: ${scenarios.length}`);
|
|
9453
9730
|
const tests = [];
|
|
9731
|
+
const generatedScenarioNames = /* @__PURE__ */ new Set();
|
|
9732
|
+
let consecutiveInfraFailures = 0;
|
|
9733
|
+
let infraStopped = false;
|
|
9454
9734
|
for (let i = 0; i < scenarios.length; i++) {
|
|
9455
9735
|
const scenario = scenarios[i];
|
|
9456
9736
|
log2(`
|
|
@@ -9471,6 +9751,7 @@ ${statePacket}` },
|
|
|
9471
9751
|
targetAppId,
|
|
9472
9752
|
log: log2
|
|
9473
9753
|
});
|
|
9754
|
+
consecutiveInfraFailures = 0;
|
|
9474
9755
|
const test = assembleTest({ scenario, recording, ctx, platform: platform3, log: log2 });
|
|
9475
9756
|
if (!test) {
|
|
9476
9757
|
log2(` scenario "${scenario.name}" produced no compilable test \u2014 skipped.`);
|
|
@@ -9487,13 +9768,31 @@ ${statePacket}` },
|
|
|
9487
9768
|
log2(` onTestGenerated callback error: ${cbErr instanceof Error ? cbErr.message : String(cbErr)}`);
|
|
9488
9769
|
}
|
|
9489
9770
|
tests.push(test);
|
|
9771
|
+
generatedScenarioNames.add(scenario.name);
|
|
9490
9772
|
} catch (err) {
|
|
9773
|
+
if ((0, ai_retry_js_1.isAIProviderError)(err)) {
|
|
9774
|
+
consecutiveInfraFailures += 1;
|
|
9775
|
+
log2(` scenario "${scenario.name}" failed \u2014 AI provider error (${consecutiveInfraFailures}/${MAX_CONSECUTIVE_INFRA_FAILURES}): ${err instanceof Error ? err.message : String(err)}`);
|
|
9776
|
+
if (consecutiveInfraFailures >= MAX_CONSECUTIVE_INFRA_FAILURES) {
|
|
9777
|
+
const remainingScenarioNames = scenarios.filter((s) => !generatedScenarioNames.has(s.name)).map((s) => s.name);
|
|
9778
|
+
infraStopped = true;
|
|
9779
|
+
log2(`
|
|
9780
|
+
\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.`);
|
|
9781
|
+
try {
|
|
9782
|
+
onInfraStop?.({ remainingScenarioNames, reason: "ai_rate_limit" });
|
|
9783
|
+
} catch (cbErr) {
|
|
9784
|
+
log2(` onInfraStop callback error: ${cbErr instanceof Error ? cbErr.message : String(cbErr)}`);
|
|
9785
|
+
}
|
|
9786
|
+
break;
|
|
9787
|
+
}
|
|
9788
|
+
continue;
|
|
9789
|
+
}
|
|
9491
9790
|
log2(` scenario "${scenario.name}" failed: ${err instanceof Error ? err.message : String(err)} \u2014 skipping.`);
|
|
9492
9791
|
continue;
|
|
9493
9792
|
}
|
|
9494
9793
|
}
|
|
9495
9794
|
log2(`
|
|
9496
|
-
Mobile generate phase complete: ${tests.length}/${scenarios.length} test(s) generated (${tests.filter((t) => t.verified).length} verified).`);
|
|
9795
|
+
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" : ""}.`);
|
|
9497
9796
|
return tests;
|
|
9498
9797
|
}
|
|
9499
9798
|
}
|
|
@@ -12772,6 +13071,10 @@ var require_snapshot_observation = __commonJS({
|
|
|
12772
13071
|
var snapshot_parser_js_1 = require_snapshot_parser();
|
|
12773
13072
|
var GENERIC_LABEL_RE = /^(input field|textbox|button|link|item|field|value|tab)$/i;
|
|
12774
13073
|
var NOISE_LABEL_RE = /^(skip\s+to|skip\s+nav|back\s+to\s+top|cookie|accept\s+(all\s+)?cookies)$/i;
|
|
13074
|
+
var DIALOG_SEGMENT_RE = /(?:^|>\s*)(?:alert)?dialog\b/i;
|
|
13075
|
+
var FORM_SEGMENT_RE = /(?:^|>\s*)form\b/i;
|
|
13076
|
+
var TABPANEL_SEGMENT_RE = /(?:^|>\s*)tabpanel\b/i;
|
|
13077
|
+
var DIALOG_NAME_SEGMENT_RE = /(?:^|>\s*)(?:alert)?dialog:([^>]+)/i;
|
|
12775
13078
|
var INTERACTIVE_ROLE_RE = /^(button|link|textbox|searchbox|checkbox|radio|switch|combobox|listbox|option|menuitem|tab|slider|spinbutton|textarea)$/i;
|
|
12776
13079
|
var INTERACTIVE_ELEMENT_TYPE_RE = /^(button|link|input|textarea|select|checkbox|radio|switch|tab|combobox|listbox|option|menuitem|slider|spinbutton)$/i;
|
|
12777
13080
|
function renderRetrievedSnapshotMemoryBlock(priorSnapshotEvidence, priorDiscoveryCheckpoints, options = {}) {
|
|
@@ -13016,10 +13319,12 @@ var require_snapshot_observation = __commonJS({
|
|
|
13016
13319
|
summary
|
|
13017
13320
|
}));
|
|
13018
13321
|
}
|
|
13322
|
+
var TEXT_DISAMBIGUATING_INPUT_TYPES = /* @__PURE__ */ new Set(["password", "text", "email", "tel", "number", "search", "url", "date", "datetime-local", "time", "month", "week"]);
|
|
13019
13323
|
function renderCompactSnapshotBlock(page, context = {}, options = {}) {
|
|
13020
13324
|
const metrics = page.snapshotMetrics ?? page.snapshotArtifact?.metrics;
|
|
13021
13325
|
const sourceKind = page.snapshotMeta?.sourceKind ?? page.snapshotArtifact?.sourceKind ?? "missing";
|
|
13022
13326
|
const artifact = page.snapshotArtifact;
|
|
13327
|
+
const richRender = options.richRender ?? process.env.DISCOVERY_SNAPSHOT_RICH_RENDER === "true";
|
|
13023
13328
|
const activeScope = inferActiveSnapshotScope(page);
|
|
13024
13329
|
const ranked = rankSnapshotElements(getScopedSnapshotElements(page, activeScope), withScopeContext(context, activeScope));
|
|
13025
13330
|
const allInteractive = ranked.filter(({ element }) => isInteractiveCandidate(element)).filter(({ element }) => {
|
|
@@ -13065,7 +13370,7 @@ var require_snapshot_observation = __commonJS({
|
|
|
13065
13370
|
if (activeScope.rootRef) {
|
|
13066
13371
|
lines.push(` root_ref: ${yamlScalar(activeScope.rootRef)}`);
|
|
13067
13372
|
}
|
|
13068
|
-
const scopePreview = buildScopeSnapshotPreview(activeScope.scopeSnapshotText);
|
|
13373
|
+
const scopePreview = buildScopeSnapshotPreview(activeScope.scopeSnapshotText, richRender);
|
|
13069
13374
|
if (scopePreview.length > 0) {
|
|
13070
13375
|
lines.push(" scope_snapshot_preview: |");
|
|
13071
13376
|
for (const previewLine of scopePreview) {
|
|
@@ -13107,6 +13412,19 @@ var require_snapshot_observation = __commonJS({
|
|
|
13107
13412
|
lines.push(` - ${yamlScalar(modal)}`);
|
|
13108
13413
|
}
|
|
13109
13414
|
}
|
|
13415
|
+
const staticMessages = page.staticMessages ?? [];
|
|
13416
|
+
if (richRender && staticMessages.length > 0) {
|
|
13417
|
+
const messageLimit = 6;
|
|
13418
|
+
const ordered = [...staticMessages].sort((a, b) => messageRolePriority(a.role) - messageRolePriority(b.role));
|
|
13419
|
+
lines.push("messages:");
|
|
13420
|
+
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.");
|
|
13421
|
+
for (const message of ordered.slice(0, messageLimit)) {
|
|
13422
|
+
lines.push(` - [${message.role}] ${yamlScalar(message.text)}`);
|
|
13423
|
+
}
|
|
13424
|
+
if (ordered.length > messageLimit) {
|
|
13425
|
+
lines.push(` # +${ordered.length - messageLimit} more message(s) not listed`);
|
|
13426
|
+
}
|
|
13427
|
+
}
|
|
13110
13428
|
lines.push("session_state:");
|
|
13111
13429
|
if (page.sessionState) {
|
|
13112
13430
|
lines.push(` authenticated: ${yamlScalar(page.sessionState.authenticated)}`);
|
|
@@ -13169,7 +13487,10 @@ var require_snapshot_observation = __commonJS({
|
|
|
13169
13487
|
}
|
|
13170
13488
|
const hasRowContext = group.rowContexts.some((rc) => !!rc);
|
|
13171
13489
|
if (hasRowContext) {
|
|
13172
|
-
|
|
13490
|
+
const firstRowRole = group.rowRoles[0] ?? null;
|
|
13491
|
+
const rowRole = firstRowRole && group.rowRoles.every((r) => r === firstRowRole) ? firstRowRole : null;
|
|
13492
|
+
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`;
|
|
13493
|
+
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}`);
|
|
13173
13494
|
for (let i = 0; i < group.rowContexts.length; i++) {
|
|
13174
13495
|
const rowContext = group.rowContexts[i];
|
|
13175
13496
|
if (!rowContext)
|
|
@@ -13199,8 +13520,13 @@ var require_snapshot_observation = __commonJS({
|
|
|
13199
13520
|
lines.push(` placeholder: ${yamlScalar(rankedElement.element.placeholder)}`);
|
|
13200
13521
|
}
|
|
13201
13522
|
const stateTokens = buildStateSummary(rankedElement.element);
|
|
13523
|
+
const addedDisabled = richRender && !!rankedElement.element.isDisabled && !stateTokens.includes("disabled");
|
|
13524
|
+
if (addedDisabled) {
|
|
13525
|
+
stateTokens.unshift("disabled");
|
|
13526
|
+
}
|
|
13202
13527
|
if (stateTokens.length > 0) {
|
|
13203
|
-
|
|
13528
|
+
const note = addedDisabled ? " # disabled = captured instant; may enable after valid input \u2014 still exercise fill\u2192submit" : "";
|
|
13529
|
+
lines.push(` state: [${stateTokens.map((token) => yamlScalar(token)).join(", ")}]${note}`);
|
|
13204
13530
|
}
|
|
13205
13531
|
if (rankedElement.element.containerPath) {
|
|
13206
13532
|
lines.push(` container_path: ${yamlScalar(rankedElement.element.containerPath)}`);
|
|
@@ -13210,6 +13536,22 @@ var require_snapshot_observation = __commonJS({
|
|
|
13210
13536
|
lines.push(` locator: ${yamlScalar((0, snapshot_parser_js_1.formatLocator)(topLocator))}`);
|
|
13211
13537
|
}
|
|
13212
13538
|
const enr = rankedElement.element.enrichment;
|
|
13539
|
+
if (richRender && enr?.inputType && TEXT_DISAMBIGUATING_INPUT_TYPES.has(enr.inputType)) {
|
|
13540
|
+
lines.push(` type: ${yamlScalar(enr.inputType)}`);
|
|
13541
|
+
}
|
|
13542
|
+
if (richRender && rankedElement.element.href) {
|
|
13543
|
+
lines.push(` url: ${yamlScalar(rankedElement.element.href)}`);
|
|
13544
|
+
}
|
|
13545
|
+
if (richRender && rankedElement.element.value && enr?.inputType !== "password") {
|
|
13546
|
+
lines.push(` value: ${yamlScalar(rankedElement.element.value)}`);
|
|
13547
|
+
}
|
|
13548
|
+
const elementOptions = rankedElement.element.options ?? [];
|
|
13549
|
+
if (richRender && elementOptions.length > 0) {
|
|
13550
|
+
const optionLimit = 12;
|
|
13551
|
+
const rendered = elementOptions.slice(0, optionLimit).map((option) => yamlScalar(option.selected ? `${option.label} (selected)` : option.label));
|
|
13552
|
+
const overflow = elementOptions.length > optionLimit ? ` # +${elementOptions.length - optionLimit} more option(s)` : "";
|
|
13553
|
+
lines.push(` options: [${rendered.join(", ")}]${overflow}`);
|
|
13554
|
+
}
|
|
13213
13555
|
if (enr?.rowContext) {
|
|
13214
13556
|
lines.push(` row_context: ${yamlScalar(enr.rowContext)}`);
|
|
13215
13557
|
}
|
|
@@ -13222,6 +13564,12 @@ var require_snapshot_observation = __commonJS({
|
|
|
13222
13564
|
lines.push(` visibility: [${vis.map((token) => yamlScalar(token)).join(", ")}]`);
|
|
13223
13565
|
}
|
|
13224
13566
|
}
|
|
13567
|
+
if (richRender) {
|
|
13568
|
+
const hidden = allInteractive.length - interactive.length;
|
|
13569
|
+
if (hidden > 0) {
|
|
13570
|
+
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.`);
|
|
13571
|
+
}
|
|
13572
|
+
}
|
|
13225
13573
|
}
|
|
13226
13574
|
const recoveredClickables = options.recoveredClickables ?? [];
|
|
13227
13575
|
if (recoveredClickables.length > 0) {
|
|
@@ -13378,11 +13726,11 @@ var require_snapshot_observation = __commonJS({
|
|
|
13378
13726
|
score += 10;
|
|
13379
13727
|
if (element.isExpanded)
|
|
13380
13728
|
score += 8;
|
|
13381
|
-
if (element.containerPath
|
|
13729
|
+
if (element.containerPath && DIALOG_SEGMENT_RE.test(element.containerPath))
|
|
13382
13730
|
score += 12;
|
|
13383
|
-
if (element.containerPath
|
|
13731
|
+
if (element.containerPath && FORM_SEGMENT_RE.test(element.containerPath))
|
|
13384
13732
|
score += 10;
|
|
13385
|
-
if (element.containerPath
|
|
13733
|
+
if (element.containerPath && TABPANEL_SEGMENT_RE.test(element.containerPath))
|
|
13386
13734
|
score += 8;
|
|
13387
13735
|
if (element.inputType === "email" || element.inputType === "password" || element.inputType === "search")
|
|
13388
13736
|
score += 6;
|
|
@@ -13453,10 +13801,10 @@ var require_snapshot_observation = __commonJS({
|
|
|
13453
13801
|
}
|
|
13454
13802
|
return merged;
|
|
13455
13803
|
}
|
|
13456
|
-
function buildScopeSnapshotPreview(scopeSnapshotText) {
|
|
13804
|
+
function buildScopeSnapshotPreview(scopeSnapshotText, stripTrailingValues = false) {
|
|
13457
13805
|
if (!scopeSnapshotText)
|
|
13458
13806
|
return [];
|
|
13459
|
-
return scopeSnapshotText.split("\n").map((line) => line.trim()).filter(Boolean).slice(0, 6);
|
|
13807
|
+
return scopeSnapshotText.split("\n").map((line) => line.trim()).map((line) => stripTrailingValues ? line.replace(/\]\s*:\s.+$/, "]") : line).filter(Boolean).slice(0, 6);
|
|
13460
13808
|
}
|
|
13461
13809
|
function dedupeLines(lines) {
|
|
13462
13810
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -13583,11 +13931,12 @@ var require_snapshot_observation = __commonJS({
|
|
|
13583
13931
|
const key = `${role}\0${name}`;
|
|
13584
13932
|
let group = groups.get(key);
|
|
13585
13933
|
if (!group) {
|
|
13586
|
-
group = { role, name, containers: [], rowContexts: [], refs: [] };
|
|
13934
|
+
group = { role, name, containers: [], rowContexts: [], rowRoles: [], refs: [] };
|
|
13587
13935
|
groups.set(key, group);
|
|
13588
13936
|
}
|
|
13589
13937
|
group.containers.push(element.containerPath ?? null);
|
|
13590
13938
|
group.rowContexts.push(element.enrichment?.rowContext ?? null);
|
|
13939
|
+
group.rowRoles.push(element.enrichment?.rowRole ?? null);
|
|
13591
13940
|
group.refs.push(element.ref ?? null);
|
|
13592
13941
|
}
|
|
13593
13942
|
return [...groups.values()].filter((group) => group.containers.length > 1);
|
|
@@ -13597,7 +13946,7 @@ var require_snapshot_observation = __commonJS({
|
|
|
13597
13946
|
for (const element of elements) {
|
|
13598
13947
|
const role = element.role?.toLowerCase() ?? "";
|
|
13599
13948
|
const elementType = element.elementType?.toLowerCase() ?? "";
|
|
13600
|
-
const isDialog = role === "dialog" || elementType === "dialog" || (element.containerPath
|
|
13949
|
+
const isDialog = role === "dialog" || elementType === "dialog" || DIALOG_NAME_SEGMENT_RE.test(element.containerPath ?? "");
|
|
13601
13950
|
if (!isDialog)
|
|
13602
13951
|
continue;
|
|
13603
13952
|
const hint = qualifyElementLabel(element) ?? dialogNameFromContainerPath(element.containerPath) ?? element.label ?? element.testId ?? "dialog";
|
|
@@ -13610,7 +13959,7 @@ var require_snapshot_observation = __commonJS({
|
|
|
13610
13959
|
function dialogNameFromContainerPath(containerPath) {
|
|
13611
13960
|
if (!containerPath)
|
|
13612
13961
|
return null;
|
|
13613
|
-
const match = containerPath.match(
|
|
13962
|
+
const match = containerPath.match(DIALOG_NAME_SEGMENT_RE);
|
|
13614
13963
|
return match ? match[1].trim() : null;
|
|
13615
13964
|
}
|
|
13616
13965
|
function renderObservationCue(observation) {
|
|
@@ -13619,6 +13968,13 @@ var require_snapshot_observation = __commonJS({
|
|
|
13619
13968
|
const base2 = observation.outcomeType === "SUCCESS" && outcome ? outcome : `${action} -> ${outcome || observation.outcomeType}`;
|
|
13620
13969
|
return base2.length > 120 ? `${base2.slice(0, 117)}...` : base2;
|
|
13621
13970
|
}
|
|
13971
|
+
function messageRolePriority(role) {
|
|
13972
|
+
if (role === "alert")
|
|
13973
|
+
return 0;
|
|
13974
|
+
if (role === "status")
|
|
13975
|
+
return 1;
|
|
13976
|
+
return 2;
|
|
13977
|
+
}
|
|
13622
13978
|
function yamlScalar(value) {
|
|
13623
13979
|
if (value == null || value === "")
|
|
13624
13980
|
return "null";
|
|
@@ -20167,7 +20523,10 @@ ${testOpen}`;
|
|
|
20167
20523
|
pressed: se.pressed,
|
|
20168
20524
|
headingLevel: se.headingLevel,
|
|
20169
20525
|
currentState: se.currentState,
|
|
20170
|
-
containerPath: se.containerPath || void 0
|
|
20526
|
+
containerPath: se.containerPath || void 0,
|
|
20527
|
+
href: se.href || void 0,
|
|
20528
|
+
value: se.value || void 0,
|
|
20529
|
+
options: se.options && se.options.length > 0 ? se.options : void 0
|
|
20171
20530
|
}));
|
|
20172
20531
|
const structuredPage = {
|
|
20173
20532
|
route: pageUrl || "/",
|
|
@@ -20175,6 +20534,7 @@ ${testOpen}`;
|
|
|
20175
20534
|
pageType: "unknown",
|
|
20176
20535
|
elements: collectedElements,
|
|
20177
20536
|
formGroups: parsed.formGroups,
|
|
20537
|
+
staticMessages: parsed.staticMessages.length > 0 ? parsed.staticMessages : void 0,
|
|
20178
20538
|
snapshotMetrics: {
|
|
20179
20539
|
rawLineCount: parsed.rawLineCount,
|
|
20180
20540
|
parsedElementLineCount: parsed.parsedElementLineCount,
|
|
@@ -21741,7 +22101,7 @@ Do NOT assert that the button/tab/heading you interacted with is merely visible
|
|
|
21741
22101
|
let stopped;
|
|
21742
22102
|
const agentModel = options?.modelOverride ? (0, ai_provider_js_1.resolveModelForTask)(options.modelOverride, "agent") : (0, ai_provider_js_1.getModel)("agent");
|
|
21743
22103
|
const auditGroupId = `generate-${(0, crypto_1.randomUUID)().slice(0, 8)}`;
|
|
21744
|
-
const aiClient = options?.modelOverride ? (0, ai_provider_js_1.
|
|
22104
|
+
const aiClient = options?.modelOverride ? (0, ai_provider_js_1.getRunnerClientForModel)(options.modelOverride) : (0, ai_provider_js_1.getRunnerAIClient)();
|
|
21745
22105
|
const credentials = variableContext.allTestVariables ?? variableContext.publicTestVariables;
|
|
21746
22106
|
logs2.push(`Phase 3 \u2014 Generate: ${(plan.scenarios ?? []).length} scenarios, max ${maxTests} tests`);
|
|
21747
22107
|
logs2.push(`Model: ${agentModel}`);
|
|
@@ -235706,7 +236066,7 @@ var require_mcp_healer = __commonJS({
|
|
|
235706
236066
|
messages.push(system, firstUser, { role: "user", content: lines.join("\n") }, ...tail);
|
|
235707
236067
|
}
|
|
235708
236068
|
var DEFAULT_MAX_SCRIPT_HEAL_ATTEMPTS = 6;
|
|
235709
|
-
function
|
|
236069
|
+
function readPositiveIntEnv2(env, name, fallback) {
|
|
235710
236070
|
const raw = env[name]?.trim();
|
|
235711
236071
|
if (!raw || !/^\d+$/.test(raw))
|
|
235712
236072
|
return fallback;
|
|
@@ -235714,7 +236074,7 @@ var require_mcp_healer = __commonJS({
|
|
|
235714
236074
|
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
235715
236075
|
}
|
|
235716
236076
|
function getScriptHealMaxAttempts(env = process.env) {
|
|
235717
|
-
return
|
|
236077
|
+
return readPositiveIntEnv2(env, "MCP_HEAL_SCRIPT_MAX_ATTEMPTS", DEFAULT_MAX_SCRIPT_HEAL_ATTEMPTS);
|
|
235718
236078
|
}
|
|
235719
236079
|
exports.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.
|
|
235720
236080
|
|
|
@@ -236050,7 +236410,7 @@ ${lines.join("\n")}
|
|
|
236050
236410
|
const landingViolation = (tags ?? []).includes("@landing-violation");
|
|
236051
236411
|
const agentModel = modelOverride ? (0, ai_provider_js_1.resolveModelForTask)(modelOverride, "strong") : (0, ai_provider_js_1.getModel)("strong");
|
|
236052
236412
|
const provider = modelOverride ? (0, ai_provider_js_1.getProviderForModel)(modelOverride) : (0, ai_provider_js_1.getAIProvider)();
|
|
236053
|
-
const aiClient = options.aiClient ?? (modelOverride ? (0, ai_provider_js_1.
|
|
236413
|
+
const aiClient = options.aiClient ?? (modelOverride ? (0, ai_provider_js_1.getRunnerClientForModel)(modelOverride) : (0, ai_provider_js_1.getRunnerAIClient)());
|
|
236054
236414
|
const runNativeTest = options.runNativeTest ?? playwright_native_js_1.executeTestViaNativePlaywright;
|
|
236055
236415
|
const modelSupportsVision = /gpt-4o|claude-3|claude-4|minimax|grok/i.test(agentModel ?? "");
|
|
236056
236416
|
const groupId = `script-heal-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -236872,10 +237232,10 @@ ${(verifyResult.logs ?? "").slice(0, 2e3)}`;
|
|
|
236872
237232
|
var DEFAULT_MAX_PHASE2_ITERATIONS = 4;
|
|
236873
237233
|
var DEFAULT_MAX_MCP_EXPLORE_ITERATIONS = 25;
|
|
236874
237234
|
function getMcpHealPhase2MaxIterations(env = process.env) {
|
|
236875
|
-
return
|
|
237235
|
+
return readPositiveIntEnv2(env, "MCP_HEAL_PHASE2_MAX_ITERATIONS", DEFAULT_MAX_PHASE2_ITERATIONS);
|
|
236876
237236
|
}
|
|
236877
237237
|
function getMcpHealExploreMaxIterations(env = process.env) {
|
|
236878
|
-
return
|
|
237238
|
+
return readPositiveIntEnv2(env, "MCP_HEAL_EXPLORE_MAX_ITERATIONS", DEFAULT_MAX_MCP_EXPLORE_ITERATIONS);
|
|
236879
237239
|
}
|
|
236880
237240
|
var DEFAULT_MCP_HEAL_MAX_WALL_CLOCK_MS = 10 * 60 * 1e3;
|
|
236881
237241
|
var DEFAULT_MCP_HEAL_MAX_TOTAL_AI_CALLS = 60;
|
|
@@ -236927,7 +237287,7 @@ Only explore the failing page and the area around the broken step.`;
|
|
|
236927
237287
|
let lastVideo;
|
|
236928
237288
|
const agentModel = modelOverride ? (0, ai_provider_js_1.resolveModelForTask)(modelOverride, "agent") : (0, ai_provider_js_1.getModel)("agent");
|
|
236929
237289
|
const provider = modelOverride ? (0, ai_provider_js_1.getProviderForModel)(modelOverride) : (0, ai_provider_js_1.getAIProvider)();
|
|
236930
|
-
const aiClient = options?.phase2Harness?.aiClient ?? (modelOverride ? (0, ai_provider_js_1.
|
|
237290
|
+
const aiClient = options?.phase2Harness?.aiClient ?? (modelOverride ? (0, ai_provider_js_1.getRunnerClientForModel)(modelOverride) : (0, ai_provider_js_1.getRunnerAIClient)());
|
|
236931
237291
|
const runPhase2NativeTest = options?.phase2Harness?.runNativeTest ?? playwright_native_js_1.executeTestViaNativePlaywright;
|
|
236932
237292
|
const modelSupportsVision = /gpt-4o|claude-3|claude-4|minimax|grok/i.test(agentModel ?? "");
|
|
236933
237293
|
const groupId = `phase2-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -237617,7 +237977,10 @@ Fix the syntax error and provide ONLY the complete corrected Playwright test as
|
|
|
237617
237977
|
pressed: se.pressed,
|
|
237618
237978
|
headingLevel: se.headingLevel,
|
|
237619
237979
|
currentState: se.currentState,
|
|
237620
|
-
containerPath: se.containerPath || void 0
|
|
237980
|
+
containerPath: se.containerPath || void 0,
|
|
237981
|
+
href: se.href || void 0,
|
|
237982
|
+
value: se.value || void 0,
|
|
237983
|
+
options: se.options && se.options.length > 0 ? se.options : void 0
|
|
237621
237984
|
}));
|
|
237622
237985
|
const structuredPage = {
|
|
237623
237986
|
route: pageUrl || "/",
|
|
@@ -237625,6 +237988,7 @@ Fix the syntax error and provide ONLY the complete corrected Playwright test as
|
|
|
237625
237988
|
pageType: "unknown",
|
|
237626
237989
|
elements: collectedElements,
|
|
237627
237990
|
formGroups: parsed.formGroups,
|
|
237991
|
+
staticMessages: parsed.staticMessages.length > 0 ? parsed.staticMessages : void 0,
|
|
237628
237992
|
snapshotMetrics: {
|
|
237629
237993
|
rawLineCount: parsed.rawLineCount,
|
|
237630
237994
|
parsedElementLineCount: parsed.parsedElementLineCount,
|
|
@@ -238061,7 +238425,7 @@ ${testBlocks}
|
|
|
238061
238425
|
- By iteration ${Math.floor(maxIterations * 0.3)}: you should have completed 1-2 tests
|
|
238062
238426
|
- By iteration ${Math.floor(maxIterations * 0.7)}: you should be past the halfway point
|
|
238063
238427
|
- At iteration ${Math.floor(maxIterations * 0.9)}: wrap up and call finish_capture`;
|
|
238064
|
-
const aiClient = modelOverride ? (0, ai_provider_js_1.
|
|
238428
|
+
const aiClient = modelOverride ? (0, ai_provider_js_1.getRunnerClientForModel)(modelOverride) : (0, ai_provider_js_1.getRunnerAIClient)();
|
|
238065
238429
|
const messages = [
|
|
238066
238430
|
{ role: "system", content: systemPrompt },
|
|
238067
238431
|
{ role: "user", content: "Start executing the tests. Navigate to the base URL and begin with Test 1." }
|
|
@@ -238244,7 +238608,10 @@ ${testBlocks}
|
|
|
238244
238608
|
pressed: se.pressed,
|
|
238245
238609
|
headingLevel: se.headingLevel,
|
|
238246
238610
|
currentState: se.currentState,
|
|
238247
|
-
containerPath: se.containerPath || void 0
|
|
238611
|
+
containerPath: se.containerPath || void 0,
|
|
238612
|
+
href: se.href || void 0,
|
|
238613
|
+
value: se.value || void 0,
|
|
238614
|
+
options: se.options && se.options.length > 0 ? se.options : void 0
|
|
238248
238615
|
}));
|
|
238249
238616
|
const structuredPage = {
|
|
238250
238617
|
route: pageUrl || "/",
|
|
@@ -238252,6 +238619,7 @@ ${testBlocks}
|
|
|
238252
238619
|
pageType: "unknown",
|
|
238253
238620
|
elements: collectedElements,
|
|
238254
238621
|
formGroups: parsed.formGroups,
|
|
238622
|
+
staticMessages: parsed.staticMessages.length > 0 ? parsed.staticMessages : void 0,
|
|
238255
238623
|
snapshotMetrics: {
|
|
238256
238624
|
rawLineCount: parsed.rawLineCount,
|
|
238257
238625
|
parsedElementLineCount: parsed.parsedElementLineCount,
|
|
@@ -300041,8 +300409,9 @@ var require_capture_page_normalizer = __commonJS({
|
|
|
300041
300409
|
let snapshotElements = [];
|
|
300042
300410
|
let formGroups = [];
|
|
300043
300411
|
let snapshotMetrics;
|
|
300412
|
+
let staticMessages;
|
|
300044
300413
|
if (normalizedSnapshotResult.snapshotText) {
|
|
300045
|
-
const parsed = (0, snapshot_parser_js_1.parseSnapshot)(normalizedSnapshotResult.snapshotText);
|
|
300414
|
+
const parsed = (0, snapshot_parser_js_1.parseSnapshot)(normalizedSnapshotResult.snapshotText, { richParse: context.richParse });
|
|
300046
300415
|
snapshotMetrics = {
|
|
300047
300416
|
rawLineCount: parsed.rawLineCount,
|
|
300048
300417
|
parsedElementLineCount: parsed.parsedElementLineCount,
|
|
@@ -300071,9 +300440,13 @@ var require_capture_page_normalizer = __commonJS({
|
|
|
300071
300440
|
pressed: se.pressed,
|
|
300072
300441
|
headingLevel: se.headingLevel,
|
|
300073
300442
|
currentState: se.currentState,
|
|
300074
|
-
containerPath: se.containerPath || void 0
|
|
300443
|
+
containerPath: se.containerPath || void 0,
|
|
300444
|
+
href: se.href || void 0,
|
|
300445
|
+
value: se.value || void 0,
|
|
300446
|
+
options: se.options && se.options.length > 0 ? se.options : void 0
|
|
300075
300447
|
}));
|
|
300076
300448
|
formGroups = parsed.formGroups;
|
|
300449
|
+
staticMessages = parsed.staticMessages.length > 0 ? parsed.staticMessages : void 0;
|
|
300077
300450
|
}
|
|
300078
300451
|
const aiElements = Array.isArray(toolArgs.elements) ? toolArgs.elements.map(normalizeAiElement) : [];
|
|
300079
300452
|
const noYamlKinds = /* @__PURE__ */ new Set(["missing", "timeout", "error", "empty"]);
|
|
@@ -300169,6 +300542,7 @@ var require_capture_page_normalizer = __commonJS({
|
|
|
300169
300542
|
} : void 0,
|
|
300170
300543
|
readinessSignals,
|
|
300171
300544
|
viewportHints,
|
|
300545
|
+
staticMessages,
|
|
300172
300546
|
provenCommands: provenCmds,
|
|
300173
300547
|
observations: rawObservations,
|
|
300174
300548
|
routeCorrected,
|
|
@@ -300477,6 +300851,10 @@ var require_perception_enricher = __commonJS({
|
|
|
300477
300851
|
const e = {};
|
|
300478
300852
|
if (p.rowContext)
|
|
300479
300853
|
e.rowContext = p.rowContext;
|
|
300854
|
+
if (p.rowContext && p.rowRole)
|
|
300855
|
+
e.rowRole = p.rowRole;
|
|
300856
|
+
if (p.inputType)
|
|
300857
|
+
e.inputType = p.inputType;
|
|
300480
300858
|
if (p.position && p.position !== "in-view")
|
|
300481
300859
|
e.position = p.position;
|
|
300482
300860
|
if (p.occluded)
|
|
@@ -300726,6 +301104,23 @@ var require_perception_enricher = __commonJS({
|
|
|
300726
301104
|
const cls = (el.getAttribute("class") || "").toLowerCase();
|
|
300727
301105
|
return /(^|[\s_-])(card|row|item|listitem|cell|tile)([\s_-]|$)/.test(cls);
|
|
300728
301106
|
};
|
|
301107
|
+
const rowRoleOf = (el) => {
|
|
301108
|
+
const explicit = (el.getAttribute("role") || "").toLowerCase().trim();
|
|
301109
|
+
if (explicit) {
|
|
301110
|
+
return explicit === "row" || explicit === "listitem" || explicit === "article" || explicit === "option" ? explicit : null;
|
|
301111
|
+
}
|
|
301112
|
+
const tag = el.tagName;
|
|
301113
|
+
if (tag === "TR") {
|
|
301114
|
+
const tbl = el.closest("table");
|
|
301115
|
+
const tblRole = tbl ? (tbl.getAttribute("role") || "").toLowerCase().trim() : "";
|
|
301116
|
+
return tblRole === "presentation" || tblRole === "none" ? null : "row";
|
|
301117
|
+
}
|
|
301118
|
+
if (tag === "LI")
|
|
301119
|
+
return el.closest('ul:not([role]),ol:not([role]),menu:not([role]),[role="list"]') ? "listitem" : null;
|
|
301120
|
+
if (tag === "ARTICLE")
|
|
301121
|
+
return "article";
|
|
301122
|
+
return null;
|
|
301123
|
+
};
|
|
300729
301124
|
const rowContextOf = (el, ownNameLower) => {
|
|
300730
301125
|
let cur = el.parentElement;
|
|
300731
301126
|
let depth = 0;
|
|
@@ -300744,7 +301139,7 @@ var require_perception_enricher = __commonJS({
|
|
|
300744
301139
|
txt = txt.slice(0, 80);
|
|
300745
301140
|
const low = txt.toLowerCase();
|
|
300746
301141
|
if (txt && low !== ownNameLower && low.length > 1)
|
|
300747
|
-
return txt;
|
|
301142
|
+
return { text: txt, role: rowRoleOf(cur) };
|
|
300748
301143
|
return null;
|
|
300749
301144
|
}
|
|
300750
301145
|
cur = cur.parentElement;
|
|
@@ -300834,11 +301229,14 @@ var require_perception_enricher = __commonJS({
|
|
|
300834
301229
|
const name = accName(el);
|
|
300835
301230
|
const lname = lower(name);
|
|
300836
301231
|
const rect = vis.rect;
|
|
301232
|
+
const rc = rowContextOf(el, lname);
|
|
300837
301233
|
out.push({
|
|
300838
301234
|
domOrder: myOrder,
|
|
300839
301235
|
roleClass: roleClassOf(tag, roleAttr, inputType),
|
|
300840
301236
|
name: lname,
|
|
300841
|
-
rowContext:
|
|
301237
|
+
rowContext: rc ? rc.text : null,
|
|
301238
|
+
rowRole: rc ? rc.role : null,
|
|
301239
|
+
inputType: inputType || null,
|
|
300842
301240
|
position: positionOf(rect),
|
|
300843
301241
|
occluded: occlusionOf(el, rect),
|
|
300844
301242
|
cursorPointer,
|
|
@@ -300875,11 +301273,14 @@ var require_perception_enricher = __commonJS({
|
|
|
300875
301273
|
if (!name)
|
|
300876
301274
|
continue;
|
|
300877
301275
|
const lname = lower(name);
|
|
301276
|
+
const rc = rowContextOf(el, lname);
|
|
300878
301277
|
out.push({
|
|
300879
301278
|
domOrder: order++,
|
|
300880
301279
|
roleClass: "button",
|
|
300881
301280
|
name: lname,
|
|
300882
|
-
rowContext:
|
|
301281
|
+
rowContext: rc ? rc.text : null,
|
|
301282
|
+
rowRole: rc ? rc.role : null,
|
|
301283
|
+
inputType: null,
|
|
300883
301284
|
position: "in-view",
|
|
300884
301285
|
occluded: false,
|
|
300885
301286
|
cursorPointer: true,
|
|
@@ -301381,7 +301782,7 @@ var require_discover_explorer = __commonJS({
|
|
|
301381
301782
|
var DEFAULT_MAX_EXPLORE_ITERATIONS_DEEP = 100;
|
|
301382
301783
|
var DEFAULT_MAX_EXPLORE_ITERATIONS_SURVEY = 100;
|
|
301383
301784
|
var DEFAULT_MAX_EXPLORE_ITERATIONS_INCREMENTAL = 40;
|
|
301384
|
-
function
|
|
301785
|
+
function readPositiveIntEnv2(env, name, fallback) {
|
|
301385
301786
|
const raw = env[name]?.trim();
|
|
301386
301787
|
if (!raw || !/^\d+$/.test(raw))
|
|
301387
301788
|
return fallback;
|
|
@@ -301512,15 +301913,15 @@ var require_discover_explorer = __commonJS({
|
|
|
301512
301913
|
}
|
|
301513
301914
|
function getMaxIterations(mode, incremental, env = process.env) {
|
|
301514
301915
|
if (mode === "survey" && incremental) {
|
|
301515
|
-
return
|
|
301916
|
+
return readPositiveIntEnv2(env, "DISCOVERY_EXPLORE_MAX_ITERATIONS_INCREMENTAL", DEFAULT_MAX_EXPLORE_ITERATIONS_INCREMENTAL);
|
|
301516
301917
|
}
|
|
301517
301918
|
if (mode === "survey") {
|
|
301518
|
-
return
|
|
301919
|
+
return readPositiveIntEnv2(env, "DISCOVERY_EXPLORE_MAX_ITERATIONS_SURVEY", DEFAULT_MAX_EXPLORE_ITERATIONS_SURVEY);
|
|
301519
301920
|
}
|
|
301520
301921
|
if (mode === "deep") {
|
|
301521
|
-
return
|
|
301922
|
+
return readPositiveIntEnv2(env, "DISCOVERY_EXPLORE_MAX_ITERATIONS_DEEP", DEFAULT_MAX_EXPLORE_ITERATIONS_DEEP);
|
|
301522
301923
|
}
|
|
301523
|
-
return
|
|
301924
|
+
return readPositiveIntEnv2(env, "DISCOVERY_EXPLORE_MAX_ITERATIONS_DEFAULT", DEFAULT_MAX_EXPLORE_ITERATIONS_DEFAULT);
|
|
301524
301925
|
}
|
|
301525
301926
|
function updatePendingRouteScreenshot(pending, { imageData, detectedRoute, fallbackRoute }) {
|
|
301526
301927
|
if (imageData) {
|
|
@@ -302246,7 +302647,7 @@ ${inboxPromptBlock}`;
|
|
|
302246
302647
|
const browserCreds = variableContext.allTestVariables ?? variableContext.publicTestVariables;
|
|
302247
302648
|
const { secrets: browserSecretsForHint } = (0, credential_tools_js_1.partitionTestVariables)(browserCreds);
|
|
302248
302649
|
const hasLoginCredentials = Object.keys(browserSecretsForHint).length > 0;
|
|
302249
|
-
const aiClient = options?.modelOverride ? (0, ai_provider_js_1.
|
|
302650
|
+
const aiClient = options?.modelOverride ? (0, ai_provider_js_1.getRunnerClientForModel)(options.modelOverride) : (0, ai_provider_js_1.getRunnerAIClient)();
|
|
302250
302651
|
const isDeepMode = options?.mode === "deep";
|
|
302251
302652
|
const maxIterations = config?.maxIterationsOverride && config.maxIterationsOverride > 0 ? config.maxIterationsOverride : getMaxIterations(options?.mode ?? "full", options?.incremental);
|
|
302252
302653
|
const configKeepTail = isDeepMode ? Math.ceil(ctxConfig.keepTail * 1.5) : ctxConfig.keepTail;
|
|
@@ -304785,7 +305186,7 @@ var require_auth_capture_run = __commonJS({
|
|
|
304785
305186
|
var scrub_credentials_js_1 = require_scrub_credentials();
|
|
304786
305187
|
var credential_tools_js_1 = require_credential_tools();
|
|
304787
305188
|
var DEFAULT_MAX_LOGIN_CAPTURE_ITERATIONS = 12;
|
|
304788
|
-
function
|
|
305189
|
+
function readPositiveIntEnv2(env, name, fallback) {
|
|
304789
305190
|
const raw = env[name]?.trim();
|
|
304790
305191
|
if (!raw || !/^\d+$/.test(raw))
|
|
304791
305192
|
return fallback;
|
|
@@ -304793,7 +305194,7 @@ var require_auth_capture_run = __commonJS({
|
|
|
304793
305194
|
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
304794
305195
|
}
|
|
304795
305196
|
function getLoginCaptureMaxIterations(env = process.env) {
|
|
304796
|
-
return
|
|
305197
|
+
return readPositiveIntEnv2(env, "AUTH_CAPTURE_LOGIN_MAX_ITERATIONS", DEFAULT_MAX_LOGIN_CAPTURE_ITERATIONS);
|
|
304797
305198
|
}
|
|
304798
305199
|
var CAPTURE_RATE_LIMIT_MAX_RETRIES = 2;
|
|
304799
305200
|
var CAPTURE_RATE_LIMIT_MAX_DELAY_MS = 8e3;
|
|
@@ -305076,7 +305477,7 @@ ${keyLines || "- (none provided)"}
|
|
|
305076
305477
|
}
|
|
305077
305478
|
var DEFAULT_MAX_REGISTER_CAPTURE_ITERATIONS = 18;
|
|
305078
305479
|
function getRegisterCaptureMaxIterations(env = process.env) {
|
|
305079
|
-
return
|
|
305480
|
+
return readPositiveIntEnv2(env, "AUTH_CAPTURE_REGISTER_MAX_ITERATIONS", DEFAULT_MAX_REGISTER_CAPTURE_ITERATIONS);
|
|
305080
305481
|
}
|
|
305081
305482
|
function buildRegisterCaptureSystemPrompt(baseUrl, availableKeys, maxIterations = getRegisterCaptureMaxIterations()) {
|
|
305082
305483
|
const keyLines = availableKeys.map((k) => `- \`${k}\``).join("\n");
|
|
@@ -311306,6 +311707,7 @@ var require_mobile_exploration_state = __commonJS({
|
|
|
311306
311707
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
311307
311708
|
exports.MobileExplorationState = void 0;
|
|
311308
311709
|
exports.deriveScreenId = deriveScreenId;
|
|
311710
|
+
var mobile_observation_js_1 = require_mobile_observation();
|
|
311309
311711
|
var DEFAULT_STALE_THRESHOLD = 4;
|
|
311310
311712
|
function deriveScreenId(signal) {
|
|
311311
311713
|
const namespace = signal.activity ?? signal.bundleId;
|
|
@@ -311531,21 +311933,33 @@ var require_mobile_exploration_state = __commonJS({
|
|
|
311531
311933
|
* Build the planner evidence shape: one `CollectedPageData` per screen, with
|
|
311532
311934
|
* the screen-id carried in BOTH `route` (the on-the-wire positional field the
|
|
311533
311935
|
* server keys screenshots by) AND the explicit `screenId` field (per the
|
|
311534
|
-
* Phase-0 `CollectedPageData.screenId` contract). `elements`
|
|
311535
|
-
* the
|
|
311536
|
-
*
|
|
311537
|
-
*
|
|
311538
|
-
*
|
|
311936
|
+
* Phase-0 `CollectedPageData.screenId` contract). `elements` are mapped from
|
|
311937
|
+
* the screen's parsed actionable elements into the platform-neutral
|
|
311938
|
+
* `CollectedPageElement[]` shape the server persists as SiteElements — without
|
|
311939
|
+
* this, mobile screens land with zero elements (coverage always 0%, no element
|
|
311940
|
+
* intelligence). `triggersNavigation` is derived from transition-source
|
|
311941
|
+
* membership: an element is flagged when its ref drove a transition FROM this
|
|
311942
|
+
* screen. Insertion order matches first-seen order.
|
|
311539
311943
|
*/
|
|
311540
311944
|
buildInMemoryScreenMap() {
|
|
311541
311945
|
const pages = [];
|
|
311542
311946
|
const ordered = [...this.screens.values()].sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
311947
|
+
const navRefsByScreen = /* @__PURE__ */ new Map();
|
|
311948
|
+
for (const transition of this.transitions) {
|
|
311949
|
+
if (!transition.viaRef)
|
|
311950
|
+
continue;
|
|
311951
|
+
const set = navRefsByScreen.get(transition.fromScreenId) ?? /* @__PURE__ */ new Set();
|
|
311952
|
+
set.add(transition.viaRef);
|
|
311953
|
+
navRefsByScreen.set(transition.fromScreenId, set);
|
|
311954
|
+
}
|
|
311543
311955
|
for (const state2 of ordered) {
|
|
311544
311956
|
const page = {
|
|
311545
311957
|
route: state2.screenId,
|
|
311546
311958
|
screenId: state2.screenId,
|
|
311547
311959
|
pageType: state2.screenType ?? "UNKNOWN",
|
|
311548
|
-
elements:
|
|
311960
|
+
elements: (0, mobile_observation_js_1.buildCollectedElementsForScreen)(state2.elements, {
|
|
311961
|
+
navigationRefs: navRefsByScreen.get(state2.screenId)
|
|
311962
|
+
})
|
|
311549
311963
|
};
|
|
311550
311964
|
if (state2.screenName) {
|
|
311551
311965
|
page.title = state2.screenName;
|
|
@@ -311575,14 +311989,51 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
311575
311989
|
"../runner-core/dist/services/mobile/mobile-discovery-agent.js"(exports) {
|
|
311576
311990
|
"use strict";
|
|
311577
311991
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
311992
|
+
exports.getMobileExploreBudget = getMobileExploreBudget;
|
|
311578
311993
|
exports.runMobileDiscoveryOnRunner = runMobileDiscoveryOnRunner2;
|
|
311579
311994
|
var mobile_explorer_js_1 = require_mobile_explorer();
|
|
311580
311995
|
var mobile_discovery_prompts_js_1 = require_mobile_discovery_prompts();
|
|
311581
311996
|
var mobile_generator_js_1 = require_mobile_generator();
|
|
311582
311997
|
var mobile_exploration_state_js_1 = require_mobile_exploration_state();
|
|
311583
311998
|
var discover_planner_js_1 = require_discover_planner();
|
|
311584
|
-
|
|
311585
|
-
|
|
311999
|
+
function checkpointErrorMessage(err) {
|
|
312000
|
+
return err instanceof Error ? err.message : String(err);
|
|
312001
|
+
}
|
|
312002
|
+
function isTerminalDiscoveryCheckpointError(err) {
|
|
312003
|
+
const message = checkpointErrorMessage(err);
|
|
312004
|
+
return /Discovery plan checkpoint rejected|Discovery run is .*not RUNNING|not RUNNING|cancelled/i.test(message);
|
|
312005
|
+
}
|
|
312006
|
+
var MOBILE_EXPLORE_BUDGET_SURVEY_FIRST = 90;
|
|
312007
|
+
var MOBILE_EXPLORE_BUDGET_SURVEY_INCREMENTAL = 40;
|
|
312008
|
+
var MOBILE_EXPLORE_BUDGET_DEEP = 60;
|
|
312009
|
+
var MOBILE_EXPLORE_BUDGET_FULL = 90;
|
|
312010
|
+
function readPositiveIntEnv2(name, fallback, env = process.env) {
|
|
312011
|
+
const raw = env[name]?.trim();
|
|
312012
|
+
if (!raw || !/^\d+$/.test(raw))
|
|
312013
|
+
return fallback;
|
|
312014
|
+
const parsed = Number(raw);
|
|
312015
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
312016
|
+
}
|
|
312017
|
+
function getMobileExploreBudget(mode, incremental, env = process.env) {
|
|
312018
|
+
if (mode === "deep")
|
|
312019
|
+
return readPositiveIntEnv2("MOBILE_EXPLORE_MAX_ITERATIONS_DEEP", MOBILE_EXPLORE_BUDGET_DEEP, env);
|
|
312020
|
+
if (mode === "full")
|
|
312021
|
+
return readPositiveIntEnv2("MOBILE_EXPLORE_MAX_ITERATIONS_FULL", MOBILE_EXPLORE_BUDGET_FULL, env);
|
|
312022
|
+
if (incremental) {
|
|
312023
|
+
return readPositiveIntEnv2("MOBILE_EXPLORE_MAX_ITERATIONS_SURVEY_INCREMENTAL", MOBILE_EXPLORE_BUDGET_SURVEY_INCREMENTAL, env);
|
|
312024
|
+
}
|
|
312025
|
+
return readPositiveIntEnv2("MOBILE_EXPLORE_MAX_ITERATIONS_SURVEY", MOBILE_EXPLORE_BUDGET_SURVEY_FIRST, env);
|
|
312026
|
+
}
|
|
312027
|
+
function compactExistingScreens(siteMap) {
|
|
312028
|
+
if (!siteMap || !Array.isArray(siteMap.pages) || siteMap.pages.length === 0)
|
|
312029
|
+
return void 0;
|
|
312030
|
+
const screens = siteMap.pages.filter((p) => typeof p.route === "string" && p.route.trim().length > 0).map((p) => ({
|
|
312031
|
+
screenId: p.route,
|
|
312032
|
+
...p.title ? { name: p.title } : {},
|
|
312033
|
+
...p.pageType ? { screenType: p.pageType } : {}
|
|
312034
|
+
}));
|
|
312035
|
+
return screens.length > 0 ? screens : void 0;
|
|
312036
|
+
}
|
|
311586
312037
|
function platformForMedium(medium) {
|
|
311587
312038
|
return medium === "IOS_NATIVE" ? "IOS" : "ANDROID";
|
|
311588
312039
|
}
|
|
@@ -311601,6 +312052,9 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
311601
312052
|
// Native screenshots are multi-megabyte PNGs. The live mirror streams them
|
|
311602
312053
|
// through heartbeat telemetry; discovery checkpoints keep the screen graph
|
|
311603
312054
|
// and transitions so large Android surveys cannot exceed server body limits.
|
|
312055
|
+
// D-D6 — pageScreenshotKeys is a small screen-id → storage-key map (NOT base64),
|
|
312056
|
+
// so it DOES ride the checkpoint: the server links each key to SitePage.screenshotKey.
|
|
312057
|
+
...explore.pageScreenshotKeys && Object.keys(explore.pageScreenshotKeys).length > 0 ? { pageScreenshotKeys: explore.pageScreenshotKeys } : {},
|
|
311604
312058
|
exploreStats: explore.exploreStats,
|
|
311605
312059
|
healthObservations: explore.healthObservations
|
|
311606
312060
|
};
|
|
@@ -311615,6 +312069,7 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
311615
312069
|
const onLog = callbacks?.onLog;
|
|
311616
312070
|
const onAICall = callbacks?.onAICall;
|
|
311617
312071
|
const onScreenshot = callbacks?.onScreenshot;
|
|
312072
|
+
const onScreenshotUpload = callbacks?.onScreenshotUpload;
|
|
311618
312073
|
const runExplore = callbacks?.runExplore ?? mobile_explorer_js_1.runMobileExplorePhase;
|
|
311619
312074
|
const runPlan = callbacks?.runPlan ?? mobile_discovery_prompts_js_1.runMobileAIPlanner;
|
|
311620
312075
|
const runGenerate = callbacks?.runGenerate ?? mobile_generator_js_1.runMobileGeneratePhase;
|
|
@@ -311641,15 +312096,77 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
311641
312096
|
elementsFound: 0,
|
|
311642
312097
|
transitionsFound: 0
|
|
311643
312098
|
};
|
|
312099
|
+
const resumeSavedScenarios = ctx.resume?.savedPlan?.scenarios ?? [];
|
|
312100
|
+
const isPlanReuseResume = mode !== "survey" && resumeSavedScenarios.length > 0;
|
|
311644
312101
|
try {
|
|
312102
|
+
if (isPlanReuseResume) {
|
|
312103
|
+
const alreadyGenerated = ctx.resume?.alreadyGeneratedScenarioNames ?? [];
|
|
312104
|
+
log2(`
|
|
312105
|
+
\u2500\u2500 Resume (plan-reuse): ${resumeSavedScenarios.length} saved scenario(s); ${alreadyGenerated.length} already generated. Skipping explore + plan; generating only the remainder. \u2500\u2500`);
|
|
312106
|
+
const scenarios2 = resumeSavedScenarios;
|
|
312107
|
+
const plans2 = toPlans(scenarios2);
|
|
312108
|
+
if (callbacks?.onPlanReady) {
|
|
312109
|
+
try {
|
|
312110
|
+
await callbacks.onPlanReady({ scenarios: scenarios2, featureGroups: void 0, fallbackUsed: false });
|
|
312111
|
+
log2("Plan checkpoint: reused scenario plan re-POSTed to server");
|
|
312112
|
+
} catch (err) {
|
|
312113
|
+
const message = checkpointErrorMessage(err);
|
|
312114
|
+
if (isTerminalDiscoveryCheckpointError(err)) {
|
|
312115
|
+
log2(`Plan checkpoint rejected for inactive discovery run: ${message}`);
|
|
312116
|
+
throw err;
|
|
312117
|
+
}
|
|
312118
|
+
log2(`Plan checkpoint POST failed (non-fatal): ${message}`);
|
|
312119
|
+
}
|
|
312120
|
+
}
|
|
312121
|
+
let paused = false;
|
|
312122
|
+
let pauseReason;
|
|
312123
|
+
let remainingScenarios;
|
|
312124
|
+
const tests2 = await runGenerate({
|
|
312125
|
+
scenarios: scenarios2,
|
|
312126
|
+
driver,
|
|
312127
|
+
ctx,
|
|
312128
|
+
onLog: (line) => log2(line),
|
|
312129
|
+
onAICall,
|
|
312130
|
+
onScreenshot,
|
|
312131
|
+
onTestGenerated: (test) => callbacks?.onTestGenerated?.(test, { phase: "verified" }),
|
|
312132
|
+
skipScenarioNames: alreadyGenerated,
|
|
312133
|
+
onInfraStop: (info) => {
|
|
312134
|
+
paused = true;
|
|
312135
|
+
pauseReason = info.reason;
|
|
312136
|
+
remainingScenarios = info.remainingScenarioNames;
|
|
312137
|
+
}
|
|
312138
|
+
});
|
|
312139
|
+
const duration2 = (Date.now() - startTime) / 1e3;
|
|
312140
|
+
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" : ""}`;
|
|
312141
|
+
log2(`
|
|
312142
|
+
=== Mobile Discovery Resume Complete (${duration2.toFixed(1)}s) ===`);
|
|
312143
|
+
log2(summary2);
|
|
312144
|
+
return {
|
|
312145
|
+
collectedPages: [],
|
|
312146
|
+
collectedTransitions: [],
|
|
312147
|
+
tests: tests2,
|
|
312148
|
+
plans: plans2,
|
|
312149
|
+
scenarios: scenarios2.length > 0 ? scenarios2 : void 0,
|
|
312150
|
+
logs: logs2.join("\n"),
|
|
312151
|
+
screenshots,
|
|
312152
|
+
summary: summary2,
|
|
312153
|
+
exploreStats: { pagesDiscovered: 0, pagesVisited: 0, elementsFound: 0, transitionsFound: 0 },
|
|
312154
|
+
duration: duration2,
|
|
312155
|
+
mode,
|
|
312156
|
+
...paused ? { paused: true, pauseReason, remainingScenarios } : {}
|
|
312157
|
+
// apiCalls intentionally UNSET — CHUNK K (runner.ts) merges them.
|
|
312158
|
+
};
|
|
312159
|
+
}
|
|
311645
312160
|
log2("\n\u2500\u2500 Phase 1: EXPLORE \u2500\u2500");
|
|
311646
312161
|
const state2 = new mobile_exploration_state_js_1.MobileExplorationState();
|
|
311647
|
-
const maxIterations = mode
|
|
312162
|
+
const maxIterations = getMobileExploreBudget(mode, ctx.incremental);
|
|
312163
|
+
const existingScreens = compactExistingScreens(ctx.existingSiteMap);
|
|
311648
312164
|
const explore = await runExplore({
|
|
311649
312165
|
ctx,
|
|
311650
312166
|
driver,
|
|
311651
312167
|
state: state2,
|
|
311652
312168
|
config: { maxIterations },
|
|
312169
|
+
existingScreens,
|
|
311653
312170
|
onLog: (line) => log2(line),
|
|
311654
312171
|
onAICall,
|
|
311655
312172
|
onScreenshot: (base64) => {
|
|
@@ -311659,7 +312176,8 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
311659
312176
|
onScreenshot?.(base64);
|
|
311660
312177
|
} catch {
|
|
311661
312178
|
}
|
|
311662
|
-
}
|
|
312179
|
+
},
|
|
312180
|
+
onScreenshotUpload
|
|
311663
312181
|
});
|
|
311664
312182
|
const collectedTransitions = toCollectedTransitions(explore);
|
|
311665
312183
|
partialCollectedPages = explore.collectedPages;
|
|
@@ -311711,14 +312229,22 @@ Plan: ${planResult.scenarios.length} scenarios, ${planResult.featureGroups.lengt
|
|
|
311711
312229
|
const plans = toPlans(scenarios);
|
|
311712
312230
|
const coverageGaps = planResult.coverageGaps && planResult.coverageGaps.length > 0 ? planResult.coverageGaps : void 0;
|
|
311713
312231
|
if (callbacks?.onPlanReady) {
|
|
311714
|
-
|
|
311715
|
-
|
|
311716
|
-
|
|
311717
|
-
|
|
311718
|
-
|
|
311719
|
-
|
|
311720
|
-
|
|
311721
|
-
|
|
312232
|
+
try {
|
|
312233
|
+
await callbacks.onPlanReady({
|
|
312234
|
+
scenarios,
|
|
312235
|
+
featureGroups,
|
|
312236
|
+
fallbackUsed: planResult.fallbackUsed ?? false,
|
|
312237
|
+
fallbackReason: planResult.fallbackReason
|
|
312238
|
+
});
|
|
312239
|
+
log2("Plan checkpoint: scenario plan POSTed to server");
|
|
312240
|
+
} catch (err) {
|
|
312241
|
+
const message = checkpointErrorMessage(err);
|
|
312242
|
+
if (isTerminalDiscoveryCheckpointError(err)) {
|
|
312243
|
+
log2(`Plan checkpoint rejected for inactive discovery run: ${message}`);
|
|
312244
|
+
throw err;
|
|
312245
|
+
}
|
|
312246
|
+
log2(`Plan checkpoint POST failed (non-fatal): ${message}`);
|
|
312247
|
+
}
|
|
311722
312248
|
}
|
|
311723
312249
|
if (isSurvey) {
|
|
311724
312250
|
const duration2 = (Date.now() - startTime) / 1e3;
|
|
@@ -311752,6 +312278,9 @@ Plan: ${planResult.scenarios.length} scenarios, ${planResult.featureGroups.lengt
|
|
|
311752
312278
|
};
|
|
311753
312279
|
}
|
|
311754
312280
|
log2("\n\u2500\u2500 Phase 3: GENERATE \u2500\u2500");
|
|
312281
|
+
let generatePaused = false;
|
|
312282
|
+
let generatePauseReason;
|
|
312283
|
+
let generateRemainingScenarios;
|
|
311755
312284
|
const tests = await runGenerate({
|
|
311756
312285
|
scenarios,
|
|
311757
312286
|
driver,
|
|
@@ -311759,7 +312288,15 @@ Plan: ${planResult.scenarios.length} scenarios, ${planResult.featureGroups.lengt
|
|
|
311759
312288
|
onLog: (line) => log2(line),
|
|
311760
312289
|
onAICall,
|
|
311761
312290
|
onScreenshot,
|
|
311762
|
-
onTestGenerated: (test) => callbacks?.onTestGenerated?.(test, { phase: "verified" })
|
|
312291
|
+
onTestGenerated: (test) => callbacks?.onTestGenerated?.(test, { phase: "verified" }),
|
|
312292
|
+
// Legacy resume (no saved plan): still skip scenarios already generated by the
|
|
312293
|
+
// paused run so the AI isn't re-invoked for them. Undefined on a fresh run.
|
|
312294
|
+
skipScenarioNames: ctx.resume?.alreadyGeneratedScenarioNames,
|
|
312295
|
+
onInfraStop: (info) => {
|
|
312296
|
+
generatePaused = true;
|
|
312297
|
+
generatePauseReason = info.reason;
|
|
312298
|
+
generateRemainingScenarios = info.remainingScenarioNames;
|
|
312299
|
+
}
|
|
311763
312300
|
});
|
|
311764
312301
|
const generateElapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
311765
312302
|
log2(`
|
|
@@ -311775,7 +312312,7 @@ Generation complete: ${tests.length} tests generated (${generateElapsed}s elapse
|
|
|
311775
312312
|
};
|
|
311776
312313
|
}
|
|
311777
312314
|
const duration = (Date.now() - startTime) / 1e3;
|
|
311778
|
-
const summary = `Mobile discovery complete: explored ${explore.exploreStats.pagesDiscovered} screens, planned ${scenarios.length} scenarios, generated ${tests.length} tests in ${duration.toFixed(1)}s`;
|
|
312315
|
+
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)" : ""}`;
|
|
311779
312316
|
log2(`
|
|
311780
312317
|
=== Mobile Discovery Pipeline Complete (${duration.toFixed(1)}s) ===`);
|
|
311781
312318
|
log2(summary);
|
|
@@ -311800,7 +312337,11 @@ Generation complete: ${tests.length} tests generated (${generateElapsed}s elapse
|
|
|
311800
312337
|
summary,
|
|
311801
312338
|
exploreStats: explore.exploreStats,
|
|
311802
312339
|
duration,
|
|
311803
|
-
mode
|
|
312340
|
+
mode,
|
|
312341
|
+
// D-D4 — a circuit-breaker trip PAUSES the run (resumable) instead of
|
|
312342
|
+
// finalizing COMPLETED/FAILED. The server's isDiscoveryPaused reads these
|
|
312343
|
+
// (forwarded by runner.ts buildFinalBody, byte-identical to the web path).
|
|
312344
|
+
...generatePaused ? { paused: true, pauseReason: generatePauseReason, remainingScenarios: generateRemainingScenarios } : {}
|
|
311804
312345
|
// apiCalls intentionally UNSET — CHUNK K (runner.ts) merges them.
|
|
311805
312346
|
};
|
|
311806
312347
|
} catch (fatalErr) {
|
|
@@ -311846,6 +312387,7 @@ var require_discover_agent = __commonJS({
|
|
|
311846
312387
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
311847
312388
|
exports.runMobileDiscoveryOnRunner = exports.buildInMemorySiteMap = void 0;
|
|
311848
312389
|
exports.runCanvasPreflight = runCanvasPreflight;
|
|
312390
|
+
exports.getFatalExploreError = getFatalExploreError;
|
|
311849
312391
|
exports.runSiteDiscoveryOnRunner = runSiteDiscoveryOnRunner2;
|
|
311850
312392
|
exports.runFeatureDiscoveryOnRunner = runFeatureDiscoveryOnRunner2;
|
|
311851
312393
|
var browser_config_js_1 = require_browser_config();
|
|
@@ -312028,7 +312570,7 @@ Respond with ONLY valid JSON:
|
|
|
312028
312570
|
"keyFindings": "What was discovered - forms, validations, behaviors. Mark inferred items with '(inferred, not tested)'.",
|
|
312029
312571
|
"testingNotes": "Gotchas, validation behaviors, error messages. List visible-but-untested routes as candidates for future exploration."
|
|
312030
312572
|
}`;
|
|
312031
|
-
const client = modelOverride ? (0, ai_provider_js_1.
|
|
312573
|
+
const client = modelOverride ? (0, ai_provider_js_1.getRunnerClientForModel)(modelOverride) : (0, ai_provider_js_1.getRunnerAIClient)();
|
|
312032
312574
|
const agentModel = modelOverride ? (0, ai_provider_js_1.resolveModelForTask)(modelOverride, "agent") : (0, ai_provider_js_1.getModel)("agent");
|
|
312033
312575
|
const callStart = Date.now();
|
|
312034
312576
|
const response = await (0, ai_retry_js_1.withAIRetry)(() => (0, ai_queue_js_1.queuedAICall)(() => client.chat.completions.create({
|
|
@@ -312159,6 +312701,10 @@ Respond with ONLY valid JSON:
|
|
|
312159
312701
|
});
|
|
312160
312702
|
return timed.catch(() => null);
|
|
312161
312703
|
}
|
|
312704
|
+
function getFatalExploreError(exploreResult) {
|
|
312705
|
+
const prefix = "Fatal error:";
|
|
312706
|
+
return exploreResult.summary.startsWith(prefix) ? exploreResult.summary.slice(prefix.length).trim() || exploreResult.summary : void 0;
|
|
312707
|
+
}
|
|
312162
312708
|
async function runSiteDiscoveryOnRunner2(ctx, onLog, onAICall, sharedBrowser, onScreenshot, callbackOptions) {
|
|
312163
312709
|
const logs2 = [];
|
|
312164
312710
|
const screenshots = [];
|
|
@@ -312343,6 +312889,7 @@ Explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elemen
|
|
|
312343
312889
|
if (loginRunP)
|
|
312344
312890
|
await loginRunP;
|
|
312345
312891
|
const duration2 = (Date.now() - startTime) / 1e3;
|
|
312892
|
+
const fatalExploreError = getFatalExploreError(exploreResult);
|
|
312346
312893
|
return {
|
|
312347
312894
|
collectedPages: exploreResult.collectedPages,
|
|
312348
312895
|
collectedTransitions: exploreResult.collectedTransitions,
|
|
@@ -312353,6 +312900,7 @@ Explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elemen
|
|
|
312353
312900
|
pageScreenshots: onScreenshot ? void 0 : Object.fromEntries(mergedScreenshots),
|
|
312354
312901
|
pageScreenshotKeys: onScreenshot ? Object.fromEntries(screenshotKeys) : void 0,
|
|
312355
312902
|
summary: exploreResult.summary || "Exploration completed but no page data captured.",
|
|
312903
|
+
error: fatalExploreError,
|
|
312356
312904
|
exploreStats: {
|
|
312357
312905
|
pagesDiscovered: exploreResult.pagesDiscovered,
|
|
312358
312906
|
pagesVisited: exploreResult.pagesVisited,
|
|
@@ -312894,6 +313442,7 @@ Feature explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResul
|
|
|
312894
313442
|
discoveryApiCalls = earlyClose.apiCalls;
|
|
312895
313443
|
postCloseHealthSummary = earlyClose.healthSummary;
|
|
312896
313444
|
const duration2 = (Date.now() - startTime) / 1e3;
|
|
313445
|
+
const fatalExploreError = getFatalExploreError(exploreResult);
|
|
312897
313446
|
return {
|
|
312898
313447
|
collectedPages: exploreResult.collectedPages,
|
|
312899
313448
|
collectedTransitions: exploreResult.collectedTransitions,
|
|
@@ -312904,7 +313453,8 @@ Feature explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResul
|
|
|
312904
313453
|
healthObservations: earlyClose.healthSummary,
|
|
312905
313454
|
pageScreenshots: onScreenshot ? void 0 : Object.fromEntries(mergedScreenshots),
|
|
312906
313455
|
pageScreenshotKeys: onScreenshot ? Object.fromEntries(screenshotKeys) : void 0,
|
|
312907
|
-
summary: "Feature exploration completed but no page data captured.",
|
|
313456
|
+
summary: fatalExploreError ? exploreResult.summary : "Feature exploration completed but no page data captured.",
|
|
313457
|
+
error: fatalExploreError,
|
|
312908
313458
|
exploreStats: {
|
|
312909
313459
|
pagesDiscovered: exploreResult.pagesDiscovered,
|
|
312910
313460
|
pagesVisited: exploreResult.pagesVisited,
|
|
@@ -326293,7 +326843,7 @@ var require_package4 = __commonJS({
|
|
|
326293
326843
|
"package.json"(exports, module) {
|
|
326294
326844
|
module.exports = {
|
|
326295
326845
|
name: "@validate.qa/runner",
|
|
326296
|
-
version: "1.0.
|
|
326846
|
+
version: "1.0.13",
|
|
326297
326847
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
326298
326848
|
bin: {
|
|
326299
326849
|
"validate-runner": "dist/cli.js"
|
|
@@ -326301,7 +326851,7 @@ var require_package4 = __commonJS({
|
|
|
326301
326851
|
scripts: {
|
|
326302
326852
|
build: "tsup",
|
|
326303
326853
|
dev: "tsx src/cli.ts",
|
|
326304
|
-
"test:mobile": "node --import tsx --test src/runner.mobile-ai-env.test.ts src/services/mobile/mobile-network-capture.smoke.test.ts",
|
|
326854
|
+
"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",
|
|
326305
326855
|
prepublishOnly: "npm run build"
|
|
326306
326856
|
},
|
|
326307
326857
|
files: [
|
|
@@ -326528,8 +327078,10 @@ async function startAppiumServer(options) {
|
|
|
326528
327078
|
return serverState;
|
|
326529
327079
|
}
|
|
326530
327080
|
const port = options?.port ?? APPIUM_PORT;
|
|
326531
|
-
const
|
|
326532
|
-
|
|
327081
|
+
const args = ["--port", String(port)];
|
|
327082
|
+
if (options?.drivers && options.drivers.length > 0) {
|
|
327083
|
+
args.push("--use-drivers", options.drivers.join(","));
|
|
327084
|
+
}
|
|
326533
327085
|
intentionalStop = false;
|
|
326534
327086
|
lastStartOptions = { ...lastStartOptions, ...options };
|
|
326535
327087
|
if (appiumGaveUp) {
|
|
@@ -326539,11 +327091,12 @@ async function startAppiumServer(options) {
|
|
|
326539
327091
|
killPort(port);
|
|
326540
327092
|
return new Promise((resolve, reject) => {
|
|
326541
327093
|
try {
|
|
326542
|
-
|
|
327094
|
+
const child = spawn("appium", args, {
|
|
326543
327095
|
stdio: ["pipe", "pipe", "pipe"],
|
|
326544
327096
|
env: { ...process.env },
|
|
326545
327097
|
detached: false
|
|
326546
327098
|
});
|
|
327099
|
+
appiumProcess = child;
|
|
326547
327100
|
serverState.pid = appiumProcess.pid ?? null;
|
|
326548
327101
|
serverState.port = port;
|
|
326549
327102
|
appiumProcess.stdout?.on("data", (data) => {
|
|
@@ -326560,7 +327113,11 @@ async function startAppiumServer(options) {
|
|
|
326560
327113
|
options?.onLog?.(line);
|
|
326561
327114
|
}
|
|
326562
327115
|
});
|
|
326563
|
-
|
|
327116
|
+
child.on("exit", (code, signal) => {
|
|
327117
|
+
if (appiumProcess !== child) {
|
|
327118
|
+
appendLog(`[appium] Ignoring exit (code=${code}, signal=${signal}) from a superseded process`);
|
|
327119
|
+
return;
|
|
327120
|
+
}
|
|
326564
327121
|
appendLog(`[appium] Process exited (code=${code}, signal=${signal})`);
|
|
326565
327122
|
const currentPort = serverState.port;
|
|
326566
327123
|
const currentRestartCount = serverState.restartCount;
|
|
@@ -326599,7 +327156,11 @@ async function startAppiumServer(options) {
|
|
|
326599
327156
|
startupAbort.abort();
|
|
326600
327157
|
reject(err);
|
|
326601
327158
|
};
|
|
326602
|
-
|
|
327159
|
+
child.on("error", (err) => {
|
|
327160
|
+
if (appiumProcess !== child) {
|
|
327161
|
+
appendLog(`[appium] Ignoring error from a superseded process: ${err.message}`);
|
|
327162
|
+
return;
|
|
327163
|
+
}
|
|
326603
327164
|
appendLog(`[appium] Process error: ${err.message}`);
|
|
326604
327165
|
resetServerState(port);
|
|
326605
327166
|
settleReject(new Error(`Failed to start Appium: ${err.message}. Is appium installed? Run: npm i -g appium`));
|
|
@@ -326718,6 +327279,7 @@ function runCommand(command, args, timeout = 1e4) {
|
|
|
326718
327279
|
}
|
|
326719
327280
|
var realIOSProfilerNegativeUntil = 0;
|
|
326720
327281
|
var REAL_IOS_PROFILER_TTL_MS = 6e4;
|
|
327282
|
+
var REAL_IOS_NO_UDID_HINT = "no paired UDID \u2014 install libimobiledevice (brew install libimobiledevice) and trust this computer on the device";
|
|
326721
327283
|
function discoverIOSSimulators() {
|
|
326722
327284
|
if (platform() !== "darwin") return [];
|
|
326723
327285
|
const json = runCommand("xcrun", ["simctl", "list", "devices", "--json"]);
|
|
@@ -326727,7 +327289,8 @@ function discoverIOSSimulators() {
|
|
|
326727
327289
|
const devices = [];
|
|
326728
327290
|
for (const [runtime, runtimeDevices] of Object.entries(parsed.devices)) {
|
|
326729
327291
|
const versionMatch = runtime.match(/iOS[- ](\d+(?:[-.]\d+)*)/i);
|
|
326730
|
-
|
|
327292
|
+
if (!versionMatch) continue;
|
|
327293
|
+
const platformVersion = versionMatch[1].replace(/-/g, ".");
|
|
326731
327294
|
for (const device of runtimeDevices) {
|
|
326732
327295
|
const state2 = device.state === "Booted" ? "BOOTED" : device.state === "Shutdown" ? "SHUTDOWN" : "LOADING";
|
|
326733
327296
|
if (state2 === "BOOTED") {
|
|
@@ -326800,11 +327363,11 @@ function discoverRealIOSDevices() {
|
|
|
326800
327363
|
const version = runCommand("ideviceinfo", ["-u", serial, "-k", "ProductVersion"]) || "unknown";
|
|
326801
327364
|
devices.push({
|
|
326802
327365
|
id: serial,
|
|
326803
|
-
name
|
|
327366
|
+
name: `${name} (${REAL_IOS_NO_UDID_HINT})`,
|
|
326804
327367
|
platform: "IOS",
|
|
326805
327368
|
platformVersion: version,
|
|
326806
327369
|
state: "BOOTED",
|
|
326807
|
-
isAvailable:
|
|
327370
|
+
isAvailable: false,
|
|
326808
327371
|
isPhysical: true
|
|
326809
327372
|
});
|
|
326810
327373
|
}
|
|
@@ -326820,11 +327383,11 @@ function discoverRealIOSDevices() {
|
|
|
326820
327383
|
if (hasIPhone || hasIPad) {
|
|
326821
327384
|
devices.push({
|
|
326822
327385
|
id: "usb-ios-device",
|
|
326823
|
-
name: hasIPhone ? "iPhone (USB)" : "iPad (USB)"
|
|
327386
|
+
name: `${hasIPhone ? "iPhone (USB)" : "iPad (USB)"} (${REAL_IOS_NO_UDID_HINT})`,
|
|
326824
327387
|
platform: "IOS",
|
|
326825
327388
|
platformVersion: "unknown",
|
|
326826
327389
|
state: "BOOTED",
|
|
326827
|
-
isAvailable:
|
|
327390
|
+
isAvailable: false,
|
|
326828
327391
|
isPhysical: true
|
|
326829
327392
|
});
|
|
326830
327393
|
}
|
|
@@ -326902,7 +327465,9 @@ function discoverAndroidDevices() {
|
|
|
326902
327465
|
const adbDevices = parseAdbDevices();
|
|
326903
327466
|
return adbDevices.filter((d) => d.type === "device" || d.type === "unauthorized").map((d) => {
|
|
326904
327467
|
const isEmulator = d.serial.startsWith("emulator-");
|
|
326905
|
-
const
|
|
327468
|
+
const authorized = d.type === "device";
|
|
327469
|
+
const bootCompleted = authorized && getAndroidProp(d.serial, "sys.boot_completed") === "1";
|
|
327470
|
+
const isAvailable = authorized && bootCompleted;
|
|
326906
327471
|
const name = d.model || d.device || (isEmulator ? "Android Emulator" : "Android Device");
|
|
326907
327472
|
const platformVersion = isAvailable ? getAndroidProp(d.serial, "ro.build.version.release") || "unknown" : "unknown";
|
|
326908
327473
|
return {
|
|
@@ -326910,7 +327475,10 @@ function discoverAndroidDevices() {
|
|
|
326910
327475
|
name,
|
|
326911
327476
|
platform: "ANDROID",
|
|
326912
327477
|
platformVersion,
|
|
326913
|
-
|
|
327478
|
+
// LOADING = authorized but still booting; an unauthorized device stays
|
|
327479
|
+
// BOOTED (isAvailable false) so the dashboard shows it with an
|
|
327480
|
+
// "accept USB debugging" hint rather than hiding it.
|
|
327481
|
+
state: authorized && !bootCompleted ? "LOADING" : "BOOTED",
|
|
326914
327482
|
isAvailable,
|
|
326915
327483
|
// Mark if it's a real device vs emulator
|
|
326916
327484
|
...isEmulator ? {} : { isPhysical: true }
|
|
@@ -326956,15 +327524,25 @@ function discoverAllDevices(options) {
|
|
|
326956
327524
|
}
|
|
326957
327525
|
return [...iosDevices, ...androidDevices];
|
|
326958
327526
|
}
|
|
327527
|
+
var DEVICE_SNAPSHOT_TTL_MS = 8e3;
|
|
327528
|
+
var cachedDeviceSnapshot = null;
|
|
327529
|
+
function getCachedDeviceSnapshot() {
|
|
327530
|
+
const now = Date.now();
|
|
327531
|
+
if (cachedDeviceSnapshot && now - cachedDeviceSnapshot.at < DEVICE_SNAPSHOT_TTL_MS) {
|
|
327532
|
+
return cachedDeviceSnapshot.devices;
|
|
327533
|
+
}
|
|
327534
|
+
const devices = discoverAllDevices();
|
|
327535
|
+
cachedDeviceSnapshot = { at: now, devices };
|
|
327536
|
+
return devices;
|
|
327537
|
+
}
|
|
326959
327538
|
function getRunnerCapabilities() {
|
|
326960
|
-
const
|
|
326961
|
-
const androidDevices = discoverAndroidDevices();
|
|
327539
|
+
const devices = getCachedDeviceSnapshot();
|
|
326962
327540
|
const isReadyDevice = (device) => device.isAvailable && device.state === "BOOTED";
|
|
326963
327541
|
return {
|
|
326964
327542
|
web: true,
|
|
326965
327543
|
// Always capable of web testing via Playwright
|
|
326966
|
-
ios:
|
|
326967
|
-
android:
|
|
327544
|
+
ios: devices.some((d) => d.platform === "IOS" && isReadyDevice(d)),
|
|
327545
|
+
android: devices.some((d) => d.platform === "ANDROID" && isReadyDevice(d))
|
|
326968
327546
|
};
|
|
326969
327547
|
}
|
|
326970
327548
|
|
|
@@ -327103,6 +327681,9 @@ var MitmProxy = class {
|
|
|
327103
327681
|
await server3.forAnyRequest().thenPassThrough({ ignoreHostHttpsErrors: true });
|
|
327104
327682
|
await server3.on("request", (req) => this.onRequest(req));
|
|
327105
327683
|
await server3.on("response", (res) => this.onResponse(res));
|
|
327684
|
+
await server3.on("abort", (req) => {
|
|
327685
|
+
this.inFlight.delete(req.id);
|
|
327686
|
+
});
|
|
327106
327687
|
await server3.on("tls-client-error", (failure) => this.onTlsClientError(failure));
|
|
327107
327688
|
await server3.on("tls-passthrough-opened", (event) => this.onTlsPassthroughOpened(event));
|
|
327108
327689
|
await (preferredPort > 0 ? server3.start(preferredPort) : server3.start());
|
|
@@ -327399,6 +327980,107 @@ async function iosSimRemoveCa(deviceId, onLog) {
|
|
|
327399
327980
|
function errMsg(err) {
|
|
327400
327981
|
return err instanceof Error ? err.message : String(err);
|
|
327401
327982
|
}
|
|
327983
|
+
function parsePrimaryNetworkService(stdout) {
|
|
327984
|
+
for (const line of stdout.split("\n")) {
|
|
327985
|
+
const m = line.match(/^\(\d+\)\s+(.+)$/);
|
|
327986
|
+
if (m) return m[1].trim();
|
|
327987
|
+
}
|
|
327988
|
+
return null;
|
|
327989
|
+
}
|
|
327990
|
+
function parseWebProxyState(stdout) {
|
|
327991
|
+
const field = (label) => {
|
|
327992
|
+
const m = stdout.match(new RegExp(`^${label}:[ \\t]*(.*)$`, "im"));
|
|
327993
|
+
return m ? m[1].trim() : "";
|
|
327994
|
+
};
|
|
327995
|
+
return {
|
|
327996
|
+
enabled: field("Enabled").toLowerCase() === "yes",
|
|
327997
|
+
server: field("Server"),
|
|
327998
|
+
port: field("Port")
|
|
327999
|
+
};
|
|
328000
|
+
}
|
|
328001
|
+
function buildHostProxySetCommands(service, host, port) {
|
|
328002
|
+
const portStr = String(port);
|
|
328003
|
+
return [
|
|
328004
|
+
["-setwebproxy", service, host, portStr],
|
|
328005
|
+
["-setsecurewebproxy", service, host, portStr]
|
|
328006
|
+
];
|
|
328007
|
+
}
|
|
328008
|
+
function buildProxyRestoreArgs(setCmd, stateCmd, service, prior) {
|
|
328009
|
+
if (prior.server) {
|
|
328010
|
+
return [
|
|
328011
|
+
[setCmd, service, prior.server, prior.port || "0"],
|
|
328012
|
+
[stateCmd, service, prior.enabled ? "on" : "off"]
|
|
328013
|
+
];
|
|
328014
|
+
}
|
|
328015
|
+
return [[stateCmd, service, "off"]];
|
|
328016
|
+
}
|
|
328017
|
+
function buildHostProxyRestoreCommands(service, prior) {
|
|
328018
|
+
return [
|
|
328019
|
+
...buildProxyRestoreArgs("-setwebproxy", "-setwebproxystate", service, prior.web),
|
|
328020
|
+
...buildProxyRestoreArgs("-setsecurewebproxy", "-setsecurewebproxystate", service, prior.secure)
|
|
328021
|
+
];
|
|
328022
|
+
}
|
|
328023
|
+
function hostProxyPriorFromMarker(marker) {
|
|
328024
|
+
return {
|
|
328025
|
+
web: {
|
|
328026
|
+
enabled: Boolean(marker.hostWebProxyWasEnabled),
|
|
328027
|
+
server: marker.hostWebProxyPriorServer ?? "",
|
|
328028
|
+
port: marker.hostWebProxyPriorPort ?? ""
|
|
328029
|
+
},
|
|
328030
|
+
secure: {
|
|
328031
|
+
enabled: Boolean(marker.hostSecureProxyWasEnabled),
|
|
328032
|
+
server: marker.hostSecureProxyPriorServer ?? "",
|
|
328033
|
+
port: marker.hostSecureProxyPriorPort ?? ""
|
|
328034
|
+
}
|
|
328035
|
+
};
|
|
328036
|
+
}
|
|
328037
|
+
function hostProxyControlSupported(platform3, isPhysical, platformName = process.platform) {
|
|
328038
|
+
if (platform3 !== "IOS") return { supported: false };
|
|
328039
|
+
if (isPhysical) {
|
|
328040
|
+
return { supported: false, reason: "physical iOS device requires a manual Wi-Fi HTTP proxy; host proxy not applied" };
|
|
328041
|
+
}
|
|
328042
|
+
if (platformName !== "darwin") {
|
|
328043
|
+
return { supported: false, reason: `iOS simulator host proxy is only controllable on macOS (host is ${platformName})` };
|
|
328044
|
+
}
|
|
328045
|
+
return { supported: true };
|
|
328046
|
+
}
|
|
328047
|
+
async function iosSimConfigureHostProxy(host, port, onLog) {
|
|
328048
|
+
try {
|
|
328049
|
+
const { stdout: order } = await execFileAsync("networksetup", ["-listnetworkserviceorder"], {
|
|
328050
|
+
timeout: DEVICE_CMD_TIMEOUT_MS
|
|
328051
|
+
});
|
|
328052
|
+
const service = parsePrimaryNetworkService(order);
|
|
328053
|
+
if (!service) {
|
|
328054
|
+
return { configured: false, reason: "iOS simulator host proxy could not be configured: no active macOS network service found" };
|
|
328055
|
+
}
|
|
328056
|
+
const [{ stdout: webOut }, { stdout: secureOut }] = await Promise.all([
|
|
328057
|
+
execFileAsync("networksetup", ["-getwebproxy", service], { timeout: DEVICE_CMD_TIMEOUT_MS }),
|
|
328058
|
+
execFileAsync("networksetup", ["-getsecurewebproxy", service], { timeout: DEVICE_CMD_TIMEOUT_MS })
|
|
328059
|
+
]);
|
|
328060
|
+
const prior = { web: parseWebProxyState(webOut), secure: parseWebProxyState(secureOut) };
|
|
328061
|
+
for (const args of buildHostProxySetCommands(service, host, port)) {
|
|
328062
|
+
await execFileAsync("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
|
|
328063
|
+
}
|
|
328064
|
+
onLog?.(`[mobile-net] macOS host proxy set on "${service}" \u2192 ${host}:${port} (iOS simulator shares the host network).`);
|
|
328065
|
+
return { configured: true, service, prior };
|
|
328066
|
+
} catch (err) {
|
|
328067
|
+
return { configured: false, reason: `iOS simulator host proxy could not be configured: ${errMsg(err)}` };
|
|
328068
|
+
}
|
|
328069
|
+
}
|
|
328070
|
+
async function iosSimRestoreHostProxy(service, prior, onLog) {
|
|
328071
|
+
const restore = prior ?? {
|
|
328072
|
+
web: { enabled: false, server: "", port: "" },
|
|
328073
|
+
secure: { enabled: false, server: "", port: "" }
|
|
328074
|
+
};
|
|
328075
|
+
for (const args of buildHostProxyRestoreCommands(service, restore)) {
|
|
328076
|
+
try {
|
|
328077
|
+
await execFileAsync("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
|
|
328078
|
+
} catch (err) {
|
|
328079
|
+
onLog?.(`[mobile-net] networksetup restore (${args[0]}) failed: ${errMsg(err)}`);
|
|
328080
|
+
}
|
|
328081
|
+
}
|
|
328082
|
+
onLog?.(`[mobile-net] macOS host proxy restored on "${service}".`);
|
|
328083
|
+
}
|
|
327402
328084
|
var activeCleanups = /* @__PURE__ */ new Map();
|
|
327403
328085
|
var exitHandlersInstalled = false;
|
|
327404
328086
|
function markerPath(sessionKey) {
|
|
@@ -327433,6 +328115,14 @@ function restoreDeviceSync(marker, onLog) {
|
|
|
327433
328115
|
run2(["-s", marker.deviceId, "shell", "rm", "-f", "/sdcard/validateqa-mobile-ca.pem"]);
|
|
327434
328116
|
}
|
|
327435
328117
|
}
|
|
328118
|
+
if (marker.platform === "IOS" && marker.hostProxySet && marker.hostProxyService && process.platform === "darwin") {
|
|
328119
|
+
for (const args of buildHostProxyRestoreCommands(marker.hostProxyService, hostProxyPriorFromMarker(marker))) {
|
|
328120
|
+
try {
|
|
328121
|
+
execFileSync3("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS, stdio: "ignore" });
|
|
328122
|
+
} catch {
|
|
328123
|
+
}
|
|
328124
|
+
}
|
|
328125
|
+
}
|
|
327436
328126
|
onLog?.(`[mobile-net] restored device ${marker.deviceId} from pending-cleanup marker.`);
|
|
327437
328127
|
}
|
|
327438
328128
|
async function reconcilePendingCleanups(onLog) {
|
|
@@ -327444,6 +328134,8 @@ async function reconcilePendingCleanups(onLog) {
|
|
|
327444
328134
|
}
|
|
327445
328135
|
for (const file of files) {
|
|
327446
328136
|
if (!file.endsWith(".json")) continue;
|
|
328137
|
+
const sessionKey = file.slice(0, -".json".length);
|
|
328138
|
+
if (activeCleanups.has(sessionKey)) continue;
|
|
327447
328139
|
const full = join2(PENDING_CLEANUP_DIR, file);
|
|
327448
328140
|
try {
|
|
327449
328141
|
const raw = await readFile(full, "utf-8");
|
|
@@ -327473,8 +328165,10 @@ function ensureExitHandlers() {
|
|
|
327473
328165
|
for (const signal of ["SIGTERM", "SIGINT"]) {
|
|
327474
328166
|
process.on(signal, () => {
|
|
327475
328167
|
drain();
|
|
327476
|
-
process.
|
|
327477
|
-
|
|
328168
|
+
if (process.listenerCount(signal) <= 1) {
|
|
328169
|
+
process.removeAllListeners(signal);
|
|
328170
|
+
process.kill(process.pid, signal);
|
|
328171
|
+
}
|
|
327478
328172
|
});
|
|
327479
328173
|
}
|
|
327480
328174
|
}
|
|
@@ -327524,6 +328218,9 @@ async function startMobileNetworkCapture(options) {
|
|
|
327524
328218
|
let wiringReason = "HTTPS interception pending: capture begins once the device routes a TLS handshake through the proxy.";
|
|
327525
328219
|
let proxySet = false;
|
|
327526
328220
|
let caInstalledOnDevice = false;
|
|
328221
|
+
let hostProxySet = false;
|
|
328222
|
+
let hostProxyService;
|
|
328223
|
+
let hostProxyPrior;
|
|
327527
328224
|
try {
|
|
327528
328225
|
if (platform3 === "ANDROID") {
|
|
327529
328226
|
proxySet = await androidSetProxy(deviceId, hostPort, onLog);
|
|
@@ -327538,25 +328235,50 @@ async function startMobileNetworkCapture(options) {
|
|
|
327538
328235
|
}
|
|
327539
328236
|
} else {
|
|
327540
328237
|
if (!isPhysical && caCertPath) {
|
|
327541
|
-
const { trusted
|
|
328238
|
+
const { trusted } = await iosSimTrustCa(deviceId, caCertPath);
|
|
327542
328239
|
caInstalledOnDevice = trusted;
|
|
327543
|
-
|
|
328240
|
+
}
|
|
328241
|
+
const support = hostProxyControlSupported(platform3, isPhysical);
|
|
328242
|
+
if (support.supported) {
|
|
328243
|
+
const hp = await iosSimConfigureHostProxy(proxyHost, proxyPort, onLog);
|
|
328244
|
+
if (hp.configured) {
|
|
328245
|
+
hostProxySet = true;
|
|
328246
|
+
hostProxyService = hp.service;
|
|
328247
|
+
hostProxyPrior = hp.prior;
|
|
328248
|
+
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).` : ".");
|
|
328249
|
+
} else {
|
|
328250
|
+
wiringReason = hp.reason ?? "iOS simulator host proxy could not be configured";
|
|
328251
|
+
onLog?.(`[mobile-net] ${wiringReason}`);
|
|
328252
|
+
}
|
|
327544
328253
|
} else if (isPhysical) {
|
|
327545
328254
|
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}).` : ".");
|
|
328255
|
+
} else {
|
|
328256
|
+
wiringReason = support.reason ?? "iOS simulator host proxy is unsupported on this host";
|
|
328257
|
+
onLog?.(`[mobile-net] ${wiringReason}`);
|
|
327546
328258
|
}
|
|
327547
328259
|
}
|
|
327548
328260
|
} catch (err) {
|
|
327549
328261
|
wiringReason = `device wiring degraded: ${errMsg(err)}`;
|
|
327550
328262
|
onLog?.(`[mobile-net] ${wiringReason}`);
|
|
327551
328263
|
}
|
|
327552
|
-
if (proxySet || caInstalledOnDevice) {
|
|
328264
|
+
if (proxySet || caInstalledOnDevice || hostProxySet) {
|
|
327553
328265
|
const marker = {
|
|
327554
328266
|
platform: platform3,
|
|
327555
328267
|
deviceId,
|
|
327556
328268
|
isPhysical,
|
|
327557
328269
|
proxySet,
|
|
327558
328270
|
originalProxy,
|
|
327559
|
-
caInstalledOnDevice
|
|
328271
|
+
caInstalledOnDevice,
|
|
328272
|
+
...hostProxySet ? {
|
|
328273
|
+
hostProxySet: true,
|
|
328274
|
+
hostProxyService,
|
|
328275
|
+
hostWebProxyWasEnabled: hostProxyPrior?.web.enabled,
|
|
328276
|
+
hostWebProxyPriorServer: hostProxyPrior?.web.server,
|
|
328277
|
+
hostWebProxyPriorPort: hostProxyPrior?.web.port,
|
|
328278
|
+
hostSecureProxyWasEnabled: hostProxyPrior?.secure.enabled,
|
|
328279
|
+
hostSecureProxyPriorServer: hostProxyPrior?.secure.server,
|
|
328280
|
+
hostSecureProxyPriorPort: hostProxyPrior?.secure.port
|
|
328281
|
+
} : {}
|
|
327560
328282
|
};
|
|
327561
328283
|
activeCleanups.set(sessionKey, marker);
|
|
327562
328284
|
await writePendingCleanup(sessionKey, marker);
|
|
@@ -327576,6 +328298,9 @@ async function startMobileNetworkCapture(options) {
|
|
|
327576
328298
|
if (proxySet && platform3 === "ANDROID") {
|
|
327577
328299
|
await androidClearProxy(deviceId, originalProxy, onLog);
|
|
327578
328300
|
}
|
|
328301
|
+
if (hostProxySet && hostProxyService && platform3 === "IOS" && !isPhysical) {
|
|
328302
|
+
await iosSimRestoreHostProxy(hostProxyService, hostProxyPrior, onLog);
|
|
328303
|
+
}
|
|
327579
328304
|
if (caInstalledOnDevice && capturedCaCertPath) {
|
|
327580
328305
|
if (platform3 === "ANDROID") {
|
|
327581
328306
|
await androidRemoveCa(deviceId, onLog);
|
|
@@ -327611,7 +328336,11 @@ async function startMobileNetworkCapture(options) {
|
|
|
327611
328336
|
|
|
327612
328337
|
// src/services/appium-executor.ts
|
|
327613
328338
|
var DEFAULT_STEP_TIMEOUT_MS = 1e4;
|
|
328339
|
+
var WD_SESSION_CREATE_TIMEOUT_MS = 12e4;
|
|
328340
|
+
var WD_ACTION_TIMEOUT_MS = 3e4;
|
|
328341
|
+
var MAX_SESSION_CREATE_ATTEMPTS = 3;
|
|
327614
328342
|
var execFileAsync2 = promisify2(execFile2);
|
|
328343
|
+
var MOBILE_TEXT_ATTRIBUTE_NAMES = ["text", "content-desc", "contentDescription", "name", "label", "value"];
|
|
327615
328344
|
var WebDriverTransportError = class extends Error {
|
|
327616
328345
|
isTransport = true;
|
|
327617
328346
|
constructor(message) {
|
|
@@ -327623,13 +328352,21 @@ function isTransportError(err) {
|
|
|
327623
328352
|
return err instanceof WebDriverTransportError || typeof err === "object" && err !== null && err.isTransport === true;
|
|
327624
328353
|
}
|
|
327625
328354
|
async function wdRequest(path, options) {
|
|
328355
|
+
const isSessionCreate = (options?.method ?? "GET").toUpperCase() === "POST" && path === "/session";
|
|
328356
|
+
const timeoutMs = isSessionCreate ? WD_SESSION_CREATE_TIMEOUT_MS : WD_ACTION_TIMEOUT_MS;
|
|
327626
328357
|
let res;
|
|
327627
328358
|
try {
|
|
327628
328359
|
res = await fetch(`http://localhost:${getAppiumState().port}${path}`, {
|
|
327629
328360
|
...options,
|
|
328361
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
327630
328362
|
headers: { "Content-Type": "application/json", ...options?.headers }
|
|
327631
328363
|
});
|
|
327632
328364
|
} catch (err) {
|
|
328365
|
+
if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
|
|
328366
|
+
throw new WebDriverTransportError(
|
|
328367
|
+
`WebDriver request to ${path} timed out after ${timeoutMs}ms (device/Appium unresponsive)`
|
|
328368
|
+
);
|
|
328369
|
+
}
|
|
327633
328370
|
throw new WebDriverTransportError(`WebDriver connection error: ${err instanceof Error ? err.message : String(err)}`);
|
|
327634
328371
|
}
|
|
327635
328372
|
if (!res.ok) {
|
|
@@ -327714,6 +328451,31 @@ async function createSession(target) {
|
|
|
327714
328451
|
if (!sessionId) throw new Error("Appium session creation returned no session ID");
|
|
327715
328452
|
return sessionId;
|
|
327716
328453
|
}
|
|
328454
|
+
var TERMINAL_SESSION_START = /not installed|no such (app|file)|could not confirm|bundle ?id .*(not|missing)|xcodeorgid|signing|provisioning|not authorized|unauthorized/i;
|
|
328455
|
+
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;
|
|
328456
|
+
function isRetryableSessionStartError(err) {
|
|
328457
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
328458
|
+
if (TERMINAL_SESSION_START.test(message)) return false;
|
|
328459
|
+
if (isTransportError(err)) return true;
|
|
328460
|
+
return RETRYABLE_SESSION_START.test(message);
|
|
328461
|
+
}
|
|
328462
|
+
async function createSessionWithRetry(target, onRetry) {
|
|
328463
|
+
let lastErr;
|
|
328464
|
+
for (let attempt = 1; attempt <= MAX_SESSION_CREATE_ATTEMPTS; attempt++) {
|
|
328465
|
+
try {
|
|
328466
|
+
return await createSession(target);
|
|
328467
|
+
} catch (err) {
|
|
328468
|
+
lastErr = err;
|
|
328469
|
+
if (attempt >= MAX_SESSION_CREATE_ATTEMPTS || !isRetryableSessionStartError(err)) throw err;
|
|
328470
|
+
const delayMs = Math.pow(2, attempt) * 1e3;
|
|
328471
|
+
onRetry?.(
|
|
328472
|
+
`Appium session start failed (attempt ${attempt}/${MAX_SESSION_CREATE_ATTEMPTS}, retrying in ${delayMs}ms): ${err instanceof Error ? err.message : String(err)}`
|
|
328473
|
+
);
|
|
328474
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
328475
|
+
}
|
|
328476
|
+
}
|
|
328477
|
+
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
328478
|
+
}
|
|
327717
328479
|
async function openDeepLink(sessionId, platform3, deeplink, appId) {
|
|
327718
328480
|
const args = platform3 === "IOS" ? [{ url: deeplink, bundleId: appId }] : [{ url: deeplink, package: appId }];
|
|
327719
328481
|
await wdRequest(`/session/${sessionId}/execute/sync`, {
|
|
@@ -327815,6 +328577,61 @@ async function takeScreenshot(sessionId) {
|
|
|
327815
328577
|
return null;
|
|
327816
328578
|
}
|
|
327817
328579
|
}
|
|
328580
|
+
function mobileRecordingEnabled() {
|
|
328581
|
+
const value = process.env.VALIDATEQA_MOBILE_RUN_VIDEO;
|
|
328582
|
+
return value !== "0" && value !== "false";
|
|
328583
|
+
}
|
|
328584
|
+
function readPositiveIntEnv(name, fallback, min, max) {
|
|
328585
|
+
const raw = process.env[name];
|
|
328586
|
+
const parsed = raw ? Number.parseInt(raw, 10) : NaN;
|
|
328587
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
328588
|
+
return Math.max(min, Math.min(max, parsed));
|
|
328589
|
+
}
|
|
328590
|
+
async function startScreenRecording(sessionId, platform3, log2) {
|
|
328591
|
+
if (!mobileRecordingEnabled()) {
|
|
328592
|
+
log2("[mobile-video] recording disabled by VALIDATEQA_MOBILE_RUN_VIDEO=0");
|
|
328593
|
+
return false;
|
|
328594
|
+
}
|
|
328595
|
+
const timeLimit = readPositiveIntEnv("VALIDATEQA_MOBILE_RECORDING_SECONDS", 180, 5, 180);
|
|
328596
|
+
const options = {
|
|
328597
|
+
timeLimit: String(timeLimit),
|
|
328598
|
+
forceRestart: true
|
|
328599
|
+
};
|
|
328600
|
+
if (platform3 === "ANDROID") {
|
|
328601
|
+
options.bitRate = readPositiveIntEnv("VALIDATEQA_MOBILE_RECORDING_BITRATE", 75e4, 1e5, 4e6);
|
|
328602
|
+
} else {
|
|
328603
|
+
options.videoQuality = process.env.VALIDATEQA_MOBILE_RECORDING_IOS_QUALITY || "low";
|
|
328604
|
+
}
|
|
328605
|
+
try {
|
|
328606
|
+
await wdRequest(`/session/${sessionId}/appium/start_recording_screen`, {
|
|
328607
|
+
method: "POST",
|
|
328608
|
+
body: JSON.stringify({ options })
|
|
328609
|
+
});
|
|
328610
|
+
log2(`[mobile-video] screen recording started (${timeLimit}s max)`);
|
|
328611
|
+
return true;
|
|
328612
|
+
} catch (err) {
|
|
328613
|
+
log2(`[mobile-video] screen recording unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
328614
|
+
return false;
|
|
328615
|
+
}
|
|
328616
|
+
}
|
|
328617
|
+
async function stopScreenRecording(sessionId, log2) {
|
|
328618
|
+
try {
|
|
328619
|
+
const result = await wdRequest(`/session/${sessionId}/appium/stop_recording_screen`, {
|
|
328620
|
+
method: "POST",
|
|
328621
|
+
body: JSON.stringify({ options: {} })
|
|
328622
|
+
});
|
|
328623
|
+
const value = typeof result?.value === "string" ? result.value.trim() : "";
|
|
328624
|
+
if (!value) {
|
|
328625
|
+
log2("[mobile-video] screen recording stopped but Appium returned no video");
|
|
328626
|
+
return void 0;
|
|
328627
|
+
}
|
|
328628
|
+
log2(`[mobile-video] screen recording captured (${Math.round(value.length / 1024)}KB base64)`);
|
|
328629
|
+
return value;
|
|
328630
|
+
} catch (err) {
|
|
328631
|
+
log2(`[mobile-video] screen recording stop failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
328632
|
+
return void 0;
|
|
328633
|
+
}
|
|
328634
|
+
}
|
|
327818
328635
|
function boundsFromStep(step) {
|
|
327819
328636
|
const bounds = step.metadata?.bounds;
|
|
327820
328637
|
if (bounds && typeof bounds === "object" && !Array.isArray(bounds) && typeof bounds.x === "number" && typeof bounds.y === "number" && typeof bounds.width === "number" && typeof bounds.height === "number") {
|
|
@@ -327934,7 +328751,8 @@ async function sendKeysToFocusedInput(sessionId, value) {
|
|
|
327934
328751
|
});
|
|
327935
328752
|
}
|
|
327936
328753
|
function encodeAndroidInputText(value) {
|
|
327937
|
-
|
|
328754
|
+
const inputEncoded = value.replace(/%/g, "%25").replace(/\s/g, "%s");
|
|
328755
|
+
return `'${inputEncoded.replace(/'/g, `'\\''`)}'`;
|
|
327938
328756
|
}
|
|
327939
328757
|
async function sendAndroidTextToFocusedInput(deviceId, value) {
|
|
327940
328758
|
if (!deviceId) {
|
|
@@ -328158,9 +328976,29 @@ async function executeAssertText(sessionId, step) {
|
|
|
328158
328976
|
const elementId = await findElement(sessionId, step.target, readStepTimeout(step));
|
|
328159
328977
|
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/text`);
|
|
328160
328978
|
const actualText = typeof result?.value === "string" ? result.value : "";
|
|
328161
|
-
|
|
328162
|
-
|
|
328979
|
+
const textCandidates = await readElementTextCandidates(sessionId, elementId, actualText);
|
|
328980
|
+
if (!textCandidates.some((value) => value.includes(step.assertion))) {
|
|
328981
|
+
throw new Error(`Text assertion failed: expected "${step.assertion}" to be contained in ${JSON.stringify(textCandidates)}`);
|
|
328982
|
+
}
|
|
328983
|
+
}
|
|
328984
|
+
async function readElementTextCandidates(sessionId, elementId, actualText) {
|
|
328985
|
+
const values = [];
|
|
328986
|
+
const push = (value) => {
|
|
328987
|
+
if (typeof value !== "string") return;
|
|
328988
|
+
const trimmed = value.trim();
|
|
328989
|
+
if (trimmed.length === 0 || values.includes(trimmed)) return;
|
|
328990
|
+
values.push(trimmed);
|
|
328991
|
+
};
|
|
328992
|
+
push(actualText);
|
|
328993
|
+
for (const attr of MOBILE_TEXT_ATTRIBUTE_NAMES) {
|
|
328994
|
+
try {
|
|
328995
|
+
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/attribute/${encodeURIComponent(attr)}`);
|
|
328996
|
+
push(result?.value);
|
|
328997
|
+
} catch (err) {
|
|
328998
|
+
if (isTransportError(err)) throw err;
|
|
328999
|
+
}
|
|
328163
329000
|
}
|
|
329001
|
+
return values;
|
|
328164
329002
|
}
|
|
328165
329003
|
async function executeMobileTest(options) {
|
|
328166
329004
|
const logLines = [];
|
|
@@ -328172,6 +329010,9 @@ async function executeMobileTest(options) {
|
|
|
328172
329010
|
let sessionId = null;
|
|
328173
329011
|
let capture = null;
|
|
328174
329012
|
let captureStopped = false;
|
|
329013
|
+
let recordingStarted = false;
|
|
329014
|
+
let recordingStopped = false;
|
|
329015
|
+
let video;
|
|
328175
329016
|
const stopCapture = async () => {
|
|
328176
329017
|
if (!capture || captureStopped) return apiCalls;
|
|
328177
329018
|
captureStopped = true;
|
|
@@ -328186,6 +329027,15 @@ async function executeMobileTest(options) {
|
|
|
328186
329027
|
}
|
|
328187
329028
|
return apiCalls;
|
|
328188
329029
|
};
|
|
329030
|
+
const stopRecording = async () => {
|
|
329031
|
+
if (!sessionId || !recordingStarted || recordingStopped) return video;
|
|
329032
|
+
recordingStopped = true;
|
|
329033
|
+
const captured = await stopScreenRecording(sessionId, log2);
|
|
329034
|
+
if (captured) {
|
|
329035
|
+
video = captured;
|
|
329036
|
+
}
|
|
329037
|
+
return video;
|
|
329038
|
+
};
|
|
328189
329039
|
try {
|
|
328190
329040
|
log2("Checking Appium server health...");
|
|
328191
329041
|
const health = await checkAppiumHealth();
|
|
@@ -328239,13 +329089,13 @@ async function executeMobileTest(options) {
|
|
|
328239
329089
|
}
|
|
328240
329090
|
log2(`Starting Appium session for ${options.target.appId} on ${selectedDevice.name}...`);
|
|
328241
329091
|
try {
|
|
328242
|
-
sessionId = await
|
|
329092
|
+
sessionId = await createSessionWithRetry({
|
|
328243
329093
|
appId: options.target.appId,
|
|
328244
329094
|
platform: options.target.platform,
|
|
328245
329095
|
deviceId: selectedDevice.id,
|
|
328246
329096
|
platformVersion: options.target.platformVersion ?? selectedDevice.platformVersion,
|
|
328247
329097
|
isPhysical: selectedDevice.isPhysical
|
|
328248
|
-
});
|
|
329098
|
+
}, (msg) => log2(msg));
|
|
328249
329099
|
} catch (err) {
|
|
328250
329100
|
const base2 = err instanceof Error ? err.message : String(err);
|
|
328251
329101
|
if (selectedDevice.isPhysical) {
|
|
@@ -328260,6 +329110,7 @@ async function executeMobileTest(options) {
|
|
|
328260
329110
|
throw err;
|
|
328261
329111
|
}
|
|
328262
329112
|
log2(`Session created: ${sessionId}`);
|
|
329113
|
+
recordingStarted = await startScreenRecording(sessionId, options.target.platform, log2);
|
|
328263
329114
|
if (options.target.launchMode === "DEEP_LINK" && options.target.deeplink) {
|
|
328264
329115
|
try {
|
|
328265
329116
|
log2(`Opening deep link: ${options.target.deeplink}`);
|
|
@@ -328320,7 +329171,9 @@ async function executeMobileTest(options) {
|
|
|
328320
329171
|
break;
|
|
328321
329172
|
}
|
|
328322
329173
|
}
|
|
328323
|
-
|
|
329174
|
+
if (step.action !== "home" && step.action !== "back") {
|
|
329175
|
+
await restoreTargetAppIfExternal(sessionId, options.target, log2, `step ${step.order} (${step.action})`, true);
|
|
329176
|
+
}
|
|
328324
329177
|
} catch (err) {
|
|
328325
329178
|
status = "failed";
|
|
328326
329179
|
stepError = err instanceof Error ? err.message : String(err);
|
|
@@ -328349,10 +329202,12 @@ async function executeMobileTest(options) {
|
|
|
328349
329202
|
const allPassed = !environmental && stepResults.every((r) => r.status === "passed" || r.status === "skipped");
|
|
328350
329203
|
const firstError = environmental ?? (stepResults.find((r) => r.status === "failed")?.error ?? null);
|
|
328351
329204
|
await stopCapture();
|
|
329205
|
+
await stopRecording();
|
|
328352
329206
|
return {
|
|
328353
329207
|
passed: allPassed,
|
|
328354
329208
|
stepResults,
|
|
328355
329209
|
screenshots,
|
|
329210
|
+
...video ? { video, videoMimeType: "video/mp4" } : {},
|
|
328356
329211
|
apiCalls,
|
|
328357
329212
|
logs: logLines.join("\n"),
|
|
328358
329213
|
error: firstError,
|
|
@@ -328363,10 +329218,12 @@ async function executeMobileTest(options) {
|
|
|
328363
329218
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
328364
329219
|
log2(`Fatal error: ${errorMsg}`);
|
|
328365
329220
|
await stopCapture();
|
|
329221
|
+
await stopRecording();
|
|
328366
329222
|
return {
|
|
328367
329223
|
passed: false,
|
|
328368
329224
|
stepResults,
|
|
328369
329225
|
screenshots,
|
|
329226
|
+
...video ? { video, videoMimeType: "video/mp4" } : {},
|
|
328370
329227
|
apiCalls,
|
|
328371
329228
|
logs: logLines.join("\n"),
|
|
328372
329229
|
error: errorMsg,
|
|
@@ -328378,6 +329235,7 @@ async function executeMobileTest(options) {
|
|
|
328378
329235
|
};
|
|
328379
329236
|
} finally {
|
|
328380
329237
|
await stopCapture();
|
|
329238
|
+
await stopRecording();
|
|
328381
329239
|
if (sessionId) {
|
|
328382
329240
|
log2("Tearing down Appium session...");
|
|
328383
329241
|
await deleteSession(sessionId);
|
|
@@ -328418,7 +329276,7 @@ async function createMobileDiscoverySession(target, options) {
|
|
|
328418
329276
|
isPhysical: selectedDevice.isPhysical === true,
|
|
328419
329277
|
platformVersion: selectedDevice.platformVersion
|
|
328420
329278
|
});
|
|
328421
|
-
const sessionId = await
|
|
329279
|
+
const sessionId = await createSessionWithRetry({
|
|
328422
329280
|
appId: target.appId,
|
|
328423
329281
|
platform: target.platform,
|
|
328424
329282
|
deviceId: selectedDevice.id,
|
|
@@ -329229,7 +330087,7 @@ function stopSessionReaper() {
|
|
|
329229
330087
|
}
|
|
329230
330088
|
}
|
|
329231
330089
|
async function reapIdleSession() {
|
|
329232
|
-
if (!state.appiumSessionId || state.mirrorClients.size > 0 || mirrorClientsEmptyAt === null || Date.now() - mirrorClientsEmptyAt < SESSION_IDLE_GRACE_MS) {
|
|
330090
|
+
if (!state.appiumSessionId || mirrorSessionOwner !== "recording" || state.mirrorClients.size > 0 || mirrorClientsEmptyAt === null || Date.now() - mirrorClientsEmptyAt < SESSION_IDLE_GRACE_MS) {
|
|
329233
330091
|
return;
|
|
329234
330092
|
}
|
|
329235
330093
|
const sessionId = state.appiumSessionId;
|
|
@@ -331746,6 +332604,11 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331746
332604
|
});
|
|
331747
332605
|
requestLiveBrowserHeartbeat();
|
|
331748
332606
|
},
|
|
332607
|
+
// D-D6 — upload ONE baseline PNG per newly-observed screen through the
|
|
332608
|
+
// same streaming /page-screenshot relay the web path uses (mobile has no
|
|
332609
|
+
// route, so the screen-id is the key). The returned storage key rides the
|
|
332610
|
+
// explore checkpoint so the server links it to SitePage.screenshotKey.
|
|
332611
|
+
onScreenshotUpload: (screenId, base64) => onScreenshot(screenId, base64),
|
|
331749
332612
|
onExploreComplete,
|
|
331750
332613
|
onTestGenerated,
|
|
331751
332614
|
onPlanReady
|
|
@@ -332048,23 +332911,39 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
332048
332911
|
log.fail("ERROR - No mobile steps");
|
|
332049
332912
|
return;
|
|
332050
332913
|
}
|
|
332914
|
+
const maxRetries = typeof run2.retryCount === "number" && Number.isFinite(run2.retryCount) ? Math.max(0, Math.min(2, Math.floor(run2.retryCount))) : 0;
|
|
332051
332915
|
setExecutorBusy(true);
|
|
332052
|
-
let result2;
|
|
332916
|
+
let result2 = null;
|
|
332917
|
+
const attemptLogs = [];
|
|
332053
332918
|
try {
|
|
332054
|
-
|
|
332055
|
-
|
|
332056
|
-
|
|
332057
|
-
|
|
332058
|
-
|
|
332059
|
-
|
|
332060
|
-
|
|
332061
|
-
|
|
332062
|
-
|
|
332063
|
-
|
|
332064
|
-
|
|
332919
|
+
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
332920
|
+
if (attempt > 0) {
|
|
332921
|
+
log.info(`Retrying mobile test after failed Appium step (${attempt}/${maxRetries})`);
|
|
332922
|
+
}
|
|
332923
|
+
result2 = await executeMobileTest({
|
|
332924
|
+
steps: mobileSteps,
|
|
332925
|
+
target: {
|
|
332926
|
+
appId: target.appId,
|
|
332927
|
+
platform: target.platform,
|
|
332928
|
+
platformVersion: target.platformVersion,
|
|
332929
|
+
recordedDeviceId: target.recordedDeviceId,
|
|
332930
|
+
launchMode: target.launchMode,
|
|
332931
|
+
deeplink: target.deeplink
|
|
332932
|
+
}
|
|
332933
|
+
});
|
|
332934
|
+
attemptLogs.push(`--- Appium attempt ${attempt + 1}/${maxRetries + 1} ---
|
|
332935
|
+
${result2.logs}`);
|
|
332936
|
+
if (result2.passed || result2.environmental || attempt >= maxRetries) break;
|
|
332937
|
+
}
|
|
332065
332938
|
} finally {
|
|
332066
332939
|
setExecutorBusy(false);
|
|
332067
332940
|
}
|
|
332941
|
+
if (!result2) {
|
|
332942
|
+
throw new Error("Appium execution did not produce a result.");
|
|
332943
|
+
}
|
|
332944
|
+
if (attemptLogs.length > 1) {
|
|
332945
|
+
result2 = { ...result2, logs: attemptLogs.join("\n\n") };
|
|
332946
|
+
}
|
|
332068
332947
|
const duration2 = Date.now() - startTime;
|
|
332069
332948
|
const status = result2.environmental ? "ERROR" : result2.passed ? "PASSED" : "FAILED";
|
|
332070
332949
|
const reportedError = result2.environmental ? `CONFIG_ISSUE: ${result2.environmental}` : result2.error ?? void 0;
|
|
@@ -332072,6 +332951,8 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
332072
332951
|
status,
|
|
332073
332952
|
stepResults: result2.stepResults,
|
|
332074
332953
|
screenshots: result2.screenshots,
|
|
332954
|
+
video: result2.video,
|
|
332955
|
+
videoMimeType: result2.videoMimeType,
|
|
332075
332956
|
apiCalls: result2.apiCalls,
|
|
332076
332957
|
logs: result2.logs,
|
|
332077
332958
|
error: reportedError,
|
|
@@ -332383,7 +333264,7 @@ async function startRunner(opts) {
|
|
|
332383
333264
|
};
|
|
332384
333265
|
const sendHeartbeat = async () => {
|
|
332385
333266
|
try {
|
|
332386
|
-
const devices = enableMobile ?
|
|
333267
|
+
const devices = enableMobile ? getCachedDeviceSnapshot() : [];
|
|
332387
333268
|
const capabilities = enableMobile ? {
|
|
332388
333269
|
web: true,
|
|
332389
333270
|
ios: devices.some((d) => d.platform === "IOS" && d.isAvailable && d.state === "BOOTED"),
|