@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.js
CHANGED
|
@@ -270,7 +270,8 @@ var require_ai_provider = __commonJS({
|
|
|
270
270
|
{ id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", provider: "anthropic", description: "Anthropic - fast & intelligent, 200k ctx", contextWindow: 2e5 },
|
|
271
271
|
{ id: "nvidia-nemotron-nano", label: "NVIDIA Nemotron Nano", provider: "nvidia", description: "NVIDIA NIM - Nemotron 30B Nano, reasoning model, 128k ctx", contextWindow: 128e3 },
|
|
272
272
|
{ id: "nvidia-kimi-k2.6", label: "NVIDIA Kimi K2.6", provider: "nvidia", description: "NVIDIA NIM - Moonshot Kimi K2.6, multimodal coding and agentic model, 256k ctx", contextWindow: 256e3 },
|
|
273
|
-
{ id: "deepseek-v4-flash", label: "DeepSeek V4 Flash", provider: "deepseek", description: "DeepSeek - V4 Flash, OpenAI-compatible, 1M ctx", contextWindow: 1e6 }
|
|
273
|
+
{ id: "deepseek-v4-flash", label: "DeepSeek V4 Flash", provider: "deepseek", description: "DeepSeek - V4 Flash, OpenAI-compatible, 1M ctx", contextWindow: 1e6 },
|
|
274
|
+
{ id: "deepseek-v4-pro", label: "DeepSeek V4 Pro", provider: "deepseek", description: "DeepSeek - V4 Pro, higher-quality reasoning tier, OpenAI-compatible, 1M ctx", contextWindow: 1e6 }
|
|
274
275
|
];
|
|
275
276
|
var PROVIDER_DEFAULT_MODEL = {
|
|
276
277
|
openai: "gpt-4o",
|
|
@@ -330,7 +331,8 @@ var require_ai_provider = __commonJS({
|
|
|
330
331
|
"claude-sonnet-4-6": { strong: "claude-sonnet-4-6", fast: "claude-sonnet-4-6", agent: "claude-sonnet-4-6" },
|
|
331
332
|
"nvidia-nemotron-nano": { strong: "nvidia/nemotron-3-nano-30b-a3b", fast: "nvidia/nemotron-3-nano-30b-a3b", agent: "nvidia/nemotron-3-nano-30b-a3b" },
|
|
332
333
|
"nvidia-kimi-k2.6": { strong: "moonshotai/kimi-k2.6", fast: "moonshotai/kimi-k2.6", agent: "moonshotai/kimi-k2.6" },
|
|
333
|
-
"deepseek-v4-flash": { strong: "deepseek-v4-flash", fast: "deepseek-v4-flash", agent: "deepseek-v4-flash" }
|
|
334
|
+
"deepseek-v4-flash": { strong: "deepseek-v4-flash", fast: "deepseek-v4-flash", agent: "deepseek-v4-flash" },
|
|
335
|
+
"deepseek-v4-pro": { strong: "deepseek-v4-pro", fast: "deepseek-v4-pro", agent: "deepseek-v4-pro" }
|
|
334
336
|
};
|
|
335
337
|
var _requestModelStorage = new async_hooks_1.AsyncLocalStorage();
|
|
336
338
|
function withModelContext(modelId, fn) {
|
|
@@ -392,7 +394,7 @@ var require_ai_provider = __commonJS({
|
|
|
392
394
|
if (modelId === "nvidia/nemotron-3-nano-30b-a3b") {
|
|
393
395
|
return { chat_template_kwargs: { enable_thinking: false } };
|
|
394
396
|
}
|
|
395
|
-
if (modelId === "deepseek-v4-flash") {
|
|
397
|
+
if (modelId === "deepseek-v4-flash" || modelId === "deepseek-v4-pro") {
|
|
396
398
|
return { thinking: { type: "disabled" } };
|
|
397
399
|
}
|
|
398
400
|
return {};
|
|
@@ -3302,6 +3304,7 @@ var init_audit_pricing = __esm({
|
|
|
3302
3304
|
"MiniMax-M2.7-highspeed": { input: 0.6, output: 2.4 },
|
|
3303
3305
|
"MiniMax-M2": { input: 0.3, output: 1.2 },
|
|
3304
3306
|
"deepseek-v4-flash": { input: 0.14, output: 0.28 },
|
|
3307
|
+
"deepseek-v4-pro": { input: 0.42, output: 0.84 },
|
|
3305
3308
|
// Whisper bills per-minute, not per-token (OpenAI + Groq)
|
|
3306
3309
|
"whisper-1": null,
|
|
3307
3310
|
"whisper-large-v3": null,
|
|
@@ -5879,6 +5882,34 @@ var require_ai_queue = __commonJS({
|
|
|
5879
5882
|
}
|
|
5880
5883
|
});
|
|
5881
5884
|
|
|
5885
|
+
// ../runner-core/dist/services/mobile/mobile-session-keepalive.js
|
|
5886
|
+
var require_mobile_session_keepalive = __commonJS({
|
|
5887
|
+
"../runner-core/dist/services/mobile/mobile-session-keepalive.js"(exports2) {
|
|
5888
|
+
"use strict";
|
|
5889
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
5890
|
+
exports2.MOBILE_KEEPALIVE_INTERVAL_MS = void 0;
|
|
5891
|
+
exports2.withMobileSessionKeepAlive = withMobileSessionKeepAlive;
|
|
5892
|
+
exports2.MOBILE_KEEPALIVE_INTERVAL_MS = (() => {
|
|
5893
|
+
const raw = Number(process.env.VALIDATEQA_MOBILE_KEEPALIVE_MS);
|
|
5894
|
+
return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 12e4;
|
|
5895
|
+
})();
|
|
5896
|
+
async function withMobileSessionKeepAlive(driver, fn, intervalMs = exports2.MOBILE_KEEPALIVE_INTERVAL_MS) {
|
|
5897
|
+
if (intervalMs <= 0)
|
|
5898
|
+
return fn();
|
|
5899
|
+
const timer = setInterval(() => {
|
|
5900
|
+
void driver.windowSize().catch(() => {
|
|
5901
|
+
});
|
|
5902
|
+
}, intervalMs);
|
|
5903
|
+
timer.unref?.();
|
|
5904
|
+
try {
|
|
5905
|
+
return await fn();
|
|
5906
|
+
} finally {
|
|
5907
|
+
clearInterval(timer);
|
|
5908
|
+
}
|
|
5909
|
+
}
|
|
5910
|
+
}
|
|
5911
|
+
});
|
|
5912
|
+
|
|
5882
5913
|
// ../runner-core/dist/services/mobile/mobile-observation.js
|
|
5883
5914
|
var require_mobile_observation = __commonJS({
|
|
5884
5915
|
"../runner-core/dist/services/mobile/mobile-observation.js"(exports2) {
|
|
@@ -6213,6 +6244,689 @@ var require_mobile_observation = __commonJS({
|
|
|
6213
6244
|
}
|
|
6214
6245
|
});
|
|
6215
6246
|
|
|
6247
|
+
// ../runner-core/dist/services/mobile/mobile-exploration-state.js
|
|
6248
|
+
var require_mobile_exploration_state = __commonJS({
|
|
6249
|
+
"../runner-core/dist/services/mobile/mobile-exploration-state.js"(exports2) {
|
|
6250
|
+
"use strict";
|
|
6251
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
6252
|
+
exports2.MobileExplorationState = void 0;
|
|
6253
|
+
exports2.isDestructiveActionLabel = isDestructiveActionLabel;
|
|
6254
|
+
exports2.isDestructiveConfirmationInput = isDestructiveConfirmationInput;
|
|
6255
|
+
exports2.stableMobileControlIdentity = stableMobileControlIdentity;
|
|
6256
|
+
exports2.classifySafeMobileNavigationControl = classifySafeMobileNavigationControl;
|
|
6257
|
+
exports2.deriveScreenId = deriveScreenId;
|
|
6258
|
+
var mobile_observation_js_1 = require_mobile_observation();
|
|
6259
|
+
var DEFAULT_STALE_THRESHOLD = 4;
|
|
6260
|
+
var MAX_SCREEN_OBSERVATIONS = 50;
|
|
6261
|
+
var EDITABLE_CONTROL_ROLE_RE = /(?:android\.widget\.(?:EditText|AutoCompleteTextView)|XCUIElementType(?:TextField|SecureTextField|SearchField|TextView)|textbox|searchbox|combobox|textarea|select)$/i;
|
|
6262
|
+
var STATEFUL_CONTROL_ROLE_RE = /(?:Switch|CheckBox|Checkbox|RadioButton|radio|Slider|SeekBar|Stepper|PickerWheel|Toggle)$/i;
|
|
6263
|
+
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;
|
|
6264
|
+
var SAFE_MODAL_TRIGGER_LABEL_RE = /^(?:create|new|add|edit|invite|configure|manage|details|preview|share)$/i;
|
|
6265
|
+
var MENU_TRIGGER_LABEL_RE = /^(?:more|menu|options|actions|filter|filters|sort|columns|customize)$/i;
|
|
6266
|
+
var GENERIC_NAVIGATION_DESTINATION_LABEL_RE = /^(?:home|feed|dashboard|profile|settings|inbox|messages?|notifications?|projects?|about|privacy|terms|learn\s+more)$/i;
|
|
6267
|
+
var SYSTEM_BACK_CONTROL_LABEL_RE = /^(?:back|go\s+back|navigate\s+up|navigation\s+up|up)$/i;
|
|
6268
|
+
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;
|
|
6269
|
+
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;
|
|
6270
|
+
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;
|
|
6271
|
+
function isDestructiveActionLabel(label) {
|
|
6272
|
+
if (!label)
|
|
6273
|
+
return false;
|
|
6274
|
+
return DESTRUCTIVE_ACTION_LABEL_RE.test(normalizeControlIdentityPart(label));
|
|
6275
|
+
}
|
|
6276
|
+
function isDestructiveConfirmationInput(value) {
|
|
6277
|
+
if (!value)
|
|
6278
|
+
return false;
|
|
6279
|
+
return DESTRUCTIVE_CONFIRMATION_INPUT_RE.test(value.trim());
|
|
6280
|
+
}
|
|
6281
|
+
function normalizeControlIdentityPart(value) {
|
|
6282
|
+
return value.trim().replace(/\s+/g, " ").toLowerCase();
|
|
6283
|
+
}
|
|
6284
|
+
function semanticControlAlias(label) {
|
|
6285
|
+
return `navigation-label:${normalizeControlIdentityPart(label)}`;
|
|
6286
|
+
}
|
|
6287
|
+
function localControlCoverageIdentity(screenId, identity) {
|
|
6288
|
+
return `screen:${screenId}:${identity}`;
|
|
6289
|
+
}
|
|
6290
|
+
function storedMobileControlIdentity(candidate) {
|
|
6291
|
+
return [
|
|
6292
|
+
candidate.identity,
|
|
6293
|
+
`kind:${candidate.kind}`,
|
|
6294
|
+
`label:${normalizeControlIdentityPart(candidate.label)}`
|
|
6295
|
+
].join(":");
|
|
6296
|
+
}
|
|
6297
|
+
function isSystemBackNavigationControl(element, label) {
|
|
6298
|
+
if (SYSTEM_BACK_CONTROL_LABEL_RE.test(normalizeControlIdentityPart(label)))
|
|
6299
|
+
return true;
|
|
6300
|
+
return [element.accessibilityId, element.resourceId].filter((value) => Boolean(value?.trim())).some((value) => {
|
|
6301
|
+
const leaf = normalizeControlIdentityPart(value).split(/[/:.]/).filter(Boolean).at(-1) ?? "";
|
|
6302
|
+
return SYSTEM_BACK_CONTROL_ID_RE.test(leaf);
|
|
6303
|
+
});
|
|
6304
|
+
}
|
|
6305
|
+
function stableMobileControlIdentity(element) {
|
|
6306
|
+
const role = normalizeControlIdentityPart(element.role || "control");
|
|
6307
|
+
const accessibilityId = element.accessibilityId?.trim();
|
|
6308
|
+
if (accessibilityId)
|
|
6309
|
+
return `a11y:${role}:${normalizeControlIdentityPart(accessibilityId)}`;
|
|
6310
|
+
const resourceId = element.resourceId?.trim();
|
|
6311
|
+
if (resourceId)
|
|
6312
|
+
return `resource:${role}:${normalizeControlIdentityPart(resourceId)}`;
|
|
6313
|
+
const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
|
|
6314
|
+
if (label)
|
|
6315
|
+
return `semantic:${role}:${normalizeControlIdentityPart(label)}`;
|
|
6316
|
+
return null;
|
|
6317
|
+
}
|
|
6318
|
+
function classifySafeMobileNavigationControl(element) {
|
|
6319
|
+
if (!element.actionable || element.enabled === false)
|
|
6320
|
+
return null;
|
|
6321
|
+
const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
|
|
6322
|
+
const safetyIdentity = [label, element.accessibilityId, element.resourceId].filter(Boolean).join(" ");
|
|
6323
|
+
if (!label || UNSAFE_OR_COMMIT_LABEL_RE.test(safetyIdentity))
|
|
6324
|
+
return null;
|
|
6325
|
+
if (isSystemBackNavigationControl(element, label))
|
|
6326
|
+
return null;
|
|
6327
|
+
const role = element.role.trim();
|
|
6328
|
+
if (EDITABLE_CONTROL_ROLE_RE.test(role) || STATEFUL_CONTROL_ROLE_RE.test(role))
|
|
6329
|
+
return null;
|
|
6330
|
+
const linkLike = /(?:Link$|^a$)/i.test(role);
|
|
6331
|
+
let kind = null;
|
|
6332
|
+
if (/(?:TabBarItem|\btab\b)/i.test(role))
|
|
6333
|
+
kind = "tab";
|
|
6334
|
+
else if (/menuitem/i.test(role))
|
|
6335
|
+
kind = "menu-item";
|
|
6336
|
+
else if (linkLike && element.navigationProvenance)
|
|
6337
|
+
kind = "link";
|
|
6338
|
+
else if (linkLike)
|
|
6339
|
+
return null;
|
|
6340
|
+
else if (element.hasPopup && element.hasPopup.toLowerCase() !== "false")
|
|
6341
|
+
kind = "menu-trigger";
|
|
6342
|
+
else if (element.expanded === false)
|
|
6343
|
+
kind = "disclosure";
|
|
6344
|
+
else if (MENU_TRIGGER_LABEL_RE.test(label))
|
|
6345
|
+
kind = "menu-trigger";
|
|
6346
|
+
else if (SAFE_MODAL_TRIGGER_LABEL_RE.test(label))
|
|
6347
|
+
kind = "modal-trigger";
|
|
6348
|
+
else if (element.navigationProvenance)
|
|
6349
|
+
kind = "navigation-button";
|
|
6350
|
+
else if (GENERIC_NAVIGATION_DESTINATION_LABEL_RE.test(label))
|
|
6351
|
+
kind = "navigation-button";
|
|
6352
|
+
if (!kind)
|
|
6353
|
+
return null;
|
|
6354
|
+
const identity = stableMobileControlIdentity(element);
|
|
6355
|
+
return identity ? { identity, label, kind } : null;
|
|
6356
|
+
}
|
|
6357
|
+
function sharedMobileControlCoverageIdentity(element, candidate, semanticFamilyId) {
|
|
6358
|
+
if (!element.navigationProvenance)
|
|
6359
|
+
return void 0;
|
|
6360
|
+
const durableId = element.accessibilityId?.trim() ? `a11y:${normalizeControlIdentityPart(element.accessibilityId)}` : element.resourceId?.trim() ? `resource:${normalizeControlIdentityPart(element.resourceId)}` : "";
|
|
6361
|
+
if (!durableId && !semanticFamilyId)
|
|
6362
|
+
return void 0;
|
|
6363
|
+
return [
|
|
6364
|
+
"shared-navigation",
|
|
6365
|
+
element.navigationProvenance,
|
|
6366
|
+
!durableId && semanticFamilyId ? `family:${semanticFamilyId}` : "",
|
|
6367
|
+
candidate.kind,
|
|
6368
|
+
durableId || "semantic-only",
|
|
6369
|
+
normalizeControlIdentityPart(candidate.label)
|
|
6370
|
+
].filter(Boolean).join(":");
|
|
6371
|
+
}
|
|
6372
|
+
function mobileNavigationRegion(element, windowSize) {
|
|
6373
|
+
if (!windowSize || windowSize.width <= 0 || windowSize.height <= 0)
|
|
6374
|
+
return "unknown";
|
|
6375
|
+
const centerX = (element.bounds.x + element.bounds.width / 2) / windowSize.width;
|
|
6376
|
+
const centerY = (element.bounds.y + element.bounds.height / 2) / windowSize.height;
|
|
6377
|
+
if (centerY >= 0.72)
|
|
6378
|
+
return "bottom";
|
|
6379
|
+
if (centerY <= 0.28)
|
|
6380
|
+
return "top";
|
|
6381
|
+
if (centerX <= 0.35)
|
|
6382
|
+
return "left";
|
|
6383
|
+
if (centerX >= 0.65)
|
|
6384
|
+
return "right";
|
|
6385
|
+
return "center";
|
|
6386
|
+
}
|
|
6387
|
+
function buildSemanticNavigationGroups(elements, windowSize) {
|
|
6388
|
+
const groups = /* @__PURE__ */ new Map();
|
|
6389
|
+
for (const element of elements) {
|
|
6390
|
+
if (!element.navigationProvenance)
|
|
6391
|
+
continue;
|
|
6392
|
+
const candidate = classifySafeMobileNavigationControl(element);
|
|
6393
|
+
if (!candidate)
|
|
6394
|
+
continue;
|
|
6395
|
+
const groupKey = `${element.navigationProvenance}:${mobileNavigationRegion(element, windowSize)}`;
|
|
6396
|
+
const labels = groups.get(groupKey) ?? /* @__PURE__ */ new Set();
|
|
6397
|
+
labels.add(`${candidate.kind}:${normalizeControlIdentityPart(candidate.label)}`);
|
|
6398
|
+
groups.set(groupKey, labels);
|
|
6399
|
+
}
|
|
6400
|
+
return new Map([...groups].filter(([, labels]) => labels.size >= 2));
|
|
6401
|
+
}
|
|
6402
|
+
function deriveScreenId(signal) {
|
|
6403
|
+
const namespace = signal.activity?.trim() || signal.bundleId?.trim();
|
|
6404
|
+
return namespace ? `${namespace}#${signal.screenHash}` : signal.screenHash;
|
|
6405
|
+
}
|
|
6406
|
+
function observedElementIdentity(element) {
|
|
6407
|
+
return (0, mobile_observation_js_1.stableParsedMobileElementIdentity)(element);
|
|
6408
|
+
}
|
|
6409
|
+
function mergeObservedElements(previous, current) {
|
|
6410
|
+
const merged = [...current];
|
|
6411
|
+
const seen = new Set(current.map(observedElementIdentity));
|
|
6412
|
+
for (const element of previous) {
|
|
6413
|
+
const identity = observedElementIdentity(element);
|
|
6414
|
+
if (seen.has(identity))
|
|
6415
|
+
continue;
|
|
6416
|
+
seen.add(identity);
|
|
6417
|
+
merged.push(element);
|
|
6418
|
+
}
|
|
6419
|
+
return merged;
|
|
6420
|
+
}
|
|
6421
|
+
function sanitizeElementsForEvidence(elements) {
|
|
6422
|
+
return elements.map((element) => {
|
|
6423
|
+
const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element);
|
|
6424
|
+
return {
|
|
6425
|
+
...element,
|
|
6426
|
+
...label ? { label } : { label: void 0 },
|
|
6427
|
+
text: void 0
|
|
6428
|
+
};
|
|
6429
|
+
});
|
|
6430
|
+
}
|
|
6431
|
+
var MobileExplorationState = class {
|
|
6432
|
+
/** screenId -> ScreenState (the mobile analog of routeState). */
|
|
6433
|
+
screens = /* @__PURE__ */ new Map();
|
|
6434
|
+
/** Observed directed transitions between screens. */
|
|
6435
|
+
transitions = [];
|
|
6436
|
+
/** Transition-announced screens that do not yet have a live observation. */
|
|
6437
|
+
frontier = /* @__PURE__ */ new Map();
|
|
6438
|
+
/** Safe navigation controls observed on each screen, keyed by stable identity. */
|
|
6439
|
+
safeControlsByScreen = /* @__PURE__ */ new Map();
|
|
6440
|
+
/** Screen-scoped coverage keys plus explicitly proven shared-navigation keys. */
|
|
6441
|
+
attemptedSafeControlCoverageIdentities = /* @__PURE__ */ new Set();
|
|
6442
|
+
successfulSafeControlAttemptCount = 0;
|
|
6443
|
+
/** Repeated id-less structural navigation families, grouped by region. */
|
|
6444
|
+
semanticNavigationFamilies = /* @__PURE__ */ new Map();
|
|
6445
|
+
nextSemanticNavigationFamilyId = 1;
|
|
6446
|
+
/** The screen-id of the most recently observed screen. */
|
|
6447
|
+
currentScreenId = null;
|
|
6448
|
+
/** Latest iteration recorded (drives stale detection + firstSeen stamps). */
|
|
6449
|
+
iteration = 0;
|
|
6450
|
+
/** Stale detector: progress mark (#screens + #total elements) at last advance. */
|
|
6451
|
+
lastProgressMark = 0;
|
|
6452
|
+
/** Consecutive iterations observed with no progress. */
|
|
6453
|
+
staleIterations = 0;
|
|
6454
|
+
staleThreshold;
|
|
6455
|
+
constructor(options = {}) {
|
|
6456
|
+
this.staleThreshold = typeof options.staleThreshold === "number" && options.staleThreshold > 0 ? Math.floor(options.staleThreshold) : DEFAULT_STALE_THRESHOLD;
|
|
6457
|
+
}
|
|
6458
|
+
// -------------------------------------------------------------------------
|
|
6459
|
+
// Observation recording
|
|
6460
|
+
// -------------------------------------------------------------------------
|
|
6461
|
+
/**
|
|
6462
|
+
* Returns true when `signal` identifies a screen this run has never observed
|
|
6463
|
+
* before. Pure read — does not mutate state.
|
|
6464
|
+
*/
|
|
6465
|
+
isNewScreen(signal) {
|
|
6466
|
+
return !this.screens.has(deriveScreenId(signal));
|
|
6467
|
+
}
|
|
6468
|
+
/**
|
|
6469
|
+
* Record an observation of `screen` at `iteration`. Creates a `ScreenState`
|
|
6470
|
+
* on first sight (stamping `firstSeenIteration`), otherwise refreshes the
|
|
6471
|
+
* stored element set (latest-wins, mirroring the web reducer's PAGE_CAPTURED
|
|
6472
|
+
* element refresh). A driver observation already contains the structural UI
|
|
6473
|
+
* tree, elements, screenshot association, and screen identity, so it is
|
|
6474
|
+
* captured evidence immediately; the model's capture_screen call only adds
|
|
6475
|
+
* a friendly name/type. Returns the resolved screen-id.
|
|
6476
|
+
*/
|
|
6477
|
+
recordObservation(signal, screen, iteration) {
|
|
6478
|
+
this.iteration = iteration;
|
|
6479
|
+
const screenId = deriveScreenId(signal);
|
|
6480
|
+
const evidenceElements = sanitizeElementsForEvidence(screen.elements);
|
|
6481
|
+
const existing = this.screens.get(screenId);
|
|
6482
|
+
if (existing) {
|
|
6483
|
+
this.screens.set(screenId, {
|
|
6484
|
+
...existing,
|
|
6485
|
+
elements: mergeObservedElements(existing.elements, evidenceElements)
|
|
6486
|
+
});
|
|
6487
|
+
} else {
|
|
6488
|
+
this.screens.set(screenId, {
|
|
6489
|
+
screenId,
|
|
6490
|
+
firstSeenIteration: iteration,
|
|
6491
|
+
elements: evidenceElements,
|
|
6492
|
+
visited: true
|
|
6493
|
+
});
|
|
6494
|
+
}
|
|
6495
|
+
this.frontier.delete(screenId);
|
|
6496
|
+
const controls = this.safeControlsByScreen.get(screenId) ?? /* @__PURE__ */ new Map();
|
|
6497
|
+
const semanticNavigationGroups = buildSemanticNavigationGroups(evidenceElements, screen.windowSize);
|
|
6498
|
+
const semanticNavigationFamilyIds = /* @__PURE__ */ new Map();
|
|
6499
|
+
for (const [groupKey, labels] of semanticNavigationGroups) {
|
|
6500
|
+
semanticNavigationFamilyIds.set(groupKey, this.resolveSemanticNavigationFamilyId(groupKey, labels));
|
|
6501
|
+
}
|
|
6502
|
+
for (const element of evidenceElements) {
|
|
6503
|
+
const candidate = classifySafeMobileNavigationControl(element);
|
|
6504
|
+
if (!candidate)
|
|
6505
|
+
continue;
|
|
6506
|
+
const storedIdentity = storedMobileControlIdentity(candidate);
|
|
6507
|
+
const semanticGroupKey = element.navigationProvenance ? `${element.navigationProvenance}:${mobileNavigationRegion(element, screen.windowSize)}` : "";
|
|
6508
|
+
controls.set(storedIdentity, {
|
|
6509
|
+
...candidate,
|
|
6510
|
+
identity: storedIdentity,
|
|
6511
|
+
sharedCoverageIdentity: sharedMobileControlCoverageIdentity(element, candidate, semanticNavigationFamilyIds.get(semanticGroupKey))
|
|
6512
|
+
});
|
|
6513
|
+
}
|
|
6514
|
+
if (controls.size > 0)
|
|
6515
|
+
this.safeControlsByScreen.set(screenId, controls);
|
|
6516
|
+
this.currentScreenId = screenId;
|
|
6517
|
+
this.updateStaleProgress();
|
|
6518
|
+
return screenId;
|
|
6519
|
+
}
|
|
6520
|
+
/**
|
|
6521
|
+
* Back-compatible acknowledgement from capture_screen. Observation already
|
|
6522
|
+
* marks structural evidence visited; this call only resolves a rare pending
|
|
6523
|
+
* transition frontier and preserves callers that annotate explicitly.
|
|
6524
|
+
*/
|
|
6525
|
+
markVisited(screenId) {
|
|
6526
|
+
const state2 = this.screens.get(screenId);
|
|
6527
|
+
if (state2 && !state2.visited) {
|
|
6528
|
+
this.screens.set(screenId, { ...state2, visited: true });
|
|
6529
|
+
}
|
|
6530
|
+
this.frontier.delete(screenId);
|
|
6531
|
+
}
|
|
6532
|
+
/**
|
|
6533
|
+
* Record a confirmed tap against a safe navigation candidate. Callers invoke
|
|
6534
|
+
* this only after the driver reports success; recovery-unknown, stale-ref, and
|
|
6535
|
+
* failed actions must remain on the breadth frontier.
|
|
6536
|
+
*/
|
|
6537
|
+
noteControlAttempt(screenId, element) {
|
|
6538
|
+
if (!screenId)
|
|
6539
|
+
return false;
|
|
6540
|
+
const candidate = classifySafeMobileNavigationControl(element);
|
|
6541
|
+
const controls = this.safeControlsByScreen.get(screenId) ?? /* @__PURE__ */ new Map();
|
|
6542
|
+
if (candidate) {
|
|
6543
|
+
const storedIdentity = storedMobileControlIdentity(candidate);
|
|
6544
|
+
const existing = controls.get(storedIdentity);
|
|
6545
|
+
controls.set(storedIdentity, {
|
|
6546
|
+
...candidate,
|
|
6547
|
+
identity: storedIdentity,
|
|
6548
|
+
sharedCoverageIdentity: existing?.sharedCoverageIdentity ?? sharedMobileControlCoverageIdentity(element, candidate)
|
|
6549
|
+
});
|
|
6550
|
+
this.safeControlsByScreen.set(screenId, controls);
|
|
6551
|
+
}
|
|
6552
|
+
const tappedLabel = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
|
|
6553
|
+
if (!tappedLabel)
|
|
6554
|
+
return false;
|
|
6555
|
+
const before = this.attemptedSafeControlCoverageIdentities.size;
|
|
6556
|
+
const alias = semanticControlAlias(tappedLabel);
|
|
6557
|
+
for (const control of controls.values()) {
|
|
6558
|
+
if (semanticControlAlias(control.label) === alias) {
|
|
6559
|
+
this.attemptedSafeControlCoverageIdentities.add(localControlCoverageIdentity(screenId, control.identity));
|
|
6560
|
+
if (control.sharedCoverageIdentity) {
|
|
6561
|
+
this.attemptedSafeControlCoverageIdentities.add(control.sharedCoverageIdentity);
|
|
6562
|
+
}
|
|
6563
|
+
}
|
|
6564
|
+
}
|
|
6565
|
+
if (this.attemptedSafeControlCoverageIdentities.size > before) {
|
|
6566
|
+
this.successfulSafeControlAttemptCount += 1;
|
|
6567
|
+
this.staleIterations = 0;
|
|
6568
|
+
return true;
|
|
6569
|
+
}
|
|
6570
|
+
return false;
|
|
6571
|
+
}
|
|
6572
|
+
/** Attach a human-friendly name / coarse type to a screen (capture_screen). */
|
|
6573
|
+
annotateScreen(screenId, annotation) {
|
|
6574
|
+
const state2 = this.screens.get(screenId);
|
|
6575
|
+
if (!state2)
|
|
6576
|
+
return;
|
|
6577
|
+
this.screens.set(screenId, {
|
|
6578
|
+
...state2,
|
|
6579
|
+
screenName: annotation.screenName ?? state2.screenName,
|
|
6580
|
+
screenType: annotation.screenType ?? state2.screenType
|
|
6581
|
+
});
|
|
6582
|
+
}
|
|
6583
|
+
/** Attach structurally observed authentication provenance to a screen. */
|
|
6584
|
+
setScreenAuthEvidence(screenId, evidence) {
|
|
6585
|
+
const state2 = this.screens.get(screenId);
|
|
6586
|
+
if (!state2)
|
|
6587
|
+
return;
|
|
6588
|
+
this.screens.set(screenId, {
|
|
6589
|
+
...state2,
|
|
6590
|
+
requiresAuth: evidence.requiresAuth,
|
|
6591
|
+
authProvenance: evidence.authProvenance,
|
|
6592
|
+
sessionState: { ...evidence.sessionState }
|
|
6593
|
+
});
|
|
6594
|
+
}
|
|
6595
|
+
/** Append a free-form observation note to a screen (observations tool). */
|
|
6596
|
+
addScreenObservation(screenId, note) {
|
|
6597
|
+
const state2 = this.screens.get(screenId);
|
|
6598
|
+
if (!state2)
|
|
6599
|
+
return;
|
|
6600
|
+
const normalized = note.trim();
|
|
6601
|
+
if (!normalized)
|
|
6602
|
+
return;
|
|
6603
|
+
const current = state2.observations ?? [];
|
|
6604
|
+
if (current.some((existing) => existing.trim().toLowerCase() === normalized.toLowerCase())) {
|
|
6605
|
+
return;
|
|
6606
|
+
}
|
|
6607
|
+
const observations = [...current, normalized].slice(-MAX_SCREEN_OBSERVATIONS);
|
|
6608
|
+
this.screens.set(screenId, { ...state2, observations });
|
|
6609
|
+
}
|
|
6610
|
+
/**
|
|
6611
|
+
* Record a directed transition between two screens. Both endpoints are
|
|
6612
|
+
* ensured to exist on the frontier if not yet observed (mirrors the web
|
|
6613
|
+
* reducer adding both ends of a TRANSITION_RECORDED to the frontier). The
|
|
6614
|
+
* `toScreenId` becomes the current screen when the transition starts from the
|
|
6615
|
+
* current screen.
|
|
6616
|
+
*/
|
|
6617
|
+
recordTransition(transition) {
|
|
6618
|
+
let recordedTransition = transition;
|
|
6619
|
+
if (!transition.viaControlIdentity && transition.viaRef) {
|
|
6620
|
+
const sourceElement = this.screens.get(transition.fromScreenId)?.elements.find((element) => element.ref === transition.viaRef);
|
|
6621
|
+
if (sourceElement) {
|
|
6622
|
+
recordedTransition = {
|
|
6623
|
+
...transition,
|
|
6624
|
+
viaControlIdentity: (0, mobile_observation_js_1.stableParsedMobileElementIdentity)(sourceElement)
|
|
6625
|
+
};
|
|
6626
|
+
}
|
|
6627
|
+
}
|
|
6628
|
+
const alreadyRecorded = this.transitions.some((existing) => existing.fromScreenId === recordedTransition.fromScreenId && existing.toScreenId === recordedTransition.toScreenId && existing.action === recordedTransition.action);
|
|
6629
|
+
if (!alreadyRecorded) {
|
|
6630
|
+
this.transitions.push(recordedTransition);
|
|
6631
|
+
}
|
|
6632
|
+
for (const endpointId of [transition.fromScreenId, transition.toScreenId]) {
|
|
6633
|
+
if (!this.screens.has(endpointId) && !this.frontier.has(endpointId)) {
|
|
6634
|
+
this.frontier.set(endpointId, {
|
|
6635
|
+
screenId: endpointId,
|
|
6636
|
+
source: "transition",
|
|
6637
|
+
discoveredAtIteration: this.iteration
|
|
6638
|
+
});
|
|
6639
|
+
}
|
|
6640
|
+
}
|
|
6641
|
+
if (transition.fromScreenId === this.currentScreenId) {
|
|
6642
|
+
this.currentScreenId = transition.toScreenId;
|
|
6643
|
+
}
|
|
6644
|
+
}
|
|
6645
|
+
// -------------------------------------------------------------------------
|
|
6646
|
+
// Selectors
|
|
6647
|
+
// -------------------------------------------------------------------------
|
|
6648
|
+
/** The current screen-id (most recently observed), or null before any observation. */
|
|
6649
|
+
getCurrentScreenId() {
|
|
6650
|
+
return this.currentScreenId;
|
|
6651
|
+
}
|
|
6652
|
+
/** ScreenState for `screenId`, if known. */
|
|
6653
|
+
getScreen(screenId) {
|
|
6654
|
+
return this.screens.get(screenId);
|
|
6655
|
+
}
|
|
6656
|
+
/** Number of distinct screens observed this run. */
|
|
6657
|
+
getScreenCount() {
|
|
6658
|
+
return this.screens.size;
|
|
6659
|
+
}
|
|
6660
|
+
/** Number of screens backed by a live structural driver observation. */
|
|
6661
|
+
getVisitedScreenCount() {
|
|
6662
|
+
let total = 0;
|
|
6663
|
+
for (const state2 of this.screens.values()) {
|
|
6664
|
+
if (state2.visited)
|
|
6665
|
+
total += 1;
|
|
6666
|
+
}
|
|
6667
|
+
return total;
|
|
6668
|
+
}
|
|
6669
|
+
/** Total elements observed across all known screens (latest snapshot each). */
|
|
6670
|
+
getTotalElementCount() {
|
|
6671
|
+
let total = 0;
|
|
6672
|
+
for (const state2 of this.screens.values()) {
|
|
6673
|
+
total += state2.elements.length;
|
|
6674
|
+
}
|
|
6675
|
+
return total;
|
|
6676
|
+
}
|
|
6677
|
+
/**
|
|
6678
|
+
* Frontier screen-ids ranked by recency (most recently discovered first),
|
|
6679
|
+
* excluding any screen already visited. Mirrors the web `getRankedFrontier`
|
|
6680
|
+
* intent (filter visited, rank by recency) — without URL prefix similarity,
|
|
6681
|
+
* which has no mobile analog.
|
|
6682
|
+
*/
|
|
6683
|
+
getFrontier() {
|
|
6684
|
+
return [...this.frontier.values()].filter((item) => {
|
|
6685
|
+
const state2 = this.screens.get(item.screenId);
|
|
6686
|
+
return !state2 || !state2.visited;
|
|
6687
|
+
}).sort((a, b) => b.discoveredAtIteration - a.discoveredAtIteration).map((item) => item.screenId);
|
|
6688
|
+
}
|
|
6689
|
+
/** Safe navigation candidates not yet attempted, deduped across shared chrome. */
|
|
6690
|
+
getUnexploredSafeNavigationCandidates() {
|
|
6691
|
+
const authenticatedProductObserved = [...this.screens.values()].some((screen) => screen.requiresAuth === true);
|
|
6692
|
+
const candidates = [];
|
|
6693
|
+
for (const [screenId, controls] of this.safeControlsByScreen) {
|
|
6694
|
+
const origin = this.screens.get(screenId);
|
|
6695
|
+
const originIsConfirmedPublicEntry = origin?.authProvenance === "public" || !origin?.authProvenance && origin?.requiresAuth !== true && /^(?:AUTH|LANDING)$/i.test(origin?.screenType ?? "");
|
|
6696
|
+
if (authenticatedProductObserved && originIsConfirmedPublicEntry)
|
|
6697
|
+
continue;
|
|
6698
|
+
for (const control of controls.values()) {
|
|
6699
|
+
const alias = semanticControlAlias(control.label);
|
|
6700
|
+
const coverageIdentity = control.sharedCoverageIdentity ?? localControlCoverageIdentity(screenId, control.identity);
|
|
6701
|
+
if (this.attemptedSafeControlCoverageIdentities.has(coverageIdentity) || this.attemptedSafeControlCoverageIdentities.has(localControlCoverageIdentity(screenId, control.identity)))
|
|
6702
|
+
continue;
|
|
6703
|
+
const existing = candidates.find((candidate) => candidate.identity === coverageIdentity || candidate.alias === alias && candidate.screenIds.has(screenId));
|
|
6704
|
+
if (existing) {
|
|
6705
|
+
existing.screenIds.add(screenId);
|
|
6706
|
+
} else {
|
|
6707
|
+
candidates.push({
|
|
6708
|
+
identity: coverageIdentity,
|
|
6709
|
+
label: control.label,
|
|
6710
|
+
kind: control.kind,
|
|
6711
|
+
alias,
|
|
6712
|
+
screenIds: /* @__PURE__ */ new Set([screenId])
|
|
6713
|
+
});
|
|
6714
|
+
}
|
|
6715
|
+
}
|
|
6716
|
+
}
|
|
6717
|
+
return candidates.map((candidate) => ({
|
|
6718
|
+
identity: candidate.identity,
|
|
6719
|
+
label: candidate.label,
|
|
6720
|
+
kind: candidate.kind,
|
|
6721
|
+
screenIds: [...candidate.screenIds]
|
|
6722
|
+
})).sort((a, b) => {
|
|
6723
|
+
const aCurrent = this.currentScreenId !== null && a.screenIds.includes(this.currentScreenId);
|
|
6724
|
+
const bCurrent = this.currentScreenId !== null && b.screenIds.includes(this.currentScreenId);
|
|
6725
|
+
if (aCurrent !== bCurrent)
|
|
6726
|
+
return aCurrent ? -1 : 1;
|
|
6727
|
+
return b.screenIds.length - a.screenIds.length;
|
|
6728
|
+
});
|
|
6729
|
+
}
|
|
6730
|
+
/** Harness-owned survey completion floor, independent of model assertions. */
|
|
6731
|
+
getSurveyBreadthCoverage() {
|
|
6732
|
+
const unvisitedScreenIds = this.getFrontier();
|
|
6733
|
+
const unexploredControls = this.getUnexploredSafeNavigationCandidates();
|
|
6734
|
+
const complete = unvisitedScreenIds.length === 0 && unexploredControls.length === 0;
|
|
6735
|
+
const missing = [];
|
|
6736
|
+
if (unvisitedScreenIds.length > 0) {
|
|
6737
|
+
missing.push(`${unvisitedScreenIds.length} uncaptured screen(s)`);
|
|
6738
|
+
}
|
|
6739
|
+
if (unexploredControls.length > 0) {
|
|
6740
|
+
missing.push(`${unexploredControls.length} safe navigation control(s)`);
|
|
6741
|
+
}
|
|
6742
|
+
return {
|
|
6743
|
+
required: true,
|
|
6744
|
+
complete,
|
|
6745
|
+
unvisitedScreenIds,
|
|
6746
|
+
unexploredControls,
|
|
6747
|
+
attemptedControlCount: this.successfulSafeControlAttemptCount,
|
|
6748
|
+
...missing.length > 0 ? { reason: `Missing ${missing.join(" and ")}.` } : {}
|
|
6749
|
+
};
|
|
6750
|
+
}
|
|
6751
|
+
/**
|
|
6752
|
+
* Monotonic structural progress token for adaptive survey leases. Frontier
|
|
6753
|
+
* cardinality alone is insufficient: attempting control A can reveal control
|
|
6754
|
+
* B and leave the remaining count unchanged even though real work happened.
|
|
6755
|
+
*/
|
|
6756
|
+
getSurveyBreadthProgressMark() {
|
|
6757
|
+
const discoveredControlIdentities = /* @__PURE__ */ new Set();
|
|
6758
|
+
for (const [screenId, controls] of this.safeControlsByScreen) {
|
|
6759
|
+
for (const control of controls.values()) {
|
|
6760
|
+
discoveredControlIdentities.add(control.sharedCoverageIdentity ?? localControlCoverageIdentity(screenId, control.identity));
|
|
6761
|
+
}
|
|
6762
|
+
}
|
|
6763
|
+
return [
|
|
6764
|
+
`screens:${this.screens.size}`,
|
|
6765
|
+
`attempts:${this.successfulSafeControlAttemptCount}`,
|
|
6766
|
+
`controls:${discoveredControlIdentities.size}`,
|
|
6767
|
+
`transitions:${this.transitions.length}`
|
|
6768
|
+
].join("|");
|
|
6769
|
+
}
|
|
6770
|
+
resolveSemanticNavigationFamilyId(groupKey, labels) {
|
|
6771
|
+
const families = this.semanticNavigationFamilies.get(groupKey) ?? [];
|
|
6772
|
+
let best;
|
|
6773
|
+
let bestIntersection = 0;
|
|
6774
|
+
for (const family of families) {
|
|
6775
|
+
let intersection = 0;
|
|
6776
|
+
for (const label of labels) {
|
|
6777
|
+
if (family.labels.has(label))
|
|
6778
|
+
intersection += 1;
|
|
6779
|
+
}
|
|
6780
|
+
const overlap = intersection / Math.min(labels.size, family.labels.size);
|
|
6781
|
+
if (intersection >= 2 && overlap >= 0.6 && intersection > bestIntersection) {
|
|
6782
|
+
best = family;
|
|
6783
|
+
bestIntersection = intersection;
|
|
6784
|
+
}
|
|
6785
|
+
}
|
|
6786
|
+
if (best) {
|
|
6787
|
+
for (const label of labels)
|
|
6788
|
+
best.labels.add(label);
|
|
6789
|
+
return best.id;
|
|
6790
|
+
}
|
|
6791
|
+
const created = {
|
|
6792
|
+
id: `semantic-nav-family-${this.nextSemanticNavigationFamilyId++}`,
|
|
6793
|
+
labels: new Set(labels)
|
|
6794
|
+
};
|
|
6795
|
+
families.push(created);
|
|
6796
|
+
this.semanticNavigationFamilies.set(groupKey, families);
|
|
6797
|
+
return created.id;
|
|
6798
|
+
}
|
|
6799
|
+
/** Observed transitions (defensive copy). */
|
|
6800
|
+
getTransitions() {
|
|
6801
|
+
return [...this.transitions];
|
|
6802
|
+
}
|
|
6803
|
+
// -------------------------------------------------------------------------
|
|
6804
|
+
// Stale-progress detection (ported semantics from the web stale guard)
|
|
6805
|
+
// -------------------------------------------------------------------------
|
|
6806
|
+
/**
|
|
6807
|
+
* Progress mark: distinct screens weighted heavily, plus total elements seen.
|
|
6808
|
+
* A run advances this mark whenever it finds a new screen OR new elements;
|
|
6809
|
+
* the stale detector triggers when the mark stops advancing for
|
|
6810
|
+
* `staleThreshold` consecutive iterations.
|
|
6811
|
+
*/
|
|
6812
|
+
getProgressMark() {
|
|
6813
|
+
return this.screens.size * 1e3 + this.transitions.length * 100 + this.successfulSafeControlAttemptCount * 10 + this.getTotalElementCount();
|
|
6814
|
+
}
|
|
6815
|
+
updateStaleProgress() {
|
|
6816
|
+
const mark = this.getProgressMark();
|
|
6817
|
+
if (mark > this.lastProgressMark) {
|
|
6818
|
+
this.lastProgressMark = mark;
|
|
6819
|
+
this.staleIterations = 0;
|
|
6820
|
+
} else {
|
|
6821
|
+
this.staleIterations += 1;
|
|
6822
|
+
}
|
|
6823
|
+
}
|
|
6824
|
+
/** Consecutive no-progress iterations observed so far. */
|
|
6825
|
+
getStaleIterations() {
|
|
6826
|
+
return this.staleIterations;
|
|
6827
|
+
}
|
|
6828
|
+
/**
|
|
6829
|
+
* True when the run has gone `staleThreshold` consecutive iterations with no
|
|
6830
|
+
* new screen and no new element — the signal the explorer uses to nudge or
|
|
6831
|
+
* finish (mirrors the web explorer's stale guard).
|
|
6832
|
+
*/
|
|
6833
|
+
isStaleProgress() {
|
|
6834
|
+
return this.staleIterations >= this.staleThreshold;
|
|
6835
|
+
}
|
|
6836
|
+
// -------------------------------------------------------------------------
|
|
6837
|
+
// Planner evidence export
|
|
6838
|
+
// -------------------------------------------------------------------------
|
|
6839
|
+
/**
|
|
6840
|
+
* Snapshot the screen-graph as a plain object (frozen mobile-types
|
|
6841
|
+
* `MobileScreenGraph`). Defensive copies so callers cannot mutate internal
|
|
6842
|
+
* state.
|
|
6843
|
+
*/
|
|
6844
|
+
toScreenGraph() {
|
|
6845
|
+
const screens = /* @__PURE__ */ new Map();
|
|
6846
|
+
for (const [id, state2] of this.screens) {
|
|
6847
|
+
screens.set(id, {
|
|
6848
|
+
...state2,
|
|
6849
|
+
elements: [...state2.elements],
|
|
6850
|
+
observations: state2.observations ? [...state2.observations] : void 0,
|
|
6851
|
+
sessionState: state2.sessionState ? { ...state2.sessionState } : void 0
|
|
6852
|
+
});
|
|
6853
|
+
}
|
|
6854
|
+
return {
|
|
6855
|
+
screens,
|
|
6856
|
+
transitions: this.transitions.map((t) => ({
|
|
6857
|
+
fromScreenId: t.fromScreenId,
|
|
6858
|
+
toScreenId: t.toScreenId,
|
|
6859
|
+
viaRef: t.viaRef,
|
|
6860
|
+
viaControlIdentity: t.viaControlIdentity,
|
|
6861
|
+
action: t.action
|
|
6862
|
+
}))
|
|
6863
|
+
};
|
|
6864
|
+
}
|
|
6865
|
+
/**
|
|
6866
|
+
* Build the planner evidence shape: one `CollectedPageData` per screen, with
|
|
6867
|
+
* the screen-id carried in BOTH `route` (the on-the-wire positional field the
|
|
6868
|
+
* server keys screenshots by) AND the explicit `screenId` field (per the
|
|
6869
|
+
* Phase-0 `CollectedPageData.screenId` contract). `elements` are mapped from
|
|
6870
|
+
* the screen's parsed actionable elements into the platform-neutral
|
|
6871
|
+
* `CollectedPageElement[]` shape the server persists as SiteElements — without
|
|
6872
|
+
* this, mobile screens land with zero elements (coverage always 0%, no element
|
|
6873
|
+
* intelligence). `triggersNavigation` is derived from the durable source
|
|
6874
|
+
* element identity captured when the transition happened (with ref-only
|
|
6875
|
+
* fallback for legacy transitions). Insertion order matches first-seen order.
|
|
6876
|
+
*/
|
|
6877
|
+
buildInMemoryScreenMap() {
|
|
6878
|
+
const pages = [];
|
|
6879
|
+
const ordered = [...this.screens.values()].sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
6880
|
+
const navRefsByScreen = /* @__PURE__ */ new Map();
|
|
6881
|
+
const navIdentitiesByScreen = /* @__PURE__ */ new Map();
|
|
6882
|
+
for (const transition of this.transitions) {
|
|
6883
|
+
if (transition.viaControlIdentity) {
|
|
6884
|
+
const set = navIdentitiesByScreen.get(transition.fromScreenId) ?? /* @__PURE__ */ new Set();
|
|
6885
|
+
set.add(transition.viaControlIdentity);
|
|
6886
|
+
navIdentitiesByScreen.set(transition.fromScreenId, set);
|
|
6887
|
+
} else if (transition.viaRef) {
|
|
6888
|
+
const set = navRefsByScreen.get(transition.fromScreenId) ?? /* @__PURE__ */ new Set();
|
|
6889
|
+
set.add(transition.viaRef);
|
|
6890
|
+
navRefsByScreen.set(transition.fromScreenId, set);
|
|
6891
|
+
}
|
|
6892
|
+
}
|
|
6893
|
+
for (const state2 of ordered) {
|
|
6894
|
+
const page = {
|
|
6895
|
+
route: state2.screenId,
|
|
6896
|
+
screenId: state2.screenId,
|
|
6897
|
+
pageType: state2.screenType ?? "UNKNOWN",
|
|
6898
|
+
elements: (0, mobile_observation_js_1.buildCollectedElementsForScreen)(state2.elements, {
|
|
6899
|
+
navigationRefs: navRefsByScreen.get(state2.screenId),
|
|
6900
|
+
navigationIdentities: navIdentitiesByScreen.get(state2.screenId)
|
|
6901
|
+
}),
|
|
6902
|
+
requiresAuth: state2.requiresAuth ?? false
|
|
6903
|
+
};
|
|
6904
|
+
if (state2.sessionState) {
|
|
6905
|
+
page.sessionState = { ...state2.sessionState };
|
|
6906
|
+
}
|
|
6907
|
+
if (state2.screenName) {
|
|
6908
|
+
page.title = state2.screenName;
|
|
6909
|
+
}
|
|
6910
|
+
if (state2.observations && state2.observations.length > 0) {
|
|
6911
|
+
page.observations = state2.observations.map((note) => ({
|
|
6912
|
+
action: "observe",
|
|
6913
|
+
outcome: note,
|
|
6914
|
+
outcomeType: "SUCCESS"
|
|
6915
|
+
}));
|
|
6916
|
+
}
|
|
6917
|
+
pages.push(page);
|
|
6918
|
+
}
|
|
6919
|
+
return pages;
|
|
6920
|
+
}
|
|
6921
|
+
/** Alias for `buildInMemoryScreenMap()` — name parity with the planner-evidence call site. */
|
|
6922
|
+
toScreenMap() {
|
|
6923
|
+
return this.buildInMemoryScreenMap();
|
|
6924
|
+
}
|
|
6925
|
+
};
|
|
6926
|
+
exports2.MobileExplorationState = MobileExplorationState;
|
|
6927
|
+
}
|
|
6928
|
+
});
|
|
6929
|
+
|
|
6216
6930
|
// ../runner-core/dist/services/mobile/mobile-keyboard.js
|
|
6217
6931
|
var require_mobile_keyboard = __commonJS({
|
|
6218
6932
|
"../runner-core/dist/services/mobile/mobile-keyboard.js"(exports2) {
|
|
@@ -63929,8 +64643,10 @@ var require_mobile_explorer = __commonJS({
|
|
|
63929
64643
|
var ai_provider_js_1 = require_ai_provider();
|
|
63930
64644
|
var ai_queue_js_1 = require_ai_queue();
|
|
63931
64645
|
var ai_retry_js_1 = require_ai_retry();
|
|
64646
|
+
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
63932
64647
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
63933
64648
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
64649
|
+
var mobile_exploration_state_js_1 = require_mobile_exploration_state();
|
|
63934
64650
|
var mobile_boundary_js_1 = require_mobile_boundary();
|
|
63935
64651
|
var mobile_keyboard_js_1 = require_mobile_keyboard();
|
|
63936
64652
|
var mobile_auth_coverage_js_1 = require_mobile_auth_coverage();
|
|
@@ -64288,6 +65004,10 @@ var require_mobile_explorer = __commonJS({
|
|
|
64288
65004
|
return void 0;
|
|
64289
65005
|
return normalized;
|
|
64290
65006
|
}
|
|
65007
|
+
function destructiveActionRefusal(label, kind = "tap") {
|
|
65008
|
+
const what = kind === "confirmation" ? `Typing "${label}" is a destructive confirmation` : `Tapping "${label}" is an irreversible, high-stakes destructive action`;
|
|
65009
|
+
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.`;
|
|
65010
|
+
}
|
|
64291
65011
|
function hybridElementForEvidence(element, credentials) {
|
|
64292
65012
|
const id = safeHybridIdentity(element.id, credentials);
|
|
64293
65013
|
const name = safeHybridIdentity(element.name, credentials);
|
|
@@ -64910,7 +65630,7 @@ Max iterations: ${maxIterations}. You will be warned near the end.`;
|
|
|
64910
65630
|
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."}
|
|
64911
65631
|
4. Use mobile_relaunch for a clean, hermetic restart before testing a distinct flow that needs fresh state.
|
|
64912
65632
|
5. mobile_swipe to scroll; mobile_back to return.
|
|
64913
|
-
6. Do NOT trigger irreversible side effects
|
|
65633
|
+
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.
|
|
64914
65634
|
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.
|
|
64915
65635
|
|
|
64916
65636
|
Max iterations: ${maxIterations}. You will be warned near the end.`;
|
|
@@ -65433,7 +66153,7 @@ ${statePacket}` }
|
|
|
65433
66153
|
const requestMessageCount = messages.length;
|
|
65434
66154
|
const requestMessages = messages.slice();
|
|
65435
66155
|
const callStart = Date.now();
|
|
65436
|
-
const response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
66156
|
+
const response = await (0, mobile_session_keepalive_js_1.withMobileSessionKeepAlive)(driver, () => (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
65437
66157
|
arm();
|
|
65438
66158
|
return aiClient.chat.completions.create({
|
|
65439
66159
|
model: agentModel,
|
|
@@ -65446,7 +66166,7 @@ ${statePacket}` }
|
|
|
65446
66166
|
label: `mobile-explore-iter-${iteration}`,
|
|
65447
66167
|
deferTimeoutUntilArmed: true,
|
|
65448
66168
|
onRetry: (attempt, reason, delayMs) => log2(` \u26A0 mobile explore AI retry ${attempt} (${reason}) \u2014 waiting ${delayMs}ms`)
|
|
65449
|
-
});
|
|
66169
|
+
}));
|
|
65450
66170
|
const iterDuration = Date.now() - callStart;
|
|
65451
66171
|
if (!response?.choices?.[0]?.message) {
|
|
65452
66172
|
throw new Error(`AI explore call returned no response (queue timeout or provider overload) at iteration ${iteration}`);
|
|
@@ -65775,6 +66495,11 @@ Exploration complete: ${summary}`);
|
|
|
65775
66495
|
if (!driver.webTap) {
|
|
65776
66496
|
return { ok: false, error: "This driver does not support hybrid WEBVIEW taps." };
|
|
65777
66497
|
}
|
|
66498
|
+
const hybridTapLabel = hybridElementForEvidence(hybrid, redactionCredentials).label ?? "";
|
|
66499
|
+
if ((0, mobile_exploration_state_js_1.isDestructiveActionLabel)(hybridTapLabel)) {
|
|
66500
|
+
log2(` \u26A0 Refused destructive tap on "${hybridTapLabel}" (account/data-deletion class); noting instead.`);
|
|
66501
|
+
return { ok: false, error: destructiveActionRefusal(hybridTapLabel) };
|
|
66502
|
+
}
|
|
65778
66503
|
const authSurfaceBeforeTap2 = currentAuthSurface();
|
|
65779
66504
|
const fromScreenId2 = state2.getCurrentScreenId();
|
|
65780
66505
|
const actionResult2 = await driver.webTap(hybrid.webviewContext, hybrid.css);
|
|
@@ -65805,6 +66530,11 @@ Exploration complete: ${summary}`);
|
|
|
65805
66530
|
if (!resolved) {
|
|
65806
66531
|
return { ok: false, error: `Unknown element ref "${String(toolArgs.ref)}". Call mobile_snapshot first; refs are only valid for the latest snapshot.` };
|
|
65807
66532
|
}
|
|
66533
|
+
const nativeTapLabel = resolved.element.label ?? resolved.element.text ?? resolved.element.accessibilityId ?? "";
|
|
66534
|
+
if ((0, mobile_exploration_state_js_1.isDestructiveActionLabel)(nativeTapLabel)) {
|
|
66535
|
+
log2(` \u26A0 Refused destructive tap on "${nativeTapLabel}" (account/data-deletion class); noting instead.`);
|
|
66536
|
+
return { ok: false, error: destructiveActionRefusal(nativeTapLabel) };
|
|
66537
|
+
}
|
|
65808
66538
|
const fromScreenId = state2.getCurrentScreenId();
|
|
65809
66539
|
const boundaryBeforeAction = currentBoundary();
|
|
65810
66540
|
const startedOutsideTarget = boundaryBeforeAction.kind !== "target" && boundaryBeforeAction.kind !== "unknown";
|
|
@@ -65845,6 +66575,10 @@ Exploration complete: ${summary}`);
|
|
|
65845
66575
|
// ----- Type -----
|
|
65846
66576
|
case "mobile_type": {
|
|
65847
66577
|
const value = typeof toolArgs.value === "string" ? toolArgs.value : String(toolArgs.value ?? "");
|
|
66578
|
+
if ((0, mobile_exploration_state_js_1.isDestructiveConfirmationInput)(value)) {
|
|
66579
|
+
log2(` \u26A0 Refused destructive confirmation input "${value}"; noting instead.`);
|
|
66580
|
+
return { ok: false, error: destructiveActionRefusal(value, "confirmation") };
|
|
66581
|
+
}
|
|
65848
66582
|
const allowedLiteral = inbox ? inbox.state.email : "";
|
|
65849
66583
|
for (const [key, secret] of Object.entries(credentials)) {
|
|
65850
66584
|
if (!secret || secret.length < 4)
|
|
@@ -67511,6 +68245,7 @@ var require_discover_ai_planner = __commonJS({
|
|
|
67511
68245
|
"use strict";
|
|
67512
68246
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
67513
68247
|
exports2.SCENARIO_SYSTEM_PROMPT = void 0;
|
|
68248
|
+
exports2.getAIPlannerMaxTokens = getAIPlannerMaxTokens;
|
|
67514
68249
|
exports2.getSnapshotConfidence = getSnapshotConfidence;
|
|
67515
68250
|
exports2.compressSiteMap = compressSiteMap;
|
|
67516
68251
|
exports2.formatEvidenceForPlanner = formatEvidenceForPlanner;
|
|
@@ -67522,10 +68257,25 @@ var require_discover_ai_planner = __commonJS({
|
|
|
67522
68257
|
var ai_retry_js_1 = require_ai_retry();
|
|
67523
68258
|
var strip_json_fences_js_1 = require_strip_json_fences();
|
|
67524
68259
|
var scenario_plan_validator_js_1 = require_scenario_plan_validator();
|
|
67525
|
-
var
|
|
67526
|
-
var
|
|
68260
|
+
var GENEROUS_AI_PLANNER_MAX_TOKENS = 65536;
|
|
68261
|
+
var CONSERVATIVE_AI_PLANNER_MAX_TOKENS = 16384;
|
|
67527
68262
|
function getAIPlannerMaxTokens(modelId) {
|
|
67528
|
-
|
|
68263
|
+
switch ((0, ai_provider_js_1.getProviderForModel)(modelId)) {
|
|
68264
|
+
case "deepseek":
|
|
68265
|
+
// V4 output ceiling = 384K — verified safe
|
|
68266
|
+
case "minimax":
|
|
68267
|
+
return GENEROUS_AI_PLANNER_MAX_TOKENS;
|
|
68268
|
+
case "xai":
|
|
68269
|
+
// grok-build-0.1 ceiling unproven (Grok-Fast/3 = 16K) — DON'T risk a 400
|
|
68270
|
+
case "openai":
|
|
68271
|
+
// gpt-4o max output = 16384 → more would 400
|
|
68272
|
+
case "anthropic":
|
|
68273
|
+
// OpenAI-compat proxy — keep conservative
|
|
68274
|
+
case "nvidia":
|
|
68275
|
+
// NIM output limits vary — keep conservative
|
|
68276
|
+
default:
|
|
68277
|
+
return CONSERVATIVE_AI_PLANNER_MAX_TOKENS;
|
|
68278
|
+
}
|
|
67529
68279
|
}
|
|
67530
68280
|
var discover_planner_js_1 = require_discover_planner();
|
|
67531
68281
|
function getSnapshotConfidence(sourceKind) {
|
|
@@ -68077,7 +68827,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
68077
68827
|
4. **Multi-page journeys** - only when transitions were actually observed
|
|
68078
68828
|
5. **Bug reports** - for observed 500 errors, crashes, or broken features
|
|
68079
68829
|
6. **Steps are human-readable** - "Fill the email field", not "page.getByLabel('Email').fill()"
|
|
68080
|
-
7. **Expected outcomes are plain-English success criteria** - describe the user-visible product result, not the controls touched along the way. Steps already cover buttons, inputs, links, fields, headings, tabs, dialogs, and menus. Expected outcomes should say what the user can now do or rely on (record saved, message sent, search results filtered, validation explained, dashboard accessible). Include durable routes or exact copy only when they are the real product outcome; do NOT list "heading visible", "button visible", "search input visible", or similar element inventory unless that exact control is the behavior under test.
|
|
68830
|
+
7. **Expected outcomes are plain-English success criteria** - describe the user-visible product result, not the controls touched along the way. Steps already cover buttons, inputs, links, fields, headings, tabs, dialogs, and menus. Expected outcomes should say what the user can now do or rely on (record saved, message sent, search results filtered, validation explained, dashboard accessible). Include durable routes or exact copy only when they are the real product outcome; do NOT list "heading visible", "button visible", "search input visible", or similar element inventory unless that exact control is the behavior under test. **Outcome-strength self-check (applies to EVERY scenario type):** before emitting a scenario, ask \u2014 would every expectedOutcome still pass if the primary action did nothing and only the page and its existing chrome rendered? If yes, it is too weak. Add at least one evidence-backed post-condition that would FAIL in that no-op case: specific result content or value, a created or removed entity, a changed count or status, persisted state after reload, handled validation copy, or a concrete session signal. A route alone suffices only when navigation or redirect is itself the behavior under test; a newly revealed control suffices only when revealing that control is the behavior under test. Never invent copy to satisfy this check \u2014 Rule 1 still governs what you may assert.
|
|
68081
68831
|
8. **Auth-aware** - mark scenarios that need login. Use process.env.TEST_USER_EMAIL / PASSWORD
|
|
68082
68832
|
9. **No duplicates** - check existing test names before adding
|
|
68083
68833
|
10. **All tests start from the landing page** - every generated test navigates to \`/\` first and uses the app's own navigation (clicking links/buttons) to reach the target page. Steps must reflect this: say "Navigate to landing page and click Sign In" not "Navigate to /auth/login". Deep-linking to internal routes causes hydration failures in SPAs. \`targetPages\` lists the pages involved in the flow.
|
|
@@ -68129,7 +68879,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
68129
68879
|
**YOUR responsibility:**
|
|
68130
68880
|
- Scenario \`name\`, \`type\`, \`goal\`, \`prerequisites\`
|
|
68131
68881
|
- \`steps\` written as user-language intent ("create a new conversation", "click the Submit button to log in"), naming observed UI labels where they help describe intent
|
|
68132
|
-
- \`expectedOutcomes\`
|
|
68882
|
+
- \`expectedOutcomes\` expressing the evidence-backed product post-condition per Rule 7 (and Rule 11 for happy paths, plus Rule 12's concrete session signal for authentication happy paths); citing observed durable labels when asserting on stable signals is encouraged
|
|
68133
68883
|
|
|
68134
68884
|
**NOT your responsibility:**
|
|
68135
68885
|
- Exact Playwright selectors (\`getByRole(...)\`, \`getByText(...)\`, \`page.locator(...)\`, etc.) \u2014 live-generate picks locators against the live snapshot
|
|
@@ -68150,7 +68900,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
68150
68900
|
- GOOD specificity: "create a new conversation by sending a message to a connection established earlier in this scenario"
|
|
68151
68901
|
- BAD specificity: "click the button labeled exactly 'Send' in the bottom-right of the chat"
|
|
68152
68902
|
|
|
68153
|
-
Citing observed labels in \`expectedOutcomes\`
|
|
68903
|
+
Citing observed labels in \`expectedOutcomes\` is fine \u2014 that's asserting on stable product signals, not implementation specifics.
|
|
68154
68904
|
|
|
68155
68905
|
## Coverage Strategy
|
|
68156
68906
|
- For every form: at least 1 HAPPY_PATH (submit valid data AND verify durable result per rule 11) + 1 EDGE_CASE (empty/invalid submission)
|
|
@@ -68736,6 +69486,24 @@ Each entry is exactly one of:
|
|
|
68736
69486
|
}
|
|
68737
69487
|
async function runAIPlanner(siteMap, evidence, context, options) {
|
|
68738
69488
|
const logs2 = [];
|
|
69489
|
+
const failurePlan = (kind, reason, legacyFallback2) => {
|
|
69490
|
+
const noFallback = context.mode === "survey" && process.env.DISCOVERY_PLANNER_NO_FALLBACK === "true";
|
|
69491
|
+
if (noFallback) {
|
|
69492
|
+
logs2.push(`Planner ${kind} \u2014 NO-FALLBACK: failing survey cleanly instead of fabricating slug features (${reason})`);
|
|
69493
|
+
return {
|
|
69494
|
+
scenarios: [],
|
|
69495
|
+
featureGroups: [],
|
|
69496
|
+
reasoning: "",
|
|
69497
|
+
logs: logs2,
|
|
69498
|
+
coverageGaps: [],
|
|
69499
|
+
plannerFailed: true,
|
|
69500
|
+
plannerFailureKind: kind,
|
|
69501
|
+
fallbackReason: reason
|
|
69502
|
+
};
|
|
69503
|
+
}
|
|
69504
|
+
return { ...legacyFallback2(), fallbackReason: reason };
|
|
69505
|
+
};
|
|
69506
|
+
let extraPlannerCallUsed = false;
|
|
68739
69507
|
const compressed = compressSiteMap(siteMap);
|
|
68740
69508
|
const agentModel = options.modelOverride ? (0, ai_provider_js_1.resolveModelForTask)(options.modelOverride, "agent") : (0, ai_provider_js_1.getModel)("agent");
|
|
68741
69509
|
const plannerMaxTokens = getAIPlannerMaxTokens(agentModel);
|
|
@@ -68771,7 +69539,7 @@ Each entry is exactly one of:
|
|
|
68771
69539
|
} catch (err) {
|
|
68772
69540
|
const message = err instanceof Error ? err.message : String(err);
|
|
68773
69541
|
logs2.push(`AI planner failed after retries: ${message}`);
|
|
68774
|
-
return
|
|
69542
|
+
return failurePlan("transport", (0, ai_retry_js_1.describeAIFailure)(err), () => buildFallbackPlan(siteMap, evidence.recordedJourneys, context.mode, logs2, context.existingTestNames ?? [], context.focusAreas, context.testBudget));
|
|
68775
69543
|
}
|
|
68776
69544
|
try {
|
|
68777
69545
|
options.onAICall?.({
|
|
@@ -68790,20 +69558,104 @@ Each entry is exactly one of:
|
|
|
68790
69558
|
});
|
|
68791
69559
|
} catch {
|
|
68792
69560
|
}
|
|
68793
|
-
const
|
|
68794
|
-
|
|
69561
|
+
const choice = response?.choices?.[0];
|
|
69562
|
+
const raw = choice?.message?.content;
|
|
69563
|
+
const finishReason = (choice?.finish_reason ?? "").toString().toLowerCase();
|
|
69564
|
+
const completionTokens = response.usage?.completion_tokens;
|
|
69565
|
+
const TRUNCATION_FINISH = /* @__PURE__ */ new Set(["length", "max_tokens", "max_output_tokens", "token_limit"]);
|
|
69566
|
+
const definitelyTruncated = TRUNCATION_FINISH.has(finishReason);
|
|
69567
|
+
const atOrOverCap = completionTokens != null && completionTokens >= plannerMaxTokens;
|
|
69568
|
+
const blocked = finishReason === "content_filter" || finishReason === "tool_calls";
|
|
69569
|
+
const usage = response.usage;
|
|
69570
|
+
logs2.push(`AI planner: ${usage?.prompt_tokens ?? "?"} input, ${usage?.completion_tokens ?? "?"} output tokens (finish=${finishReason || "?"})`);
|
|
69571
|
+
const legacyFallback = () => buildFallbackPlan(siteMap, evidence.recordedJourneys, context.mode, logs2, context.existingTestNames ?? [], context.focusAreas, context.testBudget);
|
|
69572
|
+
if (blocked) {
|
|
69573
|
+
return failurePlan("blocked", "AI response was blocked (content_filter/tool_calls)", legacyFallback);
|
|
69574
|
+
}
|
|
69575
|
+
if (definitelyTruncated) {
|
|
69576
|
+
return failurePlan("truncated", "AI planner output truncated at max_tokens (finish_reason=length)", legacyFallback);
|
|
69577
|
+
}
|
|
69578
|
+
const trimmed = raw?.trim();
|
|
69579
|
+
if (!trimmed) {
|
|
68795
69580
|
logs2.push("AI planner returned empty response");
|
|
68796
|
-
return
|
|
69581
|
+
return atOrOverCap ? failurePlan("truncated", "AI planner output truncated at max_tokens (empty body at cap)", legacyFallback) : failurePlan("empty", "AI returned an empty response", legacyFallback);
|
|
68797
69582
|
}
|
|
68798
|
-
const usage = response.usage;
|
|
68799
|
-
logs2.push(`AI planner: ${usage?.prompt_tokens ?? "?"} input, ${usage?.completion_tokens ?? "?"} output tokens`);
|
|
68800
69583
|
let aiOutput;
|
|
68801
69584
|
try {
|
|
68802
|
-
aiOutput = JSON.parse((0, strip_json_fences_js_1.stripJsonFences)(
|
|
69585
|
+
aiOutput = JSON.parse((0, strip_json_fences_js_1.stripJsonFences)(trimmed));
|
|
68803
69586
|
} catch (err) {
|
|
68804
69587
|
const message = err instanceof Error ? err.message : String(err);
|
|
68805
69588
|
logs2.push(`AI planner returned invalid JSON: ${message}`);
|
|
68806
|
-
|
|
69589
|
+
if (atOrOverCap) {
|
|
69590
|
+
logs2.push("AI planner output at/over the token cap and unparseable \u2014 treating as truncated (skipping repair)");
|
|
69591
|
+
return failurePlan("truncated", "AI planner output truncated at max_tokens (unparseable at cap)", legacyFallback);
|
|
69592
|
+
}
|
|
69593
|
+
const repairPrompt = `Your previous response was not valid JSON (parser error: ${message}). Return ONLY the corrected JSON object \u2014 no prose, no markdown fences \u2014 matching the exact schema of your previous attempt.`;
|
|
69594
|
+
const repairStart = Date.now();
|
|
69595
|
+
let repairResponse = null;
|
|
69596
|
+
try {
|
|
69597
|
+
extraPlannerCallUsed = true;
|
|
69598
|
+
repairResponse = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
69599
|
+
arm();
|
|
69600
|
+
return aiClient.chat.completions.create({
|
|
69601
|
+
model: agentModel,
|
|
69602
|
+
messages: [
|
|
69603
|
+
...messages,
|
|
69604
|
+
{ role: "assistant", content: raw },
|
|
69605
|
+
{ role: "user", content: repairPrompt }
|
|
69606
|
+
],
|
|
69607
|
+
max_tokens: plannerMaxTokens,
|
|
69608
|
+
temperature: 0.3,
|
|
69609
|
+
...(0, ai_provider_js_1.getJsonObjectResponseFormat)(agentModel),
|
|
69610
|
+
...(0, ai_provider_js_1.getNoThinkingRequestOptions)(agentModel)
|
|
69611
|
+
}, { signal });
|
|
69612
|
+
}), {
|
|
69613
|
+
label: "ai-planner-repair",
|
|
69614
|
+
deferTimeoutUntilArmed: true,
|
|
69615
|
+
onRetry: (attempt, reason, delay) => logs2.push(`AI planner repair call failed (${reason}) \u2014 retry ${attempt} in ${Math.round(delay / 1e3)}s`)
|
|
69616
|
+
});
|
|
69617
|
+
} catch (repairErr) {
|
|
69618
|
+
const repairMessage = repairErr instanceof Error ? repairErr.message : String(repairErr);
|
|
69619
|
+
logs2.push(`AI planner repair failed (${repairMessage})`);
|
|
69620
|
+
}
|
|
69621
|
+
if (repairResponse) {
|
|
69622
|
+
try {
|
|
69623
|
+
options.onAICall?.({
|
|
69624
|
+
iteration: 1,
|
|
69625
|
+
groupId: `ai-planner-${context.projectId.slice(0, 8)}`,
|
|
69626
|
+
callType: "discoveryAIPlannerRepair",
|
|
69627
|
+
model: agentModel,
|
|
69628
|
+
provider: (0, ai_provider_js_1.getProviderForModel)(agentModel),
|
|
69629
|
+
durationMs: Date.now() - repairStart,
|
|
69630
|
+
promptTokens: repairResponse.usage?.prompt_tokens,
|
|
69631
|
+
completionTokens: repairResponse.usage?.completion_tokens,
|
|
69632
|
+
totalTokens: repairResponse.usage?.total_tokens,
|
|
69633
|
+
assistantMessage: repairResponse.choices[0]?.message?.content,
|
|
69634
|
+
requestMessageCount: messages.length + 2,
|
|
69635
|
+
requestMessages: [
|
|
69636
|
+
...messages,
|
|
69637
|
+
{ role: "assistant", content: raw },
|
|
69638
|
+
{ role: "user", content: repairPrompt }
|
|
69639
|
+
]
|
|
69640
|
+
});
|
|
69641
|
+
} catch {
|
|
69642
|
+
}
|
|
69643
|
+
const repairedRaw = repairResponse.choices[0]?.message?.content?.trim();
|
|
69644
|
+
if (repairedRaw) {
|
|
69645
|
+
try {
|
|
69646
|
+
aiOutput = JSON.parse((0, strip_json_fences_js_1.stripJsonFences)(repairedRaw));
|
|
69647
|
+
} catch (repairParseErr) {
|
|
69648
|
+
const repairMessage = repairParseErr instanceof Error ? repairParseErr.message : String(repairParseErr);
|
|
69649
|
+
logs2.push(`AI planner repair returned invalid JSON (${repairMessage})`);
|
|
69650
|
+
return failurePlan("malformed", "AI returned unreadable output", legacyFallback);
|
|
69651
|
+
}
|
|
69652
|
+
} else {
|
|
69653
|
+
logs2.push("AI planner repair returned empty response");
|
|
69654
|
+
return failurePlan("malformed", "AI returned unreadable output", legacyFallback);
|
|
69655
|
+
}
|
|
69656
|
+
} else {
|
|
69657
|
+
return failurePlan("malformed", "AI returned unreadable output", legacyFallback);
|
|
69658
|
+
}
|
|
68807
69659
|
}
|
|
68808
69660
|
let { scenarios, featureGroups, warnings, coverageGaps } = validateAIPlannerOutput(aiOutput, siteMap, context.existingTestNames, context.testBudget, evidence.recordedJourneys, context.mode, context.focusAreas);
|
|
68809
69661
|
logs2.push(`AI planner: ${scenarios.length} scenarios, ${featureGroups.length} feature groups (${warnings.length} warnings, ${coverageGaps.length} coverage gaps)`);
|
|
@@ -68813,7 +69665,7 @@ Each entry is exactly one of:
|
|
|
68813
69665
|
let topLevelWarning;
|
|
68814
69666
|
const totalV1 = scenarios.length + coverageGaps.length;
|
|
68815
69667
|
const rejectionRateV1 = totalV1 > 0 ? coverageGaps.length / totalV1 : 0;
|
|
68816
|
-
if (rejectionRateV1 > 0.5 && totalV1 >= 3) {
|
|
69668
|
+
if (rejectionRateV1 > 0.5 && totalV1 >= 3 && !extraPlannerCallUsed) {
|
|
68817
69669
|
logs2.push(`AI planner: ${Math.round(rejectionRateV1 * 100)}% rejection rate (${coverageGaps.length}/${totalV1}) \u2014 running self-correction retry`);
|
|
68818
69670
|
const retryContext = [
|
|
68819
69671
|
`Your previous emission produced ${coverageGaps.length} coverage gaps out of ${totalV1} scenarios (${Math.round(rejectionRateV1 * 100)}% rejection rate).`,
|
|
@@ -68832,6 +69684,7 @@ Each entry is exactly one of:
|
|
|
68832
69684
|
const retryStart = Date.now();
|
|
68833
69685
|
let retryResponse = null;
|
|
68834
69686
|
try {
|
|
69687
|
+
extraPlannerCallUsed = true;
|
|
68835
69688
|
retryResponse = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
68836
69689
|
arm();
|
|
68837
69690
|
return aiClient.chat.completions.create({
|
|
@@ -68931,6 +69784,22 @@ Each entry is exactly one of:
|
|
|
68931
69784
|
};
|
|
68932
69785
|
}
|
|
68933
69786
|
if (context.mode === "survey" && featureGroups.length === 0 && siteMap.pages.length > 0) {
|
|
69787
|
+
const hasGroupWorthyEvidence = siteMap.pages.some((p) => (p.forms?.length ?? 0) > 0 || (p.elements?.length ?? 0) > 0);
|
|
69788
|
+
const noFallback = process.env.DISCOVERY_PLANNER_NO_FALLBACK === "true";
|
|
69789
|
+
if (noFallback && !hasGroupWorthyEvidence) {
|
|
69790
|
+
return {
|
|
69791
|
+
scenarios,
|
|
69792
|
+
featureGroups: [],
|
|
69793
|
+
reasoning: aiOutput.reasoning ?? "",
|
|
69794
|
+
logs: logs2,
|
|
69795
|
+
coverageGaps,
|
|
69796
|
+
warning: topLevelWarning,
|
|
69797
|
+
fallbackUsed: false
|
|
69798
|
+
};
|
|
69799
|
+
}
|
|
69800
|
+
if (noFallback) {
|
|
69801
|
+
return failurePlan("empty_groups", "AI produced no feature groups despite discovered content", () => buildFallbackPlan(siteMap, evidence.recordedJourneys, context.mode, logs2, context.existingTestNames ?? [], context.focusAreas, context.testBudget));
|
|
69802
|
+
}
|
|
68934
69803
|
logs2.push("Survey AI returned 0 feature groups - falling back to deterministic grouping");
|
|
68935
69804
|
const fallback = buildFallbackPlan(siteMap, evidence.recordedJourneys, context.mode, logs2, context.existingTestNames ?? [], context.focusAreas, context.testBudget);
|
|
68936
69805
|
return {
|
|
@@ -70141,6 +71010,7 @@ var require_mobile_generator = __commonJS({
|
|
|
70141
71010
|
var ai_provider_js_1 = require_ai_provider();
|
|
70142
71011
|
var ai_queue_js_1 = require_ai_queue();
|
|
70143
71012
|
var ai_retry_js_1 = require_ai_retry();
|
|
71013
|
+
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
70144
71014
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
70145
71015
|
var mobile_source_parser_js_2 = require_mobile_source_parser();
|
|
70146
71016
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
@@ -70554,7 +71424,7 @@ var require_mobile_generator = __commonJS({
|
|
|
70554
71424
|
- Re-snapshot before every assertion so the ref is fresh.
|
|
70555
71425
|
- Assert the OUTCOME of the flow (the new screen / new content), not the control you just tapped.
|
|
70556
71426
|
- 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.
|
|
70557
|
-
- Do NOT trigger irreversible side effects
|
|
71427
|
+
- 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.
|
|
70558
71428
|
- 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.
|
|
70559
71429
|
- 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.
|
|
70560
71430
|
- 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.
|
|
@@ -71048,14 +71918,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
71048
71918
|
const requestMessageCount = messages.length;
|
|
71049
71919
|
const requestMessages = messages.slice();
|
|
71050
71920
|
const callStart = Date.now();
|
|
71051
|
-
const response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
71921
|
+
const response = await (0, mobile_session_keepalive_js_1.withMobileSessionKeepAlive)(driver, () => (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
71052
71922
|
arm();
|
|
71053
71923
|
return aiClient.chat.completions.create({ model: agentModel, messages, tools: GENERATE_TOOLS, tool_choice: "auto", max_tokens: 8e3 }, { signal });
|
|
71054
71924
|
}), {
|
|
71055
71925
|
label: `mobile-generate-${scenario.name.slice(0, 24)}-iter-${iteration}`,
|
|
71056
71926
|
deferTimeoutUntilArmed: true,
|
|
71057
71927
|
onRetry: (attempt, reason, delayMs) => log2(` \u26A0 mobile generate AI retry ${attempt} (${reason}) \u2014 waiting ${delayMs}ms`)
|
|
71058
|
-
});
|
|
71928
|
+
}));
|
|
71059
71929
|
const iterDuration = Date.now() - callStart;
|
|
71060
71930
|
if (!response?.choices?.[0]?.message) {
|
|
71061
71931
|
throw new Error(`AI generate call returned no response at iteration ${iteration}`);
|
|
@@ -71667,6 +72537,21 @@ ${statePacket}`, redactionCredentials) },
|
|
|
71667
72537
|
};
|
|
71668
72538
|
}
|
|
71669
72539
|
const metadata = metadataForElement(element, screenName(), platform3);
|
|
72540
|
+
const expectedNeedle = expected.trim().toLowerCase();
|
|
72541
|
+
const identityValues = [element.accessibilityId, element.resourceId].map((value) => value?.trim().toLowerCase()).filter((value) => Boolean(value));
|
|
72542
|
+
const visibleText = (element.text ?? "").trim().toLowerCase();
|
|
72543
|
+
const isSelfLabelAssertion = expectedNeedle.length > 0 && identityValues.includes(expectedNeedle) && !visibleText.includes(expectedNeedle);
|
|
72544
|
+
if (isSelfLabelAssertion) {
|
|
72545
|
+
recordAction({ type: "ASSERT_VISIBLE", selector: (0, mobile_source_parser_js_2.preferredLocator)(element), metadata });
|
|
72546
|
+
log2(` assert_text DOWNGRADED to assert_visible (tautological self-label "${expected}"): ${metadata.targetLabel}`);
|
|
72547
|
+
return {
|
|
72548
|
+
ok: true,
|
|
72549
|
+
asserted: "visible",
|
|
72550
|
+
downgraded: true,
|
|
72551
|
+
note: `"${expected}" is the element's own accessibility identity, not product copy it displays \u2014 recorded a visibility assertion instead. To prove the outcome, assert text the screen SHOWS (an error message, a status label, a field value), not the control's own name.`,
|
|
72552
|
+
target: metadata.targetLabel
|
|
72553
|
+
};
|
|
72554
|
+
}
|
|
71670
72555
|
if (hasStableId(element)) {
|
|
71671
72556
|
recordAction({ type: "ASSERT_TEXT", value: expected, selector: (0, mobile_source_parser_js_2.preferredLocator)(element), metadata });
|
|
71672
72557
|
log2(` assert_text: ${metadata.targetLabel} ~ "${expected}"`);
|
|
@@ -72360,7 +73245,13 @@ ${statePacket}`, redactionCredentials) },
|
|
|
72360
73245
|
const strengthReason = coldResetUnavailable ? `Cold app-data reset unavailable${coldResetReason ? ` (${coldResetReason})` : ""} \u2014 generation may not match cold-boot replay. ${strengthVerdict.reason}` : !verified && recording.assertCount === 0 ? "Mobile scenario finished unverified \u2014 the agent never proved the scenario outcome live, so the recording may not demonstrate the goal (e.g. a happy path that never completed). Review before promotion." : strengthVerdict.reason;
|
|
72361
73246
|
const test = {
|
|
72362
73247
|
name: compiled.name,
|
|
72363
|
-
|
|
73248
|
+
// Description priority: the model's live finish description, then the
|
|
73249
|
+
// planner's scenario goal (always a meaningful sentence describing what the
|
|
73250
|
+
// test proves), then the generic compiled placeholder. Without the goal
|
|
73251
|
+
// fallback a scenario whose finish_generate omitted a description shipped
|
|
73252
|
+
// with the contentless "<platform> native flow generated from a recorded
|
|
73253
|
+
// mobile session." string — see mobile-test-generation.ts's compiled default.
|
|
73254
|
+
description: recording.finish?.description?.trim() || scenario.goal?.trim() || compiled.description,
|
|
72364
73255
|
framework: "APPIUM",
|
|
72365
73256
|
appiumCode: compiled.appiumCode,
|
|
72366
73257
|
steps: compiled.steps,
|
|
@@ -75037,6 +75928,118 @@ var require_har_stream = __commonJS({
|
|
|
75037
75928
|
}
|
|
75038
75929
|
});
|
|
75039
75930
|
|
|
75931
|
+
// ../runner-core/dist/services/api-auth-headers.js
|
|
75932
|
+
var require_api_auth_headers = __commonJS({
|
|
75933
|
+
"../runner-core/dist/services/api-auth-headers.js"(exports2) {
|
|
75934
|
+
"use strict";
|
|
75935
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
75936
|
+
exports2.parseApiAuthHeaders = parseApiAuthHeaders;
|
|
75937
|
+
exports2.resolveApiAuthHeaders = resolveApiAuthHeaders;
|
|
75938
|
+
exports2.isAppOriginRequest = isAppOriginRequest;
|
|
75939
|
+
exports2.buildOriginScopedApiFixtures = buildOriginScopedApiFixtures;
|
|
75940
|
+
var HEADER_NAME_RE = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
|
|
75941
|
+
function hasControlChar(v) {
|
|
75942
|
+
for (let i = 0; i < v.length; i++) {
|
|
75943
|
+
const code = v.charCodeAt(i);
|
|
75944
|
+
if (code < 32 || code === 127)
|
|
75945
|
+
return true;
|
|
75946
|
+
}
|
|
75947
|
+
return false;
|
|
75948
|
+
}
|
|
75949
|
+
function parseApiAuthHeaders(raw) {
|
|
75950
|
+
if (!raw || typeof raw !== "string")
|
|
75951
|
+
return void 0;
|
|
75952
|
+
const trimmed = raw.trim();
|
|
75953
|
+
if (!trimmed)
|
|
75954
|
+
return void 0;
|
|
75955
|
+
try {
|
|
75956
|
+
const parsed = JSON.parse(trimmed);
|
|
75957
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
75958
|
+
return void 0;
|
|
75959
|
+
const headers = {};
|
|
75960
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
75961
|
+
if (typeof k === "string" && HEADER_NAME_RE.test(k) && typeof v === "string" && !hasControlChar(v)) {
|
|
75962
|
+
headers[k] = v;
|
|
75963
|
+
}
|
|
75964
|
+
}
|
|
75965
|
+
return Object.keys(headers).length > 0 ? headers : void 0;
|
|
75966
|
+
} catch {
|
|
75967
|
+
return void 0;
|
|
75968
|
+
}
|
|
75969
|
+
}
|
|
75970
|
+
function resolveApiAuthHeaders(input) {
|
|
75971
|
+
if (input.explicit)
|
|
75972
|
+
return input.explicit;
|
|
75973
|
+
if (!input.isApiTest)
|
|
75974
|
+
return void 0;
|
|
75975
|
+
return parseApiAuthHeaders(input.apiAuthHeadersVar);
|
|
75976
|
+
}
|
|
75977
|
+
function isAppOriginRequest(url, baseUrl) {
|
|
75978
|
+
if (typeof url !== "string" || !baseUrl)
|
|
75979
|
+
return false;
|
|
75980
|
+
let appOrigin;
|
|
75981
|
+
try {
|
|
75982
|
+
appOrigin = new URL(baseUrl).origin;
|
|
75983
|
+
} catch {
|
|
75984
|
+
return false;
|
|
75985
|
+
}
|
|
75986
|
+
try {
|
|
75987
|
+
return new URL(url, baseUrl).origin === appOrigin;
|
|
75988
|
+
} catch {
|
|
75989
|
+
return false;
|
|
75990
|
+
}
|
|
75991
|
+
}
|
|
75992
|
+
function buildOriginScopedApiFixtures(headers, baseUrl) {
|
|
75993
|
+
let appOrigin;
|
|
75994
|
+
try {
|
|
75995
|
+
appOrigin = baseUrl ? new URL(baseUrl).origin : null;
|
|
75996
|
+
} catch {
|
|
75997
|
+
appOrigin = null;
|
|
75998
|
+
}
|
|
75999
|
+
return `export * from '@playwright/test';
|
|
76000
|
+
import { test as base } from '@playwright/test';
|
|
76001
|
+
|
|
76002
|
+
// Standing auth headers \u2014 attached ONLY to same-origin requests so the credential never leaks to
|
|
76003
|
+
// an off-origin host. Embedded at run time; this file lives in the ephemeral run dir.
|
|
76004
|
+
const STANDING_HEADERS = ${JSON.stringify(headers)};
|
|
76005
|
+
const APP_ORIGIN = ${JSON.stringify(appOrigin)};
|
|
76006
|
+
const BASE_URL = ${JSON.stringify(baseUrl)};
|
|
76007
|
+
const METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'fetch'];
|
|
76008
|
+
|
|
76009
|
+
function sameOrigin(url) {
|
|
76010
|
+
if (!APP_ORIGIN || typeof url !== 'string') return false;
|
|
76011
|
+
try { return new URL(url, BASE_URL).origin === APP_ORIGIN; } catch { return false; }
|
|
76012
|
+
}
|
|
76013
|
+
|
|
76014
|
+
export const test = base.extend({
|
|
76015
|
+
request: async ({ request }, use) => {
|
|
76016
|
+
const scoped = new Proxy(request, {
|
|
76017
|
+
get(target, prop) {
|
|
76018
|
+
const value = target[prop];
|
|
76019
|
+
// HTTP verbs: wrap to add standing headers on same-origin requests only.
|
|
76020
|
+
if (typeof prop === 'string' && METHODS.indexOf(prop) !== -1) {
|
|
76021
|
+
const orig = value.bind(target);
|
|
76022
|
+
return (url, options) => {
|
|
76023
|
+
options = options || {};
|
|
76024
|
+
return sameOrigin(url)
|
|
76025
|
+
? orig(url, { ...options, headers: { ...STANDING_HEADERS, ...(options.headers || {}) } })
|
|
76026
|
+
: orig(url, options);
|
|
76027
|
+
};
|
|
76028
|
+
}
|
|
76029
|
+
// Every other METHOD must run with 'this' bound to the REAL context \u2014 an APIRequestContext
|
|
76030
|
+
// uses internal slots/private fields a Proxy can't forward, so calling with 'this'=proxy
|
|
76031
|
+
// would throw. Non-function props pass through untouched.
|
|
76032
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
76033
|
+
},
|
|
76034
|
+
});
|
|
76035
|
+
await use(scoped);
|
|
76036
|
+
},
|
|
76037
|
+
});
|
|
76038
|
+
`;
|
|
76039
|
+
}
|
|
76040
|
+
}
|
|
76041
|
+
});
|
|
76042
|
+
|
|
75040
76043
|
// ../runner-core/dist/services/playwright-native.js
|
|
75041
76044
|
var require_playwright_native = __commonJS({
|
|
75042
76045
|
"../runner-core/dist/services/playwright-native.js"(exports2) {
|
|
@@ -75079,7 +76082,7 @@ var require_playwright_native = __commonJS({
|
|
|
75079
76082
|
};
|
|
75080
76083
|
})();
|
|
75081
76084
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
75082
|
-
exports2.scrubHarRequestBody = void 0;
|
|
76085
|
+
exports2.scrubHarRequestBody = exports2.buildOriginScopedApiFixtures = exports2.isAppOriginRequest = exports2.resolveApiAuthHeaders = exports2.parseApiAuthHeaders = void 0;
|
|
75083
76086
|
exports2.executeTestViaNativePlaywright = executeTestViaNativePlaywright2;
|
|
75084
76087
|
exports2.parseJsonResults = parseJsonResults2;
|
|
75085
76088
|
exports2.parseHarToApiSummary = parseHarToApiSummary;
|
|
@@ -75091,6 +76094,20 @@ var require_playwright_native = __commonJS({
|
|
|
75091
76094
|
var spawn_with_tree_kill_js_1 = require_spawn_with_tree_kill();
|
|
75092
76095
|
var api_call_redaction_js_1 = require_api_call_redaction();
|
|
75093
76096
|
var har_stream_js_1 = require_har_stream();
|
|
76097
|
+
var api_auth_headers_js_1 = require_api_auth_headers();
|
|
76098
|
+
var api_auth_headers_js_2 = require_api_auth_headers();
|
|
76099
|
+
Object.defineProperty(exports2, "parseApiAuthHeaders", { enumerable: true, get: function() {
|
|
76100
|
+
return api_auth_headers_js_2.parseApiAuthHeaders;
|
|
76101
|
+
} });
|
|
76102
|
+
Object.defineProperty(exports2, "resolveApiAuthHeaders", { enumerable: true, get: function() {
|
|
76103
|
+
return api_auth_headers_js_2.resolveApiAuthHeaders;
|
|
76104
|
+
} });
|
|
76105
|
+
Object.defineProperty(exports2, "isAppOriginRequest", { enumerable: true, get: function() {
|
|
76106
|
+
return api_auth_headers_js_2.isAppOriginRequest;
|
|
76107
|
+
} });
|
|
76108
|
+
Object.defineProperty(exports2, "buildOriginScopedApiFixtures", { enumerable: true, get: function() {
|
|
76109
|
+
return api_auth_headers_js_2.buildOriginScopedApiFixtures;
|
|
76110
|
+
} });
|
|
75094
76111
|
var api_call_redaction_js_2 = require_api_call_redaction();
|
|
75095
76112
|
Object.defineProperty(exports2, "scrubHarRequestBody", { enumerable: true, get: function() {
|
|
75096
76113
|
return api_call_redaction_js_2.scrubHarRequestBody;
|
|
@@ -75100,7 +76117,7 @@ var require_playwright_native = __commonJS({
|
|
|
75100
76117
|
var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
|
|
75101
76118
|
var PW_CLI = require.resolve("@playwright/test/cli");
|
|
75102
76119
|
async function executeTestViaNativePlaywright2(options) {
|
|
75103
|
-
const { playwrightCode, baseUrl, testName, testVariables, storageStatePath, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
|
|
76120
|
+
const { playwrightCode, baseUrl, testName, testVariables, storageStatePath, extraHttpHeaders, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
|
|
75104
76121
|
const logs2 = [];
|
|
75105
76122
|
const screenshots = [];
|
|
75106
76123
|
logs2.push(`\u{1F3AD} Native Playwright starting`);
|
|
@@ -75114,6 +76131,11 @@ var require_playwright_native = __commonJS({
|
|
|
75114
76131
|
runDir = (0, path_1.join)(RUNS_DIR, (0, crypto_1.randomUUID)());
|
|
75115
76132
|
await (0, promises_1.mkdir)(runDir, { recursive: true });
|
|
75116
76133
|
const isApiTest2 = playwrightCode.includes("@api") || playwrightCode.includes("{ request }") && !playwrightCode.includes("{ page");
|
|
76134
|
+
const resolvedExtraHeaders = (0, api_auth_headers_js_1.resolveApiAuthHeaders)({
|
|
76135
|
+
explicit: extraHttpHeaders,
|
|
76136
|
+
isApiTest: isApiTest2,
|
|
76137
|
+
apiAuthHeadersVar: testVariables?.API_AUTH_HEADERS
|
|
76138
|
+
});
|
|
75117
76139
|
const cssDisableSetup = `
|
|
75118
76140
|
const __qaLiveBrowserStateFile = process.env.VALIDATE_LIVE_BROWSER_STATE_PATH;
|
|
75119
76141
|
let __qaLiveBrowserInterval: ReturnType<typeof setInterval> | null = null;
|
|
@@ -75307,8 +76329,17 @@ test.afterEach(async ({ page }, testInfo) => {
|
|
|
75307
76329
|
if (!importMatch) {
|
|
75308
76330
|
logs2.push("\u26A0 Could not find @playwright/test import - animation disabling NOT injected");
|
|
75309
76331
|
}
|
|
75310
|
-
const
|
|
76332
|
+
const useScopedApiHeaders = isApiTest2 && !!resolvedExtraHeaders && !!importMatch && !!baseUrl;
|
|
76333
|
+
let instrumentedCode;
|
|
76334
|
+
if (isApiTest2) {
|
|
76335
|
+
instrumentedCode = useScopedApiHeaders ? playwrightCode.replace(importMatch[0], importMatch[0].replace(/from\s+['"]@playwright\/test['"]/, "from './qa-api-fixtures'")) : playwrightCode;
|
|
76336
|
+
} else {
|
|
76337
|
+
instrumentedCode = importMatch ? playwrightCode.replace(importMatch[0], importMatch[0] + cssDisableSetup) : playwrightCode;
|
|
76338
|
+
}
|
|
75311
76339
|
await (0, promises_1.writeFile)((0, path_1.join)(runDir, "test.spec.ts"), instrumentedCode);
|
|
76340
|
+
if (useScopedApiHeaders && resolvedExtraHeaders) {
|
|
76341
|
+
await (0, promises_1.writeFile)((0, path_1.join)(runDir, "qa-api-fixtures.ts"), (0, api_auth_headers_js_1.buildOriginScopedApiFixtures)(resolvedExtraHeaders, baseUrl));
|
|
76342
|
+
}
|
|
75312
76343
|
const headless = process.env.PLAYWRIGHT_HEADLESS === "true" || process.env.NODE_ENV === "production" || IS_CLOUD2;
|
|
75313
76344
|
logs2.push(`\u2699\uFE0F Config: baseURL=${JSON.stringify(baseUrl)}, headless=${headless}${isApiTest2 ? ", API mode (no screenshot/video)" : ""}`);
|
|
75314
76345
|
const absTestDir = runDir.replace(/\\/g, "/");
|
|
@@ -75322,6 +76353,7 @@ test.afterEach(async ({ page }, testInfo) => {
|
|
|
75322
76353
|
const absHarPath = wantHar ? (0, path_1.join)(runDir, "session.har").replace(/\\/g, "/") : null;
|
|
75323
76354
|
const recordHarLine = absHarPath ? `recordHar: { path: ${JSON.stringify(absHarPath)}, mode: 'full' },` : "";
|
|
75324
76355
|
const storageStateLine = storageStatePath ? `storageState: ${JSON.stringify(storageStatePath.replace(/\\/g, "/"))},` : "";
|
|
76356
|
+
const extraHeadersLine = resolvedExtraHeaders && !useScopedApiHeaders && Object.keys(resolvedExtraHeaders).length > 0 ? `extraHTTPHeaders: ${JSON.stringify(resolvedExtraHeaders)},` : "";
|
|
75325
76357
|
const launchArgs = IS_CLOUD2 ? "args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-blink-features=AutomationControlled', '--disable-dev-shm-usage', '--disable-gpu', '--disable-software-rasterizer', '--no-zygote', '--disable-accelerated-2d-canvas', '--js-flags=--max-old-space-size=512']," : "args: ['--disable-blink-features=AutomationControlled'],";
|
|
75326
76358
|
const config = `
|
|
75327
76359
|
// Plain object export - no require('@playwright/test') needed.
|
|
@@ -75335,6 +76367,7 @@ module.exports = {
|
|
|
75335
76367
|
use: {
|
|
75336
76368
|
baseURL: ${JSON.stringify(baseUrl)},
|
|
75337
76369
|
${storageStateLine}
|
|
76370
|
+
${extraHeadersLine}
|
|
75338
76371
|
browserName: 'chromium',
|
|
75339
76372
|
headless: ${headless},
|
|
75340
76373
|
screenshot: ${isApiTest2 ? "'off'" : "'on'"},
|
|
@@ -75376,6 +76409,7 @@ module.exports = {
|
|
|
75376
76409
|
// code. Forwarded (not allowlisted) so it stays a per-run toggle; absent → inert.
|
|
75377
76410
|
...process.env.SHARED_AUTH_INJECT ? { SHARED_AUTH_INJECT: process.env.SHARED_AUTH_INJECT } : {}
|
|
75378
76411
|
};
|
|
76412
|
+
delete envWithIsolation.API_AUTH_HEADERS;
|
|
75379
76413
|
logs2.push("\u{1F680} Spawning playwright test...");
|
|
75380
76414
|
const stopLiveBrowserPolling = liveBrowserStatePath && liveBrowserHandle ? startLiveBrowserStatePolling(liveBrowserStatePath, liveBrowserHandle, onLiveBrowserSnapshot) : null;
|
|
75381
76415
|
let playResult;
|
|
@@ -79227,6 +80261,34 @@ var require_anchor_harvester = __commonJS({
|
|
|
79227
80261
|
}
|
|
79228
80262
|
});
|
|
79229
80263
|
|
|
80264
|
+
// ../runner-core/dist/services/discovery-lint-flags.js
|
|
80265
|
+
var require_discovery_lint_flags = __commonJS({
|
|
80266
|
+
"../runner-core/dist/services/discovery-lint-flags.js"(exports2) {
|
|
80267
|
+
"use strict";
|
|
80268
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
80269
|
+
exports2.structuralLocatorLintMode = structuralLocatorLintMode;
|
|
80270
|
+
exports2.isPwMethodPreflightEnabled = isPwMethodPreflightEnabled;
|
|
80271
|
+
exports2.isVqaLoginDedupeEnabled = isVqaLoginDedupeEnabled;
|
|
80272
|
+
function strictOn(value) {
|
|
80273
|
+
return value === "true" || value === "1";
|
|
80274
|
+
}
|
|
80275
|
+
function structuralLocatorLintMode() {
|
|
80276
|
+
const v = (process.env.DISCOVERY_STRUCTURAL_LOCATOR_LINT || "").trim().toLowerCase();
|
|
80277
|
+
if (v === "warn")
|
|
80278
|
+
return "warn";
|
|
80279
|
+
if (v === "true" || v === "1" || v === "enforce")
|
|
80280
|
+
return "enforce";
|
|
80281
|
+
return "off";
|
|
80282
|
+
}
|
|
80283
|
+
function isPwMethodPreflightEnabled() {
|
|
80284
|
+
return strictOn(process.env.DISCOVERY_PW_METHOD_PREFLIGHT);
|
|
80285
|
+
}
|
|
80286
|
+
function isVqaLoginDedupeEnabled() {
|
|
80287
|
+
return strictOn(process.env.DISCOVERY_VQALOGIN_DEDUPE);
|
|
80288
|
+
}
|
|
80289
|
+
}
|
|
80290
|
+
});
|
|
80291
|
+
|
|
79230
80292
|
// ../runner-core/dist/services/auth-recipe.js
|
|
79231
80293
|
var require_auth_recipe = __commonJS({
|
|
79232
80294
|
"../runner-core/dist/services/auth-recipe.js"(exports2) {
|
|
@@ -79249,9 +80311,11 @@ var require_auth_recipe = __commonJS({
|
|
|
79249
80311
|
exports2.extractAuthRegion = extractAuthRegion;
|
|
79250
80312
|
exports2.stripAuthRegions = stripAuthRegions;
|
|
79251
80313
|
exports2.ensureExpectImported = ensureExpectImported;
|
|
80314
|
+
exports2.ensureLoginCall = ensureLoginCall;
|
|
79252
80315
|
exports2.upsertAuthBlock = upsertAuthBlock;
|
|
79253
80316
|
exports2.buildHealAuthStamp = buildHealAuthStamp;
|
|
79254
80317
|
var crypto_1 = require("crypto");
|
|
80318
|
+
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
79255
80319
|
exports2.AUTH_MARKER_VERSION = "v1";
|
|
79256
80320
|
var START_MARKER = `// >>> VALIDATE_QA_AUTH ${exports2.AUTH_MARKER_VERSION} \u2014 generated, do not edit >>>`;
|
|
79257
80321
|
var END_MARKER = `// <<< VALIDATE_QA_AUTH ${exports2.AUTH_MARKER_VERSION} <<<`;
|
|
@@ -79515,10 +80579,13 @@ ${tail}`;
|
|
|
79515
80579
|
var TEST_OPENER_RE = /test(?:\.(?:only|skip|fixme))?\s*\(\s*(['"`])(?:[^\\]|\\.)*?\1\s*,\s*(?:\{[\s\S]*?\}\s*,\s*)?async\s*\(\s*\{[^}]*\}[^)]*\)\s*=>\s*\{/;
|
|
79516
80580
|
var LOGIN_CALL_RE = /await\s+__vqaLogin\s*\(\s*page\s*\)/;
|
|
79517
80581
|
function ensureLoginCall(code) {
|
|
79518
|
-
|
|
79519
|
-
if (matches.length === 0)
|
|
80582
|
+
if (code.match(new RegExp(TEST_OPENER_RE.source, "g")) === null)
|
|
79520
80583
|
return code;
|
|
79521
80584
|
let result = code;
|
|
80585
|
+
if ((0, discovery_lint_flags_js_1.isVqaLoginDedupeEnabled)()) {
|
|
80586
|
+
result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
|
|
80587
|
+
}
|
|
80588
|
+
const matches = [...result.matchAll(new RegExp(TEST_OPENER_RE.source, "g"))];
|
|
79522
80589
|
for (let i = matches.length - 1; i >= 0; i--) {
|
|
79523
80590
|
const m = matches[i];
|
|
79524
80591
|
if (m.index === void 0)
|
|
@@ -83410,7 +84477,7 @@ var require_discover_generator = __commonJS({
|
|
|
83410
84477
|
"../runner-core/dist/services/discover-generator.js"(exports2) {
|
|
83411
84478
|
"use strict";
|
|
83412
84479
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
83413
|
-
exports2.MIN_BUG_DESCRIPTION_LENGTH = exports2.BLOCKED_MCP_TOOLS_SCENARIO_GEN = void 0;
|
|
84480
|
+
exports2.MIN_BUG_DESCRIPTION_LENGTH = exports2.VALID_PLAYWRIGHT_GETBY_METHODS = exports2.BLOCKED_MCP_TOOLS_SCENARIO_GEN = void 0;
|
|
83414
84481
|
exports2.filterMcpToolsForScenarioGen = filterMcpToolsForScenarioGen;
|
|
83415
84482
|
exports2.detectScenarioSignals = detectScenarioSignals;
|
|
83416
84483
|
exports2.scenarioStepsCoverAuth = scenarioStepsCoverAuth;
|
|
@@ -83433,6 +84500,7 @@ var require_discover_generator = __commonJS({
|
|
|
83433
84500
|
exports2.upgradePositionalProvenLines = upgradePositionalProvenLines;
|
|
83434
84501
|
exports2.synthesizeRefActionLines = synthesizeRefActionLines;
|
|
83435
84502
|
exports2.renderSessionRecording = renderSessionRecording;
|
|
84503
|
+
exports2.scanFragileStructuralLocators = scanFragileStructuralLocators;
|
|
83436
84504
|
exports2.validateFinalActionFidelity = validateFinalActionFidelity;
|
|
83437
84505
|
exports2.resolveSuspectedBugVerdict = resolveSuspectedBugVerdict;
|
|
83438
84506
|
exports2.buildScenarioSystemPrompt = buildScenarioSystemPrompt;
|
|
@@ -83448,6 +84516,7 @@ var require_discover_generator = __commonJS({
|
|
|
83448
84516
|
var ai_provider_js_1 = require_ai_provider();
|
|
83449
84517
|
var ai_queue_js_1 = require_ai_queue();
|
|
83450
84518
|
var ai_retry_js_1 = require_ai_retry();
|
|
84519
|
+
var discover_ai_planner_js_1 = require_discover_ai_planner();
|
|
83451
84520
|
var snapshot_parser_js_1 = require_snapshot_parser();
|
|
83452
84521
|
var scrub_credentials_js_1 = require_scrub_credentials();
|
|
83453
84522
|
var exploration_parser_js_1 = require_exploration_parser();
|
|
@@ -83460,6 +84529,7 @@ var require_discover_generator = __commonJS({
|
|
|
83460
84529
|
var auth_recipe_js_1 = require_auth_recipe();
|
|
83461
84530
|
var auth_replay_js_1 = require_auth_replay();
|
|
83462
84531
|
var agentic_discovery_prompts_js_1 = require_agentic_discovery_prompts();
|
|
84532
|
+
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
83463
84533
|
exports2.BLOCKED_MCP_TOOLS_SCENARIO_GEN = /* @__PURE__ */ new Set([
|
|
83464
84534
|
"browser_install",
|
|
83465
84535
|
"browser_close",
|
|
@@ -84184,6 +85254,15 @@ ${elementContext}${formContext}${observationContext}${interactionContext}${lowCo
|
|
|
84184
85254
|
return assembled.join(separator);
|
|
84185
85255
|
}
|
|
84186
85256
|
var SENSITIVE_ENV_KEY_RE = /email|password|token|secret|api[_-]?key|access[_-]?key|username|user(name)?|bearer|session|cookie|\bpat\b|refresh|jwt/i;
|
|
85257
|
+
exports2.VALID_PLAYWRIGHT_GETBY_METHODS = /* @__PURE__ */ new Set([
|
|
85258
|
+
"getByRole",
|
|
85259
|
+
"getByText",
|
|
85260
|
+
"getByLabel",
|
|
85261
|
+
"getByPlaceholder",
|
|
85262
|
+
"getByTestId",
|
|
85263
|
+
"getByAltText",
|
|
85264
|
+
"getByTitle"
|
|
85265
|
+
]);
|
|
84187
85266
|
var VALID_PLAYWRIGHT_MATCHERS = /* @__PURE__ */ new Set([
|
|
84188
85267
|
// Locator assertions
|
|
84189
85268
|
"toBeAttached",
|
|
@@ -84671,6 +85750,16 @@ ${testOpen}`;
|
|
|
84671
85750
|
return false;
|
|
84672
85751
|
}
|
|
84673
85752
|
}
|
|
85753
|
+
if ((0, discovery_lint_flags_js_1.isPwMethodPreflightEnabled)()) {
|
|
85754
|
+
const getByCalls = codeForMatcherScan.match(/\.getBy[A-Za-z0-9_]*\s*\(/g) ?? [];
|
|
85755
|
+
for (const call of getByCalls) {
|
|
85756
|
+
const methodName = call.replace(/[\s(]+$/, "").slice(1);
|
|
85757
|
+
if (!exports2.VALID_PLAYWRIGHT_GETBY_METHODS.has(methodName)) {
|
|
85758
|
+
logs2?.push(` \u2717 Rejected "${t.name}": invalid Playwright locator method "${methodName}" (not a real getBy* API \u2014 likely hallucinated)`);
|
|
85759
|
+
return false;
|
|
85760
|
+
}
|
|
85761
|
+
}
|
|
85762
|
+
}
|
|
84674
85763
|
if (code.includes("// TODO")) {
|
|
84675
85764
|
logs2?.push(` \u26A0 "${t.name}" contains TODO comment \u2014 marking as unverified`);
|
|
84676
85765
|
t.tags = t.tags.filter((tag) => tag !== "@verified");
|
|
@@ -85784,6 +86873,35 @@ ${testOpen}`;
|
|
|
85784
86873
|
}
|
|
85785
86874
|
return null;
|
|
85786
86875
|
}
|
|
86876
|
+
var STRUCTURAL_ACTION_RE = /\.(?:click|dblclick|tap|fill|type|pressSequentially|press|selectOption|hover|check|uncheck|setChecked|setInputFiles|dragTo|focus|clear)\s*\(/;
|
|
86877
|
+
var POSITIONAL_PSEUDO_RE = /:(?:last-child|first-child|nth-child|nth-of-type|nth-last-child|nth-last-of-type)\b/i;
|
|
86878
|
+
var TABLE_TOKEN_RE = /\b(?:tr|td|th|tbody|thead)\b/i;
|
|
86879
|
+
var ROW_ANCHOR_RE = /getByRole\s*\(\s*['"`]row['"`]|getByTestId|data-testid|data-test\b|\[role\s*=\s*['"`]row['"`]\]/i;
|
|
86880
|
+
var LOCATOR_ARG_RE = /\.locator\(\s*(['"`])((?:\\.|(?!\1)[\s\S])*?)\1/g;
|
|
86881
|
+
function scanFragileStructuralLocators(code) {
|
|
86882
|
+
const findings = [];
|
|
86883
|
+
const scanCode = code.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "));
|
|
86884
|
+
for (const rawLine of scanCode.split("\n")) {
|
|
86885
|
+
const line = rawLine.replace(/\/\/.*$/, "");
|
|
86886
|
+
if (!STRUCTURAL_ACTION_RE.test(line))
|
|
86887
|
+
continue;
|
|
86888
|
+
if (ROW_ANCHOR_RE.test(line))
|
|
86889
|
+
continue;
|
|
86890
|
+
LOCATOR_ARG_RE.lastIndex = 0;
|
|
86891
|
+
let m;
|
|
86892
|
+
while ((m = LOCATOR_ARG_RE.exec(line)) !== null) {
|
|
86893
|
+
const css = m[2];
|
|
86894
|
+
if (POSITIONAL_PSEUDO_RE.test(css) && TABLE_TOKEN_RE.test(css)) {
|
|
86895
|
+
findings.push({
|
|
86896
|
+
line: rawLine.trim().slice(0, 200),
|
|
86897
|
+
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>' }).`
|
|
86898
|
+
});
|
|
86899
|
+
break;
|
|
86900
|
+
}
|
|
86901
|
+
}
|
|
86902
|
+
}
|
|
86903
|
+
return findings;
|
|
86904
|
+
}
|
|
85787
86905
|
function validateFinalActionFidelity(playwrightCode, actionLedger, credentials, opts) {
|
|
85788
86906
|
const ledgerSigs = [];
|
|
85789
86907
|
for (const entry of actionLedger) {
|
|
@@ -86223,6 +87341,7 @@ ${tailBlock}`;
|
|
|
86223
87341
|
let actionRevisionAttempted = false;
|
|
86224
87342
|
let ambiguityRevisionAttempted = false;
|
|
86225
87343
|
let strengthRevisionAttempted = false;
|
|
87344
|
+
let structuralRevisionAttempted = false;
|
|
86226
87345
|
let lastRenderedLedgerEntryCount = 0;
|
|
86227
87346
|
const successfulVerifications = [];
|
|
86228
87347
|
const actionLedger = [];
|
|
@@ -86349,7 +87468,15 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
86349
87468
|
tools: openaiTools,
|
|
86350
87469
|
tool_choice: "auto",
|
|
86351
87470
|
temperature: 0.2,
|
|
86352
|
-
|
|
87471
|
+
// Shared provider-aware output ceiling (same helper the survey planner uses):
|
|
87472
|
+
// generous where the provider's max output is verified large (deepseek/minimax
|
|
87473
|
+
// 65536), conservative elsewhere (16384). The old flat 8192 truncated large
|
|
87474
|
+
// finish_generate emissions mid-JSON — the loop re-prompted on the parse error
|
|
87475
|
+
// (line ~4744) and, for a test that genuinely needs >8192 output tokens, could
|
|
87476
|
+
// never finish before the hard stop → zero tests for that scenario. It's a
|
|
87477
|
+
// ceiling, not a target (calls finish at finish_reason:'stop' well under it), so
|
|
87478
|
+
// this adds no per-call cost.
|
|
87479
|
+
max_tokens: (0, discover_ai_planner_js_1.getAIPlannerMaxTokens)(agentModel)
|
|
86353
87480
|
}, { signal });
|
|
86354
87481
|
}), {
|
|
86355
87482
|
label: `scenario-gen-iter-${iteration}`,
|
|
@@ -86493,6 +87620,33 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
86493
87620
|
});
|
|
86494
87621
|
continue;
|
|
86495
87622
|
}
|
|
87623
|
+
const structuralLintMode = (0, discovery_lint_flags_js_1.structuralLocatorLintMode)();
|
|
87624
|
+
const structuralFindings = structuralLintMode !== "off" ? scanFragileStructuralLocators(validated[0].playwrightCode) : [];
|
|
87625
|
+
if (structuralLintMode === "warn" && structuralFindings.length > 0) {
|
|
87626
|
+
logs2.push(` Structural-locator gate [warn]: ${structuralFindings.length} positional table-row selector(s) \u2014 ${structuralFindings.map((f) => sanitizeLedgerText(f.line)).join(" | ").slice(0, 300)}`);
|
|
87627
|
+
}
|
|
87628
|
+
if (structuralLintMode === "enforce" && structuralFindings.length > 0 && !structuralRevisionAttempted) {
|
|
87629
|
+
structuralRevisionAttempted = true;
|
|
87630
|
+
const renderedLedger = actionLedger.length > 0 ? renderSessionRecording(actionLedger, credentials) : "";
|
|
87631
|
+
if (renderedLedger)
|
|
87632
|
+
lastRenderedLedgerEntryCount = actionLedger.length;
|
|
87633
|
+
logs2.push(` Structural-locator gate: ${structuralFindings.length} positional table-row selector(s) found \u2014 requesting one revision (recording attached: ${renderedLedger ? "yes" : "no"})`);
|
|
87634
|
+
const structuralPayload = {
|
|
87635
|
+
acknowledged: false,
|
|
87636
|
+
valid: false,
|
|
87637
|
+
revisionRequired: true,
|
|
87638
|
+
reason: "fragile_structural_locator",
|
|
87639
|
+
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.",
|
|
87640
|
+
offendingLocators: structuralFindings.map((f) => ({
|
|
87641
|
+
line: sanitizeLedgerText(f.line),
|
|
87642
|
+
reason: f.reason
|
|
87643
|
+
}))
|
|
87644
|
+
};
|
|
87645
|
+
if (renderedLedger)
|
|
87646
|
+
structuralPayload.sessionRecording = renderedLedger;
|
|
87647
|
+
toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: JSON.stringify(structuralPayload) });
|
|
87648
|
+
continue;
|
|
87649
|
+
}
|
|
86496
87650
|
const ambiguityGateEval = evaluateAmbiguityGate(validated[0].playwrightCode, ambiguityRevisionAttempted, iteration < MAX_SCENARIO_ITERATIONS);
|
|
86497
87651
|
const ambiguousLocators = ambiguityGateEval.findings;
|
|
86498
87652
|
if (ambiguityGateEval.fire) {
|
|
@@ -86589,12 +87743,16 @@ Do NOT assert that the button/tab/heading you interacted with is merely visible
|
|
|
86589
87743
|
const strengthSeverity = strengthVerdict?.severity ?? "none";
|
|
86590
87744
|
const hasResidualViolations = qualityCheck.offendingLines.length > 0;
|
|
86591
87745
|
const hasResidualActionDrift = actionCheck.offendingActions.length > 0;
|
|
87746
|
+
const hasResidualStructural = structuralLintMode === "enforce" && structuralFindings.length > 0;
|
|
86592
87747
|
if (hasResidualViolations) {
|
|
86593
87748
|
logs2.push(` Assertion quality gate: violations remain after revision \u2014 downgrading to @needs-review`);
|
|
86594
87749
|
}
|
|
86595
87750
|
if (hasResidualActionDrift) {
|
|
86596
87751
|
logs2.push(` Action fidelity gate: ${actionCheck.offendingActions.length} action drift(s) remain after revision \u2014 downgrading to @needs-review`);
|
|
86597
87752
|
}
|
|
87753
|
+
if (hasResidualStructural) {
|
|
87754
|
+
logs2.push(` Structural-locator gate: ${structuralFindings.length} positional table-row selector(s) remain after revision \u2014 downgrading to @needs-review`);
|
|
87755
|
+
}
|
|
86598
87756
|
let actualVerified = verified;
|
|
86599
87757
|
if (verified && verifyCallCount === 0) {
|
|
86600
87758
|
actualVerified = false;
|
|
@@ -86647,6 +87805,16 @@ Do NOT assert that the button/tab/heading you interacted with is merely visible
|
|
|
86647
87805
|
residual: ambiguousLocators.length,
|
|
86648
87806
|
lines: ambiguousLocators.map((a) => sanitizeLedgerText(a.line))
|
|
86649
87807
|
},
|
|
87808
|
+
// G2 — omitted entirely when off so the off-path healingContext is
|
|
87809
|
+
// byte-identical; present in warn (measure) + enforce (act) modes.
|
|
87810
|
+
...structuralLintMode !== "off" ? {
|
|
87811
|
+
structuralGate: {
|
|
87812
|
+
mode: structuralLintMode,
|
|
87813
|
+
fired: structuralRevisionAttempted,
|
|
87814
|
+
residual: structuralFindings.length,
|
|
87815
|
+
lines: structuralFindings.map((f) => sanitizeLedgerText(f.line))
|
|
87816
|
+
}
|
|
87817
|
+
} : {},
|
|
86650
87818
|
// Durable marker the server promote-seam (failure-analyzer.checkTestStability)
|
|
86651
87819
|
// reads to decide ACTIVE vs QUARANTINED(@bug-found) vs hold-DRAFT. Omitted when
|
|
86652
87820
|
// the gate errored (fail-open) so a missing marker is treated as "promote".
|
|
@@ -86672,7 +87840,7 @@ Do NOT assert that the button/tab/heading you interacted with is merely visible
|
|
|
86672
87840
|
verified: actualVerified,
|
|
86673
87841
|
healingContext: { ...baseHealingContext, gateActivity }
|
|
86674
87842
|
};
|
|
86675
|
-
if ((hasResidualViolations || hasResidualActionDrift || hasUncoveredOutcomes) && !result.tags.includes("@needs-review")) {
|
|
87843
|
+
if ((hasResidualViolations || hasResidualActionDrift || hasUncoveredOutcomes || hasResidualStructural) && !result.tags.includes("@needs-review")) {
|
|
86676
87844
|
result.tags.push("@needs-review");
|
|
86677
87845
|
}
|
|
86678
87846
|
if (actualVerified) {
|
|
@@ -320906,675 +322074,6 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
320906
322074
|
}
|
|
320907
322075
|
});
|
|
320908
322076
|
|
|
320909
|
-
// ../runner-core/dist/services/mobile/mobile-exploration-state.js
|
|
320910
|
-
var require_mobile_exploration_state = __commonJS({
|
|
320911
|
-
"../runner-core/dist/services/mobile/mobile-exploration-state.js"(exports2) {
|
|
320912
|
-
"use strict";
|
|
320913
|
-
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
320914
|
-
exports2.MobileExplorationState = void 0;
|
|
320915
|
-
exports2.stableMobileControlIdentity = stableMobileControlIdentity;
|
|
320916
|
-
exports2.classifySafeMobileNavigationControl = classifySafeMobileNavigationControl;
|
|
320917
|
-
exports2.deriveScreenId = deriveScreenId;
|
|
320918
|
-
var mobile_observation_js_1 = require_mobile_observation();
|
|
320919
|
-
var DEFAULT_STALE_THRESHOLD = 4;
|
|
320920
|
-
var MAX_SCREEN_OBSERVATIONS = 50;
|
|
320921
|
-
var EDITABLE_CONTROL_ROLE_RE = /(?:android\.widget\.(?:EditText|AutoCompleteTextView)|XCUIElementType(?:TextField|SecureTextField|SearchField|TextView)|textbox|searchbox|combobox|textarea|select)$/i;
|
|
320922
|
-
var STATEFUL_CONTROL_ROLE_RE = /(?:Switch|CheckBox|Checkbox|RadioButton|radio|Slider|SeekBar|Stepper|PickerWheel|Toggle)$/i;
|
|
320923
|
-
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;
|
|
320924
|
-
var SAFE_MODAL_TRIGGER_LABEL_RE = /^(?:create|new|add|edit|invite|configure|manage|details|preview|share)$/i;
|
|
320925
|
-
var MENU_TRIGGER_LABEL_RE = /^(?:more|menu|options|actions|filter|filters|sort|columns|customize)$/i;
|
|
320926
|
-
var GENERIC_NAVIGATION_DESTINATION_LABEL_RE = /^(?:home|feed|dashboard|profile|settings|inbox|messages?|notifications?|projects?|about|privacy|terms|learn\s+more)$/i;
|
|
320927
|
-
var SYSTEM_BACK_CONTROL_LABEL_RE = /^(?:back|go\s+back|navigate\s+up|navigation\s+up|up)$/i;
|
|
320928
|
-
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;
|
|
320929
|
-
function normalizeControlIdentityPart(value) {
|
|
320930
|
-
return value.trim().replace(/\s+/g, " ").toLowerCase();
|
|
320931
|
-
}
|
|
320932
|
-
function semanticControlAlias(label) {
|
|
320933
|
-
return `navigation-label:${normalizeControlIdentityPart(label)}`;
|
|
320934
|
-
}
|
|
320935
|
-
function localControlCoverageIdentity(screenId, identity) {
|
|
320936
|
-
return `screen:${screenId}:${identity}`;
|
|
320937
|
-
}
|
|
320938
|
-
function storedMobileControlIdentity(candidate) {
|
|
320939
|
-
return [
|
|
320940
|
-
candidate.identity,
|
|
320941
|
-
`kind:${candidate.kind}`,
|
|
320942
|
-
`label:${normalizeControlIdentityPart(candidate.label)}`
|
|
320943
|
-
].join(":");
|
|
320944
|
-
}
|
|
320945
|
-
function isSystemBackNavigationControl(element, label) {
|
|
320946
|
-
if (SYSTEM_BACK_CONTROL_LABEL_RE.test(normalizeControlIdentityPart(label)))
|
|
320947
|
-
return true;
|
|
320948
|
-
return [element.accessibilityId, element.resourceId].filter((value) => Boolean(value?.trim())).some((value) => {
|
|
320949
|
-
const leaf = normalizeControlIdentityPart(value).split(/[/:.]/).filter(Boolean).at(-1) ?? "";
|
|
320950
|
-
return SYSTEM_BACK_CONTROL_ID_RE.test(leaf);
|
|
320951
|
-
});
|
|
320952
|
-
}
|
|
320953
|
-
function stableMobileControlIdentity(element) {
|
|
320954
|
-
const role = normalizeControlIdentityPart(element.role || "control");
|
|
320955
|
-
const accessibilityId = element.accessibilityId?.trim();
|
|
320956
|
-
if (accessibilityId)
|
|
320957
|
-
return `a11y:${role}:${normalizeControlIdentityPart(accessibilityId)}`;
|
|
320958
|
-
const resourceId = element.resourceId?.trim();
|
|
320959
|
-
if (resourceId)
|
|
320960
|
-
return `resource:${role}:${normalizeControlIdentityPart(resourceId)}`;
|
|
320961
|
-
const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
|
|
320962
|
-
if (label)
|
|
320963
|
-
return `semantic:${role}:${normalizeControlIdentityPart(label)}`;
|
|
320964
|
-
return null;
|
|
320965
|
-
}
|
|
320966
|
-
function classifySafeMobileNavigationControl(element) {
|
|
320967
|
-
if (!element.actionable || element.enabled === false)
|
|
320968
|
-
return null;
|
|
320969
|
-
const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
|
|
320970
|
-
const safetyIdentity = [label, element.accessibilityId, element.resourceId].filter(Boolean).join(" ");
|
|
320971
|
-
if (!label || UNSAFE_OR_COMMIT_LABEL_RE.test(safetyIdentity))
|
|
320972
|
-
return null;
|
|
320973
|
-
if (isSystemBackNavigationControl(element, label))
|
|
320974
|
-
return null;
|
|
320975
|
-
const role = element.role.trim();
|
|
320976
|
-
if (EDITABLE_CONTROL_ROLE_RE.test(role) || STATEFUL_CONTROL_ROLE_RE.test(role))
|
|
320977
|
-
return null;
|
|
320978
|
-
const linkLike = /(?:Link$|^a$)/i.test(role);
|
|
320979
|
-
let kind = null;
|
|
320980
|
-
if (/(?:TabBarItem|\btab\b)/i.test(role))
|
|
320981
|
-
kind = "tab";
|
|
320982
|
-
else if (/menuitem/i.test(role))
|
|
320983
|
-
kind = "menu-item";
|
|
320984
|
-
else if (linkLike && element.navigationProvenance)
|
|
320985
|
-
kind = "link";
|
|
320986
|
-
else if (linkLike)
|
|
320987
|
-
return null;
|
|
320988
|
-
else if (element.hasPopup && element.hasPopup.toLowerCase() !== "false")
|
|
320989
|
-
kind = "menu-trigger";
|
|
320990
|
-
else if (element.expanded === false)
|
|
320991
|
-
kind = "disclosure";
|
|
320992
|
-
else if (MENU_TRIGGER_LABEL_RE.test(label))
|
|
320993
|
-
kind = "menu-trigger";
|
|
320994
|
-
else if (SAFE_MODAL_TRIGGER_LABEL_RE.test(label))
|
|
320995
|
-
kind = "modal-trigger";
|
|
320996
|
-
else if (element.navigationProvenance)
|
|
320997
|
-
kind = "navigation-button";
|
|
320998
|
-
else if (GENERIC_NAVIGATION_DESTINATION_LABEL_RE.test(label))
|
|
320999
|
-
kind = "navigation-button";
|
|
321000
|
-
if (!kind)
|
|
321001
|
-
return null;
|
|
321002
|
-
const identity = stableMobileControlIdentity(element);
|
|
321003
|
-
return identity ? { identity, label, kind } : null;
|
|
321004
|
-
}
|
|
321005
|
-
function sharedMobileControlCoverageIdentity(element, candidate, semanticFamilyId) {
|
|
321006
|
-
if (!element.navigationProvenance)
|
|
321007
|
-
return void 0;
|
|
321008
|
-
const durableId = element.accessibilityId?.trim() ? `a11y:${normalizeControlIdentityPart(element.accessibilityId)}` : element.resourceId?.trim() ? `resource:${normalizeControlIdentityPart(element.resourceId)}` : "";
|
|
321009
|
-
if (!durableId && !semanticFamilyId)
|
|
321010
|
-
return void 0;
|
|
321011
|
-
return [
|
|
321012
|
-
"shared-navigation",
|
|
321013
|
-
element.navigationProvenance,
|
|
321014
|
-
!durableId && semanticFamilyId ? `family:${semanticFamilyId}` : "",
|
|
321015
|
-
candidate.kind,
|
|
321016
|
-
durableId || "semantic-only",
|
|
321017
|
-
normalizeControlIdentityPart(candidate.label)
|
|
321018
|
-
].filter(Boolean).join(":");
|
|
321019
|
-
}
|
|
321020
|
-
function mobileNavigationRegion(element, windowSize) {
|
|
321021
|
-
if (!windowSize || windowSize.width <= 0 || windowSize.height <= 0)
|
|
321022
|
-
return "unknown";
|
|
321023
|
-
const centerX = (element.bounds.x + element.bounds.width / 2) / windowSize.width;
|
|
321024
|
-
const centerY = (element.bounds.y + element.bounds.height / 2) / windowSize.height;
|
|
321025
|
-
if (centerY >= 0.72)
|
|
321026
|
-
return "bottom";
|
|
321027
|
-
if (centerY <= 0.28)
|
|
321028
|
-
return "top";
|
|
321029
|
-
if (centerX <= 0.35)
|
|
321030
|
-
return "left";
|
|
321031
|
-
if (centerX >= 0.65)
|
|
321032
|
-
return "right";
|
|
321033
|
-
return "center";
|
|
321034
|
-
}
|
|
321035
|
-
function buildSemanticNavigationGroups(elements, windowSize) {
|
|
321036
|
-
const groups = /* @__PURE__ */ new Map();
|
|
321037
|
-
for (const element of elements) {
|
|
321038
|
-
if (!element.navigationProvenance)
|
|
321039
|
-
continue;
|
|
321040
|
-
const candidate = classifySafeMobileNavigationControl(element);
|
|
321041
|
-
if (!candidate)
|
|
321042
|
-
continue;
|
|
321043
|
-
const groupKey = `${element.navigationProvenance}:${mobileNavigationRegion(element, windowSize)}`;
|
|
321044
|
-
const labels = groups.get(groupKey) ?? /* @__PURE__ */ new Set();
|
|
321045
|
-
labels.add(`${candidate.kind}:${normalizeControlIdentityPart(candidate.label)}`);
|
|
321046
|
-
groups.set(groupKey, labels);
|
|
321047
|
-
}
|
|
321048
|
-
return new Map([...groups].filter(([, labels]) => labels.size >= 2));
|
|
321049
|
-
}
|
|
321050
|
-
function deriveScreenId(signal) {
|
|
321051
|
-
const namespace = signal.activity?.trim() || signal.bundleId?.trim();
|
|
321052
|
-
return namespace ? `${namespace}#${signal.screenHash}` : signal.screenHash;
|
|
321053
|
-
}
|
|
321054
|
-
function observedElementIdentity(element) {
|
|
321055
|
-
return (0, mobile_observation_js_1.stableParsedMobileElementIdentity)(element);
|
|
321056
|
-
}
|
|
321057
|
-
function mergeObservedElements(previous, current) {
|
|
321058
|
-
const merged = [...current];
|
|
321059
|
-
const seen = new Set(current.map(observedElementIdentity));
|
|
321060
|
-
for (const element of previous) {
|
|
321061
|
-
const identity = observedElementIdentity(element);
|
|
321062
|
-
if (seen.has(identity))
|
|
321063
|
-
continue;
|
|
321064
|
-
seen.add(identity);
|
|
321065
|
-
merged.push(element);
|
|
321066
|
-
}
|
|
321067
|
-
return merged;
|
|
321068
|
-
}
|
|
321069
|
-
function sanitizeElementsForEvidence(elements) {
|
|
321070
|
-
return elements.map((element) => {
|
|
321071
|
-
const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element);
|
|
321072
|
-
return {
|
|
321073
|
-
...element,
|
|
321074
|
-
...label ? { label } : { label: void 0 },
|
|
321075
|
-
text: void 0
|
|
321076
|
-
};
|
|
321077
|
-
});
|
|
321078
|
-
}
|
|
321079
|
-
var MobileExplorationState = class {
|
|
321080
|
-
/** screenId -> ScreenState (the mobile analog of routeState). */
|
|
321081
|
-
screens = /* @__PURE__ */ new Map();
|
|
321082
|
-
/** Observed directed transitions between screens. */
|
|
321083
|
-
transitions = [];
|
|
321084
|
-
/** Transition-announced screens that do not yet have a live observation. */
|
|
321085
|
-
frontier = /* @__PURE__ */ new Map();
|
|
321086
|
-
/** Safe navigation controls observed on each screen, keyed by stable identity. */
|
|
321087
|
-
safeControlsByScreen = /* @__PURE__ */ new Map();
|
|
321088
|
-
/** Screen-scoped coverage keys plus explicitly proven shared-navigation keys. */
|
|
321089
|
-
attemptedSafeControlCoverageIdentities = /* @__PURE__ */ new Set();
|
|
321090
|
-
successfulSafeControlAttemptCount = 0;
|
|
321091
|
-
/** Repeated id-less structural navigation families, grouped by region. */
|
|
321092
|
-
semanticNavigationFamilies = /* @__PURE__ */ new Map();
|
|
321093
|
-
nextSemanticNavigationFamilyId = 1;
|
|
321094
|
-
/** The screen-id of the most recently observed screen. */
|
|
321095
|
-
currentScreenId = null;
|
|
321096
|
-
/** Latest iteration recorded (drives stale detection + firstSeen stamps). */
|
|
321097
|
-
iteration = 0;
|
|
321098
|
-
/** Stale detector: progress mark (#screens + #total elements) at last advance. */
|
|
321099
|
-
lastProgressMark = 0;
|
|
321100
|
-
/** Consecutive iterations observed with no progress. */
|
|
321101
|
-
staleIterations = 0;
|
|
321102
|
-
staleThreshold;
|
|
321103
|
-
constructor(options = {}) {
|
|
321104
|
-
this.staleThreshold = typeof options.staleThreshold === "number" && options.staleThreshold > 0 ? Math.floor(options.staleThreshold) : DEFAULT_STALE_THRESHOLD;
|
|
321105
|
-
}
|
|
321106
|
-
// -------------------------------------------------------------------------
|
|
321107
|
-
// Observation recording
|
|
321108
|
-
// -------------------------------------------------------------------------
|
|
321109
|
-
/**
|
|
321110
|
-
* Returns true when `signal` identifies a screen this run has never observed
|
|
321111
|
-
* before. Pure read — does not mutate state.
|
|
321112
|
-
*/
|
|
321113
|
-
isNewScreen(signal) {
|
|
321114
|
-
return !this.screens.has(deriveScreenId(signal));
|
|
321115
|
-
}
|
|
321116
|
-
/**
|
|
321117
|
-
* Record an observation of `screen` at `iteration`. Creates a `ScreenState`
|
|
321118
|
-
* on first sight (stamping `firstSeenIteration`), otherwise refreshes the
|
|
321119
|
-
* stored element set (latest-wins, mirroring the web reducer's PAGE_CAPTURED
|
|
321120
|
-
* element refresh). A driver observation already contains the structural UI
|
|
321121
|
-
* tree, elements, screenshot association, and screen identity, so it is
|
|
321122
|
-
* captured evidence immediately; the model's capture_screen call only adds
|
|
321123
|
-
* a friendly name/type. Returns the resolved screen-id.
|
|
321124
|
-
*/
|
|
321125
|
-
recordObservation(signal, screen, iteration) {
|
|
321126
|
-
this.iteration = iteration;
|
|
321127
|
-
const screenId = deriveScreenId(signal);
|
|
321128
|
-
const evidenceElements = sanitizeElementsForEvidence(screen.elements);
|
|
321129
|
-
const existing = this.screens.get(screenId);
|
|
321130
|
-
if (existing) {
|
|
321131
|
-
this.screens.set(screenId, {
|
|
321132
|
-
...existing,
|
|
321133
|
-
elements: mergeObservedElements(existing.elements, evidenceElements)
|
|
321134
|
-
});
|
|
321135
|
-
} else {
|
|
321136
|
-
this.screens.set(screenId, {
|
|
321137
|
-
screenId,
|
|
321138
|
-
firstSeenIteration: iteration,
|
|
321139
|
-
elements: evidenceElements,
|
|
321140
|
-
visited: true
|
|
321141
|
-
});
|
|
321142
|
-
}
|
|
321143
|
-
this.frontier.delete(screenId);
|
|
321144
|
-
const controls = this.safeControlsByScreen.get(screenId) ?? /* @__PURE__ */ new Map();
|
|
321145
|
-
const semanticNavigationGroups = buildSemanticNavigationGroups(evidenceElements, screen.windowSize);
|
|
321146
|
-
const semanticNavigationFamilyIds = /* @__PURE__ */ new Map();
|
|
321147
|
-
for (const [groupKey, labels] of semanticNavigationGroups) {
|
|
321148
|
-
semanticNavigationFamilyIds.set(groupKey, this.resolveSemanticNavigationFamilyId(groupKey, labels));
|
|
321149
|
-
}
|
|
321150
|
-
for (const element of evidenceElements) {
|
|
321151
|
-
const candidate = classifySafeMobileNavigationControl(element);
|
|
321152
|
-
if (!candidate)
|
|
321153
|
-
continue;
|
|
321154
|
-
const storedIdentity = storedMobileControlIdentity(candidate);
|
|
321155
|
-
const semanticGroupKey = element.navigationProvenance ? `${element.navigationProvenance}:${mobileNavigationRegion(element, screen.windowSize)}` : "";
|
|
321156
|
-
controls.set(storedIdentity, {
|
|
321157
|
-
...candidate,
|
|
321158
|
-
identity: storedIdentity,
|
|
321159
|
-
sharedCoverageIdentity: sharedMobileControlCoverageIdentity(element, candidate, semanticNavigationFamilyIds.get(semanticGroupKey))
|
|
321160
|
-
});
|
|
321161
|
-
}
|
|
321162
|
-
if (controls.size > 0)
|
|
321163
|
-
this.safeControlsByScreen.set(screenId, controls);
|
|
321164
|
-
this.currentScreenId = screenId;
|
|
321165
|
-
this.updateStaleProgress();
|
|
321166
|
-
return screenId;
|
|
321167
|
-
}
|
|
321168
|
-
/**
|
|
321169
|
-
* Back-compatible acknowledgement from capture_screen. Observation already
|
|
321170
|
-
* marks structural evidence visited; this call only resolves a rare pending
|
|
321171
|
-
* transition frontier and preserves callers that annotate explicitly.
|
|
321172
|
-
*/
|
|
321173
|
-
markVisited(screenId) {
|
|
321174
|
-
const state2 = this.screens.get(screenId);
|
|
321175
|
-
if (state2 && !state2.visited) {
|
|
321176
|
-
this.screens.set(screenId, { ...state2, visited: true });
|
|
321177
|
-
}
|
|
321178
|
-
this.frontier.delete(screenId);
|
|
321179
|
-
}
|
|
321180
|
-
/**
|
|
321181
|
-
* Record a confirmed tap against a safe navigation candidate. Callers invoke
|
|
321182
|
-
* this only after the driver reports success; recovery-unknown, stale-ref, and
|
|
321183
|
-
* failed actions must remain on the breadth frontier.
|
|
321184
|
-
*/
|
|
321185
|
-
noteControlAttempt(screenId, element) {
|
|
321186
|
-
if (!screenId)
|
|
321187
|
-
return false;
|
|
321188
|
-
const candidate = classifySafeMobileNavigationControl(element);
|
|
321189
|
-
const controls = this.safeControlsByScreen.get(screenId) ?? /* @__PURE__ */ new Map();
|
|
321190
|
-
if (candidate) {
|
|
321191
|
-
const storedIdentity = storedMobileControlIdentity(candidate);
|
|
321192
|
-
const existing = controls.get(storedIdentity);
|
|
321193
|
-
controls.set(storedIdentity, {
|
|
321194
|
-
...candidate,
|
|
321195
|
-
identity: storedIdentity,
|
|
321196
|
-
sharedCoverageIdentity: existing?.sharedCoverageIdentity ?? sharedMobileControlCoverageIdentity(element, candidate)
|
|
321197
|
-
});
|
|
321198
|
-
this.safeControlsByScreen.set(screenId, controls);
|
|
321199
|
-
}
|
|
321200
|
-
const tappedLabel = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
|
|
321201
|
-
if (!tappedLabel)
|
|
321202
|
-
return false;
|
|
321203
|
-
const before = this.attemptedSafeControlCoverageIdentities.size;
|
|
321204
|
-
const alias = semanticControlAlias(tappedLabel);
|
|
321205
|
-
for (const control of controls.values()) {
|
|
321206
|
-
if (semanticControlAlias(control.label) === alias) {
|
|
321207
|
-
this.attemptedSafeControlCoverageIdentities.add(localControlCoverageIdentity(screenId, control.identity));
|
|
321208
|
-
if (control.sharedCoverageIdentity) {
|
|
321209
|
-
this.attemptedSafeControlCoverageIdentities.add(control.sharedCoverageIdentity);
|
|
321210
|
-
}
|
|
321211
|
-
}
|
|
321212
|
-
}
|
|
321213
|
-
if (this.attemptedSafeControlCoverageIdentities.size > before) {
|
|
321214
|
-
this.successfulSafeControlAttemptCount += 1;
|
|
321215
|
-
this.staleIterations = 0;
|
|
321216
|
-
return true;
|
|
321217
|
-
}
|
|
321218
|
-
return false;
|
|
321219
|
-
}
|
|
321220
|
-
/** Attach a human-friendly name / coarse type to a screen (capture_screen). */
|
|
321221
|
-
annotateScreen(screenId, annotation) {
|
|
321222
|
-
const state2 = this.screens.get(screenId);
|
|
321223
|
-
if (!state2)
|
|
321224
|
-
return;
|
|
321225
|
-
this.screens.set(screenId, {
|
|
321226
|
-
...state2,
|
|
321227
|
-
screenName: annotation.screenName ?? state2.screenName,
|
|
321228
|
-
screenType: annotation.screenType ?? state2.screenType
|
|
321229
|
-
});
|
|
321230
|
-
}
|
|
321231
|
-
/** Attach structurally observed authentication provenance to a screen. */
|
|
321232
|
-
setScreenAuthEvidence(screenId, evidence) {
|
|
321233
|
-
const state2 = this.screens.get(screenId);
|
|
321234
|
-
if (!state2)
|
|
321235
|
-
return;
|
|
321236
|
-
this.screens.set(screenId, {
|
|
321237
|
-
...state2,
|
|
321238
|
-
requiresAuth: evidence.requiresAuth,
|
|
321239
|
-
authProvenance: evidence.authProvenance,
|
|
321240
|
-
sessionState: { ...evidence.sessionState }
|
|
321241
|
-
});
|
|
321242
|
-
}
|
|
321243
|
-
/** Append a free-form observation note to a screen (observations tool). */
|
|
321244
|
-
addScreenObservation(screenId, note) {
|
|
321245
|
-
const state2 = this.screens.get(screenId);
|
|
321246
|
-
if (!state2)
|
|
321247
|
-
return;
|
|
321248
|
-
const normalized = note.trim();
|
|
321249
|
-
if (!normalized)
|
|
321250
|
-
return;
|
|
321251
|
-
const current = state2.observations ?? [];
|
|
321252
|
-
if (current.some((existing) => existing.trim().toLowerCase() === normalized.toLowerCase())) {
|
|
321253
|
-
return;
|
|
321254
|
-
}
|
|
321255
|
-
const observations = [...current, normalized].slice(-MAX_SCREEN_OBSERVATIONS);
|
|
321256
|
-
this.screens.set(screenId, { ...state2, observations });
|
|
321257
|
-
}
|
|
321258
|
-
/**
|
|
321259
|
-
* Record a directed transition between two screens. Both endpoints are
|
|
321260
|
-
* ensured to exist on the frontier if not yet observed (mirrors the web
|
|
321261
|
-
* reducer adding both ends of a TRANSITION_RECORDED to the frontier). The
|
|
321262
|
-
* `toScreenId` becomes the current screen when the transition starts from the
|
|
321263
|
-
* current screen.
|
|
321264
|
-
*/
|
|
321265
|
-
recordTransition(transition) {
|
|
321266
|
-
let recordedTransition = transition;
|
|
321267
|
-
if (!transition.viaControlIdentity && transition.viaRef) {
|
|
321268
|
-
const sourceElement = this.screens.get(transition.fromScreenId)?.elements.find((element) => element.ref === transition.viaRef);
|
|
321269
|
-
if (sourceElement) {
|
|
321270
|
-
recordedTransition = {
|
|
321271
|
-
...transition,
|
|
321272
|
-
viaControlIdentity: (0, mobile_observation_js_1.stableParsedMobileElementIdentity)(sourceElement)
|
|
321273
|
-
};
|
|
321274
|
-
}
|
|
321275
|
-
}
|
|
321276
|
-
const alreadyRecorded = this.transitions.some((existing) => existing.fromScreenId === recordedTransition.fromScreenId && existing.toScreenId === recordedTransition.toScreenId && existing.action === recordedTransition.action);
|
|
321277
|
-
if (!alreadyRecorded) {
|
|
321278
|
-
this.transitions.push(recordedTransition);
|
|
321279
|
-
}
|
|
321280
|
-
for (const endpointId of [transition.fromScreenId, transition.toScreenId]) {
|
|
321281
|
-
if (!this.screens.has(endpointId) && !this.frontier.has(endpointId)) {
|
|
321282
|
-
this.frontier.set(endpointId, {
|
|
321283
|
-
screenId: endpointId,
|
|
321284
|
-
source: "transition",
|
|
321285
|
-
discoveredAtIteration: this.iteration
|
|
321286
|
-
});
|
|
321287
|
-
}
|
|
321288
|
-
}
|
|
321289
|
-
if (transition.fromScreenId === this.currentScreenId) {
|
|
321290
|
-
this.currentScreenId = transition.toScreenId;
|
|
321291
|
-
}
|
|
321292
|
-
}
|
|
321293
|
-
// -------------------------------------------------------------------------
|
|
321294
|
-
// Selectors
|
|
321295
|
-
// -------------------------------------------------------------------------
|
|
321296
|
-
/** The current screen-id (most recently observed), or null before any observation. */
|
|
321297
|
-
getCurrentScreenId() {
|
|
321298
|
-
return this.currentScreenId;
|
|
321299
|
-
}
|
|
321300
|
-
/** ScreenState for `screenId`, if known. */
|
|
321301
|
-
getScreen(screenId) {
|
|
321302
|
-
return this.screens.get(screenId);
|
|
321303
|
-
}
|
|
321304
|
-
/** Number of distinct screens observed this run. */
|
|
321305
|
-
getScreenCount() {
|
|
321306
|
-
return this.screens.size;
|
|
321307
|
-
}
|
|
321308
|
-
/** Number of screens backed by a live structural driver observation. */
|
|
321309
|
-
getVisitedScreenCount() {
|
|
321310
|
-
let total = 0;
|
|
321311
|
-
for (const state2 of this.screens.values()) {
|
|
321312
|
-
if (state2.visited)
|
|
321313
|
-
total += 1;
|
|
321314
|
-
}
|
|
321315
|
-
return total;
|
|
321316
|
-
}
|
|
321317
|
-
/** Total elements observed across all known screens (latest snapshot each). */
|
|
321318
|
-
getTotalElementCount() {
|
|
321319
|
-
let total = 0;
|
|
321320
|
-
for (const state2 of this.screens.values()) {
|
|
321321
|
-
total += state2.elements.length;
|
|
321322
|
-
}
|
|
321323
|
-
return total;
|
|
321324
|
-
}
|
|
321325
|
-
/**
|
|
321326
|
-
* Frontier screen-ids ranked by recency (most recently discovered first),
|
|
321327
|
-
* excluding any screen already visited. Mirrors the web `getRankedFrontier`
|
|
321328
|
-
* intent (filter visited, rank by recency) — without URL prefix similarity,
|
|
321329
|
-
* which has no mobile analog.
|
|
321330
|
-
*/
|
|
321331
|
-
getFrontier() {
|
|
321332
|
-
return [...this.frontier.values()].filter((item) => {
|
|
321333
|
-
const state2 = this.screens.get(item.screenId);
|
|
321334
|
-
return !state2 || !state2.visited;
|
|
321335
|
-
}).sort((a, b) => b.discoveredAtIteration - a.discoveredAtIteration).map((item) => item.screenId);
|
|
321336
|
-
}
|
|
321337
|
-
/** Safe navigation candidates not yet attempted, deduped across shared chrome. */
|
|
321338
|
-
getUnexploredSafeNavigationCandidates() {
|
|
321339
|
-
const authenticatedProductObserved = [...this.screens.values()].some((screen) => screen.requiresAuth === true);
|
|
321340
|
-
const candidates = [];
|
|
321341
|
-
for (const [screenId, controls] of this.safeControlsByScreen) {
|
|
321342
|
-
const origin = this.screens.get(screenId);
|
|
321343
|
-
const originIsConfirmedPublicEntry = origin?.authProvenance === "public" || !origin?.authProvenance && origin?.requiresAuth !== true && /^(?:AUTH|LANDING)$/i.test(origin?.screenType ?? "");
|
|
321344
|
-
if (authenticatedProductObserved && originIsConfirmedPublicEntry)
|
|
321345
|
-
continue;
|
|
321346
|
-
for (const control of controls.values()) {
|
|
321347
|
-
const alias = semanticControlAlias(control.label);
|
|
321348
|
-
const coverageIdentity = control.sharedCoverageIdentity ?? localControlCoverageIdentity(screenId, control.identity);
|
|
321349
|
-
if (this.attemptedSafeControlCoverageIdentities.has(coverageIdentity) || this.attemptedSafeControlCoverageIdentities.has(localControlCoverageIdentity(screenId, control.identity)))
|
|
321350
|
-
continue;
|
|
321351
|
-
const existing = candidates.find((candidate) => candidate.identity === coverageIdentity || candidate.alias === alias && candidate.screenIds.has(screenId));
|
|
321352
|
-
if (existing) {
|
|
321353
|
-
existing.screenIds.add(screenId);
|
|
321354
|
-
} else {
|
|
321355
|
-
candidates.push({
|
|
321356
|
-
identity: coverageIdentity,
|
|
321357
|
-
label: control.label,
|
|
321358
|
-
kind: control.kind,
|
|
321359
|
-
alias,
|
|
321360
|
-
screenIds: /* @__PURE__ */ new Set([screenId])
|
|
321361
|
-
});
|
|
321362
|
-
}
|
|
321363
|
-
}
|
|
321364
|
-
}
|
|
321365
|
-
return candidates.map((candidate) => ({
|
|
321366
|
-
identity: candidate.identity,
|
|
321367
|
-
label: candidate.label,
|
|
321368
|
-
kind: candidate.kind,
|
|
321369
|
-
screenIds: [...candidate.screenIds]
|
|
321370
|
-
})).sort((a, b) => {
|
|
321371
|
-
const aCurrent = this.currentScreenId !== null && a.screenIds.includes(this.currentScreenId);
|
|
321372
|
-
const bCurrent = this.currentScreenId !== null && b.screenIds.includes(this.currentScreenId);
|
|
321373
|
-
if (aCurrent !== bCurrent)
|
|
321374
|
-
return aCurrent ? -1 : 1;
|
|
321375
|
-
return b.screenIds.length - a.screenIds.length;
|
|
321376
|
-
});
|
|
321377
|
-
}
|
|
321378
|
-
/** Harness-owned survey completion floor, independent of model assertions. */
|
|
321379
|
-
getSurveyBreadthCoverage() {
|
|
321380
|
-
const unvisitedScreenIds = this.getFrontier();
|
|
321381
|
-
const unexploredControls = this.getUnexploredSafeNavigationCandidates();
|
|
321382
|
-
const complete = unvisitedScreenIds.length === 0 && unexploredControls.length === 0;
|
|
321383
|
-
const missing = [];
|
|
321384
|
-
if (unvisitedScreenIds.length > 0) {
|
|
321385
|
-
missing.push(`${unvisitedScreenIds.length} uncaptured screen(s)`);
|
|
321386
|
-
}
|
|
321387
|
-
if (unexploredControls.length > 0) {
|
|
321388
|
-
missing.push(`${unexploredControls.length} safe navigation control(s)`);
|
|
321389
|
-
}
|
|
321390
|
-
return {
|
|
321391
|
-
required: true,
|
|
321392
|
-
complete,
|
|
321393
|
-
unvisitedScreenIds,
|
|
321394
|
-
unexploredControls,
|
|
321395
|
-
attemptedControlCount: this.successfulSafeControlAttemptCount,
|
|
321396
|
-
...missing.length > 0 ? { reason: `Missing ${missing.join(" and ")}.` } : {}
|
|
321397
|
-
};
|
|
321398
|
-
}
|
|
321399
|
-
/**
|
|
321400
|
-
* Monotonic structural progress token for adaptive survey leases. Frontier
|
|
321401
|
-
* cardinality alone is insufficient: attempting control A can reveal control
|
|
321402
|
-
* B and leave the remaining count unchanged even though real work happened.
|
|
321403
|
-
*/
|
|
321404
|
-
getSurveyBreadthProgressMark() {
|
|
321405
|
-
const discoveredControlIdentities = /* @__PURE__ */ new Set();
|
|
321406
|
-
for (const [screenId, controls] of this.safeControlsByScreen) {
|
|
321407
|
-
for (const control of controls.values()) {
|
|
321408
|
-
discoveredControlIdentities.add(control.sharedCoverageIdentity ?? localControlCoverageIdentity(screenId, control.identity));
|
|
321409
|
-
}
|
|
321410
|
-
}
|
|
321411
|
-
return [
|
|
321412
|
-
`screens:${this.screens.size}`,
|
|
321413
|
-
`attempts:${this.successfulSafeControlAttemptCount}`,
|
|
321414
|
-
`controls:${discoveredControlIdentities.size}`,
|
|
321415
|
-
`transitions:${this.transitions.length}`
|
|
321416
|
-
].join("|");
|
|
321417
|
-
}
|
|
321418
|
-
resolveSemanticNavigationFamilyId(groupKey, labels) {
|
|
321419
|
-
const families = this.semanticNavigationFamilies.get(groupKey) ?? [];
|
|
321420
|
-
let best;
|
|
321421
|
-
let bestIntersection = 0;
|
|
321422
|
-
for (const family of families) {
|
|
321423
|
-
let intersection = 0;
|
|
321424
|
-
for (const label of labels) {
|
|
321425
|
-
if (family.labels.has(label))
|
|
321426
|
-
intersection += 1;
|
|
321427
|
-
}
|
|
321428
|
-
const overlap = intersection / Math.min(labels.size, family.labels.size);
|
|
321429
|
-
if (intersection >= 2 && overlap >= 0.6 && intersection > bestIntersection) {
|
|
321430
|
-
best = family;
|
|
321431
|
-
bestIntersection = intersection;
|
|
321432
|
-
}
|
|
321433
|
-
}
|
|
321434
|
-
if (best) {
|
|
321435
|
-
for (const label of labels)
|
|
321436
|
-
best.labels.add(label);
|
|
321437
|
-
return best.id;
|
|
321438
|
-
}
|
|
321439
|
-
const created = {
|
|
321440
|
-
id: `semantic-nav-family-${this.nextSemanticNavigationFamilyId++}`,
|
|
321441
|
-
labels: new Set(labels)
|
|
321442
|
-
};
|
|
321443
|
-
families.push(created);
|
|
321444
|
-
this.semanticNavigationFamilies.set(groupKey, families);
|
|
321445
|
-
return created.id;
|
|
321446
|
-
}
|
|
321447
|
-
/** Observed transitions (defensive copy). */
|
|
321448
|
-
getTransitions() {
|
|
321449
|
-
return [...this.transitions];
|
|
321450
|
-
}
|
|
321451
|
-
// -------------------------------------------------------------------------
|
|
321452
|
-
// Stale-progress detection (ported semantics from the web stale guard)
|
|
321453
|
-
// -------------------------------------------------------------------------
|
|
321454
|
-
/**
|
|
321455
|
-
* Progress mark: distinct screens weighted heavily, plus total elements seen.
|
|
321456
|
-
* A run advances this mark whenever it finds a new screen OR new elements;
|
|
321457
|
-
* the stale detector triggers when the mark stops advancing for
|
|
321458
|
-
* `staleThreshold` consecutive iterations.
|
|
321459
|
-
*/
|
|
321460
|
-
getProgressMark() {
|
|
321461
|
-
return this.screens.size * 1e3 + this.transitions.length * 100 + this.successfulSafeControlAttemptCount * 10 + this.getTotalElementCount();
|
|
321462
|
-
}
|
|
321463
|
-
updateStaleProgress() {
|
|
321464
|
-
const mark = this.getProgressMark();
|
|
321465
|
-
if (mark > this.lastProgressMark) {
|
|
321466
|
-
this.lastProgressMark = mark;
|
|
321467
|
-
this.staleIterations = 0;
|
|
321468
|
-
} else {
|
|
321469
|
-
this.staleIterations += 1;
|
|
321470
|
-
}
|
|
321471
|
-
}
|
|
321472
|
-
/** Consecutive no-progress iterations observed so far. */
|
|
321473
|
-
getStaleIterations() {
|
|
321474
|
-
return this.staleIterations;
|
|
321475
|
-
}
|
|
321476
|
-
/**
|
|
321477
|
-
* True when the run has gone `staleThreshold` consecutive iterations with no
|
|
321478
|
-
* new screen and no new element — the signal the explorer uses to nudge or
|
|
321479
|
-
* finish (mirrors the web explorer's stale guard).
|
|
321480
|
-
*/
|
|
321481
|
-
isStaleProgress() {
|
|
321482
|
-
return this.staleIterations >= this.staleThreshold;
|
|
321483
|
-
}
|
|
321484
|
-
// -------------------------------------------------------------------------
|
|
321485
|
-
// Planner evidence export
|
|
321486
|
-
// -------------------------------------------------------------------------
|
|
321487
|
-
/**
|
|
321488
|
-
* Snapshot the screen-graph as a plain object (frozen mobile-types
|
|
321489
|
-
* `MobileScreenGraph`). Defensive copies so callers cannot mutate internal
|
|
321490
|
-
* state.
|
|
321491
|
-
*/
|
|
321492
|
-
toScreenGraph() {
|
|
321493
|
-
const screens = /* @__PURE__ */ new Map();
|
|
321494
|
-
for (const [id, state2] of this.screens) {
|
|
321495
|
-
screens.set(id, {
|
|
321496
|
-
...state2,
|
|
321497
|
-
elements: [...state2.elements],
|
|
321498
|
-
observations: state2.observations ? [...state2.observations] : void 0,
|
|
321499
|
-
sessionState: state2.sessionState ? { ...state2.sessionState } : void 0
|
|
321500
|
-
});
|
|
321501
|
-
}
|
|
321502
|
-
return {
|
|
321503
|
-
screens,
|
|
321504
|
-
transitions: this.transitions.map((t) => ({
|
|
321505
|
-
fromScreenId: t.fromScreenId,
|
|
321506
|
-
toScreenId: t.toScreenId,
|
|
321507
|
-
viaRef: t.viaRef,
|
|
321508
|
-
viaControlIdentity: t.viaControlIdentity,
|
|
321509
|
-
action: t.action
|
|
321510
|
-
}))
|
|
321511
|
-
};
|
|
321512
|
-
}
|
|
321513
|
-
/**
|
|
321514
|
-
* Build the planner evidence shape: one `CollectedPageData` per screen, with
|
|
321515
|
-
* the screen-id carried in BOTH `route` (the on-the-wire positional field the
|
|
321516
|
-
* server keys screenshots by) AND the explicit `screenId` field (per the
|
|
321517
|
-
* Phase-0 `CollectedPageData.screenId` contract). `elements` are mapped from
|
|
321518
|
-
* the screen's parsed actionable elements into the platform-neutral
|
|
321519
|
-
* `CollectedPageElement[]` shape the server persists as SiteElements — without
|
|
321520
|
-
* this, mobile screens land with zero elements (coverage always 0%, no element
|
|
321521
|
-
* intelligence). `triggersNavigation` is derived from the durable source
|
|
321522
|
-
* element identity captured when the transition happened (with ref-only
|
|
321523
|
-
* fallback for legacy transitions). Insertion order matches first-seen order.
|
|
321524
|
-
*/
|
|
321525
|
-
buildInMemoryScreenMap() {
|
|
321526
|
-
const pages = [];
|
|
321527
|
-
const ordered = [...this.screens.values()].sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
321528
|
-
const navRefsByScreen = /* @__PURE__ */ new Map();
|
|
321529
|
-
const navIdentitiesByScreen = /* @__PURE__ */ new Map();
|
|
321530
|
-
for (const transition of this.transitions) {
|
|
321531
|
-
if (transition.viaControlIdentity) {
|
|
321532
|
-
const set = navIdentitiesByScreen.get(transition.fromScreenId) ?? /* @__PURE__ */ new Set();
|
|
321533
|
-
set.add(transition.viaControlIdentity);
|
|
321534
|
-
navIdentitiesByScreen.set(transition.fromScreenId, set);
|
|
321535
|
-
} else if (transition.viaRef) {
|
|
321536
|
-
const set = navRefsByScreen.get(transition.fromScreenId) ?? /* @__PURE__ */ new Set();
|
|
321537
|
-
set.add(transition.viaRef);
|
|
321538
|
-
navRefsByScreen.set(transition.fromScreenId, set);
|
|
321539
|
-
}
|
|
321540
|
-
}
|
|
321541
|
-
for (const state2 of ordered) {
|
|
321542
|
-
const page = {
|
|
321543
|
-
route: state2.screenId,
|
|
321544
|
-
screenId: state2.screenId,
|
|
321545
|
-
pageType: state2.screenType ?? "UNKNOWN",
|
|
321546
|
-
elements: (0, mobile_observation_js_1.buildCollectedElementsForScreen)(state2.elements, {
|
|
321547
|
-
navigationRefs: navRefsByScreen.get(state2.screenId),
|
|
321548
|
-
navigationIdentities: navIdentitiesByScreen.get(state2.screenId)
|
|
321549
|
-
}),
|
|
321550
|
-
requiresAuth: state2.requiresAuth ?? false
|
|
321551
|
-
};
|
|
321552
|
-
if (state2.sessionState) {
|
|
321553
|
-
page.sessionState = { ...state2.sessionState };
|
|
321554
|
-
}
|
|
321555
|
-
if (state2.screenName) {
|
|
321556
|
-
page.title = state2.screenName;
|
|
321557
|
-
}
|
|
321558
|
-
if (state2.observations && state2.observations.length > 0) {
|
|
321559
|
-
page.observations = state2.observations.map((note) => ({
|
|
321560
|
-
action: "observe",
|
|
321561
|
-
outcome: note,
|
|
321562
|
-
outcomeType: "SUCCESS"
|
|
321563
|
-
}));
|
|
321564
|
-
}
|
|
321565
|
-
pages.push(page);
|
|
321566
|
-
}
|
|
321567
|
-
return pages;
|
|
321568
|
-
}
|
|
321569
|
-
/** Alias for `buildInMemoryScreenMap()` — name parity with the planner-evidence call site. */
|
|
321570
|
-
toScreenMap() {
|
|
321571
|
-
return this.buildInMemoryScreenMap();
|
|
321572
|
-
}
|
|
321573
|
-
};
|
|
321574
|
-
exports2.MobileExplorationState = MobileExplorationState;
|
|
321575
|
-
}
|
|
321576
|
-
});
|
|
321577
|
-
|
|
321578
322077
|
// ../runner-core/dist/services/mobile/mobile-discovery-agent.js
|
|
321579
322078
|
var require_mobile_discovery_agent = __commonJS({
|
|
321580
322079
|
"../runner-core/dist/services/mobile/mobile-discovery-agent.js"(exports2) {
|
|
@@ -321588,6 +322087,7 @@ var require_mobile_discovery_agent = __commonJS({
|
|
|
321588
322087
|
var mobile_exploration_state_js_1 = require_mobile_exploration_state();
|
|
321589
322088
|
var mobile_discovery_auth_js_1 = require_mobile_discovery_auth();
|
|
321590
322089
|
var mobile_driver_errors_js_1 = require_mobile_driver_errors();
|
|
322090
|
+
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
321591
322091
|
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
321592
322092
|
var discover_planner_js_1 = require_discover_planner();
|
|
321593
322093
|
function checkpointErrorMessage(err) {
|
|
@@ -321870,7 +322370,7 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
|
|
|
321870
322370
|
};
|
|
321871
322371
|
}
|
|
321872
322372
|
if (isSurvey && explore.authCoverage && !explore.authCoverage.complete) {
|
|
321873
|
-
|
|
322373
|
+
log2("Mobile survey continuing to planning with incomplete authenticated coverage: " + (explore.authCoverage.reason ?? "No authenticated product screen was proven."));
|
|
321874
322374
|
}
|
|
321875
322375
|
if (isSurvey && explore.breadthCoverage && !explore.breadthCoverage.complete) {
|
|
321876
322376
|
log2("Mobile survey continuing to planning with residual breadth gaps: " + (explore.breadthCoverage.reason ?? "Unvisited screens or safe navigation controls remain."));
|
|
@@ -321888,7 +322388,7 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
|
|
|
321888
322388
|
const graph = state2.toScreenGraph();
|
|
321889
322389
|
const screenMap = state2.buildInMemoryScreenMap();
|
|
321890
322390
|
const testBudget = (0, discover_planner_js_1.computeTestBudget)(explore.exploreStats.pagesVisited, ctx.focusAreas);
|
|
321891
|
-
const planResult = await runPlan({
|
|
322391
|
+
const planResult = await (0, mobile_session_keepalive_js_1.withMobileSessionKeepAlive)(driver, () => runPlan({
|
|
321892
322392
|
graph,
|
|
321893
322393
|
mode,
|
|
321894
322394
|
projectId: ctx.projectId,
|
|
@@ -321912,7 +322412,7 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
|
|
|
321912
322412
|
}, {
|
|
321913
322413
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
321914
322414
|
onAICall
|
|
321915
|
-
});
|
|
322415
|
+
}));
|
|
321916
322416
|
logs2.push(...planResult.logs);
|
|
321917
322417
|
log2(`
|
|
321918
322418
|
Plan: ${planResult.scenarios.length} scenarios, ${planResult.featureGroups.length} feature groups (budget: ${testBudget})`);
|
|
@@ -322131,6 +322631,9 @@ Fatal error: ${msg}`);
|
|
|
322131
322631
|
if (explore.breadthCoverage && !explore.breadthCoverage.complete) {
|
|
322132
322632
|
warnings.push("Discovery retained the captured mobile screen graph with residual navigation gaps: " + (explore.breadthCoverage.reason ?? "some observed controls could not be revisited."));
|
|
322133
322633
|
}
|
|
322634
|
+
if (explore.authCoverage && !explore.authCoverage.complete) {
|
|
322635
|
+
warnings.push("Discovery retained the captured mobile screen graph with incomplete authenticated coverage: " + (explore.authCoverage.reason ?? "no authenticated product screen was proven."));
|
|
322636
|
+
}
|
|
322134
322637
|
if (explore.truncated) {
|
|
322135
322638
|
const reasonLabel = explore.truncatedReason === "rate_limit" ? "AI rate-limit" : explore.truncatedReason === "timeout" ? "AI timeout" : "AI error";
|
|
322136
322639
|
warnings.push(`Discovery may be incomplete: exploration aborted by ${reasonLabel}. Re-run after the limit clears for a complete screen graph.`);
|
|
@@ -322767,6 +323270,7 @@ Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.leng
|
|
|
322767
323270
|
if (surveyAuthRecipe) {
|
|
322768
323271
|
logs2.push(`Auth-prologue: captured + verified login recipe ${surveyAuthRecipe.recipeHash} during survey \u2014 persisting for reuse on later runs`);
|
|
322769
323272
|
}
|
|
323273
|
+
const plannerFailedError = planResult.plannerFailed ? `Survey planning failed (${planResult.plannerFailureKind ?? "unknown"}): ${planResult.fallbackReason ?? "planner did not produce a usable plan"}. Explore evidence was saved; re-run the survey to retry planning.` : void 0;
|
|
322770
323274
|
return {
|
|
322771
323275
|
observedOrigins: mergeObservedOrigins(exploreResult.observedOrigins, loginObservedOrigins),
|
|
322772
323276
|
resolvedAppOrigin: surveyAuthRecipe?.appOrigin ?? void 0,
|
|
@@ -322782,7 +323286,8 @@ Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.leng
|
|
|
322782
323286
|
exploreTruncatedReason: exploreResult.truncatedReason,
|
|
322783
323287
|
fallbackUsed: planResult.fallbackUsed ?? false,
|
|
322784
323288
|
fallbackReason: planResult.fallbackReason,
|
|
322785
|
-
|
|
323289
|
+
error: plannerFailedError,
|
|
323290
|
+
featureGroups: plannerFailedError ? void 0 : featureGroups,
|
|
322786
323291
|
apiCalls: discoveryApiCalls.length > 0 ? discoveryApiCalls : void 0,
|
|
322787
323292
|
healthObservations: surveyHealth,
|
|
322788
323293
|
exploreFinalMessages: exploreResult.finalMessages,
|
|
@@ -336640,7 +337145,7 @@ var require_package4 = __commonJS({
|
|
|
336640
337145
|
"package.json"(exports2, module2) {
|
|
336641
337146
|
module2.exports = {
|
|
336642
337147
|
name: "@validate.qa/runner",
|
|
336643
|
-
version: "1.0.
|
|
337148
|
+
version: "1.0.40",
|
|
336644
337149
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
336645
337150
|
bin: {
|
|
336646
337151
|
"validate-runner": "dist/cli.js"
|
|
@@ -339895,8 +340400,9 @@ async function activateTargetApp(sessionId, platform3, appId) {
|
|
|
339895
340400
|
const args = platform3 === "IOS" ? [{ bundleId: appId }] : [{ appId }];
|
|
339896
340401
|
await executeMobileScript(sessionId, "mobile: activateApp", args);
|
|
339897
340402
|
}
|
|
340403
|
+
var APP_TERMINATE_TIMEOUT_MS = 5e3;
|
|
339898
340404
|
async function terminateTargetApp(sessionId, platform3, appId) {
|
|
339899
|
-
const args = platform3 === "IOS" ? [{ bundleId: appId }] : [{ appId }];
|
|
340405
|
+
const args = platform3 === "IOS" ? [{ bundleId: appId, timeout: APP_TERMINATE_TIMEOUT_MS }] : [{ appId, timeout: APP_TERMINATE_TIMEOUT_MS }];
|
|
339900
340406
|
await executeMobileScript(sessionId, "mobile: terminateApp", args);
|
|
339901
340407
|
}
|
|
339902
340408
|
async function freshLaunchTargetApp(sessionId, platform3, appId, log2) {
|
|
@@ -342907,7 +343413,11 @@ var MobileBridgeDriver = class {
|
|
|
342907
343413
|
async relaunch() {
|
|
342908
343414
|
await this.withSessionRecovery("app relaunch", async () => {
|
|
342909
343415
|
const args = this.appLifecycleArgs();
|
|
342910
|
-
|
|
343416
|
+
try {
|
|
343417
|
+
await this.executeScript("mobile: terminateApp", [{ ...args, timeout: 5e3 }]);
|
|
343418
|
+
} catch (err) {
|
|
343419
|
+
if (isRecoverableAppiumSessionFailure(err)) throw err;
|
|
343420
|
+
}
|
|
342911
343421
|
await this.executeScript("mobile: activateApp", [args]);
|
|
342912
343422
|
try {
|
|
342913
343423
|
await waitForColdBootSettle(this.sessionPath);
|
|
@@ -345452,10 +345962,22 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345452
345962
|
onPlanReady
|
|
345453
345963
|
});
|
|
345454
345964
|
} finally {
|
|
345965
|
+
const TEARDOWN_STEP_TIMEOUT = /* @__PURE__ */ Symbol("vqa-teardown-timeout");
|
|
345966
|
+
const timeboxed = async (step) => Promise.race([
|
|
345967
|
+
step,
|
|
345968
|
+
new Promise((resolve2) => {
|
|
345969
|
+
const t = setTimeout(() => resolve2(TEARDOWN_STEP_TIMEOUT), 45e3);
|
|
345970
|
+
t.unref?.();
|
|
345971
|
+
})
|
|
345972
|
+
]);
|
|
345455
345973
|
const activeCapture = capture;
|
|
345456
345974
|
if (activeCapture) {
|
|
345457
345975
|
try {
|
|
345458
|
-
const
|
|
345976
|
+
const stopped = await timeboxed(activeCapture.stop());
|
|
345977
|
+
if (stopped === TEARDOWN_STEP_TIMEOUT) {
|
|
345978
|
+
throw new Error("capture stop timed out after 45s \u2014 continuing teardown without apiCalls");
|
|
345979
|
+
}
|
|
345980
|
+
const apiCalls = stopped;
|
|
345459
345981
|
const captureHealth = activeCapture.health();
|
|
345460
345982
|
if (discoveryResult) {
|
|
345461
345983
|
discoveryResult.apiCalls = apiCalls.length ? attachMobileDiscoveryNetworkContext(apiCalls, discoveryResult, session?.appId ?? mobileTarget.appId) : void 0;
|
|
@@ -345476,7 +345998,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345476
345998
|
if (session) {
|
|
345477
345999
|
detachMirrorSession(session.deviceId);
|
|
345478
346000
|
try {
|
|
345479
|
-
await session.stop();
|
|
346001
|
+
await timeboxed(session.stop());
|
|
345480
346002
|
} catch {
|
|
345481
346003
|
}
|
|
345482
346004
|
}
|