@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.mjs
CHANGED
|
@@ -275,7 +275,8 @@ var require_ai_provider = __commonJS({
|
|
|
275
275
|
{ id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", provider: "anthropic", description: "Anthropic - fast & intelligent, 200k ctx", contextWindow: 2e5 },
|
|
276
276
|
{ id: "nvidia-nemotron-nano", label: "NVIDIA Nemotron Nano", provider: "nvidia", description: "NVIDIA NIM - Nemotron 30B Nano, reasoning model, 128k ctx", contextWindow: 128e3 },
|
|
277
277
|
{ 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 },
|
|
278
|
-
{ id: "deepseek-v4-flash", label: "DeepSeek V4 Flash", provider: "deepseek", description: "DeepSeek - V4 Flash, OpenAI-compatible, 1M ctx", contextWindow: 1e6 }
|
|
278
|
+
{ id: "deepseek-v4-flash", label: "DeepSeek V4 Flash", provider: "deepseek", description: "DeepSeek - V4 Flash, OpenAI-compatible, 1M ctx", contextWindow: 1e6 },
|
|
279
|
+
{ id: "deepseek-v4-pro", label: "DeepSeek V4 Pro", provider: "deepseek", description: "DeepSeek - V4 Pro, higher-quality reasoning tier, OpenAI-compatible, 1M ctx", contextWindow: 1e6 }
|
|
279
280
|
];
|
|
280
281
|
var PROVIDER_DEFAULT_MODEL = {
|
|
281
282
|
openai: "gpt-4o",
|
|
@@ -335,7 +336,8 @@ var require_ai_provider = __commonJS({
|
|
|
335
336
|
"claude-sonnet-4-6": { strong: "claude-sonnet-4-6", fast: "claude-sonnet-4-6", agent: "claude-sonnet-4-6" },
|
|
336
337
|
"nvidia-nemotron-nano": { strong: "nvidia/nemotron-3-nano-30b-a3b", fast: "nvidia/nemotron-3-nano-30b-a3b", agent: "nvidia/nemotron-3-nano-30b-a3b" },
|
|
337
338
|
"nvidia-kimi-k2.6": { strong: "moonshotai/kimi-k2.6", fast: "moonshotai/kimi-k2.6", agent: "moonshotai/kimi-k2.6" },
|
|
338
|
-
"deepseek-v4-flash": { strong: "deepseek-v4-flash", fast: "deepseek-v4-flash", agent: "deepseek-v4-flash" }
|
|
339
|
+
"deepseek-v4-flash": { strong: "deepseek-v4-flash", fast: "deepseek-v4-flash", agent: "deepseek-v4-flash" },
|
|
340
|
+
"deepseek-v4-pro": { strong: "deepseek-v4-pro", fast: "deepseek-v4-pro", agent: "deepseek-v4-pro" }
|
|
339
341
|
};
|
|
340
342
|
var _requestModelStorage = new async_hooks_1.AsyncLocalStorage();
|
|
341
343
|
function withModelContext(modelId, fn) {
|
|
@@ -397,7 +399,7 @@ var require_ai_provider = __commonJS({
|
|
|
397
399
|
if (modelId === "nvidia/nemotron-3-nano-30b-a3b") {
|
|
398
400
|
return { chat_template_kwargs: { enable_thinking: false } };
|
|
399
401
|
}
|
|
400
|
-
if (modelId === "deepseek-v4-flash") {
|
|
402
|
+
if (modelId === "deepseek-v4-flash" || modelId === "deepseek-v4-pro") {
|
|
401
403
|
return { thinking: { type: "disabled" } };
|
|
402
404
|
}
|
|
403
405
|
return {};
|
|
@@ -1789,6 +1791,83 @@ var init_constants = __esm({
|
|
|
1789
1791
|
}
|
|
1790
1792
|
});
|
|
1791
1793
|
|
|
1794
|
+
// ../shared/dist/project-key.js
|
|
1795
|
+
function isValidProjectKey(key) {
|
|
1796
|
+
return typeof key === "string" && PROJECT_KEY_PATTERN.test(key) && !RESERVED_PROJECT_KEYS.has(key);
|
|
1797
|
+
}
|
|
1798
|
+
function randomChar(rng) {
|
|
1799
|
+
return KEY_ALPHABET[Math.floor(rng() * KEY_ALPHABET.length)] ?? "0";
|
|
1800
|
+
}
|
|
1801
|
+
function randomKey(length, rng) {
|
|
1802
|
+
let out = "";
|
|
1803
|
+
for (let i = 0; i < length; i++)
|
|
1804
|
+
out += randomChar(rng);
|
|
1805
|
+
return out;
|
|
1806
|
+
}
|
|
1807
|
+
function alnumLower(name) {
|
|
1808
|
+
return (name ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1809
|
+
}
|
|
1810
|
+
function deriveProjectKey(name, rng = Math.random) {
|
|
1811
|
+
const base2 = alnumLower(name).slice(0, 3);
|
|
1812
|
+
return base2.length >= PROJECT_KEY_MIN_LENGTH ? base2 : randomKey(3, rng);
|
|
1813
|
+
}
|
|
1814
|
+
function generateUniqueProjectKey(name, taken, rng = Math.random) {
|
|
1815
|
+
const isFree = (k) => isValidProjectKey(k) && !taken.has(k);
|
|
1816
|
+
const primary = deriveProjectKey(name, rng);
|
|
1817
|
+
if (isFree(primary))
|
|
1818
|
+
return primary;
|
|
1819
|
+
const prefix2 = alnumLower(name).slice(0, 2);
|
|
1820
|
+
for (const suffixLen of [1, 2]) {
|
|
1821
|
+
for (let attempt = 0; attempt < 40; attempt++) {
|
|
1822
|
+
const stem = prefix2.length >= 1 ? prefix2 : deriveProjectKey(name, rng).slice(0, 2);
|
|
1823
|
+
const candidate = stem + randomKey(suffixLen, rng);
|
|
1824
|
+
if (isFree(candidate))
|
|
1825
|
+
return candidate;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
for (let len = 4; len <= PROJECT_KEY_MAX_LENGTH; len++) {
|
|
1829
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
1830
|
+
const candidate = randomKey(len, rng);
|
|
1831
|
+
if (isFree(candidate))
|
|
1832
|
+
return candidate;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
throw new Error("Unable to generate a unique project key");
|
|
1836
|
+
}
|
|
1837
|
+
var RESERVED_PROJECT_KEYS, PROJECT_KEY_MIN_LENGTH, PROJECT_KEY_MAX_LENGTH, PROJECT_KEY_PATTERN, KEY_ALPHABET;
|
|
1838
|
+
var init_project_key = __esm({
|
|
1839
|
+
"../shared/dist/project-key.js"() {
|
|
1840
|
+
"use strict";
|
|
1841
|
+
RESERVED_PROJECT_KEYS = /* @__PURE__ */ new Set([
|
|
1842
|
+
"tests",
|
|
1843
|
+
"sessions",
|
|
1844
|
+
"settings",
|
|
1845
|
+
"reports",
|
|
1846
|
+
"account",
|
|
1847
|
+
"notifications",
|
|
1848
|
+
"support",
|
|
1849
|
+
"site-map",
|
|
1850
|
+
"api-intelligence",
|
|
1851
|
+
"selector-registry",
|
|
1852
|
+
"project-knowledge",
|
|
1853
|
+
"test-data",
|
|
1854
|
+
"audit",
|
|
1855
|
+
"validate-copilot",
|
|
1856
|
+
"qa-copilot",
|
|
1857
|
+
"insights",
|
|
1858
|
+
"regression",
|
|
1859
|
+
"reviews",
|
|
1860
|
+
"admin",
|
|
1861
|
+
"login",
|
|
1862
|
+
"admin-view"
|
|
1863
|
+
]);
|
|
1864
|
+
PROJECT_KEY_MIN_LENGTH = 2;
|
|
1865
|
+
PROJECT_KEY_MAX_LENGTH = 12;
|
|
1866
|
+
PROJECT_KEY_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,10}[a-z0-9])$/;
|
|
1867
|
+
KEY_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
1868
|
+
}
|
|
1869
|
+
});
|
|
1870
|
+
|
|
1792
1871
|
// ../shared/dist/mobile-deeplink.js
|
|
1793
1872
|
function isSafeDeepLink(value) {
|
|
1794
1873
|
if (typeof value !== "string")
|
|
@@ -3307,6 +3386,7 @@ var init_audit_pricing = __esm({
|
|
|
3307
3386
|
"MiniMax-M2.7-highspeed": { input: 0.6, output: 2.4 },
|
|
3308
3387
|
"MiniMax-M2": { input: 0.3, output: 1.2 },
|
|
3309
3388
|
"deepseek-v4-flash": { input: 0.14, output: 0.28 },
|
|
3389
|
+
"deepseek-v4-pro": { input: 0.42, output: 0.84 },
|
|
3310
3390
|
// Whisper bills per-minute, not per-token (OpenAI + Groq)
|
|
3311
3391
|
"whisper-1": null,
|
|
3312
3392
|
"whisper-large-v3": null,
|
|
@@ -4977,6 +5057,10 @@ __export(dist_exports, {
|
|
|
4977
5057
|
PLAN_DEFINITIONS: () => PLAN_DEFINITIONS,
|
|
4978
5058
|
PLAN_ORDER: () => PLAN_ORDER,
|
|
4979
5059
|
PLATFORM_ADMIN_EMAIL: () => PLATFORM_ADMIN_EMAIL,
|
|
5060
|
+
PROJECT_KEY_MAX_LENGTH: () => PROJECT_KEY_MAX_LENGTH,
|
|
5061
|
+
PROJECT_KEY_MIN_LENGTH: () => PROJECT_KEY_MIN_LENGTH,
|
|
5062
|
+
PROJECT_KEY_PATTERN: () => PROJECT_KEY_PATTERN,
|
|
5063
|
+
RESERVED_PROJECT_KEYS: () => RESERVED_PROJECT_KEYS,
|
|
4980
5064
|
SELECTOR_PRIORITIES: () => SELECTOR_PRIORITIES,
|
|
4981
5065
|
SHARED_PRICING_PLAN_MAP: () => SHARED_PRICING_PLAN_MAP,
|
|
4982
5066
|
STT_MODEL_OPTIONS: () => STT_MODEL_OPTIONS,
|
|
@@ -5010,6 +5094,7 @@ __export(dist_exports, {
|
|
|
5010
5094
|
controlSignature: () => controlSignature,
|
|
5011
5095
|
countInnerTests: () => countInnerTests,
|
|
5012
5096
|
decideHarPosted: () => decideHarPosted,
|
|
5097
|
+
deriveProjectKey: () => deriveProjectKey,
|
|
5013
5098
|
detectCrossOriginHtmlAsJson: () => detectCrossOriginHtmlAsJson,
|
|
5014
5099
|
detectsErrorBannerAssertion: () => detectsErrorBannerAssertion,
|
|
5015
5100
|
effectiveTestCount: () => effectiveTestCount,
|
|
@@ -5026,6 +5111,7 @@ __export(dist_exports, {
|
|
|
5026
5111
|
formatLimit: () => formatLimit,
|
|
5027
5112
|
fromDbPricingPlan: () => fromDbPricingPlan,
|
|
5028
5113
|
generateMobileTestFromSession: () => generateMobileTestFromSession,
|
|
5114
|
+
generateUniqueProjectKey: () => generateUniqueProjectKey,
|
|
5029
5115
|
getDefaultPlanForTier: () => getDefaultPlanForTier,
|
|
5030
5116
|
getDeploymentTierForPlan: () => getDeploymentTierForPlan,
|
|
5031
5117
|
getPlanChangeType: () => getPlanChangeType,
|
|
@@ -5055,6 +5141,7 @@ __export(dist_exports, {
|
|
|
5055
5141
|
isTrialExpired: () => isTrialExpired,
|
|
5056
5142
|
isUIVariant: () => isUIVariant,
|
|
5057
5143
|
isValidMobileAppId: () => isValidMobileAppId,
|
|
5144
|
+
isValidProjectKey: () => isValidProjectKey,
|
|
5058
5145
|
isValidSttProvider: () => isValidSttProvider,
|
|
5059
5146
|
isValidTtsModel: () => isValidTtsModel,
|
|
5060
5147
|
isValidTtsVoiceId: () => isValidTtsVoiceId,
|
|
@@ -5100,6 +5187,7 @@ var init_dist = __esm({
|
|
|
5100
5187
|
"use strict";
|
|
5101
5188
|
init_types();
|
|
5102
5189
|
init_constants();
|
|
5190
|
+
init_project_key();
|
|
5103
5191
|
init_mobile_test_generation();
|
|
5104
5192
|
init_mobile_step_healer();
|
|
5105
5193
|
init_mobile_cross_platform();
|
|
@@ -5884,6 +5972,44 @@ var require_ai_queue = __commonJS({
|
|
|
5884
5972
|
}
|
|
5885
5973
|
});
|
|
5886
5974
|
|
|
5975
|
+
// ../runner-core/dist/services/mobile/mobile-session-keepalive.js
|
|
5976
|
+
var require_mobile_session_keepalive = __commonJS({
|
|
5977
|
+
"../runner-core/dist/services/mobile/mobile-session-keepalive.js"(exports) {
|
|
5978
|
+
"use strict";
|
|
5979
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5980
|
+
exports.MOBILE_KEEPALIVE_INTERVAL_MS = void 0;
|
|
5981
|
+
exports.computeKeepAliveIntervalMs = computeKeepAliveIntervalMs;
|
|
5982
|
+
exports.withMobileSessionKeepAlive = withMobileSessionKeepAlive;
|
|
5983
|
+
function computeKeepAliveIntervalMs(keepaliveMsRaw, newCommandTimeoutSecondsRaw) {
|
|
5984
|
+
const explicit = keepaliveMsRaw?.trim() ? Number(keepaliveMsRaw) : NaN;
|
|
5985
|
+
if (Number.isFinite(explicit) && explicit >= 0)
|
|
5986
|
+
return Math.floor(explicit);
|
|
5987
|
+
const timeoutSeconds = Number(newCommandTimeoutSecondsRaw);
|
|
5988
|
+
const reapWindowMs = Number.isFinite(timeoutSeconds) && timeoutSeconds > 0 ? Math.floor(timeoutSeconds) * 1e3 : 6e5;
|
|
5989
|
+
return Math.min(12e4, Math.max(5e3, Math.floor(reapWindowMs / 3)));
|
|
5990
|
+
}
|
|
5991
|
+
exports.MOBILE_KEEPALIVE_INTERVAL_MS = computeKeepAliveIntervalMs(process.env.VALIDATEQA_MOBILE_KEEPALIVE_MS, process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT);
|
|
5992
|
+
async function withMobileSessionKeepAlive(driver, fn, intervalMs = exports.MOBILE_KEEPALIVE_INTERVAL_MS) {
|
|
5993
|
+
if (intervalMs <= 0)
|
|
5994
|
+
return fn();
|
|
5995
|
+
const timer = setInterval(() => {
|
|
5996
|
+
try {
|
|
5997
|
+
const probe = driver.keepAlivePing ? driver.keepAlivePing() : driver.windowSize();
|
|
5998
|
+
void probe.catch(() => {
|
|
5999
|
+
});
|
|
6000
|
+
} catch {
|
|
6001
|
+
}
|
|
6002
|
+
}, intervalMs);
|
|
6003
|
+
timer.unref?.();
|
|
6004
|
+
try {
|
|
6005
|
+
return await fn();
|
|
6006
|
+
} finally {
|
|
6007
|
+
clearInterval(timer);
|
|
6008
|
+
}
|
|
6009
|
+
}
|
|
6010
|
+
}
|
|
6011
|
+
});
|
|
6012
|
+
|
|
5887
6013
|
// ../runner-core/dist/services/mobile/mobile-observation.js
|
|
5888
6014
|
var require_mobile_observation = __commonJS({
|
|
5889
6015
|
"../runner-core/dist/services/mobile/mobile-observation.js"(exports) {
|
|
@@ -64617,6 +64743,7 @@ var require_mobile_explorer = __commonJS({
|
|
|
64617
64743
|
var ai_provider_js_1 = require_ai_provider();
|
|
64618
64744
|
var ai_queue_js_1 = require_ai_queue();
|
|
64619
64745
|
var ai_retry_js_1 = require_ai_retry();
|
|
64746
|
+
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
64620
64747
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
64621
64748
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
64622
64749
|
var mobile_exploration_state_js_1 = require_mobile_exploration_state();
|
|
@@ -66126,7 +66253,7 @@ ${statePacket}` }
|
|
|
66126
66253
|
const requestMessageCount = messages.length;
|
|
66127
66254
|
const requestMessages = messages.slice();
|
|
66128
66255
|
const callStart = Date.now();
|
|
66129
|
-
const response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
66256
|
+
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)(() => {
|
|
66130
66257
|
arm();
|
|
66131
66258
|
return aiClient.chat.completions.create({
|
|
66132
66259
|
model: agentModel,
|
|
@@ -66139,7 +66266,7 @@ ${statePacket}` }
|
|
|
66139
66266
|
label: `mobile-explore-iter-${iteration}`,
|
|
66140
66267
|
deferTimeoutUntilArmed: true,
|
|
66141
66268
|
onRetry: (attempt, reason, delayMs) => log2(` \u26A0 mobile explore AI retry ${attempt} (${reason}) \u2014 waiting ${delayMs}ms`)
|
|
66142
|
-
});
|
|
66269
|
+
}));
|
|
66143
66270
|
const iterDuration = Date.now() - callStart;
|
|
66144
66271
|
if (!response?.choices?.[0]?.message) {
|
|
66145
66272
|
throw new Error(`AI explore call returned no response (queue timeout or provider overload) at iteration ${iteration}`);
|
|
@@ -68218,6 +68345,7 @@ var require_discover_ai_planner = __commonJS({
|
|
|
68218
68345
|
"use strict";
|
|
68219
68346
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
68220
68347
|
exports.SCENARIO_SYSTEM_PROMPT = void 0;
|
|
68348
|
+
exports.getAIPlannerMaxTokens = getAIPlannerMaxTokens;
|
|
68221
68349
|
exports.getSnapshotConfidence = getSnapshotConfidence;
|
|
68222
68350
|
exports.compressSiteMap = compressSiteMap;
|
|
68223
68351
|
exports.formatEvidenceForPlanner = formatEvidenceForPlanner;
|
|
@@ -68229,10 +68357,25 @@ var require_discover_ai_planner = __commonJS({
|
|
|
68229
68357
|
var ai_retry_js_1 = require_ai_retry();
|
|
68230
68358
|
var strip_json_fences_js_1 = require_strip_json_fences();
|
|
68231
68359
|
var scenario_plan_validator_js_1 = require_scenario_plan_validator();
|
|
68232
|
-
var
|
|
68233
|
-
var
|
|
68360
|
+
var GENEROUS_AI_PLANNER_MAX_TOKENS = 65536;
|
|
68361
|
+
var CONSERVATIVE_AI_PLANNER_MAX_TOKENS = 16384;
|
|
68234
68362
|
function getAIPlannerMaxTokens(modelId) {
|
|
68235
|
-
|
|
68363
|
+
switch ((0, ai_provider_js_1.getProviderForModel)(modelId)) {
|
|
68364
|
+
case "deepseek":
|
|
68365
|
+
// V4 output ceiling = 384K — verified safe
|
|
68366
|
+
case "minimax":
|
|
68367
|
+
return GENEROUS_AI_PLANNER_MAX_TOKENS;
|
|
68368
|
+
case "xai":
|
|
68369
|
+
// grok-build-0.1 ceiling unproven (Grok-Fast/3 = 16K) — DON'T risk a 400
|
|
68370
|
+
case "openai":
|
|
68371
|
+
// gpt-4o max output = 16384 → more would 400
|
|
68372
|
+
case "anthropic":
|
|
68373
|
+
// OpenAI-compat proxy — keep conservative
|
|
68374
|
+
case "nvidia":
|
|
68375
|
+
// NIM output limits vary — keep conservative
|
|
68376
|
+
default:
|
|
68377
|
+
return CONSERVATIVE_AI_PLANNER_MAX_TOKENS;
|
|
68378
|
+
}
|
|
68236
68379
|
}
|
|
68237
68380
|
var discover_planner_js_1 = require_discover_planner();
|
|
68238
68381
|
function getSnapshotConfidence(sourceKind) {
|
|
@@ -68784,7 +68927,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
68784
68927
|
4. **Multi-page journeys** - only when transitions were actually observed
|
|
68785
68928
|
5. **Bug reports** - for observed 500 errors, crashes, or broken features
|
|
68786
68929
|
6. **Steps are human-readable** - "Fill the email field", not "page.getByLabel('Email').fill()"
|
|
68787
|
-
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.
|
|
68930
|
+
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.
|
|
68788
68931
|
8. **Auth-aware** - mark scenarios that need login. Use process.env.TEST_USER_EMAIL / PASSWORD
|
|
68789
68932
|
9. **No duplicates** - check existing test names before adding
|
|
68790
68933
|
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.
|
|
@@ -68836,7 +68979,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
68836
68979
|
**YOUR responsibility:**
|
|
68837
68980
|
- Scenario \`name\`, \`type\`, \`goal\`, \`prerequisites\`
|
|
68838
68981
|
- \`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
|
|
68839
|
-
- \`expectedOutcomes\`
|
|
68982
|
+
- \`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
|
|
68840
68983
|
|
|
68841
68984
|
**NOT your responsibility:**
|
|
68842
68985
|
- Exact Playwright selectors (\`getByRole(...)\`, \`getByText(...)\`, \`page.locator(...)\`, etc.) \u2014 live-generate picks locators against the live snapshot
|
|
@@ -68857,7 +69000,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
68857
69000
|
- GOOD specificity: "create a new conversation by sending a message to a connection established earlier in this scenario"
|
|
68858
69001
|
- BAD specificity: "click the button labeled exactly 'Send' in the bottom-right of the chat"
|
|
68859
69002
|
|
|
68860
|
-
Citing observed labels in \`expectedOutcomes\`
|
|
69003
|
+
Citing observed labels in \`expectedOutcomes\` is fine \u2014 that's asserting on stable product signals, not implementation specifics.
|
|
68861
69004
|
|
|
68862
69005
|
## Coverage Strategy
|
|
68863
69006
|
- For every form: at least 1 HAPPY_PATH (submit valid data AND verify durable result per rule 11) + 1 EDGE_CASE (empty/invalid submission)
|
|
@@ -69443,6 +69586,24 @@ Each entry is exactly one of:
|
|
|
69443
69586
|
}
|
|
69444
69587
|
async function runAIPlanner(siteMap, evidence, context, options) {
|
|
69445
69588
|
const logs2 = [];
|
|
69589
|
+
const failurePlan = (kind, reason, legacyFallback2) => {
|
|
69590
|
+
const noFallback = context.mode === "survey" && process.env.DISCOVERY_PLANNER_NO_FALLBACK === "true";
|
|
69591
|
+
if (noFallback) {
|
|
69592
|
+
logs2.push(`Planner ${kind} \u2014 NO-FALLBACK: failing survey cleanly instead of fabricating slug features (${reason})`);
|
|
69593
|
+
return {
|
|
69594
|
+
scenarios: [],
|
|
69595
|
+
featureGroups: [],
|
|
69596
|
+
reasoning: "",
|
|
69597
|
+
logs: logs2,
|
|
69598
|
+
coverageGaps: [],
|
|
69599
|
+
plannerFailed: true,
|
|
69600
|
+
plannerFailureKind: kind,
|
|
69601
|
+
fallbackReason: reason
|
|
69602
|
+
};
|
|
69603
|
+
}
|
|
69604
|
+
return { ...legacyFallback2(), fallbackReason: reason };
|
|
69605
|
+
};
|
|
69606
|
+
let extraPlannerCallUsed = false;
|
|
69446
69607
|
const compressed = compressSiteMap(siteMap);
|
|
69447
69608
|
const agentModel = options.modelOverride ? (0, ai_provider_js_1.resolveModelForTask)(options.modelOverride, "agent") : (0, ai_provider_js_1.getModel)("agent");
|
|
69448
69609
|
const plannerMaxTokens = getAIPlannerMaxTokens(agentModel);
|
|
@@ -69478,7 +69639,7 @@ Each entry is exactly one of:
|
|
|
69478
69639
|
} catch (err) {
|
|
69479
69640
|
const message = err instanceof Error ? err.message : String(err);
|
|
69480
69641
|
logs2.push(`AI planner failed after retries: ${message}`);
|
|
69481
|
-
return
|
|
69642
|
+
return failurePlan("transport", (0, ai_retry_js_1.describeAIFailure)(err), () => buildFallbackPlan(siteMap, evidence.recordedJourneys, context.mode, logs2, context.existingTestNames ?? [], context.focusAreas, context.testBudget));
|
|
69482
69643
|
}
|
|
69483
69644
|
try {
|
|
69484
69645
|
options.onAICall?.({
|
|
@@ -69497,20 +69658,104 @@ Each entry is exactly one of:
|
|
|
69497
69658
|
});
|
|
69498
69659
|
} catch {
|
|
69499
69660
|
}
|
|
69500
|
-
const
|
|
69501
|
-
|
|
69661
|
+
const choice = response?.choices?.[0];
|
|
69662
|
+
const raw = choice?.message?.content;
|
|
69663
|
+
const finishReason = (choice?.finish_reason ?? "").toString().toLowerCase();
|
|
69664
|
+
const completionTokens = response.usage?.completion_tokens;
|
|
69665
|
+
const TRUNCATION_FINISH = /* @__PURE__ */ new Set(["length", "max_tokens", "max_output_tokens", "token_limit"]);
|
|
69666
|
+
const definitelyTruncated = TRUNCATION_FINISH.has(finishReason);
|
|
69667
|
+
const atOrOverCap = completionTokens != null && completionTokens >= plannerMaxTokens;
|
|
69668
|
+
const blocked = finishReason === "content_filter" || finishReason === "tool_calls";
|
|
69669
|
+
const usage = response.usage;
|
|
69670
|
+
logs2.push(`AI planner: ${usage?.prompt_tokens ?? "?"} input, ${usage?.completion_tokens ?? "?"} output tokens (finish=${finishReason || "?"})`);
|
|
69671
|
+
const legacyFallback = () => buildFallbackPlan(siteMap, evidence.recordedJourneys, context.mode, logs2, context.existingTestNames ?? [], context.focusAreas, context.testBudget);
|
|
69672
|
+
if (blocked) {
|
|
69673
|
+
return failurePlan("blocked", "AI response was blocked (content_filter/tool_calls)", legacyFallback);
|
|
69674
|
+
}
|
|
69675
|
+
if (definitelyTruncated) {
|
|
69676
|
+
return failurePlan("truncated", "AI planner output truncated at max_tokens (finish_reason=length)", legacyFallback);
|
|
69677
|
+
}
|
|
69678
|
+
const trimmed = raw?.trim();
|
|
69679
|
+
if (!trimmed) {
|
|
69502
69680
|
logs2.push("AI planner returned empty response");
|
|
69503
|
-
return
|
|
69681
|
+
return atOrOverCap ? failurePlan("truncated", "AI planner output truncated at max_tokens (empty body at cap)", legacyFallback) : failurePlan("empty", "AI returned an empty response", legacyFallback);
|
|
69504
69682
|
}
|
|
69505
|
-
const usage = response.usage;
|
|
69506
|
-
logs2.push(`AI planner: ${usage?.prompt_tokens ?? "?"} input, ${usage?.completion_tokens ?? "?"} output tokens`);
|
|
69507
69683
|
let aiOutput;
|
|
69508
69684
|
try {
|
|
69509
|
-
aiOutput = JSON.parse((0, strip_json_fences_js_1.stripJsonFences)(
|
|
69685
|
+
aiOutput = JSON.parse((0, strip_json_fences_js_1.stripJsonFences)(trimmed));
|
|
69510
69686
|
} catch (err) {
|
|
69511
69687
|
const message = err instanceof Error ? err.message : String(err);
|
|
69512
69688
|
logs2.push(`AI planner returned invalid JSON: ${message}`);
|
|
69513
|
-
|
|
69689
|
+
if (atOrOverCap) {
|
|
69690
|
+
logs2.push("AI planner output at/over the token cap and unparseable \u2014 treating as truncated (skipping repair)");
|
|
69691
|
+
return failurePlan("truncated", "AI planner output truncated at max_tokens (unparseable at cap)", legacyFallback);
|
|
69692
|
+
}
|
|
69693
|
+
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.`;
|
|
69694
|
+
const repairStart = Date.now();
|
|
69695
|
+
let repairResponse = null;
|
|
69696
|
+
try {
|
|
69697
|
+
extraPlannerCallUsed = true;
|
|
69698
|
+
repairResponse = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
69699
|
+
arm();
|
|
69700
|
+
return aiClient.chat.completions.create({
|
|
69701
|
+
model: agentModel,
|
|
69702
|
+
messages: [
|
|
69703
|
+
...messages,
|
|
69704
|
+
{ role: "assistant", content: raw },
|
|
69705
|
+
{ role: "user", content: repairPrompt }
|
|
69706
|
+
],
|
|
69707
|
+
max_tokens: plannerMaxTokens,
|
|
69708
|
+
temperature: 0.3,
|
|
69709
|
+
...(0, ai_provider_js_1.getJsonObjectResponseFormat)(agentModel),
|
|
69710
|
+
...(0, ai_provider_js_1.getNoThinkingRequestOptions)(agentModel)
|
|
69711
|
+
}, { signal });
|
|
69712
|
+
}), {
|
|
69713
|
+
label: "ai-planner-repair",
|
|
69714
|
+
deferTimeoutUntilArmed: true,
|
|
69715
|
+
onRetry: (attempt, reason, delay) => logs2.push(`AI planner repair call failed (${reason}) \u2014 retry ${attempt} in ${Math.round(delay / 1e3)}s`)
|
|
69716
|
+
});
|
|
69717
|
+
} catch (repairErr) {
|
|
69718
|
+
const repairMessage = repairErr instanceof Error ? repairErr.message : String(repairErr);
|
|
69719
|
+
logs2.push(`AI planner repair failed (${repairMessage})`);
|
|
69720
|
+
}
|
|
69721
|
+
if (repairResponse) {
|
|
69722
|
+
try {
|
|
69723
|
+
options.onAICall?.({
|
|
69724
|
+
iteration: 1,
|
|
69725
|
+
groupId: `ai-planner-${context.projectId.slice(0, 8)}`,
|
|
69726
|
+
callType: "discoveryAIPlannerRepair",
|
|
69727
|
+
model: agentModel,
|
|
69728
|
+
provider: (0, ai_provider_js_1.getProviderForModel)(agentModel),
|
|
69729
|
+
durationMs: Date.now() - repairStart,
|
|
69730
|
+
promptTokens: repairResponse.usage?.prompt_tokens,
|
|
69731
|
+
completionTokens: repairResponse.usage?.completion_tokens,
|
|
69732
|
+
totalTokens: repairResponse.usage?.total_tokens,
|
|
69733
|
+
assistantMessage: repairResponse.choices[0]?.message?.content,
|
|
69734
|
+
requestMessageCount: messages.length + 2,
|
|
69735
|
+
requestMessages: [
|
|
69736
|
+
...messages,
|
|
69737
|
+
{ role: "assistant", content: raw },
|
|
69738
|
+
{ role: "user", content: repairPrompt }
|
|
69739
|
+
]
|
|
69740
|
+
});
|
|
69741
|
+
} catch {
|
|
69742
|
+
}
|
|
69743
|
+
const repairedRaw = repairResponse.choices[0]?.message?.content?.trim();
|
|
69744
|
+
if (repairedRaw) {
|
|
69745
|
+
try {
|
|
69746
|
+
aiOutput = JSON.parse((0, strip_json_fences_js_1.stripJsonFences)(repairedRaw));
|
|
69747
|
+
} catch (repairParseErr) {
|
|
69748
|
+
const repairMessage = repairParseErr instanceof Error ? repairParseErr.message : String(repairParseErr);
|
|
69749
|
+
logs2.push(`AI planner repair returned invalid JSON (${repairMessage})`);
|
|
69750
|
+
return failurePlan("malformed", "AI returned unreadable output", legacyFallback);
|
|
69751
|
+
}
|
|
69752
|
+
} else {
|
|
69753
|
+
logs2.push("AI planner repair returned empty response");
|
|
69754
|
+
return failurePlan("malformed", "AI returned unreadable output", legacyFallback);
|
|
69755
|
+
}
|
|
69756
|
+
} else {
|
|
69757
|
+
return failurePlan("malformed", "AI returned unreadable output", legacyFallback);
|
|
69758
|
+
}
|
|
69514
69759
|
}
|
|
69515
69760
|
let { scenarios, featureGroups, warnings, coverageGaps } = validateAIPlannerOutput(aiOutput, siteMap, context.existingTestNames, context.testBudget, evidence.recordedJourneys, context.mode, context.focusAreas);
|
|
69516
69761
|
logs2.push(`AI planner: ${scenarios.length} scenarios, ${featureGroups.length} feature groups (${warnings.length} warnings, ${coverageGaps.length} coverage gaps)`);
|
|
@@ -69520,7 +69765,7 @@ Each entry is exactly one of:
|
|
|
69520
69765
|
let topLevelWarning;
|
|
69521
69766
|
const totalV1 = scenarios.length + coverageGaps.length;
|
|
69522
69767
|
const rejectionRateV1 = totalV1 > 0 ? coverageGaps.length / totalV1 : 0;
|
|
69523
|
-
if (rejectionRateV1 > 0.5 && totalV1 >= 3) {
|
|
69768
|
+
if (rejectionRateV1 > 0.5 && totalV1 >= 3 && !extraPlannerCallUsed) {
|
|
69524
69769
|
logs2.push(`AI planner: ${Math.round(rejectionRateV1 * 100)}% rejection rate (${coverageGaps.length}/${totalV1}) \u2014 running self-correction retry`);
|
|
69525
69770
|
const retryContext = [
|
|
69526
69771
|
`Your previous emission produced ${coverageGaps.length} coverage gaps out of ${totalV1} scenarios (${Math.round(rejectionRateV1 * 100)}% rejection rate).`,
|
|
@@ -69539,6 +69784,7 @@ Each entry is exactly one of:
|
|
|
69539
69784
|
const retryStart = Date.now();
|
|
69540
69785
|
let retryResponse = null;
|
|
69541
69786
|
try {
|
|
69787
|
+
extraPlannerCallUsed = true;
|
|
69542
69788
|
retryResponse = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
69543
69789
|
arm();
|
|
69544
69790
|
return aiClient.chat.completions.create({
|
|
@@ -69638,6 +69884,22 @@ Each entry is exactly one of:
|
|
|
69638
69884
|
};
|
|
69639
69885
|
}
|
|
69640
69886
|
if (context.mode === "survey" && featureGroups.length === 0 && siteMap.pages.length > 0) {
|
|
69887
|
+
const hasGroupWorthyEvidence = siteMap.pages.some((p) => (p.forms?.length ?? 0) > 0 || (p.elements?.length ?? 0) > 0);
|
|
69888
|
+
const noFallback = process.env.DISCOVERY_PLANNER_NO_FALLBACK === "true";
|
|
69889
|
+
if (noFallback && !hasGroupWorthyEvidence) {
|
|
69890
|
+
return {
|
|
69891
|
+
scenarios,
|
|
69892
|
+
featureGroups: [],
|
|
69893
|
+
reasoning: aiOutput.reasoning ?? "",
|
|
69894
|
+
logs: logs2,
|
|
69895
|
+
coverageGaps,
|
|
69896
|
+
warning: topLevelWarning,
|
|
69897
|
+
fallbackUsed: false
|
|
69898
|
+
};
|
|
69899
|
+
}
|
|
69900
|
+
if (noFallback) {
|
|
69901
|
+
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));
|
|
69902
|
+
}
|
|
69641
69903
|
logs2.push("Survey AI returned 0 feature groups - falling back to deterministic grouping");
|
|
69642
69904
|
const fallback = buildFallbackPlan(siteMap, evidence.recordedJourneys, context.mode, logs2, context.existingTestNames ?? [], context.focusAreas, context.testBudget);
|
|
69643
69905
|
return {
|
|
@@ -70848,6 +71110,7 @@ var require_mobile_generator = __commonJS({
|
|
|
70848
71110
|
var ai_provider_js_1 = require_ai_provider();
|
|
70849
71111
|
var ai_queue_js_1 = require_ai_queue();
|
|
70850
71112
|
var ai_retry_js_1 = require_ai_retry();
|
|
71113
|
+
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
70851
71114
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
70852
71115
|
var mobile_source_parser_js_2 = require_mobile_source_parser();
|
|
70853
71116
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
@@ -71755,14 +72018,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
71755
72018
|
const requestMessageCount = messages.length;
|
|
71756
72019
|
const requestMessages = messages.slice();
|
|
71757
72020
|
const callStart = Date.now();
|
|
71758
|
-
const response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
72021
|
+
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)(() => {
|
|
71759
72022
|
arm();
|
|
71760
72023
|
return aiClient.chat.completions.create({ model: agentModel, messages, tools: GENERATE_TOOLS, tool_choice: "auto", max_tokens: 8e3 }, { signal });
|
|
71761
72024
|
}), {
|
|
71762
72025
|
label: `mobile-generate-${scenario.name.slice(0, 24)}-iter-${iteration}`,
|
|
71763
72026
|
deferTimeoutUntilArmed: true,
|
|
71764
72027
|
onRetry: (attempt, reason, delayMs) => log2(` \u26A0 mobile generate AI retry ${attempt} (${reason}) \u2014 waiting ${delayMs}ms`)
|
|
71765
|
-
});
|
|
72028
|
+
}));
|
|
71766
72029
|
const iterDuration = Date.now() - callStart;
|
|
71767
72030
|
if (!response?.choices?.[0]?.message) {
|
|
71768
72031
|
throw new Error(`AI generate call returned no response at iteration ${iteration}`);
|
|
@@ -72374,6 +72637,21 @@ ${statePacket}`, redactionCredentials) },
|
|
|
72374
72637
|
};
|
|
72375
72638
|
}
|
|
72376
72639
|
const metadata = metadataForElement(element, screenName(), platform3);
|
|
72640
|
+
const expectedNeedle = expected.trim().toLowerCase();
|
|
72641
|
+
const identityValues = [element.accessibilityId, element.resourceId].map((value) => value?.trim().toLowerCase()).filter((value) => Boolean(value));
|
|
72642
|
+
const visibleText = (element.text ?? "").trim().toLowerCase();
|
|
72643
|
+
const isSelfLabelAssertion = expectedNeedle.length > 0 && identityValues.includes(expectedNeedle) && !visibleText.includes(expectedNeedle);
|
|
72644
|
+
if (isSelfLabelAssertion) {
|
|
72645
|
+
recordAction({ type: "ASSERT_VISIBLE", selector: (0, mobile_source_parser_js_2.preferredLocator)(element), metadata });
|
|
72646
|
+
log2(` assert_text DOWNGRADED to assert_visible (tautological self-label "${expected}"): ${metadata.targetLabel}`);
|
|
72647
|
+
return {
|
|
72648
|
+
ok: true,
|
|
72649
|
+
asserted: "visible",
|
|
72650
|
+
downgraded: true,
|
|
72651
|
+
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.`,
|
|
72652
|
+
target: metadata.targetLabel
|
|
72653
|
+
};
|
|
72654
|
+
}
|
|
72377
72655
|
if (hasStableId(element)) {
|
|
72378
72656
|
recordAction({ type: "ASSERT_TEXT", value: expected, selector: (0, mobile_source_parser_js_2.preferredLocator)(element), metadata });
|
|
72379
72657
|
log2(` assert_text: ${metadata.targetLabel} ~ "${expected}"`);
|
|
@@ -73067,7 +73345,13 @@ ${statePacket}`, redactionCredentials) },
|
|
|
73067
73345
|
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;
|
|
73068
73346
|
const test = {
|
|
73069
73347
|
name: compiled.name,
|
|
73070
|
-
|
|
73348
|
+
// Description priority: the model's live finish description, then the
|
|
73349
|
+
// planner's scenario goal (always a meaningful sentence describing what the
|
|
73350
|
+
// test proves), then the generic compiled placeholder. Without the goal
|
|
73351
|
+
// fallback a scenario whose finish_generate omitted a description shipped
|
|
73352
|
+
// with the contentless "<platform> native flow generated from a recorded
|
|
73353
|
+
// mobile session." string — see mobile-test-generation.ts's compiled default.
|
|
73354
|
+
description: recording.finish?.description?.trim() || scenario.goal?.trim() || compiled.description,
|
|
73071
73355
|
framework: "APPIUM",
|
|
73072
73356
|
appiumCode: compiled.appiumCode,
|
|
73073
73357
|
steps: compiled.steps,
|
|
@@ -73809,6 +74093,7 @@ var require_mobile_heal_loop = __commonJS({
|
|
|
73809
74093
|
var mobile_heal_postprocess_js_1 = require_mobile_heal_postprocess();
|
|
73810
74094
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
73811
74095
|
var selector_registry_context_js_1 = require_selector_registry_context();
|
|
74096
|
+
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
73812
74097
|
exports.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.
|
|
73813
74098
|
## You MUST try to fix the steps
|
|
73814
74099
|
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).
|
|
@@ -74700,6 +74985,19 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
74700
74985
|
};
|
|
74701
74986
|
}
|
|
74702
74987
|
const maxPasses = envInt2("MOBILE_HEAL_PHASE2_MAX_PASSES") ?? DEFAULT_PHASE2_MAX_PASSES;
|
|
74988
|
+
const keepAlivePingable = {
|
|
74989
|
+
...typeof liveDriver.keepAlivePing === "function" ? { keepAlivePing: liveDriver.keepAlivePing.bind(liveDriver) } : {},
|
|
74990
|
+
// WARNING: this fallback routes through snapshot(), which sits inside
|
|
74991
|
+
// withSessionRecovery on the real driver — a failed tick could consume the
|
|
74992
|
+
// bounded recovery budget, the exact bug keepAlivePing exists to avoid.
|
|
74993
|
+
// Every current MobileLiveDriverLike provides keepAlivePing, so this only
|
|
74994
|
+
// exists for structural compatibility; implement keepAlivePing on any new
|
|
74995
|
+
// facade rather than relying on it.
|
|
74996
|
+
windowSize: async () => {
|
|
74997
|
+
await liveDriver.snapshot();
|
|
74998
|
+
return null;
|
|
74999
|
+
}
|
|
75000
|
+
};
|
|
74703
75001
|
let baseSteps = args.steps;
|
|
74704
75002
|
let priorError = args.priorError ?? null;
|
|
74705
75003
|
let bestSteps;
|
|
@@ -74734,10 +75032,10 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
74734
75032
|
];
|
|
74735
75033
|
let content;
|
|
74736
75034
|
try {
|
|
74737
|
-
const response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
75035
|
+
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)(() => {
|
|
74738
75036
|
arm();
|
|
74739
75037
|
return aiClient.chat.completions.create({ model, messages, max_tokens: 8e3, temperature: 0.1 }, { signal });
|
|
74740
|
-
}), { label: `mobile-live-heal-pass-${pass}`, deferTimeoutUntilArmed: true });
|
|
75038
|
+
}), { label: `mobile-live-heal-pass-${pass}`, deferTimeoutUntilArmed: true }));
|
|
74741
75039
|
content = response?.choices?.[0]?.message?.content ?? "";
|
|
74742
75040
|
} catch {
|
|
74743
75041
|
continue;
|
|
@@ -75744,6 +76042,118 @@ var require_har_stream = __commonJS({
|
|
|
75744
76042
|
}
|
|
75745
76043
|
});
|
|
75746
76044
|
|
|
76045
|
+
// ../runner-core/dist/services/api-auth-headers.js
|
|
76046
|
+
var require_api_auth_headers = __commonJS({
|
|
76047
|
+
"../runner-core/dist/services/api-auth-headers.js"(exports) {
|
|
76048
|
+
"use strict";
|
|
76049
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
76050
|
+
exports.parseApiAuthHeaders = parseApiAuthHeaders;
|
|
76051
|
+
exports.resolveApiAuthHeaders = resolveApiAuthHeaders;
|
|
76052
|
+
exports.isAppOriginRequest = isAppOriginRequest;
|
|
76053
|
+
exports.buildOriginScopedApiFixtures = buildOriginScopedApiFixtures;
|
|
76054
|
+
var HEADER_NAME_RE = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
|
|
76055
|
+
function hasControlChar(v) {
|
|
76056
|
+
for (let i = 0; i < v.length; i++) {
|
|
76057
|
+
const code = v.charCodeAt(i);
|
|
76058
|
+
if (code < 32 || code === 127)
|
|
76059
|
+
return true;
|
|
76060
|
+
}
|
|
76061
|
+
return false;
|
|
76062
|
+
}
|
|
76063
|
+
function parseApiAuthHeaders(raw) {
|
|
76064
|
+
if (!raw || typeof raw !== "string")
|
|
76065
|
+
return void 0;
|
|
76066
|
+
const trimmed = raw.trim();
|
|
76067
|
+
if (!trimmed)
|
|
76068
|
+
return void 0;
|
|
76069
|
+
try {
|
|
76070
|
+
const parsed = JSON.parse(trimmed);
|
|
76071
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
76072
|
+
return void 0;
|
|
76073
|
+
const headers = {};
|
|
76074
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
76075
|
+
if (typeof k === "string" && HEADER_NAME_RE.test(k) && typeof v === "string" && !hasControlChar(v)) {
|
|
76076
|
+
headers[k] = v;
|
|
76077
|
+
}
|
|
76078
|
+
}
|
|
76079
|
+
return Object.keys(headers).length > 0 ? headers : void 0;
|
|
76080
|
+
} catch {
|
|
76081
|
+
return void 0;
|
|
76082
|
+
}
|
|
76083
|
+
}
|
|
76084
|
+
function resolveApiAuthHeaders(input) {
|
|
76085
|
+
if (input.explicit)
|
|
76086
|
+
return input.explicit;
|
|
76087
|
+
if (!input.isApiTest)
|
|
76088
|
+
return void 0;
|
|
76089
|
+
return parseApiAuthHeaders(input.apiAuthHeadersVar);
|
|
76090
|
+
}
|
|
76091
|
+
function isAppOriginRequest(url, baseUrl) {
|
|
76092
|
+
if (typeof url !== "string" || !baseUrl)
|
|
76093
|
+
return false;
|
|
76094
|
+
let appOrigin;
|
|
76095
|
+
try {
|
|
76096
|
+
appOrigin = new URL(baseUrl).origin;
|
|
76097
|
+
} catch {
|
|
76098
|
+
return false;
|
|
76099
|
+
}
|
|
76100
|
+
try {
|
|
76101
|
+
return new URL(url, baseUrl).origin === appOrigin;
|
|
76102
|
+
} catch {
|
|
76103
|
+
return false;
|
|
76104
|
+
}
|
|
76105
|
+
}
|
|
76106
|
+
function buildOriginScopedApiFixtures(headers, baseUrl) {
|
|
76107
|
+
let appOrigin;
|
|
76108
|
+
try {
|
|
76109
|
+
appOrigin = baseUrl ? new URL(baseUrl).origin : null;
|
|
76110
|
+
} catch {
|
|
76111
|
+
appOrigin = null;
|
|
76112
|
+
}
|
|
76113
|
+
return `export * from '@playwright/test';
|
|
76114
|
+
import { test as base } from '@playwright/test';
|
|
76115
|
+
|
|
76116
|
+
// Standing auth headers \u2014 attached ONLY to same-origin requests so the credential never leaks to
|
|
76117
|
+
// an off-origin host. Embedded at run time; this file lives in the ephemeral run dir.
|
|
76118
|
+
const STANDING_HEADERS = ${JSON.stringify(headers)};
|
|
76119
|
+
const APP_ORIGIN = ${JSON.stringify(appOrigin)};
|
|
76120
|
+
const BASE_URL = ${JSON.stringify(baseUrl)};
|
|
76121
|
+
const METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'fetch'];
|
|
76122
|
+
|
|
76123
|
+
function sameOrigin(url) {
|
|
76124
|
+
if (!APP_ORIGIN || typeof url !== 'string') return false;
|
|
76125
|
+
try { return new URL(url, BASE_URL).origin === APP_ORIGIN; } catch { return false; }
|
|
76126
|
+
}
|
|
76127
|
+
|
|
76128
|
+
export const test = base.extend({
|
|
76129
|
+
request: async ({ request }, use) => {
|
|
76130
|
+
const scoped = new Proxy(request, {
|
|
76131
|
+
get(target, prop) {
|
|
76132
|
+
const value = target[prop];
|
|
76133
|
+
// HTTP verbs: wrap to add standing headers on same-origin requests only.
|
|
76134
|
+
if (typeof prop === 'string' && METHODS.indexOf(prop) !== -1) {
|
|
76135
|
+
const orig = value.bind(target);
|
|
76136
|
+
return (url, options) => {
|
|
76137
|
+
options = options || {};
|
|
76138
|
+
return sameOrigin(url)
|
|
76139
|
+
? orig(url, { ...options, headers: { ...STANDING_HEADERS, ...(options.headers || {}) } })
|
|
76140
|
+
: orig(url, options);
|
|
76141
|
+
};
|
|
76142
|
+
}
|
|
76143
|
+
// Every other METHOD must run with 'this' bound to the REAL context \u2014 an APIRequestContext
|
|
76144
|
+
// uses internal slots/private fields a Proxy can't forward, so calling with 'this'=proxy
|
|
76145
|
+
// would throw. Non-function props pass through untouched.
|
|
76146
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
76147
|
+
},
|
|
76148
|
+
});
|
|
76149
|
+
await use(scoped);
|
|
76150
|
+
},
|
|
76151
|
+
});
|
|
76152
|
+
`;
|
|
76153
|
+
}
|
|
76154
|
+
}
|
|
76155
|
+
});
|
|
76156
|
+
|
|
75747
76157
|
// ../runner-core/dist/services/playwright-native.js
|
|
75748
76158
|
var require_playwright_native = __commonJS({
|
|
75749
76159
|
"../runner-core/dist/services/playwright-native.js"(exports) {
|
|
@@ -75786,7 +76196,7 @@ var require_playwright_native = __commonJS({
|
|
|
75786
76196
|
};
|
|
75787
76197
|
})();
|
|
75788
76198
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
75789
|
-
exports.scrubHarRequestBody = void 0;
|
|
76199
|
+
exports.scrubHarRequestBody = exports.buildOriginScopedApiFixtures = exports.isAppOriginRequest = exports.resolveApiAuthHeaders = exports.parseApiAuthHeaders = void 0;
|
|
75790
76200
|
exports.executeTestViaNativePlaywright = executeTestViaNativePlaywright2;
|
|
75791
76201
|
exports.parseJsonResults = parseJsonResults2;
|
|
75792
76202
|
exports.parseHarToApiSummary = parseHarToApiSummary;
|
|
@@ -75798,6 +76208,20 @@ var require_playwright_native = __commonJS({
|
|
|
75798
76208
|
var spawn_with_tree_kill_js_1 = require_spawn_with_tree_kill();
|
|
75799
76209
|
var api_call_redaction_js_1 = require_api_call_redaction();
|
|
75800
76210
|
var har_stream_js_1 = require_har_stream();
|
|
76211
|
+
var api_auth_headers_js_1 = require_api_auth_headers();
|
|
76212
|
+
var api_auth_headers_js_2 = require_api_auth_headers();
|
|
76213
|
+
Object.defineProperty(exports, "parseApiAuthHeaders", { enumerable: true, get: function() {
|
|
76214
|
+
return api_auth_headers_js_2.parseApiAuthHeaders;
|
|
76215
|
+
} });
|
|
76216
|
+
Object.defineProperty(exports, "resolveApiAuthHeaders", { enumerable: true, get: function() {
|
|
76217
|
+
return api_auth_headers_js_2.resolveApiAuthHeaders;
|
|
76218
|
+
} });
|
|
76219
|
+
Object.defineProperty(exports, "isAppOriginRequest", { enumerable: true, get: function() {
|
|
76220
|
+
return api_auth_headers_js_2.isAppOriginRequest;
|
|
76221
|
+
} });
|
|
76222
|
+
Object.defineProperty(exports, "buildOriginScopedApiFixtures", { enumerable: true, get: function() {
|
|
76223
|
+
return api_auth_headers_js_2.buildOriginScopedApiFixtures;
|
|
76224
|
+
} });
|
|
75801
76225
|
var api_call_redaction_js_2 = require_api_call_redaction();
|
|
75802
76226
|
Object.defineProperty(exports, "scrubHarRequestBody", { enumerable: true, get: function() {
|
|
75803
76227
|
return api_call_redaction_js_2.scrubHarRequestBody;
|
|
@@ -75807,7 +76231,7 @@ var require_playwright_native = __commonJS({
|
|
|
75807
76231
|
var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
|
|
75808
76232
|
var PW_CLI = __require.resolve("@playwright/test/cli");
|
|
75809
76233
|
async function executeTestViaNativePlaywright2(options) {
|
|
75810
|
-
const { playwrightCode, baseUrl, testName, testVariables, storageStatePath, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
|
|
76234
|
+
const { playwrightCode, baseUrl, testName, testVariables, storageStatePath, extraHttpHeaders, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
|
|
75811
76235
|
const logs2 = [];
|
|
75812
76236
|
const screenshots = [];
|
|
75813
76237
|
logs2.push(`\u{1F3AD} Native Playwright starting`);
|
|
@@ -75821,6 +76245,11 @@ var require_playwright_native = __commonJS({
|
|
|
75821
76245
|
runDir = (0, path_1.join)(RUNS_DIR, (0, crypto_1.randomUUID)());
|
|
75822
76246
|
await (0, promises_1.mkdir)(runDir, { recursive: true });
|
|
75823
76247
|
const isApiTest2 = playwrightCode.includes("@api") || playwrightCode.includes("{ request }") && !playwrightCode.includes("{ page");
|
|
76248
|
+
const resolvedExtraHeaders = (0, api_auth_headers_js_1.resolveApiAuthHeaders)({
|
|
76249
|
+
explicit: extraHttpHeaders,
|
|
76250
|
+
isApiTest: isApiTest2,
|
|
76251
|
+
apiAuthHeadersVar: testVariables?.API_AUTH_HEADERS
|
|
76252
|
+
});
|
|
75824
76253
|
const cssDisableSetup = `
|
|
75825
76254
|
const __qaLiveBrowserStateFile = process.env.VALIDATE_LIVE_BROWSER_STATE_PATH;
|
|
75826
76255
|
let __qaLiveBrowserInterval: ReturnType<typeof setInterval> | null = null;
|
|
@@ -76014,8 +76443,17 @@ test.afterEach(async ({ page }, testInfo) => {
|
|
|
76014
76443
|
if (!importMatch) {
|
|
76015
76444
|
logs2.push("\u26A0 Could not find @playwright/test import - animation disabling NOT injected");
|
|
76016
76445
|
}
|
|
76017
|
-
const
|
|
76446
|
+
const useScopedApiHeaders = isApiTest2 && !!resolvedExtraHeaders && !!importMatch && !!baseUrl;
|
|
76447
|
+
let instrumentedCode;
|
|
76448
|
+
if (isApiTest2) {
|
|
76449
|
+
instrumentedCode = useScopedApiHeaders ? playwrightCode.replace(importMatch[0], importMatch[0].replace(/from\s+['"]@playwright\/test['"]/, "from './qa-api-fixtures'")) : playwrightCode;
|
|
76450
|
+
} else {
|
|
76451
|
+
instrumentedCode = importMatch ? playwrightCode.replace(importMatch[0], importMatch[0] + cssDisableSetup) : playwrightCode;
|
|
76452
|
+
}
|
|
76018
76453
|
await (0, promises_1.writeFile)((0, path_1.join)(runDir, "test.spec.ts"), instrumentedCode);
|
|
76454
|
+
if (useScopedApiHeaders && resolvedExtraHeaders) {
|
|
76455
|
+
await (0, promises_1.writeFile)((0, path_1.join)(runDir, "qa-api-fixtures.ts"), (0, api_auth_headers_js_1.buildOriginScopedApiFixtures)(resolvedExtraHeaders, baseUrl));
|
|
76456
|
+
}
|
|
76019
76457
|
const headless = process.env.PLAYWRIGHT_HEADLESS === "true" || process.env.NODE_ENV === "production" || IS_CLOUD2;
|
|
76020
76458
|
logs2.push(`\u2699\uFE0F Config: baseURL=${JSON.stringify(baseUrl)}, headless=${headless}${isApiTest2 ? ", API mode (no screenshot/video)" : ""}`);
|
|
76021
76459
|
const absTestDir = runDir.replace(/\\/g, "/");
|
|
@@ -76029,6 +76467,7 @@ test.afterEach(async ({ page }, testInfo) => {
|
|
|
76029
76467
|
const absHarPath = wantHar ? (0, path_1.join)(runDir, "session.har").replace(/\\/g, "/") : null;
|
|
76030
76468
|
const recordHarLine = absHarPath ? `recordHar: { path: ${JSON.stringify(absHarPath)}, mode: 'full' },` : "";
|
|
76031
76469
|
const storageStateLine = storageStatePath ? `storageState: ${JSON.stringify(storageStatePath.replace(/\\/g, "/"))},` : "";
|
|
76470
|
+
const extraHeadersLine = resolvedExtraHeaders && !useScopedApiHeaders && Object.keys(resolvedExtraHeaders).length > 0 ? `extraHTTPHeaders: ${JSON.stringify(resolvedExtraHeaders)},` : "";
|
|
76032
76471
|
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'],";
|
|
76033
76472
|
const config = `
|
|
76034
76473
|
// Plain object export - no require('@playwright/test') needed.
|
|
@@ -76042,6 +76481,7 @@ module.exports = {
|
|
|
76042
76481
|
use: {
|
|
76043
76482
|
baseURL: ${JSON.stringify(baseUrl)},
|
|
76044
76483
|
${storageStateLine}
|
|
76484
|
+
${extraHeadersLine}
|
|
76045
76485
|
browserName: 'chromium',
|
|
76046
76486
|
headless: ${headless},
|
|
76047
76487
|
screenshot: ${isApiTest2 ? "'off'" : "'on'"},
|
|
@@ -76083,6 +76523,7 @@ module.exports = {
|
|
|
76083
76523
|
// code. Forwarded (not allowlisted) so it stays a per-run toggle; absent → inert.
|
|
76084
76524
|
...process.env.SHARED_AUTH_INJECT ? { SHARED_AUTH_INJECT: process.env.SHARED_AUTH_INJECT } : {}
|
|
76085
76525
|
};
|
|
76526
|
+
delete envWithIsolation.API_AUTH_HEADERS;
|
|
76086
76527
|
logs2.push("\u{1F680} Spawning playwright test...");
|
|
76087
76528
|
const stopLiveBrowserPolling = liveBrowserStatePath && liveBrowserHandle ? startLiveBrowserStatePolling(liveBrowserStatePath, liveBrowserHandle, onLiveBrowserSnapshot) : null;
|
|
76088
76529
|
let playResult;
|
|
@@ -84189,6 +84630,7 @@ var require_discover_generator = __commonJS({
|
|
|
84189
84630
|
var ai_provider_js_1 = require_ai_provider();
|
|
84190
84631
|
var ai_queue_js_1 = require_ai_queue();
|
|
84191
84632
|
var ai_retry_js_1 = require_ai_retry();
|
|
84633
|
+
var discover_ai_planner_js_1 = require_discover_ai_planner();
|
|
84192
84634
|
var snapshot_parser_js_1 = require_snapshot_parser();
|
|
84193
84635
|
var scrub_credentials_js_1 = require_scrub_credentials();
|
|
84194
84636
|
var exploration_parser_js_1 = require_exploration_parser();
|
|
@@ -84396,6 +84838,51 @@ var require_discover_generator = __commonJS({
|
|
|
84396
84838
|
}
|
|
84397
84839
|
return s.length;
|
|
84398
84840
|
}
|
|
84841
|
+
function lineParenDeficit(line) {
|
|
84842
|
+
const stack = [];
|
|
84843
|
+
let regexSkips = 0;
|
|
84844
|
+
for (let i = 0; i < line.length; i++) {
|
|
84845
|
+
const ch = line[i];
|
|
84846
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
84847
|
+
i = skipQuotedLiteral(line, i) - 1;
|
|
84848
|
+
continue;
|
|
84849
|
+
}
|
|
84850
|
+
if (ch === "/" && line[i + 1] === "/") {
|
|
84851
|
+
i = skipLineComment(line, i) - 1;
|
|
84852
|
+
continue;
|
|
84853
|
+
}
|
|
84854
|
+
if (ch === "/" && line[i + 1] === "*") {
|
|
84855
|
+
i = skipBlockComment(line, i) - 1;
|
|
84856
|
+
continue;
|
|
84857
|
+
}
|
|
84858
|
+
if (ch === "/" && isRegexStart(line, i)) {
|
|
84859
|
+
regexSkips++;
|
|
84860
|
+
i = skipRegexLiteral(line, i) - 1;
|
|
84861
|
+
continue;
|
|
84862
|
+
}
|
|
84863
|
+
if (ch === "(" || ch === "{" || ch === "[") {
|
|
84864
|
+
stack.push(ch);
|
|
84865
|
+
continue;
|
|
84866
|
+
}
|
|
84867
|
+
if (ch === ")" || ch === "}" || ch === "]") {
|
|
84868
|
+
if (stack[stack.length - 1] !== CLOSE_TO_OPEN[ch])
|
|
84869
|
+
return { deficit: 0, regexSkips };
|
|
84870
|
+
stack.pop();
|
|
84871
|
+
}
|
|
84872
|
+
}
|
|
84873
|
+
const deficit = stack.length > 0 && stack.every((open) => open === "(") ? stack.length : 0;
|
|
84874
|
+
return { deficit, regexSkips };
|
|
84875
|
+
}
|
|
84876
|
+
function parsesAsAsyncStatement(line) {
|
|
84877
|
+
try {
|
|
84878
|
+
new Function(`return (async () => {
|
|
84879
|
+
${line}
|
|
84880
|
+
})`);
|
|
84881
|
+
return true;
|
|
84882
|
+
} catch {
|
|
84883
|
+
return false;
|
|
84884
|
+
}
|
|
84885
|
+
}
|
|
84399
84886
|
function closeUnclosedRegexLiterals(code) {
|
|
84400
84887
|
const anchors = [
|
|
84401
84888
|
{ rx: /\b(?:waitForURL|toHaveURL)\(\//g, mode: "url" },
|
|
@@ -84434,12 +84921,33 @@ var require_discover_generator = __commonJS({
|
|
|
84434
84921
|
const term = findRegexTerminator(code, slash, TERMINATORS[mode]);
|
|
84435
84922
|
out.push(code.slice(cursor, slash));
|
|
84436
84923
|
const raw = code.slice(slash + 1, term).replace(/\s+$/, "");
|
|
84924
|
+
let closedLiteral;
|
|
84437
84925
|
if (mode === "url") {
|
|
84438
84926
|
const escaped = raw.replace(/(?<!\\)\//g, "\\/");
|
|
84439
84927
|
const finalBody = escaped.startsWith("\\/") ? escaped : `\\/${escaped}`;
|
|
84440
|
-
|
|
84928
|
+
closedLiteral = `/${finalBody}/`;
|
|
84441
84929
|
} else {
|
|
84442
|
-
|
|
84930
|
+
closedLiteral = `/${raw}/`;
|
|
84931
|
+
}
|
|
84932
|
+
out.push(closedLiteral);
|
|
84933
|
+
if (code[term] === ")") {
|
|
84934
|
+
const lineStart = code.lastIndexOf("\n", slash) + 1;
|
|
84935
|
+
const nextNewline = code.indexOf("\n", term);
|
|
84936
|
+
const lineEnd = nextNewline === -1 ? code.length : nextNewline;
|
|
84937
|
+
const tail = code.slice(term, lineEnd);
|
|
84938
|
+
const physicalLine = code.slice(lineStart, lineEnd);
|
|
84939
|
+
const lineIsPlain = !physicalLine.includes("`") && !physicalLine.includes("/*") && !physicalLine.includes("*/");
|
|
84940
|
+
const soleAnchorOnLine = !hits.some((h) => h.slash !== slash && h.slash >= lineStart && h.slash < lineEnd);
|
|
84941
|
+
if (lineIsPlain && soleAnchorOnLine && /;\s*$/.test(tail)) {
|
|
84942
|
+
const repairedLine = code.slice(lineStart, slash) + "/x/" + tail;
|
|
84943
|
+
const { deficit, regexSkips } = lineParenDeficit(repairedLine);
|
|
84944
|
+
if (deficit > 0 && deficit <= 3 && regexSkips === 1) {
|
|
84945
|
+
const candidateLine = code.slice(lineStart, slash) + closedLiteral + ")".repeat(deficit) + tail;
|
|
84946
|
+
if (parsesAsAsyncStatement(candidateLine)) {
|
|
84947
|
+
out.push(")".repeat(deficit));
|
|
84948
|
+
}
|
|
84949
|
+
}
|
|
84950
|
+
}
|
|
84443
84951
|
}
|
|
84444
84952
|
cursor = term;
|
|
84445
84953
|
}
|
|
@@ -87140,7 +87648,15 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
87140
87648
|
tools: openaiTools,
|
|
87141
87649
|
tool_choice: "auto",
|
|
87142
87650
|
temperature: 0.2,
|
|
87143
|
-
|
|
87651
|
+
// Shared provider-aware output ceiling (same helper the survey planner uses):
|
|
87652
|
+
// generous where the provider's max output is verified large (deepseek/minimax
|
|
87653
|
+
// 65536), conservative elsewhere (16384). The old flat 8192 truncated large
|
|
87654
|
+
// finish_generate emissions mid-JSON — the loop re-prompted on the parse error
|
|
87655
|
+
// (line ~4744) and, for a test that genuinely needs >8192 output tokens, could
|
|
87656
|
+
// never finish before the hard stop → zero tests for that scenario. It's a
|
|
87657
|
+
// ceiling, not a target (calls finish at finish_reason:'stop' well under it), so
|
|
87658
|
+
// this adds no per-call cost.
|
|
87659
|
+
max_tokens: (0, discover_ai_planner_js_1.getAIPlannerMaxTokens)(agentModel)
|
|
87144
87660
|
}, { signal });
|
|
87145
87661
|
}), {
|
|
87146
87662
|
label: `scenario-gen-iter-${iteration}`,
|
|
@@ -321751,6 +322267,7 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
321751
322267
|
var mobile_exploration_state_js_1 = require_mobile_exploration_state();
|
|
321752
322268
|
var mobile_discovery_auth_js_1 = require_mobile_discovery_auth();
|
|
321753
322269
|
var mobile_driver_errors_js_1 = require_mobile_driver_errors();
|
|
322270
|
+
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
321754
322271
|
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
321755
322272
|
var discover_planner_js_1 = require_discover_planner();
|
|
321756
322273
|
function checkpointErrorMessage(err) {
|
|
@@ -322051,7 +322568,7 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
|
|
|
322051
322568
|
const graph = state2.toScreenGraph();
|
|
322052
322569
|
const screenMap = state2.buildInMemoryScreenMap();
|
|
322053
322570
|
const testBudget = (0, discover_planner_js_1.computeTestBudget)(explore.exploreStats.pagesVisited, ctx.focusAreas);
|
|
322054
|
-
const planResult = await runPlan({
|
|
322571
|
+
const planResult = await (0, mobile_session_keepalive_js_1.withMobileSessionKeepAlive)(driver, () => runPlan({
|
|
322055
322572
|
graph,
|
|
322056
322573
|
mode,
|
|
322057
322574
|
projectId: ctx.projectId,
|
|
@@ -322075,7 +322592,7 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
|
|
|
322075
322592
|
}, {
|
|
322076
322593
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
322077
322594
|
onAICall
|
|
322078
|
-
});
|
|
322595
|
+
}));
|
|
322079
322596
|
logs2.push(...planResult.logs);
|
|
322080
322597
|
log2(`
|
|
322081
322598
|
Plan: ${planResult.scenarios.length} scenarios, ${planResult.featureGroups.length} feature groups (budget: ${testBudget})`);
|
|
@@ -322933,6 +323450,7 @@ Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.leng
|
|
|
322933
323450
|
if (surveyAuthRecipe) {
|
|
322934
323451
|
logs2.push(`Auth-prologue: captured + verified login recipe ${surveyAuthRecipe.recipeHash} during survey \u2014 persisting for reuse on later runs`);
|
|
322935
323452
|
}
|
|
323453
|
+
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;
|
|
322936
323454
|
return {
|
|
322937
323455
|
observedOrigins: mergeObservedOrigins(exploreResult.observedOrigins, loginObservedOrigins),
|
|
322938
323456
|
resolvedAppOrigin: surveyAuthRecipe?.appOrigin ?? void 0,
|
|
@@ -322948,7 +323466,8 @@ Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.leng
|
|
|
322948
323466
|
exploreTruncatedReason: exploreResult.truncatedReason,
|
|
322949
323467
|
fallbackUsed: planResult.fallbackUsed ?? false,
|
|
322950
323468
|
fallbackReason: planResult.fallbackReason,
|
|
322951
|
-
|
|
323469
|
+
error: plannerFailedError,
|
|
323470
|
+
featureGroups: plannerFailedError ? void 0 : featureGroups,
|
|
322952
323471
|
apiCalls: discoveryApiCalls.length > 0 ? discoveryApiCalls : void 0,
|
|
322953
323472
|
healthObservations: surveyHealth,
|
|
322954
323473
|
exploreFinalMessages: exploreResult.finalMessages,
|
|
@@ -336806,7 +337325,7 @@ var require_package4 = __commonJS({
|
|
|
336806
337325
|
"package.json"(exports, module) {
|
|
336807
337326
|
module.exports = {
|
|
336808
337327
|
name: "@validate.qa/runner",
|
|
336809
|
-
version: "1.0.
|
|
337328
|
+
version: "1.0.41",
|
|
336810
337329
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
336811
337330
|
bin: {
|
|
336812
337331
|
"validate-runner": "dist/cli.js"
|
|
@@ -336815,7 +337334,7 @@ var require_package4 = __commonJS({
|
|
|
336815
337334
|
build: "tsup",
|
|
336816
337335
|
dev: "tsx src/cli.ts",
|
|
336817
337336
|
test: "node --import tsx --test src/version-check.test.ts",
|
|
336818
|
-
"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",
|
|
337337
|
+
"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",
|
|
336819
337338
|
prepublishOnly: "npm run build"
|
|
336820
337339
|
},
|
|
336821
337340
|
files: [
|
|
@@ -342966,6 +343485,17 @@ var MobileBridgeDriver = class {
|
|
|
342966
343485
|
return size;
|
|
342967
343486
|
});
|
|
342968
343487
|
}
|
|
343488
|
+
/**
|
|
343489
|
+
* Recovery-free liveness probe for keep-alive ticks (mobile-session-keepalive).
|
|
343490
|
+
* Deliberately NOT wrapped in withSessionRecovery: keep-alive ticks are
|
|
343491
|
+
* fire-and-forget, so a failing probe must never consume the bounded recovery
|
|
343492
|
+
* budget or relaunch the app from the background — a genuinely dead session
|
|
343493
|
+
* surfaces (with its precise error) on the caller's next REAL command, which
|
|
343494
|
+
* still runs the full recovery path.
|
|
343495
|
+
*/
|
|
343496
|
+
async keepAlivePing() {
|
|
343497
|
+
await this.assertSessionReachable();
|
|
343498
|
+
}
|
|
342969
343499
|
// ── Action result wrapper ──────────────────────────────
|
|
342970
343500
|
/**
|
|
342971
343501
|
* Run an action body, let the UI tree settle, then capture the post-action
|
|
@@ -345523,6 +346053,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345523
346053
|
setExecutorBusy(true);
|
|
345524
346054
|
let session = null;
|
|
345525
346055
|
let capture = null;
|
|
346056
|
+
let discoveryTeardownStarted = false;
|
|
345526
346057
|
try {
|
|
345527
346058
|
const discoverySessionTarget = {
|
|
345528
346059
|
appId: mobileTarget.appId,
|
|
@@ -345574,6 +346105,9 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345574
346105
|
allowAppReset: mobileTarget.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(platform3, session.deviceId),
|
|
345575
346106
|
maxSessionRecoveries: 1,
|
|
345576
346107
|
recoverSession: async (cause) => {
|
|
346108
|
+
if (discoveryTeardownStarted) {
|
|
346109
|
+
throw new Error("discovery teardown already started \u2014 not recreating the Appium session");
|
|
346110
|
+
}
|
|
345577
346111
|
const lost = session;
|
|
345578
346112
|
mobileOnLog(
|
|
345579
346113
|
`[appium] discovery session lost (${cause.message}); recreating once on the leased device`
|
|
@@ -345587,6 +346121,13 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345587
346121
|
}
|
|
345588
346122
|
}
|
|
345589
346123
|
const replacement = await openDiscoverySession();
|
|
346124
|
+
if (discoveryTeardownStarted) {
|
|
346125
|
+
try {
|
|
346126
|
+
await replacement.stop();
|
|
346127
|
+
} catch {
|
|
346128
|
+
}
|
|
346129
|
+
throw new Error("discovery teardown started during session recovery \u2014 replacement session closed");
|
|
346130
|
+
}
|
|
345590
346131
|
session = replacement;
|
|
345591
346132
|
attachMirrorSession(replacement.sessionId, platform3, replacement.deviceId);
|
|
345592
346133
|
liveBrowserHandle.patch({
|
|
@@ -345626,10 +346167,23 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345626
346167
|
onPlanReady
|
|
345627
346168
|
});
|
|
345628
346169
|
} finally {
|
|
346170
|
+
discoveryTeardownStarted = true;
|
|
346171
|
+
const TEARDOWN_STEP_TIMEOUT = /* @__PURE__ */ Symbol("vqa-teardown-timeout");
|
|
346172
|
+
const timeboxed = async (step) => Promise.race([
|
|
346173
|
+
step,
|
|
346174
|
+
new Promise((resolve2) => {
|
|
346175
|
+
const t = setTimeout(() => resolve2(TEARDOWN_STEP_TIMEOUT), 45e3);
|
|
346176
|
+
t.unref?.();
|
|
346177
|
+
})
|
|
346178
|
+
]);
|
|
345629
346179
|
const activeCapture = capture;
|
|
345630
346180
|
if (activeCapture) {
|
|
345631
346181
|
try {
|
|
345632
|
-
const
|
|
346182
|
+
const stopped = await timeboxed(activeCapture.stop());
|
|
346183
|
+
if (stopped === TEARDOWN_STEP_TIMEOUT) {
|
|
346184
|
+
throw new Error("capture stop timed out after 45s \u2014 continuing teardown without apiCalls");
|
|
346185
|
+
}
|
|
346186
|
+
const apiCalls = stopped;
|
|
345633
346187
|
const captureHealth = activeCapture.health();
|
|
345634
346188
|
if (discoveryResult) {
|
|
345635
346189
|
discoveryResult.apiCalls = apiCalls.length ? attachMobileDiscoveryNetworkContext(apiCalls, discoveryResult, session?.appId ?? mobileTarget.appId) : void 0;
|
|
@@ -345649,8 +346203,12 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345649
346203
|
}
|
|
345650
346204
|
if (session) {
|
|
345651
346205
|
detachMirrorSession(session.deviceId);
|
|
346206
|
+
const closing = session;
|
|
345652
346207
|
try {
|
|
345653
|
-
await
|
|
346208
|
+
const stopped = await timeboxed(closing.stop());
|
|
346209
|
+
if (stopped === TEARDOWN_STEP_TIMEOUT) {
|
|
346210
|
+
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`);
|
|
346211
|
+
}
|
|
345654
346212
|
} catch {
|
|
345655
346213
|
}
|
|
345656
346214
|
}
|
|
@@ -346088,7 +346646,13 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
346088
346646
|
relaunch: async () => {
|
|
346089
346647
|
await currentDriver().relaunch();
|
|
346090
346648
|
},
|
|
346091
|
-
resetAppState: async () => target.resetPolicy === "PRESERVE_APP_DATA" ? false : currentDriver().resetAppState()
|
|
346649
|
+
resetAppState: async () => target.resetPolicy === "PRESERVE_APP_DATA" ? false : currentDriver().resetAppState(),
|
|
346650
|
+
// Recovery-free liveness probe so the loop's keep-alive can span
|
|
346651
|
+
// long AI waits without touching the driver's recovery machinery
|
|
346652
|
+
// (see mobile-session-keepalive.ts).
|
|
346653
|
+
keepAlivePing: async () => {
|
|
346654
|
+
await currentDriver().keepAlivePing();
|
|
346655
|
+
}
|
|
346092
346656
|
};
|
|
346093
346657
|
const runNativeWithSessionHandoff = async (verifyArgs) => {
|
|
346094
346658
|
await closeLiveSession();
|