@validate.qa/runner 1.0.5 → 1.0.8
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 +496 -158
- package/dist/cli.mjs +496 -158
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1037,6 +1037,65 @@ var require_mobile_source_parser = __commonJS({
|
|
|
1037
1037
|
}
|
|
1038
1038
|
});
|
|
1039
1039
|
|
|
1040
|
+
// ../runner-core/dist/services/mobile/mobile-boundary.js
|
|
1041
|
+
var require_mobile_boundary = __commonJS({
|
|
1042
|
+
"../runner-core/dist/services/mobile/mobile-boundary.js"(exports2) {
|
|
1043
|
+
"use strict";
|
|
1044
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
1045
|
+
exports2.mobileOwnerFromScreenSignal = mobileOwnerFromScreenSignal;
|
|
1046
|
+
exports2.isTransientMobileSystemOwner = isTransientMobileSystemOwner;
|
|
1047
|
+
exports2.classifyMobileScreenBoundary = classifyMobileScreenBoundary2;
|
|
1048
|
+
exports2.isTargetLikeMobileBoundary = isTargetLikeMobileBoundary2;
|
|
1049
|
+
exports2.mobileBoundaryNote = mobileBoundaryNote;
|
|
1050
|
+
var ANDROID_TRANSIENT_SYSTEM_APP_IDS = /* @__PURE__ */ new Set([
|
|
1051
|
+
"com.android.permissioncontroller",
|
|
1052
|
+
"com.google.android.permissioncontroller",
|
|
1053
|
+
"com.android.packageinstaller",
|
|
1054
|
+
"com.google.android.packageinstaller"
|
|
1055
|
+
]);
|
|
1056
|
+
var IOS_TRANSIENT_SYSTEM_APP_IDS = /* @__PURE__ */ new Set([
|
|
1057
|
+
"com.apple.springboard"
|
|
1058
|
+
]);
|
|
1059
|
+
function mobileOwnerFromScreenSignal(signal, targetAppId) {
|
|
1060
|
+
const bundleId = signal.bundleId?.trim();
|
|
1061
|
+
if (bundleId)
|
|
1062
|
+
return bundleId;
|
|
1063
|
+
const activity = signal.activity?.trim();
|
|
1064
|
+
if (activity && targetAppId && (activity === targetAppId || activity.startsWith(`${targetAppId}.`))) {
|
|
1065
|
+
return targetAppId;
|
|
1066
|
+
}
|
|
1067
|
+
return null;
|
|
1068
|
+
}
|
|
1069
|
+
function isTransientMobileSystemOwner(owner, platform3) {
|
|
1070
|
+
return platform3 === "ANDROID" ? ANDROID_TRANSIENT_SYSTEM_APP_IDS.has(owner) : IOS_TRANSIENT_SYSTEM_APP_IDS.has(owner);
|
|
1071
|
+
}
|
|
1072
|
+
function classifyMobileScreenBoundary2(signal, targetAppId, platform3) {
|
|
1073
|
+
if (!targetAppId)
|
|
1074
|
+
return { kind: "target" };
|
|
1075
|
+
const owner = mobileOwnerFromScreenSignal(signal, targetAppId);
|
|
1076
|
+
if (!owner)
|
|
1077
|
+
return { kind: "unknown" };
|
|
1078
|
+
if (owner === targetAppId)
|
|
1079
|
+
return { kind: "target" };
|
|
1080
|
+
if (isTransientMobileSystemOwner(owner, platform3)) {
|
|
1081
|
+
return { kind: "transient_external", owner, targetAppId };
|
|
1082
|
+
}
|
|
1083
|
+
return { kind: "external", owner, targetAppId };
|
|
1084
|
+
}
|
|
1085
|
+
function isTargetLikeMobileBoundary2(boundary) {
|
|
1086
|
+
return boundary.kind === "target" || boundary.kind === "unknown";
|
|
1087
|
+
}
|
|
1088
|
+
function mobileBoundaryNote(boundary) {
|
|
1089
|
+
return [
|
|
1090
|
+
"",
|
|
1091
|
+
"## App Boundary",
|
|
1092
|
+
`Foreground app is ${boundary.owner}, outside target app ${boundary.targetAppId}.`,
|
|
1093
|
+
boundary.kind === "transient_external" ? "This looks like a system overlay. Handle or dismiss it if needed, but do not capture it as an app screen." : "The runner excluded this external app screen from the target app evidence and relaunched the target app."
|
|
1094
|
+
].join("\n");
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1040
1099
|
// ../shared/dist/types.js
|
|
1041
1100
|
function isUIVariant(value) {
|
|
1042
1101
|
return typeof value === "string" && UI_VARIANTS.includes(value);
|
|
@@ -1205,7 +1264,8 @@ var init_constants = __esm({
|
|
|
1205
1264
|
"GROK_API_KEY",
|
|
1206
1265
|
"MINIMAX_API_KEY",
|
|
1207
1266
|
"ANTHROPIC_API_KEY",
|
|
1208
|
-
"NVIDIA_API_KEY"
|
|
1267
|
+
"NVIDIA_API_KEY",
|
|
1268
|
+
"DEEPSEEK_API_KEY"
|
|
1209
1269
|
]);
|
|
1210
1270
|
SPAWNED_ENV_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
1211
1271
|
// Runtime essentials - Node.js and Playwright need these to function
|
|
@@ -1472,6 +1532,18 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
1472
1532
|
` }`,
|
|
1473
1533
|
` throw new Error('No locator resolved. Tried: ' + JSON.stringify(selectors) + (__lastError ? ' (last error: ' + String(__lastError) + ')' : ''));`,
|
|
1474
1534
|
`}`,
|
|
1535
|
+
``,
|
|
1536
|
+
`async function __tapAt(driver: WebdriverIO.Browser, x: number, y: number) {`,
|
|
1537
|
+
` await driver.performActions([{ type: 'pointer', id: 'finger1', parameters: { pointerType: 'touch' }, actions: [{ type: 'pointerMove', duration: 0, x, y }, { type: 'pointerDown', button: 0 }, { type: 'pause', duration: 100 }, { type: 'pointerUp', button: 0 }] }]);`,
|
|
1538
|
+
`}`,
|
|
1539
|
+
``,
|
|
1540
|
+
`async function __typeIntoFocused(driver: WebdriverIO.Browser, text: string) {`,
|
|
1541
|
+
` try {`,
|
|
1542
|
+
` await (await driver.getActiveElement() as unknown as WebdriverIO.Element).addValue(text);`,
|
|
1543
|
+
` } catch {`,
|
|
1544
|
+
` await driver.keys(text);`,
|
|
1545
|
+
` }`,
|
|
1546
|
+
`}`,
|
|
1475
1547
|
...hasSwipe ? [
|
|
1476
1548
|
``,
|
|
1477
1549
|
`// Read the device window with a few retries (matches the runtime executor).`,
|
|
@@ -1530,13 +1602,25 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
1530
1602
|
const y = Math.round(bounds.y + bounds.height / 2);
|
|
1531
1603
|
lines.push(` await driver.performActions([{ type: 'pointer', id: 'finger1', parameters: { pointerType: 'touch' }, actions: [{ type: 'pointerMove', duration: 0, x: ${x}, y: ${y} }, { type: 'pointerDown', button: 0 }, { type: 'pause', duration: 100 }, { type: 'pointerUp', button: 0 }] }]);`);
|
|
1532
1604
|
} else if (step.action === "type" && hasSelector) {
|
|
1533
|
-
|
|
1605
|
+
if (bounds) {
|
|
1606
|
+
const x = Math.round(bounds.x + bounds.width / 2);
|
|
1607
|
+
const y = Math.round(bounds.y + bounds.height / 2);
|
|
1608
|
+
lines.push(` try {`);
|
|
1609
|
+
lines.push(` await __tapAt(driver, ${x}, ${y});`);
|
|
1610
|
+
lines.push(` await driver.pause(${MOBILE_FOCUS_SETTLE_MS});`);
|
|
1611
|
+
lines.push(` await __typeIntoFocused(driver, ${toJsStringLiteral(step.value ?? "")});`);
|
|
1612
|
+
lines.push(` } catch {`);
|
|
1613
|
+
lines.push(` await (await findFirst(driver, ${candidatesLiteral})).addValue(${toJsStringLiteral(step.value ?? "")});`);
|
|
1614
|
+
lines.push(` }`);
|
|
1615
|
+
} else {
|
|
1616
|
+
lines.push(` await (await findFirst(driver, ${candidatesLiteral})).addValue(${toJsStringLiteral(step.value ?? "")});`);
|
|
1617
|
+
}
|
|
1534
1618
|
} else if (step.action === "type" && bounds) {
|
|
1535
1619
|
const x = Math.round(bounds.x + bounds.width / 2);
|
|
1536
1620
|
const y = Math.round(bounds.y + bounds.height / 2);
|
|
1537
|
-
lines.push(` await driver
|
|
1621
|
+
lines.push(` await __tapAt(driver, ${x}, ${y});`);
|
|
1538
1622
|
lines.push(` await driver.pause(${MOBILE_FOCUS_SETTLE_MS});`);
|
|
1539
|
-
lines.push(` await (
|
|
1623
|
+
lines.push(` await __typeIntoFocused(driver, ${toJsStringLiteral(step.value ?? "")});`);
|
|
1540
1624
|
} else if (step.action === "clear" && hasSelector) {
|
|
1541
1625
|
lines.push(` await (await findFirst(driver, ${candidatesLiteral})).clearValue();`);
|
|
1542
1626
|
} else if (step.action === "assertVisible" && hasSelector) {
|
|
@@ -2050,6 +2134,7 @@ var init_audit_pricing = __esm({
|
|
|
2050
2134
|
"MiniMax-M2.7": { input: 0.3, output: 1.2 },
|
|
2051
2135
|
"MiniMax-M2.7-highspeed": { input: 0.6, output: 2.4 },
|
|
2052
2136
|
"MiniMax-M2": { input: 0.3, output: 1.2 },
|
|
2137
|
+
"deepseek-v4-flash": { input: 0.14, output: 0.28 },
|
|
2053
2138
|
// Whisper bills per-minute, not per-token
|
|
2054
2139
|
"whisper-1": null,
|
|
2055
2140
|
// Historical / deprecated (kept so old audit rows still show a cost).
|
|
@@ -4433,21 +4518,13 @@ var require_mobile_explorer = __commonJS({
|
|
|
4433
4518
|
var ai_retry_js_1 = require_ai_retry();
|
|
4434
4519
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
4435
4520
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
4521
|
+
var mobile_boundary_js_1 = require_mobile_boundary();
|
|
4436
4522
|
var MAX_MOBILE_ITERATIONS_SURVEY = 40;
|
|
4437
4523
|
var MAX_MOBILE_ITERATIONS_DEEP = 60;
|
|
4438
4524
|
var MAX_SCREENSHOTS = 15;
|
|
4439
4525
|
var MAX_RECENT_EXCHANGES = 24;
|
|
4440
4526
|
var OBSERVATION_MAX_ELEMENTS = 50;
|
|
4441
4527
|
var NO_SNAPSHOT_NUDGE_THRESHOLD = 4;
|
|
4442
|
-
var ANDROID_TRANSIENT_SYSTEM_APP_IDS = /* @__PURE__ */ new Set([
|
|
4443
|
-
"com.android.permissioncontroller",
|
|
4444
|
-
"com.google.android.permissioncontroller",
|
|
4445
|
-
"com.android.packageinstaller",
|
|
4446
|
-
"com.google.android.packageinstaller"
|
|
4447
|
-
]);
|
|
4448
|
-
var IOS_TRANSIENT_SYSTEM_APP_IDS = /* @__PURE__ */ new Set([
|
|
4449
|
-
"com.apple.springboard"
|
|
4450
|
-
]);
|
|
4451
4528
|
var MOBILE_SNAPSHOT_TOOL = {
|
|
4452
4529
|
type: "function",
|
|
4453
4530
|
function: {
|
|
@@ -4670,40 +4747,6 @@ var require_mobile_explorer = __commonJS({
|
|
|
4670
4747
|
return "timeout";
|
|
4671
4748
|
return "ai_error";
|
|
4672
4749
|
}
|
|
4673
|
-
function ownerFromScreenSignal(signal, targetAppId) {
|
|
4674
|
-
const bundleId = signal.bundleId?.trim();
|
|
4675
|
-
if (bundleId)
|
|
4676
|
-
return bundleId;
|
|
4677
|
-
const activity = signal.activity?.trim();
|
|
4678
|
-
if (activity && targetAppId && (activity === targetAppId || activity.startsWith(`${targetAppId}.`))) {
|
|
4679
|
-
return targetAppId;
|
|
4680
|
-
}
|
|
4681
|
-
return null;
|
|
4682
|
-
}
|
|
4683
|
-
function isTransientSystemOwner(owner, platform3) {
|
|
4684
|
-
return platform3 === "ANDROID" ? ANDROID_TRANSIENT_SYSTEM_APP_IDS.has(owner) : IOS_TRANSIENT_SYSTEM_APP_IDS.has(owner);
|
|
4685
|
-
}
|
|
4686
|
-
function classifyMobileScreenBoundary(signal, targetAppId, platform3) {
|
|
4687
|
-
if (!targetAppId)
|
|
4688
|
-
return { kind: "target" };
|
|
4689
|
-
const owner = ownerFromScreenSignal(signal, targetAppId);
|
|
4690
|
-
if (!owner)
|
|
4691
|
-
return { kind: "unknown" };
|
|
4692
|
-
if (owner === targetAppId)
|
|
4693
|
-
return { kind: "target" };
|
|
4694
|
-
if (isTransientSystemOwner(owner, platform3)) {
|
|
4695
|
-
return { kind: "transient_external", owner, targetAppId };
|
|
4696
|
-
}
|
|
4697
|
-
return { kind: "external", owner, targetAppId };
|
|
4698
|
-
}
|
|
4699
|
-
function boundaryNote(boundary) {
|
|
4700
|
-
return [
|
|
4701
|
-
"",
|
|
4702
|
-
"## App Boundary",
|
|
4703
|
-
`Foreground app is ${boundary.owner}, outside target app ${boundary.targetAppId}.`,
|
|
4704
|
-
boundary.kind === "transient_external" ? "This looks like a system overlay. Handle or dismiss it if needed, but do not capture it as an app screen." : "The runner excluded this external app screen from the survey evidence."
|
|
4705
|
-
].join("\n");
|
|
4706
|
-
}
|
|
4707
4750
|
function resolveSnapshot(xml, windowSize, driverSignal, platform3) {
|
|
4708
4751
|
const parsed = (0, mobile_source_parser_js_1.parseMobileSource)(xml, platform3);
|
|
4709
4752
|
const signal = {
|
|
@@ -4901,10 +4944,10 @@ Use this to prioritize which screens and flows to cover.` : "";
|
|
|
4901
4944
|
};
|
|
4902
4945
|
const currentBoundary = () => {
|
|
4903
4946
|
const current = getLatest();
|
|
4904
|
-
return current ? classifyMobileScreenBoundary(current.signal, targetAppId, platform3) : { kind: "unknown" };
|
|
4947
|
+
return current ? (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(current.signal, targetAppId, platform3) : { kind: "unknown" };
|
|
4905
4948
|
};
|
|
4906
4949
|
const absorbResolvedObservation = async (resolved, screenshot, iterationForObservation, source) => {
|
|
4907
|
-
const boundary = classifyMobileScreenBoundary(resolved.signal, targetAppId, platform3);
|
|
4950
|
+
const boundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(resolved.signal, targetAppId, platform3);
|
|
4908
4951
|
if (boundary.kind === "target" || boundary.kind === "unknown") {
|
|
4909
4952
|
const screenId = ingestSnapshot(resolved, iterationForObservation);
|
|
4910
4953
|
captureScreenshot(screenshot, screenId);
|
|
@@ -4915,7 +4958,7 @@ Use this to prioritize which screens and flows to cover.` : "";
|
|
|
4915
4958
|
if (boundary.kind === "transient_external") {
|
|
4916
4959
|
log2(` System overlay detected from ${source}: ${boundary.owner}; not adding it to target-app survey evidence.`);
|
|
4917
4960
|
return {
|
|
4918
|
-
observation: `${resolved.observation}${
|
|
4961
|
+
observation: `${resolved.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(boundary)}`,
|
|
4919
4962
|
screenId: null,
|
|
4920
4963
|
transientExternal: true,
|
|
4921
4964
|
externalOwner: boundary.owner
|
|
@@ -4934,7 +4977,7 @@ Use this to prioritize which screens and flows to cover.` : "";
|
|
|
4934
4977
|
externalOwner: boundary.owner
|
|
4935
4978
|
};
|
|
4936
4979
|
}
|
|
4937
|
-
const recoveredBoundary = classifyMobileScreenBoundary(recovered.signal, targetAppId, platform3);
|
|
4980
|
+
const recoveredBoundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(recovered.signal, targetAppId, platform3);
|
|
4938
4981
|
latest = recovered;
|
|
4939
4982
|
lastSnapshotIteration = iterationForObservation;
|
|
4940
4983
|
if (recoveredBoundary.kind === "target" || recoveredBoundary.kind === "unknown") {
|
|
@@ -4950,7 +4993,7 @@ Use this to prioritize which screens and flows to cover.` : "";
|
|
|
4950
4993
|
if (recoveredBoundary.kind === "transient_external") {
|
|
4951
4994
|
log2(` Relaunch surfaced system overlay ${recoveredBoundary.owner}; not adding it to target-app survey evidence.`);
|
|
4952
4995
|
return {
|
|
4953
|
-
observation: `${recovered.observation}${
|
|
4996
|
+
observation: `${recovered.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(recoveredBoundary)}`,
|
|
4954
4997
|
screenId: null,
|
|
4955
4998
|
externalBlocked: true,
|
|
4956
4999
|
transientExternal: true,
|
|
@@ -8594,6 +8637,7 @@ var require_mobile_generator = __commonJS({
|
|
|
8594
8637
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
8595
8638
|
var mobile_source_parser_js_2 = require_mobile_source_parser();
|
|
8596
8639
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
8640
|
+
var mobile_boundary_js_1 = require_mobile_boundary();
|
|
8597
8641
|
var MAX_SCENARIO_ITERATIONS = 30;
|
|
8598
8642
|
var MAX_RECENT_EXCHANGES = 24;
|
|
8599
8643
|
var OBSERVATION_MAX_ELEMENTS = 50;
|
|
@@ -8779,8 +8823,9 @@ var require_mobile_generator = __commonJS({
|
|
|
8779
8823
|
function hasStableId(element) {
|
|
8780
8824
|
return !!nonEmpty2(element.accessibilityId) || !!nonEmpty2(element.resourceId);
|
|
8781
8825
|
}
|
|
8782
|
-
function buildSystemPrompt(platform3) {
|
|
8826
|
+
function buildSystemPrompt(platform3, targetAppId) {
|
|
8783
8827
|
const platformLabel = platform3 === "IOS" ? "iOS" : "Android";
|
|
8828
|
+
const targetLine = targetAppId ? ` Target app package/bundle: ${targetAppId}.` : "";
|
|
8784
8829
|
return `You are an expert mobile QA agent generating ONE automated test by driving a real ${platformLabel} app on a connected device. The app has just been relaunched into a clean state.
|
|
8785
8830
|
|
|
8786
8831
|
## How the loop works
|
|
@@ -8800,7 +8845,8 @@ var require_mobile_generator = __commonJS({
|
|
|
8800
8845
|
- Re-snapshot before every assertion so the ref is fresh.
|
|
8801
8846
|
- Assert the OUTCOME of the flow (the new screen / new content), not the control you just tapped.
|
|
8802
8847
|
- Do NOT trigger irreversible side effects (real payments, sending messages to others) \u2014 note them and assert what you safely can.
|
|
8803
|
-
- Stay inside this app.
|
|
8848
|
+
- Stay inside this app.${targetLine} Do not open or use app stores, browsers, email apps, settings, or any other app. If an action leaves the target app, the runner will relaunch the target app and reject that action.
|
|
8849
|
+
- Finish promptly once the outcome is proven.
|
|
8804
8850
|
|
|
8805
8851
|
Max iterations: ${MAX_SCENARIO_ITERATIONS}. You will be warned near the end.`;
|
|
8806
8852
|
}
|
|
@@ -8873,7 +8919,7 @@ Start by calling mobile_snapshot, then execute the steps. Finish with at least o
|
|
|
8873
8919
|
}
|
|
8874
8920
|
}
|
|
8875
8921
|
async function runScenarioLoop(args) {
|
|
8876
|
-
const { scenario, driver, platform: platform3, maxIterations, agentModel, provider, aiClient, auditGroupId, onAICall, log: log2 } = args;
|
|
8922
|
+
const { scenario, driver, platform: platform3, maxIterations, agentModel, provider, aiClient, auditGroupId, onAICall, onScreenshot, targetAppId, log: log2 } = args;
|
|
8877
8923
|
const actions = [];
|
|
8878
8924
|
let assertCount = 0;
|
|
8879
8925
|
let finish = null;
|
|
@@ -8887,14 +8933,98 @@ Start by calling mobile_snapshot, then execute the steps. Finish with at least o
|
|
|
8887
8933
|
const fromActivity = signal.activity ? signal.activity.split(".").pop() : void 0;
|
|
8888
8934
|
return nonEmpty2(fromActivity) ?? nonEmpty2(signal.bundleId) ?? `screen-${signal.screenHash.slice(0, 8)}`;
|
|
8889
8935
|
};
|
|
8890
|
-
const
|
|
8891
|
-
if (!xml)
|
|
8892
|
-
return null;
|
|
8893
|
-
const resolved = resolveSnapshot(xml, windowSize ?? latest?.screen.windowSize ?? null, driverSignal, platform3);
|
|
8936
|
+
const setLatest = (resolved) => {
|
|
8894
8937
|
latest = resolved;
|
|
8895
8938
|
lastScreenName = screenNameFor(resolved);
|
|
8896
8939
|
return resolved;
|
|
8897
8940
|
};
|
|
8941
|
+
const buildResolved = (xml, driverSignal, windowSize) => {
|
|
8942
|
+
if (!xml)
|
|
8943
|
+
return null;
|
|
8944
|
+
return resolveSnapshot(xml, windowSize ?? latest?.screen.windowSize ?? null, driverSignal, platform3);
|
|
8945
|
+
};
|
|
8946
|
+
const emitScreenshot = (base64) => {
|
|
8947
|
+
if (!base64)
|
|
8948
|
+
return;
|
|
8949
|
+
try {
|
|
8950
|
+
onScreenshot?.(base64);
|
|
8951
|
+
} catch {
|
|
8952
|
+
}
|
|
8953
|
+
};
|
|
8954
|
+
const absorbResolvedObservation = async (resolved, screenshot, source) => {
|
|
8955
|
+
const boundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(resolved.signal, targetAppId, platform3);
|
|
8956
|
+
if ((0, mobile_boundary_js_1.isTargetLikeMobileBoundary)(boundary)) {
|
|
8957
|
+
setLatest(resolved);
|
|
8958
|
+
emitScreenshot(screenshot);
|
|
8959
|
+
return { resolved, observation: resolved.observation };
|
|
8960
|
+
}
|
|
8961
|
+
setLatest(resolved);
|
|
8962
|
+
if (boundary.kind === "transient_external") {
|
|
8963
|
+
log2(` system overlay detected from ${source}: ${boundary.owner}; not recording it as a target-app step.`);
|
|
8964
|
+
return {
|
|
8965
|
+
resolved,
|
|
8966
|
+
observation: `${resolved.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(boundary)}`,
|
|
8967
|
+
transientExternal: true,
|
|
8968
|
+
externalOwner: boundary.owner
|
|
8969
|
+
};
|
|
8970
|
+
}
|
|
8971
|
+
log2(` external app detected from ${source}: ${boundary.owner}; rejecting the action and relaunching ${boundary.targetAppId}.`);
|
|
8972
|
+
try {
|
|
8973
|
+
await driver.relaunch();
|
|
8974
|
+
const snap = await driver.snapshot();
|
|
8975
|
+
const recovered = buildResolved(snap.xml, snap.screenSignal, snap.windowSize);
|
|
8976
|
+
if (!recovered) {
|
|
8977
|
+
return {
|
|
8978
|
+
resolved: null,
|
|
8979
|
+
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunched target app, but no UI tree was returned)`,
|
|
8980
|
+
externalBlocked: true,
|
|
8981
|
+
externalOwner: boundary.owner
|
|
8982
|
+
};
|
|
8983
|
+
}
|
|
8984
|
+
const recoveredBoundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(recovered.signal, targetAppId, platform3);
|
|
8985
|
+
setLatest(recovered);
|
|
8986
|
+
if ((0, mobile_boundary_js_1.isTargetLikeMobileBoundary)(recoveredBoundary)) {
|
|
8987
|
+
emitScreenshot(snap.screenshot);
|
|
8988
|
+
return {
|
|
8989
|
+
resolved: recovered,
|
|
8990
|
+
observation: recovered.observation,
|
|
8991
|
+
externalBlocked: true,
|
|
8992
|
+
externalOwner: boundary.owner
|
|
8993
|
+
};
|
|
8994
|
+
}
|
|
8995
|
+
if (recoveredBoundary.kind === "transient_external") {
|
|
8996
|
+
log2(` relaunch surfaced system overlay ${recoveredBoundary.owner}; not recording it as a target-app step.`);
|
|
8997
|
+
return {
|
|
8998
|
+
resolved: recovered,
|
|
8999
|
+
observation: `${recovered.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(recoveredBoundary)}`,
|
|
9000
|
+
externalBlocked: true,
|
|
9001
|
+
transientExternal: true,
|
|
9002
|
+
externalOwner: boundary.owner
|
|
9003
|
+
};
|
|
9004
|
+
}
|
|
9005
|
+
log2(` relaunch still outside target app (${recoveredBoundary.owner}); no target-app step recorded.`);
|
|
9006
|
+
return {
|
|
9007
|
+
resolved: recovered,
|
|
9008
|
+
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${recoveredBoundary.owner}, so no target-app step was recorded)`,
|
|
9009
|
+
externalBlocked: true,
|
|
9010
|
+
externalOwner: boundary.owner
|
|
9011
|
+
};
|
|
9012
|
+
} catch (err) {
|
|
9013
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
9014
|
+
return {
|
|
9015
|
+
resolved: null,
|
|
9016
|
+
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; failed to relaunch target app: ${message})`,
|
|
9017
|
+
externalBlocked: true,
|
|
9018
|
+
externalOwner: boundary.owner
|
|
9019
|
+
};
|
|
9020
|
+
}
|
|
9021
|
+
};
|
|
9022
|
+
const absorbObservation = async (xml, driverSignal, windowSize, screenshot, source) => {
|
|
9023
|
+
const resolved = buildResolved(xml, driverSignal, windowSize);
|
|
9024
|
+
if (!resolved)
|
|
9025
|
+
return { resolved: null, observation: null };
|
|
9026
|
+
return absorbResolvedObservation(resolved, screenshot, source);
|
|
9027
|
+
};
|
|
8898
9028
|
const resolveRef = (ref) => {
|
|
8899
9029
|
if (typeof ref !== "string" || !latest)
|
|
8900
9030
|
return null;
|
|
@@ -8902,11 +9032,11 @@ Start by calling mobile_snapshot, then execute the steps. Finish with at least o
|
|
|
8902
9032
|
};
|
|
8903
9033
|
try {
|
|
8904
9034
|
const seed = await driver.snapshot();
|
|
8905
|
-
|
|
9035
|
+
await absorbObservation(seed.xml, seed.screenSignal, seed.windowSize, seed.screenshot, "seed snapshot");
|
|
8906
9036
|
} catch (err) {
|
|
8907
9037
|
log2(` seed snapshot failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
8908
9038
|
}
|
|
8909
|
-
const systemPrompt = buildSystemPrompt(platform3);
|
|
9039
|
+
const systemPrompt = buildSystemPrompt(platform3, targetAppId);
|
|
8910
9040
|
const kickoff = buildScenarioKickoff(scenario);
|
|
8911
9041
|
const recentExchanges = [];
|
|
8912
9042
|
let iteration = 0;
|
|
@@ -9017,7 +9147,7 @@ ${statePacket}` },
|
|
|
9017
9147
|
driver,
|
|
9018
9148
|
platform: platform3,
|
|
9019
9149
|
latest: getLatest,
|
|
9020
|
-
|
|
9150
|
+
absorbObservation,
|
|
9021
9151
|
resolveRef,
|
|
9022
9152
|
screenName: () => lastScreenName || "UnknownScreen",
|
|
9023
9153
|
recordAction: (action) => {
|
|
@@ -9040,60 +9170,85 @@ ${statePacket}` },
|
|
|
9040
9170
|
return { actions, assertCount, finish, lastSnapshot: getLatest(), lastScreenName };
|
|
9041
9171
|
}
|
|
9042
9172
|
async function dispatchGenerateTool(args) {
|
|
9043
|
-
const { toolName, toolArgs, driver, platform: platform3, latest,
|
|
9173
|
+
const { toolName, toolArgs, driver, platform: platform3, latest, absorbObservation, resolveRef, screenName, recordAction, log: log2 } = args;
|
|
9044
9174
|
const unknownRef = (ref) => ({
|
|
9045
9175
|
ok: false,
|
|
9046
9176
|
error: `Unknown element ref "${String(ref)}". Call mobile_snapshot first; refs are only valid for the latest snapshot.`
|
|
9047
9177
|
});
|
|
9178
|
+
const actionPayload = (result, absorbed) => {
|
|
9179
|
+
const blocked = Boolean(absorbed.externalBlocked || absorbed.transientExternal);
|
|
9180
|
+
return {
|
|
9181
|
+
ok: result.ok && !blocked,
|
|
9182
|
+
...result.error ? { error: result.error } : {},
|
|
9183
|
+
...blocked ? { error: "Action left the target app and was not recorded. Continue from the relaunched target app." } : {},
|
|
9184
|
+
observation: absorbed.observation ?? void 0,
|
|
9185
|
+
...absorbed.externalBlocked ? { blockedExternalApp: true, externalApp: absorbed.externalOwner } : {},
|
|
9186
|
+
...absorbed.transientExternal ? { transientExternalApp: true, externalApp: absorbed.externalOwner } : {}
|
|
9187
|
+
};
|
|
9188
|
+
};
|
|
9189
|
+
const shouldRecordAction = (result, absorbed) => result.ok && !absorbed.externalBlocked && !absorbed.transientExternal;
|
|
9048
9190
|
switch (toolName) {
|
|
9049
9191
|
case "mobile_snapshot": {
|
|
9050
9192
|
const snap = await driver.snapshot();
|
|
9051
|
-
const
|
|
9052
|
-
if (!resolved)
|
|
9193
|
+
const absorbed = await absorbObservation(snap.xml, snap.screenSignal, snap.windowSize, snap.screenshot, "mobile_snapshot");
|
|
9194
|
+
if (!absorbed.resolved)
|
|
9053
9195
|
return { ok: false, error: "snapshot returned no UI tree" };
|
|
9054
|
-
log2(` snapshot: ${resolved.screen.elements.length} elements`);
|
|
9055
|
-
return {
|
|
9196
|
+
log2(` snapshot: ${absorbed.resolved.screen.elements.length} elements`);
|
|
9197
|
+
return {
|
|
9198
|
+
observation: absorbed.observation,
|
|
9199
|
+
...absorbed.externalBlocked ? { blockedExternalApp: true, externalApp: absorbed.externalOwner } : {},
|
|
9200
|
+
...absorbed.transientExternal ? { transientExternalApp: true, externalApp: absorbed.externalOwner } : {}
|
|
9201
|
+
};
|
|
9056
9202
|
}
|
|
9057
9203
|
case "mobile_tap": {
|
|
9058
9204
|
const element = resolveRef(toolArgs.ref);
|
|
9059
9205
|
if (!element)
|
|
9060
9206
|
return unknownRef(toolArgs.ref);
|
|
9207
|
+
const beforeScreenName = screenName();
|
|
9061
9208
|
const result = await driver.tap(element.bounds);
|
|
9062
|
-
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9209
|
+
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_tap");
|
|
9210
|
+
if (shouldRecordAction(result, absorbed)) {
|
|
9211
|
+
recordAction({
|
|
9212
|
+
type: "TAP",
|
|
9213
|
+
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
9214
|
+
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
9215
|
+
});
|
|
9216
|
+
}
|
|
9217
|
+
return actionPayload(result, absorbed);
|
|
9069
9218
|
}
|
|
9070
9219
|
case "mobile_type": {
|
|
9071
9220
|
const element = resolveRef(toolArgs.ref);
|
|
9072
9221
|
if (!element)
|
|
9073
9222
|
return unknownRef(toolArgs.ref);
|
|
9074
9223
|
const value = typeof toolArgs.value === "string" ? toolArgs.value : String(toolArgs.value ?? "");
|
|
9224
|
+
const beforeScreenName = screenName();
|
|
9075
9225
|
const result = await driver.type(value, element.bounds);
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
9226
|
+
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_type");
|
|
9227
|
+
if (shouldRecordAction(result, absorbed)) {
|
|
9228
|
+
recordAction({
|
|
9229
|
+
type: "TYPE",
|
|
9230
|
+
value,
|
|
9231
|
+
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
9232
|
+
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
9233
|
+
});
|
|
9234
|
+
}
|
|
9235
|
+
return actionPayload(result, absorbed);
|
|
9084
9236
|
}
|
|
9085
9237
|
case "mobile_clear": {
|
|
9086
9238
|
const element = resolveRef(toolArgs.ref);
|
|
9087
9239
|
if (!element)
|
|
9088
9240
|
return unknownRef(toolArgs.ref);
|
|
9241
|
+
const beforeScreenName = screenName();
|
|
9089
9242
|
const result = await driver.clear(element.bounds);
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9243
|
+
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_clear");
|
|
9244
|
+
if (shouldRecordAction(result, absorbed)) {
|
|
9245
|
+
recordAction({
|
|
9246
|
+
type: "CLEAR",
|
|
9247
|
+
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
9248
|
+
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
9249
|
+
});
|
|
9250
|
+
}
|
|
9251
|
+
return actionPayload(result, absorbed);
|
|
9097
9252
|
}
|
|
9098
9253
|
case "mobile_swipe": {
|
|
9099
9254
|
const direction = toolArgs.direction;
|
|
@@ -9101,30 +9256,42 @@ ${statePacket}` },
|
|
|
9101
9256
|
return { ok: false, error: "direction must be one of up|down|left|right" };
|
|
9102
9257
|
}
|
|
9103
9258
|
const distance = typeof toolArgs.distance === "number" ? toolArgs.distance : void 0;
|
|
9259
|
+
const beforeScreenName = screenName();
|
|
9104
9260
|
const result = await driver.swipe(direction, distance);
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
|
|
9111
|
-
|
|
9112
|
-
|
|
9113
|
-
|
|
9114
|
-
|
|
9261
|
+
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_swipe");
|
|
9262
|
+
if (shouldRecordAction(result, absorbed)) {
|
|
9263
|
+
recordAction({
|
|
9264
|
+
type: "SWIPE",
|
|
9265
|
+
metadata: {
|
|
9266
|
+
screenName: beforeScreenName,
|
|
9267
|
+
direction,
|
|
9268
|
+
...distance !== void 0 ? { distance } : {}
|
|
9269
|
+
}
|
|
9270
|
+
});
|
|
9271
|
+
}
|
|
9272
|
+
return actionPayload(result, absorbed);
|
|
9115
9273
|
}
|
|
9116
9274
|
case "mobile_back": {
|
|
9275
|
+
const beforeScreenName = screenName();
|
|
9117
9276
|
const result = await driver.back();
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9277
|
+
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_back");
|
|
9278
|
+
if (shouldRecordAction(result, absorbed)) {
|
|
9279
|
+
recordAction({ type: "BACK", metadata: { screenName: beforeScreenName } });
|
|
9280
|
+
}
|
|
9281
|
+
return actionPayload(result, absorbed);
|
|
9121
9282
|
}
|
|
9122
9283
|
case "mobile_relaunch": {
|
|
9123
9284
|
await driver.relaunch();
|
|
9124
9285
|
const snap = await driver.snapshot();
|
|
9125
|
-
const
|
|
9286
|
+
const absorbed = await absorbObservation(snap.xml, snap.screenSignal, snap.windowSize, snap.screenshot, "mobile_relaunch");
|
|
9126
9287
|
log2(" relaunched app for hermetic reset.");
|
|
9127
|
-
return {
|
|
9288
|
+
return {
|
|
9289
|
+
ok: true,
|
|
9290
|
+
observation: absorbed.observation,
|
|
9291
|
+
...absorbed.externalBlocked ? { blockedExternalApp: true, externalApp: absorbed.externalOwner } : {},
|
|
9292
|
+
...absorbed.transientExternal ? { transientExternalApp: true, externalApp: absorbed.externalOwner } : {},
|
|
9293
|
+
note: "App relaunched (clean state). Not recorded as a step."
|
|
9294
|
+
};
|
|
9128
9295
|
}
|
|
9129
9296
|
case "mobile_assert_visible": {
|
|
9130
9297
|
const recorded = recordAssertVisible(toolArgs, { resolveRef, screenName, platform: platform3, recordAction });
|
|
@@ -9262,7 +9429,7 @@ ${statePacket}` },
|
|
|
9262
9429
|
return test;
|
|
9263
9430
|
}
|
|
9264
9431
|
async function runMobileGeneratePhase(args) {
|
|
9265
|
-
const { scenarios, driver, ctx, config, onLog, onAICall, onTestGenerated } = args;
|
|
9432
|
+
const { scenarios, driver, ctx, config, onLog, onAICall, onTestGenerated, onScreenshot } = args;
|
|
9266
9433
|
const log2 = (line) => {
|
|
9267
9434
|
try {
|
|
9268
9435
|
onLog?.(line);
|
|
@@ -9276,6 +9443,7 @@ ${statePacket}` },
|
|
|
9276
9443
|
const provider = (0, ai_provider_js_1.getProviderForModel)(agentModel);
|
|
9277
9444
|
const aiClient = modelOverride ? (0, ai_provider_js_1.getMobileClientForModel)(modelOverride) : (0, ai_provider_js_1.getMobileAIClient)();
|
|
9278
9445
|
const auditGroupId = `mobile-generate-${(0, crypto_1.randomUUID)().slice(0, 8)}`;
|
|
9446
|
+
const targetAppId = ctx.target?.appId?.trim() || void 0;
|
|
9279
9447
|
log2(`Mobile generator starting \u2014 platform: ${platform3}, provider: ${provider}, model: ${agentModel}, scenarios: ${scenarios.length}`);
|
|
9280
9448
|
const tests = [];
|
|
9281
9449
|
for (let i = 0; i < scenarios.length; i++) {
|
|
@@ -9294,6 +9462,8 @@ ${statePacket}` },
|
|
|
9294
9462
|
aiClient,
|
|
9295
9463
|
auditGroupId,
|
|
9296
9464
|
onAICall,
|
|
9465
|
+
onScreenshot,
|
|
9466
|
+
targetAppId,
|
|
9297
9467
|
log: log2
|
|
9298
9468
|
});
|
|
9299
9469
|
const test = assembleTest({ scenario, recording, ctx, platform: platform3, log: log2 });
|
|
@@ -311423,7 +311593,9 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
311423
311593
|
return {
|
|
311424
311594
|
collectedPages: explore.collectedPages,
|
|
311425
311595
|
collectedTransitions,
|
|
311426
|
-
|
|
311596
|
+
// Native screenshots are multi-megabyte PNGs. The live mirror streams them
|
|
311597
|
+
// through heartbeat telemetry; discovery checkpoints keep the screen graph
|
|
311598
|
+
// and transitions so large Android surveys cannot exceed server body limits.
|
|
311427
311599
|
exploreStats: explore.exploreStats,
|
|
311428
311600
|
healthObservations: explore.healthObservations
|
|
311429
311601
|
};
|
|
@@ -311581,6 +311753,7 @@ Plan: ${planResult.scenarios.length} scenarios, ${planResult.featureGroups.lengt
|
|
|
311581
311753
|
ctx,
|
|
311582
311754
|
onLog: (line) => log2(line),
|
|
311583
311755
|
onAICall,
|
|
311756
|
+
onScreenshot,
|
|
311584
311757
|
onTestGenerated: (test) => callbacks?.onTestGenerated?.(test, { phase: "verified" })
|
|
311585
311758
|
});
|
|
311586
311759
|
const generateElapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
@@ -325286,7 +325459,7 @@ var require_live_browser_registry = __commonJS({
|
|
|
325286
325459
|
var THUMBNAIL_INTERVAL_MS = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_INTERVAL_MS ?? "4000", 10) || 4e3;
|
|
325287
325460
|
var THUMBNAIL_QUALITY = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_QUALITY ?? "60", 10) || 60;
|
|
325288
325461
|
var BROWSER_THUMBNAIL_MAX_BASE64_BYTES = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_MAX_BASE64_BYTES ?? "400000", 10) || 4e5;
|
|
325289
|
-
var MOBILE_THUMBNAIL_MAX_BASE64_BYTES = parseInt(process.env.LIVE_MOBILE_THUMBNAIL_MAX_BASE64_BYTES ?? "
|
|
325462
|
+
var MOBILE_THUMBNAIL_MAX_BASE64_BYTES = parseInt(process.env.LIVE_MOBILE_THUMBNAIL_MAX_BASE64_BYTES ?? "4500000", 10) || 45e5;
|
|
325290
325463
|
var STALE_SESSION_MS = parseInt(process.env.LIVE_BROWSER_STALE_MS ?? `${10 * 60 * 1e3}`, 10) || 10 * 60 * 1e3;
|
|
325291
325464
|
var MAX_SESSIONS = parseInt(process.env.LIVE_BROWSER_MAX_SESSIONS ?? "32", 10) || 32;
|
|
325292
325465
|
var LiveBrowserRegistry = class {
|
|
@@ -325782,6 +325955,7 @@ var require_dist2 = __commonJS({
|
|
|
325782
325955
|
__exportStar(require_ai_provider(), exports2);
|
|
325783
325956
|
__exportStar(require_mobile_types(), exports2);
|
|
325784
325957
|
__exportStar(require_mobile_source_parser(), exports2);
|
|
325958
|
+
__exportStar(require_mobile_boundary(), exports2);
|
|
325785
325959
|
var mobile_explorer_js_1 = require_mobile_explorer();
|
|
325786
325960
|
Object.defineProperty(exports2, "runMobileExplorePhase", { enumerable: true, get: function() {
|
|
325787
325961
|
return mobile_explorer_js_1.runMobileExplorePhase;
|
|
@@ -326114,7 +326288,7 @@ var require_package4 = __commonJS({
|
|
|
326114
326288
|
"package.json"(exports2, module2) {
|
|
326115
326289
|
module2.exports = {
|
|
326116
326290
|
name: "@validate.qa/runner",
|
|
326117
|
-
version: "1.0.
|
|
326291
|
+
version: "1.0.8",
|
|
326118
326292
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
326119
326293
|
bin: {
|
|
326120
326294
|
"validate-runner": "dist/cli.js"
|
|
@@ -326212,7 +326386,7 @@ var import_runner_core = __toESM(require_dist2());
|
|
|
326212
326386
|
var import_runner_core2 = __toESM(require_dist2());
|
|
326213
326387
|
|
|
326214
326388
|
// src/runner.ts
|
|
326215
|
-
var
|
|
326389
|
+
var import_runner_core10 = __toESM(require_dist2());
|
|
326216
326390
|
|
|
326217
326391
|
// src/services/regression-runner.ts
|
|
326218
326392
|
var import_runner_core3 = __toESM(require_dist2());
|
|
@@ -326227,7 +326401,7 @@ var import_runner_core5 = __toESM(require_dist2());
|
|
|
326227
326401
|
var import_runner_core6 = __toESM(require_dist2());
|
|
326228
326402
|
|
|
326229
326403
|
// src/runner.ts
|
|
326230
|
-
var
|
|
326404
|
+
var import_runner_core11 = __toESM(require_dist2());
|
|
326231
326405
|
|
|
326232
326406
|
// src/services/auth-state.ts
|
|
326233
326407
|
var INLINE_LOGIN_TAG = "@inline-login";
|
|
@@ -326247,6 +326421,7 @@ function shouldUseAuthState(tags, playwrightCode) {
|
|
|
326247
326421
|
|
|
326248
326422
|
// src/services/appium-executor.ts
|
|
326249
326423
|
init_dist();
|
|
326424
|
+
var import_runner_core8 = __toESM(require_dist2());
|
|
326250
326425
|
|
|
326251
326426
|
// src/services/appium-lifecycle.ts
|
|
326252
326427
|
var import_child_process = require("child_process");
|
|
@@ -327535,6 +327710,86 @@ async function openDeepLink(sessionId, platform3, deeplink, appId) {
|
|
|
327535
327710
|
body: JSON.stringify({ script: "mobile: deepLink", args })
|
|
327536
327711
|
});
|
|
327537
327712
|
}
|
|
327713
|
+
function readExecuteStringValue(result) {
|
|
327714
|
+
if (result && typeof result === "object" && "value" in result) {
|
|
327715
|
+
const value = result.value;
|
|
327716
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
327717
|
+
}
|
|
327718
|
+
return void 0;
|
|
327719
|
+
}
|
|
327720
|
+
function readActiveBundleId(result) {
|
|
327721
|
+
if (result && typeof result === "object" && "value" in result) {
|
|
327722
|
+
const value = result.value;
|
|
327723
|
+
if (value && typeof value === "object" && "bundleId" in value) {
|
|
327724
|
+
const bundleId = value.bundleId;
|
|
327725
|
+
if (typeof bundleId === "string" && bundleId.trim()) return bundleId.trim();
|
|
327726
|
+
}
|
|
327727
|
+
}
|
|
327728
|
+
return void 0;
|
|
327729
|
+
}
|
|
327730
|
+
function normalizeAndroidActivity(activity, pkg2) {
|
|
327731
|
+
const trimmed = activity?.trim();
|
|
327732
|
+
if (!trimmed) return void 0;
|
|
327733
|
+
const packageName = pkg2?.trim();
|
|
327734
|
+
if (!packageName) return trimmed;
|
|
327735
|
+
if (trimmed.startsWith(".")) return `${packageName}${trimmed}`;
|
|
327736
|
+
if (!trimmed.includes(".")) return `${packageName}.${trimmed}`;
|
|
327737
|
+
return trimmed;
|
|
327738
|
+
}
|
|
327739
|
+
async function executeMobileScript(sessionId, script, args) {
|
|
327740
|
+
return wdRequest(`/session/${sessionId}/execute/sync`, {
|
|
327741
|
+
method: "POST",
|
|
327742
|
+
body: JSON.stringify({ script, args })
|
|
327743
|
+
});
|
|
327744
|
+
}
|
|
327745
|
+
async function readForegroundScreenSignal(sessionId, platform3) {
|
|
327746
|
+
if (platform3 === "ANDROID") {
|
|
327747
|
+
const [packageResult, activityResult] = await Promise.all([
|
|
327748
|
+
executeMobileScript(sessionId, "mobile: getCurrentPackage", []),
|
|
327749
|
+
executeMobileScript(sessionId, "mobile: getCurrentActivity", []).catch(() => void 0)
|
|
327750
|
+
]);
|
|
327751
|
+
const pkg2 = readExecuteStringValue(packageResult);
|
|
327752
|
+
if (!pkg2) return null;
|
|
327753
|
+
const activity = normalizeAndroidActivity(readExecuteStringValue(activityResult), pkg2);
|
|
327754
|
+
return {
|
|
327755
|
+
platform: platform3,
|
|
327756
|
+
bundleId: pkg2,
|
|
327757
|
+
...activity ? { activity } : {},
|
|
327758
|
+
screenHash: "foreground"
|
|
327759
|
+
};
|
|
327760
|
+
}
|
|
327761
|
+
const infoResult = await executeMobileScript(sessionId, "mobile: activeAppInfo", []);
|
|
327762
|
+
const bundleId = readActiveBundleId(infoResult);
|
|
327763
|
+
if (!bundleId) return null;
|
|
327764
|
+
return { platform: platform3, bundleId, screenHash: "foreground" };
|
|
327765
|
+
}
|
|
327766
|
+
async function activateTargetApp(sessionId, platform3, appId) {
|
|
327767
|
+
const args = platform3 === "IOS" ? [{ bundleId: appId }] : [{ appId }];
|
|
327768
|
+
await executeMobileScript(sessionId, "mobile: activateApp", args);
|
|
327769
|
+
}
|
|
327770
|
+
async function restoreTargetAppIfExternal(sessionId, target, log2, source, failOnExternal) {
|
|
327771
|
+
let boundary = null;
|
|
327772
|
+
try {
|
|
327773
|
+
const signal = await readForegroundScreenSignal(sessionId, target.platform);
|
|
327774
|
+
boundary = signal ? (0, import_runner_core8.classifyMobileScreenBoundary)(signal, target.appId, target.platform) : null;
|
|
327775
|
+
} catch (err) {
|
|
327776
|
+
log2(` App boundary check skipped after ${source}: ${err instanceof Error ? err.message : String(err)}`);
|
|
327777
|
+
return;
|
|
327778
|
+
}
|
|
327779
|
+
if (!boundary || (0, import_runner_core8.isTargetLikeMobileBoundary)(boundary) || boundary.kind === "transient_external") {
|
|
327780
|
+
return;
|
|
327781
|
+
}
|
|
327782
|
+
const message = `Foreground app changed to ${boundary.owner}, outside target app ${boundary.targetAppId}`;
|
|
327783
|
+
log2(` ${message}; activating target app.`);
|
|
327784
|
+
try {
|
|
327785
|
+
await activateTargetApp(sessionId, target.platform, target.appId);
|
|
327786
|
+
} catch (err) {
|
|
327787
|
+
log2(` Could not reactivate ${target.appId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
327788
|
+
}
|
|
327789
|
+
if (failOnExternal) {
|
|
327790
|
+
throw new Error(`${message}; relaunched target app and failed the step so mobile tests do not continue in another app.`);
|
|
327791
|
+
}
|
|
327792
|
+
}
|
|
327538
327793
|
async function deleteSession(sessionId) {
|
|
327539
327794
|
try {
|
|
327540
327795
|
await wdRequest(`/session/${sessionId}`, { method: "DELETE" });
|
|
@@ -327661,6 +327916,41 @@ async function getActiveElementId(sessionId) {
|
|
|
327661
327916
|
return null;
|
|
327662
327917
|
}
|
|
327663
327918
|
}
|
|
327919
|
+
async function sendKeysToFocusedInput(sessionId, value) {
|
|
327920
|
+
await wdRequest(`/session/${sessionId}/keys`, {
|
|
327921
|
+
method: "POST",
|
|
327922
|
+
body: JSON.stringify({ text: value, value: Array.from(value) })
|
|
327923
|
+
});
|
|
327924
|
+
}
|
|
327925
|
+
async function trySendElementValue(sessionId, elementId, value) {
|
|
327926
|
+
try {
|
|
327927
|
+
await wdRequest(`/session/${sessionId}/element/${elementId}/value`, {
|
|
327928
|
+
method: "POST",
|
|
327929
|
+
body: JSON.stringify({ text: value })
|
|
327930
|
+
});
|
|
327931
|
+
return null;
|
|
327932
|
+
} catch (error2) {
|
|
327933
|
+
if (isTransportError(error2)) throw error2;
|
|
327934
|
+
return error2;
|
|
327935
|
+
}
|
|
327936
|
+
}
|
|
327937
|
+
function shouldAbortAfterStepFailure(action) {
|
|
327938
|
+
switch (action) {
|
|
327939
|
+
case "launchApp":
|
|
327940
|
+
case "tap":
|
|
327941
|
+
case "type":
|
|
327942
|
+
case "clear":
|
|
327943
|
+
case "swipe":
|
|
327944
|
+
case "scroll":
|
|
327945
|
+
case "back":
|
|
327946
|
+
case "home":
|
|
327947
|
+
case "waitForElement":
|
|
327948
|
+
return true;
|
|
327949
|
+
case "assertVisible":
|
|
327950
|
+
case "assertText":
|
|
327951
|
+
return false;
|
|
327952
|
+
}
|
|
327953
|
+
}
|
|
327664
327954
|
async function executeTap(sessionId, step) {
|
|
327665
327955
|
const bounds = boundsFromStep(step);
|
|
327666
327956
|
if (step.target) {
|
|
@@ -327682,30 +327972,64 @@ async function executeTap(sessionId, step) {
|
|
|
327682
327972
|
}
|
|
327683
327973
|
async function executeType(sessionId, step) {
|
|
327684
327974
|
if (step.value === void 0 || step.value === null || step.value === "") return;
|
|
327975
|
+
const value = String(step.value);
|
|
327685
327976
|
const bounds = boundsFromStep(step);
|
|
327686
327977
|
let elementId = null;
|
|
327978
|
+
let lastInputError = null;
|
|
327979
|
+
let tappedBounds = false;
|
|
327980
|
+
if (bounds) {
|
|
327981
|
+
await tapCoordinates(sessionId, bounds);
|
|
327982
|
+
tappedBounds = true;
|
|
327983
|
+
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
327984
|
+
elementId = await getActiveElementId(sessionId);
|
|
327985
|
+
if (elementId) {
|
|
327986
|
+
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
327987
|
+
if (!lastInputError) return;
|
|
327988
|
+
elementId = null;
|
|
327989
|
+
}
|
|
327990
|
+
}
|
|
327687
327991
|
if (step.target) {
|
|
327688
327992
|
try {
|
|
327689
327993
|
elementId = await findElement(sessionId, step.target, readStepTimeout(step));
|
|
327994
|
+
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
327995
|
+
if (!lastInputError) return;
|
|
327996
|
+
elementId = null;
|
|
327690
327997
|
} catch (error2) {
|
|
327691
327998
|
if (!bounds) throw error2;
|
|
327999
|
+
lastInputError = error2;
|
|
327692
328000
|
}
|
|
327693
328001
|
}
|
|
327694
328002
|
if (!elementId && bounds) {
|
|
327695
328003
|
await tapCoordinates(sessionId, bounds);
|
|
328004
|
+
tappedBounds = true;
|
|
327696
328005
|
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
327697
328006
|
elementId = await getActiveElementId(sessionId);
|
|
327698
328007
|
}
|
|
327699
328008
|
if (!elementId) {
|
|
327700
328009
|
elementId = await getActiveElementId(sessionId);
|
|
327701
328010
|
}
|
|
327702
|
-
if (
|
|
327703
|
-
|
|
328011
|
+
if (elementId) {
|
|
328012
|
+
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
328013
|
+
if (!lastInputError) return;
|
|
328014
|
+
elementId = null;
|
|
328015
|
+
if (!bounds) {
|
|
328016
|
+
throw lastInputError instanceof Error ? lastInputError : new Error(String(lastInputError));
|
|
328017
|
+
}
|
|
327704
328018
|
}
|
|
327705
|
-
|
|
327706
|
-
|
|
327707
|
-
|
|
327708
|
-
|
|
328019
|
+
if (bounds) {
|
|
328020
|
+
if (!tappedBounds) {
|
|
328021
|
+
await tapCoordinates(sessionId, bounds);
|
|
328022
|
+
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
328023
|
+
}
|
|
328024
|
+
try {
|
|
328025
|
+
await sendKeysToFocusedInput(sessionId, value);
|
|
328026
|
+
return;
|
|
328027
|
+
} catch (error2) {
|
|
328028
|
+
if (isTransportError(error2)) throw error2;
|
|
328029
|
+
lastInputError = error2;
|
|
328030
|
+
}
|
|
328031
|
+
}
|
|
328032
|
+
throw new Error(`TYPE step could not send text to the focused target${lastInputError instanceof Error ? `: ${lastInputError.message}` : ""}`);
|
|
327709
328033
|
}
|
|
327710
328034
|
async function executeClear(sessionId, step) {
|
|
327711
328035
|
const bounds = boundsFromStep(step);
|
|
@@ -327909,6 +328233,7 @@ async function executeMobileTest(options) {
|
|
|
327909
328233
|
log2(` \u2717 Deep link failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
327910
328234
|
}
|
|
327911
328235
|
}
|
|
328236
|
+
await restoreTargetAppIfExternal(sessionId, options.target, log2, "session start", false);
|
|
327912
328237
|
let environmental = null;
|
|
327913
328238
|
for (const step of options.steps) {
|
|
327914
328239
|
const stepStart = Date.now();
|
|
@@ -327916,6 +328241,7 @@ async function executeMobileTest(options) {
|
|
|
327916
328241
|
let status = "passed";
|
|
327917
328242
|
let stepError;
|
|
327918
328243
|
try {
|
|
328244
|
+
await restoreTargetAppIfExternal(sessionId, options.target, log2, `step ${step.order} pre-check`, false);
|
|
327919
328245
|
switch (step.action) {
|
|
327920
328246
|
case "launchApp":
|
|
327921
328247
|
log2(" App launched (handled by session creation)");
|
|
@@ -327955,6 +328281,7 @@ async function executeMobileTest(options) {
|
|
|
327955
328281
|
break;
|
|
327956
328282
|
}
|
|
327957
328283
|
}
|
|
328284
|
+
await restoreTargetAppIfExternal(sessionId, options.target, log2, `step ${step.order} (${step.action})`, true);
|
|
327958
328285
|
} catch (err) {
|
|
327959
328286
|
status = "failed";
|
|
327960
328287
|
stepError = err instanceof Error ? err.message : String(err);
|
|
@@ -327975,6 +328302,10 @@ async function executeMobileTest(options) {
|
|
|
327975
328302
|
log2(`Aborting remaining steps \u2014 Appium session is no longer usable: ${environmental}`);
|
|
327976
328303
|
break;
|
|
327977
328304
|
}
|
|
328305
|
+
if (status === "failed" && shouldAbortAfterStepFailure(step.action)) {
|
|
328306
|
+
log2(`Aborting remaining steps \u2014 ${step.action} failed, so downstream mobile steps are no longer reliable.`);
|
|
328307
|
+
break;
|
|
328308
|
+
}
|
|
327978
328309
|
}
|
|
327979
328310
|
const allPassed = !environmental && stepResults.every((r) => r.status === "passed" || r.status === "skipped");
|
|
327980
328311
|
const firstError = environmental ?? (stepResults.find((r) => r.status === "failed")?.error ?? null);
|
|
@@ -329240,7 +329571,7 @@ function detachMirrorSession(sessionId) {
|
|
|
329240
329571
|
}
|
|
329241
329572
|
|
|
329242
329573
|
// src/services/mobile/mobile-bridge-driver.ts
|
|
329243
|
-
var
|
|
329574
|
+
var import_runner_core9 = __toESM(require_dist2());
|
|
329244
329575
|
init_dist();
|
|
329245
329576
|
var APPIUM_SESSION_ID_PATTERN2 = /^[a-f0-9-]{1,64}$/i;
|
|
329246
329577
|
function readStringValue(result) {
|
|
@@ -329250,7 +329581,7 @@ function readStringValue(result) {
|
|
|
329250
329581
|
}
|
|
329251
329582
|
return void 0;
|
|
329252
329583
|
}
|
|
329253
|
-
function
|
|
329584
|
+
function normalizeAndroidActivity2(activity, pkg2) {
|
|
329254
329585
|
const trimmed = activity?.trim();
|
|
329255
329586
|
if (!trimmed) return void 0;
|
|
329256
329587
|
const packageName = pkg2?.trim();
|
|
@@ -329286,7 +329617,7 @@ var MobileBridgeDriver = class {
|
|
|
329286
329617
|
return this.platform === "ANDROID" ? { appId: this.appId } : { bundleId: this.appId };
|
|
329287
329618
|
}
|
|
329288
329619
|
baseScreenSignal(source) {
|
|
329289
|
-
const parsed = (0,
|
|
329620
|
+
const parsed = (0, import_runner_core9.parseMobileSource)(source, this.platform);
|
|
329290
329621
|
return {
|
|
329291
329622
|
platform: this.platform,
|
|
329292
329623
|
screenHash: parsed.screenSignal.screenHash,
|
|
@@ -329305,11 +329636,11 @@ var MobileBridgeDriver = class {
|
|
|
329305
329636
|
const activity = readStringValue(activityRes);
|
|
329306
329637
|
const pkg2 = readStringValue(packageRes);
|
|
329307
329638
|
if (pkg2) signal.bundleId = pkg2;
|
|
329308
|
-
const normalizedActivity =
|
|
329639
|
+
const normalizedActivity = normalizeAndroidActivity2(activity, pkg2);
|
|
329309
329640
|
if (normalizedActivity) signal.activity = normalizedActivity;
|
|
329310
329641
|
} else {
|
|
329311
329642
|
const infoRes = await this.executeScript("mobile: activeAppInfo", []).catch(() => void 0);
|
|
329312
|
-
const bundleId =
|
|
329643
|
+
const bundleId = readActiveBundleId2(infoRes);
|
|
329313
329644
|
if (bundleId) signal.bundleId = bundleId;
|
|
329314
329645
|
}
|
|
329315
329646
|
} catch {
|
|
@@ -329451,7 +329782,7 @@ var MobileBridgeDriver = class {
|
|
|
329451
329782
|
return this.enrichScreenSignal(source);
|
|
329452
329783
|
}
|
|
329453
329784
|
};
|
|
329454
|
-
function
|
|
329785
|
+
function readActiveBundleId2(result) {
|
|
329455
329786
|
if (result && typeof result === "object" && "value" in result) {
|
|
329456
329787
|
const value = result.value;
|
|
329457
329788
|
if (value && typeof value === "object" && "bundleId" in value) {
|
|
@@ -329677,7 +330008,7 @@ function printDoctorReport(report) {
|
|
|
329677
330008
|
|
|
329678
330009
|
// src/runner.ts
|
|
329679
330010
|
init_dist();
|
|
329680
|
-
var
|
|
330011
|
+
var import_runner_core12 = __toESM(require_dist2());
|
|
329681
330012
|
function fingerprintTest(test) {
|
|
329682
330013
|
const code = test.framework === "APPIUM" ? [test.appiumCode ?? "", JSON.stringify(test.steps ?? [])].join("\n--steps--\n") : test.playwrightCode ?? "";
|
|
329683
330014
|
return (0, import_node_crypto2.createHash)("sha256").update(test.name).update("\n--suite--\n").update(test.suiteName ?? "").update("\n--type--\n").update(test.testType ?? "").update("\n--framework--\n").update(test.framework ?? "").update("\n--code--\n").update(code).digest("hex").slice(0, 24);
|
|
@@ -330122,7 +330453,7 @@ function pruneCollectedPageForPost(page) {
|
|
|
330122
330453
|
if (Array.isArray(out.observations)) out.observations = out.observations.slice(-200).map((obs) => compactForAudit(obs));
|
|
330123
330454
|
return out;
|
|
330124
330455
|
}
|
|
330125
|
-
function prepareDiscoveryResultForPost(result, logs2) {
|
|
330456
|
+
function prepareDiscoveryResultForPost(result, logs2, options = {}) {
|
|
330126
330457
|
const safe = { ...result, logs: truncateTextTail(logs2, RESULT_LOG_MAX_CHARS, "discovery result log") };
|
|
330127
330458
|
if (Array.isArray(safe.collectedPages)) safe.collectedPages = safe.collectedPages.map(pruneCollectedPageForPost);
|
|
330128
330459
|
if (Array.isArray(safe.apiCalls)) safe.apiCalls = safe.apiCalls.map((call) => compactForAudit(call));
|
|
@@ -330138,6 +330469,10 @@ function prepareDiscoveryResultForPost(result, logs2) {
|
|
|
330138
330469
|
if (safe.snapshotEvidence) safe.snapshotEvidence = compactForAudit(safe.snapshotEvidence);
|
|
330139
330470
|
if (safe.discoveryCheckpoints) safe.discoveryCheckpoints = compactForAudit(safe.discoveryCheckpoints);
|
|
330140
330471
|
if (safe.pageScreenshotKeys && safe.pageScreenshots) delete safe.pageScreenshots;
|
|
330472
|
+
if (options.omitScreenshots) {
|
|
330473
|
+
if (Array.isArray(safe.screenshots)) delete safe.screenshots;
|
|
330474
|
+
if (safe.pageScreenshots) delete safe.pageScreenshots;
|
|
330475
|
+
}
|
|
330141
330476
|
if (safe.exploreAlreadySaved === true) {
|
|
330142
330477
|
if (Array.isArray(safe.screenshots)) delete safe.screenshots;
|
|
330143
330478
|
if (safe.pageScreenshots) delete safe.pageScreenshots;
|
|
@@ -330237,7 +330572,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
330237
330572
|
const runMode = run2.mode ?? "run";
|
|
330238
330573
|
const RUNNER_MODE = opts.runnerMode ?? "playwright-native";
|
|
330239
330574
|
log.info(`Running test: ${run2.testCaseId ?? "auth-setup"} (run ${run2.runId}) [mode: ${runMode === "heal" ? "heal" : runMode === "auth-setup" ? "auth-setup" : RUNNER_MODE}]`);
|
|
330240
|
-
const liveBrowserHandle =
|
|
330575
|
+
const liveBrowserHandle = import_runner_core11.liveBrowserRegistry.register({
|
|
330241
330576
|
sessionId: `${run2.runId}`,
|
|
330242
330577
|
mode: toLiveBrowserMode(runMode),
|
|
330243
330578
|
runId: run2.runId,
|
|
@@ -330246,7 +330581,10 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
330246
330581
|
testCaseId: run2.testCaseId ?? void 0
|
|
330247
330582
|
});
|
|
330248
330583
|
requestLiveBrowserHeartbeat(true);
|
|
330249
|
-
const liveBrowserHeartbeatInterval = setInterval(() =>
|
|
330584
|
+
const liveBrowserHeartbeatInterval = setInterval(() => {
|
|
330585
|
+
liveBrowserHandle.patch({});
|
|
330586
|
+
requestLiveBrowserHeartbeat();
|
|
330587
|
+
}, 5e3);
|
|
330250
330588
|
liveBrowserHeartbeatInterval.unref?.();
|
|
330251
330589
|
const runNativeWithLiveBrowser = (nativeOptions) => (0, import_runner_core.executeTestViaNativePlaywright)({
|
|
330252
330590
|
...nativeOptions,
|
|
@@ -330273,7 +330611,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
330273
330611
|
log.fail(`PRE-FLIGHT FAILED - ${preflightError}`);
|
|
330274
330612
|
return;
|
|
330275
330613
|
}
|
|
330276
|
-
const preflight = await (0,
|
|
330614
|
+
const preflight = await (0, import_runner_core12.resolveBaseUrlPreflight)(run2.baseUrl);
|
|
330277
330615
|
if (preflight.error) {
|
|
330278
330616
|
await postResult(opts.serverUrl, headers, run2.runId, { status: "ERROR", error: preflight.error });
|
|
330279
330617
|
log.fail(`PRE-FLIGHT FAILED - ${preflight.error}`);
|
|
@@ -330330,7 +330668,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
330330
330668
|
return;
|
|
330331
330669
|
}
|
|
330332
330670
|
const healLogStream = createHealLogStreamer(opts.serverUrl, headers, run2.runId);
|
|
330333
|
-
const batchResult = await (0,
|
|
330671
|
+
const batchResult = await (0, import_runner_core10.executeGrokPerBatchHeal)(tests, run2.baseUrl, {
|
|
330334
330672
|
testVariables: run2.testVariables,
|
|
330335
330673
|
runNativeTest: runNativeWithLiveBrowser,
|
|
330336
330674
|
validateContext: { runId: run2.runId, serverUrl: opts.serverUrl, authHeader: headers.Authorization },
|
|
@@ -330351,16 +330689,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
330351
330689
|
}
|
|
330352
330690
|
return;
|
|
330353
330691
|
}
|
|
330354
|
-
if (runMode === "heal") {
|
|
330355
|
-
if (run2.framework === "APPIUM") {
|
|
330356
|
-
await postResult(opts.serverUrl, headers, run2.runId, {
|
|
330357
|
-
status: "ERROR",
|
|
330358
|
-
error: "CONFIG_ISSUE: Appium (mobile) tests are not auto-healable; skipped heal.",
|
|
330359
|
-
duration: Date.now() - startTime
|
|
330360
|
-
});
|
|
330361
|
-
log.warn("Skipped heal for APPIUM test (mobile tests are not healable)");
|
|
330362
|
-
return;
|
|
330363
|
-
}
|
|
330692
|
+
if (runMode === "heal" && run2.framework !== "APPIUM") {
|
|
330364
330693
|
const isApiTest2 = Array.isArray(run2.tags) && run2.tags.includes("@api");
|
|
330365
330694
|
if (isApiTest2) {
|
|
330366
330695
|
const testCode = run2.playwrightCode ?? "";
|
|
@@ -330564,7 +330893,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
330564
330893
|
if (classifyErrorMessage(composedError ?? null) === "config_issue") {
|
|
330565
330894
|
apiHealStatus = "ERROR";
|
|
330566
330895
|
} else {
|
|
330567
|
-
const rlDetection = (0,
|
|
330896
|
+
const rlDetection = (0, import_runner_core12.detectRateLimitBlock)({
|
|
330568
330897
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
330569
330898
|
tags: run2.tags ?? [],
|
|
330570
330899
|
error: composedError ?? finalVerifyResult?.error ?? null,
|
|
@@ -330671,7 +331000,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
330671
331000
|
log.info(`Heal engine: GROK (${source})`);
|
|
330672
331001
|
}
|
|
330673
331002
|
const grokHealLogStream = useGrokHeal ? createHealLogStreamer(opts.serverUrl, headers, run2.runId) : null;
|
|
330674
|
-
const result2 = useGrokHeal ? await (0,
|
|
331003
|
+
const result2 = useGrokHeal ? await (0, import_runner_core10.executeGrokPerTestHeal)(run2.playwrightCode, run2.steps, run2.baseUrl, {
|
|
330675
331004
|
...healOpts,
|
|
330676
331005
|
onLog: grokHealLogStream.onLog
|
|
330677
331006
|
}) : await (0, import_runner_core2.executeTestWithHealing)(run2.playwrightCode, run2.steps, run2.baseUrl, healOpts);
|
|
@@ -330704,7 +331033,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
330704
331033
|
let healStatus = result2.passed ? "PASSED" : isConfigIssue ? "ERROR" : "FAILED";
|
|
330705
331034
|
let healError = result2.error;
|
|
330706
331035
|
if (!result2.passed && !isConfigIssue) {
|
|
330707
|
-
const rlDetection = (0,
|
|
331036
|
+
const rlDetection = (0, import_runner_core12.detectRateLimitBlock)({
|
|
330708
331037
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
330709
331038
|
tags: run2.tags ?? [],
|
|
330710
331039
|
error: result2.error,
|
|
@@ -330898,7 +331227,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
330898
331227
|
liveBrowserHandle,
|
|
330899
331228
|
runNativeTest: runNativeWithLiveBrowser
|
|
330900
331229
|
};
|
|
330901
|
-
const healResult = useGrokHealSuite ? await (0,
|
|
331230
|
+
const healResult = useGrokHealSuite ? await (0, import_runner_core10.executeGrokPerTestHeal)(
|
|
330902
331231
|
failedTest.playwrightCode,
|
|
330903
331232
|
failedTest.steps ?? [],
|
|
330904
331233
|
run2.baseUrl,
|
|
@@ -331186,9 +331515,16 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331186
331515
|
signal: AbortSignal.timeout(3e4)
|
|
331187
331516
|
});
|
|
331188
331517
|
if (res.ok) return;
|
|
331189
|
-
|
|
331518
|
+
const responseText = await res.text().catch(() => "");
|
|
331519
|
+
const statusMessage = responseText ? `${res.status}: ${responseText.slice(0, 500)}` : String(res.status);
|
|
331520
|
+
if (res.status === 409) {
|
|
331521
|
+
throw new Error(`Discovery plan checkpoint rejected: ${statusMessage}`);
|
|
331522
|
+
}
|
|
331523
|
+
log.warn(`[stream] discovery-plan attempt ${attempt + 1} returned ${statusMessage}`);
|
|
331190
331524
|
} catch (err) {
|
|
331191
|
-
|
|
331525
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
331526
|
+
if (/Discovery plan checkpoint rejected/i.test(message)) throw err;
|
|
331527
|
+
log.warn(`[stream] discovery-plan attempt ${attempt + 1} failed: ${message}`);
|
|
331192
331528
|
}
|
|
331193
331529
|
if (attempt < 2) await new Promise((r) => setTimeout(r, 3e3 * (attempt + 1)));
|
|
331194
331530
|
}
|
|
@@ -331196,6 +331532,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331196
331532
|
};
|
|
331197
331533
|
let discoveryResult;
|
|
331198
331534
|
const buildFinalBody = (r, overrideError, auditWarning) => {
|
|
331535
|
+
const isNativeDiscovery = ctx.medium === "IOS_NATIVE" || ctx.medium === "ANDROID_NATIVE";
|
|
331199
331536
|
const { exploreFinalMessages: _exploreFinalMessages, ...rest } = r;
|
|
331200
331537
|
void _exploreFinalMessages;
|
|
331201
331538
|
const logs2 = auditWarning ? [rest.logs, auditWarning].filter(Boolean).join("\n") : rest.logs;
|
|
@@ -331245,7 +331582,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331245
331582
|
resolvedAppOrigin: rest.resolvedAppOrigin,
|
|
331246
331583
|
plans: rest.plans,
|
|
331247
331584
|
exploreStats: rest.exploreStats,
|
|
331248
|
-
screenshots: rest.screenshots,
|
|
331585
|
+
...isNativeDiscovery ? {} : { screenshots: rest.screenshots },
|
|
331249
331586
|
snapshotEvidence: rest.snapshotEvidence,
|
|
331250
331587
|
discoveryCheckpoints: rest.discoveryCheckpoints,
|
|
331251
331588
|
streamedTestCount: streamedTestKeys.size,
|
|
@@ -331254,8 +331591,10 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331254
331591
|
...explorePosted ? {} : {
|
|
331255
331592
|
collectedPages: rest.collectedPages,
|
|
331256
331593
|
collectedTransitions: rest.collectedTransitions,
|
|
331257
|
-
|
|
331258
|
-
|
|
331594
|
+
...isNativeDiscovery ? {} : {
|
|
331595
|
+
pageScreenshots: rest.pageScreenshots,
|
|
331596
|
+
pageScreenshotKeys: rest.pageScreenshotKeys
|
|
331597
|
+
}
|
|
331259
331598
|
},
|
|
331260
331599
|
...harPosted ? {} : {
|
|
331261
331600
|
apiCalls: rest.apiCalls,
|
|
@@ -331263,7 +331602,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331263
331602
|
},
|
|
331264
331603
|
// Only send tests that were NOT confirmed streamed to avoid duplicates
|
|
331265
331604
|
tests: rest.tests.filter((t) => !streamedTestKeys.has(fingerprintTest(t)))
|
|
331266
|
-
}, logs2);
|
|
331605
|
+
}, logs2, { omitScreenshots: isNativeDiscovery });
|
|
331267
331606
|
};
|
|
331268
331607
|
const acquireLoginBrowser = async () => {
|
|
331269
331608
|
const browser = await browserPool.acquireAsync(12e3);
|
|
@@ -331367,7 +331706,6 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331367
331706
|
thumbnailCapturedAt: Date.now()
|
|
331368
331707
|
});
|
|
331369
331708
|
requestLiveBrowserHeartbeat();
|
|
331370
|
-
void onScreenshot("mobile", base64);
|
|
331371
331709
|
},
|
|
331372
331710
|
onExploreComplete,
|
|
331373
331711
|
onTestGenerated,
|
|
@@ -331394,7 +331732,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331394
331732
|
setExecutorBusy(false);
|
|
331395
331733
|
}
|
|
331396
331734
|
} else if (ctx.mode === "import") {
|
|
331397
|
-
const runRepoImport = ctx.frameworkImport?.kind === "repo-analysis" ?
|
|
331735
|
+
const runRepoImport = ctx.frameworkImport?.kind === "repo-analysis" ? import_runner_core10.runRepoAnalysisOnRunner : import_runner_core10.runFrameworkImportOnRunner;
|
|
331398
331736
|
discoveryResult = await runRepoImport(ctx, { onTestGenerated, acquireLoginBrowser, onAudit: discoveryAudit }, {
|
|
331399
331737
|
archiveUrl: ctx.frameworkImport?.source === "upload" ? `${opts.serverUrl}/api/runner/runs/${run2.runId}/import-archive` : void 0,
|
|
331400
331738
|
archiveAuthHeader: headers.Authorization,
|
|
@@ -331724,7 +332062,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331724
332062
|
let runStatus = result.passed ? "PASSED" : "FAILED";
|
|
331725
332063
|
let runError = result.error;
|
|
331726
332064
|
if (!result.passed) {
|
|
331727
|
-
const rlDetection = (0,
|
|
332065
|
+
const rlDetection = (0, import_runner_core12.detectRateLimitBlock)({
|
|
331728
332066
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
331729
332067
|
tags: run2.tags ?? [],
|
|
331730
332068
|
error: result.error,
|
|
@@ -331862,7 +332200,7 @@ async function startRunner(opts) {
|
|
|
331862
332200
|
capture: envInt("MAX_CAPTURE", DEFAULT_TYPE_LIMITS.capture),
|
|
331863
332201
|
run: envInt("MAX_RUN", DEFAULT_TYPE_LIMITS.run)
|
|
331864
332202
|
};
|
|
331865
|
-
const RESOURCE_GOVERNOR_OPTIONS = (0,
|
|
332203
|
+
const RESOURCE_GOVERNOR_OPTIONS = (0, import_runner_core12.createResourceGovernorOptions)({
|
|
331866
332204
|
isCloud: IS_CLOUD,
|
|
331867
332205
|
maxConcurrent: MAX_CONCURRENT,
|
|
331868
332206
|
typeLimits: TYPE_LIMITS
|
|
@@ -332013,8 +332351,8 @@ async function startRunner(opts) {
|
|
|
332013
332351
|
android: devices.some((d) => d.platform === "ANDROID" && d.isAvailable && d.state === "BOOTED")
|
|
332014
332352
|
} : { web: true, ios: false, android: false };
|
|
332015
332353
|
const bridge = enableMobile ? getBridgeState() : void 0;
|
|
332016
|
-
const resourceStatus = (0,
|
|
332017
|
-
const processInventory = await (0,
|
|
332354
|
+
const resourceStatus = (0, import_runner_core12.getResourceGovernorStatus)(RESOURCE_GOVERNOR_OPTIONS, Object.fromEntries(activeByType));
|
|
332355
|
+
const processInventory = await (0, import_runner_core12.getBrowserProcessInventory)();
|
|
332018
332356
|
lastInventoryPids = new Set(processInventory.processes.map((p) => p.pid));
|
|
332019
332357
|
lastInventoryKinds.clear();
|
|
332020
332358
|
for (const p of processInventory.processes) lastInventoryKinds.set(p.pid, p.kind);
|
|
@@ -332041,8 +332379,8 @@ async function startRunner(opts) {
|
|
|
332041
332379
|
resourceSnapshot: resourceStatus.snapshot,
|
|
332042
332380
|
resourceGovernor: resourceStatus.governor,
|
|
332043
332381
|
processInventory,
|
|
332044
|
-
aiQueue: (0,
|
|
332045
|
-
liveBrowsers:
|
|
332382
|
+
aiQueue: (0, import_runner_core12.getRunnerAIQueueStats)(),
|
|
332383
|
+
liveBrowsers: import_runner_core11.liveBrowserRegistry.snapshotForHeartbeat(),
|
|
332046
332384
|
// Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
|
|
332047
332385
|
// Cached & cheap — see getCachedDoctor above.
|
|
332048
332386
|
doctor: getCachedDoctor()
|
|
@@ -332136,7 +332474,7 @@ async function startRunner(opts) {
|
|
|
332136
332474
|
let pidsWatchdogWarned = false;
|
|
332137
332475
|
const pidsWatchdog = setInterval(() => {
|
|
332138
332476
|
if (shuttingDown) return;
|
|
332139
|
-
const snapshot = (0,
|
|
332477
|
+
const snapshot = (0, import_runner_core12.getResourceSnapshot)();
|
|
332140
332478
|
const pct = snapshot.pidsPct;
|
|
332141
332479
|
if (pct == null || !Number.isFinite(pct)) return;
|
|
332142
332480
|
if (pct >= PIDS_RECYCLE_PCT) {
|
|
@@ -332172,7 +332510,7 @@ async function startRunner(opts) {
|
|
|
332172
332510
|
log.dim(` Deferred ${run2.runId} (project ${runProject} at capacity ${getActiveForProject(runProject)}/${projectLimit})`);
|
|
332173
332511
|
continue;
|
|
332174
332512
|
}
|
|
332175
|
-
const admission = (0,
|
|
332513
|
+
const admission = (0, import_runner_core12.evaluateRunAdmission)(RESOURCE_GOVERNOR_OPTIONS, {
|
|
332176
332514
|
mode: runMode,
|
|
332177
332515
|
activeRuns,
|
|
332178
332516
|
activeByType: Object.fromEntries(activeByType),
|