@validate.qa/runner 1.0.43 → 1.0.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +445 -54
- package/dist/cli.mjs +445 -54
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -4784,6 +4784,204 @@ var init_origin_domain = __esm({
|
|
|
4784
4784
|
}
|
|
4785
4785
|
});
|
|
4786
4786
|
|
|
4787
|
+
// ../shared/dist/auth-plan.js
|
|
4788
|
+
function hasInlineLoginSteps(playwrightCode) {
|
|
4789
|
+
if (!playwrightCode)
|
|
4790
|
+
return false;
|
|
4791
|
+
return /process\.env\.TEST_USER_EMAIL!?/.test(playwrightCode) && /process\.env\.TEST_USER_PASSWORD!?/.test(playwrightCode);
|
|
4792
|
+
}
|
|
4793
|
+
function hasGeneratedAuthGuard(playwrightCode) {
|
|
4794
|
+
if (!playwrightCode)
|
|
4795
|
+
return false;
|
|
4796
|
+
return /\/\/ >>> VALIDATE_QA_AUTH v\d+\b[\s\S]*?process\.env\.AUTH_FILE[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH v\d+\b/.test(playwrightCode);
|
|
4797
|
+
}
|
|
4798
|
+
function legacyNeedsCookieState(tags, code, sharedAuthInjectEnabled2) {
|
|
4799
|
+
if (tags.includes("@auth"))
|
|
4800
|
+
return false;
|
|
4801
|
+
if (tags.includes("@api"))
|
|
4802
|
+
return true;
|
|
4803
|
+
if (!tags.includes("@needs-auth"))
|
|
4804
|
+
return false;
|
|
4805
|
+
if (sharedAuthInjectEnabled2 && tags.includes("@auth-guarded") && hasGeneratedAuthGuard(code))
|
|
4806
|
+
return true;
|
|
4807
|
+
const isGeneratedVariant = tags.includes("@edge") || tags.includes("@bug");
|
|
4808
|
+
const hasLegacyInlineLogin = tags.includes("@discovered") && !isGeneratedVariant && hasInlineLoginSteps(code);
|
|
4809
|
+
return !tags.includes("@inline-login") && !hasLegacyInlineLogin;
|
|
4810
|
+
}
|
|
4811
|
+
function explicitAuthTypeTags(t) {
|
|
4812
|
+
const found = /* @__PURE__ */ new Set();
|
|
4813
|
+
for (const tag of t) {
|
|
4814
|
+
if (AUTH_TYPE_TAGS.includes(tag))
|
|
4815
|
+
found.add(tag);
|
|
4816
|
+
else if (AUTH_TAG_ALIASES[tag])
|
|
4817
|
+
found.add(AUTH_TAG_ALIASES[tag]);
|
|
4818
|
+
}
|
|
4819
|
+
return [...found];
|
|
4820
|
+
}
|
|
4821
|
+
function invalidPlan(configurationError) {
|
|
4822
|
+
return {
|
|
4823
|
+
version: 1,
|
|
4824
|
+
source: "invalid",
|
|
4825
|
+
acquisition: "unsupported",
|
|
4826
|
+
presentation: "none",
|
|
4827
|
+
needsCookieState: false,
|
|
4828
|
+
needsStandingHeaders: false,
|
|
4829
|
+
standingHeadersMode: "none",
|
|
4830
|
+
needsLoginCreds: false,
|
|
4831
|
+
requiredEnvKeys: [],
|
|
4832
|
+
configurationError
|
|
4833
|
+
};
|
|
4834
|
+
}
|
|
4835
|
+
function explicitPlan(authTypeTag2, acquisition, presentation, opts) {
|
|
4836
|
+
return {
|
|
4837
|
+
version: 1,
|
|
4838
|
+
source: "explicit",
|
|
4839
|
+
authTypeTag: authTypeTag2,
|
|
4840
|
+
acquisition,
|
|
4841
|
+
presentation,
|
|
4842
|
+
needsCookieState: opts.cookie,
|
|
4843
|
+
needsStandingHeaders: opts.standingMode === "required",
|
|
4844
|
+
standingHeadersMode: opts.standingMode,
|
|
4845
|
+
needsLoginCreds: TEST_USER_ENV_KEYS.every((k) => opts.requiredEnvKeys.includes(k)),
|
|
4846
|
+
requiredEnvKeys: opts.requiredEnvKeys
|
|
4847
|
+
};
|
|
4848
|
+
}
|
|
4849
|
+
function deriveAuthPlan(tags, code, opts) {
|
|
4850
|
+
const t = Array.isArray(tags) ? tags : [];
|
|
4851
|
+
const sharedFlag = opts?.sharedAuthInjectEnabled ?? false;
|
|
4852
|
+
const isApi = t.includes("@api");
|
|
4853
|
+
const explicit = isApi ? explicitAuthTypeTags(t) : [];
|
|
4854
|
+
if (explicit.length > 0) {
|
|
4855
|
+
if (t.includes("@auth")) {
|
|
4856
|
+
return invalidPlan("@auth (self-login UI) cannot be combined with an explicit @api auth-type tag");
|
|
4857
|
+
}
|
|
4858
|
+
if (explicit.length > 1) {
|
|
4859
|
+
return invalidPlan(`conflicting auth-type tags: ${explicit.join(", ")}`);
|
|
4860
|
+
}
|
|
4861
|
+
switch (explicit[0]) {
|
|
4862
|
+
case AUTH_TAG_UNSUPPORTED:
|
|
4863
|
+
return {
|
|
4864
|
+
...invalidPlan("auth scheme not yet drivable (e.g. HMAC / mTLS / provider-replay) \u2014 needs manual configuration")
|
|
4865
|
+
};
|
|
4866
|
+
case AUTH_TAG_NONE:
|
|
4867
|
+
return explicitPlan(AUTH_TAG_NONE, "none", "none", { cookie: false, standingMode: "none", requiredEnvKeys: [] });
|
|
4868
|
+
case AUTH_TAG_STANDING:
|
|
4869
|
+
return explicitPlan(AUTH_TAG_STANDING, "standing-secret", "standing-header", { cookie: false, standingMode: "required", requiredEnvKeys: [API_AUTH_HEADERS_ENV_KEY] });
|
|
4870
|
+
case AUTH_TAG_LOGIN_BEARER:
|
|
4871
|
+
return explicitPlan(AUTH_TAG_LOGIN_BEARER, "request-login", "bearer", { cookie: false, standingMode: "none", requiredEnvKeys: [...TEST_USER_ENV_KEYS] });
|
|
4872
|
+
case AUTH_TAG_LOGIN_COOKIE:
|
|
4873
|
+
return explicitPlan(AUTH_TAG_LOGIN_COOKIE, "request-login", "cookie", { cookie: false, standingMode: "legacy-if-configured", requiredEnvKeys: [...TEST_USER_ENV_KEYS] });
|
|
4874
|
+
case AUTH_TAG_COOKIE_STATE:
|
|
4875
|
+
return explicitPlan(AUTH_TAG_COOKIE_STATE, "browser-bootstrap", "cookie", { cookie: true, standingMode: "legacy-if-configured", requiredEnvKeys: [...TEST_USER_ENV_KEYS] });
|
|
4876
|
+
}
|
|
4877
|
+
}
|
|
4878
|
+
const cookie = legacyNeedsCookieState(t, code, sharedFlag);
|
|
4879
|
+
const authedLegacy = t.includes("@auth") || t.includes("@needs-auth") || isApi;
|
|
4880
|
+
return {
|
|
4881
|
+
version: 1,
|
|
4882
|
+
source: "legacy",
|
|
4883
|
+
acquisition: cookie ? "browser-bootstrap" : authedLegacy ? "request-login" : "none",
|
|
4884
|
+
presentation: authedLegacy ? "cookie" : "none",
|
|
4885
|
+
needsCookieState: cookie,
|
|
4886
|
+
needsStandingHeaders: false,
|
|
4887
|
+
// A legacy bare-@api test keeps injecting standing headers IF the project configured them
|
|
4888
|
+
// (preserving today's behavior) — but does not require them. Non-@api legacy tests never inject.
|
|
4889
|
+
standingHeadersMode: isApi ? "legacy-if-configured" : "none",
|
|
4890
|
+
needsLoginCreds: authedLegacy,
|
|
4891
|
+
requiredEnvKeys: authedLegacy ? [...TEST_USER_ENV_KEYS] : []
|
|
4892
|
+
};
|
|
4893
|
+
}
|
|
4894
|
+
function authTypeTag(tags) {
|
|
4895
|
+
const t = Array.isArray(tags) ? tags : [];
|
|
4896
|
+
const explicit = explicitAuthTypeTags(t);
|
|
4897
|
+
return explicit.length === 1 ? explicit[0] : void 0;
|
|
4898
|
+
}
|
|
4899
|
+
var AUTH_TAG_COOKIE_STATE, AUTH_TAG_LOGIN_COOKIE, AUTH_TAG_LOGIN_BEARER, AUTH_TAG_STANDING, AUTH_TAG_NONE, AUTH_TAG_UNSUPPORTED, AUTH_TYPE_TAGS, AUTH_TAG_ALIASES, TEST_USER_ENV_KEYS, API_AUTH_HEADERS_ENV_KEY;
|
|
4900
|
+
var init_auth_plan = __esm({
|
|
4901
|
+
"../shared/dist/auth-plan.js"() {
|
|
4902
|
+
"use strict";
|
|
4903
|
+
AUTH_TAG_COOKIE_STATE = "@auth-cookie-state";
|
|
4904
|
+
AUTH_TAG_LOGIN_COOKIE = "@auth-login-cookie";
|
|
4905
|
+
AUTH_TAG_LOGIN_BEARER = "@auth-login-bearer";
|
|
4906
|
+
AUTH_TAG_STANDING = "@auth-standing";
|
|
4907
|
+
AUTH_TAG_NONE = "@auth-none";
|
|
4908
|
+
AUTH_TAG_UNSUPPORTED = "@auth-unsupported";
|
|
4909
|
+
AUTH_TYPE_TAGS = [
|
|
4910
|
+
AUTH_TAG_COOKIE_STATE,
|
|
4911
|
+
AUTH_TAG_LOGIN_COOKIE,
|
|
4912
|
+
AUTH_TAG_LOGIN_BEARER,
|
|
4913
|
+
AUTH_TAG_STANDING,
|
|
4914
|
+
AUTH_TAG_NONE,
|
|
4915
|
+
AUTH_TAG_UNSUPPORTED
|
|
4916
|
+
];
|
|
4917
|
+
AUTH_TAG_ALIASES = {
|
|
4918
|
+
"@auth-cookie": AUTH_TAG_COOKIE_STATE,
|
|
4919
|
+
"@auth-bearer": AUTH_TAG_LOGIN_BEARER,
|
|
4920
|
+
"@auth-token": AUTH_TAG_LOGIN_BEARER,
|
|
4921
|
+
"@auth-api-key": AUTH_TAG_STANDING,
|
|
4922
|
+
"@auth-standing-header": AUTH_TAG_STANDING
|
|
4923
|
+
};
|
|
4924
|
+
TEST_USER_ENV_KEYS = ["TEST_USER_EMAIL", "TEST_USER_PASSWORD"];
|
|
4925
|
+
API_AUTH_HEADERS_ENV_KEY = "API_AUTH_HEADERS";
|
|
4926
|
+
}
|
|
4927
|
+
});
|
|
4928
|
+
|
|
4929
|
+
// ../shared/dist/redact-needles.js
|
|
4930
|
+
function escapeRegExp(s) {
|
|
4931
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4932
|
+
}
|
|
4933
|
+
function buildNeedleMatcher(needles) {
|
|
4934
|
+
const usable = Array.from(new Set(needles.filter((n) => typeof n === "string" && n.length >= MIN_NEEDLE_LENGTH))).sort((a, b) => b.length - a.length);
|
|
4935
|
+
if (usable.length === 0)
|
|
4936
|
+
return null;
|
|
4937
|
+
return new RegExp(usable.map(escapeRegExp).join("|"), "g");
|
|
4938
|
+
}
|
|
4939
|
+
function redactNeedlesText(text, matcher) {
|
|
4940
|
+
if (!matcher || !text)
|
|
4941
|
+
return text;
|
|
4942
|
+
matcher.lastIndex = 0;
|
|
4943
|
+
const out = text.replace(matcher, REDACTED);
|
|
4944
|
+
return out === text ? text : out;
|
|
4945
|
+
}
|
|
4946
|
+
function redactNeedlesDeep(value, matcher) {
|
|
4947
|
+
if (!matcher)
|
|
4948
|
+
return value;
|
|
4949
|
+
if (typeof value === "string") {
|
|
4950
|
+
const scrubbed = redactNeedlesText(value, matcher);
|
|
4951
|
+
return scrubbed === value ? value : scrubbed;
|
|
4952
|
+
}
|
|
4953
|
+
if (Array.isArray(value)) {
|
|
4954
|
+
let changed = false;
|
|
4955
|
+
const next = value.map((item) => {
|
|
4956
|
+
const scrubbed = redactNeedlesDeep(item, matcher);
|
|
4957
|
+
if (scrubbed !== item)
|
|
4958
|
+
changed = true;
|
|
4959
|
+
return scrubbed;
|
|
4960
|
+
});
|
|
4961
|
+
return changed ? next : value;
|
|
4962
|
+
}
|
|
4963
|
+
if (value !== null && typeof value === "object") {
|
|
4964
|
+
let changed = false;
|
|
4965
|
+
const next = {};
|
|
4966
|
+
for (const [k, v] of Object.entries(value)) {
|
|
4967
|
+
const scrubbed = redactNeedlesDeep(v, matcher);
|
|
4968
|
+
if (scrubbed !== v)
|
|
4969
|
+
changed = true;
|
|
4970
|
+
next[k] = scrubbed;
|
|
4971
|
+
}
|
|
4972
|
+
return changed ? next : value;
|
|
4973
|
+
}
|
|
4974
|
+
return value;
|
|
4975
|
+
}
|
|
4976
|
+
var REDACTED, MIN_NEEDLE_LENGTH;
|
|
4977
|
+
var init_redact_needles = __esm({
|
|
4978
|
+
"../shared/dist/redact-needles.js"() {
|
|
4979
|
+
"use strict";
|
|
4980
|
+
REDACTED = "[REDACTED]";
|
|
4981
|
+
MIN_NEEDLE_LENGTH = 8;
|
|
4982
|
+
}
|
|
4983
|
+
});
|
|
4984
|
+
|
|
4787
4985
|
// ../shared/dist/voice-config.js
|
|
4788
4986
|
function normalizeTtsVoiceId(voiceId) {
|
|
4789
4987
|
if (!voiceId)
|
|
@@ -5058,8 +5256,16 @@ __export(dist_exports, {
|
|
|
5058
5256
|
ADDON_DEFINITIONS: () => ADDON_DEFINITIONS,
|
|
5059
5257
|
ADMIN_EMAIL_LIMITS: () => ADMIN_EMAIL_LIMITS,
|
|
5060
5258
|
AI_CREDIT_MARGIN: () => AI_CREDIT_MARGIN,
|
|
5259
|
+
API_AUTH_HEADERS_ENV_KEY: () => API_AUTH_HEADERS_ENV_KEY,
|
|
5061
5260
|
API_PREFIX: () => API_PREFIX,
|
|
5062
5261
|
AUDIO_CHUNK_DURATION_MS: () => AUDIO_CHUNK_DURATION_MS,
|
|
5262
|
+
AUTH_TAG_COOKIE_STATE: () => AUTH_TAG_COOKIE_STATE,
|
|
5263
|
+
AUTH_TAG_LOGIN_BEARER: () => AUTH_TAG_LOGIN_BEARER,
|
|
5264
|
+
AUTH_TAG_LOGIN_COOKIE: () => AUTH_TAG_LOGIN_COOKIE,
|
|
5265
|
+
AUTH_TAG_NONE: () => AUTH_TAG_NONE,
|
|
5266
|
+
AUTH_TAG_STANDING: () => AUTH_TAG_STANDING,
|
|
5267
|
+
AUTH_TAG_UNSUPPORTED: () => AUTH_TAG_UNSUPPORTED,
|
|
5268
|
+
AUTH_TYPE_TAGS: () => AUTH_TYPE_TAGS,
|
|
5063
5269
|
COST_PER_M: () => COST_PER_M,
|
|
5064
5270
|
CREDIT_PACKS_CENTS: () => CREDIT_PACKS_CENTS,
|
|
5065
5271
|
CREDIT_PRICES_CENTS: () => CREDIT_PRICES_CENTS,
|
|
@@ -5102,6 +5308,7 @@ __export(dist_exports, {
|
|
|
5102
5308
|
STT_MODEL_OPTIONS: () => STT_MODEL_OPTIONS,
|
|
5103
5309
|
STT_PROVIDER_OPTIONS: () => STT_PROVIDER_OPTIONS,
|
|
5104
5310
|
SUPPORT_EMAIL: () => SUPPORT_EMAIL,
|
|
5311
|
+
TEST_USER_ENV_KEYS: () => TEST_USER_ENV_KEYS,
|
|
5105
5312
|
TRANSIENT_CONTROL_ROLES: () => TRANSIENT_CONTROL_ROLES,
|
|
5106
5313
|
TRIAL_DURATION_DAYS: () => TRIAL_DURATION_DAYS,
|
|
5107
5314
|
TTS_MODEL_OPTIONS: () => TTS_MODEL_OPTIONS,
|
|
@@ -5111,10 +5318,12 @@ __export(dist_exports, {
|
|
|
5111
5318
|
accumulateStatements: () => accumulateStatements,
|
|
5112
5319
|
aiCostToCreditCents: () => aiCostToCreditCents,
|
|
5113
5320
|
analyzeMobileSelectorQuality: () => analyzeMobileSelectorQuality,
|
|
5321
|
+
authTypeTag: () => authTypeTag,
|
|
5114
5322
|
bugFoundQuarantineWhere: () => bugFoundQuarantineWhere,
|
|
5115
5323
|
buildAppiumCode: () => buildAppiumCode,
|
|
5116
5324
|
buildLaunchStep: () => buildLaunchStep,
|
|
5117
5325
|
buildMobileStep: () => buildMobileStep,
|
|
5326
|
+
buildNeedleMatcher: () => buildNeedleMatcher,
|
|
5118
5327
|
buildSandboxedEnv: () => buildSandboxedEnv,
|
|
5119
5328
|
buildSanitizedAgentEnv: () => buildSanitizedAgentEnv,
|
|
5120
5329
|
buildStepTarget: () => buildStepTarget,
|
|
@@ -5130,6 +5339,7 @@ __export(dist_exports, {
|
|
|
5130
5339
|
controlSignature: () => controlSignature,
|
|
5131
5340
|
countInnerTests: () => countInnerTests,
|
|
5132
5341
|
decideHarPosted: () => decideHarPosted,
|
|
5342
|
+
deriveAuthPlan: () => deriveAuthPlan,
|
|
5133
5343
|
deriveProjectKey: () => deriveProjectKey,
|
|
5134
5344
|
detectCrossOriginHtmlAsJson: () => detectCrossOriginHtmlAsJson,
|
|
5135
5345
|
detectsErrorBannerAssertion: () => detectsErrorBannerAssertion,
|
|
@@ -5197,6 +5407,8 @@ __export(dist_exports, {
|
|
|
5197
5407
|
planHasBillingPortal: () => planHasBillingPortal,
|
|
5198
5408
|
planRequiresCheckout: () => planRequiresCheckout,
|
|
5199
5409
|
platformTag: () => platformTag,
|
|
5410
|
+
redactNeedlesDeep: () => redactNeedlesDeep,
|
|
5411
|
+
redactNeedlesText: () => redactNeedlesText,
|
|
5200
5412
|
registrableDomain: () => registrableDomain,
|
|
5201
5413
|
resolveCrossOriginConfigOverride: () => resolveCrossOriginConfigOverride,
|
|
5202
5414
|
resolveMobileStepValuePlaceholders: () => resolveMobileStepValuePlaceholders,
|
|
@@ -5239,6 +5451,8 @@ var init_dist = __esm({
|
|
|
5239
5451
|
init_agent_env();
|
|
5240
5452
|
init_test_counting();
|
|
5241
5453
|
init_origin_domain();
|
|
5454
|
+
init_auth_plan();
|
|
5455
|
+
init_redact_needles();
|
|
5242
5456
|
init_voice_config();
|
|
5243
5457
|
init_feature_flags();
|
|
5244
5458
|
}
|
|
@@ -52406,11 +52620,11 @@ var require_leac = __commonJS({
|
|
|
52406
52620
|
if (r.str.length === 0) {
|
|
52407
52621
|
throw new Error(`Rule #${i} ("${r.name}") has empty "str" property, which is not allowed.`);
|
|
52408
52622
|
}
|
|
52409
|
-
return new RegExp(
|
|
52623
|
+
return new RegExp(escapeRegExp2(r.str), "y");
|
|
52410
52624
|
}
|
|
52411
|
-
return new RegExp(
|
|
52625
|
+
return new RegExp(escapeRegExp2(r.name), "y");
|
|
52412
52626
|
}
|
|
52413
|
-
function
|
|
52627
|
+
function escapeRegExp2(str) {
|
|
52414
52628
|
return str.replace(/[-[\]{}()*+!<=:?./\\^$|#\s,]/g, "\\$&");
|
|
52415
52629
|
}
|
|
52416
52630
|
function toSticky(re) {
|
|
@@ -68383,7 +68597,7 @@ var require_discover_ai_planner = __commonJS({
|
|
|
68383
68597
|
"../runner-core/dist/services/discover-ai-planner.js"(exports) {
|
|
68384
68598
|
"use strict";
|
|
68385
68599
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
68386
|
-
exports.SCENARIO_SYSTEM_PROMPT = void 0;
|
|
68600
|
+
exports.PLANNER_STRENGTHEN_GATE_PROMPT = exports.SCENARIO_SYSTEM_PROMPT = void 0;
|
|
68387
68601
|
exports.getAIPlannerMaxTokens = getAIPlannerMaxTokens;
|
|
68388
68602
|
exports.getSnapshotConfidence = getSnapshotConfidence;
|
|
68389
68603
|
exports.compressSiteMap = compressSiteMap;
|
|
@@ -68966,7 +69180,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
68966
69180
|
4. **Multi-page journeys** - only when transitions were actually observed
|
|
68967
69181
|
5. **Bug reports** - for observed 500 errors, crashes, or broken features
|
|
68968
69182
|
6. **Steps are human-readable** - "Fill the email field", not "page.getByLabel('Email').fill()"
|
|
68969
|
-
7. **Expected outcomes are plain-English success criteria** - describe the user-visible product result, not the controls touched along the way. Steps already cover buttons, inputs, links, fields, headings, tabs, dialogs, and menus. Expected outcomes should say what the user can now do or rely on (record saved, message sent, search results filtered, validation explained, dashboard accessible). Include durable routes or exact copy only when they are the real product outcome; do NOT list "heading visible", "button visible", "search input visible", or similar element inventory unless that exact control is the behavior under test. **Outcome-strength
|
|
69183
|
+
7. **Expected outcomes are plain-English success criteria** - describe the user-visible product result, not the controls touched along the way. Steps already cover buttons, inputs, links, fields, headings, tabs, dialogs, and menus. Expected outcomes should say what the user can now do or rely on (record saved, message sent, search results filtered, validation explained, dashboard accessible). Include durable routes or exact copy only when they are the real product outcome; do NOT list "heading visible", "button visible", "search input visible", or similar element inventory unless that exact control is the behavior under test. **Outcome-strength check (per outcome, applies to EVERY scenario type):** evaluate each expectedOutcome separately. Delete it if it would already be true immediately before the scenario's primary action \u2014 UNLESS navigation, redirect, or revealing that state is itself the behavior under test. Keep at least one evidence-backed discriminator that proves the primary behavior actually occurred: specific result content or value, a created or removed entity, a changed count or status, persisted state after reload, or handled validation copy. Every value, count-change, persistence, creation, or removal assertion MUST trace to an earlier evidence-backed step that establishes the baseline or exact target. If the evidence does not support such a step, drop the assertion; do not invent setup. **Silent-failure BUG_REPORT exception:** apply ONLY when evidence explicitly records an action as broken because its intended state change did not occur. It does NOT apply to expected validation, permission denial, unmet prerequisites, loading or rate-limit states, or any non-BUG scenario. Prove the unchanged state with an evidence-backed before/after discriminator tied to the attempted run-scoped target \u2014 not merely "no toast", "no redirect", or "the form is still visible". Never invent copy to satisfy this check \u2014 Rule 1 still governs what you may assert.
|
|
68970
69184
|
8. **Auth-aware** - mark scenarios that need login. Use process.env.TEST_USER_EMAIL / PASSWORD
|
|
68971
69185
|
9. **No duplicates** - check existing test names before adding
|
|
68972
69186
|
10. **All tests start from the landing page** - every generated test navigates to \`/\` first and uses the app's own navigation (clicking links/buttons) to reach the target page. Steps must reflect this: say "Navigate to landing page and click Sign In" not "Navigate to /auth/login". Deep-linking to internal routes causes hydration failures in SPAs. \`targetPages\` lists the pages involved in the flow.
|
|
@@ -68979,7 +69193,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
68979
69193
|
Example - BAD happy path: \`["Navigate to /feature", "Click Create", "Verify dialog opens"]\` \u2192 only tests the trigger.
|
|
68980
69194
|
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"]\`
|
|
68981
69195
|
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"]\`
|
|
68982
|
-
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
|
|
69196
|
+
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 concrete signed-in control, do NOT invent one or instruct the executor to "open the account/user menu" \u2014 assert only the exact observed redirect (plus any directly supported transition, e.g. the login form is no longer shown), treat the redirect and any observed transition as transition outcomes \u2014 not concrete session proof \u2014 assign each its evidence-supported reliability, and name the missing concrete signal as a coverage gap in reasoning. Never substitute a menu of generic alternatives ("such as Logout, Profile, or \u2026") for one evidenced signal in a step or expected outcome.
|
|
68983
69197
|
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.
|
|
68984
69198
|
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>."
|
|
68985
69199
|
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.
|
|
@@ -69042,6 +69256,9 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
69042
69256
|
Citing observed labels in \`expectedOutcomes\` is fine \u2014 that's asserting on stable product signals, not implementation specifics.
|
|
69043
69257
|
|
|
69044
69258
|
## Coverage Strategy
|
|
69259
|
+
**Coverage allocation \u2014 order matters.** The test budget is a ceiling, never a target. Before writing JSON, privately inventory each distinct behavior the evidence explicitly RECORDS: a SUCCESS, EMPTY_STATE, VALIDATION_ERROR, ERROR, BUG, an Explorer-Recorded Journey outcome, or an observed page/state transition. Assign each recorded behavior to exactly one scenario as its primary coverage owner \u2014 this does not prohibit repeating evidence-backed prerequisite setup and verification required by Rules 13\u201317. Bundle input variants that exercise the same purpose (e.g. two malformed-parameter cases that both return the same validation) into that one scenario. Use Page Intelligence only to make sure every described surface is reached \u2014 a merely listed control, route, provider, tab, or input does NOT by itself prove a behavior or earn its own scenario. Do NOT add an inferred scenario just to fill the budget. If a recorded behavior cannot be tested independently without unsupported mutable state, name it as a coverage gap (rule 17) rather than pretending it is self-contained. Treat a page/state transition as a distinct behavior only when navigation or revealing that state is itself the recorded product outcome; otherwise keep it as a step within the owning scenario \u2014 Page Intelligence creates no additional coverage obligation.
|
|
69260
|
+
|
|
69261
|
+
Apply the per-surface minimums below only where the evidence actually supports them (evidence support includes Rule 1's trigger-only structural exception):
|
|
69045
69262
|
- For every form: at least 1 HAPPY_PATH (submit valid data AND verify durable result per rule 11) + 1 EDGE_CASE (empty/invalid submission)
|
|
69046
69263
|
- For auth flows: login happy path (verify the post-login route AND a concrete signed-in/session signal per rule 12) + validation (wrong password, empty fields)
|
|
69047
69264
|
- For CRUD: each scenario MUST own the entity it operates on. Read/search/update/delete/cancel-on-existing/view-existing scenarios MUST include a create step earlier in the same scenario (or declare an immutable seed fixture by name). Never assume a record from prior runs or sibling tests exists. Splitting into atomic create/read/update/delete tests is fine ONLY when each test creates its own entity. When create/delete are tightly coupled (e.g. delete-only with no create flow elsewhere), bundle into a single USER_JOURNEY: create \u2192 verify \u2192 delete \u2192 verify removal.
|
|
@@ -69071,11 +69288,11 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
69071
69288
|
{ "action": "Fill the email field with test credentials" },
|
|
69072
69289
|
{ "action": "Fill the password field with test credentials" },
|
|
69073
69290
|
{ "action": "Click the Sign In button" },
|
|
69074
|
-
{ "action": "
|
|
69291
|
+
{ "action": "After reaching the observed post-login page, reload it" }
|
|
69075
69292
|
],
|
|
69076
69293
|
"expectedOutcomes": [
|
|
69077
|
-
{ "type": "url", "description": "User reaches /dashboard after signing in", "reliability": "high" },
|
|
69078
|
-
{ "type": "element-
|
|
69294
|
+
{ "type": "url", "description": "User reaches /dashboard after signing in and remains there after reload", "reliability": "high" },
|
|
69295
|
+
{ "type": "element-hidden", "description": "The login form does not return after reload", "reliability": "medium" }
|
|
69079
69296
|
],
|
|
69080
69297
|
"targetPages": ["/auth/login", "/dashboard"],
|
|
69081
69298
|
"prerequisites": []
|
|
@@ -69105,6 +69322,46 @@ Each entry is exactly one of:
|
|
|
69105
69322
|
3 = medium (navigation, secondary features)
|
|
69106
69323
|
4 = low (edge cases, cosmetic)
|
|
69107
69324
|
5 = nice-to-have`;
|
|
69325
|
+
exports.PLANNER_STRENGTHEN_GATE_PROMPT = `You are a senior QA lead performing ONE design-audit pass over a test PLAN another model wrote for a single web feature. The plan will be handed to a LIVE generator that opens the real app, follows each scenario's steps, observes the DOM, and writes the concrete assertions itself. Division of labor you MUST respect:
|
|
69326
|
+
- The PLAN owns the ORACLE: WHAT must be true for the behavior to be correct, stated in plain English.
|
|
69327
|
+
- The GENERATOR owns BINDING: locators and the exact runtime values it reads off the live page.
|
|
69328
|
+
Never write an expectation whose truth is decided by looking at the current app ("whatever the page shows"): that lets a live bug become the expected result. State the intended truth; the generator will bind it and report a mismatch if reality disagrees.
|
|
69329
|
+
|
|
69330
|
+
You are given (1) the EXACT evidence the plan was based on \u2014 the ONLY ground truth; you may use it to know WHAT behaviors exist, but you must NEVER introduce a behavior, route, element, or effect not present in or directly implied by it \u2014 and (2) the PLAN as JSON. Return the complete corrected plan.
|
|
69331
|
+
|
|
69332
|
+
## Rule 0 \u2014 REPAIR OR DROP, never merely describe (highest priority)
|
|
69333
|
+
Do NOT write a reasoning note about a fix you did not actually apply. Every change you claim must be present in the scenarios you output: if you say you strengthened an outcome, the new outcome text must be there; if you say you merged or removed a duplicate, NO surviving scenario may share the same primary action + outcome. If a defect cannot be repaired from the evidence, DROP the scenario or FLAG it \u2014 do not keep a known-weak scenario with an excuse. Flag-and-keep is forbidden.
|
|
69334
|
+
|
|
69335
|
+
Your only legal moves per scenario are: KEEP unchanged \xB7 REWRITE using a grounded fact \xB7 DROP as unsupported/duplicate \xB7 FLAG as needs-more-evidence (low reliability, honest note). You may never FORCE-REWRITE by inventing an effect the evidence does not show (a visible Delete control does not tell you whether delete removes, archives, hides, or disables \u2014 do not assume).
|
|
69336
|
+
|
|
69337
|
+
## Rule 1 \u2014 Classify each scenario, then require the matching oracle
|
|
69338
|
+
Identify the scenario's archetype and hold its expectedOutcomes to the right shape. An outcome is only valid if it would be FALSE under a plausible FAULT for that archetype:
|
|
69339
|
+
|
|
69340
|
+
- CREATE/EDIT/DELETE (mutation): the scenario must control its own target \u2014 create the entity it acts on with a self-chosen unique identifier (create \u2192 act \u2192 verify), or declare a prerequisite that resolves it (a fixture or a stable immutable reference). Never depend on unspecified pre-existing/ambient rows ("delete the first row", "open an existing order"). Oracle = the owned entity's before\u2192after: it appears after create / reflects the edit / is absent after delete (verify by the unique identifier, ideally after reload), while an unrelated sentinel is unaffected. Plausible faults to defeat: no-op, wrong-target, duplicate, non-persistence-after-reload, over-broad delete.
|
|
69341
|
+
- LIST/READ/DETAIL: oracle = the returned content corresponds to what was requested (the opened item's identifier matches the one selected). No before\u2192after is required.
|
|
69342
|
+
- FILTER/SEARCH/SORT: oracle = a delta \u2014 a known in-scope item remains AND a known out-of-scope item disappears (or order changes as specified). "Matching items are shown" alone is a no-op (a broken filter that shows everything passes).
|
|
69343
|
+
- VALIDATION/NEGATIVE: oracle must distinguish "the action was correctly rejected by validation" from "nothing happened / a dead control". Prefer the grounded error signal the evidence recorded (exact message or the specific field marked invalid) plus the absence of the forbidden effect (no entity created). If no error signal was observed, keep it structural at low reliability and FLAG.
|
|
69344
|
+
- NAVIGATION/REDIRECT/AUTH-GUARD: the destination is a legitimate primary effect \u2014 assert the route AND one observed landing signal (heading/content) so a redirect to a broken page fails. Route-presence as an outcome on a NON-navigation scenario is padding \u2014 drop it.
|
|
69345
|
+
- TOGGLE/IDEMPOTENT: assert the changed attribute/state (e.g. pressed/checked/disabled), or for idempotent actions that no duplicate is created \u2014 not mere visibility.
|
|
69346
|
+
- EXTERNAL INTEGRATION: assert the outbound URL/payload contains the expected parameters (drawn from evidence), not a frozen full string.
|
|
69347
|
+
- STATIC CONTENT: presence/label oracle is acceptable; do not inflate it. Verify shared chrome (nav/footer) ONCE, not per page.
|
|
69348
|
+
- APP BUG: exactly one BUG_REPORT per distinct root cause; assert the observed defective effect. Console errors, thrown exceptions, and network status codes are NOT page text \u2014 assert the observable page effect (no toast / no redirect / form unchanged), or set the outcome type to the right probe.
|
|
69349
|
+
|
|
69350
|
+
## Rule 2 \u2014 Exact values: judge by ORIGIN, not specificity
|
|
69351
|
+
- DROP as brittle: mutable ambient snapshots the test does not control \u2014 current counts ("8 cards"), seeded record names, other users' data, opaque IDs/slugs, phone numbers. Phrase these relationally ("the account's active business name", "the created item", "\u22651 result").
|
|
69352
|
+
- KEEP exact: values the SCENARIO itself introduced (a unique title it typed \u2014 assert it propagates), app-authored static strings (validation messages, headings, template copy, fixed error codes), and stable contract/derived facts (a contractual price, total = qty \xD7 unit price). These are real oracles, not brittleness.
|
|
69353
|
+
|
|
69354
|
+
## Rule 3 \u2014 Do not build on flows the evidence shows are broken or unrepeatable
|
|
69355
|
+
If evidence flags a route/flow as broken, a precondition as unobtainable (e.g. a single-use token already consumed, an account locked out), or a mutation as irreversible/one-shot on a shared account, you MUST NOT emit happy-path scenarios that depend on that flow succeeding or repeating. Reconcile: drop the dependent happy path, or route it through an alternate path the evidence actually shows.
|
|
69356
|
+
|
|
69357
|
+
## Rule 4 \u2014 Suite economy and completeness
|
|
69358
|
+
Budget is a ceiling, not a target. Merge scenarios that collide on (route + primary action + outcome-class). Do not enumerate one scenario per item across a long list when 1\u20133 representatives suffice. Reclaim freed budget to cover a DISTINCT behavior the evidence recorded that the plan left untested (swap, do not inflate). An action inferred only from a merely-listed control the evidence never showed exercised should be dropped and FLAGGED as a survey gap, not asserted.
|
|
69359
|
+
|
|
69360
|
+
## Rule 5 \u2014 reliability is honest
|
|
69361
|
+
high = the effect and its trigger are both directly observed in evidence; medium = observed but partial/indirect; low = structural or inferred (must be FLAGGED). Never launder an unsupported claim by labelling it low \u2014 if it is invented, drop it.
|
|
69362
|
+
|
|
69363
|
+
## Output
|
|
69364
|
+
Keep the exact JSON schema { "featureGroups":[...], "scenarios":[...], "reasoning":"..." }; each scenario keeps name, type, priority, variant, featureGroup, goal, steps[], expectedOutcomes[{type,description,reliability}], targetPages, prerequisites. In "reasoning", state ONLY changes you actually made (kept/rewritten/dropped/merged/flagged counts). Return ONLY the complete corrected JSON object \u2014 no markdown, no prose outside JSON.`;
|
|
69108
69365
|
function coerceRawPrerequisites(raw) {
|
|
69109
69366
|
if (!Array.isArray(raw))
|
|
69110
69367
|
return [];
|
|
@@ -69888,6 +70145,95 @@ Each entry is exactly one of:
|
|
|
69888
70145
|
}
|
|
69889
70146
|
}
|
|
69890
70147
|
}
|
|
70148
|
+
const gateFlag = (process.env.PLANNER_STRENGTHEN_GATE || "").trim().toLowerCase();
|
|
70149
|
+
const gateEnabled = gateFlag !== "false" && gateFlag !== "0";
|
|
70150
|
+
if (gateEnabled && context.mode !== "survey" && !options.systemPromptOverride && !options.userPromptOverride && !extraPlannerCallUsed && scenarios.length > 0) {
|
|
70151
|
+
const gateModel = options.gateModelOverride ? (0, ai_provider_js_1.resolveModelForTask)(options.gateModelOverride, "agent") : agentModel;
|
|
70152
|
+
const gateClient = options.gateModelOverride ? (0, ai_provider_js_1.getRunnerClientForModel)(options.gateModelOverride) : aiClient;
|
|
70153
|
+
const gateMaxTokens = getAIPlannerMaxTokens(gateModel);
|
|
70154
|
+
const gatePlan = { featureGroups, scenarios, reasoning: aiOutput.reasoning ?? "" };
|
|
70155
|
+
const gateUser = `EVIDENCE (ground truth):
|
|
70156
|
+
${userPrompt}
|
|
70157
|
+
|
|
70158
|
+
=====
|
|
70159
|
+
|
|
70160
|
+
PLAN TO AUDIT AND REPAIR (JSON):
|
|
70161
|
+
${JSON.stringify(gatePlan)}`;
|
|
70162
|
+
const gateStart = Date.now();
|
|
70163
|
+
let gateResponse = null;
|
|
70164
|
+
try {
|
|
70165
|
+
extraPlannerCallUsed = true;
|
|
70166
|
+
gateResponse = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
70167
|
+
arm();
|
|
70168
|
+
return gateClient.chat.completions.create({
|
|
70169
|
+
model: gateModel,
|
|
70170
|
+
messages: [
|
|
70171
|
+
{ role: "system", content: exports.PLANNER_STRENGTHEN_GATE_PROMPT },
|
|
70172
|
+
{ role: "user", content: gateUser }
|
|
70173
|
+
],
|
|
70174
|
+
max_tokens: gateMaxTokens,
|
|
70175
|
+
temperature: 0.2,
|
|
70176
|
+
...(0, ai_provider_js_1.getJsonObjectResponseFormat)(gateModel),
|
|
70177
|
+
...(0, ai_provider_js_1.getNoThinkingRequestOptions)(gateModel)
|
|
70178
|
+
}, { signal });
|
|
70179
|
+
}), {
|
|
70180
|
+
label: "ai-planner-strengthen-gate",
|
|
70181
|
+
deferTimeoutUntilArmed: true,
|
|
70182
|
+
onRetry: (attempt, reason, delay) => logs2.push(`AI planner strengthen-gate call failed (${reason}) \u2014 retry ${attempt} in ${Math.round(delay / 1e3)}s`)
|
|
70183
|
+
});
|
|
70184
|
+
} catch (err) {
|
|
70185
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
70186
|
+
logs2.push(`AI planner strengthen-gate failed (${message}) \u2014 keeping pre-gate result`);
|
|
70187
|
+
}
|
|
70188
|
+
if (gateResponse) {
|
|
70189
|
+
try {
|
|
70190
|
+
options.onAICall?.({
|
|
70191
|
+
iteration: 1,
|
|
70192
|
+
groupId: `ai-planner-${context.projectId.slice(0, 8)}`,
|
|
70193
|
+
callType: "discoveryAIPlannerStrengthenGate",
|
|
70194
|
+
model: gateModel,
|
|
70195
|
+
provider: (0, ai_provider_js_1.getProviderForModel)(gateModel),
|
|
70196
|
+
durationMs: Date.now() - gateStart,
|
|
70197
|
+
promptTokens: gateResponse.usage?.prompt_tokens,
|
|
70198
|
+
completionTokens: gateResponse.usage?.completion_tokens,
|
|
70199
|
+
totalTokens: gateResponse.usage?.total_tokens,
|
|
70200
|
+
assistantMessage: gateResponse.choices[0]?.message?.content,
|
|
70201
|
+
requestMessageCount: 2
|
|
70202
|
+
});
|
|
70203
|
+
} catch {
|
|
70204
|
+
}
|
|
70205
|
+
const gateRaw = gateResponse.choices[0]?.message?.content?.trim();
|
|
70206
|
+
if (gateRaw) {
|
|
70207
|
+
let gateOut = null;
|
|
70208
|
+
try {
|
|
70209
|
+
gateOut = JSON.parse((0, strip_json_fences_js_1.stripJsonFences)(gateRaw));
|
|
70210
|
+
} catch (err) {
|
|
70211
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
70212
|
+
logs2.push(`AI planner strengthen-gate returned invalid JSON (${message}) \u2014 keeping pre-gate result`);
|
|
70213
|
+
}
|
|
70214
|
+
if (gateOut) {
|
|
70215
|
+
try {
|
|
70216
|
+
const vg = validateAIPlannerOutput(gateOut, siteMap, context.existingTestNames, context.testBudget, evidence.recordedJourneys, context.mode, context.focusAreas);
|
|
70217
|
+
const preGateCount = scenarios.length;
|
|
70218
|
+
const keepsCoverage = vg.scenarios.length >= Math.ceil(preGateCount * 0.5);
|
|
70219
|
+
const noWorseGaps = vg.coverageGaps.length <= coverageGaps.length;
|
|
70220
|
+
if (vg.scenarios.length > 0 && keepsCoverage && noWorseGaps) {
|
|
70221
|
+
logs2.push(`AI planner strengthen-gate [${gateModel}]: ${preGateCount}\u2192${vg.scenarios.length} scenarios, gaps ${coverageGaps.length}\u2192${vg.coverageGaps.length}; taking gated result`);
|
|
70222
|
+
scenarios = vg.scenarios;
|
|
70223
|
+
featureGroups = vg.featureGroups;
|
|
70224
|
+
warnings = [...warnings, ...vg.warnings];
|
|
70225
|
+
coverageGaps = vg.coverageGaps;
|
|
70226
|
+
} else {
|
|
70227
|
+
logs2.push(`AI planner strengthen-gate rejected (scen ${preGateCount}\u2192${vg.scenarios.length}, gaps ${coverageGaps.length}\u2192${vg.coverageGaps.length}); keeping pre-gate result`);
|
|
70228
|
+
}
|
|
70229
|
+
} catch (err) {
|
|
70230
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
70231
|
+
logs2.push(`AI planner strengthen-gate validation threw (${message}) \u2014 keeping pre-gate result`);
|
|
70232
|
+
}
|
|
70233
|
+
}
|
|
70234
|
+
}
|
|
70235
|
+
}
|
|
70236
|
+
}
|
|
69891
70237
|
const totalFinal = scenarios.length + coverageGaps.length;
|
|
69892
70238
|
const rejectionRateFinal = totalFinal > 0 ? coverageGaps.length / totalFinal : 0;
|
|
69893
70239
|
if (rejectionRateFinal > 0.5 && totalFinal >= 3) {
|
|
@@ -73667,7 +74013,7 @@ var require_mobile_heal_postprocess = __commonJS({
|
|
|
73667
74013
|
// OpenAI-style secret key
|
|
73668
74014
|
];
|
|
73669
74015
|
var EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
|
|
73670
|
-
function
|
|
74016
|
+
function escapeRegExp2(value) {
|
|
73671
74017
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
73672
74018
|
}
|
|
73673
74019
|
function collectSecretVars(testVariables, minLen, keyRe) {
|
|
@@ -73686,7 +74032,7 @@ var require_mobile_heal_postprocess = __commonJS({
|
|
|
73686
74032
|
return text;
|
|
73687
74033
|
let out = text;
|
|
73688
74034
|
for (const { key, value } of collectSecretVars(testVariables, MIN_REDACT_LEN, REDACT_KEY_RE)) {
|
|
73689
|
-
out = out.replace(new RegExp(
|
|
74035
|
+
out = out.replace(new RegExp(escapeRegExp2(value), "g"), `{{${key}}}`);
|
|
73690
74036
|
}
|
|
73691
74037
|
for (const source of TOKEN_SOURCES) {
|
|
73692
74038
|
out = out.replace(new RegExp(source, "g"), "[REDACTED]");
|
|
@@ -76086,7 +76432,7 @@ var require_api_auth_headers = __commonJS({
|
|
|
76086
76432
|
"../runner-core/dist/services/api-auth-headers.js"(exports) {
|
|
76087
76433
|
"use strict";
|
|
76088
76434
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
76089
|
-
exports.parseApiAuthHeaders =
|
|
76435
|
+
exports.parseApiAuthHeaders = parseApiAuthHeaders2;
|
|
76090
76436
|
exports.resolveApiAuthHeaders = resolveApiAuthHeaders;
|
|
76091
76437
|
exports.isAppOriginRequest = isAppOriginRequest;
|
|
76092
76438
|
exports.buildOriginScopedApiFixtures = buildOriginScopedApiFixtures;
|
|
@@ -76099,7 +76445,7 @@ var require_api_auth_headers = __commonJS({
|
|
|
76099
76445
|
}
|
|
76100
76446
|
return false;
|
|
76101
76447
|
}
|
|
76102
|
-
function
|
|
76448
|
+
function parseApiAuthHeaders2(raw) {
|
|
76103
76449
|
if (!raw || typeof raw !== "string")
|
|
76104
76450
|
return void 0;
|
|
76105
76451
|
const trimmed = raw.trim();
|
|
@@ -76123,9 +76469,9 @@ var require_api_auth_headers = __commonJS({
|
|
|
76123
76469
|
function resolveApiAuthHeaders(input) {
|
|
76124
76470
|
if (input.explicit)
|
|
76125
76471
|
return input.explicit;
|
|
76126
|
-
if (
|
|
76472
|
+
if (input.standingHeadersMode === "none")
|
|
76127
76473
|
return void 0;
|
|
76128
|
-
return
|
|
76474
|
+
return parseApiAuthHeaders2(input.apiAuthHeadersVar);
|
|
76129
76475
|
}
|
|
76130
76476
|
function isAppOriginRequest(url, baseUrl) {
|
|
76131
76477
|
if (typeof url !== "string" || !baseUrl)
|
|
@@ -76392,7 +76738,7 @@ var require_playwright_native = __commonJS({
|
|
|
76392
76738
|
var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
|
|
76393
76739
|
var PW_CLI = __require.resolve("@playwright/test/cli");
|
|
76394
76740
|
async function executeTestViaNativePlaywright2(options) {
|
|
76395
|
-
const { playwrightCode, baseUrl, testName, testVariables, authStateCapturePath, storageStatePath, extraHttpHeaders, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
|
|
76741
|
+
const { playwrightCode, baseUrl, testName, testVariables, authStateCapturePath, storageStatePath, extraHttpHeaders, authStandingMode, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
|
|
76396
76742
|
const logs2 = [];
|
|
76397
76743
|
const screenshots = [];
|
|
76398
76744
|
logs2.push(`\u{1F3AD} Native Playwright starting`);
|
|
@@ -76402,16 +76748,19 @@ var require_playwright_native = __commonJS({
|
|
|
76402
76748
|
logs2.push("\u26A0\uFE0F WARNING: baseUrl is empty - test will likely fail with navigation errors");
|
|
76403
76749
|
}
|
|
76404
76750
|
let runDir = null;
|
|
76751
|
+
let standingNeedleMatcher = null;
|
|
76405
76752
|
let callbackProxy;
|
|
76406
76753
|
try {
|
|
76407
76754
|
runDir = (0, path_1.join)(RUNS_DIR, (0, crypto_1.randomUUID)());
|
|
76408
76755
|
await (0, promises_1.mkdir)(runDir, { recursive: true });
|
|
76409
76756
|
const isApiTest2 = playwrightCode.includes("@api") || playwrightCode.includes("{ request }") && !playwrightCode.includes("{ page");
|
|
76757
|
+
const effectiveStandingMode = isApiTest2 ? authStandingMode ?? "legacy-if-configured" : "none";
|
|
76410
76758
|
const resolvedExtraHeaders = (0, api_auth_headers_js_1.resolveApiAuthHeaders)({
|
|
76411
76759
|
explicit: extraHttpHeaders,
|
|
76412
|
-
|
|
76760
|
+
standingHeadersMode: effectiveStandingMode,
|
|
76413
76761
|
apiAuthHeadersVar: testVariables?.API_AUTH_HEADERS
|
|
76414
76762
|
});
|
|
76763
|
+
standingNeedleMatcher = (0, shared_1.buildNeedleMatcher)(resolvedExtraHeaders ? Object.values(resolvedExtraHeaders) : []);
|
|
76415
76764
|
const cssDisableSetup = `
|
|
76416
76765
|
const __qaLiveBrowserStateFile = process.env.VALIDATE_LIVE_BROWSER_STATE_PATH;
|
|
76417
76766
|
let __qaLiveBrowserInterval: ReturnType<typeof setInterval> | null = null;
|
|
@@ -76606,6 +76955,18 @@ test.afterEach(async ({ page }, testInfo) => {
|
|
|
76606
76955
|
logs2.push("\u26A0 Could not find @playwright/test import - animation disabling NOT injected");
|
|
76607
76956
|
}
|
|
76608
76957
|
const useScopedApiHeaders = isApiTest2 && !!resolvedExtraHeaders && !!importMatch && !!baseUrl;
|
|
76958
|
+
const authDelivery = { standingHeadersConfigured: !!resolvedExtraHeaders, standingHeadersInjected: useScopedApiHeaders };
|
|
76959
|
+
if (resolvedExtraHeaders && !useScopedApiHeaders) {
|
|
76960
|
+
logs2.push("\u26A0 Standing API auth headers were configured but could not be installed origin-scoped \u2014 failing closed (no global fallback).");
|
|
76961
|
+
return {
|
|
76962
|
+
passed: false,
|
|
76963
|
+
stepResults: [],
|
|
76964
|
+
screenshots: [],
|
|
76965
|
+
logs: logs2.join("\n"),
|
|
76966
|
+
error: 'CONFIG_ISSUE: Standing API auth headers were configured but could not be installed origin-scoped (needs a rewritable `import { test } from "@playwright/test"` and BASE_URL); refusing to attach them globally to avoid an off-origin credential leak.',
|
|
76967
|
+
authDelivery
|
|
76968
|
+
};
|
|
76969
|
+
}
|
|
76609
76970
|
let instrumentedCode;
|
|
76610
76971
|
if (isApiTest2) {
|
|
76611
76972
|
instrumentedCode = useScopedApiHeaders ? playwrightCode.replace(importMatch[0], importMatch[0].replace(/from\s+['"]@playwright\/test['"]/, "from './qa-api-fixtures'")) : playwrightCode;
|
|
@@ -76629,7 +76990,7 @@ test.afterEach(async ({ page }, testInfo) => {
|
|
|
76629
76990
|
const absHarPath = wantHar ? (0, path_1.join)(runDir, "session.har").replace(/\\/g, "/") : null;
|
|
76630
76991
|
const recordHarLine = absHarPath ? `recordHar: { path: ${JSON.stringify(absHarPath)}, mode: 'full' },` : "";
|
|
76631
76992
|
const storageStateLine = storageStatePath ? `storageState: ${JSON.stringify(storageStatePath.replace(/\\/g, "/"))},` : "";
|
|
76632
|
-
const extraHeadersLine =
|
|
76993
|
+
const extraHeadersLine = "";
|
|
76633
76994
|
const launchArgs = IS_CLOUD2 ? "args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-blink-features=AutomationControlled', '--disable-dev-shm-usage', '--disable-gpu', '--disable-software-rasterizer', '--no-zygote', '--disable-accelerated-2d-canvas', '--js-flags=--max-old-space-size=512']," : "args: ['--disable-blink-features=AutomationControlled'],";
|
|
76634
76995
|
const config = `
|
|
76635
76996
|
// Plain object export - no require('@playwright/test') needed.
|
|
@@ -76767,7 +77128,7 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
|
|
|
76767
77128
|
logs2.push(`\u26A0 HAR parse skipped: ${msg.slice(0, 200)}`);
|
|
76768
77129
|
}
|
|
76769
77130
|
}
|
|
76770
|
-
|
|
77131
|
+
const nativeResult = {
|
|
76771
77132
|
passed,
|
|
76772
77133
|
stepResults,
|
|
76773
77134
|
screenshots,
|
|
@@ -76776,19 +77137,24 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
|
|
|
76776
77137
|
harApiCalls,
|
|
76777
77138
|
logs: logs2.join("\n"),
|
|
76778
77139
|
error: passed ? void 0 : error2,
|
|
76779
|
-
suspiciousPass
|
|
77140
|
+
suspiciousPass,
|
|
77141
|
+
authDelivery
|
|
76780
77142
|
};
|
|
77143
|
+
if (!standingNeedleMatcher)
|
|
77144
|
+
return nativeResult;
|
|
77145
|
+
return (0, shared_1.redactNeedlesDeep)(nativeResult, standingNeedleMatcher);
|
|
76781
77146
|
} catch (fatalErr) {
|
|
76782
77147
|
const msg = fatalErr instanceof Error ? fatalErr.message : String(fatalErr);
|
|
76783
77148
|
logs2.push(`
|
|
76784
77149
|
\u{1F4A5} Fatal error: ${msg}`);
|
|
76785
|
-
|
|
77150
|
+
const fatalResult = {
|
|
76786
77151
|
passed: false,
|
|
76787
77152
|
stepResults: [],
|
|
76788
77153
|
screenshots,
|
|
76789
77154
|
logs: logs2.join("\n"),
|
|
76790
77155
|
error: msg
|
|
76791
77156
|
};
|
|
77157
|
+
return standingNeedleMatcher ? (0, shared_1.redactNeedlesDeep)(fatalResult, standingNeedleMatcher) : fatalResult;
|
|
76792
77158
|
} finally {
|
|
76793
77159
|
await callbackProxy?.close().catch(() => {
|
|
76794
77160
|
});
|
|
@@ -85892,7 +86258,7 @@ ${elementContext}${formContext}${observationContext}${interactionContext}${lowCo
|
|
|
85892
86258
|
}
|
|
85893
86259
|
return result;
|
|
85894
86260
|
}
|
|
85895
|
-
function
|
|
86261
|
+
function escapeRegExp2(value) {
|
|
85896
86262
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
85897
86263
|
}
|
|
85898
86264
|
function buildSensitiveEnvReplacements(testVariables) {
|
|
@@ -85914,7 +86280,7 @@ ${elementContext}${formContext}${observationContext}${interactionContext}${lowCo
|
|
|
85914
86280
|
for (const replacement of buildSensitiveEnvReplacements(testVariables)) {
|
|
85915
86281
|
if (replacement.value.includes("\n") || replacement.value.includes("\r"))
|
|
85916
86282
|
continue;
|
|
85917
|
-
const literalPattern = new RegExp(`(['"\`])${
|
|
86283
|
+
const literalPattern = new RegExp(`(['"\`])${escapeRegExp2(replacement.value)}\\1`, "g");
|
|
85918
86284
|
result = result.replace(literalPattern, replacement.envRef);
|
|
85919
86285
|
}
|
|
85920
86286
|
return result;
|
|
@@ -255170,7 +255536,7 @@ ${newComment.split("\n").map((c) => ` * ${c}`).join("\n")}
|
|
|
255170
255536
|
}
|
|
255171
255537
|
}
|
|
255172
255538
|
return result;
|
|
255173
|
-
function
|
|
255539
|
+
function escapeRegExp2(str) {
|
|
255174
255540
|
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
|
|
255175
255541
|
}
|
|
255176
255542
|
function getTodoCommentsRegExp() {
|
|
@@ -255178,7 +255544,7 @@ ${newComment.split("\n").map((c) => ` * ${c}`).join("\n")}
|
|
|
255178
255544
|
const multiLineCommentStart = /(?:\/\*+\s*)/.source;
|
|
255179
255545
|
const anyNumberOfSpacesAndAsterisksAtStartOfLine = /(?:^(?:\s|\*)*)/.source;
|
|
255180
255546
|
const preamble = "(" + anyNumberOfSpacesAndAsterisksAtStartOfLine + "|" + singleLineCommentStart + "|" + multiLineCommentStart + ")";
|
|
255181
|
-
const literals = "(?:" + map2(descriptors, (d) => "(" +
|
|
255547
|
+
const literals = "(?:" + map2(descriptors, (d) => "(" + escapeRegExp2(d.text) + ")").join("|") + ")";
|
|
255182
255548
|
const endOfLineOrEndOfComment = /(?:$|\*\/)/.source;
|
|
255183
255549
|
const messageRemainder = /(?:.*?)/.source;
|
|
255184
255550
|
const messagePortion = "(" + literals + messageRemainder + ")";
|
|
@@ -303438,7 +303804,7 @@ ${(verifyResult.logs ?? "").slice(0, 2e3)}`;
|
|
|
303438
303804
|
confidence: "low"
|
|
303439
303805
|
};
|
|
303440
303806
|
}
|
|
303441
|
-
function
|
|
303807
|
+
function escapeRegExp2(s) {
|
|
303442
303808
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
303443
303809
|
}
|
|
303444
303810
|
function tryDeterministicStrictModeFix(code, error2) {
|
|
@@ -303461,10 +303827,10 @@ ${(verifyResult.logs ?? "").slice(0, 2e3)}`;
|
|
|
303461
303827
|
const [, , role, , name, restOfOptions] = violated;
|
|
303462
303828
|
if (/exact\s*:\s*true/.test(restOfOptions))
|
|
303463
303829
|
return null;
|
|
303464
|
-
const akaPattern = new RegExp(`aka\\s+getByRole\\(\\s*(['"\`])${
|
|
303830
|
+
const akaPattern = new RegExp(`aka\\s+getByRole\\(\\s*(['"\`])${escapeRegExp2(role)}\\1\\s*,\\s*\\{[^}]*?name:\\s*(['"\`])${escapeRegExp2(name)}\\2[^}]*?exact:\\s*true[^}]*\\}\\s*\\)`);
|
|
303465
303831
|
if (!akaPattern.test(error2))
|
|
303466
303832
|
return null;
|
|
303467
|
-
const codePattern = new RegExp(`(getByRole\\(\\s*(['"\`])${
|
|
303833
|
+
const codePattern = new RegExp(`(getByRole\\(\\s*(['"\`])${escapeRegExp2(role)}\\2\\s*,\\s*\\{[^}]*?name:\\s*(['"\`])${escapeRegExp2(name)}\\3)([^}]*\\})`, "g");
|
|
303468
303834
|
let modified = false;
|
|
303469
303835
|
const patched = code.replace(codePattern, (full, prefix, _q1, _q2, suffix) => {
|
|
303470
303836
|
if (/exact\s*:\s*true/.test(suffix))
|
|
@@ -303481,11 +303847,11 @@ ${(verifyResult.logs ?? "").slice(0, 2e3)}`;
|
|
|
303481
303847
|
const [, , text, optionsBlock] = violated;
|
|
303482
303848
|
if (optionsBlock && /exact\s*:\s*true/.test(optionsBlock))
|
|
303483
303849
|
return null;
|
|
303484
|
-
const akaPattern = new RegExp(`aka\\s+getByText\\(\\s*(['"\`])${
|
|
303850
|
+
const akaPattern = new RegExp(`aka\\s+getByText\\(\\s*(['"\`])${escapeRegExp2(text)}\\1\\s*,\\s*\\{[^}]*?exact:\\s*true[^}]*\\}\\s*\\)`);
|
|
303485
303851
|
if (!akaPattern.test(error2))
|
|
303486
303852
|
return null;
|
|
303487
|
-
const noOptsPattern = new RegExp(`getByText\\(\\s*(['"\`])${
|
|
303488
|
-
const withOptsPattern = new RegExp(`(getByText\\(\\s*(['"\`])${
|
|
303853
|
+
const noOptsPattern = new RegExp(`getByText\\(\\s*(['"\`])${escapeRegExp2(text)}\\1\\s*\\)`, "g");
|
|
303854
|
+
const withOptsPattern = new RegExp(`(getByText\\(\\s*(['"\`])${escapeRegExp2(text)}\\2\\s*,\\s*\\{)([^}]*\\})`, "g");
|
|
303489
303855
|
let modified = false;
|
|
303490
303856
|
let patched = code.replace(noOptsPattern, (_full, q) => {
|
|
303491
303857
|
modified = true;
|
|
@@ -321442,9 +321808,9 @@ var require_tool_ledger = __commonJS({
|
|
|
321442
321808
|
const ledgerPath = (0, node_path_1.join)(runDir, "discovery-events.jsonl");
|
|
321443
321809
|
let checkpointIndex = 0;
|
|
321444
321810
|
let headerWritten = false;
|
|
321445
|
-
const
|
|
321811
|
+
const escapeRegExp2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
321446
321812
|
const credentialScrubbers = Array.from(credentialEnvMap.entries()).filter(([, value]) => typeof value === "string" && value.length > 0).sort((a, b) => b[1].length - a[1].length).map(([envName, value]) => ({
|
|
321447
|
-
re: new RegExp(
|
|
321813
|
+
re: new RegExp(escapeRegExp2(value), "g"),
|
|
321448
321814
|
replacement: `[REDACTED_${envName}]`
|
|
321449
321815
|
}));
|
|
321450
321816
|
const scrubCredentials = (text) => {
|
|
@@ -323604,6 +323970,7 @@ Explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elemen
|
|
|
323604
323970
|
testBudget
|
|
323605
323971
|
}, {
|
|
323606
323972
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
323973
|
+
gateModelOverride: ctx.planGateModel,
|
|
323607
323974
|
onAICall
|
|
323608
323975
|
});
|
|
323609
323976
|
logs2.push(...planResult.logs);
|
|
@@ -324182,6 +324549,7 @@ Plan (reused): ${resumeSavedScenarios.length} scenarios from the paused run.`);
|
|
|
324182
324549
|
testBudget: featureTestBudget
|
|
324183
324550
|
}, {
|
|
324184
324551
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
324552
|
+
gateModelOverride: ctx.planGateModel,
|
|
324185
324553
|
onAICall
|
|
324186
324554
|
});
|
|
324187
324555
|
logs2.push(...planResult.logs);
|
|
@@ -337993,7 +338361,7 @@ var require_package4 = __commonJS({
|
|
|
337993
338361
|
"package.json"(exports, module) {
|
|
337994
338362
|
module.exports = {
|
|
337995
338363
|
name: "@validate.qa/runner",
|
|
337996
|
-
version: "1.0.
|
|
338364
|
+
version: "1.0.44",
|
|
337997
338365
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
337998
338366
|
bin: {
|
|
337999
338367
|
"validate-runner": "dist/cli.js"
|
|
@@ -338111,32 +338479,24 @@ var import_runner_core12 = __toESM(require_dist2());
|
|
|
338111
338479
|
|
|
338112
338480
|
// src/services/auth-state.ts
|
|
338113
338481
|
init_dist();
|
|
338114
|
-
var INLINE_LOGIN_TAG = "@inline-login";
|
|
338115
338482
|
var GUARDED_LOGIN_TAG = "@auth-guarded";
|
|
338116
338483
|
function sharedAuthInjectEnabled() {
|
|
338117
338484
|
return isSharedAuthInjectEnabled();
|
|
338118
338485
|
}
|
|
338119
|
-
function
|
|
338486
|
+
function hasGeneratedAuthGuard2(playwrightCode) {
|
|
338120
338487
|
if (!playwrightCode) return false;
|
|
338121
338488
|
return /\/\/ >>> VALIDATE_QA_AUTH v\d+\b[\s\S]*?process\.env\.AUTH_FILE[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH v\d+\b/.test(playwrightCode);
|
|
338122
338489
|
}
|
|
338123
338490
|
function shouldCaptureAuthState(tags, playwrightCode) {
|
|
338124
|
-
return sharedAuthInjectEnabled() && Array.isArray(tags) && tags.includes("@needs-auth") && tags.includes(GUARDED_LOGIN_TAG) && !tags.includes("@auth") &&
|
|
338125
|
-
}
|
|
338126
|
-
function hasInlineLoginSteps(playwrightCode) {
|
|
338127
|
-
if (!playwrightCode) return false;
|
|
338128
|
-
return /process\.env\.TEST_USER_EMAIL!?/.test(playwrightCode) && /process\.env\.TEST_USER_PASSWORD!?/.test(playwrightCode);
|
|
338491
|
+
return sharedAuthInjectEnabled() && Array.isArray(tags) && tags.includes("@needs-auth") && tags.includes(GUARDED_LOGIN_TAG) && !tags.includes("@auth") && hasGeneratedAuthGuard2(playwrightCode);
|
|
338129
338492
|
}
|
|
338130
338493
|
function shouldUseAuthState(tags, playwrightCode) {
|
|
338131
338494
|
if (!sharedAuthInjectEnabled()) return false;
|
|
338132
338495
|
if (!Array.isArray(tags)) return false;
|
|
338133
|
-
|
|
338134
|
-
|
|
338135
|
-
|
|
338136
|
-
|
|
338137
|
-
const isGeneratedVariant = tags.includes("@edge") || tags.includes("@bug");
|
|
338138
|
-
const hasLegacyInlineLogin = tags.includes("@discovered") && !isGeneratedVariant && hasInlineLoginSteps(playwrightCode);
|
|
338139
|
-
return !tags.includes(INLINE_LOGIN_TAG) && !hasLegacyInlineLogin;
|
|
338496
|
+
return authPlanFor(tags, playwrightCode).needsCookieState;
|
|
338497
|
+
}
|
|
338498
|
+
function authPlanFor(tags, playwrightCode) {
|
|
338499
|
+
return deriveAuthPlan(tags, playwrightCode, { sharedAuthInjectEnabled: sharedAuthInjectEnabled() });
|
|
338140
338500
|
}
|
|
338141
338501
|
|
|
338142
338502
|
// src/version-check.ts
|
|
@@ -345535,7 +345895,18 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
345535
345895
|
requestLiveBrowserHeartbeat();
|
|
345536
345896
|
}, 5e3);
|
|
345537
345897
|
liveBrowserHeartbeatInterval.unref?.();
|
|
345898
|
+
const authPlan = authPlanFor(run2.tags, run2.playwrightCode);
|
|
345899
|
+
const runAuthCachePath = (0, import_runner_core12.localAuthStatePaths)(getRunsDir(), {
|
|
345900
|
+
projectId: run2.projectId || opts.projectId || "default",
|
|
345901
|
+
serverUrl: opts.serverUrl,
|
|
345902
|
+
baseUrl: run2.baseUrl,
|
|
345903
|
+
testVariables: run2.testVariables,
|
|
345904
|
+
authToken: opts.authToken
|
|
345905
|
+
}, run2.runId).cachePath;
|
|
345906
|
+
const wrapperStorageStatePath = authPlan.needsCookieState && runMode !== "auth-setup" ? (0, import_runner_core12.getValidLocalAuthState)(runAuthCachePath) : void 0;
|
|
345538
345907
|
const runNativeWithLiveBrowser = (nativeOptions) => (0, import_runner_core.executeTestViaNativePlaywright)({
|
|
345908
|
+
authStandingMode: authPlan.standingHeadersMode,
|
|
345909
|
+
storageStatePath: wrapperStorageStatePath,
|
|
345539
345910
|
...nativeOptions,
|
|
345540
345911
|
liveBrowserHandle,
|
|
345541
345912
|
onLiveBrowserSnapshot: requestLiveBrowserHeartbeat
|
|
@@ -345563,11 +345934,24 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
345563
345934
|
if (!run2.baseUrl) {
|
|
345564
345935
|
preflightError = "Pre-flight failed: Missing BASE_URL. Configure it in Test Data \u2192 Test Variables.";
|
|
345565
345936
|
} else {
|
|
345566
|
-
const
|
|
345567
|
-
if (
|
|
345568
|
-
preflightError =
|
|
345569
|
-
} else if (
|
|
345570
|
-
preflightError = "Pre-flight failed:
|
|
345937
|
+
const validStanding = (0, import_runner_core13.parseApiAuthHeaders)(run2.testVariables?.["API_AUTH_HEADERS"]) !== void 0;
|
|
345938
|
+
if (authPlan.configurationError) {
|
|
345939
|
+
preflightError = `Pre-flight failed: ${authPlan.configurationError}. Fix the test's auth-type tag before running.`;
|
|
345940
|
+
} else if (authPlan.needsStandingHeaders && !validStanding) {
|
|
345941
|
+
preflightError = "Pre-flight failed: This @auth-standing @api test requires a valid API_AUTH_HEADERS JSON object (header name \u2192 value).";
|
|
345942
|
+
} else {
|
|
345943
|
+
const legacyHeaderBypass = authPlan.source === "legacy" && authPlan.standingHeadersMode === "legacy-if-configured" && validStanding;
|
|
345944
|
+
const code = run2.playwrightCode ?? "";
|
|
345945
|
+
const codeProvided = code.trim().length > 0;
|
|
345946
|
+
const codeUsesCreds = codeProvided && /TEST_USER_EMAIL|TEST_USER_PASSWORD|__vqaLogin/.test(code);
|
|
345947
|
+
const tags = Array.isArray(run2.tags) ? run2.tags : [];
|
|
345948
|
+
const isHallucinationShape = codeProvided && !codeUsesCreds && tags.includes("@inline-login") && !tags.includes("@api");
|
|
345949
|
+
const requireCreds = authPlan.needsLoginCreds && !legacyHeaderBypass && !isHallucinationShape;
|
|
345950
|
+
if (requireCreds && !run2.testVariables?.["TEST_USER_EMAIL"]?.trim()) {
|
|
345951
|
+
preflightError = "Pre-flight failed: Missing TEST_USER_EMAIL. Tests that log in (tagged @auth, @needs-auth, @api, @auth-cookie-state, @auth-login-cookie, or @auth-login-bearer) require login credentials.";
|
|
345952
|
+
} else if (requireCreds && !run2.testVariables?.["TEST_USER_PASSWORD"]?.trim()) {
|
|
345953
|
+
preflightError = "Pre-flight failed: Missing TEST_USER_PASSWORD. Tests that log in (tagged @auth, @needs-auth, @api, @auth-cookie-state, @auth-login-cookie, or @auth-login-bearer) require login credentials.";
|
|
345954
|
+
}
|
|
345571
345955
|
}
|
|
345572
345956
|
}
|
|
345573
345957
|
if (preflightError) {
|
|
@@ -346176,10 +346560,13 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
346176
346560
|
});
|
|
346177
346561
|
if (healRes.ok) {
|
|
346178
346562
|
const { healedCode } = await healRes.json();
|
|
346563
|
+
const failedTestPlan = authPlanFor(failedTest.tags, healedCode);
|
|
346179
346564
|
const verifyResult = await runNativeWithLiveBrowser({
|
|
346180
346565
|
playwrightCode: healedCode,
|
|
346181
346566
|
baseUrl: run2.baseUrl,
|
|
346182
346567
|
testVariables: run2.testVariables,
|
|
346568
|
+
storageStatePath: failedTestPlan.needsCookieState ? (0, import_runner_core12.getValidLocalAuthState)(runAuthCachePath) : void 0,
|
|
346569
|
+
authStandingMode: failedTestPlan.standingHeadersMode,
|
|
346183
346570
|
validateContext: callbackContext
|
|
346184
346571
|
});
|
|
346185
346572
|
results.push({
|
|
@@ -347663,6 +348050,10 @@ ${result2.logs}`);
|
|
|
347663
348050
|
log.warn(`\u26A0 Rate-limit block detected - posting ERROR (not FAILED) to skip heal/quarantine`);
|
|
347664
348051
|
}
|
|
347665
348052
|
}
|
|
348053
|
+
if (runStatus === "FAILED" && classifyErrorMessage(runError ?? null) === "config_issue") {
|
|
348054
|
+
runStatus = "ERROR";
|
|
348055
|
+
log.warn(`\u26A0 CONFIG_ISSUE from native execution - posting ERROR (not FAILED) to skip heal/quarantine`);
|
|
348056
|
+
}
|
|
347666
348057
|
await postResult(opts.serverUrl, headers, run2.runId, {
|
|
347667
348058
|
status: runStatus,
|
|
347668
348059
|
stepResults: result.stepResults,
|
|
@@ -347678,7 +348069,7 @@ ${result2.logs}`);
|
|
|
347678
348069
|
if (result.passed) {
|
|
347679
348070
|
log.ok(`PASSED (${duration}ms)${result.suspiciousPass ? ` \u26A0 suspicious-pass (viewport ${Math.round(result.suspiciousPass.emptyRatio * 100)}% blank)` : ""}`);
|
|
347680
348071
|
} else if (runStatus === "ERROR") {
|
|
347681
|
-
log.warn(`BLOCKED (${duration}ms) - rate-limit
|
|
348072
|
+
log.warn(`BLOCKED (${duration}ms) - environmental block (rate-limit / config-issue), not counted as failure`);
|
|
347682
348073
|
} else {
|
|
347683
348074
|
log.fail(`FAILED (${duration}ms)${result.error ? ` - ${result.error}` : ""}`);
|
|
347684
348075
|
if (result.logs) {
|