@validate.qa/runner 1.0.42 → 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 +1352 -178
- package/dist/cli.mjs +1352 -178
- package/package.json +2 -2
package/dist/cli.mjs
CHANGED
|
@@ -1761,7 +1761,18 @@ var init_constants = __esm({
|
|
|
1761
1761
|
"MINIMAX_API_KEY",
|
|
1762
1762
|
"ANTHROPIC_API_KEY",
|
|
1763
1763
|
"NVIDIA_API_KEY",
|
|
1764
|
-
"DEEPSEEK_API_KEY"
|
|
1764
|
+
"DEEPSEEK_API_KEY",
|
|
1765
|
+
// Run-scoped callback capabilities. The parent runner injects these after
|
|
1766
|
+
// sanitization; customer variables must never impersonate the control plane.
|
|
1767
|
+
"AUTH_FILE",
|
|
1768
|
+
"SHARED_AUTH_INJECT",
|
|
1769
|
+
"VALIDATE_RUN_ID",
|
|
1770
|
+
"VALIDATE_CLAIM_EPOCH",
|
|
1771
|
+
"VALIDATE_AUTH_CONFIG_HASH",
|
|
1772
|
+
"VALIDATE_TASK_ARN",
|
|
1773
|
+
"VALIDATE_RUNNER_AUTH",
|
|
1774
|
+
"VALIDATE_SERVER_URL",
|
|
1775
|
+
"VALIDATE_LIVE_BROWSER_STATE_PATH"
|
|
1765
1776
|
]);
|
|
1766
1777
|
SPAWNED_ENV_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
1767
1778
|
// Runtime essentials - Node.js and Playwright need these to function
|
|
@@ -4773,6 +4784,204 @@ var init_origin_domain = __esm({
|
|
|
4773
4784
|
}
|
|
4774
4785
|
});
|
|
4775
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
|
+
|
|
4776
4985
|
// ../shared/dist/voice-config.js
|
|
4777
4986
|
function normalizeTtsVoiceId(voiceId) {
|
|
4778
4987
|
if (!voiceId)
|
|
@@ -5029,6 +5238,16 @@ var init_voice_config = __esm({
|
|
|
5029
5238
|
}
|
|
5030
5239
|
});
|
|
5031
5240
|
|
|
5241
|
+
// ../shared/dist/feature-flags.js
|
|
5242
|
+
function isSharedAuthInjectEnabled(value = process.env.SHARED_AUTH_INJECT) {
|
|
5243
|
+
return value?.trim().toLowerCase() !== "false";
|
|
5244
|
+
}
|
|
5245
|
+
var init_feature_flags = __esm({
|
|
5246
|
+
"../shared/dist/feature-flags.js"() {
|
|
5247
|
+
"use strict";
|
|
5248
|
+
}
|
|
5249
|
+
});
|
|
5250
|
+
|
|
5032
5251
|
// ../shared/dist/index.js
|
|
5033
5252
|
var dist_exports = {};
|
|
5034
5253
|
__export(dist_exports, {
|
|
@@ -5037,8 +5256,16 @@ __export(dist_exports, {
|
|
|
5037
5256
|
ADDON_DEFINITIONS: () => ADDON_DEFINITIONS,
|
|
5038
5257
|
ADMIN_EMAIL_LIMITS: () => ADMIN_EMAIL_LIMITS,
|
|
5039
5258
|
AI_CREDIT_MARGIN: () => AI_CREDIT_MARGIN,
|
|
5259
|
+
API_AUTH_HEADERS_ENV_KEY: () => API_AUTH_HEADERS_ENV_KEY,
|
|
5040
5260
|
API_PREFIX: () => API_PREFIX,
|
|
5041
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,
|
|
5042
5269
|
COST_PER_M: () => COST_PER_M,
|
|
5043
5270
|
CREDIT_PACKS_CENTS: () => CREDIT_PACKS_CENTS,
|
|
5044
5271
|
CREDIT_PRICES_CENTS: () => CREDIT_PRICES_CENTS,
|
|
@@ -5081,6 +5308,7 @@ __export(dist_exports, {
|
|
|
5081
5308
|
STT_MODEL_OPTIONS: () => STT_MODEL_OPTIONS,
|
|
5082
5309
|
STT_PROVIDER_OPTIONS: () => STT_PROVIDER_OPTIONS,
|
|
5083
5310
|
SUPPORT_EMAIL: () => SUPPORT_EMAIL,
|
|
5311
|
+
TEST_USER_ENV_KEYS: () => TEST_USER_ENV_KEYS,
|
|
5084
5312
|
TRANSIENT_CONTROL_ROLES: () => TRANSIENT_CONTROL_ROLES,
|
|
5085
5313
|
TRIAL_DURATION_DAYS: () => TRIAL_DURATION_DAYS,
|
|
5086
5314
|
TTS_MODEL_OPTIONS: () => TTS_MODEL_OPTIONS,
|
|
@@ -5090,10 +5318,12 @@ __export(dist_exports, {
|
|
|
5090
5318
|
accumulateStatements: () => accumulateStatements,
|
|
5091
5319
|
aiCostToCreditCents: () => aiCostToCreditCents,
|
|
5092
5320
|
analyzeMobileSelectorQuality: () => analyzeMobileSelectorQuality,
|
|
5321
|
+
authTypeTag: () => authTypeTag,
|
|
5093
5322
|
bugFoundQuarantineWhere: () => bugFoundQuarantineWhere,
|
|
5094
5323
|
buildAppiumCode: () => buildAppiumCode,
|
|
5095
5324
|
buildLaunchStep: () => buildLaunchStep,
|
|
5096
5325
|
buildMobileStep: () => buildMobileStep,
|
|
5326
|
+
buildNeedleMatcher: () => buildNeedleMatcher,
|
|
5097
5327
|
buildSandboxedEnv: () => buildSandboxedEnv,
|
|
5098
5328
|
buildSanitizedAgentEnv: () => buildSanitizedAgentEnv,
|
|
5099
5329
|
buildStepTarget: () => buildStepTarget,
|
|
@@ -5109,6 +5339,7 @@ __export(dist_exports, {
|
|
|
5109
5339
|
controlSignature: () => controlSignature,
|
|
5110
5340
|
countInnerTests: () => countInnerTests,
|
|
5111
5341
|
decideHarPosted: () => decideHarPosted,
|
|
5342
|
+
deriveAuthPlan: () => deriveAuthPlan,
|
|
5112
5343
|
deriveProjectKey: () => deriveProjectKey,
|
|
5113
5344
|
detectCrossOriginHtmlAsJson: () => detectCrossOriginHtmlAsJson,
|
|
5114
5345
|
detectsErrorBannerAssertion: () => detectsErrorBannerAssertion,
|
|
@@ -5152,6 +5383,7 @@ __export(dist_exports, {
|
|
|
5152
5383
|
isRunnableStep: () => isRunnableStep,
|
|
5153
5384
|
isSafeDeepLink: () => isSafeDeepLink,
|
|
5154
5385
|
isSameRegistrableDomainHost: () => isSameRegistrableDomainHost,
|
|
5386
|
+
isSharedAuthInjectEnabled: () => isSharedAuthInjectEnabled,
|
|
5155
5387
|
isSttModelForProvider: () => isSttModelForProvider,
|
|
5156
5388
|
isTestReadyForPlatform: () => isTestReadyForPlatform,
|
|
5157
5389
|
isTrialExpired: () => isTrialExpired,
|
|
@@ -5175,6 +5407,8 @@ __export(dist_exports, {
|
|
|
5175
5407
|
planHasBillingPortal: () => planHasBillingPortal,
|
|
5176
5408
|
planRequiresCheckout: () => planRequiresCheckout,
|
|
5177
5409
|
platformTag: () => platformTag,
|
|
5410
|
+
redactNeedlesDeep: () => redactNeedlesDeep,
|
|
5411
|
+
redactNeedlesText: () => redactNeedlesText,
|
|
5178
5412
|
registrableDomain: () => registrableDomain,
|
|
5179
5413
|
resolveCrossOriginConfigOverride: () => resolveCrossOriginConfigOverride,
|
|
5180
5414
|
resolveMobileStepValuePlaceholders: () => resolveMobileStepValuePlaceholders,
|
|
@@ -5217,7 +5451,10 @@ var init_dist = __esm({
|
|
|
5217
5451
|
init_agent_env();
|
|
5218
5452
|
init_test_counting();
|
|
5219
5453
|
init_origin_domain();
|
|
5454
|
+
init_auth_plan();
|
|
5455
|
+
init_redact_needles();
|
|
5220
5456
|
init_voice_config();
|
|
5457
|
+
init_feature_flags();
|
|
5221
5458
|
}
|
|
5222
5459
|
});
|
|
5223
5460
|
|
|
@@ -10681,7 +10918,7 @@ var require_transport = __commonJS({
|
|
|
10681
10918
|
"../../node_modules/pino/lib/transport.js"(exports, module) {
|
|
10682
10919
|
"use strict";
|
|
10683
10920
|
var { createRequire } = __require("module");
|
|
10684
|
-
var { existsSync:
|
|
10921
|
+
var { existsSync: existsSync3 } = __require("fs");
|
|
10685
10922
|
var getCallers = require_caller();
|
|
10686
10923
|
var { join: join7, isAbsolute, sep } = __require("path");
|
|
10687
10924
|
var { fileURLToPath } = __require("url");
|
|
@@ -10755,7 +10992,7 @@ var require_transport = __commonJS({
|
|
|
10755
10992
|
return false;
|
|
10756
10993
|
}
|
|
10757
10994
|
}
|
|
10758
|
-
return isAbsolute(path) && !
|
|
10995
|
+
return isAbsolute(path) && !existsSync3(path);
|
|
10759
10996
|
}
|
|
10760
10997
|
function stripQuotes(value) {
|
|
10761
10998
|
const first = value[0];
|
|
@@ -52383,11 +52620,11 @@ var require_leac = __commonJS({
|
|
|
52383
52620
|
if (r.str.length === 0) {
|
|
52384
52621
|
throw new Error(`Rule #${i} ("${r.name}") has empty "str" property, which is not allowed.`);
|
|
52385
52622
|
}
|
|
52386
|
-
return new RegExp(
|
|
52623
|
+
return new RegExp(escapeRegExp2(r.str), "y");
|
|
52387
52624
|
}
|
|
52388
|
-
return new RegExp(
|
|
52625
|
+
return new RegExp(escapeRegExp2(r.name), "y");
|
|
52389
52626
|
}
|
|
52390
|
-
function
|
|
52627
|
+
function escapeRegExp2(str) {
|
|
52391
52628
|
return str.replace(/[-[\]{}()*+!<=:?./\\^$|#\s,]/g, "\\$&");
|
|
52392
52629
|
}
|
|
52393
52630
|
function toSticky(re) {
|
|
@@ -68360,7 +68597,7 @@ var require_discover_ai_planner = __commonJS({
|
|
|
68360
68597
|
"../runner-core/dist/services/discover-ai-planner.js"(exports) {
|
|
68361
68598
|
"use strict";
|
|
68362
68599
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
68363
|
-
exports.SCENARIO_SYSTEM_PROMPT = void 0;
|
|
68600
|
+
exports.PLANNER_STRENGTHEN_GATE_PROMPT = exports.SCENARIO_SYSTEM_PROMPT = void 0;
|
|
68364
68601
|
exports.getAIPlannerMaxTokens = getAIPlannerMaxTokens;
|
|
68365
68602
|
exports.getSnapshotConfidence = getSnapshotConfidence;
|
|
68366
68603
|
exports.compressSiteMap = compressSiteMap;
|
|
@@ -68943,7 +69180,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
68943
69180
|
4. **Multi-page journeys** - only when transitions were actually observed
|
|
68944
69181
|
5. **Bug reports** - for observed 500 errors, crashes, or broken features
|
|
68945
69182
|
6. **Steps are human-readable** - "Fill the email field", not "page.getByLabel('Email').fill()"
|
|
68946
|
-
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.
|
|
68947
69184
|
8. **Auth-aware** - mark scenarios that need login. Use process.env.TEST_USER_EMAIL / PASSWORD
|
|
68948
69185
|
9. **No duplicates** - check existing test names before adding
|
|
68949
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.
|
|
@@ -68956,7 +69193,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
68956
69193
|
Example - BAD happy path: \`["Navigate to /feature", "Click Create", "Verify dialog opens"]\` \u2192 only tests the trigger.
|
|
68957
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"]\`
|
|
68958
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"]\`
|
|
68959
|
-
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.
|
|
68960
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.
|
|
68961
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>."
|
|
68962
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.
|
|
@@ -69019,6 +69256,9 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
69019
69256
|
Citing observed labels in \`expectedOutcomes\` is fine \u2014 that's asserting on stable product signals, not implementation specifics.
|
|
69020
69257
|
|
|
69021
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):
|
|
69022
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)
|
|
69023
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)
|
|
69024
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.
|
|
@@ -69048,11 +69288,11 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
69048
69288
|
{ "action": "Fill the email field with test credentials" },
|
|
69049
69289
|
{ "action": "Fill the password field with test credentials" },
|
|
69050
69290
|
{ "action": "Click the Sign In button" },
|
|
69051
|
-
{ "action": "
|
|
69291
|
+
{ "action": "After reaching the observed post-login page, reload it" }
|
|
69052
69292
|
],
|
|
69053
69293
|
"expectedOutcomes": [
|
|
69054
|
-
{ "type": "url", "description": "User reaches /dashboard after signing in", "reliability": "high" },
|
|
69055
|
-
{ "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" }
|
|
69056
69296
|
],
|
|
69057
69297
|
"targetPages": ["/auth/login", "/dashboard"],
|
|
69058
69298
|
"prerequisites": []
|
|
@@ -69082,6 +69322,46 @@ Each entry is exactly one of:
|
|
|
69082
69322
|
3 = medium (navigation, secondary features)
|
|
69083
69323
|
4 = low (edge cases, cosmetic)
|
|
69084
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.`;
|
|
69085
69365
|
function coerceRawPrerequisites(raw) {
|
|
69086
69366
|
if (!Array.isArray(raw))
|
|
69087
69367
|
return [];
|
|
@@ -69865,6 +70145,95 @@ Each entry is exactly one of:
|
|
|
69865
70145
|
}
|
|
69866
70146
|
}
|
|
69867
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
|
+
}
|
|
69868
70237
|
const totalFinal = scenarios.length + coverageGaps.length;
|
|
69869
70238
|
const rejectionRateFinal = totalFinal > 0 ? coverageGaps.length / totalFinal : 0;
|
|
69870
70239
|
if (rejectionRateFinal > 0.5 && totalFinal >= 3) {
|
|
@@ -73644,7 +74013,7 @@ var require_mobile_heal_postprocess = __commonJS({
|
|
|
73644
74013
|
// OpenAI-style secret key
|
|
73645
74014
|
];
|
|
73646
74015
|
var EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
|
|
73647
|
-
function
|
|
74016
|
+
function escapeRegExp2(value) {
|
|
73648
74017
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
73649
74018
|
}
|
|
73650
74019
|
function collectSecretVars(testVariables, minLen, keyRe) {
|
|
@@ -73663,7 +74032,7 @@ var require_mobile_heal_postprocess = __commonJS({
|
|
|
73663
74032
|
return text;
|
|
73664
74033
|
let out = text;
|
|
73665
74034
|
for (const { key, value } of collectSecretVars(testVariables, MIN_REDACT_LEN, REDACT_KEY_RE)) {
|
|
73666
|
-
out = out.replace(new RegExp(
|
|
74035
|
+
out = out.replace(new RegExp(escapeRegExp2(value), "g"), `{{${key}}}`);
|
|
73667
74036
|
}
|
|
73668
74037
|
for (const source of TOKEN_SOURCES) {
|
|
73669
74038
|
out = out.replace(new RegExp(source, "g"), "[REDACTED]");
|
|
@@ -76063,7 +76432,7 @@ var require_api_auth_headers = __commonJS({
|
|
|
76063
76432
|
"../runner-core/dist/services/api-auth-headers.js"(exports) {
|
|
76064
76433
|
"use strict";
|
|
76065
76434
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
76066
|
-
exports.parseApiAuthHeaders =
|
|
76435
|
+
exports.parseApiAuthHeaders = parseApiAuthHeaders2;
|
|
76067
76436
|
exports.resolveApiAuthHeaders = resolveApiAuthHeaders;
|
|
76068
76437
|
exports.isAppOriginRequest = isAppOriginRequest;
|
|
76069
76438
|
exports.buildOriginScopedApiFixtures = buildOriginScopedApiFixtures;
|
|
@@ -76076,7 +76445,7 @@ var require_api_auth_headers = __commonJS({
|
|
|
76076
76445
|
}
|
|
76077
76446
|
return false;
|
|
76078
76447
|
}
|
|
76079
|
-
function
|
|
76448
|
+
function parseApiAuthHeaders2(raw) {
|
|
76080
76449
|
if (!raw || typeof raw !== "string")
|
|
76081
76450
|
return void 0;
|
|
76082
76451
|
const trimmed = raw.trim();
|
|
@@ -76100,9 +76469,9 @@ var require_api_auth_headers = __commonJS({
|
|
|
76100
76469
|
function resolveApiAuthHeaders(input) {
|
|
76101
76470
|
if (input.explicit)
|
|
76102
76471
|
return input.explicit;
|
|
76103
|
-
if (
|
|
76472
|
+
if (input.standingHeadersMode === "none")
|
|
76104
76473
|
return void 0;
|
|
76105
|
-
return
|
|
76474
|
+
return parseApiAuthHeaders2(input.apiAuthHeadersVar);
|
|
76106
76475
|
}
|
|
76107
76476
|
function isAppOriginRequest(url, baseUrl) {
|
|
76108
76477
|
if (typeof url !== "string" || !baseUrl)
|
|
@@ -76170,6 +76539,126 @@ export const test = base.extend({
|
|
|
76170
76539
|
}
|
|
76171
76540
|
});
|
|
76172
76541
|
|
|
76542
|
+
// ../runner-core/dist/services/run-callback-proxy.js
|
|
76543
|
+
var require_run_callback_proxy = __commonJS({
|
|
76544
|
+
"../runner-core/dist/services/run-callback-proxy.js"(exports) {
|
|
76545
|
+
"use strict";
|
|
76546
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
76547
|
+
exports.startRunCallbackProxy = startRunCallbackProxy;
|
|
76548
|
+
var node_crypto_1 = __require("crypto");
|
|
76549
|
+
var node_http_1 = __require("http");
|
|
76550
|
+
var MAX_CALLBACK_BODY_BYTES = 5 * 1024 * 1024;
|
|
76551
|
+
var CALLBACK_TIMEOUT_MS = 3e4;
|
|
76552
|
+
function sendJson(res, status, body) {
|
|
76553
|
+
const payload = JSON.stringify(body);
|
|
76554
|
+
res.writeHead(status, {
|
|
76555
|
+
"cache-control": "no-store",
|
|
76556
|
+
"content-type": "application/json",
|
|
76557
|
+
"content-length": Buffer.byteLength(payload)
|
|
76558
|
+
});
|
|
76559
|
+
res.end(payload);
|
|
76560
|
+
}
|
|
76561
|
+
async function readRequestBody(req) {
|
|
76562
|
+
const chunks = [];
|
|
76563
|
+
let total = 0;
|
|
76564
|
+
for await (const chunk of req) {
|
|
76565
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
76566
|
+
total += buffer.length;
|
|
76567
|
+
if (total > MAX_CALLBACK_BODY_BYTES)
|
|
76568
|
+
throw new Error("callback body too large");
|
|
76569
|
+
chunks.push(buffer);
|
|
76570
|
+
}
|
|
76571
|
+
return Buffer.concat(chunks);
|
|
76572
|
+
}
|
|
76573
|
+
async function startRunCallbackProxy(input) {
|
|
76574
|
+
const upstream = new URL(input.serverUrl);
|
|
76575
|
+
if (upstream.protocol !== "http:" && upstream.protocol !== "https:") {
|
|
76576
|
+
throw new Error("callback server URL must use http or https");
|
|
76577
|
+
}
|
|
76578
|
+
const runPath = `/api/runner/runs/${encodeURIComponent(input.runId)}`;
|
|
76579
|
+
const allowedPaths = /* @__PURE__ */ new Set([`${runPath}/credential`, `${runPath}/auth-state`]);
|
|
76580
|
+
const token = (0, node_crypto_1.randomBytes)(32).toString("base64url");
|
|
76581
|
+
const authHeader = `Bearer ${token}`;
|
|
76582
|
+
const server3 = (0, node_http_1.createServer)(async (req, res) => {
|
|
76583
|
+
try {
|
|
76584
|
+
const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
76585
|
+
if (req.method !== "POST" || !allowedPaths.has(requestUrl.pathname) || requestUrl.search) {
|
|
76586
|
+
sendJson(res, 404, { error: "Callback route not available" });
|
|
76587
|
+
return;
|
|
76588
|
+
}
|
|
76589
|
+
if (req.headers.authorization !== authHeader) {
|
|
76590
|
+
sendJson(res, 401, { error: "Invalid callback capability" });
|
|
76591
|
+
return;
|
|
76592
|
+
}
|
|
76593
|
+
const body = await readRequestBody(req);
|
|
76594
|
+
let parsedBody;
|
|
76595
|
+
try {
|
|
76596
|
+
const parsed = JSON.parse(body.toString("utf8"));
|
|
76597
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
76598
|
+
throw new Error("invalid body");
|
|
76599
|
+
parsedBody = parsed;
|
|
76600
|
+
} catch {
|
|
76601
|
+
sendJson(res, 400, { error: "Callback body must be a JSON object" });
|
|
76602
|
+
return;
|
|
76603
|
+
}
|
|
76604
|
+
const upstreamBody = JSON.stringify({
|
|
76605
|
+
...parsedBody,
|
|
76606
|
+
...input.taskArn ? { taskArn: input.taskArn } : {},
|
|
76607
|
+
...input.claimEpoch !== void 0 ? { claimEpoch: input.claimEpoch } : {},
|
|
76608
|
+
...input.authConfigHash ? { authConfigHash: input.authConfigHash } : {}
|
|
76609
|
+
});
|
|
76610
|
+
const destination = new URL(requestUrl.pathname, upstream);
|
|
76611
|
+
const response = await fetch(destination, {
|
|
76612
|
+
method: "POST",
|
|
76613
|
+
headers: {
|
|
76614
|
+
Authorization: input.upstreamAuthHeader,
|
|
76615
|
+
"Content-Type": "application/json"
|
|
76616
|
+
},
|
|
76617
|
+
body: upstreamBody,
|
|
76618
|
+
redirect: "error",
|
|
76619
|
+
signal: AbortSignal.timeout(CALLBACK_TIMEOUT_MS)
|
|
76620
|
+
});
|
|
76621
|
+
const responseBody = Buffer.from(await response.arrayBuffer());
|
|
76622
|
+
res.writeHead(response.status, {
|
|
76623
|
+
"cache-control": "no-store",
|
|
76624
|
+
"content-type": response.headers.get("content-type") ?? "application/json",
|
|
76625
|
+
"content-length": responseBody.length
|
|
76626
|
+
});
|
|
76627
|
+
res.end(responseBody);
|
|
76628
|
+
} catch (error2) {
|
|
76629
|
+
const message = error2 instanceof Error && error2.message === "callback body too large" ? error2.message : "Callback forwarding failed";
|
|
76630
|
+
sendJson(res, message === "callback body too large" ? 413 : 502, { error: message });
|
|
76631
|
+
}
|
|
76632
|
+
});
|
|
76633
|
+
server3.on("clientError", (_error, socket) => socket.destroy());
|
|
76634
|
+
await new Promise((resolve2, reject) => {
|
|
76635
|
+
const onError = (error2) => reject(error2);
|
|
76636
|
+
server3.once("error", onError);
|
|
76637
|
+
server3.listen(0, "127.0.0.1", () => {
|
|
76638
|
+
server3.off("error", onError);
|
|
76639
|
+
resolve2();
|
|
76640
|
+
});
|
|
76641
|
+
});
|
|
76642
|
+
const address = server3.address();
|
|
76643
|
+
if (!address) {
|
|
76644
|
+
server3.close();
|
|
76645
|
+
throw new Error("callback proxy failed to bind");
|
|
76646
|
+
}
|
|
76647
|
+
let closed = false;
|
|
76648
|
+
return {
|
|
76649
|
+
serverUrl: `http://127.0.0.1:${address.port}`,
|
|
76650
|
+
authHeader,
|
|
76651
|
+
close: async () => {
|
|
76652
|
+
if (closed)
|
|
76653
|
+
return;
|
|
76654
|
+
closed = true;
|
|
76655
|
+
await new Promise((resolve2) => server3.close(() => resolve2()));
|
|
76656
|
+
}
|
|
76657
|
+
};
|
|
76658
|
+
}
|
|
76659
|
+
}
|
|
76660
|
+
});
|
|
76661
|
+
|
|
76173
76662
|
// ../runner-core/dist/services/playwright-native.js
|
|
76174
76663
|
var require_playwright_native = __commonJS({
|
|
76175
76664
|
"../runner-core/dist/services/playwright-native.js"(exports) {
|
|
@@ -76214,6 +76703,7 @@ var require_playwright_native = __commonJS({
|
|
|
76214
76703
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
76215
76704
|
exports.scrubHarRequestBody = exports.buildOriginScopedApiFixtures = exports.isAppOriginRequest = exports.resolveApiAuthHeaders = exports.parseApiAuthHeaders = void 0;
|
|
76216
76705
|
exports.executeTestViaNativePlaywright = executeTestViaNativePlaywright2;
|
|
76706
|
+
exports.buildNativePlaywrightChildEnv = buildNativePlaywrightChildEnv;
|
|
76217
76707
|
exports.parseJsonResults = parseJsonResults2;
|
|
76218
76708
|
exports.parseHarToApiSummary = parseHarToApiSummary;
|
|
76219
76709
|
var promises_1 = __require("fs/promises");
|
|
@@ -76225,6 +76715,7 @@ var require_playwright_native = __commonJS({
|
|
|
76225
76715
|
var api_call_redaction_js_1 = require_api_call_redaction();
|
|
76226
76716
|
var har_stream_js_1 = require_har_stream();
|
|
76227
76717
|
var api_auth_headers_js_1 = require_api_auth_headers();
|
|
76718
|
+
var run_callback_proxy_js_1 = require_run_callback_proxy();
|
|
76228
76719
|
var api_auth_headers_js_2 = require_api_auth_headers();
|
|
76229
76720
|
Object.defineProperty(exports, "parseApiAuthHeaders", { enumerable: true, get: function() {
|
|
76230
76721
|
return api_auth_headers_js_2.parseApiAuthHeaders;
|
|
@@ -76247,7 +76738,7 @@ var require_playwright_native = __commonJS({
|
|
|
76247
76738
|
var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
|
|
76248
76739
|
var PW_CLI = __require.resolve("@playwright/test/cli");
|
|
76249
76740
|
async function executeTestViaNativePlaywright2(options) {
|
|
76250
|
-
const { playwrightCode, baseUrl, testName, testVariables, storageStatePath, extraHttpHeaders, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
|
|
76741
|
+
const { playwrightCode, baseUrl, testName, testVariables, authStateCapturePath, storageStatePath, extraHttpHeaders, authStandingMode, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
|
|
76251
76742
|
const logs2 = [];
|
|
76252
76743
|
const screenshots = [];
|
|
76253
76744
|
logs2.push(`\u{1F3AD} Native Playwright starting`);
|
|
@@ -76257,15 +76748,19 @@ var require_playwright_native = __commonJS({
|
|
|
76257
76748
|
logs2.push("\u26A0\uFE0F WARNING: baseUrl is empty - test will likely fail with navigation errors");
|
|
76258
76749
|
}
|
|
76259
76750
|
let runDir = null;
|
|
76751
|
+
let standingNeedleMatcher = null;
|
|
76752
|
+
let callbackProxy;
|
|
76260
76753
|
try {
|
|
76261
76754
|
runDir = (0, path_1.join)(RUNS_DIR, (0, crypto_1.randomUUID)());
|
|
76262
76755
|
await (0, promises_1.mkdir)(runDir, { recursive: true });
|
|
76263
76756
|
const isApiTest2 = playwrightCode.includes("@api") || playwrightCode.includes("{ request }") && !playwrightCode.includes("{ page");
|
|
76757
|
+
const effectiveStandingMode = isApiTest2 ? authStandingMode ?? "legacy-if-configured" : "none";
|
|
76264
76758
|
const resolvedExtraHeaders = (0, api_auth_headers_js_1.resolveApiAuthHeaders)({
|
|
76265
76759
|
explicit: extraHttpHeaders,
|
|
76266
|
-
|
|
76760
|
+
standingHeadersMode: effectiveStandingMode,
|
|
76267
76761
|
apiAuthHeadersVar: testVariables?.API_AUTH_HEADERS
|
|
76268
76762
|
});
|
|
76763
|
+
standingNeedleMatcher = (0, shared_1.buildNeedleMatcher)(resolvedExtraHeaders ? Object.values(resolvedExtraHeaders) : []);
|
|
76269
76764
|
const cssDisableSetup = `
|
|
76270
76765
|
const __qaLiveBrowserStateFile = process.env.VALIDATE_LIVE_BROWSER_STATE_PATH;
|
|
76271
76766
|
let __qaLiveBrowserInterval: ReturnType<typeof setInterval> | null = null;
|
|
@@ -76460,6 +76955,18 @@ test.afterEach(async ({ page }, testInfo) => {
|
|
|
76460
76955
|
logs2.push("\u26A0 Could not find @playwright/test import - animation disabling NOT injected");
|
|
76461
76956
|
}
|
|
76462
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
|
+
}
|
|
76463
76970
|
let instrumentedCode;
|
|
76464
76971
|
if (isApiTest2) {
|
|
76465
76972
|
instrumentedCode = useScopedApiHeaders ? playwrightCode.replace(importMatch[0], importMatch[0].replace(/from\s+['"]@playwright\/test['"]/, "from './qa-api-fixtures'")) : playwrightCode;
|
|
@@ -76483,7 +76990,7 @@ test.afterEach(async ({ page }, testInfo) => {
|
|
|
76483
76990
|
const absHarPath = wantHar ? (0, path_1.join)(runDir, "session.har").replace(/\\/g, "/") : null;
|
|
76484
76991
|
const recordHarLine = absHarPath ? `recordHar: { path: ${JSON.stringify(absHarPath)}, mode: 'full' },` : "";
|
|
76485
76992
|
const storageStateLine = storageStatePath ? `storageState: ${JSON.stringify(storageStatePath.replace(/\\/g, "/"))},` : "";
|
|
76486
|
-
const extraHeadersLine =
|
|
76993
|
+
const extraHeadersLine = "";
|
|
76487
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'],";
|
|
76488
76995
|
const config = `
|
|
76489
76996
|
// Plain object export - no require('@playwright/test') needed.
|
|
@@ -76516,35 +77023,51 @@ module.exports = {
|
|
|
76516
77023
|
const configPath = (0, path_1.join)(runDir, "playwright.config.js");
|
|
76517
77024
|
await (0, promises_1.writeFile)(configPath, config);
|
|
76518
77025
|
const isolation = await (0, chromium_isolation_js_1.createChromiumIsolation)(runDir);
|
|
76519
|
-
const
|
|
76520
|
-
|
|
77026
|
+
const processClaimEpoch = Number(process.env.CLAIM_EPOCH);
|
|
77027
|
+
const callbackClaimEpoch = Number.isSafeInteger(validateContext?.claimEpoch) ? validateContext?.claimEpoch : Number.isSafeInteger(processClaimEpoch) && processClaimEpoch >= 0 ? processClaimEpoch : void 0;
|
|
77028
|
+
if (validateContext?.runId && validateContext.serverUrl && validateContext.authHeader) {
|
|
77029
|
+
try {
|
|
77030
|
+
callbackProxy = await (0, run_callback_proxy_js_1.startRunCallbackProxy)({
|
|
77031
|
+
runId: validateContext.runId,
|
|
77032
|
+
serverUrl: validateContext.serverUrl,
|
|
77033
|
+
upstreamAuthHeader: validateContext.authHeader,
|
|
77034
|
+
taskArn: process.env.VALIDATE_TASK_ARN,
|
|
77035
|
+
claimEpoch: callbackClaimEpoch,
|
|
77036
|
+
authConfigHash: process.env.VALIDATE_AUTH_CONFIG_HASH
|
|
77037
|
+
});
|
|
77038
|
+
} catch (error3) {
|
|
77039
|
+
logs2.push(`Callback proxy unavailable: ${error3 instanceof Error ? error3.message : String(error3)}`);
|
|
77040
|
+
}
|
|
77041
|
+
}
|
|
77042
|
+
const childTestVariables = { ...testVariables ?? {} };
|
|
77043
|
+
const trustedRuntimeEnv = {
|
|
76521
77044
|
...isolation.env,
|
|
76522
|
-
//
|
|
76523
|
-
//
|
|
76524
|
-
|
|
76525
|
-
|
|
76526
|
-
|
|
76527
|
-
|
|
76528
|
-
|
|
77045
|
+
// Generated callbacks receive only a random loopback capability. The global
|
|
77046
|
+
// runner bearer token and real control-plane URL stay in the parent process.
|
|
77047
|
+
...callbackProxy && validateContext?.runId ? {
|
|
77048
|
+
VALIDATE_RUN_ID: validateContext.runId,
|
|
77049
|
+
...callbackClaimEpoch !== void 0 ? { VALIDATE_CLAIM_EPOCH: String(callbackClaimEpoch) } : {},
|
|
77050
|
+
VALIDATE_SERVER_URL: callbackProxy.serverUrl,
|
|
77051
|
+
VALIDATE_RUNNER_AUTH: callbackProxy.authHeader
|
|
77052
|
+
} : {},
|
|
76529
77053
|
// Owning Fargate taskArn (a per-task constant set once at task boot in
|
|
76530
77054
|
// execute-one.ts; absent on the local/Railway runner). Read from
|
|
76531
77055
|
// process.env rather than validateContext since it's process-wide, not
|
|
76532
77056
|
// per-run. Lets persistCredential() prove ownership to /credential.
|
|
76533
77057
|
...process.env.VALIDATE_TASK_ARN ? { VALIDATE_TASK_ARN: process.env.VALIDATE_TASK_ARN } : {},
|
|
77058
|
+
...authStateCapturePath ? { AUTH_FILE: authStateCapturePath } : {},
|
|
76534
77059
|
...liveBrowserStatePath ? { VALIDATE_LIVE_BROWSER_STATE_PATH: liveBrowserStatePath } : {},
|
|
76535
|
-
//
|
|
76536
|
-
//
|
|
76537
|
-
//
|
|
76538
|
-
// this forward the flag reads as undefined in the test and the early publish is dead
|
|
76539
|
-
// code. Forwarded (not allowlisted) so it stays a per-run toggle; absent → inert.
|
|
77060
|
+
// Preserve an explicit shared-auth kill-switch in the child. Unset defaults on in
|
|
77061
|
+
// both parent and generated __vqaLogin code; "false" must still cross this process
|
|
77062
|
+
// boundary to disable the early publish POST. It remains a per-run toggle.
|
|
76540
77063
|
...process.env.SHARED_AUTH_INJECT ? { SHARED_AUTH_INJECT: process.env.SHARED_AUTH_INJECT } : {}
|
|
76541
77064
|
};
|
|
76542
|
-
delete
|
|
77065
|
+
delete childTestVariables.API_AUTH_HEADERS;
|
|
76543
77066
|
logs2.push("\u{1F680} Spawning playwright test...");
|
|
76544
77067
|
const stopLiveBrowserPolling = liveBrowserStatePath && liveBrowserHandle ? startLiveBrowserStatePolling(liveBrowserStatePath, liveBrowserHandle, onLiveBrowserSnapshot) : null;
|
|
76545
77068
|
let playResult;
|
|
76546
77069
|
try {
|
|
76547
|
-
playResult = await runPlaywright(configPath,
|
|
77070
|
+
playResult = await runPlaywright(configPath, childTestVariables, trustedRuntimeEnv);
|
|
76548
77071
|
} finally {
|
|
76549
77072
|
await stopLiveBrowserPolling?.();
|
|
76550
77073
|
}
|
|
@@ -76605,7 +77128,7 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
|
|
|
76605
77128
|
logs2.push(`\u26A0 HAR parse skipped: ${msg.slice(0, 200)}`);
|
|
76606
77129
|
}
|
|
76607
77130
|
}
|
|
76608
|
-
|
|
77131
|
+
const nativeResult = {
|
|
76609
77132
|
passed,
|
|
76610
77133
|
stepResults,
|
|
76611
77134
|
screenshots,
|
|
@@ -76614,20 +77137,27 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
|
|
|
76614
77137
|
harApiCalls,
|
|
76615
77138
|
logs: logs2.join("\n"),
|
|
76616
77139
|
error: passed ? void 0 : error2,
|
|
76617
|
-
suspiciousPass
|
|
77140
|
+
suspiciousPass,
|
|
77141
|
+
authDelivery
|
|
76618
77142
|
};
|
|
77143
|
+
if (!standingNeedleMatcher)
|
|
77144
|
+
return nativeResult;
|
|
77145
|
+
return (0, shared_1.redactNeedlesDeep)(nativeResult, standingNeedleMatcher);
|
|
76619
77146
|
} catch (fatalErr) {
|
|
76620
77147
|
const msg = fatalErr instanceof Error ? fatalErr.message : String(fatalErr);
|
|
76621
77148
|
logs2.push(`
|
|
76622
77149
|
\u{1F4A5} Fatal error: ${msg}`);
|
|
76623
|
-
|
|
77150
|
+
const fatalResult = {
|
|
76624
77151
|
passed: false,
|
|
76625
77152
|
stepResults: [],
|
|
76626
77153
|
screenshots,
|
|
76627
77154
|
logs: logs2.join("\n"),
|
|
76628
77155
|
error: msg
|
|
76629
77156
|
};
|
|
77157
|
+
return standingNeedleMatcher ? (0, shared_1.redactNeedlesDeep)(fatalResult, standingNeedleMatcher) : fatalResult;
|
|
76630
77158
|
} finally {
|
|
77159
|
+
await callbackProxy?.close().catch(() => {
|
|
77160
|
+
});
|
|
76631
77161
|
if (runDir) {
|
|
76632
77162
|
await (0, promises_1.rm)(runDir, { recursive: true, force: true }).catch(() => {
|
|
76633
77163
|
});
|
|
@@ -76679,9 +77209,9 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
|
|
|
76679
77209
|
stopped = true;
|
|
76680
77210
|
};
|
|
76681
77211
|
}
|
|
76682
|
-
function runPlaywright(configPath,
|
|
77212
|
+
function runPlaywright(configPath, testVariables, trustedRuntimeEnv) {
|
|
76683
77213
|
return new Promise((resolvePromise) => {
|
|
76684
|
-
const sandboxedEnv = (
|
|
77214
|
+
const sandboxedEnv = buildNativePlaywrightChildEnv(process.env, testVariables, trustedRuntimeEnv);
|
|
76685
77215
|
const proc = (0, spawn_with_tree_kill_js_1.spawnDetachedGroup)(process.execPath, [PW_CLI, "test", "--config", configPath], {
|
|
76686
77216
|
cwd: PACKAGE_DIR,
|
|
76687
77217
|
env: sandboxedEnv,
|
|
@@ -76724,6 +77254,12 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
|
|
|
76724
77254
|
});
|
|
76725
77255
|
});
|
|
76726
77256
|
}
|
|
77257
|
+
function buildNativePlaywrightChildEnv(processEnv, testVariables, trustedRuntimeEnv) {
|
|
77258
|
+
return {
|
|
77259
|
+
...(0, shared_1.buildSandboxedEnv)(processEnv, testVariables),
|
|
77260
|
+
...trustedRuntimeEnv ?? {}
|
|
77261
|
+
};
|
|
77262
|
+
}
|
|
76727
77263
|
async function collectScreenshots(dir, screenshots) {
|
|
76728
77264
|
let entries;
|
|
76729
77265
|
try {
|
|
@@ -80445,10 +80981,12 @@ var require_auth_recipe = __commonJS({
|
|
|
80445
80981
|
exports.upsertAuthBlock = upsertAuthBlock;
|
|
80446
80982
|
exports.buildHealAuthStamp = buildHealAuthStamp;
|
|
80447
80983
|
var crypto_1 = __require("crypto");
|
|
80984
|
+
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
80448
80985
|
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
80449
|
-
exports.AUTH_MARKER_VERSION = "
|
|
80986
|
+
exports.AUTH_MARKER_VERSION = "v3";
|
|
80450
80987
|
var START_MARKER = `// >>> VALIDATE_QA_AUTH ${exports.AUTH_MARKER_VERSION} \u2014 generated, do not edit >>>`;
|
|
80451
80988
|
var END_MARKER = `// <<< VALIDATE_QA_AUTH ${exports.AUTH_MARKER_VERSION} <<<`;
|
|
80989
|
+
var SHARED_AUTH_RUNTIME_ENABLED = `!/^false$/i.test((process.env.SHARED_AUTH_INJECT ?? '').trim())`;
|
|
80452
80990
|
var AUTH_REGION_RE = /\/\/ >>> VALIDATE_QA_AUTH[^\n]*\r?\n[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH[^\n]*\r?\n?/g;
|
|
80453
80991
|
function computeRecipeHash(input) {
|
|
80454
80992
|
const canonical = JSON.stringify({
|
|
@@ -80526,16 +81064,18 @@ var require_auth_recipe = __commonJS({
|
|
|
80526
81064
|
if (opts?.warmGuard) {
|
|
80527
81065
|
const warmLanding = recipe.redirectRoute ?? (recipe.authProof.kind === "url" ? recipe.authProof.route : recipe.preNavRoute);
|
|
80528
81066
|
const loginRoute = recipe.route ?? recipe.preNavRoute;
|
|
80529
|
-
lines.push(`
|
|
81067
|
+
lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED}) {`);
|
|
81068
|
+
lines.push(` await page.goto(${JSON.stringify(warmLanding)}, { waitUntil: 'domcontentloaded' }).catch(() => {});`);
|
|
80530
81069
|
if (recipe.authProof.kind === "url") {
|
|
80531
81070
|
const pred = urlProofPredicateExpr(recipe.authProof.route);
|
|
80532
|
-
lines.push(`
|
|
80533
|
-
lines.push(`
|
|
81071
|
+
lines.push(` await page.waitForURL((u) => !(${pred})(u), { timeout: 2000 }).catch(() => {});`);
|
|
81072
|
+
lines.push(` if ((${pred})(new URL(page.url())) && !(await page.locator('input[type="password"]').first().isVisible().catch(() => false))) return;`);
|
|
80534
81073
|
} else if (loginRoute) {
|
|
80535
|
-
lines.push(`
|
|
81074
|
+
lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false) && new URL(page.url()).pathname !== new URL(${JSON.stringify(loginRoute)}, page.url()).pathname) return;`);
|
|
80536
81075
|
} else {
|
|
80537
|
-
lines.push(`
|
|
81076
|
+
lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false)) return;`);
|
|
80538
81077
|
}
|
|
81078
|
+
lines.push(` }`);
|
|
80539
81079
|
}
|
|
80540
81080
|
lines.push(` await page.goto(${JSON.stringify(recipe.preNavRoute)}, { waitUntil: 'domcontentloaded' });`);
|
|
80541
81081
|
for (const cmd of recipe.preNavCmds)
|
|
@@ -80548,11 +81088,11 @@ var require_auth_recipe = __commonJS({
|
|
|
80548
81088
|
lines.push(renderAuthUrlWaitLine(recipe.redirectRoute));
|
|
80549
81089
|
lines.push(` ${renderAuthProofAssertion(recipe.authProof)}`);
|
|
80550
81090
|
if (opts?.warmGuard) {
|
|
80551
|
-
lines.push(` if (process.env.AUTH_FILE) await page.context().storageState({ path: process.env.AUTH_FILE });`);
|
|
80552
|
-
lines.push(` if (process.env.
|
|
81091
|
+
lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED} && process.env.AUTH_FILE) await page.context().storageState({ path: process.env.AUTH_FILE });`);
|
|
81092
|
+
lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED} && process.env.VALIDATE_TASK_ARN && process.env.VALIDATE_CLAIM_EPOCH && process.env.VALIDATE_SERVER_URL && process.env.VALIDATE_RUN_ID) {`);
|
|
80553
81093
|
lines.push(` try {`);
|
|
80554
81094
|
lines.push(` const __vqaState = await page.context().storageState();`);
|
|
80555
|
-
lines.push(` await page.request.post(process.env.VALIDATE_SERVER_URL + '/api/runner/runs/' + process.env.VALIDATE_RUN_ID + '/auth-state', { headers: { Authorization: process.env.VALIDATE_RUNNER_AUTH || '' }, data: { storageState: JSON.stringify(__vqaState), taskArn: process.env.VALIDATE_TASK_ARN }, timeout: 10000 });`);
|
|
81095
|
+
lines.push(` await page.request.post(process.env.VALIDATE_SERVER_URL + '/api/runner/runs/' + process.env.VALIDATE_RUN_ID + '/auth-state', { headers: { Authorization: process.env.VALIDATE_RUNNER_AUTH || '' }, data: { storageState: JSON.stringify(__vqaState), taskArn: process.env.VALIDATE_TASK_ARN, claimEpoch: Number(process.env.VALIDATE_CLAIM_EPOCH) }, timeout: 10000 });`);
|
|
80556
81096
|
lines.push(` } catch {}`);
|
|
80557
81097
|
lines.push(` }`);
|
|
80558
81098
|
}
|
|
@@ -80564,6 +81104,8 @@ var require_auth_recipe = __commonJS({
|
|
|
80564
81104
|
"AUTH_FILE",
|
|
80565
81105
|
"SHARED_AUTH_INJECT",
|
|
80566
81106
|
"VALIDATE_RUN_ID",
|
|
81107
|
+
"VALIDATE_CLAIM_EPOCH",
|
|
81108
|
+
"VALIDATE_AUTH_CONFIG_HASH",
|
|
80567
81109
|
"VALIDATE_TASK_ARN",
|
|
80568
81110
|
"VALIDATE_RUNNER_AUTH",
|
|
80569
81111
|
"VALIDATE_SERVER_URL"
|
|
@@ -80736,7 +81278,7 @@ ${tail}`;
|
|
|
80736
81278
|
let result = stripAuthRegions(code);
|
|
80737
81279
|
result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
|
|
80738
81280
|
result = ensureExpectImported(result);
|
|
80739
|
-
const warmGuard =
|
|
81281
|
+
const warmGuard = (0, shared_1.isSharedAuthInjectEnabled)();
|
|
80740
81282
|
result = insertAfterPwImport(result, renderAuthFn(ctx.recipe, { warmGuard }));
|
|
80741
81283
|
result = ensureLoginCall(result);
|
|
80742
81284
|
const n = countAuthRegions(result);
|
|
@@ -85716,7 +86258,7 @@ ${elementContext}${formContext}${observationContext}${interactionContext}${lowCo
|
|
|
85716
86258
|
}
|
|
85717
86259
|
return result;
|
|
85718
86260
|
}
|
|
85719
|
-
function
|
|
86261
|
+
function escapeRegExp2(value) {
|
|
85720
86262
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
85721
86263
|
}
|
|
85722
86264
|
function buildSensitiveEnvReplacements(testVariables) {
|
|
@@ -85738,7 +86280,7 @@ ${elementContext}${formContext}${observationContext}${interactionContext}${lowCo
|
|
|
85738
86280
|
for (const replacement of buildSensitiveEnvReplacements(testVariables)) {
|
|
85739
86281
|
if (replacement.value.includes("\n") || replacement.value.includes("\r"))
|
|
85740
86282
|
continue;
|
|
85741
|
-
const literalPattern = new RegExp(`(['"\`])${
|
|
86283
|
+
const literalPattern = new RegExp(`(['"\`])${escapeRegExp2(replacement.value)}\\1`, "g");
|
|
85742
86284
|
result = result.replace(literalPattern, replacement.envRef);
|
|
85743
86285
|
}
|
|
85744
86286
|
return result;
|
|
@@ -85866,9 +86408,9 @@ ${testOpen}`;
|
|
|
85866
86408
|
basePath: options?.basePath
|
|
85867
86409
|
});
|
|
85868
86410
|
const leakedSensitiveKeys = findSensitiveLiteralLeaks(playwrightCode, options?.sensitiveEnvMap);
|
|
85869
|
-
const authGuardStamped = !!options?.authRecipe && rawTags.includes("@needs-auth") && !rawTags.includes("@auth") && process
|
|
85870
|
-
const baseTags = ensureDiscoveredTag(Array.isArray(test.tags) ? test.tags : ["@discovered", "@regression"]);
|
|
85871
|
-
const tags = authGuardStamped
|
|
86411
|
+
const authGuardStamped = !!options?.authRecipe && rawTags.includes("@needs-auth") && !rawTags.includes("@auth") && (0, shared_1.isSharedAuthInjectEnabled)() && /\/\/ >>> VALIDATE_QA_AUTH v\d+\b[\s\S]*?process\.env\.AUTH_FILE[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH v\d+\b/.test(playwrightCode);
|
|
86412
|
+
const baseTags = ensureDiscoveredTag(Array.isArray(test.tags) ? test.tags : ["@discovered", "@regression"]).filter((tag) => tag !== "@auth-guarded");
|
|
86413
|
+
const tags = authGuardStamped ? [...baseTags, "@auth-guarded"] : baseTags;
|
|
85872
86414
|
return {
|
|
85873
86415
|
name: String(test.name || "Unnamed test").slice(0, 200),
|
|
85874
86416
|
description: String(test.description || test.name || "").slice(0, 500),
|
|
@@ -99683,7 +100225,7 @@ ${lanes.join("\n")}
|
|
|
99683
100225
|
return process.memoryUsage().heapUsed;
|
|
99684
100226
|
},
|
|
99685
100227
|
getFileSize(path) {
|
|
99686
|
-
const stat =
|
|
100228
|
+
const stat = statSync(path);
|
|
99687
100229
|
if (stat == null ? void 0 : stat.isFile()) {
|
|
99688
100230
|
return stat.size;
|
|
99689
100231
|
}
|
|
@@ -99727,7 +100269,7 @@ ${lanes.join("\n")}
|
|
|
99727
100269
|
}
|
|
99728
100270
|
};
|
|
99729
100271
|
return nodeSystem;
|
|
99730
|
-
function
|
|
100272
|
+
function statSync(path) {
|
|
99731
100273
|
try {
|
|
99732
100274
|
return _fs.statSync(path, statSyncOptions);
|
|
99733
100275
|
} catch {
|
|
@@ -99786,7 +100328,7 @@ ${lanes.join("\n")}
|
|
|
99786
100328
|
activeSession.post("Profiler.stop", (err, { profile }) => {
|
|
99787
100329
|
var _a;
|
|
99788
100330
|
if (!err) {
|
|
99789
|
-
if ((_a =
|
|
100331
|
+
if ((_a = statSync(profilePath)) == null ? void 0 : _a.isDirectory()) {
|
|
99790
100332
|
profilePath = _path.join(profilePath, `${(/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-")}+P${process.pid}.cpuprofile`);
|
|
99791
100333
|
}
|
|
99792
100334
|
try {
|
|
@@ -99906,7 +100448,7 @@ ${lanes.join("\n")}
|
|
|
99906
100448
|
let stat;
|
|
99907
100449
|
if (typeof dirent === "string" || dirent.isSymbolicLink()) {
|
|
99908
100450
|
const name = combinePaths(path, entry);
|
|
99909
|
-
stat =
|
|
100451
|
+
stat = statSync(name);
|
|
99910
100452
|
if (!stat) {
|
|
99911
100453
|
continue;
|
|
99912
100454
|
}
|
|
@@ -99930,7 +100472,7 @@ ${lanes.join("\n")}
|
|
|
99930
100472
|
return matchFiles(path, extensions, excludes, includes, useCaseSensitiveFileNames2, process.cwd(), depth, getAccessibleFileSystemEntries, realpath);
|
|
99931
100473
|
}
|
|
99932
100474
|
function fileSystemEntryExists(path, entryKind) {
|
|
99933
|
-
const stat =
|
|
100475
|
+
const stat = statSync(path);
|
|
99934
100476
|
if (!stat) {
|
|
99935
100477
|
return false;
|
|
99936
100478
|
}
|
|
@@ -99972,7 +100514,7 @@ ${lanes.join("\n")}
|
|
|
99972
100514
|
}
|
|
99973
100515
|
function getModifiedTime3(path) {
|
|
99974
100516
|
var _a;
|
|
99975
|
-
return (_a =
|
|
100517
|
+
return (_a = statSync(path)) == null ? void 0 : _a.mtime;
|
|
99976
100518
|
}
|
|
99977
100519
|
function setModifiedTime(path, time) {
|
|
99978
100520
|
try {
|
|
@@ -254994,7 +255536,7 @@ ${newComment.split("\n").map((c) => ` * ${c}`).join("\n")}
|
|
|
254994
255536
|
}
|
|
254995
255537
|
}
|
|
254996
255538
|
return result;
|
|
254997
|
-
function
|
|
255539
|
+
function escapeRegExp2(str) {
|
|
254998
255540
|
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
|
|
254999
255541
|
}
|
|
255000
255542
|
function getTodoCommentsRegExp() {
|
|
@@ -255002,7 +255544,7 @@ ${newComment.split("\n").map((c) => ` * ${c}`).join("\n")}
|
|
|
255002
255544
|
const multiLineCommentStart = /(?:\/\*+\s*)/.source;
|
|
255003
255545
|
const anyNumberOfSpacesAndAsterisksAtStartOfLine = /(?:^(?:\s|\*)*)/.source;
|
|
255004
255546
|
const preamble = "(" + anyNumberOfSpacesAndAsterisksAtStartOfLine + "|" + singleLineCommentStart + "|" + multiLineCommentStart + ")";
|
|
255005
|
-
const literals = "(?:" + map2(descriptors, (d) => "(" +
|
|
255547
|
+
const literals = "(?:" + map2(descriptors, (d) => "(" + escapeRegExp2(d.text) + ")").join("|") + ")";
|
|
255006
255548
|
const endOfLineOrEndOfComment = /(?:$|\*\/)/.source;
|
|
255007
255549
|
const messageRemainder = /(?:.*?)/.source;
|
|
255008
255550
|
const messagePortion = "(" + literals + messageRemainder + ")";
|
|
@@ -303262,7 +303804,7 @@ ${(verifyResult.logs ?? "").slice(0, 2e3)}`;
|
|
|
303262
303804
|
confidence: "low"
|
|
303263
303805
|
};
|
|
303264
303806
|
}
|
|
303265
|
-
function
|
|
303807
|
+
function escapeRegExp2(s) {
|
|
303266
303808
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
303267
303809
|
}
|
|
303268
303810
|
function tryDeterministicStrictModeFix(code, error2) {
|
|
@@ -303285,10 +303827,10 @@ ${(verifyResult.logs ?? "").slice(0, 2e3)}`;
|
|
|
303285
303827
|
const [, , role, , name, restOfOptions] = violated;
|
|
303286
303828
|
if (/exact\s*:\s*true/.test(restOfOptions))
|
|
303287
303829
|
return null;
|
|
303288
|
-
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*\\)`);
|
|
303289
303831
|
if (!akaPattern.test(error2))
|
|
303290
303832
|
return null;
|
|
303291
|
-
const codePattern = new RegExp(`(getByRole\\(\\s*(['"\`])${
|
|
303833
|
+
const codePattern = new RegExp(`(getByRole\\(\\s*(['"\`])${escapeRegExp2(role)}\\2\\s*,\\s*\\{[^}]*?name:\\s*(['"\`])${escapeRegExp2(name)}\\3)([^}]*\\})`, "g");
|
|
303292
303834
|
let modified = false;
|
|
303293
303835
|
const patched = code.replace(codePattern, (full, prefix, _q1, _q2, suffix) => {
|
|
303294
303836
|
if (/exact\s*:\s*true/.test(suffix))
|
|
@@ -303305,11 +303847,11 @@ ${(verifyResult.logs ?? "").slice(0, 2e3)}`;
|
|
|
303305
303847
|
const [, , text, optionsBlock] = violated;
|
|
303306
303848
|
if (optionsBlock && /exact\s*:\s*true/.test(optionsBlock))
|
|
303307
303849
|
return null;
|
|
303308
|
-
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*\\)`);
|
|
303309
303851
|
if (!akaPattern.test(error2))
|
|
303310
303852
|
return null;
|
|
303311
|
-
const noOptsPattern = new RegExp(`getByText\\(\\s*(['"\`])${
|
|
303312
|
-
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");
|
|
303313
303855
|
let modified = false;
|
|
303314
303856
|
let patched = code.replace(noOptsPattern, (_full, q) => {
|
|
303315
303857
|
modified = true;
|
|
@@ -308483,6 +309025,7 @@ var require_regression_runner = __commonJS({
|
|
|
308483
309025
|
var chromium_isolation_js_1 = require_chromium_isolation();
|
|
308484
309026
|
var browser_config_js_1 = require_browser_config();
|
|
308485
309027
|
var spawn_with_tree_kill_js_1 = require_spawn_with_tree_kill();
|
|
309028
|
+
var run_callback_proxy_js_1 = require_run_callback_proxy();
|
|
308486
309029
|
var SUITE_SPAWN_TIMEOUT_MS = 85 * 60 * 1e3;
|
|
308487
309030
|
var PACKAGE_DIR = (0, path_1.resolve)(__dirname, "..", "..");
|
|
308488
309031
|
var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
|
|
@@ -308497,6 +309040,7 @@ var require_regression_runner = __commonJS({
|
|
|
308497
309040
|
const tarPath = (0, path_1.join)(RUNS_DIR, `regression-${runId}.tar.gz`);
|
|
308498
309041
|
const startTime = Date.now();
|
|
308499
309042
|
let logs2 = "";
|
|
309043
|
+
let callbackProxy;
|
|
308500
309044
|
try {
|
|
308501
309045
|
await (0, promises_1.mkdir)(extractDir, { recursive: true });
|
|
308502
309046
|
const tarballUrl = `https://api.github.com/repos/${repoOwner}/${repoName}/tarball/${repoBranch}`;
|
|
@@ -308526,19 +309070,40 @@ var require_regression_runner = __commonJS({
|
|
|
308526
309070
|
await (0, promises_1.writeFile)((0, path_1.join)(extractDir, ".env"), envContent, "utf-8");
|
|
308527
309071
|
const jsonResultsPath = (0, path_1.join)(extractDir, "test-results", "results.json");
|
|
308528
309072
|
const isolation = await (0, chromium_isolation_js_1.createChromiumIsolation)(extractDir);
|
|
309073
|
+
const processClaimEpoch = Number(process.env.CLAIM_EPOCH);
|
|
309074
|
+
const callbackClaimEpoch = Number.isSafeInteger(opts.claimEpoch) ? opts.claimEpoch : Number.isSafeInteger(processClaimEpoch) && processClaimEpoch >= 0 ? processClaimEpoch : void 0;
|
|
309075
|
+
if (opts.validateServerUrl && opts.validateRunnerAuthHeader) {
|
|
309076
|
+
try {
|
|
309077
|
+
callbackProxy = await (0, run_callback_proxy_js_1.startRunCallbackProxy)({
|
|
309078
|
+
runId,
|
|
309079
|
+
serverUrl: opts.validateServerUrl,
|
|
309080
|
+
upstreamAuthHeader: opts.validateRunnerAuthHeader,
|
|
309081
|
+
taskArn: process.env.VALIDATE_TASK_ARN,
|
|
309082
|
+
claimEpoch: callbackClaimEpoch,
|
|
309083
|
+
authConfigHash: process.env.VALIDATE_AUTH_CONFIG_HASH
|
|
309084
|
+
});
|
|
309085
|
+
} catch (error2) {
|
|
309086
|
+
logs2 += `Callback proxy unavailable: ${error2 instanceof Error ? error2.message : String(error2)}
|
|
309087
|
+
`;
|
|
309088
|
+
}
|
|
309089
|
+
}
|
|
308529
309090
|
const runEnv = {
|
|
308530
309091
|
...(0, shared_1.buildSandboxedEnv)(process.env, testVariables),
|
|
308531
309092
|
...isolation.env,
|
|
308532
309093
|
CI: "true",
|
|
308533
309094
|
PLAYWRIGHT_JSON_OUTPUT_NAME: jsonResultsPath,
|
|
308534
|
-
//
|
|
308535
|
-
//
|
|
308536
|
-
|
|
308537
|
-
|
|
308538
|
-
|
|
309095
|
+
// The suite receives a random run-scoped loopback callback, never the
|
|
309096
|
+
// global runner bearer credential.
|
|
309097
|
+
...callbackProxy ? {
|
|
309098
|
+
VALIDATE_RUN_ID: runId,
|
|
309099
|
+
...callbackClaimEpoch !== void 0 ? { VALIDATE_CLAIM_EPOCH: String(callbackClaimEpoch) } : {},
|
|
309100
|
+
VALIDATE_SERVER_URL: callbackProxy.serverUrl,
|
|
309101
|
+
VALIDATE_RUNNER_AUTH: callbackProxy.authHeader
|
|
309102
|
+
} : {},
|
|
308539
309103
|
// Owning Fargate taskArn (per-task constant; unset off-Fargate). Forwarded
|
|
308540
309104
|
// so persistCredential() can prove ownership to the /credential fence.
|
|
308541
|
-
...process.env.VALIDATE_TASK_ARN ? { VALIDATE_TASK_ARN: process.env.VALIDATE_TASK_ARN } : {}
|
|
309105
|
+
...process.env.VALIDATE_TASK_ARN ? { VALIDATE_TASK_ARN: process.env.VALIDATE_TASK_ARN } : {},
|
|
309106
|
+
...process.env.SHARED_AUTH_INJECT ? { SHARED_AUTH_INJECT: process.env.SHARED_AUTH_INJECT } : {}
|
|
308542
309107
|
};
|
|
308543
309108
|
logs2 += `Running playwright --project=${opts.suiteProject ?? "suite"}...
|
|
308544
309109
|
`;
|
|
@@ -308567,6 +309132,8 @@ var require_regression_runner = __commonJS({
|
|
|
308567
309132
|
error: String(err)
|
|
308568
309133
|
};
|
|
308569
309134
|
} finally {
|
|
309135
|
+
await callbackProxy?.close().catch(() => {
|
|
309136
|
+
});
|
|
308570
309137
|
(0, promises_1.rm)(extractDir, { recursive: true, force: true }).catch(() => {
|
|
308571
309138
|
});
|
|
308572
309139
|
await (0, browser_config_js_1.cleanupPlaywrightTempArtifacts)().catch(() => {
|
|
@@ -321241,9 +321808,9 @@ var require_tool_ledger = __commonJS({
|
|
|
321241
321808
|
const ledgerPath = (0, node_path_1.join)(runDir, "discovery-events.jsonl");
|
|
321242
321809
|
let checkpointIndex = 0;
|
|
321243
321810
|
let headerWritten = false;
|
|
321244
|
-
const
|
|
321811
|
+
const escapeRegExp2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
321245
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]) => ({
|
|
321246
|
-
re: new RegExp(
|
|
321813
|
+
re: new RegExp(escapeRegExp2(value), "g"),
|
|
321247
321814
|
replacement: `[REDACTED_${envName}]`
|
|
321248
321815
|
}));
|
|
321249
321816
|
const scrubCredentials = (text) => {
|
|
@@ -323403,6 +323970,7 @@ Explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elemen
|
|
|
323403
323970
|
testBudget
|
|
323404
323971
|
}, {
|
|
323405
323972
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
323973
|
+
gateModelOverride: ctx.planGateModel,
|
|
323406
323974
|
onAICall
|
|
323407
323975
|
});
|
|
323408
323976
|
logs2.push(...planResult.logs);
|
|
@@ -323981,6 +324549,7 @@ Plan (reused): ${resumeSavedScenarios.length} scenarios from the paused run.`);
|
|
|
323981
324549
|
testBudget: featureTestBudget
|
|
323982
324550
|
}, {
|
|
323983
324551
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
324552
|
+
gateModelOverride: ctx.planGateModel,
|
|
323984
324553
|
onAICall
|
|
323985
324554
|
});
|
|
323986
324555
|
logs2.push(...planResult.logs);
|
|
@@ -324810,6 +325379,455 @@ var require_resource_governor = __commonJS({
|
|
|
324810
325379
|
}
|
|
324811
325380
|
});
|
|
324812
325381
|
|
|
325382
|
+
// ../runner-core/dist/services/local-auth-state.js
|
|
325383
|
+
var require_local_auth_state = __commonJS({
|
|
325384
|
+
"../runner-core/dist/services/local-auth-state.js"(exports) {
|
|
325385
|
+
"use strict";
|
|
325386
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
325387
|
+
exports.LOCAL_AUTH_STATE_LOCK_ABSOLUTE_STALE_MS = exports.LOCAL_AUTH_STATE_LOCK_STALE_MS = exports.LOCAL_AUTH_STATE_CLEANUP_INTERVAL_MS = exports.LOCAL_AUTH_STATE_WAIT_MS = exports.LOCAL_AUTH_STATE_MAX_AGE_MS = void 0;
|
|
325388
|
+
exports.shouldSkipWarmAuthSetup = shouldSkipWarmAuthSetup2;
|
|
325389
|
+
exports.shouldSkipDisabledAutomaticAuthSetup = shouldSkipDisabledAutomaticAuthSetup2;
|
|
325390
|
+
exports.localAuthStateFingerprint = localAuthStateFingerprint;
|
|
325391
|
+
exports.ensurePrivateDirectory = ensurePrivateDirectory2;
|
|
325392
|
+
exports.isValidStorageState = isValidStorageState;
|
|
325393
|
+
exports.isValidStorageStateJson = isValidStorageStateJson;
|
|
325394
|
+
exports.isValidLocalAuthStateFile = isValidLocalAuthStateFile;
|
|
325395
|
+
exports.cleanupLocalAuthStateFiles = cleanupLocalAuthStateFiles;
|
|
325396
|
+
exports.cleanupExpiredLocalAuthStateFiles = cleanupExpiredLocalAuthStateFiles2;
|
|
325397
|
+
exports.localAuthStatePaths = localAuthStatePaths2;
|
|
325398
|
+
exports.getValidLocalAuthState = getValidLocalAuthState2;
|
|
325399
|
+
exports.waitForValidLocalAuthState = waitForValidLocalAuthState;
|
|
325400
|
+
exports.waitForLocalAuthStateProducer = waitForLocalAuthStateProducer2;
|
|
325401
|
+
exports.promoteLocalAuthStateCapture = promoteLocalAuthStateCapture2;
|
|
325402
|
+
exports.discardLocalAuthStateCapture = discardLocalAuthStateCapture2;
|
|
325403
|
+
exports.acquireLocalAuthStateLock = acquireLocalAuthStateLock2;
|
|
325404
|
+
exports.localAuthStateFilename = localAuthStateFilename;
|
|
325405
|
+
var node_fs_1 = __require("fs");
|
|
325406
|
+
var node_crypto_1 = __require("crypto");
|
|
325407
|
+
var node_path_1 = __require("path");
|
|
325408
|
+
var auth_recipe_js_1 = require_auth_recipe();
|
|
325409
|
+
exports.LOCAL_AUTH_STATE_MAX_AGE_MS = 60 * 60 * 1e3;
|
|
325410
|
+
exports.LOCAL_AUTH_STATE_WAIT_MS = 3 * 60 * 1e3;
|
|
325411
|
+
exports.LOCAL_AUTH_STATE_CLEANUP_INTERVAL_MS = 15 * 60 * 1e3;
|
|
325412
|
+
exports.LOCAL_AUTH_STATE_LOCK_STALE_MS = 2 * 60 * 1e3;
|
|
325413
|
+
exports.LOCAL_AUTH_STATE_LOCK_ABSOLUTE_STALE_MS = 30 * 60 * 1e3;
|
|
325414
|
+
function shouldSkipWarmAuthSetup2(authForceRun, sharedAuthEnabled, cacheWarm) {
|
|
325415
|
+
return sharedAuthEnabled && authForceRun !== true && cacheWarm;
|
|
325416
|
+
}
|
|
325417
|
+
function shouldSkipDisabledAutomaticAuthSetup2(authForceRun, authVerify, sharedAuthEnabled) {
|
|
325418
|
+
return !sharedAuthEnabled && authForceRun !== true && authVerify !== true;
|
|
325419
|
+
}
|
|
325420
|
+
function normalizeOrigin(value) {
|
|
325421
|
+
try {
|
|
325422
|
+
return new URL(value).origin.toLowerCase();
|
|
325423
|
+
} catch {
|
|
325424
|
+
return value.trim().toLowerCase();
|
|
325425
|
+
}
|
|
325426
|
+
}
|
|
325427
|
+
function normalizeAppUrl(value) {
|
|
325428
|
+
try {
|
|
325429
|
+
const url = new URL(value);
|
|
325430
|
+
url.hash = "";
|
|
325431
|
+
return `${url.origin.toLowerCase()}${url.pathname}${url.search}`;
|
|
325432
|
+
} catch {
|
|
325433
|
+
return value.trim();
|
|
325434
|
+
}
|
|
325435
|
+
}
|
|
325436
|
+
function safeSegment(value, fallback) {
|
|
325437
|
+
const safe = value.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 120);
|
|
325438
|
+
return safe || fallback;
|
|
325439
|
+
}
|
|
325440
|
+
function stableVariables(testVariables) {
|
|
325441
|
+
return Object.entries(testVariables).filter(([key]) => !(0, auth_recipe_js_1.isReservedRunnerEnvVar)(key)).sort(([a], [b]) => a.localeCompare(b));
|
|
325442
|
+
}
|
|
325443
|
+
function localAuthStateFingerprint(input) {
|
|
325444
|
+
const payload = JSON.stringify({
|
|
325445
|
+
version: 1,
|
|
325446
|
+
projectId: input.projectId,
|
|
325447
|
+
serverOrigin: normalizeOrigin(input.serverUrl),
|
|
325448
|
+
appUrl: normalizeAppUrl(input.baseUrl),
|
|
325449
|
+
testVariables: stableVariables(input.testVariables)
|
|
325450
|
+
});
|
|
325451
|
+
const key = input.authToken || `validate-local-auth:${input.projectId}`;
|
|
325452
|
+
return (0, node_crypto_1.createHmac)("sha256", key).update(payload).digest("hex").slice(0, 24);
|
|
325453
|
+
}
|
|
325454
|
+
function ensurePrivateDirectory2(dir) {
|
|
325455
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
325456
|
+
try {
|
|
325457
|
+
(0, node_fs_1.chmodSync)(dir, 448);
|
|
325458
|
+
} catch {
|
|
325459
|
+
}
|
|
325460
|
+
}
|
|
325461
|
+
function removeFile(path) {
|
|
325462
|
+
try {
|
|
325463
|
+
(0, node_fs_1.unlinkSync)(path);
|
|
325464
|
+
} catch {
|
|
325465
|
+
}
|
|
325466
|
+
}
|
|
325467
|
+
function readLockOwner(path) {
|
|
325468
|
+
try {
|
|
325469
|
+
const parsed = JSON.parse((0, node_fs_1.readFileSync)(path, "utf8"));
|
|
325470
|
+
return typeof parsed.token === "string" && parsed.token.length > 0 && Number.isSafeInteger(parsed.pid) && (parsed.pid ?? 0) > 0 ? { token: parsed.token, pid: parsed.pid } : void 0;
|
|
325471
|
+
} catch {
|
|
325472
|
+
return void 0;
|
|
325473
|
+
}
|
|
325474
|
+
}
|
|
325475
|
+
function processIsAlive(pid) {
|
|
325476
|
+
try {
|
|
325477
|
+
process.kill(pid, 0);
|
|
325478
|
+
return true;
|
|
325479
|
+
} catch (error2) {
|
|
325480
|
+
return error2.code === "EPERM";
|
|
325481
|
+
}
|
|
325482
|
+
}
|
|
325483
|
+
function staleLockCanBeReaped(path, now, staleMs) {
|
|
325484
|
+
try {
|
|
325485
|
+
const ageMs = now - (0, node_fs_1.statSync)(path).mtimeMs;
|
|
325486
|
+
if (ageMs < staleMs)
|
|
325487
|
+
return false;
|
|
325488
|
+
const owner = readLockOwner(path);
|
|
325489
|
+
if (owner) {
|
|
325490
|
+
if (!processIsAlive(owner.pid))
|
|
325491
|
+
return true;
|
|
325492
|
+
return ageMs >= Math.max(staleMs * 5, exports.LOCAL_AUTH_STATE_LOCK_ABSOLUTE_STALE_MS);
|
|
325493
|
+
}
|
|
325494
|
+
return ageMs >= staleMs * 5;
|
|
325495
|
+
} catch {
|
|
325496
|
+
return false;
|
|
325497
|
+
}
|
|
325498
|
+
}
|
|
325499
|
+
function isRecord(value) {
|
|
325500
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
325501
|
+
}
|
|
325502
|
+
function isValidStorageState(value) {
|
|
325503
|
+
if (!isRecord(value))
|
|
325504
|
+
return false;
|
|
325505
|
+
const cookies = value.cookies;
|
|
325506
|
+
const origins = value.origins;
|
|
325507
|
+
if (!Array.isArray(cookies) || !Array.isArray(origins))
|
|
325508
|
+
return false;
|
|
325509
|
+
const cookiesValid = cookies.every((cookie) => {
|
|
325510
|
+
if (!isRecord(cookie))
|
|
325511
|
+
return false;
|
|
325512
|
+
return typeof cookie.name === "string" && typeof cookie.value === "string" && typeof cookie.domain === "string" && typeof cookie.path === "string" && typeof cookie.expires === "number" && Number.isFinite(cookie.expires) && typeof cookie.httpOnly === "boolean" && typeof cookie.secure === "boolean" && (cookie.sameSite === "Strict" || cookie.sameSite === "Lax" || cookie.sameSite === "None");
|
|
325513
|
+
});
|
|
325514
|
+
if (!cookiesValid)
|
|
325515
|
+
return false;
|
|
325516
|
+
const originsValid = origins.every((origin) => {
|
|
325517
|
+
if (!isRecord(origin) || typeof origin.origin !== "string" || !Array.isArray(origin.localStorage))
|
|
325518
|
+
return false;
|
|
325519
|
+
return origin.localStorage.every((entry) => isRecord(entry) && typeof entry.name === "string" && typeof entry.value === "string");
|
|
325520
|
+
});
|
|
325521
|
+
if (!originsValid)
|
|
325522
|
+
return false;
|
|
325523
|
+
return cookies.length > 0 || origins.some((origin) => origin.localStorage.length > 0);
|
|
325524
|
+
}
|
|
325525
|
+
function isValidStorageStateJson(value) {
|
|
325526
|
+
try {
|
|
325527
|
+
return isValidStorageState(JSON.parse(value));
|
|
325528
|
+
} catch {
|
|
325529
|
+
return false;
|
|
325530
|
+
}
|
|
325531
|
+
}
|
|
325532
|
+
function isStorageStateShape(value) {
|
|
325533
|
+
if (!value || typeof value !== "object")
|
|
325534
|
+
return false;
|
|
325535
|
+
return isValidStorageState(value);
|
|
325536
|
+
}
|
|
325537
|
+
function isValidLocalAuthStateFile(path) {
|
|
325538
|
+
try {
|
|
325539
|
+
return isStorageStateShape(JSON.parse((0, node_fs_1.readFileSync)(path, "utf8")));
|
|
325540
|
+
} catch {
|
|
325541
|
+
return false;
|
|
325542
|
+
}
|
|
325543
|
+
}
|
|
325544
|
+
function cleanupLocalAuthStateFiles(runsDir, projectId, now = Date.now(), maxAgeMs = exports.LOCAL_AUTH_STATE_MAX_AGE_MS) {
|
|
325545
|
+
ensurePrivateDirectory2(runsDir);
|
|
325546
|
+
const projectSegment = safeSegment(projectId, "default");
|
|
325547
|
+
const legacyName = `auth-state-${projectSegment}.json`;
|
|
325548
|
+
const prefix = `auth-state-${projectSegment}-`;
|
|
325549
|
+
for (const name of (0, node_fs_1.readdirSync)(runsDir)) {
|
|
325550
|
+
if (name === legacyName) {
|
|
325551
|
+
removeFile((0, node_path_1.join)(runsDir, name));
|
|
325552
|
+
continue;
|
|
325553
|
+
}
|
|
325554
|
+
if (!name.startsWith(prefix))
|
|
325555
|
+
continue;
|
|
325556
|
+
const path = (0, node_path_1.join)(runsDir, name);
|
|
325557
|
+
try {
|
|
325558
|
+
if (now - (0, node_fs_1.statSync)(path).mtimeMs >= maxAgeMs) {
|
|
325559
|
+
if (!name.endsWith(".lock") || staleLockCanBeReaped(path, now, maxAgeMs))
|
|
325560
|
+
removeFile(path);
|
|
325561
|
+
}
|
|
325562
|
+
} catch {
|
|
325563
|
+
}
|
|
325564
|
+
}
|
|
325565
|
+
}
|
|
325566
|
+
function cleanupExpiredLocalAuthStateFiles2(runsDir, now = Date.now(), maxAgeMs = exports.LOCAL_AUTH_STATE_MAX_AGE_MS) {
|
|
325567
|
+
ensurePrivateDirectory2(runsDir);
|
|
325568
|
+
for (const name of (0, node_fs_1.readdirSync)(runsDir)) {
|
|
325569
|
+
if (!name.startsWith("auth-state-"))
|
|
325570
|
+
continue;
|
|
325571
|
+
const path = (0, node_path_1.join)(runsDir, name);
|
|
325572
|
+
try {
|
|
325573
|
+
if (now - (0, node_fs_1.statSync)(path).mtimeMs >= maxAgeMs) {
|
|
325574
|
+
if (!name.endsWith(".lock") || staleLockCanBeReaped(path, now, maxAgeMs))
|
|
325575
|
+
removeFile(path);
|
|
325576
|
+
}
|
|
325577
|
+
} catch {
|
|
325578
|
+
}
|
|
325579
|
+
}
|
|
325580
|
+
}
|
|
325581
|
+
function localAuthStatePaths2(runsDir, input, runId) {
|
|
325582
|
+
cleanupLocalAuthStateFiles(runsDir, input.projectId);
|
|
325583
|
+
const projectSegment = safeSegment(input.projectId, "default");
|
|
325584
|
+
const runSegment = safeSegment(runId, `${process.pid}`);
|
|
325585
|
+
const fingerprint = localAuthStateFingerprint(input);
|
|
325586
|
+
const cachePath = (0, node_path_1.join)(runsDir, `auth-state-${projectSegment}-${fingerprint}.json`);
|
|
325587
|
+
return {
|
|
325588
|
+
cachePath,
|
|
325589
|
+
capturePath: `${cachePath}.capture-${runSegment}-${process.pid}.tmp`,
|
|
325590
|
+
fingerprint,
|
|
325591
|
+
lockKey: `${normalizeOrigin(input.serverUrl)}:${projectSegment}:${fingerprint}`,
|
|
325592
|
+
lockPath: `${cachePath}.lock`
|
|
325593
|
+
};
|
|
325594
|
+
}
|
|
325595
|
+
function getValidLocalAuthState2(path, now = Date.now(), maxAgeMs = exports.LOCAL_AUTH_STATE_MAX_AGE_MS) {
|
|
325596
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
325597
|
+
return void 0;
|
|
325598
|
+
try {
|
|
325599
|
+
if (now - (0, node_fs_1.statSync)(path).mtimeMs >= maxAgeMs || !isValidLocalAuthStateFile(path)) {
|
|
325600
|
+
removeFile(path);
|
|
325601
|
+
return void 0;
|
|
325602
|
+
}
|
|
325603
|
+
return path;
|
|
325604
|
+
} catch {
|
|
325605
|
+
return void 0;
|
|
325606
|
+
}
|
|
325607
|
+
}
|
|
325608
|
+
async function waitForValidLocalAuthState(path, timeoutMs = exports.LOCAL_AUTH_STATE_WAIT_MS, pollMs = 100) {
|
|
325609
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
325610
|
+
do {
|
|
325611
|
+
const valid = getValidLocalAuthState2(path);
|
|
325612
|
+
if (valid)
|
|
325613
|
+
return valid;
|
|
325614
|
+
const remaining = deadline - Date.now();
|
|
325615
|
+
if (remaining <= 0)
|
|
325616
|
+
return void 0;
|
|
325617
|
+
await new Promise((resolve2) => setTimeout(resolve2, Math.min(Math.max(10, pollMs), remaining)));
|
|
325618
|
+
} while (true);
|
|
325619
|
+
}
|
|
325620
|
+
async function waitForLocalAuthStateProducer2(path, lockKey, startupGraceMs = 2e3, maxWaitMs = exports.LOCAL_AUTH_STATE_WAIT_MS, pollMs = 100, lockPath) {
|
|
325621
|
+
const startedAt = Date.now();
|
|
325622
|
+
const producerIsActive = () => lockTails.has(lockKey) || !!lockPath && (0, node_fs_1.existsSync)(lockPath);
|
|
325623
|
+
let sawProducer = producerIsActive();
|
|
325624
|
+
do {
|
|
325625
|
+
const valid = getValidLocalAuthState2(path);
|
|
325626
|
+
if (valid)
|
|
325627
|
+
return valid;
|
|
325628
|
+
const producerActive = producerIsActive();
|
|
325629
|
+
if (producerActive)
|
|
325630
|
+
sawProducer = true;
|
|
325631
|
+
const elapsed = Date.now() - startedAt;
|
|
325632
|
+
if (sawProducer && !producerActive)
|
|
325633
|
+
return void 0;
|
|
325634
|
+
if (!sawProducer && elapsed >= Math.max(0, startupGraceMs))
|
|
325635
|
+
return void 0;
|
|
325636
|
+
if (elapsed >= Math.max(0, maxWaitMs))
|
|
325637
|
+
return void 0;
|
|
325638
|
+
await new Promise((resolve2) => setTimeout(resolve2, Math.max(10, pollMs)));
|
|
325639
|
+
} while (true);
|
|
325640
|
+
}
|
|
325641
|
+
function promoteLocalAuthStateCapture2(capturePath, cachePath, replaceExisting = false, lease) {
|
|
325642
|
+
if (lease && !lease.isOwner()) {
|
|
325643
|
+
removeFile(capturePath);
|
|
325644
|
+
return false;
|
|
325645
|
+
}
|
|
325646
|
+
if (!(0, node_fs_1.existsSync)(capturePath) || !isValidLocalAuthStateFile(capturePath)) {
|
|
325647
|
+
removeFile(capturePath);
|
|
325648
|
+
return false;
|
|
325649
|
+
}
|
|
325650
|
+
try {
|
|
325651
|
+
(0, node_fs_1.chmodSync)(capturePath, 384);
|
|
325652
|
+
} catch {
|
|
325653
|
+
}
|
|
325654
|
+
try {
|
|
325655
|
+
if (lease && !lease.isOwner()) {
|
|
325656
|
+
removeFile(capturePath);
|
|
325657
|
+
return false;
|
|
325658
|
+
}
|
|
325659
|
+
if ((0, node_fs_1.existsSync)(cachePath)) {
|
|
325660
|
+
if (!replaceExisting && isValidLocalAuthStateFile(cachePath)) {
|
|
325661
|
+
removeFile(capturePath);
|
|
325662
|
+
return true;
|
|
325663
|
+
}
|
|
325664
|
+
removeFile(cachePath);
|
|
325665
|
+
}
|
|
325666
|
+
if (lease && !lease.isOwner()) {
|
|
325667
|
+
removeFile(capturePath);
|
|
325668
|
+
return false;
|
|
325669
|
+
}
|
|
325670
|
+
(0, node_fs_1.renameSync)(capturePath, cachePath);
|
|
325671
|
+
try {
|
|
325672
|
+
(0, node_fs_1.chmodSync)(cachePath, 384);
|
|
325673
|
+
} catch {
|
|
325674
|
+
}
|
|
325675
|
+
return true;
|
|
325676
|
+
} catch {
|
|
325677
|
+
const winnerIsValid = (0, node_fs_1.existsSync)(cachePath) && isValidLocalAuthStateFile(cachePath);
|
|
325678
|
+
removeFile(capturePath);
|
|
325679
|
+
return winnerIsValid;
|
|
325680
|
+
}
|
|
325681
|
+
}
|
|
325682
|
+
function discardLocalAuthStateCapture2(capturePath) {
|
|
325683
|
+
removeFile(capturePath);
|
|
325684
|
+
}
|
|
325685
|
+
var lockTails = /* @__PURE__ */ new Map();
|
|
325686
|
+
function publishCompleteLockOwner(lockPath, owner, linkFile) {
|
|
325687
|
+
const metadata = JSON.stringify(owner);
|
|
325688
|
+
const tempPath = `${lockPath}.${owner.pid}.${owner.token}.tmp`;
|
|
325689
|
+
let createdLock = false;
|
|
325690
|
+
try {
|
|
325691
|
+
(0, node_fs_1.writeFileSync)(tempPath, metadata, { flag: "wx", mode: 384 });
|
|
325692
|
+
try {
|
|
325693
|
+
linkFile(tempPath, lockPath);
|
|
325694
|
+
createdLock = true;
|
|
325695
|
+
return (0, node_fs_1.openSync)(lockPath, "r+");
|
|
325696
|
+
} catch (error2) {
|
|
325697
|
+
const code = error2.code;
|
|
325698
|
+
if (!["EPERM", "EACCES", "EXDEV", "ENOSYS", "ENOTSUP", "EOPNOTSUPP"].includes(code ?? "")) {
|
|
325699
|
+
throw error2;
|
|
325700
|
+
}
|
|
325701
|
+
let fd;
|
|
325702
|
+
try {
|
|
325703
|
+
fd = (0, node_fs_1.openSync)(lockPath, "wx+", 384);
|
|
325704
|
+
createdLock = true;
|
|
325705
|
+
(0, node_fs_1.writeFileSync)(fd, metadata, { encoding: "utf8" });
|
|
325706
|
+
(0, node_fs_1.fsyncSync)(fd);
|
|
325707
|
+
return fd;
|
|
325708
|
+
} catch (fallbackError) {
|
|
325709
|
+
if (fd !== void 0) {
|
|
325710
|
+
try {
|
|
325711
|
+
(0, node_fs_1.closeSync)(fd);
|
|
325712
|
+
} catch {
|
|
325713
|
+
}
|
|
325714
|
+
}
|
|
325715
|
+
if (createdLock)
|
|
325716
|
+
removeFile(lockPath);
|
|
325717
|
+
throw fallbackError;
|
|
325718
|
+
}
|
|
325719
|
+
}
|
|
325720
|
+
} catch (error2) {
|
|
325721
|
+
if (createdLock && readLockOwner(lockPath)?.token === owner.token)
|
|
325722
|
+
removeFile(lockPath);
|
|
325723
|
+
throw error2;
|
|
325724
|
+
} finally {
|
|
325725
|
+
removeFile(tempPath);
|
|
325726
|
+
}
|
|
325727
|
+
}
|
|
325728
|
+
async function acquireExclusiveFileLock(lockPath, linkFile = node_fs_1.linkSync) {
|
|
325729
|
+
ensurePrivateDirectory2((0, node_path_1.dirname)(lockPath));
|
|
325730
|
+
const startedAt = Date.now();
|
|
325731
|
+
while (true) {
|
|
325732
|
+
try {
|
|
325733
|
+
const ownerToken = (0, node_crypto_1.randomBytes)(24).toString("base64url");
|
|
325734
|
+
const fd = publishCompleteLockOwner(lockPath, { token: ownerToken, pid: process.pid }, linkFile);
|
|
325735
|
+
const ownsPath = () => {
|
|
325736
|
+
try {
|
|
325737
|
+
const owner = readLockOwner(lockPath);
|
|
325738
|
+
return owner?.token === ownerToken && owner.pid === process.pid;
|
|
325739
|
+
} catch {
|
|
325740
|
+
return false;
|
|
325741
|
+
}
|
|
325742
|
+
};
|
|
325743
|
+
const heartbeat = setInterval(() => {
|
|
325744
|
+
try {
|
|
325745
|
+
if (!ownsPath()) {
|
|
325746
|
+
clearInterval(heartbeat);
|
|
325747
|
+
return;
|
|
325748
|
+
}
|
|
325749
|
+
const now = /* @__PURE__ */ new Date();
|
|
325750
|
+
(0, node_fs_1.futimesSync)(fd, now, now);
|
|
325751
|
+
} catch {
|
|
325752
|
+
clearInterval(heartbeat);
|
|
325753
|
+
}
|
|
325754
|
+
}, Math.max(1e3, Math.floor(exports.LOCAL_AUTH_STATE_LOCK_STALE_MS / 3)));
|
|
325755
|
+
heartbeat.unref();
|
|
325756
|
+
let released = false;
|
|
325757
|
+
const release = (() => {
|
|
325758
|
+
if (released)
|
|
325759
|
+
return;
|
|
325760
|
+
released = true;
|
|
325761
|
+
clearInterval(heartbeat);
|
|
325762
|
+
try {
|
|
325763
|
+
if (ownsPath())
|
|
325764
|
+
removeFile(lockPath);
|
|
325765
|
+
} catch {
|
|
325766
|
+
}
|
|
325767
|
+
try {
|
|
325768
|
+
(0, node_fs_1.closeSync)(fd);
|
|
325769
|
+
} catch {
|
|
325770
|
+
}
|
|
325771
|
+
});
|
|
325772
|
+
release.isOwner = () => !released && ownsPath();
|
|
325773
|
+
return release;
|
|
325774
|
+
} catch (error2) {
|
|
325775
|
+
const code = error2.code;
|
|
325776
|
+
if (code !== "EEXIST")
|
|
325777
|
+
throw error2;
|
|
325778
|
+
try {
|
|
325779
|
+
if (staleLockCanBeReaped(lockPath, Date.now(), exports.LOCAL_AUTH_STATE_LOCK_STALE_MS)) {
|
|
325780
|
+
removeFile(lockPath);
|
|
325781
|
+
continue;
|
|
325782
|
+
}
|
|
325783
|
+
} catch {
|
|
325784
|
+
continue;
|
|
325785
|
+
}
|
|
325786
|
+
if (Date.now() - startedAt >= exports.LOCAL_AUTH_STATE_WAIT_MS) {
|
|
325787
|
+
throw new Error(`Timed out waiting for auth-state lock: ${(0, node_path_1.basename)(lockPath)}`);
|
|
325788
|
+
}
|
|
325789
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
325790
|
+
}
|
|
325791
|
+
}
|
|
325792
|
+
}
|
|
325793
|
+
async function acquireLocalAuthStateLock2(key, lockPath, linkFile = node_fs_1.linkSync) {
|
|
325794
|
+
const previous = lockTails.get(key) ?? Promise.resolve();
|
|
325795
|
+
let unlockCurrent;
|
|
325796
|
+
const current = new Promise((resolve2) => {
|
|
325797
|
+
unlockCurrent = resolve2;
|
|
325798
|
+
});
|
|
325799
|
+
const tail = previous.then(() => current);
|
|
325800
|
+
lockTails.set(key, tail);
|
|
325801
|
+
await previous;
|
|
325802
|
+
let releaseFileLock;
|
|
325803
|
+
try {
|
|
325804
|
+
if (lockPath)
|
|
325805
|
+
releaseFileLock = await acquireExclusiveFileLock(lockPath, linkFile);
|
|
325806
|
+
} catch (error2) {
|
|
325807
|
+
unlockCurrent();
|
|
325808
|
+
if (lockTails.get(key) === tail)
|
|
325809
|
+
lockTails.delete(key);
|
|
325810
|
+
throw error2;
|
|
325811
|
+
}
|
|
325812
|
+
let released = false;
|
|
325813
|
+
const release = (() => {
|
|
325814
|
+
if (released)
|
|
325815
|
+
return;
|
|
325816
|
+
released = true;
|
|
325817
|
+
releaseFileLock?.();
|
|
325818
|
+
unlockCurrent();
|
|
325819
|
+
if (lockTails.get(key) === tail)
|
|
325820
|
+
lockTails.delete(key);
|
|
325821
|
+
});
|
|
325822
|
+
release.isOwner = () => !released && (releaseFileLock?.isOwner() ?? true);
|
|
325823
|
+
return release;
|
|
325824
|
+
}
|
|
325825
|
+
function localAuthStateFilename(path) {
|
|
325826
|
+
return (0, node_path_1.basename)(path);
|
|
325827
|
+
}
|
|
325828
|
+
}
|
|
325829
|
+
});
|
|
325830
|
+
|
|
324813
325831
|
// ../../node_modules/adm-zip/util/constants.js
|
|
324814
325832
|
var require_constants5 = __commonJS({
|
|
324815
325833
|
"../../node_modules/adm-zip/util/constants.js"(exports, module) {
|
|
@@ -325123,7 +326141,7 @@ var require_utils2 = __commonJS({
|
|
|
325123
326141
|
module.exports = Utils;
|
|
325124
326142
|
Utils.prototype.makeDir = function(folder) {
|
|
325125
326143
|
const self = this;
|
|
325126
|
-
function
|
|
326144
|
+
function mkdirSync4(fpath) {
|
|
325127
326145
|
let resolvedPath = fpath.split(self.sep)[0];
|
|
325128
326146
|
fpath.split(self.sep).forEach(function(name) {
|
|
325129
326147
|
if (!name || name.substr(-1, 1) === ":") return;
|
|
@@ -325141,7 +326159,7 @@ var require_utils2 = __commonJS({
|
|
|
325141
326159
|
if (stat && stat.isFile()) throw Errors.FILE_IN_THE_WAY(`"${resolvedPath}"`);
|
|
325142
326160
|
});
|
|
325143
326161
|
}
|
|
325144
|
-
|
|
326162
|
+
mkdirSync4(folder);
|
|
325145
326163
|
};
|
|
325146
326164
|
Utils.prototype.writeFileTo = function(path, content, overwrite, attr) {
|
|
325147
326165
|
const self = this;
|
|
@@ -337065,6 +338083,8 @@ var require_dist2 = __commonJS({
|
|
|
337065
338083
|
__exportStar(require_discover_agent(), exports);
|
|
337066
338084
|
__exportStar(require_browser_pool(), exports);
|
|
337067
338085
|
__exportStar(require_resource_governor(), exports);
|
|
338086
|
+
__exportStar(require_local_auth_state(), exports);
|
|
338087
|
+
__exportStar(require_run_callback_proxy(), exports);
|
|
337068
338088
|
__exportStar(require_discover_explorer(), exports);
|
|
337069
338089
|
__exportStar(require_discover_feature_explorer(), exports);
|
|
337070
338090
|
__exportStar(require_discover_planner(), exports);
|
|
@@ -337341,7 +338361,7 @@ var require_package4 = __commonJS({
|
|
|
337341
338361
|
"package.json"(exports, module) {
|
|
337342
338362
|
module.exports = {
|
|
337343
338363
|
name: "@validate.qa/runner",
|
|
337344
|
-
version: "1.0.
|
|
338364
|
+
version: "1.0.44",
|
|
337345
338365
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
337346
338366
|
bin: {
|
|
337347
338367
|
"validate-runner": "dist/cli.js"
|
|
@@ -337349,7 +338369,7 @@ var require_package4 = __commonJS({
|
|
|
337349
338369
|
scripts: {
|
|
337350
338370
|
build: "tsup",
|
|
337351
338371
|
dev: "tsx src/cli.ts",
|
|
337352
|
-
test: "node --import tsx --test src/version-check.test.ts",
|
|
338372
|
+
test: "node --import tsx --test src/version-check.test.ts src/services/auth-state.test.ts",
|
|
337353
338373
|
"test:mobile": "node --import tsx --test --test-concurrency=2 src/runner.mobile-ai-env.test.ts src/mobile-device-queue.test.ts src/device-lease.test.ts src/result-payload-budget.test.ts src/services/appium-*.test.ts src/services/*mobile*.test.ts src/services/mobile/*.test.ts",
|
|
337354
338374
|
prepublishOnly: "npm run build"
|
|
337355
338375
|
},
|
|
@@ -337428,7 +338448,6 @@ function getConfigDir() {
|
|
|
337428
338448
|
}
|
|
337429
338449
|
|
|
337430
338450
|
// src/runner.ts
|
|
337431
|
-
import { existsSync as existsSync3, statSync, mkdirSync as mkdirSync4 } from "fs";
|
|
337432
338451
|
import { join as join6 } from "path";
|
|
337433
338452
|
import { homedir as homedir3 } from "os";
|
|
337434
338453
|
import { createHash } from "crypto";
|
|
@@ -337456,26 +338475,28 @@ var import_runner_core6 = __toESM(require_dist2());
|
|
|
337456
338475
|
|
|
337457
338476
|
// src/runner.ts
|
|
337458
338477
|
var import_runner_core11 = __toESM(require_dist2());
|
|
338478
|
+
var import_runner_core12 = __toESM(require_dist2());
|
|
337459
338479
|
|
|
337460
338480
|
// src/services/auth-state.ts
|
|
337461
|
-
|
|
338481
|
+
init_dist();
|
|
337462
338482
|
var GUARDED_LOGIN_TAG = "@auth-guarded";
|
|
337463
338483
|
function sharedAuthInjectEnabled() {
|
|
337464
|
-
return
|
|
338484
|
+
return isSharedAuthInjectEnabled();
|
|
337465
338485
|
}
|
|
337466
|
-
function
|
|
338486
|
+
function hasGeneratedAuthGuard2(playwrightCode) {
|
|
337467
338487
|
if (!playwrightCode) return false;
|
|
337468
|
-
return
|
|
338488
|
+
return /\/\/ >>> VALIDATE_QA_AUTH v\d+\b[\s\S]*?process\.env\.AUTH_FILE[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH v\d+\b/.test(playwrightCode);
|
|
338489
|
+
}
|
|
338490
|
+
function shouldCaptureAuthState(tags, playwrightCode) {
|
|
338491
|
+
return sharedAuthInjectEnabled() && Array.isArray(tags) && tags.includes("@needs-auth") && tags.includes(GUARDED_LOGIN_TAG) && !tags.includes("@auth") && hasGeneratedAuthGuard2(playwrightCode);
|
|
337469
338492
|
}
|
|
337470
338493
|
function shouldUseAuthState(tags, playwrightCode) {
|
|
338494
|
+
if (!sharedAuthInjectEnabled()) return false;
|
|
337471
338495
|
if (!Array.isArray(tags)) return false;
|
|
337472
|
-
|
|
337473
|
-
|
|
337474
|
-
|
|
337475
|
-
|
|
337476
|
-
const isGeneratedVariant = tags.includes("@edge") || tags.includes("@bug");
|
|
337477
|
-
const hasLegacyInlineLogin = tags.includes("@discovered") && !isGeneratedVariant && hasInlineLoginSteps(playwrightCode);
|
|
337478
|
-
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() });
|
|
337479
338500
|
}
|
|
337480
338501
|
|
|
337481
338502
|
// src/version-check.ts
|
|
@@ -344185,8 +345206,8 @@ function withMediaReferences(result, refs) {
|
|
|
344185
345206
|
|
|
344186
345207
|
// src/runner.ts
|
|
344187
345208
|
init_dist();
|
|
344188
|
-
var import_runner_core12 = __toESM(require_dist2());
|
|
344189
345209
|
var import_runner_core13 = __toESM(require_dist2());
|
|
345210
|
+
var import_runner_core14 = __toESM(require_dist2());
|
|
344190
345211
|
function fingerprintTest(test) {
|
|
344191
345212
|
const code = test.framework === "APPIUM" ? [test.appiumCode ?? "", JSON.stringify(test.steps ?? [])].join("\n--steps--\n") : test.playwrightCode ?? "";
|
|
344192
345213
|
return createHash("sha256").update(test.name).update("\n--suite--\n").update(test.suiteName ?? "").update("\n--type--\n").update(test.testType ?? "").update("\n--framework--\n").update(test.framework ?? "").update("\n--code--\n").update(code).digest("hex").slice(0, 24);
|
|
@@ -344310,21 +345331,11 @@ var log = {
|
|
|
344310
345331
|
warn: (msg) => console.log(`${C2.yellow}\u26A0${C2.reset} ${msg}`),
|
|
344311
345332
|
dim: (msg) => console.log(`${C2.gray}${msg}${C2.reset}`)
|
|
344312
345333
|
};
|
|
344313
|
-
var AUTH_MAX_AGE_MS = 60 * 60 * 1e3;
|
|
344314
345334
|
function getRunsDir() {
|
|
344315
345335
|
const dir = process.env.RUNS_DIR || join6(homedir3(), ".validate.qa", "runs");
|
|
344316
|
-
|
|
345336
|
+
(0, import_runner_core12.ensurePrivateDirectory)(dir);
|
|
344317
345337
|
return dir;
|
|
344318
345338
|
}
|
|
344319
|
-
function authStatePath(projectId) {
|
|
344320
|
-
return join6(getRunsDir(), `auth-state-${projectId || "default"}.json`);
|
|
344321
|
-
}
|
|
344322
|
-
function getValidAuthState(projectId) {
|
|
344323
|
-
const path = authStatePath(projectId);
|
|
344324
|
-
if (!existsSync3(path)) return void 0;
|
|
344325
|
-
const age = Date.now() - statSync(path).mtimeMs;
|
|
344326
|
-
return age < AUTH_MAX_AGE_MS ? path : void 0;
|
|
344327
|
-
}
|
|
344328
345339
|
function buildHeaders(authToken, projectId) {
|
|
344329
345340
|
return {
|
|
344330
345341
|
"Content-Type": "application/json",
|
|
@@ -344373,7 +345384,13 @@ async function claimRun(serverUrl, headers, runId) {
|
|
|
344373
345384
|
if (res.status === 429) return false;
|
|
344374
345385
|
if (!res.ok) throw new Error(`Claim failed: ${res.status}`);
|
|
344375
345386
|
const body = await res.json();
|
|
344376
|
-
|
|
345387
|
+
if (!body.claimed) {
|
|
345388
|
+
throw new Error("Claim response did not confirm ownership");
|
|
345389
|
+
}
|
|
345390
|
+
if (typeof body.claimEpoch !== "number" || !Number.isSafeInteger(body.claimEpoch) || body.claimEpoch < 0) {
|
|
345391
|
+
throw new Error("Claim response did not include a valid ownership epoch");
|
|
345392
|
+
}
|
|
345393
|
+
return body.claimEpoch;
|
|
344377
345394
|
}
|
|
344378
345395
|
async function postResult(serverUrl, headers, runId, result) {
|
|
344379
345396
|
const MAX_ATTEMPTS = 3;
|
|
@@ -344541,12 +345558,12 @@ async function postCaptureResult(serverUrl, headers, runId, result) {
|
|
|
344541
345558
|
});
|
|
344542
345559
|
if (!res.ok) throw new Error(`Failed to post capture result: ${res.status}`);
|
|
344543
345560
|
}
|
|
344544
|
-
async function postCredentialRotation(serverUrl, headers, runId, body) {
|
|
345561
|
+
async function postCredentialRotation(serverUrl, headers, runId, body, claimEpoch) {
|
|
344545
345562
|
try {
|
|
344546
345563
|
const res = await fetch(`${serverUrl}/api/runner/runs/${runId}/credential`, {
|
|
344547
345564
|
method: "POST",
|
|
344548
345565
|
headers,
|
|
344549
|
-
body: JSON.stringify(body),
|
|
345566
|
+
body: JSON.stringify({ ...body, ...claimEpoch !== void 0 ? { claimEpoch } : {} }),
|
|
344550
345567
|
signal: AbortSignal.timeout(3e4)
|
|
344551
345568
|
});
|
|
344552
345569
|
if (!res.ok) {
|
|
@@ -344878,11 +345895,28 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
344878
345895
|
requestLiveBrowserHeartbeat();
|
|
344879
345896
|
}, 5e3);
|
|
344880
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;
|
|
344881
345907
|
const runNativeWithLiveBrowser = (nativeOptions) => (0, import_runner_core.executeTestViaNativePlaywright)({
|
|
345908
|
+
authStandingMode: authPlan.standingHeadersMode,
|
|
345909
|
+
storageStatePath: wrapperStorageStatePath,
|
|
344882
345910
|
...nativeOptions,
|
|
344883
345911
|
liveBrowserHandle,
|
|
344884
345912
|
onLiveBrowserSnapshot: requestLiveBrowserHeartbeat
|
|
344885
345913
|
});
|
|
345914
|
+
const callbackContext = {
|
|
345915
|
+
runId: run2.runId,
|
|
345916
|
+
serverUrl: opts.serverUrl,
|
|
345917
|
+
authHeader: headers.Authorization,
|
|
345918
|
+
claimEpoch: run2.claimEpoch
|
|
345919
|
+
};
|
|
344886
345920
|
const startTime = Date.now();
|
|
344887
345921
|
try {
|
|
344888
345922
|
if (mobileLeaseError) {
|
|
@@ -344900,11 +345934,24 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
344900
345934
|
if (!run2.baseUrl) {
|
|
344901
345935
|
preflightError = "Pre-flight failed: Missing BASE_URL. Configure it in Test Data \u2192 Test Variables.";
|
|
344902
345936
|
} else {
|
|
344903
|
-
const
|
|
344904
|
-
if (
|
|
344905
|
-
preflightError =
|
|
344906
|
-
} else if (
|
|
344907
|
-
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
|
+
}
|
|
344908
345955
|
}
|
|
344909
345956
|
}
|
|
344910
345957
|
if (preflightError) {
|
|
@@ -344912,7 +345959,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
344912
345959
|
log.fail(`PRE-FLIGHT FAILED - ${preflightError}`);
|
|
344913
345960
|
return;
|
|
344914
345961
|
}
|
|
344915
|
-
const preflight = await (0,
|
|
345962
|
+
const preflight = await (0, import_runner_core13.resolveBaseUrlPreflight)(run2.baseUrl);
|
|
344916
345963
|
if (preflight.error) {
|
|
344917
345964
|
await postResult(opts.serverUrl, headers, run2.runId, { status: "ERROR", error: preflight.error });
|
|
344918
345965
|
log.fail(`PRE-FLIGHT FAILED - ${preflight.error}`);
|
|
@@ -344929,31 +345976,72 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
344929
345976
|
log.fail("ERROR - No auth setup code");
|
|
344930
345977
|
return;
|
|
344931
345978
|
}
|
|
344932
|
-
const
|
|
344933
|
-
|
|
344934
|
-
|
|
344935
|
-
|
|
345979
|
+
const sharedAuthEnabled = sharedAuthInjectEnabled();
|
|
345980
|
+
if ((0, import_runner_core12.shouldSkipDisabledAutomaticAuthSetup)(run2.authForceRun, run2.authVerify, sharedAuthEnabled)) {
|
|
345981
|
+
const duration2 = Date.now() - startTime;
|
|
345982
|
+
await postResult(opts.serverUrl, headers, run2.runId, {
|
|
345983
|
+
status: "PASSED",
|
|
345984
|
+
logs: "Automatic auth prewarm skipped: shared session reuse is disabled.",
|
|
345985
|
+
duration: duration2
|
|
345986
|
+
});
|
|
345987
|
+
log.info(`Auth prewarm skipped (${duration2}ms) - SHARED_AUTH_INJECT=false`);
|
|
345988
|
+
return;
|
|
345989
|
+
}
|
|
345990
|
+
const authPaths2 = sharedAuthEnabled ? (0, import_runner_core12.localAuthStatePaths)(getRunsDir(), {
|
|
345991
|
+
projectId: run2.projectId || opts.projectId || "default",
|
|
345992
|
+
serverUrl: opts.serverUrl,
|
|
344936
345993
|
baseUrl: run2.baseUrl,
|
|
344937
|
-
|
|
344938
|
-
|
|
344939
|
-
|
|
344940
|
-
|
|
344941
|
-
|
|
344942
|
-
|
|
345994
|
+
testVariables: run2.testVariables,
|
|
345995
|
+
authToken: opts.authToken
|
|
345996
|
+
}, run2.runId) : void 0;
|
|
345997
|
+
const releaseAuthLock2 = authPaths2 ? await (0, import_runner_core12.acquireLocalAuthStateLock)(authPaths2.lockKey, authPaths2.lockPath) : void 0;
|
|
345998
|
+
try {
|
|
345999
|
+
if (authPaths2 && (0, import_runner_core12.shouldSkipWarmAuthSetup)(
|
|
346000
|
+
run2.authForceRun,
|
|
346001
|
+
sharedAuthEnabled,
|
|
346002
|
+
!!(0, import_runner_core12.getValidLocalAuthState)(authPaths2.cachePath)
|
|
346003
|
+
)) {
|
|
346004
|
+
const duration3 = Date.now() - startTime;
|
|
346005
|
+
await postResult(opts.serverUrl, headers, run2.runId, {
|
|
346006
|
+
status: "PASSED",
|
|
346007
|
+
logs: "Auth setup skipped: the credential-bound local session is already warm.",
|
|
346008
|
+
duration: duration3
|
|
346009
|
+
});
|
|
346010
|
+
log.ok(`Auth setup skipped (${duration3}ms) - local session already warm`);
|
|
346011
|
+
return;
|
|
344943
346012
|
}
|
|
344944
|
-
|
|
344945
|
-
|
|
344946
|
-
|
|
344947
|
-
|
|
344948
|
-
|
|
344949
|
-
|
|
344950
|
-
|
|
344951
|
-
|
|
344952
|
-
|
|
344953
|
-
|
|
344954
|
-
|
|
344955
|
-
|
|
344956
|
-
|
|
346013
|
+
log.info(run2.authVerify ? "Auth verification: replaying the current login recipe" : sharedAuthEnabled ? "Auth setup: capturing a credential-bound local session" : "Auth setup: running login with shared session reuse disabled");
|
|
346014
|
+
const capturePath = sharedAuthEnabled && !run2.authVerify ? authPaths2?.capturePath : void 0;
|
|
346015
|
+
const result2 = await runNativeWithLiveBrowser({
|
|
346016
|
+
playwrightCode: run2.playwrightCode,
|
|
346017
|
+
baseUrl: run2.baseUrl,
|
|
346018
|
+
testName: "auth-setup",
|
|
346019
|
+
testVariables: run2.testVariables,
|
|
346020
|
+
authStateCapturePath: capturePath,
|
|
346021
|
+
validateContext: callbackContext
|
|
346022
|
+
});
|
|
346023
|
+
const captured = !!capturePath && !!authPaths2 && result2.passed && !result2.suspiciousPass && (0, import_runner_core12.promoteLocalAuthStateCapture)(
|
|
346024
|
+
capturePath,
|
|
346025
|
+
authPaths2.cachePath,
|
|
346026
|
+
run2.authForceRun === true,
|
|
346027
|
+
releaseAuthLock2
|
|
346028
|
+
);
|
|
346029
|
+
const duration2 = Date.now() - startTime;
|
|
346030
|
+
await postResult(opts.serverUrl, headers, run2.runId, {
|
|
346031
|
+
status: result2.passed ? "PASSED" : "FAILED",
|
|
346032
|
+
apiCalls: result2.apiCalls,
|
|
346033
|
+
logs: result2.logs,
|
|
346034
|
+
error: result2.error,
|
|
346035
|
+
duration: duration2
|
|
346036
|
+
});
|
|
346037
|
+
log[result2.passed ? "ok" : "fail"](
|
|
346038
|
+
`Auth setup ${result2.passed ? "PASSED" : "FAILED"} (${duration2}ms)${captured ? " - session cached" : ""}${result2.error ? ` - ${result2.error}` : ""}`
|
|
346039
|
+
);
|
|
346040
|
+
return;
|
|
346041
|
+
} finally {
|
|
346042
|
+
if (authPaths2?.capturePath) (0, import_runner_core12.discardLocalAuthStateCapture)(authPaths2.capturePath);
|
|
346043
|
+
releaseAuthLock2?.();
|
|
346044
|
+
}
|
|
344957
346045
|
}
|
|
344958
346046
|
if (runMode === "batch-heal") {
|
|
344959
346047
|
const tests = run2.batchHealTests ?? [];
|
|
@@ -344972,7 +346060,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
344972
346060
|
const batchResult = await (0, import_runner_core10.executeGrokPerBatchHeal)(tests, run2.baseUrl, {
|
|
344973
346061
|
testVariables: run2.testVariables,
|
|
344974
346062
|
runNativeTest: runNativeWithLiveBrowser,
|
|
344975
|
-
validateContext:
|
|
346063
|
+
validateContext: callbackContext,
|
|
344976
346064
|
onLog: healLogStream.onLog
|
|
344977
346065
|
});
|
|
344978
346066
|
await healLogStream.flushNow();
|
|
@@ -345008,6 +346096,15 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
345008
346096
|
);
|
|
345009
346097
|
const describeKind = isSerialDescribe ? "serial" : "non-serial";
|
|
345010
346098
|
log.info(`API test heal - ${subTestCount} sub-test(s) [${describeKind}], ${failingSubTestCount} budgeted, ${MAX_API_HEAL_ATTEMPTS} attempts`);
|
|
346099
|
+
const apiHealAuthPaths = shouldUseAuthState(run2.tags, run2.playwrightCode) ? (0, import_runner_core12.localAuthStatePaths)(getRunsDir(), {
|
|
346100
|
+
projectId: run2.projectId || opts.projectId,
|
|
346101
|
+
serverUrl: opts.serverUrl,
|
|
346102
|
+
baseUrl: run2.baseUrl,
|
|
346103
|
+
testVariables: run2.testVariables,
|
|
346104
|
+
authToken: opts.authToken
|
|
346105
|
+
}, run2.runId) : void 0;
|
|
346106
|
+
const apiHealStorageStatePath = apiHealAuthPaths ? (0, import_runner_core12.getValidLocalAuthState)(apiHealAuthPaths.cachePath) : void 0;
|
|
346107
|
+
if (apiHealStorageStatePath) log.info("API heal verification is using saved auth state");
|
|
345011
346108
|
const stickyAppBugs = /* @__PURE__ */ new Map();
|
|
345012
346109
|
const lastChangeByTitle = /* @__PURE__ */ new Map();
|
|
345013
346110
|
let currentError = run2.lastFailureError ?? "Test failed";
|
|
@@ -345073,7 +346170,8 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
345073
346170
|
playwrightCode: lastHealedCode,
|
|
345074
346171
|
baseUrl: run2.baseUrl,
|
|
345075
346172
|
testVariables: run2.testVariables,
|
|
345076
|
-
|
|
346173
|
+
storageStatePath: apiHealStorageStatePath,
|
|
346174
|
+
validateContext: callbackContext
|
|
345077
346175
|
});
|
|
345078
346176
|
if (finalVerifyResult.passed) {
|
|
345079
346177
|
log.ok(` Attempt ${attempt} terminal verification PASSED (${bugCount} app_bug, ${passCount} pass)`);
|
|
@@ -345093,7 +346191,9 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345093
346191
|
finalVerifyResult = await runNativeWithLiveBrowser({
|
|
345094
346192
|
playwrightCode: lastHealedCode,
|
|
345095
346193
|
baseUrl: run2.baseUrl,
|
|
345096
|
-
testVariables: run2.testVariables
|
|
346194
|
+
testVariables: run2.testVariables,
|
|
346195
|
+
storageStatePath: apiHealStorageStatePath,
|
|
346196
|
+
validateContext: callbackContext
|
|
345097
346197
|
});
|
|
345098
346198
|
const stepResults = finalVerifyResult.stepResults ?? [];
|
|
345099
346199
|
const nonAppBugFails = stepResults.filter(
|
|
@@ -345143,7 +346243,8 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345143
346243
|
playwrightCode: lastHealedCode,
|
|
345144
346244
|
baseUrl: run2.baseUrl,
|
|
345145
346245
|
testVariables: run2.testVariables,
|
|
345146
|
-
|
|
346246
|
+
storageStatePath: apiHealStorageStatePath,
|
|
346247
|
+
validateContext: callbackContext
|
|
345147
346248
|
});
|
|
345148
346249
|
if (finalVerifyResult.passed) {
|
|
345149
346250
|
log.ok(` Attempt ${attempt} verification PASSED`);
|
|
@@ -345196,7 +346297,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345196
346297
|
if (classifyErrorMessage(composedError ?? null) === "config_issue") {
|
|
345197
346298
|
apiHealStatus = "ERROR";
|
|
345198
346299
|
} else {
|
|
345199
|
-
const rlDetection = (0,
|
|
346300
|
+
const rlDetection = (0, import_runner_core13.detectRateLimitBlock)({
|
|
345200
346301
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
345201
346302
|
tags: run2.tags ?? [],
|
|
345202
346303
|
error: composedError ?? finalVerifyResult?.error ?? null,
|
|
@@ -345293,8 +346394,8 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345293
346394
|
lastHealAttempt: run2.lastHealAttempt ?? null,
|
|
345294
346395
|
lastHealedCode: run2.lastHealedCode ?? null,
|
|
345295
346396
|
failureScreenshot: run2.failureScreenshot ?? null,
|
|
345296
|
-
onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-healer" }),
|
|
345297
|
-
validateContext:
|
|
346397
|
+
onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-healer" }, run2.claimEpoch),
|
|
346398
|
+
validateContext: callbackContext,
|
|
345298
346399
|
liveBrowserHandle,
|
|
345299
346400
|
runNativeTest: runNativeWithLiveBrowser
|
|
345300
346401
|
};
|
|
@@ -345336,7 +346437,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345336
346437
|
let healStatus = result2.passed ? "PASSED" : isConfigIssue ? "ERROR" : "FAILED";
|
|
345337
346438
|
let healError = result2.error;
|
|
345338
346439
|
if (!result2.passed && !isConfigIssue) {
|
|
345339
|
-
const rlDetection = (0,
|
|
346440
|
+
const rlDetection = (0, import_runner_core13.detectRateLimitBlock)({
|
|
345340
346441
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
345341
346442
|
tags: run2.tags ?? [],
|
|
345342
346443
|
error: result2.error,
|
|
@@ -345407,7 +346508,8 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345407
346508
|
// Allow generated `persistCredential(...)` calls inside the suite to
|
|
345408
346509
|
// POST back to the server with the runner's bearer token.
|
|
345409
346510
|
validateServerUrl: opts.serverUrl,
|
|
345410
|
-
validateRunnerAuthHeader: headers.Authorization
|
|
346511
|
+
validateRunnerAuthHeader: headers.Authorization,
|
|
346512
|
+
claimEpoch: run2.claimEpoch
|
|
345411
346513
|
});
|
|
345412
346514
|
const overall = result2.error && result2.testResults.length === 0 ? "ERROR" : result2.passed ? "PASSED" : "FAILED";
|
|
345413
346515
|
const rr = await fetch(`${opts.serverUrl}/api/runner/runs/${run2.runId}/regression-result`, {
|
|
@@ -345458,11 +346560,14 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345458
346560
|
});
|
|
345459
346561
|
if (healRes.ok) {
|
|
345460
346562
|
const { healedCode } = await healRes.json();
|
|
346563
|
+
const failedTestPlan = authPlanFor(failedTest.tags, healedCode);
|
|
345461
346564
|
const verifyResult = await runNativeWithLiveBrowser({
|
|
345462
346565
|
playwrightCode: healedCode,
|
|
345463
346566
|
baseUrl: run2.baseUrl,
|
|
345464
346567
|
testVariables: run2.testVariables,
|
|
345465
|
-
|
|
346568
|
+
storageStatePath: failedTestPlan.needsCookieState ? (0, import_runner_core12.getValidLocalAuthState)(runAuthCachePath) : void 0,
|
|
346569
|
+
authStandingMode: failedTestPlan.standingHeadersMode,
|
|
346570
|
+
validateContext: callbackContext
|
|
345466
346571
|
});
|
|
345467
346572
|
results.push({
|
|
345468
346573
|
testCaseId: failedTest.testCaseId,
|
|
@@ -345525,8 +346630,8 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345525
346630
|
onAICall: healAudit,
|
|
345526
346631
|
modelOverride: run2.healerModel,
|
|
345527
346632
|
consecutiveFails: failedTest.consecutiveFails,
|
|
345528
|
-
onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-healer" }),
|
|
345529
|
-
validateContext:
|
|
346633
|
+
onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-healer" }, run2.claimEpoch),
|
|
346634
|
+
validateContext: callbackContext,
|
|
345530
346635
|
liveBrowserHandle,
|
|
345531
346636
|
runNativeTest: runNativeWithLiveBrowser
|
|
345532
346637
|
};
|
|
@@ -345731,6 +346836,13 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345731
346836
|
}
|
|
345732
346837
|
return;
|
|
345733
346838
|
}
|
|
346839
|
+
ctx.onCredentialUpdate = async (input) => postCredentialRotation(
|
|
346840
|
+
opts.serverUrl,
|
|
346841
|
+
headers,
|
|
346842
|
+
run2.runId,
|
|
346843
|
+
{ ...input, actor: "ai-explorer" },
|
|
346844
|
+
run2.claimEpoch
|
|
346845
|
+
);
|
|
345734
346846
|
log.info(`Discovery (${ctx.mode}) on ${ctx.baseUrl}`);
|
|
345735
346847
|
const { push: pushAudit, drain: drainAudit } = createAuditFlusher(
|
|
345736
346848
|
`${opts.serverUrl}/api/runner/runs/${run2.runId}/ai-audit-batch`,
|
|
@@ -346433,7 +347545,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
346433
347545
|
featureDescription: ctx.featureDescription,
|
|
346434
347546
|
featurePages: ctx.featurePages,
|
|
346435
347547
|
appBrief: ctx.appBrief,
|
|
346436
|
-
onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-capture" })
|
|
347548
|
+
onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-capture" }, run2.claimEpoch)
|
|
346437
347549
|
});
|
|
346438
347550
|
if (logBuffer.length > 0) {
|
|
346439
347551
|
postCaptureLog(opts.serverUrl, headers, run2.runId, ctx.jobId, truncateTextTail(logBuffer.join("\n"), DISCOVERY_LOG_POST_MAX_CHARS, "capture log"));
|
|
@@ -346678,7 +347790,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
346678
347790
|
}
|
|
346679
347791
|
return verify;
|
|
346680
347792
|
};
|
|
346681
|
-
return await (0,
|
|
347793
|
+
return await (0, import_runner_core14.executeMobileLiveHeal)({
|
|
346682
347794
|
steps: a.steps,
|
|
346683
347795
|
target: a.target,
|
|
346684
347796
|
testVariables: a.testVariables,
|
|
@@ -346695,7 +347807,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
346695
347807
|
}
|
|
346696
347808
|
};
|
|
346697
347809
|
log.info("\u{1FA7A} Mobile AI heal \u2014 Phase 1 (script) \u2192 Phase 2 (live device, lazy)...");
|
|
346698
|
-
const heal = await (0,
|
|
347810
|
+
const heal = await (0, import_runner_core14.executeMobileTestWithHealing)(
|
|
346699
347811
|
mobileSteps,
|
|
346700
347812
|
healTarget,
|
|
346701
347813
|
{
|
|
@@ -346708,7 +347820,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
346708
347820
|
healerOptions
|
|
346709
347821
|
);
|
|
346710
347822
|
const finalRes = nativeVerify.last;
|
|
346711
|
-
const outcome = (0,
|
|
347823
|
+
const outcome = (0, import_runner_core14.computeMobileHealOutcome)({
|
|
346712
347824
|
heal,
|
|
346713
347825
|
finalError: finalRes?.error ?? null,
|
|
346714
347826
|
reproduceError,
|
|
@@ -346854,26 +347966,77 @@ ${result2.logs}`);
|
|
|
346854
347966
|
}
|
|
346855
347967
|
return;
|
|
346856
347968
|
}
|
|
346857
|
-
const
|
|
346858
|
-
|
|
346859
|
-
|
|
346860
|
-
|
|
346861
|
-
|
|
346862
|
-
|
|
347969
|
+
const projectId = run2.projectId || opts.projectId;
|
|
347970
|
+
const authStateExpected = shouldUseAuthState(run2.tags, run2.playwrightCode);
|
|
347971
|
+
const captureAuth = shouldCaptureAuthState(run2.tags, run2.playwrightCode);
|
|
347972
|
+
const authPaths = authStateExpected || captureAuth ? (0, import_runner_core12.localAuthStatePaths)(getRunsDir(), {
|
|
347973
|
+
projectId,
|
|
347974
|
+
serverUrl: opts.serverUrl,
|
|
346863
347975
|
baseUrl: run2.baseUrl,
|
|
346864
347976
|
testVariables: run2.testVariables,
|
|
346865
|
-
|
|
346866
|
-
|
|
346867
|
-
|
|
346868
|
-
|
|
346869
|
-
|
|
347977
|
+
authToken: opts.authToken
|
|
347978
|
+
}, run2.runId) : void 0;
|
|
347979
|
+
let releaseAuthLock;
|
|
347980
|
+
let storageStatePath;
|
|
347981
|
+
if (authPaths && authStateExpected) {
|
|
347982
|
+
releaseAuthLock = await (0, import_runner_core12.acquireLocalAuthStateLock)(authPaths.lockKey, authPaths.lockPath);
|
|
347983
|
+
storageStatePath = (0, import_runner_core12.getValidLocalAuthState)(authPaths.cachePath);
|
|
347984
|
+
if (storageStatePath || !captureAuth) {
|
|
347985
|
+
releaseAuthLock();
|
|
347986
|
+
releaseAuthLock = void 0;
|
|
347987
|
+
}
|
|
347988
|
+
if (!storageStatePath && !captureAuth && sharedAuthInjectEnabled()) {
|
|
347989
|
+
log.info(`Waiting for the auth producer to warm project ${projectId}`);
|
|
347990
|
+
storageStatePath = await (0, import_runner_core12.waitForLocalAuthStateProducer)(
|
|
347991
|
+
authPaths.cachePath,
|
|
347992
|
+
authPaths.lockKey,
|
|
347993
|
+
2e3,
|
|
347994
|
+
void 0,
|
|
347995
|
+
100,
|
|
347996
|
+
authPaths.lockPath
|
|
347997
|
+
);
|
|
346870
347998
|
}
|
|
346871
|
-
}
|
|
347999
|
+
}
|
|
348000
|
+
if (storageStatePath) {
|
|
348001
|
+
log.info(`Using auth state for project ${projectId}`);
|
|
348002
|
+
}
|
|
348003
|
+
const runAuthPath = captureAuth ? authPaths?.capturePath : void 0;
|
|
348004
|
+
const result = await (async () => {
|
|
348005
|
+
try {
|
|
348006
|
+
return await runNativeWithLiveBrowser({
|
|
348007
|
+
playwrightCode: run2.playwrightCode,
|
|
348008
|
+
baseUrl: run2.baseUrl,
|
|
348009
|
+
testVariables: run2.testVariables,
|
|
348010
|
+
authStateCapturePath: runAuthPath,
|
|
348011
|
+
storageStatePath,
|
|
348012
|
+
validateContext: callbackContext
|
|
348013
|
+
});
|
|
348014
|
+
} finally {
|
|
348015
|
+
if (runAuthPath && authPaths) {
|
|
348016
|
+
if ((0, import_runner_core12.getValidLocalAuthState)(runAuthPath)) {
|
|
348017
|
+
const releasePromotionLock = releaseAuthLock ? void 0 : await (0, import_runner_core12.acquireLocalAuthStateLock)(authPaths.lockKey, authPaths.lockPath);
|
|
348018
|
+
try {
|
|
348019
|
+
const captured = (0, import_runner_core12.promoteLocalAuthStateCapture)(
|
|
348020
|
+
runAuthPath,
|
|
348021
|
+
authPaths.cachePath,
|
|
348022
|
+
!!storageStatePath,
|
|
348023
|
+
releaseAuthLock ?? releasePromotionLock
|
|
348024
|
+
);
|
|
348025
|
+
if (captured) log.info(`Captured auth state for project ${projectId}`);
|
|
348026
|
+
} finally {
|
|
348027
|
+
releasePromotionLock?.();
|
|
348028
|
+
}
|
|
348029
|
+
}
|
|
348030
|
+
(0, import_runner_core12.discardLocalAuthStateCapture)(runAuthPath);
|
|
348031
|
+
}
|
|
348032
|
+
releaseAuthLock?.();
|
|
348033
|
+
}
|
|
348034
|
+
})();
|
|
346872
348035
|
const duration = Date.now() - startTime;
|
|
346873
348036
|
let runStatus = result.passed ? "PASSED" : "FAILED";
|
|
346874
348037
|
let runError = result.error;
|
|
346875
348038
|
if (!result.passed) {
|
|
346876
|
-
const rlDetection = (0,
|
|
348039
|
+
const rlDetection = (0, import_runner_core13.detectRateLimitBlock)({
|
|
346877
348040
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
346878
348041
|
tags: run2.tags ?? [],
|
|
346879
348042
|
error: result.error,
|
|
@@ -346887,6 +348050,10 @@ ${result2.logs}`);
|
|
|
346887
348050
|
log.warn(`\u26A0 Rate-limit block detected - posting ERROR (not FAILED) to skip heal/quarantine`);
|
|
346888
348051
|
}
|
|
346889
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
|
+
}
|
|
346890
348057
|
await postResult(opts.serverUrl, headers, run2.runId, {
|
|
346891
348058
|
status: runStatus,
|
|
346892
348059
|
stepResults: result.stepResults,
|
|
@@ -346902,7 +348069,7 @@ ${result2.logs}`);
|
|
|
346902
348069
|
if (result.passed) {
|
|
346903
348070
|
log.ok(`PASSED (${duration}ms)${result.suspiciousPass ? ` \u26A0 suspicious-pass (viewport ${Math.round(result.suspiciousPass.emptyRatio * 100)}% blank)` : ""}`);
|
|
346904
348071
|
} else if (runStatus === "ERROR") {
|
|
346905
|
-
log.warn(`BLOCKED (${duration}ms) - rate-limit
|
|
348072
|
+
log.warn(`BLOCKED (${duration}ms) - environmental block (rate-limit / config-issue), not counted as failure`);
|
|
346906
348073
|
} else {
|
|
346907
348074
|
log.fail(`FAILED (${duration}ms)${result.error ? ` - ${result.error}` : ""}`);
|
|
346908
348075
|
if (result.logs) {
|
|
@@ -346934,6 +348101,12 @@ async function startRunner(opts) {
|
|
|
346934
348101
|
process.env.AUTH_TOKEN = authToken;
|
|
346935
348102
|
process.env.RUNNER_TOKEN = authToken;
|
|
346936
348103
|
if (projectId) process.env.PROJECT_ID = projectId;
|
|
348104
|
+
(0, import_runner_core12.cleanupExpiredLocalAuthStateFiles)(getRunsDir());
|
|
348105
|
+
const authCleanupTimer = setInterval(
|
|
348106
|
+
() => (0, import_runner_core12.cleanupExpiredLocalAuthStateFiles)(getRunsDir()),
|
|
348107
|
+
import_runner_core12.LOCAL_AUTH_STATE_CLEANUP_INTERVAL_MS
|
|
348108
|
+
);
|
|
348109
|
+
authCleanupTimer.unref?.();
|
|
346937
348110
|
const headers = buildHeaders(authToken, projectId);
|
|
346938
348111
|
console.log("");
|
|
346939
348112
|
console.log(`${C2.bold}${C2.cyan}validate.qa Local Runner${C2.reset}`);
|
|
@@ -347042,7 +348215,7 @@ async function startRunner(opts) {
|
|
|
347042
348215
|
capture: envInt("MAX_CAPTURE", DEFAULT_TYPE_LIMITS.capture),
|
|
347043
348216
|
run: envInt("MAX_RUN", DEFAULT_TYPE_LIMITS.run)
|
|
347044
348217
|
};
|
|
347045
|
-
const RESOURCE_GOVERNOR_OPTIONS = (0,
|
|
348218
|
+
const RESOURCE_GOVERNOR_OPTIONS = (0, import_runner_core13.createResourceGovernorOptions)({
|
|
347046
348219
|
isCloud: IS_CLOUD,
|
|
347047
348220
|
maxConcurrent: MAX_CONCURRENT,
|
|
347048
348221
|
typeLimits: TYPE_LIMITS
|
|
@@ -347210,8 +348383,8 @@ async function startRunner(opts) {
|
|
|
347210
348383
|
android: devices.some((d) => d.platform === "ANDROID" && d.isAvailable && d.state === "BOOTED")
|
|
347211
348384
|
} : { web: true, ios: false, android: false };
|
|
347212
348385
|
const bridge = enableMobile ? getBridgeState() : void 0;
|
|
347213
|
-
const resourceStatus = (0,
|
|
347214
|
-
const processInventory = await (0,
|
|
348386
|
+
const resourceStatus = (0, import_runner_core13.getResourceGovernorStatus)(RESOURCE_GOVERNOR_OPTIONS, Object.fromEntries(activeByType));
|
|
348387
|
+
const processInventory = await (0, import_runner_core13.getBrowserProcessInventory)();
|
|
347215
348388
|
lastInventoryPids = new Set(processInventory.processes.map((p) => p.pid));
|
|
347216
348389
|
lastInventoryKinds.clear();
|
|
347217
348390
|
for (const p of processInventory.processes) lastInventoryKinds.set(p.pid, p.kind);
|
|
@@ -347241,7 +348414,7 @@ async function startRunner(opts) {
|
|
|
347241
348414
|
resourceSnapshot: resourceStatus.snapshot,
|
|
347242
348415
|
resourceGovernor: resourceStatus.governor,
|
|
347243
348416
|
processInventory,
|
|
347244
|
-
aiQueue: (0,
|
|
348417
|
+
aiQueue: (0, import_runner_core13.getRunnerAIQueueStats)(),
|
|
347245
348418
|
liveBrowsers: import_runner_core11.liveBrowserRegistry.snapshotForHeartbeat(),
|
|
347246
348419
|
// Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
|
|
347247
348420
|
// Cached & cheap — see getCachedDoctor above.
|
|
@@ -347352,7 +348525,7 @@ async function startRunner(opts) {
|
|
|
347352
348525
|
let pidsWatchdogWarned = false;
|
|
347353
348526
|
const pidsWatchdog = setInterval(() => {
|
|
347354
348527
|
if (shuttingDown) return;
|
|
347355
|
-
const snapshot = (0,
|
|
348528
|
+
const snapshot = (0, import_runner_core13.getResourceSnapshot)();
|
|
347356
348529
|
const pct = snapshot.pidsPct;
|
|
347357
348530
|
if (pct == null || !Number.isFinite(pct)) return;
|
|
347358
348531
|
if (pct >= PIDS_RECYCLE_PCT) {
|
|
@@ -347392,7 +348565,7 @@ async function startRunner(opts) {
|
|
|
347392
348565
|
log.dim(` Deferred ${run2.runId} (project ${runProject} at capacity ${getActiveForProject(runProject)}/${projectLimit})`);
|
|
347393
348566
|
continue;
|
|
347394
348567
|
}
|
|
347395
|
-
const admission = (0,
|
|
348568
|
+
const admission = (0, import_runner_core13.evaluateRunAdmission)(RESOURCE_GOVERNOR_OPTIONS, {
|
|
347396
348569
|
mode: runMode,
|
|
347397
348570
|
activeRuns,
|
|
347398
348571
|
activeByType: Object.fromEntries(activeByType),
|
|
@@ -347470,9 +348643,9 @@ async function startRunner(opts) {
|
|
|
347470
348643
|
leasedDeviceId = freeDeviceId;
|
|
347471
348644
|
leasedDeviceIds.set(freeDeviceId, run2.runId);
|
|
347472
348645
|
}
|
|
347473
|
-
let
|
|
348646
|
+
let claimEpoch = false;
|
|
347474
348647
|
try {
|
|
347475
|
-
|
|
348648
|
+
claimEpoch = await claimRun(serverUrl, headers, run2.runId);
|
|
347476
348649
|
} catch (err) {
|
|
347477
348650
|
if (leasedDeviceId) {
|
|
347478
348651
|
leasedDeviceIds.delete(leasedDeviceId);
|
|
@@ -347480,7 +348653,7 @@ async function startRunner(opts) {
|
|
|
347480
348653
|
}
|
|
347481
348654
|
throw err;
|
|
347482
348655
|
}
|
|
347483
|
-
if (
|
|
348656
|
+
if (claimEpoch === false) {
|
|
347484
348657
|
log.dim(` Skipped ${run2.runId} (already claimed)`);
|
|
347485
348658
|
if (leasedDeviceId) {
|
|
347486
348659
|
leasedDeviceIds.delete(leasedDeviceId);
|
|
@@ -347488,6 +348661,7 @@ async function startRunner(opts) {
|
|
|
347488
348661
|
}
|
|
347489
348662
|
continue;
|
|
347490
348663
|
}
|
|
348664
|
+
run2.claimEpoch = claimEpoch;
|
|
347491
348665
|
activeRuns++;
|
|
347492
348666
|
incrementType(runMode);
|
|
347493
348667
|
incrementProject(runProject);
|