@validate.qa/runner 1.0.33 → 1.0.35
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/README.md +24 -1
- package/dist/cli.js +566 -194
- package/dist/cli.mjs +566 -194
- package/package.json +2 -1
package/dist/cli.mjs
CHANGED
|
@@ -1354,11 +1354,13 @@ function escapeEnvValue(value) {
|
|
|
1354
1354
|
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
|
|
1355
1355
|
return `"${escaped}"`;
|
|
1356
1356
|
}
|
|
1357
|
-
var API_PREFIX, AUDIO_CHUNK_DURATION_MS, ACTION_BATCH_INTERVAL_MS, ACTION_BATCH_MAX_SIZE, MOBILE_SCREENSHOT_INTERVAL_MS, MOBILE_ACTION_BATCH_MAX_SIZE, MOBILE_BRIDGE_PORT, MOBILE_SNAPSHOT_MAX_SIZE_BYTES, MOBILE_APP_ID_MAX_LENGTH, MOBILE_APP_ID_PATTERN, MOBILE_FOCUS_SETTLE_MS, MOBILE_DEFAULT_SWIPE_DISTANCE, MOBILE_MAX_SWIPE_DISTANCE, SELECTOR_PRIORITIES, MOBILE_STEP_TYPES, BLOCKED_ENV_VARS, SPAWNED_ENV_ALLOWLIST;
|
|
1357
|
+
var API_PREFIX, SUPPORT_EMAIL, PLATFORM_ADMIN_EMAIL, AUDIO_CHUNK_DURATION_MS, ACTION_BATCH_INTERVAL_MS, ACTION_BATCH_MAX_SIZE, MOBILE_SCREENSHOT_INTERVAL_MS, MOBILE_ACTION_BATCH_MAX_SIZE, MOBILE_BRIDGE_PORT, MOBILE_SNAPSHOT_MAX_SIZE_BYTES, MOBILE_APP_ID_MAX_LENGTH, MOBILE_APP_ID_PATTERN, MOBILE_FOCUS_SETTLE_MS, MOBILE_DEFAULT_SWIPE_DISTANCE, MOBILE_MAX_SWIPE_DISTANCE, SELECTOR_PRIORITIES, MOBILE_STEP_TYPES, BLOCKED_ENV_VARS, SPAWNED_ENV_ALLOWLIST;
|
|
1358
1358
|
var init_constants = __esm({
|
|
1359
1359
|
"../shared/dist/constants.js"() {
|
|
1360
1360
|
"use strict";
|
|
1361
1361
|
API_PREFIX = "/api";
|
|
1362
|
+
SUPPORT_EMAIL = "support@validate.qa";
|
|
1363
|
+
PLATFORM_ADMIN_EMAIL = "admin@validate.qa";
|
|
1362
1364
|
AUDIO_CHUNK_DURATION_MS = 3e4;
|
|
1363
1365
|
ACTION_BATCH_INTERVAL_MS = 2e3;
|
|
1364
1366
|
ACTION_BATCH_MAX_SIZE = 10;
|
|
@@ -2562,6 +2564,8 @@ var init_audit_pricing = __esm({
|
|
|
2562
2564
|
// Current defaults
|
|
2563
2565
|
"grok-build-0.1": { input: 1, output: 2 },
|
|
2564
2566
|
"claude-opus-4-7": { input: 5, output: 25 },
|
|
2567
|
+
// Introductory pricing through 2026-08-31 ($2/$10); standard is $3/$15.
|
|
2568
|
+
"claude-sonnet-5": { input: 2, output: 10 },
|
|
2565
2569
|
"claude-sonnet-4-6": { input: 3, output: 15 },
|
|
2566
2570
|
"claude-haiku-4-5-20251001": { input: 1, output: 5 },
|
|
2567
2571
|
"gpt-4o": { input: 2.5, output: 10 },
|
|
@@ -3598,7 +3602,7 @@ var init_agent_env = __esm({
|
|
|
3598
3602
|
"CommonProgramFiles"
|
|
3599
3603
|
]);
|
|
3600
3604
|
ALLOW_PREFIX = [
|
|
3601
|
-
// AI provider / CLI config (grok-build, claude-code
|
|
3605
|
+
// AI provider / CLI config (grok-build, claude-code)
|
|
3602
3606
|
"GROK_",
|
|
3603
3607
|
"XAI_",
|
|
3604
3608
|
"ANTHROPIC_",
|
|
@@ -3905,8 +3909,10 @@ __export(dist_exports, {
|
|
|
3905
3909
|
MOBILE_STEP_TYPES: () => MOBILE_STEP_TYPES,
|
|
3906
3910
|
PLAN_DEFINITIONS: () => PLAN_DEFINITIONS,
|
|
3907
3911
|
PLAN_ORDER: () => PLAN_ORDER,
|
|
3912
|
+
PLATFORM_ADMIN_EMAIL: () => PLATFORM_ADMIN_EMAIL,
|
|
3908
3913
|
SELECTOR_PRIORITIES: () => SELECTOR_PRIORITIES,
|
|
3909
3914
|
SHARED_PRICING_PLAN_MAP: () => SHARED_PRICING_PLAN_MAP,
|
|
3915
|
+
SUPPORT_EMAIL: () => SUPPORT_EMAIL,
|
|
3910
3916
|
TRANSIENT_CONTROL_ROLES: () => TRANSIENT_CONTROL_ROLES,
|
|
3911
3917
|
TRIAL_DURATION_DAYS: () => TRIAL_DURATION_DAYS,
|
|
3912
3918
|
UI_VARIANTS: () => UI_VARIANTS,
|
|
@@ -65168,7 +65174,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
65168
65174
|
Example - BAD happy path: \`["Navigate to /feature", "Click Create", "Verify dialog opens"]\` \u2192 only tests the trigger.
|
|
65169
65175
|
Example - GOOD happy path (generic, async): \`["Navigate to landing and log in", "Navigate to the feature page", "Note the current count of items on the list", "Click the primary action button", "Fill required inputs and submit", "Wait for completion signal (up to 60s)", "Verify the item count increased by 1", "Open the newly created item", "Verify its content is non-empty and contains the input I provided"]\`
|
|
65170
65176
|
Example - GOOD happy path (sync CRUD): \`["Navigate to landing and log in", "Navigate to the list page", "Click New", "Fill required fields", "Click Save", "Verify the new record appears in the list", "Click the record and verify its detail view shows the values I entered"]\`
|
|
65171
|
-
12. **Auth happy paths require concrete session proof.** A login/auth HAPPY_PATH is incomplete if it only proves "user reaches dashboard" or "authenticated page is visible." It MUST verify at least one durable signed-in signal that was observed in the evidence: user/account menu opens, Logout/Sign out is available, the observed display name/email/avatar/account item is present, authenticated navigation remains available after a reload, or another app-specific session control is usable. Prefer concrete observed labels from the evidence (for example "Logout", "
|
|
65177
|
+
12. **Auth happy paths require concrete session proof.** A login/auth HAPPY_PATH is incomplete if it only proves "user reaches dashboard" or "authenticated page is visible." It MUST verify at least one durable signed-in signal that was observed in the evidence: user/account menu opens, Logout/Sign out is available, the observed display name/email/avatar/account item is present, authenticated navigation remains available after a reload, or another app-specific session control is usable. Prefer concrete observed labels from the evidence (for example "Logout", "Sign out", "Account", "Profile", or the user's own displayed name/email from evidence) over generic phrases like "personalized content." If the explorer only observed a redirect and no signed-in signal, keep the route outcome but mark the session-proof outcome reliability "low" and state what concrete signal was missing.
|
|
65172
65178
|
13. **Independent execution contract.** Every scenario MUST pass when run alone, first, last, in random order, and in parallel against an environment with no relevant feature data except explicitly guaranteed immutable fixtures. A scenario that depends on a sibling scenario's side effects (e.g. "delete the conversation that another test created") is invalid. If two flows are inherently coupled, bundle them as a single USER_JOURNEY scenario.
|
|
65173
65179
|
14. **Scenario-owned state.** If a scenario reads, searches, opens, updates, messages, cancels, or deletes an entity, that scenario MUST create or seed the entity in its own setup steps. "Use an existing record" / "the conversation in the demo account" / "the existing user named X" are forbidden phrasings \u2014 they assume mutable pre-existing app data. Carve-out: baseline observations (current count, list length, "the dashboard heading is visible") are allowed as prerequisite reads ONLY when the durable assertion targets a self-created entity. Phrase setup as: "Click New, fill required fields with run-scoped values, submit and verify the new record appears, THEN proceed to <feature under test>."
|
|
65174
65180
|
15. **Run-scoped test data.** Generated entities MUST use unique, run-scoped identifiers \u2014 phrase as "test message <Date.now()>" or "QA-<unique id>" in steps. Subsequent assertions MUST target that unique identifier, not "the first item", "the latest message", "the last row", or any visible display name observed during exploration. Hardcoding any specific display name, username, email, conversation title, or other identifier observed in the explorer's snapshot is forbidden \u2014 that data is incidental, not a stable contract. The same identifiers will not exist (or will look different) when the test runs in a fresh environment or against a different account.
|
|
@@ -66128,7 +66134,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
66128
66134
|
9. **No duplicates** - check existing test names before adding
|
|
66129
66135
|
10. **All tests start from the app launch entry** - every generated test starts by launching the target app. The runner clears app data when the device/reset policy safely supports it; otherwise it performs a terminate + activate relaunch, which can preserve app session data. Therefore scenarios MUST navigate through the app's own UI from whatever launch state is valid, and MUST NOT rely on mutable data from a prior scenario disappearing. Steps must reflect this: say "Launch the app and tap Sign In" not "deep-link to the login screen". Do NOT navigate by deep-link; the app's launch + tab/drawer navigation is the only entry path. \`targetPages\` lists the screen-ids involved in the flow.
|
|
66130
66136
|
11. **targetPages must be copied CHARACTER-FOR-CHARACTER from the screen-ids in the evidence** (they look like \`com.example.app.MainActivity#ab12cd34\`). NEVER invent, prettify, or abbreviate them \u2014 no "Landing", "AUTH", "Home Screen", or any name that is not literally in the provided screens. A scenario whose targetPages contains even ONE id not present in the evidence is REJECTED WHOLESALE by the validator, so a single made-up route silently destroys the entire scenario. If you are unsure which screen-id a step touches, use the screen-id of the screen where the flow STARTS.
|
|
66131
|
-
12. **Happy paths must verify the DURABLE OUTCOME, not just the trigger.** A HAPPY_PATH is NOT complete until it verifies the feature's real output. "Button tapped" / "Sheet opened" / "Spinner appeared" is NOT a happy path - that is a UI interaction test. Apply this to any feature where an action produces a result the user then sees or uses: CRUD creates, photo/file uploads, searches that return results, imports, exports, payments, messages sent, AI/ML outputs, generated documents, sync runs, analysis runs, conversion, data processing. The scenario MUST include:
|
|
66137
|
+
12. **Happy paths must verify the DURABLE OUTCOME, not just the trigger.** A HAPPY_PATH is NOT complete until it verifies the feature's real output. "Button tapped" / "Sheet opened" / "Spinner appeared" / "Settings screen opened" is NOT a happy path - that is a UI interaction test. Form-mutation happy paths (change password, create/edit/save a record, send a message, checkout, etc.) MUST include typing into the relevant fields AND committing the form (submit/save/send or keyboard Enter/Done) \u2014 navigating to the form screen alone is insufficient. Apply this to any feature where an action produces a result the user then sees or uses: CRUD creates, photo/file uploads, searches that return results, imports, exports, payments, messages sent, AI/ML outputs, generated documents, sync runs, analysis runs, conversion, data processing. The scenario MUST include:
|
|
66132
66138
|
- The trigger step (Tap Submit / Generate / Send / Upload / Run / Analyze / Save / Pay / etc.)
|
|
66133
66139
|
- A wait step for the operation to complete. Phrase it by SIGNAL, not wall-clock ("Wait for the result to appear - up to 60 seconds"). Prefer signals the app actually uses: spinner disappears, status badge flips to a success state, a new row appears, an item count increments, a success toast/snackbar shows, the screen transitions to a result screen.
|
|
66134
66140
|
- A verification step on the DURABLE RESULT - the thing that outlives the transient UI: a new list row, a persisted record, a received message, a committed state change, visible output text. Do NOT assert on the spinner disappearing alone; assert on what replaced it.
|
|
@@ -66799,6 +66805,9 @@ var require_mobile_generator = __commonJS({
|
|
|
66799
66805
|
"../runner-core/dist/services/mobile/mobile-generator.js"(exports) {
|
|
66800
66806
|
"use strict";
|
|
66801
66807
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
66808
|
+
exports.collapseConsecutiveBacks = collapseConsecutiveBacks;
|
|
66809
|
+
exports.scenarioIntentMismatchReason = scenarioIntentMismatchReason;
|
|
66810
|
+
exports.dedupeConsecutiveIdenticalActions = dedupeConsecutiveIdenticalActions;
|
|
66802
66811
|
exports.collapseRetryCycles = collapseRetryCycles;
|
|
66803
66812
|
exports.runMobileGeneratePhase = runMobileGeneratePhase;
|
|
66804
66813
|
var crypto_1 = __require("crypto");
|
|
@@ -67069,7 +67078,7 @@ var require_mobile_generator = __commonJS({
|
|
|
67069
67078
|
## Your job
|
|
67070
67079
|
1. mobile_snapshot the current screen.
|
|
67071
67080
|
2. Execute the scenario's steps using the real UI: tap buttons, fill fields (mobile_type), submit, navigate.
|
|
67072
|
-
3. COMPLETE THE ACTION the scenario is about \u2014 do not stop half-way. If the scenario submits a form (sign in, sign up, search, create), you MUST tap the submit/primary button
|
|
67081
|
+
3. COMPLETE THE ACTION the scenario is about \u2014 do not stop half-way. If the scenario submits a form (sign in, sign up, search, create, change password, edit profile), you MUST commit it \u2014 tap the submit/primary button OR press the keyboard's Enter/Done action on the last field (mobile_type with a trailing newline on the focused field when no button is visible). Filling fields alone is NOT the behavior under test. Navigating to a form screen without changing its fields is also NOT enough for mutation scenarios (change/create/update/edit). If it navigates, you MUST land on the destination. A "happy path" that fills a form but never commits \u2014 or only opens the form without typing \u2014 is WRONG.
|
|
67073
67082
|
4. When you reach the target screen / complete the flow, PROVE the outcome with an assert tool:
|
|
67074
67083
|
- mobile_assert_visible({ref}) \u2014 the target screen's stable element is on screen.
|
|
67075
67084
|
- mobile_assert_text({ref, expected}) \u2014 an element contains expected text (only durable when the element has a stable accessibility-id / resource-id).
|
|
@@ -67084,6 +67093,8 @@ var require_mobile_generator = __commonJS({
|
|
|
67084
67093
|
- 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.
|
|
67085
67094
|
- 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.
|
|
67086
67095
|
- 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.
|
|
67096
|
+
- Do NOT thrash the system Back button. One intentional back/navigation is fine; a chain of backs while lost is not a test \u2014 re-snapshot, re-orient, and take the productive path once.
|
|
67097
|
+
- Prefer product identity locators (accessibility-id, resource-id, visible label). Never assert on bare hierarchy roots or unlabeled full-screen containers \u2014 those prove nothing.
|
|
67087
67098
|
- Do ONLY the actions this scenario needs. Do NOT re-open a menu you already opened, re-select an option that is already selected, toggle a setting back and forth, or repeat a step that already succeeded \u2014 EVERY action you take is baked into the generated test verbatim, so each extra action becomes a permanent junk step.
|
|
67088
67099
|
- Each action must make NEW progress toward the goal. If an action leaves the screen unchanged, do NOT repeat it \u2014 pick a different element or move on.
|
|
67089
67100
|
- Finish promptly once the outcome is proven: the instant the expected outcome is visible, assert it and call finish_generate. Do not keep exploring after the goal is proven.
|
|
@@ -67102,7 +67113,7 @@ This app has stored login credentials under these keys:
|
|
|
67102
67113
|
${keyList}
|
|
67103
67114
|
|
|
67104
67115
|
When a scenario needs a signed-in session, complete the FULL login sequence \u2014 filling the fields alone does NOT log you in:
|
|
67105
|
-
1. Reach the email/password form. 2. Fill each credential field with \`mobile_fill_credential\` (pass the field ref and the KEY, e.g. \`{ ref: "e5", credentialKey: "TEST_USER_EMAIL" }\`) \u2014 the runner types the real value on the device and the generated test stores a \`{{KEY}}\` placeholder that replay resolves; you never see the secret. 3.
|
|
67116
|
+
1. Reach the email/password form. 2. Fill each credential field with \`mobile_fill_credential\` (pass the field ref and the KEY, e.g. \`{ ref: "e5", credentialKey: "TEST_USER_EMAIL" }\`) \u2014 the runner types the real value on the device and the generated test stores a \`{{KEY}}\` placeholder that replay resolves; you never see the secret. 3. COMMIT the form \u2014 tap the submit/primary button (Sign In / Log In / Continue) OR, if no button is visible, focus the last field and \`mobile_type\` a trailing newline (\`\\n\`) to trigger the keyboard's Enter/Done action. 4. Re-snapshot and VERIFY an authenticated screen appeared. If login fails twice, do NOT keep retrying or wander: call finish_generate with verified:false and say the login could not be completed (the credentials may be invalid for this app).
|
|
67106
67117
|
|
|
67107
67118
|
SECURITY RULES:
|
|
67108
67119
|
- NEVER call \`mobile_type\` with a real email, password, or token value \u2014 the runner will REJECT the call and you lose an iteration. Use \`mobile_fill_credential\` for every credential field.
|
|
@@ -67452,6 +67463,7 @@ ${statePacket}` },
|
|
|
67452
67463
|
if (action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT")
|
|
67453
67464
|
assertCount++;
|
|
67454
67465
|
},
|
|
67466
|
+
recordedActions: actions,
|
|
67455
67467
|
log: log2
|
|
67456
67468
|
});
|
|
67457
67469
|
pushResult(dispatchResult);
|
|
@@ -67467,7 +67479,7 @@ ${statePacket}` },
|
|
|
67467
67479
|
return { actions, assertCount, finish, lastSnapshot: getLatest(), lastScreenName };
|
|
67468
67480
|
}
|
|
67469
67481
|
async function dispatchGenerateTool(args) {
|
|
67470
|
-
const { toolName, toolArgs, driver, platform: platform3, latest, absorbObservation, resolveRef, credentials, screenName, recordAction, log: log2 } = args;
|
|
67482
|
+
const { toolName, toolArgs, driver, platform: platform3, latest, absorbObservation, resolveRef, credentials, screenName, recordAction, recordedActions, log: log2 } = args;
|
|
67471
67483
|
const unknownRef = (ref) => ({
|
|
67472
67484
|
ok: false,
|
|
67473
67485
|
error: `Unknown element ref "${String(ref)}". Call mobile_snapshot first; refs are only valid for the latest snapshot.`
|
|
@@ -67532,14 +67544,23 @@ ${statePacket}` },
|
|
|
67532
67544
|
const screenChanged = changedScreen(before, absorbed);
|
|
67533
67545
|
const identityTap = isIdentityBearingTapTarget(element);
|
|
67534
67546
|
if (shouldRecordAction(result, absorbed) && (screenChanged || identityTap)) {
|
|
67535
|
-
|
|
67536
|
-
log2(` tap left content signature unchanged but target has identity \u2014 recording commit control "${labelForElement(element)}".`);
|
|
67537
|
-
}
|
|
67538
|
-
recordAction({
|
|
67547
|
+
const tapAction = {
|
|
67539
67548
|
type: "TAP",
|
|
67540
67549
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
67541
|
-
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
67542
|
-
|
|
67550
|
+
metadata: metadataForElement(element, beforeScreenName, platform3),
|
|
67551
|
+
innerText: null,
|
|
67552
|
+
url: "",
|
|
67553
|
+
value: null
|
|
67554
|
+
};
|
|
67555
|
+
const lastRecorded = recordedActions[recordedActions.length - 1];
|
|
67556
|
+
if (lastRecorded && actionCycleKey(lastRecorded) === actionCycleKey(tapAction)) {
|
|
67557
|
+
log2(` tap left content signature unchanged but duplicates the prior step \u2014 not recording again.`);
|
|
67558
|
+
} else if (!screenChanged && identityTap) {
|
|
67559
|
+
log2(` tap left content signature unchanged but target has identity \u2014 recording commit control "${labelForElement(element)}".`);
|
|
67560
|
+
recordAction(tapAction);
|
|
67561
|
+
} else {
|
|
67562
|
+
recordAction(tapAction);
|
|
67563
|
+
}
|
|
67543
67564
|
} else if (shouldRecordAction(result, absorbed)) {
|
|
67544
67565
|
log2(" tap changed nothing on screen \u2014 not recording it as a step (dead action).");
|
|
67545
67566
|
}
|
|
@@ -67883,6 +67904,105 @@ ${statePacket}` },
|
|
|
67883
67904
|
...scenario.expectedOutcomes.flatMap((outcome) => [outcome.description, outcome.hint])
|
|
67884
67905
|
].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").toLowerCase();
|
|
67885
67906
|
}
|
|
67907
|
+
var INSPECTION_OUTCOME_TYPES = /* @__PURE__ */ new Set([
|
|
67908
|
+
"element-visible",
|
|
67909
|
+
"text-visible",
|
|
67910
|
+
"element-hidden",
|
|
67911
|
+
"screen-visible"
|
|
67912
|
+
]);
|
|
67913
|
+
function isInspectionOnlyScenario(scenario, actions) {
|
|
67914
|
+
if (actions.length === 0 || scenario.expectedOutcomes.length === 0)
|
|
67915
|
+
return false;
|
|
67916
|
+
if (!scenario.expectedOutcomes.every((o) => INSPECTION_OUTCOME_TYPES.has(o.type))) {
|
|
67917
|
+
return false;
|
|
67918
|
+
}
|
|
67919
|
+
if (actions.some((a) => a.type === "TYPE" || a.type === "CLEAR")) {
|
|
67920
|
+
return false;
|
|
67921
|
+
}
|
|
67922
|
+
const text = scenarioText(scenario);
|
|
67923
|
+
const visibilityOnlyGoal = (/\b(entry\s+points?|providers?|options?|provider\s+buttons?)\b/.test(text) || /\b(show|display)\s+(all|the)?\s*(provider|option|button|link|entry)/.test(text) || /\b(all\s+(three\s+)?providers?)\b/.test(text)) && !/\b(change|update|create|save|send|register|edit|with\s+valid|successful\s+(sign|log)|complete\s+(the\s+)?(login|sign[- ]?in)|submit\s+(the\s+)?(form|credentials)|enter\s+(an?\s+)?(email|password))\b/.test(text);
|
|
67924
|
+
if (visibilityOnlyGoal) {
|
|
67925
|
+
const assertCount2 = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT").length;
|
|
67926
|
+
return assertCount2 > 0;
|
|
67927
|
+
}
|
|
67928
|
+
const completionIntent = scenario.type === "HAPPY_PATH" || /\b(happy\s*path|successful|with\s+valid|complete\s+(the\s+)?(login|sign[- ]?in|registration|checkout|purchase))\b/.test(text);
|
|
67929
|
+
const formIntent = /\b(sign\s*in|log\s*in|login|authenticate|register|sign\s*up|checkout|create|save|submit|send|change|update|edit)\b/.test(text);
|
|
67930
|
+
if (completionIntent && formIntent)
|
|
67931
|
+
return false;
|
|
67932
|
+
const assertCount = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT").length;
|
|
67933
|
+
return assertCount > 0;
|
|
67934
|
+
}
|
|
67935
|
+
function isNavigationFocusedScenario(scenario) {
|
|
67936
|
+
const text = scenarioText(scenario);
|
|
67937
|
+
const navigates = /\b(back|return|go\s+home|navigate\s+back|leave|exit|cancel\s+out|dismiss|previous\s+screen)\b/.test(text);
|
|
67938
|
+
const completesFlow = /\b(happy\s*path|successful|complete\s+(the\s+)?(login|sign[- ]?in|registration|checkout)|with\s+valid)\b/.test(text);
|
|
67939
|
+
return navigates && !completesFlow;
|
|
67940
|
+
}
|
|
67941
|
+
function isFormValidationScenario(scenario) {
|
|
67942
|
+
if (scenario.variant === "validation")
|
|
67943
|
+
return true;
|
|
67944
|
+
if (scenario.type !== "EDGE_CASE")
|
|
67945
|
+
return false;
|
|
67946
|
+
const text = scenarioText(scenario);
|
|
67947
|
+
return /\b(empty|required|missing|blank|invalid|reject|error|validation|wrong|incorrect|mismatch)\b/.test(text);
|
|
67948
|
+
}
|
|
67949
|
+
function isFormMutationHappyPath(scenario) {
|
|
67950
|
+
if (isNavigationFocusedScenario(scenario))
|
|
67951
|
+
return false;
|
|
67952
|
+
if (isFormValidationScenario(scenario))
|
|
67953
|
+
return false;
|
|
67954
|
+
const text = scenarioText(scenario);
|
|
67955
|
+
const isHappy = scenario.type === "HAPPY_PATH" || /\b(happy\s*path|successful|success)\b/.test(text);
|
|
67956
|
+
if (!isHappy)
|
|
67957
|
+
return false;
|
|
67958
|
+
if (/\b(sign\s*in|log\s*in|login|authenticate|sign\s*out|log\s*out)\b/.test(text) && !/\b(change|update|reset)\s+(the\s+)?password\b/.test(text)) {
|
|
67959
|
+
return false;
|
|
67960
|
+
}
|
|
67961
|
+
if (/\b(show|display|visible|options?|providers?|links?)\b/.test(text) && !/\b(change|update|create|save|send|register|edit|post|write|compose|delete|add|set)\b/.test(text)) {
|
|
67962
|
+
return false;
|
|
67963
|
+
}
|
|
67964
|
+
const mutates = /\b(change|update|create|save|send|register|sign\s*up|checkout|edit|post|write|compose|set|add|delete|remove)\b/.test(text);
|
|
67965
|
+
const hasObject = /\b(password|email|profile|message|post|item|record|form|settings|account|name|title|description|comment|bio|phone|address|payment|card|user|note|task|entry)\b/.test(text);
|
|
67966
|
+
return mutates && hasObject;
|
|
67967
|
+
}
|
|
67968
|
+
function isNavigationChromeTap(action) {
|
|
67969
|
+
if (action.type !== "TAP")
|
|
67970
|
+
return false;
|
|
67971
|
+
const textForAction = actionText(action);
|
|
67972
|
+
return /\b(go\s+back|back\s+to|←|return\s+to|cancel|close|dismiss|not\s+now|skip|maybe\s+later)\b/.test(textForAction);
|
|
67973
|
+
}
|
|
67974
|
+
function isAuthMethodChooserTap(action) {
|
|
67975
|
+
if (action.type !== "TAP")
|
|
67976
|
+
return false;
|
|
67977
|
+
const textForAction = actionText(action);
|
|
67978
|
+
if (/\b(submit|confirm|sign\s*in|log\s*in|login)\b/.test(textForAction) && !/\b(with|using|via)\b/.test(textForAction)) {
|
|
67979
|
+
return false;
|
|
67980
|
+
}
|
|
67981
|
+
return /\b(continue\s+with|sign\s+in\s+with|log\s+in\s+with|use\s+(email|phone)|choose|pick|select)\b/.test(textForAction);
|
|
67982
|
+
}
|
|
67983
|
+
function isCredentialSubmitTap(action) {
|
|
67984
|
+
if (action.type !== "TAP")
|
|
67985
|
+
return false;
|
|
67986
|
+
if (isNavigationChromeTap(action) || isAuthMethodChooserTap(action))
|
|
67987
|
+
return false;
|
|
67988
|
+
const textForAction = actionText(action);
|
|
67989
|
+
return /\b(sign\s*in|log\s*in|login|continue|submit|done|next|confirm)\b/.test(textForAction);
|
|
67990
|
+
}
|
|
67991
|
+
function isKeyboardFormCommit(action) {
|
|
67992
|
+
return action.type === "TYPE" && (action.value ?? "").endsWith("\n");
|
|
67993
|
+
}
|
|
67994
|
+
function isFormCommitAction(action) {
|
|
67995
|
+
if (isKeyboardFormCommit(action))
|
|
67996
|
+
return true;
|
|
67997
|
+
if (isCredentialSubmitTap(action))
|
|
67998
|
+
return true;
|
|
67999
|
+
if (action.type !== "TAP")
|
|
68000
|
+
return false;
|
|
68001
|
+
if (isNavigationChromeTap(action) || isAuthMethodChooserTap(action))
|
|
68002
|
+
return false;
|
|
68003
|
+
const textForAction = actionText(action);
|
|
68004
|
+
return /\b(save|create|send|confirm|next|apply|post|checkout|pay|finish|update|register|sign\s*up|done|continue|submit)\b/i.test(textForAction);
|
|
68005
|
+
}
|
|
67886
68006
|
function actionText(action) {
|
|
67887
68007
|
const bundle = action.metadata.locatorBundle;
|
|
67888
68008
|
return [
|
|
@@ -67899,9 +68019,80 @@ ${statePacket}` },
|
|
|
67899
68019
|
bundle?.xpath
|
|
67900
68020
|
].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").toLowerCase();
|
|
67901
68021
|
}
|
|
68022
|
+
function isStructuralOnlyAssert(action) {
|
|
68023
|
+
if (action.type !== "ASSERT_VISIBLE" && action.type !== "ASSERT_TEXT")
|
|
68024
|
+
return false;
|
|
68025
|
+
const sel = (action.selector ?? "").trim();
|
|
68026
|
+
if (/^\/hierarchy(\[\d+\])?(\/android\.widget\.FrameLayout\[\d+\])*$/i.test(sel))
|
|
68027
|
+
return true;
|
|
68028
|
+
if (isStructuralAssertionIdentity(sel) || isStructuralAssertionIdentity(action.metadata.targetLabel)) {
|
|
68029
|
+
const bundle = action.metadata.locatorBundle;
|
|
68030
|
+
const human = Boolean(nonEmpty2(action.innerText) || nonEmpty2(action.metadata.targetLabel) || nonEmpty2(bundle?.text) || nonEmpty2(bundle?.contentDesc));
|
|
68031
|
+
if (!human)
|
|
68032
|
+
return true;
|
|
68033
|
+
}
|
|
68034
|
+
if (/^\/hierarchy/i.test(sel)) {
|
|
68035
|
+
const bundle = action.metadata.locatorBundle;
|
|
68036
|
+
const hasIdentity = Boolean(nonEmpty2(action.metadata.targetLabel) || nonEmpty2(action.innerText) || nonEmpty2(bundle?.text) || nonEmpty2(bundle?.contentDesc) || nonEmpty2(bundle?.accessibilityId) || nonEmpty2(bundle?.resourceId) && !isStructuralAssertionIdentity(bundle?.resourceId));
|
|
68037
|
+
if (!hasIdentity)
|
|
68038
|
+
return true;
|
|
68039
|
+
}
|
|
68040
|
+
return false;
|
|
68041
|
+
}
|
|
68042
|
+
function collapseConsecutiveBacks(actions) {
|
|
68043
|
+
if (actions.length <= 1)
|
|
68044
|
+
return actions;
|
|
68045
|
+
const out = [actions[0]];
|
|
68046
|
+
for (let i = 1; i < actions.length; i++) {
|
|
68047
|
+
if (actions[i].type === "BACK" && out[out.length - 1].type === "BACK")
|
|
68048
|
+
continue;
|
|
68049
|
+
out.push(actions[i]);
|
|
68050
|
+
}
|
|
68051
|
+
return out;
|
|
68052
|
+
}
|
|
67902
68053
|
function scenarioIntentMismatchReason(scenario, actions) {
|
|
67903
68054
|
const text = scenarioText(scenario);
|
|
67904
|
-
|
|
68055
|
+
if (isInspectionOnlyScenario(scenario, actions)) {
|
|
68056
|
+
return null;
|
|
68057
|
+
}
|
|
68058
|
+
if (isNavigationFocusedScenario(scenario)) {
|
|
68059
|
+
return null;
|
|
68060
|
+
}
|
|
68061
|
+
if (isFormValidationScenario(scenario)) {
|
|
68062
|
+
const typedFields = actions.some((a) => a.type === "TYPE");
|
|
68063
|
+
const hasCommit = actions.some((a) => isFormCommitAction(a));
|
|
68064
|
+
const hasValidationAssert = actions.some((a) => {
|
|
68065
|
+
if (a.type !== "ASSERT_VISIBLE" && a.type !== "ASSERT_TEXT")
|
|
68066
|
+
return false;
|
|
68067
|
+
return /\b(error|required|invalid|empty|missing|incorrect|wrong|try again|mismatch|does not match|must match)\b/.test(actionText(a));
|
|
68068
|
+
});
|
|
68069
|
+
const emptyFieldIntent = /\b(empty|required|missing|blank)\b/.test(text);
|
|
68070
|
+
const invalidValueIntent = /\b(wrong|incorrect|invalid|mismatch|reject)\b/.test(text);
|
|
68071
|
+
if (emptyFieldIntent && !invalidValueIntent) {
|
|
68072
|
+
if (!hasCommit && !hasValidationAssert) {
|
|
68073
|
+
return "validation scenario did not record a form commit or validation outcome";
|
|
68074
|
+
}
|
|
68075
|
+
return null;
|
|
68076
|
+
}
|
|
68077
|
+
if (typedFields && !hasCommit) {
|
|
68078
|
+
return "validation scenario filled fields but never committed the form";
|
|
68079
|
+
}
|
|
68080
|
+
if (!hasCommit && !hasValidationAssert) {
|
|
68081
|
+
return "validation scenario did not record a form commit or validation outcome";
|
|
68082
|
+
}
|
|
68083
|
+
return null;
|
|
68084
|
+
}
|
|
68085
|
+
if (isFormMutationHappyPath(scenario)) {
|
|
68086
|
+
const hasType = actions.some((a) => a.type === "TYPE");
|
|
68087
|
+
const hasCommit = actions.some((a) => isFormCommitAction(a));
|
|
68088
|
+
if (!hasType) {
|
|
68089
|
+
return "form-mutation happy path did not record any field entry";
|
|
68090
|
+
}
|
|
68091
|
+
if (!hasCommit) {
|
|
68092
|
+
return "form-mutation happy path did not record a form commit";
|
|
68093
|
+
}
|
|
68094
|
+
}
|
|
68095
|
+
const isCredentialSignIn = /\b(sign\s*in|log\s*in|login|authenticate|auth)\b/.test(text) && /\b(email|e-mail|password|credential|valid credentials?)\b/.test(text) && !/\b(reset|forgot|recover|logout|log\s*out|sign\s*out|change\s+password|update\s+password)\b/.test(text);
|
|
67905
68096
|
const typedEmailIndexes = [];
|
|
67906
68097
|
const typedPasswordIndexes = [];
|
|
67907
68098
|
actions.forEach((action, index) => {
|
|
@@ -67918,19 +68109,19 @@ ${statePacket}` },
|
|
|
67918
68109
|
const lastCredentialIndex = Math.max(...typedEmailIndexes, ...typedPasswordIndexes);
|
|
67919
68110
|
const hasAnyCredentialEntry = Number.isFinite(lastCredentialIndex);
|
|
67920
68111
|
const submitAfterCredentials = actions.some((action, index) => {
|
|
67921
|
-
if (
|
|
68112
|
+
if (index < lastCredentialIndex)
|
|
67922
68113
|
return false;
|
|
67923
|
-
|
|
67924
|
-
if (!/\b(sign\s*in|log\s*in|login|continue|submit)\b/.test(textForAction))
|
|
68114
|
+
if (!isFormCommitAction(action))
|
|
67925
68115
|
return false;
|
|
67926
|
-
|
|
68116
|
+
if (action.type === "TYPE" && index === lastCredentialIndex)
|
|
68117
|
+
return true;
|
|
68118
|
+
return index > lastCredentialIndex;
|
|
67927
68119
|
});
|
|
67928
68120
|
const isAuthLikeScenario = /\b(sign(?:ed)?\s*in|log(?:ged)?\s*in|login|authenticate|auth|credential|email|e-mail|password)\b/.test(text);
|
|
67929
68121
|
if (isAuthLikeScenario && hasAnyCredentialEntry && !submitAfterCredentials) {
|
|
67930
|
-
return "credential fields were filled but no
|
|
68122
|
+
return "credential fields were filled but no form commit was recorded after them";
|
|
67931
68123
|
}
|
|
67932
68124
|
const uiOnlyCredentialScenario = /\b(mask|masked|visibility|show password|hide password|toggle|keyboard|keypad|max(?:imum)?\s*length|field focus|autofill)\b/.test(text);
|
|
67933
|
-
const emptyFieldValidationScenario = /\b(empty|required|missing|blank)\b/.test(text);
|
|
67934
68125
|
const sessionLifecycleScenario = /\b(logout|log\s*out|sign\s*out|session persistence|remain(?:s)? signed in|remember me)\b/.test(text);
|
|
67935
68126
|
const loginTapWithoutCredentialPath = actions.some((action) => {
|
|
67936
68127
|
if (action.type !== "TAP")
|
|
@@ -67938,9 +68129,11 @@ ${statePacket}` },
|
|
|
67938
68129
|
const textForAction = actionText(action);
|
|
67939
68130
|
if (/\b(logout|log\s*out|sign\s*out)\b/.test(textForAction))
|
|
67940
68131
|
return false;
|
|
68132
|
+
if (isAuthMethodChooserTap(action) || isNavigationChromeTap(action))
|
|
68133
|
+
return false;
|
|
67941
68134
|
return /\b(sign\s*in|log\s*in|login)\b/.test(textForAction);
|
|
67942
68135
|
});
|
|
67943
|
-
if (loginTapWithoutCredentialPath && !hasAnyCredentialEntry && !uiOnlyCredentialScenario &&
|
|
68136
|
+
if (loginTapWithoutCredentialPath && !hasAnyCredentialEntry && !uiOnlyCredentialScenario && (sessionLifecycleScenario || /\b(valid credentials?|happy path|successful sign\s*in|successful login)\b/.test(text))) {
|
|
67944
68137
|
return "auth/session scenario tapped a login or sign-in control without recording a credential path";
|
|
67945
68138
|
}
|
|
67946
68139
|
if (isCredentialSignIn) {
|
|
@@ -67952,7 +68145,7 @@ ${statePacket}` },
|
|
|
67952
68145
|
const missing = [
|
|
67953
68146
|
typedEmail ? null : "email/username entry",
|
|
67954
68147
|
typedPassword ? null : "password entry",
|
|
67955
|
-
tappedSubmit ? null : "
|
|
68148
|
+
tappedSubmit ? null : "form commit (submit tap or keyboard Enter)"
|
|
67956
68149
|
].filter((value) => value !== null);
|
|
67957
68150
|
return `credential sign-in scenario did not record ${missing.join(", ")}`;
|
|
67958
68151
|
}
|
|
@@ -67973,11 +68166,12 @@ ${statePacket}` },
|
|
|
67973
68166
|
const typeScreen = actions[lastTypeIndex]?.metadata?.screenName?.trim() ?? "";
|
|
67974
68167
|
const assertScreen = actions[lastAssertIndex]?.metadata?.screenName?.trim() ?? "";
|
|
67975
68168
|
const screensDiffer = typeScreen !== "" && assertScreen !== "" && typeScreen !== assertScreen;
|
|
67976
|
-
const
|
|
67977
|
-
|
|
68169
|
+
const lastTypeAction = actions[lastTypeIndex];
|
|
68170
|
+
const keyboardCommitOnLastType = lastTypeAction != null && isKeyboardFormCommit(lastTypeAction);
|
|
68171
|
+
const commitAfterType = keyboardCommitOnLastType || actions.some((action, index) => {
|
|
68172
|
+
if (index <= lastTypeIndex || index >= lastAssertIndex)
|
|
67978
68173
|
return false;
|
|
67979
|
-
|
|
67980
|
-
return /\b(sign\s*in|log\s*in|login|continue|submit|save|create|send|confirm|done|next|apply|post|checkout|pay|finish|update|register|sign\s*up)\b/i.test(textForAction);
|
|
68174
|
+
return isFormCommitAction(action);
|
|
67981
68175
|
});
|
|
67982
68176
|
if (screensDiffer && !commitAfterType) {
|
|
67983
68177
|
return "form fields were filled then a success assert was recorded without a commit/submit tap between them";
|
|
@@ -67988,6 +68182,17 @@ ${statePacket}` },
|
|
|
67988
68182
|
return `${action.type}|${action.selector}|${action.value ?? ""}`;
|
|
67989
68183
|
}
|
|
67990
68184
|
var MAX_RETRY_CYCLE_LENGTH = 8;
|
|
68185
|
+
function dedupeConsecutiveIdenticalActions(actions) {
|
|
68186
|
+
if (actions.length <= 1)
|
|
68187
|
+
return actions;
|
|
68188
|
+
const out = [actions[0]];
|
|
68189
|
+
for (let i = 1; i < actions.length; i++) {
|
|
68190
|
+
if (actionCycleKey(actions[i]) === actionCycleKey(out[out.length - 1]))
|
|
68191
|
+
continue;
|
|
68192
|
+
out.push(actions[i]);
|
|
68193
|
+
}
|
|
68194
|
+
return out;
|
|
68195
|
+
}
|
|
67991
68196
|
function collapseRetryCycles(actions) {
|
|
67992
68197
|
const out = [...actions];
|
|
67993
68198
|
let changed = true;
|
|
@@ -68028,15 +68233,30 @@ ${statePacket}` },
|
|
|
68028
68233
|
}
|
|
68029
68234
|
function assembleTest(args) {
|
|
68030
68235
|
const { scenario, recording, ctx, platform: platform3, appDataCleared, log: log2 } = args;
|
|
68031
|
-
let actions =
|
|
68236
|
+
let actions = dedupeConsecutiveIdenticalActions(recording.actions);
|
|
68032
68237
|
if (actions.length < recording.actions.length) {
|
|
68033
|
-
log2(`
|
|
68238
|
+
log2(` deduped ${recording.actions.length - actions.length} consecutive identical step(s) (${recording.actions.length} \u2192 ${actions.length}).`);
|
|
68239
|
+
}
|
|
68240
|
+
const beforeBackCollapse = actions.length;
|
|
68241
|
+
actions = collapseConsecutiveBacks(actions);
|
|
68242
|
+
if (actions.length < beforeBackCollapse) {
|
|
68243
|
+
log2(` collapsed ${beforeBackCollapse - actions.length} consecutive BACK step(s) (${beforeBackCollapse} \u2192 ${actions.length}).`);
|
|
68244
|
+
}
|
|
68245
|
+
const beforeCycleCollapse = actions.length;
|
|
68246
|
+
actions = collapseRetryCycles(actions);
|
|
68247
|
+
if (actions.length < beforeCycleCollapse) {
|
|
68248
|
+
log2(` collapsed ${beforeCycleCollapse - actions.length} retry-cycle step(s) out of the recorded trace (${beforeCycleCollapse} \u2192 ${actions.length}).`);
|
|
68034
68249
|
}
|
|
68035
68250
|
const beforeContainerDrop = actions.length;
|
|
68036
68251
|
actions = actions.filter((a) => !isBareContainerTap(a));
|
|
68037
68252
|
if (actions.length < beforeContainerDrop) {
|
|
68038
68253
|
log2(` dropped ${beforeContainerDrop - actions.length} bare-container tap step(s) (no identity \u2014 un-replayable ambient taps).`);
|
|
68039
68254
|
}
|
|
68255
|
+
const beforeWeakAssertDrop = actions.length;
|
|
68256
|
+
actions = actions.filter((a) => !isStructuralOnlyAssert(a));
|
|
68257
|
+
if (actions.length < beforeWeakAssertDrop) {
|
|
68258
|
+
log2(` dropped ${beforeWeakAssertDrop - actions.length} structural-only assert step(s) (no product identity).`);
|
|
68259
|
+
}
|
|
68040
68260
|
const intentMismatch = scenarioIntentMismatchReason(scenario, actions);
|
|
68041
68261
|
if (intentMismatch) {
|
|
68042
68262
|
log2(` skipped scenario "${scenario.name}": ${intentMismatch}.`);
|
|
@@ -68848,10 +69068,22 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
68848
69068
|
if (!/\b(tap|click|press)\b/i.test(step.action))
|
|
68849
69069
|
return false;
|
|
68850
69070
|
const text = mobileStepSearchText(step);
|
|
68851
|
-
if (/\b(logout|log\s*out|sign\s*out|reset|forgot|
|
|
69071
|
+
if (/\b(logout|log\s*out|sign\s*out|reset|forgot|go\s+back|back\s+to|cancel|close|dismiss)\b/.test(text)) {
|
|
68852
69072
|
return false;
|
|
68853
69073
|
}
|
|
68854
|
-
|
|
69074
|
+
if (/\b(continue\s+with|sign\s+in\s+with|log\s+in\s+with|use\s+(email|phone)|choose|pick|select)\b/.test(text) && !/\b(submit|confirm)\b/.test(text)) {
|
|
69075
|
+
return false;
|
|
69076
|
+
}
|
|
69077
|
+
return /\b(sign\s*in|signin|log\s*in|login|continue|submit|done|next|confirm)\b/.test(text);
|
|
69078
|
+
}
|
|
69079
|
+
function isKeyboardFormCommitStep(step) {
|
|
69080
|
+
if (!/\b(type|fill|input|enter)\b/i.test(step.action))
|
|
69081
|
+
return false;
|
|
69082
|
+
const value = typeof step.value === "string" ? step.value : "";
|
|
69083
|
+
return value.endsWith("\n");
|
|
69084
|
+
}
|
|
69085
|
+
function isFormCommitStep(step) {
|
|
69086
|
+
return isCredentialSubmitTap(step) || isKeyboardFormCommitStep(step);
|
|
68855
69087
|
}
|
|
68856
69088
|
function detectInvalidAuthValidationSetup(steps, error2) {
|
|
68857
69089
|
if (!/\b(element not found|no such element|not visible|timeout|assert|could not be located|unexpected screen|screen drift)\b/i.test(error2 ?? "")) {
|
|
@@ -68876,20 +69108,23 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
68876
69108
|
if ((step.order ?? -1) >= lastCredentialEditOrder)
|
|
68877
69109
|
return false;
|
|
68878
69110
|
const text = mobileStepSearchText(step);
|
|
68879
|
-
return /\b(
|
|
69111
|
+
return /\b(sign\s+in\s+with\s+email|log\s+in\s+with\s+email|email\s+and\s+password|login\s+form|credential\s+form|email|e-mail|username|user\s*name)\b/.test(text);
|
|
68880
69112
|
});
|
|
68881
69113
|
if (hasCredentialFormEvidence)
|
|
68882
69114
|
return null;
|
|
68883
69115
|
return "credential-control test edits a credential field without recording a path to the email/password form; stale auth-control test is invalid until generation records a complete form path";
|
|
68884
69116
|
}
|
|
68885
|
-
const hasSubmitAfterCredentialEdit = steps.some((step) => (step.order ?? -1) > lastCredentialEditOrder &&
|
|
69117
|
+
const hasSubmitAfterCredentialEdit = steps.some((step) => (step.order ?? -1) > lastCredentialEditOrder && isFormCommitStep(step));
|
|
68886
69118
|
if (hasSubmitAfterCredentialEdit)
|
|
68887
69119
|
return null;
|
|
69120
|
+
const hasKeyboardCommitOnCredential = steps.some((step) => (step.order ?? -1) === lastCredentialEditOrder && isKeyboardFormCommitStep(step));
|
|
69121
|
+
if (hasKeyboardCommitOnCredential)
|
|
69122
|
+
return null;
|
|
68888
69123
|
const hasValidationAssertion = steps.some((step) => {
|
|
68889
69124
|
if (!/\b(assert|wait)\b/i.test(step.action))
|
|
68890
69125
|
return false;
|
|
68891
69126
|
const text = mobileStepSearchText(step);
|
|
68892
|
-
return /\b(invalid|required|error|empty|missing|incorrect|wrong|try again|validation|must enter|enter (an )?(email|password)|email is required|password is required)\b/.test(text);
|
|
69127
|
+
return /\b(invalid|required|error|empty|missing|incorrect|wrong|try again|validation|must enter|enter (an )?(email|password)|email is required|password is required|mismatch|does not match)\b/.test(text);
|
|
68893
69128
|
});
|
|
68894
69129
|
if (hasValidationAssertion)
|
|
68895
69130
|
return null;
|
|
@@ -298745,7 +298980,8 @@ var require_bootstrap_grok_auth = __commonJS({
|
|
|
298745
298980
|
const home = process.env.HOME ?? "/home/appuser";
|
|
298746
298981
|
const authPath = node_path_1.default.join(home, ".grok", "auth.json");
|
|
298747
298982
|
const b64 = process.env.GROK_AUTH_JSON_B64;
|
|
298748
|
-
|
|
298983
|
+
const force = process.env.GROK_AUTH_FORCE_RESEED === "true" || process.env.GROK_AUTH_FORCE_RESEED === "1";
|
|
298984
|
+
if (!force && node_fs_1.default.existsSync(authPath)) {
|
|
298749
298985
|
return;
|
|
298750
298986
|
}
|
|
298751
298987
|
if (!b64) {
|
|
@@ -298756,7 +298992,7 @@ var require_bootstrap_grok_auth = __commonJS({
|
|
|
298756
298992
|
node_fs_1.default.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
298757
298993
|
node_fs_1.default.writeFileSync(authPath, Buffer.from(b64, "base64"));
|
|
298758
298994
|
node_fs_1.default.chmodSync(authPath, 384);
|
|
298759
|
-
console.log(`[grok-auth][runner] Seeded ${authPath} from GROK_AUTH_JSON_B64`);
|
|
298995
|
+
console.log(force ? `[grok-auth][runner] Force-reseeded ${authPath} from GROK_AUTH_JSON_B64` : `[grok-auth][runner] Seeded ${authPath} from GROK_AUTH_JSON_B64`);
|
|
298760
298996
|
} catch (err) {
|
|
298761
298997
|
const message = err instanceof Error ? err.message : String(err);
|
|
298762
298998
|
console.warn(`[grok-auth][runner] Failed to seed auth.json: ${message}`);
|
|
@@ -299401,6 +299637,37 @@ var require_proven_actions_adapter = __commonJS({
|
|
|
299401
299637
|
}
|
|
299402
299638
|
});
|
|
299403
299639
|
|
|
299640
|
+
// ../runner-core/dist/services/agentic-cli-constants.js
|
|
299641
|
+
var require_agentic_cli_constants = __commonJS({
|
|
299642
|
+
"../runner-core/dist/services/agentic-cli-constants.js"(exports) {
|
|
299643
|
+
"use strict";
|
|
299644
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
299645
|
+
exports.GROK_CLI_DEFAULT_REASONING_EFFORT = exports.GROK_CLI_DEFAULT_MODEL = exports.CLAUDE_CODE_DEFAULT_EFFORT = exports.CLAUDE_CODE_DEFAULT_MODEL = exports.MODEL_CLAUDE_CODE = exports.MODEL_GROK_BUILD = void 0;
|
|
299646
|
+
exports.isGrokBuildModel = isGrokBuildModel;
|
|
299647
|
+
exports.isClaudeCodeModel = isClaudeCodeModel;
|
|
299648
|
+
exports.getAgenticEngine = getAgenticEngine;
|
|
299649
|
+
exports.MODEL_GROK_BUILD = "grok-build";
|
|
299650
|
+
function isGrokBuildModel(id) {
|
|
299651
|
+
return id === exports.MODEL_GROK_BUILD;
|
|
299652
|
+
}
|
|
299653
|
+
exports.MODEL_CLAUDE_CODE = "claude-code";
|
|
299654
|
+
function isClaudeCodeModel(id) {
|
|
299655
|
+
return id === exports.MODEL_CLAUDE_CODE;
|
|
299656
|
+
}
|
|
299657
|
+
function getAgenticEngine(modelId) {
|
|
299658
|
+
if (isGrokBuildModel(modelId))
|
|
299659
|
+
return "grok-build";
|
|
299660
|
+
if (isClaudeCodeModel(modelId))
|
|
299661
|
+
return "claude-code";
|
|
299662
|
+
return "native";
|
|
299663
|
+
}
|
|
299664
|
+
exports.CLAUDE_CODE_DEFAULT_MODEL = "claude-sonnet-5";
|
|
299665
|
+
exports.CLAUDE_CODE_DEFAULT_EFFORT = "max";
|
|
299666
|
+
exports.GROK_CLI_DEFAULT_MODEL = "grok-4.5";
|
|
299667
|
+
exports.GROK_CLI_DEFAULT_REASONING_EFFORT = "high";
|
|
299668
|
+
}
|
|
299669
|
+
});
|
|
299670
|
+
|
|
299404
299671
|
// ../runner-core/dist/services/grok-cli.js
|
|
299405
299672
|
var require_grok_cli = __commonJS({
|
|
299406
299673
|
"../runner-core/dist/services/grok-cli.js"(exports) {
|
|
@@ -299411,6 +299678,7 @@ var require_grok_cli = __commonJS({
|
|
|
299411
299678
|
exports.runGrok = runGrok;
|
|
299412
299679
|
var node_child_process_1 = __require("child_process");
|
|
299413
299680
|
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
299681
|
+
var agentic_cli_constants_js_1 = require_agentic_cli_constants();
|
|
299414
299682
|
function extractAssistantJson(stdout) {
|
|
299415
299683
|
const tryParse = (s) => {
|
|
299416
299684
|
const trimmed = s.trim();
|
|
@@ -299505,7 +299773,8 @@ var require_grok_cli = __commonJS({
|
|
|
299505
299773
|
function runGrok(options) {
|
|
299506
299774
|
const {
|
|
299507
299775
|
prompt,
|
|
299508
|
-
model =
|
|
299776
|
+
model = agentic_cli_constants_js_1.GROK_CLI_DEFAULT_MODEL,
|
|
299777
|
+
reasoningEffort = agentic_cli_constants_js_1.GROK_CLI_DEFAULT_REASONING_EFFORT,
|
|
299509
299778
|
maxTurns,
|
|
299510
299779
|
// undefined = let grok decide when to quit
|
|
299511
299780
|
timeoutMs = 6e5,
|
|
@@ -299534,6 +299803,9 @@ var require_grok_cli = __commonJS({
|
|
|
299534
299803
|
"--always-approve",
|
|
299535
299804
|
"--no-alt-screen"
|
|
299536
299805
|
];
|
|
299806
|
+
if (reasoningEffort) {
|
|
299807
|
+
args.push("--reasoning-effort", reasoningEffort);
|
|
299808
|
+
}
|
|
299537
299809
|
if (typeof maxTurns === "number" && maxTurns > 0) {
|
|
299538
299810
|
args.push("--max-turns", String(maxTurns));
|
|
299539
299811
|
}
|
|
@@ -299679,7 +299951,7 @@ var require_grok_per_test_healer = __commonJS({
|
|
|
299679
299951
|
var HEAL_MCP_CAPABILITIES = ["core", "core-navigation", "core-input", "core-tabs", "testing"];
|
|
299680
299952
|
var grok_cli_js_1 = require_grok_cli();
|
|
299681
299953
|
var DEFAULT_MAX_ATTEMPTS = 3;
|
|
299682
|
-
var
|
|
299954
|
+
var DEFAULT_TIMEOUT_MS2 = 30 * 60 * 1e3;
|
|
299683
299955
|
var IDLE_WATCHDOG_MS = 10 * 60 * 1e3;
|
|
299684
299956
|
function getMaxAttempts() {
|
|
299685
299957
|
const raw = process.env.GROK_PER_TEST_HEAL_MAX_ATTEMPTS;
|
|
@@ -299689,7 +299961,7 @@ var require_grok_per_test_healer = __commonJS({
|
|
|
299689
299961
|
function getAttemptTimeoutMs() {
|
|
299690
299962
|
const raw = process.env.GROK_PER_TEST_HEAL_TIMEOUT_MS;
|
|
299691
299963
|
const parsed = raw ? parseInt(raw, 10) : NaN;
|
|
299692
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed :
|
|
299964
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TIMEOUT_MS2;
|
|
299693
299965
|
}
|
|
299694
299966
|
async function executeGrokPerTestHeal2(playwrightCode, steps, baseUrl, options = {}) {
|
|
299695
299967
|
const externalLogSink = options.onLog;
|
|
@@ -300102,7 +300374,7 @@ Output the JSON envelope exactly ONCE as your final message, then exit.`;
|
|
|
300102
300374
|
"-p",
|
|
300103
300375
|
'Read BRIEF.md in this directory. Heal TEST_UNDER_HEAL.spec.ts as instructed. Your FINAL assistant message MUST be ONLY a JSON object \u2014 no prose around it \u2014 with this shape: {"verdict":"fix|app_bug|config_issue|prereq_unmet|state_stale|test_invalid","playwrightCode":"<full file if verdict=fix>","changeDescription":"<one sentence>","reason":"<terse, when not fix>","confidence":"high|medium|low"}',
|
|
300104
300376
|
"-m",
|
|
300105
|
-
"grok-
|
|
300377
|
+
"grok-4.5",
|
|
300106
300378
|
// streaming-json: per-event NDJSON so the UI's GrokSessionLog drawer can
|
|
300107
300379
|
// show progress instead of a frozen "starting…" until the final envelope.
|
|
300108
300380
|
"--output-format",
|
|
@@ -301322,7 +301594,7 @@ When done with all ${input.tests.length} tests in this chunk, exit.`;
|
|
|
301322
301594
|
"-p",
|
|
301323
301595
|
"Read BRIEF.md in this directory. Heal every test under tests/*.spec.ts as instructed and emit a verdict file per test under verdicts/<testCaseId>.json (write to .tmp then rename \u2014 the runner is polling). When done, exit.",
|
|
301324
301596
|
"-m",
|
|
301325
|
-
"grok-
|
|
301597
|
+
"grok-4.5",
|
|
301326
301598
|
// streaming-json so per-token output reaches the UI for live progress.
|
|
301327
301599
|
"--output-format",
|
|
301328
301600
|
"streaming-json",
|
|
@@ -308811,47 +309083,6 @@ var require_sitemap_builder = __commonJS({
|
|
|
308811
309083
|
}
|
|
308812
309084
|
});
|
|
308813
309085
|
|
|
308814
|
-
// ../runner-core/dist/services/agentic-cli-constants.js
|
|
308815
|
-
var require_agentic_cli_constants = __commonJS({
|
|
308816
|
-
"../runner-core/dist/services/agentic-cli-constants.js"(exports) {
|
|
308817
|
-
"use strict";
|
|
308818
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
308819
|
-
exports.CLAUDE_CODE_MINIMAX_FALLBACK_CHAIN = exports.CLAUDE_CODE_MINIMAX_DEFAULT_MODEL = exports.CLAUDE_CODE_DEFAULT_MODEL = exports.MODEL_CLAUDE_CODE_MINIMAX = exports.MODEL_CLAUDE_CODE = exports.MODEL_GROK_BUILD = void 0;
|
|
308820
|
-
exports.isGrokBuildModel = isGrokBuildModel;
|
|
308821
|
-
exports.isClaudeCodeModel = isClaudeCodeModel;
|
|
308822
|
-
exports.isClaudeCodeMinimaxModel = isClaudeCodeMinimaxModel;
|
|
308823
|
-
exports.getAgenticEngine = getAgenticEngine;
|
|
308824
|
-
exports.MODEL_GROK_BUILD = "grok-build";
|
|
308825
|
-
function isGrokBuildModel(id) {
|
|
308826
|
-
return id === exports.MODEL_GROK_BUILD;
|
|
308827
|
-
}
|
|
308828
|
-
exports.MODEL_CLAUDE_CODE = "claude-code";
|
|
308829
|
-
function isClaudeCodeModel(id) {
|
|
308830
|
-
return id === exports.MODEL_CLAUDE_CODE;
|
|
308831
|
-
}
|
|
308832
|
-
exports.MODEL_CLAUDE_CODE_MINIMAX = "claude-code-minimax";
|
|
308833
|
-
function isClaudeCodeMinimaxModel(id) {
|
|
308834
|
-
return id === exports.MODEL_CLAUDE_CODE_MINIMAX;
|
|
308835
|
-
}
|
|
308836
|
-
function getAgenticEngine(modelId) {
|
|
308837
|
-
if (isGrokBuildModel(modelId))
|
|
308838
|
-
return "grok-build";
|
|
308839
|
-
if (isClaudeCodeMinimaxModel(modelId))
|
|
308840
|
-
return "claude-code-minimax";
|
|
308841
|
-
if (isClaudeCodeModel(modelId))
|
|
308842
|
-
return "claude-code";
|
|
308843
|
-
return "native";
|
|
308844
|
-
}
|
|
308845
|
-
exports.CLAUDE_CODE_DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
308846
|
-
exports.CLAUDE_CODE_MINIMAX_DEFAULT_MODEL = "MiniMax-M3";
|
|
308847
|
-
exports.CLAUDE_CODE_MINIMAX_FALLBACK_CHAIN = [
|
|
308848
|
-
"MiniMax-M3",
|
|
308849
|
-
"MiniMax-M2.7-highspeed",
|
|
308850
|
-
"MiniMax-M2.7"
|
|
308851
|
-
];
|
|
308852
|
-
}
|
|
308853
|
-
});
|
|
308854
|
-
|
|
308855
309086
|
// ../runner-core/dist/services/auth-capture-run.js
|
|
308856
309087
|
var require_auth_capture_run = __commonJS({
|
|
308857
309088
|
"../runner-core/dist/services/auth-capture-run.js"(exports) {
|
|
@@ -309655,7 +309886,7 @@ var require_grok_survey_runner = __commonJS({
|
|
|
309655
309886
|
var scrub_credentials_js_1 = require_scrub_credentials();
|
|
309656
309887
|
var agentic_survey_prompts_js_1 = require_agentic_survey_prompts();
|
|
309657
309888
|
var MCP_CAPABILITIES = ["core", "core-navigation", "core-input", "core-tabs", "testing"];
|
|
309658
|
-
var
|
|
309889
|
+
var DEFAULT_TIMEOUT_MS2 = 15 * 60 * 1e3;
|
|
309659
309890
|
var RETRY_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
309660
309891
|
function extractCredentials(vars) {
|
|
309661
309892
|
if (!vars)
|
|
@@ -310091,7 +310322,7 @@ var require_grok_survey_runner = __commonJS({
|
|
|
310091
310322
|
}
|
|
310092
310323
|
async function runGrokSurveyOnRunner(ctx, options = {}) {
|
|
310093
310324
|
const maxTurns = options.maxTurns;
|
|
310094
|
-
const timeoutMs = options.timeoutMs ??
|
|
310325
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
310095
310326
|
const logs2 = [];
|
|
310096
310327
|
const log2 = (line) => {
|
|
310097
310328
|
logs2.push(line);
|
|
@@ -310225,7 +310456,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
310225
310456
|
try {
|
|
310226
310457
|
const result = await (0, grok_cli_js_1.runGrok)({
|
|
310227
310458
|
prompt: kickoffPrompt,
|
|
310228
|
-
model: "grok-
|
|
310459
|
+
model: "grok-4.5",
|
|
310229
310460
|
maxTurns,
|
|
310230
310461
|
timeoutMs,
|
|
310231
310462
|
homeDir: grokHomeDir,
|
|
@@ -310272,7 +310503,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
310272
310503
|
try {
|
|
310273
310504
|
const retryResult = await (0, grok_cli_js_1.runGrok)({
|
|
310274
310505
|
prompt: fixPrompt,
|
|
310275
|
-
model: "grok-
|
|
310506
|
+
model: "grok-4.5",
|
|
310276
310507
|
// Retry: also let grok decide turns; the shorter retry budget lives
|
|
310277
310508
|
// in the wall-clock timeout below.
|
|
310278
310509
|
timeoutMs: RETRY_TIMEOUT_MS,
|
|
@@ -310405,20 +310636,19 @@ var require_canvas_detector = __commonJS({
|
|
|
310405
310636
|
return { kind: "override", canvasMode: false };
|
|
310406
310637
|
if (opts.engine === "claude-code")
|
|
310407
310638
|
return { kind: "canvas-mode", canvasMode: true };
|
|
310408
|
-
if (opts.engine === "
|
|
310639
|
+
if (opts.engine === "native")
|
|
310409
310640
|
return { kind: "bail" };
|
|
310410
310641
|
return { kind: "proceed", canvasMode: false };
|
|
310411
310642
|
}
|
|
310412
310643
|
function modelSupportsVision(modelId) {
|
|
310413
310644
|
if (!modelId)
|
|
310414
310645
|
return false;
|
|
310415
|
-
if (modelId === "claude-code-minimax")
|
|
310416
|
-
return false;
|
|
310417
310646
|
if (modelId === "claude-code")
|
|
310418
310647
|
return true;
|
|
310419
310648
|
if (modelId === "grok-build")
|
|
310420
310649
|
return false;
|
|
310421
310650
|
const visionCapable = /* @__PURE__ */ new Set([
|
|
310651
|
+
"claude-sonnet-5",
|
|
310422
310652
|
"claude-sonnet-4-6",
|
|
310423
310653
|
"claude-opus-4-7",
|
|
310424
310654
|
"claude-haiku-4-5",
|
|
@@ -311595,7 +311825,7 @@ var require_grok_discovery_runner = __commonJS({
|
|
|
311595
311825
|
"core-tabs",
|
|
311596
311826
|
"testing"
|
|
311597
311827
|
];
|
|
311598
|
-
var
|
|
311828
|
+
var DEFAULT_TIMEOUT_MS2 = 20 * 60 * 1e3;
|
|
311599
311829
|
var RETRY_TIMEOUT_MS = 6 * 60 * 1e3;
|
|
311600
311830
|
var DEFAULT_TARGET = { min: 6, max: 15 };
|
|
311601
311831
|
var WATCHER_DEBOUNCE_MS = 500;
|
|
@@ -311759,7 +311989,7 @@ var require_grok_discovery_runner = __commonJS({
|
|
|
311759
311989
|
const manifestFeatureRoutes = Array.isArray(manifestShape?.feature?.pages) ? manifestShape.feature.pages.filter((r) => typeof r === "string" && r.length > 0) : [];
|
|
311760
311990
|
const featureRoutes = manifestFeatureRoutes.length > 0 ? manifestFeatureRoutes : ctx.featureContext?.pageIntel?.map((p) => p.route) ?? ctx.focusAreas ?? [];
|
|
311761
311991
|
const targetTestCount = options.targetTestCount ?? DEFAULT_TARGET;
|
|
311762
|
-
const timeoutMs = options.timeoutMs ??
|
|
311992
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
311763
311993
|
const logs2 = [];
|
|
311764
311994
|
const log2 = (line) => {
|
|
311765
311995
|
logs2.push(line);
|
|
@@ -312364,7 +312594,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
312364
312594
|
try {
|
|
312365
312595
|
const result = await (0, grok_cli_js_1.runGrok)({
|
|
312366
312596
|
prompt: kickoffPrompt,
|
|
312367
|
-
model: "grok-
|
|
312597
|
+
model: "grok-4.5",
|
|
312368
312598
|
timeoutMs,
|
|
312369
312599
|
homeDir: grokHomeDir,
|
|
312370
312600
|
cwd: runDir,
|
|
@@ -312444,7 +312674,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
312444
312674
|
try {
|
|
312445
312675
|
const retryResult = await (0, grok_cli_js_1.runGrok)({
|
|
312446
312676
|
prompt: fixPrompt,
|
|
312447
|
-
model: "grok-
|
|
312677
|
+
model: "grok-4.5",
|
|
312448
312678
|
timeoutMs: RETRY_TIMEOUT_MS,
|
|
312449
312679
|
homeDir: grokHomeDir,
|
|
312450
312680
|
cwd: runDir,
|
|
@@ -312676,7 +312906,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
312676
312906
|
const manifestFeatureRoutes = Array.isArray(manifestShape?.feature?.pages) ? manifestShape.feature.pages.filter((r) => typeof r === "string" && r.length > 0) : [];
|
|
312677
312907
|
const featureRoutes = manifestFeatureRoutes.length > 0 ? manifestFeatureRoutes : ctx.featureContext?.pageIntel?.map((p) => p.route) ?? ctx.focusAreas ?? [];
|
|
312678
312908
|
const targetTestCount = options.targetTestCount ?? DEFAULT_TARGET;
|
|
312679
|
-
const callerTimeoutMs = options.timeoutMs ??
|
|
312909
|
+
const callerTimeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
312680
312910
|
const call1TimeoutMs = Math.max(callerTimeoutMs, GROK_V2_CALL1_TIMEOUT_MS);
|
|
312681
312911
|
const logs2 = [];
|
|
312682
312912
|
const log2 = (line) => {
|
|
@@ -313193,24 +313423,12 @@ var require_claude_cli = __commonJS({
|
|
|
313193
313423
|
return s.includes("authentication_failed") || s.includes("oauth_org_not_allowed") || s.includes("invalid_grant") || s.includes("401 unauthorized") || s.includes("invalid api key") || s.includes("invalid bearer token") || /\boauth token\b.*\b(expired|invalid)\b/.test(s) || /\b(token|credentials?)\b.*\bexpired\b/.test(s);
|
|
313194
313424
|
}
|
|
313195
313425
|
function runClaudeOnce(options) {
|
|
313196
|
-
const { prompt, model =
|
|
313426
|
+
const { prompt, model = agentic_cli_constants_js_1.CLAUDE_CODE_DEFAULT_MODEL, effort = agentic_cli_constants_js_1.CLAUDE_CODE_DEFAULT_EFFORT, provider = "anthropic", maxTurns, timeoutMs = 6e5, cwd, mcpConfigPath, addDirs, appendSystemPrompt, disallowedTools = ["Task"], onChunk, idleKillMs = 5 * 60 * 1e3, abortRef } = options;
|
|
313197
313427
|
return new Promise((resolve, reject) => {
|
|
313198
|
-
|
|
313199
|
-
|
|
313200
|
-
|
|
313201
|
-
|
|
313202
|
-
minimaxBaseUrl = process.env.MINIMAX_ANTHROPIC_BASE_URL;
|
|
313203
|
-
minimaxApiKey = process.env.MINIMAX_API_KEY;
|
|
313204
|
-
if (!minimaxBaseUrl || !minimaxApiKey) {
|
|
313205
|
-
reject(new ClaudeSpawnError("Claude Code (MiniMax) engine selected but MINIMAX_ANTHROPIC_BASE_URL and/or MINIMAX_API_KEY are not set. Set both as Railway env vars on the runner service.", -1, "", "", true));
|
|
313206
|
-
return;
|
|
313207
|
-
}
|
|
313208
|
-
} else {
|
|
313209
|
-
oauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
313210
|
-
if (!oauthToken) {
|
|
313211
|
-
reject(new ClaudeSpawnError("Claude Code engine selected but CLAUDE_CODE_OAUTH_TOKEN is not set. Set it as a Railway env var on the runner service.", -1, "", "", true));
|
|
313212
|
-
return;
|
|
313213
|
-
}
|
|
313428
|
+
const oauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
313429
|
+
if (!oauthToken) {
|
|
313430
|
+
reject(new ClaudeSpawnError("Claude Code engine selected but CLAUDE_CODE_OAUTH_TOKEN is not set. Set it as a Railway env var on the runner service.", -1, "", "", true));
|
|
313431
|
+
return;
|
|
313214
313432
|
}
|
|
313215
313433
|
const args = [
|
|
313216
313434
|
"-p",
|
|
@@ -313232,8 +313450,8 @@ var require_claude_cli = __commonJS({
|
|
|
313232
313450
|
// Stateless container — don't write session transcripts to disk.
|
|
313233
313451
|
"--no-session-persistence"
|
|
313234
313452
|
];
|
|
313235
|
-
if (
|
|
313236
|
-
args.push("--
|
|
313453
|
+
if (effort && effort.trim().length > 0) {
|
|
313454
|
+
args.push("--effort", effort.trim());
|
|
313237
313455
|
}
|
|
313238
313456
|
if (typeof maxTurns === "number" && maxTurns > 0) {
|
|
313239
313457
|
args.push("--max-turns", String(maxTurns));
|
|
@@ -313258,21 +313476,9 @@ var require_claude_cli = __commonJS({
|
|
|
313258
313476
|
MCP_TIMEOUT: process.env.MCP_TIMEOUT ?? "120000"
|
|
313259
313477
|
}
|
|
313260
313478
|
});
|
|
313261
|
-
|
|
313262
|
-
|
|
313263
|
-
|
|
313264
|
-
spawnEnv.ANTHROPIC_AUTH_TOKEN = minimaxApiKey;
|
|
313265
|
-
delete spawnEnv.CLAUDE_CODE_OAUTH_TOKEN;
|
|
313266
|
-
delete spawnEnv.ANTHROPIC_MODEL;
|
|
313267
|
-
delete spawnEnv.ANTHROPIC_DEFAULT_SONNET_MODEL;
|
|
313268
|
-
delete spawnEnv.ANTHROPIC_DEFAULT_OPUS_MODEL;
|
|
313269
|
-
delete spawnEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL;
|
|
313270
|
-
delete spawnEnv.ANTHROPIC_SMALL_FAST_MODEL;
|
|
313271
|
-
} else {
|
|
313272
|
-
spawnEnv.CLAUDE_CODE_OAUTH_TOKEN = oauthToken;
|
|
313273
|
-
delete spawnEnv.ANTHROPIC_API_KEY;
|
|
313274
|
-
delete spawnEnv.ANTHROPIC_AUTH_TOKEN;
|
|
313275
|
-
}
|
|
313479
|
+
spawnEnv.CLAUDE_CODE_OAUTH_TOKEN = oauthToken;
|
|
313480
|
+
delete spawnEnv.ANTHROPIC_API_KEY;
|
|
313481
|
+
delete spawnEnv.ANTHROPIC_AUTH_TOKEN;
|
|
313276
313482
|
const isWindows = process.platform === "win32";
|
|
313277
313483
|
const proc = (0, node_child_process_1.spawn)("claude", args, {
|
|
313278
313484
|
env: spawnEnv,
|
|
@@ -313384,7 +313590,7 @@ var require_claude_cli = __commonJS({
|
|
|
313384
313590
|
}
|
|
313385
313591
|
if (exitCode !== 0) {
|
|
313386
313592
|
const authFailure = looksLikeAuthFailure(stdout + "\n" + stderr);
|
|
313387
|
-
const msg = authFailure ?
|
|
313593
|
+
const msg = authFailure ? "claude session failed to authenticate \u2014 CLAUDE_CODE_OAUTH_TOKEN is missing, expired, or invalid. Regenerate it with `claude setup-token` and update the Railway env var on the runner service." : `claude exited with code ${exitCode}${signal ? ` (signal=${signal})` : ""}`;
|
|
313388
313594
|
reject(new ClaudeSpawnError(msg, exitCode, stderr.trim(), stdout.trim(), authFailure));
|
|
313389
313595
|
return;
|
|
313390
313596
|
}
|
|
@@ -313397,39 +313603,8 @@ var require_claude_cli = __commonJS({
|
|
|
313397
313603
|
});
|
|
313398
313604
|
});
|
|
313399
313605
|
}
|
|
313400
|
-
function minimaxClaudeFallbackModels(requested) {
|
|
313401
|
-
const chain = agentic_cli_constants_js_1.CLAUDE_CODE_MINIMAX_FALLBACK_CHAIN;
|
|
313402
|
-
const idx = chain.indexOf(requested);
|
|
313403
|
-
if (idx === -1)
|
|
313404
|
-
return [requested];
|
|
313405
|
-
return chain.slice(idx);
|
|
313406
|
-
}
|
|
313407
313606
|
async function runClaude(options) {
|
|
313408
|
-
|
|
313409
|
-
return runClaudeOnce(options);
|
|
313410
|
-
const requested = options.model ?? agentic_cli_constants_js_1.CLAUDE_CODE_MINIMAX_DEFAULT_MODEL;
|
|
313411
|
-
const chain = minimaxClaudeFallbackModels(requested);
|
|
313412
|
-
if (chain.length <= 1)
|
|
313413
|
-
return runClaudeOnce({ ...options, model: requested });
|
|
313414
|
-
let lastError;
|
|
313415
|
-
for (let i = 0; i < chain.length; i++) {
|
|
313416
|
-
const model = chain[i];
|
|
313417
|
-
const isLast = i === chain.length - 1;
|
|
313418
|
-
try {
|
|
313419
|
-
const result = await runClaudeOnce({ ...options, model });
|
|
313420
|
-
if (i > 0)
|
|
313421
|
-
console.warn(`[claude-cli] MiniMax CLI failover succeeded on ${model} (requested ${requested})`);
|
|
313422
|
-
return result;
|
|
313423
|
-
} catch (err) {
|
|
313424
|
-
lastError = err;
|
|
313425
|
-
const spawnErr = err instanceof ClaudeSpawnError ? err : null;
|
|
313426
|
-
if (isLast || spawnErr?.isAuthError || spawnErr?.isTimeout)
|
|
313427
|
-
throw err;
|
|
313428
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
313429
|
-
console.warn(`[claude-cli] MiniMax CLI ${model} failed (${reason}) \u2014 failing over to ${chain[i + 1]}`);
|
|
313430
|
-
}
|
|
313431
|
-
}
|
|
313432
|
-
throw lastError;
|
|
313607
|
+
return runClaudeOnce(options);
|
|
313433
313608
|
}
|
|
313434
313609
|
}
|
|
313435
313610
|
});
|
|
@@ -313814,6 +313989,8 @@ var require_claude_pricing = __commonJS({
|
|
|
313814
313989
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
313815
313990
|
exports.computeClaudeCostUsd = computeClaudeCostUsd;
|
|
313816
313991
|
var CLAUDE_PRICE_PER_M = {
|
|
313992
|
+
// Introductory pricing through 2026-08-31 ($2/$10); standard is $3/$15.
|
|
313993
|
+
"claude-sonnet-5": { input: 2, output: 10 },
|
|
313817
313994
|
"claude-sonnet-4-6": { input: 3, output: 15 },
|
|
313818
313995
|
"claude-haiku-4-5-20251001": { input: 1, output: 5 },
|
|
313819
313996
|
"claude-opus-4-7": { input: 5, output: 25 }
|
|
@@ -313853,7 +314030,7 @@ var require_claude_survey_runner = __commonJS({
|
|
|
313853
314030
|
var agentic_survey_prompts_js_1 = require_agentic_survey_prompts();
|
|
313854
314031
|
var MCP_CAPABILITIES = ["core", "core-navigation", "core-input", "core-tabs", "testing"];
|
|
313855
314032
|
var CANVAS_MODE_EXTRA_CAPABILITIES = ["vision"];
|
|
313856
|
-
var
|
|
314033
|
+
var DEFAULT_TIMEOUT_MS2 = 15 * 60 * 1e3;
|
|
313857
314034
|
var RETRY_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
313858
314035
|
async function logResultFile(path, log2) {
|
|
313859
314036
|
try {
|
|
@@ -313865,10 +314042,10 @@ ${preview}`);
|
|
|
313865
314042
|
log2("[diagnostic] result file does not exist \u2014 Claude never wrote it");
|
|
313866
314043
|
}
|
|
313867
314044
|
}
|
|
313868
|
-
async function runClaudeSurveyOnRunner(ctx, options = {}
|
|
314045
|
+
async function runClaudeSurveyOnRunner(ctx, options = {}) {
|
|
313869
314046
|
const maxTurns = options.maxTurns;
|
|
313870
|
-
const timeoutMs = options.timeoutMs ??
|
|
313871
|
-
const claudeDefaultModel =
|
|
314047
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
314048
|
+
const claudeDefaultModel = agentic_cli_constants_js_1.CLAUDE_CODE_DEFAULT_MODEL;
|
|
313872
314049
|
const logs2 = [];
|
|
313873
314050
|
const log2 = (line) => {
|
|
313874
314051
|
logs2.push(line);
|
|
@@ -314012,7 +314189,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
314012
314189
|
console.log(`[claude-survey] ${line}`);
|
|
314013
314190
|
};
|
|
314014
314191
|
const onAudit = options.onAudit;
|
|
314015
|
-
const claudeProviderLabel =
|
|
314192
|
+
const claudeProviderLabel = "anthropic";
|
|
314016
314193
|
const claudeModelLabel = claudeDefaultModel;
|
|
314017
314194
|
const surveyExploreCallType = "surveyExplore";
|
|
314018
314195
|
const sessionStartedAt = Date.now();
|
|
@@ -314048,7 +314225,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
314048
314225
|
groupId: auditGroupId,
|
|
314049
314226
|
callType: surveyExploreCallType,
|
|
314050
314227
|
model: claudeModelLabel,
|
|
314051
|
-
provider:
|
|
314228
|
+
provider: "anthropic",
|
|
314052
314229
|
durationMs: turn.durationMs ?? 0,
|
|
314053
314230
|
promptTokens,
|
|
314054
314231
|
completionTokens,
|
|
@@ -314091,7 +314268,6 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
314091
314268
|
const result = await (0, claude_cli_js_1.runClaude)({
|
|
314092
314269
|
prompt: kickoffPrompt,
|
|
314093
314270
|
model: claudeDefaultModel,
|
|
314094
|
-
provider,
|
|
314095
314271
|
maxTurns,
|
|
314096
314272
|
timeoutMs,
|
|
314097
314273
|
cwd: runDir,
|
|
@@ -314151,7 +314327,6 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
314151
314327
|
const retryResult = await (0, claude_cli_js_1.runClaude)({
|
|
314152
314328
|
prompt: fixPrompt,
|
|
314153
314329
|
model: claudeDefaultModel,
|
|
314154
|
-
provider,
|
|
314155
314330
|
timeoutMs: RETRY_TIMEOUT_MS,
|
|
314156
314331
|
cwd: runDir,
|
|
314157
314332
|
mcpConfigPath,
|
|
@@ -314207,7 +314382,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
314207
314382
|
groupId: auditGroupId,
|
|
314208
314383
|
callType: surveyExploreCallType,
|
|
314209
314384
|
model: claudeModelLabel,
|
|
314210
|
-
provider:
|
|
314385
|
+
provider: "anthropic",
|
|
314211
314386
|
durationMs: sessionDurationMs,
|
|
314212
314387
|
promptTokens: sessionInputTokens,
|
|
314213
314388
|
completionTokens: sessionOutputTokens,
|
|
@@ -314583,11 +314758,11 @@ var require_claude_discovery_adapter = __commonJS({
|
|
|
314583
314758
|
var PER_TEST_IDLE_KILL_MS = 10 * 60 * 1e3;
|
|
314584
314759
|
var PER_TEST_MAX_TURNS = 120;
|
|
314585
314760
|
function createClaudeDiscoveryAdapter(args) {
|
|
314586
|
-
const {
|
|
314761
|
+
const { claudeDefaultModel } = args;
|
|
314587
314762
|
return {
|
|
314588
|
-
engine:
|
|
314763
|
+
engine: "claude-code",
|
|
314589
314764
|
auditModel: claudeDefaultModel,
|
|
314590
|
-
auditProvider:
|
|
314765
|
+
auditProvider: "anthropic",
|
|
314591
314766
|
computeSessionCostUsd(usage) {
|
|
314592
314767
|
return (0, claude_pricing_js_1.computeClaudeCostUsd)(claudeDefaultModel, {
|
|
314593
314768
|
inputTokens: usage.inputTokens,
|
|
@@ -314628,7 +314803,7 @@ var require_claude_discovery_adapter = __commonJS({
|
|
|
314628
314803
|
prompt,
|
|
314629
314804
|
appendSystemPrompt,
|
|
314630
314805
|
model: claudeDefaultModel,
|
|
314631
|
-
provider,
|
|
314806
|
+
provider: "anthropic",
|
|
314632
314807
|
maxTurns: PER_TEST_MAX_TURNS,
|
|
314633
314808
|
// A per-test session needs only browser MCP + the file tools — never a
|
|
314634
314809
|
// shell. Disallow Bash to shrink the off-task surface (and Task, the
|
|
@@ -314700,17 +314875,17 @@ var require_claude_discovery_runner = __commonJS({
|
|
|
314700
314875
|
];
|
|
314701
314876
|
var CALL1_TIMEOUT_MS = 38 * 60 * 1e3;
|
|
314702
314877
|
var DISCOVERY_IDLE_KILL_MS = 15 * 60 * 1e3;
|
|
314703
|
-
var
|
|
314878
|
+
var DEFAULT_TIMEOUT_MS2 = 20 * 60 * 1e3;
|
|
314704
314879
|
var DEFAULT_TARGET = { min: 6, max: 15 };
|
|
314705
|
-
async function runClaudeDiscoveryOnRunner(ctx, callbackOptions, options = {}
|
|
314706
|
-
const claudeDefaultModel =
|
|
314707
|
-
const claudeProviderLabel =
|
|
314880
|
+
async function runClaudeDiscoveryOnRunner(ctx, callbackOptions, options = {}) {
|
|
314881
|
+
const claudeDefaultModel = agentic_cli_constants_js_1.CLAUDE_CODE_DEFAULT_MODEL;
|
|
314882
|
+
const claudeProviderLabel = "anthropic";
|
|
314708
314883
|
const featureName = ctx.featureName ?? "Untitled Feature";
|
|
314709
314884
|
const manifestShape = (0, grok_discovery_runner_js_1.narrowManifest)(ctx.featureContextManifest);
|
|
314710
314885
|
const manifestFeatureRoutes = Array.isArray(manifestShape?.feature?.pages) ? manifestShape.feature.pages.filter((r) => typeof r === "string" && r.length > 0) : [];
|
|
314711
314886
|
const featureRoutes = manifestFeatureRoutes.length > 0 ? manifestFeatureRoutes : ctx.featureContext?.pageIntel?.map((p) => p.route) ?? ctx.focusAreas ?? [];
|
|
314712
314887
|
const targetTestCount = options.targetTestCount ?? DEFAULT_TARGET;
|
|
314713
|
-
const callerTimeoutMs = options.timeoutMs ??
|
|
314888
|
+
const callerTimeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
314714
314889
|
const call1TimeoutMs = Math.max(callerTimeoutMs, CALL1_TIMEOUT_MS);
|
|
314715
314890
|
const logs2 = [];
|
|
314716
314891
|
const log2 = (line) => {
|
|
@@ -314957,7 +315132,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
314957
315132
|
groupId: ctx.sessionId ?? featureName,
|
|
314958
315133
|
callType: featureExploreCallType,
|
|
314959
315134
|
model: claudeModelLabel,
|
|
314960
|
-
provider:
|
|
315135
|
+
provider: "anthropic",
|
|
314961
315136
|
durationMs: turn.durationMs ?? 0,
|
|
314962
315137
|
promptTokens,
|
|
314963
315138
|
completionTokens,
|
|
@@ -315068,7 +315243,6 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
315068
315243
|
// they actually get put there.
|
|
315069
315244
|
appendSystemPrompt: canvasMode ? (0, agentic_discovery_prompts_js_1.buildAuthoringRules)() + (0, canvas_detector_js_1.buildCanvasModeAuthoringAddendum)() : (0, agentic_discovery_prompts_js_1.buildAuthoringRules)(),
|
|
315070
315245
|
model: claudeDefaultModel,
|
|
315071
|
-
provider,
|
|
315072
315246
|
timeoutMs: call1TimeoutMs,
|
|
315073
315247
|
idleKillMs: DISCOVERY_IDLE_KILL_MS,
|
|
315074
315248
|
cwd: runDir,
|
|
@@ -315178,7 +315352,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
315178
315352
|
groupId: ctx.sessionId ?? featureName,
|
|
315179
315353
|
callType: featureExploreCallType,
|
|
315180
315354
|
model: claudeModelLabel,
|
|
315181
|
-
provider:
|
|
315355
|
+
provider: "anthropic",
|
|
315182
315356
|
durationMs: sessionDurationMs,
|
|
315183
315357
|
promptTokens: sessionInputTokens,
|
|
315184
315358
|
completionTokens: sessionOutputTokens,
|
|
@@ -315270,7 +315444,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
315270
315444
|
},
|
|
315271
315445
|
onAudit,
|
|
315272
315446
|
auditGroupId: ctx.sessionId ?? featureName,
|
|
315273
|
-
adapter: (0, claude_discovery_adapter_js_1.createClaudeDiscoveryAdapter)({
|
|
315447
|
+
adapter: (0, claude_discovery_adapter_js_1.createClaudeDiscoveryAdapter)({ claudeDefaultModel })
|
|
315274
315448
|
});
|
|
315275
315449
|
log2(`Verify + Call 2: complete \u2014 ${capturedTests.length} test(s) saved, ${localUnverified.length} unverified`);
|
|
315276
315450
|
}
|
|
@@ -316196,7 +316370,7 @@ var require_discover_agent = __commonJS({
|
|
|
316196
316370
|
} else if (cached === "standard") {
|
|
316197
316371
|
return { renderType: "standard", canvasMode: false, probeRan: false };
|
|
316198
316372
|
} else {
|
|
316199
|
-
const bailEligible = engine === "native"
|
|
316373
|
+
const bailEligible = engine === "native";
|
|
316200
316374
|
const cannotConfirm = (reason) => {
|
|
316201
316375
|
const note = bailEligible ? ` \u26A0 engine=${engine} would have bailed on canvas \u2014 if discovery loops, set Settings \u2192 App renderer manually.` : "";
|
|
316202
316376
|
log2(`[canvas] preflight could not run: ${reason} \u2014 proceeding as standard.${note}`);
|
|
@@ -316515,7 +316689,7 @@ Respond with ONLY valid JSON:
|
|
|
316515
316689
|
// wiring is in place when it does.
|
|
316516
316690
|
onAudit: onAICall
|
|
316517
316691
|
};
|
|
316518
|
-
const cliResult = surveyEngine === "claude-code
|
|
316692
|
+
const cliResult = surveyEngine === "claude-code" ? await (0, claude_survey_runner_js_1.runClaudeSurveyOnRunner)(ctx, surveyOpts) : await (0, grok_survey_runner_js_1.runGrokSurveyOnRunner)(ctx, surveyOpts);
|
|
316519
316693
|
logs2.push(`${surveyEngine} survey runner finished cleanly.`);
|
|
316520
316694
|
onLog?.("done", logs2.join("\n"));
|
|
316521
316695
|
return {
|
|
@@ -317037,7 +317211,7 @@ Fatal error: ${msg}`);
|
|
|
317037
317211
|
// through the existing DiscoveryAuditCallback (→ aiAuditBatch).
|
|
317038
317212
|
onAudit: onAICall
|
|
317039
317213
|
};
|
|
317040
|
-
const cliResult = deepEngine === "claude-code
|
|
317214
|
+
const cliResult = deepEngine === "claude-code" ? await (0, claude_discovery_runner_js_1.runClaudeDiscoveryOnRunner)(ctx, deepCallbacks, deepOpts) : await (0, grok_discovery_runner_js_1.runGrokDiscoveryOnRunner)(ctx, deepCallbacks, deepOpts);
|
|
317041
317215
|
logs2.push(`${deepEngine} deep discovery finished cleanly. ${cliResult.testsEmitted} tests emitted, ${cliResult.unverified.length} unverified.`);
|
|
317042
317216
|
onLog?.("done", logs2.join("\n"));
|
|
317043
317217
|
return {
|
|
@@ -327917,9 +328091,8 @@ var require_framework_import_runner = __commonJS({
|
|
|
327917
328091
|
const imp = ctx.frameworkImport;
|
|
327918
328092
|
if (!imp)
|
|
327919
328093
|
throw new Error("runFrameworkImportOnRunner: ctx.frameworkImport is required");
|
|
327920
|
-
const
|
|
327921
|
-
const
|
|
327922
|
-
const model = provider === "minimax" ? agentic_cli_constants_js_1.CLAUDE_CODE_MINIMAX_DEFAULT_MODEL : agentic_cli_constants_js_1.CLAUDE_CODE_DEFAULT_MODEL;
|
|
328094
|
+
const provider = "anthropic";
|
|
328095
|
+
const model = agentic_cli_constants_js_1.CLAUDE_CODE_DEFAULT_MODEL;
|
|
327923
328096
|
log2("=== Framework Import Runner (Claude CLI, no browser) ===");
|
|
327924
328097
|
log2(`Source: ${imp.source}${imp.source === "github" ? ` (${imp.repoOwner}/${imp.repoName}@${imp.branch ?? "default"})` : ""}`);
|
|
327925
328098
|
const runDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "framework-import-"));
|
|
@@ -329049,8 +329222,8 @@ var require_repo_analysis_runner = __commonJS({
|
|
|
329049
329222
|
const maxEndpointsPerFeature = intEnv("REPO_ANALYSIS_MAX_ENDPOINTS_PER_FEATURE", 80, 1, 400);
|
|
329050
329223
|
const maxFacts = intEnv("REPO_ANALYSIS_MAX_FACTS", 40, 1, 120);
|
|
329051
329224
|
const engine = (0, agentic_cli_constants_js_1.getAgenticEngine)(ctx.exploreModel);
|
|
329052
|
-
const provider =
|
|
329053
|
-
const model =
|
|
329225
|
+
const provider = "anthropic";
|
|
329226
|
+
const model = agentic_cli_constants_js_1.CLAUDE_CODE_DEFAULT_MODEL;
|
|
329054
329227
|
log2("=== Repo Analysis Runner (Claude CLI, no browser) ===");
|
|
329055
329228
|
log2(`Source: ${imp.source}${imp.source === "github" ? ` (${imp.repoOwner}/${imp.repoName}@${imp.branch ?? "default"})` : ""}`);
|
|
329056
329229
|
const runDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "repo-analysis-"));
|
|
@@ -329533,7 +329706,7 @@ var require_process_inventory = __commonJS({
|
|
|
329533
329706
|
var node_util_1 = __require("util");
|
|
329534
329707
|
var execFileAsync3 = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
329535
329708
|
var DEFAULT_LIMIT = 60;
|
|
329536
|
-
var
|
|
329709
|
+
var DEFAULT_TIMEOUT_MS2 = 800;
|
|
329537
329710
|
var MAX_COMMAND_LENGTH = 320;
|
|
329538
329711
|
function roundOne(value) {
|
|
329539
329712
|
return Math.round(value * 10) / 10;
|
|
@@ -329682,7 +329855,7 @@ var require_process_inventory = __commonJS({
|
|
|
329682
329855
|
}
|
|
329683
329856
|
async function getBrowserProcessInventory2(options = {}) {
|
|
329684
329857
|
const limit = Math.max(1, Math.min(200, options.limit ?? DEFAULT_LIMIT));
|
|
329685
|
-
const timeoutMs = Math.max(100, Math.min(5e3, options.timeoutMs ??
|
|
329858
|
+
const timeoutMs = Math.max(100, Math.min(5e3, options.timeoutMs ?? DEFAULT_TIMEOUT_MS2));
|
|
329686
329859
|
try {
|
|
329687
329860
|
const rawProcesses = process.platform === "win32" ? await collectWindowsProcesses(timeoutMs) : await collectUnixProcesses(timeoutMs);
|
|
329688
329861
|
const processes = [...rawProcesses].sort((a, b) => (b.cpuPct ?? 0) - (a.cpuPct ?? 0) || (b.rssMB ?? 0) - (a.rssMB ?? 0)).slice(0, limit);
|
|
@@ -330649,7 +330822,7 @@ var require_package4 = __commonJS({
|
|
|
330649
330822
|
"package.json"(exports, module) {
|
|
330650
330823
|
module.exports = {
|
|
330651
330824
|
name: "@validate.qa/runner",
|
|
330652
|
-
version: "1.0.
|
|
330825
|
+
version: "1.0.35",
|
|
330653
330826
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
330654
330827
|
bin: {
|
|
330655
330828
|
"validate-runner": "dist/cli.js"
|
|
@@ -330657,6 +330830,7 @@ var require_package4 = __commonJS({
|
|
|
330657
330830
|
scripts: {
|
|
330658
330831
|
build: "tsup",
|
|
330659
330832
|
dev: "tsx src/cli.ts",
|
|
330833
|
+
test: "node --import tsx --test src/version-check.test.ts",
|
|
330660
330834
|
"test:mobile": "node --import tsx --test src/runner.mobile-ai-env.test.ts src/services/mobile/mobile-network-capture.smoke.test.ts src/services/mobile/mobile-host-proxy.test.ts",
|
|
330661
330835
|
prepublishOnly: "npm run build"
|
|
330662
330836
|
},
|
|
@@ -330785,6 +330959,136 @@ function shouldUseAuthState(tags, playwrightCode) {
|
|
|
330785
330959
|
return !tags.includes(INLINE_LOGIN_TAG) && !hasLegacyInlineLogin;
|
|
330786
330960
|
}
|
|
330787
330961
|
|
|
330962
|
+
// src/version-check.ts
|
|
330963
|
+
var RUNNER_NPM_PACKAGE = "@validate.qa/runner";
|
|
330964
|
+
var NPM_LATEST_URL = `https://registry.npmjs.org/${encodeURIComponent(RUNNER_NPM_PACKAGE)}/latest`;
|
|
330965
|
+
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
330966
|
+
function parseSemver(raw) {
|
|
330967
|
+
const cleaned = raw.trim().replace(/^v/i, "");
|
|
330968
|
+
const match = cleaned.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
|
|
330969
|
+
if (!match) return null;
|
|
330970
|
+
return {
|
|
330971
|
+
major: Number(match[1]),
|
|
330972
|
+
minor: Number(match[2]),
|
|
330973
|
+
patch: Number(match[3]),
|
|
330974
|
+
prerelease: match[4] ?? null
|
|
330975
|
+
};
|
|
330976
|
+
}
|
|
330977
|
+
function compareSemver(a, b) {
|
|
330978
|
+
const pa = parseSemver(a);
|
|
330979
|
+
const pb = parseSemver(b);
|
|
330980
|
+
if (!pa || !pb) return null;
|
|
330981
|
+
if (pa.major !== pb.major) return pa.major < pb.major ? -1 : 1;
|
|
330982
|
+
if (pa.minor !== pb.minor) return pa.minor < pb.minor ? -1 : 1;
|
|
330983
|
+
if (pa.patch !== pb.patch) return pa.patch < pb.patch ? -1 : 1;
|
|
330984
|
+
if (pa.prerelease === pb.prerelease) return 0;
|
|
330985
|
+
if (pa.prerelease === null) return 1;
|
|
330986
|
+
if (pb.prerelease === null) return -1;
|
|
330987
|
+
return pa.prerelease < pb.prerelease ? -1 : pa.prerelease > pb.prerelease ? 1 : 0;
|
|
330988
|
+
}
|
|
330989
|
+
function isVersionOutdated(current, latest) {
|
|
330990
|
+
const cmp = compareSemver(current, latest);
|
|
330991
|
+
return cmp === -1;
|
|
330992
|
+
}
|
|
330993
|
+
function shouldSkipVersionCheck() {
|
|
330994
|
+
if (process.env.RUNNER_CLOUD === "true") {
|
|
330995
|
+
return "cloud runner (RUNNER_CLOUD=true)";
|
|
330996
|
+
}
|
|
330997
|
+
if (process.env.VALIDATE_RUNNER_ALLOW_OUTDATED === "1" || process.env.VALIDATE_RUNNER_ALLOW_OUTDATED === "true") {
|
|
330998
|
+
return "VALIDATE_RUNNER_ALLOW_OUTDATED is set";
|
|
330999
|
+
}
|
|
331000
|
+
return null;
|
|
331001
|
+
}
|
|
331002
|
+
async function fetchLatestRunnerVersion(timeoutMs = DEFAULT_TIMEOUT_MS, fetchImpl = fetch) {
|
|
331003
|
+
const controller = new AbortController();
|
|
331004
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
331005
|
+
try {
|
|
331006
|
+
const res = await fetchImpl(NPM_LATEST_URL, {
|
|
331007
|
+
headers: {
|
|
331008
|
+
Accept: "application/json",
|
|
331009
|
+
// Identify ourselves; helps npm operators + caching intermediaries.
|
|
331010
|
+
"User-Agent": `${RUNNER_NPM_PACKAGE}-version-check`
|
|
331011
|
+
},
|
|
331012
|
+
signal: controller.signal
|
|
331013
|
+
});
|
|
331014
|
+
if (!res.ok) return null;
|
|
331015
|
+
const body = await res.json();
|
|
331016
|
+
return typeof body.version === "string" && body.version.length > 0 ? body.version : null;
|
|
331017
|
+
} catch {
|
|
331018
|
+
return null;
|
|
331019
|
+
} finally {
|
|
331020
|
+
clearTimeout(timer);
|
|
331021
|
+
}
|
|
331022
|
+
}
|
|
331023
|
+
async function checkRunnerVersion(currentVersion, opts = {}) {
|
|
331024
|
+
const skip = shouldSkipVersionCheck();
|
|
331025
|
+
if (skip) {
|
|
331026
|
+
return { outdated: false, currentVersion, latestVersion: null, skippedReason: skip };
|
|
331027
|
+
}
|
|
331028
|
+
const latestVersion = await fetchLatestRunnerVersion(opts.timeoutMs, opts.fetchImpl);
|
|
331029
|
+
if (!latestVersion) {
|
|
331030
|
+
return {
|
|
331031
|
+
outdated: false,
|
|
331032
|
+
currentVersion,
|
|
331033
|
+
latestVersion: null,
|
|
331034
|
+
skippedReason: "could not reach npm registry (or response was invalid)"
|
|
331035
|
+
};
|
|
331036
|
+
}
|
|
331037
|
+
if (!parseSemver(currentVersion) || !parseSemver(latestVersion)) {
|
|
331038
|
+
return {
|
|
331039
|
+
outdated: false,
|
|
331040
|
+
currentVersion,
|
|
331041
|
+
latestVersion,
|
|
331042
|
+
skippedReason: "unparseable version string"
|
|
331043
|
+
};
|
|
331044
|
+
}
|
|
331045
|
+
return {
|
|
331046
|
+
outdated: isVersionOutdated(currentVersion, latestVersion),
|
|
331047
|
+
currentVersion,
|
|
331048
|
+
latestVersion
|
|
331049
|
+
};
|
|
331050
|
+
}
|
|
331051
|
+
function formatUpgradeInstructions(latestVersion) {
|
|
331052
|
+
const lines = [
|
|
331053
|
+
"",
|
|
331054
|
+
"\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
|
|
331055
|
+
"\u2551 VALIDATE.QA RUNNER UPDATE REQUIRED \u2551",
|
|
331056
|
+
"\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563",
|
|
331057
|
+
"\u2551 A newer runner is published on npm. Each runner release changes \u2551",
|
|
331058
|
+
"\u2551 the workflow, so this version can no longer run. \u2551",
|
|
331059
|
+
`\u2551 Latest: ${latestVersion.padEnd(54)}\u2551`,
|
|
331060
|
+
"\u2551 \u2551",
|
|
331061
|
+
"\u2551 Upgrade with: \u2551",
|
|
331062
|
+
`\u2551 npm install -g ${RUNNER_NPM_PACKAGE}@latest`.padEnd(69) + "\u2551",
|
|
331063
|
+
"\u2551 \u2551",
|
|
331064
|
+
"\u2551 Then restart: \u2551",
|
|
331065
|
+
"\u2551 validate-runner start \u2551",
|
|
331066
|
+
"\u2551 # or: npx @validate.qa/runner@latest start \u2551",
|
|
331067
|
+
"\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D",
|
|
331068
|
+
""
|
|
331069
|
+
];
|
|
331070
|
+
return lines.join("\n");
|
|
331071
|
+
}
|
|
331072
|
+
async function enforceRunnerUpToDate(currentVersion, opts = {}) {
|
|
331073
|
+
const log2 = opts.log ?? ((msg) => console.error(msg));
|
|
331074
|
+
const result = await checkRunnerVersion(currentVersion, opts);
|
|
331075
|
+
if (result.skippedReason && !result.outdated) {
|
|
331076
|
+
if (result.skippedReason.startsWith("could not reach") || result.skippedReason.startsWith("unparseable")) {
|
|
331077
|
+
log2(`\u26A0 Runner version check skipped: ${result.skippedReason}`);
|
|
331078
|
+
}
|
|
331079
|
+
return result;
|
|
331080
|
+
}
|
|
331081
|
+
if (result.outdated && result.latestVersion) {
|
|
331082
|
+
log2(formatUpgradeInstructions(result.latestVersion));
|
|
331083
|
+
log2(` Installed: ${result.currentVersion}`);
|
|
331084
|
+
log2(` Required: ${result.latestVersion} (or newer)`);
|
|
331085
|
+
log2("");
|
|
331086
|
+
const exit = opts.exitFn ?? ((code) => process.exit(code));
|
|
331087
|
+
exit(1);
|
|
331088
|
+
}
|
|
331089
|
+
return result;
|
|
331090
|
+
}
|
|
331091
|
+
|
|
330788
331092
|
// src/services/appium-executor.ts
|
|
330789
331093
|
init_dist();
|
|
330790
331094
|
|
|
@@ -335821,13 +336125,31 @@ function buildHeaders(authToken, projectId) {
|
|
|
335821
336125
|
return {
|
|
335822
336126
|
"Content-Type": "application/json",
|
|
335823
336127
|
Authorization: `Bearer ${authToken}`,
|
|
335824
|
-
"X-Project-Id": projectId
|
|
336128
|
+
"X-Project-Id": projectId,
|
|
336129
|
+
// Lets the server force-upgrade outdated local installs on /pending too
|
|
336130
|
+
// (not only heartbeat), so they cannot claim work after a new npm publish.
|
|
336131
|
+
"X-Runner-Version": RUNNER_VERSION,
|
|
336132
|
+
"X-Runner-Name": RUNNER_NAME
|
|
335825
336133
|
};
|
|
335826
336134
|
}
|
|
336135
|
+
var RunnerUpgradeRequiredError = class extends Error {
|
|
336136
|
+
latestVersion;
|
|
336137
|
+
constructor(message, latestVersion) {
|
|
336138
|
+
super(message);
|
|
336139
|
+
this.name = "RunnerUpgradeRequiredError";
|
|
336140
|
+
this.latestVersion = latestVersion;
|
|
336141
|
+
}
|
|
336142
|
+
};
|
|
335827
336143
|
async function fetchPendingRuns(serverUrl, headers) {
|
|
335828
336144
|
const res = await fetch(`${serverUrl}/api/runner/pending`, { headers, signal: AbortSignal.timeout(3e4) });
|
|
335829
336145
|
if (!res.ok) {
|
|
335830
336146
|
if (res.status === 401) throw new Error("Runner token is invalid or expired. Run: validate.qa-runner login");
|
|
336147
|
+
if (res.status === 426) {
|
|
336148
|
+
const body = await res.json().catch(() => null);
|
|
336149
|
+
const latest = typeof body?.latestRunnerVersion === "string" ? body.latestRunnerVersion : null;
|
|
336150
|
+
const msg = typeof body?.error === "string" ? body.error : "A newer @validate.qa/runner is required. Run: npm install -g @validate.qa/runner@latest";
|
|
336151
|
+
throw new RunnerUpgradeRequiredError(msg, latest);
|
|
336152
|
+
}
|
|
335831
336153
|
if (res.status === 429) {
|
|
335832
336154
|
const retryAfter = res.headers.get("Retry-After");
|
|
335833
336155
|
const waitSecs = retryAfter ? parseInt(retryAfter, 10) : 60;
|
|
@@ -338340,10 +338662,34 @@ async function startRunner(opts) {
|
|
|
338340
338662
|
}
|
|
338341
338663
|
console.log("");
|
|
338342
338664
|
log.ok(`Runner started - waiting for test runs...${enableMobile ? " (web + mobile)" : ""}`);
|
|
338665
|
+
log.info(`Version: ${RUNNER_VERSION}`);
|
|
338343
338666
|
log.dim("Press Ctrl+C to stop");
|
|
338344
338667
|
console.log("");
|
|
338345
338668
|
let runsCompleted = 0;
|
|
338346
338669
|
let activeRuns = 0;
|
|
338670
|
+
const VERSION_RECHECK_MS = Math.max(
|
|
338671
|
+
6e4,
|
|
338672
|
+
envInt("VALIDATE_RUNNER_VERSION_CHECK_MS", 15 * 6e4)
|
|
338673
|
+
);
|
|
338674
|
+
const exitForUpgrade = (latestVersion, installed) => {
|
|
338675
|
+
console.error(formatUpgradeInstructions(latestVersion));
|
|
338676
|
+
console.error(` Installed: ${installed}`);
|
|
338677
|
+
console.error(` Required: ${latestVersion} (or newer)`);
|
|
338678
|
+
console.error("");
|
|
338679
|
+
process.exit(1);
|
|
338680
|
+
};
|
|
338681
|
+
const recheckRunnerVersion = async () => {
|
|
338682
|
+
if (activeRuns > 0) return;
|
|
338683
|
+
const result = await checkRunnerVersion(RUNNER_VERSION);
|
|
338684
|
+
if (result.outdated && result.latestVersion) {
|
|
338685
|
+
log.warn("A newer runner was published on npm \u2014 shutting down so you can upgrade.");
|
|
338686
|
+
exitForUpgrade(result.latestVersion, RUNNER_VERSION);
|
|
338687
|
+
}
|
|
338688
|
+
};
|
|
338689
|
+
const versionRecheckTimer = setInterval(() => {
|
|
338690
|
+
void recheckRunnerVersion();
|
|
338691
|
+
}, VERSION_RECHECK_MS);
|
|
338692
|
+
versionRecheckTimer.unref?.();
|
|
338347
338693
|
const MAX_CONCURRENT = Math.max(1, envInt("MAX_CONCURRENT", DEFAULT_MAX_CONCURRENT));
|
|
338348
338694
|
const MAX_PROJECT_CONCURRENT = Math.max(1, envInt("MAX_PROJECT_CONCURRENT", DEFAULT_MAX_PROJECT_CONCURRENT));
|
|
338349
338695
|
const TYPE_LIMITS = {
|
|
@@ -338569,6 +338915,22 @@ async function startRunner(opts) {
|
|
|
338569
338915
|
if (body && Array.isArray(body.commands) && body.commands.length > 0) {
|
|
338570
338916
|
void handleControlCommands(body.commands);
|
|
338571
338917
|
}
|
|
338918
|
+
if (body && body.forceUpgrade === true && typeof body.latestRunnerVersion === "string" && isVersionOutdated(RUNNER_VERSION, body.latestRunnerVersion) && activeRuns === 0) {
|
|
338919
|
+
log.warn(
|
|
338920
|
+
typeof body.upgradeMessage === "string" && body.upgradeMessage ? body.upgradeMessage : "Server requires a newer runner version \u2014 shutting down."
|
|
338921
|
+
);
|
|
338922
|
+
exitForUpgrade(body.latestRunnerVersion, RUNNER_VERSION);
|
|
338923
|
+
}
|
|
338924
|
+
} else if (response.status === 426) {
|
|
338925
|
+
const body = await response.json().catch(() => null);
|
|
338926
|
+
const latest = typeof body?.latestRunnerVersion === "string" ? body.latestRunnerVersion : null;
|
|
338927
|
+
if (latest) {
|
|
338928
|
+
exitForUpgrade(latest, RUNNER_VERSION);
|
|
338929
|
+
}
|
|
338930
|
+
log.warn(
|
|
338931
|
+
typeof body?.error === "string" ? body.error : "Server rejected this runner version (HTTP 426). Upgrade @validate.qa/runner."
|
|
338932
|
+
);
|
|
338933
|
+
process.exit(1);
|
|
338572
338934
|
}
|
|
338573
338935
|
} catch {
|
|
338574
338936
|
}
|
|
@@ -338809,6 +339171,10 @@ async function startRunner(opts) {
|
|
|
338809
339171
|
consecutivePollErrors = 0;
|
|
338810
339172
|
}
|
|
338811
339173
|
} catch (err) {
|
|
339174
|
+
if (err instanceof RunnerUpgradeRequiredError) {
|
|
339175
|
+
log.warn(err.message);
|
|
339176
|
+
exitForUpgrade(err.latestVersion ?? "latest", RUNNER_VERSION);
|
|
339177
|
+
}
|
|
338812
339178
|
const message = err instanceof Error ? err.message : String(err);
|
|
338813
339179
|
consecutivePollErrors++;
|
|
338814
339180
|
const backoffMs = Math.min(pollIntervalMs * Math.pow(2, consecutivePollErrors - 1), MAX_BACKOFF_MS);
|
|
@@ -338828,7 +339194,11 @@ async function startRunner(opts) {
|
|
|
338828
339194
|
var pkg = require_package4();
|
|
338829
339195
|
var program = new Command();
|
|
338830
339196
|
program.name("validate-runner").description("validate.qa local test runner - execute Playwright and Appium tests on your machine").version(pkg.version);
|
|
339197
|
+
async function requireLatestRunner() {
|
|
339198
|
+
await enforceRunnerUpToDate(pkg.version);
|
|
339199
|
+
}
|
|
338831
339200
|
program.command("login").description("Save runner credentials for future use").requiredOption("--token <token>", "Runner token (urt_... from validate.qa dashboard)").requiredOption("--server <url>", "validate.qa server URL").option("--project <id>", "Project ID (from dashboard URL)").action(async (opts) => {
|
|
339201
|
+
await requireLatestRunner();
|
|
338832
339202
|
const serverUrl = opts.server.replace(/\/+$/, "");
|
|
338833
339203
|
if (!opts.token.startsWith("urt_") && !opts.token.startsWith("rt_") && !opts.token.startsWith("eyJ")) {
|
|
338834
339204
|
console.error("\u26A0 Token should start with urt_ (from the dashboard Runner Token)");
|
|
@@ -338860,6 +339230,7 @@ program.command("login").description("Save runner credentials for future use").r
|
|
|
338860
339230
|
console.log("Run: validate-runner start");
|
|
338861
339231
|
});
|
|
338862
339232
|
program.command("start").description("Start the local test runner").option("--token <token>", "Runner token (overrides saved credentials)").option("--server <url>", "Server URL (overrides saved credentials)").option("--project <id>", "Project ID (overrides saved credentials)").option("--mode <mode>", "Runner mode: playwright-native (default)", "playwright-native").option("--poll-interval <ms>", "Poll interval in milliseconds", "2000").option("--mobile", "Enable mobile testing (Appium + mobile bridge server)").option("--tunnel", "Enable ngrok tunnel for mobile bridge (allows HTTPS dashboard to connect)").action(async (opts) => {
|
|
339233
|
+
await requireLatestRunner();
|
|
338863
339234
|
const saved = loadCredentials();
|
|
338864
339235
|
const authToken = opts.token || process.env.AUTH_TOKEN || saved?.token;
|
|
338865
339236
|
const serverUrl = (opts.server || process.env.SERVER_URL || saved?.serverUrl)?.replace(/\/+$/, "");
|
|
@@ -338912,6 +339283,7 @@ program.command("logout").description("Clear saved runner credentials").action((
|
|
|
338912
339283
|
console.log("\u2713 Credentials cleared");
|
|
338913
339284
|
});
|
|
338914
339285
|
program.command("status").description("Check connection to validate.qa server").action(async () => {
|
|
339286
|
+
await requireLatestRunner();
|
|
338915
339287
|
const saved = loadCredentials();
|
|
338916
339288
|
if (!saved) {
|
|
338917
339289
|
console.log("No saved credentials. Run: validate-runner login");
|