@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.mjs
CHANGED
|
@@ -1042,6 +1042,65 @@ var require_mobile_source_parser = __commonJS({
|
|
|
1042
1042
|
}
|
|
1043
1043
|
});
|
|
1044
1044
|
|
|
1045
|
+
// ../runner-core/dist/services/mobile/mobile-boundary.js
|
|
1046
|
+
var require_mobile_boundary = __commonJS({
|
|
1047
|
+
"../runner-core/dist/services/mobile/mobile-boundary.js"(exports) {
|
|
1048
|
+
"use strict";
|
|
1049
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1050
|
+
exports.mobileOwnerFromScreenSignal = mobileOwnerFromScreenSignal;
|
|
1051
|
+
exports.isTransientMobileSystemOwner = isTransientMobileSystemOwner;
|
|
1052
|
+
exports.classifyMobileScreenBoundary = classifyMobileScreenBoundary2;
|
|
1053
|
+
exports.isTargetLikeMobileBoundary = isTargetLikeMobileBoundary2;
|
|
1054
|
+
exports.mobileBoundaryNote = mobileBoundaryNote;
|
|
1055
|
+
var ANDROID_TRANSIENT_SYSTEM_APP_IDS = /* @__PURE__ */ new Set([
|
|
1056
|
+
"com.android.permissioncontroller",
|
|
1057
|
+
"com.google.android.permissioncontroller",
|
|
1058
|
+
"com.android.packageinstaller",
|
|
1059
|
+
"com.google.android.packageinstaller"
|
|
1060
|
+
]);
|
|
1061
|
+
var IOS_TRANSIENT_SYSTEM_APP_IDS = /* @__PURE__ */ new Set([
|
|
1062
|
+
"com.apple.springboard"
|
|
1063
|
+
]);
|
|
1064
|
+
function mobileOwnerFromScreenSignal(signal, targetAppId) {
|
|
1065
|
+
const bundleId = signal.bundleId?.trim();
|
|
1066
|
+
if (bundleId)
|
|
1067
|
+
return bundleId;
|
|
1068
|
+
const activity = signal.activity?.trim();
|
|
1069
|
+
if (activity && targetAppId && (activity === targetAppId || activity.startsWith(`${targetAppId}.`))) {
|
|
1070
|
+
return targetAppId;
|
|
1071
|
+
}
|
|
1072
|
+
return null;
|
|
1073
|
+
}
|
|
1074
|
+
function isTransientMobileSystemOwner(owner, platform3) {
|
|
1075
|
+
return platform3 === "ANDROID" ? ANDROID_TRANSIENT_SYSTEM_APP_IDS.has(owner) : IOS_TRANSIENT_SYSTEM_APP_IDS.has(owner);
|
|
1076
|
+
}
|
|
1077
|
+
function classifyMobileScreenBoundary2(signal, targetAppId, platform3) {
|
|
1078
|
+
if (!targetAppId)
|
|
1079
|
+
return { kind: "target" };
|
|
1080
|
+
const owner = mobileOwnerFromScreenSignal(signal, targetAppId);
|
|
1081
|
+
if (!owner)
|
|
1082
|
+
return { kind: "unknown" };
|
|
1083
|
+
if (owner === targetAppId)
|
|
1084
|
+
return { kind: "target" };
|
|
1085
|
+
if (isTransientMobileSystemOwner(owner, platform3)) {
|
|
1086
|
+
return { kind: "transient_external", owner, targetAppId };
|
|
1087
|
+
}
|
|
1088
|
+
return { kind: "external", owner, targetAppId };
|
|
1089
|
+
}
|
|
1090
|
+
function isTargetLikeMobileBoundary2(boundary) {
|
|
1091
|
+
return boundary.kind === "target" || boundary.kind === "unknown";
|
|
1092
|
+
}
|
|
1093
|
+
function mobileBoundaryNote(boundary) {
|
|
1094
|
+
return [
|
|
1095
|
+
"",
|
|
1096
|
+
"## App Boundary",
|
|
1097
|
+
`Foreground app is ${boundary.owner}, outside target app ${boundary.targetAppId}.`,
|
|
1098
|
+
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."
|
|
1099
|
+
].join("\n");
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1045
1104
|
// ../shared/dist/types.js
|
|
1046
1105
|
function isUIVariant(value) {
|
|
1047
1106
|
return typeof value === "string" && UI_VARIANTS.includes(value);
|
|
@@ -1210,7 +1269,8 @@ var init_constants = __esm({
|
|
|
1210
1269
|
"GROK_API_KEY",
|
|
1211
1270
|
"MINIMAX_API_KEY",
|
|
1212
1271
|
"ANTHROPIC_API_KEY",
|
|
1213
|
-
"NVIDIA_API_KEY"
|
|
1272
|
+
"NVIDIA_API_KEY",
|
|
1273
|
+
"DEEPSEEK_API_KEY"
|
|
1214
1274
|
]);
|
|
1215
1275
|
SPAWNED_ENV_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
1216
1276
|
// Runtime essentials - Node.js and Playwright need these to function
|
|
@@ -1477,6 +1537,18 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
1477
1537
|
` }`,
|
|
1478
1538
|
` throw new Error('No locator resolved. Tried: ' + JSON.stringify(selectors) + (__lastError ? ' (last error: ' + String(__lastError) + ')' : ''));`,
|
|
1479
1539
|
`}`,
|
|
1540
|
+
``,
|
|
1541
|
+
`async function __tapAt(driver: WebdriverIO.Browser, x: number, y: number) {`,
|
|
1542
|
+
` 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 }] }]);`,
|
|
1543
|
+
`}`,
|
|
1544
|
+
``,
|
|
1545
|
+
`async function __typeIntoFocused(driver: WebdriverIO.Browser, text: string) {`,
|
|
1546
|
+
` try {`,
|
|
1547
|
+
` await (await driver.getActiveElement() as unknown as WebdriverIO.Element).addValue(text);`,
|
|
1548
|
+
` } catch {`,
|
|
1549
|
+
` await driver.keys(text);`,
|
|
1550
|
+
` }`,
|
|
1551
|
+
`}`,
|
|
1480
1552
|
...hasSwipe ? [
|
|
1481
1553
|
``,
|
|
1482
1554
|
`// Read the device window with a few retries (matches the runtime executor).`,
|
|
@@ -1535,13 +1607,25 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
1535
1607
|
const y = Math.round(bounds.y + bounds.height / 2);
|
|
1536
1608
|
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 }] }]);`);
|
|
1537
1609
|
} else if (step.action === "type" && hasSelector) {
|
|
1538
|
-
|
|
1610
|
+
if (bounds) {
|
|
1611
|
+
const x = Math.round(bounds.x + bounds.width / 2);
|
|
1612
|
+
const y = Math.round(bounds.y + bounds.height / 2);
|
|
1613
|
+
lines.push(` try {`);
|
|
1614
|
+
lines.push(` await __tapAt(driver, ${x}, ${y});`);
|
|
1615
|
+
lines.push(` await driver.pause(${MOBILE_FOCUS_SETTLE_MS});`);
|
|
1616
|
+
lines.push(` await __typeIntoFocused(driver, ${toJsStringLiteral(step.value ?? "")});`);
|
|
1617
|
+
lines.push(` } catch {`);
|
|
1618
|
+
lines.push(` await (await findFirst(driver, ${candidatesLiteral})).addValue(${toJsStringLiteral(step.value ?? "")});`);
|
|
1619
|
+
lines.push(` }`);
|
|
1620
|
+
} else {
|
|
1621
|
+
lines.push(` await (await findFirst(driver, ${candidatesLiteral})).addValue(${toJsStringLiteral(step.value ?? "")});`);
|
|
1622
|
+
}
|
|
1539
1623
|
} else if (step.action === "type" && bounds) {
|
|
1540
1624
|
const x = Math.round(bounds.x + bounds.width / 2);
|
|
1541
1625
|
const y = Math.round(bounds.y + bounds.height / 2);
|
|
1542
|
-
lines.push(` await driver
|
|
1626
|
+
lines.push(` await __tapAt(driver, ${x}, ${y});`);
|
|
1543
1627
|
lines.push(` await driver.pause(${MOBILE_FOCUS_SETTLE_MS});`);
|
|
1544
|
-
lines.push(` await (
|
|
1628
|
+
lines.push(` await __typeIntoFocused(driver, ${toJsStringLiteral(step.value ?? "")});`);
|
|
1545
1629
|
} else if (step.action === "clear" && hasSelector) {
|
|
1546
1630
|
lines.push(` await (await findFirst(driver, ${candidatesLiteral})).clearValue();`);
|
|
1547
1631
|
} else if (step.action === "assertVisible" && hasSelector) {
|
|
@@ -2055,6 +2139,7 @@ var init_audit_pricing = __esm({
|
|
|
2055
2139
|
"MiniMax-M2.7": { input: 0.3, output: 1.2 },
|
|
2056
2140
|
"MiniMax-M2.7-highspeed": { input: 0.6, output: 2.4 },
|
|
2057
2141
|
"MiniMax-M2": { input: 0.3, output: 1.2 },
|
|
2142
|
+
"deepseek-v4-flash": { input: 0.14, output: 0.28 },
|
|
2058
2143
|
// Whisper bills per-minute, not per-token
|
|
2059
2144
|
"whisper-1": null,
|
|
2060
2145
|
// Historical / deprecated (kept so old audit rows still show a cost).
|
|
@@ -4438,21 +4523,13 @@ var require_mobile_explorer = __commonJS({
|
|
|
4438
4523
|
var ai_retry_js_1 = require_ai_retry();
|
|
4439
4524
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
4440
4525
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
4526
|
+
var mobile_boundary_js_1 = require_mobile_boundary();
|
|
4441
4527
|
var MAX_MOBILE_ITERATIONS_SURVEY = 40;
|
|
4442
4528
|
var MAX_MOBILE_ITERATIONS_DEEP = 60;
|
|
4443
4529
|
var MAX_SCREENSHOTS = 15;
|
|
4444
4530
|
var MAX_RECENT_EXCHANGES = 24;
|
|
4445
4531
|
var OBSERVATION_MAX_ELEMENTS = 50;
|
|
4446
4532
|
var NO_SNAPSHOT_NUDGE_THRESHOLD = 4;
|
|
4447
|
-
var ANDROID_TRANSIENT_SYSTEM_APP_IDS = /* @__PURE__ */ new Set([
|
|
4448
|
-
"com.android.permissioncontroller",
|
|
4449
|
-
"com.google.android.permissioncontroller",
|
|
4450
|
-
"com.android.packageinstaller",
|
|
4451
|
-
"com.google.android.packageinstaller"
|
|
4452
|
-
]);
|
|
4453
|
-
var IOS_TRANSIENT_SYSTEM_APP_IDS = /* @__PURE__ */ new Set([
|
|
4454
|
-
"com.apple.springboard"
|
|
4455
|
-
]);
|
|
4456
4533
|
var MOBILE_SNAPSHOT_TOOL = {
|
|
4457
4534
|
type: "function",
|
|
4458
4535
|
function: {
|
|
@@ -4675,40 +4752,6 @@ var require_mobile_explorer = __commonJS({
|
|
|
4675
4752
|
return "timeout";
|
|
4676
4753
|
return "ai_error";
|
|
4677
4754
|
}
|
|
4678
|
-
function ownerFromScreenSignal(signal, targetAppId) {
|
|
4679
|
-
const bundleId = signal.bundleId?.trim();
|
|
4680
|
-
if (bundleId)
|
|
4681
|
-
return bundleId;
|
|
4682
|
-
const activity = signal.activity?.trim();
|
|
4683
|
-
if (activity && targetAppId && (activity === targetAppId || activity.startsWith(`${targetAppId}.`))) {
|
|
4684
|
-
return targetAppId;
|
|
4685
|
-
}
|
|
4686
|
-
return null;
|
|
4687
|
-
}
|
|
4688
|
-
function isTransientSystemOwner(owner, platform3) {
|
|
4689
|
-
return platform3 === "ANDROID" ? ANDROID_TRANSIENT_SYSTEM_APP_IDS.has(owner) : IOS_TRANSIENT_SYSTEM_APP_IDS.has(owner);
|
|
4690
|
-
}
|
|
4691
|
-
function classifyMobileScreenBoundary(signal, targetAppId, platform3) {
|
|
4692
|
-
if (!targetAppId)
|
|
4693
|
-
return { kind: "target" };
|
|
4694
|
-
const owner = ownerFromScreenSignal(signal, targetAppId);
|
|
4695
|
-
if (!owner)
|
|
4696
|
-
return { kind: "unknown" };
|
|
4697
|
-
if (owner === targetAppId)
|
|
4698
|
-
return { kind: "target" };
|
|
4699
|
-
if (isTransientSystemOwner(owner, platform3)) {
|
|
4700
|
-
return { kind: "transient_external", owner, targetAppId };
|
|
4701
|
-
}
|
|
4702
|
-
return { kind: "external", owner, targetAppId };
|
|
4703
|
-
}
|
|
4704
|
-
function boundaryNote(boundary) {
|
|
4705
|
-
return [
|
|
4706
|
-
"",
|
|
4707
|
-
"## App Boundary",
|
|
4708
|
-
`Foreground app is ${boundary.owner}, outside target app ${boundary.targetAppId}.`,
|
|
4709
|
-
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."
|
|
4710
|
-
].join("\n");
|
|
4711
|
-
}
|
|
4712
4755
|
function resolveSnapshot(xml, windowSize, driverSignal, platform3) {
|
|
4713
4756
|
const parsed = (0, mobile_source_parser_js_1.parseMobileSource)(xml, platform3);
|
|
4714
4757
|
const signal = {
|
|
@@ -4906,10 +4949,10 @@ Use this to prioritize which screens and flows to cover.` : "";
|
|
|
4906
4949
|
};
|
|
4907
4950
|
const currentBoundary = () => {
|
|
4908
4951
|
const current = getLatest();
|
|
4909
|
-
return current ? classifyMobileScreenBoundary(current.signal, targetAppId, platform3) : { kind: "unknown" };
|
|
4952
|
+
return current ? (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(current.signal, targetAppId, platform3) : { kind: "unknown" };
|
|
4910
4953
|
};
|
|
4911
4954
|
const absorbResolvedObservation = async (resolved, screenshot, iterationForObservation, source) => {
|
|
4912
|
-
const boundary = classifyMobileScreenBoundary(resolved.signal, targetAppId, platform3);
|
|
4955
|
+
const boundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(resolved.signal, targetAppId, platform3);
|
|
4913
4956
|
if (boundary.kind === "target" || boundary.kind === "unknown") {
|
|
4914
4957
|
const screenId = ingestSnapshot(resolved, iterationForObservation);
|
|
4915
4958
|
captureScreenshot(screenshot, screenId);
|
|
@@ -4920,7 +4963,7 @@ Use this to prioritize which screens and flows to cover.` : "";
|
|
|
4920
4963
|
if (boundary.kind === "transient_external") {
|
|
4921
4964
|
log2(` System overlay detected from ${source}: ${boundary.owner}; not adding it to target-app survey evidence.`);
|
|
4922
4965
|
return {
|
|
4923
|
-
observation: `${resolved.observation}${
|
|
4966
|
+
observation: `${resolved.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(boundary)}`,
|
|
4924
4967
|
screenId: null,
|
|
4925
4968
|
transientExternal: true,
|
|
4926
4969
|
externalOwner: boundary.owner
|
|
@@ -4939,7 +4982,7 @@ Use this to prioritize which screens and flows to cover.` : "";
|
|
|
4939
4982
|
externalOwner: boundary.owner
|
|
4940
4983
|
};
|
|
4941
4984
|
}
|
|
4942
|
-
const recoveredBoundary = classifyMobileScreenBoundary(recovered.signal, targetAppId, platform3);
|
|
4985
|
+
const recoveredBoundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(recovered.signal, targetAppId, platform3);
|
|
4943
4986
|
latest = recovered;
|
|
4944
4987
|
lastSnapshotIteration = iterationForObservation;
|
|
4945
4988
|
if (recoveredBoundary.kind === "target" || recoveredBoundary.kind === "unknown") {
|
|
@@ -4955,7 +4998,7 @@ Use this to prioritize which screens and flows to cover.` : "";
|
|
|
4955
4998
|
if (recoveredBoundary.kind === "transient_external") {
|
|
4956
4999
|
log2(` Relaunch surfaced system overlay ${recoveredBoundary.owner}; not adding it to target-app survey evidence.`);
|
|
4957
5000
|
return {
|
|
4958
|
-
observation: `${recovered.observation}${
|
|
5001
|
+
observation: `${recovered.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(recoveredBoundary)}`,
|
|
4959
5002
|
screenId: null,
|
|
4960
5003
|
externalBlocked: true,
|
|
4961
5004
|
transientExternal: true,
|
|
@@ -8599,6 +8642,7 @@ var require_mobile_generator = __commonJS({
|
|
|
8599
8642
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
8600
8643
|
var mobile_source_parser_js_2 = require_mobile_source_parser();
|
|
8601
8644
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
8645
|
+
var mobile_boundary_js_1 = require_mobile_boundary();
|
|
8602
8646
|
var MAX_SCENARIO_ITERATIONS = 30;
|
|
8603
8647
|
var MAX_RECENT_EXCHANGES = 24;
|
|
8604
8648
|
var OBSERVATION_MAX_ELEMENTS = 50;
|
|
@@ -8784,8 +8828,9 @@ var require_mobile_generator = __commonJS({
|
|
|
8784
8828
|
function hasStableId(element) {
|
|
8785
8829
|
return !!nonEmpty2(element.accessibilityId) || !!nonEmpty2(element.resourceId);
|
|
8786
8830
|
}
|
|
8787
|
-
function buildSystemPrompt(platform3) {
|
|
8831
|
+
function buildSystemPrompt(platform3, targetAppId) {
|
|
8788
8832
|
const platformLabel = platform3 === "IOS" ? "iOS" : "Android";
|
|
8833
|
+
const targetLine = targetAppId ? ` Target app package/bundle: ${targetAppId}.` : "";
|
|
8789
8834
|
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.
|
|
8790
8835
|
|
|
8791
8836
|
## How the loop works
|
|
@@ -8805,7 +8850,8 @@ var require_mobile_generator = __commonJS({
|
|
|
8805
8850
|
- Re-snapshot before every assertion so the ref is fresh.
|
|
8806
8851
|
- Assert the OUTCOME of the flow (the new screen / new content), not the control you just tapped.
|
|
8807
8852
|
- Do NOT trigger irreversible side effects (real payments, sending messages to others) \u2014 note them and assert what you safely can.
|
|
8808
|
-
- Stay inside this app.
|
|
8853
|
+
- 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.
|
|
8854
|
+
- Finish promptly once the outcome is proven.
|
|
8809
8855
|
|
|
8810
8856
|
Max iterations: ${MAX_SCENARIO_ITERATIONS}. You will be warned near the end.`;
|
|
8811
8857
|
}
|
|
@@ -8878,7 +8924,7 @@ Start by calling mobile_snapshot, then execute the steps. Finish with at least o
|
|
|
8878
8924
|
}
|
|
8879
8925
|
}
|
|
8880
8926
|
async function runScenarioLoop(args) {
|
|
8881
|
-
const { scenario, driver, platform: platform3, maxIterations, agentModel, provider, aiClient, auditGroupId, onAICall, log: log2 } = args;
|
|
8927
|
+
const { scenario, driver, platform: platform3, maxIterations, agentModel, provider, aiClient, auditGroupId, onAICall, onScreenshot, targetAppId, log: log2 } = args;
|
|
8882
8928
|
const actions = [];
|
|
8883
8929
|
let assertCount = 0;
|
|
8884
8930
|
let finish = null;
|
|
@@ -8892,14 +8938,98 @@ Start by calling mobile_snapshot, then execute the steps. Finish with at least o
|
|
|
8892
8938
|
const fromActivity = signal.activity ? signal.activity.split(".").pop() : void 0;
|
|
8893
8939
|
return nonEmpty2(fromActivity) ?? nonEmpty2(signal.bundleId) ?? `screen-${signal.screenHash.slice(0, 8)}`;
|
|
8894
8940
|
};
|
|
8895
|
-
const
|
|
8896
|
-
if (!xml)
|
|
8897
|
-
return null;
|
|
8898
|
-
const resolved = resolveSnapshot(xml, windowSize ?? latest?.screen.windowSize ?? null, driverSignal, platform3);
|
|
8941
|
+
const setLatest = (resolved) => {
|
|
8899
8942
|
latest = resolved;
|
|
8900
8943
|
lastScreenName = screenNameFor(resolved);
|
|
8901
8944
|
return resolved;
|
|
8902
8945
|
};
|
|
8946
|
+
const buildResolved = (xml, driverSignal, windowSize) => {
|
|
8947
|
+
if (!xml)
|
|
8948
|
+
return null;
|
|
8949
|
+
return resolveSnapshot(xml, windowSize ?? latest?.screen.windowSize ?? null, driverSignal, platform3);
|
|
8950
|
+
};
|
|
8951
|
+
const emitScreenshot = (base64) => {
|
|
8952
|
+
if (!base64)
|
|
8953
|
+
return;
|
|
8954
|
+
try {
|
|
8955
|
+
onScreenshot?.(base64);
|
|
8956
|
+
} catch {
|
|
8957
|
+
}
|
|
8958
|
+
};
|
|
8959
|
+
const absorbResolvedObservation = async (resolved, screenshot, source) => {
|
|
8960
|
+
const boundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(resolved.signal, targetAppId, platform3);
|
|
8961
|
+
if ((0, mobile_boundary_js_1.isTargetLikeMobileBoundary)(boundary)) {
|
|
8962
|
+
setLatest(resolved);
|
|
8963
|
+
emitScreenshot(screenshot);
|
|
8964
|
+
return { resolved, observation: resolved.observation };
|
|
8965
|
+
}
|
|
8966
|
+
setLatest(resolved);
|
|
8967
|
+
if (boundary.kind === "transient_external") {
|
|
8968
|
+
log2(` system overlay detected from ${source}: ${boundary.owner}; not recording it as a target-app step.`);
|
|
8969
|
+
return {
|
|
8970
|
+
resolved,
|
|
8971
|
+
observation: `${resolved.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(boundary)}`,
|
|
8972
|
+
transientExternal: true,
|
|
8973
|
+
externalOwner: boundary.owner
|
|
8974
|
+
};
|
|
8975
|
+
}
|
|
8976
|
+
log2(` external app detected from ${source}: ${boundary.owner}; rejecting the action and relaunching ${boundary.targetAppId}.`);
|
|
8977
|
+
try {
|
|
8978
|
+
await driver.relaunch();
|
|
8979
|
+
const snap = await driver.snapshot();
|
|
8980
|
+
const recovered = buildResolved(snap.xml, snap.screenSignal, snap.windowSize);
|
|
8981
|
+
if (!recovered) {
|
|
8982
|
+
return {
|
|
8983
|
+
resolved: null,
|
|
8984
|
+
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunched target app, but no UI tree was returned)`,
|
|
8985
|
+
externalBlocked: true,
|
|
8986
|
+
externalOwner: boundary.owner
|
|
8987
|
+
};
|
|
8988
|
+
}
|
|
8989
|
+
const recoveredBoundary = (0, mobile_boundary_js_1.classifyMobileScreenBoundary)(recovered.signal, targetAppId, platform3);
|
|
8990
|
+
setLatest(recovered);
|
|
8991
|
+
if ((0, mobile_boundary_js_1.isTargetLikeMobileBoundary)(recoveredBoundary)) {
|
|
8992
|
+
emitScreenshot(snap.screenshot);
|
|
8993
|
+
return {
|
|
8994
|
+
resolved: recovered,
|
|
8995
|
+
observation: recovered.observation,
|
|
8996
|
+
externalBlocked: true,
|
|
8997
|
+
externalOwner: boundary.owner
|
|
8998
|
+
};
|
|
8999
|
+
}
|
|
9000
|
+
if (recoveredBoundary.kind === "transient_external") {
|
|
9001
|
+
log2(` relaunch surfaced system overlay ${recoveredBoundary.owner}; not recording it as a target-app step.`);
|
|
9002
|
+
return {
|
|
9003
|
+
resolved: recovered,
|
|
9004
|
+
observation: `${recovered.observation}${(0, mobile_boundary_js_1.mobileBoundaryNote)(recoveredBoundary)}`,
|
|
9005
|
+
externalBlocked: true,
|
|
9006
|
+
transientExternal: true,
|
|
9007
|
+
externalOwner: boundary.owner
|
|
9008
|
+
};
|
|
9009
|
+
}
|
|
9010
|
+
log2(` relaunch still outside target app (${recoveredBoundary.owner}); no target-app step recorded.`);
|
|
9011
|
+
return {
|
|
9012
|
+
resolved: recovered,
|
|
9013
|
+
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; relaunch still showed ${recoveredBoundary.owner}, so no target-app step was recorded)`,
|
|
9014
|
+
externalBlocked: true,
|
|
9015
|
+
externalOwner: boundary.owner
|
|
9016
|
+
};
|
|
9017
|
+
} catch (err) {
|
|
9018
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
9019
|
+
return {
|
|
9020
|
+
resolved: null,
|
|
9021
|
+
observation: `(left target app ${boundary.targetAppId} for ${boundary.owner}; failed to relaunch target app: ${message})`,
|
|
9022
|
+
externalBlocked: true,
|
|
9023
|
+
externalOwner: boundary.owner
|
|
9024
|
+
};
|
|
9025
|
+
}
|
|
9026
|
+
};
|
|
9027
|
+
const absorbObservation = async (xml, driverSignal, windowSize, screenshot, source) => {
|
|
9028
|
+
const resolved = buildResolved(xml, driverSignal, windowSize);
|
|
9029
|
+
if (!resolved)
|
|
9030
|
+
return { resolved: null, observation: null };
|
|
9031
|
+
return absorbResolvedObservation(resolved, screenshot, source);
|
|
9032
|
+
};
|
|
8903
9033
|
const resolveRef = (ref) => {
|
|
8904
9034
|
if (typeof ref !== "string" || !latest)
|
|
8905
9035
|
return null;
|
|
@@ -8907,11 +9037,11 @@ Start by calling mobile_snapshot, then execute the steps. Finish with at least o
|
|
|
8907
9037
|
};
|
|
8908
9038
|
try {
|
|
8909
9039
|
const seed = await driver.snapshot();
|
|
8910
|
-
|
|
9040
|
+
await absorbObservation(seed.xml, seed.screenSignal, seed.windowSize, seed.screenshot, "seed snapshot");
|
|
8911
9041
|
} catch (err) {
|
|
8912
9042
|
log2(` seed snapshot failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
8913
9043
|
}
|
|
8914
|
-
const systemPrompt = buildSystemPrompt(platform3);
|
|
9044
|
+
const systemPrompt = buildSystemPrompt(platform3, targetAppId);
|
|
8915
9045
|
const kickoff = buildScenarioKickoff(scenario);
|
|
8916
9046
|
const recentExchanges = [];
|
|
8917
9047
|
let iteration = 0;
|
|
@@ -9022,7 +9152,7 @@ ${statePacket}` },
|
|
|
9022
9152
|
driver,
|
|
9023
9153
|
platform: platform3,
|
|
9024
9154
|
latest: getLatest,
|
|
9025
|
-
|
|
9155
|
+
absorbObservation,
|
|
9026
9156
|
resolveRef,
|
|
9027
9157
|
screenName: () => lastScreenName || "UnknownScreen",
|
|
9028
9158
|
recordAction: (action) => {
|
|
@@ -9045,60 +9175,85 @@ ${statePacket}` },
|
|
|
9045
9175
|
return { actions, assertCount, finish, lastSnapshot: getLatest(), lastScreenName };
|
|
9046
9176
|
}
|
|
9047
9177
|
async function dispatchGenerateTool(args) {
|
|
9048
|
-
const { toolName, toolArgs, driver, platform: platform3, latest,
|
|
9178
|
+
const { toolName, toolArgs, driver, platform: platform3, latest, absorbObservation, resolveRef, screenName, recordAction, log: log2 } = args;
|
|
9049
9179
|
const unknownRef = (ref) => ({
|
|
9050
9180
|
ok: false,
|
|
9051
9181
|
error: `Unknown element ref "${String(ref)}". Call mobile_snapshot first; refs are only valid for the latest snapshot.`
|
|
9052
9182
|
});
|
|
9183
|
+
const actionPayload = (result, absorbed) => {
|
|
9184
|
+
const blocked = Boolean(absorbed.externalBlocked || absorbed.transientExternal);
|
|
9185
|
+
return {
|
|
9186
|
+
ok: result.ok && !blocked,
|
|
9187
|
+
...result.error ? { error: result.error } : {},
|
|
9188
|
+
...blocked ? { error: "Action left the target app and was not recorded. Continue from the relaunched target app." } : {},
|
|
9189
|
+
observation: absorbed.observation ?? void 0,
|
|
9190
|
+
...absorbed.externalBlocked ? { blockedExternalApp: true, externalApp: absorbed.externalOwner } : {},
|
|
9191
|
+
...absorbed.transientExternal ? { transientExternalApp: true, externalApp: absorbed.externalOwner } : {}
|
|
9192
|
+
};
|
|
9193
|
+
};
|
|
9194
|
+
const shouldRecordAction = (result, absorbed) => result.ok && !absorbed.externalBlocked && !absorbed.transientExternal;
|
|
9053
9195
|
switch (toolName) {
|
|
9054
9196
|
case "mobile_snapshot": {
|
|
9055
9197
|
const snap = await driver.snapshot();
|
|
9056
|
-
const
|
|
9057
|
-
if (!resolved)
|
|
9198
|
+
const absorbed = await absorbObservation(snap.xml, snap.screenSignal, snap.windowSize, snap.screenshot, "mobile_snapshot");
|
|
9199
|
+
if (!absorbed.resolved)
|
|
9058
9200
|
return { ok: false, error: "snapshot returned no UI tree" };
|
|
9059
|
-
log2(` snapshot: ${resolved.screen.elements.length} elements`);
|
|
9060
|
-
return {
|
|
9201
|
+
log2(` snapshot: ${absorbed.resolved.screen.elements.length} elements`);
|
|
9202
|
+
return {
|
|
9203
|
+
observation: absorbed.observation,
|
|
9204
|
+
...absorbed.externalBlocked ? { blockedExternalApp: true, externalApp: absorbed.externalOwner } : {},
|
|
9205
|
+
...absorbed.transientExternal ? { transientExternalApp: true, externalApp: absorbed.externalOwner } : {}
|
|
9206
|
+
};
|
|
9061
9207
|
}
|
|
9062
9208
|
case "mobile_tap": {
|
|
9063
9209
|
const element = resolveRef(toolArgs.ref);
|
|
9064
9210
|
if (!element)
|
|
9065
9211
|
return unknownRef(toolArgs.ref);
|
|
9212
|
+
const beforeScreenName = screenName();
|
|
9066
9213
|
const result = await driver.tap(element.bounds);
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
9214
|
+
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_tap");
|
|
9215
|
+
if (shouldRecordAction(result, absorbed)) {
|
|
9216
|
+
recordAction({
|
|
9217
|
+
type: "TAP",
|
|
9218
|
+
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
9219
|
+
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
9220
|
+
});
|
|
9221
|
+
}
|
|
9222
|
+
return actionPayload(result, absorbed);
|
|
9074
9223
|
}
|
|
9075
9224
|
case "mobile_type": {
|
|
9076
9225
|
const element = resolveRef(toolArgs.ref);
|
|
9077
9226
|
if (!element)
|
|
9078
9227
|
return unknownRef(toolArgs.ref);
|
|
9079
9228
|
const value = typeof toolArgs.value === "string" ? toolArgs.value : String(toolArgs.value ?? "");
|
|
9229
|
+
const beforeScreenName = screenName();
|
|
9080
9230
|
const result = await driver.type(value, element.bounds);
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9231
|
+
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_type");
|
|
9232
|
+
if (shouldRecordAction(result, absorbed)) {
|
|
9233
|
+
recordAction({
|
|
9234
|
+
type: "TYPE",
|
|
9235
|
+
value,
|
|
9236
|
+
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
9237
|
+
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
9238
|
+
});
|
|
9239
|
+
}
|
|
9240
|
+
return actionPayload(result, absorbed);
|
|
9089
9241
|
}
|
|
9090
9242
|
case "mobile_clear": {
|
|
9091
9243
|
const element = resolveRef(toolArgs.ref);
|
|
9092
9244
|
if (!element)
|
|
9093
9245
|
return unknownRef(toolArgs.ref);
|
|
9246
|
+
const beforeScreenName = screenName();
|
|
9094
9247
|
const result = await driver.clear(element.bounds);
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9248
|
+
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_clear");
|
|
9249
|
+
if (shouldRecordAction(result, absorbed)) {
|
|
9250
|
+
recordAction({
|
|
9251
|
+
type: "CLEAR",
|
|
9252
|
+
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
9253
|
+
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
9254
|
+
});
|
|
9255
|
+
}
|
|
9256
|
+
return actionPayload(result, absorbed);
|
|
9102
9257
|
}
|
|
9103
9258
|
case "mobile_swipe": {
|
|
9104
9259
|
const direction = toolArgs.direction;
|
|
@@ -9106,30 +9261,42 @@ ${statePacket}` },
|
|
|
9106
9261
|
return { ok: false, error: "direction must be one of up|down|left|right" };
|
|
9107
9262
|
}
|
|
9108
9263
|
const distance = typeof toolArgs.distance === "number" ? toolArgs.distance : void 0;
|
|
9264
|
+
const beforeScreenName = screenName();
|
|
9109
9265
|
const result = await driver.swipe(direction, distance);
|
|
9110
|
-
|
|
9111
|
-
|
|
9112
|
-
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9266
|
+
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_swipe");
|
|
9267
|
+
if (shouldRecordAction(result, absorbed)) {
|
|
9268
|
+
recordAction({
|
|
9269
|
+
type: "SWIPE",
|
|
9270
|
+
metadata: {
|
|
9271
|
+
screenName: beforeScreenName,
|
|
9272
|
+
direction,
|
|
9273
|
+
...distance !== void 0 ? { distance } : {}
|
|
9274
|
+
}
|
|
9275
|
+
});
|
|
9276
|
+
}
|
|
9277
|
+
return actionPayload(result, absorbed);
|
|
9120
9278
|
}
|
|
9121
9279
|
case "mobile_back": {
|
|
9280
|
+
const beforeScreenName = screenName();
|
|
9122
9281
|
const result = await driver.back();
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9282
|
+
const absorbed = await absorbObservation(result.source, result.screenSignal, latest()?.screen.windowSize ?? null, result.screenshot, "mobile_back");
|
|
9283
|
+
if (shouldRecordAction(result, absorbed)) {
|
|
9284
|
+
recordAction({ type: "BACK", metadata: { screenName: beforeScreenName } });
|
|
9285
|
+
}
|
|
9286
|
+
return actionPayload(result, absorbed);
|
|
9126
9287
|
}
|
|
9127
9288
|
case "mobile_relaunch": {
|
|
9128
9289
|
await driver.relaunch();
|
|
9129
9290
|
const snap = await driver.snapshot();
|
|
9130
|
-
const
|
|
9291
|
+
const absorbed = await absorbObservation(snap.xml, snap.screenSignal, snap.windowSize, snap.screenshot, "mobile_relaunch");
|
|
9131
9292
|
log2(" relaunched app for hermetic reset.");
|
|
9132
|
-
return {
|
|
9293
|
+
return {
|
|
9294
|
+
ok: true,
|
|
9295
|
+
observation: absorbed.observation,
|
|
9296
|
+
...absorbed.externalBlocked ? { blockedExternalApp: true, externalApp: absorbed.externalOwner } : {},
|
|
9297
|
+
...absorbed.transientExternal ? { transientExternalApp: true, externalApp: absorbed.externalOwner } : {},
|
|
9298
|
+
note: "App relaunched (clean state). Not recorded as a step."
|
|
9299
|
+
};
|
|
9133
9300
|
}
|
|
9134
9301
|
case "mobile_assert_visible": {
|
|
9135
9302
|
const recorded = recordAssertVisible(toolArgs, { resolveRef, screenName, platform: platform3, recordAction });
|
|
@@ -9267,7 +9434,7 @@ ${statePacket}` },
|
|
|
9267
9434
|
return test;
|
|
9268
9435
|
}
|
|
9269
9436
|
async function runMobileGeneratePhase(args) {
|
|
9270
|
-
const { scenarios, driver, ctx, config, onLog, onAICall, onTestGenerated } = args;
|
|
9437
|
+
const { scenarios, driver, ctx, config, onLog, onAICall, onTestGenerated, onScreenshot } = args;
|
|
9271
9438
|
const log2 = (line) => {
|
|
9272
9439
|
try {
|
|
9273
9440
|
onLog?.(line);
|
|
@@ -9281,6 +9448,7 @@ ${statePacket}` },
|
|
|
9281
9448
|
const provider = (0, ai_provider_js_1.getProviderForModel)(agentModel);
|
|
9282
9449
|
const aiClient = modelOverride ? (0, ai_provider_js_1.getMobileClientForModel)(modelOverride) : (0, ai_provider_js_1.getMobileAIClient)();
|
|
9283
9450
|
const auditGroupId = `mobile-generate-${(0, crypto_1.randomUUID)().slice(0, 8)}`;
|
|
9451
|
+
const targetAppId = ctx.target?.appId?.trim() || void 0;
|
|
9284
9452
|
log2(`Mobile generator starting \u2014 platform: ${platform3}, provider: ${provider}, model: ${agentModel}, scenarios: ${scenarios.length}`);
|
|
9285
9453
|
const tests = [];
|
|
9286
9454
|
for (let i = 0; i < scenarios.length; i++) {
|
|
@@ -9299,6 +9467,8 @@ ${statePacket}` },
|
|
|
9299
9467
|
aiClient,
|
|
9300
9468
|
auditGroupId,
|
|
9301
9469
|
onAICall,
|
|
9470
|
+
onScreenshot,
|
|
9471
|
+
targetAppId,
|
|
9302
9472
|
log: log2
|
|
9303
9473
|
});
|
|
9304
9474
|
const test = assembleTest({ scenario, recording, ctx, platform: platform3, log: log2 });
|
|
@@ -311428,7 +311598,9 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
311428
311598
|
return {
|
|
311429
311599
|
collectedPages: explore.collectedPages,
|
|
311430
311600
|
collectedTransitions,
|
|
311431
|
-
|
|
311601
|
+
// Native screenshots are multi-megabyte PNGs. The live mirror streams them
|
|
311602
|
+
// through heartbeat telemetry; discovery checkpoints keep the screen graph
|
|
311603
|
+
// and transitions so large Android surveys cannot exceed server body limits.
|
|
311432
311604
|
exploreStats: explore.exploreStats,
|
|
311433
311605
|
healthObservations: explore.healthObservations
|
|
311434
311606
|
};
|
|
@@ -311586,6 +311758,7 @@ Plan: ${planResult.scenarios.length} scenarios, ${planResult.featureGroups.lengt
|
|
|
311586
311758
|
ctx,
|
|
311587
311759
|
onLog: (line) => log2(line),
|
|
311588
311760
|
onAICall,
|
|
311761
|
+
onScreenshot,
|
|
311589
311762
|
onTestGenerated: (test) => callbacks?.onTestGenerated?.(test, { phase: "verified" })
|
|
311590
311763
|
});
|
|
311591
311764
|
const generateElapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
@@ -325291,7 +325464,7 @@ var require_live_browser_registry = __commonJS({
|
|
|
325291
325464
|
var THUMBNAIL_INTERVAL_MS = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_INTERVAL_MS ?? "4000", 10) || 4e3;
|
|
325292
325465
|
var THUMBNAIL_QUALITY = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_QUALITY ?? "60", 10) || 60;
|
|
325293
325466
|
var BROWSER_THUMBNAIL_MAX_BASE64_BYTES = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_MAX_BASE64_BYTES ?? "400000", 10) || 4e5;
|
|
325294
|
-
var MOBILE_THUMBNAIL_MAX_BASE64_BYTES = parseInt(process.env.LIVE_MOBILE_THUMBNAIL_MAX_BASE64_BYTES ?? "
|
|
325467
|
+
var MOBILE_THUMBNAIL_MAX_BASE64_BYTES = parseInt(process.env.LIVE_MOBILE_THUMBNAIL_MAX_BASE64_BYTES ?? "4500000", 10) || 45e5;
|
|
325295
325468
|
var STALE_SESSION_MS = parseInt(process.env.LIVE_BROWSER_STALE_MS ?? `${10 * 60 * 1e3}`, 10) || 10 * 60 * 1e3;
|
|
325296
325469
|
var MAX_SESSIONS = parseInt(process.env.LIVE_BROWSER_MAX_SESSIONS ?? "32", 10) || 32;
|
|
325297
325470
|
var LiveBrowserRegistry = class {
|
|
@@ -325787,6 +325960,7 @@ var require_dist2 = __commonJS({
|
|
|
325787
325960
|
__exportStar(require_ai_provider(), exports);
|
|
325788
325961
|
__exportStar(require_mobile_types(), exports);
|
|
325789
325962
|
__exportStar(require_mobile_source_parser(), exports);
|
|
325963
|
+
__exportStar(require_mobile_boundary(), exports);
|
|
325790
325964
|
var mobile_explorer_js_1 = require_mobile_explorer();
|
|
325791
325965
|
Object.defineProperty(exports, "runMobileExplorePhase", { enumerable: true, get: function() {
|
|
325792
325966
|
return mobile_explorer_js_1.runMobileExplorePhase;
|
|
@@ -326119,7 +326293,7 @@ var require_package4 = __commonJS({
|
|
|
326119
326293
|
"package.json"(exports, module) {
|
|
326120
326294
|
module.exports = {
|
|
326121
326295
|
name: "@validate.qa/runner",
|
|
326122
|
-
version: "1.0.
|
|
326296
|
+
version: "1.0.8",
|
|
326123
326297
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
326124
326298
|
bin: {
|
|
326125
326299
|
"validate-runner": "dist/cli.js"
|
|
@@ -326217,7 +326391,7 @@ var import_runner_core = __toESM(require_dist2());
|
|
|
326217
326391
|
var import_runner_core2 = __toESM(require_dist2());
|
|
326218
326392
|
|
|
326219
326393
|
// src/runner.ts
|
|
326220
|
-
var
|
|
326394
|
+
var import_runner_core10 = __toESM(require_dist2());
|
|
326221
326395
|
|
|
326222
326396
|
// src/services/regression-runner.ts
|
|
326223
326397
|
var import_runner_core3 = __toESM(require_dist2());
|
|
@@ -326232,7 +326406,7 @@ var import_runner_core5 = __toESM(require_dist2());
|
|
|
326232
326406
|
var import_runner_core6 = __toESM(require_dist2());
|
|
326233
326407
|
|
|
326234
326408
|
// src/runner.ts
|
|
326235
|
-
var
|
|
326409
|
+
var import_runner_core11 = __toESM(require_dist2());
|
|
326236
326410
|
|
|
326237
326411
|
// src/services/auth-state.ts
|
|
326238
326412
|
var INLINE_LOGIN_TAG = "@inline-login";
|
|
@@ -326252,6 +326426,7 @@ function shouldUseAuthState(tags, playwrightCode) {
|
|
|
326252
326426
|
|
|
326253
326427
|
// src/services/appium-executor.ts
|
|
326254
326428
|
init_dist();
|
|
326429
|
+
var import_runner_core8 = __toESM(require_dist2());
|
|
326255
326430
|
|
|
326256
326431
|
// src/services/appium-lifecycle.ts
|
|
326257
326432
|
import { spawn, execFileSync } from "child_process";
|
|
@@ -327543,6 +327718,86 @@ async function openDeepLink(sessionId, platform3, deeplink, appId) {
|
|
|
327543
327718
|
body: JSON.stringify({ script: "mobile: deepLink", args })
|
|
327544
327719
|
});
|
|
327545
327720
|
}
|
|
327721
|
+
function readExecuteStringValue(result) {
|
|
327722
|
+
if (result && typeof result === "object" && "value" in result) {
|
|
327723
|
+
const value = result.value;
|
|
327724
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
327725
|
+
}
|
|
327726
|
+
return void 0;
|
|
327727
|
+
}
|
|
327728
|
+
function readActiveBundleId(result) {
|
|
327729
|
+
if (result && typeof result === "object" && "value" in result) {
|
|
327730
|
+
const value = result.value;
|
|
327731
|
+
if (value && typeof value === "object" && "bundleId" in value) {
|
|
327732
|
+
const bundleId = value.bundleId;
|
|
327733
|
+
if (typeof bundleId === "string" && bundleId.trim()) return bundleId.trim();
|
|
327734
|
+
}
|
|
327735
|
+
}
|
|
327736
|
+
return void 0;
|
|
327737
|
+
}
|
|
327738
|
+
function normalizeAndroidActivity(activity, pkg2) {
|
|
327739
|
+
const trimmed = activity?.trim();
|
|
327740
|
+
if (!trimmed) return void 0;
|
|
327741
|
+
const packageName = pkg2?.trim();
|
|
327742
|
+
if (!packageName) return trimmed;
|
|
327743
|
+
if (trimmed.startsWith(".")) return `${packageName}${trimmed}`;
|
|
327744
|
+
if (!trimmed.includes(".")) return `${packageName}.${trimmed}`;
|
|
327745
|
+
return trimmed;
|
|
327746
|
+
}
|
|
327747
|
+
async function executeMobileScript(sessionId, script, args) {
|
|
327748
|
+
return wdRequest(`/session/${sessionId}/execute/sync`, {
|
|
327749
|
+
method: "POST",
|
|
327750
|
+
body: JSON.stringify({ script, args })
|
|
327751
|
+
});
|
|
327752
|
+
}
|
|
327753
|
+
async function readForegroundScreenSignal(sessionId, platform3) {
|
|
327754
|
+
if (platform3 === "ANDROID") {
|
|
327755
|
+
const [packageResult, activityResult] = await Promise.all([
|
|
327756
|
+
executeMobileScript(sessionId, "mobile: getCurrentPackage", []),
|
|
327757
|
+
executeMobileScript(sessionId, "mobile: getCurrentActivity", []).catch(() => void 0)
|
|
327758
|
+
]);
|
|
327759
|
+
const pkg2 = readExecuteStringValue(packageResult);
|
|
327760
|
+
if (!pkg2) return null;
|
|
327761
|
+
const activity = normalizeAndroidActivity(readExecuteStringValue(activityResult), pkg2);
|
|
327762
|
+
return {
|
|
327763
|
+
platform: platform3,
|
|
327764
|
+
bundleId: pkg2,
|
|
327765
|
+
...activity ? { activity } : {},
|
|
327766
|
+
screenHash: "foreground"
|
|
327767
|
+
};
|
|
327768
|
+
}
|
|
327769
|
+
const infoResult = await executeMobileScript(sessionId, "mobile: activeAppInfo", []);
|
|
327770
|
+
const bundleId = readActiveBundleId(infoResult);
|
|
327771
|
+
if (!bundleId) return null;
|
|
327772
|
+
return { platform: platform3, bundleId, screenHash: "foreground" };
|
|
327773
|
+
}
|
|
327774
|
+
async function activateTargetApp(sessionId, platform3, appId) {
|
|
327775
|
+
const args = platform3 === "IOS" ? [{ bundleId: appId }] : [{ appId }];
|
|
327776
|
+
await executeMobileScript(sessionId, "mobile: activateApp", args);
|
|
327777
|
+
}
|
|
327778
|
+
async function restoreTargetAppIfExternal(sessionId, target, log2, source, failOnExternal) {
|
|
327779
|
+
let boundary = null;
|
|
327780
|
+
try {
|
|
327781
|
+
const signal = await readForegroundScreenSignal(sessionId, target.platform);
|
|
327782
|
+
boundary = signal ? (0, import_runner_core8.classifyMobileScreenBoundary)(signal, target.appId, target.platform) : null;
|
|
327783
|
+
} catch (err) {
|
|
327784
|
+
log2(` App boundary check skipped after ${source}: ${err instanceof Error ? err.message : String(err)}`);
|
|
327785
|
+
return;
|
|
327786
|
+
}
|
|
327787
|
+
if (!boundary || (0, import_runner_core8.isTargetLikeMobileBoundary)(boundary) || boundary.kind === "transient_external") {
|
|
327788
|
+
return;
|
|
327789
|
+
}
|
|
327790
|
+
const message = `Foreground app changed to ${boundary.owner}, outside target app ${boundary.targetAppId}`;
|
|
327791
|
+
log2(` ${message}; activating target app.`);
|
|
327792
|
+
try {
|
|
327793
|
+
await activateTargetApp(sessionId, target.platform, target.appId);
|
|
327794
|
+
} catch (err) {
|
|
327795
|
+
log2(` Could not reactivate ${target.appId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
327796
|
+
}
|
|
327797
|
+
if (failOnExternal) {
|
|
327798
|
+
throw new Error(`${message}; relaunched target app and failed the step so mobile tests do not continue in another app.`);
|
|
327799
|
+
}
|
|
327800
|
+
}
|
|
327546
327801
|
async function deleteSession(sessionId) {
|
|
327547
327802
|
try {
|
|
327548
327803
|
await wdRequest(`/session/${sessionId}`, { method: "DELETE" });
|
|
@@ -327669,6 +327924,41 @@ async function getActiveElementId(sessionId) {
|
|
|
327669
327924
|
return null;
|
|
327670
327925
|
}
|
|
327671
327926
|
}
|
|
327927
|
+
async function sendKeysToFocusedInput(sessionId, value) {
|
|
327928
|
+
await wdRequest(`/session/${sessionId}/keys`, {
|
|
327929
|
+
method: "POST",
|
|
327930
|
+
body: JSON.stringify({ text: value, value: Array.from(value) })
|
|
327931
|
+
});
|
|
327932
|
+
}
|
|
327933
|
+
async function trySendElementValue(sessionId, elementId, value) {
|
|
327934
|
+
try {
|
|
327935
|
+
await wdRequest(`/session/${sessionId}/element/${elementId}/value`, {
|
|
327936
|
+
method: "POST",
|
|
327937
|
+
body: JSON.stringify({ text: value })
|
|
327938
|
+
});
|
|
327939
|
+
return null;
|
|
327940
|
+
} catch (error2) {
|
|
327941
|
+
if (isTransportError(error2)) throw error2;
|
|
327942
|
+
return error2;
|
|
327943
|
+
}
|
|
327944
|
+
}
|
|
327945
|
+
function shouldAbortAfterStepFailure(action) {
|
|
327946
|
+
switch (action) {
|
|
327947
|
+
case "launchApp":
|
|
327948
|
+
case "tap":
|
|
327949
|
+
case "type":
|
|
327950
|
+
case "clear":
|
|
327951
|
+
case "swipe":
|
|
327952
|
+
case "scroll":
|
|
327953
|
+
case "back":
|
|
327954
|
+
case "home":
|
|
327955
|
+
case "waitForElement":
|
|
327956
|
+
return true;
|
|
327957
|
+
case "assertVisible":
|
|
327958
|
+
case "assertText":
|
|
327959
|
+
return false;
|
|
327960
|
+
}
|
|
327961
|
+
}
|
|
327672
327962
|
async function executeTap(sessionId, step) {
|
|
327673
327963
|
const bounds = boundsFromStep(step);
|
|
327674
327964
|
if (step.target) {
|
|
@@ -327690,30 +327980,64 @@ async function executeTap(sessionId, step) {
|
|
|
327690
327980
|
}
|
|
327691
327981
|
async function executeType(sessionId, step) {
|
|
327692
327982
|
if (step.value === void 0 || step.value === null || step.value === "") return;
|
|
327983
|
+
const value = String(step.value);
|
|
327693
327984
|
const bounds = boundsFromStep(step);
|
|
327694
327985
|
let elementId = null;
|
|
327986
|
+
let lastInputError = null;
|
|
327987
|
+
let tappedBounds = false;
|
|
327988
|
+
if (bounds) {
|
|
327989
|
+
await tapCoordinates(sessionId, bounds);
|
|
327990
|
+
tappedBounds = true;
|
|
327991
|
+
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
327992
|
+
elementId = await getActiveElementId(sessionId);
|
|
327993
|
+
if (elementId) {
|
|
327994
|
+
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
327995
|
+
if (!lastInputError) return;
|
|
327996
|
+
elementId = null;
|
|
327997
|
+
}
|
|
327998
|
+
}
|
|
327695
327999
|
if (step.target) {
|
|
327696
328000
|
try {
|
|
327697
328001
|
elementId = await findElement(sessionId, step.target, readStepTimeout(step));
|
|
328002
|
+
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
328003
|
+
if (!lastInputError) return;
|
|
328004
|
+
elementId = null;
|
|
327698
328005
|
} catch (error2) {
|
|
327699
328006
|
if (!bounds) throw error2;
|
|
328007
|
+
lastInputError = error2;
|
|
327700
328008
|
}
|
|
327701
328009
|
}
|
|
327702
328010
|
if (!elementId && bounds) {
|
|
327703
328011
|
await tapCoordinates(sessionId, bounds);
|
|
328012
|
+
tappedBounds = true;
|
|
327704
328013
|
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
327705
328014
|
elementId = await getActiveElementId(sessionId);
|
|
327706
328015
|
}
|
|
327707
328016
|
if (!elementId) {
|
|
327708
328017
|
elementId = await getActiveElementId(sessionId);
|
|
327709
328018
|
}
|
|
327710
|
-
if (
|
|
327711
|
-
|
|
328019
|
+
if (elementId) {
|
|
328020
|
+
lastInputError = await trySendElementValue(sessionId, elementId, value);
|
|
328021
|
+
if (!lastInputError) return;
|
|
328022
|
+
elementId = null;
|
|
328023
|
+
if (!bounds) {
|
|
328024
|
+
throw lastInputError instanceof Error ? lastInputError : new Error(String(lastInputError));
|
|
328025
|
+
}
|
|
327712
328026
|
}
|
|
327713
|
-
|
|
327714
|
-
|
|
327715
|
-
|
|
327716
|
-
|
|
328027
|
+
if (bounds) {
|
|
328028
|
+
if (!tappedBounds) {
|
|
328029
|
+
await tapCoordinates(sessionId, bounds);
|
|
328030
|
+
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
328031
|
+
}
|
|
328032
|
+
try {
|
|
328033
|
+
await sendKeysToFocusedInput(sessionId, value);
|
|
328034
|
+
return;
|
|
328035
|
+
} catch (error2) {
|
|
328036
|
+
if (isTransportError(error2)) throw error2;
|
|
328037
|
+
lastInputError = error2;
|
|
328038
|
+
}
|
|
328039
|
+
}
|
|
328040
|
+
throw new Error(`TYPE step could not send text to the focused target${lastInputError instanceof Error ? `: ${lastInputError.message}` : ""}`);
|
|
327717
328041
|
}
|
|
327718
328042
|
async function executeClear(sessionId, step) {
|
|
327719
328043
|
const bounds = boundsFromStep(step);
|
|
@@ -327917,6 +328241,7 @@ async function executeMobileTest(options) {
|
|
|
327917
328241
|
log2(` \u2717 Deep link failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
327918
328242
|
}
|
|
327919
328243
|
}
|
|
328244
|
+
await restoreTargetAppIfExternal(sessionId, options.target, log2, "session start", false);
|
|
327920
328245
|
let environmental = null;
|
|
327921
328246
|
for (const step of options.steps) {
|
|
327922
328247
|
const stepStart = Date.now();
|
|
@@ -327924,6 +328249,7 @@ async function executeMobileTest(options) {
|
|
|
327924
328249
|
let status = "passed";
|
|
327925
328250
|
let stepError;
|
|
327926
328251
|
try {
|
|
328252
|
+
await restoreTargetAppIfExternal(sessionId, options.target, log2, `step ${step.order} pre-check`, false);
|
|
327927
328253
|
switch (step.action) {
|
|
327928
328254
|
case "launchApp":
|
|
327929
328255
|
log2(" App launched (handled by session creation)");
|
|
@@ -327963,6 +328289,7 @@ async function executeMobileTest(options) {
|
|
|
327963
328289
|
break;
|
|
327964
328290
|
}
|
|
327965
328291
|
}
|
|
328292
|
+
await restoreTargetAppIfExternal(sessionId, options.target, log2, `step ${step.order} (${step.action})`, true);
|
|
327966
328293
|
} catch (err) {
|
|
327967
328294
|
status = "failed";
|
|
327968
328295
|
stepError = err instanceof Error ? err.message : String(err);
|
|
@@ -327983,6 +328310,10 @@ async function executeMobileTest(options) {
|
|
|
327983
328310
|
log2(`Aborting remaining steps \u2014 Appium session is no longer usable: ${environmental}`);
|
|
327984
328311
|
break;
|
|
327985
328312
|
}
|
|
328313
|
+
if (status === "failed" && shouldAbortAfterStepFailure(step.action)) {
|
|
328314
|
+
log2(`Aborting remaining steps \u2014 ${step.action} failed, so downstream mobile steps are no longer reliable.`);
|
|
328315
|
+
break;
|
|
328316
|
+
}
|
|
327986
328317
|
}
|
|
327987
328318
|
const allPassed = !environmental && stepResults.every((r) => r.status === "passed" || r.status === "skipped");
|
|
327988
328319
|
const firstError = environmental ?? (stepResults.find((r) => r.status === "failed")?.error ?? null);
|
|
@@ -329248,7 +329579,7 @@ function detachMirrorSession(sessionId) {
|
|
|
329248
329579
|
}
|
|
329249
329580
|
|
|
329250
329581
|
// src/services/mobile/mobile-bridge-driver.ts
|
|
329251
|
-
var
|
|
329582
|
+
var import_runner_core9 = __toESM(require_dist2());
|
|
329252
329583
|
init_dist();
|
|
329253
329584
|
var APPIUM_SESSION_ID_PATTERN2 = /^[a-f0-9-]{1,64}$/i;
|
|
329254
329585
|
function readStringValue(result) {
|
|
@@ -329258,7 +329589,7 @@ function readStringValue(result) {
|
|
|
329258
329589
|
}
|
|
329259
329590
|
return void 0;
|
|
329260
329591
|
}
|
|
329261
|
-
function
|
|
329592
|
+
function normalizeAndroidActivity2(activity, pkg2) {
|
|
329262
329593
|
const trimmed = activity?.trim();
|
|
329263
329594
|
if (!trimmed) return void 0;
|
|
329264
329595
|
const packageName = pkg2?.trim();
|
|
@@ -329294,7 +329625,7 @@ var MobileBridgeDriver = class {
|
|
|
329294
329625
|
return this.platform === "ANDROID" ? { appId: this.appId } : { bundleId: this.appId };
|
|
329295
329626
|
}
|
|
329296
329627
|
baseScreenSignal(source) {
|
|
329297
|
-
const parsed = (0,
|
|
329628
|
+
const parsed = (0, import_runner_core9.parseMobileSource)(source, this.platform);
|
|
329298
329629
|
return {
|
|
329299
329630
|
platform: this.platform,
|
|
329300
329631
|
screenHash: parsed.screenSignal.screenHash,
|
|
@@ -329313,11 +329644,11 @@ var MobileBridgeDriver = class {
|
|
|
329313
329644
|
const activity = readStringValue(activityRes);
|
|
329314
329645
|
const pkg2 = readStringValue(packageRes);
|
|
329315
329646
|
if (pkg2) signal.bundleId = pkg2;
|
|
329316
|
-
const normalizedActivity =
|
|
329647
|
+
const normalizedActivity = normalizeAndroidActivity2(activity, pkg2);
|
|
329317
329648
|
if (normalizedActivity) signal.activity = normalizedActivity;
|
|
329318
329649
|
} else {
|
|
329319
329650
|
const infoRes = await this.executeScript("mobile: activeAppInfo", []).catch(() => void 0);
|
|
329320
|
-
const bundleId =
|
|
329651
|
+
const bundleId = readActiveBundleId2(infoRes);
|
|
329321
329652
|
if (bundleId) signal.bundleId = bundleId;
|
|
329322
329653
|
}
|
|
329323
329654
|
} catch {
|
|
@@ -329459,7 +329790,7 @@ var MobileBridgeDriver = class {
|
|
|
329459
329790
|
return this.enrichScreenSignal(source);
|
|
329460
329791
|
}
|
|
329461
329792
|
};
|
|
329462
|
-
function
|
|
329793
|
+
function readActiveBundleId2(result) {
|
|
329463
329794
|
if (result && typeof result === "object" && "value" in result) {
|
|
329464
329795
|
const value = result.value;
|
|
329465
329796
|
if (value && typeof value === "object" && "bundleId" in value) {
|
|
@@ -329685,7 +330016,7 @@ function printDoctorReport(report) {
|
|
|
329685
330016
|
|
|
329686
330017
|
// src/runner.ts
|
|
329687
330018
|
init_dist();
|
|
329688
|
-
var
|
|
330019
|
+
var import_runner_core12 = __toESM(require_dist2());
|
|
329689
330020
|
function fingerprintTest(test) {
|
|
329690
330021
|
const code = test.framework === "APPIUM" ? [test.appiumCode ?? "", JSON.stringify(test.steps ?? [])].join("\n--steps--\n") : test.playwrightCode ?? "";
|
|
329691
330022
|
return 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);
|
|
@@ -330130,7 +330461,7 @@ function pruneCollectedPageForPost(page) {
|
|
|
330130
330461
|
if (Array.isArray(out.observations)) out.observations = out.observations.slice(-200).map((obs) => compactForAudit(obs));
|
|
330131
330462
|
return out;
|
|
330132
330463
|
}
|
|
330133
|
-
function prepareDiscoveryResultForPost(result, logs2) {
|
|
330464
|
+
function prepareDiscoveryResultForPost(result, logs2, options = {}) {
|
|
330134
330465
|
const safe = { ...result, logs: truncateTextTail(logs2, RESULT_LOG_MAX_CHARS, "discovery result log") };
|
|
330135
330466
|
if (Array.isArray(safe.collectedPages)) safe.collectedPages = safe.collectedPages.map(pruneCollectedPageForPost);
|
|
330136
330467
|
if (Array.isArray(safe.apiCalls)) safe.apiCalls = safe.apiCalls.map((call) => compactForAudit(call));
|
|
@@ -330146,6 +330477,10 @@ function prepareDiscoveryResultForPost(result, logs2) {
|
|
|
330146
330477
|
if (safe.snapshotEvidence) safe.snapshotEvidence = compactForAudit(safe.snapshotEvidence);
|
|
330147
330478
|
if (safe.discoveryCheckpoints) safe.discoveryCheckpoints = compactForAudit(safe.discoveryCheckpoints);
|
|
330148
330479
|
if (safe.pageScreenshotKeys && safe.pageScreenshots) delete safe.pageScreenshots;
|
|
330480
|
+
if (options.omitScreenshots) {
|
|
330481
|
+
if (Array.isArray(safe.screenshots)) delete safe.screenshots;
|
|
330482
|
+
if (safe.pageScreenshots) delete safe.pageScreenshots;
|
|
330483
|
+
}
|
|
330149
330484
|
if (safe.exploreAlreadySaved === true) {
|
|
330150
330485
|
if (Array.isArray(safe.screenshots)) delete safe.screenshots;
|
|
330151
330486
|
if (safe.pageScreenshots) delete safe.pageScreenshots;
|
|
@@ -330245,7 +330580,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
330245
330580
|
const runMode = run2.mode ?? "run";
|
|
330246
330581
|
const RUNNER_MODE = opts.runnerMode ?? "playwright-native";
|
|
330247
330582
|
log.info(`Running test: ${run2.testCaseId ?? "auth-setup"} (run ${run2.runId}) [mode: ${runMode === "heal" ? "heal" : runMode === "auth-setup" ? "auth-setup" : RUNNER_MODE}]`);
|
|
330248
|
-
const liveBrowserHandle =
|
|
330583
|
+
const liveBrowserHandle = import_runner_core11.liveBrowserRegistry.register({
|
|
330249
330584
|
sessionId: `${run2.runId}`,
|
|
330250
330585
|
mode: toLiveBrowserMode(runMode),
|
|
330251
330586
|
runId: run2.runId,
|
|
@@ -330254,7 +330589,10 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
330254
330589
|
testCaseId: run2.testCaseId ?? void 0
|
|
330255
330590
|
});
|
|
330256
330591
|
requestLiveBrowserHeartbeat(true);
|
|
330257
|
-
const liveBrowserHeartbeatInterval = setInterval(() =>
|
|
330592
|
+
const liveBrowserHeartbeatInterval = setInterval(() => {
|
|
330593
|
+
liveBrowserHandle.patch({});
|
|
330594
|
+
requestLiveBrowserHeartbeat();
|
|
330595
|
+
}, 5e3);
|
|
330258
330596
|
liveBrowserHeartbeatInterval.unref?.();
|
|
330259
330597
|
const runNativeWithLiveBrowser = (nativeOptions) => (0, import_runner_core.executeTestViaNativePlaywright)({
|
|
330260
330598
|
...nativeOptions,
|
|
@@ -330281,7 +330619,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
330281
330619
|
log.fail(`PRE-FLIGHT FAILED - ${preflightError}`);
|
|
330282
330620
|
return;
|
|
330283
330621
|
}
|
|
330284
|
-
const preflight = await (0,
|
|
330622
|
+
const preflight = await (0, import_runner_core12.resolveBaseUrlPreflight)(run2.baseUrl);
|
|
330285
330623
|
if (preflight.error) {
|
|
330286
330624
|
await postResult(opts.serverUrl, headers, run2.runId, { status: "ERROR", error: preflight.error });
|
|
330287
330625
|
log.fail(`PRE-FLIGHT FAILED - ${preflight.error}`);
|
|
@@ -330338,7 +330676,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
330338
330676
|
return;
|
|
330339
330677
|
}
|
|
330340
330678
|
const healLogStream = createHealLogStreamer(opts.serverUrl, headers, run2.runId);
|
|
330341
|
-
const batchResult = await (0,
|
|
330679
|
+
const batchResult = await (0, import_runner_core10.executeGrokPerBatchHeal)(tests, run2.baseUrl, {
|
|
330342
330680
|
testVariables: run2.testVariables,
|
|
330343
330681
|
runNativeTest: runNativeWithLiveBrowser,
|
|
330344
330682
|
validateContext: { runId: run2.runId, serverUrl: opts.serverUrl, authHeader: headers.Authorization },
|
|
@@ -330359,16 +330697,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
330359
330697
|
}
|
|
330360
330698
|
return;
|
|
330361
330699
|
}
|
|
330362
|
-
if (runMode === "heal") {
|
|
330363
|
-
if (run2.framework === "APPIUM") {
|
|
330364
|
-
await postResult(opts.serverUrl, headers, run2.runId, {
|
|
330365
|
-
status: "ERROR",
|
|
330366
|
-
error: "CONFIG_ISSUE: Appium (mobile) tests are not auto-healable; skipped heal.",
|
|
330367
|
-
duration: Date.now() - startTime
|
|
330368
|
-
});
|
|
330369
|
-
log.warn("Skipped heal for APPIUM test (mobile tests are not healable)");
|
|
330370
|
-
return;
|
|
330371
|
-
}
|
|
330700
|
+
if (runMode === "heal" && run2.framework !== "APPIUM") {
|
|
330372
330701
|
const isApiTest2 = Array.isArray(run2.tags) && run2.tags.includes("@api");
|
|
330373
330702
|
if (isApiTest2) {
|
|
330374
330703
|
const testCode = run2.playwrightCode ?? "";
|
|
@@ -330572,7 +330901,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
330572
330901
|
if (classifyErrorMessage(composedError ?? null) === "config_issue") {
|
|
330573
330902
|
apiHealStatus = "ERROR";
|
|
330574
330903
|
} else {
|
|
330575
|
-
const rlDetection = (0,
|
|
330904
|
+
const rlDetection = (0, import_runner_core12.detectRateLimitBlock)({
|
|
330576
330905
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
330577
330906
|
tags: run2.tags ?? [],
|
|
330578
330907
|
error: composedError ?? finalVerifyResult?.error ?? null,
|
|
@@ -330679,7 +331008,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
330679
331008
|
log.info(`Heal engine: GROK (${source})`);
|
|
330680
331009
|
}
|
|
330681
331010
|
const grokHealLogStream = useGrokHeal ? createHealLogStreamer(opts.serverUrl, headers, run2.runId) : null;
|
|
330682
|
-
const result2 = useGrokHeal ? await (0,
|
|
331011
|
+
const result2 = useGrokHeal ? await (0, import_runner_core10.executeGrokPerTestHeal)(run2.playwrightCode, run2.steps, run2.baseUrl, {
|
|
330683
331012
|
...healOpts,
|
|
330684
331013
|
onLog: grokHealLogStream.onLog
|
|
330685
331014
|
}) : await (0, import_runner_core2.executeTestWithHealing)(run2.playwrightCode, run2.steps, run2.baseUrl, healOpts);
|
|
@@ -330712,7 +331041,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
330712
331041
|
let healStatus = result2.passed ? "PASSED" : isConfigIssue ? "ERROR" : "FAILED";
|
|
330713
331042
|
let healError = result2.error;
|
|
330714
331043
|
if (!result2.passed && !isConfigIssue) {
|
|
330715
|
-
const rlDetection = (0,
|
|
331044
|
+
const rlDetection = (0, import_runner_core12.detectRateLimitBlock)({
|
|
330716
331045
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
330717
331046
|
tags: run2.tags ?? [],
|
|
330718
331047
|
error: result2.error,
|
|
@@ -330906,7 +331235,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
330906
331235
|
liveBrowserHandle,
|
|
330907
331236
|
runNativeTest: runNativeWithLiveBrowser
|
|
330908
331237
|
};
|
|
330909
|
-
const healResult = useGrokHealSuite ? await (0,
|
|
331238
|
+
const healResult = useGrokHealSuite ? await (0, import_runner_core10.executeGrokPerTestHeal)(
|
|
330910
331239
|
failedTest.playwrightCode,
|
|
330911
331240
|
failedTest.steps ?? [],
|
|
330912
331241
|
run2.baseUrl,
|
|
@@ -331194,9 +331523,16 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331194
331523
|
signal: AbortSignal.timeout(3e4)
|
|
331195
331524
|
});
|
|
331196
331525
|
if (res.ok) return;
|
|
331197
|
-
|
|
331526
|
+
const responseText = await res.text().catch(() => "");
|
|
331527
|
+
const statusMessage = responseText ? `${res.status}: ${responseText.slice(0, 500)}` : String(res.status);
|
|
331528
|
+
if (res.status === 409) {
|
|
331529
|
+
throw new Error(`Discovery plan checkpoint rejected: ${statusMessage}`);
|
|
331530
|
+
}
|
|
331531
|
+
log.warn(`[stream] discovery-plan attempt ${attempt + 1} returned ${statusMessage}`);
|
|
331198
331532
|
} catch (err) {
|
|
331199
|
-
|
|
331533
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
331534
|
+
if (/Discovery plan checkpoint rejected/i.test(message)) throw err;
|
|
331535
|
+
log.warn(`[stream] discovery-plan attempt ${attempt + 1} failed: ${message}`);
|
|
331200
331536
|
}
|
|
331201
331537
|
if (attempt < 2) await new Promise((r) => setTimeout(r, 3e3 * (attempt + 1)));
|
|
331202
331538
|
}
|
|
@@ -331204,6 +331540,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331204
331540
|
};
|
|
331205
331541
|
let discoveryResult;
|
|
331206
331542
|
const buildFinalBody = (r, overrideError, auditWarning) => {
|
|
331543
|
+
const isNativeDiscovery = ctx.medium === "IOS_NATIVE" || ctx.medium === "ANDROID_NATIVE";
|
|
331207
331544
|
const { exploreFinalMessages: _exploreFinalMessages, ...rest } = r;
|
|
331208
331545
|
void _exploreFinalMessages;
|
|
331209
331546
|
const logs2 = auditWarning ? [rest.logs, auditWarning].filter(Boolean).join("\n") : rest.logs;
|
|
@@ -331253,7 +331590,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331253
331590
|
resolvedAppOrigin: rest.resolvedAppOrigin,
|
|
331254
331591
|
plans: rest.plans,
|
|
331255
331592
|
exploreStats: rest.exploreStats,
|
|
331256
|
-
screenshots: rest.screenshots,
|
|
331593
|
+
...isNativeDiscovery ? {} : { screenshots: rest.screenshots },
|
|
331257
331594
|
snapshotEvidence: rest.snapshotEvidence,
|
|
331258
331595
|
discoveryCheckpoints: rest.discoveryCheckpoints,
|
|
331259
331596
|
streamedTestCount: streamedTestKeys.size,
|
|
@@ -331262,8 +331599,10 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331262
331599
|
...explorePosted ? {} : {
|
|
331263
331600
|
collectedPages: rest.collectedPages,
|
|
331264
331601
|
collectedTransitions: rest.collectedTransitions,
|
|
331265
|
-
|
|
331266
|
-
|
|
331602
|
+
...isNativeDiscovery ? {} : {
|
|
331603
|
+
pageScreenshots: rest.pageScreenshots,
|
|
331604
|
+
pageScreenshotKeys: rest.pageScreenshotKeys
|
|
331605
|
+
}
|
|
331267
331606
|
},
|
|
331268
331607
|
...harPosted ? {} : {
|
|
331269
331608
|
apiCalls: rest.apiCalls,
|
|
@@ -331271,7 +331610,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331271
331610
|
},
|
|
331272
331611
|
// Only send tests that were NOT confirmed streamed to avoid duplicates
|
|
331273
331612
|
tests: rest.tests.filter((t) => !streamedTestKeys.has(fingerprintTest(t)))
|
|
331274
|
-
}, logs2);
|
|
331613
|
+
}, logs2, { omitScreenshots: isNativeDiscovery });
|
|
331275
331614
|
};
|
|
331276
331615
|
const acquireLoginBrowser = async () => {
|
|
331277
331616
|
const browser = await browserPool.acquireAsync(12e3);
|
|
@@ -331375,7 +331714,6 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331375
331714
|
thumbnailCapturedAt: Date.now()
|
|
331376
331715
|
});
|
|
331377
331716
|
requestLiveBrowserHeartbeat();
|
|
331378
|
-
void onScreenshot("mobile", base64);
|
|
331379
331717
|
},
|
|
331380
331718
|
onExploreComplete,
|
|
331381
331719
|
onTestGenerated,
|
|
@@ -331402,7 +331740,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331402
331740
|
setExecutorBusy(false);
|
|
331403
331741
|
}
|
|
331404
331742
|
} else if (ctx.mode === "import") {
|
|
331405
|
-
const runRepoImport = ctx.frameworkImport?.kind === "repo-analysis" ?
|
|
331743
|
+
const runRepoImport = ctx.frameworkImport?.kind === "repo-analysis" ? import_runner_core10.runRepoAnalysisOnRunner : import_runner_core10.runFrameworkImportOnRunner;
|
|
331406
331744
|
discoveryResult = await runRepoImport(ctx, { onTestGenerated, acquireLoginBrowser, onAudit: discoveryAudit }, {
|
|
331407
331745
|
archiveUrl: ctx.frameworkImport?.source === "upload" ? `${opts.serverUrl}/api/runner/runs/${run2.runId}/import-archive` : void 0,
|
|
331408
331746
|
archiveAuthHeader: headers.Authorization,
|
|
@@ -331732,7 +332070,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
331732
332070
|
let runStatus = result.passed ? "PASSED" : "FAILED";
|
|
331733
332071
|
let runError = result.error;
|
|
331734
332072
|
if (!result.passed) {
|
|
331735
|
-
const rlDetection = (0,
|
|
332073
|
+
const rlDetection = (0, import_runner_core12.detectRateLimitBlock)({
|
|
331736
332074
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
331737
332075
|
tags: run2.tags ?? [],
|
|
331738
332076
|
error: result.error,
|
|
@@ -331870,7 +332208,7 @@ async function startRunner(opts) {
|
|
|
331870
332208
|
capture: envInt("MAX_CAPTURE", DEFAULT_TYPE_LIMITS.capture),
|
|
331871
332209
|
run: envInt("MAX_RUN", DEFAULT_TYPE_LIMITS.run)
|
|
331872
332210
|
};
|
|
331873
|
-
const RESOURCE_GOVERNOR_OPTIONS = (0,
|
|
332211
|
+
const RESOURCE_GOVERNOR_OPTIONS = (0, import_runner_core12.createResourceGovernorOptions)({
|
|
331874
332212
|
isCloud: IS_CLOUD,
|
|
331875
332213
|
maxConcurrent: MAX_CONCURRENT,
|
|
331876
332214
|
typeLimits: TYPE_LIMITS
|
|
@@ -332021,8 +332359,8 @@ async function startRunner(opts) {
|
|
|
332021
332359
|
android: devices.some((d) => d.platform === "ANDROID" && d.isAvailable && d.state === "BOOTED")
|
|
332022
332360
|
} : { web: true, ios: false, android: false };
|
|
332023
332361
|
const bridge = enableMobile ? getBridgeState() : void 0;
|
|
332024
|
-
const resourceStatus = (0,
|
|
332025
|
-
const processInventory = await (0,
|
|
332362
|
+
const resourceStatus = (0, import_runner_core12.getResourceGovernorStatus)(RESOURCE_GOVERNOR_OPTIONS, Object.fromEntries(activeByType));
|
|
332363
|
+
const processInventory = await (0, import_runner_core12.getBrowserProcessInventory)();
|
|
332026
332364
|
lastInventoryPids = new Set(processInventory.processes.map((p) => p.pid));
|
|
332027
332365
|
lastInventoryKinds.clear();
|
|
332028
332366
|
for (const p of processInventory.processes) lastInventoryKinds.set(p.pid, p.kind);
|
|
@@ -332049,8 +332387,8 @@ async function startRunner(opts) {
|
|
|
332049
332387
|
resourceSnapshot: resourceStatus.snapshot,
|
|
332050
332388
|
resourceGovernor: resourceStatus.governor,
|
|
332051
332389
|
processInventory,
|
|
332052
|
-
aiQueue: (0,
|
|
332053
|
-
liveBrowsers:
|
|
332390
|
+
aiQueue: (0, import_runner_core12.getRunnerAIQueueStats)(),
|
|
332391
|
+
liveBrowsers: import_runner_core11.liveBrowserRegistry.snapshotForHeartbeat(),
|
|
332054
332392
|
// Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
|
|
332055
332393
|
// Cached & cheap — see getCachedDoctor above.
|
|
332056
332394
|
doctor: getCachedDoctor()
|
|
@@ -332144,7 +332482,7 @@ async function startRunner(opts) {
|
|
|
332144
332482
|
let pidsWatchdogWarned = false;
|
|
332145
332483
|
const pidsWatchdog = setInterval(() => {
|
|
332146
332484
|
if (shuttingDown) return;
|
|
332147
|
-
const snapshot = (0,
|
|
332485
|
+
const snapshot = (0, import_runner_core12.getResourceSnapshot)();
|
|
332148
332486
|
const pct = snapshot.pidsPct;
|
|
332149
332487
|
if (pct == null || !Number.isFinite(pct)) return;
|
|
332150
332488
|
if (pct >= PIDS_RECYCLE_PCT) {
|
|
@@ -332180,7 +332518,7 @@ async function startRunner(opts) {
|
|
|
332180
332518
|
log.dim(` Deferred ${run2.runId} (project ${runProject} at capacity ${getActiveForProject(runProject)}/${projectLimit})`);
|
|
332181
332519
|
continue;
|
|
332182
332520
|
}
|
|
332183
|
-
const admission = (0,
|
|
332521
|
+
const admission = (0, import_runner_core12.evaluateRunAdmission)(RESOURCE_GOVERNOR_OPTIONS, {
|
|
332184
332522
|
mode: runMode,
|
|
332185
332523
|
activeRuns,
|
|
332186
332524
|
activeByType: Object.fromEntries(activeByType),
|