@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.js
CHANGED
|
@@ -1349,11 +1349,13 @@ function escapeEnvValue(value) {
|
|
|
1349
1349
|
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
|
|
1350
1350
|
return `"${escaped}"`;
|
|
1351
1351
|
}
|
|
1352
|
-
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;
|
|
1352
|
+
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;
|
|
1353
1353
|
var init_constants = __esm({
|
|
1354
1354
|
"../shared/dist/constants.js"() {
|
|
1355
1355
|
"use strict";
|
|
1356
1356
|
API_PREFIX = "/api";
|
|
1357
|
+
SUPPORT_EMAIL = "support@validate.qa";
|
|
1358
|
+
PLATFORM_ADMIN_EMAIL = "admin@validate.qa";
|
|
1357
1359
|
AUDIO_CHUNK_DURATION_MS = 3e4;
|
|
1358
1360
|
ACTION_BATCH_INTERVAL_MS = 2e3;
|
|
1359
1361
|
ACTION_BATCH_MAX_SIZE = 10;
|
|
@@ -2557,6 +2559,8 @@ var init_audit_pricing = __esm({
|
|
|
2557
2559
|
// Current defaults
|
|
2558
2560
|
"grok-build-0.1": { input: 1, output: 2 },
|
|
2559
2561
|
"claude-opus-4-7": { input: 5, output: 25 },
|
|
2562
|
+
// Introductory pricing through 2026-08-31 ($2/$10); standard is $3/$15.
|
|
2563
|
+
"claude-sonnet-5": { input: 2, output: 10 },
|
|
2560
2564
|
"claude-sonnet-4-6": { input: 3, output: 15 },
|
|
2561
2565
|
"claude-haiku-4-5-20251001": { input: 1, output: 5 },
|
|
2562
2566
|
"gpt-4o": { input: 2.5, output: 10 },
|
|
@@ -3593,7 +3597,7 @@ var init_agent_env = __esm({
|
|
|
3593
3597
|
"CommonProgramFiles"
|
|
3594
3598
|
]);
|
|
3595
3599
|
ALLOW_PREFIX = [
|
|
3596
|
-
// AI provider / CLI config (grok-build, claude-code
|
|
3600
|
+
// AI provider / CLI config (grok-build, claude-code)
|
|
3597
3601
|
"GROK_",
|
|
3598
3602
|
"XAI_",
|
|
3599
3603
|
"ANTHROPIC_",
|
|
@@ -3900,8 +3904,10 @@ __export(dist_exports, {
|
|
|
3900
3904
|
MOBILE_STEP_TYPES: () => MOBILE_STEP_TYPES,
|
|
3901
3905
|
PLAN_DEFINITIONS: () => PLAN_DEFINITIONS,
|
|
3902
3906
|
PLAN_ORDER: () => PLAN_ORDER,
|
|
3907
|
+
PLATFORM_ADMIN_EMAIL: () => PLATFORM_ADMIN_EMAIL,
|
|
3903
3908
|
SELECTOR_PRIORITIES: () => SELECTOR_PRIORITIES,
|
|
3904
3909
|
SHARED_PRICING_PLAN_MAP: () => SHARED_PRICING_PLAN_MAP,
|
|
3910
|
+
SUPPORT_EMAIL: () => SUPPORT_EMAIL,
|
|
3905
3911
|
TRANSIENT_CONTROL_ROLES: () => TRANSIENT_CONTROL_ROLES,
|
|
3906
3912
|
TRIAL_DURATION_DAYS: () => TRIAL_DURATION_DAYS,
|
|
3907
3913
|
UI_VARIANTS: () => UI_VARIANTS,
|
|
@@ -65163,7 +65169,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
65163
65169
|
Example - BAD happy path: \`["Navigate to /feature", "Click Create", "Verify dialog opens"]\` \u2192 only tests the trigger.
|
|
65164
65170
|
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"]\`
|
|
65165
65171
|
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"]\`
|
|
65166
|
-
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", "
|
|
65172
|
+
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.
|
|
65167
65173
|
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.
|
|
65168
65174
|
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>."
|
|
65169
65175
|
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.
|
|
@@ -66123,7 +66129,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
66123
66129
|
9. **No duplicates** - check existing test names before adding
|
|
66124
66130
|
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.
|
|
66125
66131
|
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.
|
|
66126
|
-
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:
|
|
66132
|
+
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:
|
|
66127
66133
|
- The trigger step (Tap Submit / Generate / Send / Upload / Run / Analyze / Save / Pay / etc.)
|
|
66128
66134
|
- 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.
|
|
66129
66135
|
- 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.
|
|
@@ -66794,6 +66800,9 @@ var require_mobile_generator = __commonJS({
|
|
|
66794
66800
|
"../runner-core/dist/services/mobile/mobile-generator.js"(exports2) {
|
|
66795
66801
|
"use strict";
|
|
66796
66802
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
66803
|
+
exports2.collapseConsecutiveBacks = collapseConsecutiveBacks;
|
|
66804
|
+
exports2.scenarioIntentMismatchReason = scenarioIntentMismatchReason;
|
|
66805
|
+
exports2.dedupeConsecutiveIdenticalActions = dedupeConsecutiveIdenticalActions;
|
|
66797
66806
|
exports2.collapseRetryCycles = collapseRetryCycles;
|
|
66798
66807
|
exports2.runMobileGeneratePhase = runMobileGeneratePhase;
|
|
66799
66808
|
var crypto_1 = require("crypto");
|
|
@@ -67064,7 +67073,7 @@ var require_mobile_generator = __commonJS({
|
|
|
67064
67073
|
## Your job
|
|
67065
67074
|
1. mobile_snapshot the current screen.
|
|
67066
67075
|
2. Execute the scenario's steps using the real UI: tap buttons, fill fields (mobile_type), submit, navigate.
|
|
67067
|
-
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
|
|
67076
|
+
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.
|
|
67068
67077
|
4. When you reach the target screen / complete the flow, PROVE the outcome with an assert tool:
|
|
67069
67078
|
- mobile_assert_visible({ref}) \u2014 the target screen's stable element is on screen.
|
|
67070
67079
|
- mobile_assert_text({ref, expected}) \u2014 an element contains expected text (only durable when the element has a stable accessibility-id / resource-id).
|
|
@@ -67079,6 +67088,8 @@ var require_mobile_generator = __commonJS({
|
|
|
67079
67088
|
- 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.
|
|
67080
67089
|
- 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.
|
|
67081
67090
|
- 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.
|
|
67091
|
+
- 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.
|
|
67092
|
+
- 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.
|
|
67082
67093
|
- 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.
|
|
67083
67094
|
- 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.
|
|
67084
67095
|
- 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.
|
|
@@ -67097,7 +67108,7 @@ This app has stored login credentials under these keys:
|
|
|
67097
67108
|
${keyList}
|
|
67098
67109
|
|
|
67099
67110
|
When a scenario needs a signed-in session, complete the FULL login sequence \u2014 filling the fields alone does NOT log you in:
|
|
67100
|
-
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.
|
|
67111
|
+
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).
|
|
67101
67112
|
|
|
67102
67113
|
SECURITY RULES:
|
|
67103
67114
|
- 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.
|
|
@@ -67447,6 +67458,7 @@ ${statePacket}` },
|
|
|
67447
67458
|
if (action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT")
|
|
67448
67459
|
assertCount++;
|
|
67449
67460
|
},
|
|
67461
|
+
recordedActions: actions,
|
|
67450
67462
|
log: log2
|
|
67451
67463
|
});
|
|
67452
67464
|
pushResult(dispatchResult);
|
|
@@ -67462,7 +67474,7 @@ ${statePacket}` },
|
|
|
67462
67474
|
return { actions, assertCount, finish, lastSnapshot: getLatest(), lastScreenName };
|
|
67463
67475
|
}
|
|
67464
67476
|
async function dispatchGenerateTool(args) {
|
|
67465
|
-
const { toolName, toolArgs, driver, platform: platform3, latest, absorbObservation, resolveRef, credentials, screenName, recordAction, log: log2 } = args;
|
|
67477
|
+
const { toolName, toolArgs, driver, platform: platform3, latest, absorbObservation, resolveRef, credentials, screenName, recordAction, recordedActions, log: log2 } = args;
|
|
67466
67478
|
const unknownRef = (ref) => ({
|
|
67467
67479
|
ok: false,
|
|
67468
67480
|
error: `Unknown element ref "${String(ref)}". Call mobile_snapshot first; refs are only valid for the latest snapshot.`
|
|
@@ -67527,14 +67539,23 @@ ${statePacket}` },
|
|
|
67527
67539
|
const screenChanged = changedScreen(before, absorbed);
|
|
67528
67540
|
const identityTap = isIdentityBearingTapTarget(element);
|
|
67529
67541
|
if (shouldRecordAction(result, absorbed) && (screenChanged || identityTap)) {
|
|
67530
|
-
|
|
67531
|
-
log2(` tap left content signature unchanged but target has identity \u2014 recording commit control "${labelForElement(element)}".`);
|
|
67532
|
-
}
|
|
67533
|
-
recordAction({
|
|
67542
|
+
const tapAction = {
|
|
67534
67543
|
type: "TAP",
|
|
67535
67544
|
selector: (0, mobile_source_parser_js_2.preferredLocator)(element),
|
|
67536
|
-
metadata: metadataForElement(element, beforeScreenName, platform3)
|
|
67537
|
-
|
|
67545
|
+
metadata: metadataForElement(element, beforeScreenName, platform3),
|
|
67546
|
+
innerText: null,
|
|
67547
|
+
url: "",
|
|
67548
|
+
value: null
|
|
67549
|
+
};
|
|
67550
|
+
const lastRecorded = recordedActions[recordedActions.length - 1];
|
|
67551
|
+
if (lastRecorded && actionCycleKey(lastRecorded) === actionCycleKey(tapAction)) {
|
|
67552
|
+
log2(` tap left content signature unchanged but duplicates the prior step \u2014 not recording again.`);
|
|
67553
|
+
} else if (!screenChanged && identityTap) {
|
|
67554
|
+
log2(` tap left content signature unchanged but target has identity \u2014 recording commit control "${labelForElement(element)}".`);
|
|
67555
|
+
recordAction(tapAction);
|
|
67556
|
+
} else {
|
|
67557
|
+
recordAction(tapAction);
|
|
67558
|
+
}
|
|
67538
67559
|
} else if (shouldRecordAction(result, absorbed)) {
|
|
67539
67560
|
log2(" tap changed nothing on screen \u2014 not recording it as a step (dead action).");
|
|
67540
67561
|
}
|
|
@@ -67878,6 +67899,105 @@ ${statePacket}` },
|
|
|
67878
67899
|
...scenario.expectedOutcomes.flatMap((outcome) => [outcome.description, outcome.hint])
|
|
67879
67900
|
].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").toLowerCase();
|
|
67880
67901
|
}
|
|
67902
|
+
var INSPECTION_OUTCOME_TYPES = /* @__PURE__ */ new Set([
|
|
67903
|
+
"element-visible",
|
|
67904
|
+
"text-visible",
|
|
67905
|
+
"element-hidden",
|
|
67906
|
+
"screen-visible"
|
|
67907
|
+
]);
|
|
67908
|
+
function isInspectionOnlyScenario(scenario, actions) {
|
|
67909
|
+
if (actions.length === 0 || scenario.expectedOutcomes.length === 0)
|
|
67910
|
+
return false;
|
|
67911
|
+
if (!scenario.expectedOutcomes.every((o) => INSPECTION_OUTCOME_TYPES.has(o.type))) {
|
|
67912
|
+
return false;
|
|
67913
|
+
}
|
|
67914
|
+
if (actions.some((a) => a.type === "TYPE" || a.type === "CLEAR")) {
|
|
67915
|
+
return false;
|
|
67916
|
+
}
|
|
67917
|
+
const text = scenarioText(scenario);
|
|
67918
|
+
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);
|
|
67919
|
+
if (visibilityOnlyGoal) {
|
|
67920
|
+
const assertCount2 = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT").length;
|
|
67921
|
+
return assertCount2 > 0;
|
|
67922
|
+
}
|
|
67923
|
+
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);
|
|
67924
|
+
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);
|
|
67925
|
+
if (completionIntent && formIntent)
|
|
67926
|
+
return false;
|
|
67927
|
+
const assertCount = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT").length;
|
|
67928
|
+
return assertCount > 0;
|
|
67929
|
+
}
|
|
67930
|
+
function isNavigationFocusedScenario(scenario) {
|
|
67931
|
+
const text = scenarioText(scenario);
|
|
67932
|
+
const navigates = /\b(back|return|go\s+home|navigate\s+back|leave|exit|cancel\s+out|dismiss|previous\s+screen)\b/.test(text);
|
|
67933
|
+
const completesFlow = /\b(happy\s*path|successful|complete\s+(the\s+)?(login|sign[- ]?in|registration|checkout)|with\s+valid)\b/.test(text);
|
|
67934
|
+
return navigates && !completesFlow;
|
|
67935
|
+
}
|
|
67936
|
+
function isFormValidationScenario(scenario) {
|
|
67937
|
+
if (scenario.variant === "validation")
|
|
67938
|
+
return true;
|
|
67939
|
+
if (scenario.type !== "EDGE_CASE")
|
|
67940
|
+
return false;
|
|
67941
|
+
const text = scenarioText(scenario);
|
|
67942
|
+
return /\b(empty|required|missing|blank|invalid|reject|error|validation|wrong|incorrect|mismatch)\b/.test(text);
|
|
67943
|
+
}
|
|
67944
|
+
function isFormMutationHappyPath(scenario) {
|
|
67945
|
+
if (isNavigationFocusedScenario(scenario))
|
|
67946
|
+
return false;
|
|
67947
|
+
if (isFormValidationScenario(scenario))
|
|
67948
|
+
return false;
|
|
67949
|
+
const text = scenarioText(scenario);
|
|
67950
|
+
const isHappy = scenario.type === "HAPPY_PATH" || /\b(happy\s*path|successful|success)\b/.test(text);
|
|
67951
|
+
if (!isHappy)
|
|
67952
|
+
return false;
|
|
67953
|
+
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)) {
|
|
67954
|
+
return false;
|
|
67955
|
+
}
|
|
67956
|
+
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)) {
|
|
67957
|
+
return false;
|
|
67958
|
+
}
|
|
67959
|
+
const mutates = /\b(change|update|create|save|send|register|sign\s*up|checkout|edit|post|write|compose|set|add|delete|remove)\b/.test(text);
|
|
67960
|
+
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);
|
|
67961
|
+
return mutates && hasObject;
|
|
67962
|
+
}
|
|
67963
|
+
function isNavigationChromeTap(action) {
|
|
67964
|
+
if (action.type !== "TAP")
|
|
67965
|
+
return false;
|
|
67966
|
+
const textForAction = actionText(action);
|
|
67967
|
+
return /\b(go\s+back|back\s+to|←|return\s+to|cancel|close|dismiss|not\s+now|skip|maybe\s+later)\b/.test(textForAction);
|
|
67968
|
+
}
|
|
67969
|
+
function isAuthMethodChooserTap(action) {
|
|
67970
|
+
if (action.type !== "TAP")
|
|
67971
|
+
return false;
|
|
67972
|
+
const textForAction = actionText(action);
|
|
67973
|
+
if (/\b(submit|confirm|sign\s*in|log\s*in|login)\b/.test(textForAction) && !/\b(with|using|via)\b/.test(textForAction)) {
|
|
67974
|
+
return false;
|
|
67975
|
+
}
|
|
67976
|
+
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);
|
|
67977
|
+
}
|
|
67978
|
+
function isCredentialSubmitTap(action) {
|
|
67979
|
+
if (action.type !== "TAP")
|
|
67980
|
+
return false;
|
|
67981
|
+
if (isNavigationChromeTap(action) || isAuthMethodChooserTap(action))
|
|
67982
|
+
return false;
|
|
67983
|
+
const textForAction = actionText(action);
|
|
67984
|
+
return /\b(sign\s*in|log\s*in|login|continue|submit|done|next|confirm)\b/.test(textForAction);
|
|
67985
|
+
}
|
|
67986
|
+
function isKeyboardFormCommit(action) {
|
|
67987
|
+
return action.type === "TYPE" && (action.value ?? "").endsWith("\n");
|
|
67988
|
+
}
|
|
67989
|
+
function isFormCommitAction(action) {
|
|
67990
|
+
if (isKeyboardFormCommit(action))
|
|
67991
|
+
return true;
|
|
67992
|
+
if (isCredentialSubmitTap(action))
|
|
67993
|
+
return true;
|
|
67994
|
+
if (action.type !== "TAP")
|
|
67995
|
+
return false;
|
|
67996
|
+
if (isNavigationChromeTap(action) || isAuthMethodChooserTap(action))
|
|
67997
|
+
return false;
|
|
67998
|
+
const textForAction = actionText(action);
|
|
67999
|
+
return /\b(save|create|send|confirm|next|apply|post|checkout|pay|finish|update|register|sign\s*up|done|continue|submit)\b/i.test(textForAction);
|
|
68000
|
+
}
|
|
67881
68001
|
function actionText(action) {
|
|
67882
68002
|
const bundle = action.metadata.locatorBundle;
|
|
67883
68003
|
return [
|
|
@@ -67894,9 +68014,80 @@ ${statePacket}` },
|
|
|
67894
68014
|
bundle?.xpath
|
|
67895
68015
|
].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").toLowerCase();
|
|
67896
68016
|
}
|
|
68017
|
+
function isStructuralOnlyAssert(action) {
|
|
68018
|
+
if (action.type !== "ASSERT_VISIBLE" && action.type !== "ASSERT_TEXT")
|
|
68019
|
+
return false;
|
|
68020
|
+
const sel = (action.selector ?? "").trim();
|
|
68021
|
+
if (/^\/hierarchy(\[\d+\])?(\/android\.widget\.FrameLayout\[\d+\])*$/i.test(sel))
|
|
68022
|
+
return true;
|
|
68023
|
+
if (isStructuralAssertionIdentity(sel) || isStructuralAssertionIdentity(action.metadata.targetLabel)) {
|
|
68024
|
+
const bundle = action.metadata.locatorBundle;
|
|
68025
|
+
const human = Boolean(nonEmpty2(action.innerText) || nonEmpty2(action.metadata.targetLabel) || nonEmpty2(bundle?.text) || nonEmpty2(bundle?.contentDesc));
|
|
68026
|
+
if (!human)
|
|
68027
|
+
return true;
|
|
68028
|
+
}
|
|
68029
|
+
if (/^\/hierarchy/i.test(sel)) {
|
|
68030
|
+
const bundle = action.metadata.locatorBundle;
|
|
68031
|
+
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));
|
|
68032
|
+
if (!hasIdentity)
|
|
68033
|
+
return true;
|
|
68034
|
+
}
|
|
68035
|
+
return false;
|
|
68036
|
+
}
|
|
68037
|
+
function collapseConsecutiveBacks(actions) {
|
|
68038
|
+
if (actions.length <= 1)
|
|
68039
|
+
return actions;
|
|
68040
|
+
const out = [actions[0]];
|
|
68041
|
+
for (let i = 1; i < actions.length; i++) {
|
|
68042
|
+
if (actions[i].type === "BACK" && out[out.length - 1].type === "BACK")
|
|
68043
|
+
continue;
|
|
68044
|
+
out.push(actions[i]);
|
|
68045
|
+
}
|
|
68046
|
+
return out;
|
|
68047
|
+
}
|
|
67897
68048
|
function scenarioIntentMismatchReason(scenario, actions) {
|
|
67898
68049
|
const text = scenarioText(scenario);
|
|
67899
|
-
|
|
68050
|
+
if (isInspectionOnlyScenario(scenario, actions)) {
|
|
68051
|
+
return null;
|
|
68052
|
+
}
|
|
68053
|
+
if (isNavigationFocusedScenario(scenario)) {
|
|
68054
|
+
return null;
|
|
68055
|
+
}
|
|
68056
|
+
if (isFormValidationScenario(scenario)) {
|
|
68057
|
+
const typedFields = actions.some((a) => a.type === "TYPE");
|
|
68058
|
+
const hasCommit = actions.some((a) => isFormCommitAction(a));
|
|
68059
|
+
const hasValidationAssert = actions.some((a) => {
|
|
68060
|
+
if (a.type !== "ASSERT_VISIBLE" && a.type !== "ASSERT_TEXT")
|
|
68061
|
+
return false;
|
|
68062
|
+
return /\b(error|required|invalid|empty|missing|incorrect|wrong|try again|mismatch|does not match|must match)\b/.test(actionText(a));
|
|
68063
|
+
});
|
|
68064
|
+
const emptyFieldIntent = /\b(empty|required|missing|blank)\b/.test(text);
|
|
68065
|
+
const invalidValueIntent = /\b(wrong|incorrect|invalid|mismatch|reject)\b/.test(text);
|
|
68066
|
+
if (emptyFieldIntent && !invalidValueIntent) {
|
|
68067
|
+
if (!hasCommit && !hasValidationAssert) {
|
|
68068
|
+
return "validation scenario did not record a form commit or validation outcome";
|
|
68069
|
+
}
|
|
68070
|
+
return null;
|
|
68071
|
+
}
|
|
68072
|
+
if (typedFields && !hasCommit) {
|
|
68073
|
+
return "validation scenario filled fields but never committed the form";
|
|
68074
|
+
}
|
|
68075
|
+
if (!hasCommit && !hasValidationAssert) {
|
|
68076
|
+
return "validation scenario did not record a form commit or validation outcome";
|
|
68077
|
+
}
|
|
68078
|
+
return null;
|
|
68079
|
+
}
|
|
68080
|
+
if (isFormMutationHappyPath(scenario)) {
|
|
68081
|
+
const hasType = actions.some((a) => a.type === "TYPE");
|
|
68082
|
+
const hasCommit = actions.some((a) => isFormCommitAction(a));
|
|
68083
|
+
if (!hasType) {
|
|
68084
|
+
return "form-mutation happy path did not record any field entry";
|
|
68085
|
+
}
|
|
68086
|
+
if (!hasCommit) {
|
|
68087
|
+
return "form-mutation happy path did not record a form commit";
|
|
68088
|
+
}
|
|
68089
|
+
}
|
|
68090
|
+
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);
|
|
67900
68091
|
const typedEmailIndexes = [];
|
|
67901
68092
|
const typedPasswordIndexes = [];
|
|
67902
68093
|
actions.forEach((action, index) => {
|
|
@@ -67913,19 +68104,19 @@ ${statePacket}` },
|
|
|
67913
68104
|
const lastCredentialIndex = Math.max(...typedEmailIndexes, ...typedPasswordIndexes);
|
|
67914
68105
|
const hasAnyCredentialEntry = Number.isFinite(lastCredentialIndex);
|
|
67915
68106
|
const submitAfterCredentials = actions.some((action, index) => {
|
|
67916
|
-
if (
|
|
68107
|
+
if (index < lastCredentialIndex)
|
|
67917
68108
|
return false;
|
|
67918
|
-
|
|
67919
|
-
if (!/\b(sign\s*in|log\s*in|login|continue|submit)\b/.test(textForAction))
|
|
68109
|
+
if (!isFormCommitAction(action))
|
|
67920
68110
|
return false;
|
|
67921
|
-
|
|
68111
|
+
if (action.type === "TYPE" && index === lastCredentialIndex)
|
|
68112
|
+
return true;
|
|
68113
|
+
return index > lastCredentialIndex;
|
|
67922
68114
|
});
|
|
67923
68115
|
const isAuthLikeScenario = /\b(sign(?:ed)?\s*in|log(?:ged)?\s*in|login|authenticate|auth|credential|email|e-mail|password)\b/.test(text);
|
|
67924
68116
|
if (isAuthLikeScenario && hasAnyCredentialEntry && !submitAfterCredentials) {
|
|
67925
|
-
return "credential fields were filled but no
|
|
68117
|
+
return "credential fields were filled but no form commit was recorded after them";
|
|
67926
68118
|
}
|
|
67927
68119
|
const uiOnlyCredentialScenario = /\b(mask|masked|visibility|show password|hide password|toggle|keyboard|keypad|max(?:imum)?\s*length|field focus|autofill)\b/.test(text);
|
|
67928
|
-
const emptyFieldValidationScenario = /\b(empty|required|missing|blank)\b/.test(text);
|
|
67929
68120
|
const sessionLifecycleScenario = /\b(logout|log\s*out|sign\s*out|session persistence|remain(?:s)? signed in|remember me)\b/.test(text);
|
|
67930
68121
|
const loginTapWithoutCredentialPath = actions.some((action) => {
|
|
67931
68122
|
if (action.type !== "TAP")
|
|
@@ -67933,9 +68124,11 @@ ${statePacket}` },
|
|
|
67933
68124
|
const textForAction = actionText(action);
|
|
67934
68125
|
if (/\b(logout|log\s*out|sign\s*out)\b/.test(textForAction))
|
|
67935
68126
|
return false;
|
|
68127
|
+
if (isAuthMethodChooserTap(action) || isNavigationChromeTap(action))
|
|
68128
|
+
return false;
|
|
67936
68129
|
return /\b(sign\s*in|log\s*in|login)\b/.test(textForAction);
|
|
67937
68130
|
});
|
|
67938
|
-
if (loginTapWithoutCredentialPath && !hasAnyCredentialEntry && !uiOnlyCredentialScenario &&
|
|
68131
|
+
if (loginTapWithoutCredentialPath && !hasAnyCredentialEntry && !uiOnlyCredentialScenario && (sessionLifecycleScenario || /\b(valid credentials?|happy path|successful sign\s*in|successful login)\b/.test(text))) {
|
|
67939
68132
|
return "auth/session scenario tapped a login or sign-in control without recording a credential path";
|
|
67940
68133
|
}
|
|
67941
68134
|
if (isCredentialSignIn) {
|
|
@@ -67947,7 +68140,7 @@ ${statePacket}` },
|
|
|
67947
68140
|
const missing = [
|
|
67948
68141
|
typedEmail ? null : "email/username entry",
|
|
67949
68142
|
typedPassword ? null : "password entry",
|
|
67950
|
-
tappedSubmit ? null : "
|
|
68143
|
+
tappedSubmit ? null : "form commit (submit tap or keyboard Enter)"
|
|
67951
68144
|
].filter((value) => value !== null);
|
|
67952
68145
|
return `credential sign-in scenario did not record ${missing.join(", ")}`;
|
|
67953
68146
|
}
|
|
@@ -67968,11 +68161,12 @@ ${statePacket}` },
|
|
|
67968
68161
|
const typeScreen = actions[lastTypeIndex]?.metadata?.screenName?.trim() ?? "";
|
|
67969
68162
|
const assertScreen = actions[lastAssertIndex]?.metadata?.screenName?.trim() ?? "";
|
|
67970
68163
|
const screensDiffer = typeScreen !== "" && assertScreen !== "" && typeScreen !== assertScreen;
|
|
67971
|
-
const
|
|
67972
|
-
|
|
68164
|
+
const lastTypeAction = actions[lastTypeIndex];
|
|
68165
|
+
const keyboardCommitOnLastType = lastTypeAction != null && isKeyboardFormCommit(lastTypeAction);
|
|
68166
|
+
const commitAfterType = keyboardCommitOnLastType || actions.some((action, index) => {
|
|
68167
|
+
if (index <= lastTypeIndex || index >= lastAssertIndex)
|
|
67973
68168
|
return false;
|
|
67974
|
-
|
|
67975
|
-
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);
|
|
68169
|
+
return isFormCommitAction(action);
|
|
67976
68170
|
});
|
|
67977
68171
|
if (screensDiffer && !commitAfterType) {
|
|
67978
68172
|
return "form fields were filled then a success assert was recorded without a commit/submit tap between them";
|
|
@@ -67983,6 +68177,17 @@ ${statePacket}` },
|
|
|
67983
68177
|
return `${action.type}|${action.selector}|${action.value ?? ""}`;
|
|
67984
68178
|
}
|
|
67985
68179
|
var MAX_RETRY_CYCLE_LENGTH = 8;
|
|
68180
|
+
function dedupeConsecutiveIdenticalActions(actions) {
|
|
68181
|
+
if (actions.length <= 1)
|
|
68182
|
+
return actions;
|
|
68183
|
+
const out = [actions[0]];
|
|
68184
|
+
for (let i = 1; i < actions.length; i++) {
|
|
68185
|
+
if (actionCycleKey(actions[i]) === actionCycleKey(out[out.length - 1]))
|
|
68186
|
+
continue;
|
|
68187
|
+
out.push(actions[i]);
|
|
68188
|
+
}
|
|
68189
|
+
return out;
|
|
68190
|
+
}
|
|
67986
68191
|
function collapseRetryCycles(actions) {
|
|
67987
68192
|
const out = [...actions];
|
|
67988
68193
|
let changed = true;
|
|
@@ -68023,15 +68228,30 @@ ${statePacket}` },
|
|
|
68023
68228
|
}
|
|
68024
68229
|
function assembleTest(args) {
|
|
68025
68230
|
const { scenario, recording, ctx, platform: platform3, appDataCleared, log: log2 } = args;
|
|
68026
|
-
let actions =
|
|
68231
|
+
let actions = dedupeConsecutiveIdenticalActions(recording.actions);
|
|
68027
68232
|
if (actions.length < recording.actions.length) {
|
|
68028
|
-
log2(`
|
|
68233
|
+
log2(` deduped ${recording.actions.length - actions.length} consecutive identical step(s) (${recording.actions.length} \u2192 ${actions.length}).`);
|
|
68234
|
+
}
|
|
68235
|
+
const beforeBackCollapse = actions.length;
|
|
68236
|
+
actions = collapseConsecutiveBacks(actions);
|
|
68237
|
+
if (actions.length < beforeBackCollapse) {
|
|
68238
|
+
log2(` collapsed ${beforeBackCollapse - actions.length} consecutive BACK step(s) (${beforeBackCollapse} \u2192 ${actions.length}).`);
|
|
68239
|
+
}
|
|
68240
|
+
const beforeCycleCollapse = actions.length;
|
|
68241
|
+
actions = collapseRetryCycles(actions);
|
|
68242
|
+
if (actions.length < beforeCycleCollapse) {
|
|
68243
|
+
log2(` collapsed ${beforeCycleCollapse - actions.length} retry-cycle step(s) out of the recorded trace (${beforeCycleCollapse} \u2192 ${actions.length}).`);
|
|
68029
68244
|
}
|
|
68030
68245
|
const beforeContainerDrop = actions.length;
|
|
68031
68246
|
actions = actions.filter((a) => !isBareContainerTap(a));
|
|
68032
68247
|
if (actions.length < beforeContainerDrop) {
|
|
68033
68248
|
log2(` dropped ${beforeContainerDrop - actions.length} bare-container tap step(s) (no identity \u2014 un-replayable ambient taps).`);
|
|
68034
68249
|
}
|
|
68250
|
+
const beforeWeakAssertDrop = actions.length;
|
|
68251
|
+
actions = actions.filter((a) => !isStructuralOnlyAssert(a));
|
|
68252
|
+
if (actions.length < beforeWeakAssertDrop) {
|
|
68253
|
+
log2(` dropped ${beforeWeakAssertDrop - actions.length} structural-only assert step(s) (no product identity).`);
|
|
68254
|
+
}
|
|
68035
68255
|
const intentMismatch = scenarioIntentMismatchReason(scenario, actions);
|
|
68036
68256
|
if (intentMismatch) {
|
|
68037
68257
|
log2(` skipped scenario "${scenario.name}": ${intentMismatch}.`);
|
|
@@ -68843,10 +69063,22 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
68843
69063
|
if (!/\b(tap|click|press)\b/i.test(step.action))
|
|
68844
69064
|
return false;
|
|
68845
69065
|
const text = mobileStepSearchText(step);
|
|
68846
|
-
if (/\b(logout|log\s*out|sign\s*out|reset|forgot|
|
|
69066
|
+
if (/\b(logout|log\s*out|sign\s*out|reset|forgot|go\s+back|back\s+to|cancel|close|dismiss)\b/.test(text)) {
|
|
68847
69067
|
return false;
|
|
68848
69068
|
}
|
|
68849
|
-
|
|
69069
|
+
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)) {
|
|
69070
|
+
return false;
|
|
69071
|
+
}
|
|
69072
|
+
return /\b(sign\s*in|signin|log\s*in|login|continue|submit|done|next|confirm)\b/.test(text);
|
|
69073
|
+
}
|
|
69074
|
+
function isKeyboardFormCommitStep(step) {
|
|
69075
|
+
if (!/\b(type|fill|input|enter)\b/i.test(step.action))
|
|
69076
|
+
return false;
|
|
69077
|
+
const value = typeof step.value === "string" ? step.value : "";
|
|
69078
|
+
return value.endsWith("\n");
|
|
69079
|
+
}
|
|
69080
|
+
function isFormCommitStep(step) {
|
|
69081
|
+
return isCredentialSubmitTap(step) || isKeyboardFormCommitStep(step);
|
|
68850
69082
|
}
|
|
68851
69083
|
function detectInvalidAuthValidationSetup(steps, error2) {
|
|
68852
69084
|
if (!/\b(element not found|no such element|not visible|timeout|assert|could not be located|unexpected screen|screen drift)\b/i.test(error2 ?? "")) {
|
|
@@ -68871,20 +69103,23 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
68871
69103
|
if ((step.order ?? -1) >= lastCredentialEditOrder)
|
|
68872
69104
|
return false;
|
|
68873
69105
|
const text = mobileStepSearchText(step);
|
|
68874
|
-
return /\b(
|
|
69106
|
+
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);
|
|
68875
69107
|
});
|
|
68876
69108
|
if (hasCredentialFormEvidence)
|
|
68877
69109
|
return null;
|
|
68878
69110
|
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";
|
|
68879
69111
|
}
|
|
68880
|
-
const hasSubmitAfterCredentialEdit = steps.some((step) => (step.order ?? -1) > lastCredentialEditOrder &&
|
|
69112
|
+
const hasSubmitAfterCredentialEdit = steps.some((step) => (step.order ?? -1) > lastCredentialEditOrder && isFormCommitStep(step));
|
|
68881
69113
|
if (hasSubmitAfterCredentialEdit)
|
|
68882
69114
|
return null;
|
|
69115
|
+
const hasKeyboardCommitOnCredential = steps.some((step) => (step.order ?? -1) === lastCredentialEditOrder && isKeyboardFormCommitStep(step));
|
|
69116
|
+
if (hasKeyboardCommitOnCredential)
|
|
69117
|
+
return null;
|
|
68883
69118
|
const hasValidationAssertion = steps.some((step) => {
|
|
68884
69119
|
if (!/\b(assert|wait)\b/i.test(step.action))
|
|
68885
69120
|
return false;
|
|
68886
69121
|
const text = mobileStepSearchText(step);
|
|
68887
|
-
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);
|
|
69122
|
+
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);
|
|
68888
69123
|
});
|
|
68889
69124
|
if (hasValidationAssertion)
|
|
68890
69125
|
return null;
|
|
@@ -298740,7 +298975,8 @@ var require_bootstrap_grok_auth = __commonJS({
|
|
|
298740
298975
|
const home = process.env.HOME ?? "/home/appuser";
|
|
298741
298976
|
const authPath = node_path_1.default.join(home, ".grok", "auth.json");
|
|
298742
298977
|
const b64 = process.env.GROK_AUTH_JSON_B64;
|
|
298743
|
-
|
|
298978
|
+
const force = process.env.GROK_AUTH_FORCE_RESEED === "true" || process.env.GROK_AUTH_FORCE_RESEED === "1";
|
|
298979
|
+
if (!force && node_fs_1.default.existsSync(authPath)) {
|
|
298744
298980
|
return;
|
|
298745
298981
|
}
|
|
298746
298982
|
if (!b64) {
|
|
@@ -298751,7 +298987,7 @@ var require_bootstrap_grok_auth = __commonJS({
|
|
|
298751
298987
|
node_fs_1.default.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
298752
298988
|
node_fs_1.default.writeFileSync(authPath, Buffer.from(b64, "base64"));
|
|
298753
298989
|
node_fs_1.default.chmodSync(authPath, 384);
|
|
298754
|
-
console.log(`[grok-auth][runner] Seeded ${authPath} from GROK_AUTH_JSON_B64`);
|
|
298990
|
+
console.log(force ? `[grok-auth][runner] Force-reseeded ${authPath} from GROK_AUTH_JSON_B64` : `[grok-auth][runner] Seeded ${authPath} from GROK_AUTH_JSON_B64`);
|
|
298755
298991
|
} catch (err) {
|
|
298756
298992
|
const message = err instanceof Error ? err.message : String(err);
|
|
298757
298993
|
console.warn(`[grok-auth][runner] Failed to seed auth.json: ${message}`);
|
|
@@ -299396,6 +299632,37 @@ var require_proven_actions_adapter = __commonJS({
|
|
|
299396
299632
|
}
|
|
299397
299633
|
});
|
|
299398
299634
|
|
|
299635
|
+
// ../runner-core/dist/services/agentic-cli-constants.js
|
|
299636
|
+
var require_agentic_cli_constants = __commonJS({
|
|
299637
|
+
"../runner-core/dist/services/agentic-cli-constants.js"(exports2) {
|
|
299638
|
+
"use strict";
|
|
299639
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
299640
|
+
exports2.GROK_CLI_DEFAULT_REASONING_EFFORT = exports2.GROK_CLI_DEFAULT_MODEL = exports2.CLAUDE_CODE_DEFAULT_EFFORT = exports2.CLAUDE_CODE_DEFAULT_MODEL = exports2.MODEL_CLAUDE_CODE = exports2.MODEL_GROK_BUILD = void 0;
|
|
299641
|
+
exports2.isGrokBuildModel = isGrokBuildModel;
|
|
299642
|
+
exports2.isClaudeCodeModel = isClaudeCodeModel;
|
|
299643
|
+
exports2.getAgenticEngine = getAgenticEngine;
|
|
299644
|
+
exports2.MODEL_GROK_BUILD = "grok-build";
|
|
299645
|
+
function isGrokBuildModel(id) {
|
|
299646
|
+
return id === exports2.MODEL_GROK_BUILD;
|
|
299647
|
+
}
|
|
299648
|
+
exports2.MODEL_CLAUDE_CODE = "claude-code";
|
|
299649
|
+
function isClaudeCodeModel(id) {
|
|
299650
|
+
return id === exports2.MODEL_CLAUDE_CODE;
|
|
299651
|
+
}
|
|
299652
|
+
function getAgenticEngine(modelId) {
|
|
299653
|
+
if (isGrokBuildModel(modelId))
|
|
299654
|
+
return "grok-build";
|
|
299655
|
+
if (isClaudeCodeModel(modelId))
|
|
299656
|
+
return "claude-code";
|
|
299657
|
+
return "native";
|
|
299658
|
+
}
|
|
299659
|
+
exports2.CLAUDE_CODE_DEFAULT_MODEL = "claude-sonnet-5";
|
|
299660
|
+
exports2.CLAUDE_CODE_DEFAULT_EFFORT = "max";
|
|
299661
|
+
exports2.GROK_CLI_DEFAULT_MODEL = "grok-4.5";
|
|
299662
|
+
exports2.GROK_CLI_DEFAULT_REASONING_EFFORT = "high";
|
|
299663
|
+
}
|
|
299664
|
+
});
|
|
299665
|
+
|
|
299399
299666
|
// ../runner-core/dist/services/grok-cli.js
|
|
299400
299667
|
var require_grok_cli = __commonJS({
|
|
299401
299668
|
"../runner-core/dist/services/grok-cli.js"(exports2) {
|
|
@@ -299406,6 +299673,7 @@ var require_grok_cli = __commonJS({
|
|
|
299406
299673
|
exports2.runGrok = runGrok;
|
|
299407
299674
|
var node_child_process_1 = require("child_process");
|
|
299408
299675
|
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
299676
|
+
var agentic_cli_constants_js_1 = require_agentic_cli_constants();
|
|
299409
299677
|
function extractAssistantJson(stdout) {
|
|
299410
299678
|
const tryParse = (s) => {
|
|
299411
299679
|
const trimmed = s.trim();
|
|
@@ -299500,7 +299768,8 @@ var require_grok_cli = __commonJS({
|
|
|
299500
299768
|
function runGrok(options) {
|
|
299501
299769
|
const {
|
|
299502
299770
|
prompt,
|
|
299503
|
-
model =
|
|
299771
|
+
model = agentic_cli_constants_js_1.GROK_CLI_DEFAULT_MODEL,
|
|
299772
|
+
reasoningEffort = agentic_cli_constants_js_1.GROK_CLI_DEFAULT_REASONING_EFFORT,
|
|
299504
299773
|
maxTurns,
|
|
299505
299774
|
// undefined = let grok decide when to quit
|
|
299506
299775
|
timeoutMs = 6e5,
|
|
@@ -299529,6 +299798,9 @@ var require_grok_cli = __commonJS({
|
|
|
299529
299798
|
"--always-approve",
|
|
299530
299799
|
"--no-alt-screen"
|
|
299531
299800
|
];
|
|
299801
|
+
if (reasoningEffort) {
|
|
299802
|
+
args.push("--reasoning-effort", reasoningEffort);
|
|
299803
|
+
}
|
|
299532
299804
|
if (typeof maxTurns === "number" && maxTurns > 0) {
|
|
299533
299805
|
args.push("--max-turns", String(maxTurns));
|
|
299534
299806
|
}
|
|
@@ -299674,7 +299946,7 @@ var require_grok_per_test_healer = __commonJS({
|
|
|
299674
299946
|
var HEAL_MCP_CAPABILITIES = ["core", "core-navigation", "core-input", "core-tabs", "testing"];
|
|
299675
299947
|
var grok_cli_js_1 = require_grok_cli();
|
|
299676
299948
|
var DEFAULT_MAX_ATTEMPTS = 3;
|
|
299677
|
-
var
|
|
299949
|
+
var DEFAULT_TIMEOUT_MS2 = 30 * 60 * 1e3;
|
|
299678
299950
|
var IDLE_WATCHDOG_MS = 10 * 60 * 1e3;
|
|
299679
299951
|
function getMaxAttempts() {
|
|
299680
299952
|
const raw = process.env.GROK_PER_TEST_HEAL_MAX_ATTEMPTS;
|
|
@@ -299684,7 +299956,7 @@ var require_grok_per_test_healer = __commonJS({
|
|
|
299684
299956
|
function getAttemptTimeoutMs() {
|
|
299685
299957
|
const raw = process.env.GROK_PER_TEST_HEAL_TIMEOUT_MS;
|
|
299686
299958
|
const parsed = raw ? parseInt(raw, 10) : NaN;
|
|
299687
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed :
|
|
299959
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TIMEOUT_MS2;
|
|
299688
299960
|
}
|
|
299689
299961
|
async function executeGrokPerTestHeal2(playwrightCode, steps, baseUrl, options = {}) {
|
|
299690
299962
|
const externalLogSink = options.onLog;
|
|
@@ -300097,7 +300369,7 @@ Output the JSON envelope exactly ONCE as your final message, then exit.`;
|
|
|
300097
300369
|
"-p",
|
|
300098
300370
|
'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"}',
|
|
300099
300371
|
"-m",
|
|
300100
|
-
"grok-
|
|
300372
|
+
"grok-4.5",
|
|
300101
300373
|
// streaming-json: per-event NDJSON so the UI's GrokSessionLog drawer can
|
|
300102
300374
|
// show progress instead of a frozen "starting…" until the final envelope.
|
|
300103
300375
|
"--output-format",
|
|
@@ -301317,7 +301589,7 @@ When done with all ${input.tests.length} tests in this chunk, exit.`;
|
|
|
301317
301589
|
"-p",
|
|
301318
301590
|
"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.",
|
|
301319
301591
|
"-m",
|
|
301320
|
-
"grok-
|
|
301592
|
+
"grok-4.5",
|
|
301321
301593
|
// streaming-json so per-token output reaches the UI for live progress.
|
|
301322
301594
|
"--output-format",
|
|
301323
301595
|
"streaming-json",
|
|
@@ -308806,47 +309078,6 @@ var require_sitemap_builder = __commonJS({
|
|
|
308806
309078
|
}
|
|
308807
309079
|
});
|
|
308808
309080
|
|
|
308809
|
-
// ../runner-core/dist/services/agentic-cli-constants.js
|
|
308810
|
-
var require_agentic_cli_constants = __commonJS({
|
|
308811
|
-
"../runner-core/dist/services/agentic-cli-constants.js"(exports2) {
|
|
308812
|
-
"use strict";
|
|
308813
|
-
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
308814
|
-
exports2.CLAUDE_CODE_MINIMAX_FALLBACK_CHAIN = exports2.CLAUDE_CODE_MINIMAX_DEFAULT_MODEL = exports2.CLAUDE_CODE_DEFAULT_MODEL = exports2.MODEL_CLAUDE_CODE_MINIMAX = exports2.MODEL_CLAUDE_CODE = exports2.MODEL_GROK_BUILD = void 0;
|
|
308815
|
-
exports2.isGrokBuildModel = isGrokBuildModel;
|
|
308816
|
-
exports2.isClaudeCodeModel = isClaudeCodeModel;
|
|
308817
|
-
exports2.isClaudeCodeMinimaxModel = isClaudeCodeMinimaxModel;
|
|
308818
|
-
exports2.getAgenticEngine = getAgenticEngine;
|
|
308819
|
-
exports2.MODEL_GROK_BUILD = "grok-build";
|
|
308820
|
-
function isGrokBuildModel(id) {
|
|
308821
|
-
return id === exports2.MODEL_GROK_BUILD;
|
|
308822
|
-
}
|
|
308823
|
-
exports2.MODEL_CLAUDE_CODE = "claude-code";
|
|
308824
|
-
function isClaudeCodeModel(id) {
|
|
308825
|
-
return id === exports2.MODEL_CLAUDE_CODE;
|
|
308826
|
-
}
|
|
308827
|
-
exports2.MODEL_CLAUDE_CODE_MINIMAX = "claude-code-minimax";
|
|
308828
|
-
function isClaudeCodeMinimaxModel(id) {
|
|
308829
|
-
return id === exports2.MODEL_CLAUDE_CODE_MINIMAX;
|
|
308830
|
-
}
|
|
308831
|
-
function getAgenticEngine(modelId) {
|
|
308832
|
-
if (isGrokBuildModel(modelId))
|
|
308833
|
-
return "grok-build";
|
|
308834
|
-
if (isClaudeCodeMinimaxModel(modelId))
|
|
308835
|
-
return "claude-code-minimax";
|
|
308836
|
-
if (isClaudeCodeModel(modelId))
|
|
308837
|
-
return "claude-code";
|
|
308838
|
-
return "native";
|
|
308839
|
-
}
|
|
308840
|
-
exports2.CLAUDE_CODE_DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
308841
|
-
exports2.CLAUDE_CODE_MINIMAX_DEFAULT_MODEL = "MiniMax-M3";
|
|
308842
|
-
exports2.CLAUDE_CODE_MINIMAX_FALLBACK_CHAIN = [
|
|
308843
|
-
"MiniMax-M3",
|
|
308844
|
-
"MiniMax-M2.7-highspeed",
|
|
308845
|
-
"MiniMax-M2.7"
|
|
308846
|
-
];
|
|
308847
|
-
}
|
|
308848
|
-
});
|
|
308849
|
-
|
|
308850
309081
|
// ../runner-core/dist/services/auth-capture-run.js
|
|
308851
309082
|
var require_auth_capture_run = __commonJS({
|
|
308852
309083
|
"../runner-core/dist/services/auth-capture-run.js"(exports2) {
|
|
@@ -309650,7 +309881,7 @@ var require_grok_survey_runner = __commonJS({
|
|
|
309650
309881
|
var scrub_credentials_js_1 = require_scrub_credentials();
|
|
309651
309882
|
var agentic_survey_prompts_js_1 = require_agentic_survey_prompts();
|
|
309652
309883
|
var MCP_CAPABILITIES = ["core", "core-navigation", "core-input", "core-tabs", "testing"];
|
|
309653
|
-
var
|
|
309884
|
+
var DEFAULT_TIMEOUT_MS2 = 15 * 60 * 1e3;
|
|
309654
309885
|
var RETRY_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
309655
309886
|
function extractCredentials(vars) {
|
|
309656
309887
|
if (!vars)
|
|
@@ -310086,7 +310317,7 @@ var require_grok_survey_runner = __commonJS({
|
|
|
310086
310317
|
}
|
|
310087
310318
|
async function runGrokSurveyOnRunner(ctx, options = {}) {
|
|
310088
310319
|
const maxTurns = options.maxTurns;
|
|
310089
|
-
const timeoutMs = options.timeoutMs ??
|
|
310320
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
310090
310321
|
const logs2 = [];
|
|
310091
310322
|
const log2 = (line) => {
|
|
310092
310323
|
logs2.push(line);
|
|
@@ -310220,7 +310451,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
310220
310451
|
try {
|
|
310221
310452
|
const result = await (0, grok_cli_js_1.runGrok)({
|
|
310222
310453
|
prompt: kickoffPrompt,
|
|
310223
|
-
model: "grok-
|
|
310454
|
+
model: "grok-4.5",
|
|
310224
310455
|
maxTurns,
|
|
310225
310456
|
timeoutMs,
|
|
310226
310457
|
homeDir: grokHomeDir,
|
|
@@ -310267,7 +310498,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
310267
310498
|
try {
|
|
310268
310499
|
const retryResult = await (0, grok_cli_js_1.runGrok)({
|
|
310269
310500
|
prompt: fixPrompt,
|
|
310270
|
-
model: "grok-
|
|
310501
|
+
model: "grok-4.5",
|
|
310271
310502
|
// Retry: also let grok decide turns; the shorter retry budget lives
|
|
310272
310503
|
// in the wall-clock timeout below.
|
|
310273
310504
|
timeoutMs: RETRY_TIMEOUT_MS,
|
|
@@ -310400,20 +310631,19 @@ var require_canvas_detector = __commonJS({
|
|
|
310400
310631
|
return { kind: "override", canvasMode: false };
|
|
310401
310632
|
if (opts.engine === "claude-code")
|
|
310402
310633
|
return { kind: "canvas-mode", canvasMode: true };
|
|
310403
|
-
if (opts.engine === "
|
|
310634
|
+
if (opts.engine === "native")
|
|
310404
310635
|
return { kind: "bail" };
|
|
310405
310636
|
return { kind: "proceed", canvasMode: false };
|
|
310406
310637
|
}
|
|
310407
310638
|
function modelSupportsVision(modelId) {
|
|
310408
310639
|
if (!modelId)
|
|
310409
310640
|
return false;
|
|
310410
|
-
if (modelId === "claude-code-minimax")
|
|
310411
|
-
return false;
|
|
310412
310641
|
if (modelId === "claude-code")
|
|
310413
310642
|
return true;
|
|
310414
310643
|
if (modelId === "grok-build")
|
|
310415
310644
|
return false;
|
|
310416
310645
|
const visionCapable = /* @__PURE__ */ new Set([
|
|
310646
|
+
"claude-sonnet-5",
|
|
310417
310647
|
"claude-sonnet-4-6",
|
|
310418
310648
|
"claude-opus-4-7",
|
|
310419
310649
|
"claude-haiku-4-5",
|
|
@@ -311590,7 +311820,7 @@ var require_grok_discovery_runner = __commonJS({
|
|
|
311590
311820
|
"core-tabs",
|
|
311591
311821
|
"testing"
|
|
311592
311822
|
];
|
|
311593
|
-
var
|
|
311823
|
+
var DEFAULT_TIMEOUT_MS2 = 20 * 60 * 1e3;
|
|
311594
311824
|
var RETRY_TIMEOUT_MS = 6 * 60 * 1e3;
|
|
311595
311825
|
var DEFAULT_TARGET = { min: 6, max: 15 };
|
|
311596
311826
|
var WATCHER_DEBOUNCE_MS = 500;
|
|
@@ -311754,7 +311984,7 @@ var require_grok_discovery_runner = __commonJS({
|
|
|
311754
311984
|
const manifestFeatureRoutes = Array.isArray(manifestShape?.feature?.pages) ? manifestShape.feature.pages.filter((r) => typeof r === "string" && r.length > 0) : [];
|
|
311755
311985
|
const featureRoutes = manifestFeatureRoutes.length > 0 ? manifestFeatureRoutes : ctx.featureContext?.pageIntel?.map((p) => p.route) ?? ctx.focusAreas ?? [];
|
|
311756
311986
|
const targetTestCount = options.targetTestCount ?? DEFAULT_TARGET;
|
|
311757
|
-
const timeoutMs = options.timeoutMs ??
|
|
311987
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
311758
311988
|
const logs2 = [];
|
|
311759
311989
|
const log2 = (line) => {
|
|
311760
311990
|
logs2.push(line);
|
|
@@ -312359,7 +312589,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
312359
312589
|
try {
|
|
312360
312590
|
const result = await (0, grok_cli_js_1.runGrok)({
|
|
312361
312591
|
prompt: kickoffPrompt,
|
|
312362
|
-
model: "grok-
|
|
312592
|
+
model: "grok-4.5",
|
|
312363
312593
|
timeoutMs,
|
|
312364
312594
|
homeDir: grokHomeDir,
|
|
312365
312595
|
cwd: runDir,
|
|
@@ -312439,7 +312669,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
312439
312669
|
try {
|
|
312440
312670
|
const retryResult = await (0, grok_cli_js_1.runGrok)({
|
|
312441
312671
|
prompt: fixPrompt,
|
|
312442
|
-
model: "grok-
|
|
312672
|
+
model: "grok-4.5",
|
|
312443
312673
|
timeoutMs: RETRY_TIMEOUT_MS,
|
|
312444
312674
|
homeDir: grokHomeDir,
|
|
312445
312675
|
cwd: runDir,
|
|
@@ -312671,7 +312901,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
312671
312901
|
const manifestFeatureRoutes = Array.isArray(manifestShape?.feature?.pages) ? manifestShape.feature.pages.filter((r) => typeof r === "string" && r.length > 0) : [];
|
|
312672
312902
|
const featureRoutes = manifestFeatureRoutes.length > 0 ? manifestFeatureRoutes : ctx.featureContext?.pageIntel?.map((p) => p.route) ?? ctx.focusAreas ?? [];
|
|
312673
312903
|
const targetTestCount = options.targetTestCount ?? DEFAULT_TARGET;
|
|
312674
|
-
const callerTimeoutMs = options.timeoutMs ??
|
|
312904
|
+
const callerTimeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
312675
312905
|
const call1TimeoutMs = Math.max(callerTimeoutMs, GROK_V2_CALL1_TIMEOUT_MS);
|
|
312676
312906
|
const logs2 = [];
|
|
312677
312907
|
const log2 = (line) => {
|
|
@@ -313188,24 +313418,12 @@ var require_claude_cli = __commonJS({
|
|
|
313188
313418
|
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);
|
|
313189
313419
|
}
|
|
313190
313420
|
function runClaudeOnce(options) {
|
|
313191
|
-
const { prompt, model =
|
|
313421
|
+
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;
|
|
313192
313422
|
return new Promise((resolve, reject) => {
|
|
313193
|
-
|
|
313194
|
-
|
|
313195
|
-
|
|
313196
|
-
|
|
313197
|
-
minimaxBaseUrl = process.env.MINIMAX_ANTHROPIC_BASE_URL;
|
|
313198
|
-
minimaxApiKey = process.env.MINIMAX_API_KEY;
|
|
313199
|
-
if (!minimaxBaseUrl || !minimaxApiKey) {
|
|
313200
|
-
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));
|
|
313201
|
-
return;
|
|
313202
|
-
}
|
|
313203
|
-
} else {
|
|
313204
|
-
oauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
313205
|
-
if (!oauthToken) {
|
|
313206
|
-
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));
|
|
313207
|
-
return;
|
|
313208
|
-
}
|
|
313423
|
+
const oauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
313424
|
+
if (!oauthToken) {
|
|
313425
|
+
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));
|
|
313426
|
+
return;
|
|
313209
313427
|
}
|
|
313210
313428
|
const args = [
|
|
313211
313429
|
"-p",
|
|
@@ -313227,8 +313445,8 @@ var require_claude_cli = __commonJS({
|
|
|
313227
313445
|
// Stateless container — don't write session transcripts to disk.
|
|
313228
313446
|
"--no-session-persistence"
|
|
313229
313447
|
];
|
|
313230
|
-
if (
|
|
313231
|
-
args.push("--
|
|
313448
|
+
if (effort && effort.trim().length > 0) {
|
|
313449
|
+
args.push("--effort", effort.trim());
|
|
313232
313450
|
}
|
|
313233
313451
|
if (typeof maxTurns === "number" && maxTurns > 0) {
|
|
313234
313452
|
args.push("--max-turns", String(maxTurns));
|
|
@@ -313253,21 +313471,9 @@ var require_claude_cli = __commonJS({
|
|
|
313253
313471
|
MCP_TIMEOUT: process.env.MCP_TIMEOUT ?? "120000"
|
|
313254
313472
|
}
|
|
313255
313473
|
});
|
|
313256
|
-
|
|
313257
|
-
|
|
313258
|
-
|
|
313259
|
-
spawnEnv.ANTHROPIC_AUTH_TOKEN = minimaxApiKey;
|
|
313260
|
-
delete spawnEnv.CLAUDE_CODE_OAUTH_TOKEN;
|
|
313261
|
-
delete spawnEnv.ANTHROPIC_MODEL;
|
|
313262
|
-
delete spawnEnv.ANTHROPIC_DEFAULT_SONNET_MODEL;
|
|
313263
|
-
delete spawnEnv.ANTHROPIC_DEFAULT_OPUS_MODEL;
|
|
313264
|
-
delete spawnEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL;
|
|
313265
|
-
delete spawnEnv.ANTHROPIC_SMALL_FAST_MODEL;
|
|
313266
|
-
} else {
|
|
313267
|
-
spawnEnv.CLAUDE_CODE_OAUTH_TOKEN = oauthToken;
|
|
313268
|
-
delete spawnEnv.ANTHROPIC_API_KEY;
|
|
313269
|
-
delete spawnEnv.ANTHROPIC_AUTH_TOKEN;
|
|
313270
|
-
}
|
|
313474
|
+
spawnEnv.CLAUDE_CODE_OAUTH_TOKEN = oauthToken;
|
|
313475
|
+
delete spawnEnv.ANTHROPIC_API_KEY;
|
|
313476
|
+
delete spawnEnv.ANTHROPIC_AUTH_TOKEN;
|
|
313271
313477
|
const isWindows = process.platform === "win32";
|
|
313272
313478
|
const proc = (0, node_child_process_1.spawn)("claude", args, {
|
|
313273
313479
|
env: spawnEnv,
|
|
@@ -313379,7 +313585,7 @@ var require_claude_cli = __commonJS({
|
|
|
313379
313585
|
}
|
|
313380
313586
|
if (exitCode !== 0) {
|
|
313381
313587
|
const authFailure = looksLikeAuthFailure(stdout + "\n" + stderr);
|
|
313382
|
-
const msg = authFailure ?
|
|
313588
|
+
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})` : ""}`;
|
|
313383
313589
|
reject(new ClaudeSpawnError(msg, exitCode, stderr.trim(), stdout.trim(), authFailure));
|
|
313384
313590
|
return;
|
|
313385
313591
|
}
|
|
@@ -313392,39 +313598,8 @@ var require_claude_cli = __commonJS({
|
|
|
313392
313598
|
});
|
|
313393
313599
|
});
|
|
313394
313600
|
}
|
|
313395
|
-
function minimaxClaudeFallbackModels(requested) {
|
|
313396
|
-
const chain = agentic_cli_constants_js_1.CLAUDE_CODE_MINIMAX_FALLBACK_CHAIN;
|
|
313397
|
-
const idx = chain.indexOf(requested);
|
|
313398
|
-
if (idx === -1)
|
|
313399
|
-
return [requested];
|
|
313400
|
-
return chain.slice(idx);
|
|
313401
|
-
}
|
|
313402
313601
|
async function runClaude(options) {
|
|
313403
|
-
|
|
313404
|
-
return runClaudeOnce(options);
|
|
313405
|
-
const requested = options.model ?? agentic_cli_constants_js_1.CLAUDE_CODE_MINIMAX_DEFAULT_MODEL;
|
|
313406
|
-
const chain = minimaxClaudeFallbackModels(requested);
|
|
313407
|
-
if (chain.length <= 1)
|
|
313408
|
-
return runClaudeOnce({ ...options, model: requested });
|
|
313409
|
-
let lastError;
|
|
313410
|
-
for (let i = 0; i < chain.length; i++) {
|
|
313411
|
-
const model = chain[i];
|
|
313412
|
-
const isLast = i === chain.length - 1;
|
|
313413
|
-
try {
|
|
313414
|
-
const result = await runClaudeOnce({ ...options, model });
|
|
313415
|
-
if (i > 0)
|
|
313416
|
-
console.warn(`[claude-cli] MiniMax CLI failover succeeded on ${model} (requested ${requested})`);
|
|
313417
|
-
return result;
|
|
313418
|
-
} catch (err) {
|
|
313419
|
-
lastError = err;
|
|
313420
|
-
const spawnErr = err instanceof ClaudeSpawnError ? err : null;
|
|
313421
|
-
if (isLast || spawnErr?.isAuthError || spawnErr?.isTimeout)
|
|
313422
|
-
throw err;
|
|
313423
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
313424
|
-
console.warn(`[claude-cli] MiniMax CLI ${model} failed (${reason}) \u2014 failing over to ${chain[i + 1]}`);
|
|
313425
|
-
}
|
|
313426
|
-
}
|
|
313427
|
-
throw lastError;
|
|
313602
|
+
return runClaudeOnce(options);
|
|
313428
313603
|
}
|
|
313429
313604
|
}
|
|
313430
313605
|
});
|
|
@@ -313809,6 +313984,8 @@ var require_claude_pricing = __commonJS({
|
|
|
313809
313984
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
313810
313985
|
exports2.computeClaudeCostUsd = computeClaudeCostUsd;
|
|
313811
313986
|
var CLAUDE_PRICE_PER_M = {
|
|
313987
|
+
// Introductory pricing through 2026-08-31 ($2/$10); standard is $3/$15.
|
|
313988
|
+
"claude-sonnet-5": { input: 2, output: 10 },
|
|
313812
313989
|
"claude-sonnet-4-6": { input: 3, output: 15 },
|
|
313813
313990
|
"claude-haiku-4-5-20251001": { input: 1, output: 5 },
|
|
313814
313991
|
"claude-opus-4-7": { input: 5, output: 25 }
|
|
@@ -313848,7 +314025,7 @@ var require_claude_survey_runner = __commonJS({
|
|
|
313848
314025
|
var agentic_survey_prompts_js_1 = require_agentic_survey_prompts();
|
|
313849
314026
|
var MCP_CAPABILITIES = ["core", "core-navigation", "core-input", "core-tabs", "testing"];
|
|
313850
314027
|
var CANVAS_MODE_EXTRA_CAPABILITIES = ["vision"];
|
|
313851
|
-
var
|
|
314028
|
+
var DEFAULT_TIMEOUT_MS2 = 15 * 60 * 1e3;
|
|
313852
314029
|
var RETRY_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
313853
314030
|
async function logResultFile(path, log2) {
|
|
313854
314031
|
try {
|
|
@@ -313860,10 +314037,10 @@ ${preview}`);
|
|
|
313860
314037
|
log2("[diagnostic] result file does not exist \u2014 Claude never wrote it");
|
|
313861
314038
|
}
|
|
313862
314039
|
}
|
|
313863
|
-
async function runClaudeSurveyOnRunner(ctx, options = {}
|
|
314040
|
+
async function runClaudeSurveyOnRunner(ctx, options = {}) {
|
|
313864
314041
|
const maxTurns = options.maxTurns;
|
|
313865
|
-
const timeoutMs = options.timeoutMs ??
|
|
313866
|
-
const claudeDefaultModel =
|
|
314042
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
314043
|
+
const claudeDefaultModel = agentic_cli_constants_js_1.CLAUDE_CODE_DEFAULT_MODEL;
|
|
313867
314044
|
const logs2 = [];
|
|
313868
314045
|
const log2 = (line) => {
|
|
313869
314046
|
logs2.push(line);
|
|
@@ -314007,7 +314184,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
314007
314184
|
console.log(`[claude-survey] ${line}`);
|
|
314008
314185
|
};
|
|
314009
314186
|
const onAudit = options.onAudit;
|
|
314010
|
-
const claudeProviderLabel =
|
|
314187
|
+
const claudeProviderLabel = "anthropic";
|
|
314011
314188
|
const claudeModelLabel = claudeDefaultModel;
|
|
314012
314189
|
const surveyExploreCallType = "surveyExplore";
|
|
314013
314190
|
const sessionStartedAt = Date.now();
|
|
@@ -314043,7 +314220,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
314043
314220
|
groupId: auditGroupId,
|
|
314044
314221
|
callType: surveyExploreCallType,
|
|
314045
314222
|
model: claudeModelLabel,
|
|
314046
|
-
provider:
|
|
314223
|
+
provider: "anthropic",
|
|
314047
314224
|
durationMs: turn.durationMs ?? 0,
|
|
314048
314225
|
promptTokens,
|
|
314049
314226
|
completionTokens,
|
|
@@ -314086,7 +314263,6 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
314086
314263
|
const result = await (0, claude_cli_js_1.runClaude)({
|
|
314087
314264
|
prompt: kickoffPrompt,
|
|
314088
314265
|
model: claudeDefaultModel,
|
|
314089
|
-
provider,
|
|
314090
314266
|
maxTurns,
|
|
314091
314267
|
timeoutMs,
|
|
314092
314268
|
cwd: runDir,
|
|
@@ -314146,7 +314322,6 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
314146
314322
|
const retryResult = await (0, claude_cli_js_1.runClaude)({
|
|
314147
314323
|
prompt: fixPrompt,
|
|
314148
314324
|
model: claudeDefaultModel,
|
|
314149
|
-
provider,
|
|
314150
314325
|
timeoutMs: RETRY_TIMEOUT_MS,
|
|
314151
314326
|
cwd: runDir,
|
|
314152
314327
|
mcpConfigPath,
|
|
@@ -314202,7 +314377,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
314202
314377
|
groupId: auditGroupId,
|
|
314203
314378
|
callType: surveyExploreCallType,
|
|
314204
314379
|
model: claudeModelLabel,
|
|
314205
|
-
provider:
|
|
314380
|
+
provider: "anthropic",
|
|
314206
314381
|
durationMs: sessionDurationMs,
|
|
314207
314382
|
promptTokens: sessionInputTokens,
|
|
314208
314383
|
completionTokens: sessionOutputTokens,
|
|
@@ -314578,11 +314753,11 @@ var require_claude_discovery_adapter = __commonJS({
|
|
|
314578
314753
|
var PER_TEST_IDLE_KILL_MS = 10 * 60 * 1e3;
|
|
314579
314754
|
var PER_TEST_MAX_TURNS = 120;
|
|
314580
314755
|
function createClaudeDiscoveryAdapter(args) {
|
|
314581
|
-
const {
|
|
314756
|
+
const { claudeDefaultModel } = args;
|
|
314582
314757
|
return {
|
|
314583
|
-
engine:
|
|
314758
|
+
engine: "claude-code",
|
|
314584
314759
|
auditModel: claudeDefaultModel,
|
|
314585
|
-
auditProvider:
|
|
314760
|
+
auditProvider: "anthropic",
|
|
314586
314761
|
computeSessionCostUsd(usage) {
|
|
314587
314762
|
return (0, claude_pricing_js_1.computeClaudeCostUsd)(claudeDefaultModel, {
|
|
314588
314763
|
inputTokens: usage.inputTokens,
|
|
@@ -314623,7 +314798,7 @@ var require_claude_discovery_adapter = __commonJS({
|
|
|
314623
314798
|
prompt,
|
|
314624
314799
|
appendSystemPrompt,
|
|
314625
314800
|
model: claudeDefaultModel,
|
|
314626
|
-
provider,
|
|
314801
|
+
provider: "anthropic",
|
|
314627
314802
|
maxTurns: PER_TEST_MAX_TURNS,
|
|
314628
314803
|
// A per-test session needs only browser MCP + the file tools — never a
|
|
314629
314804
|
// shell. Disallow Bash to shrink the off-task surface (and Task, the
|
|
@@ -314695,17 +314870,17 @@ var require_claude_discovery_runner = __commonJS({
|
|
|
314695
314870
|
];
|
|
314696
314871
|
var CALL1_TIMEOUT_MS = 38 * 60 * 1e3;
|
|
314697
314872
|
var DISCOVERY_IDLE_KILL_MS = 15 * 60 * 1e3;
|
|
314698
|
-
var
|
|
314873
|
+
var DEFAULT_TIMEOUT_MS2 = 20 * 60 * 1e3;
|
|
314699
314874
|
var DEFAULT_TARGET = { min: 6, max: 15 };
|
|
314700
|
-
async function runClaudeDiscoveryOnRunner(ctx, callbackOptions, options = {}
|
|
314701
|
-
const claudeDefaultModel =
|
|
314702
|
-
const claudeProviderLabel =
|
|
314875
|
+
async function runClaudeDiscoveryOnRunner(ctx, callbackOptions, options = {}) {
|
|
314876
|
+
const claudeDefaultModel = agentic_cli_constants_js_1.CLAUDE_CODE_DEFAULT_MODEL;
|
|
314877
|
+
const claudeProviderLabel = "anthropic";
|
|
314703
314878
|
const featureName = ctx.featureName ?? "Untitled Feature";
|
|
314704
314879
|
const manifestShape = (0, grok_discovery_runner_js_1.narrowManifest)(ctx.featureContextManifest);
|
|
314705
314880
|
const manifestFeatureRoutes = Array.isArray(manifestShape?.feature?.pages) ? manifestShape.feature.pages.filter((r) => typeof r === "string" && r.length > 0) : [];
|
|
314706
314881
|
const featureRoutes = manifestFeatureRoutes.length > 0 ? manifestFeatureRoutes : ctx.featureContext?.pageIntel?.map((p) => p.route) ?? ctx.focusAreas ?? [];
|
|
314707
314882
|
const targetTestCount = options.targetTestCount ?? DEFAULT_TARGET;
|
|
314708
|
-
const callerTimeoutMs = options.timeoutMs ??
|
|
314883
|
+
const callerTimeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
314709
314884
|
const call1TimeoutMs = Math.max(callerTimeoutMs, CALL1_TIMEOUT_MS);
|
|
314710
314885
|
const logs2 = [];
|
|
314711
314886
|
const log2 = (line) => {
|
|
@@ -314952,7 +315127,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
314952
315127
|
groupId: ctx.sessionId ?? featureName,
|
|
314953
315128
|
callType: featureExploreCallType,
|
|
314954
315129
|
model: claudeModelLabel,
|
|
314955
|
-
provider:
|
|
315130
|
+
provider: "anthropic",
|
|
314956
315131
|
durationMs: turn.durationMs ?? 0,
|
|
314957
315132
|
promptTokens,
|
|
314958
315133
|
completionTokens,
|
|
@@ -315063,7 +315238,6 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
315063
315238
|
// they actually get put there.
|
|
315064
315239
|
appendSystemPrompt: canvasMode ? (0, agentic_discovery_prompts_js_1.buildAuthoringRules)() + (0, canvas_detector_js_1.buildCanvasModeAuthoringAddendum)() : (0, agentic_discovery_prompts_js_1.buildAuthoringRules)(),
|
|
315065
315240
|
model: claudeDefaultModel,
|
|
315066
|
-
provider,
|
|
315067
315241
|
timeoutMs: call1TimeoutMs,
|
|
315068
315242
|
idleKillMs: DISCOVERY_IDLE_KILL_MS,
|
|
315069
315243
|
cwd: runDir,
|
|
@@ -315173,7 +315347,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
315173
315347
|
groupId: ctx.sessionId ?? featureName,
|
|
315174
315348
|
callType: featureExploreCallType,
|
|
315175
315349
|
model: claudeModelLabel,
|
|
315176
|
-
provider:
|
|
315350
|
+
provider: "anthropic",
|
|
315177
315351
|
durationMs: sessionDurationMs,
|
|
315178
315352
|
promptTokens: sessionInputTokens,
|
|
315179
315353
|
completionTokens: sessionOutputTokens,
|
|
@@ -315265,7 +315439,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
315265
315439
|
},
|
|
315266
315440
|
onAudit,
|
|
315267
315441
|
auditGroupId: ctx.sessionId ?? featureName,
|
|
315268
|
-
adapter: (0, claude_discovery_adapter_js_1.createClaudeDiscoveryAdapter)({
|
|
315442
|
+
adapter: (0, claude_discovery_adapter_js_1.createClaudeDiscoveryAdapter)({ claudeDefaultModel })
|
|
315269
315443
|
});
|
|
315270
315444
|
log2(`Verify + Call 2: complete \u2014 ${capturedTests.length} test(s) saved, ${localUnverified.length} unverified`);
|
|
315271
315445
|
}
|
|
@@ -316191,7 +316365,7 @@ var require_discover_agent = __commonJS({
|
|
|
316191
316365
|
} else if (cached === "standard") {
|
|
316192
316366
|
return { renderType: "standard", canvasMode: false, probeRan: false };
|
|
316193
316367
|
} else {
|
|
316194
|
-
const bailEligible = engine === "native"
|
|
316368
|
+
const bailEligible = engine === "native";
|
|
316195
316369
|
const cannotConfirm = (reason) => {
|
|
316196
316370
|
const note = bailEligible ? ` \u26A0 engine=${engine} would have bailed on canvas \u2014 if discovery loops, set Settings \u2192 App renderer manually.` : "";
|
|
316197
316371
|
log2(`[canvas] preflight could not run: ${reason} \u2014 proceeding as standard.${note}`);
|
|
@@ -316510,7 +316684,7 @@ Respond with ONLY valid JSON:
|
|
|
316510
316684
|
// wiring is in place when it does.
|
|
316511
316685
|
onAudit: onAICall
|
|
316512
316686
|
};
|
|
316513
|
-
const cliResult = surveyEngine === "claude-code
|
|
316687
|
+
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);
|
|
316514
316688
|
logs2.push(`${surveyEngine} survey runner finished cleanly.`);
|
|
316515
316689
|
onLog?.("done", logs2.join("\n"));
|
|
316516
316690
|
return {
|
|
@@ -317032,7 +317206,7 @@ Fatal error: ${msg}`);
|
|
|
317032
317206
|
// through the existing DiscoveryAuditCallback (→ aiAuditBatch).
|
|
317033
317207
|
onAudit: onAICall
|
|
317034
317208
|
};
|
|
317035
|
-
const cliResult = deepEngine === "claude-code
|
|
317209
|
+
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);
|
|
317036
317210
|
logs2.push(`${deepEngine} deep discovery finished cleanly. ${cliResult.testsEmitted} tests emitted, ${cliResult.unverified.length} unverified.`);
|
|
317037
317211
|
onLog?.("done", logs2.join("\n"));
|
|
317038
317212
|
return {
|
|
@@ -327912,9 +328086,8 @@ var require_framework_import_runner = __commonJS({
|
|
|
327912
328086
|
const imp = ctx.frameworkImport;
|
|
327913
328087
|
if (!imp)
|
|
327914
328088
|
throw new Error("runFrameworkImportOnRunner: ctx.frameworkImport is required");
|
|
327915
|
-
const
|
|
327916
|
-
const
|
|
327917
|
-
const model = provider === "minimax" ? agentic_cli_constants_js_1.CLAUDE_CODE_MINIMAX_DEFAULT_MODEL : agentic_cli_constants_js_1.CLAUDE_CODE_DEFAULT_MODEL;
|
|
328089
|
+
const provider = "anthropic";
|
|
328090
|
+
const model = agentic_cli_constants_js_1.CLAUDE_CODE_DEFAULT_MODEL;
|
|
327918
328091
|
log2("=== Framework Import Runner (Claude CLI, no browser) ===");
|
|
327919
328092
|
log2(`Source: ${imp.source}${imp.source === "github" ? ` (${imp.repoOwner}/${imp.repoName}@${imp.branch ?? "default"})` : ""}`);
|
|
327920
328093
|
const runDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "framework-import-"));
|
|
@@ -329044,8 +329217,8 @@ var require_repo_analysis_runner = __commonJS({
|
|
|
329044
329217
|
const maxEndpointsPerFeature = intEnv("REPO_ANALYSIS_MAX_ENDPOINTS_PER_FEATURE", 80, 1, 400);
|
|
329045
329218
|
const maxFacts = intEnv("REPO_ANALYSIS_MAX_FACTS", 40, 1, 120);
|
|
329046
329219
|
const engine = (0, agentic_cli_constants_js_1.getAgenticEngine)(ctx.exploreModel);
|
|
329047
|
-
const provider =
|
|
329048
|
-
const model =
|
|
329220
|
+
const provider = "anthropic";
|
|
329221
|
+
const model = agentic_cli_constants_js_1.CLAUDE_CODE_DEFAULT_MODEL;
|
|
329049
329222
|
log2("=== Repo Analysis Runner (Claude CLI, no browser) ===");
|
|
329050
329223
|
log2(`Source: ${imp.source}${imp.source === "github" ? ` (${imp.repoOwner}/${imp.repoName}@${imp.branch ?? "default"})` : ""}`);
|
|
329051
329224
|
const runDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "repo-analysis-"));
|
|
@@ -329528,7 +329701,7 @@ var require_process_inventory = __commonJS({
|
|
|
329528
329701
|
var node_util_1 = require("util");
|
|
329529
329702
|
var execFileAsync3 = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
329530
329703
|
var DEFAULT_LIMIT = 60;
|
|
329531
|
-
var
|
|
329704
|
+
var DEFAULT_TIMEOUT_MS2 = 800;
|
|
329532
329705
|
var MAX_COMMAND_LENGTH = 320;
|
|
329533
329706
|
function roundOne(value) {
|
|
329534
329707
|
return Math.round(value * 10) / 10;
|
|
@@ -329677,7 +329850,7 @@ var require_process_inventory = __commonJS({
|
|
|
329677
329850
|
}
|
|
329678
329851
|
async function getBrowserProcessInventory2(options = {}) {
|
|
329679
329852
|
const limit = Math.max(1, Math.min(200, options.limit ?? DEFAULT_LIMIT));
|
|
329680
|
-
const timeoutMs = Math.max(100, Math.min(5e3, options.timeoutMs ??
|
|
329853
|
+
const timeoutMs = Math.max(100, Math.min(5e3, options.timeoutMs ?? DEFAULT_TIMEOUT_MS2));
|
|
329681
329854
|
try {
|
|
329682
329855
|
const rawProcesses = process.platform === "win32" ? await collectWindowsProcesses(timeoutMs) : await collectUnixProcesses(timeoutMs);
|
|
329683
329856
|
const processes = [...rawProcesses].sort((a, b) => (b.cpuPct ?? 0) - (a.cpuPct ?? 0) || (b.rssMB ?? 0) - (a.rssMB ?? 0)).slice(0, limit);
|
|
@@ -330644,7 +330817,7 @@ var require_package4 = __commonJS({
|
|
|
330644
330817
|
"package.json"(exports2, module2) {
|
|
330645
330818
|
module2.exports = {
|
|
330646
330819
|
name: "@validate.qa/runner",
|
|
330647
|
-
version: "1.0.
|
|
330820
|
+
version: "1.0.35",
|
|
330648
330821
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
330649
330822
|
bin: {
|
|
330650
330823
|
"validate-runner": "dist/cli.js"
|
|
@@ -330652,6 +330825,7 @@ var require_package4 = __commonJS({
|
|
|
330652
330825
|
scripts: {
|
|
330653
330826
|
build: "tsup",
|
|
330654
330827
|
dev: "tsx src/cli.ts",
|
|
330828
|
+
test: "node --import tsx --test src/version-check.test.ts",
|
|
330655
330829
|
"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",
|
|
330656
330830
|
prepublishOnly: "npm run build"
|
|
330657
330831
|
},
|
|
@@ -330780,6 +330954,136 @@ function shouldUseAuthState(tags, playwrightCode) {
|
|
|
330780
330954
|
return !tags.includes(INLINE_LOGIN_TAG) && !hasLegacyInlineLogin;
|
|
330781
330955
|
}
|
|
330782
330956
|
|
|
330957
|
+
// src/version-check.ts
|
|
330958
|
+
var RUNNER_NPM_PACKAGE = "@validate.qa/runner";
|
|
330959
|
+
var NPM_LATEST_URL = `https://registry.npmjs.org/${encodeURIComponent(RUNNER_NPM_PACKAGE)}/latest`;
|
|
330960
|
+
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
330961
|
+
function parseSemver(raw) {
|
|
330962
|
+
const cleaned = raw.trim().replace(/^v/i, "");
|
|
330963
|
+
const match = cleaned.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
|
|
330964
|
+
if (!match) return null;
|
|
330965
|
+
return {
|
|
330966
|
+
major: Number(match[1]),
|
|
330967
|
+
minor: Number(match[2]),
|
|
330968
|
+
patch: Number(match[3]),
|
|
330969
|
+
prerelease: match[4] ?? null
|
|
330970
|
+
};
|
|
330971
|
+
}
|
|
330972
|
+
function compareSemver(a, b) {
|
|
330973
|
+
const pa = parseSemver(a);
|
|
330974
|
+
const pb = parseSemver(b);
|
|
330975
|
+
if (!pa || !pb) return null;
|
|
330976
|
+
if (pa.major !== pb.major) return pa.major < pb.major ? -1 : 1;
|
|
330977
|
+
if (pa.minor !== pb.minor) return pa.minor < pb.minor ? -1 : 1;
|
|
330978
|
+
if (pa.patch !== pb.patch) return pa.patch < pb.patch ? -1 : 1;
|
|
330979
|
+
if (pa.prerelease === pb.prerelease) return 0;
|
|
330980
|
+
if (pa.prerelease === null) return 1;
|
|
330981
|
+
if (pb.prerelease === null) return -1;
|
|
330982
|
+
return pa.prerelease < pb.prerelease ? -1 : pa.prerelease > pb.prerelease ? 1 : 0;
|
|
330983
|
+
}
|
|
330984
|
+
function isVersionOutdated(current, latest) {
|
|
330985
|
+
const cmp = compareSemver(current, latest);
|
|
330986
|
+
return cmp === -1;
|
|
330987
|
+
}
|
|
330988
|
+
function shouldSkipVersionCheck() {
|
|
330989
|
+
if (process.env.RUNNER_CLOUD === "true") {
|
|
330990
|
+
return "cloud runner (RUNNER_CLOUD=true)";
|
|
330991
|
+
}
|
|
330992
|
+
if (process.env.VALIDATE_RUNNER_ALLOW_OUTDATED === "1" || process.env.VALIDATE_RUNNER_ALLOW_OUTDATED === "true") {
|
|
330993
|
+
return "VALIDATE_RUNNER_ALLOW_OUTDATED is set";
|
|
330994
|
+
}
|
|
330995
|
+
return null;
|
|
330996
|
+
}
|
|
330997
|
+
async function fetchLatestRunnerVersion(timeoutMs = DEFAULT_TIMEOUT_MS, fetchImpl = fetch) {
|
|
330998
|
+
const controller = new AbortController();
|
|
330999
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
331000
|
+
try {
|
|
331001
|
+
const res = await fetchImpl(NPM_LATEST_URL, {
|
|
331002
|
+
headers: {
|
|
331003
|
+
Accept: "application/json",
|
|
331004
|
+
// Identify ourselves; helps npm operators + caching intermediaries.
|
|
331005
|
+
"User-Agent": `${RUNNER_NPM_PACKAGE}-version-check`
|
|
331006
|
+
},
|
|
331007
|
+
signal: controller.signal
|
|
331008
|
+
});
|
|
331009
|
+
if (!res.ok) return null;
|
|
331010
|
+
const body = await res.json();
|
|
331011
|
+
return typeof body.version === "string" && body.version.length > 0 ? body.version : null;
|
|
331012
|
+
} catch {
|
|
331013
|
+
return null;
|
|
331014
|
+
} finally {
|
|
331015
|
+
clearTimeout(timer);
|
|
331016
|
+
}
|
|
331017
|
+
}
|
|
331018
|
+
async function checkRunnerVersion(currentVersion, opts = {}) {
|
|
331019
|
+
const skip = shouldSkipVersionCheck();
|
|
331020
|
+
if (skip) {
|
|
331021
|
+
return { outdated: false, currentVersion, latestVersion: null, skippedReason: skip };
|
|
331022
|
+
}
|
|
331023
|
+
const latestVersion = await fetchLatestRunnerVersion(opts.timeoutMs, opts.fetchImpl);
|
|
331024
|
+
if (!latestVersion) {
|
|
331025
|
+
return {
|
|
331026
|
+
outdated: false,
|
|
331027
|
+
currentVersion,
|
|
331028
|
+
latestVersion: null,
|
|
331029
|
+
skippedReason: "could not reach npm registry (or response was invalid)"
|
|
331030
|
+
};
|
|
331031
|
+
}
|
|
331032
|
+
if (!parseSemver(currentVersion) || !parseSemver(latestVersion)) {
|
|
331033
|
+
return {
|
|
331034
|
+
outdated: false,
|
|
331035
|
+
currentVersion,
|
|
331036
|
+
latestVersion,
|
|
331037
|
+
skippedReason: "unparseable version string"
|
|
331038
|
+
};
|
|
331039
|
+
}
|
|
331040
|
+
return {
|
|
331041
|
+
outdated: isVersionOutdated(currentVersion, latestVersion),
|
|
331042
|
+
currentVersion,
|
|
331043
|
+
latestVersion
|
|
331044
|
+
};
|
|
331045
|
+
}
|
|
331046
|
+
function formatUpgradeInstructions(latestVersion) {
|
|
331047
|
+
const lines = [
|
|
331048
|
+
"",
|
|
331049
|
+
"\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",
|
|
331050
|
+
"\u2551 VALIDATE.QA RUNNER UPDATE REQUIRED \u2551",
|
|
331051
|
+
"\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",
|
|
331052
|
+
"\u2551 A newer runner is published on npm. Each runner release changes \u2551",
|
|
331053
|
+
"\u2551 the workflow, so this version can no longer run. \u2551",
|
|
331054
|
+
`\u2551 Latest: ${latestVersion.padEnd(54)}\u2551`,
|
|
331055
|
+
"\u2551 \u2551",
|
|
331056
|
+
"\u2551 Upgrade with: \u2551",
|
|
331057
|
+
`\u2551 npm install -g ${RUNNER_NPM_PACKAGE}@latest`.padEnd(69) + "\u2551",
|
|
331058
|
+
"\u2551 \u2551",
|
|
331059
|
+
"\u2551 Then restart: \u2551",
|
|
331060
|
+
"\u2551 validate-runner start \u2551",
|
|
331061
|
+
"\u2551 # or: npx @validate.qa/runner@latest start \u2551",
|
|
331062
|
+
"\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",
|
|
331063
|
+
""
|
|
331064
|
+
];
|
|
331065
|
+
return lines.join("\n");
|
|
331066
|
+
}
|
|
331067
|
+
async function enforceRunnerUpToDate(currentVersion, opts = {}) {
|
|
331068
|
+
const log2 = opts.log ?? ((msg) => console.error(msg));
|
|
331069
|
+
const result = await checkRunnerVersion(currentVersion, opts);
|
|
331070
|
+
if (result.skippedReason && !result.outdated) {
|
|
331071
|
+
if (result.skippedReason.startsWith("could not reach") || result.skippedReason.startsWith("unparseable")) {
|
|
331072
|
+
log2(`\u26A0 Runner version check skipped: ${result.skippedReason}`);
|
|
331073
|
+
}
|
|
331074
|
+
return result;
|
|
331075
|
+
}
|
|
331076
|
+
if (result.outdated && result.latestVersion) {
|
|
331077
|
+
log2(formatUpgradeInstructions(result.latestVersion));
|
|
331078
|
+
log2(` Installed: ${result.currentVersion}`);
|
|
331079
|
+
log2(` Required: ${result.latestVersion} (or newer)`);
|
|
331080
|
+
log2("");
|
|
331081
|
+
const exit = opts.exitFn ?? ((code) => process.exit(code));
|
|
331082
|
+
exit(1);
|
|
331083
|
+
}
|
|
331084
|
+
return result;
|
|
331085
|
+
}
|
|
331086
|
+
|
|
330783
331087
|
// src/services/appium-executor.ts
|
|
330784
331088
|
init_dist();
|
|
330785
331089
|
|
|
@@ -335813,13 +336117,31 @@ function buildHeaders(authToken, projectId) {
|
|
|
335813
336117
|
return {
|
|
335814
336118
|
"Content-Type": "application/json",
|
|
335815
336119
|
Authorization: `Bearer ${authToken}`,
|
|
335816
|
-
"X-Project-Id": projectId
|
|
336120
|
+
"X-Project-Id": projectId,
|
|
336121
|
+
// Lets the server force-upgrade outdated local installs on /pending too
|
|
336122
|
+
// (not only heartbeat), so they cannot claim work after a new npm publish.
|
|
336123
|
+
"X-Runner-Version": RUNNER_VERSION,
|
|
336124
|
+
"X-Runner-Name": RUNNER_NAME
|
|
335817
336125
|
};
|
|
335818
336126
|
}
|
|
336127
|
+
var RunnerUpgradeRequiredError = class extends Error {
|
|
336128
|
+
latestVersion;
|
|
336129
|
+
constructor(message, latestVersion) {
|
|
336130
|
+
super(message);
|
|
336131
|
+
this.name = "RunnerUpgradeRequiredError";
|
|
336132
|
+
this.latestVersion = latestVersion;
|
|
336133
|
+
}
|
|
336134
|
+
};
|
|
335819
336135
|
async function fetchPendingRuns(serverUrl, headers) {
|
|
335820
336136
|
const res = await fetch(`${serverUrl}/api/runner/pending`, { headers, signal: AbortSignal.timeout(3e4) });
|
|
335821
336137
|
if (!res.ok) {
|
|
335822
336138
|
if (res.status === 401) throw new Error("Runner token is invalid or expired. Run: validate.qa-runner login");
|
|
336139
|
+
if (res.status === 426) {
|
|
336140
|
+
const body = await res.json().catch(() => null);
|
|
336141
|
+
const latest = typeof body?.latestRunnerVersion === "string" ? body.latestRunnerVersion : null;
|
|
336142
|
+
const msg = typeof body?.error === "string" ? body.error : "A newer @validate.qa/runner is required. Run: npm install -g @validate.qa/runner@latest";
|
|
336143
|
+
throw new RunnerUpgradeRequiredError(msg, latest);
|
|
336144
|
+
}
|
|
335823
336145
|
if (res.status === 429) {
|
|
335824
336146
|
const retryAfter = res.headers.get("Retry-After");
|
|
335825
336147
|
const waitSecs = retryAfter ? parseInt(retryAfter, 10) : 60;
|
|
@@ -338332,10 +338654,34 @@ async function startRunner(opts) {
|
|
|
338332
338654
|
}
|
|
338333
338655
|
console.log("");
|
|
338334
338656
|
log.ok(`Runner started - waiting for test runs...${enableMobile ? " (web + mobile)" : ""}`);
|
|
338657
|
+
log.info(`Version: ${RUNNER_VERSION}`);
|
|
338335
338658
|
log.dim("Press Ctrl+C to stop");
|
|
338336
338659
|
console.log("");
|
|
338337
338660
|
let runsCompleted = 0;
|
|
338338
338661
|
let activeRuns = 0;
|
|
338662
|
+
const VERSION_RECHECK_MS = Math.max(
|
|
338663
|
+
6e4,
|
|
338664
|
+
envInt("VALIDATE_RUNNER_VERSION_CHECK_MS", 15 * 6e4)
|
|
338665
|
+
);
|
|
338666
|
+
const exitForUpgrade = (latestVersion, installed) => {
|
|
338667
|
+
console.error(formatUpgradeInstructions(latestVersion));
|
|
338668
|
+
console.error(` Installed: ${installed}`);
|
|
338669
|
+
console.error(` Required: ${latestVersion} (or newer)`);
|
|
338670
|
+
console.error("");
|
|
338671
|
+
process.exit(1);
|
|
338672
|
+
};
|
|
338673
|
+
const recheckRunnerVersion = async () => {
|
|
338674
|
+
if (activeRuns > 0) return;
|
|
338675
|
+
const result = await checkRunnerVersion(RUNNER_VERSION);
|
|
338676
|
+
if (result.outdated && result.latestVersion) {
|
|
338677
|
+
log.warn("A newer runner was published on npm \u2014 shutting down so you can upgrade.");
|
|
338678
|
+
exitForUpgrade(result.latestVersion, RUNNER_VERSION);
|
|
338679
|
+
}
|
|
338680
|
+
};
|
|
338681
|
+
const versionRecheckTimer = setInterval(() => {
|
|
338682
|
+
void recheckRunnerVersion();
|
|
338683
|
+
}, VERSION_RECHECK_MS);
|
|
338684
|
+
versionRecheckTimer.unref?.();
|
|
338339
338685
|
const MAX_CONCURRENT = Math.max(1, envInt("MAX_CONCURRENT", DEFAULT_MAX_CONCURRENT));
|
|
338340
338686
|
const MAX_PROJECT_CONCURRENT = Math.max(1, envInt("MAX_PROJECT_CONCURRENT", DEFAULT_MAX_PROJECT_CONCURRENT));
|
|
338341
338687
|
const TYPE_LIMITS = {
|
|
@@ -338561,6 +338907,22 @@ async function startRunner(opts) {
|
|
|
338561
338907
|
if (body && Array.isArray(body.commands) && body.commands.length > 0) {
|
|
338562
338908
|
void handleControlCommands(body.commands);
|
|
338563
338909
|
}
|
|
338910
|
+
if (body && body.forceUpgrade === true && typeof body.latestRunnerVersion === "string" && isVersionOutdated(RUNNER_VERSION, body.latestRunnerVersion) && activeRuns === 0) {
|
|
338911
|
+
log.warn(
|
|
338912
|
+
typeof body.upgradeMessage === "string" && body.upgradeMessage ? body.upgradeMessage : "Server requires a newer runner version \u2014 shutting down."
|
|
338913
|
+
);
|
|
338914
|
+
exitForUpgrade(body.latestRunnerVersion, RUNNER_VERSION);
|
|
338915
|
+
}
|
|
338916
|
+
} else if (response.status === 426) {
|
|
338917
|
+
const body = await response.json().catch(() => null);
|
|
338918
|
+
const latest = typeof body?.latestRunnerVersion === "string" ? body.latestRunnerVersion : null;
|
|
338919
|
+
if (latest) {
|
|
338920
|
+
exitForUpgrade(latest, RUNNER_VERSION);
|
|
338921
|
+
}
|
|
338922
|
+
log.warn(
|
|
338923
|
+
typeof body?.error === "string" ? body.error : "Server rejected this runner version (HTTP 426). Upgrade @validate.qa/runner."
|
|
338924
|
+
);
|
|
338925
|
+
process.exit(1);
|
|
338564
338926
|
}
|
|
338565
338927
|
} catch {
|
|
338566
338928
|
}
|
|
@@ -338801,6 +339163,10 @@ async function startRunner(opts) {
|
|
|
338801
339163
|
consecutivePollErrors = 0;
|
|
338802
339164
|
}
|
|
338803
339165
|
} catch (err) {
|
|
339166
|
+
if (err instanceof RunnerUpgradeRequiredError) {
|
|
339167
|
+
log.warn(err.message);
|
|
339168
|
+
exitForUpgrade(err.latestVersion ?? "latest", RUNNER_VERSION);
|
|
339169
|
+
}
|
|
338804
339170
|
const message = err instanceof Error ? err.message : String(err);
|
|
338805
339171
|
consecutivePollErrors++;
|
|
338806
339172
|
const backoffMs = Math.min(pollIntervalMs * Math.pow(2, consecutivePollErrors - 1), MAX_BACKOFF_MS);
|
|
@@ -338820,7 +339186,11 @@ async function startRunner(opts) {
|
|
|
338820
339186
|
var pkg = require_package4();
|
|
338821
339187
|
var program = new import_commander.Command();
|
|
338822
339188
|
program.name("validate-runner").description("validate.qa local test runner - execute Playwright and Appium tests on your machine").version(pkg.version);
|
|
339189
|
+
async function requireLatestRunner() {
|
|
339190
|
+
await enforceRunnerUpToDate(pkg.version);
|
|
339191
|
+
}
|
|
338823
339192
|
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) => {
|
|
339193
|
+
await requireLatestRunner();
|
|
338824
339194
|
const serverUrl = opts.server.replace(/\/+$/, "");
|
|
338825
339195
|
if (!opts.token.startsWith("urt_") && !opts.token.startsWith("rt_") && !opts.token.startsWith("eyJ")) {
|
|
338826
339196
|
console.error("\u26A0 Token should start with urt_ (from the dashboard Runner Token)");
|
|
@@ -338852,6 +339222,7 @@ program.command("login").description("Save runner credentials for future use").r
|
|
|
338852
339222
|
console.log("Run: validate-runner start");
|
|
338853
339223
|
});
|
|
338854
339224
|
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) => {
|
|
339225
|
+
await requireLatestRunner();
|
|
338855
339226
|
const saved = loadCredentials();
|
|
338856
339227
|
const authToken = opts.token || process.env.AUTH_TOKEN || saved?.token;
|
|
338857
339228
|
const serverUrl = (opts.server || process.env.SERVER_URL || saved?.serverUrl)?.replace(/\/+$/, "");
|
|
@@ -338904,6 +339275,7 @@ program.command("logout").description("Clear saved runner credentials").action((
|
|
|
338904
339275
|
console.log("\u2713 Credentials cleared");
|
|
338905
339276
|
});
|
|
338906
339277
|
program.command("status").description("Check connection to validate.qa server").action(async () => {
|
|
339278
|
+
await requireLatestRunner();
|
|
338907
339279
|
const saved = loadCredentials();
|
|
338908
339280
|
if (!saved) {
|
|
338909
339281
|
console.log("No saved credentials. Run: validate-runner login");
|