@validate.qa/runner 1.0.13 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +145 -11
- package/dist/cli.mjs +145 -11
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1357,6 +1357,9 @@ var init_constants = __esm({
|
|
|
1357
1357
|
function toJsStringLiteral(value) {
|
|
1358
1358
|
return JSON.stringify(value);
|
|
1359
1359
|
}
|
|
1360
|
+
function toInlineCommentText(value) {
|
|
1361
|
+
return value.replace(/[\r\n\u2028\u2029]+/g, " ");
|
|
1362
|
+
}
|
|
1360
1363
|
function isPlainObject(value) {
|
|
1361
1364
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1362
1365
|
}
|
|
@@ -1562,7 +1565,7 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
1562
1565
|
`// 3. A booted device/simulator matching the capabilities below`,
|
|
1563
1566
|
`// This is NOT a WDIO testrunner spec: it uses the standalone remote() client`,
|
|
1564
1567
|
`// (driver.$, no auto-injected $/expect/describe globals) so it has no harness`,
|
|
1565
|
-
`// dependency and runs with a plain \`tsx ${testName}.spec.ts\`.`,
|
|
1568
|
+
`// dependency and runs with a plain \`tsx ${toInlineCommentText(testName)}.spec.ts\`.`,
|
|
1566
1569
|
`import { remote } from 'webdriverio';`,
|
|
1567
1570
|
``,
|
|
1568
1571
|
`// Resolve the first selector that exists, mirroring the runner's locator`,
|
|
@@ -1645,7 +1648,7 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
1645
1648
|
` // Canonical execution source: steps JSON`
|
|
1646
1649
|
];
|
|
1647
1650
|
const stepLines = steps.flatMap((step) => {
|
|
1648
|
-
const lines = [` // Step ${step.order + 1}: ${step.description}`];
|
|
1651
|
+
const lines = [` // Step ${step.order + 1}: ${toInlineCommentText(step.description)}`];
|
|
1649
1652
|
const candidates = selectorCandidatesForStep(step);
|
|
1650
1653
|
const candidatesLiteral = `[${candidates.map(toJsStringLiteral).join(", ")}]`;
|
|
1651
1654
|
const hasSelector = candidates.length > 0;
|
|
@@ -9696,7 +9699,26 @@ ${statePacket}` },
|
|
|
9696
9699
|
steps: compiled.steps,
|
|
9697
9700
|
playwrightCode: "",
|
|
9698
9701
|
verified,
|
|
9699
|
-
tags
|
|
9702
|
+
tags,
|
|
9703
|
+
// Carry the planner scenario identity so incremental/resume can match an
|
|
9704
|
+
// already-generated scenario reliably (scenarioRef.name === the plan's
|
|
9705
|
+
// scenario name). Without this the resume skip-set falls back to the
|
|
9706
|
+
// model-chosen test name and may regenerate completed scenarios. Mobile has
|
|
9707
|
+
// no live command/locator ledger, so evidence is an empty (but valid) bundle
|
|
9708
|
+
// — scenarioRef is the load-bearing field for the skip-set.
|
|
9709
|
+
healingContext: {
|
|
9710
|
+
scenarioRef: {
|
|
9711
|
+
name: scenario.name,
|
|
9712
|
+
featureGroup: scenario.featureGroup,
|
|
9713
|
+
goal: scenario.goal,
|
|
9714
|
+
type: scenario.type,
|
|
9715
|
+
variant: scenario.variant,
|
|
9716
|
+
entryRoute: scenario.entryRoute,
|
|
9717
|
+
startFromLanding: scenario.startFromLanding,
|
|
9718
|
+
targetPages: [...scenario.targetPages]
|
|
9719
|
+
},
|
|
9720
|
+
evidence: { observedLocators: [], observedTransitions: [] }
|
|
9721
|
+
}
|
|
9700
9722
|
};
|
|
9701
9723
|
return test;
|
|
9702
9724
|
}
|
|
@@ -326838,7 +326860,7 @@ var require_package4 = __commonJS({
|
|
|
326838
326860
|
"package.json"(exports2, module2) {
|
|
326839
326861
|
module2.exports = {
|
|
326840
326862
|
name: "@validate.qa/runner",
|
|
326841
|
-
version: "1.0.
|
|
326863
|
+
version: "1.0.14",
|
|
326842
326864
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
326843
326865
|
bin: {
|
|
326844
326866
|
"validate-runner": "dist/cli.js"
|
|
@@ -327557,6 +327579,8 @@ var DEVICE_CMD_TIMEOUT_MS = 15e3;
|
|
|
327557
327579
|
var CA_KEY_BITS = 2048;
|
|
327558
327580
|
var ANDROID_EMULATOR_HOST_LOOPBACK = "10.0.2.2";
|
|
327559
327581
|
var PENDING_CLEANUP_DIR = (0, import_node_path.join)((0, import_node_os.tmpdir)(), "validateqa-mobile-capture-pending");
|
|
327582
|
+
var IOS_SIM_HOST_PROXY_ENV = "VALIDATEQA_MOBILE_IOS_SIM_HOST_PROXY";
|
|
327583
|
+
var STALE_MARKER_RECONCILE_MS = 6 * 60 * 60 * 1e3;
|
|
327560
327584
|
function envFlag(name) {
|
|
327561
327585
|
const value = (process.env[name] ?? "").trim().toLowerCase();
|
|
327562
327586
|
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
@@ -328026,7 +328050,7 @@ function hostProxyPriorFromMarker(marker) {
|
|
|
328026
328050
|
}
|
|
328027
328051
|
};
|
|
328028
328052
|
}
|
|
328029
|
-
function hostProxyControlSupported(platform3, isPhysical, platformName = process.platform) {
|
|
328053
|
+
function hostProxyControlSupported(platform3, isPhysical, platformName = process.platform, optedIn = envFlag(IOS_SIM_HOST_PROXY_ENV)) {
|
|
328030
328054
|
if (platform3 !== "IOS") return { supported: false };
|
|
328031
328055
|
if (isPhysical) {
|
|
328032
328056
|
return { supported: false, reason: "physical iOS device requires a manual Wi-Fi HTTP proxy; host proxy not applied" };
|
|
@@ -328034,28 +328058,46 @@ function hostProxyControlSupported(platform3, isPhysical, platformName = process
|
|
|
328034
328058
|
if (platformName !== "darwin") {
|
|
328035
328059
|
return { supported: false, reason: `iOS simulator host proxy is only controllable on macOS (host is ${platformName})` };
|
|
328036
328060
|
}
|
|
328061
|
+
if (!optedIn) {
|
|
328062
|
+
return {
|
|
328063
|
+
supported: false,
|
|
328064
|
+
reason: `iOS simulator HTTPS body capture is disabled: it requires repointing the macOS SYSTEM-WIDE HTTP/HTTPS proxy, which would break host HTTPS (Safari/Chrome/Electron) and capture the host's own traffic for this run. Not applied without consent \u2014 set ${IOS_SIM_HOST_PROXY_ENV}=1 to enable it.`
|
|
328065
|
+
};
|
|
328066
|
+
}
|
|
328037
328067
|
return { supported: true };
|
|
328038
328068
|
}
|
|
328039
328069
|
async function iosSimConfigureHostProxy(host, port, onLog) {
|
|
328070
|
+
let service;
|
|
328071
|
+
let prior;
|
|
328040
328072
|
try {
|
|
328041
328073
|
const { stdout: order } = await execFileAsync("networksetup", ["-listnetworkserviceorder"], {
|
|
328042
328074
|
timeout: DEVICE_CMD_TIMEOUT_MS
|
|
328043
328075
|
});
|
|
328044
|
-
const
|
|
328045
|
-
if (!
|
|
328076
|
+
const svc = parsePrimaryNetworkService(order);
|
|
328077
|
+
if (!svc) {
|
|
328046
328078
|
return { configured: false, reason: "iOS simulator host proxy could not be configured: no active macOS network service found" };
|
|
328047
328079
|
}
|
|
328080
|
+
service = svc;
|
|
328048
328081
|
const [{ stdout: webOut }, { stdout: secureOut }] = await Promise.all([
|
|
328049
328082
|
execFileAsync("networksetup", ["-getwebproxy", service], { timeout: DEVICE_CMD_TIMEOUT_MS }),
|
|
328050
328083
|
execFileAsync("networksetup", ["-getsecurewebproxy", service], { timeout: DEVICE_CMD_TIMEOUT_MS })
|
|
328051
328084
|
]);
|
|
328052
|
-
|
|
328085
|
+
prior = { web: parseWebProxyState(webOut), secure: parseWebProxyState(secureOut) };
|
|
328053
328086
|
for (const args of buildHostProxySetCommands(service, host, port)) {
|
|
328054
328087
|
await execFileAsync("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
|
|
328055
328088
|
}
|
|
328056
328089
|
onLog?.(`[mobile-net] macOS host proxy set on "${service}" \u2192 ${host}:${port} (iOS simulator shares the host network).`);
|
|
328057
328090
|
return { configured: true, service, prior };
|
|
328058
328091
|
} catch (err) {
|
|
328092
|
+
if (service && prior) {
|
|
328093
|
+
onLog?.(`[mobile-net] host proxy set failed mid-way \u2014 rolling back "${service}" to prior state: ${errMsg(err)}`);
|
|
328094
|
+
for (const args of buildHostProxyRestoreCommands(service, prior)) {
|
|
328095
|
+
try {
|
|
328096
|
+
await execFileAsync("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
|
|
328097
|
+
} catch {
|
|
328098
|
+
}
|
|
328099
|
+
}
|
|
328100
|
+
}
|
|
328059
328101
|
return { configured: false, reason: `iOS simulator host proxy could not be configured: ${errMsg(err)}` };
|
|
328060
328102
|
}
|
|
328061
328103
|
}
|
|
@@ -330895,6 +330937,79 @@ function printDoctorReport(report) {
|
|
|
330895
330937
|
console.log("");
|
|
330896
330938
|
}
|
|
330897
330939
|
|
|
330940
|
+
// src/result-payload-budget.ts
|
|
330941
|
+
var SERVER_RESULT_BODY_LIMIT_BYTES = 50 * 1024 * 1024;
|
|
330942
|
+
var RESULT_BODY_BUDGET_BYTES = 48 * 1024 * 1024;
|
|
330943
|
+
var SERVER_VIDEO_STORE_CAP_BYTES = 24 * 1024 * 1024;
|
|
330944
|
+
function serializedBytes(value) {
|
|
330945
|
+
return Buffer.byteLength(JSON.stringify(value) ?? "", "utf8");
|
|
330946
|
+
}
|
|
330947
|
+
function budgetResultPayload(result, maxBytes = RESULT_BODY_BUDGET_BYTES, videoStoreCapBytes = SERVER_VIDEO_STORE_CAP_BYTES) {
|
|
330948
|
+
const oversizedVideo = typeof result.video === "string" && result.video.length > videoStoreCapBytes;
|
|
330949
|
+
let bytes = serializedBytes(result);
|
|
330950
|
+
if (bytes <= maxBytes && !oversizedVideo) {
|
|
330951
|
+
return { payload: result, shed: [], bytes };
|
|
330952
|
+
}
|
|
330953
|
+
const shed = [];
|
|
330954
|
+
let payload = result;
|
|
330955
|
+
const drop = (fields, label) => {
|
|
330956
|
+
const next = { ...payload };
|
|
330957
|
+
for (const field of fields) next[field] = void 0;
|
|
330958
|
+
payload = next;
|
|
330959
|
+
shed.push(label);
|
|
330960
|
+
bytes = serializedBytes(payload);
|
|
330961
|
+
};
|
|
330962
|
+
if ((bytes > maxBytes || oversizedVideo) && payload.video != null) {
|
|
330963
|
+
drop(["video", "videoMimeType"], "video");
|
|
330964
|
+
}
|
|
330965
|
+
if (bytes > maxBytes && Array.isArray(payload.screenshots) && payload.screenshots.length > 0) {
|
|
330966
|
+
const shots = [...payload.screenshots];
|
|
330967
|
+
const bySizeDesc = shots.map((shot, i) => ({ i, len: typeof shot === "string" ? shot.length : 0 })).sort((a, b) => b.len - a.len);
|
|
330968
|
+
const overshoot = bytes - maxBytes;
|
|
330969
|
+
let shaved = 0;
|
|
330970
|
+
let blanked = 0;
|
|
330971
|
+
for (const { i, len } of bySizeDesc) {
|
|
330972
|
+
if (shaved >= overshoot || len === 0) break;
|
|
330973
|
+
if (shots[i] === "") continue;
|
|
330974
|
+
shots[i] = "";
|
|
330975
|
+
shaved += len;
|
|
330976
|
+
blanked++;
|
|
330977
|
+
}
|
|
330978
|
+
if (blanked > 0) {
|
|
330979
|
+
payload = { ...payload, screenshots: shots };
|
|
330980
|
+
bytes = serializedBytes(payload);
|
|
330981
|
+
shed.push(blanked === shots.length ? "screenshots" : `${blanked}/${shots.length} screenshots`);
|
|
330982
|
+
}
|
|
330983
|
+
}
|
|
330984
|
+
if (bytes > maxBytes && payload.harApiCalls != null) drop(["harApiCalls"], "harApiCalls");
|
|
330985
|
+
if (bytes > maxBytes && payload.apiCalls != null) drop(["apiCalls"], "apiCalls");
|
|
330986
|
+
if (shed.length > 0) {
|
|
330987
|
+
const note = `
|
|
330988
|
+
|
|
330989
|
+
\u26A0 [runner] Result media trimmed before upload \u2014 payload exceeded the ${Math.round(maxBytes / (1024 * 1024))}MB server limit; dropped: ${shed.join(", ")}.`;
|
|
330990
|
+
payload = {
|
|
330991
|
+
...payload,
|
|
330992
|
+
logs: (typeof payload.logs === "string" ? payload.logs : "") + note
|
|
330993
|
+
};
|
|
330994
|
+
bytes = serializedBytes(payload);
|
|
330995
|
+
}
|
|
330996
|
+
if (bytes > maxBytes && typeof payload.logs === "string" && payload.logs.length > 4096) {
|
|
330997
|
+
const over = bytes - maxBytes;
|
|
330998
|
+
const targetLen = Math.max(2048, payload.logs.length - over - 1024);
|
|
330999
|
+
if (targetLen < payload.logs.length) {
|
|
331000
|
+
const half = Math.floor(targetLen / 2);
|
|
331001
|
+
const head = payload.logs.slice(0, half);
|
|
331002
|
+
const tail = payload.logs.slice(payload.logs.length - half);
|
|
331003
|
+
payload = { ...payload, logs: `${head}
|
|
331004
|
+
\u2026 [logs truncated to fit upload] \u2026
|
|
331005
|
+
${tail}` };
|
|
331006
|
+
if (!shed.includes("logs (truncated)")) shed.push("logs (truncated)");
|
|
331007
|
+
bytes = serializedBytes(payload);
|
|
331008
|
+
}
|
|
331009
|
+
}
|
|
331010
|
+
return { payload, shed, bytes };
|
|
331011
|
+
}
|
|
331012
|
+
|
|
330898
331013
|
// src/runner.ts
|
|
330899
331014
|
init_dist();
|
|
330900
331015
|
var import_runner_core12 = __toESM(require_dist2());
|
|
@@ -331063,15 +331178,33 @@ async function claimRun(serverUrl, headers, runId) {
|
|
|
331063
331178
|
async function postResult(serverUrl, headers, runId, result) {
|
|
331064
331179
|
const MAX_ATTEMPTS = 3;
|
|
331065
331180
|
let lastError;
|
|
331181
|
+
const budgeted = budgetResultPayload(result);
|
|
331182
|
+
if (budgeted.shed.length > 0) {
|
|
331183
|
+
console.warn(
|
|
331184
|
+
`[runner] Result payload for run ${runId} exceeded the upload budget (${Math.round(budgeted.bytes / (1024 * 1024))}MB after trim); dropped: ${budgeted.shed.join(", ")}.`
|
|
331185
|
+
);
|
|
331186
|
+
}
|
|
331187
|
+
let payload = budgeted.payload;
|
|
331066
331188
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
331067
331189
|
try {
|
|
331068
331190
|
const res = await fetch(`${serverUrl}/api/runner/runs/${runId}/result`, {
|
|
331069
331191
|
method: "POST",
|
|
331070
331192
|
headers,
|
|
331071
|
-
body: JSON.stringify(
|
|
331193
|
+
body: JSON.stringify(payload),
|
|
331072
331194
|
signal: AbortSignal.timeout(6e4)
|
|
331073
331195
|
});
|
|
331074
331196
|
if (res.ok) return;
|
|
331197
|
+
if (res.status === 413) {
|
|
331198
|
+
const minimal = budgetResultPayload(payload, 0);
|
|
331199
|
+
if (minimal.shed.length > 0) {
|
|
331200
|
+
console.warn(
|
|
331201
|
+
`[runner] Server rejected result for run ${runId} as too large (413); retrying with a verdict-only payload (dropped: ${minimal.shed.join(", ")}).`
|
|
331202
|
+
);
|
|
331203
|
+
payload = minimal.payload;
|
|
331204
|
+
lastError = new Error("HTTP 413 (payload trimmed, retrying)");
|
|
331205
|
+
continue;
|
|
331206
|
+
}
|
|
331207
|
+
}
|
|
331075
331208
|
lastError = new Error(`HTTP ${res.status}`);
|
|
331076
331209
|
} catch (err) {
|
|
331077
331210
|
lastError = err instanceof Error ? err : new Error(String(err));
|
|
@@ -332606,9 +332739,10 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
332606
332739
|
onPlanReady
|
|
332607
332740
|
});
|
|
332608
332741
|
} finally {
|
|
332609
|
-
|
|
332742
|
+
const activeCapture = capture;
|
|
332743
|
+
if (activeCapture) {
|
|
332610
332744
|
try {
|
|
332611
|
-
const apiCalls = await
|
|
332745
|
+
const apiCalls = await activeCapture.stop();
|
|
332612
332746
|
if (discoveryResult) {
|
|
332613
332747
|
discoveryResult.apiCalls = apiCalls.length ? attachMobileDiscoveryNetworkContext(apiCalls, discoveryResult, session?.appId ?? mobileTarget.appId) : void 0;
|
|
332614
332748
|
}
|
package/dist/cli.mjs
CHANGED
|
@@ -1362,6 +1362,9 @@ var init_constants = __esm({
|
|
|
1362
1362
|
function toJsStringLiteral(value) {
|
|
1363
1363
|
return JSON.stringify(value);
|
|
1364
1364
|
}
|
|
1365
|
+
function toInlineCommentText(value) {
|
|
1366
|
+
return value.replace(/[\r\n\u2028\u2029]+/g, " ");
|
|
1367
|
+
}
|
|
1365
1368
|
function isPlainObject(value) {
|
|
1366
1369
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1367
1370
|
}
|
|
@@ -1567,7 +1570,7 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
1567
1570
|
`// 3. A booted device/simulator matching the capabilities below`,
|
|
1568
1571
|
`// This is NOT a WDIO testrunner spec: it uses the standalone remote() client`,
|
|
1569
1572
|
`// (driver.$, no auto-injected $/expect/describe globals) so it has no harness`,
|
|
1570
|
-
`// dependency and runs with a plain \`tsx ${testName}.spec.ts\`.`,
|
|
1573
|
+
`// dependency and runs with a plain \`tsx ${toInlineCommentText(testName)}.spec.ts\`.`,
|
|
1571
1574
|
`import { remote } from 'webdriverio';`,
|
|
1572
1575
|
``,
|
|
1573
1576
|
`// Resolve the first selector that exists, mirroring the runner's locator`,
|
|
@@ -1650,7 +1653,7 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
1650
1653
|
` // Canonical execution source: steps JSON`
|
|
1651
1654
|
];
|
|
1652
1655
|
const stepLines = steps.flatMap((step) => {
|
|
1653
|
-
const lines = [` // Step ${step.order + 1}: ${step.description}`];
|
|
1656
|
+
const lines = [` // Step ${step.order + 1}: ${toInlineCommentText(step.description)}`];
|
|
1654
1657
|
const candidates = selectorCandidatesForStep(step);
|
|
1655
1658
|
const candidatesLiteral = `[${candidates.map(toJsStringLiteral).join(", ")}]`;
|
|
1656
1659
|
const hasSelector = candidates.length > 0;
|
|
@@ -9701,7 +9704,26 @@ ${statePacket}` },
|
|
|
9701
9704
|
steps: compiled.steps,
|
|
9702
9705
|
playwrightCode: "",
|
|
9703
9706
|
verified,
|
|
9704
|
-
tags
|
|
9707
|
+
tags,
|
|
9708
|
+
// Carry the planner scenario identity so incremental/resume can match an
|
|
9709
|
+
// already-generated scenario reliably (scenarioRef.name === the plan's
|
|
9710
|
+
// scenario name). Without this the resume skip-set falls back to the
|
|
9711
|
+
// model-chosen test name and may regenerate completed scenarios. Mobile has
|
|
9712
|
+
// no live command/locator ledger, so evidence is an empty (but valid) bundle
|
|
9713
|
+
// — scenarioRef is the load-bearing field for the skip-set.
|
|
9714
|
+
healingContext: {
|
|
9715
|
+
scenarioRef: {
|
|
9716
|
+
name: scenario.name,
|
|
9717
|
+
featureGroup: scenario.featureGroup,
|
|
9718
|
+
goal: scenario.goal,
|
|
9719
|
+
type: scenario.type,
|
|
9720
|
+
variant: scenario.variant,
|
|
9721
|
+
entryRoute: scenario.entryRoute,
|
|
9722
|
+
startFromLanding: scenario.startFromLanding,
|
|
9723
|
+
targetPages: [...scenario.targetPages]
|
|
9724
|
+
},
|
|
9725
|
+
evidence: { observedLocators: [], observedTransitions: [] }
|
|
9726
|
+
}
|
|
9705
9727
|
};
|
|
9706
9728
|
return test;
|
|
9707
9729
|
}
|
|
@@ -326843,7 +326865,7 @@ var require_package4 = __commonJS({
|
|
|
326843
326865
|
"package.json"(exports, module) {
|
|
326844
326866
|
module.exports = {
|
|
326845
326867
|
name: "@validate.qa/runner",
|
|
326846
|
-
version: "1.0.
|
|
326868
|
+
version: "1.0.14",
|
|
326847
326869
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
326848
326870
|
bin: {
|
|
326849
326871
|
"validate-runner": "dist/cli.js"
|
|
@@ -327565,6 +327587,8 @@ var DEVICE_CMD_TIMEOUT_MS = 15e3;
|
|
|
327565
327587
|
var CA_KEY_BITS = 2048;
|
|
327566
327588
|
var ANDROID_EMULATOR_HOST_LOOPBACK = "10.0.2.2";
|
|
327567
327589
|
var PENDING_CLEANUP_DIR = join2(tmpdir(), "validateqa-mobile-capture-pending");
|
|
327590
|
+
var IOS_SIM_HOST_PROXY_ENV = "VALIDATEQA_MOBILE_IOS_SIM_HOST_PROXY";
|
|
327591
|
+
var STALE_MARKER_RECONCILE_MS = 6 * 60 * 60 * 1e3;
|
|
327568
327592
|
function envFlag(name) {
|
|
327569
327593
|
const value = (process.env[name] ?? "").trim().toLowerCase();
|
|
327570
327594
|
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
@@ -328034,7 +328058,7 @@ function hostProxyPriorFromMarker(marker) {
|
|
|
328034
328058
|
}
|
|
328035
328059
|
};
|
|
328036
328060
|
}
|
|
328037
|
-
function hostProxyControlSupported(platform3, isPhysical, platformName = process.platform) {
|
|
328061
|
+
function hostProxyControlSupported(platform3, isPhysical, platformName = process.platform, optedIn = envFlag(IOS_SIM_HOST_PROXY_ENV)) {
|
|
328038
328062
|
if (platform3 !== "IOS") return { supported: false };
|
|
328039
328063
|
if (isPhysical) {
|
|
328040
328064
|
return { supported: false, reason: "physical iOS device requires a manual Wi-Fi HTTP proxy; host proxy not applied" };
|
|
@@ -328042,28 +328066,46 @@ function hostProxyControlSupported(platform3, isPhysical, platformName = process
|
|
|
328042
328066
|
if (platformName !== "darwin") {
|
|
328043
328067
|
return { supported: false, reason: `iOS simulator host proxy is only controllable on macOS (host is ${platformName})` };
|
|
328044
328068
|
}
|
|
328069
|
+
if (!optedIn) {
|
|
328070
|
+
return {
|
|
328071
|
+
supported: false,
|
|
328072
|
+
reason: `iOS simulator HTTPS body capture is disabled: it requires repointing the macOS SYSTEM-WIDE HTTP/HTTPS proxy, which would break host HTTPS (Safari/Chrome/Electron) and capture the host's own traffic for this run. Not applied without consent \u2014 set ${IOS_SIM_HOST_PROXY_ENV}=1 to enable it.`
|
|
328073
|
+
};
|
|
328074
|
+
}
|
|
328045
328075
|
return { supported: true };
|
|
328046
328076
|
}
|
|
328047
328077
|
async function iosSimConfigureHostProxy(host, port, onLog) {
|
|
328078
|
+
let service;
|
|
328079
|
+
let prior;
|
|
328048
328080
|
try {
|
|
328049
328081
|
const { stdout: order } = await execFileAsync("networksetup", ["-listnetworkserviceorder"], {
|
|
328050
328082
|
timeout: DEVICE_CMD_TIMEOUT_MS
|
|
328051
328083
|
});
|
|
328052
|
-
const
|
|
328053
|
-
if (!
|
|
328084
|
+
const svc = parsePrimaryNetworkService(order);
|
|
328085
|
+
if (!svc) {
|
|
328054
328086
|
return { configured: false, reason: "iOS simulator host proxy could not be configured: no active macOS network service found" };
|
|
328055
328087
|
}
|
|
328088
|
+
service = svc;
|
|
328056
328089
|
const [{ stdout: webOut }, { stdout: secureOut }] = await Promise.all([
|
|
328057
328090
|
execFileAsync("networksetup", ["-getwebproxy", service], { timeout: DEVICE_CMD_TIMEOUT_MS }),
|
|
328058
328091
|
execFileAsync("networksetup", ["-getsecurewebproxy", service], { timeout: DEVICE_CMD_TIMEOUT_MS })
|
|
328059
328092
|
]);
|
|
328060
|
-
|
|
328093
|
+
prior = { web: parseWebProxyState(webOut), secure: parseWebProxyState(secureOut) };
|
|
328061
328094
|
for (const args of buildHostProxySetCommands(service, host, port)) {
|
|
328062
328095
|
await execFileAsync("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
|
|
328063
328096
|
}
|
|
328064
328097
|
onLog?.(`[mobile-net] macOS host proxy set on "${service}" \u2192 ${host}:${port} (iOS simulator shares the host network).`);
|
|
328065
328098
|
return { configured: true, service, prior };
|
|
328066
328099
|
} catch (err) {
|
|
328100
|
+
if (service && prior) {
|
|
328101
|
+
onLog?.(`[mobile-net] host proxy set failed mid-way \u2014 rolling back "${service}" to prior state: ${errMsg(err)}`);
|
|
328102
|
+
for (const args of buildHostProxyRestoreCommands(service, prior)) {
|
|
328103
|
+
try {
|
|
328104
|
+
await execFileAsync("networksetup", args, { timeout: DEVICE_CMD_TIMEOUT_MS });
|
|
328105
|
+
} catch {
|
|
328106
|
+
}
|
|
328107
|
+
}
|
|
328108
|
+
}
|
|
328067
328109
|
return { configured: false, reason: `iOS simulator host proxy could not be configured: ${errMsg(err)}` };
|
|
328068
328110
|
}
|
|
328069
328111
|
}
|
|
@@ -330903,6 +330945,79 @@ function printDoctorReport(report) {
|
|
|
330903
330945
|
console.log("");
|
|
330904
330946
|
}
|
|
330905
330947
|
|
|
330948
|
+
// src/result-payload-budget.ts
|
|
330949
|
+
var SERVER_RESULT_BODY_LIMIT_BYTES = 50 * 1024 * 1024;
|
|
330950
|
+
var RESULT_BODY_BUDGET_BYTES = 48 * 1024 * 1024;
|
|
330951
|
+
var SERVER_VIDEO_STORE_CAP_BYTES = 24 * 1024 * 1024;
|
|
330952
|
+
function serializedBytes(value) {
|
|
330953
|
+
return Buffer.byteLength(JSON.stringify(value) ?? "", "utf8");
|
|
330954
|
+
}
|
|
330955
|
+
function budgetResultPayload(result, maxBytes = RESULT_BODY_BUDGET_BYTES, videoStoreCapBytes = SERVER_VIDEO_STORE_CAP_BYTES) {
|
|
330956
|
+
const oversizedVideo = typeof result.video === "string" && result.video.length > videoStoreCapBytes;
|
|
330957
|
+
let bytes = serializedBytes(result);
|
|
330958
|
+
if (bytes <= maxBytes && !oversizedVideo) {
|
|
330959
|
+
return { payload: result, shed: [], bytes };
|
|
330960
|
+
}
|
|
330961
|
+
const shed = [];
|
|
330962
|
+
let payload = result;
|
|
330963
|
+
const drop = (fields, label) => {
|
|
330964
|
+
const next = { ...payload };
|
|
330965
|
+
for (const field of fields) next[field] = void 0;
|
|
330966
|
+
payload = next;
|
|
330967
|
+
shed.push(label);
|
|
330968
|
+
bytes = serializedBytes(payload);
|
|
330969
|
+
};
|
|
330970
|
+
if ((bytes > maxBytes || oversizedVideo) && payload.video != null) {
|
|
330971
|
+
drop(["video", "videoMimeType"], "video");
|
|
330972
|
+
}
|
|
330973
|
+
if (bytes > maxBytes && Array.isArray(payload.screenshots) && payload.screenshots.length > 0) {
|
|
330974
|
+
const shots = [...payload.screenshots];
|
|
330975
|
+
const bySizeDesc = shots.map((shot, i) => ({ i, len: typeof shot === "string" ? shot.length : 0 })).sort((a, b) => b.len - a.len);
|
|
330976
|
+
const overshoot = bytes - maxBytes;
|
|
330977
|
+
let shaved = 0;
|
|
330978
|
+
let blanked = 0;
|
|
330979
|
+
for (const { i, len } of bySizeDesc) {
|
|
330980
|
+
if (shaved >= overshoot || len === 0) break;
|
|
330981
|
+
if (shots[i] === "") continue;
|
|
330982
|
+
shots[i] = "";
|
|
330983
|
+
shaved += len;
|
|
330984
|
+
blanked++;
|
|
330985
|
+
}
|
|
330986
|
+
if (blanked > 0) {
|
|
330987
|
+
payload = { ...payload, screenshots: shots };
|
|
330988
|
+
bytes = serializedBytes(payload);
|
|
330989
|
+
shed.push(blanked === shots.length ? "screenshots" : `${blanked}/${shots.length} screenshots`);
|
|
330990
|
+
}
|
|
330991
|
+
}
|
|
330992
|
+
if (bytes > maxBytes && payload.harApiCalls != null) drop(["harApiCalls"], "harApiCalls");
|
|
330993
|
+
if (bytes > maxBytes && payload.apiCalls != null) drop(["apiCalls"], "apiCalls");
|
|
330994
|
+
if (shed.length > 0) {
|
|
330995
|
+
const note = `
|
|
330996
|
+
|
|
330997
|
+
\u26A0 [runner] Result media trimmed before upload \u2014 payload exceeded the ${Math.round(maxBytes / (1024 * 1024))}MB server limit; dropped: ${shed.join(", ")}.`;
|
|
330998
|
+
payload = {
|
|
330999
|
+
...payload,
|
|
331000
|
+
logs: (typeof payload.logs === "string" ? payload.logs : "") + note
|
|
331001
|
+
};
|
|
331002
|
+
bytes = serializedBytes(payload);
|
|
331003
|
+
}
|
|
331004
|
+
if (bytes > maxBytes && typeof payload.logs === "string" && payload.logs.length > 4096) {
|
|
331005
|
+
const over = bytes - maxBytes;
|
|
331006
|
+
const targetLen = Math.max(2048, payload.logs.length - over - 1024);
|
|
331007
|
+
if (targetLen < payload.logs.length) {
|
|
331008
|
+
const half = Math.floor(targetLen / 2);
|
|
331009
|
+
const head = payload.logs.slice(0, half);
|
|
331010
|
+
const tail = payload.logs.slice(payload.logs.length - half);
|
|
331011
|
+
payload = { ...payload, logs: `${head}
|
|
331012
|
+
\u2026 [logs truncated to fit upload] \u2026
|
|
331013
|
+
${tail}` };
|
|
331014
|
+
if (!shed.includes("logs (truncated)")) shed.push("logs (truncated)");
|
|
331015
|
+
bytes = serializedBytes(payload);
|
|
331016
|
+
}
|
|
331017
|
+
}
|
|
331018
|
+
return { payload, shed, bytes };
|
|
331019
|
+
}
|
|
331020
|
+
|
|
330906
331021
|
// src/runner.ts
|
|
330907
331022
|
init_dist();
|
|
330908
331023
|
var import_runner_core12 = __toESM(require_dist2());
|
|
@@ -331071,15 +331186,33 @@ async function claimRun(serverUrl, headers, runId) {
|
|
|
331071
331186
|
async function postResult(serverUrl, headers, runId, result) {
|
|
331072
331187
|
const MAX_ATTEMPTS = 3;
|
|
331073
331188
|
let lastError;
|
|
331189
|
+
const budgeted = budgetResultPayload(result);
|
|
331190
|
+
if (budgeted.shed.length > 0) {
|
|
331191
|
+
console.warn(
|
|
331192
|
+
`[runner] Result payload for run ${runId} exceeded the upload budget (${Math.round(budgeted.bytes / (1024 * 1024))}MB after trim); dropped: ${budgeted.shed.join(", ")}.`
|
|
331193
|
+
);
|
|
331194
|
+
}
|
|
331195
|
+
let payload = budgeted.payload;
|
|
331074
331196
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
331075
331197
|
try {
|
|
331076
331198
|
const res = await fetch(`${serverUrl}/api/runner/runs/${runId}/result`, {
|
|
331077
331199
|
method: "POST",
|
|
331078
331200
|
headers,
|
|
331079
|
-
body: JSON.stringify(
|
|
331201
|
+
body: JSON.stringify(payload),
|
|
331080
331202
|
signal: AbortSignal.timeout(6e4)
|
|
331081
331203
|
});
|
|
331082
331204
|
if (res.ok) return;
|
|
331205
|
+
if (res.status === 413) {
|
|
331206
|
+
const minimal = budgetResultPayload(payload, 0);
|
|
331207
|
+
if (minimal.shed.length > 0) {
|
|
331208
|
+
console.warn(
|
|
331209
|
+
`[runner] Server rejected result for run ${runId} as too large (413); retrying with a verdict-only payload (dropped: ${minimal.shed.join(", ")}).`
|
|
331210
|
+
);
|
|
331211
|
+
payload = minimal.payload;
|
|
331212
|
+
lastError = new Error("HTTP 413 (payload trimmed, retrying)");
|
|
331213
|
+
continue;
|
|
331214
|
+
}
|
|
331215
|
+
}
|
|
331083
331216
|
lastError = new Error(`HTTP ${res.status}`);
|
|
331084
331217
|
} catch (err) {
|
|
331085
331218
|
lastError = err instanceof Error ? err : new Error(String(err));
|
|
@@ -332614,9 +332747,10 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
332614
332747
|
onPlanReady
|
|
332615
332748
|
});
|
|
332616
332749
|
} finally {
|
|
332617
|
-
|
|
332750
|
+
const activeCapture = capture;
|
|
332751
|
+
if (activeCapture) {
|
|
332618
332752
|
try {
|
|
332619
|
-
const apiCalls = await
|
|
332753
|
+
const apiCalls = await activeCapture.stop();
|
|
332620
332754
|
if (discoveryResult) {
|
|
332621
332755
|
discoveryResult.apiCalls = apiCalls.length ? attachMobileDiscoveryNetworkContext(apiCalls, discoveryResult, session?.appId ?? mobileTarget.appId) : void 0;
|
|
332622
332756
|
}
|
package/package.json
CHANGED