@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.js
CHANGED
|
@@ -270,7 +270,8 @@ var require_ai_provider = __commonJS({
|
|
|
270
270
|
{ id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", provider: "anthropic", description: "Anthropic - fast & intelligent, 200k ctx", contextWindow: 2e5 },
|
|
271
271
|
{ id: "nvidia-nemotron-nano", label: "NVIDIA Nemotron Nano", provider: "nvidia", description: "NVIDIA NIM - Nemotron 30B Nano, reasoning model, 128k ctx", contextWindow: 128e3 },
|
|
272
272
|
{ id: "nvidia-kimi-k2.6", label: "NVIDIA Kimi K2.6", provider: "nvidia", description: "NVIDIA NIM - Moonshot Kimi K2.6, multimodal coding and agentic model, 256k ctx", contextWindow: 256e3 },
|
|
273
|
-
{ id: "deepseek-v4-flash", label: "DeepSeek V4 Flash", provider: "deepseek", description: "DeepSeek - V4 Flash, OpenAI-compatible, 1M ctx", contextWindow: 1e6 }
|
|
273
|
+
{ id: "deepseek-v4-flash", label: "DeepSeek V4 Flash", provider: "deepseek", description: "DeepSeek - V4 Flash, OpenAI-compatible, 1M ctx", contextWindow: 1e6 },
|
|
274
|
+
{ id: "deepseek-v4-pro", label: "DeepSeek V4 Pro", provider: "deepseek", description: "DeepSeek - V4 Pro, higher-quality reasoning tier, OpenAI-compatible, 1M ctx", contextWindow: 1e6 }
|
|
274
275
|
];
|
|
275
276
|
var PROVIDER_DEFAULT_MODEL = {
|
|
276
277
|
openai: "gpt-4o",
|
|
@@ -330,7 +331,8 @@ var require_ai_provider = __commonJS({
|
|
|
330
331
|
"claude-sonnet-4-6": { strong: "claude-sonnet-4-6", fast: "claude-sonnet-4-6", agent: "claude-sonnet-4-6" },
|
|
331
332
|
"nvidia-nemotron-nano": { strong: "nvidia/nemotron-3-nano-30b-a3b", fast: "nvidia/nemotron-3-nano-30b-a3b", agent: "nvidia/nemotron-3-nano-30b-a3b" },
|
|
332
333
|
"nvidia-kimi-k2.6": { strong: "moonshotai/kimi-k2.6", fast: "moonshotai/kimi-k2.6", agent: "moonshotai/kimi-k2.6" },
|
|
333
|
-
"deepseek-v4-flash": { strong: "deepseek-v4-flash", fast: "deepseek-v4-flash", agent: "deepseek-v4-flash" }
|
|
334
|
+
"deepseek-v4-flash": { strong: "deepseek-v4-flash", fast: "deepseek-v4-flash", agent: "deepseek-v4-flash" },
|
|
335
|
+
"deepseek-v4-pro": { strong: "deepseek-v4-pro", fast: "deepseek-v4-pro", agent: "deepseek-v4-pro" }
|
|
334
336
|
};
|
|
335
337
|
var _requestModelStorage = new async_hooks_1.AsyncLocalStorage();
|
|
336
338
|
function withModelContext(modelId, fn) {
|
|
@@ -392,7 +394,7 @@ var require_ai_provider = __commonJS({
|
|
|
392
394
|
if (modelId === "nvidia/nemotron-3-nano-30b-a3b") {
|
|
393
395
|
return { chat_template_kwargs: { enable_thinking: false } };
|
|
394
396
|
}
|
|
395
|
-
if (modelId === "deepseek-v4-flash") {
|
|
397
|
+
if (modelId === "deepseek-v4-flash" || modelId === "deepseek-v4-pro") {
|
|
396
398
|
return { thinking: { type: "disabled" } };
|
|
397
399
|
}
|
|
398
400
|
return {};
|
|
@@ -3302,6 +3304,7 @@ var init_audit_pricing = __esm({
|
|
|
3302
3304
|
"MiniMax-M2.7-highspeed": { input: 0.6, output: 2.4 },
|
|
3303
3305
|
"MiniMax-M2": { input: 0.3, output: 1.2 },
|
|
3304
3306
|
"deepseek-v4-flash": { input: 0.14, output: 0.28 },
|
|
3307
|
+
"deepseek-v4-pro": { input: 0.42, output: 0.84 },
|
|
3305
3308
|
// Whisper bills per-minute, not per-token (OpenAI + Groq)
|
|
3306
3309
|
"whisper-1": null,
|
|
3307
3310
|
"whisper-large-v3": null,
|
|
@@ -5879,6 +5882,34 @@ var require_ai_queue = __commonJS({
|
|
|
5879
5882
|
}
|
|
5880
5883
|
});
|
|
5881
5884
|
|
|
5885
|
+
// ../runner-core/dist/services/mobile/mobile-session-keepalive.js
|
|
5886
|
+
var require_mobile_session_keepalive = __commonJS({
|
|
5887
|
+
"../runner-core/dist/services/mobile/mobile-session-keepalive.js"(exports2) {
|
|
5888
|
+
"use strict";
|
|
5889
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
5890
|
+
exports2.MOBILE_KEEPALIVE_INTERVAL_MS = void 0;
|
|
5891
|
+
exports2.withMobileSessionKeepAlive = withMobileSessionKeepAlive;
|
|
5892
|
+
exports2.MOBILE_KEEPALIVE_INTERVAL_MS = (() => {
|
|
5893
|
+
const raw = Number(process.env.VALIDATEQA_MOBILE_KEEPALIVE_MS);
|
|
5894
|
+
return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 12e4;
|
|
5895
|
+
})();
|
|
5896
|
+
async function withMobileSessionKeepAlive(driver, fn, intervalMs = exports2.MOBILE_KEEPALIVE_INTERVAL_MS) {
|
|
5897
|
+
if (intervalMs <= 0)
|
|
5898
|
+
return fn();
|
|
5899
|
+
const timer = setInterval(() => {
|
|
5900
|
+
void driver.windowSize().catch(() => {
|
|
5901
|
+
});
|
|
5902
|
+
}, intervalMs);
|
|
5903
|
+
timer.unref?.();
|
|
5904
|
+
try {
|
|
5905
|
+
return await fn();
|
|
5906
|
+
} finally {
|
|
5907
|
+
clearInterval(timer);
|
|
5908
|
+
}
|
|
5909
|
+
}
|
|
5910
|
+
}
|
|
5911
|
+
});
|
|
5912
|
+
|
|
5882
5913
|
// ../runner-core/dist/services/mobile/mobile-observation.js
|
|
5883
5914
|
var require_mobile_observation = __commonJS({
|
|
5884
5915
|
"../runner-core/dist/services/mobile/mobile-observation.js"(exports2) {
|
|
@@ -64612,6 +64643,7 @@ var require_mobile_explorer = __commonJS({
|
|
|
64612
64643
|
var ai_provider_js_1 = require_ai_provider();
|
|
64613
64644
|
var ai_queue_js_1 = require_ai_queue();
|
|
64614
64645
|
var ai_retry_js_1 = require_ai_retry();
|
|
64646
|
+
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
64615
64647
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
64616
64648
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
64617
64649
|
var mobile_exploration_state_js_1 = require_mobile_exploration_state();
|
|
@@ -66121,7 +66153,7 @@ ${statePacket}` }
|
|
|
66121
66153
|
const requestMessageCount = messages.length;
|
|
66122
66154
|
const requestMessages = messages.slice();
|
|
66123
66155
|
const callStart = Date.now();
|
|
66124
|
-
const response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
66156
|
+
const response = await (0, mobile_session_keepalive_js_1.withMobileSessionKeepAlive)(driver, () => (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
66125
66157
|
arm();
|
|
66126
66158
|
return aiClient.chat.completions.create({
|
|
66127
66159
|
model: agentModel,
|
|
@@ -66134,7 +66166,7 @@ ${statePacket}` }
|
|
|
66134
66166
|
label: `mobile-explore-iter-${iteration}`,
|
|
66135
66167
|
deferTimeoutUntilArmed: true,
|
|
66136
66168
|
onRetry: (attempt, reason, delayMs) => log2(` \u26A0 mobile explore AI retry ${attempt} (${reason}) \u2014 waiting ${delayMs}ms`)
|
|
66137
|
-
});
|
|
66169
|
+
}));
|
|
66138
66170
|
const iterDuration = Date.now() - callStart;
|
|
66139
66171
|
if (!response?.choices?.[0]?.message) {
|
|
66140
66172
|
throw new Error(`AI explore call returned no response (queue timeout or provider overload) at iteration ${iteration}`);
|
|
@@ -68213,6 +68245,7 @@ var require_discover_ai_planner = __commonJS({
|
|
|
68213
68245
|
"use strict";
|
|
68214
68246
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
68215
68247
|
exports2.SCENARIO_SYSTEM_PROMPT = void 0;
|
|
68248
|
+
exports2.getAIPlannerMaxTokens = getAIPlannerMaxTokens;
|
|
68216
68249
|
exports2.getSnapshotConfidence = getSnapshotConfidence;
|
|
68217
68250
|
exports2.compressSiteMap = compressSiteMap;
|
|
68218
68251
|
exports2.formatEvidenceForPlanner = formatEvidenceForPlanner;
|
|
@@ -68224,10 +68257,25 @@ var require_discover_ai_planner = __commonJS({
|
|
|
68224
68257
|
var ai_retry_js_1 = require_ai_retry();
|
|
68225
68258
|
var strip_json_fences_js_1 = require_strip_json_fences();
|
|
68226
68259
|
var scenario_plan_validator_js_1 = require_scenario_plan_validator();
|
|
68227
|
-
var
|
|
68228
|
-
var
|
|
68260
|
+
var GENEROUS_AI_PLANNER_MAX_TOKENS = 65536;
|
|
68261
|
+
var CONSERVATIVE_AI_PLANNER_MAX_TOKENS = 16384;
|
|
68229
68262
|
function getAIPlannerMaxTokens(modelId) {
|
|
68230
|
-
|
|
68263
|
+
switch ((0, ai_provider_js_1.getProviderForModel)(modelId)) {
|
|
68264
|
+
case "deepseek":
|
|
68265
|
+
// V4 output ceiling = 384K — verified safe
|
|
68266
|
+
case "minimax":
|
|
68267
|
+
return GENEROUS_AI_PLANNER_MAX_TOKENS;
|
|
68268
|
+
case "xai":
|
|
68269
|
+
// grok-build-0.1 ceiling unproven (Grok-Fast/3 = 16K) — DON'T risk a 400
|
|
68270
|
+
case "openai":
|
|
68271
|
+
// gpt-4o max output = 16384 → more would 400
|
|
68272
|
+
case "anthropic":
|
|
68273
|
+
// OpenAI-compat proxy — keep conservative
|
|
68274
|
+
case "nvidia":
|
|
68275
|
+
// NIM output limits vary — keep conservative
|
|
68276
|
+
default:
|
|
68277
|
+
return CONSERVATIVE_AI_PLANNER_MAX_TOKENS;
|
|
68278
|
+
}
|
|
68231
68279
|
}
|
|
68232
68280
|
var discover_planner_js_1 = require_discover_planner();
|
|
68233
68281
|
function getSnapshotConfidence(sourceKind) {
|
|
@@ -68779,7 +68827,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
68779
68827
|
4. **Multi-page journeys** - only when transitions were actually observed
|
|
68780
68828
|
5. **Bug reports** - for observed 500 errors, crashes, or broken features
|
|
68781
68829
|
6. **Steps are human-readable** - "Fill the email field", not "page.getByLabel('Email').fill()"
|
|
68782
|
-
7. **Expected outcomes are plain-English success criteria** - describe the user-visible product result, not the controls touched along the way. Steps already cover buttons, inputs, links, fields, headings, tabs, dialogs, and menus. Expected outcomes should say what the user can now do or rely on (record saved, message sent, search results filtered, validation explained, dashboard accessible). Include durable routes or exact copy only when they are the real product outcome; do NOT list "heading visible", "button visible", "search input visible", or similar element inventory unless that exact control is the behavior under test.
|
|
68830
|
+
7. **Expected outcomes are plain-English success criteria** - describe the user-visible product result, not the controls touched along the way. Steps already cover buttons, inputs, links, fields, headings, tabs, dialogs, and menus. Expected outcomes should say what the user can now do or rely on (record saved, message sent, search results filtered, validation explained, dashboard accessible). Include durable routes or exact copy only when they are the real product outcome; do NOT list "heading visible", "button visible", "search input visible", or similar element inventory unless that exact control is the behavior under test. **Outcome-strength self-check (applies to EVERY scenario type):** before emitting a scenario, ask \u2014 would every expectedOutcome still pass if the primary action did nothing and only the page and its existing chrome rendered? If yes, it is too weak. Add at least one evidence-backed post-condition that would FAIL in that no-op case: specific result content or value, a created or removed entity, a changed count or status, persisted state after reload, handled validation copy, or a concrete session signal. A route alone suffices only when navigation or redirect is itself the behavior under test; a newly revealed control suffices only when revealing that control is the behavior under test. Never invent copy to satisfy this check \u2014 Rule 1 still governs what you may assert.
|
|
68783
68831
|
8. **Auth-aware** - mark scenarios that need login. Use process.env.TEST_USER_EMAIL / PASSWORD
|
|
68784
68832
|
9. **No duplicates** - check existing test names before adding
|
|
68785
68833
|
10. **All tests start from the landing page** - every generated test navigates to \`/\` first and uses the app's own navigation (clicking links/buttons) to reach the target page. Steps must reflect this: say "Navigate to landing page and click Sign In" not "Navigate to /auth/login". Deep-linking to internal routes causes hydration failures in SPAs. \`targetPages\` lists the pages involved in the flow.
|
|
@@ -68831,7 +68879,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
68831
68879
|
**YOUR responsibility:**
|
|
68832
68880
|
- Scenario \`name\`, \`type\`, \`goal\`, \`prerequisites\`
|
|
68833
68881
|
- \`steps\` written as user-language intent ("create a new conversation", "click the Submit button to log in"), naming observed UI labels where they help describe intent
|
|
68834
|
-
- \`expectedOutcomes\`
|
|
68882
|
+
- \`expectedOutcomes\` expressing the evidence-backed product post-condition per Rule 7 (and Rule 11 for happy paths, plus Rule 12's concrete session signal for authentication happy paths); citing observed durable labels when asserting on stable signals is encouraged
|
|
68835
68883
|
|
|
68836
68884
|
**NOT your responsibility:**
|
|
68837
68885
|
- Exact Playwright selectors (\`getByRole(...)\`, \`getByText(...)\`, \`page.locator(...)\`, etc.) \u2014 live-generate picks locators against the live snapshot
|
|
@@ -68852,7 +68900,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
68852
68900
|
- GOOD specificity: "create a new conversation by sending a message to a connection established earlier in this scenario"
|
|
68853
68901
|
- BAD specificity: "click the button labeled exactly 'Send' in the bottom-right of the chat"
|
|
68854
68902
|
|
|
68855
|
-
Citing observed labels in \`expectedOutcomes\`
|
|
68903
|
+
Citing observed labels in \`expectedOutcomes\` is fine \u2014 that's asserting on stable product signals, not implementation specifics.
|
|
68856
68904
|
|
|
68857
68905
|
## Coverage Strategy
|
|
68858
68906
|
- For every form: at least 1 HAPPY_PATH (submit valid data AND verify durable result per rule 11) + 1 EDGE_CASE (empty/invalid submission)
|
|
@@ -69438,6 +69486,24 @@ Each entry is exactly one of:
|
|
|
69438
69486
|
}
|
|
69439
69487
|
async function runAIPlanner(siteMap, evidence, context, options) {
|
|
69440
69488
|
const logs2 = [];
|
|
69489
|
+
const failurePlan = (kind, reason, legacyFallback2) => {
|
|
69490
|
+
const noFallback = context.mode === "survey" && process.env.DISCOVERY_PLANNER_NO_FALLBACK === "true";
|
|
69491
|
+
if (noFallback) {
|
|
69492
|
+
logs2.push(`Planner ${kind} \u2014 NO-FALLBACK: failing survey cleanly instead of fabricating slug features (${reason})`);
|
|
69493
|
+
return {
|
|
69494
|
+
scenarios: [],
|
|
69495
|
+
featureGroups: [],
|
|
69496
|
+
reasoning: "",
|
|
69497
|
+
logs: logs2,
|
|
69498
|
+
coverageGaps: [],
|
|
69499
|
+
plannerFailed: true,
|
|
69500
|
+
plannerFailureKind: kind,
|
|
69501
|
+
fallbackReason: reason
|
|
69502
|
+
};
|
|
69503
|
+
}
|
|
69504
|
+
return { ...legacyFallback2(), fallbackReason: reason };
|
|
69505
|
+
};
|
|
69506
|
+
let extraPlannerCallUsed = false;
|
|
69441
69507
|
const compressed = compressSiteMap(siteMap);
|
|
69442
69508
|
const agentModel = options.modelOverride ? (0, ai_provider_js_1.resolveModelForTask)(options.modelOverride, "agent") : (0, ai_provider_js_1.getModel)("agent");
|
|
69443
69509
|
const plannerMaxTokens = getAIPlannerMaxTokens(agentModel);
|
|
@@ -69473,7 +69539,7 @@ Each entry is exactly one of:
|
|
|
69473
69539
|
} catch (err) {
|
|
69474
69540
|
const message = err instanceof Error ? err.message : String(err);
|
|
69475
69541
|
logs2.push(`AI planner failed after retries: ${message}`);
|
|
69476
|
-
return
|
|
69542
|
+
return failurePlan("transport", (0, ai_retry_js_1.describeAIFailure)(err), () => buildFallbackPlan(siteMap, evidence.recordedJourneys, context.mode, logs2, context.existingTestNames ?? [], context.focusAreas, context.testBudget));
|
|
69477
69543
|
}
|
|
69478
69544
|
try {
|
|
69479
69545
|
options.onAICall?.({
|
|
@@ -69492,20 +69558,104 @@ Each entry is exactly one of:
|
|
|
69492
69558
|
});
|
|
69493
69559
|
} catch {
|
|
69494
69560
|
}
|
|
69495
|
-
const
|
|
69496
|
-
|
|
69561
|
+
const choice = response?.choices?.[0];
|
|
69562
|
+
const raw = choice?.message?.content;
|
|
69563
|
+
const finishReason = (choice?.finish_reason ?? "").toString().toLowerCase();
|
|
69564
|
+
const completionTokens = response.usage?.completion_tokens;
|
|
69565
|
+
const TRUNCATION_FINISH = /* @__PURE__ */ new Set(["length", "max_tokens", "max_output_tokens", "token_limit"]);
|
|
69566
|
+
const definitelyTruncated = TRUNCATION_FINISH.has(finishReason);
|
|
69567
|
+
const atOrOverCap = completionTokens != null && completionTokens >= plannerMaxTokens;
|
|
69568
|
+
const blocked = finishReason === "content_filter" || finishReason === "tool_calls";
|
|
69569
|
+
const usage = response.usage;
|
|
69570
|
+
logs2.push(`AI planner: ${usage?.prompt_tokens ?? "?"} input, ${usage?.completion_tokens ?? "?"} output tokens (finish=${finishReason || "?"})`);
|
|
69571
|
+
const legacyFallback = () => buildFallbackPlan(siteMap, evidence.recordedJourneys, context.mode, logs2, context.existingTestNames ?? [], context.focusAreas, context.testBudget);
|
|
69572
|
+
if (blocked) {
|
|
69573
|
+
return failurePlan("blocked", "AI response was blocked (content_filter/tool_calls)", legacyFallback);
|
|
69574
|
+
}
|
|
69575
|
+
if (definitelyTruncated) {
|
|
69576
|
+
return failurePlan("truncated", "AI planner output truncated at max_tokens (finish_reason=length)", legacyFallback);
|
|
69577
|
+
}
|
|
69578
|
+
const trimmed = raw?.trim();
|
|
69579
|
+
if (!trimmed) {
|
|
69497
69580
|
logs2.push("AI planner returned empty response");
|
|
69498
|
-
return
|
|
69581
|
+
return atOrOverCap ? failurePlan("truncated", "AI planner output truncated at max_tokens (empty body at cap)", legacyFallback) : failurePlan("empty", "AI returned an empty response", legacyFallback);
|
|
69499
69582
|
}
|
|
69500
|
-
const usage = response.usage;
|
|
69501
|
-
logs2.push(`AI planner: ${usage?.prompt_tokens ?? "?"} input, ${usage?.completion_tokens ?? "?"} output tokens`);
|
|
69502
69583
|
let aiOutput;
|
|
69503
69584
|
try {
|
|
69504
|
-
aiOutput = JSON.parse((0, strip_json_fences_js_1.stripJsonFences)(
|
|
69585
|
+
aiOutput = JSON.parse((0, strip_json_fences_js_1.stripJsonFences)(trimmed));
|
|
69505
69586
|
} catch (err) {
|
|
69506
69587
|
const message = err instanceof Error ? err.message : String(err);
|
|
69507
69588
|
logs2.push(`AI planner returned invalid JSON: ${message}`);
|
|
69508
|
-
|
|
69589
|
+
if (atOrOverCap) {
|
|
69590
|
+
logs2.push("AI planner output at/over the token cap and unparseable \u2014 treating as truncated (skipping repair)");
|
|
69591
|
+
return failurePlan("truncated", "AI planner output truncated at max_tokens (unparseable at cap)", legacyFallback);
|
|
69592
|
+
}
|
|
69593
|
+
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.`;
|
|
69594
|
+
const repairStart = Date.now();
|
|
69595
|
+
let repairResponse = null;
|
|
69596
|
+
try {
|
|
69597
|
+
extraPlannerCallUsed = true;
|
|
69598
|
+
repairResponse = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
69599
|
+
arm();
|
|
69600
|
+
return aiClient.chat.completions.create({
|
|
69601
|
+
model: agentModel,
|
|
69602
|
+
messages: [
|
|
69603
|
+
...messages,
|
|
69604
|
+
{ role: "assistant", content: raw },
|
|
69605
|
+
{ role: "user", content: repairPrompt }
|
|
69606
|
+
],
|
|
69607
|
+
max_tokens: plannerMaxTokens,
|
|
69608
|
+
temperature: 0.3,
|
|
69609
|
+
...(0, ai_provider_js_1.getJsonObjectResponseFormat)(agentModel),
|
|
69610
|
+
...(0, ai_provider_js_1.getNoThinkingRequestOptions)(agentModel)
|
|
69611
|
+
}, { signal });
|
|
69612
|
+
}), {
|
|
69613
|
+
label: "ai-planner-repair",
|
|
69614
|
+
deferTimeoutUntilArmed: true,
|
|
69615
|
+
onRetry: (attempt, reason, delay) => logs2.push(`AI planner repair call failed (${reason}) \u2014 retry ${attempt} in ${Math.round(delay / 1e3)}s`)
|
|
69616
|
+
});
|
|
69617
|
+
} catch (repairErr) {
|
|
69618
|
+
const repairMessage = repairErr instanceof Error ? repairErr.message : String(repairErr);
|
|
69619
|
+
logs2.push(`AI planner repair failed (${repairMessage})`);
|
|
69620
|
+
}
|
|
69621
|
+
if (repairResponse) {
|
|
69622
|
+
try {
|
|
69623
|
+
options.onAICall?.({
|
|
69624
|
+
iteration: 1,
|
|
69625
|
+
groupId: `ai-planner-${context.projectId.slice(0, 8)}`,
|
|
69626
|
+
callType: "discoveryAIPlannerRepair",
|
|
69627
|
+
model: agentModel,
|
|
69628
|
+
provider: (0, ai_provider_js_1.getProviderForModel)(agentModel),
|
|
69629
|
+
durationMs: Date.now() - repairStart,
|
|
69630
|
+
promptTokens: repairResponse.usage?.prompt_tokens,
|
|
69631
|
+
completionTokens: repairResponse.usage?.completion_tokens,
|
|
69632
|
+
totalTokens: repairResponse.usage?.total_tokens,
|
|
69633
|
+
assistantMessage: repairResponse.choices[0]?.message?.content,
|
|
69634
|
+
requestMessageCount: messages.length + 2,
|
|
69635
|
+
requestMessages: [
|
|
69636
|
+
...messages,
|
|
69637
|
+
{ role: "assistant", content: raw },
|
|
69638
|
+
{ role: "user", content: repairPrompt }
|
|
69639
|
+
]
|
|
69640
|
+
});
|
|
69641
|
+
} catch {
|
|
69642
|
+
}
|
|
69643
|
+
const repairedRaw = repairResponse.choices[0]?.message?.content?.trim();
|
|
69644
|
+
if (repairedRaw) {
|
|
69645
|
+
try {
|
|
69646
|
+
aiOutput = JSON.parse((0, strip_json_fences_js_1.stripJsonFences)(repairedRaw));
|
|
69647
|
+
} catch (repairParseErr) {
|
|
69648
|
+
const repairMessage = repairParseErr instanceof Error ? repairParseErr.message : String(repairParseErr);
|
|
69649
|
+
logs2.push(`AI planner repair returned invalid JSON (${repairMessage})`);
|
|
69650
|
+
return failurePlan("malformed", "AI returned unreadable output", legacyFallback);
|
|
69651
|
+
}
|
|
69652
|
+
} else {
|
|
69653
|
+
logs2.push("AI planner repair returned empty response");
|
|
69654
|
+
return failurePlan("malformed", "AI returned unreadable output", legacyFallback);
|
|
69655
|
+
}
|
|
69656
|
+
} else {
|
|
69657
|
+
return failurePlan("malformed", "AI returned unreadable output", legacyFallback);
|
|
69658
|
+
}
|
|
69509
69659
|
}
|
|
69510
69660
|
let { scenarios, featureGroups, warnings, coverageGaps } = validateAIPlannerOutput(aiOutput, siteMap, context.existingTestNames, context.testBudget, evidence.recordedJourneys, context.mode, context.focusAreas);
|
|
69511
69661
|
logs2.push(`AI planner: ${scenarios.length} scenarios, ${featureGroups.length} feature groups (${warnings.length} warnings, ${coverageGaps.length} coverage gaps)`);
|
|
@@ -69515,7 +69665,7 @@ Each entry is exactly one of:
|
|
|
69515
69665
|
let topLevelWarning;
|
|
69516
69666
|
const totalV1 = scenarios.length + coverageGaps.length;
|
|
69517
69667
|
const rejectionRateV1 = totalV1 > 0 ? coverageGaps.length / totalV1 : 0;
|
|
69518
|
-
if (rejectionRateV1 > 0.5 && totalV1 >= 3) {
|
|
69668
|
+
if (rejectionRateV1 > 0.5 && totalV1 >= 3 && !extraPlannerCallUsed) {
|
|
69519
69669
|
logs2.push(`AI planner: ${Math.round(rejectionRateV1 * 100)}% rejection rate (${coverageGaps.length}/${totalV1}) \u2014 running self-correction retry`);
|
|
69520
69670
|
const retryContext = [
|
|
69521
69671
|
`Your previous emission produced ${coverageGaps.length} coverage gaps out of ${totalV1} scenarios (${Math.round(rejectionRateV1 * 100)}% rejection rate).`,
|
|
@@ -69534,6 +69684,7 @@ Each entry is exactly one of:
|
|
|
69534
69684
|
const retryStart = Date.now();
|
|
69535
69685
|
let retryResponse = null;
|
|
69536
69686
|
try {
|
|
69687
|
+
extraPlannerCallUsed = true;
|
|
69537
69688
|
retryResponse = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
69538
69689
|
arm();
|
|
69539
69690
|
return aiClient.chat.completions.create({
|
|
@@ -69633,6 +69784,22 @@ Each entry is exactly one of:
|
|
|
69633
69784
|
};
|
|
69634
69785
|
}
|
|
69635
69786
|
if (context.mode === "survey" && featureGroups.length === 0 && siteMap.pages.length > 0) {
|
|
69787
|
+
const hasGroupWorthyEvidence = siteMap.pages.some((p) => (p.forms?.length ?? 0) > 0 || (p.elements?.length ?? 0) > 0);
|
|
69788
|
+
const noFallback = process.env.DISCOVERY_PLANNER_NO_FALLBACK === "true";
|
|
69789
|
+
if (noFallback && !hasGroupWorthyEvidence) {
|
|
69790
|
+
return {
|
|
69791
|
+
scenarios,
|
|
69792
|
+
featureGroups: [],
|
|
69793
|
+
reasoning: aiOutput.reasoning ?? "",
|
|
69794
|
+
logs: logs2,
|
|
69795
|
+
coverageGaps,
|
|
69796
|
+
warning: topLevelWarning,
|
|
69797
|
+
fallbackUsed: false
|
|
69798
|
+
};
|
|
69799
|
+
}
|
|
69800
|
+
if (noFallback) {
|
|
69801
|
+
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));
|
|
69802
|
+
}
|
|
69636
69803
|
logs2.push("Survey AI returned 0 feature groups - falling back to deterministic grouping");
|
|
69637
69804
|
const fallback = buildFallbackPlan(siteMap, evidence.recordedJourneys, context.mode, logs2, context.existingTestNames ?? [], context.focusAreas, context.testBudget);
|
|
69638
69805
|
return {
|
|
@@ -70843,6 +71010,7 @@ var require_mobile_generator = __commonJS({
|
|
|
70843
71010
|
var ai_provider_js_1 = require_ai_provider();
|
|
70844
71011
|
var ai_queue_js_1 = require_ai_queue();
|
|
70845
71012
|
var ai_retry_js_1 = require_ai_retry();
|
|
71013
|
+
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
70846
71014
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
70847
71015
|
var mobile_source_parser_js_2 = require_mobile_source_parser();
|
|
70848
71016
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
@@ -71750,14 +71918,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
71750
71918
|
const requestMessageCount = messages.length;
|
|
71751
71919
|
const requestMessages = messages.slice();
|
|
71752
71920
|
const callStart = Date.now();
|
|
71753
|
-
const response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
71921
|
+
const response = await (0, mobile_session_keepalive_js_1.withMobileSessionKeepAlive)(driver, () => (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
71754
71922
|
arm();
|
|
71755
71923
|
return aiClient.chat.completions.create({ model: agentModel, messages, tools: GENERATE_TOOLS, tool_choice: "auto", max_tokens: 8e3 }, { signal });
|
|
71756
71924
|
}), {
|
|
71757
71925
|
label: `mobile-generate-${scenario.name.slice(0, 24)}-iter-${iteration}`,
|
|
71758
71926
|
deferTimeoutUntilArmed: true,
|
|
71759
71927
|
onRetry: (attempt, reason, delayMs) => log2(` \u26A0 mobile generate AI retry ${attempt} (${reason}) \u2014 waiting ${delayMs}ms`)
|
|
71760
|
-
});
|
|
71928
|
+
}));
|
|
71761
71929
|
const iterDuration = Date.now() - callStart;
|
|
71762
71930
|
if (!response?.choices?.[0]?.message) {
|
|
71763
71931
|
throw new Error(`AI generate call returned no response at iteration ${iteration}`);
|
|
@@ -72369,6 +72537,21 @@ ${statePacket}`, redactionCredentials) },
|
|
|
72369
72537
|
};
|
|
72370
72538
|
}
|
|
72371
72539
|
const metadata = metadataForElement(element, screenName(), platform3);
|
|
72540
|
+
const expectedNeedle = expected.trim().toLowerCase();
|
|
72541
|
+
const identityValues = [element.accessibilityId, element.resourceId].map((value) => value?.trim().toLowerCase()).filter((value) => Boolean(value));
|
|
72542
|
+
const visibleText = (element.text ?? "").trim().toLowerCase();
|
|
72543
|
+
const isSelfLabelAssertion = expectedNeedle.length > 0 && identityValues.includes(expectedNeedle) && !visibleText.includes(expectedNeedle);
|
|
72544
|
+
if (isSelfLabelAssertion) {
|
|
72545
|
+
recordAction({ type: "ASSERT_VISIBLE", selector: (0, mobile_source_parser_js_2.preferredLocator)(element), metadata });
|
|
72546
|
+
log2(` assert_text DOWNGRADED to assert_visible (tautological self-label "${expected}"): ${metadata.targetLabel}`);
|
|
72547
|
+
return {
|
|
72548
|
+
ok: true,
|
|
72549
|
+
asserted: "visible",
|
|
72550
|
+
downgraded: true,
|
|
72551
|
+
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.`,
|
|
72552
|
+
target: metadata.targetLabel
|
|
72553
|
+
};
|
|
72554
|
+
}
|
|
72372
72555
|
if (hasStableId(element)) {
|
|
72373
72556
|
recordAction({ type: "ASSERT_TEXT", value: expected, selector: (0, mobile_source_parser_js_2.preferredLocator)(element), metadata });
|
|
72374
72557
|
log2(` assert_text: ${metadata.targetLabel} ~ "${expected}"`);
|
|
@@ -73062,7 +73245,13 @@ ${statePacket}`, redactionCredentials) },
|
|
|
73062
73245
|
const strengthReason = coldResetUnavailable ? `Cold app-data reset unavailable${coldResetReason ? ` (${coldResetReason})` : ""} \u2014 generation may not match cold-boot replay. ${strengthVerdict.reason}` : !verified && recording.assertCount === 0 ? "Mobile scenario finished unverified \u2014 the agent never proved the scenario outcome live, so the recording may not demonstrate the goal (e.g. a happy path that never completed). Review before promotion." : strengthVerdict.reason;
|
|
73063
73246
|
const test = {
|
|
73064
73247
|
name: compiled.name,
|
|
73065
|
-
|
|
73248
|
+
// Description priority: the model's live finish description, then the
|
|
73249
|
+
// planner's scenario goal (always a meaningful sentence describing what the
|
|
73250
|
+
// test proves), then the generic compiled placeholder. Without the goal
|
|
73251
|
+
// fallback a scenario whose finish_generate omitted a description shipped
|
|
73252
|
+
// with the contentless "<platform> native flow generated from a recorded
|
|
73253
|
+
// mobile session." string — see mobile-test-generation.ts's compiled default.
|
|
73254
|
+
description: recording.finish?.description?.trim() || scenario.goal?.trim() || compiled.description,
|
|
73066
73255
|
framework: "APPIUM",
|
|
73067
73256
|
appiumCode: compiled.appiumCode,
|
|
73068
73257
|
steps: compiled.steps,
|
|
@@ -75739,6 +75928,118 @@ var require_har_stream = __commonJS({
|
|
|
75739
75928
|
}
|
|
75740
75929
|
});
|
|
75741
75930
|
|
|
75931
|
+
// ../runner-core/dist/services/api-auth-headers.js
|
|
75932
|
+
var require_api_auth_headers = __commonJS({
|
|
75933
|
+
"../runner-core/dist/services/api-auth-headers.js"(exports2) {
|
|
75934
|
+
"use strict";
|
|
75935
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
75936
|
+
exports2.parseApiAuthHeaders = parseApiAuthHeaders;
|
|
75937
|
+
exports2.resolveApiAuthHeaders = resolveApiAuthHeaders;
|
|
75938
|
+
exports2.isAppOriginRequest = isAppOriginRequest;
|
|
75939
|
+
exports2.buildOriginScopedApiFixtures = buildOriginScopedApiFixtures;
|
|
75940
|
+
var HEADER_NAME_RE = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
|
|
75941
|
+
function hasControlChar(v) {
|
|
75942
|
+
for (let i = 0; i < v.length; i++) {
|
|
75943
|
+
const code = v.charCodeAt(i);
|
|
75944
|
+
if (code < 32 || code === 127)
|
|
75945
|
+
return true;
|
|
75946
|
+
}
|
|
75947
|
+
return false;
|
|
75948
|
+
}
|
|
75949
|
+
function parseApiAuthHeaders(raw) {
|
|
75950
|
+
if (!raw || typeof raw !== "string")
|
|
75951
|
+
return void 0;
|
|
75952
|
+
const trimmed = raw.trim();
|
|
75953
|
+
if (!trimmed)
|
|
75954
|
+
return void 0;
|
|
75955
|
+
try {
|
|
75956
|
+
const parsed = JSON.parse(trimmed);
|
|
75957
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
75958
|
+
return void 0;
|
|
75959
|
+
const headers = {};
|
|
75960
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
75961
|
+
if (typeof k === "string" && HEADER_NAME_RE.test(k) && typeof v === "string" && !hasControlChar(v)) {
|
|
75962
|
+
headers[k] = v;
|
|
75963
|
+
}
|
|
75964
|
+
}
|
|
75965
|
+
return Object.keys(headers).length > 0 ? headers : void 0;
|
|
75966
|
+
} catch {
|
|
75967
|
+
return void 0;
|
|
75968
|
+
}
|
|
75969
|
+
}
|
|
75970
|
+
function resolveApiAuthHeaders(input) {
|
|
75971
|
+
if (input.explicit)
|
|
75972
|
+
return input.explicit;
|
|
75973
|
+
if (!input.isApiTest)
|
|
75974
|
+
return void 0;
|
|
75975
|
+
return parseApiAuthHeaders(input.apiAuthHeadersVar);
|
|
75976
|
+
}
|
|
75977
|
+
function isAppOriginRequest(url, baseUrl) {
|
|
75978
|
+
if (typeof url !== "string" || !baseUrl)
|
|
75979
|
+
return false;
|
|
75980
|
+
let appOrigin;
|
|
75981
|
+
try {
|
|
75982
|
+
appOrigin = new URL(baseUrl).origin;
|
|
75983
|
+
} catch {
|
|
75984
|
+
return false;
|
|
75985
|
+
}
|
|
75986
|
+
try {
|
|
75987
|
+
return new URL(url, baseUrl).origin === appOrigin;
|
|
75988
|
+
} catch {
|
|
75989
|
+
return false;
|
|
75990
|
+
}
|
|
75991
|
+
}
|
|
75992
|
+
function buildOriginScopedApiFixtures(headers, baseUrl) {
|
|
75993
|
+
let appOrigin;
|
|
75994
|
+
try {
|
|
75995
|
+
appOrigin = baseUrl ? new URL(baseUrl).origin : null;
|
|
75996
|
+
} catch {
|
|
75997
|
+
appOrigin = null;
|
|
75998
|
+
}
|
|
75999
|
+
return `export * from '@playwright/test';
|
|
76000
|
+
import { test as base } from '@playwright/test';
|
|
76001
|
+
|
|
76002
|
+
// Standing auth headers \u2014 attached ONLY to same-origin requests so the credential never leaks to
|
|
76003
|
+
// an off-origin host. Embedded at run time; this file lives in the ephemeral run dir.
|
|
76004
|
+
const STANDING_HEADERS = ${JSON.stringify(headers)};
|
|
76005
|
+
const APP_ORIGIN = ${JSON.stringify(appOrigin)};
|
|
76006
|
+
const BASE_URL = ${JSON.stringify(baseUrl)};
|
|
76007
|
+
const METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'fetch'];
|
|
76008
|
+
|
|
76009
|
+
function sameOrigin(url) {
|
|
76010
|
+
if (!APP_ORIGIN || typeof url !== 'string') return false;
|
|
76011
|
+
try { return new URL(url, BASE_URL).origin === APP_ORIGIN; } catch { return false; }
|
|
76012
|
+
}
|
|
76013
|
+
|
|
76014
|
+
export const test = base.extend({
|
|
76015
|
+
request: async ({ request }, use) => {
|
|
76016
|
+
const scoped = new Proxy(request, {
|
|
76017
|
+
get(target, prop) {
|
|
76018
|
+
const value = target[prop];
|
|
76019
|
+
// HTTP verbs: wrap to add standing headers on same-origin requests only.
|
|
76020
|
+
if (typeof prop === 'string' && METHODS.indexOf(prop) !== -1) {
|
|
76021
|
+
const orig = value.bind(target);
|
|
76022
|
+
return (url, options) => {
|
|
76023
|
+
options = options || {};
|
|
76024
|
+
return sameOrigin(url)
|
|
76025
|
+
? orig(url, { ...options, headers: { ...STANDING_HEADERS, ...(options.headers || {}) } })
|
|
76026
|
+
: orig(url, options);
|
|
76027
|
+
};
|
|
76028
|
+
}
|
|
76029
|
+
// Every other METHOD must run with 'this' bound to the REAL context \u2014 an APIRequestContext
|
|
76030
|
+
// uses internal slots/private fields a Proxy can't forward, so calling with 'this'=proxy
|
|
76031
|
+
// would throw. Non-function props pass through untouched.
|
|
76032
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
76033
|
+
},
|
|
76034
|
+
});
|
|
76035
|
+
await use(scoped);
|
|
76036
|
+
},
|
|
76037
|
+
});
|
|
76038
|
+
`;
|
|
76039
|
+
}
|
|
76040
|
+
}
|
|
76041
|
+
});
|
|
76042
|
+
|
|
75742
76043
|
// ../runner-core/dist/services/playwright-native.js
|
|
75743
76044
|
var require_playwright_native = __commonJS({
|
|
75744
76045
|
"../runner-core/dist/services/playwright-native.js"(exports2) {
|
|
@@ -75781,7 +76082,7 @@ var require_playwright_native = __commonJS({
|
|
|
75781
76082
|
};
|
|
75782
76083
|
})();
|
|
75783
76084
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
75784
|
-
exports2.scrubHarRequestBody = void 0;
|
|
76085
|
+
exports2.scrubHarRequestBody = exports2.buildOriginScopedApiFixtures = exports2.isAppOriginRequest = exports2.resolveApiAuthHeaders = exports2.parseApiAuthHeaders = void 0;
|
|
75785
76086
|
exports2.executeTestViaNativePlaywright = executeTestViaNativePlaywright2;
|
|
75786
76087
|
exports2.parseJsonResults = parseJsonResults2;
|
|
75787
76088
|
exports2.parseHarToApiSummary = parseHarToApiSummary;
|
|
@@ -75793,6 +76094,20 @@ var require_playwright_native = __commonJS({
|
|
|
75793
76094
|
var spawn_with_tree_kill_js_1 = require_spawn_with_tree_kill();
|
|
75794
76095
|
var api_call_redaction_js_1 = require_api_call_redaction();
|
|
75795
76096
|
var har_stream_js_1 = require_har_stream();
|
|
76097
|
+
var api_auth_headers_js_1 = require_api_auth_headers();
|
|
76098
|
+
var api_auth_headers_js_2 = require_api_auth_headers();
|
|
76099
|
+
Object.defineProperty(exports2, "parseApiAuthHeaders", { enumerable: true, get: function() {
|
|
76100
|
+
return api_auth_headers_js_2.parseApiAuthHeaders;
|
|
76101
|
+
} });
|
|
76102
|
+
Object.defineProperty(exports2, "resolveApiAuthHeaders", { enumerable: true, get: function() {
|
|
76103
|
+
return api_auth_headers_js_2.resolveApiAuthHeaders;
|
|
76104
|
+
} });
|
|
76105
|
+
Object.defineProperty(exports2, "isAppOriginRequest", { enumerable: true, get: function() {
|
|
76106
|
+
return api_auth_headers_js_2.isAppOriginRequest;
|
|
76107
|
+
} });
|
|
76108
|
+
Object.defineProperty(exports2, "buildOriginScopedApiFixtures", { enumerable: true, get: function() {
|
|
76109
|
+
return api_auth_headers_js_2.buildOriginScopedApiFixtures;
|
|
76110
|
+
} });
|
|
75796
76111
|
var api_call_redaction_js_2 = require_api_call_redaction();
|
|
75797
76112
|
Object.defineProperty(exports2, "scrubHarRequestBody", { enumerable: true, get: function() {
|
|
75798
76113
|
return api_call_redaction_js_2.scrubHarRequestBody;
|
|
@@ -75802,7 +76117,7 @@ var require_playwright_native = __commonJS({
|
|
|
75802
76117
|
var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
|
|
75803
76118
|
var PW_CLI = require.resolve("@playwright/test/cli");
|
|
75804
76119
|
async function executeTestViaNativePlaywright2(options) {
|
|
75805
|
-
const { playwrightCode, baseUrl, testName, testVariables, storageStatePath, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
|
|
76120
|
+
const { playwrightCode, baseUrl, testName, testVariables, storageStatePath, extraHttpHeaders, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
|
|
75806
76121
|
const logs2 = [];
|
|
75807
76122
|
const screenshots = [];
|
|
75808
76123
|
logs2.push(`\u{1F3AD} Native Playwright starting`);
|
|
@@ -75816,6 +76131,11 @@ var require_playwright_native = __commonJS({
|
|
|
75816
76131
|
runDir = (0, path_1.join)(RUNS_DIR, (0, crypto_1.randomUUID)());
|
|
75817
76132
|
await (0, promises_1.mkdir)(runDir, { recursive: true });
|
|
75818
76133
|
const isApiTest2 = playwrightCode.includes("@api") || playwrightCode.includes("{ request }") && !playwrightCode.includes("{ page");
|
|
76134
|
+
const resolvedExtraHeaders = (0, api_auth_headers_js_1.resolveApiAuthHeaders)({
|
|
76135
|
+
explicit: extraHttpHeaders,
|
|
76136
|
+
isApiTest: isApiTest2,
|
|
76137
|
+
apiAuthHeadersVar: testVariables?.API_AUTH_HEADERS
|
|
76138
|
+
});
|
|
75819
76139
|
const cssDisableSetup = `
|
|
75820
76140
|
const __qaLiveBrowserStateFile = process.env.VALIDATE_LIVE_BROWSER_STATE_PATH;
|
|
75821
76141
|
let __qaLiveBrowserInterval: ReturnType<typeof setInterval> | null = null;
|
|
@@ -76009,8 +76329,17 @@ test.afterEach(async ({ page }, testInfo) => {
|
|
|
76009
76329
|
if (!importMatch) {
|
|
76010
76330
|
logs2.push("\u26A0 Could not find @playwright/test import - animation disabling NOT injected");
|
|
76011
76331
|
}
|
|
76012
|
-
const
|
|
76332
|
+
const useScopedApiHeaders = isApiTest2 && !!resolvedExtraHeaders && !!importMatch && !!baseUrl;
|
|
76333
|
+
let instrumentedCode;
|
|
76334
|
+
if (isApiTest2) {
|
|
76335
|
+
instrumentedCode = useScopedApiHeaders ? playwrightCode.replace(importMatch[0], importMatch[0].replace(/from\s+['"]@playwright\/test['"]/, "from './qa-api-fixtures'")) : playwrightCode;
|
|
76336
|
+
} else {
|
|
76337
|
+
instrumentedCode = importMatch ? playwrightCode.replace(importMatch[0], importMatch[0] + cssDisableSetup) : playwrightCode;
|
|
76338
|
+
}
|
|
76013
76339
|
await (0, promises_1.writeFile)((0, path_1.join)(runDir, "test.spec.ts"), instrumentedCode);
|
|
76340
|
+
if (useScopedApiHeaders && resolvedExtraHeaders) {
|
|
76341
|
+
await (0, promises_1.writeFile)((0, path_1.join)(runDir, "qa-api-fixtures.ts"), (0, api_auth_headers_js_1.buildOriginScopedApiFixtures)(resolvedExtraHeaders, baseUrl));
|
|
76342
|
+
}
|
|
76014
76343
|
const headless = process.env.PLAYWRIGHT_HEADLESS === "true" || process.env.NODE_ENV === "production" || IS_CLOUD2;
|
|
76015
76344
|
logs2.push(`\u2699\uFE0F Config: baseURL=${JSON.stringify(baseUrl)}, headless=${headless}${isApiTest2 ? ", API mode (no screenshot/video)" : ""}`);
|
|
76016
76345
|
const absTestDir = runDir.replace(/\\/g, "/");
|
|
@@ -76024,6 +76353,7 @@ test.afterEach(async ({ page }, testInfo) => {
|
|
|
76024
76353
|
const absHarPath = wantHar ? (0, path_1.join)(runDir, "session.har").replace(/\\/g, "/") : null;
|
|
76025
76354
|
const recordHarLine = absHarPath ? `recordHar: { path: ${JSON.stringify(absHarPath)}, mode: 'full' },` : "";
|
|
76026
76355
|
const storageStateLine = storageStatePath ? `storageState: ${JSON.stringify(storageStatePath.replace(/\\/g, "/"))},` : "";
|
|
76356
|
+
const extraHeadersLine = resolvedExtraHeaders && !useScopedApiHeaders && Object.keys(resolvedExtraHeaders).length > 0 ? `extraHTTPHeaders: ${JSON.stringify(resolvedExtraHeaders)},` : "";
|
|
76027
76357
|
const launchArgs = IS_CLOUD2 ? "args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-blink-features=AutomationControlled', '--disable-dev-shm-usage', '--disable-gpu', '--disable-software-rasterizer', '--no-zygote', '--disable-accelerated-2d-canvas', '--js-flags=--max-old-space-size=512']," : "args: ['--disable-blink-features=AutomationControlled'],";
|
|
76028
76358
|
const config = `
|
|
76029
76359
|
// Plain object export - no require('@playwright/test') needed.
|
|
@@ -76037,6 +76367,7 @@ module.exports = {
|
|
|
76037
76367
|
use: {
|
|
76038
76368
|
baseURL: ${JSON.stringify(baseUrl)},
|
|
76039
76369
|
${storageStateLine}
|
|
76370
|
+
${extraHeadersLine}
|
|
76040
76371
|
browserName: 'chromium',
|
|
76041
76372
|
headless: ${headless},
|
|
76042
76373
|
screenshot: ${isApiTest2 ? "'off'" : "'on'"},
|
|
@@ -76078,6 +76409,7 @@ module.exports = {
|
|
|
76078
76409
|
// code. Forwarded (not allowlisted) so it stays a per-run toggle; absent → inert.
|
|
76079
76410
|
...process.env.SHARED_AUTH_INJECT ? { SHARED_AUTH_INJECT: process.env.SHARED_AUTH_INJECT } : {}
|
|
76080
76411
|
};
|
|
76412
|
+
delete envWithIsolation.API_AUTH_HEADERS;
|
|
76081
76413
|
logs2.push("\u{1F680} Spawning playwright test...");
|
|
76082
76414
|
const stopLiveBrowserPolling = liveBrowserStatePath && liveBrowserHandle ? startLiveBrowserStatePolling(liveBrowserStatePath, liveBrowserHandle, onLiveBrowserSnapshot) : null;
|
|
76083
76415
|
let playResult;
|
|
@@ -84184,6 +84516,7 @@ var require_discover_generator = __commonJS({
|
|
|
84184
84516
|
var ai_provider_js_1 = require_ai_provider();
|
|
84185
84517
|
var ai_queue_js_1 = require_ai_queue();
|
|
84186
84518
|
var ai_retry_js_1 = require_ai_retry();
|
|
84519
|
+
var discover_ai_planner_js_1 = require_discover_ai_planner();
|
|
84187
84520
|
var snapshot_parser_js_1 = require_snapshot_parser();
|
|
84188
84521
|
var scrub_credentials_js_1 = require_scrub_credentials();
|
|
84189
84522
|
var exploration_parser_js_1 = require_exploration_parser();
|
|
@@ -87135,7 +87468,15 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
87135
87468
|
tools: openaiTools,
|
|
87136
87469
|
tool_choice: "auto",
|
|
87137
87470
|
temperature: 0.2,
|
|
87138
|
-
|
|
87471
|
+
// Shared provider-aware output ceiling (same helper the survey planner uses):
|
|
87472
|
+
// generous where the provider's max output is verified large (deepseek/minimax
|
|
87473
|
+
// 65536), conservative elsewhere (16384). The old flat 8192 truncated large
|
|
87474
|
+
// finish_generate emissions mid-JSON — the loop re-prompted on the parse error
|
|
87475
|
+
// (line ~4744) and, for a test that genuinely needs >8192 output tokens, could
|
|
87476
|
+
// never finish before the hard stop → zero tests for that scenario. It's a
|
|
87477
|
+
// ceiling, not a target (calls finish at finish_reason:'stop' well under it), so
|
|
87478
|
+
// this adds no per-call cost.
|
|
87479
|
+
max_tokens: (0, discover_ai_planner_js_1.getAIPlannerMaxTokens)(agentModel)
|
|
87139
87480
|
}, { signal });
|
|
87140
87481
|
}), {
|
|
87141
87482
|
label: `scenario-gen-iter-${iteration}`,
|
|
@@ -321746,6 +322087,7 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
321746
322087
|
var mobile_exploration_state_js_1 = require_mobile_exploration_state();
|
|
321747
322088
|
var mobile_discovery_auth_js_1 = require_mobile_discovery_auth();
|
|
321748
322089
|
var mobile_driver_errors_js_1 = require_mobile_driver_errors();
|
|
322090
|
+
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
321749
322091
|
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
321750
322092
|
var discover_planner_js_1 = require_discover_planner();
|
|
321751
322093
|
function checkpointErrorMessage(err) {
|
|
@@ -322046,7 +322388,7 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
|
|
|
322046
322388
|
const graph = state2.toScreenGraph();
|
|
322047
322389
|
const screenMap = state2.buildInMemoryScreenMap();
|
|
322048
322390
|
const testBudget = (0, discover_planner_js_1.computeTestBudget)(explore.exploreStats.pagesVisited, ctx.focusAreas);
|
|
322049
|
-
const planResult = await runPlan({
|
|
322391
|
+
const planResult = await (0, mobile_session_keepalive_js_1.withMobileSessionKeepAlive)(driver, () => runPlan({
|
|
322050
322392
|
graph,
|
|
322051
322393
|
mode,
|
|
322052
322394
|
projectId: ctx.projectId,
|
|
@@ -322070,7 +322412,7 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
|
|
|
322070
322412
|
}, {
|
|
322071
322413
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
322072
322414
|
onAICall
|
|
322073
|
-
});
|
|
322415
|
+
}));
|
|
322074
322416
|
logs2.push(...planResult.logs);
|
|
322075
322417
|
log2(`
|
|
322076
322418
|
Plan: ${planResult.scenarios.length} scenarios, ${planResult.featureGroups.length} feature groups (budget: ${testBudget})`);
|
|
@@ -322928,6 +323270,7 @@ Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.leng
|
|
|
322928
323270
|
if (surveyAuthRecipe) {
|
|
322929
323271
|
logs2.push(`Auth-prologue: captured + verified login recipe ${surveyAuthRecipe.recipeHash} during survey \u2014 persisting for reuse on later runs`);
|
|
322930
323272
|
}
|
|
323273
|
+
const plannerFailedError = planResult.plannerFailed ? `Survey planning failed (${planResult.plannerFailureKind ?? "unknown"}): ${planResult.fallbackReason ?? "planner did not produce a usable plan"}. Explore evidence was saved; re-run the survey to retry planning.` : void 0;
|
|
322931
323274
|
return {
|
|
322932
323275
|
observedOrigins: mergeObservedOrigins(exploreResult.observedOrigins, loginObservedOrigins),
|
|
322933
323276
|
resolvedAppOrigin: surveyAuthRecipe?.appOrigin ?? void 0,
|
|
@@ -322943,7 +323286,8 @@ Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.leng
|
|
|
322943
323286
|
exploreTruncatedReason: exploreResult.truncatedReason,
|
|
322944
323287
|
fallbackUsed: planResult.fallbackUsed ?? false,
|
|
322945
323288
|
fallbackReason: planResult.fallbackReason,
|
|
322946
|
-
|
|
323289
|
+
error: plannerFailedError,
|
|
323290
|
+
featureGroups: plannerFailedError ? void 0 : featureGroups,
|
|
322947
323291
|
apiCalls: discoveryApiCalls.length > 0 ? discoveryApiCalls : void 0,
|
|
322948
323292
|
healthObservations: surveyHealth,
|
|
322949
323293
|
exploreFinalMessages: exploreResult.finalMessages,
|
|
@@ -336801,7 +337145,7 @@ var require_package4 = __commonJS({
|
|
|
336801
337145
|
"package.json"(exports2, module2) {
|
|
336802
337146
|
module2.exports = {
|
|
336803
337147
|
name: "@validate.qa/runner",
|
|
336804
|
-
version: "1.0.
|
|
337148
|
+
version: "1.0.40",
|
|
336805
337149
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
336806
337150
|
bin: {
|
|
336807
337151
|
"validate-runner": "dist/cli.js"
|
|
@@ -345618,10 +345962,22 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345618
345962
|
onPlanReady
|
|
345619
345963
|
});
|
|
345620
345964
|
} finally {
|
|
345965
|
+
const TEARDOWN_STEP_TIMEOUT = /* @__PURE__ */ Symbol("vqa-teardown-timeout");
|
|
345966
|
+
const timeboxed = async (step) => Promise.race([
|
|
345967
|
+
step,
|
|
345968
|
+
new Promise((resolve2) => {
|
|
345969
|
+
const t = setTimeout(() => resolve2(TEARDOWN_STEP_TIMEOUT), 45e3);
|
|
345970
|
+
t.unref?.();
|
|
345971
|
+
})
|
|
345972
|
+
]);
|
|
345621
345973
|
const activeCapture = capture;
|
|
345622
345974
|
if (activeCapture) {
|
|
345623
345975
|
try {
|
|
345624
|
-
const
|
|
345976
|
+
const stopped = await timeboxed(activeCapture.stop());
|
|
345977
|
+
if (stopped === TEARDOWN_STEP_TIMEOUT) {
|
|
345978
|
+
throw new Error("capture stop timed out after 45s \u2014 continuing teardown without apiCalls");
|
|
345979
|
+
}
|
|
345980
|
+
const apiCalls = stopped;
|
|
345625
345981
|
const captureHealth = activeCapture.health();
|
|
345626
345982
|
if (discoveryResult) {
|
|
345627
345983
|
discoveryResult.apiCalls = apiCalls.length ? attachMobileDiscoveryNetworkContext(apiCalls, discoveryResult, session?.appId ?? mobileTarget.appId) : void 0;
|
|
@@ -345642,7 +345998,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345642
345998
|
if (session) {
|
|
345643
345999
|
detachMirrorSession(session.deviceId);
|
|
345644
346000
|
try {
|
|
345645
|
-
await session.stop();
|
|
346001
|
+
await timeboxed(session.stop());
|
|
345646
346002
|
} catch {
|
|
345647
346003
|
}
|
|
345648
346004
|
}
|