@validate.qa/runner 1.0.39 → 1.0.41
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 +603 -39
- package/dist/cli.mjs +603 -39
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -270,7 +270,8 @@ var require_ai_provider = __commonJS({
|
|
|
270
270
|
{ id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", provider: "anthropic", description: "Anthropic - fast & intelligent, 200k ctx", contextWindow: 2e5 },
|
|
271
271
|
{ id: "nvidia-nemotron-nano", label: "NVIDIA Nemotron Nano", provider: "nvidia", description: "NVIDIA NIM - Nemotron 30B Nano, reasoning model, 128k ctx", contextWindow: 128e3 },
|
|
272
272
|
{ id: "nvidia-kimi-k2.6", label: "NVIDIA Kimi K2.6", provider: "nvidia", description: "NVIDIA NIM - Moonshot Kimi K2.6, multimodal coding and agentic model, 256k ctx", contextWindow: 256e3 },
|
|
273
|
-
{ id: "deepseek-v4-flash", label: "DeepSeek V4 Flash", provider: "deepseek", description: "DeepSeek - V4 Flash, OpenAI-compatible, 1M ctx", contextWindow: 1e6 }
|
|
273
|
+
{ id: "deepseek-v4-flash", label: "DeepSeek V4 Flash", provider: "deepseek", description: "DeepSeek - V4 Flash, OpenAI-compatible, 1M ctx", contextWindow: 1e6 },
|
|
274
|
+
{ id: "deepseek-v4-pro", label: "DeepSeek V4 Pro", provider: "deepseek", description: "DeepSeek - V4 Pro, higher-quality reasoning tier, OpenAI-compatible, 1M ctx", contextWindow: 1e6 }
|
|
274
275
|
];
|
|
275
276
|
var PROVIDER_DEFAULT_MODEL = {
|
|
276
277
|
openai: "gpt-4o",
|
|
@@ -330,7 +331,8 @@ var require_ai_provider = __commonJS({
|
|
|
330
331
|
"claude-sonnet-4-6": { strong: "claude-sonnet-4-6", fast: "claude-sonnet-4-6", agent: "claude-sonnet-4-6" },
|
|
331
332
|
"nvidia-nemotron-nano": { strong: "nvidia/nemotron-3-nano-30b-a3b", fast: "nvidia/nemotron-3-nano-30b-a3b", agent: "nvidia/nemotron-3-nano-30b-a3b" },
|
|
332
333
|
"nvidia-kimi-k2.6": { strong: "moonshotai/kimi-k2.6", fast: "moonshotai/kimi-k2.6", agent: "moonshotai/kimi-k2.6" },
|
|
333
|
-
"deepseek-v4-flash": { strong: "deepseek-v4-flash", fast: "deepseek-v4-flash", agent: "deepseek-v4-flash" }
|
|
334
|
+
"deepseek-v4-flash": { strong: "deepseek-v4-flash", fast: "deepseek-v4-flash", agent: "deepseek-v4-flash" },
|
|
335
|
+
"deepseek-v4-pro": { strong: "deepseek-v4-pro", fast: "deepseek-v4-pro", agent: "deepseek-v4-pro" }
|
|
334
336
|
};
|
|
335
337
|
var _requestModelStorage = new async_hooks_1.AsyncLocalStorage();
|
|
336
338
|
function withModelContext(modelId, fn) {
|
|
@@ -392,7 +394,7 @@ var require_ai_provider = __commonJS({
|
|
|
392
394
|
if (modelId === "nvidia/nemotron-3-nano-30b-a3b") {
|
|
393
395
|
return { chat_template_kwargs: { enable_thinking: false } };
|
|
394
396
|
}
|
|
395
|
-
if (modelId === "deepseek-v4-flash") {
|
|
397
|
+
if (modelId === "deepseek-v4-flash" || modelId === "deepseek-v4-pro") {
|
|
396
398
|
return { thinking: { type: "disabled" } };
|
|
397
399
|
}
|
|
398
400
|
return {};
|
|
@@ -1784,6 +1786,83 @@ var init_constants = __esm({
|
|
|
1784
1786
|
}
|
|
1785
1787
|
});
|
|
1786
1788
|
|
|
1789
|
+
// ../shared/dist/project-key.js
|
|
1790
|
+
function isValidProjectKey(key) {
|
|
1791
|
+
return typeof key === "string" && PROJECT_KEY_PATTERN.test(key) && !RESERVED_PROJECT_KEYS.has(key);
|
|
1792
|
+
}
|
|
1793
|
+
function randomChar(rng) {
|
|
1794
|
+
return KEY_ALPHABET[Math.floor(rng() * KEY_ALPHABET.length)] ?? "0";
|
|
1795
|
+
}
|
|
1796
|
+
function randomKey(length, rng) {
|
|
1797
|
+
let out = "";
|
|
1798
|
+
for (let i = 0; i < length; i++)
|
|
1799
|
+
out += randomChar(rng);
|
|
1800
|
+
return out;
|
|
1801
|
+
}
|
|
1802
|
+
function alnumLower(name) {
|
|
1803
|
+
return (name ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1804
|
+
}
|
|
1805
|
+
function deriveProjectKey(name, rng = Math.random) {
|
|
1806
|
+
const base2 = alnumLower(name).slice(0, 3);
|
|
1807
|
+
return base2.length >= PROJECT_KEY_MIN_LENGTH ? base2 : randomKey(3, rng);
|
|
1808
|
+
}
|
|
1809
|
+
function generateUniqueProjectKey(name, taken, rng = Math.random) {
|
|
1810
|
+
const isFree = (k) => isValidProjectKey(k) && !taken.has(k);
|
|
1811
|
+
const primary = deriveProjectKey(name, rng);
|
|
1812
|
+
if (isFree(primary))
|
|
1813
|
+
return primary;
|
|
1814
|
+
const prefix2 = alnumLower(name).slice(0, 2);
|
|
1815
|
+
for (const suffixLen of [1, 2]) {
|
|
1816
|
+
for (let attempt = 0; attempt < 40; attempt++) {
|
|
1817
|
+
const stem = prefix2.length >= 1 ? prefix2 : deriveProjectKey(name, rng).slice(0, 2);
|
|
1818
|
+
const candidate = stem + randomKey(suffixLen, rng);
|
|
1819
|
+
if (isFree(candidate))
|
|
1820
|
+
return candidate;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
for (let len = 4; len <= PROJECT_KEY_MAX_LENGTH; len++) {
|
|
1824
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
1825
|
+
const candidate = randomKey(len, rng);
|
|
1826
|
+
if (isFree(candidate))
|
|
1827
|
+
return candidate;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
throw new Error("Unable to generate a unique project key");
|
|
1831
|
+
}
|
|
1832
|
+
var RESERVED_PROJECT_KEYS, PROJECT_KEY_MIN_LENGTH, PROJECT_KEY_MAX_LENGTH, PROJECT_KEY_PATTERN, KEY_ALPHABET;
|
|
1833
|
+
var init_project_key = __esm({
|
|
1834
|
+
"../shared/dist/project-key.js"() {
|
|
1835
|
+
"use strict";
|
|
1836
|
+
RESERVED_PROJECT_KEYS = /* @__PURE__ */ new Set([
|
|
1837
|
+
"tests",
|
|
1838
|
+
"sessions",
|
|
1839
|
+
"settings",
|
|
1840
|
+
"reports",
|
|
1841
|
+
"account",
|
|
1842
|
+
"notifications",
|
|
1843
|
+
"support",
|
|
1844
|
+
"site-map",
|
|
1845
|
+
"api-intelligence",
|
|
1846
|
+
"selector-registry",
|
|
1847
|
+
"project-knowledge",
|
|
1848
|
+
"test-data",
|
|
1849
|
+
"audit",
|
|
1850
|
+
"validate-copilot",
|
|
1851
|
+
"qa-copilot",
|
|
1852
|
+
"insights",
|
|
1853
|
+
"regression",
|
|
1854
|
+
"reviews",
|
|
1855
|
+
"admin",
|
|
1856
|
+
"login",
|
|
1857
|
+
"admin-view"
|
|
1858
|
+
]);
|
|
1859
|
+
PROJECT_KEY_MIN_LENGTH = 2;
|
|
1860
|
+
PROJECT_KEY_MAX_LENGTH = 12;
|
|
1861
|
+
PROJECT_KEY_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,10}[a-z0-9])$/;
|
|
1862
|
+
KEY_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
1863
|
+
}
|
|
1864
|
+
});
|
|
1865
|
+
|
|
1787
1866
|
// ../shared/dist/mobile-deeplink.js
|
|
1788
1867
|
function isSafeDeepLink(value) {
|
|
1789
1868
|
if (typeof value !== "string")
|
|
@@ -3302,6 +3381,7 @@ var init_audit_pricing = __esm({
|
|
|
3302
3381
|
"MiniMax-M2.7-highspeed": { input: 0.6, output: 2.4 },
|
|
3303
3382
|
"MiniMax-M2": { input: 0.3, output: 1.2 },
|
|
3304
3383
|
"deepseek-v4-flash": { input: 0.14, output: 0.28 },
|
|
3384
|
+
"deepseek-v4-pro": { input: 0.42, output: 0.84 },
|
|
3305
3385
|
// Whisper bills per-minute, not per-token (OpenAI + Groq)
|
|
3306
3386
|
"whisper-1": null,
|
|
3307
3387
|
"whisper-large-v3": null,
|
|
@@ -4972,6 +5052,10 @@ __export(dist_exports, {
|
|
|
4972
5052
|
PLAN_DEFINITIONS: () => PLAN_DEFINITIONS,
|
|
4973
5053
|
PLAN_ORDER: () => PLAN_ORDER,
|
|
4974
5054
|
PLATFORM_ADMIN_EMAIL: () => PLATFORM_ADMIN_EMAIL,
|
|
5055
|
+
PROJECT_KEY_MAX_LENGTH: () => PROJECT_KEY_MAX_LENGTH,
|
|
5056
|
+
PROJECT_KEY_MIN_LENGTH: () => PROJECT_KEY_MIN_LENGTH,
|
|
5057
|
+
PROJECT_KEY_PATTERN: () => PROJECT_KEY_PATTERN,
|
|
5058
|
+
RESERVED_PROJECT_KEYS: () => RESERVED_PROJECT_KEYS,
|
|
4975
5059
|
SELECTOR_PRIORITIES: () => SELECTOR_PRIORITIES,
|
|
4976
5060
|
SHARED_PRICING_PLAN_MAP: () => SHARED_PRICING_PLAN_MAP,
|
|
4977
5061
|
STT_MODEL_OPTIONS: () => STT_MODEL_OPTIONS,
|
|
@@ -5005,6 +5089,7 @@ __export(dist_exports, {
|
|
|
5005
5089
|
controlSignature: () => controlSignature,
|
|
5006
5090
|
countInnerTests: () => countInnerTests,
|
|
5007
5091
|
decideHarPosted: () => decideHarPosted,
|
|
5092
|
+
deriveProjectKey: () => deriveProjectKey,
|
|
5008
5093
|
detectCrossOriginHtmlAsJson: () => detectCrossOriginHtmlAsJson,
|
|
5009
5094
|
detectsErrorBannerAssertion: () => detectsErrorBannerAssertion,
|
|
5010
5095
|
effectiveTestCount: () => effectiveTestCount,
|
|
@@ -5021,6 +5106,7 @@ __export(dist_exports, {
|
|
|
5021
5106
|
formatLimit: () => formatLimit,
|
|
5022
5107
|
fromDbPricingPlan: () => fromDbPricingPlan,
|
|
5023
5108
|
generateMobileTestFromSession: () => generateMobileTestFromSession,
|
|
5109
|
+
generateUniqueProjectKey: () => generateUniqueProjectKey,
|
|
5024
5110
|
getDefaultPlanForTier: () => getDefaultPlanForTier,
|
|
5025
5111
|
getDeploymentTierForPlan: () => getDeploymentTierForPlan,
|
|
5026
5112
|
getPlanChangeType: () => getPlanChangeType,
|
|
@@ -5050,6 +5136,7 @@ __export(dist_exports, {
|
|
|
5050
5136
|
isTrialExpired: () => isTrialExpired,
|
|
5051
5137
|
isUIVariant: () => isUIVariant,
|
|
5052
5138
|
isValidMobileAppId: () => isValidMobileAppId,
|
|
5139
|
+
isValidProjectKey: () => isValidProjectKey,
|
|
5053
5140
|
isValidSttProvider: () => isValidSttProvider,
|
|
5054
5141
|
isValidTtsModel: () => isValidTtsModel,
|
|
5055
5142
|
isValidTtsVoiceId: () => isValidTtsVoiceId,
|
|
@@ -5095,6 +5182,7 @@ var init_dist = __esm({
|
|
|
5095
5182
|
"use strict";
|
|
5096
5183
|
init_types();
|
|
5097
5184
|
init_constants();
|
|
5185
|
+
init_project_key();
|
|
5098
5186
|
init_mobile_test_generation();
|
|
5099
5187
|
init_mobile_step_healer();
|
|
5100
5188
|
init_mobile_cross_platform();
|
|
@@ -5879,6 +5967,44 @@ var require_ai_queue = __commonJS({
|
|
|
5879
5967
|
}
|
|
5880
5968
|
});
|
|
5881
5969
|
|
|
5970
|
+
// ../runner-core/dist/services/mobile/mobile-session-keepalive.js
|
|
5971
|
+
var require_mobile_session_keepalive = __commonJS({
|
|
5972
|
+
"../runner-core/dist/services/mobile/mobile-session-keepalive.js"(exports2) {
|
|
5973
|
+
"use strict";
|
|
5974
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
5975
|
+
exports2.MOBILE_KEEPALIVE_INTERVAL_MS = void 0;
|
|
5976
|
+
exports2.computeKeepAliveIntervalMs = computeKeepAliveIntervalMs;
|
|
5977
|
+
exports2.withMobileSessionKeepAlive = withMobileSessionKeepAlive;
|
|
5978
|
+
function computeKeepAliveIntervalMs(keepaliveMsRaw, newCommandTimeoutSecondsRaw) {
|
|
5979
|
+
const explicit = keepaliveMsRaw?.trim() ? Number(keepaliveMsRaw) : NaN;
|
|
5980
|
+
if (Number.isFinite(explicit) && explicit >= 0)
|
|
5981
|
+
return Math.floor(explicit);
|
|
5982
|
+
const timeoutSeconds = Number(newCommandTimeoutSecondsRaw);
|
|
5983
|
+
const reapWindowMs = Number.isFinite(timeoutSeconds) && timeoutSeconds > 0 ? Math.floor(timeoutSeconds) * 1e3 : 6e5;
|
|
5984
|
+
return Math.min(12e4, Math.max(5e3, Math.floor(reapWindowMs / 3)));
|
|
5985
|
+
}
|
|
5986
|
+
exports2.MOBILE_KEEPALIVE_INTERVAL_MS = computeKeepAliveIntervalMs(process.env.VALIDATEQA_MOBILE_KEEPALIVE_MS, process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT);
|
|
5987
|
+
async function withMobileSessionKeepAlive(driver, fn, intervalMs = exports2.MOBILE_KEEPALIVE_INTERVAL_MS) {
|
|
5988
|
+
if (intervalMs <= 0)
|
|
5989
|
+
return fn();
|
|
5990
|
+
const timer = setInterval(() => {
|
|
5991
|
+
try {
|
|
5992
|
+
const probe = driver.keepAlivePing ? driver.keepAlivePing() : driver.windowSize();
|
|
5993
|
+
void probe.catch(() => {
|
|
5994
|
+
});
|
|
5995
|
+
} catch {
|
|
5996
|
+
}
|
|
5997
|
+
}, intervalMs);
|
|
5998
|
+
timer.unref?.();
|
|
5999
|
+
try {
|
|
6000
|
+
return await fn();
|
|
6001
|
+
} finally {
|
|
6002
|
+
clearInterval(timer);
|
|
6003
|
+
}
|
|
6004
|
+
}
|
|
6005
|
+
}
|
|
6006
|
+
});
|
|
6007
|
+
|
|
5882
6008
|
// ../runner-core/dist/services/mobile/mobile-observation.js
|
|
5883
6009
|
var require_mobile_observation = __commonJS({
|
|
5884
6010
|
"../runner-core/dist/services/mobile/mobile-observation.js"(exports2) {
|
|
@@ -64612,6 +64738,7 @@ var require_mobile_explorer = __commonJS({
|
|
|
64612
64738
|
var ai_provider_js_1 = require_ai_provider();
|
|
64613
64739
|
var ai_queue_js_1 = require_ai_queue();
|
|
64614
64740
|
var ai_retry_js_1 = require_ai_retry();
|
|
64741
|
+
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
64615
64742
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
64616
64743
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
64617
64744
|
var mobile_exploration_state_js_1 = require_mobile_exploration_state();
|
|
@@ -66121,7 +66248,7 @@ ${statePacket}` }
|
|
|
66121
66248
|
const requestMessageCount = messages.length;
|
|
66122
66249
|
const requestMessages = messages.slice();
|
|
66123
66250
|
const callStart = Date.now();
|
|
66124
|
-
const response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
66251
|
+
const response = await (0, mobile_session_keepalive_js_1.withMobileSessionKeepAlive)(driver, () => (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
66125
66252
|
arm();
|
|
66126
66253
|
return aiClient.chat.completions.create({
|
|
66127
66254
|
model: agentModel,
|
|
@@ -66134,7 +66261,7 @@ ${statePacket}` }
|
|
|
66134
66261
|
label: `mobile-explore-iter-${iteration}`,
|
|
66135
66262
|
deferTimeoutUntilArmed: true,
|
|
66136
66263
|
onRetry: (attempt, reason, delayMs) => log2(` \u26A0 mobile explore AI retry ${attempt} (${reason}) \u2014 waiting ${delayMs}ms`)
|
|
66137
|
-
});
|
|
66264
|
+
}));
|
|
66138
66265
|
const iterDuration = Date.now() - callStart;
|
|
66139
66266
|
if (!response?.choices?.[0]?.message) {
|
|
66140
66267
|
throw new Error(`AI explore call returned no response (queue timeout or provider overload) at iteration ${iteration}`);
|
|
@@ -68213,6 +68340,7 @@ var require_discover_ai_planner = __commonJS({
|
|
|
68213
68340
|
"use strict";
|
|
68214
68341
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
68215
68342
|
exports2.SCENARIO_SYSTEM_PROMPT = void 0;
|
|
68343
|
+
exports2.getAIPlannerMaxTokens = getAIPlannerMaxTokens;
|
|
68216
68344
|
exports2.getSnapshotConfidence = getSnapshotConfidence;
|
|
68217
68345
|
exports2.compressSiteMap = compressSiteMap;
|
|
68218
68346
|
exports2.formatEvidenceForPlanner = formatEvidenceForPlanner;
|
|
@@ -68224,10 +68352,25 @@ var require_discover_ai_planner = __commonJS({
|
|
|
68224
68352
|
var ai_retry_js_1 = require_ai_retry();
|
|
68225
68353
|
var strip_json_fences_js_1 = require_strip_json_fences();
|
|
68226
68354
|
var scenario_plan_validator_js_1 = require_scenario_plan_validator();
|
|
68227
|
-
var
|
|
68228
|
-
var
|
|
68355
|
+
var GENEROUS_AI_PLANNER_MAX_TOKENS = 65536;
|
|
68356
|
+
var CONSERVATIVE_AI_PLANNER_MAX_TOKENS = 16384;
|
|
68229
68357
|
function getAIPlannerMaxTokens(modelId) {
|
|
68230
|
-
|
|
68358
|
+
switch ((0, ai_provider_js_1.getProviderForModel)(modelId)) {
|
|
68359
|
+
case "deepseek":
|
|
68360
|
+
// V4 output ceiling = 384K — verified safe
|
|
68361
|
+
case "minimax":
|
|
68362
|
+
return GENEROUS_AI_PLANNER_MAX_TOKENS;
|
|
68363
|
+
case "xai":
|
|
68364
|
+
// grok-build-0.1 ceiling unproven (Grok-Fast/3 = 16K) — DON'T risk a 400
|
|
68365
|
+
case "openai":
|
|
68366
|
+
// gpt-4o max output = 16384 → more would 400
|
|
68367
|
+
case "anthropic":
|
|
68368
|
+
// OpenAI-compat proxy — keep conservative
|
|
68369
|
+
case "nvidia":
|
|
68370
|
+
// NIM output limits vary — keep conservative
|
|
68371
|
+
default:
|
|
68372
|
+
return CONSERVATIVE_AI_PLANNER_MAX_TOKENS;
|
|
68373
|
+
}
|
|
68231
68374
|
}
|
|
68232
68375
|
var discover_planner_js_1 = require_discover_planner();
|
|
68233
68376
|
function getSnapshotConfidence(sourceKind) {
|
|
@@ -68779,7 +68922,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
68779
68922
|
4. **Multi-page journeys** - only when transitions were actually observed
|
|
68780
68923
|
5. **Bug reports** - for observed 500 errors, crashes, or broken features
|
|
68781
68924
|
6. **Steps are human-readable** - "Fill the email field", not "page.getByLabel('Email').fill()"
|
|
68782
|
-
7. **Expected outcomes are plain-English success criteria** - describe the user-visible product result, not the controls touched along the way. Steps already cover buttons, inputs, links, fields, headings, tabs, dialogs, and menus. Expected outcomes should say what the user can now do or rely on (record saved, message sent, search results filtered, validation explained, dashboard accessible). Include durable routes or exact copy only when they are the real product outcome; do NOT list "heading visible", "button visible", "search input visible", or similar element inventory unless that exact control is the behavior under test.
|
|
68925
|
+
7. **Expected outcomes are plain-English success criteria** - describe the user-visible product result, not the controls touched along the way. Steps already cover buttons, inputs, links, fields, headings, tabs, dialogs, and menus. Expected outcomes should say what the user can now do or rely on (record saved, message sent, search results filtered, validation explained, dashboard accessible). Include durable routes or exact copy only when they are the real product outcome; do NOT list "heading visible", "button visible", "search input visible", or similar element inventory unless that exact control is the behavior under test. **Outcome-strength self-check (applies to EVERY scenario type):** before emitting a scenario, ask \u2014 would every expectedOutcome still pass if the primary action did nothing and only the page and its existing chrome rendered? If yes, it is too weak. Add at least one evidence-backed post-condition that would FAIL in that no-op case: specific result content or value, a created or removed entity, a changed count or status, persisted state after reload, handled validation copy, or a concrete session signal. A route alone suffices only when navigation or redirect is itself the behavior under test; a newly revealed control suffices only when revealing that control is the behavior under test. Never invent copy to satisfy this check \u2014 Rule 1 still governs what you may assert.
|
|
68783
68926
|
8. **Auth-aware** - mark scenarios that need login. Use process.env.TEST_USER_EMAIL / PASSWORD
|
|
68784
68927
|
9. **No duplicates** - check existing test names before adding
|
|
68785
68928
|
10. **All tests start from the landing page** - every generated test navigates to \`/\` first and uses the app's own navigation (clicking links/buttons) to reach the target page. Steps must reflect this: say "Navigate to landing page and click Sign In" not "Navigate to /auth/login". Deep-linking to internal routes causes hydration failures in SPAs. \`targetPages\` lists the pages involved in the flow.
|
|
@@ -68831,7 +68974,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
68831
68974
|
**YOUR responsibility:**
|
|
68832
68975
|
- Scenario \`name\`, \`type\`, \`goal\`, \`prerequisites\`
|
|
68833
68976
|
- \`steps\` written as user-language intent ("create a new conversation", "click the Submit button to log in"), naming observed UI labels where they help describe intent
|
|
68834
|
-
- \`expectedOutcomes\`
|
|
68977
|
+
- \`expectedOutcomes\` expressing the evidence-backed product post-condition per Rule 7 (and Rule 11 for happy paths, plus Rule 12's concrete session signal for authentication happy paths); citing observed durable labels when asserting on stable signals is encouraged
|
|
68835
68978
|
|
|
68836
68979
|
**NOT your responsibility:**
|
|
68837
68980
|
- Exact Playwright selectors (\`getByRole(...)\`, \`getByText(...)\`, \`page.locator(...)\`, etc.) \u2014 live-generate picks locators against the live snapshot
|
|
@@ -68852,7 +68995,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
68852
68995
|
- GOOD specificity: "create a new conversation by sending a message to a connection established earlier in this scenario"
|
|
68853
68996
|
- BAD specificity: "click the button labeled exactly 'Send' in the bottom-right of the chat"
|
|
68854
68997
|
|
|
68855
|
-
Citing observed labels in \`expectedOutcomes\`
|
|
68998
|
+
Citing observed labels in \`expectedOutcomes\` is fine \u2014 that's asserting on stable product signals, not implementation specifics.
|
|
68856
68999
|
|
|
68857
69000
|
## Coverage Strategy
|
|
68858
69001
|
- For every form: at least 1 HAPPY_PATH (submit valid data AND verify durable result per rule 11) + 1 EDGE_CASE (empty/invalid submission)
|
|
@@ -69438,6 +69581,24 @@ Each entry is exactly one of:
|
|
|
69438
69581
|
}
|
|
69439
69582
|
async function runAIPlanner(siteMap, evidence, context, options) {
|
|
69440
69583
|
const logs2 = [];
|
|
69584
|
+
const failurePlan = (kind, reason, legacyFallback2) => {
|
|
69585
|
+
const noFallback = context.mode === "survey" && process.env.DISCOVERY_PLANNER_NO_FALLBACK === "true";
|
|
69586
|
+
if (noFallback) {
|
|
69587
|
+
logs2.push(`Planner ${kind} \u2014 NO-FALLBACK: failing survey cleanly instead of fabricating slug features (${reason})`);
|
|
69588
|
+
return {
|
|
69589
|
+
scenarios: [],
|
|
69590
|
+
featureGroups: [],
|
|
69591
|
+
reasoning: "",
|
|
69592
|
+
logs: logs2,
|
|
69593
|
+
coverageGaps: [],
|
|
69594
|
+
plannerFailed: true,
|
|
69595
|
+
plannerFailureKind: kind,
|
|
69596
|
+
fallbackReason: reason
|
|
69597
|
+
};
|
|
69598
|
+
}
|
|
69599
|
+
return { ...legacyFallback2(), fallbackReason: reason };
|
|
69600
|
+
};
|
|
69601
|
+
let extraPlannerCallUsed = false;
|
|
69441
69602
|
const compressed = compressSiteMap(siteMap);
|
|
69442
69603
|
const agentModel = options.modelOverride ? (0, ai_provider_js_1.resolveModelForTask)(options.modelOverride, "agent") : (0, ai_provider_js_1.getModel)("agent");
|
|
69443
69604
|
const plannerMaxTokens = getAIPlannerMaxTokens(agentModel);
|
|
@@ -69473,7 +69634,7 @@ Each entry is exactly one of:
|
|
|
69473
69634
|
} catch (err) {
|
|
69474
69635
|
const message = err instanceof Error ? err.message : String(err);
|
|
69475
69636
|
logs2.push(`AI planner failed after retries: ${message}`);
|
|
69476
|
-
return
|
|
69637
|
+
return failurePlan("transport", (0, ai_retry_js_1.describeAIFailure)(err), () => buildFallbackPlan(siteMap, evidence.recordedJourneys, context.mode, logs2, context.existingTestNames ?? [], context.focusAreas, context.testBudget));
|
|
69477
69638
|
}
|
|
69478
69639
|
try {
|
|
69479
69640
|
options.onAICall?.({
|
|
@@ -69492,20 +69653,104 @@ Each entry is exactly one of:
|
|
|
69492
69653
|
});
|
|
69493
69654
|
} catch {
|
|
69494
69655
|
}
|
|
69495
|
-
const
|
|
69496
|
-
|
|
69656
|
+
const choice = response?.choices?.[0];
|
|
69657
|
+
const raw = choice?.message?.content;
|
|
69658
|
+
const finishReason = (choice?.finish_reason ?? "").toString().toLowerCase();
|
|
69659
|
+
const completionTokens = response.usage?.completion_tokens;
|
|
69660
|
+
const TRUNCATION_FINISH = /* @__PURE__ */ new Set(["length", "max_tokens", "max_output_tokens", "token_limit"]);
|
|
69661
|
+
const definitelyTruncated = TRUNCATION_FINISH.has(finishReason);
|
|
69662
|
+
const atOrOverCap = completionTokens != null && completionTokens >= plannerMaxTokens;
|
|
69663
|
+
const blocked = finishReason === "content_filter" || finishReason === "tool_calls";
|
|
69664
|
+
const usage = response.usage;
|
|
69665
|
+
logs2.push(`AI planner: ${usage?.prompt_tokens ?? "?"} input, ${usage?.completion_tokens ?? "?"} output tokens (finish=${finishReason || "?"})`);
|
|
69666
|
+
const legacyFallback = () => buildFallbackPlan(siteMap, evidence.recordedJourneys, context.mode, logs2, context.existingTestNames ?? [], context.focusAreas, context.testBudget);
|
|
69667
|
+
if (blocked) {
|
|
69668
|
+
return failurePlan("blocked", "AI response was blocked (content_filter/tool_calls)", legacyFallback);
|
|
69669
|
+
}
|
|
69670
|
+
if (definitelyTruncated) {
|
|
69671
|
+
return failurePlan("truncated", "AI planner output truncated at max_tokens (finish_reason=length)", legacyFallback);
|
|
69672
|
+
}
|
|
69673
|
+
const trimmed = raw?.trim();
|
|
69674
|
+
if (!trimmed) {
|
|
69497
69675
|
logs2.push("AI planner returned empty response");
|
|
69498
|
-
return
|
|
69676
|
+
return atOrOverCap ? failurePlan("truncated", "AI planner output truncated at max_tokens (empty body at cap)", legacyFallback) : failurePlan("empty", "AI returned an empty response", legacyFallback);
|
|
69499
69677
|
}
|
|
69500
|
-
const usage = response.usage;
|
|
69501
|
-
logs2.push(`AI planner: ${usage?.prompt_tokens ?? "?"} input, ${usage?.completion_tokens ?? "?"} output tokens`);
|
|
69502
69678
|
let aiOutput;
|
|
69503
69679
|
try {
|
|
69504
|
-
aiOutput = JSON.parse((0, strip_json_fences_js_1.stripJsonFences)(
|
|
69680
|
+
aiOutput = JSON.parse((0, strip_json_fences_js_1.stripJsonFences)(trimmed));
|
|
69505
69681
|
} catch (err) {
|
|
69506
69682
|
const message = err instanceof Error ? err.message : String(err);
|
|
69507
69683
|
logs2.push(`AI planner returned invalid JSON: ${message}`);
|
|
69508
|
-
|
|
69684
|
+
if (atOrOverCap) {
|
|
69685
|
+
logs2.push("AI planner output at/over the token cap and unparseable \u2014 treating as truncated (skipping repair)");
|
|
69686
|
+
return failurePlan("truncated", "AI planner output truncated at max_tokens (unparseable at cap)", legacyFallback);
|
|
69687
|
+
}
|
|
69688
|
+
const repairPrompt = `Your previous response was not valid JSON (parser error: ${message}). Return ONLY the corrected JSON object \u2014 no prose, no markdown fences \u2014 matching the exact schema of your previous attempt.`;
|
|
69689
|
+
const repairStart = Date.now();
|
|
69690
|
+
let repairResponse = null;
|
|
69691
|
+
try {
|
|
69692
|
+
extraPlannerCallUsed = true;
|
|
69693
|
+
repairResponse = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
69694
|
+
arm();
|
|
69695
|
+
return aiClient.chat.completions.create({
|
|
69696
|
+
model: agentModel,
|
|
69697
|
+
messages: [
|
|
69698
|
+
...messages,
|
|
69699
|
+
{ role: "assistant", content: raw },
|
|
69700
|
+
{ role: "user", content: repairPrompt }
|
|
69701
|
+
],
|
|
69702
|
+
max_tokens: plannerMaxTokens,
|
|
69703
|
+
temperature: 0.3,
|
|
69704
|
+
...(0, ai_provider_js_1.getJsonObjectResponseFormat)(agentModel),
|
|
69705
|
+
...(0, ai_provider_js_1.getNoThinkingRequestOptions)(agentModel)
|
|
69706
|
+
}, { signal });
|
|
69707
|
+
}), {
|
|
69708
|
+
label: "ai-planner-repair",
|
|
69709
|
+
deferTimeoutUntilArmed: true,
|
|
69710
|
+
onRetry: (attempt, reason, delay) => logs2.push(`AI planner repair call failed (${reason}) \u2014 retry ${attempt} in ${Math.round(delay / 1e3)}s`)
|
|
69711
|
+
});
|
|
69712
|
+
} catch (repairErr) {
|
|
69713
|
+
const repairMessage = repairErr instanceof Error ? repairErr.message : String(repairErr);
|
|
69714
|
+
logs2.push(`AI planner repair failed (${repairMessage})`);
|
|
69715
|
+
}
|
|
69716
|
+
if (repairResponse) {
|
|
69717
|
+
try {
|
|
69718
|
+
options.onAICall?.({
|
|
69719
|
+
iteration: 1,
|
|
69720
|
+
groupId: `ai-planner-${context.projectId.slice(0, 8)}`,
|
|
69721
|
+
callType: "discoveryAIPlannerRepair",
|
|
69722
|
+
model: agentModel,
|
|
69723
|
+
provider: (0, ai_provider_js_1.getProviderForModel)(agentModel),
|
|
69724
|
+
durationMs: Date.now() - repairStart,
|
|
69725
|
+
promptTokens: repairResponse.usage?.prompt_tokens,
|
|
69726
|
+
completionTokens: repairResponse.usage?.completion_tokens,
|
|
69727
|
+
totalTokens: repairResponse.usage?.total_tokens,
|
|
69728
|
+
assistantMessage: repairResponse.choices[0]?.message?.content,
|
|
69729
|
+
requestMessageCount: messages.length + 2,
|
|
69730
|
+
requestMessages: [
|
|
69731
|
+
...messages,
|
|
69732
|
+
{ role: "assistant", content: raw },
|
|
69733
|
+
{ role: "user", content: repairPrompt }
|
|
69734
|
+
]
|
|
69735
|
+
});
|
|
69736
|
+
} catch {
|
|
69737
|
+
}
|
|
69738
|
+
const repairedRaw = repairResponse.choices[0]?.message?.content?.trim();
|
|
69739
|
+
if (repairedRaw) {
|
|
69740
|
+
try {
|
|
69741
|
+
aiOutput = JSON.parse((0, strip_json_fences_js_1.stripJsonFences)(repairedRaw));
|
|
69742
|
+
} catch (repairParseErr) {
|
|
69743
|
+
const repairMessage = repairParseErr instanceof Error ? repairParseErr.message : String(repairParseErr);
|
|
69744
|
+
logs2.push(`AI planner repair returned invalid JSON (${repairMessage})`);
|
|
69745
|
+
return failurePlan("malformed", "AI returned unreadable output", legacyFallback);
|
|
69746
|
+
}
|
|
69747
|
+
} else {
|
|
69748
|
+
logs2.push("AI planner repair returned empty response");
|
|
69749
|
+
return failurePlan("malformed", "AI returned unreadable output", legacyFallback);
|
|
69750
|
+
}
|
|
69751
|
+
} else {
|
|
69752
|
+
return failurePlan("malformed", "AI returned unreadable output", legacyFallback);
|
|
69753
|
+
}
|
|
69509
69754
|
}
|
|
69510
69755
|
let { scenarios, featureGroups, warnings, coverageGaps } = validateAIPlannerOutput(aiOutput, siteMap, context.existingTestNames, context.testBudget, evidence.recordedJourneys, context.mode, context.focusAreas);
|
|
69511
69756
|
logs2.push(`AI planner: ${scenarios.length} scenarios, ${featureGroups.length} feature groups (${warnings.length} warnings, ${coverageGaps.length} coverage gaps)`);
|
|
@@ -69515,7 +69760,7 @@ Each entry is exactly one of:
|
|
|
69515
69760
|
let topLevelWarning;
|
|
69516
69761
|
const totalV1 = scenarios.length + coverageGaps.length;
|
|
69517
69762
|
const rejectionRateV1 = totalV1 > 0 ? coverageGaps.length / totalV1 : 0;
|
|
69518
|
-
if (rejectionRateV1 > 0.5 && totalV1 >= 3) {
|
|
69763
|
+
if (rejectionRateV1 > 0.5 && totalV1 >= 3 && !extraPlannerCallUsed) {
|
|
69519
69764
|
logs2.push(`AI planner: ${Math.round(rejectionRateV1 * 100)}% rejection rate (${coverageGaps.length}/${totalV1}) \u2014 running self-correction retry`);
|
|
69520
69765
|
const retryContext = [
|
|
69521
69766
|
`Your previous emission produced ${coverageGaps.length} coverage gaps out of ${totalV1} scenarios (${Math.round(rejectionRateV1 * 100)}% rejection rate).`,
|
|
@@ -69534,6 +69779,7 @@ Each entry is exactly one of:
|
|
|
69534
69779
|
const retryStart = Date.now();
|
|
69535
69780
|
let retryResponse = null;
|
|
69536
69781
|
try {
|
|
69782
|
+
extraPlannerCallUsed = true;
|
|
69537
69783
|
retryResponse = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
69538
69784
|
arm();
|
|
69539
69785
|
return aiClient.chat.completions.create({
|
|
@@ -69633,6 +69879,22 @@ Each entry is exactly one of:
|
|
|
69633
69879
|
};
|
|
69634
69880
|
}
|
|
69635
69881
|
if (context.mode === "survey" && featureGroups.length === 0 && siteMap.pages.length > 0) {
|
|
69882
|
+
const hasGroupWorthyEvidence = siteMap.pages.some((p) => (p.forms?.length ?? 0) > 0 || (p.elements?.length ?? 0) > 0);
|
|
69883
|
+
const noFallback = process.env.DISCOVERY_PLANNER_NO_FALLBACK === "true";
|
|
69884
|
+
if (noFallback && !hasGroupWorthyEvidence) {
|
|
69885
|
+
return {
|
|
69886
|
+
scenarios,
|
|
69887
|
+
featureGroups: [],
|
|
69888
|
+
reasoning: aiOutput.reasoning ?? "",
|
|
69889
|
+
logs: logs2,
|
|
69890
|
+
coverageGaps,
|
|
69891
|
+
warning: topLevelWarning,
|
|
69892
|
+
fallbackUsed: false
|
|
69893
|
+
};
|
|
69894
|
+
}
|
|
69895
|
+
if (noFallback) {
|
|
69896
|
+
return failurePlan("empty_groups", "AI produced no feature groups despite discovered content", () => buildFallbackPlan(siteMap, evidence.recordedJourneys, context.mode, logs2, context.existingTestNames ?? [], context.focusAreas, context.testBudget));
|
|
69897
|
+
}
|
|
69636
69898
|
logs2.push("Survey AI returned 0 feature groups - falling back to deterministic grouping");
|
|
69637
69899
|
const fallback = buildFallbackPlan(siteMap, evidence.recordedJourneys, context.mode, logs2, context.existingTestNames ?? [], context.focusAreas, context.testBudget);
|
|
69638
69900
|
return {
|
|
@@ -70843,6 +71105,7 @@ var require_mobile_generator = __commonJS({
|
|
|
70843
71105
|
var ai_provider_js_1 = require_ai_provider();
|
|
70844
71106
|
var ai_queue_js_1 = require_ai_queue();
|
|
70845
71107
|
var ai_retry_js_1 = require_ai_retry();
|
|
71108
|
+
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
70846
71109
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
70847
71110
|
var mobile_source_parser_js_2 = require_mobile_source_parser();
|
|
70848
71111
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
@@ -71750,14 +72013,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
71750
72013
|
const requestMessageCount = messages.length;
|
|
71751
72014
|
const requestMessages = messages.slice();
|
|
71752
72015
|
const callStart = Date.now();
|
|
71753
|
-
const response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
72016
|
+
const response = await (0, mobile_session_keepalive_js_1.withMobileSessionKeepAlive)(driver, () => (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
71754
72017
|
arm();
|
|
71755
72018
|
return aiClient.chat.completions.create({ model: agentModel, messages, tools: GENERATE_TOOLS, tool_choice: "auto", max_tokens: 8e3 }, { signal });
|
|
71756
72019
|
}), {
|
|
71757
72020
|
label: `mobile-generate-${scenario.name.slice(0, 24)}-iter-${iteration}`,
|
|
71758
72021
|
deferTimeoutUntilArmed: true,
|
|
71759
72022
|
onRetry: (attempt, reason, delayMs) => log2(` \u26A0 mobile generate AI retry ${attempt} (${reason}) \u2014 waiting ${delayMs}ms`)
|
|
71760
|
-
});
|
|
72023
|
+
}));
|
|
71761
72024
|
const iterDuration = Date.now() - callStart;
|
|
71762
72025
|
if (!response?.choices?.[0]?.message) {
|
|
71763
72026
|
throw new Error(`AI generate call returned no response at iteration ${iteration}`);
|
|
@@ -72369,6 +72632,21 @@ ${statePacket}`, redactionCredentials) },
|
|
|
72369
72632
|
};
|
|
72370
72633
|
}
|
|
72371
72634
|
const metadata = metadataForElement(element, screenName(), platform3);
|
|
72635
|
+
const expectedNeedle = expected.trim().toLowerCase();
|
|
72636
|
+
const identityValues = [element.accessibilityId, element.resourceId].map((value) => value?.trim().toLowerCase()).filter((value) => Boolean(value));
|
|
72637
|
+
const visibleText = (element.text ?? "").trim().toLowerCase();
|
|
72638
|
+
const isSelfLabelAssertion = expectedNeedle.length > 0 && identityValues.includes(expectedNeedle) && !visibleText.includes(expectedNeedle);
|
|
72639
|
+
if (isSelfLabelAssertion) {
|
|
72640
|
+
recordAction({ type: "ASSERT_VISIBLE", selector: (0, mobile_source_parser_js_2.preferredLocator)(element), metadata });
|
|
72641
|
+
log2(` assert_text DOWNGRADED to assert_visible (tautological self-label "${expected}"): ${metadata.targetLabel}`);
|
|
72642
|
+
return {
|
|
72643
|
+
ok: true,
|
|
72644
|
+
asserted: "visible",
|
|
72645
|
+
downgraded: true,
|
|
72646
|
+
note: `"${expected}" is the element's own accessibility identity, not product copy it displays \u2014 recorded a visibility assertion instead. To prove the outcome, assert text the screen SHOWS (an error message, a status label, a field value), not the control's own name.`,
|
|
72647
|
+
target: metadata.targetLabel
|
|
72648
|
+
};
|
|
72649
|
+
}
|
|
72372
72650
|
if (hasStableId(element)) {
|
|
72373
72651
|
recordAction({ type: "ASSERT_TEXT", value: expected, selector: (0, mobile_source_parser_js_2.preferredLocator)(element), metadata });
|
|
72374
72652
|
log2(` assert_text: ${metadata.targetLabel} ~ "${expected}"`);
|
|
@@ -73062,7 +73340,13 @@ ${statePacket}`, redactionCredentials) },
|
|
|
73062
73340
|
const strengthReason = coldResetUnavailable ? `Cold app-data reset unavailable${coldResetReason ? ` (${coldResetReason})` : ""} \u2014 generation may not match cold-boot replay. ${strengthVerdict.reason}` : !verified && recording.assertCount === 0 ? "Mobile scenario finished unverified \u2014 the agent never proved the scenario outcome live, so the recording may not demonstrate the goal (e.g. a happy path that never completed). Review before promotion." : strengthVerdict.reason;
|
|
73063
73341
|
const test = {
|
|
73064
73342
|
name: compiled.name,
|
|
73065
|
-
|
|
73343
|
+
// Description priority: the model's live finish description, then the
|
|
73344
|
+
// planner's scenario goal (always a meaningful sentence describing what the
|
|
73345
|
+
// test proves), then the generic compiled placeholder. Without the goal
|
|
73346
|
+
// fallback a scenario whose finish_generate omitted a description shipped
|
|
73347
|
+
// with the contentless "<platform> native flow generated from a recorded
|
|
73348
|
+
// mobile session." string — see mobile-test-generation.ts's compiled default.
|
|
73349
|
+
description: recording.finish?.description?.trim() || scenario.goal?.trim() || compiled.description,
|
|
73066
73350
|
framework: "APPIUM",
|
|
73067
73351
|
appiumCode: compiled.appiumCode,
|
|
73068
73352
|
steps: compiled.steps,
|
|
@@ -73804,6 +74088,7 @@ var require_mobile_heal_loop = __commonJS({
|
|
|
73804
74088
|
var mobile_heal_postprocess_js_1 = require_mobile_heal_postprocess();
|
|
73805
74089
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
73806
74090
|
var selector_registry_context_js_1 = require_selector_registry_context();
|
|
74091
|
+
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
73807
74092
|
exports2.MOBILE_SCRIPT_HEAL_SYSTEM_PROMPT = `You are a QA engineer fixing a failing Appium (mobile UI automation) test. You receive: the test STEPS as JSON, the failure ERROR + step-level results, and the PAGE-SOURCE accessibility tree captured at the moment of failure (this tree is your 'screenshot'), plus optional known-good selector candidates from a registry.
|
|
73808
74093
|
## You MUST try to fix the steps
|
|
73809
74094
|
Reason over steps + error + tree + registry and return working healed steps. Do NOT return needs_device unless you have genuinely exhausted that reasoning (and NEVER before your final attempt).
|
|
@@ -74695,6 +74980,19 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
74695
74980
|
};
|
|
74696
74981
|
}
|
|
74697
74982
|
const maxPasses = envInt2("MOBILE_HEAL_PHASE2_MAX_PASSES") ?? DEFAULT_PHASE2_MAX_PASSES;
|
|
74983
|
+
const keepAlivePingable = {
|
|
74984
|
+
...typeof liveDriver.keepAlivePing === "function" ? { keepAlivePing: liveDriver.keepAlivePing.bind(liveDriver) } : {},
|
|
74985
|
+
// WARNING: this fallback routes through snapshot(), which sits inside
|
|
74986
|
+
// withSessionRecovery on the real driver — a failed tick could consume the
|
|
74987
|
+
// bounded recovery budget, the exact bug keepAlivePing exists to avoid.
|
|
74988
|
+
// Every current MobileLiveDriverLike provides keepAlivePing, so this only
|
|
74989
|
+
// exists for structural compatibility; implement keepAlivePing on any new
|
|
74990
|
+
// facade rather than relying on it.
|
|
74991
|
+
windowSize: async () => {
|
|
74992
|
+
await liveDriver.snapshot();
|
|
74993
|
+
return null;
|
|
74994
|
+
}
|
|
74995
|
+
};
|
|
74698
74996
|
let baseSteps = args.steps;
|
|
74699
74997
|
let priorError = args.priorError ?? null;
|
|
74700
74998
|
let bestSteps;
|
|
@@ -74729,10 +75027,10 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
74729
75027
|
];
|
|
74730
75028
|
let content;
|
|
74731
75029
|
try {
|
|
74732
|
-
const response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
75030
|
+
const response = await (0, mobile_session_keepalive_js_1.withMobileSessionKeepAlive)(keepAlivePingable, () => (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
74733
75031
|
arm();
|
|
74734
75032
|
return aiClient.chat.completions.create({ model, messages, max_tokens: 8e3, temperature: 0.1 }, { signal });
|
|
74735
|
-
}), { label: `mobile-live-heal-pass-${pass}`, deferTimeoutUntilArmed: true });
|
|
75033
|
+
}), { label: `mobile-live-heal-pass-${pass}`, deferTimeoutUntilArmed: true }));
|
|
74736
75034
|
content = response?.choices?.[0]?.message?.content ?? "";
|
|
74737
75035
|
} catch {
|
|
74738
75036
|
continue;
|
|
@@ -75739,6 +76037,118 @@ var require_har_stream = __commonJS({
|
|
|
75739
76037
|
}
|
|
75740
76038
|
});
|
|
75741
76039
|
|
|
76040
|
+
// ../runner-core/dist/services/api-auth-headers.js
|
|
76041
|
+
var require_api_auth_headers = __commonJS({
|
|
76042
|
+
"../runner-core/dist/services/api-auth-headers.js"(exports2) {
|
|
76043
|
+
"use strict";
|
|
76044
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
76045
|
+
exports2.parseApiAuthHeaders = parseApiAuthHeaders;
|
|
76046
|
+
exports2.resolveApiAuthHeaders = resolveApiAuthHeaders;
|
|
76047
|
+
exports2.isAppOriginRequest = isAppOriginRequest;
|
|
76048
|
+
exports2.buildOriginScopedApiFixtures = buildOriginScopedApiFixtures;
|
|
76049
|
+
var HEADER_NAME_RE = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
|
|
76050
|
+
function hasControlChar(v) {
|
|
76051
|
+
for (let i = 0; i < v.length; i++) {
|
|
76052
|
+
const code = v.charCodeAt(i);
|
|
76053
|
+
if (code < 32 || code === 127)
|
|
76054
|
+
return true;
|
|
76055
|
+
}
|
|
76056
|
+
return false;
|
|
76057
|
+
}
|
|
76058
|
+
function parseApiAuthHeaders(raw) {
|
|
76059
|
+
if (!raw || typeof raw !== "string")
|
|
76060
|
+
return void 0;
|
|
76061
|
+
const trimmed = raw.trim();
|
|
76062
|
+
if (!trimmed)
|
|
76063
|
+
return void 0;
|
|
76064
|
+
try {
|
|
76065
|
+
const parsed = JSON.parse(trimmed);
|
|
76066
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
76067
|
+
return void 0;
|
|
76068
|
+
const headers = {};
|
|
76069
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
76070
|
+
if (typeof k === "string" && HEADER_NAME_RE.test(k) && typeof v === "string" && !hasControlChar(v)) {
|
|
76071
|
+
headers[k] = v;
|
|
76072
|
+
}
|
|
76073
|
+
}
|
|
76074
|
+
return Object.keys(headers).length > 0 ? headers : void 0;
|
|
76075
|
+
} catch {
|
|
76076
|
+
return void 0;
|
|
76077
|
+
}
|
|
76078
|
+
}
|
|
76079
|
+
function resolveApiAuthHeaders(input) {
|
|
76080
|
+
if (input.explicit)
|
|
76081
|
+
return input.explicit;
|
|
76082
|
+
if (!input.isApiTest)
|
|
76083
|
+
return void 0;
|
|
76084
|
+
return parseApiAuthHeaders(input.apiAuthHeadersVar);
|
|
76085
|
+
}
|
|
76086
|
+
function isAppOriginRequest(url, baseUrl) {
|
|
76087
|
+
if (typeof url !== "string" || !baseUrl)
|
|
76088
|
+
return false;
|
|
76089
|
+
let appOrigin;
|
|
76090
|
+
try {
|
|
76091
|
+
appOrigin = new URL(baseUrl).origin;
|
|
76092
|
+
} catch {
|
|
76093
|
+
return false;
|
|
76094
|
+
}
|
|
76095
|
+
try {
|
|
76096
|
+
return new URL(url, baseUrl).origin === appOrigin;
|
|
76097
|
+
} catch {
|
|
76098
|
+
return false;
|
|
76099
|
+
}
|
|
76100
|
+
}
|
|
76101
|
+
function buildOriginScopedApiFixtures(headers, baseUrl) {
|
|
76102
|
+
let appOrigin;
|
|
76103
|
+
try {
|
|
76104
|
+
appOrigin = baseUrl ? new URL(baseUrl).origin : null;
|
|
76105
|
+
} catch {
|
|
76106
|
+
appOrigin = null;
|
|
76107
|
+
}
|
|
76108
|
+
return `export * from '@playwright/test';
|
|
76109
|
+
import { test as base } from '@playwright/test';
|
|
76110
|
+
|
|
76111
|
+
// Standing auth headers \u2014 attached ONLY to same-origin requests so the credential never leaks to
|
|
76112
|
+
// an off-origin host. Embedded at run time; this file lives in the ephemeral run dir.
|
|
76113
|
+
const STANDING_HEADERS = ${JSON.stringify(headers)};
|
|
76114
|
+
const APP_ORIGIN = ${JSON.stringify(appOrigin)};
|
|
76115
|
+
const BASE_URL = ${JSON.stringify(baseUrl)};
|
|
76116
|
+
const METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'fetch'];
|
|
76117
|
+
|
|
76118
|
+
function sameOrigin(url) {
|
|
76119
|
+
if (!APP_ORIGIN || typeof url !== 'string') return false;
|
|
76120
|
+
try { return new URL(url, BASE_URL).origin === APP_ORIGIN; } catch { return false; }
|
|
76121
|
+
}
|
|
76122
|
+
|
|
76123
|
+
export const test = base.extend({
|
|
76124
|
+
request: async ({ request }, use) => {
|
|
76125
|
+
const scoped = new Proxy(request, {
|
|
76126
|
+
get(target, prop) {
|
|
76127
|
+
const value = target[prop];
|
|
76128
|
+
// HTTP verbs: wrap to add standing headers on same-origin requests only.
|
|
76129
|
+
if (typeof prop === 'string' && METHODS.indexOf(prop) !== -1) {
|
|
76130
|
+
const orig = value.bind(target);
|
|
76131
|
+
return (url, options) => {
|
|
76132
|
+
options = options || {};
|
|
76133
|
+
return sameOrigin(url)
|
|
76134
|
+
? orig(url, { ...options, headers: { ...STANDING_HEADERS, ...(options.headers || {}) } })
|
|
76135
|
+
: orig(url, options);
|
|
76136
|
+
};
|
|
76137
|
+
}
|
|
76138
|
+
// Every other METHOD must run with 'this' bound to the REAL context \u2014 an APIRequestContext
|
|
76139
|
+
// uses internal slots/private fields a Proxy can't forward, so calling with 'this'=proxy
|
|
76140
|
+
// would throw. Non-function props pass through untouched.
|
|
76141
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
76142
|
+
},
|
|
76143
|
+
});
|
|
76144
|
+
await use(scoped);
|
|
76145
|
+
},
|
|
76146
|
+
});
|
|
76147
|
+
`;
|
|
76148
|
+
}
|
|
76149
|
+
}
|
|
76150
|
+
});
|
|
76151
|
+
|
|
75742
76152
|
// ../runner-core/dist/services/playwright-native.js
|
|
75743
76153
|
var require_playwright_native = __commonJS({
|
|
75744
76154
|
"../runner-core/dist/services/playwright-native.js"(exports2) {
|
|
@@ -75781,7 +76191,7 @@ var require_playwright_native = __commonJS({
|
|
|
75781
76191
|
};
|
|
75782
76192
|
})();
|
|
75783
76193
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
75784
|
-
exports2.scrubHarRequestBody = void 0;
|
|
76194
|
+
exports2.scrubHarRequestBody = exports2.buildOriginScopedApiFixtures = exports2.isAppOriginRequest = exports2.resolveApiAuthHeaders = exports2.parseApiAuthHeaders = void 0;
|
|
75785
76195
|
exports2.executeTestViaNativePlaywright = executeTestViaNativePlaywright2;
|
|
75786
76196
|
exports2.parseJsonResults = parseJsonResults2;
|
|
75787
76197
|
exports2.parseHarToApiSummary = parseHarToApiSummary;
|
|
@@ -75793,6 +76203,20 @@ var require_playwright_native = __commonJS({
|
|
|
75793
76203
|
var spawn_with_tree_kill_js_1 = require_spawn_with_tree_kill();
|
|
75794
76204
|
var api_call_redaction_js_1 = require_api_call_redaction();
|
|
75795
76205
|
var har_stream_js_1 = require_har_stream();
|
|
76206
|
+
var api_auth_headers_js_1 = require_api_auth_headers();
|
|
76207
|
+
var api_auth_headers_js_2 = require_api_auth_headers();
|
|
76208
|
+
Object.defineProperty(exports2, "parseApiAuthHeaders", { enumerable: true, get: function() {
|
|
76209
|
+
return api_auth_headers_js_2.parseApiAuthHeaders;
|
|
76210
|
+
} });
|
|
76211
|
+
Object.defineProperty(exports2, "resolveApiAuthHeaders", { enumerable: true, get: function() {
|
|
76212
|
+
return api_auth_headers_js_2.resolveApiAuthHeaders;
|
|
76213
|
+
} });
|
|
76214
|
+
Object.defineProperty(exports2, "isAppOriginRequest", { enumerable: true, get: function() {
|
|
76215
|
+
return api_auth_headers_js_2.isAppOriginRequest;
|
|
76216
|
+
} });
|
|
76217
|
+
Object.defineProperty(exports2, "buildOriginScopedApiFixtures", { enumerable: true, get: function() {
|
|
76218
|
+
return api_auth_headers_js_2.buildOriginScopedApiFixtures;
|
|
76219
|
+
} });
|
|
75796
76220
|
var api_call_redaction_js_2 = require_api_call_redaction();
|
|
75797
76221
|
Object.defineProperty(exports2, "scrubHarRequestBody", { enumerable: true, get: function() {
|
|
75798
76222
|
return api_call_redaction_js_2.scrubHarRequestBody;
|
|
@@ -75802,7 +76226,7 @@ var require_playwright_native = __commonJS({
|
|
|
75802
76226
|
var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
|
|
75803
76227
|
var PW_CLI = require.resolve("@playwright/test/cli");
|
|
75804
76228
|
async function executeTestViaNativePlaywright2(options) {
|
|
75805
|
-
const { playwrightCode, baseUrl, testName, testVariables, storageStatePath, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
|
|
76229
|
+
const { playwrightCode, baseUrl, testName, testVariables, storageStatePath, extraHttpHeaders, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
|
|
75806
76230
|
const logs2 = [];
|
|
75807
76231
|
const screenshots = [];
|
|
75808
76232
|
logs2.push(`\u{1F3AD} Native Playwright starting`);
|
|
@@ -75816,6 +76240,11 @@ var require_playwright_native = __commonJS({
|
|
|
75816
76240
|
runDir = (0, path_1.join)(RUNS_DIR, (0, crypto_1.randomUUID)());
|
|
75817
76241
|
await (0, promises_1.mkdir)(runDir, { recursive: true });
|
|
75818
76242
|
const isApiTest2 = playwrightCode.includes("@api") || playwrightCode.includes("{ request }") && !playwrightCode.includes("{ page");
|
|
76243
|
+
const resolvedExtraHeaders = (0, api_auth_headers_js_1.resolveApiAuthHeaders)({
|
|
76244
|
+
explicit: extraHttpHeaders,
|
|
76245
|
+
isApiTest: isApiTest2,
|
|
76246
|
+
apiAuthHeadersVar: testVariables?.API_AUTH_HEADERS
|
|
76247
|
+
});
|
|
75819
76248
|
const cssDisableSetup = `
|
|
75820
76249
|
const __qaLiveBrowserStateFile = process.env.VALIDATE_LIVE_BROWSER_STATE_PATH;
|
|
75821
76250
|
let __qaLiveBrowserInterval: ReturnType<typeof setInterval> | null = null;
|
|
@@ -76009,8 +76438,17 @@ test.afterEach(async ({ page }, testInfo) => {
|
|
|
76009
76438
|
if (!importMatch) {
|
|
76010
76439
|
logs2.push("\u26A0 Could not find @playwright/test import - animation disabling NOT injected");
|
|
76011
76440
|
}
|
|
76012
|
-
const
|
|
76441
|
+
const useScopedApiHeaders = isApiTest2 && !!resolvedExtraHeaders && !!importMatch && !!baseUrl;
|
|
76442
|
+
let instrumentedCode;
|
|
76443
|
+
if (isApiTest2) {
|
|
76444
|
+
instrumentedCode = useScopedApiHeaders ? playwrightCode.replace(importMatch[0], importMatch[0].replace(/from\s+['"]@playwright\/test['"]/, "from './qa-api-fixtures'")) : playwrightCode;
|
|
76445
|
+
} else {
|
|
76446
|
+
instrumentedCode = importMatch ? playwrightCode.replace(importMatch[0], importMatch[0] + cssDisableSetup) : playwrightCode;
|
|
76447
|
+
}
|
|
76013
76448
|
await (0, promises_1.writeFile)((0, path_1.join)(runDir, "test.spec.ts"), instrumentedCode);
|
|
76449
|
+
if (useScopedApiHeaders && resolvedExtraHeaders) {
|
|
76450
|
+
await (0, promises_1.writeFile)((0, path_1.join)(runDir, "qa-api-fixtures.ts"), (0, api_auth_headers_js_1.buildOriginScopedApiFixtures)(resolvedExtraHeaders, baseUrl));
|
|
76451
|
+
}
|
|
76014
76452
|
const headless = process.env.PLAYWRIGHT_HEADLESS === "true" || process.env.NODE_ENV === "production" || IS_CLOUD2;
|
|
76015
76453
|
logs2.push(`\u2699\uFE0F Config: baseURL=${JSON.stringify(baseUrl)}, headless=${headless}${isApiTest2 ? ", API mode (no screenshot/video)" : ""}`);
|
|
76016
76454
|
const absTestDir = runDir.replace(/\\/g, "/");
|
|
@@ -76024,6 +76462,7 @@ test.afterEach(async ({ page }, testInfo) => {
|
|
|
76024
76462
|
const absHarPath = wantHar ? (0, path_1.join)(runDir, "session.har").replace(/\\/g, "/") : null;
|
|
76025
76463
|
const recordHarLine = absHarPath ? `recordHar: { path: ${JSON.stringify(absHarPath)}, mode: 'full' },` : "";
|
|
76026
76464
|
const storageStateLine = storageStatePath ? `storageState: ${JSON.stringify(storageStatePath.replace(/\\/g, "/"))},` : "";
|
|
76465
|
+
const extraHeadersLine = resolvedExtraHeaders && !useScopedApiHeaders && Object.keys(resolvedExtraHeaders).length > 0 ? `extraHTTPHeaders: ${JSON.stringify(resolvedExtraHeaders)},` : "";
|
|
76027
76466
|
const launchArgs = IS_CLOUD2 ? "args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-blink-features=AutomationControlled', '--disable-dev-shm-usage', '--disable-gpu', '--disable-software-rasterizer', '--no-zygote', '--disable-accelerated-2d-canvas', '--js-flags=--max-old-space-size=512']," : "args: ['--disable-blink-features=AutomationControlled'],";
|
|
76028
76467
|
const config = `
|
|
76029
76468
|
// Plain object export - no require('@playwright/test') needed.
|
|
@@ -76037,6 +76476,7 @@ module.exports = {
|
|
|
76037
76476
|
use: {
|
|
76038
76477
|
baseURL: ${JSON.stringify(baseUrl)},
|
|
76039
76478
|
${storageStateLine}
|
|
76479
|
+
${extraHeadersLine}
|
|
76040
76480
|
browserName: 'chromium',
|
|
76041
76481
|
headless: ${headless},
|
|
76042
76482
|
screenshot: ${isApiTest2 ? "'off'" : "'on'"},
|
|
@@ -76078,6 +76518,7 @@ module.exports = {
|
|
|
76078
76518
|
// code. Forwarded (not allowlisted) so it stays a per-run toggle; absent → inert.
|
|
76079
76519
|
...process.env.SHARED_AUTH_INJECT ? { SHARED_AUTH_INJECT: process.env.SHARED_AUTH_INJECT } : {}
|
|
76080
76520
|
};
|
|
76521
|
+
delete envWithIsolation.API_AUTH_HEADERS;
|
|
76081
76522
|
logs2.push("\u{1F680} Spawning playwright test...");
|
|
76082
76523
|
const stopLiveBrowserPolling = liveBrowserStatePath && liveBrowserHandle ? startLiveBrowserStatePolling(liveBrowserStatePath, liveBrowserHandle, onLiveBrowserSnapshot) : null;
|
|
76083
76524
|
let playResult;
|
|
@@ -84184,6 +84625,7 @@ var require_discover_generator = __commonJS({
|
|
|
84184
84625
|
var ai_provider_js_1 = require_ai_provider();
|
|
84185
84626
|
var ai_queue_js_1 = require_ai_queue();
|
|
84186
84627
|
var ai_retry_js_1 = require_ai_retry();
|
|
84628
|
+
var discover_ai_planner_js_1 = require_discover_ai_planner();
|
|
84187
84629
|
var snapshot_parser_js_1 = require_snapshot_parser();
|
|
84188
84630
|
var scrub_credentials_js_1 = require_scrub_credentials();
|
|
84189
84631
|
var exploration_parser_js_1 = require_exploration_parser();
|
|
@@ -84391,6 +84833,51 @@ var require_discover_generator = __commonJS({
|
|
|
84391
84833
|
}
|
|
84392
84834
|
return s.length;
|
|
84393
84835
|
}
|
|
84836
|
+
function lineParenDeficit(line) {
|
|
84837
|
+
const stack = [];
|
|
84838
|
+
let regexSkips = 0;
|
|
84839
|
+
for (let i = 0; i < line.length; i++) {
|
|
84840
|
+
const ch = line[i];
|
|
84841
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
84842
|
+
i = skipQuotedLiteral(line, i) - 1;
|
|
84843
|
+
continue;
|
|
84844
|
+
}
|
|
84845
|
+
if (ch === "/" && line[i + 1] === "/") {
|
|
84846
|
+
i = skipLineComment(line, i) - 1;
|
|
84847
|
+
continue;
|
|
84848
|
+
}
|
|
84849
|
+
if (ch === "/" && line[i + 1] === "*") {
|
|
84850
|
+
i = skipBlockComment(line, i) - 1;
|
|
84851
|
+
continue;
|
|
84852
|
+
}
|
|
84853
|
+
if (ch === "/" && isRegexStart(line, i)) {
|
|
84854
|
+
regexSkips++;
|
|
84855
|
+
i = skipRegexLiteral(line, i) - 1;
|
|
84856
|
+
continue;
|
|
84857
|
+
}
|
|
84858
|
+
if (ch === "(" || ch === "{" || ch === "[") {
|
|
84859
|
+
stack.push(ch);
|
|
84860
|
+
continue;
|
|
84861
|
+
}
|
|
84862
|
+
if (ch === ")" || ch === "}" || ch === "]") {
|
|
84863
|
+
if (stack[stack.length - 1] !== CLOSE_TO_OPEN[ch])
|
|
84864
|
+
return { deficit: 0, regexSkips };
|
|
84865
|
+
stack.pop();
|
|
84866
|
+
}
|
|
84867
|
+
}
|
|
84868
|
+
const deficit = stack.length > 0 && stack.every((open) => open === "(") ? stack.length : 0;
|
|
84869
|
+
return { deficit, regexSkips };
|
|
84870
|
+
}
|
|
84871
|
+
function parsesAsAsyncStatement(line) {
|
|
84872
|
+
try {
|
|
84873
|
+
new Function(`return (async () => {
|
|
84874
|
+
${line}
|
|
84875
|
+
})`);
|
|
84876
|
+
return true;
|
|
84877
|
+
} catch {
|
|
84878
|
+
return false;
|
|
84879
|
+
}
|
|
84880
|
+
}
|
|
84394
84881
|
function closeUnclosedRegexLiterals(code) {
|
|
84395
84882
|
const anchors = [
|
|
84396
84883
|
{ rx: /\b(?:waitForURL|toHaveURL)\(\//g, mode: "url" },
|
|
@@ -84429,12 +84916,33 @@ var require_discover_generator = __commonJS({
|
|
|
84429
84916
|
const term = findRegexTerminator(code, slash, TERMINATORS[mode]);
|
|
84430
84917
|
out.push(code.slice(cursor, slash));
|
|
84431
84918
|
const raw = code.slice(slash + 1, term).replace(/\s+$/, "");
|
|
84919
|
+
let closedLiteral;
|
|
84432
84920
|
if (mode === "url") {
|
|
84433
84921
|
const escaped = raw.replace(/(?<!\\)\//g, "\\/");
|
|
84434
84922
|
const finalBody = escaped.startsWith("\\/") ? escaped : `\\/${escaped}`;
|
|
84435
|
-
|
|
84923
|
+
closedLiteral = `/${finalBody}/`;
|
|
84436
84924
|
} else {
|
|
84437
|
-
|
|
84925
|
+
closedLiteral = `/${raw}/`;
|
|
84926
|
+
}
|
|
84927
|
+
out.push(closedLiteral);
|
|
84928
|
+
if (code[term] === ")") {
|
|
84929
|
+
const lineStart = code.lastIndexOf("\n", slash) + 1;
|
|
84930
|
+
const nextNewline = code.indexOf("\n", term);
|
|
84931
|
+
const lineEnd = nextNewline === -1 ? code.length : nextNewline;
|
|
84932
|
+
const tail = code.slice(term, lineEnd);
|
|
84933
|
+
const physicalLine = code.slice(lineStart, lineEnd);
|
|
84934
|
+
const lineIsPlain = !physicalLine.includes("`") && !physicalLine.includes("/*") && !physicalLine.includes("*/");
|
|
84935
|
+
const soleAnchorOnLine = !hits.some((h) => h.slash !== slash && h.slash >= lineStart && h.slash < lineEnd);
|
|
84936
|
+
if (lineIsPlain && soleAnchorOnLine && /;\s*$/.test(tail)) {
|
|
84937
|
+
const repairedLine = code.slice(lineStart, slash) + "/x/" + tail;
|
|
84938
|
+
const { deficit, regexSkips } = lineParenDeficit(repairedLine);
|
|
84939
|
+
if (deficit > 0 && deficit <= 3 && regexSkips === 1) {
|
|
84940
|
+
const candidateLine = code.slice(lineStart, slash) + closedLiteral + ")".repeat(deficit) + tail;
|
|
84941
|
+
if (parsesAsAsyncStatement(candidateLine)) {
|
|
84942
|
+
out.push(")".repeat(deficit));
|
|
84943
|
+
}
|
|
84944
|
+
}
|
|
84945
|
+
}
|
|
84438
84946
|
}
|
|
84439
84947
|
cursor = term;
|
|
84440
84948
|
}
|
|
@@ -87135,7 +87643,15 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
87135
87643
|
tools: openaiTools,
|
|
87136
87644
|
tool_choice: "auto",
|
|
87137
87645
|
temperature: 0.2,
|
|
87138
|
-
|
|
87646
|
+
// Shared provider-aware output ceiling (same helper the survey planner uses):
|
|
87647
|
+
// generous where the provider's max output is verified large (deepseek/minimax
|
|
87648
|
+
// 65536), conservative elsewhere (16384). The old flat 8192 truncated large
|
|
87649
|
+
// finish_generate emissions mid-JSON — the loop re-prompted on the parse error
|
|
87650
|
+
// (line ~4744) and, for a test that genuinely needs >8192 output tokens, could
|
|
87651
|
+
// never finish before the hard stop → zero tests for that scenario. It's a
|
|
87652
|
+
// ceiling, not a target (calls finish at finish_reason:'stop' well under it), so
|
|
87653
|
+
// this adds no per-call cost.
|
|
87654
|
+
max_tokens: (0, discover_ai_planner_js_1.getAIPlannerMaxTokens)(agentModel)
|
|
87139
87655
|
}, { signal });
|
|
87140
87656
|
}), {
|
|
87141
87657
|
label: `scenario-gen-iter-${iteration}`,
|
|
@@ -321746,6 +322262,7 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
321746
322262
|
var mobile_exploration_state_js_1 = require_mobile_exploration_state();
|
|
321747
322263
|
var mobile_discovery_auth_js_1 = require_mobile_discovery_auth();
|
|
321748
322264
|
var mobile_driver_errors_js_1 = require_mobile_driver_errors();
|
|
322265
|
+
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
321749
322266
|
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
321750
322267
|
var discover_planner_js_1 = require_discover_planner();
|
|
321751
322268
|
function checkpointErrorMessage(err) {
|
|
@@ -322046,7 +322563,7 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
|
|
|
322046
322563
|
const graph = state2.toScreenGraph();
|
|
322047
322564
|
const screenMap = state2.buildInMemoryScreenMap();
|
|
322048
322565
|
const testBudget = (0, discover_planner_js_1.computeTestBudget)(explore.exploreStats.pagesVisited, ctx.focusAreas);
|
|
322049
|
-
const planResult = await runPlan({
|
|
322566
|
+
const planResult = await (0, mobile_session_keepalive_js_1.withMobileSessionKeepAlive)(driver, () => runPlan({
|
|
322050
322567
|
graph,
|
|
322051
322568
|
mode,
|
|
322052
322569
|
projectId: ctx.projectId,
|
|
@@ -322070,7 +322587,7 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
|
|
|
322070
322587
|
}, {
|
|
322071
322588
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
322072
322589
|
onAICall
|
|
322073
|
-
});
|
|
322590
|
+
}));
|
|
322074
322591
|
logs2.push(...planResult.logs);
|
|
322075
322592
|
log2(`
|
|
322076
322593
|
Plan: ${planResult.scenarios.length} scenarios, ${planResult.featureGroups.length} feature groups (budget: ${testBudget})`);
|
|
@@ -322928,6 +323445,7 @@ Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.leng
|
|
|
322928
323445
|
if (surveyAuthRecipe) {
|
|
322929
323446
|
logs2.push(`Auth-prologue: captured + verified login recipe ${surveyAuthRecipe.recipeHash} during survey \u2014 persisting for reuse on later runs`);
|
|
322930
323447
|
}
|
|
323448
|
+
const plannerFailedError = planResult.plannerFailed ? `Survey planning failed (${planResult.plannerFailureKind ?? "unknown"}): ${planResult.fallbackReason ?? "planner did not produce a usable plan"}. Explore evidence was saved; re-run the survey to retry planning.` : void 0;
|
|
322931
323449
|
return {
|
|
322932
323450
|
observedOrigins: mergeObservedOrigins(exploreResult.observedOrigins, loginObservedOrigins),
|
|
322933
323451
|
resolvedAppOrigin: surveyAuthRecipe?.appOrigin ?? void 0,
|
|
@@ -322943,7 +323461,8 @@ Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.leng
|
|
|
322943
323461
|
exploreTruncatedReason: exploreResult.truncatedReason,
|
|
322944
323462
|
fallbackUsed: planResult.fallbackUsed ?? false,
|
|
322945
323463
|
fallbackReason: planResult.fallbackReason,
|
|
322946
|
-
|
|
323464
|
+
error: plannerFailedError,
|
|
323465
|
+
featureGroups: plannerFailedError ? void 0 : featureGroups,
|
|
322947
323466
|
apiCalls: discoveryApiCalls.length > 0 ? discoveryApiCalls : void 0,
|
|
322948
323467
|
healthObservations: surveyHealth,
|
|
322949
323468
|
exploreFinalMessages: exploreResult.finalMessages,
|
|
@@ -336801,7 +337320,7 @@ var require_package4 = __commonJS({
|
|
|
336801
337320
|
"package.json"(exports2, module2) {
|
|
336802
337321
|
module2.exports = {
|
|
336803
337322
|
name: "@validate.qa/runner",
|
|
336804
|
-
version: "1.0.
|
|
337323
|
+
version: "1.0.41",
|
|
336805
337324
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
336806
337325
|
bin: {
|
|
336807
337326
|
"validate-runner": "dist/cli.js"
|
|
@@ -336810,7 +337329,7 @@ var require_package4 = __commonJS({
|
|
|
336810
337329
|
build: "tsup",
|
|
336811
337330
|
dev: "tsx src/cli.ts",
|
|
336812
337331
|
test: "node --import tsx --test src/version-check.test.ts",
|
|
336813
|
-
"test:mobile": "node --import tsx --test src/runner.mobile-ai-env.test.ts src/mobile-device-queue.test.ts src/device-lease.test.ts src/result-payload-budget.test.ts src/services/appium-*.test.ts src/services/*mobile*.test.ts src/services/mobile/*.test.ts",
|
|
337332
|
+
"test:mobile": "node --import tsx --test --test-concurrency=2 src/runner.mobile-ai-env.test.ts src/mobile-device-queue.test.ts src/device-lease.test.ts src/result-payload-budget.test.ts src/services/appium-*.test.ts src/services/*mobile*.test.ts src/services/mobile/*.test.ts",
|
|
336814
337333
|
prepublishOnly: "npm run build"
|
|
336815
337334
|
},
|
|
336816
337335
|
files: [
|
|
@@ -342958,6 +343477,17 @@ var MobileBridgeDriver = class {
|
|
|
342958
343477
|
return size;
|
|
342959
343478
|
});
|
|
342960
343479
|
}
|
|
343480
|
+
/**
|
|
343481
|
+
* Recovery-free liveness probe for keep-alive ticks (mobile-session-keepalive).
|
|
343482
|
+
* Deliberately NOT wrapped in withSessionRecovery: keep-alive ticks are
|
|
343483
|
+
* fire-and-forget, so a failing probe must never consume the bounded recovery
|
|
343484
|
+
* budget or relaunch the app from the background — a genuinely dead session
|
|
343485
|
+
* surfaces (with its precise error) on the caller's next REAL command, which
|
|
343486
|
+
* still runs the full recovery path.
|
|
343487
|
+
*/
|
|
343488
|
+
async keepAlivePing() {
|
|
343489
|
+
await this.assertSessionReachable();
|
|
343490
|
+
}
|
|
342961
343491
|
// ── Action result wrapper ──────────────────────────────
|
|
342962
343492
|
/**
|
|
342963
343493
|
* Run an action body, let the UI tree settle, then capture the post-action
|
|
@@ -345515,6 +346045,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345515
346045
|
setExecutorBusy(true);
|
|
345516
346046
|
let session = null;
|
|
345517
346047
|
let capture = null;
|
|
346048
|
+
let discoveryTeardownStarted = false;
|
|
345518
346049
|
try {
|
|
345519
346050
|
const discoverySessionTarget = {
|
|
345520
346051
|
appId: mobileTarget.appId,
|
|
@@ -345566,6 +346097,9 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345566
346097
|
allowAppReset: mobileTarget.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(platform3, session.deviceId),
|
|
345567
346098
|
maxSessionRecoveries: 1,
|
|
345568
346099
|
recoverSession: async (cause) => {
|
|
346100
|
+
if (discoveryTeardownStarted) {
|
|
346101
|
+
throw new Error("discovery teardown already started \u2014 not recreating the Appium session");
|
|
346102
|
+
}
|
|
345569
346103
|
const lost = session;
|
|
345570
346104
|
mobileOnLog(
|
|
345571
346105
|
`[appium] discovery session lost (${cause.message}); recreating once on the leased device`
|
|
@@ -345579,6 +346113,13 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345579
346113
|
}
|
|
345580
346114
|
}
|
|
345581
346115
|
const replacement = await openDiscoverySession();
|
|
346116
|
+
if (discoveryTeardownStarted) {
|
|
346117
|
+
try {
|
|
346118
|
+
await replacement.stop();
|
|
346119
|
+
} catch {
|
|
346120
|
+
}
|
|
346121
|
+
throw new Error("discovery teardown started during session recovery \u2014 replacement session closed");
|
|
346122
|
+
}
|
|
345582
346123
|
session = replacement;
|
|
345583
346124
|
attachMirrorSession(replacement.sessionId, platform3, replacement.deviceId);
|
|
345584
346125
|
liveBrowserHandle.patch({
|
|
@@ -345618,10 +346159,23 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345618
346159
|
onPlanReady
|
|
345619
346160
|
});
|
|
345620
346161
|
} finally {
|
|
346162
|
+
discoveryTeardownStarted = true;
|
|
346163
|
+
const TEARDOWN_STEP_TIMEOUT = /* @__PURE__ */ Symbol("vqa-teardown-timeout");
|
|
346164
|
+
const timeboxed = async (step) => Promise.race([
|
|
346165
|
+
step,
|
|
346166
|
+
new Promise((resolve2) => {
|
|
346167
|
+
const t = setTimeout(() => resolve2(TEARDOWN_STEP_TIMEOUT), 45e3);
|
|
346168
|
+
t.unref?.();
|
|
346169
|
+
})
|
|
346170
|
+
]);
|
|
345621
346171
|
const activeCapture = capture;
|
|
345622
346172
|
if (activeCapture) {
|
|
345623
346173
|
try {
|
|
345624
|
-
const
|
|
346174
|
+
const stopped = await timeboxed(activeCapture.stop());
|
|
346175
|
+
if (stopped === TEARDOWN_STEP_TIMEOUT) {
|
|
346176
|
+
throw new Error("capture stop timed out after 45s \u2014 continuing teardown without apiCalls");
|
|
346177
|
+
}
|
|
346178
|
+
const apiCalls = stopped;
|
|
345625
346179
|
const captureHealth = activeCapture.health();
|
|
345626
346180
|
if (discoveryResult) {
|
|
345627
346181
|
discoveryResult.apiCalls = apiCalls.length ? attachMobileDiscoveryNetworkContext(apiCalls, discoveryResult, session?.appId ?? mobileTarget.appId) : void 0;
|
|
@@ -345641,8 +346195,12 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345641
346195
|
}
|
|
345642
346196
|
if (session) {
|
|
345643
346197
|
detachMirrorSession(session.deviceId);
|
|
346198
|
+
const closing = session;
|
|
345644
346199
|
try {
|
|
345645
|
-
await
|
|
346200
|
+
const stopped = await timeboxed(closing.stop());
|
|
346201
|
+
if (stopped === TEARDOWN_STEP_TIMEOUT) {
|
|
346202
|
+
mobileOnLog(`[appium] session ${closing.sessionId} stop timed out after 45s \u2014 releasing device ${closing.deviceId} anyway; Appium may hold the session until newCommandTimeout reaps it`);
|
|
346203
|
+
}
|
|
345646
346204
|
} catch {
|
|
345647
346205
|
}
|
|
345648
346206
|
}
|
|
@@ -346080,7 +346638,13 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
346080
346638
|
relaunch: async () => {
|
|
346081
346639
|
await currentDriver().relaunch();
|
|
346082
346640
|
},
|
|
346083
|
-
resetAppState: async () => target.resetPolicy === "PRESERVE_APP_DATA" ? false : currentDriver().resetAppState()
|
|
346641
|
+
resetAppState: async () => target.resetPolicy === "PRESERVE_APP_DATA" ? false : currentDriver().resetAppState(),
|
|
346642
|
+
// Recovery-free liveness probe so the loop's keep-alive can span
|
|
346643
|
+
// long AI waits without touching the driver's recovery machinery
|
|
346644
|
+
// (see mobile-session-keepalive.ts).
|
|
346645
|
+
keepAlivePing: async () => {
|
|
346646
|
+
await currentDriver().keepAlivePing();
|
|
346647
|
+
}
|
|
346084
346648
|
};
|
|
346085
346649
|
const runNativeWithSessionHandoff = async (verifyArgs) => {
|
|
346086
346650
|
await closeLiveSession();
|