@validate.qa/runner 1.0.38 → 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 +1233 -711
- package/dist/cli.mjs +1233 -711
- 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) {
|
|
@@ -6218,6 +6249,689 @@ var require_mobile_observation = __commonJS({
|
|
|
6218
6249
|
}
|
|
6219
6250
|
});
|
|
6220
6251
|
|
|
6252
|
+
// ../runner-core/dist/services/mobile/mobile-exploration-state.js
|
|
6253
|
+
var require_mobile_exploration_state = __commonJS({
|
|
6254
|
+
"../runner-core/dist/services/mobile/mobile-exploration-state.js"(exports) {
|
|
6255
|
+
"use strict";
|
|
6256
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6257
|
+
exports.MobileExplorationState = void 0;
|
|
6258
|
+
exports.isDestructiveActionLabel = isDestructiveActionLabel;
|
|
6259
|
+
exports.isDestructiveConfirmationInput = isDestructiveConfirmationInput;
|
|
6260
|
+
exports.stableMobileControlIdentity = stableMobileControlIdentity;
|
|
6261
|
+
exports.classifySafeMobileNavigationControl = classifySafeMobileNavigationControl;
|
|
6262
|
+
exports.deriveScreenId = deriveScreenId;
|
|
6263
|
+
var mobile_observation_js_1 = require_mobile_observation();
|
|
6264
|
+
var DEFAULT_STALE_THRESHOLD = 4;
|
|
6265
|
+
var MAX_SCREEN_OBSERVATIONS = 50;
|
|
6266
|
+
var EDITABLE_CONTROL_ROLE_RE = /(?:android\.widget\.(?:EditText|AutoCompleteTextView)|XCUIElementType(?:TextField|SecureTextField|SearchField|TextView)|textbox|searchbox|combobox|textarea|select)$/i;
|
|
6267
|
+
var STATEFUL_CONTROL_ROLE_RE = /(?:Switch|CheckBox|Checkbox|RadioButton|radio|Slider|SeekBar|Stepper|PickerWheel|Toggle)$/i;
|
|
6268
|
+
var UNSAFE_OR_COMMIT_LABEL_RE = /\b(?:submit|save|confirm|delete|remove|archive|reset|disconnect|deactivate|destroy|purge|pay|purchase|buy|checkout|place\s+order|send|post|publish|upload|import|export|sign\s*out|log\s*out|logout|accept|agree|subscribe|unsubscribe|follow|unfollow|enable|disable|book|reserve|cancel|continue|next|previous|done|apply|retry|close|dismiss|sign\s*in|log\s*in|register)\b/i;
|
|
6269
|
+
var SAFE_MODAL_TRIGGER_LABEL_RE = /^(?:create|new|add|edit|invite|configure|manage|details|preview|share)$/i;
|
|
6270
|
+
var MENU_TRIGGER_LABEL_RE = /^(?:more|menu|options|actions|filter|filters|sort|columns|customize)$/i;
|
|
6271
|
+
var GENERIC_NAVIGATION_DESTINATION_LABEL_RE = /^(?:home|feed|dashboard|profile|settings|inbox|messages?|notifications?|projects?|about|privacy|terms|learn\s+more)$/i;
|
|
6272
|
+
var SYSTEM_BACK_CONTROL_LABEL_RE = /^(?:back|go\s+back|navigate\s+up|navigation\s+up|up)$/i;
|
|
6273
|
+
var SYSTEM_BACK_CONTROL_ID_RE = /^(?:action_bar_up|arrow_back|back_arrow|navigate_up|navigation_up|nav_up|toolbar_back|action_back|btn_back|button_back|back_button|back_btn|back_icon|icon_back)$/i;
|
|
6274
|
+
var DESTRUCTIVE_ACTION_LABEL_RE = /\b(?:deactivate|delete\s+account|delete\s+profile|close\s+account|cancel\s+account|remove\s+account|delete\s+(?:my\s+)?(?:data|history|everything|account)|delete\s+(?:all|forever|permanently)|permanently\s+(?:delete|remove|erase|deactivate)|erase\s+(?:all|everything|account|data)|wipe\s+(?:account|data|everything|all|device)|purge\s+(?:account|data|everything|all)|destroy\s+(?:account|data)|factory\s+reset|reset\s+(?:all\s+data|everything))\b/i;
|
|
6275
|
+
var DESTRUCTIVE_CONFIRMATION_INPUT_RE = /^(?:delete|delete\s+my\s+account|delete\s+account|delete\s+forever|permanently\s+delete|confirm\s+delete|erase|erase\s+all|wipe|deactivate|remove\s+account|close\s+account|yes,?\s*delete(?:\s+my\s+account)?)$/i;
|
|
6276
|
+
function isDestructiveActionLabel(label) {
|
|
6277
|
+
if (!label)
|
|
6278
|
+
return false;
|
|
6279
|
+
return DESTRUCTIVE_ACTION_LABEL_RE.test(normalizeControlIdentityPart(label));
|
|
6280
|
+
}
|
|
6281
|
+
function isDestructiveConfirmationInput(value) {
|
|
6282
|
+
if (!value)
|
|
6283
|
+
return false;
|
|
6284
|
+
return DESTRUCTIVE_CONFIRMATION_INPUT_RE.test(value.trim());
|
|
6285
|
+
}
|
|
6286
|
+
function normalizeControlIdentityPart(value) {
|
|
6287
|
+
return value.trim().replace(/\s+/g, " ").toLowerCase();
|
|
6288
|
+
}
|
|
6289
|
+
function semanticControlAlias(label) {
|
|
6290
|
+
return `navigation-label:${normalizeControlIdentityPart(label)}`;
|
|
6291
|
+
}
|
|
6292
|
+
function localControlCoverageIdentity(screenId, identity) {
|
|
6293
|
+
return `screen:${screenId}:${identity}`;
|
|
6294
|
+
}
|
|
6295
|
+
function storedMobileControlIdentity(candidate) {
|
|
6296
|
+
return [
|
|
6297
|
+
candidate.identity,
|
|
6298
|
+
`kind:${candidate.kind}`,
|
|
6299
|
+
`label:${normalizeControlIdentityPart(candidate.label)}`
|
|
6300
|
+
].join(":");
|
|
6301
|
+
}
|
|
6302
|
+
function isSystemBackNavigationControl(element, label) {
|
|
6303
|
+
if (SYSTEM_BACK_CONTROL_LABEL_RE.test(normalizeControlIdentityPart(label)))
|
|
6304
|
+
return true;
|
|
6305
|
+
return [element.accessibilityId, element.resourceId].filter((value) => Boolean(value?.trim())).some((value) => {
|
|
6306
|
+
const leaf = normalizeControlIdentityPart(value).split(/[/:.]/).filter(Boolean).at(-1) ?? "";
|
|
6307
|
+
return SYSTEM_BACK_CONTROL_ID_RE.test(leaf);
|
|
6308
|
+
});
|
|
6309
|
+
}
|
|
6310
|
+
function stableMobileControlIdentity(element) {
|
|
6311
|
+
const role = normalizeControlIdentityPart(element.role || "control");
|
|
6312
|
+
const accessibilityId = element.accessibilityId?.trim();
|
|
6313
|
+
if (accessibilityId)
|
|
6314
|
+
return `a11y:${role}:${normalizeControlIdentityPart(accessibilityId)}`;
|
|
6315
|
+
const resourceId = element.resourceId?.trim();
|
|
6316
|
+
if (resourceId)
|
|
6317
|
+
return `resource:${role}:${normalizeControlIdentityPart(resourceId)}`;
|
|
6318
|
+
const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
|
|
6319
|
+
if (label)
|
|
6320
|
+
return `semantic:${role}:${normalizeControlIdentityPart(label)}`;
|
|
6321
|
+
return null;
|
|
6322
|
+
}
|
|
6323
|
+
function classifySafeMobileNavigationControl(element) {
|
|
6324
|
+
if (!element.actionable || element.enabled === false)
|
|
6325
|
+
return null;
|
|
6326
|
+
const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
|
|
6327
|
+
const safetyIdentity = [label, element.accessibilityId, element.resourceId].filter(Boolean).join(" ");
|
|
6328
|
+
if (!label || UNSAFE_OR_COMMIT_LABEL_RE.test(safetyIdentity))
|
|
6329
|
+
return null;
|
|
6330
|
+
if (isSystemBackNavigationControl(element, label))
|
|
6331
|
+
return null;
|
|
6332
|
+
const role = element.role.trim();
|
|
6333
|
+
if (EDITABLE_CONTROL_ROLE_RE.test(role) || STATEFUL_CONTROL_ROLE_RE.test(role))
|
|
6334
|
+
return null;
|
|
6335
|
+
const linkLike = /(?:Link$|^a$)/i.test(role);
|
|
6336
|
+
let kind = null;
|
|
6337
|
+
if (/(?:TabBarItem|\btab\b)/i.test(role))
|
|
6338
|
+
kind = "tab";
|
|
6339
|
+
else if (/menuitem/i.test(role))
|
|
6340
|
+
kind = "menu-item";
|
|
6341
|
+
else if (linkLike && element.navigationProvenance)
|
|
6342
|
+
kind = "link";
|
|
6343
|
+
else if (linkLike)
|
|
6344
|
+
return null;
|
|
6345
|
+
else if (element.hasPopup && element.hasPopup.toLowerCase() !== "false")
|
|
6346
|
+
kind = "menu-trigger";
|
|
6347
|
+
else if (element.expanded === false)
|
|
6348
|
+
kind = "disclosure";
|
|
6349
|
+
else if (MENU_TRIGGER_LABEL_RE.test(label))
|
|
6350
|
+
kind = "menu-trigger";
|
|
6351
|
+
else if (SAFE_MODAL_TRIGGER_LABEL_RE.test(label))
|
|
6352
|
+
kind = "modal-trigger";
|
|
6353
|
+
else if (element.navigationProvenance)
|
|
6354
|
+
kind = "navigation-button";
|
|
6355
|
+
else if (GENERIC_NAVIGATION_DESTINATION_LABEL_RE.test(label))
|
|
6356
|
+
kind = "navigation-button";
|
|
6357
|
+
if (!kind)
|
|
6358
|
+
return null;
|
|
6359
|
+
const identity = stableMobileControlIdentity(element);
|
|
6360
|
+
return identity ? { identity, label, kind } : null;
|
|
6361
|
+
}
|
|
6362
|
+
function sharedMobileControlCoverageIdentity(element, candidate, semanticFamilyId) {
|
|
6363
|
+
if (!element.navigationProvenance)
|
|
6364
|
+
return void 0;
|
|
6365
|
+
const durableId = element.accessibilityId?.trim() ? `a11y:${normalizeControlIdentityPart(element.accessibilityId)}` : element.resourceId?.trim() ? `resource:${normalizeControlIdentityPart(element.resourceId)}` : "";
|
|
6366
|
+
if (!durableId && !semanticFamilyId)
|
|
6367
|
+
return void 0;
|
|
6368
|
+
return [
|
|
6369
|
+
"shared-navigation",
|
|
6370
|
+
element.navigationProvenance,
|
|
6371
|
+
!durableId && semanticFamilyId ? `family:${semanticFamilyId}` : "",
|
|
6372
|
+
candidate.kind,
|
|
6373
|
+
durableId || "semantic-only",
|
|
6374
|
+
normalizeControlIdentityPart(candidate.label)
|
|
6375
|
+
].filter(Boolean).join(":");
|
|
6376
|
+
}
|
|
6377
|
+
function mobileNavigationRegion(element, windowSize) {
|
|
6378
|
+
if (!windowSize || windowSize.width <= 0 || windowSize.height <= 0)
|
|
6379
|
+
return "unknown";
|
|
6380
|
+
const centerX = (element.bounds.x + element.bounds.width / 2) / windowSize.width;
|
|
6381
|
+
const centerY = (element.bounds.y + element.bounds.height / 2) / windowSize.height;
|
|
6382
|
+
if (centerY >= 0.72)
|
|
6383
|
+
return "bottom";
|
|
6384
|
+
if (centerY <= 0.28)
|
|
6385
|
+
return "top";
|
|
6386
|
+
if (centerX <= 0.35)
|
|
6387
|
+
return "left";
|
|
6388
|
+
if (centerX >= 0.65)
|
|
6389
|
+
return "right";
|
|
6390
|
+
return "center";
|
|
6391
|
+
}
|
|
6392
|
+
function buildSemanticNavigationGroups(elements, windowSize) {
|
|
6393
|
+
const groups = /* @__PURE__ */ new Map();
|
|
6394
|
+
for (const element of elements) {
|
|
6395
|
+
if (!element.navigationProvenance)
|
|
6396
|
+
continue;
|
|
6397
|
+
const candidate = classifySafeMobileNavigationControl(element);
|
|
6398
|
+
if (!candidate)
|
|
6399
|
+
continue;
|
|
6400
|
+
const groupKey = `${element.navigationProvenance}:${mobileNavigationRegion(element, windowSize)}`;
|
|
6401
|
+
const labels = groups.get(groupKey) ?? /* @__PURE__ */ new Set();
|
|
6402
|
+
labels.add(`${candidate.kind}:${normalizeControlIdentityPart(candidate.label)}`);
|
|
6403
|
+
groups.set(groupKey, labels);
|
|
6404
|
+
}
|
|
6405
|
+
return new Map([...groups].filter(([, labels]) => labels.size >= 2));
|
|
6406
|
+
}
|
|
6407
|
+
function deriveScreenId(signal) {
|
|
6408
|
+
const namespace = signal.activity?.trim() || signal.bundleId?.trim();
|
|
6409
|
+
return namespace ? `${namespace}#${signal.screenHash}` : signal.screenHash;
|
|
6410
|
+
}
|
|
6411
|
+
function observedElementIdentity(element) {
|
|
6412
|
+
return (0, mobile_observation_js_1.stableParsedMobileElementIdentity)(element);
|
|
6413
|
+
}
|
|
6414
|
+
function mergeObservedElements(previous, current) {
|
|
6415
|
+
const merged = [...current];
|
|
6416
|
+
const seen = new Set(current.map(observedElementIdentity));
|
|
6417
|
+
for (const element of previous) {
|
|
6418
|
+
const identity = observedElementIdentity(element);
|
|
6419
|
+
if (seen.has(identity))
|
|
6420
|
+
continue;
|
|
6421
|
+
seen.add(identity);
|
|
6422
|
+
merged.push(element);
|
|
6423
|
+
}
|
|
6424
|
+
return merged;
|
|
6425
|
+
}
|
|
6426
|
+
function sanitizeElementsForEvidence(elements) {
|
|
6427
|
+
return elements.map((element) => {
|
|
6428
|
+
const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element);
|
|
6429
|
+
return {
|
|
6430
|
+
...element,
|
|
6431
|
+
...label ? { label } : { label: void 0 },
|
|
6432
|
+
text: void 0
|
|
6433
|
+
};
|
|
6434
|
+
});
|
|
6435
|
+
}
|
|
6436
|
+
var MobileExplorationState = class {
|
|
6437
|
+
/** screenId -> ScreenState (the mobile analog of routeState). */
|
|
6438
|
+
screens = /* @__PURE__ */ new Map();
|
|
6439
|
+
/** Observed directed transitions between screens. */
|
|
6440
|
+
transitions = [];
|
|
6441
|
+
/** Transition-announced screens that do not yet have a live observation. */
|
|
6442
|
+
frontier = /* @__PURE__ */ new Map();
|
|
6443
|
+
/** Safe navigation controls observed on each screen, keyed by stable identity. */
|
|
6444
|
+
safeControlsByScreen = /* @__PURE__ */ new Map();
|
|
6445
|
+
/** Screen-scoped coverage keys plus explicitly proven shared-navigation keys. */
|
|
6446
|
+
attemptedSafeControlCoverageIdentities = /* @__PURE__ */ new Set();
|
|
6447
|
+
successfulSafeControlAttemptCount = 0;
|
|
6448
|
+
/** Repeated id-less structural navigation families, grouped by region. */
|
|
6449
|
+
semanticNavigationFamilies = /* @__PURE__ */ new Map();
|
|
6450
|
+
nextSemanticNavigationFamilyId = 1;
|
|
6451
|
+
/** The screen-id of the most recently observed screen. */
|
|
6452
|
+
currentScreenId = null;
|
|
6453
|
+
/** Latest iteration recorded (drives stale detection + firstSeen stamps). */
|
|
6454
|
+
iteration = 0;
|
|
6455
|
+
/** Stale detector: progress mark (#screens + #total elements) at last advance. */
|
|
6456
|
+
lastProgressMark = 0;
|
|
6457
|
+
/** Consecutive iterations observed with no progress. */
|
|
6458
|
+
staleIterations = 0;
|
|
6459
|
+
staleThreshold;
|
|
6460
|
+
constructor(options = {}) {
|
|
6461
|
+
this.staleThreshold = typeof options.staleThreshold === "number" && options.staleThreshold > 0 ? Math.floor(options.staleThreshold) : DEFAULT_STALE_THRESHOLD;
|
|
6462
|
+
}
|
|
6463
|
+
// -------------------------------------------------------------------------
|
|
6464
|
+
// Observation recording
|
|
6465
|
+
// -------------------------------------------------------------------------
|
|
6466
|
+
/**
|
|
6467
|
+
* Returns true when `signal` identifies a screen this run has never observed
|
|
6468
|
+
* before. Pure read — does not mutate state.
|
|
6469
|
+
*/
|
|
6470
|
+
isNewScreen(signal) {
|
|
6471
|
+
return !this.screens.has(deriveScreenId(signal));
|
|
6472
|
+
}
|
|
6473
|
+
/**
|
|
6474
|
+
* Record an observation of `screen` at `iteration`. Creates a `ScreenState`
|
|
6475
|
+
* on first sight (stamping `firstSeenIteration`), otherwise refreshes the
|
|
6476
|
+
* stored element set (latest-wins, mirroring the web reducer's PAGE_CAPTURED
|
|
6477
|
+
* element refresh). A driver observation already contains the structural UI
|
|
6478
|
+
* tree, elements, screenshot association, and screen identity, so it is
|
|
6479
|
+
* captured evidence immediately; the model's capture_screen call only adds
|
|
6480
|
+
* a friendly name/type. Returns the resolved screen-id.
|
|
6481
|
+
*/
|
|
6482
|
+
recordObservation(signal, screen, iteration) {
|
|
6483
|
+
this.iteration = iteration;
|
|
6484
|
+
const screenId = deriveScreenId(signal);
|
|
6485
|
+
const evidenceElements = sanitizeElementsForEvidence(screen.elements);
|
|
6486
|
+
const existing = this.screens.get(screenId);
|
|
6487
|
+
if (existing) {
|
|
6488
|
+
this.screens.set(screenId, {
|
|
6489
|
+
...existing,
|
|
6490
|
+
elements: mergeObservedElements(existing.elements, evidenceElements)
|
|
6491
|
+
});
|
|
6492
|
+
} else {
|
|
6493
|
+
this.screens.set(screenId, {
|
|
6494
|
+
screenId,
|
|
6495
|
+
firstSeenIteration: iteration,
|
|
6496
|
+
elements: evidenceElements,
|
|
6497
|
+
visited: true
|
|
6498
|
+
});
|
|
6499
|
+
}
|
|
6500
|
+
this.frontier.delete(screenId);
|
|
6501
|
+
const controls = this.safeControlsByScreen.get(screenId) ?? /* @__PURE__ */ new Map();
|
|
6502
|
+
const semanticNavigationGroups = buildSemanticNavigationGroups(evidenceElements, screen.windowSize);
|
|
6503
|
+
const semanticNavigationFamilyIds = /* @__PURE__ */ new Map();
|
|
6504
|
+
for (const [groupKey, labels] of semanticNavigationGroups) {
|
|
6505
|
+
semanticNavigationFamilyIds.set(groupKey, this.resolveSemanticNavigationFamilyId(groupKey, labels));
|
|
6506
|
+
}
|
|
6507
|
+
for (const element of evidenceElements) {
|
|
6508
|
+
const candidate = classifySafeMobileNavigationControl(element);
|
|
6509
|
+
if (!candidate)
|
|
6510
|
+
continue;
|
|
6511
|
+
const storedIdentity = storedMobileControlIdentity(candidate);
|
|
6512
|
+
const semanticGroupKey = element.navigationProvenance ? `${element.navigationProvenance}:${mobileNavigationRegion(element, screen.windowSize)}` : "";
|
|
6513
|
+
controls.set(storedIdentity, {
|
|
6514
|
+
...candidate,
|
|
6515
|
+
identity: storedIdentity,
|
|
6516
|
+
sharedCoverageIdentity: sharedMobileControlCoverageIdentity(element, candidate, semanticNavigationFamilyIds.get(semanticGroupKey))
|
|
6517
|
+
});
|
|
6518
|
+
}
|
|
6519
|
+
if (controls.size > 0)
|
|
6520
|
+
this.safeControlsByScreen.set(screenId, controls);
|
|
6521
|
+
this.currentScreenId = screenId;
|
|
6522
|
+
this.updateStaleProgress();
|
|
6523
|
+
return screenId;
|
|
6524
|
+
}
|
|
6525
|
+
/**
|
|
6526
|
+
* Back-compatible acknowledgement from capture_screen. Observation already
|
|
6527
|
+
* marks structural evidence visited; this call only resolves a rare pending
|
|
6528
|
+
* transition frontier and preserves callers that annotate explicitly.
|
|
6529
|
+
*/
|
|
6530
|
+
markVisited(screenId) {
|
|
6531
|
+
const state2 = this.screens.get(screenId);
|
|
6532
|
+
if (state2 && !state2.visited) {
|
|
6533
|
+
this.screens.set(screenId, { ...state2, visited: true });
|
|
6534
|
+
}
|
|
6535
|
+
this.frontier.delete(screenId);
|
|
6536
|
+
}
|
|
6537
|
+
/**
|
|
6538
|
+
* Record a confirmed tap against a safe navigation candidate. Callers invoke
|
|
6539
|
+
* this only after the driver reports success; recovery-unknown, stale-ref, and
|
|
6540
|
+
* failed actions must remain on the breadth frontier.
|
|
6541
|
+
*/
|
|
6542
|
+
noteControlAttempt(screenId, element) {
|
|
6543
|
+
if (!screenId)
|
|
6544
|
+
return false;
|
|
6545
|
+
const candidate = classifySafeMobileNavigationControl(element);
|
|
6546
|
+
const controls = this.safeControlsByScreen.get(screenId) ?? /* @__PURE__ */ new Map();
|
|
6547
|
+
if (candidate) {
|
|
6548
|
+
const storedIdentity = storedMobileControlIdentity(candidate);
|
|
6549
|
+
const existing = controls.get(storedIdentity);
|
|
6550
|
+
controls.set(storedIdentity, {
|
|
6551
|
+
...candidate,
|
|
6552
|
+
identity: storedIdentity,
|
|
6553
|
+
sharedCoverageIdentity: existing?.sharedCoverageIdentity ?? sharedMobileControlCoverageIdentity(element, candidate)
|
|
6554
|
+
});
|
|
6555
|
+
this.safeControlsByScreen.set(screenId, controls);
|
|
6556
|
+
}
|
|
6557
|
+
const tappedLabel = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
|
|
6558
|
+
if (!tappedLabel)
|
|
6559
|
+
return false;
|
|
6560
|
+
const before = this.attemptedSafeControlCoverageIdentities.size;
|
|
6561
|
+
const alias = semanticControlAlias(tappedLabel);
|
|
6562
|
+
for (const control of controls.values()) {
|
|
6563
|
+
if (semanticControlAlias(control.label) === alias) {
|
|
6564
|
+
this.attemptedSafeControlCoverageIdentities.add(localControlCoverageIdentity(screenId, control.identity));
|
|
6565
|
+
if (control.sharedCoverageIdentity) {
|
|
6566
|
+
this.attemptedSafeControlCoverageIdentities.add(control.sharedCoverageIdentity);
|
|
6567
|
+
}
|
|
6568
|
+
}
|
|
6569
|
+
}
|
|
6570
|
+
if (this.attemptedSafeControlCoverageIdentities.size > before) {
|
|
6571
|
+
this.successfulSafeControlAttemptCount += 1;
|
|
6572
|
+
this.staleIterations = 0;
|
|
6573
|
+
return true;
|
|
6574
|
+
}
|
|
6575
|
+
return false;
|
|
6576
|
+
}
|
|
6577
|
+
/** Attach a human-friendly name / coarse type to a screen (capture_screen). */
|
|
6578
|
+
annotateScreen(screenId, annotation) {
|
|
6579
|
+
const state2 = this.screens.get(screenId);
|
|
6580
|
+
if (!state2)
|
|
6581
|
+
return;
|
|
6582
|
+
this.screens.set(screenId, {
|
|
6583
|
+
...state2,
|
|
6584
|
+
screenName: annotation.screenName ?? state2.screenName,
|
|
6585
|
+
screenType: annotation.screenType ?? state2.screenType
|
|
6586
|
+
});
|
|
6587
|
+
}
|
|
6588
|
+
/** Attach structurally observed authentication provenance to a screen. */
|
|
6589
|
+
setScreenAuthEvidence(screenId, evidence) {
|
|
6590
|
+
const state2 = this.screens.get(screenId);
|
|
6591
|
+
if (!state2)
|
|
6592
|
+
return;
|
|
6593
|
+
this.screens.set(screenId, {
|
|
6594
|
+
...state2,
|
|
6595
|
+
requiresAuth: evidence.requiresAuth,
|
|
6596
|
+
authProvenance: evidence.authProvenance,
|
|
6597
|
+
sessionState: { ...evidence.sessionState }
|
|
6598
|
+
});
|
|
6599
|
+
}
|
|
6600
|
+
/** Append a free-form observation note to a screen (observations tool). */
|
|
6601
|
+
addScreenObservation(screenId, note) {
|
|
6602
|
+
const state2 = this.screens.get(screenId);
|
|
6603
|
+
if (!state2)
|
|
6604
|
+
return;
|
|
6605
|
+
const normalized = note.trim();
|
|
6606
|
+
if (!normalized)
|
|
6607
|
+
return;
|
|
6608
|
+
const current = state2.observations ?? [];
|
|
6609
|
+
if (current.some((existing) => existing.trim().toLowerCase() === normalized.toLowerCase())) {
|
|
6610
|
+
return;
|
|
6611
|
+
}
|
|
6612
|
+
const observations = [...current, normalized].slice(-MAX_SCREEN_OBSERVATIONS);
|
|
6613
|
+
this.screens.set(screenId, { ...state2, observations });
|
|
6614
|
+
}
|
|
6615
|
+
/**
|
|
6616
|
+
* Record a directed transition between two screens. Both endpoints are
|
|
6617
|
+
* ensured to exist on the frontier if not yet observed (mirrors the web
|
|
6618
|
+
* reducer adding both ends of a TRANSITION_RECORDED to the frontier). The
|
|
6619
|
+
* `toScreenId` becomes the current screen when the transition starts from the
|
|
6620
|
+
* current screen.
|
|
6621
|
+
*/
|
|
6622
|
+
recordTransition(transition) {
|
|
6623
|
+
let recordedTransition = transition;
|
|
6624
|
+
if (!transition.viaControlIdentity && transition.viaRef) {
|
|
6625
|
+
const sourceElement = this.screens.get(transition.fromScreenId)?.elements.find((element) => element.ref === transition.viaRef);
|
|
6626
|
+
if (sourceElement) {
|
|
6627
|
+
recordedTransition = {
|
|
6628
|
+
...transition,
|
|
6629
|
+
viaControlIdentity: (0, mobile_observation_js_1.stableParsedMobileElementIdentity)(sourceElement)
|
|
6630
|
+
};
|
|
6631
|
+
}
|
|
6632
|
+
}
|
|
6633
|
+
const alreadyRecorded = this.transitions.some((existing) => existing.fromScreenId === recordedTransition.fromScreenId && existing.toScreenId === recordedTransition.toScreenId && existing.action === recordedTransition.action);
|
|
6634
|
+
if (!alreadyRecorded) {
|
|
6635
|
+
this.transitions.push(recordedTransition);
|
|
6636
|
+
}
|
|
6637
|
+
for (const endpointId of [transition.fromScreenId, transition.toScreenId]) {
|
|
6638
|
+
if (!this.screens.has(endpointId) && !this.frontier.has(endpointId)) {
|
|
6639
|
+
this.frontier.set(endpointId, {
|
|
6640
|
+
screenId: endpointId,
|
|
6641
|
+
source: "transition",
|
|
6642
|
+
discoveredAtIteration: this.iteration
|
|
6643
|
+
});
|
|
6644
|
+
}
|
|
6645
|
+
}
|
|
6646
|
+
if (transition.fromScreenId === this.currentScreenId) {
|
|
6647
|
+
this.currentScreenId = transition.toScreenId;
|
|
6648
|
+
}
|
|
6649
|
+
}
|
|
6650
|
+
// -------------------------------------------------------------------------
|
|
6651
|
+
// Selectors
|
|
6652
|
+
// -------------------------------------------------------------------------
|
|
6653
|
+
/** The current screen-id (most recently observed), or null before any observation. */
|
|
6654
|
+
getCurrentScreenId() {
|
|
6655
|
+
return this.currentScreenId;
|
|
6656
|
+
}
|
|
6657
|
+
/** ScreenState for `screenId`, if known. */
|
|
6658
|
+
getScreen(screenId) {
|
|
6659
|
+
return this.screens.get(screenId);
|
|
6660
|
+
}
|
|
6661
|
+
/** Number of distinct screens observed this run. */
|
|
6662
|
+
getScreenCount() {
|
|
6663
|
+
return this.screens.size;
|
|
6664
|
+
}
|
|
6665
|
+
/** Number of screens backed by a live structural driver observation. */
|
|
6666
|
+
getVisitedScreenCount() {
|
|
6667
|
+
let total = 0;
|
|
6668
|
+
for (const state2 of this.screens.values()) {
|
|
6669
|
+
if (state2.visited)
|
|
6670
|
+
total += 1;
|
|
6671
|
+
}
|
|
6672
|
+
return total;
|
|
6673
|
+
}
|
|
6674
|
+
/** Total elements observed across all known screens (latest snapshot each). */
|
|
6675
|
+
getTotalElementCount() {
|
|
6676
|
+
let total = 0;
|
|
6677
|
+
for (const state2 of this.screens.values()) {
|
|
6678
|
+
total += state2.elements.length;
|
|
6679
|
+
}
|
|
6680
|
+
return total;
|
|
6681
|
+
}
|
|
6682
|
+
/**
|
|
6683
|
+
* Frontier screen-ids ranked by recency (most recently discovered first),
|
|
6684
|
+
* excluding any screen already visited. Mirrors the web `getRankedFrontier`
|
|
6685
|
+
* intent (filter visited, rank by recency) — without URL prefix similarity,
|
|
6686
|
+
* which has no mobile analog.
|
|
6687
|
+
*/
|
|
6688
|
+
getFrontier() {
|
|
6689
|
+
return [...this.frontier.values()].filter((item) => {
|
|
6690
|
+
const state2 = this.screens.get(item.screenId);
|
|
6691
|
+
return !state2 || !state2.visited;
|
|
6692
|
+
}).sort((a, b) => b.discoveredAtIteration - a.discoveredAtIteration).map((item) => item.screenId);
|
|
6693
|
+
}
|
|
6694
|
+
/** Safe navigation candidates not yet attempted, deduped across shared chrome. */
|
|
6695
|
+
getUnexploredSafeNavigationCandidates() {
|
|
6696
|
+
const authenticatedProductObserved = [...this.screens.values()].some((screen) => screen.requiresAuth === true);
|
|
6697
|
+
const candidates = [];
|
|
6698
|
+
for (const [screenId, controls] of this.safeControlsByScreen) {
|
|
6699
|
+
const origin = this.screens.get(screenId);
|
|
6700
|
+
const originIsConfirmedPublicEntry = origin?.authProvenance === "public" || !origin?.authProvenance && origin?.requiresAuth !== true && /^(?:AUTH|LANDING)$/i.test(origin?.screenType ?? "");
|
|
6701
|
+
if (authenticatedProductObserved && originIsConfirmedPublicEntry)
|
|
6702
|
+
continue;
|
|
6703
|
+
for (const control of controls.values()) {
|
|
6704
|
+
const alias = semanticControlAlias(control.label);
|
|
6705
|
+
const coverageIdentity = control.sharedCoverageIdentity ?? localControlCoverageIdentity(screenId, control.identity);
|
|
6706
|
+
if (this.attemptedSafeControlCoverageIdentities.has(coverageIdentity) || this.attemptedSafeControlCoverageIdentities.has(localControlCoverageIdentity(screenId, control.identity)))
|
|
6707
|
+
continue;
|
|
6708
|
+
const existing = candidates.find((candidate) => candidate.identity === coverageIdentity || candidate.alias === alias && candidate.screenIds.has(screenId));
|
|
6709
|
+
if (existing) {
|
|
6710
|
+
existing.screenIds.add(screenId);
|
|
6711
|
+
} else {
|
|
6712
|
+
candidates.push({
|
|
6713
|
+
identity: coverageIdentity,
|
|
6714
|
+
label: control.label,
|
|
6715
|
+
kind: control.kind,
|
|
6716
|
+
alias,
|
|
6717
|
+
screenIds: /* @__PURE__ */ new Set([screenId])
|
|
6718
|
+
});
|
|
6719
|
+
}
|
|
6720
|
+
}
|
|
6721
|
+
}
|
|
6722
|
+
return candidates.map((candidate) => ({
|
|
6723
|
+
identity: candidate.identity,
|
|
6724
|
+
label: candidate.label,
|
|
6725
|
+
kind: candidate.kind,
|
|
6726
|
+
screenIds: [...candidate.screenIds]
|
|
6727
|
+
})).sort((a, b) => {
|
|
6728
|
+
const aCurrent = this.currentScreenId !== null && a.screenIds.includes(this.currentScreenId);
|
|
6729
|
+
const bCurrent = this.currentScreenId !== null && b.screenIds.includes(this.currentScreenId);
|
|
6730
|
+
if (aCurrent !== bCurrent)
|
|
6731
|
+
return aCurrent ? -1 : 1;
|
|
6732
|
+
return b.screenIds.length - a.screenIds.length;
|
|
6733
|
+
});
|
|
6734
|
+
}
|
|
6735
|
+
/** Harness-owned survey completion floor, independent of model assertions. */
|
|
6736
|
+
getSurveyBreadthCoverage() {
|
|
6737
|
+
const unvisitedScreenIds = this.getFrontier();
|
|
6738
|
+
const unexploredControls = this.getUnexploredSafeNavigationCandidates();
|
|
6739
|
+
const complete = unvisitedScreenIds.length === 0 && unexploredControls.length === 0;
|
|
6740
|
+
const missing = [];
|
|
6741
|
+
if (unvisitedScreenIds.length > 0) {
|
|
6742
|
+
missing.push(`${unvisitedScreenIds.length} uncaptured screen(s)`);
|
|
6743
|
+
}
|
|
6744
|
+
if (unexploredControls.length > 0) {
|
|
6745
|
+
missing.push(`${unexploredControls.length} safe navigation control(s)`);
|
|
6746
|
+
}
|
|
6747
|
+
return {
|
|
6748
|
+
required: true,
|
|
6749
|
+
complete,
|
|
6750
|
+
unvisitedScreenIds,
|
|
6751
|
+
unexploredControls,
|
|
6752
|
+
attemptedControlCount: this.successfulSafeControlAttemptCount,
|
|
6753
|
+
...missing.length > 0 ? { reason: `Missing ${missing.join(" and ")}.` } : {}
|
|
6754
|
+
};
|
|
6755
|
+
}
|
|
6756
|
+
/**
|
|
6757
|
+
* Monotonic structural progress token for adaptive survey leases. Frontier
|
|
6758
|
+
* cardinality alone is insufficient: attempting control A can reveal control
|
|
6759
|
+
* B and leave the remaining count unchanged even though real work happened.
|
|
6760
|
+
*/
|
|
6761
|
+
getSurveyBreadthProgressMark() {
|
|
6762
|
+
const discoveredControlIdentities = /* @__PURE__ */ new Set();
|
|
6763
|
+
for (const [screenId, controls] of this.safeControlsByScreen) {
|
|
6764
|
+
for (const control of controls.values()) {
|
|
6765
|
+
discoveredControlIdentities.add(control.sharedCoverageIdentity ?? localControlCoverageIdentity(screenId, control.identity));
|
|
6766
|
+
}
|
|
6767
|
+
}
|
|
6768
|
+
return [
|
|
6769
|
+
`screens:${this.screens.size}`,
|
|
6770
|
+
`attempts:${this.successfulSafeControlAttemptCount}`,
|
|
6771
|
+
`controls:${discoveredControlIdentities.size}`,
|
|
6772
|
+
`transitions:${this.transitions.length}`
|
|
6773
|
+
].join("|");
|
|
6774
|
+
}
|
|
6775
|
+
resolveSemanticNavigationFamilyId(groupKey, labels) {
|
|
6776
|
+
const families = this.semanticNavigationFamilies.get(groupKey) ?? [];
|
|
6777
|
+
let best;
|
|
6778
|
+
let bestIntersection = 0;
|
|
6779
|
+
for (const family of families) {
|
|
6780
|
+
let intersection = 0;
|
|
6781
|
+
for (const label of labels) {
|
|
6782
|
+
if (family.labels.has(label))
|
|
6783
|
+
intersection += 1;
|
|
6784
|
+
}
|
|
6785
|
+
const overlap = intersection / Math.min(labels.size, family.labels.size);
|
|
6786
|
+
if (intersection >= 2 && overlap >= 0.6 && intersection > bestIntersection) {
|
|
6787
|
+
best = family;
|
|
6788
|
+
bestIntersection = intersection;
|
|
6789
|
+
}
|
|
6790
|
+
}
|
|
6791
|
+
if (best) {
|
|
6792
|
+
for (const label of labels)
|
|
6793
|
+
best.labels.add(label);
|
|
6794
|
+
return best.id;
|
|
6795
|
+
}
|
|
6796
|
+
const created = {
|
|
6797
|
+
id: `semantic-nav-family-${this.nextSemanticNavigationFamilyId++}`,
|
|
6798
|
+
labels: new Set(labels)
|
|
6799
|
+
};
|
|
6800
|
+
families.push(created);
|
|
6801
|
+
this.semanticNavigationFamilies.set(groupKey, families);
|
|
6802
|
+
return created.id;
|
|
6803
|
+
}
|
|
6804
|
+
/** Observed transitions (defensive copy). */
|
|
6805
|
+
getTransitions() {
|
|
6806
|
+
return [...this.transitions];
|
|
6807
|
+
}
|
|
6808
|
+
// -------------------------------------------------------------------------
|
|
6809
|
+
// Stale-progress detection (ported semantics from the web stale guard)
|
|
6810
|
+
// -------------------------------------------------------------------------
|
|
6811
|
+
/**
|
|
6812
|
+
* Progress mark: distinct screens weighted heavily, plus total elements seen.
|
|
6813
|
+
* A run advances this mark whenever it finds a new screen OR new elements;
|
|
6814
|
+
* the stale detector triggers when the mark stops advancing for
|
|
6815
|
+
* `staleThreshold` consecutive iterations.
|
|
6816
|
+
*/
|
|
6817
|
+
getProgressMark() {
|
|
6818
|
+
return this.screens.size * 1e3 + this.transitions.length * 100 + this.successfulSafeControlAttemptCount * 10 + this.getTotalElementCount();
|
|
6819
|
+
}
|
|
6820
|
+
updateStaleProgress() {
|
|
6821
|
+
const mark = this.getProgressMark();
|
|
6822
|
+
if (mark > this.lastProgressMark) {
|
|
6823
|
+
this.lastProgressMark = mark;
|
|
6824
|
+
this.staleIterations = 0;
|
|
6825
|
+
} else {
|
|
6826
|
+
this.staleIterations += 1;
|
|
6827
|
+
}
|
|
6828
|
+
}
|
|
6829
|
+
/** Consecutive no-progress iterations observed so far. */
|
|
6830
|
+
getStaleIterations() {
|
|
6831
|
+
return this.staleIterations;
|
|
6832
|
+
}
|
|
6833
|
+
/**
|
|
6834
|
+
* True when the run has gone `staleThreshold` consecutive iterations with no
|
|
6835
|
+
* new screen and no new element — the signal the explorer uses to nudge or
|
|
6836
|
+
* finish (mirrors the web explorer's stale guard).
|
|
6837
|
+
*/
|
|
6838
|
+
isStaleProgress() {
|
|
6839
|
+
return this.staleIterations >= this.staleThreshold;
|
|
6840
|
+
}
|
|
6841
|
+
// -------------------------------------------------------------------------
|
|
6842
|
+
// Planner evidence export
|
|
6843
|
+
// -------------------------------------------------------------------------
|
|
6844
|
+
/**
|
|
6845
|
+
* Snapshot the screen-graph as a plain object (frozen mobile-types
|
|
6846
|
+
* `MobileScreenGraph`). Defensive copies so callers cannot mutate internal
|
|
6847
|
+
* state.
|
|
6848
|
+
*/
|
|
6849
|
+
toScreenGraph() {
|
|
6850
|
+
const screens = /* @__PURE__ */ new Map();
|
|
6851
|
+
for (const [id, state2] of this.screens) {
|
|
6852
|
+
screens.set(id, {
|
|
6853
|
+
...state2,
|
|
6854
|
+
elements: [...state2.elements],
|
|
6855
|
+
observations: state2.observations ? [...state2.observations] : void 0,
|
|
6856
|
+
sessionState: state2.sessionState ? { ...state2.sessionState } : void 0
|
|
6857
|
+
});
|
|
6858
|
+
}
|
|
6859
|
+
return {
|
|
6860
|
+
screens,
|
|
6861
|
+
transitions: this.transitions.map((t) => ({
|
|
6862
|
+
fromScreenId: t.fromScreenId,
|
|
6863
|
+
toScreenId: t.toScreenId,
|
|
6864
|
+
viaRef: t.viaRef,
|
|
6865
|
+
viaControlIdentity: t.viaControlIdentity,
|
|
6866
|
+
action: t.action
|
|
6867
|
+
}))
|
|
6868
|
+
};
|
|
6869
|
+
}
|
|
6870
|
+
/**
|
|
6871
|
+
* Build the planner evidence shape: one `CollectedPageData` per screen, with
|
|
6872
|
+
* the screen-id carried in BOTH `route` (the on-the-wire positional field the
|
|
6873
|
+
* server keys screenshots by) AND the explicit `screenId` field (per the
|
|
6874
|
+
* Phase-0 `CollectedPageData.screenId` contract). `elements` are mapped from
|
|
6875
|
+
* the screen's parsed actionable elements into the platform-neutral
|
|
6876
|
+
* `CollectedPageElement[]` shape the server persists as SiteElements — without
|
|
6877
|
+
* this, mobile screens land with zero elements (coverage always 0%, no element
|
|
6878
|
+
* intelligence). `triggersNavigation` is derived from the durable source
|
|
6879
|
+
* element identity captured when the transition happened (with ref-only
|
|
6880
|
+
* fallback for legacy transitions). Insertion order matches first-seen order.
|
|
6881
|
+
*/
|
|
6882
|
+
buildInMemoryScreenMap() {
|
|
6883
|
+
const pages = [];
|
|
6884
|
+
const ordered = [...this.screens.values()].sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
6885
|
+
const navRefsByScreen = /* @__PURE__ */ new Map();
|
|
6886
|
+
const navIdentitiesByScreen = /* @__PURE__ */ new Map();
|
|
6887
|
+
for (const transition of this.transitions) {
|
|
6888
|
+
if (transition.viaControlIdentity) {
|
|
6889
|
+
const set = navIdentitiesByScreen.get(transition.fromScreenId) ?? /* @__PURE__ */ new Set();
|
|
6890
|
+
set.add(transition.viaControlIdentity);
|
|
6891
|
+
navIdentitiesByScreen.set(transition.fromScreenId, set);
|
|
6892
|
+
} else if (transition.viaRef) {
|
|
6893
|
+
const set = navRefsByScreen.get(transition.fromScreenId) ?? /* @__PURE__ */ new Set();
|
|
6894
|
+
set.add(transition.viaRef);
|
|
6895
|
+
navRefsByScreen.set(transition.fromScreenId, set);
|
|
6896
|
+
}
|
|
6897
|
+
}
|
|
6898
|
+
for (const state2 of ordered) {
|
|
6899
|
+
const page = {
|
|
6900
|
+
route: state2.screenId,
|
|
6901
|
+
screenId: state2.screenId,
|
|
6902
|
+
pageType: state2.screenType ?? "UNKNOWN",
|
|
6903
|
+
elements: (0, mobile_observation_js_1.buildCollectedElementsForScreen)(state2.elements, {
|
|
6904
|
+
navigationRefs: navRefsByScreen.get(state2.screenId),
|
|
6905
|
+
navigationIdentities: navIdentitiesByScreen.get(state2.screenId)
|
|
6906
|
+
}),
|
|
6907
|
+
requiresAuth: state2.requiresAuth ?? false
|
|
6908
|
+
};
|
|
6909
|
+
if (state2.sessionState) {
|
|
6910
|
+
page.sessionState = { ...state2.sessionState };
|
|
6911
|
+
}
|
|
6912
|
+
if (state2.screenName) {
|
|
6913
|
+
page.title = state2.screenName;
|
|
6914
|
+
}
|
|
6915
|
+
if (state2.observations && state2.observations.length > 0) {
|
|
6916
|
+
page.observations = state2.observations.map((note) => ({
|
|
6917
|
+
action: "observe",
|
|
6918
|
+
outcome: note,
|
|
6919
|
+
outcomeType: "SUCCESS"
|
|
6920
|
+
}));
|
|
6921
|
+
}
|
|
6922
|
+
pages.push(page);
|
|
6923
|
+
}
|
|
6924
|
+
return pages;
|
|
6925
|
+
}
|
|
6926
|
+
/** Alias for `buildInMemoryScreenMap()` — name parity with the planner-evidence call site. */
|
|
6927
|
+
toScreenMap() {
|
|
6928
|
+
return this.buildInMemoryScreenMap();
|
|
6929
|
+
}
|
|
6930
|
+
};
|
|
6931
|
+
exports.MobileExplorationState = MobileExplorationState;
|
|
6932
|
+
}
|
|
6933
|
+
});
|
|
6934
|
+
|
|
6221
6935
|
// ../runner-core/dist/services/mobile/mobile-keyboard.js
|
|
6222
6936
|
var require_mobile_keyboard = __commonJS({
|
|
6223
6937
|
"../runner-core/dist/services/mobile/mobile-keyboard.js"(exports) {
|
|
@@ -63934,8 +64648,10 @@ var require_mobile_explorer = __commonJS({
|
|
|
63934
64648
|
var ai_provider_js_1 = require_ai_provider();
|
|
63935
64649
|
var ai_queue_js_1 = require_ai_queue();
|
|
63936
64650
|
var ai_retry_js_1 = require_ai_retry();
|
|
64651
|
+
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
63937
64652
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
63938
64653
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
64654
|
+
var mobile_exploration_state_js_1 = require_mobile_exploration_state();
|
|
63939
64655
|
var mobile_boundary_js_1 = require_mobile_boundary();
|
|
63940
64656
|
var mobile_keyboard_js_1 = require_mobile_keyboard();
|
|
63941
64657
|
var mobile_auth_coverage_js_1 = require_mobile_auth_coverage();
|
|
@@ -64293,6 +65009,10 @@ var require_mobile_explorer = __commonJS({
|
|
|
64293
65009
|
return void 0;
|
|
64294
65010
|
return normalized;
|
|
64295
65011
|
}
|
|
65012
|
+
function destructiveActionRefusal(label, kind = "tap") {
|
|
65013
|
+
const what = kind === "confirmation" ? `Typing "${label}" is a destructive confirmation` : `Tapping "${label}" is an irreversible, high-stakes destructive action`;
|
|
65014
|
+
return `Refused: ${what} (account/data deletion, deactivation, wipe, or similar). Autonomous testing must never execute irreversible destruction on the account under test. Record it as an untested destructive flow in your summary and choose a non-destructive control instead.`;
|
|
65015
|
+
}
|
|
64296
65016
|
function hybridElementForEvidence(element, credentials) {
|
|
64297
65017
|
const id = safeHybridIdentity(element.id, credentials);
|
|
64298
65018
|
const name = safeHybridIdentity(element.name, credentials);
|
|
@@ -64915,7 +65635,7 @@ Max iterations: ${maxIterations}. You will be warned near the end.`;
|
|
|
64915
65635
|
3. After each navigation, capture_screen the destination and record_transition. After completing a meaningful flow (login, search, create), ${enableRecordJourney ? "call record_journey with the steps and expected outcome." : "continue exploring."}
|
|
64916
65636
|
4. Use mobile_relaunch for a clean, hermetic restart before testing a distinct flow that needs fresh state.
|
|
64917
65637
|
5. mobile_swipe to scroll; mobile_back to return.
|
|
64918
|
-
6. Do NOT trigger irreversible side effects
|
|
65638
|
+
6. Do NOT trigger irreversible or destructive side effects \u2014 real payments; sending messages/invites to others; and ESPECIALLY account/data deletion, deactivation, "delete account", factory reset, or any "type DELETE to confirm" gate. The runner hard-blocks destructive taps and confirmation keystrokes, so attempting them only wastes an iteration. Note such flows in your summary and explore elsewhere.
|
|
64919
65639
|
7. Stay inside this app.${targetLine} If a tap opens an app store, settings, browser, or another app, do not explore it; return with mobile_back or mobile_relaunch.
|
|
64920
65640
|
|
|
64921
65641
|
Max iterations: ${maxIterations}. You will be warned near the end.`;
|
|
@@ -65438,7 +66158,7 @@ ${statePacket}` }
|
|
|
65438
66158
|
const requestMessageCount = messages.length;
|
|
65439
66159
|
const requestMessages = messages.slice();
|
|
65440
66160
|
const callStart = Date.now();
|
|
65441
|
-
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)(() => {
|
|
65442
66162
|
arm();
|
|
65443
66163
|
return aiClient.chat.completions.create({
|
|
65444
66164
|
model: agentModel,
|
|
@@ -65451,7 +66171,7 @@ ${statePacket}` }
|
|
|
65451
66171
|
label: `mobile-explore-iter-${iteration}`,
|
|
65452
66172
|
deferTimeoutUntilArmed: true,
|
|
65453
66173
|
onRetry: (attempt, reason, delayMs) => log2(` \u26A0 mobile explore AI retry ${attempt} (${reason}) \u2014 waiting ${delayMs}ms`)
|
|
65454
|
-
});
|
|
66174
|
+
}));
|
|
65455
66175
|
const iterDuration = Date.now() - callStart;
|
|
65456
66176
|
if (!response?.choices?.[0]?.message) {
|
|
65457
66177
|
throw new Error(`AI explore call returned no response (queue timeout or provider overload) at iteration ${iteration}`);
|
|
@@ -65780,6 +66500,11 @@ Exploration complete: ${summary}`);
|
|
|
65780
66500
|
if (!driver.webTap) {
|
|
65781
66501
|
return { ok: false, error: "This driver does not support hybrid WEBVIEW taps." };
|
|
65782
66502
|
}
|
|
66503
|
+
const hybridTapLabel = hybridElementForEvidence(hybrid, redactionCredentials).label ?? "";
|
|
66504
|
+
if ((0, mobile_exploration_state_js_1.isDestructiveActionLabel)(hybridTapLabel)) {
|
|
66505
|
+
log2(` \u26A0 Refused destructive tap on "${hybridTapLabel}" (account/data-deletion class); noting instead.`);
|
|
66506
|
+
return { ok: false, error: destructiveActionRefusal(hybridTapLabel) };
|
|
66507
|
+
}
|
|
65783
66508
|
const authSurfaceBeforeTap2 = currentAuthSurface();
|
|
65784
66509
|
const fromScreenId2 = state2.getCurrentScreenId();
|
|
65785
66510
|
const actionResult2 = await driver.webTap(hybrid.webviewContext, hybrid.css);
|
|
@@ -65810,6 +66535,11 @@ Exploration complete: ${summary}`);
|
|
|
65810
66535
|
if (!resolved) {
|
|
65811
66536
|
return { ok: false, error: `Unknown element ref "${String(toolArgs.ref)}". Call mobile_snapshot first; refs are only valid for the latest snapshot.` };
|
|
65812
66537
|
}
|
|
66538
|
+
const nativeTapLabel = resolved.element.label ?? resolved.element.text ?? resolved.element.accessibilityId ?? "";
|
|
66539
|
+
if ((0, mobile_exploration_state_js_1.isDestructiveActionLabel)(nativeTapLabel)) {
|
|
66540
|
+
log2(` \u26A0 Refused destructive tap on "${nativeTapLabel}" (account/data-deletion class); noting instead.`);
|
|
66541
|
+
return { ok: false, error: destructiveActionRefusal(nativeTapLabel) };
|
|
66542
|
+
}
|
|
65813
66543
|
const fromScreenId = state2.getCurrentScreenId();
|
|
65814
66544
|
const boundaryBeforeAction = currentBoundary();
|
|
65815
66545
|
const startedOutsideTarget = boundaryBeforeAction.kind !== "target" && boundaryBeforeAction.kind !== "unknown";
|
|
@@ -65850,6 +66580,10 @@ Exploration complete: ${summary}`);
|
|
|
65850
66580
|
// ----- Type -----
|
|
65851
66581
|
case "mobile_type": {
|
|
65852
66582
|
const value = typeof toolArgs.value === "string" ? toolArgs.value : String(toolArgs.value ?? "");
|
|
66583
|
+
if ((0, mobile_exploration_state_js_1.isDestructiveConfirmationInput)(value)) {
|
|
66584
|
+
log2(` \u26A0 Refused destructive confirmation input "${value}"; noting instead.`);
|
|
66585
|
+
return { ok: false, error: destructiveActionRefusal(value, "confirmation") };
|
|
66586
|
+
}
|
|
65853
66587
|
const allowedLiteral = inbox ? inbox.state.email : "";
|
|
65854
66588
|
for (const [key, secret] of Object.entries(credentials)) {
|
|
65855
66589
|
if (!secret || secret.length < 4)
|
|
@@ -67516,6 +68250,7 @@ var require_discover_ai_planner = __commonJS({
|
|
|
67516
68250
|
"use strict";
|
|
67517
68251
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
67518
68252
|
exports.SCENARIO_SYSTEM_PROMPT = void 0;
|
|
68253
|
+
exports.getAIPlannerMaxTokens = getAIPlannerMaxTokens;
|
|
67519
68254
|
exports.getSnapshotConfidence = getSnapshotConfidence;
|
|
67520
68255
|
exports.compressSiteMap = compressSiteMap;
|
|
67521
68256
|
exports.formatEvidenceForPlanner = formatEvidenceForPlanner;
|
|
@@ -67527,10 +68262,25 @@ var require_discover_ai_planner = __commonJS({
|
|
|
67527
68262
|
var ai_retry_js_1 = require_ai_retry();
|
|
67528
68263
|
var strip_json_fences_js_1 = require_strip_json_fences();
|
|
67529
68264
|
var scenario_plan_validator_js_1 = require_scenario_plan_validator();
|
|
67530
|
-
var
|
|
67531
|
-
var
|
|
68265
|
+
var GENEROUS_AI_PLANNER_MAX_TOKENS = 65536;
|
|
68266
|
+
var CONSERVATIVE_AI_PLANNER_MAX_TOKENS = 16384;
|
|
67532
68267
|
function getAIPlannerMaxTokens(modelId) {
|
|
67533
|
-
|
|
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
|
+
}
|
|
67534
68284
|
}
|
|
67535
68285
|
var discover_planner_js_1 = require_discover_planner();
|
|
67536
68286
|
function getSnapshotConfidence(sourceKind) {
|
|
@@ -68082,7 +68832,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
68082
68832
|
4. **Multi-page journeys** - only when transitions were actually observed
|
|
68083
68833
|
5. **Bug reports** - for observed 500 errors, crashes, or broken features
|
|
68084
68834
|
6. **Steps are human-readable** - "Fill the email field", not "page.getByLabel('Email').fill()"
|
|
68085
|
-
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.
|
|
68086
68836
|
8. **Auth-aware** - mark scenarios that need login. Use process.env.TEST_USER_EMAIL / PASSWORD
|
|
68087
68837
|
9. **No duplicates** - check existing test names before adding
|
|
68088
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.
|
|
@@ -68134,7 +68884,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
68134
68884
|
**YOUR responsibility:**
|
|
68135
68885
|
- Scenario \`name\`, \`type\`, \`goal\`, \`prerequisites\`
|
|
68136
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
|
|
68137
|
-
- \`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
|
|
68138
68888
|
|
|
68139
68889
|
**NOT your responsibility:**
|
|
68140
68890
|
- Exact Playwright selectors (\`getByRole(...)\`, \`getByText(...)\`, \`page.locator(...)\`, etc.) \u2014 live-generate picks locators against the live snapshot
|
|
@@ -68155,7 +68905,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
68155
68905
|
- GOOD specificity: "create a new conversation by sending a message to a connection established earlier in this scenario"
|
|
68156
68906
|
- BAD specificity: "click the button labeled exactly 'Send' in the bottom-right of the chat"
|
|
68157
68907
|
|
|
68158
|
-
Citing observed labels in \`expectedOutcomes\`
|
|
68908
|
+
Citing observed labels in \`expectedOutcomes\` is fine \u2014 that's asserting on stable product signals, not implementation specifics.
|
|
68159
68909
|
|
|
68160
68910
|
## Coverage Strategy
|
|
68161
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)
|
|
@@ -68741,6 +69491,24 @@ Each entry is exactly one of:
|
|
|
68741
69491
|
}
|
|
68742
69492
|
async function runAIPlanner(siteMap, evidence, context, options) {
|
|
68743
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;
|
|
68744
69512
|
const compressed = compressSiteMap(siteMap);
|
|
68745
69513
|
const agentModel = options.modelOverride ? (0, ai_provider_js_1.resolveModelForTask)(options.modelOverride, "agent") : (0, ai_provider_js_1.getModel)("agent");
|
|
68746
69514
|
const plannerMaxTokens = getAIPlannerMaxTokens(agentModel);
|
|
@@ -68776,7 +69544,7 @@ Each entry is exactly one of:
|
|
|
68776
69544
|
} catch (err) {
|
|
68777
69545
|
const message = err instanceof Error ? err.message : String(err);
|
|
68778
69546
|
logs2.push(`AI planner failed after retries: ${message}`);
|
|
68779
|
-
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));
|
|
68780
69548
|
}
|
|
68781
69549
|
try {
|
|
68782
69550
|
options.onAICall?.({
|
|
@@ -68795,20 +69563,104 @@ Each entry is exactly one of:
|
|
|
68795
69563
|
});
|
|
68796
69564
|
} catch {
|
|
68797
69565
|
}
|
|
68798
|
-
const
|
|
68799
|
-
|
|
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) {
|
|
68800
69585
|
logs2.push("AI planner returned empty response");
|
|
68801
|
-
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);
|
|
68802
69587
|
}
|
|
68803
|
-
const usage = response.usage;
|
|
68804
|
-
logs2.push(`AI planner: ${usage?.prompt_tokens ?? "?"} input, ${usage?.completion_tokens ?? "?"} output tokens`);
|
|
68805
69588
|
let aiOutput;
|
|
68806
69589
|
try {
|
|
68807
|
-
aiOutput = JSON.parse((0, strip_json_fences_js_1.stripJsonFences)(
|
|
69590
|
+
aiOutput = JSON.parse((0, strip_json_fences_js_1.stripJsonFences)(trimmed));
|
|
68808
69591
|
} catch (err) {
|
|
68809
69592
|
const message = err instanceof Error ? err.message : String(err);
|
|
68810
69593
|
logs2.push(`AI planner returned invalid JSON: ${message}`);
|
|
68811
|
-
|
|
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
|
+
}
|
|
68812
69664
|
}
|
|
68813
69665
|
let { scenarios, featureGroups, warnings, coverageGaps } = validateAIPlannerOutput(aiOutput, siteMap, context.existingTestNames, context.testBudget, evidence.recordedJourneys, context.mode, context.focusAreas);
|
|
68814
69666
|
logs2.push(`AI planner: ${scenarios.length} scenarios, ${featureGroups.length} feature groups (${warnings.length} warnings, ${coverageGaps.length} coverage gaps)`);
|
|
@@ -68818,7 +69670,7 @@ Each entry is exactly one of:
|
|
|
68818
69670
|
let topLevelWarning;
|
|
68819
69671
|
const totalV1 = scenarios.length + coverageGaps.length;
|
|
68820
69672
|
const rejectionRateV1 = totalV1 > 0 ? coverageGaps.length / totalV1 : 0;
|
|
68821
|
-
if (rejectionRateV1 > 0.5 && totalV1 >= 3) {
|
|
69673
|
+
if (rejectionRateV1 > 0.5 && totalV1 >= 3 && !extraPlannerCallUsed) {
|
|
68822
69674
|
logs2.push(`AI planner: ${Math.round(rejectionRateV1 * 100)}% rejection rate (${coverageGaps.length}/${totalV1}) \u2014 running self-correction retry`);
|
|
68823
69675
|
const retryContext = [
|
|
68824
69676
|
`Your previous emission produced ${coverageGaps.length} coverage gaps out of ${totalV1} scenarios (${Math.round(rejectionRateV1 * 100)}% rejection rate).`,
|
|
@@ -68837,6 +69689,7 @@ Each entry is exactly one of:
|
|
|
68837
69689
|
const retryStart = Date.now();
|
|
68838
69690
|
let retryResponse = null;
|
|
68839
69691
|
try {
|
|
69692
|
+
extraPlannerCallUsed = true;
|
|
68840
69693
|
retryResponse = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
68841
69694
|
arm();
|
|
68842
69695
|
return aiClient.chat.completions.create({
|
|
@@ -68936,6 +69789,22 @@ Each entry is exactly one of:
|
|
|
68936
69789
|
};
|
|
68937
69790
|
}
|
|
68938
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
|
+
}
|
|
68939
69808
|
logs2.push("Survey AI returned 0 feature groups - falling back to deterministic grouping");
|
|
68940
69809
|
const fallback = buildFallbackPlan(siteMap, evidence.recordedJourneys, context.mode, logs2, context.existingTestNames ?? [], context.focusAreas, context.testBudget);
|
|
68941
69810
|
return {
|
|
@@ -70146,6 +71015,7 @@ var require_mobile_generator = __commonJS({
|
|
|
70146
71015
|
var ai_provider_js_1 = require_ai_provider();
|
|
70147
71016
|
var ai_queue_js_1 = require_ai_queue();
|
|
70148
71017
|
var ai_retry_js_1 = require_ai_retry();
|
|
71018
|
+
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
70149
71019
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
70150
71020
|
var mobile_source_parser_js_2 = require_mobile_source_parser();
|
|
70151
71021
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
@@ -70559,7 +71429,7 @@ var require_mobile_generator = __commonJS({
|
|
|
70559
71429
|
- Re-snapshot before every assertion so the ref is fresh.
|
|
70560
71430
|
- Assert the OUTCOME of the flow (the new screen / new content), not the control you just tapped.
|
|
70561
71431
|
- Assert only DURABLE elements \u2014 screen chrome that exists on every visit (tab labels, headers, nav bars, the entity you just created). NEVER assert one-time / data-dependent UI: empty-state CTAs, first-run or onboarding prompts, or counters \u2014 they vanish as soon as the account has data, and the test then fails forever on a healthy app.
|
|
70562
|
-
- Do NOT trigger irreversible side effects
|
|
71432
|
+
- Do NOT trigger irreversible or destructive side effects \u2014 real payments; sending messages to others; and ESPECIALLY account/data deletion, deactivation, or any "type DELETE to confirm" gate (the runner hard-blocks destructive taps and confirmation keystrokes). Note them and assert what you safely can.
|
|
70563
71433
|
- Stay inside this app.${targetLine} Do not open or use app stores, browsers, email apps, settings, or any other app. If an action leaves the target app, the runner will relaunch the target app and reject that action.
|
|
70564
71434
|
- NEVER tap the phone's home screen / launcher (app icons, "Predicted app" chips, search bars, widgets). Those are NOT part of the app under test \u2014 tapping them is junk that poisons the test. If the app ever backgrounds or you find yourself on the launcher, use mobile_relaunch to cold-start the app; do NOT tap a launcher icon or app-prediction to get back in.
|
|
70565
71435
|
- Do NOT re-do a sequence you already completed. If you filled a form then navigated away, do NOT come back and fill it again \u2014 the scenario needs ONE clean pass. Filling the login form, going back, and re-filling it is exactly the kind of wandering that produces a bloated, broken test.
|
|
@@ -71053,14 +71923,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
71053
71923
|
const requestMessageCount = messages.length;
|
|
71054
71924
|
const requestMessages = messages.slice();
|
|
71055
71925
|
const callStart = Date.now();
|
|
71056
|
-
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)(() => {
|
|
71057
71927
|
arm();
|
|
71058
71928
|
return aiClient.chat.completions.create({ model: agentModel, messages, tools: GENERATE_TOOLS, tool_choice: "auto", max_tokens: 8e3 }, { signal });
|
|
71059
71929
|
}), {
|
|
71060
71930
|
label: `mobile-generate-${scenario.name.slice(0, 24)}-iter-${iteration}`,
|
|
71061
71931
|
deferTimeoutUntilArmed: true,
|
|
71062
71932
|
onRetry: (attempt, reason, delayMs) => log2(` \u26A0 mobile generate AI retry ${attempt} (${reason}) \u2014 waiting ${delayMs}ms`)
|
|
71063
|
-
});
|
|
71933
|
+
}));
|
|
71064
71934
|
const iterDuration = Date.now() - callStart;
|
|
71065
71935
|
if (!response?.choices?.[0]?.message) {
|
|
71066
71936
|
throw new Error(`AI generate call returned no response at iteration ${iteration}`);
|
|
@@ -71672,6 +72542,21 @@ ${statePacket}`, redactionCredentials) },
|
|
|
71672
72542
|
};
|
|
71673
72543
|
}
|
|
71674
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
|
+
}
|
|
71675
72560
|
if (hasStableId(element)) {
|
|
71676
72561
|
recordAction({ type: "ASSERT_TEXT", value: expected, selector: (0, mobile_source_parser_js_2.preferredLocator)(element), metadata });
|
|
71677
72562
|
log2(` assert_text: ${metadata.targetLabel} ~ "${expected}"`);
|
|
@@ -72365,7 +73250,13 @@ ${statePacket}`, redactionCredentials) },
|
|
|
72365
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;
|
|
72366
73251
|
const test = {
|
|
72367
73252
|
name: compiled.name,
|
|
72368
|
-
|
|
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,
|
|
72369
73260
|
framework: "APPIUM",
|
|
72370
73261
|
appiumCode: compiled.appiumCode,
|
|
72371
73262
|
steps: compiled.steps,
|
|
@@ -75042,6 +75933,118 @@ var require_har_stream = __commonJS({
|
|
|
75042
75933
|
}
|
|
75043
75934
|
});
|
|
75044
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
|
+
|
|
75045
76048
|
// ../runner-core/dist/services/playwright-native.js
|
|
75046
76049
|
var require_playwright_native = __commonJS({
|
|
75047
76050
|
"../runner-core/dist/services/playwright-native.js"(exports) {
|
|
@@ -75084,7 +76087,7 @@ var require_playwright_native = __commonJS({
|
|
|
75084
76087
|
};
|
|
75085
76088
|
})();
|
|
75086
76089
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
75087
|
-
exports.scrubHarRequestBody = void 0;
|
|
76090
|
+
exports.scrubHarRequestBody = exports.buildOriginScopedApiFixtures = exports.isAppOriginRequest = exports.resolveApiAuthHeaders = exports.parseApiAuthHeaders = void 0;
|
|
75088
76091
|
exports.executeTestViaNativePlaywright = executeTestViaNativePlaywright2;
|
|
75089
76092
|
exports.parseJsonResults = parseJsonResults2;
|
|
75090
76093
|
exports.parseHarToApiSummary = parseHarToApiSummary;
|
|
@@ -75096,6 +76099,20 @@ var require_playwright_native = __commonJS({
|
|
|
75096
76099
|
var spawn_with_tree_kill_js_1 = require_spawn_with_tree_kill();
|
|
75097
76100
|
var api_call_redaction_js_1 = require_api_call_redaction();
|
|
75098
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
|
+
} });
|
|
75099
76116
|
var api_call_redaction_js_2 = require_api_call_redaction();
|
|
75100
76117
|
Object.defineProperty(exports, "scrubHarRequestBody", { enumerable: true, get: function() {
|
|
75101
76118
|
return api_call_redaction_js_2.scrubHarRequestBody;
|
|
@@ -75105,7 +76122,7 @@ var require_playwright_native = __commonJS({
|
|
|
75105
76122
|
var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
|
|
75106
76123
|
var PW_CLI = __require.resolve("@playwright/test/cli");
|
|
75107
76124
|
async function executeTestViaNativePlaywright2(options) {
|
|
75108
|
-
const { playwrightCode, baseUrl, testName, testVariables, storageStatePath, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
|
|
76125
|
+
const { playwrightCode, baseUrl, testName, testVariables, storageStatePath, extraHttpHeaders, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
|
|
75109
76126
|
const logs2 = [];
|
|
75110
76127
|
const screenshots = [];
|
|
75111
76128
|
logs2.push(`\u{1F3AD} Native Playwright starting`);
|
|
@@ -75119,6 +76136,11 @@ var require_playwright_native = __commonJS({
|
|
|
75119
76136
|
runDir = (0, path_1.join)(RUNS_DIR, (0, crypto_1.randomUUID)());
|
|
75120
76137
|
await (0, promises_1.mkdir)(runDir, { recursive: true });
|
|
75121
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
|
+
});
|
|
75122
76144
|
const cssDisableSetup = `
|
|
75123
76145
|
const __qaLiveBrowserStateFile = process.env.VALIDATE_LIVE_BROWSER_STATE_PATH;
|
|
75124
76146
|
let __qaLiveBrowserInterval: ReturnType<typeof setInterval> | null = null;
|
|
@@ -75312,8 +76334,17 @@ test.afterEach(async ({ page }, testInfo) => {
|
|
|
75312
76334
|
if (!importMatch) {
|
|
75313
76335
|
logs2.push("\u26A0 Could not find @playwright/test import - animation disabling NOT injected");
|
|
75314
76336
|
}
|
|
75315
|
-
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
|
+
}
|
|
75316
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
|
+
}
|
|
75317
76348
|
const headless = process.env.PLAYWRIGHT_HEADLESS === "true" || process.env.NODE_ENV === "production" || IS_CLOUD2;
|
|
75318
76349
|
logs2.push(`\u2699\uFE0F Config: baseURL=${JSON.stringify(baseUrl)}, headless=${headless}${isApiTest2 ? ", API mode (no screenshot/video)" : ""}`);
|
|
75319
76350
|
const absTestDir = runDir.replace(/\\/g, "/");
|
|
@@ -75327,6 +76358,7 @@ test.afterEach(async ({ page }, testInfo) => {
|
|
|
75327
76358
|
const absHarPath = wantHar ? (0, path_1.join)(runDir, "session.har").replace(/\\/g, "/") : null;
|
|
75328
76359
|
const recordHarLine = absHarPath ? `recordHar: { path: ${JSON.stringify(absHarPath)}, mode: 'full' },` : "";
|
|
75329
76360
|
const storageStateLine = storageStatePath ? `storageState: ${JSON.stringify(storageStatePath.replace(/\\/g, "/"))},` : "";
|
|
76361
|
+
const extraHeadersLine = resolvedExtraHeaders && !useScopedApiHeaders && Object.keys(resolvedExtraHeaders).length > 0 ? `extraHTTPHeaders: ${JSON.stringify(resolvedExtraHeaders)},` : "";
|
|
75330
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'],";
|
|
75331
76363
|
const config = `
|
|
75332
76364
|
// Plain object export - no require('@playwright/test') needed.
|
|
@@ -75340,6 +76372,7 @@ module.exports = {
|
|
|
75340
76372
|
use: {
|
|
75341
76373
|
baseURL: ${JSON.stringify(baseUrl)},
|
|
75342
76374
|
${storageStateLine}
|
|
76375
|
+
${extraHeadersLine}
|
|
75343
76376
|
browserName: 'chromium',
|
|
75344
76377
|
headless: ${headless},
|
|
75345
76378
|
screenshot: ${isApiTest2 ? "'off'" : "'on'"},
|
|
@@ -75381,6 +76414,7 @@ module.exports = {
|
|
|
75381
76414
|
// code. Forwarded (not allowlisted) so it stays a per-run toggle; absent → inert.
|
|
75382
76415
|
...process.env.SHARED_AUTH_INJECT ? { SHARED_AUTH_INJECT: process.env.SHARED_AUTH_INJECT } : {}
|
|
75383
76416
|
};
|
|
76417
|
+
delete envWithIsolation.API_AUTH_HEADERS;
|
|
75384
76418
|
logs2.push("\u{1F680} Spawning playwright test...");
|
|
75385
76419
|
const stopLiveBrowserPolling = liveBrowserStatePath && liveBrowserHandle ? startLiveBrowserStatePolling(liveBrowserStatePath, liveBrowserHandle, onLiveBrowserSnapshot) : null;
|
|
75386
76420
|
let playResult;
|
|
@@ -79232,6 +80266,34 @@ var require_anchor_harvester = __commonJS({
|
|
|
79232
80266
|
}
|
|
79233
80267
|
});
|
|
79234
80268
|
|
|
80269
|
+
// ../runner-core/dist/services/discovery-lint-flags.js
|
|
80270
|
+
var require_discovery_lint_flags = __commonJS({
|
|
80271
|
+
"../runner-core/dist/services/discovery-lint-flags.js"(exports) {
|
|
80272
|
+
"use strict";
|
|
80273
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
80274
|
+
exports.structuralLocatorLintMode = structuralLocatorLintMode;
|
|
80275
|
+
exports.isPwMethodPreflightEnabled = isPwMethodPreflightEnabled;
|
|
80276
|
+
exports.isVqaLoginDedupeEnabled = isVqaLoginDedupeEnabled;
|
|
80277
|
+
function strictOn(value) {
|
|
80278
|
+
return value === "true" || value === "1";
|
|
80279
|
+
}
|
|
80280
|
+
function structuralLocatorLintMode() {
|
|
80281
|
+
const v = (process.env.DISCOVERY_STRUCTURAL_LOCATOR_LINT || "").trim().toLowerCase();
|
|
80282
|
+
if (v === "warn")
|
|
80283
|
+
return "warn";
|
|
80284
|
+
if (v === "true" || v === "1" || v === "enforce")
|
|
80285
|
+
return "enforce";
|
|
80286
|
+
return "off";
|
|
80287
|
+
}
|
|
80288
|
+
function isPwMethodPreflightEnabled() {
|
|
80289
|
+
return strictOn(process.env.DISCOVERY_PW_METHOD_PREFLIGHT);
|
|
80290
|
+
}
|
|
80291
|
+
function isVqaLoginDedupeEnabled() {
|
|
80292
|
+
return strictOn(process.env.DISCOVERY_VQALOGIN_DEDUPE);
|
|
80293
|
+
}
|
|
80294
|
+
}
|
|
80295
|
+
});
|
|
80296
|
+
|
|
79235
80297
|
// ../runner-core/dist/services/auth-recipe.js
|
|
79236
80298
|
var require_auth_recipe = __commonJS({
|
|
79237
80299
|
"../runner-core/dist/services/auth-recipe.js"(exports) {
|
|
@@ -79254,9 +80316,11 @@ var require_auth_recipe = __commonJS({
|
|
|
79254
80316
|
exports.extractAuthRegion = extractAuthRegion;
|
|
79255
80317
|
exports.stripAuthRegions = stripAuthRegions;
|
|
79256
80318
|
exports.ensureExpectImported = ensureExpectImported;
|
|
80319
|
+
exports.ensureLoginCall = ensureLoginCall;
|
|
79257
80320
|
exports.upsertAuthBlock = upsertAuthBlock;
|
|
79258
80321
|
exports.buildHealAuthStamp = buildHealAuthStamp;
|
|
79259
80322
|
var crypto_1 = __require("crypto");
|
|
80323
|
+
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
79260
80324
|
exports.AUTH_MARKER_VERSION = "v1";
|
|
79261
80325
|
var START_MARKER = `// >>> VALIDATE_QA_AUTH ${exports.AUTH_MARKER_VERSION} \u2014 generated, do not edit >>>`;
|
|
79262
80326
|
var END_MARKER = `// <<< VALIDATE_QA_AUTH ${exports.AUTH_MARKER_VERSION} <<<`;
|
|
@@ -79520,10 +80584,13 @@ ${tail}`;
|
|
|
79520
80584
|
var TEST_OPENER_RE = /test(?:\.(?:only|skip|fixme))?\s*\(\s*(['"`])(?:[^\\]|\\.)*?\1\s*,\s*(?:\{[\s\S]*?\}\s*,\s*)?async\s*\(\s*\{[^}]*\}[^)]*\)\s*=>\s*\{/;
|
|
79521
80585
|
var LOGIN_CALL_RE = /await\s+__vqaLogin\s*\(\s*page\s*\)/;
|
|
79522
80586
|
function ensureLoginCall(code) {
|
|
79523
|
-
|
|
79524
|
-
if (matches.length === 0)
|
|
80587
|
+
if (code.match(new RegExp(TEST_OPENER_RE.source, "g")) === null)
|
|
79525
80588
|
return code;
|
|
79526
80589
|
let result = code;
|
|
80590
|
+
if ((0, discovery_lint_flags_js_1.isVqaLoginDedupeEnabled)()) {
|
|
80591
|
+
result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
|
|
80592
|
+
}
|
|
80593
|
+
const matches = [...result.matchAll(new RegExp(TEST_OPENER_RE.source, "g"))];
|
|
79527
80594
|
for (let i = matches.length - 1; i >= 0; i--) {
|
|
79528
80595
|
const m = matches[i];
|
|
79529
80596
|
if (m.index === void 0)
|
|
@@ -83415,7 +84482,7 @@ var require_discover_generator = __commonJS({
|
|
|
83415
84482
|
"../runner-core/dist/services/discover-generator.js"(exports) {
|
|
83416
84483
|
"use strict";
|
|
83417
84484
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
83418
|
-
exports.MIN_BUG_DESCRIPTION_LENGTH = exports.BLOCKED_MCP_TOOLS_SCENARIO_GEN = void 0;
|
|
84485
|
+
exports.MIN_BUG_DESCRIPTION_LENGTH = exports.VALID_PLAYWRIGHT_GETBY_METHODS = exports.BLOCKED_MCP_TOOLS_SCENARIO_GEN = void 0;
|
|
83419
84486
|
exports.filterMcpToolsForScenarioGen = filterMcpToolsForScenarioGen;
|
|
83420
84487
|
exports.detectScenarioSignals = detectScenarioSignals;
|
|
83421
84488
|
exports.scenarioStepsCoverAuth = scenarioStepsCoverAuth;
|
|
@@ -83438,6 +84505,7 @@ var require_discover_generator = __commonJS({
|
|
|
83438
84505
|
exports.upgradePositionalProvenLines = upgradePositionalProvenLines;
|
|
83439
84506
|
exports.synthesizeRefActionLines = synthesizeRefActionLines;
|
|
83440
84507
|
exports.renderSessionRecording = renderSessionRecording;
|
|
84508
|
+
exports.scanFragileStructuralLocators = scanFragileStructuralLocators;
|
|
83441
84509
|
exports.validateFinalActionFidelity = validateFinalActionFidelity;
|
|
83442
84510
|
exports.resolveSuspectedBugVerdict = resolveSuspectedBugVerdict;
|
|
83443
84511
|
exports.buildScenarioSystemPrompt = buildScenarioSystemPrompt;
|
|
@@ -83453,6 +84521,7 @@ var require_discover_generator = __commonJS({
|
|
|
83453
84521
|
var ai_provider_js_1 = require_ai_provider();
|
|
83454
84522
|
var ai_queue_js_1 = require_ai_queue();
|
|
83455
84523
|
var ai_retry_js_1 = require_ai_retry();
|
|
84524
|
+
var discover_ai_planner_js_1 = require_discover_ai_planner();
|
|
83456
84525
|
var snapshot_parser_js_1 = require_snapshot_parser();
|
|
83457
84526
|
var scrub_credentials_js_1 = require_scrub_credentials();
|
|
83458
84527
|
var exploration_parser_js_1 = require_exploration_parser();
|
|
@@ -83465,6 +84534,7 @@ var require_discover_generator = __commonJS({
|
|
|
83465
84534
|
var auth_recipe_js_1 = require_auth_recipe();
|
|
83466
84535
|
var auth_replay_js_1 = require_auth_replay();
|
|
83467
84536
|
var agentic_discovery_prompts_js_1 = require_agentic_discovery_prompts();
|
|
84537
|
+
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
83468
84538
|
exports.BLOCKED_MCP_TOOLS_SCENARIO_GEN = /* @__PURE__ */ new Set([
|
|
83469
84539
|
"browser_install",
|
|
83470
84540
|
"browser_close",
|
|
@@ -84189,6 +85259,15 @@ ${elementContext}${formContext}${observationContext}${interactionContext}${lowCo
|
|
|
84189
85259
|
return assembled.join(separator);
|
|
84190
85260
|
}
|
|
84191
85261
|
var SENSITIVE_ENV_KEY_RE = /email|password|token|secret|api[_-]?key|access[_-]?key|username|user(name)?|bearer|session|cookie|\bpat\b|refresh|jwt/i;
|
|
85262
|
+
exports.VALID_PLAYWRIGHT_GETBY_METHODS = /* @__PURE__ */ new Set([
|
|
85263
|
+
"getByRole",
|
|
85264
|
+
"getByText",
|
|
85265
|
+
"getByLabel",
|
|
85266
|
+
"getByPlaceholder",
|
|
85267
|
+
"getByTestId",
|
|
85268
|
+
"getByAltText",
|
|
85269
|
+
"getByTitle"
|
|
85270
|
+
]);
|
|
84192
85271
|
var VALID_PLAYWRIGHT_MATCHERS = /* @__PURE__ */ new Set([
|
|
84193
85272
|
// Locator assertions
|
|
84194
85273
|
"toBeAttached",
|
|
@@ -84676,6 +85755,16 @@ ${testOpen}`;
|
|
|
84676
85755
|
return false;
|
|
84677
85756
|
}
|
|
84678
85757
|
}
|
|
85758
|
+
if ((0, discovery_lint_flags_js_1.isPwMethodPreflightEnabled)()) {
|
|
85759
|
+
const getByCalls = codeForMatcherScan.match(/\.getBy[A-Za-z0-9_]*\s*\(/g) ?? [];
|
|
85760
|
+
for (const call of getByCalls) {
|
|
85761
|
+
const methodName = call.replace(/[\s(]+$/, "").slice(1);
|
|
85762
|
+
if (!exports.VALID_PLAYWRIGHT_GETBY_METHODS.has(methodName)) {
|
|
85763
|
+
logs2?.push(` \u2717 Rejected "${t.name}": invalid Playwright locator method "${methodName}" (not a real getBy* API \u2014 likely hallucinated)`);
|
|
85764
|
+
return false;
|
|
85765
|
+
}
|
|
85766
|
+
}
|
|
85767
|
+
}
|
|
84679
85768
|
if (code.includes("// TODO")) {
|
|
84680
85769
|
logs2?.push(` \u26A0 "${t.name}" contains TODO comment \u2014 marking as unverified`);
|
|
84681
85770
|
t.tags = t.tags.filter((tag) => tag !== "@verified");
|
|
@@ -85789,6 +86878,35 @@ ${testOpen}`;
|
|
|
85789
86878
|
}
|
|
85790
86879
|
return null;
|
|
85791
86880
|
}
|
|
86881
|
+
var STRUCTURAL_ACTION_RE = /\.(?:click|dblclick|tap|fill|type|pressSequentially|press|selectOption|hover|check|uncheck|setChecked|setInputFiles|dragTo|focus|clear)\s*\(/;
|
|
86882
|
+
var POSITIONAL_PSEUDO_RE = /:(?:last-child|first-child|nth-child|nth-of-type|nth-last-child|nth-last-of-type)\b/i;
|
|
86883
|
+
var TABLE_TOKEN_RE = /\b(?:tr|td|th|tbody|thead)\b/i;
|
|
86884
|
+
var ROW_ANCHOR_RE = /getByRole\s*\(\s*['"`]row['"`]|getByTestId|data-testid|data-test\b|\[role\s*=\s*['"`]row['"`]\]/i;
|
|
86885
|
+
var LOCATOR_ARG_RE = /\.locator\(\s*(['"`])((?:\\.|(?!\1)[\s\S])*?)\1/g;
|
|
86886
|
+
function scanFragileStructuralLocators(code) {
|
|
86887
|
+
const findings = [];
|
|
86888
|
+
const scanCode = code.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "));
|
|
86889
|
+
for (const rawLine of scanCode.split("\n")) {
|
|
86890
|
+
const line = rawLine.replace(/\/\/.*$/, "");
|
|
86891
|
+
if (!STRUCTURAL_ACTION_RE.test(line))
|
|
86892
|
+
continue;
|
|
86893
|
+
if (ROW_ANCHOR_RE.test(line))
|
|
86894
|
+
continue;
|
|
86895
|
+
LOCATOR_ARG_RE.lastIndex = 0;
|
|
86896
|
+
let m;
|
|
86897
|
+
while ((m = LOCATOR_ARG_RE.exec(line)) !== null) {
|
|
86898
|
+
const css = m[2];
|
|
86899
|
+
if (POSITIONAL_PSEUDO_RE.test(css) && TABLE_TOKEN_RE.test(css)) {
|
|
86900
|
+
findings.push({
|
|
86901
|
+
line: rawLine.trim().slice(0, 200),
|
|
86902
|
+
reason: `Positional table-row selector "${css.slice(0, 120)}" is not scoped to a row identity \u2014 it breaks when rows reorder, sort, filter, or a row is inserted. Scope through the row: page.getByRole('row', { name: '<a stable cell value in that row>' }).getByRole('button', { name: '<verb>' }).`
|
|
86903
|
+
});
|
|
86904
|
+
break;
|
|
86905
|
+
}
|
|
86906
|
+
}
|
|
86907
|
+
}
|
|
86908
|
+
return findings;
|
|
86909
|
+
}
|
|
85792
86910
|
function validateFinalActionFidelity(playwrightCode, actionLedger, credentials, opts) {
|
|
85793
86911
|
const ledgerSigs = [];
|
|
85794
86912
|
for (const entry of actionLedger) {
|
|
@@ -86228,6 +87346,7 @@ ${tailBlock}`;
|
|
|
86228
87346
|
let actionRevisionAttempted = false;
|
|
86229
87347
|
let ambiguityRevisionAttempted = false;
|
|
86230
87348
|
let strengthRevisionAttempted = false;
|
|
87349
|
+
let structuralRevisionAttempted = false;
|
|
86231
87350
|
let lastRenderedLedgerEntryCount = 0;
|
|
86232
87351
|
const successfulVerifications = [];
|
|
86233
87352
|
const actionLedger = [];
|
|
@@ -86354,7 +87473,15 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
86354
87473
|
tools: openaiTools,
|
|
86355
87474
|
tool_choice: "auto",
|
|
86356
87475
|
temperature: 0.2,
|
|
86357
|
-
|
|
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)
|
|
86358
87485
|
}, { signal });
|
|
86359
87486
|
}), {
|
|
86360
87487
|
label: `scenario-gen-iter-${iteration}`,
|
|
@@ -86498,6 +87625,33 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
86498
87625
|
});
|
|
86499
87626
|
continue;
|
|
86500
87627
|
}
|
|
87628
|
+
const structuralLintMode = (0, discovery_lint_flags_js_1.structuralLocatorLintMode)();
|
|
87629
|
+
const structuralFindings = structuralLintMode !== "off" ? scanFragileStructuralLocators(validated[0].playwrightCode) : [];
|
|
87630
|
+
if (structuralLintMode === "warn" && structuralFindings.length > 0) {
|
|
87631
|
+
logs2.push(` Structural-locator gate [warn]: ${structuralFindings.length} positional table-row selector(s) \u2014 ${structuralFindings.map((f) => sanitizeLedgerText(f.line)).join(" | ").slice(0, 300)}`);
|
|
87632
|
+
}
|
|
87633
|
+
if (structuralLintMode === "enforce" && structuralFindings.length > 0 && !structuralRevisionAttempted) {
|
|
87634
|
+
structuralRevisionAttempted = true;
|
|
87635
|
+
const renderedLedger = actionLedger.length > 0 ? renderSessionRecording(actionLedger, credentials) : "";
|
|
87636
|
+
if (renderedLedger)
|
|
87637
|
+
lastRenderedLedgerEntryCount = actionLedger.length;
|
|
87638
|
+
logs2.push(` Structural-locator gate: ${structuralFindings.length} positional table-row selector(s) found \u2014 requesting one revision (recording attached: ${renderedLedger ? "yes" : "no"})`);
|
|
87639
|
+
const structuralPayload = {
|
|
87640
|
+
acknowledged: false,
|
|
87641
|
+
valid: false,
|
|
87642
|
+
revisionRequired: true,
|
|
87643
|
+
reason: "fragile_structural_locator",
|
|
87644
|
+
instruction: "One or more locators below select a table row by POSITION (tr:last-child, tr:nth-child, td:nth-child) instead of by identity. Positional row selectors break the moment rows are reordered, sorted, filtered, or a row is inserted.\nFor EACH flagged locator:\n1. Scope through the row IDENTITY: page.getByRole('row', { name: '<a stable cell value in that row \u2014 an id, name, email>' }).getByRole('button', { name: '<verb>' }).\n2. If the row genuinely has no stable text, use a data-testid on the row if one exists.\n3. Keep every other line exactly as-is. Submit the COMPLETE corrected test and call finish_generate again.",
|
|
87645
|
+
offendingLocators: structuralFindings.map((f) => ({
|
|
87646
|
+
line: sanitizeLedgerText(f.line),
|
|
87647
|
+
reason: f.reason
|
|
87648
|
+
}))
|
|
87649
|
+
};
|
|
87650
|
+
if (renderedLedger)
|
|
87651
|
+
structuralPayload.sessionRecording = renderedLedger;
|
|
87652
|
+
toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: JSON.stringify(structuralPayload) });
|
|
87653
|
+
continue;
|
|
87654
|
+
}
|
|
86501
87655
|
const ambiguityGateEval = evaluateAmbiguityGate(validated[0].playwrightCode, ambiguityRevisionAttempted, iteration < MAX_SCENARIO_ITERATIONS);
|
|
86502
87656
|
const ambiguousLocators = ambiguityGateEval.findings;
|
|
86503
87657
|
if (ambiguityGateEval.fire) {
|
|
@@ -86594,12 +87748,16 @@ Do NOT assert that the button/tab/heading you interacted with is merely visible
|
|
|
86594
87748
|
const strengthSeverity = strengthVerdict?.severity ?? "none";
|
|
86595
87749
|
const hasResidualViolations = qualityCheck.offendingLines.length > 0;
|
|
86596
87750
|
const hasResidualActionDrift = actionCheck.offendingActions.length > 0;
|
|
87751
|
+
const hasResidualStructural = structuralLintMode === "enforce" && structuralFindings.length > 0;
|
|
86597
87752
|
if (hasResidualViolations) {
|
|
86598
87753
|
logs2.push(` Assertion quality gate: violations remain after revision \u2014 downgrading to @needs-review`);
|
|
86599
87754
|
}
|
|
86600
87755
|
if (hasResidualActionDrift) {
|
|
86601
87756
|
logs2.push(` Action fidelity gate: ${actionCheck.offendingActions.length} action drift(s) remain after revision \u2014 downgrading to @needs-review`);
|
|
86602
87757
|
}
|
|
87758
|
+
if (hasResidualStructural) {
|
|
87759
|
+
logs2.push(` Structural-locator gate: ${structuralFindings.length} positional table-row selector(s) remain after revision \u2014 downgrading to @needs-review`);
|
|
87760
|
+
}
|
|
86603
87761
|
let actualVerified = verified;
|
|
86604
87762
|
if (verified && verifyCallCount === 0) {
|
|
86605
87763
|
actualVerified = false;
|
|
@@ -86652,6 +87810,16 @@ Do NOT assert that the button/tab/heading you interacted with is merely visible
|
|
|
86652
87810
|
residual: ambiguousLocators.length,
|
|
86653
87811
|
lines: ambiguousLocators.map((a) => sanitizeLedgerText(a.line))
|
|
86654
87812
|
},
|
|
87813
|
+
// G2 — omitted entirely when off so the off-path healingContext is
|
|
87814
|
+
// byte-identical; present in warn (measure) + enforce (act) modes.
|
|
87815
|
+
...structuralLintMode !== "off" ? {
|
|
87816
|
+
structuralGate: {
|
|
87817
|
+
mode: structuralLintMode,
|
|
87818
|
+
fired: structuralRevisionAttempted,
|
|
87819
|
+
residual: structuralFindings.length,
|
|
87820
|
+
lines: structuralFindings.map((f) => sanitizeLedgerText(f.line))
|
|
87821
|
+
}
|
|
87822
|
+
} : {},
|
|
86655
87823
|
// Durable marker the server promote-seam (failure-analyzer.checkTestStability)
|
|
86656
87824
|
// reads to decide ACTIVE vs QUARANTINED(@bug-found) vs hold-DRAFT. Omitted when
|
|
86657
87825
|
// the gate errored (fail-open) so a missing marker is treated as "promote".
|
|
@@ -86677,7 +87845,7 @@ Do NOT assert that the button/tab/heading you interacted with is merely visible
|
|
|
86677
87845
|
verified: actualVerified,
|
|
86678
87846
|
healingContext: { ...baseHealingContext, gateActivity }
|
|
86679
87847
|
};
|
|
86680
|
-
if ((hasResidualViolations || hasResidualActionDrift || hasUncoveredOutcomes) && !result.tags.includes("@needs-review")) {
|
|
87848
|
+
if ((hasResidualViolations || hasResidualActionDrift || hasUncoveredOutcomes || hasResidualStructural) && !result.tags.includes("@needs-review")) {
|
|
86681
87849
|
result.tags.push("@needs-review");
|
|
86682
87850
|
}
|
|
86683
87851
|
if (actualVerified) {
|
|
@@ -320911,675 +322079,6 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
320911
322079
|
}
|
|
320912
322080
|
});
|
|
320913
322081
|
|
|
320914
|
-
// ../runner-core/dist/services/mobile/mobile-exploration-state.js
|
|
320915
|
-
var require_mobile_exploration_state = __commonJS({
|
|
320916
|
-
"../runner-core/dist/services/mobile/mobile-exploration-state.js"(exports) {
|
|
320917
|
-
"use strict";
|
|
320918
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
320919
|
-
exports.MobileExplorationState = void 0;
|
|
320920
|
-
exports.stableMobileControlIdentity = stableMobileControlIdentity;
|
|
320921
|
-
exports.classifySafeMobileNavigationControl = classifySafeMobileNavigationControl;
|
|
320922
|
-
exports.deriveScreenId = deriveScreenId;
|
|
320923
|
-
var mobile_observation_js_1 = require_mobile_observation();
|
|
320924
|
-
var DEFAULT_STALE_THRESHOLD = 4;
|
|
320925
|
-
var MAX_SCREEN_OBSERVATIONS = 50;
|
|
320926
|
-
var EDITABLE_CONTROL_ROLE_RE = /(?:android\.widget\.(?:EditText|AutoCompleteTextView)|XCUIElementType(?:TextField|SecureTextField|SearchField|TextView)|textbox|searchbox|combobox|textarea|select)$/i;
|
|
320927
|
-
var STATEFUL_CONTROL_ROLE_RE = /(?:Switch|CheckBox|Checkbox|RadioButton|radio|Slider|SeekBar|Stepper|PickerWheel|Toggle)$/i;
|
|
320928
|
-
var UNSAFE_OR_COMMIT_LABEL_RE = /\b(?:submit|save|confirm|delete|remove|archive|reset|disconnect|deactivate|destroy|purge|pay|purchase|buy|checkout|place\s+order|send|post|publish|upload|import|export|sign\s*out|log\s*out|logout|accept|agree|subscribe|unsubscribe|follow|unfollow|enable|disable|book|reserve|cancel|continue|next|previous|done|apply|retry|close|dismiss|sign\s*in|log\s*in|register)\b/i;
|
|
320929
|
-
var SAFE_MODAL_TRIGGER_LABEL_RE = /^(?:create|new|add|edit|invite|configure|manage|details|preview|share)$/i;
|
|
320930
|
-
var MENU_TRIGGER_LABEL_RE = /^(?:more|menu|options|actions|filter|filters|sort|columns|customize)$/i;
|
|
320931
|
-
var GENERIC_NAVIGATION_DESTINATION_LABEL_RE = /^(?:home|feed|dashboard|profile|settings|inbox|messages?|notifications?|projects?|about|privacy|terms|learn\s+more)$/i;
|
|
320932
|
-
var SYSTEM_BACK_CONTROL_LABEL_RE = /^(?:back|go\s+back|navigate\s+up|navigation\s+up|up)$/i;
|
|
320933
|
-
var SYSTEM_BACK_CONTROL_ID_RE = /^(?:action_bar_up|arrow_back|back_arrow|navigate_up|navigation_up|nav_up|toolbar_back|action_back|btn_back|button_back|back_button|back_btn|back_icon|icon_back)$/i;
|
|
320934
|
-
function normalizeControlIdentityPart(value) {
|
|
320935
|
-
return value.trim().replace(/\s+/g, " ").toLowerCase();
|
|
320936
|
-
}
|
|
320937
|
-
function semanticControlAlias(label) {
|
|
320938
|
-
return `navigation-label:${normalizeControlIdentityPart(label)}`;
|
|
320939
|
-
}
|
|
320940
|
-
function localControlCoverageIdentity(screenId, identity) {
|
|
320941
|
-
return `screen:${screenId}:${identity}`;
|
|
320942
|
-
}
|
|
320943
|
-
function storedMobileControlIdentity(candidate) {
|
|
320944
|
-
return [
|
|
320945
|
-
candidate.identity,
|
|
320946
|
-
`kind:${candidate.kind}`,
|
|
320947
|
-
`label:${normalizeControlIdentityPart(candidate.label)}`
|
|
320948
|
-
].join(":");
|
|
320949
|
-
}
|
|
320950
|
-
function isSystemBackNavigationControl(element, label) {
|
|
320951
|
-
if (SYSTEM_BACK_CONTROL_LABEL_RE.test(normalizeControlIdentityPart(label)))
|
|
320952
|
-
return true;
|
|
320953
|
-
return [element.accessibilityId, element.resourceId].filter((value) => Boolean(value?.trim())).some((value) => {
|
|
320954
|
-
const leaf = normalizeControlIdentityPart(value).split(/[/:.]/).filter(Boolean).at(-1) ?? "";
|
|
320955
|
-
return SYSTEM_BACK_CONTROL_ID_RE.test(leaf);
|
|
320956
|
-
});
|
|
320957
|
-
}
|
|
320958
|
-
function stableMobileControlIdentity(element) {
|
|
320959
|
-
const role = normalizeControlIdentityPart(element.role || "control");
|
|
320960
|
-
const accessibilityId = element.accessibilityId?.trim();
|
|
320961
|
-
if (accessibilityId)
|
|
320962
|
-
return `a11y:${role}:${normalizeControlIdentityPart(accessibilityId)}`;
|
|
320963
|
-
const resourceId = element.resourceId?.trim();
|
|
320964
|
-
if (resourceId)
|
|
320965
|
-
return `resource:${role}:${normalizeControlIdentityPart(resourceId)}`;
|
|
320966
|
-
const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
|
|
320967
|
-
if (label)
|
|
320968
|
-
return `semantic:${role}:${normalizeControlIdentityPart(label)}`;
|
|
320969
|
-
return null;
|
|
320970
|
-
}
|
|
320971
|
-
function classifySafeMobileNavigationControl(element) {
|
|
320972
|
-
if (!element.actionable || element.enabled === false)
|
|
320973
|
-
return null;
|
|
320974
|
-
const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
|
|
320975
|
-
const safetyIdentity = [label, element.accessibilityId, element.resourceId].filter(Boolean).join(" ");
|
|
320976
|
-
if (!label || UNSAFE_OR_COMMIT_LABEL_RE.test(safetyIdentity))
|
|
320977
|
-
return null;
|
|
320978
|
-
if (isSystemBackNavigationControl(element, label))
|
|
320979
|
-
return null;
|
|
320980
|
-
const role = element.role.trim();
|
|
320981
|
-
if (EDITABLE_CONTROL_ROLE_RE.test(role) || STATEFUL_CONTROL_ROLE_RE.test(role))
|
|
320982
|
-
return null;
|
|
320983
|
-
const linkLike = /(?:Link$|^a$)/i.test(role);
|
|
320984
|
-
let kind = null;
|
|
320985
|
-
if (/(?:TabBarItem|\btab\b)/i.test(role))
|
|
320986
|
-
kind = "tab";
|
|
320987
|
-
else if (/menuitem/i.test(role))
|
|
320988
|
-
kind = "menu-item";
|
|
320989
|
-
else if (linkLike && element.navigationProvenance)
|
|
320990
|
-
kind = "link";
|
|
320991
|
-
else if (linkLike)
|
|
320992
|
-
return null;
|
|
320993
|
-
else if (element.hasPopup && element.hasPopup.toLowerCase() !== "false")
|
|
320994
|
-
kind = "menu-trigger";
|
|
320995
|
-
else if (element.expanded === false)
|
|
320996
|
-
kind = "disclosure";
|
|
320997
|
-
else if (MENU_TRIGGER_LABEL_RE.test(label))
|
|
320998
|
-
kind = "menu-trigger";
|
|
320999
|
-
else if (SAFE_MODAL_TRIGGER_LABEL_RE.test(label))
|
|
321000
|
-
kind = "modal-trigger";
|
|
321001
|
-
else if (element.navigationProvenance)
|
|
321002
|
-
kind = "navigation-button";
|
|
321003
|
-
else if (GENERIC_NAVIGATION_DESTINATION_LABEL_RE.test(label))
|
|
321004
|
-
kind = "navigation-button";
|
|
321005
|
-
if (!kind)
|
|
321006
|
-
return null;
|
|
321007
|
-
const identity = stableMobileControlIdentity(element);
|
|
321008
|
-
return identity ? { identity, label, kind } : null;
|
|
321009
|
-
}
|
|
321010
|
-
function sharedMobileControlCoverageIdentity(element, candidate, semanticFamilyId) {
|
|
321011
|
-
if (!element.navigationProvenance)
|
|
321012
|
-
return void 0;
|
|
321013
|
-
const durableId = element.accessibilityId?.trim() ? `a11y:${normalizeControlIdentityPart(element.accessibilityId)}` : element.resourceId?.trim() ? `resource:${normalizeControlIdentityPart(element.resourceId)}` : "";
|
|
321014
|
-
if (!durableId && !semanticFamilyId)
|
|
321015
|
-
return void 0;
|
|
321016
|
-
return [
|
|
321017
|
-
"shared-navigation",
|
|
321018
|
-
element.navigationProvenance,
|
|
321019
|
-
!durableId && semanticFamilyId ? `family:${semanticFamilyId}` : "",
|
|
321020
|
-
candidate.kind,
|
|
321021
|
-
durableId || "semantic-only",
|
|
321022
|
-
normalizeControlIdentityPart(candidate.label)
|
|
321023
|
-
].filter(Boolean).join(":");
|
|
321024
|
-
}
|
|
321025
|
-
function mobileNavigationRegion(element, windowSize) {
|
|
321026
|
-
if (!windowSize || windowSize.width <= 0 || windowSize.height <= 0)
|
|
321027
|
-
return "unknown";
|
|
321028
|
-
const centerX = (element.bounds.x + element.bounds.width / 2) / windowSize.width;
|
|
321029
|
-
const centerY = (element.bounds.y + element.bounds.height / 2) / windowSize.height;
|
|
321030
|
-
if (centerY >= 0.72)
|
|
321031
|
-
return "bottom";
|
|
321032
|
-
if (centerY <= 0.28)
|
|
321033
|
-
return "top";
|
|
321034
|
-
if (centerX <= 0.35)
|
|
321035
|
-
return "left";
|
|
321036
|
-
if (centerX >= 0.65)
|
|
321037
|
-
return "right";
|
|
321038
|
-
return "center";
|
|
321039
|
-
}
|
|
321040
|
-
function buildSemanticNavigationGroups(elements, windowSize) {
|
|
321041
|
-
const groups = /* @__PURE__ */ new Map();
|
|
321042
|
-
for (const element of elements) {
|
|
321043
|
-
if (!element.navigationProvenance)
|
|
321044
|
-
continue;
|
|
321045
|
-
const candidate = classifySafeMobileNavigationControl(element);
|
|
321046
|
-
if (!candidate)
|
|
321047
|
-
continue;
|
|
321048
|
-
const groupKey = `${element.navigationProvenance}:${mobileNavigationRegion(element, windowSize)}`;
|
|
321049
|
-
const labels = groups.get(groupKey) ?? /* @__PURE__ */ new Set();
|
|
321050
|
-
labels.add(`${candidate.kind}:${normalizeControlIdentityPart(candidate.label)}`);
|
|
321051
|
-
groups.set(groupKey, labels);
|
|
321052
|
-
}
|
|
321053
|
-
return new Map([...groups].filter(([, labels]) => labels.size >= 2));
|
|
321054
|
-
}
|
|
321055
|
-
function deriveScreenId(signal) {
|
|
321056
|
-
const namespace = signal.activity?.trim() || signal.bundleId?.trim();
|
|
321057
|
-
return namespace ? `${namespace}#${signal.screenHash}` : signal.screenHash;
|
|
321058
|
-
}
|
|
321059
|
-
function observedElementIdentity(element) {
|
|
321060
|
-
return (0, mobile_observation_js_1.stableParsedMobileElementIdentity)(element);
|
|
321061
|
-
}
|
|
321062
|
-
function mergeObservedElements(previous, current) {
|
|
321063
|
-
const merged = [...current];
|
|
321064
|
-
const seen = new Set(current.map(observedElementIdentity));
|
|
321065
|
-
for (const element of previous) {
|
|
321066
|
-
const identity = observedElementIdentity(element);
|
|
321067
|
-
if (seen.has(identity))
|
|
321068
|
-
continue;
|
|
321069
|
-
seen.add(identity);
|
|
321070
|
-
merged.push(element);
|
|
321071
|
-
}
|
|
321072
|
-
return merged;
|
|
321073
|
-
}
|
|
321074
|
-
function sanitizeElementsForEvidence(elements) {
|
|
321075
|
-
return elements.map((element) => {
|
|
321076
|
-
const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element);
|
|
321077
|
-
return {
|
|
321078
|
-
...element,
|
|
321079
|
-
...label ? { label } : { label: void 0 },
|
|
321080
|
-
text: void 0
|
|
321081
|
-
};
|
|
321082
|
-
});
|
|
321083
|
-
}
|
|
321084
|
-
var MobileExplorationState = class {
|
|
321085
|
-
/** screenId -> ScreenState (the mobile analog of routeState). */
|
|
321086
|
-
screens = /* @__PURE__ */ new Map();
|
|
321087
|
-
/** Observed directed transitions between screens. */
|
|
321088
|
-
transitions = [];
|
|
321089
|
-
/** Transition-announced screens that do not yet have a live observation. */
|
|
321090
|
-
frontier = /* @__PURE__ */ new Map();
|
|
321091
|
-
/** Safe navigation controls observed on each screen, keyed by stable identity. */
|
|
321092
|
-
safeControlsByScreen = /* @__PURE__ */ new Map();
|
|
321093
|
-
/** Screen-scoped coverage keys plus explicitly proven shared-navigation keys. */
|
|
321094
|
-
attemptedSafeControlCoverageIdentities = /* @__PURE__ */ new Set();
|
|
321095
|
-
successfulSafeControlAttemptCount = 0;
|
|
321096
|
-
/** Repeated id-less structural navigation families, grouped by region. */
|
|
321097
|
-
semanticNavigationFamilies = /* @__PURE__ */ new Map();
|
|
321098
|
-
nextSemanticNavigationFamilyId = 1;
|
|
321099
|
-
/** The screen-id of the most recently observed screen. */
|
|
321100
|
-
currentScreenId = null;
|
|
321101
|
-
/** Latest iteration recorded (drives stale detection + firstSeen stamps). */
|
|
321102
|
-
iteration = 0;
|
|
321103
|
-
/** Stale detector: progress mark (#screens + #total elements) at last advance. */
|
|
321104
|
-
lastProgressMark = 0;
|
|
321105
|
-
/** Consecutive iterations observed with no progress. */
|
|
321106
|
-
staleIterations = 0;
|
|
321107
|
-
staleThreshold;
|
|
321108
|
-
constructor(options = {}) {
|
|
321109
|
-
this.staleThreshold = typeof options.staleThreshold === "number" && options.staleThreshold > 0 ? Math.floor(options.staleThreshold) : DEFAULT_STALE_THRESHOLD;
|
|
321110
|
-
}
|
|
321111
|
-
// -------------------------------------------------------------------------
|
|
321112
|
-
// Observation recording
|
|
321113
|
-
// -------------------------------------------------------------------------
|
|
321114
|
-
/**
|
|
321115
|
-
* Returns true when `signal` identifies a screen this run has never observed
|
|
321116
|
-
* before. Pure read — does not mutate state.
|
|
321117
|
-
*/
|
|
321118
|
-
isNewScreen(signal) {
|
|
321119
|
-
return !this.screens.has(deriveScreenId(signal));
|
|
321120
|
-
}
|
|
321121
|
-
/**
|
|
321122
|
-
* Record an observation of `screen` at `iteration`. Creates a `ScreenState`
|
|
321123
|
-
* on first sight (stamping `firstSeenIteration`), otherwise refreshes the
|
|
321124
|
-
* stored element set (latest-wins, mirroring the web reducer's PAGE_CAPTURED
|
|
321125
|
-
* element refresh). A driver observation already contains the structural UI
|
|
321126
|
-
* tree, elements, screenshot association, and screen identity, so it is
|
|
321127
|
-
* captured evidence immediately; the model's capture_screen call only adds
|
|
321128
|
-
* a friendly name/type. Returns the resolved screen-id.
|
|
321129
|
-
*/
|
|
321130
|
-
recordObservation(signal, screen, iteration) {
|
|
321131
|
-
this.iteration = iteration;
|
|
321132
|
-
const screenId = deriveScreenId(signal);
|
|
321133
|
-
const evidenceElements = sanitizeElementsForEvidence(screen.elements);
|
|
321134
|
-
const existing = this.screens.get(screenId);
|
|
321135
|
-
if (existing) {
|
|
321136
|
-
this.screens.set(screenId, {
|
|
321137
|
-
...existing,
|
|
321138
|
-
elements: mergeObservedElements(existing.elements, evidenceElements)
|
|
321139
|
-
});
|
|
321140
|
-
} else {
|
|
321141
|
-
this.screens.set(screenId, {
|
|
321142
|
-
screenId,
|
|
321143
|
-
firstSeenIteration: iteration,
|
|
321144
|
-
elements: evidenceElements,
|
|
321145
|
-
visited: true
|
|
321146
|
-
});
|
|
321147
|
-
}
|
|
321148
|
-
this.frontier.delete(screenId);
|
|
321149
|
-
const controls = this.safeControlsByScreen.get(screenId) ?? /* @__PURE__ */ new Map();
|
|
321150
|
-
const semanticNavigationGroups = buildSemanticNavigationGroups(evidenceElements, screen.windowSize);
|
|
321151
|
-
const semanticNavigationFamilyIds = /* @__PURE__ */ new Map();
|
|
321152
|
-
for (const [groupKey, labels] of semanticNavigationGroups) {
|
|
321153
|
-
semanticNavigationFamilyIds.set(groupKey, this.resolveSemanticNavigationFamilyId(groupKey, labels));
|
|
321154
|
-
}
|
|
321155
|
-
for (const element of evidenceElements) {
|
|
321156
|
-
const candidate = classifySafeMobileNavigationControl(element);
|
|
321157
|
-
if (!candidate)
|
|
321158
|
-
continue;
|
|
321159
|
-
const storedIdentity = storedMobileControlIdentity(candidate);
|
|
321160
|
-
const semanticGroupKey = element.navigationProvenance ? `${element.navigationProvenance}:${mobileNavigationRegion(element, screen.windowSize)}` : "";
|
|
321161
|
-
controls.set(storedIdentity, {
|
|
321162
|
-
...candidate,
|
|
321163
|
-
identity: storedIdentity,
|
|
321164
|
-
sharedCoverageIdentity: sharedMobileControlCoverageIdentity(element, candidate, semanticNavigationFamilyIds.get(semanticGroupKey))
|
|
321165
|
-
});
|
|
321166
|
-
}
|
|
321167
|
-
if (controls.size > 0)
|
|
321168
|
-
this.safeControlsByScreen.set(screenId, controls);
|
|
321169
|
-
this.currentScreenId = screenId;
|
|
321170
|
-
this.updateStaleProgress();
|
|
321171
|
-
return screenId;
|
|
321172
|
-
}
|
|
321173
|
-
/**
|
|
321174
|
-
* Back-compatible acknowledgement from capture_screen. Observation already
|
|
321175
|
-
* marks structural evidence visited; this call only resolves a rare pending
|
|
321176
|
-
* transition frontier and preserves callers that annotate explicitly.
|
|
321177
|
-
*/
|
|
321178
|
-
markVisited(screenId) {
|
|
321179
|
-
const state2 = this.screens.get(screenId);
|
|
321180
|
-
if (state2 && !state2.visited) {
|
|
321181
|
-
this.screens.set(screenId, { ...state2, visited: true });
|
|
321182
|
-
}
|
|
321183
|
-
this.frontier.delete(screenId);
|
|
321184
|
-
}
|
|
321185
|
-
/**
|
|
321186
|
-
* Record a confirmed tap against a safe navigation candidate. Callers invoke
|
|
321187
|
-
* this only after the driver reports success; recovery-unknown, stale-ref, and
|
|
321188
|
-
* failed actions must remain on the breadth frontier.
|
|
321189
|
-
*/
|
|
321190
|
-
noteControlAttempt(screenId, element) {
|
|
321191
|
-
if (!screenId)
|
|
321192
|
-
return false;
|
|
321193
|
-
const candidate = classifySafeMobileNavigationControl(element);
|
|
321194
|
-
const controls = this.safeControlsByScreen.get(screenId) ?? /* @__PURE__ */ new Map();
|
|
321195
|
-
if (candidate) {
|
|
321196
|
-
const storedIdentity = storedMobileControlIdentity(candidate);
|
|
321197
|
-
const existing = controls.get(storedIdentity);
|
|
321198
|
-
controls.set(storedIdentity, {
|
|
321199
|
-
...candidate,
|
|
321200
|
-
identity: storedIdentity,
|
|
321201
|
-
sharedCoverageIdentity: existing?.sharedCoverageIdentity ?? sharedMobileControlCoverageIdentity(element, candidate)
|
|
321202
|
-
});
|
|
321203
|
-
this.safeControlsByScreen.set(screenId, controls);
|
|
321204
|
-
}
|
|
321205
|
-
const tappedLabel = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
|
|
321206
|
-
if (!tappedLabel)
|
|
321207
|
-
return false;
|
|
321208
|
-
const before = this.attemptedSafeControlCoverageIdentities.size;
|
|
321209
|
-
const alias = semanticControlAlias(tappedLabel);
|
|
321210
|
-
for (const control of controls.values()) {
|
|
321211
|
-
if (semanticControlAlias(control.label) === alias) {
|
|
321212
|
-
this.attemptedSafeControlCoverageIdentities.add(localControlCoverageIdentity(screenId, control.identity));
|
|
321213
|
-
if (control.sharedCoverageIdentity) {
|
|
321214
|
-
this.attemptedSafeControlCoverageIdentities.add(control.sharedCoverageIdentity);
|
|
321215
|
-
}
|
|
321216
|
-
}
|
|
321217
|
-
}
|
|
321218
|
-
if (this.attemptedSafeControlCoverageIdentities.size > before) {
|
|
321219
|
-
this.successfulSafeControlAttemptCount += 1;
|
|
321220
|
-
this.staleIterations = 0;
|
|
321221
|
-
return true;
|
|
321222
|
-
}
|
|
321223
|
-
return false;
|
|
321224
|
-
}
|
|
321225
|
-
/** Attach a human-friendly name / coarse type to a screen (capture_screen). */
|
|
321226
|
-
annotateScreen(screenId, annotation) {
|
|
321227
|
-
const state2 = this.screens.get(screenId);
|
|
321228
|
-
if (!state2)
|
|
321229
|
-
return;
|
|
321230
|
-
this.screens.set(screenId, {
|
|
321231
|
-
...state2,
|
|
321232
|
-
screenName: annotation.screenName ?? state2.screenName,
|
|
321233
|
-
screenType: annotation.screenType ?? state2.screenType
|
|
321234
|
-
});
|
|
321235
|
-
}
|
|
321236
|
-
/** Attach structurally observed authentication provenance to a screen. */
|
|
321237
|
-
setScreenAuthEvidence(screenId, evidence) {
|
|
321238
|
-
const state2 = this.screens.get(screenId);
|
|
321239
|
-
if (!state2)
|
|
321240
|
-
return;
|
|
321241
|
-
this.screens.set(screenId, {
|
|
321242
|
-
...state2,
|
|
321243
|
-
requiresAuth: evidence.requiresAuth,
|
|
321244
|
-
authProvenance: evidence.authProvenance,
|
|
321245
|
-
sessionState: { ...evidence.sessionState }
|
|
321246
|
-
});
|
|
321247
|
-
}
|
|
321248
|
-
/** Append a free-form observation note to a screen (observations tool). */
|
|
321249
|
-
addScreenObservation(screenId, note) {
|
|
321250
|
-
const state2 = this.screens.get(screenId);
|
|
321251
|
-
if (!state2)
|
|
321252
|
-
return;
|
|
321253
|
-
const normalized = note.trim();
|
|
321254
|
-
if (!normalized)
|
|
321255
|
-
return;
|
|
321256
|
-
const current = state2.observations ?? [];
|
|
321257
|
-
if (current.some((existing) => existing.trim().toLowerCase() === normalized.toLowerCase())) {
|
|
321258
|
-
return;
|
|
321259
|
-
}
|
|
321260
|
-
const observations = [...current, normalized].slice(-MAX_SCREEN_OBSERVATIONS);
|
|
321261
|
-
this.screens.set(screenId, { ...state2, observations });
|
|
321262
|
-
}
|
|
321263
|
-
/**
|
|
321264
|
-
* Record a directed transition between two screens. Both endpoints are
|
|
321265
|
-
* ensured to exist on the frontier if not yet observed (mirrors the web
|
|
321266
|
-
* reducer adding both ends of a TRANSITION_RECORDED to the frontier). The
|
|
321267
|
-
* `toScreenId` becomes the current screen when the transition starts from the
|
|
321268
|
-
* current screen.
|
|
321269
|
-
*/
|
|
321270
|
-
recordTransition(transition) {
|
|
321271
|
-
let recordedTransition = transition;
|
|
321272
|
-
if (!transition.viaControlIdentity && transition.viaRef) {
|
|
321273
|
-
const sourceElement = this.screens.get(transition.fromScreenId)?.elements.find((element) => element.ref === transition.viaRef);
|
|
321274
|
-
if (sourceElement) {
|
|
321275
|
-
recordedTransition = {
|
|
321276
|
-
...transition,
|
|
321277
|
-
viaControlIdentity: (0, mobile_observation_js_1.stableParsedMobileElementIdentity)(sourceElement)
|
|
321278
|
-
};
|
|
321279
|
-
}
|
|
321280
|
-
}
|
|
321281
|
-
const alreadyRecorded = this.transitions.some((existing) => existing.fromScreenId === recordedTransition.fromScreenId && existing.toScreenId === recordedTransition.toScreenId && existing.action === recordedTransition.action);
|
|
321282
|
-
if (!alreadyRecorded) {
|
|
321283
|
-
this.transitions.push(recordedTransition);
|
|
321284
|
-
}
|
|
321285
|
-
for (const endpointId of [transition.fromScreenId, transition.toScreenId]) {
|
|
321286
|
-
if (!this.screens.has(endpointId) && !this.frontier.has(endpointId)) {
|
|
321287
|
-
this.frontier.set(endpointId, {
|
|
321288
|
-
screenId: endpointId,
|
|
321289
|
-
source: "transition",
|
|
321290
|
-
discoveredAtIteration: this.iteration
|
|
321291
|
-
});
|
|
321292
|
-
}
|
|
321293
|
-
}
|
|
321294
|
-
if (transition.fromScreenId === this.currentScreenId) {
|
|
321295
|
-
this.currentScreenId = transition.toScreenId;
|
|
321296
|
-
}
|
|
321297
|
-
}
|
|
321298
|
-
// -------------------------------------------------------------------------
|
|
321299
|
-
// Selectors
|
|
321300
|
-
// -------------------------------------------------------------------------
|
|
321301
|
-
/** The current screen-id (most recently observed), or null before any observation. */
|
|
321302
|
-
getCurrentScreenId() {
|
|
321303
|
-
return this.currentScreenId;
|
|
321304
|
-
}
|
|
321305
|
-
/** ScreenState for `screenId`, if known. */
|
|
321306
|
-
getScreen(screenId) {
|
|
321307
|
-
return this.screens.get(screenId);
|
|
321308
|
-
}
|
|
321309
|
-
/** Number of distinct screens observed this run. */
|
|
321310
|
-
getScreenCount() {
|
|
321311
|
-
return this.screens.size;
|
|
321312
|
-
}
|
|
321313
|
-
/** Number of screens backed by a live structural driver observation. */
|
|
321314
|
-
getVisitedScreenCount() {
|
|
321315
|
-
let total = 0;
|
|
321316
|
-
for (const state2 of this.screens.values()) {
|
|
321317
|
-
if (state2.visited)
|
|
321318
|
-
total += 1;
|
|
321319
|
-
}
|
|
321320
|
-
return total;
|
|
321321
|
-
}
|
|
321322
|
-
/** Total elements observed across all known screens (latest snapshot each). */
|
|
321323
|
-
getTotalElementCount() {
|
|
321324
|
-
let total = 0;
|
|
321325
|
-
for (const state2 of this.screens.values()) {
|
|
321326
|
-
total += state2.elements.length;
|
|
321327
|
-
}
|
|
321328
|
-
return total;
|
|
321329
|
-
}
|
|
321330
|
-
/**
|
|
321331
|
-
* Frontier screen-ids ranked by recency (most recently discovered first),
|
|
321332
|
-
* excluding any screen already visited. Mirrors the web `getRankedFrontier`
|
|
321333
|
-
* intent (filter visited, rank by recency) — without URL prefix similarity,
|
|
321334
|
-
* which has no mobile analog.
|
|
321335
|
-
*/
|
|
321336
|
-
getFrontier() {
|
|
321337
|
-
return [...this.frontier.values()].filter((item) => {
|
|
321338
|
-
const state2 = this.screens.get(item.screenId);
|
|
321339
|
-
return !state2 || !state2.visited;
|
|
321340
|
-
}).sort((a, b) => b.discoveredAtIteration - a.discoveredAtIteration).map((item) => item.screenId);
|
|
321341
|
-
}
|
|
321342
|
-
/** Safe navigation candidates not yet attempted, deduped across shared chrome. */
|
|
321343
|
-
getUnexploredSafeNavigationCandidates() {
|
|
321344
|
-
const authenticatedProductObserved = [...this.screens.values()].some((screen) => screen.requiresAuth === true);
|
|
321345
|
-
const candidates = [];
|
|
321346
|
-
for (const [screenId, controls] of this.safeControlsByScreen) {
|
|
321347
|
-
const origin = this.screens.get(screenId);
|
|
321348
|
-
const originIsConfirmedPublicEntry = origin?.authProvenance === "public" || !origin?.authProvenance && origin?.requiresAuth !== true && /^(?:AUTH|LANDING)$/i.test(origin?.screenType ?? "");
|
|
321349
|
-
if (authenticatedProductObserved && originIsConfirmedPublicEntry)
|
|
321350
|
-
continue;
|
|
321351
|
-
for (const control of controls.values()) {
|
|
321352
|
-
const alias = semanticControlAlias(control.label);
|
|
321353
|
-
const coverageIdentity = control.sharedCoverageIdentity ?? localControlCoverageIdentity(screenId, control.identity);
|
|
321354
|
-
if (this.attemptedSafeControlCoverageIdentities.has(coverageIdentity) || this.attemptedSafeControlCoverageIdentities.has(localControlCoverageIdentity(screenId, control.identity)))
|
|
321355
|
-
continue;
|
|
321356
|
-
const existing = candidates.find((candidate) => candidate.identity === coverageIdentity || candidate.alias === alias && candidate.screenIds.has(screenId));
|
|
321357
|
-
if (existing) {
|
|
321358
|
-
existing.screenIds.add(screenId);
|
|
321359
|
-
} else {
|
|
321360
|
-
candidates.push({
|
|
321361
|
-
identity: coverageIdentity,
|
|
321362
|
-
label: control.label,
|
|
321363
|
-
kind: control.kind,
|
|
321364
|
-
alias,
|
|
321365
|
-
screenIds: /* @__PURE__ */ new Set([screenId])
|
|
321366
|
-
});
|
|
321367
|
-
}
|
|
321368
|
-
}
|
|
321369
|
-
}
|
|
321370
|
-
return candidates.map((candidate) => ({
|
|
321371
|
-
identity: candidate.identity,
|
|
321372
|
-
label: candidate.label,
|
|
321373
|
-
kind: candidate.kind,
|
|
321374
|
-
screenIds: [...candidate.screenIds]
|
|
321375
|
-
})).sort((a, b) => {
|
|
321376
|
-
const aCurrent = this.currentScreenId !== null && a.screenIds.includes(this.currentScreenId);
|
|
321377
|
-
const bCurrent = this.currentScreenId !== null && b.screenIds.includes(this.currentScreenId);
|
|
321378
|
-
if (aCurrent !== bCurrent)
|
|
321379
|
-
return aCurrent ? -1 : 1;
|
|
321380
|
-
return b.screenIds.length - a.screenIds.length;
|
|
321381
|
-
});
|
|
321382
|
-
}
|
|
321383
|
-
/** Harness-owned survey completion floor, independent of model assertions. */
|
|
321384
|
-
getSurveyBreadthCoverage() {
|
|
321385
|
-
const unvisitedScreenIds = this.getFrontier();
|
|
321386
|
-
const unexploredControls = this.getUnexploredSafeNavigationCandidates();
|
|
321387
|
-
const complete = unvisitedScreenIds.length === 0 && unexploredControls.length === 0;
|
|
321388
|
-
const missing = [];
|
|
321389
|
-
if (unvisitedScreenIds.length > 0) {
|
|
321390
|
-
missing.push(`${unvisitedScreenIds.length} uncaptured screen(s)`);
|
|
321391
|
-
}
|
|
321392
|
-
if (unexploredControls.length > 0) {
|
|
321393
|
-
missing.push(`${unexploredControls.length} safe navigation control(s)`);
|
|
321394
|
-
}
|
|
321395
|
-
return {
|
|
321396
|
-
required: true,
|
|
321397
|
-
complete,
|
|
321398
|
-
unvisitedScreenIds,
|
|
321399
|
-
unexploredControls,
|
|
321400
|
-
attemptedControlCount: this.successfulSafeControlAttemptCount,
|
|
321401
|
-
...missing.length > 0 ? { reason: `Missing ${missing.join(" and ")}.` } : {}
|
|
321402
|
-
};
|
|
321403
|
-
}
|
|
321404
|
-
/**
|
|
321405
|
-
* Monotonic structural progress token for adaptive survey leases. Frontier
|
|
321406
|
-
* cardinality alone is insufficient: attempting control A can reveal control
|
|
321407
|
-
* B and leave the remaining count unchanged even though real work happened.
|
|
321408
|
-
*/
|
|
321409
|
-
getSurveyBreadthProgressMark() {
|
|
321410
|
-
const discoveredControlIdentities = /* @__PURE__ */ new Set();
|
|
321411
|
-
for (const [screenId, controls] of this.safeControlsByScreen) {
|
|
321412
|
-
for (const control of controls.values()) {
|
|
321413
|
-
discoveredControlIdentities.add(control.sharedCoverageIdentity ?? localControlCoverageIdentity(screenId, control.identity));
|
|
321414
|
-
}
|
|
321415
|
-
}
|
|
321416
|
-
return [
|
|
321417
|
-
`screens:${this.screens.size}`,
|
|
321418
|
-
`attempts:${this.successfulSafeControlAttemptCount}`,
|
|
321419
|
-
`controls:${discoveredControlIdentities.size}`,
|
|
321420
|
-
`transitions:${this.transitions.length}`
|
|
321421
|
-
].join("|");
|
|
321422
|
-
}
|
|
321423
|
-
resolveSemanticNavigationFamilyId(groupKey, labels) {
|
|
321424
|
-
const families = this.semanticNavigationFamilies.get(groupKey) ?? [];
|
|
321425
|
-
let best;
|
|
321426
|
-
let bestIntersection = 0;
|
|
321427
|
-
for (const family of families) {
|
|
321428
|
-
let intersection = 0;
|
|
321429
|
-
for (const label of labels) {
|
|
321430
|
-
if (family.labels.has(label))
|
|
321431
|
-
intersection += 1;
|
|
321432
|
-
}
|
|
321433
|
-
const overlap = intersection / Math.min(labels.size, family.labels.size);
|
|
321434
|
-
if (intersection >= 2 && overlap >= 0.6 && intersection > bestIntersection) {
|
|
321435
|
-
best = family;
|
|
321436
|
-
bestIntersection = intersection;
|
|
321437
|
-
}
|
|
321438
|
-
}
|
|
321439
|
-
if (best) {
|
|
321440
|
-
for (const label of labels)
|
|
321441
|
-
best.labels.add(label);
|
|
321442
|
-
return best.id;
|
|
321443
|
-
}
|
|
321444
|
-
const created = {
|
|
321445
|
-
id: `semantic-nav-family-${this.nextSemanticNavigationFamilyId++}`,
|
|
321446
|
-
labels: new Set(labels)
|
|
321447
|
-
};
|
|
321448
|
-
families.push(created);
|
|
321449
|
-
this.semanticNavigationFamilies.set(groupKey, families);
|
|
321450
|
-
return created.id;
|
|
321451
|
-
}
|
|
321452
|
-
/** Observed transitions (defensive copy). */
|
|
321453
|
-
getTransitions() {
|
|
321454
|
-
return [...this.transitions];
|
|
321455
|
-
}
|
|
321456
|
-
// -------------------------------------------------------------------------
|
|
321457
|
-
// Stale-progress detection (ported semantics from the web stale guard)
|
|
321458
|
-
// -------------------------------------------------------------------------
|
|
321459
|
-
/**
|
|
321460
|
-
* Progress mark: distinct screens weighted heavily, plus total elements seen.
|
|
321461
|
-
* A run advances this mark whenever it finds a new screen OR new elements;
|
|
321462
|
-
* the stale detector triggers when the mark stops advancing for
|
|
321463
|
-
* `staleThreshold` consecutive iterations.
|
|
321464
|
-
*/
|
|
321465
|
-
getProgressMark() {
|
|
321466
|
-
return this.screens.size * 1e3 + this.transitions.length * 100 + this.successfulSafeControlAttemptCount * 10 + this.getTotalElementCount();
|
|
321467
|
-
}
|
|
321468
|
-
updateStaleProgress() {
|
|
321469
|
-
const mark = this.getProgressMark();
|
|
321470
|
-
if (mark > this.lastProgressMark) {
|
|
321471
|
-
this.lastProgressMark = mark;
|
|
321472
|
-
this.staleIterations = 0;
|
|
321473
|
-
} else {
|
|
321474
|
-
this.staleIterations += 1;
|
|
321475
|
-
}
|
|
321476
|
-
}
|
|
321477
|
-
/** Consecutive no-progress iterations observed so far. */
|
|
321478
|
-
getStaleIterations() {
|
|
321479
|
-
return this.staleIterations;
|
|
321480
|
-
}
|
|
321481
|
-
/**
|
|
321482
|
-
* True when the run has gone `staleThreshold` consecutive iterations with no
|
|
321483
|
-
* new screen and no new element — the signal the explorer uses to nudge or
|
|
321484
|
-
* finish (mirrors the web explorer's stale guard).
|
|
321485
|
-
*/
|
|
321486
|
-
isStaleProgress() {
|
|
321487
|
-
return this.staleIterations >= this.staleThreshold;
|
|
321488
|
-
}
|
|
321489
|
-
// -------------------------------------------------------------------------
|
|
321490
|
-
// Planner evidence export
|
|
321491
|
-
// -------------------------------------------------------------------------
|
|
321492
|
-
/**
|
|
321493
|
-
* Snapshot the screen-graph as a plain object (frozen mobile-types
|
|
321494
|
-
* `MobileScreenGraph`). Defensive copies so callers cannot mutate internal
|
|
321495
|
-
* state.
|
|
321496
|
-
*/
|
|
321497
|
-
toScreenGraph() {
|
|
321498
|
-
const screens = /* @__PURE__ */ new Map();
|
|
321499
|
-
for (const [id, state2] of this.screens) {
|
|
321500
|
-
screens.set(id, {
|
|
321501
|
-
...state2,
|
|
321502
|
-
elements: [...state2.elements],
|
|
321503
|
-
observations: state2.observations ? [...state2.observations] : void 0,
|
|
321504
|
-
sessionState: state2.sessionState ? { ...state2.sessionState } : void 0
|
|
321505
|
-
});
|
|
321506
|
-
}
|
|
321507
|
-
return {
|
|
321508
|
-
screens,
|
|
321509
|
-
transitions: this.transitions.map((t) => ({
|
|
321510
|
-
fromScreenId: t.fromScreenId,
|
|
321511
|
-
toScreenId: t.toScreenId,
|
|
321512
|
-
viaRef: t.viaRef,
|
|
321513
|
-
viaControlIdentity: t.viaControlIdentity,
|
|
321514
|
-
action: t.action
|
|
321515
|
-
}))
|
|
321516
|
-
};
|
|
321517
|
-
}
|
|
321518
|
-
/**
|
|
321519
|
-
* Build the planner evidence shape: one `CollectedPageData` per screen, with
|
|
321520
|
-
* the screen-id carried in BOTH `route` (the on-the-wire positional field the
|
|
321521
|
-
* server keys screenshots by) AND the explicit `screenId` field (per the
|
|
321522
|
-
* Phase-0 `CollectedPageData.screenId` contract). `elements` are mapped from
|
|
321523
|
-
* the screen's parsed actionable elements into the platform-neutral
|
|
321524
|
-
* `CollectedPageElement[]` shape the server persists as SiteElements — without
|
|
321525
|
-
* this, mobile screens land with zero elements (coverage always 0%, no element
|
|
321526
|
-
* intelligence). `triggersNavigation` is derived from the durable source
|
|
321527
|
-
* element identity captured when the transition happened (with ref-only
|
|
321528
|
-
* fallback for legacy transitions). Insertion order matches first-seen order.
|
|
321529
|
-
*/
|
|
321530
|
-
buildInMemoryScreenMap() {
|
|
321531
|
-
const pages = [];
|
|
321532
|
-
const ordered = [...this.screens.values()].sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
321533
|
-
const navRefsByScreen = /* @__PURE__ */ new Map();
|
|
321534
|
-
const navIdentitiesByScreen = /* @__PURE__ */ new Map();
|
|
321535
|
-
for (const transition of this.transitions) {
|
|
321536
|
-
if (transition.viaControlIdentity) {
|
|
321537
|
-
const set = navIdentitiesByScreen.get(transition.fromScreenId) ?? /* @__PURE__ */ new Set();
|
|
321538
|
-
set.add(transition.viaControlIdentity);
|
|
321539
|
-
navIdentitiesByScreen.set(transition.fromScreenId, set);
|
|
321540
|
-
} else if (transition.viaRef) {
|
|
321541
|
-
const set = navRefsByScreen.get(transition.fromScreenId) ?? /* @__PURE__ */ new Set();
|
|
321542
|
-
set.add(transition.viaRef);
|
|
321543
|
-
navRefsByScreen.set(transition.fromScreenId, set);
|
|
321544
|
-
}
|
|
321545
|
-
}
|
|
321546
|
-
for (const state2 of ordered) {
|
|
321547
|
-
const page = {
|
|
321548
|
-
route: state2.screenId,
|
|
321549
|
-
screenId: state2.screenId,
|
|
321550
|
-
pageType: state2.screenType ?? "UNKNOWN",
|
|
321551
|
-
elements: (0, mobile_observation_js_1.buildCollectedElementsForScreen)(state2.elements, {
|
|
321552
|
-
navigationRefs: navRefsByScreen.get(state2.screenId),
|
|
321553
|
-
navigationIdentities: navIdentitiesByScreen.get(state2.screenId)
|
|
321554
|
-
}),
|
|
321555
|
-
requiresAuth: state2.requiresAuth ?? false
|
|
321556
|
-
};
|
|
321557
|
-
if (state2.sessionState) {
|
|
321558
|
-
page.sessionState = { ...state2.sessionState };
|
|
321559
|
-
}
|
|
321560
|
-
if (state2.screenName) {
|
|
321561
|
-
page.title = state2.screenName;
|
|
321562
|
-
}
|
|
321563
|
-
if (state2.observations && state2.observations.length > 0) {
|
|
321564
|
-
page.observations = state2.observations.map((note) => ({
|
|
321565
|
-
action: "observe",
|
|
321566
|
-
outcome: note,
|
|
321567
|
-
outcomeType: "SUCCESS"
|
|
321568
|
-
}));
|
|
321569
|
-
}
|
|
321570
|
-
pages.push(page);
|
|
321571
|
-
}
|
|
321572
|
-
return pages;
|
|
321573
|
-
}
|
|
321574
|
-
/** Alias for `buildInMemoryScreenMap()` — name parity with the planner-evidence call site. */
|
|
321575
|
-
toScreenMap() {
|
|
321576
|
-
return this.buildInMemoryScreenMap();
|
|
321577
|
-
}
|
|
321578
|
-
};
|
|
321579
|
-
exports.MobileExplorationState = MobileExplorationState;
|
|
321580
|
-
}
|
|
321581
|
-
});
|
|
321582
|
-
|
|
321583
322082
|
// ../runner-core/dist/services/mobile/mobile-discovery-agent.js
|
|
321584
322083
|
var require_mobile_discovery_agent = __commonJS({
|
|
321585
322084
|
"../runner-core/dist/services/mobile/mobile-discovery-agent.js"(exports) {
|
|
@@ -321593,6 +322092,7 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
321593
322092
|
var mobile_exploration_state_js_1 = require_mobile_exploration_state();
|
|
321594
322093
|
var mobile_discovery_auth_js_1 = require_mobile_discovery_auth();
|
|
321595
322094
|
var mobile_driver_errors_js_1 = require_mobile_driver_errors();
|
|
322095
|
+
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
321596
322096
|
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
321597
322097
|
var discover_planner_js_1 = require_discover_planner();
|
|
321598
322098
|
function checkpointErrorMessage(err) {
|
|
@@ -321875,7 +322375,7 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
|
|
|
321875
322375
|
};
|
|
321876
322376
|
}
|
|
321877
322377
|
if (isSurvey && explore.authCoverage && !explore.authCoverage.complete) {
|
|
321878
|
-
|
|
322378
|
+
log2("Mobile survey continuing to planning with incomplete authenticated coverage: " + (explore.authCoverage.reason ?? "No authenticated product screen was proven."));
|
|
321879
322379
|
}
|
|
321880
322380
|
if (isSurvey && explore.breadthCoverage && !explore.breadthCoverage.complete) {
|
|
321881
322381
|
log2("Mobile survey continuing to planning with residual breadth gaps: " + (explore.breadthCoverage.reason ?? "Unvisited screens or safe navigation controls remain."));
|
|
@@ -321893,7 +322393,7 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
|
|
|
321893
322393
|
const graph = state2.toScreenGraph();
|
|
321894
322394
|
const screenMap = state2.buildInMemoryScreenMap();
|
|
321895
322395
|
const testBudget = (0, discover_planner_js_1.computeTestBudget)(explore.exploreStats.pagesVisited, ctx.focusAreas);
|
|
321896
|
-
const planResult = await runPlan({
|
|
322396
|
+
const planResult = await (0, mobile_session_keepalive_js_1.withMobileSessionKeepAlive)(driver, () => runPlan({
|
|
321897
322397
|
graph,
|
|
321898
322398
|
mode,
|
|
321899
322399
|
projectId: ctx.projectId,
|
|
@@ -321917,7 +322417,7 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
|
|
|
321917
322417
|
}, {
|
|
321918
322418
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
321919
322419
|
onAICall
|
|
321920
|
-
});
|
|
322420
|
+
}));
|
|
321921
322421
|
logs2.push(...planResult.logs);
|
|
321922
322422
|
log2(`
|
|
321923
322423
|
Plan: ${planResult.scenarios.length} scenarios, ${planResult.featureGroups.length} feature groups (budget: ${testBudget})`);
|
|
@@ -322136,6 +322636,9 @@ Fatal error: ${msg}`);
|
|
|
322136
322636
|
if (explore.breadthCoverage && !explore.breadthCoverage.complete) {
|
|
322137
322637
|
warnings.push("Discovery retained the captured mobile screen graph with residual navigation gaps: " + (explore.breadthCoverage.reason ?? "some observed controls could not be revisited."));
|
|
322138
322638
|
}
|
|
322639
|
+
if (explore.authCoverage && !explore.authCoverage.complete) {
|
|
322640
|
+
warnings.push("Discovery retained the captured mobile screen graph with incomplete authenticated coverage: " + (explore.authCoverage.reason ?? "no authenticated product screen was proven."));
|
|
322641
|
+
}
|
|
322139
322642
|
if (explore.truncated) {
|
|
322140
322643
|
const reasonLabel = explore.truncatedReason === "rate_limit" ? "AI rate-limit" : explore.truncatedReason === "timeout" ? "AI timeout" : "AI error";
|
|
322141
322644
|
warnings.push(`Discovery may be incomplete: exploration aborted by ${reasonLabel}. Re-run after the limit clears for a complete screen graph.`);
|
|
@@ -322772,6 +323275,7 @@ Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.leng
|
|
|
322772
323275
|
if (surveyAuthRecipe) {
|
|
322773
323276
|
logs2.push(`Auth-prologue: captured + verified login recipe ${surveyAuthRecipe.recipeHash} during survey \u2014 persisting for reuse on later runs`);
|
|
322774
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;
|
|
322775
323279
|
return {
|
|
322776
323280
|
observedOrigins: mergeObservedOrigins(exploreResult.observedOrigins, loginObservedOrigins),
|
|
322777
323281
|
resolvedAppOrigin: surveyAuthRecipe?.appOrigin ?? void 0,
|
|
@@ -322787,7 +323291,8 @@ Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.leng
|
|
|
322787
323291
|
exploreTruncatedReason: exploreResult.truncatedReason,
|
|
322788
323292
|
fallbackUsed: planResult.fallbackUsed ?? false,
|
|
322789
323293
|
fallbackReason: planResult.fallbackReason,
|
|
322790
|
-
|
|
323294
|
+
error: plannerFailedError,
|
|
323295
|
+
featureGroups: plannerFailedError ? void 0 : featureGroups,
|
|
322791
323296
|
apiCalls: discoveryApiCalls.length > 0 ? discoveryApiCalls : void 0,
|
|
322792
323297
|
healthObservations: surveyHealth,
|
|
322793
323298
|
exploreFinalMessages: exploreResult.finalMessages,
|
|
@@ -336645,7 +337150,7 @@ var require_package4 = __commonJS({
|
|
|
336645
337150
|
"package.json"(exports, module) {
|
|
336646
337151
|
module.exports = {
|
|
336647
337152
|
name: "@validate.qa/runner",
|
|
336648
|
-
version: "1.0.
|
|
337153
|
+
version: "1.0.40",
|
|
336649
337154
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
336650
337155
|
bin: {
|
|
336651
337156
|
"validate-runner": "dist/cli.js"
|
|
@@ -339903,8 +340408,9 @@ async function activateTargetApp(sessionId, platform3, appId) {
|
|
|
339903
340408
|
const args = platform3 === "IOS" ? [{ bundleId: appId }] : [{ appId }];
|
|
339904
340409
|
await executeMobileScript(sessionId, "mobile: activateApp", args);
|
|
339905
340410
|
}
|
|
340411
|
+
var APP_TERMINATE_TIMEOUT_MS = 5e3;
|
|
339906
340412
|
async function terminateTargetApp(sessionId, platform3, appId) {
|
|
339907
|
-
const args = platform3 === "IOS" ? [{ bundleId: appId }] : [{ appId }];
|
|
340413
|
+
const args = platform3 === "IOS" ? [{ bundleId: appId, timeout: APP_TERMINATE_TIMEOUT_MS }] : [{ appId, timeout: APP_TERMINATE_TIMEOUT_MS }];
|
|
339908
340414
|
await executeMobileScript(sessionId, "mobile: terminateApp", args);
|
|
339909
340415
|
}
|
|
339910
340416
|
async function freshLaunchTargetApp(sessionId, platform3, appId, log2) {
|
|
@@ -342915,7 +343421,11 @@ var MobileBridgeDriver = class {
|
|
|
342915
343421
|
async relaunch() {
|
|
342916
343422
|
await this.withSessionRecovery("app relaunch", async () => {
|
|
342917
343423
|
const args = this.appLifecycleArgs();
|
|
342918
|
-
|
|
343424
|
+
try {
|
|
343425
|
+
await this.executeScript("mobile: terminateApp", [{ ...args, timeout: 5e3 }]);
|
|
343426
|
+
} catch (err) {
|
|
343427
|
+
if (isRecoverableAppiumSessionFailure(err)) throw err;
|
|
343428
|
+
}
|
|
342919
343429
|
await this.executeScript("mobile: activateApp", [args]);
|
|
342920
343430
|
try {
|
|
342921
343431
|
await waitForColdBootSettle(this.sessionPath);
|
|
@@ -345460,10 +345970,22 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345460
345970
|
onPlanReady
|
|
345461
345971
|
});
|
|
345462
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
|
+
]);
|
|
345463
345981
|
const activeCapture = capture;
|
|
345464
345982
|
if (activeCapture) {
|
|
345465
345983
|
try {
|
|
345466
|
-
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;
|
|
345467
345989
|
const captureHealth = activeCapture.health();
|
|
345468
345990
|
if (discoveryResult) {
|
|
345469
345991
|
discoveryResult.apiCalls = apiCalls.length ? attachMobileDiscoveryNetworkContext(apiCalls, discoveryResult, session?.appId ?? mobileTarget.appId) : void 0;
|
|
@@ -345484,7 +346006,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345484
346006
|
if (session) {
|
|
345485
346007
|
detachMirrorSession(session.deviceId);
|
|
345486
346008
|
try {
|
|
345487
|
-
await session.stop();
|
|
346009
|
+
await timeboxed(session.stop());
|
|
345488
346010
|
} catch {
|
|
345489
346011
|
}
|
|
345490
346012
|
}
|