@validate.qa/runner 1.0.39 → 1.0.40
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 +389 -33
- package/dist/cli.mjs +389 -33
- package/package.json +1 -1
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 {};
|
|
@@ -3307,6 +3309,7 @@ var init_audit_pricing = __esm({
|
|
|
3307
3309
|
"MiniMax-M2.7-highspeed": { input: 0.6, output: 2.4 },
|
|
3308
3310
|
"MiniMax-M2": { input: 0.3, output: 1.2 },
|
|
3309
3311
|
"deepseek-v4-flash": { input: 0.14, output: 0.28 },
|
|
3312
|
+
"deepseek-v4-pro": { input: 0.42, output: 0.84 },
|
|
3310
3313
|
// Whisper bills per-minute, not per-token (OpenAI + Groq)
|
|
3311
3314
|
"whisper-1": null,
|
|
3312
3315
|
"whisper-large-v3": null,
|
|
@@ -5884,6 +5887,34 @@ var require_ai_queue = __commonJS({
|
|
|
5884
5887
|
}
|
|
5885
5888
|
});
|
|
5886
5889
|
|
|
5890
|
+
// ../runner-core/dist/services/mobile/mobile-session-keepalive.js
|
|
5891
|
+
var require_mobile_session_keepalive = __commonJS({
|
|
5892
|
+
"../runner-core/dist/services/mobile/mobile-session-keepalive.js"(exports) {
|
|
5893
|
+
"use strict";
|
|
5894
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5895
|
+
exports.MOBILE_KEEPALIVE_INTERVAL_MS = void 0;
|
|
5896
|
+
exports.withMobileSessionKeepAlive = withMobileSessionKeepAlive;
|
|
5897
|
+
exports.MOBILE_KEEPALIVE_INTERVAL_MS = (() => {
|
|
5898
|
+
const raw = Number(process.env.VALIDATEQA_MOBILE_KEEPALIVE_MS);
|
|
5899
|
+
return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 12e4;
|
|
5900
|
+
})();
|
|
5901
|
+
async function withMobileSessionKeepAlive(driver, fn, intervalMs = exports.MOBILE_KEEPALIVE_INTERVAL_MS) {
|
|
5902
|
+
if (intervalMs <= 0)
|
|
5903
|
+
return fn();
|
|
5904
|
+
const timer = setInterval(() => {
|
|
5905
|
+
void driver.windowSize().catch(() => {
|
|
5906
|
+
});
|
|
5907
|
+
}, intervalMs);
|
|
5908
|
+
timer.unref?.();
|
|
5909
|
+
try {
|
|
5910
|
+
return await fn();
|
|
5911
|
+
} finally {
|
|
5912
|
+
clearInterval(timer);
|
|
5913
|
+
}
|
|
5914
|
+
}
|
|
5915
|
+
}
|
|
5916
|
+
});
|
|
5917
|
+
|
|
5887
5918
|
// ../runner-core/dist/services/mobile/mobile-observation.js
|
|
5888
5919
|
var require_mobile_observation = __commonJS({
|
|
5889
5920
|
"../runner-core/dist/services/mobile/mobile-observation.js"(exports) {
|
|
@@ -64617,6 +64648,7 @@ var require_mobile_explorer = __commonJS({
|
|
|
64617
64648
|
var ai_provider_js_1 = require_ai_provider();
|
|
64618
64649
|
var ai_queue_js_1 = require_ai_queue();
|
|
64619
64650
|
var ai_retry_js_1 = require_ai_retry();
|
|
64651
|
+
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
64620
64652
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
64621
64653
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
64622
64654
|
var mobile_exploration_state_js_1 = require_mobile_exploration_state();
|
|
@@ -66126,7 +66158,7 @@ ${statePacket}` }
|
|
|
66126
66158
|
const requestMessageCount = messages.length;
|
|
66127
66159
|
const requestMessages = messages.slice();
|
|
66128
66160
|
const callStart = Date.now();
|
|
66129
|
-
const response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
66161
|
+
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
66162
|
arm();
|
|
66131
66163
|
return aiClient.chat.completions.create({
|
|
66132
66164
|
model: agentModel,
|
|
@@ -66139,7 +66171,7 @@ ${statePacket}` }
|
|
|
66139
66171
|
label: `mobile-explore-iter-${iteration}`,
|
|
66140
66172
|
deferTimeoutUntilArmed: true,
|
|
66141
66173
|
onRetry: (attempt, reason, delayMs) => log2(` \u26A0 mobile explore AI retry ${attempt} (${reason}) \u2014 waiting ${delayMs}ms`)
|
|
66142
|
-
});
|
|
66174
|
+
}));
|
|
66143
66175
|
const iterDuration = Date.now() - callStart;
|
|
66144
66176
|
if (!response?.choices?.[0]?.message) {
|
|
66145
66177
|
throw new Error(`AI explore call returned no response (queue timeout or provider overload) at iteration ${iteration}`);
|
|
@@ -68218,6 +68250,7 @@ var require_discover_ai_planner = __commonJS({
|
|
|
68218
68250
|
"use strict";
|
|
68219
68251
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
68220
68252
|
exports.SCENARIO_SYSTEM_PROMPT = void 0;
|
|
68253
|
+
exports.getAIPlannerMaxTokens = getAIPlannerMaxTokens;
|
|
68221
68254
|
exports.getSnapshotConfidence = getSnapshotConfidence;
|
|
68222
68255
|
exports.compressSiteMap = compressSiteMap;
|
|
68223
68256
|
exports.formatEvidenceForPlanner = formatEvidenceForPlanner;
|
|
@@ -68229,10 +68262,25 @@ var require_discover_ai_planner = __commonJS({
|
|
|
68229
68262
|
var ai_retry_js_1 = require_ai_retry();
|
|
68230
68263
|
var strip_json_fences_js_1 = require_strip_json_fences();
|
|
68231
68264
|
var scenario_plan_validator_js_1 = require_scenario_plan_validator();
|
|
68232
|
-
var
|
|
68233
|
-
var
|
|
68265
|
+
var GENEROUS_AI_PLANNER_MAX_TOKENS = 65536;
|
|
68266
|
+
var CONSERVATIVE_AI_PLANNER_MAX_TOKENS = 16384;
|
|
68234
68267
|
function getAIPlannerMaxTokens(modelId) {
|
|
68235
|
-
|
|
68268
|
+
switch ((0, ai_provider_js_1.getProviderForModel)(modelId)) {
|
|
68269
|
+
case "deepseek":
|
|
68270
|
+
// V4 output ceiling = 384K — verified safe
|
|
68271
|
+
case "minimax":
|
|
68272
|
+
return GENEROUS_AI_PLANNER_MAX_TOKENS;
|
|
68273
|
+
case "xai":
|
|
68274
|
+
// grok-build-0.1 ceiling unproven (Grok-Fast/3 = 16K) — DON'T risk a 400
|
|
68275
|
+
case "openai":
|
|
68276
|
+
// gpt-4o max output = 16384 → more would 400
|
|
68277
|
+
case "anthropic":
|
|
68278
|
+
// OpenAI-compat proxy — keep conservative
|
|
68279
|
+
case "nvidia":
|
|
68280
|
+
// NIM output limits vary — keep conservative
|
|
68281
|
+
default:
|
|
68282
|
+
return CONSERVATIVE_AI_PLANNER_MAX_TOKENS;
|
|
68283
|
+
}
|
|
68236
68284
|
}
|
|
68237
68285
|
var discover_planner_js_1 = require_discover_planner();
|
|
68238
68286
|
function getSnapshotConfidence(sourceKind) {
|
|
@@ -68784,7 +68832,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
68784
68832
|
4. **Multi-page journeys** - only when transitions were actually observed
|
|
68785
68833
|
5. **Bug reports** - for observed 500 errors, crashes, or broken features
|
|
68786
68834
|
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.
|
|
68835
|
+
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
68836
|
8. **Auth-aware** - mark scenarios that need login. Use process.env.TEST_USER_EMAIL / PASSWORD
|
|
68789
68837
|
9. **No duplicates** - check existing test names before adding
|
|
68790
68838
|
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 +68884,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
68836
68884
|
**YOUR responsibility:**
|
|
68837
68885
|
- Scenario \`name\`, \`type\`, \`goal\`, \`prerequisites\`
|
|
68838
68886
|
- \`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\`
|
|
68887
|
+
- \`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
68888
|
|
|
68841
68889
|
**NOT your responsibility:**
|
|
68842
68890
|
- Exact Playwright selectors (\`getByRole(...)\`, \`getByText(...)\`, \`page.locator(...)\`, etc.) \u2014 live-generate picks locators against the live snapshot
|
|
@@ -68857,7 +68905,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
68857
68905
|
- GOOD specificity: "create a new conversation by sending a message to a connection established earlier in this scenario"
|
|
68858
68906
|
- BAD specificity: "click the button labeled exactly 'Send' in the bottom-right of the chat"
|
|
68859
68907
|
|
|
68860
|
-
Citing observed labels in \`expectedOutcomes\`
|
|
68908
|
+
Citing observed labels in \`expectedOutcomes\` is fine \u2014 that's asserting on stable product signals, not implementation specifics.
|
|
68861
68909
|
|
|
68862
68910
|
## Coverage Strategy
|
|
68863
68911
|
- 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 +69491,24 @@ Each entry is exactly one of:
|
|
|
69443
69491
|
}
|
|
69444
69492
|
async function runAIPlanner(siteMap, evidence, context, options) {
|
|
69445
69493
|
const logs2 = [];
|
|
69494
|
+
const failurePlan = (kind, reason, legacyFallback2) => {
|
|
69495
|
+
const noFallback = context.mode === "survey" && process.env.DISCOVERY_PLANNER_NO_FALLBACK === "true";
|
|
69496
|
+
if (noFallback) {
|
|
69497
|
+
logs2.push(`Planner ${kind} \u2014 NO-FALLBACK: failing survey cleanly instead of fabricating slug features (${reason})`);
|
|
69498
|
+
return {
|
|
69499
|
+
scenarios: [],
|
|
69500
|
+
featureGroups: [],
|
|
69501
|
+
reasoning: "",
|
|
69502
|
+
logs: logs2,
|
|
69503
|
+
coverageGaps: [],
|
|
69504
|
+
plannerFailed: true,
|
|
69505
|
+
plannerFailureKind: kind,
|
|
69506
|
+
fallbackReason: reason
|
|
69507
|
+
};
|
|
69508
|
+
}
|
|
69509
|
+
return { ...legacyFallback2(), fallbackReason: reason };
|
|
69510
|
+
};
|
|
69511
|
+
let extraPlannerCallUsed = false;
|
|
69446
69512
|
const compressed = compressSiteMap(siteMap);
|
|
69447
69513
|
const agentModel = options.modelOverride ? (0, ai_provider_js_1.resolveModelForTask)(options.modelOverride, "agent") : (0, ai_provider_js_1.getModel)("agent");
|
|
69448
69514
|
const plannerMaxTokens = getAIPlannerMaxTokens(agentModel);
|
|
@@ -69478,7 +69544,7 @@ Each entry is exactly one of:
|
|
|
69478
69544
|
} catch (err) {
|
|
69479
69545
|
const message = err instanceof Error ? err.message : String(err);
|
|
69480
69546
|
logs2.push(`AI planner failed after retries: ${message}`);
|
|
69481
|
-
return
|
|
69547
|
+
return failurePlan("transport", (0, ai_retry_js_1.describeAIFailure)(err), () => buildFallbackPlan(siteMap, evidence.recordedJourneys, context.mode, logs2, context.existingTestNames ?? [], context.focusAreas, context.testBudget));
|
|
69482
69548
|
}
|
|
69483
69549
|
try {
|
|
69484
69550
|
options.onAICall?.({
|
|
@@ -69497,20 +69563,104 @@ Each entry is exactly one of:
|
|
|
69497
69563
|
});
|
|
69498
69564
|
} catch {
|
|
69499
69565
|
}
|
|
69500
|
-
const
|
|
69501
|
-
|
|
69566
|
+
const choice = response?.choices?.[0];
|
|
69567
|
+
const raw = choice?.message?.content;
|
|
69568
|
+
const finishReason = (choice?.finish_reason ?? "").toString().toLowerCase();
|
|
69569
|
+
const completionTokens = response.usage?.completion_tokens;
|
|
69570
|
+
const TRUNCATION_FINISH = /* @__PURE__ */ new Set(["length", "max_tokens", "max_output_tokens", "token_limit"]);
|
|
69571
|
+
const definitelyTruncated = TRUNCATION_FINISH.has(finishReason);
|
|
69572
|
+
const atOrOverCap = completionTokens != null && completionTokens >= plannerMaxTokens;
|
|
69573
|
+
const blocked = finishReason === "content_filter" || finishReason === "tool_calls";
|
|
69574
|
+
const usage = response.usage;
|
|
69575
|
+
logs2.push(`AI planner: ${usage?.prompt_tokens ?? "?"} input, ${usage?.completion_tokens ?? "?"} output tokens (finish=${finishReason || "?"})`);
|
|
69576
|
+
const legacyFallback = () => buildFallbackPlan(siteMap, evidence.recordedJourneys, context.mode, logs2, context.existingTestNames ?? [], context.focusAreas, context.testBudget);
|
|
69577
|
+
if (blocked) {
|
|
69578
|
+
return failurePlan("blocked", "AI response was blocked (content_filter/tool_calls)", legacyFallback);
|
|
69579
|
+
}
|
|
69580
|
+
if (definitelyTruncated) {
|
|
69581
|
+
return failurePlan("truncated", "AI planner output truncated at max_tokens (finish_reason=length)", legacyFallback);
|
|
69582
|
+
}
|
|
69583
|
+
const trimmed = raw?.trim();
|
|
69584
|
+
if (!trimmed) {
|
|
69502
69585
|
logs2.push("AI planner returned empty response");
|
|
69503
|
-
return
|
|
69586
|
+
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
69587
|
}
|
|
69505
|
-
const usage = response.usage;
|
|
69506
|
-
logs2.push(`AI planner: ${usage?.prompt_tokens ?? "?"} input, ${usage?.completion_tokens ?? "?"} output tokens`);
|
|
69507
69588
|
let aiOutput;
|
|
69508
69589
|
try {
|
|
69509
|
-
aiOutput = JSON.parse((0, strip_json_fences_js_1.stripJsonFences)(
|
|
69590
|
+
aiOutput = JSON.parse((0, strip_json_fences_js_1.stripJsonFences)(trimmed));
|
|
69510
69591
|
} catch (err) {
|
|
69511
69592
|
const message = err instanceof Error ? err.message : String(err);
|
|
69512
69593
|
logs2.push(`AI planner returned invalid JSON: ${message}`);
|
|
69513
|
-
|
|
69594
|
+
if (atOrOverCap) {
|
|
69595
|
+
logs2.push("AI planner output at/over the token cap and unparseable \u2014 treating as truncated (skipping repair)");
|
|
69596
|
+
return failurePlan("truncated", "AI planner output truncated at max_tokens (unparseable at cap)", legacyFallback);
|
|
69597
|
+
}
|
|
69598
|
+
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.`;
|
|
69599
|
+
const repairStart = Date.now();
|
|
69600
|
+
let repairResponse = null;
|
|
69601
|
+
try {
|
|
69602
|
+
extraPlannerCallUsed = true;
|
|
69603
|
+
repairResponse = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
69604
|
+
arm();
|
|
69605
|
+
return aiClient.chat.completions.create({
|
|
69606
|
+
model: agentModel,
|
|
69607
|
+
messages: [
|
|
69608
|
+
...messages,
|
|
69609
|
+
{ role: "assistant", content: raw },
|
|
69610
|
+
{ role: "user", content: repairPrompt }
|
|
69611
|
+
],
|
|
69612
|
+
max_tokens: plannerMaxTokens,
|
|
69613
|
+
temperature: 0.3,
|
|
69614
|
+
...(0, ai_provider_js_1.getJsonObjectResponseFormat)(agentModel),
|
|
69615
|
+
...(0, ai_provider_js_1.getNoThinkingRequestOptions)(agentModel)
|
|
69616
|
+
}, { signal });
|
|
69617
|
+
}), {
|
|
69618
|
+
label: "ai-planner-repair",
|
|
69619
|
+
deferTimeoutUntilArmed: true,
|
|
69620
|
+
onRetry: (attempt, reason, delay) => logs2.push(`AI planner repair call failed (${reason}) \u2014 retry ${attempt} in ${Math.round(delay / 1e3)}s`)
|
|
69621
|
+
});
|
|
69622
|
+
} catch (repairErr) {
|
|
69623
|
+
const repairMessage = repairErr instanceof Error ? repairErr.message : String(repairErr);
|
|
69624
|
+
logs2.push(`AI planner repair failed (${repairMessage})`);
|
|
69625
|
+
}
|
|
69626
|
+
if (repairResponse) {
|
|
69627
|
+
try {
|
|
69628
|
+
options.onAICall?.({
|
|
69629
|
+
iteration: 1,
|
|
69630
|
+
groupId: `ai-planner-${context.projectId.slice(0, 8)}`,
|
|
69631
|
+
callType: "discoveryAIPlannerRepair",
|
|
69632
|
+
model: agentModel,
|
|
69633
|
+
provider: (0, ai_provider_js_1.getProviderForModel)(agentModel),
|
|
69634
|
+
durationMs: Date.now() - repairStart,
|
|
69635
|
+
promptTokens: repairResponse.usage?.prompt_tokens,
|
|
69636
|
+
completionTokens: repairResponse.usage?.completion_tokens,
|
|
69637
|
+
totalTokens: repairResponse.usage?.total_tokens,
|
|
69638
|
+
assistantMessage: repairResponse.choices[0]?.message?.content,
|
|
69639
|
+
requestMessageCount: messages.length + 2,
|
|
69640
|
+
requestMessages: [
|
|
69641
|
+
...messages,
|
|
69642
|
+
{ role: "assistant", content: raw },
|
|
69643
|
+
{ role: "user", content: repairPrompt }
|
|
69644
|
+
]
|
|
69645
|
+
});
|
|
69646
|
+
} catch {
|
|
69647
|
+
}
|
|
69648
|
+
const repairedRaw = repairResponse.choices[0]?.message?.content?.trim();
|
|
69649
|
+
if (repairedRaw) {
|
|
69650
|
+
try {
|
|
69651
|
+
aiOutput = JSON.parse((0, strip_json_fences_js_1.stripJsonFences)(repairedRaw));
|
|
69652
|
+
} catch (repairParseErr) {
|
|
69653
|
+
const repairMessage = repairParseErr instanceof Error ? repairParseErr.message : String(repairParseErr);
|
|
69654
|
+
logs2.push(`AI planner repair returned invalid JSON (${repairMessage})`);
|
|
69655
|
+
return failurePlan("malformed", "AI returned unreadable output", legacyFallback);
|
|
69656
|
+
}
|
|
69657
|
+
} else {
|
|
69658
|
+
logs2.push("AI planner repair returned empty response");
|
|
69659
|
+
return failurePlan("malformed", "AI returned unreadable output", legacyFallback);
|
|
69660
|
+
}
|
|
69661
|
+
} else {
|
|
69662
|
+
return failurePlan("malformed", "AI returned unreadable output", legacyFallback);
|
|
69663
|
+
}
|
|
69514
69664
|
}
|
|
69515
69665
|
let { scenarios, featureGroups, warnings, coverageGaps } = validateAIPlannerOutput(aiOutput, siteMap, context.existingTestNames, context.testBudget, evidence.recordedJourneys, context.mode, context.focusAreas);
|
|
69516
69666
|
logs2.push(`AI planner: ${scenarios.length} scenarios, ${featureGroups.length} feature groups (${warnings.length} warnings, ${coverageGaps.length} coverage gaps)`);
|
|
@@ -69520,7 +69670,7 @@ Each entry is exactly one of:
|
|
|
69520
69670
|
let topLevelWarning;
|
|
69521
69671
|
const totalV1 = scenarios.length + coverageGaps.length;
|
|
69522
69672
|
const rejectionRateV1 = totalV1 > 0 ? coverageGaps.length / totalV1 : 0;
|
|
69523
|
-
if (rejectionRateV1 > 0.5 && totalV1 >= 3) {
|
|
69673
|
+
if (rejectionRateV1 > 0.5 && totalV1 >= 3 && !extraPlannerCallUsed) {
|
|
69524
69674
|
logs2.push(`AI planner: ${Math.round(rejectionRateV1 * 100)}% rejection rate (${coverageGaps.length}/${totalV1}) \u2014 running self-correction retry`);
|
|
69525
69675
|
const retryContext = [
|
|
69526
69676
|
`Your previous emission produced ${coverageGaps.length} coverage gaps out of ${totalV1} scenarios (${Math.round(rejectionRateV1 * 100)}% rejection rate).`,
|
|
@@ -69539,6 +69689,7 @@ Each entry is exactly one of:
|
|
|
69539
69689
|
const retryStart = Date.now();
|
|
69540
69690
|
let retryResponse = null;
|
|
69541
69691
|
try {
|
|
69692
|
+
extraPlannerCallUsed = true;
|
|
69542
69693
|
retryResponse = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
69543
69694
|
arm();
|
|
69544
69695
|
return aiClient.chat.completions.create({
|
|
@@ -69638,6 +69789,22 @@ Each entry is exactly one of:
|
|
|
69638
69789
|
};
|
|
69639
69790
|
}
|
|
69640
69791
|
if (context.mode === "survey" && featureGroups.length === 0 && siteMap.pages.length > 0) {
|
|
69792
|
+
const hasGroupWorthyEvidence = siteMap.pages.some((p) => (p.forms?.length ?? 0) > 0 || (p.elements?.length ?? 0) > 0);
|
|
69793
|
+
const noFallback = process.env.DISCOVERY_PLANNER_NO_FALLBACK === "true";
|
|
69794
|
+
if (noFallback && !hasGroupWorthyEvidence) {
|
|
69795
|
+
return {
|
|
69796
|
+
scenarios,
|
|
69797
|
+
featureGroups: [],
|
|
69798
|
+
reasoning: aiOutput.reasoning ?? "",
|
|
69799
|
+
logs: logs2,
|
|
69800
|
+
coverageGaps,
|
|
69801
|
+
warning: topLevelWarning,
|
|
69802
|
+
fallbackUsed: false
|
|
69803
|
+
};
|
|
69804
|
+
}
|
|
69805
|
+
if (noFallback) {
|
|
69806
|
+
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));
|
|
69807
|
+
}
|
|
69641
69808
|
logs2.push("Survey AI returned 0 feature groups - falling back to deterministic grouping");
|
|
69642
69809
|
const fallback = buildFallbackPlan(siteMap, evidence.recordedJourneys, context.mode, logs2, context.existingTestNames ?? [], context.focusAreas, context.testBudget);
|
|
69643
69810
|
return {
|
|
@@ -70848,6 +71015,7 @@ var require_mobile_generator = __commonJS({
|
|
|
70848
71015
|
var ai_provider_js_1 = require_ai_provider();
|
|
70849
71016
|
var ai_queue_js_1 = require_ai_queue();
|
|
70850
71017
|
var ai_retry_js_1 = require_ai_retry();
|
|
71018
|
+
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
70851
71019
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
70852
71020
|
var mobile_source_parser_js_2 = require_mobile_source_parser();
|
|
70853
71021
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
@@ -71755,14 +71923,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
71755
71923
|
const requestMessageCount = messages.length;
|
|
71756
71924
|
const requestMessages = messages.slice();
|
|
71757
71925
|
const callStart = Date.now();
|
|
71758
|
-
const response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
71926
|
+
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
71927
|
arm();
|
|
71760
71928
|
return aiClient.chat.completions.create({ model: agentModel, messages, tools: GENERATE_TOOLS, tool_choice: "auto", max_tokens: 8e3 }, { signal });
|
|
71761
71929
|
}), {
|
|
71762
71930
|
label: `mobile-generate-${scenario.name.slice(0, 24)}-iter-${iteration}`,
|
|
71763
71931
|
deferTimeoutUntilArmed: true,
|
|
71764
71932
|
onRetry: (attempt, reason, delayMs) => log2(` \u26A0 mobile generate AI retry ${attempt} (${reason}) \u2014 waiting ${delayMs}ms`)
|
|
71765
|
-
});
|
|
71933
|
+
}));
|
|
71766
71934
|
const iterDuration = Date.now() - callStart;
|
|
71767
71935
|
if (!response?.choices?.[0]?.message) {
|
|
71768
71936
|
throw new Error(`AI generate call returned no response at iteration ${iteration}`);
|
|
@@ -72374,6 +72542,21 @@ ${statePacket}`, redactionCredentials) },
|
|
|
72374
72542
|
};
|
|
72375
72543
|
}
|
|
72376
72544
|
const metadata = metadataForElement(element, screenName(), platform3);
|
|
72545
|
+
const expectedNeedle = expected.trim().toLowerCase();
|
|
72546
|
+
const identityValues = [element.accessibilityId, element.resourceId].map((value) => value?.trim().toLowerCase()).filter((value) => Boolean(value));
|
|
72547
|
+
const visibleText = (element.text ?? "").trim().toLowerCase();
|
|
72548
|
+
const isSelfLabelAssertion = expectedNeedle.length > 0 && identityValues.includes(expectedNeedle) && !visibleText.includes(expectedNeedle);
|
|
72549
|
+
if (isSelfLabelAssertion) {
|
|
72550
|
+
recordAction({ type: "ASSERT_VISIBLE", selector: (0, mobile_source_parser_js_2.preferredLocator)(element), metadata });
|
|
72551
|
+
log2(` assert_text DOWNGRADED to assert_visible (tautological self-label "${expected}"): ${metadata.targetLabel}`);
|
|
72552
|
+
return {
|
|
72553
|
+
ok: true,
|
|
72554
|
+
asserted: "visible",
|
|
72555
|
+
downgraded: true,
|
|
72556
|
+
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.`,
|
|
72557
|
+
target: metadata.targetLabel
|
|
72558
|
+
};
|
|
72559
|
+
}
|
|
72377
72560
|
if (hasStableId(element)) {
|
|
72378
72561
|
recordAction({ type: "ASSERT_TEXT", value: expected, selector: (0, mobile_source_parser_js_2.preferredLocator)(element), metadata });
|
|
72379
72562
|
log2(` assert_text: ${metadata.targetLabel} ~ "${expected}"`);
|
|
@@ -73067,7 +73250,13 @@ ${statePacket}`, redactionCredentials) },
|
|
|
73067
73250
|
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
73251
|
const test = {
|
|
73069
73252
|
name: compiled.name,
|
|
73070
|
-
|
|
73253
|
+
// Description priority: the model's live finish description, then the
|
|
73254
|
+
// planner's scenario goal (always a meaningful sentence describing what the
|
|
73255
|
+
// test proves), then the generic compiled placeholder. Without the goal
|
|
73256
|
+
// fallback a scenario whose finish_generate omitted a description shipped
|
|
73257
|
+
// with the contentless "<platform> native flow generated from a recorded
|
|
73258
|
+
// mobile session." string — see mobile-test-generation.ts's compiled default.
|
|
73259
|
+
description: recording.finish?.description?.trim() || scenario.goal?.trim() || compiled.description,
|
|
73071
73260
|
framework: "APPIUM",
|
|
73072
73261
|
appiumCode: compiled.appiumCode,
|
|
73073
73262
|
steps: compiled.steps,
|
|
@@ -75744,6 +75933,118 @@ var require_har_stream = __commonJS({
|
|
|
75744
75933
|
}
|
|
75745
75934
|
});
|
|
75746
75935
|
|
|
75936
|
+
// ../runner-core/dist/services/api-auth-headers.js
|
|
75937
|
+
var require_api_auth_headers = __commonJS({
|
|
75938
|
+
"../runner-core/dist/services/api-auth-headers.js"(exports) {
|
|
75939
|
+
"use strict";
|
|
75940
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
75941
|
+
exports.parseApiAuthHeaders = parseApiAuthHeaders;
|
|
75942
|
+
exports.resolveApiAuthHeaders = resolveApiAuthHeaders;
|
|
75943
|
+
exports.isAppOriginRequest = isAppOriginRequest;
|
|
75944
|
+
exports.buildOriginScopedApiFixtures = buildOriginScopedApiFixtures;
|
|
75945
|
+
var HEADER_NAME_RE = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
|
|
75946
|
+
function hasControlChar(v) {
|
|
75947
|
+
for (let i = 0; i < v.length; i++) {
|
|
75948
|
+
const code = v.charCodeAt(i);
|
|
75949
|
+
if (code < 32 || code === 127)
|
|
75950
|
+
return true;
|
|
75951
|
+
}
|
|
75952
|
+
return false;
|
|
75953
|
+
}
|
|
75954
|
+
function parseApiAuthHeaders(raw) {
|
|
75955
|
+
if (!raw || typeof raw !== "string")
|
|
75956
|
+
return void 0;
|
|
75957
|
+
const trimmed = raw.trim();
|
|
75958
|
+
if (!trimmed)
|
|
75959
|
+
return void 0;
|
|
75960
|
+
try {
|
|
75961
|
+
const parsed = JSON.parse(trimmed);
|
|
75962
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
75963
|
+
return void 0;
|
|
75964
|
+
const headers = {};
|
|
75965
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
75966
|
+
if (typeof k === "string" && HEADER_NAME_RE.test(k) && typeof v === "string" && !hasControlChar(v)) {
|
|
75967
|
+
headers[k] = v;
|
|
75968
|
+
}
|
|
75969
|
+
}
|
|
75970
|
+
return Object.keys(headers).length > 0 ? headers : void 0;
|
|
75971
|
+
} catch {
|
|
75972
|
+
return void 0;
|
|
75973
|
+
}
|
|
75974
|
+
}
|
|
75975
|
+
function resolveApiAuthHeaders(input) {
|
|
75976
|
+
if (input.explicit)
|
|
75977
|
+
return input.explicit;
|
|
75978
|
+
if (!input.isApiTest)
|
|
75979
|
+
return void 0;
|
|
75980
|
+
return parseApiAuthHeaders(input.apiAuthHeadersVar);
|
|
75981
|
+
}
|
|
75982
|
+
function isAppOriginRequest(url, baseUrl) {
|
|
75983
|
+
if (typeof url !== "string" || !baseUrl)
|
|
75984
|
+
return false;
|
|
75985
|
+
let appOrigin;
|
|
75986
|
+
try {
|
|
75987
|
+
appOrigin = new URL(baseUrl).origin;
|
|
75988
|
+
} catch {
|
|
75989
|
+
return false;
|
|
75990
|
+
}
|
|
75991
|
+
try {
|
|
75992
|
+
return new URL(url, baseUrl).origin === appOrigin;
|
|
75993
|
+
} catch {
|
|
75994
|
+
return false;
|
|
75995
|
+
}
|
|
75996
|
+
}
|
|
75997
|
+
function buildOriginScopedApiFixtures(headers, baseUrl) {
|
|
75998
|
+
let appOrigin;
|
|
75999
|
+
try {
|
|
76000
|
+
appOrigin = baseUrl ? new URL(baseUrl).origin : null;
|
|
76001
|
+
} catch {
|
|
76002
|
+
appOrigin = null;
|
|
76003
|
+
}
|
|
76004
|
+
return `export * from '@playwright/test';
|
|
76005
|
+
import { test as base } from '@playwright/test';
|
|
76006
|
+
|
|
76007
|
+
// Standing auth headers \u2014 attached ONLY to same-origin requests so the credential never leaks to
|
|
76008
|
+
// an off-origin host. Embedded at run time; this file lives in the ephemeral run dir.
|
|
76009
|
+
const STANDING_HEADERS = ${JSON.stringify(headers)};
|
|
76010
|
+
const APP_ORIGIN = ${JSON.stringify(appOrigin)};
|
|
76011
|
+
const BASE_URL = ${JSON.stringify(baseUrl)};
|
|
76012
|
+
const METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'fetch'];
|
|
76013
|
+
|
|
76014
|
+
function sameOrigin(url) {
|
|
76015
|
+
if (!APP_ORIGIN || typeof url !== 'string') return false;
|
|
76016
|
+
try { return new URL(url, BASE_URL).origin === APP_ORIGIN; } catch { return false; }
|
|
76017
|
+
}
|
|
76018
|
+
|
|
76019
|
+
export const test = base.extend({
|
|
76020
|
+
request: async ({ request }, use) => {
|
|
76021
|
+
const scoped = new Proxy(request, {
|
|
76022
|
+
get(target, prop) {
|
|
76023
|
+
const value = target[prop];
|
|
76024
|
+
// HTTP verbs: wrap to add standing headers on same-origin requests only.
|
|
76025
|
+
if (typeof prop === 'string' && METHODS.indexOf(prop) !== -1) {
|
|
76026
|
+
const orig = value.bind(target);
|
|
76027
|
+
return (url, options) => {
|
|
76028
|
+
options = options || {};
|
|
76029
|
+
return sameOrigin(url)
|
|
76030
|
+
? orig(url, { ...options, headers: { ...STANDING_HEADERS, ...(options.headers || {}) } })
|
|
76031
|
+
: orig(url, options);
|
|
76032
|
+
};
|
|
76033
|
+
}
|
|
76034
|
+
// Every other METHOD must run with 'this' bound to the REAL context \u2014 an APIRequestContext
|
|
76035
|
+
// uses internal slots/private fields a Proxy can't forward, so calling with 'this'=proxy
|
|
76036
|
+
// would throw. Non-function props pass through untouched.
|
|
76037
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
76038
|
+
},
|
|
76039
|
+
});
|
|
76040
|
+
await use(scoped);
|
|
76041
|
+
},
|
|
76042
|
+
});
|
|
76043
|
+
`;
|
|
76044
|
+
}
|
|
76045
|
+
}
|
|
76046
|
+
});
|
|
76047
|
+
|
|
75747
76048
|
// ../runner-core/dist/services/playwright-native.js
|
|
75748
76049
|
var require_playwright_native = __commonJS({
|
|
75749
76050
|
"../runner-core/dist/services/playwright-native.js"(exports) {
|
|
@@ -75786,7 +76087,7 @@ var require_playwright_native = __commonJS({
|
|
|
75786
76087
|
};
|
|
75787
76088
|
})();
|
|
75788
76089
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
75789
|
-
exports.scrubHarRequestBody = void 0;
|
|
76090
|
+
exports.scrubHarRequestBody = exports.buildOriginScopedApiFixtures = exports.isAppOriginRequest = exports.resolveApiAuthHeaders = exports.parseApiAuthHeaders = void 0;
|
|
75790
76091
|
exports.executeTestViaNativePlaywright = executeTestViaNativePlaywright2;
|
|
75791
76092
|
exports.parseJsonResults = parseJsonResults2;
|
|
75792
76093
|
exports.parseHarToApiSummary = parseHarToApiSummary;
|
|
@@ -75798,6 +76099,20 @@ var require_playwright_native = __commonJS({
|
|
|
75798
76099
|
var spawn_with_tree_kill_js_1 = require_spawn_with_tree_kill();
|
|
75799
76100
|
var api_call_redaction_js_1 = require_api_call_redaction();
|
|
75800
76101
|
var har_stream_js_1 = require_har_stream();
|
|
76102
|
+
var api_auth_headers_js_1 = require_api_auth_headers();
|
|
76103
|
+
var api_auth_headers_js_2 = require_api_auth_headers();
|
|
76104
|
+
Object.defineProperty(exports, "parseApiAuthHeaders", { enumerable: true, get: function() {
|
|
76105
|
+
return api_auth_headers_js_2.parseApiAuthHeaders;
|
|
76106
|
+
} });
|
|
76107
|
+
Object.defineProperty(exports, "resolveApiAuthHeaders", { enumerable: true, get: function() {
|
|
76108
|
+
return api_auth_headers_js_2.resolveApiAuthHeaders;
|
|
76109
|
+
} });
|
|
76110
|
+
Object.defineProperty(exports, "isAppOriginRequest", { enumerable: true, get: function() {
|
|
76111
|
+
return api_auth_headers_js_2.isAppOriginRequest;
|
|
76112
|
+
} });
|
|
76113
|
+
Object.defineProperty(exports, "buildOriginScopedApiFixtures", { enumerable: true, get: function() {
|
|
76114
|
+
return api_auth_headers_js_2.buildOriginScopedApiFixtures;
|
|
76115
|
+
} });
|
|
75801
76116
|
var api_call_redaction_js_2 = require_api_call_redaction();
|
|
75802
76117
|
Object.defineProperty(exports, "scrubHarRequestBody", { enumerable: true, get: function() {
|
|
75803
76118
|
return api_call_redaction_js_2.scrubHarRequestBody;
|
|
@@ -75807,7 +76122,7 @@ var require_playwright_native = __commonJS({
|
|
|
75807
76122
|
var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
|
|
75808
76123
|
var PW_CLI = __require.resolve("@playwright/test/cli");
|
|
75809
76124
|
async function executeTestViaNativePlaywright2(options) {
|
|
75810
|
-
const { playwrightCode, baseUrl, testName, testVariables, storageStatePath, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
|
|
76125
|
+
const { playwrightCode, baseUrl, testName, testVariables, storageStatePath, extraHttpHeaders, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
|
|
75811
76126
|
const logs2 = [];
|
|
75812
76127
|
const screenshots = [];
|
|
75813
76128
|
logs2.push(`\u{1F3AD} Native Playwright starting`);
|
|
@@ -75821,6 +76136,11 @@ var require_playwright_native = __commonJS({
|
|
|
75821
76136
|
runDir = (0, path_1.join)(RUNS_DIR, (0, crypto_1.randomUUID)());
|
|
75822
76137
|
await (0, promises_1.mkdir)(runDir, { recursive: true });
|
|
75823
76138
|
const isApiTest2 = playwrightCode.includes("@api") || playwrightCode.includes("{ request }") && !playwrightCode.includes("{ page");
|
|
76139
|
+
const resolvedExtraHeaders = (0, api_auth_headers_js_1.resolveApiAuthHeaders)({
|
|
76140
|
+
explicit: extraHttpHeaders,
|
|
76141
|
+
isApiTest: isApiTest2,
|
|
76142
|
+
apiAuthHeadersVar: testVariables?.API_AUTH_HEADERS
|
|
76143
|
+
});
|
|
75824
76144
|
const cssDisableSetup = `
|
|
75825
76145
|
const __qaLiveBrowserStateFile = process.env.VALIDATE_LIVE_BROWSER_STATE_PATH;
|
|
75826
76146
|
let __qaLiveBrowserInterval: ReturnType<typeof setInterval> | null = null;
|
|
@@ -76014,8 +76334,17 @@ test.afterEach(async ({ page }, testInfo) => {
|
|
|
76014
76334
|
if (!importMatch) {
|
|
76015
76335
|
logs2.push("\u26A0 Could not find @playwright/test import - animation disabling NOT injected");
|
|
76016
76336
|
}
|
|
76017
|
-
const
|
|
76337
|
+
const useScopedApiHeaders = isApiTest2 && !!resolvedExtraHeaders && !!importMatch && !!baseUrl;
|
|
76338
|
+
let instrumentedCode;
|
|
76339
|
+
if (isApiTest2) {
|
|
76340
|
+
instrumentedCode = useScopedApiHeaders ? playwrightCode.replace(importMatch[0], importMatch[0].replace(/from\s+['"]@playwright\/test['"]/, "from './qa-api-fixtures'")) : playwrightCode;
|
|
76341
|
+
} else {
|
|
76342
|
+
instrumentedCode = importMatch ? playwrightCode.replace(importMatch[0], importMatch[0] + cssDisableSetup) : playwrightCode;
|
|
76343
|
+
}
|
|
76018
76344
|
await (0, promises_1.writeFile)((0, path_1.join)(runDir, "test.spec.ts"), instrumentedCode);
|
|
76345
|
+
if (useScopedApiHeaders && resolvedExtraHeaders) {
|
|
76346
|
+
await (0, promises_1.writeFile)((0, path_1.join)(runDir, "qa-api-fixtures.ts"), (0, api_auth_headers_js_1.buildOriginScopedApiFixtures)(resolvedExtraHeaders, baseUrl));
|
|
76347
|
+
}
|
|
76019
76348
|
const headless = process.env.PLAYWRIGHT_HEADLESS === "true" || process.env.NODE_ENV === "production" || IS_CLOUD2;
|
|
76020
76349
|
logs2.push(`\u2699\uFE0F Config: baseURL=${JSON.stringify(baseUrl)}, headless=${headless}${isApiTest2 ? ", API mode (no screenshot/video)" : ""}`);
|
|
76021
76350
|
const absTestDir = runDir.replace(/\\/g, "/");
|
|
@@ -76029,6 +76358,7 @@ test.afterEach(async ({ page }, testInfo) => {
|
|
|
76029
76358
|
const absHarPath = wantHar ? (0, path_1.join)(runDir, "session.har").replace(/\\/g, "/") : null;
|
|
76030
76359
|
const recordHarLine = absHarPath ? `recordHar: { path: ${JSON.stringify(absHarPath)}, mode: 'full' },` : "";
|
|
76031
76360
|
const storageStateLine = storageStatePath ? `storageState: ${JSON.stringify(storageStatePath.replace(/\\/g, "/"))},` : "";
|
|
76361
|
+
const extraHeadersLine = resolvedExtraHeaders && !useScopedApiHeaders && Object.keys(resolvedExtraHeaders).length > 0 ? `extraHTTPHeaders: ${JSON.stringify(resolvedExtraHeaders)},` : "";
|
|
76032
76362
|
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
76363
|
const config = `
|
|
76034
76364
|
// Plain object export - no require('@playwright/test') needed.
|
|
@@ -76042,6 +76372,7 @@ module.exports = {
|
|
|
76042
76372
|
use: {
|
|
76043
76373
|
baseURL: ${JSON.stringify(baseUrl)},
|
|
76044
76374
|
${storageStateLine}
|
|
76375
|
+
${extraHeadersLine}
|
|
76045
76376
|
browserName: 'chromium',
|
|
76046
76377
|
headless: ${headless},
|
|
76047
76378
|
screenshot: ${isApiTest2 ? "'off'" : "'on'"},
|
|
@@ -76083,6 +76414,7 @@ module.exports = {
|
|
|
76083
76414
|
// code. Forwarded (not allowlisted) so it stays a per-run toggle; absent → inert.
|
|
76084
76415
|
...process.env.SHARED_AUTH_INJECT ? { SHARED_AUTH_INJECT: process.env.SHARED_AUTH_INJECT } : {}
|
|
76085
76416
|
};
|
|
76417
|
+
delete envWithIsolation.API_AUTH_HEADERS;
|
|
76086
76418
|
logs2.push("\u{1F680} Spawning playwright test...");
|
|
76087
76419
|
const stopLiveBrowserPolling = liveBrowserStatePath && liveBrowserHandle ? startLiveBrowserStatePolling(liveBrowserStatePath, liveBrowserHandle, onLiveBrowserSnapshot) : null;
|
|
76088
76420
|
let playResult;
|
|
@@ -84189,6 +84521,7 @@ var require_discover_generator = __commonJS({
|
|
|
84189
84521
|
var ai_provider_js_1 = require_ai_provider();
|
|
84190
84522
|
var ai_queue_js_1 = require_ai_queue();
|
|
84191
84523
|
var ai_retry_js_1 = require_ai_retry();
|
|
84524
|
+
var discover_ai_planner_js_1 = require_discover_ai_planner();
|
|
84192
84525
|
var snapshot_parser_js_1 = require_snapshot_parser();
|
|
84193
84526
|
var scrub_credentials_js_1 = require_scrub_credentials();
|
|
84194
84527
|
var exploration_parser_js_1 = require_exploration_parser();
|
|
@@ -87140,7 +87473,15 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
87140
87473
|
tools: openaiTools,
|
|
87141
87474
|
tool_choice: "auto",
|
|
87142
87475
|
temperature: 0.2,
|
|
87143
|
-
|
|
87476
|
+
// Shared provider-aware output ceiling (same helper the survey planner uses):
|
|
87477
|
+
// generous where the provider's max output is verified large (deepseek/minimax
|
|
87478
|
+
// 65536), conservative elsewhere (16384). The old flat 8192 truncated large
|
|
87479
|
+
// finish_generate emissions mid-JSON — the loop re-prompted on the parse error
|
|
87480
|
+
// (line ~4744) and, for a test that genuinely needs >8192 output tokens, could
|
|
87481
|
+
// never finish before the hard stop → zero tests for that scenario. It's a
|
|
87482
|
+
// ceiling, not a target (calls finish at finish_reason:'stop' well under it), so
|
|
87483
|
+
// this adds no per-call cost.
|
|
87484
|
+
max_tokens: (0, discover_ai_planner_js_1.getAIPlannerMaxTokens)(agentModel)
|
|
87144
87485
|
}, { signal });
|
|
87145
87486
|
}), {
|
|
87146
87487
|
label: `scenario-gen-iter-${iteration}`,
|
|
@@ -321751,6 +322092,7 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
321751
322092
|
var mobile_exploration_state_js_1 = require_mobile_exploration_state();
|
|
321752
322093
|
var mobile_discovery_auth_js_1 = require_mobile_discovery_auth();
|
|
321753
322094
|
var mobile_driver_errors_js_1 = require_mobile_driver_errors();
|
|
322095
|
+
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
321754
322096
|
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
321755
322097
|
var discover_planner_js_1 = require_discover_planner();
|
|
321756
322098
|
function checkpointErrorMessage(err) {
|
|
@@ -322051,7 +322393,7 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
|
|
|
322051
322393
|
const graph = state2.toScreenGraph();
|
|
322052
322394
|
const screenMap = state2.buildInMemoryScreenMap();
|
|
322053
322395
|
const testBudget = (0, discover_planner_js_1.computeTestBudget)(explore.exploreStats.pagesVisited, ctx.focusAreas);
|
|
322054
|
-
const planResult = await runPlan({
|
|
322396
|
+
const planResult = await (0, mobile_session_keepalive_js_1.withMobileSessionKeepAlive)(driver, () => runPlan({
|
|
322055
322397
|
graph,
|
|
322056
322398
|
mode,
|
|
322057
322399
|
projectId: ctx.projectId,
|
|
@@ -322075,7 +322417,7 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
|
|
|
322075
322417
|
}, {
|
|
322076
322418
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
322077
322419
|
onAICall
|
|
322078
|
-
});
|
|
322420
|
+
}));
|
|
322079
322421
|
logs2.push(...planResult.logs);
|
|
322080
322422
|
log2(`
|
|
322081
322423
|
Plan: ${planResult.scenarios.length} scenarios, ${planResult.featureGroups.length} feature groups (budget: ${testBudget})`);
|
|
@@ -322933,6 +323275,7 @@ Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.leng
|
|
|
322933
323275
|
if (surveyAuthRecipe) {
|
|
322934
323276
|
logs2.push(`Auth-prologue: captured + verified login recipe ${surveyAuthRecipe.recipeHash} during survey \u2014 persisting for reuse on later runs`);
|
|
322935
323277
|
}
|
|
323278
|
+
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
323279
|
return {
|
|
322937
323280
|
observedOrigins: mergeObservedOrigins(exploreResult.observedOrigins, loginObservedOrigins),
|
|
322938
323281
|
resolvedAppOrigin: surveyAuthRecipe?.appOrigin ?? void 0,
|
|
@@ -322948,7 +323291,8 @@ Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.leng
|
|
|
322948
323291
|
exploreTruncatedReason: exploreResult.truncatedReason,
|
|
322949
323292
|
fallbackUsed: planResult.fallbackUsed ?? false,
|
|
322950
323293
|
fallbackReason: planResult.fallbackReason,
|
|
322951
|
-
|
|
323294
|
+
error: plannerFailedError,
|
|
323295
|
+
featureGroups: plannerFailedError ? void 0 : featureGroups,
|
|
322952
323296
|
apiCalls: discoveryApiCalls.length > 0 ? discoveryApiCalls : void 0,
|
|
322953
323297
|
healthObservations: surveyHealth,
|
|
322954
323298
|
exploreFinalMessages: exploreResult.finalMessages,
|
|
@@ -336806,7 +337150,7 @@ var require_package4 = __commonJS({
|
|
|
336806
337150
|
"package.json"(exports, module) {
|
|
336807
337151
|
module.exports = {
|
|
336808
337152
|
name: "@validate.qa/runner",
|
|
336809
|
-
version: "1.0.
|
|
337153
|
+
version: "1.0.40",
|
|
336810
337154
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
336811
337155
|
bin: {
|
|
336812
337156
|
"validate-runner": "dist/cli.js"
|
|
@@ -345626,10 +345970,22 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345626
345970
|
onPlanReady
|
|
345627
345971
|
});
|
|
345628
345972
|
} finally {
|
|
345973
|
+
const TEARDOWN_STEP_TIMEOUT = /* @__PURE__ */ Symbol("vqa-teardown-timeout");
|
|
345974
|
+
const timeboxed = async (step) => Promise.race([
|
|
345975
|
+
step,
|
|
345976
|
+
new Promise((resolve2) => {
|
|
345977
|
+
const t = setTimeout(() => resolve2(TEARDOWN_STEP_TIMEOUT), 45e3);
|
|
345978
|
+
t.unref?.();
|
|
345979
|
+
})
|
|
345980
|
+
]);
|
|
345629
345981
|
const activeCapture = capture;
|
|
345630
345982
|
if (activeCapture) {
|
|
345631
345983
|
try {
|
|
345632
|
-
const
|
|
345984
|
+
const stopped = await timeboxed(activeCapture.stop());
|
|
345985
|
+
if (stopped === TEARDOWN_STEP_TIMEOUT) {
|
|
345986
|
+
throw new Error("capture stop timed out after 45s \u2014 continuing teardown without apiCalls");
|
|
345987
|
+
}
|
|
345988
|
+
const apiCalls = stopped;
|
|
345633
345989
|
const captureHealth = activeCapture.health();
|
|
345634
345990
|
if (discoveryResult) {
|
|
345635
345991
|
discoveryResult.apiCalls = apiCalls.length ? attachMobileDiscoveryNetworkContext(apiCalls, discoveryResult, session?.appId ?? mobileTarget.appId) : void 0;
|
|
@@ -345650,7 +346006,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345650
346006
|
if (session) {
|
|
345651
346007
|
detachMirrorSession(session.deviceId);
|
|
345652
346008
|
try {
|
|
345653
|
-
await session.stop();
|
|
346009
|
+
await timeboxed(session.stop());
|
|
345654
346010
|
} catch {
|
|
345655
346011
|
}
|
|
345656
346012
|
}
|